From 1be4cf5e473b6f8f7098a178223d38ab42bb128c Mon Sep 17 00:00:00 2001 From: Askanaz Torosyan <46795157+nVxx@users.noreply.github.com> Date: Fri, 16 Jun 2023 08:25:07 +0200 Subject: [PATCH] Release/28.0.0-rc1 (#99) * Oss release 28.0.0-rc created 2023-06-15-13-56 see CHANGELOG.md for details Original commit sha: 546173fb8024cc03c89f82caf65be3f3afa2c0aa Co-authored-by: Askanaz Torosyan <46795157+nVxx@users.noreply.github.com> Co-authored-by: Daniel Haas <25718295+bojackHaasman@users.noreply.github.com> Co-authored-by: Mirko Sova <64351017+smirko-dev@users.noreply.github.com> Co-authored-by: Violin Yanev Co-authored-by: Carsten Rohn <710234+delyas@users.noreply.github.com> Co-authored-by: Tobias Hammer Co-authored-by: Bernhard Kisslinger <65217745+bkisslinger@users.noreply.github.com> Co-authored-by: Martin Veith <3490591+veithm@users.noreply.github.com> Co-authored-by: Jonathan Conrad <833168+jcsneaker@users.noreply.github.com> Co-authored-by: Mohamed Sharaf-El-Deen <769940+mohhsharaf@users.noreply.github.com> Co-authored-by: Markus Keppler <92277233+markuskeppler@users.noreply.github.com> * Fixed ios build config. * Applied iOS config to mac build in general. * Separated CMAKE_SYSTEM_NAME for iOS from macOS. * Move all XCode setup into iphone config. --------- Co-authored-by: Ramses Tech User <94632088+ramses-tech-user@users.noreply.github.com> Co-authored-by: Daniel Haas <25718295+bojackHaasman@users.noreply.github.com> Co-authored-by: Mirko Sova <64351017+smirko-dev@users.noreply.github.com> Co-authored-by: Violin Yanev Co-authored-by: Carsten Rohn <710234+delyas@users.noreply.github.com> Co-authored-by: Tobias Hammer Co-authored-by: Bernhard Kisslinger <65217745+bkisslinger@users.noreply.github.com> Co-authored-by: Martin Veith <3490591+veithm@users.noreply.github.com> Co-authored-by: Jonathan Conrad <833168+jcsneaker@users.noreply.github.com> Co-authored-by: Mohamed Sharaf-El-Deen <769940+mohhsharaf@users.noreply.github.com> Co-authored-by: Markus Keppler <92277233+markuskeppler@users.noreply.github.com> Co-authored-by: Askanaz Torosyan --- .ansible-lint | 14 + .clang-tidy | 8 +- .github/workflows/test.yml | 8 +- .gitlint | 80 + cmake/ramses/tools.cmake => .readthedocs.yml | 20 +- .../run_tests_locally.py => .yamllint | 32 +- CHANGELOG.txt => CHANGELOG.md | 199 + CMakeLists.txt | 520 +- CONTRIBUTING.rst | 251 + README.md | 9 +- SECURITY.md | 16 + benchmarks/CMakeLists.txt | 30 + benchmarks/animation.cpp | 166 + benchmarks/compile_lua.cpp | 53 + benchmarks/getproperty.cpp | 411 ++ benchmarks/links.cpp | 113 + benchmarks/property.cpp | 54 + benchmarks/serialization.cpp | 105 + benchmarks/update.cpp | 203 + client/CMakeLists.txt | 215 + client/logic/.clang-tidy | 506 ++ .../logic/cmake/flatbuffersGeneration.cmake | 71 + client/logic/cmake/platformConfig.cmake | 209 + .../logic/include/ramses-logic/AnchorPoint.h | 100 + .../include/ramses-logic/AnimationNode.h | 97 + .../ramses-logic/AnimationNodeConfig.h | 115 + .../include/ramses-logic/AnimationTypes.h | 70 + .../logic/include/ramses-logic/Collection.h | 166 + client/logic/include/ramses-logic/DataArray.h | 91 + .../include/ramses-logic/ELuaSavingMode.h | 30 + .../include/ramses-logic/EPropertyType.h | 318 ++ .../include/ramses-logic/EStandardModule.h | 27 + client/logic/include/ramses-logic/ErrorData.h | 52 + client/logic/include/ramses-logic/Iterator.h | 208 + client/logic/include/ramses-logic/Logger.h | 73 + .../logic/include/ramses-logic/LogicEngine.h | 943 ++++ .../include/ramses-logic/LogicEngineReport.h | 129 + client/logic/include/ramses-logic/LogicNode.h | 80 + .../logic/include/ramses-logic/LogicObject.h | 148 + client/logic/include/ramses-logic/LuaConfig.h | 110 + .../logic/include/ramses-logic/LuaInterface.h | 61 + client/logic/include/ramses-logic/LuaModule.h | 79 + client/logic/include/ramses-logic/LuaScript.h | 91 + client/logic/include/ramses-logic/Property.h | 270 ++ .../logic/include/ramses-logic/PropertyLink.h | 32 + .../ramses-logic/RamsesAppearanceBinding.h | 74 + .../include/ramses-logic/RamsesBinding.h | 34 + .../ramses-logic/RamsesCameraBinding.h | 104 + .../include/ramses-logic/RamsesLogicVersion.h | 38 + .../ramses-logic/RamsesMeshNodeBinding.h | 79 + .../include/ramses-logic/RamsesNodeBinding.h | 101 + .../ramses-logic/RamsesRenderGroupBinding.h | 101 + .../RamsesRenderGroupBindingElements.h | 75 + .../ramses-logic/RamsesRenderPassBinding.h | 86 + .../include/ramses-logic/SaveFileConfig.h | 115 + .../logic/include/ramses-logic/SkinBinding.h | 93 + client/logic/include/ramses-logic/TimerNode.h | 58 + .../logic/include/ramses-logic/WarningData.h | 78 + .../flatbuffers/generated/AnchorPointGen.h | 140 + .../flatbuffers/generated/AnimationNodeGen.h | 381 ++ .../lib/flatbuffers/generated/ApiObjectsGen.h | 382 ++ .../lib/flatbuffers/generated/DataArrayGen.h | 493 ++ .../logic/lib/flatbuffers/generated/LinkGen.h | 113 + .../flatbuffers/generated/LogicEngineGen.h | 433 ++ .../flatbuffers/generated/LogicObjectGen.h | 134 + .../flatbuffers/generated/LuaInterfaceGen.h | 103 + .../lib/flatbuffers/generated/LuaModuleGen.h | 255 + .../lib/flatbuffers/generated/LuaScriptGen.h | 195 + .../lib/flatbuffers/generated/PropertyGen.h | 1045 +++++ .../generated/RamsesAppearanceBindingGen.h | 149 + .../flatbuffers/generated/RamsesBindingGen.h | 118 + .../generated/RamsesCameraBindingGen.h | 91 + .../generated/RamsesMeshNodeBindingGen.h | 91 + .../generated/RamsesNodeBindingGen.h | 103 + .../generated/RamsesReferenceGen.h | 94 + .../generated/RamsesRenderGroupBindingGen.h | 214 + .../generated/RamsesRenderPassBindingGen.h | 91 + .../flatbuffers/generated/SkinBindingGen.h | 161 + .../lib/flatbuffers/generated/TimerNodeGen.h | 116 + .../lib/flatbuffers/schemas/AnchorPoint.fbs | 21 +- .../lib/flatbuffers/schemas/AnimationNode.fbs | 41 + .../lib/flatbuffers/schemas/ApiObjects.fbs | 45 + .../lib/flatbuffers/schemas/DataArray.fbs | 41 + .../logic/lib/flatbuffers/schemas/Link.fbs | 14 +- .../lib/flatbuffers/schemas/LogicEngine.fbs | 46 + .../lib/flatbuffers/schemas/LogicObject.fbs | 17 + .../lib/flatbuffers/schemas/LuaInterface.fbs | 14 +- .../lib/flatbuffers/schemas/LuaModule.fbs | 26 + .../lib/flatbuffers/schemas/LuaScript.fbs | 25 + .../lib/flatbuffers/schemas/Property.fbs | 104 + .../schemas/RamsesAppearanceBinding.fbs | 21 +- .../lib/flatbuffers/schemas/RamsesBinding.fbs | 23 + .../schemas/RamsesCameraBinding.fbs | 8 +- .../schemas/RamsesMeshNodeBinding.fbs | 10 +- .../flatbuffers/schemas/RamsesNodeBinding.fbs | 17 + .../flatbuffers/schemas/RamsesReference.fbs | 10 +- .../schemas/RamsesRenderGroupBinding.fbs | 20 +- .../schemas/RamsesRenderPassBinding.fbs | 10 +- .../lib/flatbuffers/schemas/SkinBinding.fbs | 18 +- .../lib/flatbuffers/schemas/TimerNode.fbs | 12 +- client/logic/lib/impl/AnchorPoint.cpp | 32 + client/logic/lib/impl/AnchorPointImpl.cpp | 155 + client/logic/lib/impl/AnchorPointImpl.h | 63 + client/logic/lib/impl/AnimationNode.cpp | 25 + client/logic/lib/impl/AnimationNodeConfig.cpp | 54 + .../lib/impl/AnimationNodeConfigImpl.cpp | 164 + .../logic/lib/impl/AnimationNodeConfigImpl.h | 28 + client/logic/lib/impl/AnimationNodeImpl.cpp | 520 ++ client/logic/lib/impl/AnimationNodeImpl.h | 93 + client/logic/lib/impl/Collection.cpp | 23 + client/logic/lib/impl/DataArray.cpp | 46 + client/logic/lib/impl/DataArrayImpl.cpp | 352 ++ client/logic/lib/impl/DataArrayImpl.h | 71 + client/logic/lib/impl/Iterator.cpp | 35 + client/logic/lib/impl/Logger.cpp | 33 + client/logic/lib/impl/LoggerImpl.cpp | 38 + client/logic/lib/impl/LoggerImpl.h | 184 + client/logic/lib/impl/LogicEngine.cpp | 368 ++ client/logic/lib/impl/LogicEngineImpl.cpp | 857 ++++ client/logic/lib/impl/LogicEngineImpl.h | 187 + client/logic/lib/impl/LogicEngineReport.cpp | 53 + .../logic/lib/impl/LogicEngineReportImpl.cpp | 55 + client/logic/lib/impl/LogicEngineReportImpl.h | 38 + client/logic/lib/impl/LogicNode.cpp | 35 + client/logic/lib/impl/LogicNodeImpl.cpp | 71 + client/logic/lib/impl/LogicNodeImpl.h | 54 + client/logic/lib/impl/LogicObject.cpp | 107 + client/logic/lib/impl/LogicObjectImpl.cpp | 121 + client/logic/lib/impl/LogicObjectImpl.h | 68 + client/logic/lib/impl/LuaConfig.cpp | 50 + client/logic/lib/impl/LuaConfigImpl.cpp | 86 + client/logic/lib/impl/LuaConfigImpl.h | 44 + client/logic/lib/impl/LuaInterface.cpp | 20 + client/logic/lib/impl/LuaInterfaceImpl.cpp | 118 + client/logic/lib/impl/LuaInterfaceImpl.h | 68 + .../lib/impl/LuaModule.cpp} | 20 +- client/logic/lib/impl/LuaModuleImpl.cpp | 196 + client/logic/lib/impl/LuaModuleImpl.h | 69 + .../lib/impl/LuaScript.cpp} | 22 +- client/logic/lib/impl/LuaScriptImpl.cpp | 270 ++ client/logic/lib/impl/LuaScriptImpl.h | 85 + client/logic/lib/impl/Property.cpp | 153 + client/logic/lib/impl/PropertyImpl.cpp | 733 +++ client/logic/lib/impl/PropertyImpl.h | 160 + .../lib/impl/RamsesAppearanceBinding.cpp | 26 + .../lib/impl/RamsesAppearanceBindingImpl.cpp | 271 ++ .../lib/impl/RamsesAppearanceBindingImpl.h | 75 + .../lib/impl/RamsesBinding.cpp} | 17 +- client/logic/lib/impl/RamsesBindingImpl.cpp | 44 + client/logic/lib/impl/RamsesBindingImpl.h | 45 + client/logic/lib/impl/RamsesCameraBinding.cpp | 25 + .../lib/impl/RamsesCameraBindingImpl.cpp | 299 ++ .../logic/lib/impl/RamsesCameraBindingImpl.h | 102 + client/logic/lib/impl/RamsesLogicVersion.cpp | 24 + .../logic/lib/impl/RamsesMeshNodeBinding.cpp | 30 + .../lib/impl/RamsesMeshNodeBindingImpl.cpp | 189 + .../lib/impl/RamsesMeshNodeBindingImpl.h | 79 + client/logic/lib/impl/RamsesNodeBinding.cpp | 31 + .../logic/lib/impl/RamsesNodeBindingImpl.cpp | 290 ++ client/logic/lib/impl/RamsesNodeBindingImpl.h | 84 + .../lib/impl/RamsesRenderGroupBinding.cpp | 30 + .../impl/RamsesRenderGroupBindingElements.cpp | 46 + .../RamsesRenderGroupBindingElementsImpl.cpp | 42 + .../RamsesRenderGroupBindingElementsImpl.h | 33 + .../lib/impl/RamsesRenderGroupBindingImpl.cpp | 261 ++ .../lib/impl/RamsesRenderGroupBindingImpl.h | 71 + .../lib/impl/RamsesRenderPassBinding.cpp | 30 + .../lib/impl/RamsesRenderPassBindingImpl.cpp | 192 + .../lib/impl/RamsesRenderPassBindingImpl.h | 79 + client/logic/lib/impl/SaveFileConfig.cpp | 55 + client/logic/lib/impl/SaveFileConfigImpl.cpp | 75 + client/logic/lib/impl/SaveFileConfigImpl.h | 47 + client/logic/lib/impl/SkinBinding.cpp | 32 + client/logic/lib/impl/SkinBindingImpl.cpp | 190 + client/logic/lib/impl/SkinBindingImpl.h | 78 + .../lib/impl/TimerNode.cpp} | 20 +- client/logic/lib/impl/TimerNodeImpl.cpp | 123 + client/logic/lib/impl/TimerNodeImpl.h | 52 + client/logic/lib/internals/ApiObjects.cpp | 1550 ++++++ client/logic/lib/internals/ApiObjects.h | 220 + .../internals/ApiObjectsSerializedSize.cpp | 212 + .../lib/internals/ApiObjectsSerializedSize.h | 25 + .../logic/lib/internals/DeserializationMap.h | 92 + .../lib/internals/DirectedAcyclicGraph.cpp | 244 + .../lib/internals/DirectedAcyclicGraph.h | 76 + .../logic/lib/internals/EPropertySemantics.h | 23 +- .../lib/internals/EnvironmentProtection.cpp | 307 ++ .../lib/internals/EnvironmentProtection.h | 73 + client/logic/lib/internals/ErrorReporting.cpp | 40 + .../logic/lib/internals/ErrorReporting.h | 26 +- client/logic/lib/internals/FileUtils.cpp | 74 + client/logic/lib/internals/FileUtils.h | 24 + .../lib/internals/InterfaceTypeFunctions.cpp | 44 + .../lib/internals/InterfaceTypeFunctions.h | 39 + .../logic/lib/internals/InterfaceTypeInfo.h | 27 +- .../lib/internals/LogicNodeDependencies.cpp | 227 + .../lib/internals/LogicNodeDependencies.h | 54 + .../internals/LogicNodeUpdateStatistics.cpp | 143 + .../lib/internals/LogicNodeUpdateStatistics.h | 79 + .../lib/internals/LuaCompilationUtils.cpp | 540 +++ .../logic/lib/internals/LuaCompilationUtils.h | 112 + .../logic/lib/internals/LuaCustomizations.cpp | 380 ++ .../logic/lib/internals/LuaCustomizations.h | 40 + .../lib/internals/LuaTypeConversions.cpp | 279 ++ .../logic/lib/internals/LuaTypeConversions.h | 77 + .../lib/internals/PropertyTypeExtractor.cpp | 286 ++ .../lib/internals/PropertyTypeExtractor.h | 52 + client/logic/lib/internals/RamsesHelper.h | 56 + .../lib/internals/RamsesObjectResolver.cpp | 89 + .../lib/internals/RamsesObjectResolver.h | 61 + .../logic/lib/internals/SerializationHelper.h | 58 + client/logic/lib/internals/SerializationMap.h | 80 + client/logic/lib/internals/SolHelper.h | 54 + client/logic/lib/internals/SolState.cpp | 192 + client/logic/lib/internals/SolState.h | 70 + client/logic/lib/internals/SolWrapper.h | 143 + .../lib/internals/StdFilesystemWrapper.h | 36 + client/logic/lib/internals/TypeData.cpp | 46 + client/logic/lib/internals/TypeData.h | 69 + client/logic/lib/internals/TypeUtils.h | 138 + client/logic/lib/internals/UpdateReport.cpp | 83 + client/logic/lib/internals/UpdateReport.h | 63 + .../logic/lib/internals/ValidationResults.cpp | 39 + .../logic/lib/internals/ValidationResults.h | 27 +- .../lib/internals/WrappedLuaProperty.cpp | 600 +++ .../logic/lib/internals/WrappedLuaProperty.h | 76 + .../logic/tools/CMakeLists.txt | 8 +- .../ramses-logic-viewer-test/CMakeLists.txt | 68 + .../LogicViewerAppTest.cpp | 1358 ++++++ .../LogicViewerLuaTest.cpp | 1085 +++++ .../LogicViewerTest.cpp | 59 + .../LogicViewerTestBase.cpp | 15 + .../LogicViewerTestBase.h | 152 + .../res/ALogicViewerApp_clearColor.png | Bin 0 -> 1730 bytes .../res/ALogicViewerApp_clearColorCmdLine.png | Bin 0 -> 1730 bytes .../res/ALogicViewerApp_red.png | Bin 0 -> 1730 bytes .../res/ALogicViewerApp_red_500x700.png | Bin 0 -> 774 bytes .../res/ALogicViewerApp_white.png | Bin 0 -> 1712 bytes .../res/ALogicViewerApp_yellow.png | Bin 0 -> 1730 bytes .../tools/ramses-logic-viewer/Arguments.h | 120 + .../tools/ramses-logic-viewer/CMakeLists.txt | 67 + .../ramses-logic-viewer/ImguiClientHelper.cpp | 541 +++ .../ramses-logic-viewer/ImguiClientHelper.h | 145 + .../tools/ramses-logic-viewer/ImguiWrapper.h | 19 +- .../tools/ramses-logic-viewer/LogicViewer.cpp | 374 ++ .../tools/ramses-logic-viewer/LogicViewer.h | 216 + .../ramses-logic-viewer/LogicViewerApp.cpp | 145 + .../ramses-logic-viewer/LogicViewerApp.h | 98 + .../ramses-logic-viewer/LogicViewerGui.cpp | 1364 ++++++ .../ramses-logic-viewer/LogicViewerGui.h | 110 + .../ramses-logic-viewer/LogicViewerGuiApp.cpp | 226 + .../ramses-logic-viewer/LogicViewerGuiApp.h | 74 + .../LogicViewerHeadlessApp.cpp | 88 + .../LogicViewerHeadlessApp.h | 26 +- .../ramses-logic-viewer/LogicViewerLog.cpp | 137 + .../ramses-logic-viewer/LogicViewerLog.h | 61 + .../LogicViewerLuaTypes.cpp | 347 ++ .../ramses-logic-viewer/LogicViewerLuaTypes.h | 98 + .../LogicViewerSettings.cpp | 144 + .../ramses-logic-viewer/LogicViewerSettings.h | 41 + .../logic/tools/ramses-logic-viewer/Result.h | 57 + .../tools/ramses-logic-viewer/SceneSetup.h | 167 + .../ramses-logic-viewer/UpdateReportSummary.h | 152 + .../logic/tools/ramses-logic-viewer}/main.cpp | 8 +- .../ramses-logic-viewer/main_headless.cpp | 15 + .../logic/unittests/api/AnchorPointTest.cpp | 479 ++ .../unittests/api/AnimationNodeConfigTest.cpp | 269 ++ .../logic/unittests/api/AnimationNodeTest.cpp | 893 ++++ .../AnimationNodeWithDataPropertiesTest.cpp | 336 ++ client/logic/unittests/api/CollectionTest.cpp | 182 + client/logic/unittests/api/DataArrayTest.cpp | 464 ++ client/logic/unittests/api/IteratorTest.cpp | 160 + client/logic/unittests/api/LoggerTest.cpp | 102 + .../api/LogicEngineTest_Animations.cpp | 301 ++ .../api/LogicEngineTest_Compatibility.cpp | 450 ++ .../LogicEngineTest_DependencyExtraction.cpp | 198 + .../api/LogicEngineTest_Dirtiness.cpp | 569 +++ .../api/LogicEngineTest_ErrorHandling.cpp | 113 + .../unittests/api/LogicEngineTest_Factory.cpp | 683 +++ .../unittests/api/LogicEngineTest_Linking.cpp | 2453 ++++++++++ ...icEngineTest_LogicNodeUpdateStatistics.cpp | 143 + .../unittests/api/LogicEngineTest_Lookup.cpp | 550 +++ .../api/LogicEngineTest_Serialization.cpp | 1216 +++++ .../api/LogicEngineTest_SerializedSize.cpp | 397 ++ .../unittests/api/LogicEngineTest_Update.cpp | 474 ++ .../api/LogicEngineTest_UpdateReport.cpp | 292 ++ .../api/LogicEngineTest_Validation.cpp | 566 +++ client/logic/unittests/api/LogicNodeTest.cpp | 138 + client/logic/unittests/api/LuaConfigTest.cpp | 198 + .../logic/unittests/api/LuaInterfaceTest.cpp | 1158 +++++ client/logic/unittests/api/LuaModuleTest.cpp | 682 +++ .../unittests/api/LuaScriptTest_Debug.cpp | 248 + .../unittests/api/LuaScriptTest_Init.cpp | 562 +++ .../unittests/api/LuaScriptTest_Interface.cpp | 742 +++ .../unittests/api/LuaScriptTest_Lifecycle.cpp | 422 ++ .../unittests/api/LuaScriptTest_Modules.cpp | 1340 ++++++ .../unittests/api/LuaScriptTest_Runtime.cpp | 2431 ++++++++++ .../api/LuaScriptTest_Serialization.cpp | 812 ++++ .../unittests/api/LuaScriptTest_Syntax.cpp | 581 +++ .../unittests/api/LuaScriptTest_Types.cpp | 247 + client/logic/unittests/api/PropertyTest.cpp | 1154 +++++ .../logic/unittests/api/PropertyTypeTest.cpp | 132 + .../api/RamsesAppearanceBindingTest.cpp | 953 ++++ .../unittests/api/RamsesCameraBindingTest.cpp | 1573 +++++++ .../api/RamsesMeshNodeBindingTest.cpp | 408 ++ .../unittests/api/RamsesNodeBindingTest.cpp | 1356 ++++++ .../RamsesRenderGroupBindingElementsTest.cpp | 81 + .../api/RamsesRenderGroupBindingTest.cpp | 495 ++ .../api/RamsesRenderPassBindingTest.cpp | 590 +++ .../unittests/api/SaveFileConfigTest.cpp | 87 + .../logic/unittests/api/SkinBindingTest.cpp | 276 ++ client/logic/unittests/api/TimerNodeTest.cpp | 260 + .../unittests/internal/ApiObjectsTest.cpp | 2494 ++++++++++ .../internal/DirectedAcyclicGraphTest.cpp | 578 +++ .../internal/EnvironmentProtectionTest.cpp | 865 ++++ .../unittests/internal/ErrorReportingTest.cpp | 107 + .../internal/LogicNodeDependenciesTest.cpp | 502 ++ .../internal/LuaCustomizationsTest.cpp | 1040 ++++ .../internal/LuaTypeConversionsTest.cpp | 452 ++ .../internal/PropertyTypeExtractorTest.cpp | 416 ++ .../unittests/internal/RamsesHelperTest.cpp | 35 + .../internal/RamsesObjectResolverTest.cpp | 140 + .../unittests/internal/SolHelperTest.cpp | 29 + .../logic/unittests/internal/SolStateTest.cpp | 244 + .../logic/unittests/internal/TypeDataTest.cpp | 122 + .../unittests/internal/TypeUtilsTest.cpp | 88 + .../internal/WrappedLuaPropertyTest.cpp | 533 +++ .../logic/unittests/res/testLogic_01.rlogic | Bin 0 -> 19416 bytes .../logic/unittests/res/testScene_01.ramses | Bin 0 -> 5852 bytes .../unittests/shared/FeatureLevelTestValues.h | 24 +- client/logic/unittests/shared/LogTestUtils.h | 76 + .../unittests/shared/LogicEngineTest_Base.h | 167 + .../logic/unittests/shared/LogicNodeDummy.cpp | 4 +- .../logic/unittests/shared/LogicNodeDummy.h | 86 + .../unittests/shared/LuaScriptTest_Base.h | 71 + .../unittests/shared/PropertyLinkTestUtils.h | 101 + .../shared/RamsesObjectResolverMock.h | 27 + .../logic/unittests/shared/RamsesTestUtils.h | 177 + .../unittests/shared/SerializationTestUtils.h | 112 + .../unittests/shared/WithTempDirectory.h | 39 + .../testAssetProducer}/CMakeLists.txt | 13 +- .../unittests/testAssetProducer/main.cpp | 355 ++ client/ramses-client-api/Appearance.cpp | 325 ++ client/ramses-client-api/ArrayBuffer.cpp | 73 + .../ramses-client-api/ArrayResource.cpp | 14 +- client/ramses-client-api/AttributeInput.cpp | 49 + .../ramses-client-api/BlitPass.cpp | 26 +- .../ramses-client-api/Camera.cpp | 64 +- .../ramses-client-api/ClientObject.cpp | 10 +- client/ramses-client-api/DataObject.cpp | 91 + client/ramses-client-api/Effect.cpp | 74 + .../ramses-client-api/EffectDescription.cpp | 138 + .../ramses-client-api/EffectInput.cpp | 17 +- .../ramses-client-api/GeometryBinding.cpp | 24 +- .../ramses-client-api/MeshNode.cpp | 40 +- client/ramses-client-api/Node.cpp | 180 + .../ramses-client-api/OrthographicCamera.cpp | 9 +- .../ramses-client-api/PerspectiveCamera.cpp | 14 +- .../ramses-client-api/PickableObject.cpp | 22 +- .../ramses-client-api/RamsesClient.cpp | 62 +- .../ramses-client-api/RamsesObject.cpp | 22 +- .../ramses-client-api/RenderBuffer.cpp | 22 +- .../ramses-client-api/RenderGroup.cpp | 30 +- .../ramses-client-api/RenderPass.cpp | 63 +- .../ramses-client-api/RenderTarget.cpp | 14 +- .../RenderTargetDescription.cpp | 58 + .../ramses-client-api/Resource.cpp | 12 +- .../ramses-client-api/Scene.cpp | 331 +- client/ramses-client-api/SceneConfig.cpp | 58 + .../ramses-client-api/SceneGraphIterator.cpp | 12 +- .../ramses-client-api/SceneObject.cpp | 14 +- .../ramses-client-api/SceneReference.cpp | 18 +- .../ramses-client-api/Texture2D.cpp | 18 +- client/ramses-client-api/Texture2DBuffer.cpp | 55 + .../ramses-client-api/Texture3D.cpp | 18 +- .../ramses-client-api/TextureCube.cpp | 16 +- .../ramses-client-api/TextureEnums.cpp | 26 +- .../ramses-client-api/TextureSampler.cpp | 42 +- .../TextureSamplerExternal.cpp | 10 +- .../ramses-client-api/TextureSamplerMS.cpp | 10 +- client/ramses-client-api/UniformInput.cpp | 54 + .../include/ramses-client-api/Appearance.h | 511 ++ .../include/ramses-client-api/ArrayBuffer.h | 63 +- .../include/ramses-client-api/ArrayResource.h | 38 +- .../ramses-client-api/AttributeInput.h | 69 + .../include/ramses-client-api/BlitPass.h | 34 +- .../include/ramses-client-api/Camera.h | 79 +- .../include/ramses-client-api/ClientObject.h | 33 +- .../include/ramses-client-api/DataObject.h | 101 + .../include/ramses-client-api/ERotationType.h | 48 + .../ramses-client-api/EScenePublicationMode.h | 7 +- .../ramses-client-api/EVisibilityMode.h | 3 +- .../include/ramses-client-api/Effect.h | 60 +- .../ramses-client-api/EffectDescription.h | 73 +- .../include/ramses-client-api/EffectInput.h | 33 +- .../ramses-client-api/EffectInputSemantic.h | 2 + .../ramses-client-api/GeometryBinding.h | 46 +- .../ramses-client-api/IClientEventHandler.h | 7 +- .../include/ramses-client-api/MeshNode.h | 62 +- .../include/ramses-client-api/MipLevelData.h | 39 +- .../include/ramses-client-api/Node.h | 293 ++ .../ramses-client-api/OrthographicCamera.h | 12 +- .../ramses-client-api/PerspectiveCamera.h | 18 +- .../ramses-client-api/PickableObject.h | 30 +- .../include/ramses-client-api/RamsesClient.h | 85 +- .../include/ramses-client-api/RamsesObject.h | 38 +- .../ramses-client-api/RamsesObjectTypes.h | 63 + .../include/ramses-client-api/RenderBuffer.h | 29 +- .../include/ramses-client-api/RenderGroup.h | 36 +- .../RenderGroupMeshIterator.h | 14 +- .../include/ramses-client-api/RenderPass.h | 65 +- .../RenderPassGroupIterator.h | 14 +- .../include/ramses-client-api/RenderTarget.h | 20 +- .../RenderTargetDescription.h | 41 +- .../include/ramses-client-api/Resource.h | 21 +- .../include/ramses-client-api/Scene.h | 361 +- .../include/ramses-client-api/SceneConfig.h | 45 +- .../ramses-client-api/SceneGraphIterator.h | 36 +- .../include/ramses-client-api/SceneIterator.h | 13 +- .../include/ramses-client-api/SceneObject.h | 37 +- .../ramses-client-api/SceneObjectIterator.h | 13 +- .../ramses-client-api/SceneReference.h | 26 +- .../include/ramses-client-api/Texture2D.h | 48 +- .../ramses-client-api/Texture2DBuffer.h | 29 +- .../include/ramses-client-api/Texture3D.h | 48 +- .../include/ramses-client-api/TextureCube.h | 32 +- .../include/ramses-client-api/TextureEnums.h | 114 +- .../ramses-client-api/TextureSampler.h | 43 +- .../TextureSamplerExternal.h | 16 +- .../ramses-client-api/TextureSamplerMS.h | 16 +- .../ramses-client-api/TextureSwizzle.h | 1 + .../include/ramses-client-api/UniformInput.h | 43 +- .../ramses-client-api/include/ramses-client.h | 53 +- .../ramses-client-api/include/ramses-utils.h | 50 +- client/ramses-client/CMakeLists.txt | 96 - .../impl/AnimatedPropertyFactory.cpp | 160 - .../impl/AnimatedPropertyFactory.h | 43 - .../impl/AnimatedPropertyImpl.cpp | 129 - .../ramses-client/impl/AnimatedPropertyImpl.h | 52 - .../impl/AnimatedPropertyUtils.cpp | 80 - .../impl/AnimatedPropertyUtils.h | 39 - client/ramses-client/impl/AnimationImpl.cpp | 168 - client/ramses-client/impl/AnimationImpl.h | 55 - .../impl/AnimationObjectImpl.cpp | 43 - .../ramses-client/impl/AnimationObjectImpl.h | 40 - .../impl/AnimationSequenceImpl.cpp | 455 -- .../impl/AnimationSequenceImpl.h | 90 - .../impl/AnimationSystemData.cpp | 295 -- .../ramses-client/impl/AnimationSystemData.h | 92 - .../impl/AnimationSystemImpl.cpp | 498 -- .../ramses-client/impl/AnimationSystemImpl.h | 139 - .../impl/AnimationSystemIteratorImpl.h | 30 - .../impl/AnimationSystemObjectIterator.cpp | 32 - client/ramses-client/impl/AppearanceImpl.cpp | 247 +- client/ramses-client/impl/AppearanceImpl.h | 36 +- client/ramses-client/impl/AppearanceUtils.h | 457 +- client/ramses-client/impl/ArrayBufferImpl.cpp | 14 +- client/ramses-client/impl/ArrayBufferImpl.h | 18 +- .../ramses-client/impl/ArrayResourceImpl.cpp | 4 +- client/ramses-client/impl/ArrayResourceImpl.h | 12 +- client/ramses-client/impl/BlitPassImpl.cpp | 8 +- client/ramses-client/impl/BlitPassImpl.h | 16 +- client/ramses-client/impl/CameraNodeImpl.cpp | 60 +- client/ramses-client/impl/CameraNodeImpl.h | 26 +- .../impl/ClientApplicationLogic.cpp | 2 +- .../impl/ClientApplicationLogic.h | 16 +- .../impl/ClientCommands/DumpSceneToFile.cpp | 4 +- .../impl/ClientCommands/DumpSceneToFile.h | 7 +- .../impl/ClientCommands/FlushSceneVersion.cpp | 2 +- .../impl/ClientCommands/FlushSceneVersion.h | 2 +- .../ClientCommands/ForceFallbackImage.cpp | 34 - .../impl/ClientCommands/ForceFallbackImage.h | 34 - .../impl/ClientCommands/LogMemoryUtils.cpp | 8 +- .../impl/ClientCommands/LogMemoryUtils.h | 7 +- .../ClientCommands/LogResourceMemoryUsage.cpp | 3 +- .../ClientCommands/LogResourceMemoryUsage.h | 2 +- .../impl/ClientCommands/PrintSceneList.cpp | 9 +- .../impl/ClientCommands/PrintSceneList.h | 2 +- .../impl/ClientCommands/SceneCommandBuffer.h | 20 +- .../ClientCommands/SceneCommandVisitor.cpp | 24 +- .../impl/ClientCommands/SceneCommandVisitor.h | 7 +- .../impl/ClientCommands/ValidateCommand.cpp | 14 +- .../impl/ClientCommands/ValidateCommand.h | 7 +- client/ramses-client/impl/ClientFactory.cpp | 8 +- client/ramses-client/impl/ClientFactory.h | 4 +- .../ramses-client/impl/ClientObjectImpl.cpp | 2 +- client/ramses-client/impl/ClientObjectImpl.h | 6 +- client/ramses-client/impl/DataObjectImpl.cpp | 103 +- client/ramses-client/impl/DataObjectImpl.h | 35 +- client/ramses-client/impl/DataSlotUtils.cpp | 10 +- client/ramses-client/impl/DataTypeUtils.h | 122 - .../impl/EffectDescriptionImpl.cpp | 44 +- .../impl/EffectDescriptionImpl.h | 47 +- client/ramses-client/impl/EffectImpl.cpp | 60 +- client/ramses-client/impl/EffectImpl.h | 35 +- client/ramses-client/impl/EffectInputImpl.cpp | 41 +- client/ramses-client/impl/EffectInputImpl.h | 31 +- client/ramses-client/impl/EffectInputUtils.h | 69 - .../impl/GeometryBindingImpl.cpp | 32 +- .../ramses-client/impl/GeometryBindingImpl.h | 16 +- .../impl/IRamsesObjectRegistry.h | 5 +- client/ramses-client/impl/IteratorImpl.h | 10 +- client/ramses-client/impl/MeshNodeImpl.cpp | 12 +- client/ramses-client/impl/MeshNodeImpl.h | 16 +- client/ramses-client/impl/NodeImpl.cpp | 207 +- client/ramses-client/impl/NodeImpl.h | 62 +- .../ramses-client/impl/PickableObjectImpl.cpp | 16 +- .../ramses-client/impl/PickableObjectImpl.h | 16 +- .../ramses-client/impl/RamsesClientImpl.cpp | 242 +- client/ramses-client/impl/RamsesClientImpl.h | 86 +- .../impl/RamsesClientTypesImpl.h | 4 +- .../ramses-client/impl/RamsesObjectImpl.cpp | 20 +- client/ramses-client/impl/RamsesObjectImpl.h | 19 +- .../impl/RamsesObjectRegistry.cpp | 68 +- .../ramses-client/impl/RamsesObjectRegistry.h | 60 +- .../impl/RamsesObjectRegistryIterator.h | 2 +- .../impl/RamsesObjectTypeTraits.h | 225 +- .../impl/RamsesObjectTypeUtils.cpp | 64 +- client/ramses-client/impl/RamsesVersion.cpp | 81 +- client/ramses-client/impl/RamsesVersion.h | 20 +- .../ramses-client/impl/RenderBufferImpl.cpp | 12 +- client/ramses-client/impl/RenderBufferImpl.h | 14 +- client/ramses-client/impl/RenderGroupImpl.cpp | 8 +- client/ramses-client/impl/RenderGroupImpl.h | 16 +- .../impl/RenderGroupMeshIterator.cpp | 11 +- .../impl/RenderPassGroupIterator.cpp | 11 +- client/ramses-client/impl/RenderPassImpl.cpp | 22 +- client/ramses-client/impl/RenderPassImpl.h | 22 +- .../impl/RenderTargetDescriptionImpl.cpp | 4 +- .../impl/RenderTargetDescriptionImpl.h | 4 +- .../ramses-client/impl/RenderTargetImpl.cpp | 4 +- client/ramses-client/impl/RenderTargetImpl.h | 12 +- .../impl/ResourceDataPoolImpl.cpp | 419 -- .../ramses-client/impl/ResourceDataPoolImpl.h | 90 - client/ramses-client/impl/ResourceImpl.cpp | 11 +- client/ramses-client/impl/ResourceImpl.h | 18 +- .../impl/RotationConventionUtils.h | 68 - client/ramses-client/impl/RotationTypeUtils.h | 66 + client/ramses-client/impl/SceneConfigImpl.h | 2 +- client/ramses-client/impl/SceneDumper.cpp | 98 +- client/ramses-client/impl/SceneDumper.h | 12 +- client/ramses-client/impl/SceneFactory.cpp | 32 +- client/ramses-client/impl/SceneFactory.h | 11 +- .../impl/SceneGraphIteratorImpl.cpp | 12 +- client/ramses-client/impl/SceneImpl.cpp | 1000 ++-- client/ramses-client/impl/SceneImpl.h | 158 +- client/ramses-client/impl/SceneIterator.cpp | 10 +- client/ramses-client/impl/SceneIteratorImpl.h | 16 +- client/ramses-client/impl/SceneObjectImpl.cpp | 2 +- client/ramses-client/impl/SceneObjectImpl.h | 10 +- .../impl/SceneObjectIterator.cpp | 9 +- .../ramses-client/impl/SceneReferenceImpl.cpp | 4 +- .../ramses-client/impl/SceneReferenceImpl.h | 10 +- client/ramses-client/impl/SceneUtils.h | 9 +- .../impl/SerializationContext.cpp | 6 +- .../ramses-client/impl/SerializationContext.h | 6 +- .../ramses-client/impl/SerializationHelper.h | 8 +- client/ramses-client/impl/SplineImpl.cpp | 591 --- client/ramses-client/impl/SplineImpl.h | 102 - .../ramses-client/impl/StreamTextureImpl.cpp | 104 - client/ramses-client/impl/StreamTextureImpl.h | 46 - .../impl/Texture2DBufferImpl.cpp | 24 +- .../ramses-client/impl/Texture2DBufferImpl.h | 26 +- client/ramses-client/impl/Texture2DImpl.cpp | 6 +- client/ramses-client/impl/Texture2DImpl.h | 12 +- client/ramses-client/impl/Texture3DImpl.cpp | 6 +- client/ramses-client/impl/Texture3DImpl.h | 10 +- client/ramses-client/impl/TextureCubeImpl.cpp | 4 +- client/ramses-client/impl/TextureCubeImpl.h | 10 +- .../ramses-client/impl/TextureSamplerImpl.cpp | 116 +- .../ramses-client/impl/TextureSamplerImpl.h | 17 +- client/ramses-client/impl/TextureUtils.cpp | 11 +- client/ramses-client/impl/TextureUtils.h | 315 +- .../impl/VisibilityModeUtils.cpp | 2 +- .../impl/glslEffectBlock/GLSlang.h | 8 - .../impl/glslEffectBlock/GlslEffect.cpp | 57 +- .../impl/glslEffectBlock/GlslEffect.h | 50 +- .../impl/glslEffectBlock/GlslLimits.h | 29 +- .../glslEffectBlock/GlslToEffectConverter.cpp | 37 +- .../glslEffectBlock/GlslToEffectConverter.h | 26 +- .../ramses-client/impl/ramses-hmi-utils.cpp | 82 - .../impl/ramses-text/FontRegistryImpl.cpp | 4 +- .../impl/ramses-text/FontRegistryImpl.h | 6 +- .../ramses-text/Freetype2FontInstance.cpp | 1 + .../impl/ramses-text/Freetype2FontInstance.h | 18 +- .../impl/ramses-text/FreetypeFontFace.cpp | 4 +- .../impl/ramses-text/FreetypeFontFace.h | 4 +- .../impl/ramses-text/GlyphGeometry.h | 5 +- .../impl/ramses-text/GlyphTextureAtlas.cpp | 24 +- .../impl/ramses-text/GlyphTexturePage.cpp | 8 +- .../impl/ramses-text/GlyphTexturePage.h | 8 +- .../impl/ramses-text/HarfbuzzFontInstance.h | 4 +- client/ramses-client/impl/ramses-text/Quad.h | 10 +- .../impl/ramses-text/TextCacheImpl.cpp | 3 +- client/ramses-client/impl/ramses-utils.cpp | 147 +- .../ramses-client-api/Animation.cpp | 36 - .../ramses-client-api/AnimationSequence.cpp | 160 - .../ramses-client-api/AnimationSystem.cpp | 311 -- .../AnimationSystemRealTime.cpp | 32 - .../ramses-client-api/Appearance.cpp | 495 -- .../ramses-client-api/AppearanceEnums.cpp | 204 - .../ramses-client-api/ArrayBuffer.cpp | 53 - .../ramses-client-api/AttributeInput.cpp | 28 - .../ramses-client-api/DataFloat.cpp | 32 - .../ramses-client-api/DataInt32.cpp | 32 - .../ramses-client-api/DataMatrix22f.cpp | 42 - .../ramses-client-api/DataMatrix33f.cpp | 51 - .../ramses-client-api/DataMatrix44f.cpp | 56 - .../ramses-client-api/DataVector2f.cpp | 38 - .../ramses-client-api/DataVector2i.cpp | 38 - .../ramses-client-api/DataVector3f.cpp | 39 - .../ramses-client-api/DataVector3i.cpp | 39 - .../ramses-client-api/DataVector4f.cpp | 40 - .../ramses-client-api/DataVector4i.cpp | 40 - .../ramses-client-api/Effect.cpp | 78 - .../ramses-client-api/EffectDescription.cpp | 110 - .../ramses-client/ramses-client-api/Node.cpp | 186 - .../RenderTargetDescription.cpp | 30 - .../ramses-client-api/ResourceDataPool.cpp | 104 - .../ramses-client-api/SceneConfig.cpp | 40 - .../ramses-client-api/SplineBezierFloat.cpp | 37 - .../ramses-client-api/SplineBezierInt32.cpp | 37 - .../SplineBezierVector2f.cpp | 37 - .../SplineBezierVector2i.cpp | 37 - .../SplineBezierVector3f.cpp | 37 - .../SplineBezierVector3i.cpp | 37 - .../SplineBezierVector4f.cpp | 37 - .../SplineBezierVector4i.cpp | 37 - .../ramses-client-api/SplineLinearFloat.cpp | 37 - .../ramses-client-api/SplineLinearInt32.cpp | 37 - .../SplineLinearVector2f.cpp | 37 - .../SplineLinearVector2i.cpp | 37 - .../SplineLinearVector3f.cpp | 37 - .../SplineLinearVector3i.cpp | 37 - .../SplineLinearVector4f.cpp | 37 - .../SplineLinearVector4i.cpp | 37 - .../ramses-client-api/SplineStepBool.cpp | 37 - .../ramses-client-api/SplineStepFloat.cpp | 37 - .../ramses-client-api/SplineStepInt32.cpp | 37 - .../ramses-client-api/SplineStepVector2f.cpp | 37 - .../ramses-client-api/SplineStepVector2i.cpp | 37 - .../ramses-client-api/SplineStepVector3f.cpp | 37 - .../ramses-client-api/SplineStepVector3i.cpp | 37 - .../ramses-client-api/SplineStepVector4f.cpp | 37 - .../ramses-client-api/SplineStepVector4i.cpp | 37 - .../ramses-client-api/StreamTexture.cpp | 41 - .../ramses-client-api/Texture2DBuffer.cpp | 59 - .../ramses-client-api/UniformInput.cpp | 34 - .../ramses-client-api/AnimatedProperty.h | 82 - .../include/ramses-client-api/Animation.h | 78 - .../ramses-client-api/AnimationObject.h | 62 - .../ramses-client-api/AnimationSequence.h | 277 -- .../ramses-client-api/AnimationSystem.h | 418 -- .../ramses-client-api/AnimationSystemEnums.h | 26 - .../AnimationSystemObjectIterator.h | 57 - .../AnimationSystemRealTime.h | 93 - .../ramses-client-api/AnimationTypes.h | 26 - .../include/ramses-client-api/Appearance.h | 929 ---- .../ramses-client-api/AppearanceEnums.h | 279 -- .../ramses-client-api/AttributeInput.h | 45 - .../include/ramses-client-api/DataFloat.h | 62 - .../include/ramses-client-api/DataInt32.h | 62 - .../include/ramses-client-api/DataMatrix22f.h | 62 - .../include/ramses-client-api/DataMatrix33f.h | 62 - .../include/ramses-client-api/DataMatrix44f.h | 62 - .../include/ramses-client-api/DataObject.h | 56 - .../include/ramses-client-api/DataVector2f.h | 64 - .../include/ramses-client-api/DataVector2i.h | 64 - .../include/ramses-client-api/DataVector3f.h | 66 - .../include/ramses-client-api/DataVector3i.h | 66 - .../include/ramses-client-api/DataVector4f.h | 68 - .../include/ramses-client-api/DataVector4i.h | 68 - .../include/ramses-client-api/EDataType.h | 78 - .../ramses-client-api/ERotationConvention.h | 39 - .../ramses-client-api/EffectInputDataType.h | 41 - .../include/ramses-client-api/Node.h | 355 -- .../ramses-client-api/RamsesObjectTypes.h | 104 - .../ramses-client-api/ResourceDataPool.h | 266 -- .../include/ramses-client-api/Spline.h | 54 - .../ramses-client-api/SplineBezierFloat.h | 72 - .../ramses-client-api/SplineBezierInt32.h | 72 - .../ramses-client-api/SplineBezierVector2f.h | 74 - .../ramses-client-api/SplineBezierVector2i.h | 74 - .../ramses-client-api/SplineBezierVector3f.h | 76 - .../ramses-client-api/SplineBezierVector3i.h | 76 - .../ramses-client-api/SplineBezierVector4f.h | 78 - .../ramses-client-api/SplineBezierVector4i.h | 78 - .../ramses-client-api/SplineLinearFloat.h | 64 - .../ramses-client-api/SplineLinearInt32.h | 64 - .../ramses-client-api/SplineLinearVector2f.h | 66 - .../ramses-client-api/SplineLinearVector2i.h | 66 - .../ramses-client-api/SplineLinearVector3f.h | 68 - .../ramses-client-api/SplineLinearVector3i.h | 68 - .../ramses-client-api/SplineLinearVector4f.h | 70 - .../ramses-client-api/SplineLinearVector4i.h | 70 - .../ramses-client-api/SplineStepBool.h | 64 - .../ramses-client-api/SplineStepFloat.h | 64 - .../ramses-client-api/SplineStepInt32.h | 64 - .../ramses-client-api/SplineStepVector2f.h | 66 - .../ramses-client-api/SplineStepVector2i.h | 66 - .../ramses-client-api/SplineStepVector3f.h | 68 - .../ramses-client-api/SplineStepVector3i.h | 68 - .../ramses-client-api/SplineStepVector4f.h | 70 - .../ramses-client-api/SplineStepVector4i.h | 70 - .../include/ramses-client-api/StreamTexture.h | 77 - .../include/ramses-hmi-utils.h | 80 - .../test/AnimatedPropertyTest.cpp | 308 -- .../test/AnimationSequenceTest.cpp | 639 --- .../AnimationSystemObjectIteratorTest.cpp | 90 - .../test/AnimationSystemTest.cpp | 534 --- client/ramses-client/test/AnimationTest.cpp | 112 - client/ramses-client/test/CreationHelper.cpp | 466 -- client/ramses-client/test/CreationHelper.h | 195 - client/ramses-client/test/DataBufferTest.cpp | 233 - client/ramses-client/test/DataObjectTest.cpp | 405 -- client/ramses-client/test/EDataTypeTest.cpp | 58 - client/ramses-client/test/EffectInputTest.cpp | 256 - .../test/NodeTransformationTest.cpp | 289 -- .../RamsesAnimationObjectOwnershipTest.cpp | 139 - .../ramses-client/test/RamsesHmiUtilsTest.cpp | 182 - .../test/RamsesObjectRegistryTest.cpp | 208 - .../ramses-client/test/RamsesObjectTest1.cpp | 56 - .../ramses-client/test/RamsesObjectTest2.cpp | 44 - .../test/RamsesObjectTestCommon.h | 157 - .../test/RamsesObjectTestTypes.h | 275 -- .../test/RenderTargetDescriptionTest.cpp | 160 - .../test/ResourceDataPoolPersistationTest.cpp | 518 -- .../test/ResourceDataPoolPersistationTest.h | 59 - .../test/ResourceDataPoolTest.cpp | 396 -- client/ramses-client/test/SplineTest.cpp | 261 -- client/ramses-client/test/SplineTestMacros.h | 148 - .../ramses-client/test/StreamTextureTest.cpp | 70 - .../ramses-text-api/FontRegistry.cpp | 2 +- .../ramses-text-api/TextCache.cpp | 0 .../ramses-text-api/UtfUtils.cpp | 6 +- .../include/ramses-text-api/FontCascade.h | 1 + .../include/ramses-text-api/FontInstanceId.h | 5 +- .../ramses-text-api/FontInstanceOffsets.h | 1 + .../include/ramses-text-api/FontRegistry.h | 9 +- .../include/ramses-text-api/Glyph.h | 1 + .../include/ramses-text-api/GlyphMetrics.h | 1 + .../include/ramses-text-api/IFontAccessor.h | 3 +- .../include/ramses-text-api/IFontInstance.h | 11 +- .../include/ramses-text-api/LayoutUtils.h | 3 + .../include/ramses-text-api/TextCache.h | 17 +- .../include/ramses-text-api/TextLine.h | 9 +- .../include/ramses-text-api/UtfUtils.h | 4 +- .../ramses-text-api/include/ramses-text.h | 5 + .../test/AppearanceTest.cpp | 691 +-- client/test/ArrayBufferTest.cpp | 292 ++ .../{ramses-client => }/test/BlitPassTest.cpp | 40 +- .../{ramses-client => }/test/CameraTest.cpp | 246 +- .../test/ClientApplicationLogicTest.cpp | 28 +- .../ClientCommands/SceneCommandBufferTest.cpp | 23 +- .../test/ClientEventHandlerMock.cpp | 0 .../test/ClientEventHandlerMock.h | 8 +- .../test/ClientTestUtils.h | 66 +- client/test/CreationHelper.cpp | 222 + client/test/CreationHelper.h | 110 + client/test/DataObjectTest.cpp | 117 + .../test/EffectDescriptionTest.cpp | 81 +- client/test/EffectInputTest.cpp | 369 ++ .../{ramses-client => }/test/EffectTest.cpp | 152 +- .../test/GeometryBindingTest.cpp | 220 +- .../test/GlslEffectTest.cpp | 74 +- .../test/GlslLimitsTest.cpp | 13 +- .../{ramses-client => }/test/IteratorTest.cpp | 0 .../{ramses-client => }/test/MeshNodeTest.cpp | 24 +- .../test/MockActionCollector.h | 8 +- .../test/NodeLazyTransformTest.cpp | 70 +- client/{ramses-client => }/test/NodeTest.cpp | 391 +- client/test/NodeTransformationTest.cpp | 243 + .../test/NodeVisibilityTest.cpp | 34 +- .../test/PickableObjectTest.cpp | 14 +- client/{ramses-client => }/test/QuadTest.cpp | 0 .../test/RamsesClientTest.cpp | 87 +- .../test/RamsesObjectOwnershipTest.cpp | 62 +- client/test/RamsesObjectRegistryTest.cpp | 182 + client/test/RamsesObjectTest.cpp | 171 + client/test/RamsesObjectTestTypes.h | 118 + .../test/RamsesUtilsTest.cpp | 46 +- .../test/RamsesVersionTest.cpp | 171 +- .../test/RenderBufferTest.cpp | 60 +- .../test/RenderGroupMeshIteratorTest.cpp | 0 .../test/RenderGroupTest.cpp | 42 +- .../test/RenderPassGroupIteratorTest.cpp | 0 .../test/RenderPassTest.cpp | 51 +- client/test/RenderTargetDescriptionTest.cpp | 206 + .../test/RenderTargetTest.cpp | 6 +- .../{ramses-client => }/test/ResourceTest.cpp | 356 +- .../test/SceneDistributionTest.cpp | 20 +- .../test/SceneFactoryTest.cpp | 8 +- .../test/SceneGraphIteratorTest.cpp | 8 +- .../test/SceneIteratorTest.cpp | 0 .../test/SceneObjectIteratorTest.cpp | 14 +- .../test/ScenePersistationTest.cpp | 1060 ++--- .../test/ScenePersistationTest.h | 69 +- .../test/ScenePersistationThreadedTest.cpp | 61 +- .../test/SceneReferenceTest.cpp | 34 +- client/{ramses-client => }/test/SceneTest.cpp | 570 +-- .../test/SerializationHelperTest.cpp | 0 .../test/SimpleSceneTopology.h | 8 +- .../test/TestEffectCreator.h | 6 +- client/{ramses-client => }/test/TestEffects.h | 50 +- .../test/Texture2DBufferTest.cpp | 18 +- .../test/TextureSamplerTest.cpp | 280 +- .../res/ramses-client-test_minimalShader.frag | 0 .../res/ramses-client-test_minimalShader.geom | 0 .../res/ramses-client-test_minimalShader.vert | 0 .../test/res/ramses-client-test_shader.vert | 0 .../res/ramses-text-DroidKufi-Regular.ttf | Bin .../test/res/ramses-text-Roboto-Bold.ttf | Bin .../test/res/ramses-text-Roboto-Regular.ttf | Bin .../test/res/rgba8_expectedFlipped.png | Bin .../test/res/sampleTexture.png | Bin .../test/res/sampleTexture_invalid.png | 0 .../test => test/text}/FontCascadeTest.cpp | 3 +- .../test => test/text}/FontRegistryTest.cpp | 0 .../text}/Freetype2FontInstanceTest.cpp | 0 .../text}/GlyphTextureAtlasTest.cpp | 62 +- .../text}/GlyphTexturePageTest.cpp | 17 +- .../text}/HarfbuzzFontInstanceTest.cpp | 0 .../test => test/text}/LayoutUtilsHMITest.cpp | 3 +- .../test => test/text}/LayoutUtilsTest.cpp | 0 .../test => test/text}/TextCacheTest.cpp | 2 +- .../test => test/text}/UtfUtilsTest.cpp | 0 cmake/modules/FindAndroidSDK.cmake | 14 +- cmake/modules/FindBoost.cmake | 4 +- cmake/modules/FindEGL.cmake | 27 +- cmake/modules/FindLinuxInput.cmake | 16 +- cmake/modules/FindOpenGL.cmake | 2 +- cmake/modules/FindSphinx.cmake | 22 + cmake/ramses/addCheckerTargets.cmake | 14 +- cmake/ramses/addSubdirectory.cmake | 29 + cmake/ramses/buildConfig.cmake | 72 + cmake/ramses/createPackage.cmake | 45 + cmake/ramses/createTarget.cmake | 233 + cmake/ramses/externaltools.cmake | 6 +- cmake/ramses/folderize.cmake | 45 + cmake/ramses/getGitInformation.cmake | 29 +- cmake/ramses/makeTestFromTarget.cmake | 102 + cmake/ramses/platformConfig.cmake | 157 +- cmake/ramses/platformDetection.cmake | 27 +- cmake/ramses/platformTargets.cmake | 4 +- cmake/ramses/rendererModulePerConfig.cmake | 83 - cmake/ramses/resourceCopy.cmake | 80 + cmake/ramses/setCmakePolicies.cmake | 6 + cmake/ramses/testConfig.cmake | 2 +- cmake/templates/build-config.h.in | 19 + ...es-shared-lib-client-onlyTemplate.cmake.in | 73 - ...amses-shared-lib-headlessTemplate.cmake.in | 88 + .../ramses-shared-libTemplate.cmake.in | 82 +- cmake/templates/ramses-version.in | 6 - demo/CMakeLists.txt | 8 +- demo/android/DemoJNIInterface/CMakeLists.txt | 18 +- .../DemoJNIInterface/src/JNIInterface.cpp | 13 +- .../DemoJNIInterface/src/RendererBundle.cpp | 15 +- .../src/UniformInputWrapper.cpp | 2 +- .../DemoRamsesAndroidModule/build.gradle | 2 +- demo/android/README.md | 4 +- .../app/CMakeLists.txt | 2 +- .../app/build.gradle | 1 + .../repositories.gradle | 2 +- demo/ramses-dcsm-list/CMakeLists.txt | 24 - .../res/ramses-dcsm-list-line.png | Bin 11748 -> 0 bytes demo/ramses-dcsm-list/src/main.cpp | 294 -- ...s-demo-scene-references-roboto-regular.ttf | Bin 171676 -> 0 bytes .../src/OverlayClient.cpp | 105 - .../src/OverlayClient.h | 47 - .../src/main.cpp | 329 -- demo/ramses-text-layout-demo/CMakeLists.txt | 24 - .../res/ramses-layout-demo-colored-quad.vert | 19 - .../res/ramses-layout-demo-red-text.vert | 24 - ...ramses-text-layout-demo-Roboto-Regular.ttf | Bin 171676 -> 0 bytes ...ses-text-layout-demo-WenQuanYiMicroHei.ttf | Bin 4505016 -> 0 bytes .../src/TextLayoutDemo.cpp | 244 - .../src/TextLayoutDemo.h | 65 - demo/ramses-text-shadow-demo/CMakeLists.txt | 24 - .../ramses-text-shadow-demo-Roboto-Bold.ttf | Bin 170760 -> 0 bytes .../ramses-text-shadow-demo-a-texturing.frag | 22 - .../ramses-text-shadow-demo-a-texturing.vert | 24 - .../ramses-text-shadow-demo-background.png | Bin 9931 -> 0 bytes ...amses-text-shadow-demo-gauss-filter-h.frag | 52 - ...amses-text-shadow-demo-gauss-filter-h.vert | 24 - ...amses-text-shadow-demo-gauss-filter-v.frag | 52 - ...amses-text-shadow-demo-gauss-filter-v.vert | 24 - ...amses-text-shadow-demo-rgba-texturing.frag | 21 - ...amses-text-shadow-demo-rgba-texturing.vert | 22 - .../res/ramses-text-shadow.frag | 22 - .../res/ramses-text-shadow.vert | 22 - .../src/GaussFilter.cpp | 141 - .../ramses-text-shadow-demo/src/GaussFilter.h | 35 - .../src/GraphicalItem.cpp | 49 - .../src/GraphicalItem.h | 28 - demo/ramses-text-shadow-demo/src/ImageBox.cpp | 125 - demo/ramses-text-shadow-demo/src/ImageBox.h | 50 - demo/ramses-text-shadow-demo/src/TextBox.cpp | 146 - demo/ramses-text-shadow-demo/src/TextBox.h | 52 - .../src/TextBoxWithShadow.cpp | 89 - .../src/TextBoxWithShadow.h | 44 - demo/ramses-text-shadow-demo/src/main.cpp | 117 - doc/CMakeLists.txt | 144 +- doc/developer/images/initiator_with_old.dot | 80 - doc/doxygen/Doxyfile.in | 37 +- doc/doxygen/DoxygenLayout.xml | 192 - doc/general/00_MainPage.dox | 95 - doc/general/10_Client_API.dox | 65 - doc/general/11_Validation.dox | 83 - doc/general/12_Resources.dox | 43 - doc/general/13_Effects.dox | 71 - doc/general/15_Text_Rendering.dox | 90 - doc/general/20_Renderer_API.dox | 99 - doc/general/30_Offscreen_Buffers.dox | 51 - doc/general/60_DCSM_Protocol.dox | 156 - doc/general/80_EmbeddedCompositing.dox | 130 - .../images/ramses_logo_with_alpha2.png | Bin 182174 -> 0 bytes doc/general/images/sequence1a.png | Bin 34353 -> 0 bytes doc/general/images/sequence1b.png | Bin 50978 -> 0 bytes doc/general/images/sequence2.png | Bin 34965 -> 0 bytes doc/main.dox | 36 - .../developer/00_MainPage.dox | 1 - .../developer/10_GettingStarted.dox | 5 +- .../developer/20_Contributing.dox | 0 .../developer/30_CodeStyleGuide.dox | 0 .../developer/40_ClientAPI.dox | 2 +- doc/{ => old_ramses}/developer/50_Testing.dox | 0 .../developer/images/guide-which-tests.png | Bin .../developer/images/initiator.dot | 0 .../images/ramses_logo_with_alpha2.png | Bin .../developer/images/responder.dot | 0 .../developer/images/responder_with_old.dot | 0 doc/{ => old_ramses}/general/40_Examples.dox | 31 +- .../general/50_ContentExpiration.dox | 0 .../general/70_SceneReferencing.dox | 56 +- doc/{ => old_ramses}/tools/00_MainPage.dox | 19 - .../dlt-logging/10_GeneralIntroduction.dox | 0 .../tools/dlt-logging/20_UsingDLT.dox | 0 .../dlt-logging/images/dltviewer_connect.png | Bin .../images/dltviewer_connect_tcp.png | Bin .../images/dltviewer_context_inject.png | Bin .../dlt-logging/images/dltviewer_contexts.png | Bin .../images/dltviewer_injection.png | Bin .../dlt-logging/images/dltviewer_messages.png | Bin .../dlt-logging/images/dltviewer_plugin.png | Bin .../tools/profiling/10_Profiling.dox | 3 - .../tools/scene-viewer/10_SceneViewer.dox | 0 .../_static/logo.png} | Bin doc/sphinx/build.rst | 235 + doc/sphinx/changelog_ref.md | 2 + doc/sphinx/classes/core.rst | 19 + doc/sphinx/classes/generate_classes.py | 343 ++ doc/sphinx/classes/index.rst | 45 + doc/sphinx/classes/logic.rst | 19 + doc/sphinx/classes/renderer.rst | 19 + doc/sphinx/classes/text.rst | 19 + doc/sphinx/classes/utils.rst | 19 + doc/sphinx/conf.py | 84 + doc/sphinx/core.rst | 705 +++ doc/sphinx/dev.rst | 259 + doc/sphinx/examples/core/00_minimal.rst | 19 + doc/sphinx/examples/logic/00_minimal.rst | 19 + .../logic/01a_primitive_properties.rst | 18 + .../examples/logic/01b_struct_properties.rst | 19 + .../examples/logic/01c_array_properties.rst | 19 + .../examples/logic/02_errors_compile_time.rst | 19 + .../examples/logic/03_errors_runtime.rst | 19 + doc/sphinx/examples/logic/04_ramses_scene.rst | 19 + .../examples/logic/05_serialization.rst | 19 + doc/sphinx/examples/logic/07_links.rst | 19 + .../examples/logic/08a_static_animation.rst | 19 + .../examples/logic/08b_dynamic_animation.rst | 19 + doc/sphinx/examples/logic/09_modules.rst | 19 + doc/sphinx/examples/logic/10_globals.rst | 19 + doc/sphinx/examples/logic/11_interfaces.rst | 19 + doc/sphinx/examples/logic/12_anchor_point.rst | 19 + doc/sphinx/examples/logic/13_render_order.rst | 19 + doc/sphinx/examples/logic/14_skinbinding.rst | 19 + .../examples/logic/15_meshnodebinding.rst | 19 + doc/sphinx/index.rst | 66 + doc/sphinx/logic.rst | 736 +++ doc/sphinx/lua_syntax.rst | 565 +++ doc/sphinx/readme_ref.md | 2 + doc/sphinx/requirements.txt | 40 + doc/sphinx/res/architecture.png | Bin 0 -> 43735 bytes .../images => sphinx/res}/glyph_atlas.png | Bin .../images => sphinx/res}/glyph_metrics.png | Bin .../images => sphinx/res}/glyph_zoomed.png | Bin doc/sphinx/res/overview.svg | 286 ++ .../res}/ramses_api_text_geometry.png | Bin .../res}/renderer_scene_states.png | Bin doc/sphinx/viewer.rst | 271 ++ .../resource-tools/10_EffectLifecycle.dox | 83 - .../resource-tools/20_GLSLToBinaryShader.dox | 77 - .../30_GLSLToEffectResource.dox | 59 - .../resource-tools/40_ResourcePacker.dox | 55 - .../50_ExampleUsageAllTools.dox | 61 - .../resource-tools/images/ShaderTools.png | Bin 56363 -> 0 bytes examples/CMakeLists.txt | 66 +- examples/logic/00_minimal/main.cpp | 71 + .../logic/01a_primitive_properties/main.cpp | 80 + examples/logic/01b_struct_properties/main.cpp | 74 + examples/logic/01c_array_properties/main.cpp | 85 + .../logic/02_errors_compile_time/main.cpp | 55 + examples/logic/03_errors_runtime/main.cpp | 59 + examples/logic/04_ramses_scene/main.cpp | 197 + examples/logic/05_serialization/main.cpp | 221 + examples/logic/07_links/main.cpp | 79 + examples/logic/08a_static_animation/main.cpp | 296 ++ examples/logic/08b_dynamic_animation/main.cpp | 318 ++ examples/logic/09_modules/main.cpp | 74 + examples/logic/10_globals/main.cpp | 51 + examples/logic/11_interfaces/main.cpp | 68 + examples/logic/12_anchor_point/main.cpp | 284 ++ examples/logic/13_render_order/main.cpp | 198 + examples/logic/14_skinbinding/main.cpp | 346 ++ examples/logic/15_meshnodebinding/main.cpp | 235 + examples/logic/CMakeLists.txt | 55 + examples/logic/shared/SimpleRenderer.h | 84 + .../CMakeLists.txt | 24 - .../src/main.cpp | 181 - .../CMakeLists.txt | 24 - .../src/main.cpp | 142 - .../CMakeLists.txt | 17 +- .../src/main.cpp | 39 +- .../CMakeLists.txt | 24 - .../src/main.cpp | 117 - .../CMakeLists.txt | 17 +- .../src/main.cpp | 21 +- .../CMakeLists.txt | 17 +- .../src/main.cpp | 130 +- .../CMakeLists.txt | 17 +- .../src/main.cpp | 13 +- .../CMakeLists.txt | 17 +- .../src/main.cpp | 25 +- .../CMakeLists.txt | 17 +- .../src/main.cpp | 19 +- .../CMakeLists.txt | 17 +- .../src/main.cpp | 39 +- .../CMakeLists.txt | 17 +- .../src/main.cpp | 23 +- .../CMakeLists.txt | 17 +- .../src/main.cpp | 28 +- .../CMakeLists.txt | 17 +- .../src/main.cpp | 35 +- .../CMakeLists.txt | 17 +- .../src/main.cpp | 62 +- .../CMakeLists.txt | 24 - .../ramses-example-dcsm-provider-outline.frag | 42 - .../ramses-example-dcsm-provider-outline.vert | 22 - .../ramses-example-dcsm-provider-texture.png | Bin 256263 -> 0 bytes .../res/ramses-example-dcsm-provider.frag | 20 - .../res/ramses-example-dcsm-provider.vert | 22 - .../ramses-example-dcsm-provider/src/main.cpp | 352 -- .../CMakeLists.txt | 17 +- .../src/main.cpp | 61 +- .../CMakeLists.txt | 17 +- .../src/main.cpp | 12 +- .../src/main.cpp | 365 -- .../CMakeLists.txt | 8 +- .../ramses-example-local-client/src/main.cpp | 66 +- .../CMakeLists.txt | 8 +- ...ses-example-local-compositing-texture.png} | Bin .../ramses-example-local-compositing.frag} | 0 .../ramses-example-local-compositing.vert} | 0 .../src/main.cpp | 245 + .../CMakeLists.txt | 8 +- .../src/main.cpp | 157 +- .../CMakeLists.txt | 8 +- .../src/main.cpp | 119 +- .../CMakeLists.txt | 9 +- .../src/main.cpp | 101 +- .../CMakeLists.txt | 8 +- .../src/main.cpp | 53 +- .../CMakeLists.txt | 8 +- .../src/main.cpp | 76 +- .../CMakeLists.txt | 14 +- .../src/main.cpp | 65 +- .../CMakeLists.txt | 10 +- .../src/main.cpp | 111 +- .../CMakeLists.txt | 8 +- .../src/main.cpp | 142 +- .../ramses-example-minimal/CMakeLists.txt | 15 +- examples/ramses-example-minimal/src/main.cpp | 5 +- .../ramses-example-renderonce/CMakeLists.txt | 17 +- .../ramses-example-renderonce/src/main.cpp | 52 +- .../ramses-example-text-basic/CMakeLists.txt | 15 +- .../ramses-example-text-basic/src/main.cpp | 17 +- .../CMakeLists.txt | 15 +- .../src/main.cpp | 19 +- external/CMakeLists.txt | 397 +- external/acme2/LICENSE.txt | 202 - external/acme2/acme2.cmake | 190 - external/acme2/internal/api.cmake | 102 - external/acme2/internal/build-config.h.in | 27 - .../acme2/internal/create_build_config.cmake | 91 - external/acme2/internal/create_package.cmake | 56 - external/acme2/internal/module_template.cmake | 163 - external/acme2/internal/tools.cmake | 177 - external/acme2/tests/CMakeLists.txt | 174 - .../shared_resources/shared_resource.txt | 0 external/acme2/tests/test1/CMakeLists.txt | 13 - external/acme2/tests/test1/test1.cpp | 0 external/acme2/tests/test10/CMakeLists.txt | 14 - external/acme2/tests/test10/test10.cpp | 0 external/acme2/tests/test11/CMakeLists.txt | 15 - external/acme2/tests/test11/test11.cpp | 0 external/acme2/tests/test2/CMakeLists.txt | 44 - external/acme2/tests/test2/test2.cpp | 0 .../executableInclude.h | 0 .../test2_include/staticlibraryInclude.h | 0 .../acme2/tests/test2/test2_staticLibrary.cpp | 0 external/acme2/tests/test3/CMakeLists.txt | 19 - .../acme2/tests/test3/FindCustomLibrary.cmake | 30 - .../staticlibraryInclude.h | 0 external/acme2/tests/test3/test3.cpp | 0 external/acme2/tests/test8/CMakeLists.txt | 57 - .../test8/test8_executable/include/inc.h | 0 .../test8/test8_executable/test8_exe.cpp | 0 .../tests/test8/test8_staticA/include/inc.h | 0 .../test8/test8_staticA/test8_staticA.cpp | 0 .../tests/test8/test8_staticB/include/inc.h | 0 .../test8/test8_staticB/test8_staticB.cpp | 0 external/asio | 2 +- external/freetype | 2 +- .../simple-dmabuf-egl.c | 1310 ++++++ .../ActionCollectingAnimationSystem.h | 69 - .../include/Animation/AnimatableTypeTraits.h | 539 --- .../Animation/include/Animation/Animation.h | 50 - .../Animation/AnimationCollectionTypes.h | 25 - .../include/Animation/AnimationCommon.h | 84 - .../include/Animation/AnimationData.h | 140 - .../include/Animation/AnimationDataBind.h | 360 -- .../include/Animation/AnimationDataListener.h | 56 - .../include/Animation/AnimationDataNotifier.h | 38 - .../include/Animation/AnimationInstance.h | 76 - .../include/Animation/AnimationLogic.h | 84 - .../Animation/AnimationLogicListener.h | 69 - .../Animation/AnimationLogicNotifier.h | 41 - .../include/Animation/AnimationProcessData.h | 42 - .../Animation/AnimationProcessDataCache.h | 92 - .../Animation/AnimationProcessDataDispatch.h | 76 - .../include/Animation/AnimationProcessing.h | 47 - .../Animation/AnimationProcessingFinished.h | 37 - .../Animation/AnimationStateChangeCollector.h | 37 - .../include/Animation/AnimationSystem.h | 118 - .../Animation/AnimationSystemDescriber.h | 64 - .../Animation/AnimationSystemFactory.h | 41 - .../include/Animation/AnimationTime.h | 112 - .../include/Animation/Interpolator.h | 204 - .../Animation/include/Animation/Spline.h | 148 - .../Animation/include/Animation/SplineBase.h | 66 - .../include/Animation/SplineIterator.h | 74 - .../Animation/include/Animation/SplineKey.h | 37 - .../include/Animation/SplineKeyTangents.h | 45 - .../include/Animation/SplineSegment.h | 49 - .../include/Animation/SplineSolver.h | 119 - .../include/Animation/SplineSolverHelper.h | 99 - .../src/ActionCollectingAnimationSystem.cpp | 220 - .../Animation/Animation/src/Animation.cpp | 21 - .../Animation/Animation/src/AnimationData.cpp | 336 -- .../Animation/src/AnimationDataNotifier.cpp | 82 - .../Animation/src/AnimationInstance.cpp | 38 - .../Animation/src/AnimationLogic.cpp | 294 -- .../Animation/src/AnimationLogicNotifier.cpp | 75 - .../src/AnimationProcessDataDispatch.cpp | 163 - .../Animation/src/AnimationProcessing.cpp | 112 - .../src/AnimationProcessingFinished.cpp | 82 - .../src/AnimationStateChangeCollector.cpp | 53 - .../Animation/src/AnimationSystem.cpp | 581 --- .../src/AnimationSystemDescriber.cpp | 221 - .../Animation/src/AnimationSystemFactory.cpp | 40 - .../Animation/src/SplineIterator.cpp | 155 - .../ActionCollectingAnimationSystemTest.cpp | 189 - .../Animation/test/AnimationDataBindTest.cpp | 345 -- .../test/AnimationDataBindTestUtils.h | 243 - .../Animation/test/AnimationDataTest.cpp | 351 -- .../Animation/test/AnimationDataTest.h | 56 - .../Animation/test/AnimationInstanceTest.cpp | 64 - .../Animation/test/AnimationLogicTest.cpp | 372 -- .../Animation/test/AnimationLogicTest.h | 37 - .../test/AnimationProcessingTest.cpp | 612 --- .../Animation/test/AnimationProcessingTest.h | 54 - .../AnimationStateChangeCollectorTest.cpp | 88 - .../Animation/test/AnimationSyncTest.cpp | 247 - .../Animation/test/AnimationSyncTest.h | 111 - .../test/AnimationSystemDescriberTest.cpp | 169 - .../test/AnimationSystemSyncTest.cpp | 224 - .../Animation/test/AnimationSystemSyncTest.h | 81 - .../Animation/test/AnimationSystemTest.cpp | 102 - .../Animation/test/AnimationTestTypes.h | 44 - .../Animation/test/AnimationTestUtils.cpp | 28 - .../Animation/test/AnimationTestUtils.h | 214 - .../Animation/test/AnimationTimeTest.cpp | 65 - .../Animation/test/InterpolatorTest.cpp | 57 - .../Animation/test/SplineIteratorTest.cpp | 211 - .../Animation/test/SplineIteratorTest.h | 38 - .../Animation/test/SplineSolverTest.cpp | 283 -- .../Animation/test/SplineSolverTest.h | 47 - .../Animation/Animation/test/SplineTest.cpp | 200 - .../Animation/test/SplineTestUtils.h | 110 - .../AnimationSystemSizeInformation.h | 45 - .../include/AnimationAPI/IAnimationSystem.h | 104 - framework/CMakeLists.txt | 461 +- .../ConnectionStatusUpdateNotifier.h | 6 +- .../TransportCommon/ConnectionSystemBase.h | 1280 ----- .../ConnectionSystemInitiatorResponder.h | 1377 ------ .../TransportCommon/DcsmConnectionSystem.h | 76 - .../TransportCommon/EConnectionProtocol.h | 10 +- .../TransportCommon/EConnectionStatus.h | 2 +- .../FakeConnectionStatusUpdateNotifier.h | 4 +- .../TransportCommon/FakeConnectionSystem.h | 104 +- .../TransportCommon/ICommunicationSystem.h | 31 +- .../TransportCommon/ISomeIPDcsmStack.h | 69 - .../TransportCommon/ISomeIPRamsesStack.h | 89 - .../TransportCommon/LogConnectionInfo.h | 3 +- .../TransportCommon/RamsesConnectionSystem.h | 76 - .../RamsesTransportProtocolVersion.h | 2 +- .../TransportCommon/SceneUpdateSerializer.h | 4 +- .../ServiceHandlerInterfaces.h | 28 +- .../include/TransportCommon/SomeIPAdapter.h | 86 - .../SomeIPConnectionSystemMultiplexer.h | 98 - .../TransportCommon/SomeIPStackCommon.h | 86 - .../src/CommunicationSystemFactory.cpp | 22 +- .../src/DcsmConnectionSystem.cpp | 244 - .../src/RamsesConnectionSystem.cpp | 225 - .../src/SceneUpdateSerializationHelper.cpp | 16 +- .../src/SingleSceneUpdateWriter.cpp | 16 +- .../src/SomeIPConnectionSystemMultiplexer.cpp | 354 -- .../test/CommunicationSystemTest.cpp | 29 +- .../test/CommunicationSystemTest.h | 2 - .../test/CommunicationSystemTestWrapper.cpp | 17 +- .../test/CommunicationSystemTestWrapper.h | 12 +- .../test/ConcreteConnectionSystemCommonTest.h | 95 - .../test/ConnectionSystemBaseStressTest.cpp | 136 - .../test/ConnectionSystemBaseTest.cpp | 1498 ------ ...ConnectionSystemBaseThreadedStressTest.cpp | 730 --- ...ConnectionSystemInitiatorResponderTest.cpp | 2690 ----------- .../test/ConnectionSystemTestCommon.h | 139 - .../test/ConnectionSystemTestHelper.cpp | 7 +- .../test/ConnectionSystemTestHelper.h | 8 +- .../test/DcsmConnectionSystemTest.cpp | 239 - ...lushInformationSerializationHelperTest.cpp | 8 +- .../test/RamsesConnectionSystemTest.cpp | 281 -- .../SceneUpdateSerializationHelperTest.cpp | 2 +- .../test/SceneUpdateSerializationTest.cpp | 4 +- .../test/SceneUpdateSerializerTestHelper.h | 2 +- .../SomeIPConnectionSystemMultiplexerTest.cpp | 404 -- .../test/SomeIPStackCommonTest.cpp | 27 - .../TransportCommon/test/SomeIPStackMocks.cpp | 24 - .../TransportCommon/test/SomeIPStackMocks.h | 133 - .../include/TransportTCP/AsioWrapper.h | 17 +- .../include/TransportTCP/EMessageId.h | 24 +- .../TransportTCP/NetworkParticipantAddress.h | 13 +- .../TransportTCP/TCPConnectionSystem.h | 73 +- .../TransportTCP/src/TCPConnectionSystem.cpp | 382 +- .../include/Components/CategoryInfo.h | 135 - .../include/Components/ClientSceneLogicBase.h | 10 +- .../Components/ClientSceneLogicDirect.h | 2 +- .../Components/ClientSceneLogicShadowCopy.h | 4 +- .../include/Components/DcsmComponent.h | 198 - .../include/Components/DcsmMetadata.h | 210 - .../Components/include/Components/DcsmTypes.h | 91 - .../include/Components/EffectUniformTime.h | 2 +- .../Components/FileInputStreamContainer.h | 4 +- .../include/Components/FlushInformation.h | 4 +- .../include/Components/IDcsmComponent.h | 55 - .../Components/IDcsmProviderEventHandler.h | 32 - .../IManagedResourceDeleterCallback.h | 2 +- .../Components/IResourceProviderComponent.h | 5 +- .../include/Components/ISceneGraphSender.h | 4 +- .../Components/MemoryInputStreamContainer.h | 2 + .../include/Components/ResourceComponent.h | 26 +- .../Components/ResourceFilesRegistry.h | 2 +- .../include/Components/ResourceHashUsage.h | 4 +- .../Components/ResourceSerializationHelper.h | 7 +- .../include/Components/ResourceStorage.h | 12 +- .../Components/ResourceTableOfContents.h | 12 +- .../include/Components/SceneGraphComponent.h | 79 +- .../Components/SingleResourceSerialization.h | 2 +- framework/Components/src/CategoryInfo.cpp | 312 -- .../Components/src/ClientSceneLogicBase.cpp | 9 +- .../Components/src/ClientSceneLogicDirect.cpp | 5 +- .../src/ClientSceneLogicShadowCopy.cpp | 10 +- framework/Components/src/DcsmComponent.cpp | 1952 -------- framework/Components/src/DcsmMetadata.cpp | 804 ---- .../Components/src/EffectUniformTime.cpp | 6 +- framework/Components/src/LogDcsmInfo.cpp | 30 - .../Components/src/ResourceComponent.cpp | 5 - .../Components/src/ResourcePersistation.cpp | 24 +- .../src/ResourceSerializationHelper.cpp | 20 +- framework/Components/src/ResourceStorage.cpp | 1 - .../src/ResourceTableOfContents.cpp | 10 +- .../Components/src/SceneGraphComponent.cpp | 37 +- .../src/SingleResourceSerialization.cpp | 2 +- .../test/AbstractSenderAndReceiverTest.h | 2 +- .../Components/test/CategoryInfoTest.cpp | 285 -- .../Components/test/ClientSceneLogicTest.cpp | 118 +- .../Components/test/DcsmComponentTest.cpp | 3683 --------------- .../test/DcsmComponentThreadTest.cpp | 249 - .../Components/test/DcsmMetadataTest.cpp | 851 ---- .../test/DcsmSenderAndReceiverTest.cpp | 199 - .../Components/test/ResourceComponentTest.cpp | 28 +- .../test/ResourceFileRegistryTest.cpp | 6 +- .../test/ResourcePersistationTest.cpp | 48 +- .../test/ResourceSerializationTestHelper.h | 25 +- .../test/ResourceTableOfContentsTest.cpp | 12 +- .../test/SceneGraphComponentTest.cpp | 111 +- ...ceneGraphProtocolSenderAndReceiverTest.cpp | 12 +- .../Core/Common/include/Common/MemoryHandle.h | 2 +- .../include/Common/ParticipantIdentifier.h | 14 +- .../Common/include/Common/TypedMemoryHandle.h | 32 +- .../Core/Common/src/ParticipantIdentifier.cpp | 2 +- .../include/Math3d/CameraMatrixHelper.h | 42 +- .../Core/Math3d/include/Math3d/Matrix22f.h | 228 - .../Core/Math3d/include/Math3d/Matrix33f.h | 233 - .../Core/Math3d/include/Math3d/Matrix44f.h | 448 -- .../Math3d/include/Math3d/ProjectionParams.h | 26 +- framework/Core/Math3d/include/Math3d/Quad.h | 16 +- .../Vector3.cpp => include/Math3d/Rotation.h} | 14 +- .../Core/Math3d/include/Math3d/Vector2.h | 227 - .../Core/Math3d/include/Math3d/Vector2i.h | 215 - .../Core/Math3d/include/Math3d/Vector3.h | 263 -- .../Core/Math3d/include/Math3d/Vector3i.h | 235 - .../Core/Math3d/include/Math3d/Vector4.h | 287 -- .../Core/Math3d/include/Math3d/Vector4i.h | 249 - framework/Core/Math3d/src/Matrix33f.cpp | 277 -- framework/Core/Math3d/src/Matrix44f.cpp | 22 - .../Core/Math3d/src/ProjectionParams.cpp | 12 +- framework/Core/Math3d/src/Quad.cpp | 4 +- framework/Core/Math3d/src/Rotation.cpp | 73 + .../Math3d/test/CameraMatrixHelperTest.cpp | 88 +- framework/Core/Math3d/test/Matrix22fTest.cpp | 148 - framework/Core/Math3d/test/Matrix33fTest.cpp | 351 -- framework/Core/Math3d/test/Matrix44Test.cpp | 672 --- framework/Core/Math3d/test/RotationTest.cpp | 164 + framework/Core/Math3d/test/Vector2Test.cpp | 199 - framework/Core/Math3d/test/Vector2iTest.cpp | 183 - framework/Core/Math3d/test/Vector3Test.cpp | 233 - framework/Core/Math3d/test/Vector3iTest.cpp | 197 - framework/Core/Math3d/test/Vector4Test.cpp | 245 - framework/Core/Math3d/test/Vector4iTest.cpp | 214 - .../EnqueueOnlyOneAtATimeQueue.h | 8 +- .../TaskFramework/ProcessingTaskQueue.h | 2 +- .../include/TaskFramework/RefCounted.h | 2 +- .../TaskFramework/TaskExecutingThread.h | 2 +- .../TaskFramework/TaskExecutingThreadPool.h | 2 +- .../TaskFinishHandlerDecorator.h | 4 +- .../TaskFramework/TaskForwardingQueue.h | 8 +- .../TaskFramework/ThreadedTaskExecutor.h | 8 +- .../TaskFramework/src/TaskExecutingThread.cpp | 8 - .../src/TaskExecutingThreadPool.cpp | 4 +- .../src/ThreadedTaskExecutor.cpp | 4 +- framework/Core/TaskFramework/test/MockITask.h | 2 +- .../Core/TaskFramework/test/MockTaskQueue.h | 2 +- .../test/ProcessingTaskQueueTest.cpp | 2 +- .../test/TaskForwardingQueueTest.cpp | 4 +- .../test/ThreadedTaskExecutorTest.cpp | 28 +- .../Utils/include/Utils/Adler32Checksum.h | 10 +- framework/Core/Utils/include/Utils/Argument.h | 222 - .../Core/Utils/include/Utils/ArgumentBool.h | 85 - .../Core/Utils/include/Utils/AssertMovable.h | 14 +- .../include/Utils/BinaryFileInputStream.h | 2 +- .../include/Utils/BinaryFileOutputStream.h | 2 +- .../Utils/include/Utils/BinaryInputStream.h | 6 +- .../Utils/BinaryOffsetFileInputStream.h | 2 +- .../Utils/include/Utils/BinaryOutputStream.h | 6 +- .../Utils/include/Utils/CommandLineArgument.h | 66 - .../Utils/include/Utils/CommandLineParser.h | 35 - .../Utils/include/Utils/ConsoleLogAppender.h | 4 +- framework/Core/Utils/include/Utils/DataBind.h | 255 - .../Core/Utils/include/Utils/DataBindCommon.h | 95 - .../Core/Utils/include/Utils/DataBindTypes.h | 142 - .../Core/Utils/include/Utils/DataTypeUtils.h | 55 +- framework/Core/Utils/include/Utils/File.h | 14 +- .../Core/Utils/include/Utils/HandlePool.h | 28 +- framework/Core/Utils/include/Utils/Image.h | 42 +- .../include/Utils/InplaceStringTokenizer.h | 10 +- .../Core/Utils/include/Utils/LogContext.h | 19 +- .../Core/Utils/include/Utils/LogHelper.h | 14 +- framework/Core/Utils/include/Utils/LogLevel.h | 14 +- .../Core/Utils/include/Utils/LogMacros.h | 2 - .../Core/Utils/include/Utils/LogMessage.h | 6 +- .../Core/Utils/include/Utils/LoggingUtils.h | 33 +- .../Core/Utils/include/Utils/MemoryPool.h | 30 +- .../Utils/include/Utils/MemoryPoolExplicit.h | 32 +- .../Core/Utils/include/Utils/MemoryUtils.h | 4 +- .../Core/Utils/include/Utils/MessagePool.h | 57 +- .../Core/Utils/include/Utils/PeriodicLogger.h | 16 +- .../include/Utils/PeriodicLoggerHelper.h | 4 +- .../Core/Utils/include/Utils/RamsesLogger.h | 47 +- .../include/Utils/RawBinaryOutputStream.h | 6 +- .../Utils/include/Utils/StatisticCollection.h | 68 +- .../Utils/StringOutputSpecialWrapper.h | 20 - .../Core/Utils/include/Utils/StringUtils.h | 27 +- .../Utils/include/Utils/TextureMathUtils.h | 22 +- .../Utils/include/Utils/UserLogAppender.h | 2 +- .../include/Utils/VectorBinaryOutputStream.h | 2 +- .../Utils/include/Utils/VoidOutputStream.h | 2 +- .../Core/Utils/src/CommandLineParser.cpp | 105 - framework/Core/Utils/src/File.cpp | 30 +- framework/Core/Utils/src/Image.cpp | 49 +- framework/Core/Utils/src/LogHelper.cpp | 54 +- framework/Core/Utils/src/LogMacros.cpp | 1 - framework/Core/Utils/src/PeriodicLogger.cpp | 14 +- framework/Core/Utils/src/RamsesLogger.cpp | 183 +- .../Core/Utils/src/StatisticCollection.cpp | 10 +- framework/Core/Utils/src/StringUtils.cpp | 74 +- framework/Core/Utils/src/ThreadLocalLog.cpp | 2 + .../Utils/test/BinaryFileInputStreamTest.cpp | 7 +- .../Utils/test/BinaryFileOutputStreamTest.cpp | 17 +- .../Core/Utils/test/BinaryInputStreamTest.cpp | 11 +- .../Utils/test/BinaryOutputStreamTest.cpp | 5 +- .../Core/Utils/test/CommandLineParserTest.cpp | 609 --- framework/Core/Utils/test/DataBindTest.cpp | 173 - framework/Core/Utils/test/DataBindTestUtils.h | 229 - framework/Core/Utils/test/FileTest.cpp | 6 +- framework/Core/Utils/test/ImageTest.cpp | 50 +- .../Utils/test/InplaceStringTokenizerTest.cpp | 8 +- framework/Core/Utils/test/LogHelperTest.cpp | 23 + .../Core/Utils/test/LoggingUtilsTest.cpp | 4 +- .../Utils/test/MemoryPoolExplicitTest.cpp | 16 +- .../Utils/test/MemoryPoolIteratorTest.cpp | 14 +- framework/Core/Utils/test/MemoryPoolTest.cpp | 26 +- framework/Core/Utils/test/MessagePoolTest.cpp | 31 +- .../Utils/test/PeriodicLoggerHelperTest.cpp | 4 +- framework/Core/Utils/test/StringUtilsTest.cpp | 69 +- .../Core/Utils/test/ThreadBarrierTest.cpp | 2 +- .../Core/Utils/test/VoidOutputStreamTest.cpp | 44 +- .../DltAdapterDummy/DltAdapterDummy.h | 15 +- .../DltAdapterImpl/DltAdapterImpl.h | 22 +- .../include/DltLogAppender/DltLogAppender.h | 2 +- .../include/DltLogAppender/IDltAdapter.h | 10 +- .../src/DltAdapterImpl/DltAdapterImpl.cpp | 21 +- .../DltLogAppender/test/DltAdapterTest.cpp | 7 +- .../include/CommunicationSystemMock.h | 20 +- .../include/ComponentMocks.h | 12 +- .../include/DcsmEventHandlerMocks.h | 67 - .../include/DcsmGmockPrinter.h | 88 - .../include/DummyResource.h | 12 +- .../include/FileDescriptorHelper.h | 1 + .../include/IOStreamTester.h | 10 +- .../FrameworkTestUtils/include/ResourceMock.h | 14 +- .../include/SceneRendererHandlerMock.h | 4 +- .../include/ServiceHandlerMocks.h | 34 +- .../include/TestEqualHelper.h | 14 +- .../FrameworkTestUtils/include/TestRandom.h | 4 +- .../include/framework_common_gmock_header.h | 14 +- .../src/CommunicationSystemMock.cpp | 8 - .../src/DcsmEventHandlerMocks.cpp | 24 - .../src/ServiceHandlerMocks.cpp | 16 - .../src/framework_common_gmock_header.cpp | 42 +- .../Monitoring/include/Monitoring/Monitor.h | 55 - framework/Monitoring/src/Monitor.cpp | 80 - .../include/Collections/Guid.h | 11 +- .../include/Collections/HashMap.h | 7 +- .../include/Collections/HeapArray.h | 15 +- .../include/Collections/IInputStream.h | 66 +- .../include/Collections/IOutputStream.h | 8 + .../include/Collections/String.h | 505 -- .../PlatformEnvironmentVariables.h | 22 +- .../PlatformAbstraction/PlatformError.h | 2 +- .../PlatformAbstraction/PlatformMemory.h | 6 +- .../PlatformAbstraction/PlatformThread.h | 19 +- .../PlatformAbstraction/PlatformTime.h | 24 +- .../PlatformAbstraction/PlatformTypes.h | 14 - .../PlatformAbstraction/VariantWrapper.h | 7 +- .../PlatformAbstraction/internal/GPTPHelper.h | 65 - .../internal/Thread_Posix.h | 41 +- .../PlatformAbstraction/synchronized_clock.h | 17 +- .../PlatformAbstraction/src/ConsoleInput.cpp | 36 +- framework/PlatformAbstraction/src/Guid.cpp | 4 +- .../src/PlatformSignal.cpp | 6 - .../PlatformAbstraction/src/PlatformTypes.cpp | 2 +- .../src/synchronized_clock.cpp | 23 - .../PlatformAbstraction/test/AbseilTest.cpp | 7 +- .../PlatformAbstraction/test/GuidTest.cpp | 18 +- .../test/NumericLimitsTest.cpp | 2 +- .../test/PlatformEndianess.cpp | 1 + .../test/PlatformEnvironmentVariablesTest.cpp | 8 +- .../test/PlatformThreadTest.cpp | 2 +- .../test/PlatformTimeTest.cpp | 2 +- .../test/StringOutputStreamTest.cpp | 13 +- .../PlatformAbstraction/test/StringTest.cpp | 764 --- framework/Ramsh/include/Ramsh/RamshCommand.h | 6 +- .../include/Ramsh/RamshCommandArguments.h | 30 +- .../Ramsh/RamshCommandArgumentsConverter.h | 58 +- .../Ramsh/RamshCommandArgumentsDataProvider.h | 44 +- .../Ramsh/RamshCommandArgumentsUtils.h | 4 +- .../Ramsh/RamshCommandDcsmStatusMessage.h | 36 - .../Ramsh/include/Ramsh/RamshCommandExit.h | 2 +- .../Ramsh/RamshCommandPrintBuildConfig.h | 2 +- .../include/Ramsh/RamshCommandPrintHelp.h | 2 +- .../Ramsh/RamshCommandPrintLogLevels.h | 2 +- .../Ramsh/RamshCommandPrintRamsesVersion.h | 2 +- .../Ramsh/RamshCommandSetConsoleLogLevel.h | 2 +- .../Ramsh/RamshCommandSetContextLogLevel.h | 2 +- .../RamshCommandSetContextLogLevelFilter.h | 2 +- .../Ramsh/RamshCommunicationChannelConsole.h | 22 +- .../Ramsh/RamshCommunicationChannelDLT.h | 4 +- .../Ramsh/include/Ramsh/RamshStandardSetup.h | 9 +- framework/Ramsh/include/Ramsh/RamshTools.h | 10 +- framework/Ramsh/include/Ramsh/RamshTypeInfo.h | 33 +- .../src/RamshCommandDcsmStatusMessage.cpp | 50 - .../src/RamshCommandPrintBuildConfig.cpp | 3 +- .../Ramsh/src/RamshCommandPrintLogLevels.cpp | 4 +- .../src/RamshCommandPrintRamsesVersion.cpp | 2 +- .../src/RamshCommandSetConsoleLogLevel.cpp | 2 +- .../src/RamshCommandSetContextLogLevel.cpp | 4 +- .../RamshCommandSetContextLogLevelFilter.cpp | 2 +- .../src/RamshCommunicationChannelConsole.cpp | 12 +- ...mmunicationChannelConsoleSignalHandler.cpp | 4 +- .../src/RamshCommunicationChannelDLT.cpp | 7 +- framework/Ramsh/src/RamshStandardSetup.cpp | 6 +- framework/Ramsh/src/RamshTools.cpp | 54 +- framework/Ramsh/test/RamshTest.cpp | 72 +- .../Resource/include/Resource/ArrayResource.h | 20 +- .../include/Resource/BufferResource.h | 2 +- .../include/Resource/EffectInputInformation.h | 12 +- .../include/Resource/EffectResource.h | 31 +- .../Resource/include/Resource/IResource.h | 27 +- .../Resource/include/Resource/ResourceBase.h | 26 +- .../Resource/include/Resource/ResourceInfo.h | 6 +- .../Resource/include/Resource/ResourceTypes.h | 4 +- .../include/Resource/TextureMetaInfo.h | 10 +- .../include/Resource/TextureResource.h | 40 +- .../Resource/src/EffectResource.cpp | 112 +- .../SceneGraph/Resource/src/ResourceBase.cpp | 2 +- .../Resource/test/ArrayResourceTest.cpp | 11 +- .../SceneGraph/Resource/test/CityhashTest.cpp | 24 +- .../Resource/test/EffectResourceTest.cpp | 69 +- .../Resource/test/LZ4CompressionUtilsTest.cpp | 6 +- .../SceneGraph/Resource/test/ResourceTest.cpp | 40 +- .../Resource/test/TextureResourceTest.cpp | 4 +- .../include/Scene/ActionCollectingScene.h | 226 +- .../Scene/include/Scene/DataInstance.h | 10 +- .../Scene/include/Scene/DataLayout.h | 20 +- .../include/Scene/DataLayoutCachedScene.h | 8 +- .../Scene/include/Scene/ESceneActionId.h | 20 +- .../include/Scene/EScenePublicationMode.h | 2 +- .../Scene/include/Scene/MatrixCacheEntry.h | 7 +- .../Scene/ResourceChangeCollectingScene.h | 52 +- .../Scene/include/Scene/ResourceChanges.h | 11 +- .../SceneGraph/Scene/include/Scene/Scene.h | 567 +-- .../Scene/include/Scene/SceneActionApplier.h | 5 +- .../include/Scene/SceneActionApplierHelper.h | 41 - .../include/Scene/SceneActionCollection.h | 190 +- .../Scene/SceneActionCollectionCreator.h | 114 +- .../Scene/include/Scene/SceneDataBinding.h | 34 - .../Scene/include/Scene/SceneDescriber.h | 2 - .../Scene/include/Scene/ScenePersistation.h | 8 +- .../Scene/include/Scene/TopologyTransform.h | 12 +- .../include/Scene/TransformPropertyType.h | 22 - .../include/Scene/TransformationCachedScene.h | 34 +- .../Scene/src/ActionCollectingScene.cpp | 111 +- .../Scene/src/DataLayoutCachedScene.cpp | 8 +- .../src/ResourceChangeCollectingScene.cpp | 25 +- .../SceneGraph/Scene/src/ResourceChanges.cpp | 14 +- framework/SceneGraph/Scene/src/Scene.cpp | 351 +- .../Scene/src/SceneActionApplier.cpp | 981 +--- .../Scene/src/SceneActionCollection.cpp | 2 +- .../src/SceneActionCollectionCreator.cpp | 543 +-- .../SceneGraph/Scene/src/SceneDataBinding.cpp | 30 - .../SceneGraph/Scene/src/SceneDescriber.cpp | 155 +- .../Scene/src/ScenePersistation.cpp | 42 +- .../Scene/src/TransformationCachedScene.cpp | 53 +- .../SceneGraph/Scene/test/ActionTestScene.cpp | 270 +- .../SceneGraph/Scene/test/ActionTestScene.h | 441 +- .../ResourceChangeCollectingSceneTest.cpp | 65 - .../Scene/test/ResourceUtilsTest.cpp | 42 +- .../SceneActionCollectionBasicTypesTest.cpp | 85 +- .../SceneActionCollectionComplexTypesTest.cpp | 61 +- .../Scene/test/SceneActionCollectionTest.cpp | 20 +- .../test/SceneActionHelperAndApplierTest.cpp | 38 +- .../Scene/test/SceneActionUtils.cpp | 10 +- .../SceneGraph/Scene/test/SceneActionUtils.h | 4 +- .../Scene/test/SceneDescriberTest.cpp | 31 +- .../Scene/test/ScenePersistationTest.cpp | 4 +- .../Scene/test/SceneTest_AnimationSystem.cpp | 118 - .../Scene/test/SceneTest_DataBinding.cpp | 39 - .../Scene/test/SceneTest_DataInstances.cpp | 85 +- .../Scene/test/SceneTest_Generic.cpp | 9 +- .../test/SceneTest_IteratableMemoryPools.cpp | 25 +- .../Scene/test/SceneTest_RenderPasses.cpp | 12 +- .../Scene/test/SceneTest_States.cpp | 2 +- .../Scene/test/SceneTest_StreamTexture.cpp | 60 - .../Scene/test/SceneTest_TextureBuffers.cpp | 16 +- .../Scene/test/SceneTest_TextureSamplers.cpp | 10 - .../Scene/test/SceneTest_Transforms.cpp | 41 +- .../SceneGraph/Scene/test/TestingScene.h | 135 +- .../test/TransformationCachedSceneTest.cpp | 289 +- .../SceneAPI/include/SceneAPI/BlitPass.h | 2 +- .../SceneAPI/include/SceneAPI/DataFieldInfo.h | 4 +- .../SceneAPI/include/SceneAPI/DataSlot.h | 2 +- .../include/SceneAPI/ECameraProjectionType.h | 2 +- .../include/SceneAPI/EDataBufferType.h | 4 +- .../SceneAPI/include/SceneAPI/EDataSlotType.h | 2 +- .../SceneAPI/include/SceneAPI/EDataType.h | 115 +- .../include/SceneAPI/EFixedSemantics.h | 2 +- .../SceneAPI/ERenderableDataSlotType.h | 2 +- .../include/SceneAPI/ERotationConvention.h | 56 - .../SceneAPI/include/SceneAPI/ERotationType.h | 54 + .../include/SceneAPI/GeometryDataBuffer.h | 2 +- .../SceneAPI/include/SceneAPI/Handles.h | 6 - .../SceneAPI/include/SceneAPI/IScene.h | 314 +- .../SceneAPI/include/SceneAPI/MipMapSize.h | 4 +- .../SceneAPI/include/SceneAPI/RenderBuffer.h | 8 +- .../SceneAPI/include/SceneAPI/RenderGroup.h | 2 +- .../SceneAPI/include/SceneAPI/RenderPass.h | 8 +- .../SceneAPI/include/SceneAPI/RenderState.h | 248 +- .../SceneAPI/include/SceneAPI/Renderable.h | 11 +- .../include/SceneAPI/RendererSceneState.h | 2 +- .../include/SceneAPI/ResourceContentHash.h | 10 +- .../SceneAPI/include/SceneAPI/ResourceField.h | 6 +- .../SceneAPI/SceneCreationInformation.h | 8 +- .../SceneAPI/include/SceneAPI/SceneId.h | 14 +- .../include/SceneAPI/SceneSizeInformation.h | 92 +- .../SceneAPI/include/SceneAPI/SceneTypes.h | 5 +- .../include/SceneAPI/SceneVersionTag.h | 2 +- .../SceneAPI/include/SceneAPI/StreamTexture.h | 25 - .../SceneAPI/include/SceneAPI/TextureBuffer.h | 8 +- .../SceneAPI/include/SceneAPI/TextureEnums.h | 14 +- .../include/SceneAPI/TextureSampler.h | 11 +- .../include/SceneAPI/TextureSamplerStates.h | 6 +- .../SceneAPI/include/SceneAPI/Viewport.h | 10 +- .../include/SceneAPI/WaylandIviSurfaceId.h | 27 - .../include/SceneUtils/DataInstanceHelper.h | 32 +- .../SceneUtils/DataLayoutCreationHelper.h | 3 +- .../SceneUtils/ISceneDataArrayAccessor.h | 46 +- .../include/SceneUtils/ResourceUtils.h | 22 +- .../SceneUtils/src/DataInstanceHelper.cpp | 46 +- .../src/DataLayoutCreationHelper.cpp | 4 +- .../SceneUtils/src/ResourceUtils.cpp | 1 - .../include/Watchdog/IThreadAliveNotifier.h | 2 +- .../include/Watchdog/PlatformWatchdog.h | 14 +- .../Watchdog/ThreadAliveNotifierMock.h | 2 +- .../include/Watchdog/ThreadWatchdog.h | 8 +- framework/Watchdog/src/ThreadWatchdog.cpp | 5 +- .../Watchdog/test/PlatformWatchDogTest.cpp | 48 +- .../Watchdog/test/ThreadWatchdogTest.cpp | 22 +- .../include/ramses-framework-api/APIExport.h | 4 - .../ramses-framework-api/AppearanceEnums.h | 235 + .../CarModelViewMetadata.h | 109 - .../ramses-framework-api/CategoryInfoUpdate.h | 245 - .../include/ramses-framework-api/DataTypes.h | 208 + .../ramses-framework-api/DcsmApiTypes.h | 197 - .../ramses-framework-api/DcsmConsumer.h | 145 - .../DcsmMetadataCreator.h | 216 - .../ramses-framework-api/DcsmMetadataUpdate.h | 316 -- .../ramses-framework-api/DcsmProvider.h | 257 - .../ramses-framework-api/DcsmStatusMessage.h | 195 - .../include/ramses-framework-api/EDataType.h | 145 + .../ramses-framework-api/EFeatureLevel.h | 63 + .../EValidationSeverity.h | 8 +- .../IDcsmConsumerEventHandler.h | 99 - .../IDcsmProviderEventHandler.h | 127 - .../ramses-framework-api/IRamshCommand.h | 4 +- .../IThreadWatchdogNotification.h | 8 +- .../ramses-framework-api/RamsesFramework.h | 167 +- .../RamsesFrameworkConfig.h | 198 +- .../RamsesFrameworkTypes.h | 46 +- .../ramses-framework-api/RendererSceneState.h | 2 +- .../ramses-framework-api/StatusObject.h | 48 +- .../ramses-framework-api/StronglyTypedValue.h | 4 +- .../include/APILoggingMacros.h | 11 + .../include/AppearanceEnumsImpl.h | 123 + .../include/CategoryInfoUpdateImpl.h | 48 - framework/ramses-framework/include/CommandT.h | 2 +- .../ramses-framework/include/DataTypeUtils.h | 173 + .../ramses-framework/include/DataTypesImpl.h | 62 + .../include/DcsmConsumerImpl.h | 40 - .../include/DcsmMetadataCreatorImpl.h | 47 - .../include/DcsmMetadataUpdateImpl.h | 80 - .../include/DcsmProviderImpl.h | 81 - .../include/DcsmStatusMessageImpl.h | 92 - .../include/IDcsmConsumerImpl.h | 35 - .../include/RamsesFrameworkConfigImpl.h | 56 +- .../include/RamsesFrameworkImpl.h | 49 +- .../include/RamsesFrameworkTypesImpl.h | 56 +- .../include/RamsesObjectFactoryInterfaces.h | 5 +- .../ramses-framework/include/SOMEIPICConfig.h | 36 - .../include/StatusObjectImpl.h | 9 +- .../ramses-framework/include/TCPConfig.h | 23 +- .../include/ThreadWatchdogConfig.h | 18 +- .../ramses-framework/src/AppearanceEnums.cpp | 57 + .../src/CategoryInfoUpdate.cpp | 138 - .../src/CategoryInfoUpdateImpl.cpp | 94 - .../ramses-framework/src/DcsmConsumer.cpp | 70 - .../ramses-framework/src/DcsmConsumerImpl.cpp | 100 - .../src/DcsmMetadataCreator.cpp | 131 - .../src/DcsmMetadataCreatorImpl.cpp | 126 - .../src/DcsmMetadataUpdate.cpp | 181 - .../src/DcsmMetadataUpdateImpl.cpp | 181 - .../ramses-framework/src/DcsmProvider.cpp | 108 - .../ramses-framework/src/DcsmProviderImpl.cpp | 310 -- .../src/DcsmStatusMessage.cpp | 119 - .../src/DcsmStatusMessageImpl.cpp | 63 - .../ramses-framework/src/RamsesFramework.cpp | 69 +- .../src/RamsesFrameworkConfig.cpp | 121 +- .../src/RamsesFrameworkConfigImpl.cpp | 218 +- .../src/RamsesFrameworkImpl.cpp | 229 +- .../ramses-framework/src/RamsesVersion.cpp | 2 +- .../ramses-framework/src/SOMEIPICConfig.cpp | 27 - .../ramses-framework/src/StatusObject.cpp | 16 +- .../ramses-framework/src/StatusObjectImpl.cpp | 24 +- framework/ramses-framework/src/TCPConfig.cpp | 12 +- .../test/CategoryInfoUpdateTest.cpp | 235 - .../ramses-framework/test/DataTypeTest.cpp | 281 ++ .../test/DcsmApiTypesTest.cpp | 79 - .../test/DcsmConsumerTest.cpp | 87 - .../test/DcsmMetadataCreatorTest.cpp | 184 - .../test/DcsmMetadataUpdateTest.cpp | 178 - .../test/DcsmProviderTest.cpp | 1074 ----- .../test/DcsmStatusMessageTest.cpp | 124 - .../ramses-framework/test/DcsmSystemTests.cpp | 377 -- .../test/RamsesFrameworkConfigTest.cpp | 185 +- .../test/RamsesFrameworkTest.cpp | 63 +- .../test/RamsesVersionTest.cpp | 2 +- .../test/StatusObjectTest.cpp | 40 +- integration/CMakeLists.txt | 12 +- .../RenderBackendTests/CMakeLists.txt | 54 +- .../RenderBackendTests/src/PlatformTest.cpp | 19 +- .../src/ShaderUploadTest.cpp | 89 +- .../RenderBackendTests/src/main.cpp | 26 +- .../RendererTests/CMakeLists.txt | 275 +- .../DmaOffscreenBufferRenderingTests.h | 5 +- .../DmaOffscreenBufferTests.cpp | 10 +- .../DmaOffscreenBufferTests.h | 8 +- .../DmaOffscreenBufferRenderingTests/main.cpp | 43 +- .../EmbeddedCompositingTestsFramework.cpp | 40 +- .../EmbeddedCompositingTestsFramework.h | 37 +- .../IEmbeddedCompositingTest.h | 6 +- .../NamedPipe.h | 28 +- .../TestForkerApplication.cpp | 4 +- .../TestForkerApplication.h | 10 +- .../TestForkingController.cpp | 10 +- .../TestForkingController.h | 11 +- .../TestSignalHandler.cpp | 6 +- .../TestSignalHandler.h | 7 +- .../TestCases/EmbeddedCompositingTests.h | 7 +- .../EmbeddedCompositingTestsWithFD.cpp | 4 +- .../EmbeddedCompositingTestsWithFD.h | 6 +- .../MultiDisplayStreamTextureTests.cpp | 111 +- .../MultiDisplayStreamTextureTests.h | 6 +- .../MultiSceneStreamTextureTests.cpp | 53 +- .../TestCases/MultiSceneStreamTextureTests.h | 4 +- .../TestCases/MultiStreamTextureTests.cpp | 75 +- .../TestCases/MultiStreamTextureTests.h | 4 +- ...ffscreenBuffersWithStreamTexturesTests.cpp | 22 +- .../OffscreenBuffersWithStreamTexturesTests.h | 4 +- .../TestCases/SharedMemoryBufferTests.cpp | 275 +- .../TestCases/SharedMemoryBufferTests.h | 10 +- .../TestCases/SingleStreamTextureTests.cpp | 351 +- .../TestCases/SingleStreamTextureTests.h | 12 +- .../TestCases/StreamBufferTests.cpp | 1 - .../TestCases/StreamBufferTests.h | 4 +- .../StreamTextureRendererEventTests.cpp | 107 +- .../StreamTextureRendererEventTests.h | 4 +- ...landApplicationWithRamsesRendererTests.cpp | 14 +- ...aylandApplicationWithRamsesRendererTests.h | 4 +- .../TestCases/WaylandOutputTests.cpp | 5 +- .../TestCases/WaylandOutputTests.h | 4 +- .../TestCases/main.cpp | 52 +- .../TestWaylandApplication/SHMBuffer.cpp | 6 +- .../TestWaylandApplication/SHMBuffer.h | 17 +- .../TestApplicationShellSurfaceId.h | 2 +- .../TestApplicationSurfaceId.h | 2 +- .../TestWaylandApplication.cpp | 52 +- .../TestWaylandApplication.h | 13 +- .../TestWaylandApplication/WaylandHandler.cpp | 7 +- .../TestWaylandApplication/WaylandHandler.h | 17 +- .../ExternalWindowTests.cpp | 18 +- .../RendererLifecycleTests.cpp | 168 +- .../RendererLifecycleTests.h | 9 +- .../RendererLifecycleTests/main.cpp | 27 +- .../RendererTestsFramework.cpp | 74 +- .../RendererTestsFramework.h | 52 +- .../RendererTestFramework/RenderingTestCase.h | 11 +- .../RendererTestFramework/TestRenderer.cpp | 45 +- .../RendererTestFramework/TestRenderer.h | 24 +- .../RendererTestFramework/TestScenes.cpp | 2 +- .../RendererTestFramework/TestScenes.h | 13 +- .../TestScenesAndRenderer.cpp | 2 +- .../TestScenesAndRenderer.h | 28 +- .../RenderingTests/DataLinkingTests.cpp | 38 +- .../RenderingTests/DataLinkingTests.h | 14 +- .../RenderingTests/DisplayRenderingTests.cpp | 128 +- .../RenderingTests/DisplayRenderingTests.h | 12 +- .../RenderingTests/EffectRenderingTests.cpp | 2 +- .../RenderingTests/EffectRenderingTests.h | 6 +- .../InterruptibleOffscreenBufferLinkTests.cpp | 38 +- .../InterruptibleOffscreenBufferLinkTests.h | 12 +- .../OffscreenBufferLinkTests.cpp | 24 +- .../RenderingTests/OffscreenBufferLinkTests.h | 16 +- .../RenderingTests/RenderPassRenderingTests.h | 4 +- .../RenderTargetRenderingTests.cpp | 2 +- .../RenderTargetRenderingTests.h | 8 +- .../RenderingTests/RenderingTests.h | 5 +- .../RenderingTests/SceneRenderingTests.cpp | 88 +- .../RenderingTests/SceneRenderingTests.h | 18 +- .../RenderingTests/TextureRenderingTests.cpp | 39 +- .../RenderingTests/TextureRenderingTests.h | 11 +- .../RendererTests/RenderingTests/main.cpp | 44 +- .../res/ARendererDisplays_Warped.PNG | Bin 524 -> 0 bytes .../res/RenderTargetScene_Warping.PNG | Bin 1699 -> 0 bytes .../res/RenderTargetScene_Warping2.PNG | Bin 2386 -> 0 bytes .../ramses-local-client-test/CMakeLists.txt | 15 +- .../ramses-local-client-test/src/main.cpp | 317 +- .../ramses-test-client/CMakeLists.txt | 21 +- .../ramses-test-client/src/main.cpp | 141 +- .../ResourceStressTests/CMakeLists.txt | 21 +- .../src/DynamicQuad_Base.cpp | 25 +- .../src/DynamicQuad_Base.h | 6 +- .../src/DynamicQuad_OffscreenRenderTarget.cpp | 12 +- .../src/DynamicQuad_OffscreenRenderTarget.h | 2 +- .../src/DynamicQuad_Resources.cpp | 12 +- .../src/DynamicQuad_Resources.h | 2 +- .../src/DynamicQuad_SceneResources.cpp | 35 +- .../src/DynamicQuad_SceneResources.h | 2 +- .../src/ResourceStressTestScene.cpp | 6 +- .../src/ResourceStressTests.cpp | 48 +- .../src/ResourceStressTests.h | 28 +- .../ResourceStressTests/src/ScreenspaceQuad.h | 30 +- .../src/StressTestRenderer.cpp | 8 +- .../src/StressTestRenderer.h | 4 +- .../ResourceStressTests/src/main.cpp | 58 +- integration/TestContent/CMakeLists.txt | 20 +- .../TestScenes/AnimatedTrianglesScene.h | 54 - .../include/TestScenes/AntiAliasingScene.h | 2 +- .../{DataBufferScene.h => ArrayBufferScene.h} | 20 +- .../include/TestScenes/ArrayInputScene.h | 4 +- .../include/TestScenes/ArrayResourceScene.h | 6 +- .../include/TestScenes/BlitPassScene.h | 18 +- .../include/TestScenes/CameraDataLinkScene.h | 18 +- .../TestScenes/CommonRenderBufferTestScene.h | 4 +- .../include/TestScenes/CubeTextureScene.h | 12 +- .../TestScenes/CustomShaderTestScene.h | 6 +- .../include/TestScenes/DataLinkScene.h | 2 +- .../include/TestScenes/DcsmScene.h | 44 - .../TestScenes/EmbeddedCompositorScene.h | 16 +- .../include/TestScenes/FileLoadingScene.h | 12 +- .../TestScenes/GeometryInstanceScene.h | 6 +- .../include/TestScenes/GeometryShaderScene.h | 14 +- .../HierarchicalRedTrianglesScene.h | 2 +- .../include/TestScenes/IndexArray32BitScene.h | 4 +- .../include/TestScenes/IntegrationScene.h | 10 +- .../TestContent/include/TestScenes/Line.h | 2 +- .../TestScenes/MsaaRenderBufferScene.h | 8 +- .../TestScenes/MultiLanguageTextScene.h | 5 +- .../TestScenes/MultiTransformationLinkScene.h | 12 +- .../include/TestScenes/MultiTypeLinkScene.h | 17 +- .../TestScenes/MultipleGeometryScene.h | 6 +- .../TestScenes/MultipleRenderTargetScene.h | 15 +- .../TestScenes/MultipleTrianglesScene.h | 4 +- .../include/TestScenes/RenderBufferScene.h | 8 +- .../include/TestScenes/RenderPassClearScene.h | 2 +- .../include/TestScenes/RenderPassOnceScene.h | 4 +- .../include/TestScenes/RenderPassScene.h | 2 +- .../include/TestScenes/RenderTargetScene.h | 8 +- .../include/TestScenes/SceneFromPath.h | 5 +- .../include/TestScenes/ShaderTestScene.h | 8 +- .../TestScenes/SingleAppearanceScene.h | 6 +- .../include/TestScenes/StreamTextureScene.h | 33 +- .../include/TestScenes/TextScene.h | 7 +- .../include/TestScenes/TextScene_Base.h | 5 +- ...exture2DAnisotropicTextureFilteringScene.h | 2 +- .../Texture2DCompressedMipMapScene.h | 2 +- .../include/TestScenes/Texture2DFormatScene.h | 2 +- .../TestScenes/Texture2DGenerateMipMapScene.h | 4 +- .../TestScenes/Texture2DSamplingScene.h | 6 +- .../include/TestScenes/Texture3DScene.h | 4 +- .../include/TestScenes/TextureAddressScene.h | 6 +- .../include/TestScenes/TextureBufferScene.h | 7 +- ...tureCubeAnisotropicTextureFilteringScene.h | 2 +- .../include/TestScenes/TextureLinkScene.h | 2 +- .../include/TestScenes/TextureSamplerScene.h | 5 +- .../TestScenes/TransformationLinkScene.h | 10 +- .../TestContent/include/TestScenes/Triangle.h | 4 +- .../include/TestScenes/TriangleAppearance.h | 4 +- .../include/TestScenes/TriangleGeometry.h | 1 - .../include/TestScenes/VisibilityScene.h | 4 +- .../TestContent/include/TestStepCommand.h | 4 +- .../src/AnimatedTrianglesScene.cpp | 240 - .../TestContent/src/AntiAliasingScene.cpp | 63 +- ...taBufferScene.cpp => ArrayBufferScene.cpp} | 119 +- .../TestContent/src/ArrayInputScene.cpp | 27 +- .../TestContent/src/ArrayResourceScene.cpp | 18 +- integration/TestContent/src/BlitPassScene.cpp | 44 +- .../TestContent/src/CameraDataLinkScene.cpp | 47 +- .../src/CommonRenderBufferTestScene.cpp | 33 +- .../TestContent/src/CubeTextureScene.cpp | 36 +- .../TestContent/src/CustomShaderTestScene.cpp | 38 +- integration/TestContent/src/DataLinkScene.cpp | 23 +- integration/TestContent/src/DcsmScene.cpp | 43 - .../src/EmbeddedCompositorScene.cpp | 105 +- .../TestContent/src/FileLoadingScene.cpp | 99 +- .../TestContent/src/GeometryInstanceScene.cpp | 61 +- .../TestContent/src/GeometryShaderScene.cpp | 42 +- .../src/HierarchicalRedTrianglesScene.cpp | 22 +- .../TestContent/src/IndexArray32BitScene.cpp | 42 +- .../TestContent/src/IntegrationScene.cpp | 7 +- integration/TestContent/src/Line.cpp | 50 +- .../TestContent/src/MsaaRenderBufferScene.cpp | 53 +- .../src/MultiLanguageTextScene.cpp | 30 +- .../src/MultiTransformationLinkScene.cpp | 32 +- .../TestContent/src/MultiTriangleGeometry.cpp | 32 +- .../TestContent/src/MultiTypeLinkScene.cpp | 67 +- .../TestContent/src/MultipleGeometryScene.cpp | 132 +- .../src/MultipleRenderTargetScene.cpp | 69 +- .../src/MultipleTrianglesScene.cpp | 186 +- .../TestContent/src/RenderBufferScene.cpp | 30 +- .../TestContent/src/RenderPassClearScene.cpp | 61 +- .../TestContent/src/RenderPassOnceScene.cpp | 40 +- .../TestContent/src/RenderPassScene.cpp | 6 +- .../TestContent/src/RenderTargetScene.cpp | 70 +- integration/TestContent/src/SceneFromPath.cpp | 6 +- .../TestContent/src/ShaderTestScene.cpp | 34 +- .../TestContent/src/SingleAppearanceScene.cpp | 21 +- .../TestContent/src/StreamTextureScene.cpp | 120 +- .../TestContent/src/TestStepCommand.cpp | 4 +- integration/TestContent/src/TextScene.cpp | 78 +- .../TestContent/src/TextScene_Base.cpp | 8 +- ...ture2DAnisotropicTextureFilteringScene.cpp | 54 +- .../src/Texture2DCompressedMipMapScene.cpp | 56 +- .../TestContent/src/Texture2DFormatScene.cpp | 45 +- .../src/Texture2DGenerateMipMapScene.cpp | 78 +- .../src/Texture2DSamplingScene.cpp | 140 +- .../TestContent/src/Texture3DScene.cpp | 44 +- .../TestContent/src/TextureAddressScene.cpp | 40 +- .../TestContent/src/TextureBufferScene.cpp | 76 +- ...reCubeAnisotropicTextureFilteringScene.cpp | 50 +- .../TestContent/src/TextureLinkScene.cpp | 27 +- .../TestContent/src/TextureSamplerScene.cpp | 46 +- .../src/TransformationLinkScene.cpp | 21 +- integration/TestContent/src/Triangle.cpp | 4 +- .../TestContent/src/TriangleAppearance.cpp | 14 +- .../TestContent/src/TriangleGeometry.cpp | 14 +- .../TestContent/src/VisibilityScene.cpp | 10 +- ramses-cli/CMakeLists.txt | 39 + ramses-cli/include/ramses-cli.h | 159 + ramses-cli/include/ramses-framework-cli.h | 132 + ramses-cli/test/ramses-cli-test.cpp | 348 ++ ramses-daemon/CMakeLists.txt | 12 +- ramses-daemon/main.cpp | 30 +- ramses-shared-lib/CMakeLists.txt | 34 +- renderer/CMakeLists.txt | 58 + renderer/Platform/Context_EGL/CMakeLists.txt | 19 +- .../include/Context_EGL/Context_EGL.h | 12 +- .../Platform/Context_EGL/src/Context_EGL.cpp | 24 +- renderer/Platform/Context_WGL/CMakeLists.txt | 19 +- .../include/Context_WGL/Context_WGL.h | 16 +- .../include/Context_WGL/WglExtensions.h | 12 +- .../Platform/Context_WGL/src/Context_WGL.cpp | 63 +- .../Context_WGL/src/WglExtensions.cpp | 10 +- .../Device_EGL_Extension/CMakeLists.txt | 29 +- .../Device_EGL_Extension.h | 28 +- .../src/Device_EGL_Extension.cpp | 2 +- renderer/Platform/Device_GL/CMakeLists.txt | 28 +- .../Device_GL/include/Device_GL/DebugOutput.h | 12 +- .../Device_GL/include/Device_GL/Device_GL.h | 239 +- .../include/Device_GL/Device_GL_platform.h | 9 +- .../include/Device_GL/ShaderGPUResource_GL.h | 12 +- .../include/Device_GL/ShaderUploader_GL.h | 13 +- .../include/Device_GL/TypesConversion_GL.h | 6 +- .../Device_GL/include/Device_GL/Types_GL.h | 6 +- .../Platform/Device_GL/src/DebugOutput.cpp | 14 +- renderer/Platform/Device_GL/src/Device_GL.cpp | 188 +- .../Device_GL/src/ShaderGPUResource_GL.cpp | 12 +- .../Device_GL/src/ShaderUploader_GL.cpp | 38 +- .../Device_GL/src/TypesConversion_GL.cpp | 14 +- .../EmbeddedCompositor_Wayland/CMakeLists.txt | 65 +- .../EmbeddedCompositor_Wayland.h | 70 +- .../INativeWaylandResource.h | 5 +- .../IWaylandBuffer.h | 4 +- .../IWaylandClient.h | 2 +- .../IWaylandDisplay.h | 5 +- .../IWaylandIVISurface.h | 3 +- .../IWaylandShellSurface.h | 4 +- .../IWaylandSurface.h | 21 +- .../EmbeddedCompositor_Wayland/LinuxDmabuf.h | 2 +- .../LinuxDmabufConnection.h | 2 +- .../LinuxDmabufParams.h | 2 +- .../NativeWaylandResource.h | 14 +- .../TextureUploadingAdapter_Wayland.h | 6 +- .../WaylandBuffer.h | 8 +- .../WaylandBufferResource.h | 8 +- .../WaylandClient.h | 8 +- .../WaylandClientCredentials.h | 6 +- .../WaylandCompositorConnection.h | 10 +- .../WaylandCompositorGlobal.h | 6 +- .../WaylandDisplay.h | 23 +- .../WaylandGlobal.h | 2 +- .../WaylandIVIApplicationConnection.h | 8 +- .../WaylandIVISurface.h | 10 +- .../WaylandRegion.h | 6 +- .../WaylandShellConnection.h | 8 +- .../WaylandShellSurface.h | 33 +- .../WaylandSurface.h | 58 +- .../src/EmbeddedCompositor_Wayland.cpp | 34 +- .../src/LinuxDmabufConnection.cpp | 4 +- .../src/LinuxDmabufParams.cpp | 18 +- .../src/NativeWaylandResource.cpp | 5 +- .../src/TextureUploadingAdapter_Wayland.cpp | 5 +- .../src/WaylandDisplay.cpp | 30 +- .../src/WaylandOutputConnection.cpp | 2 + .../src/WaylandOutputGlobal.cpp | 2 + .../src/WaylandShellSurface.cpp | 2 +- .../src/WaylandSurface.cpp | 8 +- .../test/EmbeddedCompositor_Wayland_Test.cpp | 52 +- .../test/NativeWaylandResourceMock.h | 4 +- .../test/WaylandBufferResourceMock.h | 2 +- .../test/WaylandBufferResource_Test.cpp | 10 +- .../test/WaylandCallbackResourceMock.h | 2 +- .../test/WaylandCallbackResource_Test.cpp | 8 +- .../test/WaylandClient_Test.cpp | 6 +- .../test/WaylandCompositorConnection_Test.cpp | 2 +- .../test/WaylandGlobal_Test.cpp | 2 +- .../test/WaylandResourceLifecycleTest.cpp | 4 +- .../test/WaylandResource_Test.cpp | 4 +- .../test/WaylandShellSurfaceMock.h | 2 +- .../test/WaylandShellSurface_Test.cpp | 2 +- .../test/WaylandSurfaceMock.h | 8 +- .../test/WaylandSurface_Test.cpp | 6 +- .../EmbeddedCompositor_Wayland/test/main.cpp | 5 - .../Platform/Platform_Android/CMakeLists.txt | 34 - .../Platform_Android_EGL_ES_3_0.h | 23 - .../src/Platform_Android_EGL_ES_3_0.cpp | 22 - .../Platform_Android_EGL/CMakeLists.txt | 17 + .../Platform_Android_EGL.h | 5 +- .../Platform_Android_EGL}/Window_Android.h | 4 +- .../src/Platform_Android_EGL.cpp | 2 +- .../src/Window_Android.cpp | 8 +- renderer/Platform/Platform_EGL/CMakeLists.txt | 19 +- .../include/Platform_EGL/Platform_EGL.h | 25 +- .../Platform_Integrity_RGL/CMakeLists.txt | 34 - .../Platform_Integrity_RGL_EGL_ES_3_0.h | 28 - .../Window_Integrity_RGL.h | 54 - .../src/Platform_Integrity_RGL_EGL_ES_3_0.cpp | 41 - .../src/Window_Integrity_RGL.cpp | 160 - .../Platform_Wayland_EGL/CMakeLists.txt | 19 +- .../Platform_Wayland_EGL.h | 10 +- .../src/Platform_Wayland_EGL.cpp | 4 +- .../Platform_Wayland_IVI_EGL/CMakeLists.txt | 26 +- .../src/Platform_Wayland_IVI_EGL_ES_3_0.cpp | 6 - .../Platform_Wayland_Shell_EGL/CMakeLists.txt | 26 +- .../src/Platform_Wayland_Shell_EGL_ES_3_0.cpp | 5 - .../Platform_Windows_WGL/CMakeLists.txt | 20 +- .../Platform_Windows_WGL.h | 16 +- .../src/Platform_Windows_WGL.cpp | 26 +- .../CMakeLists.txt | 34 - .../Platform_Windows_WGL_4_2_core.h | 29 - .../src/Platform_Windows_WGL_4_2_core.cpp | 69 - .../Platform_Windows_WGL_4_5/CMakeLists.txt | 34 - .../Platform_Windows_WGL_4_5.h | 29 - .../src/Platform_Windows_WGL_4_5.cpp | 70 - .../CMakeLists.txt | 34 - .../Platform_Windows_WGL_ES_3_0.h | 31 - .../src/Platform_Windows_WGL_ES_3_0.cpp | 69 - renderer/Platform/Platform_X11/CMakeLists.txt | 57 - .../Platform_X11/Platform_X11_EGL_ES_3_0.h | 26 - .../src/Platform_X11_EGL_ES_3_0.cpp | 27 - .../Platform/Platform_X11_EGL/CMakeLists.txt | 35 + .../Platform_X11_EGL}/Platform_X11_EGL.h | 8 +- .../include/Platform_X11_EGL}/Window_X11.h | 25 +- .../src/Platform_X11_EGL.cpp | 7 +- .../src/Window_X11.cpp | 40 +- .../test/Window_X11_Test.cpp | 14 +- .../CMakeLists.txt | 59 +- .../IVIControllerScreen.h | 7 +- .../IVIControllerSurface.h | 3 +- .../SystemCompositorController_Wayland_IVI.h | 42 +- .../src/IVIControllerScreen.cpp | 2 +- ...SystemCompositorController_Wayland_IVI.cpp | 38 +- ...mCompositorController_Wayland_IVI_Test.cpp | 2 +- .../test/main.cpp | 18 - .../WaylandEGLExtensionProcs/CMakeLists.txt | 19 +- .../WaylandEGLExtensionProcs.h | 11 +- .../src/WaylandEGLExtensionProcs.cpp | 12 +- .../Platform/WaylandUtilities/CMakeLists.txt | 40 +- .../TestWithWaylandEnvironment.h | 8 +- .../WaylandUtilities/UnixDomainSocket.h | 25 +- .../WaylandEnvironmentUtils.h | 14 +- .../WaylandUtilities/src/UnixDomainSocket.cpp | 5 +- .../src/WaylandEnvironmentUtils.cpp | 31 +- .../Platform/Window_Wayland/CMakeLists.txt | 19 +- .../Window_Wayland/InputHandling_Wayland.h | 6 +- .../include/Window_Wayland/Window_Wayland.h | 18 +- .../include/Window_Wayland/WlContext.h | 2 +- .../src/InputHandling_Wayland.cpp | 8 +- .../Window_Wayland/src/Window_Wayland.cpp | 9 +- .../Window_Wayland_IVI/CMakeLists.txt | 74 +- .../Window_Wayland_IVI/Window_Wayland_IVI.h | 6 +- .../src/Window_Wayland_IVI.cpp | 2 +- .../test/Window_Wayland_IVI_Test.cpp | 9 - .../Platform/Window_Wayland_IVI/test/main.cpp | 6 +- .../Window_Wayland_Shell/CMakeLists.txt | 64 +- .../Window_Wayland_Shell.h | 11 +- .../src/Window_Wayland_Shell.cpp | 4 +- .../test/Window_Wayland_Shell_Test.cpp | 9 - .../Window_Wayland_Shell/test/main.cpp | 6 - .../Window_Wayland_Test/CMakeLists.txt | 19 +- .../Window_Wayland_Test/AWindowWayland.h | 15 +- .../AWindowWaylandWithEventHandling.h | 26 +- .../include/Window_Wayland_Test/TestCases.h | 8 +- .../Platform/Window_Windows/CMakeLists.txt | 55 +- .../include/Window_Windows/HiddenWindow.h | 4 +- .../include/Window_Windows/Window_Windows.h | 34 +- .../Window_Windows/src/HiddenWindow.cpp | 2 +- .../Window_Windows/src/Window_Windows.cpp | 38 +- .../test/Window_Window_Test.cpp | 10 +- renderer/PlatformFactory/CMakeLists.txt | 37 + .../include/PlatformFactory/PlatformFactory.h | 18 +- .../PlatformFactory/src/PlatformFactory.cpp | 69 + renderer/RendererLib/CMakeLists.txt | 166 +- .../include/Platform_Base/Context_Base.h | 14 +- .../Platform_Base/DeviceResourceMapper.h | 10 +- .../include/Platform_Base/Device_Base.h | 12 +- .../Platform_Base/EmbeddedCompositor_Dummy.h | 39 +- .../include/Platform_Base/GpuResource.h | 10 +- .../Platform_Base/IndexBufferGPUResource.h | 6 +- .../include/Platform_Base/Platform_Base.h | 14 +- .../Platform_Base/RenderBufferGPUResource.h | 20 +- .../Platform_Base/RenderTargetGpuResource.h | 2 +- .../include/Platform_Base/RendererLimits.h | 16 +- .../include/Platform_Base/ShaderGPUResource.h | 2 +- .../TextureUploadingAdapter_Base.h | 2 +- .../Platform_Base/VertexArrayGPUResource.h | 4 +- .../include/Platform_Base/Window_Base.h | 44 +- .../Platform_Base/src/Context_Base.cpp | 12 +- .../src/DeviceResourceMapper.cpp | 4 +- .../Platform_Base/src/Device_Base.cpp | 4 +- .../src/EmbeddedCompositor_Dummy.cpp | 24 +- .../src/RenderTargetGpuResource.cpp | 2 +- .../Platform_Base/src/RendererLimits.cpp | 2 +- .../src/TextureUploadingAdapter_Base.cpp | 2 +- .../Platform_Base/src/Window_Base.cpp | 29 +- .../include/RendererAPI/EDeviceType.h | 15 +- .../include/RendererAPI/EDeviceTypeId.h | 51 - .../include/RendererAPI/EWindowType.h | 46 + .../include/RendererAPI/IBinaryShaderCache.h | 12 +- .../include/RendererAPI/IContext.h | 2 +- .../RendererAPI/include/RendererAPI/IDevice.h | 98 +- .../include/RendererAPI/IDisplayController.h | 22 +- .../RendererAPI/IEmbeddedCompositingManager.h | 8 +- .../include/RendererAPI/IEmbeddedCompositor.h | 23 +- .../include/RendererAPI/IPlatformFactory.h | 22 +- .../include/RendererAPI/IRenderBackend.h | 10 +- .../RendererAPI/IRendererResourceCache.h | 8 +- .../IResourceUploadRenderBackend.h | 4 +- .../RendererAPI/ISystemCompositorController.h | 20 +- .../RendererAPI/ITextureUploadingAdapter.h | 2 +- .../RendererAPI/include/RendererAPI/IWindow.h | 26 +- .../include/RendererAPI/IWindowEventHandler.h | 8 +- .../include/RendererAPI/RenderingContext.h | 3 +- .../SceneRenderExecutionIterator.h | 16 +- .../RendererAPI/include/RendererAPI/Types.h | 52 +- .../RendererLib/RendererAPI/src/Types.cpp | 1 - .../RendererCommands/LogRendererInfo.h | 9 +- .../RendererCommands/PrintStatistics.h | 2 +- .../include/RendererCommands/Screenshot.h | 2 +- .../include/RendererCommands/SetClearColor.h | 4 +- .../RendererCommands/SetFrameTimeLimits.h | 2 +- .../SetSkippingOfUnmodifiedBuffers.h | 4 +- .../RendererCommands/ShowFrameProfiler.h | 32 - ...temCompositorControllerAddSurfaceToLayer.h | 4 +- ...SystemCompositorControllerDestroySurface.h | 4 +- ...ystemCompositorControllerListIviSurfaces.h | 2 +- ...mpositorControllerRemoveSurfaceFromLayer.h | 4 +- .../SystemCompositorControllerScreenshot.h | 6 +- ...emCompositorControllerSetLayerVisibility.h | 4 +- ...positorControllerSetSurfaceDestRectangle.h | 4 +- ...temCompositorControllerSetSurfaceOpacity.h | 4 +- ...CompositorControllerSetSurfaceVisibility.h | 4 +- .../RendererCommands/TriggerPickEvent.h | 4 +- .../RendererCommands/src/LogRendererInfo.cpp | 24 +- .../RendererCommands/src/PrintStatistics.cpp | 2 +- .../RendererCommands/src/Screenshot.cpp | 6 +- .../RendererCommands/src/SetClearColor.cpp | 4 +- .../src/SetFrameTimeLimits.cpp | 2 +- .../src/SetSkippingOfUnmodifiedBuffers.cpp | 2 +- .../src/ShowFrameProfiler.cpp | 131 - ...mCompositorControllerAddSurfaceToLayer.cpp | 2 +- ...stemCompositorControllerDestroySurface.cpp | 2 +- ...temCompositorControllerListIviSurfaces.cpp | 2 +- ...ositorControllerRemoveSurfaceFromLayer.cpp | 2 +- .../SystemCompositorControllerScreenshot.cpp | 2 +- ...CompositorControllerSetLayerVisibility.cpp | 2 +- ...sitorControllerSetSurfaceDestRectangle.cpp | 2 +- ...mCompositorControllerSetSurfaceOpacity.cpp | 2 +- ...mpositorControllerSetSurfaceVisibility.cpp | 2 +- .../RendererCommands/src/TriggerPickEvent.cpp | 5 +- .../RendererCommands/test/ScreenshotTest.cpp | 6 +- .../RendererFrameworkLogic.h | 26 +- .../test/RendererFrameworkLogicTest.cpp | 4 +- .../RendererLib/include/RenderExecutor.h | 12 +- .../RenderExecutorInternalRenderStates.h | 28 +- .../include/RenderExecutorInternalState.h | 147 +- .../include/RendererEventCollector.h | 6 +- .../include/RendererLib/AsyncEffectUploader.h | 4 +- .../include/RendererLib/BufferLinks.h | 8 +- .../include/RendererLib/ConstantLogger.h | 37 +- .../include/RendererLib/DataLinkUtils.h | 2 +- .../DataReferenceLinkCachedScene.h | 26 +- .../RendererLib/DataReferenceLinkManager.h | 4 +- .../include/RendererLib/DisplayBundle.h | 39 +- .../include/RendererLib/DisplayConfig.h | 152 +- .../include/RendererLib/DisplayController.h | 39 +- .../include/RendererLib/DisplayDispatcher.h | 20 +- .../include/RendererLib/DisplayEventHandler.h | 12 +- .../include/RendererLib/DisplaySetup.h | 28 +- .../include/RendererLib/DisplayThread.h | 16 +- .../include/RendererLib/EKeyCode.h | 2 +- .../include/RendererLib/EKeyEventType.h | 2 +- .../include/RendererLib/EKeyModifier.h | 7 +- .../include/RendererLib/EMouseEventType.h | 2 +- .../include/RendererLib/EResourceStatus.h | 2 +- .../include/RendererLib/ESceneState.h | 2 +- .../RendererLib/EmbeddedCompositingManager.h | 20 +- .../RendererLib/FrameProfileRenderer.h | 131 - .../RendererLib/FrameProfilerStatistics.h | 83 +- .../include/RendererLib/FrameTimer.h | 12 +- .../include/RendererLib/GpuMemorySample.h | 120 - .../RendererLib/IRendererResourceManager.h | 34 +- .../RendererLib/IRendererSceneUpdater.h | 17 +- .../IResourceDeviceHandleAccessor.h | 30 +- .../include/RendererLib/IResourceUploader.h | 4 +- .../include/RendererLib/IntersectionUtils.h | 28 +- .../include/RendererLib/LinkManagerBase.h | 8 +- .../include/RendererLib/LoggingDevice.h | 194 +- .../include/RendererLib/MemoryStatistics.h | 38 - .../RendererLib/PendingSceneResourcesUtils.h | 4 +- .../include/RendererLib/Postprocessing.h | 56 - .../include/RendererLib/RenderBackend.h | 12 +- .../RendererLib/RenderableComparator.h | 2 +- .../include/RendererLib/Renderer.h | 51 +- .../include/RendererLib/RendererCachedScene.h | 48 +- .../RendererLib/RendererCommandExecutor.h | 6 - .../RendererLib/RendererCommandUtils.h | 39 +- .../include/RendererLib/RendererCommands.h | 57 +- .../include/RendererLib/RendererConfig.h | 34 +- .../include/RendererLib/RendererConfigUtils.h | 29 - .../include/RendererLib/RendererEvent.h | 25 +- .../RendererLib/RendererInterruptState.h | 12 +- .../include/RendererLib/RendererLogContext.h | 19 +- .../include/RendererLib/RendererLogger.h | 9 +- .../RendererLib/RendererPeriodicLogSupplier.h | 4 +- .../RendererLib/RendererResourceManager.h | 114 +- .../RendererResourceManagerUtils.h | 2 +- .../RendererLib/RendererResourceRegistry.h | 20 +- .../RendererLib/RendererSceneControlLogic.h | 12 +- .../RendererSceneResourceRegistry.h | 43 +- .../RendererLib/RendererSceneUpdater.h | 92 +- .../include/RendererLib/RendererScenes.h | 14 +- .../include/RendererLib/RendererStatistics.h | 101 +- .../include/RendererLib/RenderingPassInfo.h | 8 +- .../include/RendererLib/ResourceCachedScene.h | 84 +- .../include/RendererLib/ResourceDescriptor.h | 6 +- .../RendererLib/ResourceUploadRenderBackend.h | 6 +- .../include/RendererLib/ResourceUploader.h | 8 +- .../RendererLib/ResourceUploadingManager.h | 24 +- .../include/RendererLib/SceneDisplayTracker.h | 100 +- .../RendererLib/SceneExpirationMonitor.h | 6 +- .../include/RendererLib/SceneLinkScene.h | 4 +- .../include/RendererLib/SceneLinks.h | 10 +- .../include/RendererLib/SceneLinksManager.h | 6 +- .../include/RendererLib/SceneReferenceLogic.h | 8 +- .../include/RendererLib/SceneStateExecutor.h | 46 +- .../include/RendererLib/SceneStateInfo.h | 6 +- .../include/RendererLib/StagingInfo.h | 2 +- .../RendererLib/TextureLinkCachedScene.h | 12 +- .../include/RendererLib/TextureLinkManager.h | 24 +- .../TransformationLinkCachedScene.h | 18 +- .../RendererLib/TransformationLinkManager.h | 12 +- .../include/RendererLib/WarpingMeshData.h | 36 - .../include/RendererLib/WarpingPass.h | 58 - .../include/RenderingPassOrderComparator.h | 8 +- .../include/SceneDependencyChecker.h | 10 +- .../RendererLib/src/AsyncEffectUploader.cpp | 23 +- .../RendererLib/src/BufferLinks.cpp | 6 +- .../RendererLib/src/ConstantLogger.cpp | 40 +- .../src/DataReferenceLinkCachedScene.cpp | 22 +- .../src/DataReferenceLinkManager.cpp | 4 +- .../RendererLib/src/DisplayBundle.cpp | 42 +- .../RendererLib/src/DisplayConfig.cpp | 113 +- .../RendererLib/src/DisplayController.cpp | 39 +- .../RendererLib/src/DisplayDispatcher.cpp | 71 +- .../RendererLib/src/DisplayEventHandler.cpp | 8 +- .../RendererLib/src/DisplaySetup.cpp | 12 +- .../RendererLib/src/DisplayThread.cpp | 11 +- .../src/EmbeddedCompositingManager.cpp | 4 +- .../RendererLib/src/FrameProfileRenderer.cpp | 613 --- .../src/FrameProfilerStatistics.cpp | 116 +- .../RendererLib/src/GpuMemorySample.cpp | 102 - .../RendererLib/src/IntersectionUtils.cpp | 112 +- .../RendererLib/src/LinkManagerBase.cpp | 4 +- .../RendererLib/src/LoggingDevice.cpp | 84 +- .../RendererLib/src/MemoryStatistics.cpp | 83 - .../src/PendingSceneResourcesUtils.cpp | 20 +- .../RendererLib/src/Postprocessing.cpp | 108 - .../RendererLib/src/RenderExecutor.cpp | 66 +- .../src/RenderExecutorInternalState.cpp | 10 +- .../RendererLib/src/RenderExecutorLogger.cpp | 12 +- .../RendererLib/RendererLib/src/Renderer.cpp | 71 +- .../RendererLib/src/RendererCachedScene.cpp | 88 +- .../src/RendererCommandExecutor.cpp | 51 +- .../RendererLib/src/RendererConfig.cpp | 59 +- .../RendererLib/src/RendererConfigUtils.cpp | 193 - .../src/RendererEventCollector.cpp | 3 +- .../RendererLib/src/RendererLogContext.cpp | 14 +- .../RendererLib/src/RendererLogger.cpp | 208 +- .../src/RendererResourceManager.cpp | 76 +- .../src/RendererResourceManagerUtils.cpp | 4 +- .../src/RendererResourceRegistry.cpp | 10 +- .../src/RendererSceneResourceRegistry.cpp | 48 +- .../RendererLib/src/RendererSceneUpdater.cpp | 164 +- .../RendererLib/src/RendererScenes.cpp | 2 +- .../RendererLib/src/RendererStatistics.cpp | 30 +- .../RendererLib/src/ResourceCachedScene.cpp | 205 +- .../RendererLib/src/ResourceUploader.cpp | 30 +- .../src/ResourceUploadingManager.cpp | 38 +- .../src/SceneDependencyChecker.cpp | 10 +- .../src/SceneExpirationMonitor.cpp | 10 +- .../RendererLib/src/SceneLinks.cpp | 8 +- .../RendererLib/src/SceneLinksManager.cpp | 4 +- .../RendererLib/src/SceneReferenceLogic.cpp | 10 +- .../RendererLib/src/SceneResourceUploader.cpp | 10 +- .../RendererLib/src/SceneStateExecutor.cpp | 44 +- .../RendererLib/src/SceneStateInfo.cpp | 2 +- .../src/TextureLinkCachedScene.cpp | 22 +- .../RendererLib/src/TextureLinkManager.cpp | 9 +- .../src/TransformationLinkCachedScene.cpp | 20 +- .../src/TransformationLinkManager.cpp | 8 +- .../RendererLib/src/WarpingMeshData.cpp | 73 - .../RendererLib/src/WarpingPass.cpp | 142 - .../test/AsyncEffectUploaderTest.cpp | 10 +- .../RendererLib/test/BufferLinksTest.cpp | 4 +- .../test/DataReferenceLinkManagerTest.cpp | 58 +- .../RendererLib/test/DisplayBundleMock.h | 2 +- .../RendererLib/test/DisplayConfigTest.cpp | 102 +- .../test/DisplayControllerTest.cpp | 28 +- .../test/DisplayControllerTestBase.h | 43 - .../test/DisplayDispatcherMock.cpp | 13 +- .../RendererLib/test/DisplayDispatcherMock.h | 10 +- .../test/DisplayDispatcherTest.cpp | 31 +- .../test/DisplayDispatcherThreadedTest.cpp | 83 +- .../test/DisplayEventHandlerTest.cpp | 14 +- .../RendererLib/test/DisplaySetupTest.cpp | 8 +- .../RendererLib/test/DisplayThreadMock.h | 2 +- .../RendererLib/test/EDeviceTypeIdTest.cpp | 38 - .../test/EmbeddedCompositingManagerTest.cpp | 2 - .../test/IntersectionUtilsTest.cpp | 287 +- .../test/PendingSceneResourcesUtilsTest.cpp | 9 - .../test/RenderExecutorInternalStateTest.cpp | 112 +- .../RendererLib/test/RenderExecutorTest.cpp | 324 +- .../test/RendererCachedSceneTest.cpp | 204 +- .../test/RendererCommandBufferTest.cpp | 5 +- .../test/RendererCommandExecutorTest.cpp | 78 +- .../RendererLib/test/RendererConfigTest.cpp | 67 +- .../test/RendererEventCollectorTest.cpp | 27 +- .../test/RendererLogContextTest.cpp | 2 +- .../test/RendererResourceManagerTest.cpp | 107 +- .../test/RendererSceneControlLogicMock.h | 2 +- .../test/RendererSceneControlLogicTest.cpp | 4 +- .../RendererSceneResourceRegistryTest.cpp | 29 - .../test/RendererSceneStateControlMock.h | 2 +- .../test/RendererSceneUpdaterMock.h | 15 +- .../test/RendererSceneUpdaterTest.cpp | 579 +-- .../test/RendererSceneUpdaterTest.h | 233 +- .../RendererSceneUpdaterTest_Resources.cpp | 96 +- .../RendererLib/test/RendererScenesTest.cpp | 8 +- .../test/RendererStatisticsTest.cpp | 2 +- .../RendererLib/test/RendererTest.cpp | 72 +- .../test/ResourceCachedSceneTest.cpp | 214 +- .../RendererLib/test/ResourceUploaderTest.cpp | 92 +- .../test/ResourceUploadingManagerTest.cpp | 46 +- .../RendererLib/test/SceneAllocateHelper.cpp | 59 +- .../RendererLib/test/SceneAllocateHelper.h | 10 +- .../test/SceneDependencyCheckerTest.cpp | 4 +- .../test/SceneDisplayTrackerTest.cpp | 6 - .../test/SceneExpirationMonitorTest.cpp | 4 +- .../test/SceneLinksManagerTest.cpp | 2 +- .../RendererLib/test/SceneLinksTest.cpp | 2 +- .../test/SceneReferenceLogicMock.h | 2 +- .../test/SceneReferenceLogicTest.cpp | 13 +- ...ceneReferenceLogicWithSceneUpdaterTest.cpp | 7 +- .../test/SceneStateExecutorTest.cpp | 4 +- .../RendererLib/test/SceneStateInfoTest.cpp | 4 +- .../RendererLib/test/TestSceneHelper.h | 1 - .../test/TextureLinkManagerTest.cpp | 2 +- .../TransformationLinkCachedSceneTest.cpp | 149 +- .../test/TransformationLinkManagerTest.cpp | 30 +- .../RendererLib/test/WarpingPassTest.cpp | 65 - .../ContextMock.cpp | 0 .../ContextMock.h | 4 +- .../src => RendererTestCommon}/DeviceMock.cpp | 1 - .../DeviceMock.h | 90 +- .../DisplayControllerMock.cpp | 0 .../DisplayControllerMock.h | 14 +- .../EmbeddedCompositingManagerMock.cpp | 0 .../EmbeddedCompositingManagerMock.h | 0 .../EmbeddedCompositorMock.cpp | 0 .../EmbeddedCompositorMock.h | 21 +- .../MockResourceHash.h | 11 +- .../RendererTestCommon/PlatformFactoryMock.h | 35 + .../PlatformMock.cpp | 0 .../PlatformMock.h | 0 .../Platform_BaseMock.cpp | 0 .../Platform_BaseMock.h | 2 +- .../RenderBackendMock.cpp | 0 .../RenderBackendMock.h | 2 +- .../RendererCommandVisitorMock.cpp | 2 +- .../RendererCommandVisitorMock.h | 25 +- .../RendererMock.cpp | 2 +- .../RendererMock.h | 14 +- .../RendererResourceCacheFake.h | 6 +- .../RendererResourceCacheMock.h | 8 +- .../RendererResourceManagerMock.cpp | 0 .../RendererResourceManagerMock.h | 28 +- .../RendererSceneEventSenderMock.h | 2 +- .../RendererSceneUpdaterFacade.cpp | 2 +- .../RendererSceneUpdaterFacade.h | 14 +- .../ResourceDeviceHandleAccessorMock.h | 2 +- .../ResourceUploadRenderBackendMock.h | 2 +- .../ResourceUploaderMock.cpp | 0 .../ResourceUploaderMock.h | 2 +- .../SystemCompositorControllerMock.cpp | 0 .../SystemCompositorControllerMock.h | 20 +- .../WindowEventHandlerMock.cpp | 0 .../WindowEventHandlerMock.h | 8 +- .../src => RendererTestCommon}/WindowMock.cpp | 2 +- .../WindowMock.h | 24 +- .../renderer_common_gmock_header.cpp | 3 +- .../renderer_common_gmock_header.h | 0 .../ramses-renderer-api/DcsmContentControl.h | 380 -- .../IDcsmContentControlEventHandler.h | 479 -- .../ramses-renderer-api/RendererConfig.h | 271 -- .../ramses-renderer-api/WarpingMeshData.h | 58 - .../include/BinaryShaderCacheProxy.h | 44 - .../include/ContentStates.h | 40 - .../include/DcsmContentControlImpl.h | 235 - .../include/SharedSceneState.h | 60 - .../include/WarpingMeshDataImpl.h | 29 - .../src/DcsmContentControl.cpp | 141 - .../src/DcsmContentControlImpl.cpp | 1501 ------ .../src/DisplayConfig.cpp | 262 -- .../src/RendererConfig.cpp | 124 - .../src/RendererConfigImpl.cpp | 144 - .../src/SharedSceneState.cpp | 96 - .../src/WarpingMeshData.cpp | 26 - .../src/WarpingMeshDataImpl.cpp | 23 - .../test/DcsmConsumerMock.cpp | 24 - .../test/DcsmConsumerMock.h | 35 - .../test/DcsmContentControlTest.cpp | 4172 ----------------- .../test/RendererConfigTest.cpp | 122 - .../test/SharedSceneStateTest.cpp | 278 -- .../RendererTestUtils/CMakeLists.txt | 22 +- .../include/ReadPixelCallbackHandler.h | 4 +- .../RendererAndSceneTestEventHandler.h | 30 +- .../include/RendererTestUtils.h | 54 +- .../src/RendererTestUtils.cpp | 67 +- renderer/ramses-renderer-api/CMakeLists.txt | 23 + .../ramses-renderer-api/BinaryShaderCache.h | 25 +- .../DefaultRendererResourceCache.h | 11 +- .../ramses-renderer-api/DisplayConfig.h | 186 +- .../ramses-renderer-api/IBinaryShaderCache.h | 9 +- .../IRendererEventHandler.h | 64 +- .../IRendererResourceCache.h | 3 +- .../IRendererSceneControlEventHandler.h | 67 +- .../ramses-renderer-api/RamsesRenderer.h | 279 +- .../ramses-renderer-api/RendererConfig.h | 152 + .../RendererSceneControl.h | 69 +- .../include/ramses-renderer-api/Types.h | 106 +- renderer/ramses-renderer-impl/CMakeLists.txt | 47 + .../include/BinaryShaderCacheImpl.h | 6 +- .../include/BinaryShaderCacheProxy.h | 44 + .../include/CommandDispatchingThread.h | 4 +- .../DefaultRendererResourceCacheImpl.h | 16 +- .../include/DisplayConfigImpl.h | 36 +- .../include/RamsesRendererImpl.h | 34 +- .../include/RamsesRendererUtils.h | 0 .../include/RendererConfigImpl.h | 20 +- .../include/RendererEventChainer.h | 70 +- .../include/RendererFactory.h | 2 +- .../include/RendererMate.h | 16 +- .../include/RendererMateRamshCommands.h | 14 +- .../include/RendererResourceCacheProxy.h | 10 +- .../include/RendererSceneControlImpl.h | 42 +- .../src/BinaryShaderCache.cpp | 4 +- .../src/BinaryShaderCacheImpl.cpp | 10 +- .../src/BinaryShaderCacheProxy.cpp | 2 +- .../src/CommandDispatchingThread.cpp | 9 - .../src/DefaultRendererResourceCache.cpp | 4 +- .../src/DefaultRendererResourceCacheImpl.cpp | 12 +- .../src/DisplayConfig.cpp | 276 ++ .../src/DisplayConfigImpl.cpp | 143 +- .../src/RamsesRenderer.cpp | 145 +- .../src/RamsesRendererImpl.cpp | 230 +- .../src/RamsesRendererUtils.cpp | 42 +- .../src/RendererConfig.cpp | 104 + .../src/RendererConfigImpl.cpp | 80 + .../src/RendererFactory.cpp | 7 +- .../ramses-renderer-impl/src/RendererMate.cpp | 14 +- .../src/RendererMateRamshCommands.cpp | 8 +- .../src/RendererResourceCacheProxy.cpp | 0 .../src/RendererSceneControl.cpp | 33 +- .../src/RendererSceneControlImpl.cpp | 74 +- .../test/BinaryShaderCacheProxyTest.cpp | 4 +- .../test/BinaryShaderCacheTest.cpp | 70 +- .../test/DefaultRendererResourceCacheTest.cpp | 32 +- .../test/DisplayConfigTest.cpp | 142 +- .../test/RamsesRendererDispatchTest.cpp | 129 +- .../test/RamsesRendererPickingTest.cpp | 41 +- .../test/RamsesRendererTest.cpp | 331 +- .../test/RendererConfigTest.cpp | 109 + .../test/RendererEventTestHandler.h | 56 +- .../test/RendererMateTest.cpp | 15 +- .../RendererSceneControlEventHandlerMock.cpp | 0 .../RendererSceneControlEventHandlerMock.h | 7 +- .../test/RendererSceneControlTest.cpp | 99 +- .../RendererSceneControlWithRendererTest.cpp | 36 +- renderer/ramses-renderer-main/CMakeLists.txt | 11 +- renderer/ramses-renderer-main/src/main.cpp | 216 +- scripts/ci/build/build.py | 191 + scripts/ci/build/common.py | 52 + scripts/ci/build/test-cmake-configurations.py | 90 + scripts/ci/clang-tidy-wrapper.py | 248 + scripts/ci/collect-coverage.py | 111 + scripts/ci/common/clangtidy.py | 159 + scripts/ci/common/compilationdb.py | 88 + scripts/ci/common/lists.py | 81 + scripts/ci/common/repo.py | 48 + scripts/ci/common/yamlconfig.py | 23 + scripts/ci/config/clang-tidy-wrapper.yaml | 55 + .../ci/config/sanitizer/asan_suppressions.txt | 1 + .../ci/config/sanitizer/lsan_suppressions.txt | 15 + .../ci/config/sanitizer/tsan_blacklist.txt | 3 + scripts/ci/config/valgrind/suppressions | 278 ++ .../check-build-with-install-shared-lib.bat | 55 + scripts/ci/installation-check/check-build.py | 49 + .../installation-check/check-installation.py | 240 + .../ci/installation-check/cmake/Findglm.cmake | 30 + .../shared-lib-check/CMakeLists.txt | 68 + .../ramses-shared-lib-check.cpp | 43 + .../shared-lib-headless-check/CMakeLists.txt | 55 + .../ramses-shared-lib-check.cpp | 32 + .../static-lib-check/CMakeLists.txt | 47 + .../ramses-static-lib-check.cpp | 39 + scripts/ci/repo-copy.py | 165 + scripts/ci/test/test_clang-tidy-wrapper.py | 297 ++ scripts/ci/test/test_clangtidy.py | 286 ++ scripts/ci/test/test_compilationdb.py | 104 + scripts/ci/test/test_lists.py | 99 + scripts/ci/test/test_repo.py | 71 + scripts/ci/test/test_yamlconfig.py | 90 + .../code_style_checker/check_all_styles.py | 45 +- .../check_api_export_symbols.py | 21 +- scripts/code_style_checker/check_license.py | 18 +- scripts/docker/runtime-files/build-ramses.sh | 3 +- scripts/docker/runtime-files/run-unittests.sh | 1 - scripts/integration_tests/.gitignore | 2 - scripts/integration_tests/__init__.py | 1 - .../configuration/__init__.py | 1 - .../configuration/base_remote_config.py | 62 - .../configuration/common_config.py | 28 - .../configuration/local_config.py | 28 - .../example_run_tests_on_remote_targets.py | 29 - .../images_desired/black.png | Bin 168 -> 0 bytes .../images_desired/black_600x600.png | Bin 469 -> 0 bytes .../images_desired/black_rgb.png | Bin 1866 -> 0 bytes .../red_triangle_on_blue_background.png | Bin 4430 -> 0 bytes .../renderer_with_dest_rectangle.png | Bin 38222 -> 0 bytes .../scc_big_red_gear_left_and_cube.png | Bin 181680 -> 0 bytes .../images_desired/scc_only_cube.png | Bin 170485 -> 0 bytes .../scc_red_gear_left_and_cube.png | Bin 179288 -> 0 bytes ...left_white_gear_right_lowered_and_cube.png | Bin 188664 -> 0 bytes ...scc_red_transparent_gear_left_and_cube.png | Bin 178603 -> 0 bytes .../scc_white_gear_left_and_cube.png | Bin 179914 -> 0 bytes ..._gear_left_big_red_gear_right_and_cube.png | Bin 129714 -> 0 bytes ...hite_gear_left_red_gear_right_and_cube.png | Bin 185520 -> 0 bytes .../images_desired/testClient_animation.png | Bin 2580 -> 0 bytes ...lient_compositing_allSidesSameFallback.png | Bin 184141 -> 0 bytes ...SidesSameFallback_oneAndTwoCompositing.png | Bin 96232 -> 0 bytes ...ng_allSidesSameFallback_oneCompositing.png | Bin 138502 -> 0 bytes ...ompositing_allSurfacesFallbackTextures.png | Bin 325374 -> 0 bytes ...testClient_compositing_fallbacktexture.png | Bin 168280 -> 0 bytes ...nt_compositing_fiveSurfacesCompositing.png | Bin 116708 -> 0 bytes ...veSurfacesCompositingDmaBufAndIviGears.png | Bin 50176 -> 0 bytes ...nt_compositing_sameSourceMultiFallback.png | Bin 28169 -> 0 bytes ...t_compositing_threeSurfacesCompositing.png | Bin 225432 -> 0 bytes ...ositing_threeSurfacesCompositingDmaBuf.png | Bin 209748 -> 0 bytes ...eeSurfacesCompositingDmaBufAndIviGears.png | Bin 213104 -> 0 bytes ...ent_compositing_twoSurfacesCompositing.png | Bin 280953 -> 0 bytes ...mpositing_twoSurfacesCompositingDmaBuf.png | Bin 252569 -> 0 bytes .../testClient_dataLink_linked.png | Bin 1508 -> 0 bytes .../testClient_dataLink_noLinks.png | Bin 1360 -> 0 bytes .../testClient_destroyedScene.png | Bin 1004 -> 0 bytes .../testClient_disconnectedClient.png | Bin 962 -> 0 bytes ...estClient_distributedScene_redTriangle.png | Bin 605 -> 0 bytes .../testClient_file_loading.png | Bin 173418 -> 0 bytes .../testClient_move_between_IVI_layers_A.png | Bin 2812 -> 0 bytes .../testClient_move_between_IVI_layers_B.png | Bin 2532 -> 0 bytes .../images_desired/testClient_multiScene.png | Bin 1502 -> 0 bytes .../testClient_multipleClients.png | Bin 1958 -> 0 bytes .../testClient_multipleRenderTargets.png | Bin 846 -> 0 bytes .../testClient_multiple_ivi_layers.png | Bin 3610 -> 0 bytes .../testClient_streamtexture_1.png | Bin 115841 -> 0 bytes .../testClient_streamtexture_dmabuf_1.png | Bin 106512 -> 0 bytes .../images_desired/testClient_text_scene.png | Bin 70496 -> 0 bytes .../testClient_threeTriangles.png | Bin 935 -> 0 bytes .../testClient_triangleBlue.png | Bin 772 -> 0 bytes .../images_desired/testClient_triangleRed.png | Bin 765 -> 0 bytes .../testClient_triangleRedAndBlue.png | Bin 1527 -> 0 bytes .../testClient_twoDisplaysPlacedTogether.png | Bin 6034 -> 0 bytes .../images_desired/testClient_twoScenes.png | Bin 1591 -> 0 bytes .../testClient_twoScenes_displ0.png | Bin 1144 -> 0 bytes .../testClient_twoScenes_displ1.png | Bin 1308 -> 0 bytes .../testForceFallbackImage1.png | Bin 108624 -> 0 bytes .../testForceFallbackImage2.png | Bin 152094 -> 0 bytes .../testForceFallbackImage3.png | Bin 71949 -> 0 bytes .../ramses_test_framework/__init__.py | 1 - .../ramses_test_framework/application.py | 167 - .../asynchronousreader.py | 63 - .../ramses_test_framework/buffer.py | 133 - .../ramses_test_framework/cores/__init__.py | 7 - .../ramses_test_framework/cores/core.py | 40 - .../ramses_test_framework/cores/core_impl.py | 226 - .../cores/remote_core_impl.py | 153 - .../ramses_test_framework/helper.py | 77 - .../ramses_test_framework/image_utils.py | 106 - .../local_application.py | 29 - .../ramses_test_framework/log.py | 162 - .../ramses_test_framework/power_device.py | 72 - .../ramses_test_extensions.py | 192 - .../ramses_test_framework/targets/__init__.py | 0 .../targets/linux_remote_targets.py | 23 - .../targets/local_target.py | 119 - .../targets/remote_target.py | 322 -- .../ramses_test_framework/targets/target.py | 392 -- .../targets/targetInfo.py | 50 - .../targets/windows_cygwin_target.py | 55 - .../ramses_test_framework/test_classes.py | 197 - scripts/integration_tests/tests/__init__.py | 1 - .../embedded_compositor_base/__init__.py | 0 .../embedded_compositor_base.py | 108 - .../__init__.py | 0 .../system_compositor_controller_base.py | 159 - .../tests/test_localClientTest.py | 31 - .../tests/test_local_and_remote_renderer.py | 48 - .../tests/test_local_and_remote_scenes.py | 61 - .../tests/test_renderertwodisplays.py | 52 - .../tests/test_renderertwotargets.py | 58 - .../tests/test_run_no_initial_black_frame.py | 63 - .../tests/test_scenemanagerdifferenttarget.py | 49 - .../test_start_renderer_with_dst_rectangle.py | 55 - ...tor_controller_can_add_surface_to_layer.py | 29 - ...r_controller_can_change_surface_opacity.py | 26 - ...s_same_id_than_destroyed_surface_before.py | 43 - ...egisters_while_renderer_already_running.py | 31 - ...trol_surface_which_is_not_yet_allocated.py | 34 - ...mpositor_controller_can_destroy_surface.py | 28 - ...tor_controller_can_make_layer_invisible.py | 29 - ...sitor_controller_can_make_layer_visible.py | 30 - ...r_controller_can_make_surface_invisible.py | 26 - ...tor_controller_can_make_surface_visible.py | 25 - ...ontroller_can_make_two_surfaces_visible.py | 26 - ...compositor_controller_can_move_surfaces.py | 29 - ...ontroller_can_remove_surface_from_layer.py | 28 - ...e_from_list_when_destroyed_from_outside.py | 31 - .../test_testclient_EC_dmabuf_confidence.py | 47 - .../test_testclient_EC_dmabuf_lifecycle.py | 33 - .../tests/test_testclient_EC_lifecycle.py | 33 - ...testclient_EC_lifecycle_client_cleansUp.py | 38 - ...t_testclient_EC_lifecycle_client_killed.py | 33 - ...estclient_EC_multi_fallback_same_source.py | 32 - ...tclient_EC_multi_sources_multi_fallback.py | 42 - ...stclient_EC_multi_sources_same_fallback.py | 37 - .../tests/test_testclient_animation.py | 40 - ...testclient_compositing_fallback_texture.py | 42 - .../tests/test_testclient_datalink.py | 52 - .../tests/test_testclient_destroyedScene.py | 41 - .../tests/test_testclient_file_loading_tcp.py | 43 - .../test_testclient_forcefallbackimage.py | 39 - ...test_testclient_move_between_IVI_layers.py | 84 - .../tests/test_testclient_multiScene.py | 45 - .../test_testclient_multipleRenderTarget.py | 41 - .../test_testclient_multiple_IVI_layers.py | 93 - .../tests/test_testclient_multipleclients.py | 47 - .../tests/test_testclient_reconnectClient.py | 58 - .../tests/test_testclient_sceneMapping.py | 48 - .../tests/test_testclient_sceneRelease.py | 55 - .../tests/test_testclient_twoScenes.py | 42 - .../tests/test_text_rendering.py | 41 - tox.ini | 41 - utils/CMakeLists.txt | 11 +- utils/ramses-imgui/CMakeLists.txt | 19 +- .../ramses-imgui/include/ImguiClientHelper.h | 6 +- utils/ramses-imgui/include/ImguiWidgets.h | 18 +- utils/ramses-imgui/src/ImguiClientHelper.cpp | 82 +- utils/ramses-imgui/src/ImguiImageCache.cpp | 8 +- utils/ramses-imgui/src/ImguiWidgets.cpp | 149 +- utils/ramses-resource-tools/CMakeLists.txt | 72 - .../include/FilePathsConfig.h | 30 - .../include/RamsesResourcePackerArguments.h | 40 - .../include/ResourcePacker.h | 20 - .../ramses-resource-packer/main.cpp | 31 - .../src/FilePathsConfig.cpp | 94 - .../src/RamsesResourcePackerArguments.cpp | 84 - .../src/ResourcePacker.cpp | 72 - .../test/FilePathesConfigTest.cpp | 49 - .../RamsesResourcePackerArgumentsTest.cpp | 76 - .../test/ResourcePackerTest.cpp | 228 - .../res/ramses-resource-tools-Roboto-Bold.ttf | Bin 170760 -> 0 bytes ...mses-resource-tools-empty.filepathesconfig | 0 ...nvalid-duplicate-filepath.filepathesconfig | 5 - ...nvalid-non-exist-filepath.filepathesconfig | 5 - ...-invalid-wrong-num-tokens.filepathesconfig | 4 - ...tools-resourcepackerinput.filepathesconfig | 3 - ...amses-resource-tools-test.filepathesconfig | 4 - .../test/res/ramses-resource-tools-test1.res | 0 .../test/res/ramses-resource-tools-test2.res | 0 .../test/res/ramses-resource-tools-test3.res | 0 .../test/res/ramses-resource-tools-test4.res | 0 ...al-spaces-and-empty-lines.filepathesconfig | 6 - utils/ramses-scene-viewer/CMakeLists.txt | 13 +- .../ramses-scene-viewer/src/ProgressMonitor.h | 8 +- utils/ramses-scene-viewer/src/ResourceList.h | 32 +- utils/ramses-scene-viewer/src/SceneSetup.h | 26 +- utils/ramses-scene-viewer/src/SceneViewer.cpp | 186 +- utils/ramses-scene-viewer/src/SceneViewer.h | 39 +- .../src/SceneViewerGui.cpp | 952 ++-- .../ramses-scene-viewer/src/SceneViewerGui.h | 36 +- utils/ramses-scene-viewer/src/main.cpp | 6 +- utils/ramses-shader-tools/CMakeLists.txt | 92 - .../include/EffectResourceCreator.h | 32 - .../RamsesEffectFromGLSLShaderArguments.h | 54 - .../RamsesShaderFromGLSLShaderArguments.h | 51 - .../include/ShaderConverter.h | 33 - .../ramses-effect-from-glsl-shader/main.cpp | 30 - .../ramses-shader-from-glsl-shader/main.cpp | 31 - .../src/EffectResourceCreator.cpp | 79 - .../RamsesEffectFromGLSLShaderArguments.cpp | 184 - .../RamsesShaderFromGLSLShaderArguments.cpp | 176 - .../src/ShaderConverter.cpp | 78 - .../test/EffectResourceCreatorTest.cpp | 176 - ...amsesEffectFromGLSLShaderArgumentsTest.cpp | 220 - ...amsesShaderFromGLSLShaderArgumentsTest.cpp | 311 -- .../test/ShaderConverterTest.cpp | 250 - ...ses-shader-tools-custom-text-effect.config | 3 - ...amses-shader-tools-custom-text-effect.frag | 26 - ...amses-shader-tools-custom-text-effect.vert | 26 - ...-attribute-semantic-invalid-keyword.config | 5 - ...ses-shader-tools-invalid-test.vertexshader | 70 - .../test/res/ramses-shader-tools-test.config | 5 - .../ramses-shader-tools-test.fragmentshader | 14 - .../res/ramses-shader-tools-test.vertexshader | 68 - ...tools-test_without_compiler_defines.config | 3 - utils/ramses-stream-viewer/CMakeLists.txt | 11 +- utils/ramses-stream-viewer/src/main.cpp | 323 +- utils/ramses-utils/CMakeLists.txt | 51 - utils/ramses-utils/include/Arguments.h | 46 - utils/ramses-utils/include/ConsoleUtils.h | 19 - utils/ramses-utils/include/EffectConfig.h | 62 - utils/ramses-utils/include/FileUtils.h | 31 - utils/ramses-utils/src/Arguments.cpp | 84 - utils/ramses-utils/src/EffectConfig.cpp | 307 -- utils/ramses-utils/src/FileUtils.cpp | 104 - utils/ramses-utils/test/EffectConfigTest.cpp | 108 - .../test/res/ramses-utils-empty.config | 0 ...-attribute-semantic-invalid-keyword.config | 5 - ...tribute-semantic-invalid-num-tokens.config | 7 - ...attribute-semantic-invalid-semantic.config | 7 - ...lid-comipler-define-invalid-keyword.config | 5 - ...-comipler-define-invalid-num-tokens.config | 5 - ...id-uniform-semantic-invalid-keyword.config | 5 - ...uniform-semantic-invalid-num-tokens.config | 5 - ...d-uniform-semantic-invalid-semantic.config | 5 - .../test/res/ramses-utils-test.config | 5 - ...h-additional-spaces-and-empty-lines.config | 8 - 2777 files changed, 117300 insertions(+), 123874 deletions(-) create mode 100644 .ansible-lint create mode 100644 .gitlint rename cmake/ramses/tools.cmake => .readthedocs.yml (59%) rename scripts/integration_tests/run_tests_locally.py => .yamllint (50%) mode change 100755 => 100644 rename CHANGELOG.txt => CHANGELOG.md (80%) create mode 100644 CONTRIBUTING.rst create mode 100644 SECURITY.md create mode 100644 benchmarks/CMakeLists.txt create mode 100644 benchmarks/animation.cpp create mode 100644 benchmarks/compile_lua.cpp create mode 100644 benchmarks/getproperty.cpp create mode 100644 benchmarks/links.cpp create mode 100644 benchmarks/property.cpp create mode 100644 benchmarks/serialization.cpp create mode 100644 benchmarks/update.cpp create mode 100644 client/CMakeLists.txt create mode 100644 client/logic/.clang-tidy create mode 100644 client/logic/cmake/flatbuffersGeneration.cmake create mode 100644 client/logic/cmake/platformConfig.cmake create mode 100644 client/logic/include/ramses-logic/AnchorPoint.h create mode 100644 client/logic/include/ramses-logic/AnimationNode.h create mode 100644 client/logic/include/ramses-logic/AnimationNodeConfig.h create mode 100644 client/logic/include/ramses-logic/AnimationTypes.h create mode 100644 client/logic/include/ramses-logic/Collection.h create mode 100644 client/logic/include/ramses-logic/DataArray.h create mode 100644 client/logic/include/ramses-logic/ELuaSavingMode.h create mode 100644 client/logic/include/ramses-logic/EPropertyType.h create mode 100644 client/logic/include/ramses-logic/EStandardModule.h create mode 100644 client/logic/include/ramses-logic/ErrorData.h create mode 100644 client/logic/include/ramses-logic/Iterator.h create mode 100644 client/logic/include/ramses-logic/Logger.h create mode 100644 client/logic/include/ramses-logic/LogicEngine.h create mode 100644 client/logic/include/ramses-logic/LogicEngineReport.h create mode 100644 client/logic/include/ramses-logic/LogicNode.h create mode 100644 client/logic/include/ramses-logic/LogicObject.h create mode 100644 client/logic/include/ramses-logic/LuaConfig.h create mode 100644 client/logic/include/ramses-logic/LuaInterface.h create mode 100644 client/logic/include/ramses-logic/LuaModule.h create mode 100644 client/logic/include/ramses-logic/LuaScript.h create mode 100644 client/logic/include/ramses-logic/Property.h create mode 100644 client/logic/include/ramses-logic/PropertyLink.h create mode 100644 client/logic/include/ramses-logic/RamsesAppearanceBinding.h create mode 100644 client/logic/include/ramses-logic/RamsesBinding.h create mode 100644 client/logic/include/ramses-logic/RamsesCameraBinding.h create mode 100644 client/logic/include/ramses-logic/RamsesLogicVersion.h create mode 100644 client/logic/include/ramses-logic/RamsesMeshNodeBinding.h create mode 100644 client/logic/include/ramses-logic/RamsesNodeBinding.h create mode 100644 client/logic/include/ramses-logic/RamsesRenderGroupBinding.h create mode 100644 client/logic/include/ramses-logic/RamsesRenderGroupBindingElements.h create mode 100644 client/logic/include/ramses-logic/RamsesRenderPassBinding.h create mode 100644 client/logic/include/ramses-logic/SaveFileConfig.h create mode 100644 client/logic/include/ramses-logic/SkinBinding.h create mode 100644 client/logic/include/ramses-logic/TimerNode.h create mode 100644 client/logic/include/ramses-logic/WarningData.h create mode 100644 client/logic/lib/flatbuffers/generated/AnchorPointGen.h create mode 100644 client/logic/lib/flatbuffers/generated/AnimationNodeGen.h create mode 100644 client/logic/lib/flatbuffers/generated/ApiObjectsGen.h create mode 100644 client/logic/lib/flatbuffers/generated/DataArrayGen.h create mode 100644 client/logic/lib/flatbuffers/generated/LinkGen.h create mode 100644 client/logic/lib/flatbuffers/generated/LogicEngineGen.h create mode 100644 client/logic/lib/flatbuffers/generated/LogicObjectGen.h create mode 100644 client/logic/lib/flatbuffers/generated/LuaInterfaceGen.h create mode 100644 client/logic/lib/flatbuffers/generated/LuaModuleGen.h create mode 100644 client/logic/lib/flatbuffers/generated/LuaScriptGen.h create mode 100644 client/logic/lib/flatbuffers/generated/PropertyGen.h create mode 100644 client/logic/lib/flatbuffers/generated/RamsesAppearanceBindingGen.h create mode 100644 client/logic/lib/flatbuffers/generated/RamsesBindingGen.h create mode 100644 client/logic/lib/flatbuffers/generated/RamsesCameraBindingGen.h create mode 100644 client/logic/lib/flatbuffers/generated/RamsesMeshNodeBindingGen.h create mode 100644 client/logic/lib/flatbuffers/generated/RamsesNodeBindingGen.h create mode 100644 client/logic/lib/flatbuffers/generated/RamsesReferenceGen.h create mode 100644 client/logic/lib/flatbuffers/generated/RamsesRenderGroupBindingGen.h create mode 100644 client/logic/lib/flatbuffers/generated/RamsesRenderPassBindingGen.h create mode 100644 client/logic/lib/flatbuffers/generated/SkinBindingGen.h create mode 100644 client/logic/lib/flatbuffers/generated/TimerNodeGen.h rename demo/ramses-dcsm-list/res/ramses-dcsm-list.vert => client/logic/lib/flatbuffers/schemas/AnchorPoint.fbs (63%) create mode 100644 client/logic/lib/flatbuffers/schemas/AnimationNode.fbs create mode 100644 client/logic/lib/flatbuffers/schemas/ApiObjects.fbs create mode 100644 client/logic/lib/flatbuffers/schemas/DataArray.fbs rename examples/ramses-example-basic-animation-realtime/res/ramses-example-basic-animation-realtime.vert => client/logic/lib/flatbuffers/schemas/Link.fbs (71%) create mode 100644 client/logic/lib/flatbuffers/schemas/LogicEngine.fbs create mode 100644 client/logic/lib/flatbuffers/schemas/LogicObject.fbs rename examples/ramses-example-basic-animation/res/ramses-example-basic-animation.vert => client/logic/lib/flatbuffers/schemas/LuaInterface.fbs (71%) create mode 100644 client/logic/lib/flatbuffers/schemas/LuaModule.fbs create mode 100644 client/logic/lib/flatbuffers/schemas/LuaScript.fbs create mode 100644 client/logic/lib/flatbuffers/schemas/Property.fbs rename demo/ramses-dcsm-scene-references-demo/res/ramses-demo-scene-references-text-effect.vert => client/logic/lib/flatbuffers/schemas/RamsesAppearanceBinding.fbs (66%) create mode 100644 client/logic/lib/flatbuffers/schemas/RamsesBinding.fbs rename framework/PlatformAbstraction/src/String.cpp => client/logic/lib/flatbuffers/schemas/RamsesCameraBinding.fbs (80%) rename demo/ramses-text-layout-demo/res/ramses-layout-demo-colored-quad.frag => client/logic/lib/flatbuffers/schemas/RamsesMeshNodeBinding.fbs (75%) create mode 100644 client/logic/lib/flatbuffers/schemas/RamsesNodeBinding.fbs rename examples/ramses-example-basic-animation/res/ramses-example-basic-animation.frag => client/logic/lib/flatbuffers/schemas/RamsesReference.fbs (77%) rename demo/ramses-text-layout-demo/res/ramses-layout-demo-red-text.frag => client/logic/lib/flatbuffers/schemas/RamsesRenderGroupBinding.fbs (65%) rename examples/ramses-example-basic-animation-realtime/res/ramses-example-basic-animation-realtime.frag => client/logic/lib/flatbuffers/schemas/RamsesRenderPassBinding.fbs (75%) rename demo/ramses-dcsm-list/res/ramses-dcsm-list.frag => client/logic/lib/flatbuffers/schemas/SkinBinding.fbs (61%) rename framework/Communication/TransportCommon/src/ConnectionSystemInitiatorResponder.cpp => client/logic/lib/flatbuffers/schemas/TimerNode.fbs (72%) create mode 100644 client/logic/lib/impl/AnchorPoint.cpp create mode 100644 client/logic/lib/impl/AnchorPointImpl.cpp create mode 100644 client/logic/lib/impl/AnchorPointImpl.h create mode 100644 client/logic/lib/impl/AnimationNode.cpp create mode 100644 client/logic/lib/impl/AnimationNodeConfig.cpp create mode 100644 client/logic/lib/impl/AnimationNodeConfigImpl.cpp create mode 100644 client/logic/lib/impl/AnimationNodeConfigImpl.h create mode 100644 client/logic/lib/impl/AnimationNodeImpl.cpp create mode 100644 client/logic/lib/impl/AnimationNodeImpl.h create mode 100644 client/logic/lib/impl/Collection.cpp create mode 100644 client/logic/lib/impl/DataArray.cpp create mode 100644 client/logic/lib/impl/DataArrayImpl.cpp create mode 100644 client/logic/lib/impl/DataArrayImpl.h create mode 100644 client/logic/lib/impl/Iterator.cpp create mode 100644 client/logic/lib/impl/Logger.cpp create mode 100644 client/logic/lib/impl/LoggerImpl.cpp create mode 100644 client/logic/lib/impl/LoggerImpl.h create mode 100644 client/logic/lib/impl/LogicEngine.cpp create mode 100644 client/logic/lib/impl/LogicEngineImpl.cpp create mode 100644 client/logic/lib/impl/LogicEngineImpl.h create mode 100644 client/logic/lib/impl/LogicEngineReport.cpp create mode 100644 client/logic/lib/impl/LogicEngineReportImpl.cpp create mode 100644 client/logic/lib/impl/LogicEngineReportImpl.h create mode 100644 client/logic/lib/impl/LogicNode.cpp create mode 100644 client/logic/lib/impl/LogicNodeImpl.cpp create mode 100644 client/logic/lib/impl/LogicNodeImpl.h create mode 100644 client/logic/lib/impl/LogicObject.cpp create mode 100644 client/logic/lib/impl/LogicObjectImpl.cpp create mode 100644 client/logic/lib/impl/LogicObjectImpl.h create mode 100644 client/logic/lib/impl/LuaConfig.cpp create mode 100644 client/logic/lib/impl/LuaConfigImpl.cpp create mode 100644 client/logic/lib/impl/LuaConfigImpl.h create mode 100644 client/logic/lib/impl/LuaInterface.cpp create mode 100644 client/logic/lib/impl/LuaInterfaceImpl.cpp create mode 100644 client/logic/lib/impl/LuaInterfaceImpl.h rename client/{ramses-client/ramses-client-api/AnimatedProperty.cpp => logic/lib/impl/LuaModule.cpp} (55%) create mode 100644 client/logic/lib/impl/LuaModuleImpl.cpp create mode 100644 client/logic/lib/impl/LuaModuleImpl.h rename client/{ramses-client/ramses-client-api/Spline.cpp => logic/lib/impl/LuaScript.cpp} (56%) create mode 100644 client/logic/lib/impl/LuaScriptImpl.cpp create mode 100644 client/logic/lib/impl/LuaScriptImpl.h create mode 100644 client/logic/lib/impl/Property.cpp create mode 100644 client/logic/lib/impl/PropertyImpl.cpp create mode 100644 client/logic/lib/impl/PropertyImpl.h create mode 100644 client/logic/lib/impl/RamsesAppearanceBinding.cpp create mode 100644 client/logic/lib/impl/RamsesAppearanceBindingImpl.cpp create mode 100644 client/logic/lib/impl/RamsesAppearanceBindingImpl.h rename client/{ramses-client/ramses-client-api/DataObject.cpp => logic/lib/impl/RamsesBinding.cpp} (63%) create mode 100644 client/logic/lib/impl/RamsesBindingImpl.cpp create mode 100644 client/logic/lib/impl/RamsesBindingImpl.h create mode 100644 client/logic/lib/impl/RamsesCameraBinding.cpp create mode 100644 client/logic/lib/impl/RamsesCameraBindingImpl.cpp create mode 100644 client/logic/lib/impl/RamsesCameraBindingImpl.h create mode 100644 client/logic/lib/impl/RamsesLogicVersion.cpp create mode 100644 client/logic/lib/impl/RamsesMeshNodeBinding.cpp create mode 100644 client/logic/lib/impl/RamsesMeshNodeBindingImpl.cpp create mode 100644 client/logic/lib/impl/RamsesMeshNodeBindingImpl.h create mode 100644 client/logic/lib/impl/RamsesNodeBinding.cpp create mode 100644 client/logic/lib/impl/RamsesNodeBindingImpl.cpp create mode 100644 client/logic/lib/impl/RamsesNodeBindingImpl.h create mode 100644 client/logic/lib/impl/RamsesRenderGroupBinding.cpp create mode 100644 client/logic/lib/impl/RamsesRenderGroupBindingElements.cpp create mode 100644 client/logic/lib/impl/RamsesRenderGroupBindingElementsImpl.cpp create mode 100644 client/logic/lib/impl/RamsesRenderGroupBindingElementsImpl.h create mode 100644 client/logic/lib/impl/RamsesRenderGroupBindingImpl.cpp create mode 100644 client/logic/lib/impl/RamsesRenderGroupBindingImpl.h create mode 100644 client/logic/lib/impl/RamsesRenderPassBinding.cpp create mode 100644 client/logic/lib/impl/RamsesRenderPassBindingImpl.cpp create mode 100644 client/logic/lib/impl/RamsesRenderPassBindingImpl.h create mode 100644 client/logic/lib/impl/SaveFileConfig.cpp create mode 100644 client/logic/lib/impl/SaveFileConfigImpl.cpp create mode 100644 client/logic/lib/impl/SaveFileConfigImpl.h create mode 100644 client/logic/lib/impl/SkinBinding.cpp create mode 100644 client/logic/lib/impl/SkinBindingImpl.cpp create mode 100644 client/logic/lib/impl/SkinBindingImpl.h rename client/{ramses-client/ramses-client-api/AnimationObject.cpp => logic/lib/impl/TimerNode.cpp} (55%) create mode 100644 client/logic/lib/impl/TimerNodeImpl.cpp create mode 100644 client/logic/lib/impl/TimerNodeImpl.h create mode 100644 client/logic/lib/internals/ApiObjects.cpp create mode 100644 client/logic/lib/internals/ApiObjects.h create mode 100644 client/logic/lib/internals/ApiObjectsSerializedSize.cpp create mode 100644 client/logic/lib/internals/ApiObjectsSerializedSize.h create mode 100644 client/logic/lib/internals/DeserializationMap.h create mode 100644 client/logic/lib/internals/DirectedAcyclicGraph.cpp create mode 100644 client/logic/lib/internals/DirectedAcyclicGraph.h rename doc/developer/60_SomeIPConnectionLogic.dox => client/logic/lib/internals/EPropertySemantics.h (64%) create mode 100644 client/logic/lib/internals/EnvironmentProtection.cpp create mode 100644 client/logic/lib/internals/EnvironmentProtection.h create mode 100644 client/logic/lib/internals/ErrorReporting.cpp rename framework/Components/include/Components/LogDcsmInfo.h => client/logic/lib/internals/ErrorReporting.h (54%) create mode 100644 client/logic/lib/internals/FileUtils.cpp create mode 100644 client/logic/lib/internals/FileUtils.h create mode 100644 client/logic/lib/internals/InterfaceTypeFunctions.cpp create mode 100644 client/logic/lib/internals/InterfaceTypeFunctions.h rename framework/Core/Utils/test/DataBindTest.h => client/logic/lib/internals/InterfaceTypeInfo.h (54%) create mode 100644 client/logic/lib/internals/LogicNodeDependencies.cpp create mode 100644 client/logic/lib/internals/LogicNodeDependencies.h create mode 100644 client/logic/lib/internals/LogicNodeUpdateStatistics.cpp create mode 100644 client/logic/lib/internals/LogicNodeUpdateStatistics.h create mode 100644 client/logic/lib/internals/LuaCompilationUtils.cpp create mode 100644 client/logic/lib/internals/LuaCompilationUtils.h create mode 100644 client/logic/lib/internals/LuaCustomizations.cpp create mode 100644 client/logic/lib/internals/LuaCustomizations.h create mode 100644 client/logic/lib/internals/LuaTypeConversions.cpp create mode 100644 client/logic/lib/internals/LuaTypeConversions.h create mode 100644 client/logic/lib/internals/PropertyTypeExtractor.cpp create mode 100644 client/logic/lib/internals/PropertyTypeExtractor.h create mode 100644 client/logic/lib/internals/RamsesHelper.h create mode 100644 client/logic/lib/internals/RamsesObjectResolver.cpp create mode 100644 client/logic/lib/internals/RamsesObjectResolver.h create mode 100644 client/logic/lib/internals/SerializationHelper.h create mode 100644 client/logic/lib/internals/SerializationMap.h create mode 100644 client/logic/lib/internals/SolHelper.h create mode 100644 client/logic/lib/internals/SolState.cpp create mode 100644 client/logic/lib/internals/SolState.h create mode 100644 client/logic/lib/internals/SolWrapper.h create mode 100644 client/logic/lib/internals/StdFilesystemWrapper.h create mode 100644 client/logic/lib/internals/TypeData.cpp create mode 100644 client/logic/lib/internals/TypeData.h create mode 100644 client/logic/lib/internals/TypeUtils.h create mode 100644 client/logic/lib/internals/UpdateReport.cpp create mode 100644 client/logic/lib/internals/UpdateReport.h create mode 100644 client/logic/lib/internals/ValidationResults.cpp rename framework/Animation/Animation/test/AnimationDataBindTest.h => client/logic/lib/internals/ValidationResults.h (51%) create mode 100644 client/logic/lib/internals/WrappedLuaProperty.cpp create mode 100644 client/logic/lib/internals/WrappedLuaProperty.h rename scripts/integration_tests/framework-self-tests/test_foo.py => client/logic/tools/CMakeLists.txt (79%) create mode 100644 client/logic/tools/ramses-logic-viewer-test/CMakeLists.txt create mode 100644 client/logic/tools/ramses-logic-viewer-test/LogicViewerAppTest.cpp create mode 100644 client/logic/tools/ramses-logic-viewer-test/LogicViewerLuaTest.cpp create mode 100644 client/logic/tools/ramses-logic-viewer-test/LogicViewerTest.cpp create mode 100644 client/logic/tools/ramses-logic-viewer-test/LogicViewerTestBase.cpp create mode 100644 client/logic/tools/ramses-logic-viewer-test/LogicViewerTestBase.h create mode 100644 client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_clearColor.png create mode 100644 client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_clearColorCmdLine.png create mode 100644 client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_red.png create mode 100644 client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_red_500x700.png create mode 100644 client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_white.png create mode 100644 client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_yellow.png create mode 100644 client/logic/tools/ramses-logic-viewer/Arguments.h create mode 100644 client/logic/tools/ramses-logic-viewer/CMakeLists.txt create mode 100644 client/logic/tools/ramses-logic-viewer/ImguiClientHelper.cpp create mode 100644 client/logic/tools/ramses-logic-viewer/ImguiClientHelper.h rename demo/ramses-dcsm-scene-references-demo/res/ramses-demo-scene-references-text-effect.frag => client/logic/tools/ramses-logic-viewer/ImguiWrapper.h (68%) create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewer.cpp create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewer.h create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerApp.cpp create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerApp.h create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerGui.cpp create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerGui.h create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerGuiApp.cpp create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerGuiApp.h create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerHeadlessApp.cpp rename framework/ramses-framework-api/include/ramses-framework-api/EDcsmOfferingMode.h => client/logic/tools/ramses-logic-viewer/LogicViewerHeadlessApp.h (55%) create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerLog.cpp create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerLog.h create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerLuaTypes.cpp create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerLuaTypes.h create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerSettings.cpp create mode 100644 client/logic/tools/ramses-logic-viewer/LogicViewerSettings.h create mode 100644 client/logic/tools/ramses-logic-viewer/Result.h create mode 100644 client/logic/tools/ramses-logic-viewer/SceneSetup.h create mode 100644 client/logic/tools/ramses-logic-viewer/UpdateReportSummary.h rename {demo/ramses-text-layout-demo/src => client/logic/tools/ramses-logic-viewer}/main.cpp (78%) create mode 100644 client/logic/tools/ramses-logic-viewer/main_headless.cpp create mode 100644 client/logic/unittests/api/AnchorPointTest.cpp create mode 100644 client/logic/unittests/api/AnimationNodeConfigTest.cpp create mode 100644 client/logic/unittests/api/AnimationNodeTest.cpp create mode 100644 client/logic/unittests/api/AnimationNodeWithDataPropertiesTest.cpp create mode 100644 client/logic/unittests/api/CollectionTest.cpp create mode 100644 client/logic/unittests/api/DataArrayTest.cpp create mode 100644 client/logic/unittests/api/IteratorTest.cpp create mode 100644 client/logic/unittests/api/LoggerTest.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_Animations.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_Compatibility.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_DependencyExtraction.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_Dirtiness.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_ErrorHandling.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_Factory.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_Linking.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_LogicNodeUpdateStatistics.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_Lookup.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_Serialization.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_SerializedSize.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_Update.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_UpdateReport.cpp create mode 100644 client/logic/unittests/api/LogicEngineTest_Validation.cpp create mode 100644 client/logic/unittests/api/LogicNodeTest.cpp create mode 100644 client/logic/unittests/api/LuaConfigTest.cpp create mode 100644 client/logic/unittests/api/LuaInterfaceTest.cpp create mode 100644 client/logic/unittests/api/LuaModuleTest.cpp create mode 100644 client/logic/unittests/api/LuaScriptTest_Debug.cpp create mode 100644 client/logic/unittests/api/LuaScriptTest_Init.cpp create mode 100644 client/logic/unittests/api/LuaScriptTest_Interface.cpp create mode 100644 client/logic/unittests/api/LuaScriptTest_Lifecycle.cpp create mode 100644 client/logic/unittests/api/LuaScriptTest_Modules.cpp create mode 100644 client/logic/unittests/api/LuaScriptTest_Runtime.cpp create mode 100644 client/logic/unittests/api/LuaScriptTest_Serialization.cpp create mode 100644 client/logic/unittests/api/LuaScriptTest_Syntax.cpp create mode 100644 client/logic/unittests/api/LuaScriptTest_Types.cpp create mode 100644 client/logic/unittests/api/PropertyTest.cpp create mode 100644 client/logic/unittests/api/PropertyTypeTest.cpp create mode 100644 client/logic/unittests/api/RamsesAppearanceBindingTest.cpp create mode 100644 client/logic/unittests/api/RamsesCameraBindingTest.cpp create mode 100644 client/logic/unittests/api/RamsesMeshNodeBindingTest.cpp create mode 100644 client/logic/unittests/api/RamsesNodeBindingTest.cpp create mode 100644 client/logic/unittests/api/RamsesRenderGroupBindingElementsTest.cpp create mode 100644 client/logic/unittests/api/RamsesRenderGroupBindingTest.cpp create mode 100644 client/logic/unittests/api/RamsesRenderPassBindingTest.cpp create mode 100644 client/logic/unittests/api/SaveFileConfigTest.cpp create mode 100644 client/logic/unittests/api/SkinBindingTest.cpp create mode 100644 client/logic/unittests/api/TimerNodeTest.cpp create mode 100644 client/logic/unittests/internal/ApiObjectsTest.cpp create mode 100644 client/logic/unittests/internal/DirectedAcyclicGraphTest.cpp create mode 100644 client/logic/unittests/internal/EnvironmentProtectionTest.cpp create mode 100644 client/logic/unittests/internal/ErrorReportingTest.cpp create mode 100644 client/logic/unittests/internal/LogicNodeDependenciesTest.cpp create mode 100644 client/logic/unittests/internal/LuaCustomizationsTest.cpp create mode 100644 client/logic/unittests/internal/LuaTypeConversionsTest.cpp create mode 100644 client/logic/unittests/internal/PropertyTypeExtractorTest.cpp create mode 100644 client/logic/unittests/internal/RamsesHelperTest.cpp create mode 100644 client/logic/unittests/internal/RamsesObjectResolverTest.cpp create mode 100644 client/logic/unittests/internal/SolHelperTest.cpp create mode 100644 client/logic/unittests/internal/SolStateTest.cpp create mode 100644 client/logic/unittests/internal/TypeDataTest.cpp create mode 100644 client/logic/unittests/internal/TypeUtilsTest.cpp create mode 100644 client/logic/unittests/internal/WrappedLuaPropertyTest.cpp create mode 100644 client/logic/unittests/res/testLogic_01.rlogic create mode 100644 client/logic/unittests/res/testScene_01.ramses rename framework/Animation/Animation/test/AnimationInstanceTest.h => client/logic/unittests/shared/FeatureLevelTestValues.h (58%) create mode 100644 client/logic/unittests/shared/LogTestUtils.h create mode 100644 client/logic/unittests/shared/LogicEngineTest_Base.h rename framework/Communication/TransportCommon/src/ConnectionSystemBase.cpp => client/logic/unittests/shared/LogicNodeDummy.cpp (83%) create mode 100644 client/logic/unittests/shared/LogicNodeDummy.h create mode 100644 client/logic/unittests/shared/LuaScriptTest_Base.h create mode 100644 client/logic/unittests/shared/PropertyLinkTestUtils.h create mode 100644 client/logic/unittests/shared/RamsesObjectResolverMock.h create mode 100644 client/logic/unittests/shared/RamsesTestUtils.h create mode 100644 client/logic/unittests/shared/SerializationTestUtils.h create mode 100644 client/logic/unittests/shared/WithTempDirectory.h rename {demo/ramses-dcsm-scene-references-demo => client/logic/unittests/testAssetProducer}/CMakeLists.txt (69%) create mode 100644 client/logic/unittests/testAssetProducer/main.cpp create mode 100644 client/ramses-client-api/Appearance.cpp create mode 100644 client/ramses-client-api/ArrayBuffer.cpp rename client/{ramses-client => }/ramses-client-api/ArrayResource.cpp (73%) create mode 100644 client/ramses-client-api/AttributeInput.cpp rename client/{ramses-client => }/ramses-client-api/BlitPass.cpp (70%) rename client/{ramses-client => }/ramses-client-api/Camera.cpp (59%) rename client/{ramses-client => }/ramses-client-api/ClientObject.cpp (76%) create mode 100644 client/ramses-client-api/DataObject.cpp create mode 100644 client/ramses-client-api/Effect.cpp create mode 100644 client/ramses-client-api/EffectDescription.cpp rename client/{ramses-client => }/ramses-client-api/EffectInput.cpp (64%) rename client/{ramses-client => }/ramses-client-api/GeometryBinding.cpp (74%) rename client/{ramses-client => }/ramses-client-api/MeshNode.cpp (69%) create mode 100644 client/ramses-client-api/Node.cpp rename client/{ramses-client => }/ramses-client-api/OrthographicCamera.cpp (78%) rename client/{ramses-client => }/ramses-client-api/PerspectiveCamera.cpp (74%) rename client/{ramses-client => }/ramses-client-api/PickableObject.cpp (73%) rename client/{ramses-client => }/ramses-client-api/RamsesClient.cpp (54%) rename client/{ramses-client => }/ramses-client-api/RamsesObject.cpp (66%) rename client/{ramses-client => }/ramses-client-api/RenderBuffer.cpp (71%) rename client/{ramses-client => }/ramses-client-api/RenderGroup.cpp (71%) rename client/{ramses-client => }/ramses-client-api/RenderPass.cpp (64%) rename client/{ramses-client => }/ramses-client-api/RenderTarget.cpp (74%) create mode 100644 client/ramses-client-api/RenderTargetDescription.cpp rename client/{ramses-client => }/ramses-client-api/Resource.cpp (76%) rename client/{ramses-client => }/ramses-client-api/Scene.cpp (50%) create mode 100644 client/ramses-client-api/SceneConfig.cpp rename client/{ramses-client => }/ramses-client-api/SceneGraphIterator.cpp (80%) rename client/{ramses-client => }/ramses-client-api/SceneObject.cpp (73%) rename client/{ramses-client => }/ramses-client-api/SceneReference.cpp (71%) rename client/{ramses-client => }/ramses-client-api/Texture2D.cpp (72%) create mode 100644 client/ramses-client-api/Texture2DBuffer.cpp rename client/{ramses-client => }/ramses-client-api/Texture3D.cpp (72%) rename client/{ramses-client => }/ramses-client-api/TextureCube.cpp (72%) rename client/{ramses-client => }/ramses-client-api/TextureEnums.cpp (77%) rename client/{ramses-client => }/ramses-client-api/TextureSampler.cpp (68%) rename client/{ramses-client => }/ramses-client-api/TextureSamplerExternal.cpp (74%) rename client/{ramses-client => }/ramses-client-api/TextureSamplerMS.cpp (75%) create mode 100644 client/ramses-client-api/UniformInput.cpp create mode 100644 client/ramses-client-api/include/ramses-client-api/Appearance.h rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/ArrayBuffer.h (61%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/ArrayResource.h (60%) create mode 100644 client/ramses-client-api/include/ramses-client-api/AttributeInput.h rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/BlitPass.h (81%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/Camera.h (81%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/ClientObject.h (51%) create mode 100644 client/ramses-client-api/include/ramses-client-api/DataObject.h create mode 100644 client/ramses-client-api/include/ramses-client-api/ERotationType.h rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/EScenePublicationMode.h (87%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/EVisibilityMode.h (95%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/Effect.h (69%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/EffectDescription.h (69%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/EffectInput.h (69%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/EffectInputSemantic.h (98%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/GeometryBinding.h (74%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/IClientEventHandler.h (96%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/MeshNode.h (76%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/MipLevelData.h (85%) create mode 100644 client/ramses-client-api/include/ramses-client-api/Node.h rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/OrthographicCamera.h (74%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/PerspectiveCamera.h (87%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/PickableObject.h (87%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/RamsesClient.h (79%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/RamsesObject.h (65%) create mode 100644 client/ramses-client-api/include/ramses-client-api/RamsesObjectTypes.h rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/RenderBuffer.h (75%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/RenderGroup.h (83%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/RenderGroupMeshIterator.h (79%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/RenderPass.h (82%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/RenderPassGroupIterator.h (79%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/RenderTarget.h (73%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/RenderTargetDescription.h (60%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/Resource.h (66%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/Scene.h (76%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/SceneConfig.h (61%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/SceneGraphIterator.h (64%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/SceneIterator.h (82%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/SceneObject.h (56%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/SceneObjectIterator.h (78%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/SceneReference.h (90%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/Texture2D.h (60%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/Texture2DBuffer.h (83%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/Texture3D.h (60%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/TextureCube.h (73%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/TextureEnums.h (70%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/TextureSampler.h (71%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/TextureSamplerExternal.h (72%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/TextureSamplerMS.h (73%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/TextureSwizzle.h (98%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client-api/UniformInput.h (50%) rename client/{ramses-client => }/ramses-client-api/include/ramses-client.h (50%) rename client/{ramses-client => }/ramses-client-api/include/ramses-utils.h (87%) delete mode 100644 client/ramses-client/CMakeLists.txt delete mode 100644 client/ramses-client/impl/AnimatedPropertyFactory.cpp delete mode 100644 client/ramses-client/impl/AnimatedPropertyFactory.h delete mode 100644 client/ramses-client/impl/AnimatedPropertyImpl.cpp delete mode 100644 client/ramses-client/impl/AnimatedPropertyImpl.h delete mode 100644 client/ramses-client/impl/AnimatedPropertyUtils.cpp delete mode 100644 client/ramses-client/impl/AnimatedPropertyUtils.h delete mode 100644 client/ramses-client/impl/AnimationImpl.cpp delete mode 100644 client/ramses-client/impl/AnimationImpl.h delete mode 100644 client/ramses-client/impl/AnimationObjectImpl.cpp delete mode 100644 client/ramses-client/impl/AnimationObjectImpl.h delete mode 100644 client/ramses-client/impl/AnimationSequenceImpl.cpp delete mode 100644 client/ramses-client/impl/AnimationSequenceImpl.h delete mode 100644 client/ramses-client/impl/AnimationSystemData.cpp delete mode 100644 client/ramses-client/impl/AnimationSystemData.h delete mode 100644 client/ramses-client/impl/AnimationSystemImpl.cpp delete mode 100644 client/ramses-client/impl/AnimationSystemImpl.h delete mode 100644 client/ramses-client/impl/AnimationSystemIteratorImpl.h delete mode 100644 client/ramses-client/impl/AnimationSystemObjectIterator.cpp delete mode 100644 client/ramses-client/impl/ClientCommands/ForceFallbackImage.cpp delete mode 100644 client/ramses-client/impl/ClientCommands/ForceFallbackImage.h delete mode 100644 client/ramses-client/impl/DataTypeUtils.h delete mode 100644 client/ramses-client/impl/EffectInputUtils.h delete mode 100644 client/ramses-client/impl/ResourceDataPoolImpl.cpp delete mode 100644 client/ramses-client/impl/ResourceDataPoolImpl.h delete mode 100644 client/ramses-client/impl/RotationConventionUtils.h create mode 100644 client/ramses-client/impl/RotationTypeUtils.h delete mode 100644 client/ramses-client/impl/SplineImpl.cpp delete mode 100644 client/ramses-client/impl/SplineImpl.h delete mode 100644 client/ramses-client/impl/StreamTextureImpl.cpp delete mode 100644 client/ramses-client/impl/StreamTextureImpl.h delete mode 100644 client/ramses-client/impl/ramses-hmi-utils.cpp delete mode 100644 client/ramses-client/ramses-client-api/Animation.cpp delete mode 100644 client/ramses-client/ramses-client-api/AnimationSequence.cpp delete mode 100644 client/ramses-client/ramses-client-api/AnimationSystem.cpp delete mode 100644 client/ramses-client/ramses-client-api/AnimationSystemRealTime.cpp delete mode 100644 client/ramses-client/ramses-client-api/Appearance.cpp delete mode 100644 client/ramses-client/ramses-client-api/AppearanceEnums.cpp delete mode 100644 client/ramses-client/ramses-client-api/ArrayBuffer.cpp delete mode 100644 client/ramses-client/ramses-client-api/AttributeInput.cpp delete mode 100644 client/ramses-client/ramses-client-api/DataFloat.cpp delete mode 100644 client/ramses-client/ramses-client-api/DataInt32.cpp delete mode 100644 client/ramses-client/ramses-client-api/DataMatrix22f.cpp delete mode 100644 client/ramses-client/ramses-client-api/DataMatrix33f.cpp delete mode 100644 client/ramses-client/ramses-client-api/DataMatrix44f.cpp delete mode 100644 client/ramses-client/ramses-client-api/DataVector2f.cpp delete mode 100644 client/ramses-client/ramses-client-api/DataVector2i.cpp delete mode 100644 client/ramses-client/ramses-client-api/DataVector3f.cpp delete mode 100644 client/ramses-client/ramses-client-api/DataVector3i.cpp delete mode 100644 client/ramses-client/ramses-client-api/DataVector4f.cpp delete mode 100644 client/ramses-client/ramses-client-api/DataVector4i.cpp delete mode 100644 client/ramses-client/ramses-client-api/Effect.cpp delete mode 100644 client/ramses-client/ramses-client-api/EffectDescription.cpp delete mode 100644 client/ramses-client/ramses-client-api/Node.cpp delete mode 100644 client/ramses-client/ramses-client-api/RenderTargetDescription.cpp delete mode 100644 client/ramses-client/ramses-client-api/ResourceDataPool.cpp delete mode 100644 client/ramses-client/ramses-client-api/SceneConfig.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineBezierFloat.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineBezierInt32.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineBezierVector2f.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineBezierVector2i.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineBezierVector3f.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineBezierVector3i.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineBezierVector4f.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineBezierVector4i.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineLinearFloat.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineLinearInt32.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineLinearVector2f.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineLinearVector2i.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineLinearVector3f.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineLinearVector3i.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineLinearVector4f.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineLinearVector4i.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineStepBool.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineStepFloat.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineStepInt32.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineStepVector2f.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineStepVector2i.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineStepVector3f.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineStepVector3i.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineStepVector4f.cpp delete mode 100644 client/ramses-client/ramses-client-api/SplineStepVector4i.cpp delete mode 100644 client/ramses-client/ramses-client-api/StreamTexture.cpp delete mode 100644 client/ramses-client/ramses-client-api/Texture2DBuffer.cpp delete mode 100644 client/ramses-client/ramses-client-api/UniformInput.cpp delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/AnimatedProperty.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/Animation.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationObject.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSequence.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystem.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemEnums.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemObjectIterator.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemRealTime.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationTypes.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/Appearance.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/AppearanceEnums.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/AttributeInput.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataFloat.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataInt32.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix22f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix33f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix44f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataObject.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector2f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector2i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector3f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector3i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector4f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector4i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/EDataType.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/ERotationConvention.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/EffectInputDataType.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/Node.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/RamsesObjectTypes.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/ResourceDataPool.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/Spline.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierFloat.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierInt32.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector2f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector2i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector3f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector3i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector4f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector4i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearFloat.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearInt32.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector2f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector2i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector3f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector3i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector4f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector4i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepBool.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepFloat.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepInt32.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector2f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector2i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector3f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector3i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector4f.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector4i.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-client-api/StreamTexture.h delete mode 100644 client/ramses-client/ramses-client-api/include/ramses-hmi-utils.h delete mode 100644 client/ramses-client/test/AnimatedPropertyTest.cpp delete mode 100644 client/ramses-client/test/AnimationSequenceTest.cpp delete mode 100644 client/ramses-client/test/AnimationSystemObjectIteratorTest.cpp delete mode 100644 client/ramses-client/test/AnimationSystemTest.cpp delete mode 100644 client/ramses-client/test/AnimationTest.cpp delete mode 100644 client/ramses-client/test/CreationHelper.cpp delete mode 100644 client/ramses-client/test/CreationHelper.h delete mode 100644 client/ramses-client/test/DataBufferTest.cpp delete mode 100644 client/ramses-client/test/DataObjectTest.cpp delete mode 100644 client/ramses-client/test/EDataTypeTest.cpp delete mode 100644 client/ramses-client/test/EffectInputTest.cpp delete mode 100644 client/ramses-client/test/NodeTransformationTest.cpp delete mode 100644 client/ramses-client/test/RamsesAnimationObjectOwnershipTest.cpp delete mode 100644 client/ramses-client/test/RamsesHmiUtilsTest.cpp delete mode 100644 client/ramses-client/test/RamsesObjectRegistryTest.cpp delete mode 100644 client/ramses-client/test/RamsesObjectTest1.cpp delete mode 100644 client/ramses-client/test/RamsesObjectTest2.cpp delete mode 100644 client/ramses-client/test/RamsesObjectTestCommon.h delete mode 100644 client/ramses-client/test/RamsesObjectTestTypes.h delete mode 100644 client/ramses-client/test/RenderTargetDescriptionTest.cpp delete mode 100644 client/ramses-client/test/ResourceDataPoolPersistationTest.cpp delete mode 100644 client/ramses-client/test/ResourceDataPoolPersistationTest.h delete mode 100644 client/ramses-client/test/ResourceDataPoolTest.cpp delete mode 100644 client/ramses-client/test/SplineTest.cpp delete mode 100644 client/ramses-client/test/SplineTestMacros.h delete mode 100644 client/ramses-client/test/StreamTextureTest.cpp rename client/{ramses-client => }/ramses-text-api/FontRegistry.cpp (96%) rename client/{ramses-client => }/ramses-text-api/TextCache.cpp (100%) rename client/{ramses-client => }/ramses-text-api/UtfUtils.cpp (98%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/FontCascade.h (99%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/FontInstanceId.h (93%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/FontInstanceOffsets.h (98%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/FontRegistry.h (94%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/Glyph.h (99%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/GlyphMetrics.h (98%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/IFontAccessor.h (91%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/IFontInstance.h (90%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/LayoutUtils.h (97%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/TextCache.h (94%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/TextLine.h (87%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text-api/UtfUtils.h (98%) rename client/{ramses-client => }/ramses-text-api/include/ramses-text.h (90%) rename client/{ramses-client => }/test/AppearanceTest.cpp (61%) create mode 100644 client/test/ArrayBufferTest.cpp rename client/{ramses-client => }/test/BlitPassTest.cpp (75%) rename client/{ramses-client => }/test/CameraTest.cpp (75%) rename client/{ramses-client => }/test/ClientApplicationLogicTest.cpp (90%) rename client/{ramses-client => }/test/ClientCommands/SceneCommandBufferTest.cpp (86%) rename client/{ramses-client => }/test/ClientEventHandlerMock.cpp (100%) rename client/{ramses-client => }/test/ClientEventHandlerMock.h (83%) rename client/{ramses-client => }/test/ClientTestUtils.h (74%) create mode 100644 client/test/CreationHelper.cpp create mode 100644 client/test/CreationHelper.h create mode 100644 client/test/DataObjectTest.cpp rename client/{ramses-client => }/test/EffectDescriptionTest.cpp (71%) create mode 100644 client/test/EffectInputTest.cpp rename client/{ramses-client => }/test/EffectTest.cpp (77%) rename client/{ramses-client => }/test/GeometryBindingTest.cpp (84%) rename client/{ramses-client => }/test/GlslEffectTest.cpp (95%) rename client/{ramses-client => }/test/GlslLimitsTest.cpp (74%) rename client/{ramses-client => }/test/IteratorTest.cpp (100%) rename client/{ramses-client => }/test/MeshNodeTest.cpp (94%) rename client/{ramses-client => }/test/MockActionCollector.h (82%) rename client/{ramses-client => }/test/NodeLazyTransformTest.cpp (63%) rename client/{ramses-client => }/test/NodeTest.cpp (60%) create mode 100644 client/test/NodeTransformationTest.cpp rename client/{ramses-client => }/test/NodeVisibilityTest.cpp (80%) rename client/{ramses-client => }/test/PickableObjectTest.cpp (90%) rename client/{ramses-client => }/test/QuadTest.cpp (100%) rename client/{ramses-client => }/test/RamsesClientTest.cpp (83%) rename client/{ramses-client => }/test/RamsesObjectOwnershipTest.cpp (77%) create mode 100644 client/test/RamsesObjectRegistryTest.cpp create mode 100644 client/test/RamsesObjectTest.cpp create mode 100644 client/test/RamsesObjectTestTypes.h rename client/{ramses-client => }/test/RamsesUtilsTest.cpp (93%) rename client/{ramses-client => }/test/RamsesVersionTest.cpp (60%) rename client/{ramses-client => }/test/RenderBufferTest.cpp (65%) rename client/{ramses-client => }/test/RenderGroupMeshIteratorTest.cpp (100%) rename client/{ramses-client => }/test/RenderGroupTest.cpp (93%) rename client/{ramses-client => }/test/RenderPassGroupIteratorTest.cpp (100%) rename client/{ramses-client => }/test/RenderPassTest.cpp (90%) create mode 100644 client/test/RenderTargetDescriptionTest.cpp rename client/{ramses-client => }/test/RenderTargetTest.cpp (87%) rename client/{ramses-client => }/test/ResourceTest.cpp (78%) rename client/{ramses-client => }/test/SceneDistributionTest.cpp (86%) rename client/{ramses-client => }/test/SceneFactoryTest.cpp (87%) rename client/{ramses-client => }/test/SceneGraphIteratorTest.cpp (89%) rename client/{ramses-client => }/test/SceneIteratorTest.cpp (100%) rename client/{ramses-client => }/test/SceneObjectIteratorTest.cpp (84%) rename client/{ramses-client => }/test/ScenePersistationTest.cpp (57%) rename client/{ramses-client => }/test/ScenePersistationTest.h (70%) rename client/{ramses-client => }/test/ScenePersistationThreadedTest.cpp (66%) rename client/{ramses-client => }/test/SceneReferenceTest.cpp (90%) rename client/{ramses-client => }/test/SceneTest.cpp (71%) rename client/{ramses-client => }/test/SerializationHelperTest.cpp (100%) rename client/{ramses-client => }/test/SimpleSceneTopology.h (85%) rename client/{ramses-client => }/test/TestEffectCreator.h (97%) rename client/{ramses-client => }/test/TestEffects.h (59%) rename client/{ramses-client => }/test/Texture2DBufferTest.cpp (92%) rename client/{ramses-client => }/test/TextureSamplerTest.cpp (57%) rename client/{ramses-client => }/test/res/ramses-client-test_minimalShader.frag (100%) rename client/{ramses-client => }/test/res/ramses-client-test_minimalShader.geom (100%) rename client/{ramses-client => }/test/res/ramses-client-test_minimalShader.vert (100%) rename client/{ramses-client => }/test/res/ramses-client-test_shader.vert (100%) rename client/{ramses-client => }/test/res/ramses-text-DroidKufi-Regular.ttf (100%) rename client/{ramses-client => }/test/res/ramses-text-Roboto-Bold.ttf (100%) rename client/{ramses-client => }/test/res/ramses-text-Roboto-Regular.ttf (100%) rename client/{ramses-client => }/test/res/rgba8_expectedFlipped.png (100%) rename client/{ramses-client => }/test/res/sampleTexture.png (100%) rename client/{ramses-client => }/test/res/sampleTexture_invalid.png (100%) rename client/{ramses-client/test => test/text}/FontCascadeTest.cpp (98%) rename client/{ramses-client/test => test/text}/FontRegistryTest.cpp (100%) rename client/{ramses-client/test => test/text}/Freetype2FontInstanceTest.cpp (100%) rename client/{ramses-client/test => test/text}/GlyphTextureAtlasTest.cpp (87%) rename client/{ramses-client/test => test/text}/GlyphTexturePageTest.cpp (97%) rename client/{ramses-client/test => test/text}/HarfbuzzFontInstanceTest.cpp (100%) rename client/{ramses-client/test => test/text}/LayoutUtilsHMITest.cpp (96%) rename client/{ramses-client/test => test/text}/LayoutUtilsTest.cpp (100%) rename client/{ramses-client/test => test/text}/TextCacheTest.cpp (99%) rename client/{ramses-client/test => test/text}/UtfUtilsTest.cpp (100%) create mode 100644 cmake/modules/FindSphinx.cmake create mode 100644 cmake/ramses/addSubdirectory.cmake create mode 100644 cmake/ramses/createPackage.cmake create mode 100644 cmake/ramses/createTarget.cmake create mode 100644 cmake/ramses/folderize.cmake create mode 100644 cmake/ramses/makeTestFromTarget.cmake delete mode 100644 cmake/ramses/rendererModulePerConfig.cmake create mode 100644 cmake/ramses/resourceCopy.cmake create mode 100644 cmake/templates/build-config.h.in delete mode 100644 cmake/templates/ramses-shared-lib-client-onlyTemplate.cmake.in create mode 100644 cmake/templates/ramses-shared-lib-headlessTemplate.cmake.in delete mode 100644 cmake/templates/ramses-version.in delete mode 100644 demo/ramses-dcsm-list/CMakeLists.txt delete mode 100644 demo/ramses-dcsm-list/res/ramses-dcsm-list-line.png delete mode 100644 demo/ramses-dcsm-list/src/main.cpp delete mode 100644 demo/ramses-dcsm-scene-references-demo/res/ramses-demo-scene-references-roboto-regular.ttf delete mode 100644 demo/ramses-dcsm-scene-references-demo/src/OverlayClient.cpp delete mode 100644 demo/ramses-dcsm-scene-references-demo/src/OverlayClient.h delete mode 100644 demo/ramses-dcsm-scene-references-demo/src/main.cpp delete mode 100644 demo/ramses-text-layout-demo/CMakeLists.txt delete mode 100644 demo/ramses-text-layout-demo/res/ramses-layout-demo-colored-quad.vert delete mode 100644 demo/ramses-text-layout-demo/res/ramses-layout-demo-red-text.vert delete mode 100644 demo/ramses-text-layout-demo/res/ramses-text-layout-demo-Roboto-Regular.ttf delete mode 100644 demo/ramses-text-layout-demo/res/ramses-text-layout-demo-WenQuanYiMicroHei.ttf delete mode 100644 demo/ramses-text-layout-demo/src/TextLayoutDemo.cpp delete mode 100644 demo/ramses-text-layout-demo/src/TextLayoutDemo.h delete mode 100644 demo/ramses-text-shadow-demo/CMakeLists.txt delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-Roboto-Bold.ttf delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-a-texturing.frag delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-a-texturing.vert delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-background.png delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-gauss-filter-h.frag delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-gauss-filter-h.vert delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-gauss-filter-v.frag delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-gauss-filter-v.vert delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-rgba-texturing.frag delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-rgba-texturing.vert delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow.frag delete mode 100644 demo/ramses-text-shadow-demo/res/ramses-text-shadow.vert delete mode 100644 demo/ramses-text-shadow-demo/src/GaussFilter.cpp delete mode 100644 demo/ramses-text-shadow-demo/src/GaussFilter.h delete mode 100644 demo/ramses-text-shadow-demo/src/GraphicalItem.cpp delete mode 100644 demo/ramses-text-shadow-demo/src/GraphicalItem.h delete mode 100644 demo/ramses-text-shadow-demo/src/ImageBox.cpp delete mode 100644 demo/ramses-text-shadow-demo/src/ImageBox.h delete mode 100644 demo/ramses-text-shadow-demo/src/TextBox.cpp delete mode 100644 demo/ramses-text-shadow-demo/src/TextBox.h delete mode 100644 demo/ramses-text-shadow-demo/src/TextBoxWithShadow.cpp delete mode 100644 demo/ramses-text-shadow-demo/src/TextBoxWithShadow.h delete mode 100644 demo/ramses-text-shadow-demo/src/main.cpp delete mode 100644 doc/developer/images/initiator_with_old.dot delete mode 100644 doc/doxygen/DoxygenLayout.xml delete mode 100644 doc/general/00_MainPage.dox delete mode 100644 doc/general/10_Client_API.dox delete mode 100644 doc/general/11_Validation.dox delete mode 100644 doc/general/12_Resources.dox delete mode 100644 doc/general/13_Effects.dox delete mode 100644 doc/general/15_Text_Rendering.dox delete mode 100644 doc/general/20_Renderer_API.dox delete mode 100644 doc/general/30_Offscreen_Buffers.dox delete mode 100644 doc/general/60_DCSM_Protocol.dox delete mode 100644 doc/general/80_EmbeddedCompositing.dox delete mode 100644 doc/general/images/ramses_logo_with_alpha2.png delete mode 100644 doc/general/images/sequence1a.png delete mode 100644 doc/general/images/sequence1b.png delete mode 100644 doc/general/images/sequence2.png delete mode 100644 doc/main.dox rename doc/{ => old_ramses}/developer/00_MainPage.dox (97%) rename doc/{ => old_ramses}/developer/10_GettingStarted.dox (98%) rename doc/{ => old_ramses}/developer/20_Contributing.dox (100%) rename doc/{ => old_ramses}/developer/30_CodeStyleGuide.dox (100%) rename doc/{ => old_ramses}/developer/40_ClientAPI.dox (98%) rename doc/{ => old_ramses}/developer/50_Testing.dox (100%) rename doc/{ => old_ramses}/developer/images/guide-which-tests.png (100%) rename doc/{ => old_ramses}/developer/images/initiator.dot (100%) rename doc/{ => old_ramses}/developer/images/ramses_logo_with_alpha2.png (100%) rename doc/{ => old_ramses}/developer/images/responder.dot (100%) rename doc/{ => old_ramses}/developer/images/responder_with_old.dot (100%) rename doc/{ => old_ramses}/general/40_Examples.dox (88%) rename doc/{ => old_ramses}/general/50_ContentExpiration.dox (100%) rename doc/{ => old_ramses}/general/70_SceneReferencing.dox (63%) rename doc/{ => old_ramses}/tools/00_MainPage.dox (77%) rename doc/{ => old_ramses}/tools/dlt-logging/10_GeneralIntroduction.dox (100%) rename doc/{ => old_ramses}/tools/dlt-logging/20_UsingDLT.dox (100%) rename doc/{ => old_ramses}/tools/dlt-logging/images/dltviewer_connect.png (100%) rename doc/{ => old_ramses}/tools/dlt-logging/images/dltviewer_connect_tcp.png (100%) rename doc/{ => old_ramses}/tools/dlt-logging/images/dltviewer_context_inject.png (100%) rename doc/{ => old_ramses}/tools/dlt-logging/images/dltviewer_contexts.png (100%) rename doc/{ => old_ramses}/tools/dlt-logging/images/dltviewer_injection.png (100%) rename doc/{ => old_ramses}/tools/dlt-logging/images/dltviewer_messages.png (100%) rename doc/{ => old_ramses}/tools/dlt-logging/images/dltviewer_plugin.png (100%) rename doc/{ => old_ramses}/tools/profiling/10_Profiling.dox (98%) rename doc/{ => old_ramses}/tools/scene-viewer/10_SceneViewer.dox (100%) rename doc/{doxygen/ramses_small.png => sphinx/_static/logo.png} (100%) create mode 100644 doc/sphinx/build.rst create mode 100644 doc/sphinx/changelog_ref.md create mode 100644 doc/sphinx/classes/core.rst create mode 100755 doc/sphinx/classes/generate_classes.py create mode 100644 doc/sphinx/classes/index.rst create mode 100644 doc/sphinx/classes/logic.rst create mode 100644 doc/sphinx/classes/renderer.rst create mode 100644 doc/sphinx/classes/text.rst create mode 100644 doc/sphinx/classes/utils.rst create mode 100644 doc/sphinx/conf.py create mode 100644 doc/sphinx/core.rst create mode 100644 doc/sphinx/dev.rst create mode 100644 doc/sphinx/examples/core/00_minimal.rst create mode 100644 doc/sphinx/examples/logic/00_minimal.rst create mode 100644 doc/sphinx/examples/logic/01a_primitive_properties.rst create mode 100644 doc/sphinx/examples/logic/01b_struct_properties.rst create mode 100644 doc/sphinx/examples/logic/01c_array_properties.rst create mode 100644 doc/sphinx/examples/logic/02_errors_compile_time.rst create mode 100644 doc/sphinx/examples/logic/03_errors_runtime.rst create mode 100644 doc/sphinx/examples/logic/04_ramses_scene.rst create mode 100644 doc/sphinx/examples/logic/05_serialization.rst create mode 100644 doc/sphinx/examples/logic/07_links.rst create mode 100644 doc/sphinx/examples/logic/08a_static_animation.rst create mode 100644 doc/sphinx/examples/logic/08b_dynamic_animation.rst create mode 100644 doc/sphinx/examples/logic/09_modules.rst create mode 100644 doc/sphinx/examples/logic/10_globals.rst create mode 100644 doc/sphinx/examples/logic/11_interfaces.rst create mode 100644 doc/sphinx/examples/logic/12_anchor_point.rst create mode 100644 doc/sphinx/examples/logic/13_render_order.rst create mode 100644 doc/sphinx/examples/logic/14_skinbinding.rst create mode 100644 doc/sphinx/examples/logic/15_meshnodebinding.rst create mode 100644 doc/sphinx/index.rst create mode 100644 doc/sphinx/logic.rst create mode 100644 doc/sphinx/lua_syntax.rst create mode 100644 doc/sphinx/readme_ref.md create mode 100644 doc/sphinx/requirements.txt create mode 100644 doc/sphinx/res/architecture.png rename doc/{general/images => sphinx/res}/glyph_atlas.png (100%) rename doc/{general/images => sphinx/res}/glyph_metrics.png (100%) rename doc/{general/images => sphinx/res}/glyph_zoomed.png (100%) create mode 100644 doc/sphinx/res/overview.svg rename doc/{general/images => sphinx/res}/ramses_api_text_geometry.png (100%) rename doc/{general/images => sphinx/res}/renderer_scene_states.png (100%) create mode 100644 doc/sphinx/viewer.rst delete mode 100644 doc/tools/resource-tools/10_EffectLifecycle.dox delete mode 100644 doc/tools/resource-tools/20_GLSLToBinaryShader.dox delete mode 100644 doc/tools/resource-tools/30_GLSLToEffectResource.dox delete mode 100644 doc/tools/resource-tools/40_ResourcePacker.dox delete mode 100644 doc/tools/resource-tools/50_ExampleUsageAllTools.dox delete mode 100644 doc/tools/resource-tools/images/ShaderTools.png create mode 100644 examples/logic/00_minimal/main.cpp create mode 100644 examples/logic/01a_primitive_properties/main.cpp create mode 100644 examples/logic/01b_struct_properties/main.cpp create mode 100644 examples/logic/01c_array_properties/main.cpp create mode 100644 examples/logic/02_errors_compile_time/main.cpp create mode 100644 examples/logic/03_errors_runtime/main.cpp create mode 100644 examples/logic/04_ramses_scene/main.cpp create mode 100644 examples/logic/05_serialization/main.cpp create mode 100644 examples/logic/07_links/main.cpp create mode 100644 examples/logic/08a_static_animation/main.cpp create mode 100644 examples/logic/08b_dynamic_animation/main.cpp create mode 100644 examples/logic/09_modules/main.cpp create mode 100644 examples/logic/10_globals/main.cpp create mode 100644 examples/logic/11_interfaces/main.cpp create mode 100644 examples/logic/12_anchor_point/main.cpp create mode 100644 examples/logic/13_render_order/main.cpp create mode 100644 examples/logic/14_skinbinding/main.cpp create mode 100644 examples/logic/15_meshnodebinding/main.cpp create mode 100644 examples/logic/CMakeLists.txt create mode 100644 examples/logic/shared/SimpleRenderer.h delete mode 100644 examples/ramses-example-basic-animation-realtime/CMakeLists.txt delete mode 100644 examples/ramses-example-basic-animation-realtime/src/main.cpp delete mode 100644 examples/ramses-example-basic-animation/CMakeLists.txt delete mode 100644 examples/ramses-example-basic-animation/src/main.cpp delete mode 100644 examples/ramses-example-basic-compositing/CMakeLists.txt delete mode 100644 examples/ramses-example-basic-compositing/src/main.cpp delete mode 100644 examples/ramses-example-dcsm-provider/CMakeLists.txt delete mode 100644 examples/ramses-example-dcsm-provider/res/ramses-example-dcsm-provider-outline.frag delete mode 100644 examples/ramses-example-dcsm-provider/res/ramses-example-dcsm-provider-outline.vert delete mode 100644 examples/ramses-example-dcsm-provider/res/ramses-example-dcsm-provider-texture.png delete mode 100644 examples/ramses-example-dcsm-provider/res/ramses-example-dcsm-provider.frag delete mode 100644 examples/ramses-example-dcsm-provider/res/ramses-example-dcsm-provider.vert delete mode 100644 examples/ramses-example-dcsm-provider/src/main.cpp delete mode 100644 examples/ramses-example-local-client-dcsm/src/main.cpp rename examples/{ramses-example-local-client-dcsm => ramses-example-local-compositing}/CMakeLists.txt (71%) rename examples/{ramses-example-basic-compositing/res/ramses-example-basic-compositing-texture.png => ramses-example-local-compositing/res/ramses-example-local-compositing-texture.png} (100%) rename examples/{ramses-example-basic-compositing/res/ramses-example-basic-compositing.frag => ramses-example-local-compositing/res/ramses-example-local-compositing.frag} (100%) rename examples/{ramses-example-basic-compositing/res/ramses-example-basic-compositing.vert => ramses-example-local-compositing/res/ramses-example-local-compositing.vert} (100%) create mode 100644 examples/ramses-example-local-compositing/src/main.cpp delete mode 100644 external/acme2/LICENSE.txt delete mode 100644 external/acme2/acme2.cmake delete mode 100644 external/acme2/internal/api.cmake delete mode 100644 external/acme2/internal/build-config.h.in delete mode 100644 external/acme2/internal/create_build_config.cmake delete mode 100644 external/acme2/internal/create_package.cmake delete mode 100644 external/acme2/internal/module_template.cmake delete mode 100644 external/acme2/internal/tools.cmake delete mode 100644 external/acme2/tests/CMakeLists.txt delete mode 100644 external/acme2/tests/shared_resources/shared_resource.txt delete mode 100644 external/acme2/tests/test1/CMakeLists.txt delete mode 100644 external/acme2/tests/test1/test1.cpp delete mode 100644 external/acme2/tests/test10/CMakeLists.txt delete mode 100644 external/acme2/tests/test10/test10.cpp delete mode 100644 external/acme2/tests/test11/CMakeLists.txt delete mode 100644 external/acme2/tests/test11/test11.cpp delete mode 100644 external/acme2/tests/test2/CMakeLists.txt delete mode 100644 external/acme2/tests/test2/test2.cpp delete mode 100644 external/acme2/tests/test2/test2_executable_include/executableInclude.h delete mode 100644 external/acme2/tests/test2/test2_include/staticlibraryInclude.h delete mode 100644 external/acme2/tests/test2/test2_staticLibrary.cpp delete mode 100644 external/acme2/tests/test3/CMakeLists.txt delete mode 100644 external/acme2/tests/test3/FindCustomLibrary.cmake delete mode 100644 external/acme2/tests/test3/findscriptIncludePath/staticlibraryInclude.h delete mode 100644 external/acme2/tests/test3/test3.cpp delete mode 100644 external/acme2/tests/test8/CMakeLists.txt delete mode 100644 external/acme2/tests/test8/test8_executable/include/inc.h delete mode 100644 external/acme2/tests/test8/test8_executable/test8_exe.cpp delete mode 100644 external/acme2/tests/test8/test8_staticA/include/inc.h delete mode 100644 external/acme2/tests/test8/test8_staticA/test8_staticA.cpp delete mode 100644 external/acme2/tests/test8/test8_staticB/include/inc.h delete mode 100644 external/acme2/tests/test8/test8_staticB/test8_staticB.cpp create mode 100644 external/wayland-ivi-example-client/simple-dmabuf-egl.c delete mode 100644 framework/Animation/Animation/include/Animation/ActionCollectingAnimationSystem.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimatableTypeTraits.h delete mode 100644 framework/Animation/Animation/include/Animation/Animation.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationCollectionTypes.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationCommon.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationData.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationDataBind.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationDataListener.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationDataNotifier.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationInstance.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationLogic.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationLogicListener.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationLogicNotifier.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationProcessData.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationProcessDataCache.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationProcessDataDispatch.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationProcessing.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationProcessingFinished.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationStateChangeCollector.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationSystem.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationSystemDescriber.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationSystemFactory.h delete mode 100644 framework/Animation/Animation/include/Animation/AnimationTime.h delete mode 100644 framework/Animation/Animation/include/Animation/Interpolator.h delete mode 100644 framework/Animation/Animation/include/Animation/Spline.h delete mode 100644 framework/Animation/Animation/include/Animation/SplineBase.h delete mode 100644 framework/Animation/Animation/include/Animation/SplineIterator.h delete mode 100644 framework/Animation/Animation/include/Animation/SplineKey.h delete mode 100644 framework/Animation/Animation/include/Animation/SplineKeyTangents.h delete mode 100644 framework/Animation/Animation/include/Animation/SplineSegment.h delete mode 100644 framework/Animation/Animation/include/Animation/SplineSolver.h delete mode 100644 framework/Animation/Animation/include/Animation/SplineSolverHelper.h delete mode 100644 framework/Animation/Animation/src/ActionCollectingAnimationSystem.cpp delete mode 100644 framework/Animation/Animation/src/Animation.cpp delete mode 100644 framework/Animation/Animation/src/AnimationData.cpp delete mode 100644 framework/Animation/Animation/src/AnimationDataNotifier.cpp delete mode 100644 framework/Animation/Animation/src/AnimationInstance.cpp delete mode 100644 framework/Animation/Animation/src/AnimationLogic.cpp delete mode 100644 framework/Animation/Animation/src/AnimationLogicNotifier.cpp delete mode 100644 framework/Animation/Animation/src/AnimationProcessDataDispatch.cpp delete mode 100644 framework/Animation/Animation/src/AnimationProcessing.cpp delete mode 100644 framework/Animation/Animation/src/AnimationProcessingFinished.cpp delete mode 100644 framework/Animation/Animation/src/AnimationStateChangeCollector.cpp delete mode 100644 framework/Animation/Animation/src/AnimationSystem.cpp delete mode 100644 framework/Animation/Animation/src/AnimationSystemDescriber.cpp delete mode 100644 framework/Animation/Animation/src/AnimationSystemFactory.cpp delete mode 100644 framework/Animation/Animation/src/SplineIterator.cpp delete mode 100644 framework/Animation/Animation/test/ActionCollectingAnimationSystemTest.cpp delete mode 100644 framework/Animation/Animation/test/AnimationDataBindTest.cpp delete mode 100644 framework/Animation/Animation/test/AnimationDataBindTestUtils.h delete mode 100644 framework/Animation/Animation/test/AnimationDataTest.cpp delete mode 100644 framework/Animation/Animation/test/AnimationDataTest.h delete mode 100644 framework/Animation/Animation/test/AnimationInstanceTest.cpp delete mode 100644 framework/Animation/Animation/test/AnimationLogicTest.cpp delete mode 100644 framework/Animation/Animation/test/AnimationLogicTest.h delete mode 100644 framework/Animation/Animation/test/AnimationProcessingTest.cpp delete mode 100644 framework/Animation/Animation/test/AnimationProcessingTest.h delete mode 100644 framework/Animation/Animation/test/AnimationStateChangeCollectorTest.cpp delete mode 100644 framework/Animation/Animation/test/AnimationSyncTest.cpp delete mode 100644 framework/Animation/Animation/test/AnimationSyncTest.h delete mode 100644 framework/Animation/Animation/test/AnimationSystemDescriberTest.cpp delete mode 100644 framework/Animation/Animation/test/AnimationSystemSyncTest.cpp delete mode 100644 framework/Animation/Animation/test/AnimationSystemSyncTest.h delete mode 100644 framework/Animation/Animation/test/AnimationSystemTest.cpp delete mode 100644 framework/Animation/Animation/test/AnimationTestTypes.h delete mode 100644 framework/Animation/Animation/test/AnimationTestUtils.cpp delete mode 100644 framework/Animation/Animation/test/AnimationTestUtils.h delete mode 100644 framework/Animation/Animation/test/AnimationTimeTest.cpp delete mode 100644 framework/Animation/Animation/test/InterpolatorTest.cpp delete mode 100644 framework/Animation/Animation/test/SplineIteratorTest.cpp delete mode 100644 framework/Animation/Animation/test/SplineIteratorTest.h delete mode 100644 framework/Animation/Animation/test/SplineSolverTest.cpp delete mode 100644 framework/Animation/Animation/test/SplineSolverTest.h delete mode 100644 framework/Animation/Animation/test/SplineTest.cpp delete mode 100644 framework/Animation/Animation/test/SplineTestUtils.h delete mode 100644 framework/Animation/AnimationAPI/include/AnimationAPI/AnimationSystemSizeInformation.h delete mode 100644 framework/Animation/AnimationAPI/include/AnimationAPI/IAnimationSystem.h delete mode 100644 framework/Communication/TransportCommon/include/TransportCommon/ConnectionSystemBase.h delete mode 100644 framework/Communication/TransportCommon/include/TransportCommon/ConnectionSystemInitiatorResponder.h delete mode 100644 framework/Communication/TransportCommon/include/TransportCommon/DcsmConnectionSystem.h delete mode 100644 framework/Communication/TransportCommon/include/TransportCommon/ISomeIPDcsmStack.h delete mode 100644 framework/Communication/TransportCommon/include/TransportCommon/ISomeIPRamsesStack.h delete mode 100644 framework/Communication/TransportCommon/include/TransportCommon/RamsesConnectionSystem.h delete mode 100644 framework/Communication/TransportCommon/include/TransportCommon/SomeIPAdapter.h delete mode 100644 framework/Communication/TransportCommon/include/TransportCommon/SomeIPConnectionSystemMultiplexer.h delete mode 100644 framework/Communication/TransportCommon/include/TransportCommon/SomeIPStackCommon.h delete mode 100644 framework/Communication/TransportCommon/src/DcsmConnectionSystem.cpp delete mode 100644 framework/Communication/TransportCommon/src/RamsesConnectionSystem.cpp delete mode 100644 framework/Communication/TransportCommon/src/SomeIPConnectionSystemMultiplexer.cpp delete mode 100644 framework/Communication/TransportCommon/test/ConcreteConnectionSystemCommonTest.h delete mode 100644 framework/Communication/TransportCommon/test/ConnectionSystemBaseStressTest.cpp delete mode 100644 framework/Communication/TransportCommon/test/ConnectionSystemBaseTest.cpp delete mode 100644 framework/Communication/TransportCommon/test/ConnectionSystemBaseThreadedStressTest.cpp delete mode 100644 framework/Communication/TransportCommon/test/ConnectionSystemInitiatorResponderTest.cpp delete mode 100644 framework/Communication/TransportCommon/test/ConnectionSystemTestCommon.h delete mode 100644 framework/Communication/TransportCommon/test/DcsmConnectionSystemTest.cpp delete mode 100644 framework/Communication/TransportCommon/test/RamsesConnectionSystemTest.cpp delete mode 100644 framework/Communication/TransportCommon/test/SomeIPConnectionSystemMultiplexerTest.cpp delete mode 100644 framework/Communication/TransportCommon/test/SomeIPStackCommonTest.cpp delete mode 100644 framework/Communication/TransportCommon/test/SomeIPStackMocks.cpp delete mode 100644 framework/Communication/TransportCommon/test/SomeIPStackMocks.h delete mode 100644 framework/Components/include/Components/CategoryInfo.h delete mode 100644 framework/Components/include/Components/DcsmComponent.h delete mode 100644 framework/Components/include/Components/DcsmMetadata.h delete mode 100644 framework/Components/include/Components/DcsmTypes.h delete mode 100644 framework/Components/include/Components/IDcsmComponent.h delete mode 100644 framework/Components/include/Components/IDcsmProviderEventHandler.h delete mode 100644 framework/Components/src/CategoryInfo.cpp delete mode 100644 framework/Components/src/DcsmComponent.cpp delete mode 100644 framework/Components/src/DcsmMetadata.cpp delete mode 100644 framework/Components/src/LogDcsmInfo.cpp delete mode 100644 framework/Components/test/CategoryInfoTest.cpp delete mode 100644 framework/Components/test/DcsmComponentTest.cpp delete mode 100644 framework/Components/test/DcsmComponentThreadTest.cpp delete mode 100644 framework/Components/test/DcsmMetadataTest.cpp delete mode 100644 framework/Components/test/DcsmSenderAndReceiverTest.cpp delete mode 100644 framework/Core/Math3d/include/Math3d/Matrix22f.h delete mode 100644 framework/Core/Math3d/include/Math3d/Matrix33f.h delete mode 100644 framework/Core/Math3d/include/Math3d/Matrix44f.h rename framework/Core/Math3d/{src/Vector3.cpp => include/Math3d/Rotation.h} (69%) delete mode 100644 framework/Core/Math3d/include/Math3d/Vector2.h delete mode 100644 framework/Core/Math3d/include/Math3d/Vector2i.h delete mode 100644 framework/Core/Math3d/include/Math3d/Vector3.h delete mode 100644 framework/Core/Math3d/include/Math3d/Vector3i.h delete mode 100644 framework/Core/Math3d/include/Math3d/Vector4.h delete mode 100644 framework/Core/Math3d/include/Math3d/Vector4i.h delete mode 100644 framework/Core/Math3d/src/Matrix33f.cpp delete mode 100644 framework/Core/Math3d/src/Matrix44f.cpp create mode 100644 framework/Core/Math3d/src/Rotation.cpp delete mode 100644 framework/Core/Math3d/test/Matrix22fTest.cpp delete mode 100644 framework/Core/Math3d/test/Matrix33fTest.cpp delete mode 100644 framework/Core/Math3d/test/Matrix44Test.cpp create mode 100644 framework/Core/Math3d/test/RotationTest.cpp delete mode 100644 framework/Core/Math3d/test/Vector2Test.cpp delete mode 100644 framework/Core/Math3d/test/Vector2iTest.cpp delete mode 100644 framework/Core/Math3d/test/Vector3Test.cpp delete mode 100644 framework/Core/Math3d/test/Vector3iTest.cpp delete mode 100644 framework/Core/Math3d/test/Vector4Test.cpp delete mode 100644 framework/Core/Math3d/test/Vector4iTest.cpp delete mode 100644 framework/Core/Utils/include/Utils/Argument.h delete mode 100644 framework/Core/Utils/include/Utils/ArgumentBool.h delete mode 100644 framework/Core/Utils/include/Utils/CommandLineArgument.h delete mode 100644 framework/Core/Utils/include/Utils/CommandLineParser.h delete mode 100644 framework/Core/Utils/include/Utils/DataBind.h delete mode 100644 framework/Core/Utils/include/Utils/DataBindCommon.h delete mode 100644 framework/Core/Utils/include/Utils/DataBindTypes.h delete mode 100644 framework/Core/Utils/include/Utils/StringOutputSpecialWrapper.h delete mode 100644 framework/Core/Utils/src/CommandLineParser.cpp delete mode 100644 framework/Core/Utils/test/CommandLineParserTest.cpp delete mode 100644 framework/Core/Utils/test/DataBindTest.cpp delete mode 100644 framework/Core/Utils/test/DataBindTestUtils.h delete mode 100644 framework/FrameworkTestUtils/include/DcsmEventHandlerMocks.h delete mode 100644 framework/FrameworkTestUtils/include/DcsmGmockPrinter.h delete mode 100644 framework/FrameworkTestUtils/src/DcsmEventHandlerMocks.cpp delete mode 100644 framework/Monitoring/include/Monitoring/Monitor.h delete mode 100644 framework/Monitoring/src/Monitor.cpp delete mode 100644 framework/PlatformAbstraction/include/Collections/String.h delete mode 100644 framework/PlatformAbstraction/include/PlatformAbstraction/internal/GPTPHelper.h delete mode 100644 framework/PlatformAbstraction/test/StringTest.cpp delete mode 100644 framework/Ramsh/include/Ramsh/RamshCommandDcsmStatusMessage.h delete mode 100644 framework/Ramsh/src/RamshCommandDcsmStatusMessage.cpp delete mode 100644 framework/SceneGraph/Scene/include/Scene/SceneActionApplierHelper.h delete mode 100644 framework/SceneGraph/Scene/include/Scene/SceneDataBinding.h delete mode 100644 framework/SceneGraph/Scene/include/Scene/TransformPropertyType.h delete mode 100644 framework/SceneGraph/Scene/src/SceneDataBinding.cpp delete mode 100644 framework/SceneGraph/Scene/test/SceneTest_AnimationSystem.cpp delete mode 100644 framework/SceneGraph/Scene/test/SceneTest_DataBinding.cpp delete mode 100644 framework/SceneGraph/Scene/test/SceneTest_StreamTexture.cpp delete mode 100644 framework/SceneGraph/SceneAPI/include/SceneAPI/ERotationConvention.h create mode 100644 framework/SceneGraph/SceneAPI/include/SceneAPI/ERotationType.h delete mode 100644 framework/SceneGraph/SceneAPI/include/SceneAPI/StreamTexture.h delete mode 100644 framework/SceneGraph/SceneAPI/include/SceneAPI/WaylandIviSurfaceId.h create mode 100644 framework/ramses-framework-api/include/ramses-framework-api/AppearanceEnums.h delete mode 100644 framework/ramses-framework-api/include/ramses-framework-api/CarModelViewMetadata.h delete mode 100644 framework/ramses-framework-api/include/ramses-framework-api/CategoryInfoUpdate.h create mode 100644 framework/ramses-framework-api/include/ramses-framework-api/DataTypes.h delete mode 100644 framework/ramses-framework-api/include/ramses-framework-api/DcsmApiTypes.h delete mode 100644 framework/ramses-framework-api/include/ramses-framework-api/DcsmConsumer.h delete mode 100644 framework/ramses-framework-api/include/ramses-framework-api/DcsmMetadataCreator.h delete mode 100644 framework/ramses-framework-api/include/ramses-framework-api/DcsmMetadataUpdate.h delete mode 100644 framework/ramses-framework-api/include/ramses-framework-api/DcsmProvider.h delete mode 100644 framework/ramses-framework-api/include/ramses-framework-api/DcsmStatusMessage.h create mode 100644 framework/ramses-framework-api/include/ramses-framework-api/EDataType.h create mode 100644 framework/ramses-framework-api/include/ramses-framework-api/EFeatureLevel.h delete mode 100644 framework/ramses-framework-api/include/ramses-framework-api/IDcsmConsumerEventHandler.h delete mode 100644 framework/ramses-framework-api/include/ramses-framework-api/IDcsmProviderEventHandler.h create mode 100644 framework/ramses-framework/include/AppearanceEnumsImpl.h delete mode 100644 framework/ramses-framework/include/CategoryInfoUpdateImpl.h create mode 100644 framework/ramses-framework/include/DataTypeUtils.h create mode 100644 framework/ramses-framework/include/DataTypesImpl.h delete mode 100644 framework/ramses-framework/include/DcsmConsumerImpl.h delete mode 100644 framework/ramses-framework/include/DcsmMetadataCreatorImpl.h delete mode 100644 framework/ramses-framework/include/DcsmMetadataUpdateImpl.h delete mode 100644 framework/ramses-framework/include/DcsmProviderImpl.h delete mode 100644 framework/ramses-framework/include/DcsmStatusMessageImpl.h delete mode 100644 framework/ramses-framework/include/IDcsmConsumerImpl.h delete mode 100644 framework/ramses-framework/include/SOMEIPICConfig.h create mode 100644 framework/ramses-framework/src/AppearanceEnums.cpp delete mode 100644 framework/ramses-framework/src/CategoryInfoUpdate.cpp delete mode 100644 framework/ramses-framework/src/CategoryInfoUpdateImpl.cpp delete mode 100644 framework/ramses-framework/src/DcsmConsumer.cpp delete mode 100644 framework/ramses-framework/src/DcsmConsumerImpl.cpp delete mode 100644 framework/ramses-framework/src/DcsmMetadataCreator.cpp delete mode 100644 framework/ramses-framework/src/DcsmMetadataCreatorImpl.cpp delete mode 100644 framework/ramses-framework/src/DcsmMetadataUpdate.cpp delete mode 100644 framework/ramses-framework/src/DcsmMetadataUpdateImpl.cpp delete mode 100644 framework/ramses-framework/src/DcsmProvider.cpp delete mode 100644 framework/ramses-framework/src/DcsmProviderImpl.cpp delete mode 100644 framework/ramses-framework/src/DcsmStatusMessage.cpp delete mode 100644 framework/ramses-framework/src/DcsmStatusMessageImpl.cpp delete mode 100644 framework/ramses-framework/src/SOMEIPICConfig.cpp delete mode 100644 framework/ramses-framework/test/CategoryInfoUpdateTest.cpp create mode 100644 framework/ramses-framework/test/DataTypeTest.cpp delete mode 100644 framework/ramses-framework/test/DcsmApiTypesTest.cpp delete mode 100644 framework/ramses-framework/test/DcsmConsumerTest.cpp delete mode 100644 framework/ramses-framework/test/DcsmMetadataCreatorTest.cpp delete mode 100644 framework/ramses-framework/test/DcsmMetadataUpdateTest.cpp delete mode 100644 framework/ramses-framework/test/DcsmProviderTest.cpp delete mode 100644 framework/ramses-framework/test/DcsmStatusMessageTest.cpp delete mode 100644 framework/ramses-framework/test/DcsmSystemTests.cpp delete mode 100644 integration/SandwichTests/RendererTests/res/ARendererDisplays_Warped.PNG delete mode 100644 integration/SandwichTests/RendererTests/res/RenderTargetScene_Warping.PNG delete mode 100644 integration/SandwichTests/RendererTests/res/RenderTargetScene_Warping2.PNG delete mode 100644 integration/TestContent/include/TestScenes/AnimatedTrianglesScene.h rename integration/TestContent/include/TestScenes/{DataBufferScene.h => ArrayBufferScene.h} (78%) delete mode 100644 integration/TestContent/include/TestScenes/DcsmScene.h delete mode 100644 integration/TestContent/src/AnimatedTrianglesScene.cpp rename integration/TestContent/src/{DataBufferScene.cpp => ArrayBufferScene.cpp} (78%) delete mode 100644 integration/TestContent/src/DcsmScene.cpp create mode 100644 ramses-cli/CMakeLists.txt create mode 100644 ramses-cli/include/ramses-cli.h create mode 100644 ramses-cli/include/ramses-framework-cli.h create mode 100644 ramses-cli/test/ramses-cli-test.cpp create mode 100644 renderer/CMakeLists.txt delete mode 100644 renderer/Platform/Platform_Android/CMakeLists.txt delete mode 100644 renderer/Platform/Platform_Android/include/Platform_Android/Platform_Android_EGL_ES_3_0.h delete mode 100644 renderer/Platform/Platform_Android/src/Platform_Android_EGL_ES_3_0.cpp create mode 100644 renderer/Platform/Platform_Android_EGL/CMakeLists.txt rename renderer/Platform/{Platform_Android/include/Platform_Android => Platform_Android_EGL/include/Platform_Android_EGL}/Platform_Android_EGL.h (94%) rename renderer/Platform/{Platform_Android/include/Platform_Android => Platform_Android_EGL/include/Platform_Android_EGL}/Window_Android.h (92%) rename renderer/Platform/{Platform_Android => Platform_Android_EGL}/src/Platform_Android_EGL.cpp (95%) rename renderer/Platform/{Platform_Android => Platform_Android_EGL}/src/Window_Android.cpp (89%) delete mode 100644 renderer/Platform/Platform_Integrity_RGL/CMakeLists.txt delete mode 100644 renderer/Platform/Platform_Integrity_RGL/include/Platform_Integrity_RGL/Platform_Integrity_RGL_EGL_ES_3_0.h delete mode 100644 renderer/Platform/Platform_Integrity_RGL/include/Platform_Integrity_RGL/Window_Integrity_RGL.h delete mode 100644 renderer/Platform/Platform_Integrity_RGL/src/Platform_Integrity_RGL_EGL_ES_3_0.cpp delete mode 100644 renderer/Platform/Platform_Integrity_RGL/src/Window_Integrity_RGL.cpp delete mode 100644 renderer/Platform/Platform_Windows_WGL_4_2_core/CMakeLists.txt delete mode 100644 renderer/Platform/Platform_Windows_WGL_4_2_core/include/Platform_Windows_WGL_4_2_core/Platform_Windows_WGL_4_2_core.h delete mode 100644 renderer/Platform/Platform_Windows_WGL_4_2_core/src/Platform_Windows_WGL_4_2_core.cpp delete mode 100644 renderer/Platform/Platform_Windows_WGL_4_5/CMakeLists.txt delete mode 100644 renderer/Platform/Platform_Windows_WGL_4_5/include/Platform_Windows_WGL_4_5/Platform_Windows_WGL_4_5.h delete mode 100644 renderer/Platform/Platform_Windows_WGL_4_5/src/Platform_Windows_WGL_4_5.cpp delete mode 100644 renderer/Platform/Platform_Windows_WGL_ES_3_0/CMakeLists.txt delete mode 100644 renderer/Platform/Platform_Windows_WGL_ES_3_0/include/Platform_Windows_WGL_ES_3_0/Platform_Windows_WGL_ES_3_0.h delete mode 100644 renderer/Platform/Platform_Windows_WGL_ES_3_0/src/Platform_Windows_WGL_ES_3_0.cpp delete mode 100644 renderer/Platform/Platform_X11/CMakeLists.txt delete mode 100644 renderer/Platform/Platform_X11/include/Platform_X11/Platform_X11_EGL_ES_3_0.h delete mode 100644 renderer/Platform/Platform_X11/src/Platform_X11_EGL_ES_3_0.cpp create mode 100644 renderer/Platform/Platform_X11_EGL/CMakeLists.txt rename renderer/Platform/{Platform_X11/include/Platform_X11 => Platform_X11_EGL/include/Platform_X11_EGL}/Platform_X11_EGL.h (77%) rename renderer/Platform/{Platform_X11/include/Platform_X11 => Platform_X11_EGL/include/Platform_X11_EGL}/Window_X11.h (75%) rename renderer/Platform/{Platform_X11 => Platform_X11_EGL}/src/Platform_X11_EGL.cpp (89%) rename renderer/Platform/{Platform_X11 => Platform_X11_EGL}/src/Window_X11.cpp (94%) rename renderer/Platform/{Platform_X11 => Platform_X11_EGL}/test/Window_X11_Test.cpp (96%) create mode 100644 renderer/PlatformFactory/CMakeLists.txt rename framework/Animation/Animation/test/SplineTest.h => renderer/PlatformFactory/include/PlatformFactory/PlatformFactory.h (60%) create mode 100644 renderer/PlatformFactory/src/PlatformFactory.cpp rename framework/Core/Math3d/src/Matrix22f.cpp => renderer/RendererLib/RendererAPI/include/RendererAPI/EDeviceType.h (71%) delete mode 100644 renderer/RendererLib/RendererAPI/include/RendererAPI/EDeviceTypeId.h create mode 100644 renderer/RendererLib/RendererAPI/include/RendererAPI/EWindowType.h rename framework/Animation/Animation/test/AnimationTimeTest.h => renderer/RendererLib/RendererAPI/include/RendererAPI/IPlatformFactory.h (55%) delete mode 100644 renderer/RendererLib/RendererCommands/include/RendererCommands/ShowFrameProfiler.h delete mode 100644 renderer/RendererLib/RendererCommands/src/ShowFrameProfiler.cpp delete mode 100644 renderer/RendererLib/RendererLib/include/RendererLib/FrameProfileRenderer.h delete mode 100644 renderer/RendererLib/RendererLib/include/RendererLib/GpuMemorySample.h delete mode 100644 renderer/RendererLib/RendererLib/include/RendererLib/MemoryStatistics.h delete mode 100644 renderer/RendererLib/RendererLib/include/RendererLib/Postprocessing.h delete mode 100644 renderer/RendererLib/RendererLib/include/RendererLib/RendererConfigUtils.h delete mode 100644 renderer/RendererLib/RendererLib/include/RendererLib/WarpingMeshData.h delete mode 100644 renderer/RendererLib/RendererLib/include/RendererLib/WarpingPass.h delete mode 100644 renderer/RendererLib/RendererLib/src/FrameProfileRenderer.cpp delete mode 100644 renderer/RendererLib/RendererLib/src/GpuMemorySample.cpp delete mode 100644 renderer/RendererLib/RendererLib/src/MemoryStatistics.cpp delete mode 100644 renderer/RendererLib/RendererLib/src/Postprocessing.cpp delete mode 100644 renderer/RendererLib/RendererLib/src/RendererConfigUtils.cpp delete mode 100644 renderer/RendererLib/RendererLib/src/WarpingMeshData.cpp delete mode 100644 renderer/RendererLib/RendererLib/src/WarpingPass.cpp delete mode 100644 renderer/RendererLib/RendererLib/test/EDeviceTypeIdTest.cpp delete mode 100644 renderer/RendererLib/RendererLib/test/WarpingPassTest.cpp rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/ContextMock.cpp (100%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/ContextMock.h (89%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/DeviceMock.cpp (98%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/DeviceMock.h (65%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/DisplayControllerMock.cpp (100%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/DisplayControllerMock.h (77%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/EmbeddedCompositingManagerMock.cpp (100%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/EmbeddedCompositingManagerMock.h (100%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/EmbeddedCompositorMock.cpp (100%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/EmbeddedCompositorMock.h (65%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/MockResourceHash.h (84%) create mode 100644 renderer/RendererLib/RendererTestCommon/PlatformFactoryMock.h rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/PlatformMock.cpp (100%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/PlatformMock.h (100%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/Platform_BaseMock.cpp (100%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/Platform_BaseMock.h (98%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/RenderBackendMock.cpp (100%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/RenderBackendMock.h (97%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/RendererCommandVisitorMock.cpp (96%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/RendererCommandVisitorMock.h (93%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/RendererMock.cpp (98%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/RendererMock.h (81%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/RendererResourceCacheFake.h (80%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/RendererResourceCacheMock.h (75%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/RendererResourceManagerMock.cpp (100%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/RendererResourceManagerMock.h (80%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/RendererSceneEventSenderMock.h (95%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/RendererSceneUpdaterFacade.cpp (99%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/RendererSceneUpdaterFacade.h (83%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/ResourceDeviceHandleAccessorMock.h (96%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/ResourceUploadRenderBackendMock.h (96%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/ResourceUploaderMock.cpp (100%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/ResourceUploaderMock.h (89%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/SystemCompositorControllerMock.cpp (100%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/SystemCompositorControllerMock.h (58%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/WindowEventHandlerMock.cpp (100%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/WindowEventHandlerMock.h (76%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/WindowMock.cpp (96%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/WindowMock.h (62%) rename renderer/RendererLib/{RendererTestUtils/src => RendererTestCommon}/renderer_common_gmock_header.cpp (94%) rename renderer/RendererLib/{RendererTestUtils/include => RendererTestCommon}/renderer_common_gmock_header.h (100%) delete mode 100644 renderer/RendererLib/ramses-renderer-api/include/ramses-renderer-api/DcsmContentControl.h delete mode 100644 renderer/RendererLib/ramses-renderer-api/include/ramses-renderer-api/IDcsmContentControlEventHandler.h delete mode 100644 renderer/RendererLib/ramses-renderer-api/include/ramses-renderer-api/RendererConfig.h delete mode 100644 renderer/RendererLib/ramses-renderer-api/include/ramses-renderer-api/WarpingMeshData.h delete mode 100644 renderer/RendererLib/ramses-renderer-impl/include/BinaryShaderCacheProxy.h delete mode 100644 renderer/RendererLib/ramses-renderer-impl/include/ContentStates.h delete mode 100644 renderer/RendererLib/ramses-renderer-impl/include/DcsmContentControlImpl.h delete mode 100644 renderer/RendererLib/ramses-renderer-impl/include/SharedSceneState.h delete mode 100644 renderer/RendererLib/ramses-renderer-impl/include/WarpingMeshDataImpl.h delete mode 100644 renderer/RendererLib/ramses-renderer-impl/src/DcsmContentControl.cpp delete mode 100644 renderer/RendererLib/ramses-renderer-impl/src/DcsmContentControlImpl.cpp delete mode 100644 renderer/RendererLib/ramses-renderer-impl/src/DisplayConfig.cpp delete mode 100644 renderer/RendererLib/ramses-renderer-impl/src/RendererConfig.cpp delete mode 100644 renderer/RendererLib/ramses-renderer-impl/src/RendererConfigImpl.cpp delete mode 100644 renderer/RendererLib/ramses-renderer-impl/src/SharedSceneState.cpp delete mode 100644 renderer/RendererLib/ramses-renderer-impl/src/WarpingMeshData.cpp delete mode 100644 renderer/RendererLib/ramses-renderer-impl/src/WarpingMeshDataImpl.cpp delete mode 100644 renderer/RendererLib/ramses-renderer-impl/test/DcsmConsumerMock.cpp delete mode 100644 renderer/RendererLib/ramses-renderer-impl/test/DcsmConsumerMock.h delete mode 100644 renderer/RendererLib/ramses-renderer-impl/test/DcsmContentControlTest.cpp delete mode 100644 renderer/RendererLib/ramses-renderer-impl/test/RendererConfigTest.cpp delete mode 100644 renderer/RendererLib/ramses-renderer-impl/test/SharedSceneStateTest.cpp rename scripts/integration_tests/ramses_test_framework/argparser.py => renderer/RendererTestUtils/CMakeLists.txt (50%) rename renderer/{RendererLib => }/RendererTestUtils/include/ReadPixelCallbackHandler.h (80%) rename renderer/{RendererLib => }/RendererTestUtils/include/RendererAndSceneTestEventHandler.h (82%) rename renderer/{RendererLib => }/RendererTestUtils/include/RendererTestUtils.h (67%) rename renderer/{RendererLib => }/RendererTestUtils/src/RendererTestUtils.cpp (80%) create mode 100644 renderer/ramses-renderer-api/CMakeLists.txt rename renderer/{RendererLib => }/ramses-renderer-api/include/ramses-renderer-api/BinaryShaderCache.h (83%) rename renderer/{RendererLib => }/ramses-renderer-api/include/ramses-renderer-api/DefaultRendererResourceCache.h (93%) rename renderer/{RendererLib => }/ramses-renderer-api/include/ramses-renderer-api/DisplayConfig.h (82%) rename renderer/{RendererLib => }/ramses-renderer-api/include/ramses-renderer-api/IBinaryShaderCache.h (96%) rename renderer/{RendererLib => }/ramses-renderer-api/include/ramses-renderer-api/IRendererEventHandler.h (78%) rename renderer/{RendererLib => }/ramses-renderer-api/include/ramses-renderer-api/IRendererResourceCache.h (95%) rename renderer/{RendererLib => }/ramses-renderer-api/include/ramses-renderer-api/IRendererSceneControlEventHandler.h (84%) rename renderer/{RendererLib => }/ramses-renderer-api/include/ramses-renderer-api/RamsesRenderer.h (72%) create mode 100644 renderer/ramses-renderer-api/include/ramses-renderer-api/RendererConfig.h rename renderer/{RendererLib => }/ramses-renderer-api/include/ramses-renderer-api/RendererSceneControl.h (83%) rename renderer/{RendererLib => }/ramses-renderer-api/include/ramses-renderer-api/Types.h (77%) create mode 100644 renderer/ramses-renderer-impl/CMakeLists.txt rename renderer/{RendererLib => }/ramses-renderer-impl/include/BinaryShaderCacheImpl.h (96%) create mode 100644 renderer/ramses-renderer-impl/include/BinaryShaderCacheProxy.h rename renderer/{RendererLib => }/ramses-renderer-impl/include/CommandDispatchingThread.h (93%) rename renderer/{RendererLib => }/ramses-renderer-impl/include/DefaultRendererResourceCacheImpl.h (70%) rename renderer/{RendererLib => }/ramses-renderer-impl/include/DisplayConfigImpl.h (76%) rename renderer/{RendererLib => }/ramses-renderer-impl/include/RamsesRendererImpl.h (86%) rename renderer/{RendererLib => }/ramses-renderer-impl/include/RamsesRendererUtils.h (100%) rename renderer/{RendererLib => }/ramses-renderer-impl/include/RendererConfigImpl.h (68%) rename renderer/{RendererLib => }/ramses-renderer-impl/include/RendererEventChainer.h (66%) rename renderer/{RendererLib => }/ramses-renderer-impl/include/RendererFactory.h (86%) rename renderer/{RendererLib => }/ramses-renderer-impl/include/RendererMate.h (87%) rename renderer/{RendererLib => }/ramses-renderer-impl/include/RendererMateRamshCommands.h (79%) rename renderer/{RendererLib => }/ramses-renderer-impl/include/RendererResourceCacheProxy.h (64%) rename renderer/{RendererLib => }/ramses-renderer-impl/include/RendererSceneControlImpl.h (54%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/BinaryShaderCache.cpp (95%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/BinaryShaderCacheImpl.cpp (96%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/BinaryShaderCacheProxy.cpp (97%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/CommandDispatchingThread.cpp (85%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/DefaultRendererResourceCache.cpp (92%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/DefaultRendererResourceCacheImpl.cpp (96%) create mode 100644 renderer/ramses-renderer-impl/src/DisplayConfig.cpp rename renderer/{RendererLib => }/ramses-renderer-impl/src/DisplayConfigImpl.cpp (68%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/RamsesRenderer.cpp (51%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/RamsesRendererImpl.cpp (75%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/RamsesRendererUtils.cpp (90%) create mode 100644 renderer/ramses-renderer-impl/src/RendererConfig.cpp create mode 100644 renderer/ramses-renderer-impl/src/RendererConfigImpl.cpp rename renderer/{RendererLib => }/ramses-renderer-impl/src/RendererFactory.cpp (76%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/RendererMate.cpp (93%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/RendererMateRamshCommands.cpp (96%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/RendererResourceCacheProxy.cpp (100%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/RendererSceneControl.cpp (68%) rename renderer/{RendererLib => }/ramses-renderer-impl/src/RendererSceneControlImpl.cpp (82%) rename renderer/{RendererLib/RendererLib => ramses-renderer-impl}/test/BinaryShaderCacheProxyTest.cpp (98%) rename renderer/{RendererLib/RendererLib => ramses-renderer-impl}/test/BinaryShaderCacheTest.cpp (83%) rename renderer/{RendererLib/RendererLib => ramses-renderer-impl}/test/DefaultRendererResourceCacheTest.cpp (95%) rename renderer/{RendererLib => }/ramses-renderer-impl/test/DisplayConfigTest.cpp (61%) rename renderer/{RendererLib => }/ramses-renderer-impl/test/RamsesRendererDispatchTest.cpp (70%) rename renderer/{RendererLib => }/ramses-renderer-impl/test/RamsesRendererPickingTest.cpp (78%) rename renderer/{RendererLib => }/ramses-renderer-impl/test/RamsesRendererTest.cpp (75%) create mode 100644 renderer/ramses-renderer-impl/test/RendererConfigTest.cpp rename renderer/{RendererLib => }/ramses-renderer-impl/test/RendererEventTestHandler.h (81%) rename renderer/{RendererLib => }/ramses-renderer-impl/test/RendererMateTest.cpp (93%) rename renderer/{RendererLib => }/ramses-renderer-impl/test/RendererSceneControlEventHandlerMock.cpp (100%) rename renderer/{RendererLib => }/ramses-renderer-impl/test/RendererSceneControlEventHandlerMock.h (91%) rename renderer/{RendererLib => }/ramses-renderer-impl/test/RendererSceneControlTest.cpp (90%) rename renderer/{RendererLib => }/ramses-renderer-impl/test/RendererSceneControlWithRendererTest.cpp (92%) create mode 100644 scripts/ci/build/build.py create mode 100755 scripts/ci/build/common.py create mode 100755 scripts/ci/build/test-cmake-configurations.py create mode 100755 scripts/ci/clang-tidy-wrapper.py create mode 100755 scripts/ci/collect-coverage.py create mode 100644 scripts/ci/common/clangtidy.py create mode 100644 scripts/ci/common/compilationdb.py create mode 100644 scripts/ci/common/lists.py create mode 100644 scripts/ci/common/repo.py create mode 100644 scripts/ci/common/yamlconfig.py create mode 100644 scripts/ci/config/clang-tidy-wrapper.yaml create mode 100644 scripts/ci/config/sanitizer/asan_suppressions.txt create mode 100644 scripts/ci/config/sanitizer/lsan_suppressions.txt create mode 100644 scripts/ci/config/sanitizer/tsan_blacklist.txt create mode 100644 scripts/ci/config/valgrind/suppressions create mode 100644 scripts/ci/installation-check/check-build-with-install-shared-lib.bat create mode 100755 scripts/ci/installation-check/check-build.py create mode 100755 scripts/ci/installation-check/check-installation.py create mode 100644 scripts/ci/installation-check/cmake/Findglm.cmake create mode 100644 scripts/ci/installation-check/shared-lib-check/CMakeLists.txt create mode 100644 scripts/ci/installation-check/shared-lib-check/ramses-shared-lib-check.cpp create mode 100644 scripts/ci/installation-check/shared-lib-headless-check/CMakeLists.txt create mode 100644 scripts/ci/installation-check/shared-lib-headless-check/ramses-shared-lib-check.cpp create mode 100644 scripts/ci/installation-check/static-lib-check/CMakeLists.txt create mode 100644 scripts/ci/installation-check/static-lib-check/ramses-static-lib-check.cpp create mode 100755 scripts/ci/repo-copy.py create mode 100644 scripts/ci/test/test_clang-tidy-wrapper.py create mode 100644 scripts/ci/test/test_clangtidy.py create mode 100644 scripts/ci/test/test_compilationdb.py create mode 100644 scripts/ci/test/test_lists.py create mode 100644 scripts/ci/test/test_repo.py create mode 100644 scripts/ci/test/test_yamlconfig.py delete mode 100644 scripts/integration_tests/.gitignore delete mode 100644 scripts/integration_tests/__init__.py delete mode 100644 scripts/integration_tests/configuration/__init__.py delete mode 100644 scripts/integration_tests/configuration/base_remote_config.py delete mode 100644 scripts/integration_tests/configuration/common_config.py delete mode 100644 scripts/integration_tests/configuration/local_config.py delete mode 100755 scripts/integration_tests/example_run_tests_on_remote_targets.py delete mode 100644 scripts/integration_tests/images_desired/black.png delete mode 100644 scripts/integration_tests/images_desired/black_600x600.png delete mode 100644 scripts/integration_tests/images_desired/black_rgb.png delete mode 100644 scripts/integration_tests/images_desired/red_triangle_on_blue_background.png delete mode 100644 scripts/integration_tests/images_desired/renderer_with_dest_rectangle.png delete mode 100644 scripts/integration_tests/images_desired/scc_big_red_gear_left_and_cube.png delete mode 100644 scripts/integration_tests/images_desired/scc_only_cube.png delete mode 100644 scripts/integration_tests/images_desired/scc_red_gear_left_and_cube.png delete mode 100644 scripts/integration_tests/images_desired/scc_red_gear_left_white_gear_right_lowered_and_cube.png delete mode 100644 scripts/integration_tests/images_desired/scc_red_transparent_gear_left_and_cube.png delete mode 100644 scripts/integration_tests/images_desired/scc_white_gear_left_and_cube.png delete mode 100644 scripts/integration_tests/images_desired/scc_white_gear_left_big_red_gear_right_and_cube.png delete mode 100644 scripts/integration_tests/images_desired/scc_white_gear_left_red_gear_right_and_cube.png delete mode 100644 scripts/integration_tests/images_desired/testClient_animation.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_allSidesSameFallback.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_allSidesSameFallback_oneAndTwoCompositing.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_allSidesSameFallback_oneCompositing.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_allSurfacesFallbackTextures.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_fallbacktexture.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_fiveSurfacesCompositing.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_fiveSurfacesCompositingDmaBufAndIviGears.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_sameSourceMultiFallback.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_threeSurfacesCompositing.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_threeSurfacesCompositingDmaBuf.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_threeSurfacesCompositingDmaBufAndIviGears.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_twoSurfacesCompositing.png delete mode 100644 scripts/integration_tests/images_desired/testClient_compositing_twoSurfacesCompositingDmaBuf.png delete mode 100644 scripts/integration_tests/images_desired/testClient_dataLink_linked.png delete mode 100644 scripts/integration_tests/images_desired/testClient_dataLink_noLinks.png delete mode 100644 scripts/integration_tests/images_desired/testClient_destroyedScene.png delete mode 100644 scripts/integration_tests/images_desired/testClient_disconnectedClient.png delete mode 100644 scripts/integration_tests/images_desired/testClient_distributedScene_redTriangle.png delete mode 100644 scripts/integration_tests/images_desired/testClient_file_loading.png delete mode 100644 scripts/integration_tests/images_desired/testClient_move_between_IVI_layers_A.png delete mode 100644 scripts/integration_tests/images_desired/testClient_move_between_IVI_layers_B.png delete mode 100644 scripts/integration_tests/images_desired/testClient_multiScene.png delete mode 100644 scripts/integration_tests/images_desired/testClient_multipleClients.png delete mode 100644 scripts/integration_tests/images_desired/testClient_multipleRenderTargets.png delete mode 100644 scripts/integration_tests/images_desired/testClient_multiple_ivi_layers.png delete mode 100644 scripts/integration_tests/images_desired/testClient_streamtexture_1.png delete mode 100644 scripts/integration_tests/images_desired/testClient_streamtexture_dmabuf_1.png delete mode 100644 scripts/integration_tests/images_desired/testClient_text_scene.png delete mode 100644 scripts/integration_tests/images_desired/testClient_threeTriangles.png delete mode 100644 scripts/integration_tests/images_desired/testClient_triangleBlue.png delete mode 100644 scripts/integration_tests/images_desired/testClient_triangleRed.png delete mode 100644 scripts/integration_tests/images_desired/testClient_triangleRedAndBlue.png delete mode 100644 scripts/integration_tests/images_desired/testClient_twoDisplaysPlacedTogether.png delete mode 100644 scripts/integration_tests/images_desired/testClient_twoScenes.png delete mode 100644 scripts/integration_tests/images_desired/testClient_twoScenes_displ0.png delete mode 100644 scripts/integration_tests/images_desired/testClient_twoScenes_displ1.png delete mode 100644 scripts/integration_tests/images_desired/testForceFallbackImage1.png delete mode 100644 scripts/integration_tests/images_desired/testForceFallbackImage2.png delete mode 100644 scripts/integration_tests/images_desired/testForceFallbackImage3.png delete mode 100644 scripts/integration_tests/ramses_test_framework/__init__.py delete mode 100644 scripts/integration_tests/ramses_test_framework/application.py delete mode 100644 scripts/integration_tests/ramses_test_framework/asynchronousreader.py delete mode 100644 scripts/integration_tests/ramses_test_framework/buffer.py delete mode 100644 scripts/integration_tests/ramses_test_framework/cores/__init__.py delete mode 100644 scripts/integration_tests/ramses_test_framework/cores/core.py delete mode 100644 scripts/integration_tests/ramses_test_framework/cores/core_impl.py delete mode 100644 scripts/integration_tests/ramses_test_framework/cores/remote_core_impl.py delete mode 100644 scripts/integration_tests/ramses_test_framework/helper.py delete mode 100644 scripts/integration_tests/ramses_test_framework/image_utils.py delete mode 100644 scripts/integration_tests/ramses_test_framework/local_application.py delete mode 100644 scripts/integration_tests/ramses_test_framework/log.py delete mode 100644 scripts/integration_tests/ramses_test_framework/power_device.py delete mode 100644 scripts/integration_tests/ramses_test_framework/ramses_test_extensions.py delete mode 100644 scripts/integration_tests/ramses_test_framework/targets/__init__.py delete mode 100644 scripts/integration_tests/ramses_test_framework/targets/linux_remote_targets.py delete mode 100644 scripts/integration_tests/ramses_test_framework/targets/local_target.py delete mode 100644 scripts/integration_tests/ramses_test_framework/targets/remote_target.py delete mode 100644 scripts/integration_tests/ramses_test_framework/targets/target.py delete mode 100644 scripts/integration_tests/ramses_test_framework/targets/targetInfo.py delete mode 100644 scripts/integration_tests/ramses_test_framework/targets/windows_cygwin_target.py delete mode 100644 scripts/integration_tests/ramses_test_framework/test_classes.py delete mode 100644 scripts/integration_tests/tests/__init__.py delete mode 100644 scripts/integration_tests/tests/embedded_compositor_base/__init__.py delete mode 100644 scripts/integration_tests/tests/embedded_compositor_base/embedded_compositor_base.py delete mode 100644 scripts/integration_tests/tests/system_compositor_controller_base/__init__.py delete mode 100644 scripts/integration_tests/tests/system_compositor_controller_base/system_compositor_controller_base.py delete mode 100644 scripts/integration_tests/tests/test_localClientTest.py delete mode 100644 scripts/integration_tests/tests/test_local_and_remote_renderer.py delete mode 100644 scripts/integration_tests/tests/test_local_and_remote_scenes.py delete mode 100644 scripts/integration_tests/tests/test_renderertwodisplays.py delete mode 100644 scripts/integration_tests/tests/test_renderertwotargets.py delete mode 100644 scripts/integration_tests/tests/test_run_no_initial_black_frame.py delete mode 100644 scripts/integration_tests/tests/test_scenemanagerdifferenttarget.py delete mode 100644 scripts/integration_tests/tests/test_start_renderer_with_dst_rectangle.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_add_surface_to_layer.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_change_surface_opacity.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_control_new_surface_which_has_same_id_than_destroyed_surface_before.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_control_new_surface_which_registers_while_renderer_already_running.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_control_surface_which_is_not_yet_allocated.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_destroy_surface.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_make_layer_invisible.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_make_layer_visible.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_make_surface_invisible.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_make_surface_visible.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_make_two_surfaces_visible.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_move_surfaces.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_can_remove_surface_from_layer.py delete mode 100644 scripts/integration_tests/tests/test_system_compositor_controller_removes_surface_from_list_when_destroyed_from_outside.py delete mode 100644 scripts/integration_tests/tests/test_testclient_EC_dmabuf_confidence.py delete mode 100644 scripts/integration_tests/tests/test_testclient_EC_dmabuf_lifecycle.py delete mode 100644 scripts/integration_tests/tests/test_testclient_EC_lifecycle.py delete mode 100644 scripts/integration_tests/tests/test_testclient_EC_lifecycle_client_cleansUp.py delete mode 100644 scripts/integration_tests/tests/test_testclient_EC_lifecycle_client_killed.py delete mode 100644 scripts/integration_tests/tests/test_testclient_EC_multi_fallback_same_source.py delete mode 100644 scripts/integration_tests/tests/test_testclient_EC_multi_sources_multi_fallback.py delete mode 100644 scripts/integration_tests/tests/test_testclient_EC_multi_sources_same_fallback.py delete mode 100644 scripts/integration_tests/tests/test_testclient_animation.py delete mode 100644 scripts/integration_tests/tests/test_testclient_compositing_fallback_texture.py delete mode 100644 scripts/integration_tests/tests/test_testclient_datalink.py delete mode 100644 scripts/integration_tests/tests/test_testclient_destroyedScene.py delete mode 100644 scripts/integration_tests/tests/test_testclient_file_loading_tcp.py delete mode 100644 scripts/integration_tests/tests/test_testclient_forcefallbackimage.py delete mode 100644 scripts/integration_tests/tests/test_testclient_move_between_IVI_layers.py delete mode 100644 scripts/integration_tests/tests/test_testclient_multiScene.py delete mode 100644 scripts/integration_tests/tests/test_testclient_multipleRenderTarget.py delete mode 100644 scripts/integration_tests/tests/test_testclient_multiple_IVI_layers.py delete mode 100644 scripts/integration_tests/tests/test_testclient_multipleclients.py delete mode 100644 scripts/integration_tests/tests/test_testclient_reconnectClient.py delete mode 100644 scripts/integration_tests/tests/test_testclient_sceneMapping.py delete mode 100644 scripts/integration_tests/tests/test_testclient_sceneRelease.py delete mode 100644 scripts/integration_tests/tests/test_testclient_twoScenes.py delete mode 100644 scripts/integration_tests/tests/test_text_rendering.py delete mode 100644 tox.ini delete mode 100644 utils/ramses-resource-tools/CMakeLists.txt delete mode 100644 utils/ramses-resource-tools/include/FilePathsConfig.h delete mode 100644 utils/ramses-resource-tools/include/RamsesResourcePackerArguments.h delete mode 100644 utils/ramses-resource-tools/include/ResourcePacker.h delete mode 100644 utils/ramses-resource-tools/ramses-resource-packer/main.cpp delete mode 100644 utils/ramses-resource-tools/src/FilePathsConfig.cpp delete mode 100644 utils/ramses-resource-tools/src/RamsesResourcePackerArguments.cpp delete mode 100644 utils/ramses-resource-tools/src/ResourcePacker.cpp delete mode 100644 utils/ramses-resource-tools/test/FilePathesConfigTest.cpp delete mode 100644 utils/ramses-resource-tools/test/RamsesResourcePackerArgumentsTest.cpp delete mode 100644 utils/ramses-resource-tools/test/ResourcePackerTest.cpp delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-Roboto-Bold.ttf delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-empty.filepathesconfig delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-invalid-duplicate-filepath.filepathesconfig delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-invalid-non-exist-filepath.filepathesconfig delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-invalid-wrong-num-tokens.filepathesconfig delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-resourcepackerinput.filepathesconfig delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-test.filepathesconfig delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-test1.res delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-test2.res delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-test3.res delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-test4.res delete mode 100644 utils/ramses-resource-tools/test/res/ramses-resource-tools-valid-with-additional-spaces-and-empty-lines.filepathesconfig delete mode 100644 utils/ramses-shader-tools/CMakeLists.txt delete mode 100644 utils/ramses-shader-tools/include/EffectResourceCreator.h delete mode 100644 utils/ramses-shader-tools/include/RamsesEffectFromGLSLShaderArguments.h delete mode 100644 utils/ramses-shader-tools/include/RamsesShaderFromGLSLShaderArguments.h delete mode 100644 utils/ramses-shader-tools/include/ShaderConverter.h delete mode 100644 utils/ramses-shader-tools/ramses-effect-from-glsl-shader/main.cpp delete mode 100644 utils/ramses-shader-tools/ramses-shader-from-glsl-shader/main.cpp delete mode 100644 utils/ramses-shader-tools/src/EffectResourceCreator.cpp delete mode 100644 utils/ramses-shader-tools/src/RamsesEffectFromGLSLShaderArguments.cpp delete mode 100644 utils/ramses-shader-tools/src/RamsesShaderFromGLSLShaderArguments.cpp delete mode 100644 utils/ramses-shader-tools/src/ShaderConverter.cpp delete mode 100644 utils/ramses-shader-tools/test/EffectResourceCreatorTest.cpp delete mode 100644 utils/ramses-shader-tools/test/RamsesEffectFromGLSLShaderArgumentsTest.cpp delete mode 100644 utils/ramses-shader-tools/test/RamsesShaderFromGLSLShaderArgumentsTest.cpp delete mode 100644 utils/ramses-shader-tools/test/ShaderConverterTest.cpp delete mode 100644 utils/ramses-shader-tools/test/res/ramses-shader-tools-custom-text-effect.config delete mode 100644 utils/ramses-shader-tools/test/res/ramses-shader-tools-custom-text-effect.frag delete mode 100644 utils/ramses-shader-tools/test/res/ramses-shader-tools-custom-text-effect.vert delete mode 100644 utils/ramses-shader-tools/test/res/ramses-shader-tools-invalid-attribute-semantic-invalid-keyword.config delete mode 100644 utils/ramses-shader-tools/test/res/ramses-shader-tools-invalid-test.vertexshader delete mode 100644 utils/ramses-shader-tools/test/res/ramses-shader-tools-test.config delete mode 100644 utils/ramses-shader-tools/test/res/ramses-shader-tools-test.fragmentshader delete mode 100644 utils/ramses-shader-tools/test/res/ramses-shader-tools-test.vertexshader delete mode 100644 utils/ramses-shader-tools/test/res/ramses-shader-tools-test_without_compiler_defines.config delete mode 100644 utils/ramses-utils/CMakeLists.txt delete mode 100644 utils/ramses-utils/include/Arguments.h delete mode 100644 utils/ramses-utils/include/ConsoleUtils.h delete mode 100644 utils/ramses-utils/include/EffectConfig.h delete mode 100644 utils/ramses-utils/include/FileUtils.h delete mode 100644 utils/ramses-utils/src/Arguments.cpp delete mode 100644 utils/ramses-utils/src/EffectConfig.cpp delete mode 100644 utils/ramses-utils/src/FileUtils.cpp delete mode 100644 utils/ramses-utils/test/EffectConfigTest.cpp delete mode 100644 utils/ramses-utils/test/res/ramses-utils-empty.config delete mode 100644 utils/ramses-utils/test/res/ramses-utils-invalid-attribute-semantic-invalid-keyword.config delete mode 100644 utils/ramses-utils/test/res/ramses-utils-invalid-attribute-semantic-invalid-num-tokens.config delete mode 100644 utils/ramses-utils/test/res/ramses-utils-invalid-attribute-semantic-invalid-semantic.config delete mode 100644 utils/ramses-utils/test/res/ramses-utils-invalid-comipler-define-invalid-keyword.config delete mode 100644 utils/ramses-utils/test/res/ramses-utils-invalid-comipler-define-invalid-num-tokens.config delete mode 100644 utils/ramses-utils/test/res/ramses-utils-invalid-uniform-semantic-invalid-keyword.config delete mode 100644 utils/ramses-utils/test/res/ramses-utils-invalid-uniform-semantic-invalid-num-tokens.config delete mode 100644 utils/ramses-utils/test/res/ramses-utils-invalid-uniform-semantic-invalid-semantic.config delete mode 100644 utils/ramses-utils/test/res/ramses-utils-test.config delete mode 100644 utils/ramses-utils/test/res/ramses-utils-valid-with-additional-spaces-and-empty-lines.config diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 000000000..054f07809 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +skip_list: + - '106' # Role name ramses-windows-build does not match ``^[a-z][a-z0-9_]+$`` pattern + - '204' # TODO(tobias) Lines should be no longer than 160 chars + - '301' # Commands should not change things if nothing needs doing + - experimental # all rules tagged as experimental +use_default_rules: true diff --git a/.clang-tidy b/.clang-tidy index 4e034bf2c..76673a577 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -159,7 +159,7 @@ Checks: # TODO hicpp-named-parameter, hicpp-no-assembler, hicpp-noexcept-move, - # TODO hicpp-signed-bitwise, + hicpp-signed-bitwise, hicpp-exception-baseclass, misc-redundant-expression, @@ -174,6 +174,7 @@ Checks: misc-unused-using-decls, misc-unconventional-assign-operator, + modernize-avoid-c-arrays, modernize-use-nullptr, modernize-use-bool-literals, modernize-raw-string-literal, @@ -182,6 +183,7 @@ Checks: modernize-deprecated-headers, modernize-make-unique, modernize-make-shared, + modernize-use-override, performance-faster-string-find, performance-for-range-copy, @@ -261,3 +263,7 @@ CheckOptions: value: true - key: readability-implicit-bool-conversion.AllowPointerConditions value: true + - key: modernize-use-override.AllowOverrideAndFinal + value: true + - key: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals + value: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cae40d88d..1777d72e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,12 +80,18 @@ jobs: cmake_args+=" -DCMAKE_CONFIGURATION_TYPES=${{ matrix.type }}" fi + if [[ "${{matrix.os}}" == "macos"* ]]; then + cmake_args+=" -Dramses-sdk_ENABLE_FLATBUFFERS_GENERATION=OFF" + cmake_args+=" -Dramses-sdk_ENABLE_DEFAULT_WINDOW_TYPE=OFF" + cmake_args+=" -Dramses-sdk_BUILD_FULL_SHARED_LIB=OFF" + cmake_args+=" -Dramses-sdk_BUILD_HEADLESS_SHARED_LIB=ON" + fi + if [[ "${{matrix.target-platform}}" == "iphone"* ]]; then cmake_args+=" -GXcode" cmake_args+=" -DCMAKE_OSX_SYSROOT=${{ matrix.target-platform }}" cmake_args+=" -DCMAKE_SYSTEM_NAME=iOS" cmake_args+=" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO" - cmake_args+=" -Dramses-sdk_ENABLE_FLATBUFFERS_GENERATION=OFF" fi cmake $GITHUB_WORKSPACE ${cmake_args} diff --git a/.gitlint b/.gitlint new file mode 100644 index 000000000..862da8b82 --- /dev/null +++ b/.gitlint @@ -0,0 +1,80 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +# Available rules: +# +# T1: title-max-length +# T2: title-trailing-whitespace +# T3: title-trailing-punctuation (disabled) +# T4: title-hard-tab +# T5: title-must-not-contain-word (disabled) +# T6: title-leading-whitespace +# T7: title-match-regex (disabled) +# B1: body-max-line-length +# B2: body-trailing-whitespace +# B3: body-hard-tab +# B4: body-first-line-empty +# B5: body-min-length (disabled) +# B6: body-is-missing (disabled) +# B7: body-changed-file-mention (disabled) +# +# See http://jorisroovers.github.io/gitlint/rules/ for a full description. + +[general] +ignore=T3,T5,T7,B1,B5,B6,B7,CCB1 + +# verbosity should be a value between 1 and 3, the commandline -v flags take precedence over this +verbosity = 3 + +ignore-merge-commits=true + +# Enable debug mode (prints more output). Disabled by default. +# debug=true + +[title-max-length] +line-length=90 + +# [title-must-not-contain-word] +# Comma-separated list of words that should not occur in the title. Matching is case +# insensitive. It's fine if the keyword occurs as part of a larger word (so "WIPING" +# will not cause a violation, but "WIP: my title" will. +# words=wip + +# [title-match-regex] +# python like regex (https://docs.python.org/2/library/re.html) that the +# commit-msg title must be matched to. +# Note that the regex can contradict with other rules if not used correctly +# (e.g. title-must-not-contain-word). +# regex=^US[0-9]* + +[body-max-line-length] +line-length=80 + +# [body-min-length] +# min-length=5 + +# [body-is-missing] +# Whether to ignore this rule on merge commits (which typically only have a title) +# default = True +# ignore-merge-commits=false + +# [body-changed-file-mention] +# List of files that need to be explicitly mentioned in the body when they are changed +# This is useful for when developers often erroneously edit certain files or git submodules. +# By specifying this rule, developers can only change the file when they explicitly reference +# it in the commit message. +# files=gitlint/rules.py,README.md + +# Custom rules +# [soft-body-max-line-length] +# line-length=80 + +# [body-match-regex] +# python like regex (https://docs.python.org/2/library/re.html) that the +# commit-msg body must be matched to. +# regex=(?i)ISSUE-[0-9]* diff --git a/cmake/ramses/tools.cmake b/.readthedocs.yml similarity index 59% rename from cmake/ramses/tools.cmake rename to .readthedocs.yml index 67c8c6463..e2eecca32 100644 --- a/cmake/ramses/tools.cmake +++ b/.readthedocs.yml @@ -6,7 +6,19 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. # ------------------------------------------------------------------------- -function(ramses_glob_target_sources target bind_type expr) - file(GLOB out ${expr} ${ARGN}) - target_sources(${target} ${bind_type} ${out}) -endfunction() +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +--- +version: 2 + +sphinx: + configuration: doc/sphinx/conf.py + fail_on_warning: true + +python: + version: 3.7 + install: + - requirements: doc/sphinx/requirements.txt +... diff --git a/scripts/integration_tests/run_tests_locally.py b/.yamllint old mode 100755 new mode 100644 similarity index 50% rename from scripts/integration_tests/run_tests_locally.py rename to .yamllint index 53097c2d9..718bcb8a8 --- a/scripts/integration_tests/run_tests_locally.py +++ b/.yamllint @@ -1,19 +1,29 @@ -#!/usr/bin/env python3 # ------------------------------------------------------------------------- -# Copyright (C) 2015 BMW Car IT GmbH +# Copyright (C) 2018 BMW Car IT GmbH # ------------------------------------------------------------------------- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # ------------------------------------------------------------------------- -from ramses_test_framework.cores import core -from configuration.local_config import LocalConfig +--- +extends: default -_core = core.Core(core.LOCAL, LocalConfig()) -_core.read_arguments() -_core.setup() -result = _core.run_tests() -_core.tear_down(shutdownTargets=False) -if not result: - exit(1) +ignore: | + /external/googletest/ + /external/asio/ + /external/fmt/ + /external/glm/ + /external/imgui/ + /external/cli11/ + /external/harfbuzz/ + /external/freetype/ + /external/flatbuffers/ + /external/sol/ + /external/google-benchmark/ + /external/glslang/ + /external/lua/ + +rules: + line-length: disable + document-start: disable diff --git a/CHANGELOG.txt b/CHANGELOG.md similarity index 80% rename from CHANGELOG.txt rename to CHANGELOG.md index 5393ec380..123f44e07 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.md @@ -1,4 +1,179 @@ +# Ramses Changelog + + +28.0.0-rc1 +------------------- +### Added + +- Added Ramses logic to the Ramses repository + - If you want to exclude logic, disable it over CMake (ramses-sdk_ENABLE_LOGIC=OFF) + - The version which was merged is 1.4.2. You can find the changelog/docs of Logic [here](https://ramses-logic.readthedocs.io/en/v1.4.2/) + - Moved all types from rlogic namespace to ramses namespace +- Added support for feature levels, RamsesFramework can now be constructed with a feature level specified + (using RamsesFrameworkConfig), see EFeatureLevel for more information. +- Added ramses-sdk_TEXT_SUPPORT, a CMake option to disable text support. + - Disabled by default on iOS and MacOS builds. + - Other platforms remain unaffected by the option. +- Added [[nodiscard]] flag for const getters. +- Added createStreamBuffer and destroyStreamBuffer to RamsesRenderer. +- Added linkStreamBuffer to RendererSceneControl, and streamBufferLinked to IRendererSceneControlEventHandler. +- Added glm math type aliases: vec2/3/4f, vec2/3/4i, matrix22/33/44f, quat +- Added options to RamsesFrameworkConfig API that previously required command line parsing +- Added rotation by quaternion: ramses::Node::setRotation(const quat&) +- Added Node::getRotationType() +- Added DisplayConfig::setDeviceType() and DisplayConfig::getDeviceType() to select device type for display creation +- Added DisplayConfig::setWindowType() and DisplayConfig::getWindowType() to select window type for display creation + +### Changed + +- RamsesFrameworkConfig constructor needs a mandatory argument to specify feature level (see EFeatureLevel for more information) +- Replaced uint32_t with size_t throughout the API where applicable: `Appearance`, `Effect`, `EffectDescription`, `Node`, `RamsesUtils`, `Scene`, `Texture2DBuffer`, `UniformInput`, `IRendererSceneControlEventHandler::objectsPicked()`. +- Ramses shared lib with renderer is always called ramses-shared-lib (without platform dependant postfix) + - Contents are controlled using cmake variables ramses-sdk_ENABLE_WINDOW_TYPE_* +- Ramses client only shared lib is renamed to ramses-shared-lib-headless +- Ramses examples, demos, tests and tools are enabled by default only if ramses is a top level project, otherwise + they must be explicitly enabled using respective cmake options +- Changed enums to enum classes. +- Renamed enum values for `ramses::ERendererEventResult`: OK -> Ok, FAIL -> Failed, INDIRECT -> Indirect +- Replaced all enum to string methods by: `const char* ramses::toString(T)` +- Upgraded the minimum version of the C++ standard in Ramses to 17. + - All modern compilers support C++ 17 meanwhile. + - Some of the dependencies of Ramses have also switched to C++17. + - Some tools and linters work better and detect more issues with a higher version of the standard. + - Draw mode is checked against geometry shader input type (Appearance::setDrawMode) also when effect was loaded from file + (previously it was checked only when effect created in same session without loading). +- Modified ramses-stream-viewer command line options: + - renamed -fps,--framesPerSecond to --fps +- Stream buffers can be linked even if content from wayland surface is not available (yet). +- Stream buffers do not get unlinked from texture consumers if content from wayland surface becomes unavailable. +- ramses-scene-viewer: renamed command line options: + - -nv,--no-validation: removed short option (-nv) + - -ns,--no-skub: removed short option (-ns) + - renamed -vd,--validation-output-directory to --output + - renamed -x,--screenshot-file to -x,--screenshot + - scene file can be applied as positional argument +- ramses standalone renderer: renamed command line options: + - renamed -nd,--numDisplays to --displays + - renamed -sm,--sceneMappings to -m,--scene-mapping + - renamed -nomap,--disableAutoMapping to --no-auto-show + - -skub,--skub: removed short option(-skub) +- Functions that were kept static for API/ABI backcompatibility became proper member methods: + - DisplayConfig::setAsyncEffectUploadEnabled, DisplayConfig::setDepthStencilBufferType + - Effect::hasGeometryShader, Effect::getGeometryShaderInputType + - RamsesRenderer::createOffscreenBuffer and createInterruptibleOffscreenBuffer + - RamsesRenderer::setDisplayBufferClearFlags +- Renderer event handler reports creation and destruction of external buffers. + - Removed workaround with 'getExternalBufferGlId' - id can be now obtained through externalBufferCreated(). +- IRendererEventHandler::renderThreadLoopTimings now reports timings per display (with additional displayId argument). + - IRendererEventHandler::renderThreadLoopTimingsPerDisplay was removed. +- Renamed RamsesRenderer::set/getMaximumFramerate to set/getFramerateLimit, + display ID argument is mandatory now, there is no global framerate limit anymore. +- RamsesFrameworkConfig/RendererConfig/DisplayConfig: replaced const char* by std::string_view +- Moved 'ramses-client-api/EDataType.h' to 'ramses-framework-api/EDataType.h' +- Renamed ERotationConvention to ERotationType +- Renamed all enums in ERotationType to start with "Euler_" prefix and so that the order is inverted + to represent extrinsic rotation rather than instrinsic notation. + - Any rotation previously defined as ERotationConvention::ABC is now renamed to ERotationType::Euler_CBA + - Extrinsic rotation convention means a rotation around any order of axes ABC is done in the specified order around + the original world axes, which are fixed and do not rotate while applying rotation to the object. + This is more conformant with existing tools. +- Node.h uses vec3f for rotation, translation, scaling +- Node::setRotation(vec3f,rotationType) has a default value Euler_XYZ for rotation convention +- Appearance::set/getBlendingColor now uses vec4f as argument. +- Appearance::set/getInputValue is now templated and uses new data types to pass vector/matrix values + (vecXf, vecXi, matrixXXf) instead of int/float arrays. +- RenderPass::setClearColor, RamsesRenderer::setDisplayBufferClearColor, DisplayConfig::setClearColor: clear color is passed as vec4f +- ArrayBuffer::updateData and ArrayBuffer::getData must be used with concrete types instead of void ptr +- ERamsesObjectType_DataBufferObject renamed to ERamsesObjectType_ArrayBufferObject to match the class ArrayBuffer name +- Removed EEffectInputDataType used in UniformInput and AttributeInput, EDataType is used instead + - Uniform/AttributeInput::getDataType now returns std::optional for case when the input is not valid (not or wrongly initialized) +- Added other data types used in API to EDataType enum: EDataType::Int32, EDataType::Vector2/3/4I, EDataType::Matrix22/33/44F +- Scene::createArrayResource must be used with concrete data type instead of void ptr and data type enum + - example how to port old code: float twoTriangles[6]={..}; myScene.createArrayResource(EDataType::Vector3F, 2, twoTriangles); + to new code: vec3f twoTriangles[2]={..}; myScene.createArrayResource(2, twoTriangles); + - make sure to use the right math type to determine the ArrayResource data type (e.g. no floats to represent vecX, which was typical before) +- LogicEngine instance now has mandatory argument to specify feature level (use EFeatureLevel::Latest if do not care and want all features) +- DataObject class can now hold any of the data types previously held by the removed DataXXX classes + - example how to port old code: DataVector2I* obj = myScene.createDataVector2I(); obj->setValue(1, 2); + to new code: DataObject* obj = myScene.createDataObject(EDataType::Vector2I); obj->setValue(vec2i{ 1, 2 }); + - make sure to use the right math type when setting or getting the value +- Node::getModelMatrix() / Node::getInverseModelMatrix() use matrix44f as argument +- Replaced std::array type aliases by glm: matrix44f, vec2f, vec3f, vec4f, vec2i, vec3i, vec4i +- LogicEngine::getTotalSerializedSize and getSerializedSize can now be set to use a specific ELuaSavingMode for size calculation +- Unmanaged objects can now be copy or move constructed, and copy or move assigned: + - RamsesFrameworkConfig, RendererConfig, DisplayConfig, UniformInput, AttributeInput, EffectDescription, RenderTargetDescription + ETextureCubeFace, ERenderTargetDepthBufferType, ERenderBufferType, ERenderBufferFormat, ERenderBufferAccessMode +- The const char* parameters of the public API are replaced by std::string_view. + +### Removed + +- Removed cmake option ramses-sdk_CONSOLE_LOGLEVEL (use RamsesFrameworkConfig to configure log levels) +- Removed environment variables CONSOLE_LOGLEVEL and RAMSES_LOGLEVEL (use RamsesFrameworkConfig to configure log levels) +- Removed RamsesFramework::SetConsoleLogLevel (use RamsesFrameworkConfig::setLogLevelConsole) +- Removed ramses-ptx-export utility (Use RamsesComposer to create scene files). +- Removed AnimationSystem and all related classes, animations moved from Ramses to Ramses Logic. +- Removed text generator. +- Removed visual frame profiler ('fp' Ramsh command). +- Removed SomeIP support. +- Removed tools for generating and packing effects from shaders (ramses-utils, ramses-resource-tools, ramses-shader-tools). +- Removed ramses-packet-player. +- Removed DCSM support and all related API. +- Removed RamsesHMIUtils (only helper to identify unneeded objects was kept and moved to RamsesUtils). +- Removed support for Integrity. + - Ramses 27 is the last official release which supports Integrity. +- Removed RamsesFramework(argc, argv) constructor, use RamsesFrameworkConfig instead. +- Removed support for warping: + - removed DisplayConfig::enableWarpingPostEffect() + - removed IRendererEventHandler::warpingMeshDataUpdated() + - removed RamsesRenderer::updateWarpingMeshData() +- Removed stream texture from client API. Renderer side stream buffers must be used for embedded compositing on wayland. +- Removed RamsesFrameworkConfig(argc, argv), DisplayConfig(argc, argv), RendererConfig(argc, argv) constructors +- Removed deprecated embedded compositor settings from RendererConfig (use DisplayConfig instead) +- Removed RamsesFrameworkConfig::setPeriodicLogsEnabled (use setPeriodicLogInterval(0) instead) +- Removed legacy ZYX (left handed) rotation convention + - Remove related APIs: Node::rotate, Node::setRotation(x,y,z) and Node::getRotation(x,y,z) + - RamsesNode_setRotationNative in JNI Interface uses right-handed Euler_XYZ rotation convention +- Removed rlogic::EFeatureLevel, ramses::EFeatureLevel is used in whole Ramses SDK instead +- Removed all typed data object classes (DataInt32, DataFloat, DataVector2/3/4/f/i, DataMatrix22/33/44), + all their functionality was moved into DataObject class (see Changed section) +- Removed rlogic::ERotationType (replaced by ramses::ERotationType) +- Removed rlogic::ELogMessageType (replaced by ramses::ELogLevel) +- Removed deprecated enum ESceneResourceStatus + +### Fixed + +- Fixed potential renderer crash when having more than one displays in non-threaded mode and destroying all but one due to missing context enable +- Fixed race condition when instantiating multiple RamsesFramework instances in different threads +- RenderGroup no more reports 'contains no meshnodes' validation warning if it has nested RenderGroups with MeshNode(s) + +## Older releases + +27.0.132 +------------------- + Bugfixes + ------------------------------------------------------------------------ + - Fixed wrong events given by DcsmContentControlEventHandlerMock::contentLinkedToTextureConsumer + Previously could report linked events for non-linked contents (also with wrong datalink id) when the + contents shared technical id (like same wayland surface) internally + +27.0.131 +------------------- + General changes + ------------------------------------------------------------------------ + - ramses-packet-player: + - enable remote rendering for replayed scenes + - added "--dcsm" command line parameter to replay on DcsmConsumers + - added "--someip" command line parameter to connect to SomeIP remotes + - added "--play" option to automatically replay after start + - show details for array resources + - added "--ivi-surface", "--ivi-layer" options to run packet-player as wayland-ivi client + - added inspection gui for the current scene state + + API changes + ------------------------------------------------------------------------ + - Added predefined IDs for FocusRequest handling + 27.0.130 ------------------- General changes @@ -321,6 +496,30 @@ - Fix segfault caused by activating VAO that was not uploaded due to wrong handling of dirtiness in resource cached scene. +27.0.106 +------------------- + General changes + ------------------------------------------------------------------------ + - Display creation DOES fail if EC creation fails + - A change was introduced as a temporary measure in 27.0.105 that lets display + creation not fail if EC creation fails. This change is reverted and a proper + fix is introduced. See API changes of RendererConfig and DisplayConfig. + + API changes + ------------------------------------------------------------------------ + - API for configuring EC is moved from RendererConfig to DisplayConfig + - Allows configuring EC per display + - API in RendererConfig is not removed. It is marked as deprecated and always fails + - Add layoutAvailability field to DCSM metadata + + Bugfixes + ------------------------------------------------------------------------ + - Failing to create EGL image from DMA buffer does not lead to a segfault + in RAMSES renderer. An error code ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_WL_BUFFER + is sent to wayland client. + - Fix segfault caused by activating VAO that was not uploaded due to wrong handling of dirtiness + in resource cached scene. + 27.0.105 ------------------- General changes diff --git a/CMakeLists.txt b/CMakeLists.txt index bce420530..45dfb565f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,101 +8,180 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.10) -SET(RAMSES_VERSION_MAJOR 27) -SET(RAMSES_VERSION_MINOR 0) -SET(RAMSES_VERSION_PATCH 130) -SET(RAMSES_VERSION_POSTFIX "") +set(RAMSES_VERSION_MAJOR 28) +set(RAMSES_VERSION_MINOR 0) +set(RAMSES_VERSION_PATCH 0) +set(RAMSES_VERSION_POSTFIX "-rc1") +set(RAMSES_VERSION "${RAMSES_VERSION_MAJOR}.${RAMSES_VERSION_MINOR}.${RAMSES_VERSION_PATCH}${RAMSES_VERSION_POSTFIX}") + +project(ramses-sdk + VERSION ${RAMSES_VERSION_MAJOR}.${RAMSES_VERSION_MINOR}.${RAMSES_VERSION_PATCH} + DESCRIPTION "RAMSES - Rendering Architecture for Multi-Screen EnvironmentS" + HOMEPAGE_URL "https://ramses3d.org/" + LANGUAGES C CXX +) + +if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) + set(RAMSES_TOPLEVEL ON) +else() + set(RAMSES_TOPLEVEL OFF) +endif() + + +# build options +set(ramses-sdk_CPP_VERSION "17" CACHE STRING "Set used c++ version, must be 17 or 20") +option(ramses-sdk_WARNINGS_AS_ERRORS "If on, warnings are treated as errors during build." ON) +# TODO let all ramses components which are getting installed to respect this option +option(ramses-sdk_ENABLE_INSTALL "Enable/disable installation of Ramses" ON) +option(ramses-sdk_BUILD_WITH_LTO "Build all targets with link time optimization enabled (supported only on some platforms)." OFF) + +# window type configuration. Several window types can be enabled in same build +option(ramses-sdk_ENABLE_DEFAULT_WINDOW_TYPE "Enable a default window type if none explicitly enabled" ON) +option(ramses-sdk_ENABLE_WINDOW_TYPE_WINDOWS "Enable building for Windows window" OFF) +option(ramses-sdk_ENABLE_WINDOW_TYPE_X11 "Enable building for X11 window" OFF) +option(ramses-sdk_ENABLE_WINDOW_TYPE_ANDROID "Enable building for Android window" OFF) +option(ramses-sdk_ENABLE_WINDOW_TYPE_WAYLAND_IVI "Enable building for Wayland ivi window" OFF) +option(ramses-sdk_ENABLE_WINDOW_TYPE_WAYLAND_WL_SHELL "Enable building for Wayland wl_shell window" OFF) + +# shared lib options +option(ramses-sdk_BUILD_FULL_SHARED_LIB "Build per-renderer shared libraries." ON) +option(ramses-sdk_BUILD_HEADLESS_SHARED_LIB "Enable building headless shared library without renderer" OFF) + +# optional components +option(ramses-sdk_ENABLE_LOGIC "Enable ramses logic - a component for scripting and animation." ON) +option(ramses-sdk_BUILD_DAEMON "Build the ramses daemon." ON) +option(ramses-sdk_TEXT_SUPPORT "Enable/disable the ramses text API." ON) +option(ramses-sdk_ENABLE_TCP_SUPPORT "Enable use of TCP communication." ON) +option(ramses-sdk_ENABLE_DLT "Enable DLT logging support." ON) + +option(ramses-sdk_BUILD_EXAMPLES "Build examples." ${RAMSES_TOPLEVEL}) +option(ramses-sdk_BUILD_DEMOS "Build demos." ${RAMSES_TOPLEVEL}) +option(ramses-sdk_BUILD_TOOLS "Build tool binaries." ${RAMSES_TOPLEVEL}) +option(ramses-sdk_BUILD_TESTS "Build ramses tests." ${RAMSES_TOPLEVEL}) + +option(ramses-sdk_FORCE_BUILD_DOCS "Build documentation, report error and abort if Doxygen and Dot are found on the system." OFF) +option(ramses-sdk_USE_LINUX_DEV_PTP "Enable support for synchronized PTP time on Linux." OFF) +option(ramses-sdk_ENABLE_COVERAGE "Build with code coverage enabled (gcc: gcov, clang: source based coverage)." OFF) +option(ramses-sdk_USE_CCACHE "Enable ccache when configured and available" OFF) +option(ramses-sdk_ENABLE_FLATBUFFERS_GENERATION "Enable generation of flatbuffers code" ${RAMSES_TOPLEVEL}) +option(ramses-sdk_USE_IMAGEMAGICK "Enable tests depending on image magick compare" OFF) + +# find options +option(ramses-sdk_ALLOW_PLATFORM_GLM "Enable to search for platform provided OpenGL Math libary (glm)." ON) +option(ramses-sdk_ALLOW_PLATFORM_FREETYPE "Enable to search for platform provided freetype and harfbuzz." ON) +option(ramses-sdk_ALLOW_CUSTOM_FREETYPE "Allow usage of custom freetype and harfbuzz if platform provided one was not found." ON) + +# other options +set(ramses-sdk_FOLDER_PREFIX "" CACHE STRING "Optional folder prefix for targets in visual studio.") -CMAKE_POLICY(SET CMP0048 NEW) -PROJECT(ramses-sdk VERSION ${RAMSES_VERSION_MAJOR}.${RAMSES_VERSION_MINOR}.${RAMSES_VERSION_PATCH}) +set(ramses-sdk_ENABLE_SANITIZER "" CACHE STRING "Enable build with a clang sanitizer.") +set_property(CACHE ramses-sdk_ENABLE_SANITIZER PROPERTY STRINGS "" "ubsan" "tsan" "asan" "asan+ubsan") +set(ramses-sdk_FOLDER_PREFIX "" CACHE STRING "Set a custom prefix for target folders in Visual Studio. If not set, will be set based on project's relative path.") +set(ramses-sdk_USE_LINKER_OVERWRITE "" CACHE STRING "Specify used linker (gcc/clang only).") +set(ramses-sdk_CPACK_GENERATOR "TGZ" CACHE STRING "CPack package type (default: TGZ).") + +set(ramses-sdk_VERSION "${RAMSES_VERSION_MAJOR}.${RAMSES_VERSION_MINOR}.${RAMSES_VERSION_PATCH}" CACHE STRING "Ramses version" FORCE) +message(STATUS "RAMSES_VERSION=${RAMSES_VERSION}") -# TODO(tobias) must be here because used inside platformConfig, rework order of all options to solve properly -OPTION(ramses-sdk_WARNINGS_AS_ERRORS "Enable warnings as errors during build" OFF) -set(ramses-sdk_CPP_VERSION "14" CACHE STRING "Set used c++ version, must be 14, 17 or 20") +set_property(GLOBAL PROPERTY USE_FOLDERS ON) include(cmake/ramses/setCmakePolicies.cmake NO_POLICY_SCOPE) # we want to propagate policy settings out! include(cmake/ramses/platformDetection.cmake) include(cmake/ramses/platformConfig.cmake) include(cmake/ramses/getGitInformation.cmake) include(cmake/ramses/externaltools.cmake) -include(cmake/ramses/tools.cmake) +include(cmake/ramses/folderize.cmake) include(cmake/ramses/testConfig.cmake) include(cmake/ramses/buildConfig.cmake) +include(cmake/ramses/createTarget.cmake) +include(cmake/ramses/addSubdirectory.cmake) -SET(CMAKE_MODULE_PATH +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ) -SET_PROPERTY(GLOBAL PROPERTY USE_FOLDERS ON) +if (CMAKE_BUILD_TYPE) + # Case-insensitive comparison required, build type is not case-sensitive + string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER_CASE) + if(NOT("${CMAKE_BUILD_TYPE_LOWER_CASE}" STREQUAL "debug" OR "${CMAKE_BUILD_TYPE_LOWER_CASE}" STREQUAL "release" OR + "${CMAKE_BUILD_TYPE_LOWER_CASE}" STREQUAL "relwithdebinfo" OR "${CMAKE_BUILD_TYPE_LOWER_CASE}" STREQUAL "minsizerel")) + message(FATAL_ERROR "Build type set to unsupported type ${CMAKE_BUILD_TYPE}.") + endif () +endif () -INCLUDE(external/acme2/acme2.cmake) +message(STATUS "Ramses Build Config: SystemName ${CMAKE_SYSTEM_NAME}, SystemVersion ${CMAKE_SYSTEM_VERSION}, CompilerID ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}, BuildType ${CMAKE_BUILD_TYPE}, TargetBitness ${TARGET_BITNESS} c++${ramses-sdk_CPP_VERSION}") -# if no build configuration is defined, use build config 'Release' -IF (NOT CMAKE_BUILD_TYPE) - ACME_INFO("Build type not defined. Using default build type 'Release'.") - SET(CMAKE_BUILD_TYPE Release CACHE STRING "Choose build type: Debug, Release." FORCE) -ELSE () - # Case-insensitive comparison required, built type is not case-sensitive - STRING(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER_CASE) - IF(NOT("${CMAKE_BUILD_TYPE_LOWER_CASE}" STREQUAL "debug" OR "${CMAKE_BUILD_TYPE_LOWER_CASE}" STREQUAL "release" OR - "${CMAKE_BUILD_TYPE_LOWER_CASE}" STREQUAL "relwithdebinfo" OR "${CMAKE_BUILD_TYPE_LOWER_CASE}" STREQUAL "minsizerel")) - ACME_ERROR("Build type set to unsupported type ${CMAKE_BUILD_TYPE}.") - ENDIF () -ENDIF (NOT CMAKE_BUILD_TYPE) +if((${CMAKE_SYSTEM_NAME} MATCHES "Darwin") OR (${CMAKE_SYSTEM_NAME} MATCHES "iOS") AND ramses-sdk_TEXT_SUPPORT) + message(WARNING "Text rendering is not supported on iOS and MacOS currently! Disabling it!") + set(ramses-sdk_TEXT_SUPPORT OFF CACHE STRING "Enable/disable the ramses text API." FORCE) +endif() -MESSAGE(STATUS "Ramses Build Config: SystemName ${CMAKE_SYSTEM_NAME}, SystemVersion ${CMAKE_SYSTEM_VERSION}, CompilerID ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}, TargetOS ${TARGET_OS}, BuildType ${CMAKE_BUILD_TYPE}, TargetBitness ${TARGET_BITNESS} c++${ramses-sdk_CPP_VERSION}") +# check if valid configuration of window types in relation with shared libs +set(ANY_WINDOW_TYPE_ENABLED OFF) -# find options -OPTION(ramses-sdk_ALLOW_PLATFORM_FREETYPE "Enable to search for platform provided freetype and harfbuzz" ON) -OPTION(ramses-sdk_ALLOW_CUSTOM_FREETYPE "Allow usage of custom freetype and harfbuzz if platform provided one was not found" ON) +if( ramses-sdk_ENABLE_WINDOW_TYPE_WINDOWS + OR ramses-sdk_ENABLE_WINDOW_TYPE_X11 + OR ramses-sdk_ENABLE_WINDOW_TYPE_ANDROID + OR ramses-sdk_ENABLE_WINDOW_TYPE_WAYLAND_IVI + OR ramses-sdk_ENABLE_WINDOW_TYPE_WAYLAND_WL_SHELL) + set(ANY_WINDOW_TYPE_ENABLED ON) +endif() -# build options -OPTION(ramses-sdk_BUILD_EXAMPLES "Build Example targets: ON, OFF" ON) -OPTION(ramses-sdk_BUILD_TOOLS "Build tools: ON, OFF" ON) -OPTION(ramses-sdk_BUILD_SMOKE_TESTS "Build smoke test targets: ON, OFF" ON) -OPTION(ramses-sdk_BUILD_DEMOS "Build demo targets: ON, OFF" ON) -OPTION(ramses-sdk_BUILD_CLIENT_ONLY_SHARED_LIB "Build client only shared library" OFF) -OPTION(ramses-sdk_BUILD_FULL_SHARED_LIB "Build per renderer shared libraries" ON) -OPTION(ramses-sdk_BUILD_DOCS "Build documentation if Doxygen is found" ON) -OPTION(ramses-sdk_ENABLE_WAYLAND_IVI "Build a version of ramses renderer which uses wayland ivi surfaces" ON) -OPTION(ramses-sdk_ENABLE_WAYLAND_SHELL "Build a version of ramses renderer which uses wayland shell surfaces" ON) -OPTION(ramses-sdk_ENABLE_TCP_SUPPORT "Enable use of TCP communication" ON) -OPTION(ramses-sdk_ENABLE_FALLBACK_FAKE_COMMUNICATION_SYSTEM "Allow fake communication system as last resort" ON) -OPTION(ramses-sdk_ENABLE_DLT "Enable dlt support" ON) -OPTION(ramses-sdk_USE_LINUX_DEV_PTP "Enable support for synchronized ptp time on linux" OFF) -OPTION(ramses-sdk_BUILD_WITH_LTO "Build all targets with link time optimization enabled (not supported on all platforms)" OFF) -option(ramses-sdk_ENABLE_COVERAGE "Build withcode coverage enabled (gcc: gcov, clang: source based coverage)" OFF) - -SET(ramses-sdk_CONSOLE_LOGLEVEL "" CACHE STRING "Console log level.") -SET_PROPERTY(CACHE ramses-sdk_CONSOLE_LOGLEVEL PROPERTY STRINGS "off" "fatal" "error" "warn" "info" "debug" "trace" "") - -SET(ramses-sdk_ENABLE_SANITIZER "" CACHE STRING "Enable build with a clang sanitizer.") -SET_PROPERTY(CACHE ramses-sdk_ENABLE_SANITIZER PROPERTY STRINGS "" "ubsan" "tsan" "asan" "asan+ubsan") - -set(ramses-sdk_FOLDER_PREFIX "" CACHE STRING "Set a custom prefix for target folders in Visual Studio. If not set, will be set based on project's relative path") - -set(ramses-sdk_USE_LINKER_OVERWRITE "" CACHE STRING "Specify used linker (gcc/clang only)") - -set(ramses-sdk_CPACK_GENERATOR "TGZ" CACHE STRING "CPack package type (default: TGZ)") - -SET(RAMSES_VERSION "${RAMSES_VERSION_MAJOR}.${RAMSES_VERSION_MINOR}.${RAMSES_VERSION_PATCH}${RAMSES_VERSION_POSTFIX}") -set(ramses-sdk_VERSION "${RAMSES_VERSION_MAJOR}.${RAMSES_VERSION_MINOR}.${RAMSES_VERSION_PATCH}" CACHE STRING "Ramses version" FORCE) +if(ANY_WINDOW_TYPE_ENABLED AND ramses-sdk_ENABLE_DEFAULT_WINDOW_TYPE) + message(WARNING "Default window type setting will be ignored since a window type is enabled explicitly.") +endif() -MESSAGE(STATUS "RAMSES_VERSION=${RAMSES_VERSION}") +#if no window type explicitly enabled, check if default window type is (implicitly) enabled +if(NOT ANY_WINDOW_TYPE_ENABLED) + if(ramses-sdk_ENABLE_DEFAULT_WINDOW_TYPE) + + macro(setDefaultWindowType cacheVariable windowTypeName) + message(STATUS "Setting ${windowTypeName} as default window type. Value set to ${cacheVariable} will be overridden") + + # This overrides the cache variable with a non-cache cmake variable of the same name to avoid over writing value + # set by the user in gui or ccache if any + set(${cacheVariable} ON) + set(ANY_WINDOW_TYPE_ENABLED ON) + endmacro() + + if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + setDefaultWindowType(ramses-sdk_ENABLE_WINDOW_TYPE_WINDOWS "Windows") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + setDefaultWindowType(ramses-sdk_ENABLE_WINDOW_TYPE_X11 "X11") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Android") + setDefaultWindowType(ramses-sdk_ENABLE_WINDOW_TYPE_ANDROID "Android") + else() + message(WARNING "Incorrect configuration. No default window type is known for the build operating system '${CMAKE_SYSTEM_NAME}':\n" + "* Either disable default window type by setting ramses-sdk_ENABLE_DEFAULT_WINDOW_TYPE=OFF\n" + "* or enable a window type explicitly by setting any of the options ramses-sdk_ENABLE_WINDOW_TYPE_* to ON\n" + "For current configuration ramses-sdk_BUILD_HEADLESS_SHARED_LIB will be overridden to ON, and headless shared lib will be built.") + + # Override cmake option (with normal/non-cached cmake variable) + set(ramses-sdk_BUILD_HEADLESS_SHARED_LIB ON) + endif() + endif() +endif() -SET(ramses-sdk_VERSION_DIR "ramses-${RAMSES_VERSION_MAJOR}.${RAMSES_VERSION_MINOR}") -IF (ramses-sdk_USE_VERSIONED_BINARIES) - SET(ramses-sdk_BINARY_DIR_BASE "bin/${ramses-sdk_VERSION_DIR}") -ELSE() - SET(ramses-sdk_BINARY_DIR_BASE "bin") -ENDIF() +if(NOT ANY_WINDOW_TYPE_ENABLED AND NOT ramses-sdk_BUILD_HEADLESS_SHARED_LIB) + message(FATAL_ERROR "Invalid configuration. Neither any window type, nor headless shared lib enabled:\n" + "* Either enable at least one window type by setting any of the options ramses-sdk_ENABLE_WINDOW_TYPE_* to ON\n" + "* or enable building headless shared lib (without renderer) by setting ramses-sdk_BUILD_HEADLESS_SHARED_LIB=ON") +endif() -include(cmake/ramses/rendererModulePerConfig.cmake) +if(NOT ANY_WINDOW_TYPE_ENABLED AND ramses-sdk_BUILD_FULL_SHARED_LIB) + message(WARNING "Incorrect configuration. Trying to build full shared lib with renderer while no window type is enabled:\n" + "* If trying to build renderer enable at least one window type by setting any of the options ramses-sdk_ENABLE_WINDOW_TYPE_* to ON\n" + "* Otherwise disable building full shared lib (with renderer) by setting ramses-sdk_BUILD_FULL_SHARED_LIB=OFF\n" + "For current configuration ramses-sdk_BUILD_FULL_SHARED_LIB=ON will be ignored!") -ADD_DEFINITIONS(-DRAMSES_LINK_STATIC) + # Override cmake option (with normal/non-cached cmake variable) + set(ramses-sdk_BUILD_FULL_SHARED_LIB OFF) +endif() +# TODO this should not be here +add_definitions(-DRAMSES_LINK_STATIC) if (NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) message(STATUS "Redirect ramses library output to ${CMAKE_CURRENT_BINARY_DIR}/bin") @@ -118,192 +197,139 @@ set(ramses-shared-lib-MIXIN CACHE INTERNAL "") set(ramses-shared-lib-renderer-MIXIN CACHE INTERNAL "") include(cmake/ramses/platformTargets.cmake) +include(cmake/ramses/makeTestFromTarget.cmake) +include(cmake/ramses/resourceCopy.cmake) -ACME2_PROJECT( - #========================================================================== - # project metadata - #========================================================================== - NAME ramses-sdk - VERSION_MAJOR ${RAMSES_VERSION_MAJOR} - VERSION_MINOR ${RAMSES_VERSION_MINOR} - VERSION_PATCH ${RAMSES_VERSION_PATCH} - VERSION_STRING ${RAMSES_VERSION} - DESCRIPTION "RAMSES - Rendering Architecture for Multi-Screen EnvironmentS" - URL "https://github.com/bmwcarit/ramses" - ENABLE_INSTALL OFF - - #========================================================================== - # Test partitioned into following categories: - # UNITTEST = default - generally, test for classes (Units) - # RNDSANDWICHTEST = sandwich tests, which need a rendering backend (can draw pixels) - # RNDSANDWICHTEST_SWRAST = subset of RNDSANDWICHTEST which can be executed on a sw. rasterizer (Mesa in docker) - # RNDSANDWICHTEST_VALGRINDGATE = subset of RNDSANDWICHTEST which are executed with valgrind in the gate - #========================================================================== - ALLOWED_TEST_SUFFIXES UNITTEST RNDSANDWICHTEST RNDSANDWICHTEST_SWRAST RNDSANDWICHTEST_VALGRINDGATE - - #========================================================================== - # install path setup - #========================================================================== - INSTALL_HEADER "include/${ramses-sdk_VERSION_DIR}" - INSTALL_BINARY "${ramses-sdk_BINARY_DIR_BASE}" - INSTALL_RESOURCE "${ramses-sdk_BINARY_DIR_BASE}/res" - INSTALL_STATIC_LIB "lib/${ramses-sdk_VERSION_DIR}" - - #========================================================================== - # content - #========================================================================== - CONTENT AUTO external - ON framework - - ON client/ramses-client - - ON ramses-daemon - - ON renderer/RendererLib - - # Auxilary platform components - AUTO renderer/Platform/WaylandEGLExtensionProcs - AUTO renderer/Platform/WaylandUtilities - AUTO renderer/Platform/SystemCompositorController_Wayland_IVI - - # Window Components - AUTO renderer/Platform/Window_Windows - AUTO renderer/Platform/Window_Wayland - - # Context Components - AUTO renderer/Platform/Context_WGL - AUTO renderer/Platform/Context_EGL - - # Device Components - AUTO renderer/Platform/Device_GL - AUTO renderer/Platform/Device_EGL_Extension - - # Other Components - # TODO Mohamed: those components should be possible to move before window components (have less deps) - AUTO renderer/Platform/EmbeddedCompositor_Wayland - AUTO renderer/Platform/Window_Wayland_Test - AUTO renderer/Platform/Window_Wayland_IVI - AUTO renderer/Platform/Window_Wayland_Shell - - # Platforms - AUTO renderer/Platform/Platform_Windows_WGL - AUTO renderer/Platform/Platform_Windows_WGL_4_2_core - AUTO renderer/Platform/Platform_Windows_WGL_4_5 - AUTO renderer/Platform/Platform_Windows_WGL_ES_3_0 - AUTO renderer/Platform/Platform_EGL - AUTO renderer/Platform/Platform_Wayland_EGL - AUTO renderer/Platform/Platform_Wayland_IVI_EGL - AUTO renderer/Platform/Platform_Wayland_Shell_EGL - AUTO renderer/Platform/Platform_X11 - AUTO renderer/Platform/Platform_Integrity_RGL - AUTO renderer/Platform/Platform_Android - - AUTO ramses-shared-lib - - AUTO renderer/ramses-renderer-main - - AUTO examples - - AUTO integration - - AUTO utils - - - AUTO demo -) +if(ramses-sdk_ENABLE_FLATBUFFERS_GENERATION) + if(${CMAKE_SYSTEM_NAME} MATCHES "Android") + message(FATAL_ERROR "The Android build systems don't allow modifying the source tree. Please set ramses-sdk_ENABLE_FLATBUFFERS_GENERATION to OFF.") + endif() +endif() + +set(RAMSES_INSTALL_HEADERS_PATH include) +set(RAMSES_INSTALL_RUNTIME_PATH bin) +set(RAMSES_INSTALL_LIBRARY_PATH lib) +set(RAMSES_INSTALL_ARCHIVE_PATH lib) +set(RAMSES_INSTALL_RESOURCES_PATH "bin/res") + +if (ramses-sdk_ALLOW_PLATFORM_GLM) + find_package(glm QUIET) + if (glm_FOUND AND NOT TARGET glm::glm) + # older versions of glm may not export the "glm::glm" target, but "glm" + add_library(glm::glm ALIAS glm) + endif() +endif() -#========================================================================== -# target: code style checker -#========================================================================== +createBuildConfig() + +addSubdirectory(MODE AUTO PATH external) +addSubdirectory(MODE ON PATH framework) + +addSubdirectory(MODE ON PATH client) + +if(ANY_WINDOW_TYPE_ENABLED) + addSubdirectory(MODE ON PATH renderer) +endif() + +addSubdirectory(MODE AUTO PATH ramses-shared-lib) +addSubdirectory(MODE AUTO PATH ramses-cli) + +if(ramses-sdk_BUILD_TOOLS) + addSubdirectory(MODE AUTO PATH utils) +endif() + +if(ramses-sdk_BUILD_DAEMON) + addSubdirectory(MODE ON PATH ramses-daemon) +endif() + +if(ramses-sdk_BUILD_TOOLS) + addSubdirectory(MODE AUTO PATH renderer/ramses-renderer-main) +endif() +if(ramses-sdk_BUILD_EXAMPLES) + addSubdirectory(MODE AUTO PATH examples) +endif() +if(ramses-sdk_BUILD_TESTS) + addSubdirectory(MODE AUTO PATH integration) +endif() +if(ramses-sdk_BUILD_DEMOS) + addSubdirectory(MODE AUTO PATH demo) +endif() + +if(RAMSES_TOPLEVEL AND ramses-sdk_BUILD_TOOLS AND ramses-sdk_ENABLE_LOGIC) + addSubdirectory(MODE AUTO PATH client/logic/tools) + # TODO Fix the benchmarks - they need to link statically, ramses doesn't allow that currently + # OR rework the benchmarks to only benchmark public API + #addSubdirectory(MODE ON PATH benchmarks) +endif() + +# Only enable testAssetProducer if built as top-level project +if(RAMSES_TOPLEVEL AND ramses-sdk_BUILD_TESTS AND ramses-sdk_ENABLE_LOGIC) + addSubdirectory(MODE AUTO PATH client/logic/unittests/testAssetProducer) + if(TARGET testAssetProducer) + add_custom_target(RL_REGEN_TEST_ASSETS + COMMAND testAssetProducer ${PROJECT_SOURCE_DIR}/client/logic/unittests/res) + set_property(TARGET RL_REGEN_TEST_ASSETS PROPERTY FOLDER "CMakePredefinedTargets") + endif() +endif() + +include(cmake/ramses/createPackage.cmake) include(cmake/ramses/addCheckerTargets.cmake) -#========================================================================== -# build and install documentation -#========================================================================== -if (ramses-sdk_BUILD_DOCS) +if(RAMSES_TOPLEVEL OR ramses-sdk_FORCE_BUILD_DOCS) add_subdirectory(doc) endif() -INSTALL(FILES README.md CHANGELOG.txt LICENSE.txt - DESTINATION share/doc/${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH} - COMPONENT ${PROJECT_NAME}-${PROJECT_VERSION} - ) - -#========================================================================== -# generate CMake config files -#========================================================================== -INCLUDE(CMakePackageConfigHelpers) - -SET(configmodules) # initially empty - -IF (ramses-sdk_BUILD_CLIENT_ONLY_SHARED_LIB) - LIST(APPEND configmodules ramses-shared-lib-client-only) -ENDIF() -IF (ramses-sdk_BUILD_FULL_SHARED_LIB) - # only install full shared lib cmake find script if built at least one - foreach (PLATFORM_NAME ${RENDERER_CONFIG_LIST}) - if (TARGET ramses-shared-lib-${PLATFORM_NAME}) - LIST(APPEND configmodules ramses-shared-lib) - break() +if(ramses-sdk_ENABLE_INSTALL) + install(FILES README.md CHANGELOG.md LICENSE.txt CONTRIBUTING.rst + DESTINATION share/doc/ramses-sdk-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH} + COMPONENT ramses-sdk-${PROJECT_VERSION} + ) + + include(CMakePackageConfigHelpers) + + set(configmodules) # initially empty + if (ramses-sdk_BUILD_HEADLESS_SHARED_LIB) + list(APPEND configmodules ramses-shared-lib-headless) + endif() + if (ramses-sdk_BUILD_FULL_SHARED_LIB) + list(APPEND configmodules ramses-shared-lib) + endif() + + foreach(configmodule ${configmodules}) + # install paths for find/config script must differ on windows and linux because of different search + # rules used by find_package. See https://cmake.org/cmake/help/git-master/command/find_package.html + # for details + set(configmodule-VERSION_DIR "${configmodule}-${RAMSES_VERSION_MAJOR}.${RAMSES_VERSION_MINOR}") + if (CMAKE_SYSTEM_NAME MATCHES "Windows") + set(ramses-sdk_FIND_SCRIPT_INSTALL_DIR "lib/${configmodule-VERSION_DIR}/cmake") + else() + set(ramses-sdk_FIND_SCRIPT_INSTALL_DIR "lib/cmake/${configmodule-VERSION_DIR}") endif() - endforeach() -ENDIF() - -FOREACH(configmodule ${configmodules}) - - # install paths for find/config script must differ on windows and linux because of different search - # rules used by find_package. See https://cmake.org/cmake/help/git-master/command/find_package.html - # for details - SET(configmodule-VERSION_DIR "${configmodule}-${RAMSES_VERSION_MAJOR}.${RAMSES_VERSION_MINOR}") - IF (CMAKE_SYSTEM_NAME MATCHES "Windows") - SET(ramses-sdk_FIND_SCRIPT_INSTALL_DIR "lib/${configmodule-VERSION_DIR}/cmake") - ELSE() - SET(ramses-sdk_FIND_SCRIPT_INSTALL_DIR "lib/cmake/${configmodule-VERSION_DIR}") - ENDIF() - - # generate CMake config file (use ${configmodule}Template.cmake.in as base) - CONFIGURE_PACKAGE_CONFIG_FILE( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${configmodule}Template.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/${configmodule}Config.cmake" - INSTALL_DESTINATION "${ramses-sdk_FIND_SCRIPT_INSTALL_DIR}" - PATH_VARS PROJECT_INSTALL_HEADER PROJECT_INSTALL_SHARED_LIB PROJECT_INSTALL_BINARY - NO_SET_AND_CHECK_MACRO - NO_CHECK_REQUIRED_COMPONENTS_MACRO - ) - INSTALL( - FILES ${CMAKE_CURRENT_BINARY_DIR}/${configmodule}Config.cmake - DESTINATION ${ramses-sdk_FIND_SCRIPT_INSTALL_DIR} - ) - ACME_INFO("G ${configmodule}Config.cmake") - - # generate CMake version file - WRITE_BASIC_PACKAGE_VERSION_FILE("${CMAKE_CURRENT_BINARY_DIR}/${configmodule}ConfigVersion.cmake" - VERSION ${RAMSES_VERSION} - COMPATIBILITY SameMajorVersion) - INSTALL( - FILES ${CMAKE_CURRENT_BINARY_DIR}/${configmodule}ConfigVersion.cmake - DESTINATION ${ramses-sdk_FIND_SCRIPT_INSTALL_DIR} - ) - ACME_INFO("G ${configmodule}ConfigVersion.cmake") - -ENDFOREACH() - -# produce a version file -IF(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ramses-version") -# case when using a ascent source package already containing the originally configured file - INSTALL( - FILES "${CMAKE_CURRENT_SOURCE_DIR}/ramses-version" - DESTINATION ${PROJECT_INSTALL_HEADER} + + # generate CMake config file (use ${configmodule}Template.cmake.in as base) + configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${configmodule}Template.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/${configmodule}Config.cmake" + INSTALL_DESTINATION "${ramses-sdk_FIND_SCRIPT_INSTALL_DIR}" + PATH_VARS RAMSES_INSTALL_HEADERS_PATH RAMSES_INSTALL_LIBRARY_PATH RAMSES_INSTALL_RUNTIME_PATH + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO ) -ELSE() - STRING(TIMESTAMP RAMSES_BUILD_TIME UTC) - CONFIGURE_FILE( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/ramses-version.in" - "${CMAKE_CURRENT_BINARY_DIR}/ramses-version" - @ONLY + install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/${configmodule}Config.cmake + DESTINATION ${ramses-sdk_FIND_SCRIPT_INSTALL_DIR} ) - INSTALL( - FILES "${CMAKE_CURRENT_BINARY_DIR}/ramses-version" - DESTINATION ${PROJECT_INSTALL_HEADER} + message(STATUS "G ${configmodule}Config.cmake") + + # generate CMake version file + write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/${configmodule}ConfigVersion.cmake" + VERSION ${RAMSES_VERSION} + COMPATIBILITY SameMajorVersion) + install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/${configmodule}ConfigVersion.cmake + DESTINATION ${ramses-sdk_FIND_SCRIPT_INSTALL_DIR} ) -ENDIF() + message(STATUS "G ${configmodule}ConfigVersion.cmake") + + endforeach() +endif() diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..0bda5a70b --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,251 @@ +.. + ------------------------------------------------------------------------- + Copyright (C) 2020 BMW AG + ------------------------------------------------------------------------- + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + ------------------------------------------------------------------------- + +================================== +Pull requests +================================== + +All contributions must be offered as GitHub pull requests. Please read Github documentation for +more info how to fork projects and create pull requests. + +Please make sure the PR has a good description - what does it do? Which were the considerations +while implementing it, if relevant? Did you consider other options, but decided against them +for a good reason? + +================================================================= +Commit guidelines +================================================================= + +There are no strict rules how commits have to be arranged, but we offer some guidelines which +make the review easier and ensures faster integration. + +----------------------------------------------------------------- +Make smaller but expressive commits +----------------------------------------------------------------- + +Commits should be small, yet expressive. If you apply a renaming schema to all files in the +repository, you should not split them in hundreds of commits - put them in one commit, as +long as the reviewer can see a pattern. If you implement multiple classes/libraries which +build on top of each other, could be reasonable to split them in multiple commits to "tell +a story". If you need to forward fix something - it's ok to have a "FIX" commit, don't have +to rebase the whole branch. This works especially well after a first review round, when you +want to only fix a reviewer's comment without rewriting the code. + +----------------------------------------------------------------- +Bundle commits into multiple PRs when it makes sense +----------------------------------------------------------------- + +If you have a complex feature which can be split into multiple, smaller PRs, in a way that +some of them can be integrated earlier than others, then do so. For example, if you need +some refactoring before you add the actual feature, and the refactoring makes sense in +general even without the new feature - then consider creating two PRs. This increases the +chances that the refactoring is merged earlier, thus avoiding conflicts and rebases, while +the feature is subject of further discussion and refinement. + +.. note:: When submitting multiple PRs which depend on each other, please mark their + dependency by adding a line like this: "Depends-On: " where + is a link to another PR which needs to go in first. + +================================================================= +Review +================================================================= + +All code is subject to peer review. We try to be objective and focus more on technical +question rather than cosmetic preferences, but all reviewers are human and inevitably +have preferences. We try to acknowledge that everyone has their own style, but we also try +to keep things consistent across a large codebase. We strive to maintain friendly and helpful +language when reviewing, give concrete suggestions how things could be done instead, in +favor of just disliking a piece of code. + +----------------------------------------------------------------- +Review requirements +----------------------------------------------------------------- + +Before asking for review, please make sure `the code works `_, +there are unit tests (even for proof-of-concept code), and there are `no code style or +formatting violations `_. +The PR source branch has to be based on the latest +released branch HEAD revision, unless the requested change is a hotfix for an existing +release. Please don't rebase branches after you asked for a review, unless the reviewer +explicitly asked for it. You can merge the latest HEAD of the target branch into the PR +source branch, if you need a change that was integrated after the first review round. + +----------------------------------------------------------------- +Review steps +----------------------------------------------------------------- + +As soon as a PR is created, it will be looked at by a reviewer. If you want to signal +that it's being worked on/changed, put a WIP in the name, or add a WIP label. After a +review round, address the comments of the review, and wait for the reviewer to mark +them as 'resolved'. Once everything is resolved, the PR will enter the `integration process `_. + +================================================================= +Code style +================================================================= + +The code style is checked using LLVM tools (Clang-tidy) as well as a custom Python-based +style script which performs additional checks. Both checks have to be successful before +code can be meaningfully reviewed. + +----------------------------------------------------------------- +Clang Tidy +----------------------------------------------------------------- + +Clang-tidy is performed as an automated build step within the `continuous integration pipeline `_. +Check its documentation for instructions how to execute it locally before submitting code. + +----------------------------------------------------------------- +Custom style check +----------------------------------------------------------------- + +Some things can't be easily checked with off-the-shelve tools, so we have our own scripts +to check them (we are generally trying to shift towards established tools when possible). +For example license headers, header guards, or tab/space restrictions. You can execute +those scripts on your local machine with Python like this: + +.. code-block:: bash + + cd + python3 scripts/code_style_checker/check_all_styles.py + +----------------------------------------------------------------- +Additional conventions +----------------------------------------------------------------- + +When it comes to code style, not everything can be checked automatically with tools. The following +list describes additional conventions and style patterns used throughout the project: + +* Namespaces + All code must be in a namespace. Code in namespaces is indented. Nested namespaces + must use the compact C++17 notation (namespace1::namespace2::namespaces3) + + .. code:: cpp + + // Good + namespace namespace1::namespace2 + { + class MyClass; + } + + // Bad + namespace namespace1 + { + namespace namespace2 + { + class MyClass; + } + } + + // Also bad + namespace namespace1{ + namespace namespace2 { + + class MyClass; + + } + } + + +* Usage of ``auto`` + The C++ community is divided when it comes to usage of the ``auto`` keyword. Therefore + we don't enforce strict rules, except for some concrete cases listed below + + * When declaring primitive types (int, strings, bool etc.), don't use auto: + + .. code:: cpp + + // Good + std::string myString = "hello"; + // Bad + auto myString = "hello"; + + * When using template functions which have the type as explicit template parameter, don't repeat it but use auto instead + + .. code:: cpp + + // Good + auto myUniquePtr = std::make_unique(); + // Bad + std::unique_ptr myUniquePtr = std::make_unique(); + + * When using loops and iterators, use auto, but don't omit const and reference qualifiers if used + + .. code:: cpp + + // Good + for(const auto& readIterator : myVector) + { + std::cout << readIterator; + } + + // Bad + for(const std::vector::iterator readIterator : myVector) + { + std::cout << *readIterator; + } + + * When using loops and iterators, use auto, but don't omit const and reference qualifiers if used + + .. code:: cpp + + // Good + for(const auto& readIterator : myVector) + { + std::cout << readIterator; + } + + // Bad + for(const std::vector::iterator readIterator : myVector) + { + std::cout << *readIterator; + } + + // Bad + for(auto readIterator : myVector) + { + // code which doesn't require non-const access to readIterator + } + + * For all other cases, apply common sense. If using ``auto`` makes it more difficult to understand/read the code, + then don't use it. If the type is obvious and auto makes the code more readable, use auto! + +================================================================= +Continuous integration +================================================================= + +There is no support for a public CI service yet, it will be added in the future. If you want to contribute +to the project, you can ensure your code gets merged quickly by executing some or all of the tests yourself +before submitting a PR. + +We suggest executing the following set of builds in order to maximize the chance that the PR will be merged +without further changes: + +* A GCC-based build in Release mode (Linux or Windows WSL) +* A LLVM-based build in Debug mode (Linux or Windows WSL) +* A Visual Studio 2017 CE Release build (Windows) +* A CLANG_TIDY run in Docker (Linux or Windows WSL) +* A LLVM_L64_COVERAGE run in Docker (Linux or Windows WSL) + + The following subchapters explain how to execute these builds. + +----------------------------------------------------------------- +Testing Windows builds locally +----------------------------------------------------------------- + +You can follow the `build instructions for Windows `_ and then execute the RUN_TESTS target of Visual Studio, +or use the ctest command in the build folder. + +================================================================= +Branching +================================================================= + +Currently, we don't maintain multiple branches. We have a single mainline branch +where releases are pushed and tagged. After we have reached a level of feature +completeness where we feel comfortable to support older branches, we will +do so. diff --git a/README.md b/README.md index b64150c9b..d738e479b 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,10 @@ RAMSES is 3D rendering engine with focus on bandwidth and resource efficiency. For a broader overview and introduction to the ecosystem and tools, visit our -[SDK docs](https://ramses-sdk.readthedocs.io/). +[homepage](https://ramses3d.org/). Have a look at our showcase video: [![RAMSES Distributed Rendering Engine Showcase Video](https://img.youtube.com/vi/tyzvEI25BMg/0.jpg)](https://www.youtube.com/watch?v=tyzvEI25BMg) -You can find additional resources here: -* [The Github wiki pages](https://github.com/bmwcarit/ramses/wiki) -* [The doxygen API docs](https://bmwcarit.github.io/ramses) - ## Obtaining the source code RAMSES can be cloned from its BMW Car IT repository using git: @@ -100,16 +96,17 @@ Some of these are shipped directly with the sourcecode, others are included as g RAMSES also includes some assets (e.g. font files) which are licensed under different open source licenses. Directly included: -- ACME2 (Licensed under Apache 2.0) - cityhash (Licensed under MIT License) - Khronos Headers (Licensed under Khronos Group License) - lodepng (Licensed under zlib License) - Wayland-IVI Extension (Licensed under MIT License) +- wayland-zwp-linux-dmabuf-v1-extension (Licensed under MIT License) - Wayland-IVI example client (Licensed under MIT License) Submodule reference: - Freetype 2 (Licensed under FTL, also containing code under BSD and ZLib) - GLSLang (Licensed under BSD-3 and Khronos Group License) +- OpenGL Mathematics (Licensed under MIT License) - Googletest (Licensed under BSD-3) - Harfbuzz (Licensed under MIT and ISC; see external/harfbuzz/COPYING) - Asio (Boost Software License - Version 1.0) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..cfa39a4ab --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Supported Versions + +Following versions of Ramses receive security updates: + +| Version | Supported | +| ------- | ------------------ | +| >= 27.0.0 | :white_check_mark: | +| < 27.0.0 | :x: | + + +## Reporting a Vulnerability + +Please report security vulnerabilities using Github. Specify the affected version, describe +the inputs which are dangerous, and if possible propose expected fix. diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt new file mode 100644 index 000000000..a12dc1023 --- /dev/null +++ b/benchmarks/CMakeLists.txt @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +file(GLOB src *.cpp) + +add_executable(benchmarks + ${src} +) + +target_link_libraries(benchmarks + PRIVATE + rlogic::ramses-logic-static + ramses::google-benchmark-main + fmt::fmt + sol2::sol2 + lua::lua +) + +target_include_directories(benchmarks + PRIVATE + $ + $ +) + +folderizeTarget(benchmarks) diff --git a/benchmarks/animation.cpp b/benchmarks/animation.cpp new file mode 100644 index 000000000..a20843e27 --- /dev/null +++ b/benchmarks/animation.cpp @@ -0,0 +1,166 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "benchmark/benchmark.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/AnimationTypes.h" +#include "fmt/format.h" + +namespace ramses +{ + const auto animationIterations = 100; + + static void RunAnimation(LogicEngine& logicEngine, benchmark::State& state, Property* progressProp) + { + while (state.KeepRunning()) + { + for (int i = 0; i < animationIterations; ++i) + { + progressProp->set(float(i) / animationIterations); + if (!logicEngine.update()) + state.SkipWithError("failure running update()"); + } + } + } + + static void RunScript(benchmark::State& state, const std::string& src) + { + LogicEngine logicEngine; + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Base); + auto* script = logicEngine.createLuaScript(src, config); + if (!script) + { + state.SkipWithError("Script creation failed"); + return; + } + auto* timeProp = script->getInputs()->getChild("time"); + + while (state.KeepRunning()) + { + auto i = animationIterations; + float time = 0.0f; + while ((i--) != 0) + { + time += 0.015f; + timeProp->set(time); + if (!logicEngine.update()) + { + state.SkipWithError("failure running update()"); + } + } + } + } + + static void BM_AnimationScriptLinear(benchmark::State& state) + { + const std::string src = fmt::format(R"( + function interface(IN,OUT) + IN.time = Type:Float() + OUT.value = Type:Vec3f() + end + function run(IN,OUT) + local z = 0 + for i = 0,{},1 do + z = 360 * IN.time / 1.5 + OUT.value = {{0, 0, z}} + end + end + )", state.range(0)); + RunScript(state, src); + } + + static void BM_AnimationScriptKeyframes(benchmark::State& state) + { + const std::string src = fmt::format(R"( + function interface(IN,OUT) + IN.time = Type:Float() + OUT.value = Type:Vec3f() + end + function run(IN,OUT) + local z = 0 + for i = 0,{},1 do + GLOBAL.pos = IN.time + if GLOBAL.pos < 0.5 then + z = 360 * GLOBAL.pos / 0.5 + elseif GLOBAL.pos < 1.0 then + z = 180 - 80 * (GLOBAL.pos - 0.5) / 1.0 + elseif GLOBAL.pos < 1.5 then + z = 100 + 260 * (GLOBAL.pos - 1) / 0.5 + end + OUT.value = {{0, 0, z}} + end + end + )", state.range(0)); + RunScript(state, src); + } + + static void BM_AnimationLinear(benchmark::State& state) + { + LogicEngine logicEngine; + const auto* animTimestamps = logicEngine.createDataArray(std::vector{ 0.f, 1.5f }); // will be interpreted as seconds + const auto* animKeyframes = logicEngine.createDataArray(std::vector{ {0.f, 0.f, 0.f}, {0.f, 0.f, 360.f} }); + const ramses::AnimationChannel channelLinear { "rotationZ", animTimestamps, animKeyframes, ramses::EInterpolationType::Linear }; + + AnimationNodeConfig config; + for (int64_t i = 0; i < state.range(0); ++i) + config.addChannel(channelLinear); + auto* node = logicEngine.createAnimationNode(config); + auto* progressProp = node->getInputs()->getChild("progress"); + + RunAnimation(logicEngine, state, progressProp); + } + + static void BM_AnimationKeyframes(benchmark::State& state) + { + LogicEngine logicEngine; + const auto* animTimestamps = logicEngine.createDataArray(std::vector{ 0.f, 0.5f, 1.f, 1.5f }); // will be interpreted as seconds + const auto* animKeyframes = logicEngine.createDataArray(std::vector{ {0.f, 0.f, 0.f}, {0.f, 0.f, 180.f}, {0.f, 0.f, 100.f}, {0.f, 0.f, 360.f} }); + const ramses::AnimationChannel channelLinear { "rotationZ", animTimestamps, animKeyframes, ramses::EInterpolationType::Linear }; + + AnimationNodeConfig config; + for (int64_t i = 0; i < state.range(0); ++i) + config.addChannel(channelLinear); + auto* node = logicEngine.createAnimationNode(config); + auto* progressProp = node->getInputs()->getChild("progress"); + + RunAnimation(logicEngine, state, progressProp); + } + + static void BM_AnimationKeyframesCubic(benchmark::State& state) + { + LogicEngine logicEngine; + const auto* animTimestamps = logicEngine.createDataArray(std::vector{ 0.f, 0.5f, 1.f, 1.5f }); // will be interpreted as seconds + const auto* animKeyframes = logicEngine.createDataArray(std::vector{ {0.f, 0.f, 0.f}, {0.f, 0.f, 180.f}, {0.f, 0.f, 100.f}, {0.f, 0.f, 360.f} }); + const auto* cubicAnimTangentsIn = logicEngine.createDataArray(std::vector{ {0.f, 0.f, 0.f}, { 0.f, 0.f, 0.f }, { 0.f, 0.f, 0.f }, { 0.f, 0.f, 0.f } }); + const auto* cubicAnimTangentsOut = logicEngine.createDataArray(std::vector{ {0.f, 0.f, 0.f}, { 0.f, 0.f, 0.f }, { 0.f, 0.f, 0.f }, { 0.f, 0.f, 0.f } }); + const ramses::AnimationChannel channelCubic { "rotationZcubic", animTimestamps, animKeyframes, ramses::EInterpolationType::Cubic, cubicAnimTangentsIn, cubicAnimTangentsOut }; + + AnimationNodeConfig config; + for (int64_t i = 0; i < state.range(0); ++i) + config.addChannel(channelCubic); + auto* node = logicEngine.createAnimationNode(config); + auto* progressProp = node->getInputs()->getChild("progress"); + + RunAnimation(logicEngine, state, progressProp); + } + + // Compares animation objects with animations done in lua + // ARG: number of animation channels + BENCHMARK(BM_AnimationScriptLinear)->Arg(1)->Arg(10); + BENCHMARK(BM_AnimationScriptKeyframes)->Arg(1)->Arg(10); + BENCHMARK(BM_AnimationLinear)->Arg(1)->Arg(10); + BENCHMARK(BM_AnimationKeyframes)->Arg(1)->Arg(10); + BENCHMARK(BM_AnimationKeyframesCubic)->Arg(1)->Arg(10); +} + diff --git a/benchmarks/compile_lua.cpp b/benchmarks/compile_lua.cpp new file mode 100644 index 000000000..7a8c4bf18 --- /dev/null +++ b/benchmarks/compile_lua.cpp @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "benchmark/benchmark.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "fmt/format.h" + +namespace ramses +{ + static void CompileLua(LogicEngine& logicEngine, std::string_view src, const LuaConfig& config) + { + LuaScript* script = logicEngine.createLuaScript(src, config); + logicEngine.destroy(*script); + } + + static void BM_CompileLua_Interface(benchmark::State& state) + { + LogicEngine logicEngine; + + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Base); + + const int64_t scriptSize = state.range(0); + + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + for i = 0,{},1 do + IN["param"..tostring(i)] = Type:Int32() + end + end + function run(IN,OUT) + end + )", scriptSize); + + for (auto _ : state) // NOLINT(clang-analyzer-deadcode.DeadStores) False positive + { + CompileLua(logicEngine, scriptSrc, config); + } + } + + // Measures compilation times depending on the number of inputs in the interface + // ARG: number of inputs in script's interface() + BENCHMARK(BM_CompileLua_Interface)->Arg(1)->Arg(10)->Arg(100); +} + + diff --git a/benchmarks/getproperty.cpp b/benchmarks/getproperty.cpp new file mode 100644 index 000000000..ed6cb881d --- /dev/null +++ b/benchmarks/getproperty.cpp @@ -0,0 +1,411 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "benchmark/benchmark.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" +#include "impl/LogicEngineImpl.h" +#include "internals/SolWrapper.h" +#include "fmt/format.h" + +namespace ramses +{ + static void Run(benchmark::State& state, const std::string& src) + { + LogicEngine logicEngine; + + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Base); + + logicEngine.createLuaScript(src, config); + logicEngine.m_impl->disableTrackingDirtyNodes(); + + while (state.KeepRunning()) + { + if (!logicEngine.update()) + { + state.SkipWithError("failure running update()"); + } + } + } + + static void BM_GetPropertyLocal(benchmark::State& state) + { + const int64_t scriptSize = state.range(0); + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + for i = 0,{},1 do + IN["param"..tostring(i)] = Type:Int32() + end + OUT.param = Type:Int32() + end + function run(IN,OUT) + local result = 0 + local p0 = IN.param0 + local p1 = IN.param{} + for i = 0,10000,1 do + result = result + p0 + p1 + end + OUT.param = result + end + )", scriptSize, scriptSize); + Run(state, scriptSrc); + } + + static void BM_GetPropertyGlobal(benchmark::State& state) + { + const int64_t scriptSize = state.range(0); + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + for i = 0,{},1 do + IN["param"..tostring(i)] = Type:Int32() + end + OUT.param = Type:Int32() + end + function run(IN,OUT) + local result = 0 + GLOBAL.p0 = IN.param0 + GLOBAL.p1 = IN.param{} + for i = 0,10000,1 do + result = result + GLOBAL.p0 + GLOBAL.p1 + end + OUT.param = result + end + )", scriptSize, scriptSize); + Run(state, scriptSrc); + } + + static void BM_GetProperty(benchmark::State& state) + { + const int64_t scriptSize = state.range(0); + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + for i = 0,{},1 do + IN["param"..tostring(i)] = Type:Int32() + end + OUT.param = Type:Int32() + end + function run(IN,OUT) + local result = 0 + for i = 0,10000,1 do + result = result + IN.param0 + IN.param{} + end + OUT.param = result + end + )", scriptSize, scriptSize); + Run(state, scriptSrc); + } + + static void BM_GetPropertyNested(benchmark::State& state) + { + const int64_t scriptSize = state.range(0); + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + for i = 0,{},1 do + IN["param"..tostring(i)] = {{ + deeply = {{ + nested = {{ + val = Type:Int32() + }} + }} + }} + end + OUT.param = Type:Int32() + end + function run(IN,OUT) + local result = 0 + for i = 0,10000,1 do + result = result + IN.param0.deeply.nested.val + IN.param{}.deeply.nested.val + end + OUT.param = result + end + )", scriptSize, scriptSize); + Run(state, scriptSrc); + } + + struct Userdata + { + inline static const char* const name = "Userdata"; + int param = 0; + int param0 = 0; + int param1 = 1; + int param2 = 2; + int param3 = 3; + int param4 = 4; + int param5 = 5; + int param6 = 6; + int param7 = 7; + int param8 = 8; + int param9 = 9; + int param10 = 10; + int param100 = 100; + + int getValue(const std::string& strKey) + { + int retval = -1; + if (strKey == "param0") + { + retval = param0; + } + else if (strKey == "param10") + { + retval = param10; + } + else if (strKey == "param100") + { + retval = param100; + } + return retval; + } + + void setValue(const std::string& strKey, int value) + { + if (strKey == "param") + { + param = value; + } + } + + sol::object index(sol::stack_object key, sol::this_state L) + { + auto strKey = key.as(); + return sol::object(L, sol::in_place, getValue(strKey)); + } + + void new_index(const sol::object& key, const sol::object& value) + { + auto strKey = key.as(); + auto intValue = value.as(); + setValue(strKey, intValue); + } + + static int lua_index(lua_State* L) + { + // 1st param (self) + auto* userdata = static_cast(luaL_checkudata(L, 1, Userdata::name)); + // 2nd param (key) + const std::string key = luaL_checkstring(L, 2); + // 1 return value + lua_pushinteger(L, userdata->getValue(key)); + return 1; + } + + static int lua_new_index(lua_State* L) + { + // 1st param (self) + auto* userdata = static_cast(luaL_checkudata(L, 1, Userdata::name)); + // 2nd param (key) + const std::string key = luaL_checkstring(L, 2); + // 3rd param (value) + auto value = static_cast(luaL_checkinteger(L, 3)); + userdata->setValue(key, value); + return 0; + } + }; + + static void BM_GetSolUserdataBind(benchmark::State& state) + { + const int64_t scriptSize = state.range(0); + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + for i = 0,{},1 do + IN["param"..tostring(i)] = Type:Int32() + end + OUT.param = Type:Int32() + end + function run(IN,OUT) + local result = 0 + for i = 0,10000,1 do + result = result + IN.param0 + IN.param{} + end + OUT.param = result + end + )", scriptSize, scriptSize); + + + Userdata userdata; + auto sol = sol::state(); + sol.new_usertype( + Userdata::name, + sol::no_constructor, + "param", + &Userdata::param, + "param0", + &Userdata::param0, + "param1", + &Userdata::param1, + "param2", + &Userdata::param2, + "param3", + &Userdata::param3, + "param4", + &Userdata::param4, + "param5", + &Userdata::param5, + "param6", + &Userdata::param6, + "param7", + &Userdata::param7, + "param8", + &Userdata::param8, + "param9", + &Userdata::param9, + "param10", + &Userdata::param10, + "param100", + &Userdata::param100 + ); + + auto result = sol.script(scriptSrc); + if (!result.valid()) + { + sol::error err = result; + state.SkipWithError(err.what()); + } + + while (state.KeepRunning()) + { + result = sol["run"](std::ref(userdata), std::ref(userdata)); + if (!result.valid()) + { + sol::error err = result; + state.SkipWithError(err.what()); + } + } + } + + static void BM_GetSolUserdataIndex(benchmark::State& state) + { + const int64_t scriptSize = state.range(0); + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + for i = 0,{},1 do + IN["param"..tostring(i)] = Type:Int32() + end + OUT.param = Type:Int32() + end + function run(IN,OUT) + local result = 0 + for i = 0,10000,1 do + result = result + IN.param0 + IN.param{} + end + OUT.param = result + end + )", scriptSize, scriptSize); + + + Userdata userdata; + auto sol = sol::state(); + sol.new_usertype( + Userdata::name, + sol::no_constructor, + sol::meta_function::index, + &Userdata::index, + sol::meta_function::new_index, + &Userdata::new_index + ); + + auto result = sol.script(scriptSrc); + if (!result.valid()) + { + sol::error err = result; + state.SkipWithError(err.what()); + } + + while (state.KeepRunning()) + { + result = sol["run"](std::ref(userdata), std::ref(userdata)); + if (!result.valid()) + { + sol::error err = result; + state.SkipWithError(err.what()); + } + } + } + + static void BM_GetLuaUserdataIndex(benchmark::State& state) + { + const int64_t scriptSize = state.range(0); + const std::string scriptSrc = fmt::format(R"( + function interface() + for i = 0,{},1 do + IN["param"..tostring(i)] = Type:Int32() + end + OUT.param = Type:Int32() + end + function run() + local result = 0 + for i = 0,10000,1 do + result = result + IN.param0 + IN.param{} + end + OUT.param = result + end + )", scriptSize, scriptSize); + + auto sol = sol::state(); + auto L = sol.lua_state(); + { + // Register Userdata type + // NOLINTNEXTLINE(modernize-avoid-c-arrays) C array for C API + static struct luaL_Reg userdata_methods[] = {{"__index", Userdata::lua_index}, {"__newindex", Userdata::lua_new_index}, {nullptr, nullptr}}; + luaL_newmetatable(L, Userdata::name); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + luaL_setfuncs(L, userdata_methods, 0); + lua_pop(L, 1); + } + { + // sol["IN"] + void* buf = lua_newuserdata(L, sizeof(Userdata)); + new (buf) Userdata(); + luaL_getmetatable(L, "Userdata"); + lua_setmetatable(L, -2); + lua_setglobal(L, "IN"); + } + { + // sol["OUT"] + void* buf = lua_newuserdata(L, sizeof(Userdata)); + new (buf) Userdata(); + luaL_getmetatable(L, "Userdata"); + lua_setmetatable(L, -2); + lua_setglobal(L, "OUT"); + } + + auto result = sol.script(scriptSrc); + if (!result.valid()) + { + sol::error err = result; + state.SkipWithError(err.what()); + } + + while (state.KeepRunning()) + { + result = sol["run"](); + if (!result.valid()) + { + sol::error err = result; + state.SkipWithError(err.what()); + } + } + } + + // Compares direct access to properties vs. access to a local or global lua variable + // ARG: number of inputs in script's interface() + BENCHMARK(BM_GetPropertyLocal)->Arg(1)->Arg(10)->Arg(100)->Unit(benchmark::TimeUnit::kMillisecond); + BENCHMARK(BM_GetPropertyGlobal)->Arg(1)->Arg(10)->Arg(100)->Unit(benchmark::TimeUnit::kMillisecond); + BENCHMARK(BM_GetProperty)->Arg(1)->Arg(10)->Arg(100)->Unit(benchmark::TimeUnit::kMillisecond); + BENCHMARK(BM_GetPropertyNested)->Arg(1)->Arg(10)->Arg(100)->Unit(benchmark::TimeUnit::kMillisecond); + // for comparison: Simple userdata with pure sol + BENCHMARK(BM_GetSolUserdataIndex)->Arg(1)->Arg(10)->Arg(100)->Unit(benchmark::TimeUnit::kMillisecond); + BENCHMARK(BM_GetSolUserdataBind)->Arg(1)->Arg(10)->Arg(100)->Unit(benchmark::TimeUnit::kMillisecond); + BENCHMARK(BM_GetLuaUserdataIndex)->Arg(1)->Arg(10)->Arg(100)->Unit(benchmark::TimeUnit::kMillisecond); +} + + diff --git a/benchmarks/links.cpp b/benchmarks/links.cpp new file mode 100644 index 000000000..fbd0bf9b6 --- /dev/null +++ b/benchmarks/links.cpp @@ -0,0 +1,113 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "benchmark/benchmark.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "impl/LogicEngineImpl.h" +#include "ramses-logic/Property.h" + +#include "fmt/format.h" + +namespace ramses +{ + static void BM_Links_CreateDestroyLink(benchmark::State& state) + { + LogicEngine logicEngine; + + const int64_t propertyCount = state.range(0); + + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + for i = 0,{},1 do + IN["target"..tostring(i)] = Type:Int32() + OUT["src"..tostring(i)] = Type:Int32() + end + end + function run(IN,OUT) + end + )", propertyCount); + + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Base); + + LuaScript* srcScript = logicEngine.createLuaScript(scriptSrc, config); + LuaScript* destScript = logicEngine.createLuaScript(scriptSrc, config); + const Property* from = srcScript->getOutputs()->getChild("src0"); + Property* to = destScript->getInputs()->getChild("target0"); + + logicEngine.m_impl->disableTrackingDirtyNodes(); + for (auto _ : state) // NOLINT(clang-analyzer-deadcode.DeadStores) False positive + { + logicEngine.link(*from, *to); + logicEngine.unlink(*from, *to); + logicEngine.update(); + } + } + + // Measures time to create and immediately destroy a link between two scripts. + // The benchmark does both (create AND destroy) to ensure that the context is the same for the + // entire measurement, as opposed to when creating new links and incrementing the total link count + // ARG: property count in the script (measures decrease in link time depending on number of properties per script) + BENCHMARK(BM_Links_CreateDestroyLink)->Arg(1)->Arg(10)->Arg(100)->Arg(1000); + + static void BM_Links_CreateDestroyLink_ManyScripts(benchmark::State& state) + { + LogicEngine logicEngine; + + const std::size_t scriptCount = static_cast(state.range(0)); + + const std::string scriptSrc = R"( + function interface(IN,OUT) + for i = 0,20,1 do + IN["dest"..tostring(i)] = Type:Int32() + OUT["src"..tostring(i)] = Type:Int32() + end + end + function run(IN,OUT) + end + )"; + + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Base); + + std::vector scripts(scriptCount); + for (std::size_t i = 0; i < scriptCount; ++i) + { + scripts[i] = logicEngine.createLuaScript(scriptSrc, config); + + if (i >= 1) + { + for (std::size_t link = 0; link < 20; ++link) + { + auto target = scripts[i]->getInputs()->getChild(fmt::format("dest{}", link)); + auto src = scripts[i-1]->getOutputs()->getChild(fmt::format("src{}", link)); + logicEngine.link(*src, *target); + } + } + } + + const std::size_t halfwayIndex = scriptCount/2; + const Property* srcPropertyInTheMiddle = scripts[halfwayIndex]->getOutputs()->getChild("src10"); + const Property* destPropertyInTheMiddle = scripts[halfwayIndex+1]->getInputs()->getChild("dest10"); + + logicEngine.m_impl->disableTrackingDirtyNodes(); + for (auto _ : state) // NOLINT(clang-analyzer-deadcode.DeadStores) False positive + { + logicEngine.unlink(*srcPropertyInTheMiddle, *destPropertyInTheMiddle); + logicEngine.link(*srcPropertyInTheMiddle, *destPropertyInTheMiddle); + logicEngine.update(); + } + } + + // Same as BM_Links_CreateDestroyLink, but tests with many scripts (how fast is link (re)creation depending on scripts count) + // ARG: script count + BENCHMARK(BM_Links_CreateDestroyLink_ManyScripts)->Arg(8)->Arg(32)->Arg(128); +} + diff --git a/benchmarks/property.cpp b/benchmarks/property.cpp new file mode 100644 index 000000000..0c189f254 --- /dev/null +++ b/benchmarks/property.cpp @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "benchmark/benchmark.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" + +#include "impl/LogicEngineImpl.h" +#include "fmt/format.h" + +namespace ramses +{ + static void BM_Property_SetIntValue(benchmark::State& state) + { + LogicEngine logicEngine; + + const int64_t propertyCount = state.range(0); + + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + for i = 0,{},1 do + IN["param"..tostring(i)] = Type:Int32() + end + end + function run(IN,OUT) + end + )", propertyCount); + + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Base); + LuaScript* script = logicEngine.createLuaScript(scriptSrc, config); + Property* property = script->getInputs()->getChild("param0"); + // Need different value, otherwise triggers internal caching (can't disable value check) + int32_t increasingValue = 0; + + for (auto _ : state) // NOLINT(clang-analyzer-deadcode.DeadStores) False positive + { + property->set(increasingValue++); + } + } + + // Measures time to set the value of a property to script based on how many properties are there in the script's interface() + // ARG: how many properties are in the script's interface + BENCHMARK(BM_Property_SetIntValue)->Arg(10)->Arg(100)->Arg(1000); +} + + diff --git a/benchmarks/serialization.cpp b/benchmarks/serialization.cpp new file mode 100644 index 000000000..b0726a392 --- /dev/null +++ b/benchmarks/serialization.cpp @@ -0,0 +1,105 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "benchmark/benchmark.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/Logger.h" + +#include "fmt/format.h" +#include + +namespace ramses +{ + static std::vector CreateLargeLogicEngineBuffer(std::string_view fileName, std::size_t scriptCount) + { + Logger::SetLogVerbosityLimit(ELogLevel::Off); + + LogicEngine logicEngine; + + const std::string scriptSrc = R"( + function interface(IN,OUT) + for i = 0,20,1 do + IN["dest"..tostring(i)] = Type:Int32() + OUT["src"..tostring(i)] = Type:Int32() + end + end + function run(IN,OUT) + end + )"; + + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Base); + + std::vector scripts(scriptCount); + for (std::size_t i = 0; i < scriptCount; ++i) + { + scripts[i] = logicEngine.createLuaScript(scriptSrc, config); + + if (i >= 1) + { + for (std::size_t link = 0; link < 20; ++link) + { + auto target = scripts[i]->getInputs()->getChild(fmt::format("dest{}", link)); + auto src = scripts[i - 1]->getOutputs()->getChild(fmt::format("src{}", link)); + logicEngine.link(*src, *target); + } + } + } + + SaveFileConfig configNoValidation; + configNoValidation.setValidationEnabled(false); + logicEngine.saveToFile(fileName, configNoValidation); + + std::ifstream fileStream(std::string(fileName), std::ifstream::binary); + fileStream.seekg(0, std::ios::end); + std::vector byteBuffer(static_cast(fileStream.tellg())); + fileStream.seekg(0, std::ios::beg); + fileStream.read(byteBuffer.data(), static_cast(byteBuffer.size())); + return byteBuffer; + } + + static void BM_LoadFromBuffer_WithVerifier(benchmark::State& state) + { + Logger::SetLogVerbosityLimit(ELogLevel::Off); + + const std::size_t scriptCount = static_cast(state.range(0)); + + const std::vector buffer = CreateLargeLogicEngineBuffer("largeFile.bin", scriptCount); + + for (auto _ : state) // NOLINT(clang-analyzer-deadcode.DeadStores) False positive + { + LogicEngine logicEngine; + logicEngine.loadFromBuffer(buffer.data(), buffer.size(), nullptr, true); + } + } + + // ARG: script count + BENCHMARK(BM_LoadFromBuffer_WithVerifier)->Arg(8)->Arg(32)->Arg(128)->Unit(benchmark::kMicrosecond); + + static void BM_LoadFromBuffer_WithoutVerifier(benchmark::State& state) + { + Logger::SetLogVerbosityLimit(ELogLevel::Off); + + const std::size_t scriptCount = static_cast(state.range(0)); + + const std::vector buffer = CreateLargeLogicEngineBuffer("largeFile.bin", scriptCount); + + for (auto _ : state) // NOLINT(clang-analyzer-deadcode.DeadStores) False positive + { + LogicEngine logicEngine; + logicEngine.loadFromBuffer(buffer.data(), buffer.size(), nullptr, false); + } + } + + // ARG: script count + BENCHMARK(BM_LoadFromBuffer_WithoutVerifier)->Arg(8)->Arg(32)->Arg(128)->Unit(benchmark::kMicrosecond); +} + diff --git a/benchmarks/update.cpp b/benchmarks/update.cpp new file mode 100644 index 000000000..2fb1ad4f9 --- /dev/null +++ b/benchmarks/update.cpp @@ -0,0 +1,203 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "benchmark/benchmark.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" + +#include "impl/LogicEngineImpl.h" +#include "fmt/format.h" + +namespace ramses +{ + static void BM_Update_AssignProperty(benchmark::State& state) + { + LogicEngine logicEngine; + + const int64_t loopCount = state.range(0); + + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + IN.param = Type:Int32() + OUT.param = Type:Int32() + end + function run(IN,OUT) + for i = 0,{},1 do + OUT.param = IN.param + end + end + )", loopCount); + + logicEngine.createLuaScript(scriptSrc); + + logicEngine.m_impl->disableTrackingDirtyNodes(); + for (auto _ : state) // NOLINT(clang-analyzer-deadcode.DeadStores) False positive + { + logicEngine.update(); + } + } + + // Measures update() speed based on how many properties are assigned in the script's run() method + // Dirty handling: off + // ARG: how many assignments are made in run() to the same property + BENCHMARK(BM_Update_AssignProperty)->Arg(1)->Arg(10)->Arg(100)->Arg(1000); + + static void BM_Update_AssignStruct(benchmark::State& state) + { + LogicEngine logicEngine; + + const int64_t loopCount = state.range(0); + + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + IN.struct = {{ + int = Type:Int32(), + float = Type:Float(), + vec4f = Type:Vec4f(), + nested = {{ + int = Type:Int32(), + float = Type:Float(), + vec4f = Type:Vec4f() + }} + }} + OUT.struct = {{ + int = Type:Int32(), + float = Type:Float(), + vec4f = Type:Vec4f(), + nested = {{ + int = Type:Int32(), + float = Type:Float(), + vec4f = Type:Vec4f() + }} + }} + end + function run(IN,OUT) + for i = 0,{},1 do + OUT.struct = IN.struct + end + end + )", loopCount); + + logicEngine.createLuaScript(scriptSrc); + + logicEngine.m_impl->disableTrackingDirtyNodes(); + for (auto _ : state) // NOLINT(clang-analyzer-deadcode.DeadStores) False positive + { + logicEngine.update(); + } + } + + // Same as BM_Update_AssignProperty, but with structs + BENCHMARK(BM_Update_AssignStruct)->Arg(1)->Arg(10)->Arg(100)->Arg(1000)->Unit(benchmark::kMicrosecond); + + static void BM_Update_AssignArray(benchmark::State& state) + { + LogicEngine logicEngine; + + const int64_t loopCount = state.range(0); + + const std::string scriptSrc = fmt::format(R"( + function interface(IN,OUT) + IN.array = Type:Array(255, Type:Vec4f()) + OUT.array = Type:Array(255, Type:Vec4f()) + end + function run(IN,OUT) + for i = 0,{},1 do + OUT.array = IN.array + end + end + )", loopCount); + + logicEngine.createLuaScript(scriptSrc); + + logicEngine.m_impl->disableTrackingDirtyNodes(); + for (auto _ : state) // NOLINT(clang-analyzer-deadcode.DeadStores) False positive + { + logicEngine.update(); + } + } + + // Same as BM_Update_AssignProperty, but with arrays + BENCHMARK(BM_Update_AssignArray)->Arg(1)->Arg(10)->Arg(100)->Arg(1000)->Unit(benchmark::kMicrosecond); + + // Test that update is faster when fewer scripts have to be updated (i.e. the dirty handling works + // as expected). This benchmark creates a static list of linearly linked scripts so that if the + // first in the list has its 'dirty_trigger' value changed, all scripts in the change will be + // triggered for re-execution, whereas setting the trigger on the last script will only have the + // last script executed + // Read the results like this: the higher the arg (0 .. 99) the faster the update should be, ideally + // when the arg is close to 99, the time to update should be close to zero + static void BM_Update_IsFasterWithFewerDirtyScripts(benchmark::State& state) + { + LogicEngine logicEngine; + + const std::size_t scriptToSetDirty = static_cast(state.range(0)); + constexpr int64_t scriptCount = 100; + + const std::string scriptSrc = R"( + function interface(IN,OUT) + IN.dirty_trigger = Type:Int32() + IN.lots_of_data = {} + OUT.lots_of_data = {} + for i = 0,10,1 do + IN.lots_of_data["arr"..tostring(i)] = Type:Array(10, Type:Int32()) + OUT.lots_of_data["arr"..tostring(i)] = Type:Array(10, Type:Int32()) + end + end + function run(IN,OUT) + -- heavy data operation (deep copy of struct of arrays) + OUT.lots_of_data = IN.lots_of_data + -- trigger the dirty mechanism by updating one of the values based on the 'dirty trigger' + OUT.lots_of_data.arr0[1] = IN.lots_of_data.arr0[1] + IN.dirty_trigger + end + )"; + + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Base); + + // To make sure there were no API errors + bool success = false; + (void)success; + std::vector scripts(scriptCount); + for (std::size_t i = 0; i < scriptCount; ++i) + { + scripts[i] = logicEngine.createLuaScript(scriptSrc, config, fmt::format("script{}", i)); + + if (i >= 1) + { + for (std::size_t link = 0; link < 10; ++link) + { + auto target = scripts[i]->getInputs()->getChild("lots_of_data")->getChild(fmt::format("arr{}", link)); + auto src = scripts[i - 1]->getOutputs()->getChild("lots_of_data")->getChild(fmt::format("arr{}", link)); + + for (std::size_t array_element = 0; array_element < 10; ++array_element) + { + success = logicEngine.link(*src->getChild(array_element), *target->getChild(array_element)); + assert(success); + } + } + } + } + + Property* dirtyTrigger = scripts[scriptToSetDirty]->getInputs()->getChild("dirty_trigger"); + int32_t valueForDirtyTriggering = 1; + for (auto _ : state) // NOLINT(clang-analyzer-deadcode.DeadStores) False positive + { + success = dirtyTrigger->set(valueForDirtyTriggering++); + assert(success); + success = logicEngine.update(); + assert(success); + } + } + + BENCHMARK(BM_Update_IsFasterWithFewerDirtyScripts)->Arg(0)->Arg(49)->Arg(99)->Unit(benchmark::kMillisecond); +} + + diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 000000000..ebd0bf451 --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1,215 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2018 BMW Car IT GmbH +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +file(GLOB + RAMSES_CLIENT_FILES_SOURCE + ramses-client/impl/*.cpp + ramses-client/impl/glslEffectBlock/*.cpp + ramses-client/impl/ClientCommands/*.cpp + ramses-client-api/*.cpp) + +file(GLOB + RAMSES_CLIENT_API_INCLUDE_BASE + ramses-client-api/include) + +add_library(ramses-client-api INTERFACE) + +if(ramses-sdk_TEXT_SUPPORT) + set(client_text_dependencies + freetype + harfbuzz) + set(client_text_includes + ramses-client/impl/ramses-text/*.h + ramses-text-api/include/*.h + ramses-text-api/include/ramses-text-api/*.h + ) + + file(GLOB + RAMSES_CLIENT_TEXT_FILES_SOURCE + ramses-client/impl/ramses-text/*.cpp + ramses-text-api/*.cpp) + file(GLOB RAMSES_CLIENT_TEXT_API_INCLUDE_BASE + ramses-text-api/include) + file(GLOB + RAMSES_CLIENT_TEXT_TEST_FILES_SOURCE + test/text/*.cpp) + file(GLOB + RAMSES_CLIENT_TEXT_TEST_FILES_INCLUDES + test/text/*.h) + + target_compile_definitions(ramses-client-api INTERFACE RAMSES_TEXT_ENABLED) +else() + set(client_text_dependencies "") + set(client_text_includes "") + + set(RAMSES_CLIENT_TEXT_FILES_SOURCE "") + set(RAMSES_CLIENT_TEXT_API_INCLUDE_BASE "") + set(RAMSES_CLIENT_TEXT_TEST_FILES_SOURCE "") + set(RAMSES_CLIENT_TEXT_TEST_FILES_INCLUDES "") +endif() + + +# Logic hacky integration TODO fix and improve + +if(NOT ramses-sdk_ENABLE_LOGIC) + set(ramses_logic_api_include_base "") + set(ramses_logic_internal_includes "") + set(ramses_logic_src_files "") + set(ramses_logic_deps "") +else() + if(ramses-sdk_ENABLE_FLATBUFFERS_GENERATION) + include(${CMAKE_CURRENT_SOURCE_DIR}/logic/cmake/flatbuffersGeneration.cmake) + endif() + + file(GLOB + ramses_logic_api_include_base + logic/include) + + set(ramses_logic_internal_includes + logic/include + logic/lib + logic/lib/flatbuffers + ) + + file(GLOB logic_public_headers logic/include/ramses-logic/*.h) + file(GLOB logic_impl_headers logic/lib/impl/*.h) + file(GLOB logic_internal_headers logic/lib/internals/*.h) + file(GLOB logic_flatbuf_gen_headers logic/lib/flatbuffers/generated/*.h) + file(GLOB logic_flatbuf_schemas logic/lib/flatbuffers/schemas/*.fbs) + + file(GLOB logic_impl_src logic/lib/impl/*.cpp) + file(GLOB logic_internals_src logic/lib/internals/*.cpp) + + # Attach sol headers to ramses-logic-obj to be able to navigate/debug with them + # Workaround for MSVC (ignores interface targets otherwise if not attached to a non-interface target) + file(GLOB logic_sol_headers + ${PROJECT_SOURCE_DIR}/external/sol/include/sol/*.hpp + ${PROJECT_SOURCE_DIR}/external/sol/include/sol/compatibility/* + ) + + set(ramses_logic_src_files + ${logic_public_headers} + ${logic_impl_headers} + ${logic_internal_headers} + ${logic_flatbuf_gen_headers} + ${logic_flatbuf_schemas} + ${logic_impl_src} + ${logic_internals_src} + ) + + # This is the only robust way to add files to a VS project with CMake + # without having them treated as source files + set_source_files_properties(${logic_flatbuf_gen_headers} PROPERTIES HEADER_FILE_ONLY TRUE) + set_source_files_properties(${logic_flatbuf_schemas} PROPERTIES HEADER_FILE_ONLY TRUE) + set_source_files_properties(${logic_sol_headers} PROPERTIES HEADER_FILE_ONLY TRUE) + + set(ramses_logic_deps + sol2::sol2 + lua::lua + ramses::flatbuffers + fmt::fmt + ) + + + file(GLOB logic_tests_src + logic/unittests/api/*.cpp + logic/unittests/api/*.h + logic/unittests/internal/*.cpp + logic/unittests/internal/*.h + logic/unittests/shared/*.cpp + logic/unittests/shared/*.h + ) + + set(logic_tests_include_dirs + logic/lib/flatbuffers + logic/lib + logic/unittests/shared + ) + + set(logic_tests_res + logic/unittests/res + ) + + # TODO fix this, doesn't work with the ramses build system... + #if(TARGET FlatbufGen) + # set(ramses_logic_deps ${ramses_logic_deps} FlatbufGen) + #endif() +endif() + + +target_include_directories(ramses-client-api INTERFACE ${RAMSES_CLIENT_API_INCLUDE_BASE} ${RAMSES_CLIENT_TEXT_API_INCLUDE_BASE} ${ramses_logic_api_include_base}) + +createModule( + NAME ramses-client + TYPE STATIC_LIBRARY + ENABLE_INSTALL OFF + INCLUDE_PATHS ramses-client/impl + ${ramses_logic_internal_includes} + SRC_FILES ramses-client/impl/*.h + ramses-client/impl/glslEffectBlock/*.h + ramses-client/impl/ClientCommands/*.h + ramses-client-api/include/*.h + ramses-client-api/include/ramses-client-api/*.h + ${RAMSES_CLIENT_FILES_SOURCE} + ${RAMSES_CLIENT_TEXT_FILES_SOURCE} + ${ramses_logic_src_files} + ${client_text_includes} + DEPENDENCIES ramses-client-api + ramses-framework + ramses-glslang + ${client_text_dependencies} + ${ramses_logic_deps} +) + +if(ramses-sdk_ENABLE_INSTALL) + install(DIRECTORY ramses-client-api/include/ DESTINATION "${RAMSES_INSTALL_HEADERS_PATH}" COMPONENT ramses-sdk-devel) + if(ramses-sdk_TEXT_SUPPORT) + install(DIRECTORY ramses-text-api/include/ DESTINATION "${RAMSES_INSTALL_HEADERS_PATH}" COMPONENT ramses-sdk-devel) + endif() + if(ramses-sdk_ENABLE_LOGIC) + install(DIRECTORY logic/include/ DESTINATION "${RAMSES_INSTALL_HEADERS_PATH}" COMPONENT ramses-sdk-devel) + endif() +endif() + +if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + target_compile_options(ramses-client PRIVATE "-Wsuggest-override") +endif() + + +if (${ramses-sdk_BUILD_TESTS}) + createModule( + NAME ramses-client-test + TYPE BINARY + INCLUDE_PATHS test + ${logic_tests_include_dirs} + SRC_FILES test/*.h + test/ClientCommands/*.h + ${RAMSES_CLIENT_TEXT_TEST_FILES_INCLUDES} + test/*.cpp + ${RAMSES_CLIENT_TEXT_TEST_FILES_SOURCE} + test/ClientCommands/*.cpp + ${logic_tests_src} + RESOURCE_FOLDERS test/res + ${logic_tests_res} + DEPENDENCIES ramses-client + FrameworkTestUtils + ramses-gmock-main + ) + + makeTestFromTarget( + TARGET ramses-client-test + SUFFIX UNITTEST) + +endif() + +set(ramses-shared-lib-MIXIN + ${ramses-shared-lib-MIXIN} + INCLUDE_PATHS ${RAMSES_CLIENT_API_INCLUDE_BASE} ${RAMSES_CLIENT_TEXT_API_INCLUDE_BASE} ${ramses_logic_api_include_base} + SRC_FILES ${RAMSES_CLIENT_FILES_SOURCE} ${RAMSES_CLIENT_TEXT_FILES_SOURCE} ${ramses_logic_src_files} + DEPENDENCIES ramses-client + CACHE INTERNAL "") diff --git a/client/logic/.clang-tidy b/client/logic/.clang-tidy new file mode 100644 index 000000000..c8130c25f --- /dev/null +++ b/client/logic/.clang-tidy @@ -0,0 +1,506 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +--- +Checks: + '-*, + + boost-use-to-string, + + # We are not using abseil + #abseil-*, + + # Android specific checks + #android-*, + + # Coding style, projects can optionally enable this. + #bugprone-argument-comment, + bugprone-assert-side-effect, + bugprone-bad-signal-to-kill-thread, + bugprone-bool-pointer-implicit-conversion, + # Incompatible with C++ attribute fallthrough in switch/case statements. + #bugprone-branch-clone, + bugprone-copy-constructor-init, + bugprone-dangling-handle, + bugprone-dynamic-static-initializers, + bugprone-exception-escape, + bugprone-fold-init-type, + bugprone-forward-declaration-namespace, + bugprone-forwarding-reference-overload, + bugprone-inaccurate-erase, + bugprone-incorrect-roundings, + bugprone-infinite-loop, + bugprone-integer-division, + bugprone-lambda-function-name, + bugprone-macro-parentheses, + bugprone-macro-repeated-side-effects, + bugprone-misplaced-operator-in-strlen-in-alloc, + bugprone-misplaced-widening-cast, + bugprone-move-forwarding-reference, + bugprone-multiple-statement-macro, + # Alias: cppcoreguidelines-narrowing-conversions + #bugprone-narrowing-conversions, + bugprone-not-null-terminated-result, + bugprone-parent-virtual-call, + bugprone-posix-return, + bugprone-signed-char-misuse, + bugprone-sizeof-container, + bugprone-sizeof-expression, + bugprone-string-constructor, + bugprone-string-integer-assignment, + bugprone-string-literal-with-embedded-nul, + bugprone-suspicious-enum-usage, + bugprone-suspicious-memset-usage, + bugprone-suspicious-missing-comma, + bugprone-suspicious-semicolon, + bugprone-suspicious-string-compare, + bugprone-swapped-arguments, + bugprone-terminating-continue, + bugprone-throw-keyword-missing, + bugprone-too-small-loop-variable, + bugprone-undefined-memory-manipulation, + bugprone-undelegated-constructor, + bugprone-unhandled-self-assignment, + bugprone-unused-raii, + bugprone-unused-return-value, + bugprone-use-after-move, + bugprone-virtual-near-miss, + + # Alias: misc-static-assert + #cert-dcl03-c, + cert-dcl16-c, + cert-dcl21-cpp, + cert-dcl50-cpp, + # Alias: misc-new-delete-overloads + #cert-dcl54-cpp, + cert-dcl58-cpp, + cert-dcl59-cpp, + cert-env33-c, + # Alias: misc-throw-by-value-catch-by-reference + #cert-err09-cpp, + cert-err34-c, + cert-err52-cpp, + # This prevents defining any global const that is not a POD (std::string/vectors/...) + #cert-err58-cpp, + cert-err60-cpp, + # Alias: misc-throw-by-value-catch-by-reference + #cert-err61-cpp, + # Alias: misc-non-copyable-objects + #cert-fio38-c, + #cert-flp30-c, + cert-mem57-cpp, + # Alias: cert-msc50-cpp + #cert-msc30-c, + # Alias: cert-msc51-cpp + #cert-msc32-c, + cert-msc50-cpp, + cert-msc51-cpp, + # Alias: performance-move-constructor-init + #cert-oop11-cpp, + # Alias: bugprone-unhandled-self-assignment + #cert-oop54-cpp, + cert-oop58-cpp, + # Alias: bugprone-bad-signal-to-kill-thread + #cert-pos44-c + + # No documentation available + #clang-analyzer-apiModeling.google.GTest, + # No documentation available + #clang-analyzer-apiModeling.llvm.CastValue, + # No documentation available + #clang-analyzer-apiModeling.llvm.ReturnValue, + clang-analyzer-apiModeling.StdCLibraryFunctions, + # No documentation available + #clang-analyzer-apiModeling.TrustNonnull, + clang-analyzer-core.CallAndMessage, + clang-analyzer-core.DivideZero, + clang-analyzer-core.DynamicTypePropagation, + clang-analyzer-core.NonNullParamChecker, + clang-analyzer-core.NonnilStringConstants, + clang-analyzer-core.NullDereference, + clang-analyzer-core.StackAddrEscapeBase, + clang-analyzer-core.StackAddressEscape, + clang-analyzer-core.UndefinedBinaryOperatorResult, + clang-analyzer-core.VLASize, + clang-analyzer-core.builtin.BuiltinFunctions, + clang-analyzer-core.builtin.NoReturnFunctions, + clang-analyzer-core.uninitialized.ArraySubscript, + clang-analyzer-core.uninitialized.Assign, + clang-analyzer-core.uninitialized.Branch, + clang-analyzer-core.uninitialized.CapturedBlockVariable, + clang-analyzer-core.uninitialized.UndefReturn, + clang-analyzer-cplusplus.InnerPointer, + clang-analyzer-cplusplus.Move, + clang-analyzer-cplusplus.NewDelete, + clang-analyzer-cplusplus.NewDeleteLeaks, + clang-analyzer-cplusplus.PureVirtualCall, + clang-analyzer-cplusplus.SelfAssignment, + clang-analyzer-cplusplus.SmartPtr, + clang-analyzer-cplusplus.VirtualCallModeling, + clang-analyzer-deadcode.DeadStores, + # We are not using Fuchsia + #clang-analyzer-fuchsia.HandleChecker, + # ObjectiveC only + #clang-analyzer-nullability.*, + clang-analyzer-optin.cplusplus.UninitializedObject, + clang-analyzer-optin.cplusplus.VirtualCall, + # We are not using the MPI library + #clang-analyzer-optin.mpi.*, + # We are not developing for osx + #clang-analyzer-optin.osx.*, + # We are not using GCD + #clang-analyzer-optin.performance.GCDAntipattern, + clang-analyzer-optin.performance.Padding, + clang-analyzer-optin.portability.UnixAPI, + # We are not using std::experimental::simd + #clang-analyzer-optin.portability-simd-intrinsics, + # We are not developing for osx + #clang-analyzer-osx.*, + clang-analyzer-security.FloatLoopCounter, + clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, + clang-analyzer-security.insecureAPI.SecuritySyntaxChecker, + clang-analyzer-security.insecureAPI.UncheckedReturn, + clang-analyzer-security.insecureAPI.bcmp, + clang-analyzer-security.insecureAPI.bcopy, + clang-analyzer-security.insecureAPI.bzero, + # We are not using ObjectiveC + #clang-analyzer-security.insecureAPI.decodeValueOfObjCType, + clang-analyzer-security.insecureAPI.getpw, + clang-analyzer-security.insecureAPI.gets, + clang-analyzer-security.insecureAPI.mkstemp, + clang-analyzer-security.insecureAPI.mktemp, + clang-analyzer-security.insecureAPI.rand, + clang-analyzer-security.insecureAPI.strcpy, + clang-analyzer-security.insecureAPI.vfork, + clang-analyzer-unix.API, + clang-analyzer-unix.DynamicMemoryModeling, + clang-analyzer-unix.Malloc, + clang-analyzer-unix.MallocSizeof, + clang-analyzer-unix.MismatchedDeallocator, + clang-analyzer-unix.Vfork, + clang-analyzer-unix.cstring.BadSizeArg, + clang-analyzer-unix.cstring.CStringModeling, + clang-analyzer-unix.cstring.NullArg, + clang-analyzer-valist.CopyToSelf, + clang-analyzer-valist.Uninitialized, + clang-analyzer-valist.Unterminated, + clang-analyzer-valist.ValistBase, + + # Alias: modernize-avoid-c-arrays + #cppcoreguidelines-avoid-c-arrays, + cppcoreguidelines-avoid-goto, + # Alias: readability-magic-numbers + #cppcoreguidelines-avoid-magic-numbers, + cppcoreguidelines-c-copy-assignment-signature, + # Alias: modernize-use-override + #cppcoreguidelines-explicit-virtual-functions, + cppcoreguidelines-init-variables, + cppcoreguidelines-interfaces-global-init, + # This check does not work well with exporting API symbols + #cppcoreguidelines-macro-usage, + cppcoreguidelines-narrowing-conversions, + cppcoreguidelines-no-malloc, + # Alias: misc-non-private-member-variables-in-classes + #cppcoreguidelines-non-private-member-variables-in-classes, + # We are not using GSL + #cppcoreguidelines-owning-memory, + # Many false positives (for example in MGU NaRE::logInfo) + #cppcoreguidelines-pro-bounds-array-to-pointer-decay, + # Many false positives + #cppcoreguidelines-pro-bounds-constant-array-index, + cppcoreguidelines-pro-bounds-pointer-arithmetic, + cppcoreguidelines-pro-type-const-cast, + cppcoreguidelines-pro-type-cstyle-cast, + cppcoreguidelines-pro-type-member-init, + cppcoreguidelines-pro-type-reinterpret-cast, + cppcoreguidelines-pro-type-static-cast-downcast, + cppcoreguidelines-pro-type-union-access, + cppcoreguidelines-pro-type-vararg, + cppcoreguidelines-slicing, + # TODO Violin check if we can workaround these issues with our clang-tidy wrapper + # Google test templated tests TEST_P macros break this rule + #cppcoreguidelines-special-member-functions, + # Google test does not comply to this rule + #cppcoreguidelines-avoid-non-const-global-variables, + + # We are not using Darwin + #darwin-*, + + # We are not using Fuchsia + #fuchsia-*, + + google-build-explicit-make-pair, + # Alias: cert-dcl59-cpp + #google-build-namespaces, + # Coding style, projects can optionally enable this. + #google-build-using-namespace, + google-default-arguments, + google-explicit-constructor, + google-global-names-in-headers, + # We are not using ObjectiveC + #google-objc-*, + # Coding style, projects can optionally enable this. + #google-readability-avoid-underscore-in-googletest-name, + # Alias: readability-braces-around-statements + #google-readability-braces-around-statements, + google-readability-casting, + # Coding style, projects can optionally enable this. + #google-readability-function-size, + # Alias: llvm-namespace-comment + #google-readability-namespace-comments, + # Coding style, projects can optionally enable this. + #google-readability-todo, + google-runtime-int, + google-runtime-operator, + # Coding style, projects can optionally enable this. + #google-runtime-references, + # Coding style, projects can optionally enable this. + google-upgrade-googletest-case, + + # Alias: modernize-avoid-c-arrays + #hicpp-avoid-c-arrays, + hicpp-avoid-goto, + # Alias: readability-braces-around-statements + #hicpp-braces-around-statements, + # Alias: modernize-deprecated-headers + #hicpp-deprecated-headers, + # Coding style, projects can optionally enable this. + #hicpp-exception-baseclass, + # Alias: google-explicit-constructor + #hicpp-explicit-conversions, + # Alias: readability-function-size + #hicpp-function-size, + # Alias: bugprone-use-after-move + #hicpp-invalid-access-moved, + # Alias: cppcoreguidelines-pro-type-member-init + #hicpp-member-init, + hicpp-move-const-arg, + hicpp-multiway-paths-covered, + hicpp-named-parameter, + # Alias: misc-new-delete-overloads + #hicpp-new-delete-operators, + # Alias: cppcoreguidelines-pro-bounds-array-to-pointer-decay + #hicpp-no-array-decay, + hicpp-no-assembler, + hicpp-noexcept-move, + # Alias: cppcoreguidelines-no-malloc + #hicpp-no-malloc, + hicpp-signed-bitwise, + # Alias: cppcoreguidelines-special-member-functions + #hicpp-special-member-functions, + # Alias: misc-static-assert + #hicpp-static-assert, + # Alias: bugprone-undelegated-constructor + #hicpp-undelegated-constructor, + # Alias: readability-uppercase-literal-suffix + #hicpp-uppercase-literal-suffix, + # Alias: modernize-use-auto + #hicpp-use-auto, + # Alias: modernize-use-emplace + #hicpp-use-emplace, + # Alias: modernize-use-equals-default + #hicpp-use-equals-default, + # Alias: modernize-use-equals-delete + #hicpp-use-equals-delete, + # Alias: modernize-use-noexcept + #hicpp-use-noexcept, + # Alias: modernize-use-nullptr + #hicpp-use-nullptr, + # Alias: modernize-use-override + #hicpp-use-override, + # Alias: cppcoreguidelines-pro-type-vararg + #hicpp-vararg, + + # We are not working on the Linux kernel + #linuxkernel-*, + + # Not relevant for us, too llvm specific + #llvm-header-guard, + #llvm-include-order, + #llvm-namespace-comment, + #llvm-prefer-isa-or-dyn-cast-in-conditionals, + + # We are not using the LLVM::Register class + #llvm-prefer-register-over-unsigned, + # Alias: readability-qualified-auto + #llvm-qualified-auto, + # We are not using the LLVM::Twine class + #llvm-twine-local, + + misc-definitions-in-headers, + misc-misplaced-const, + misc-new-delete-overloads, + misc-non-copyable-objects, + # Coding style, projects can optionally enable this. + #misc-non-private-member-variables-in-classes, + misc-redundant-expression, + misc-static-assert, + misc-throw-by-value-catch-by-reference, + misc-unconventional-assign-operator, + misc-uniqueptr-reset-release, + misc-unused-alias-decls, + misc-unused-parameters, + misc-unused-using-decls, + + # Coding style, projects can optionally enable this. + #modernize-avoid-bind, + modernize-avoid-c-arrays, + # MGU code that uses Qt MOC cannot use nested namespaces. + #modernize-concat-nested-namespaces, + modernize-deprecated-ios-base-aliases, + modernize-deprecated-headers, + modernize-loop-convert, + modernize-make-shared, + modernize-make-unique, + modernize-pass-by-value, + modernize-raw-string-literal, + modernize-redundant-void-arg, + modernize-replace-auto-ptr, + modernize-replace-random-shuffle, + modernize-return-braced-init-list, + modernize-shrink-to-fit, + modernize-unary-static-assert, + modernize-use-auto, + modernize-use-bool-literals, + modernize-use-default-member-init, + modernize-use-emplace, + modernize-use-equals-default, + modernize-use-equals-delete, + modernize-use-nodiscard, + modernize-use-noexcept, + modernize-use-nullptr, + modernize-use-override, + # Coding style, projects can optionally enable this. + #modernize-use-trailing-return-type, + # Coding style, projects can optionally enable this. + #modernize-use-transparent-functors, + modernize-use-uncaught-exceptions, + modernize-use-using, + + # We are not using MPI + #mpi-*, + + # We are not using ObjectiveC + #objc-*, + + # We are not using OpenMP + #openmp-*, + + performance-faster-string-find, + performance-for-range-copy, + performance-implicit-conversion-in-loop, + performance-inefficient-algorithm, + performance-inefficient-string-concatenation, + performance-inefficient-vector-operation, + performance-move-const-arg, + performance-move-constructor-init, + performance-no-automatic-move, + performance-noexcept-move-constructor, + performance-trivially-destructible, + performance-type-promotion-in-math-fn, + performance-unnecessary-copy-initialization, + performance-unnecessary-value-param, + + # We are not using std::experimental::simd + #portability-simd-*, + + # Coding style, projects can optionally enable this. + #readability-avoid-const-params-in-decls, + readability-braces-around-statements, + readability-const-return-type, + readability-container-size-empty, + readability-convert-member-functions-to-static, + readability-delete-null-pointer, + readability-deleted-default, + readability-else-after-return, + # Coding style, projects can optionally enable this. + #readability-function-size, + # Coding style, projects can optionally enable this. + #readability-identifier-naming, + readability-implicit-bool-conversion, + readability-inconsistent-declaration-parameter-name, + readability-isolate-declaration, + # Coding style, projects can optionally enable this. + #readability-magic-numbers, + # Disabled because not working properly with public pimpl members + #readability-make-member-function-const, + readability-misleading-indentation, + readability-misplaced-array-index, + readability-named-parameter, + readability-non-const-parameter, + # Coding style, projects can optionally enable this. + #readability-qualified-auto, + # Coding style, projects can optionally enable this. + #readability-redundant-access-specifiers, + readability-redundant-control-flow, + readability-redundant-declaration, + readability-redundant-function-ptr-dereference, + readability-redundant-member-init, + readability-redundant-preprocessor, + readability-redundant-smartptr-get, + readability-redundant-string-cstr, + readability-redundant-string-init, + # Coding style, projects can optionally enable this. + #readability-simplify-boolean-expr, + readability-simplify-subscript-expr, + readability-static-accessed-through-instance, + readability-static-definition-in-anonymous-namespace, + readability-string-compare, + readability-uniqueptr-delete-release, + # Coding style, projects can optionally enable this. + #readability-uppercase-literal-suffix + + # We are not using the Zircon kernel + #zircon-temporary-objects +' + +# everything that is enabled is also an error +WarningsAsErrors: '*' + +HeaderFilterRegex: '.*' + +AnalyzeTemporaryDtors: false +FormatStyle: File + +CheckOptions: + # https://releases.llvm.org/9.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/bugprone-assert-side-effect.html#cmdoption-arg-assertmacros + - key: bugprone-assert-side-effect.AssertMacros + value: 'Q_ASSERT,Q_ASSERT_X,Q_CHECK_PTR' + # https://releases.llvm.org/9.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/bugprone-assert-side-effect.html#cmdoption-arg-checkfunctioncalls + - key: bugprone-assert-side-effect.CheckFunctionCalls + value: '1' + # https://releases.llvm.org/9.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/bugprone-sizeof-expression.html#cmdoption-arg-warnonsizeofintegerexpression + - key: bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression + value: '1' + # https://releases.llvm.org/9.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/bugprone-suspicious-string-compare.html#cmdoption-arg-warnonlogicalnotcomparison + - key: bugprone-suspicious-string-compare.WarnOnLogicalNotComparison + value: '1' + # https://releases.llvm.org/9.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/misc-throw-by-value-catch-by-reference.html#cmdoption-arg-warnonlargeobject + - key: misc-throw-by-value-catch-by-reference.WarnOnLargeObject + value: '1' + # https://releases.llvm.org/9.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/cppcoreguidelines-narrowing-conversions.html#cmdoption-arg-pedanticmode + - key: cppcoreguidelines-narrowing-conversions.PedanticMode + value: '1' + # https://releases.llvm.org/9.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/cppcoreguidelines-special-member-functions.html#cmdoption-arg-allowsoledefaultdtor + - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctions + value: '1' + # https://releases.llvm.org/9.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/cppcoreguidelines-special-member-functions.html#cmdoption-arg-allowsoledefaultdtor + - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor + value: '1' + # https://releases.llvm.org/9.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/hicpp-multiway-paths-covered.html#cmdoption-arg-warnonmissingelse + - key: hicpp-multiway-paths-covered.WarnOnMissingElse + value: '0' + # https://releases.llvm.org/9.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/readability-implicit-bool-conversion.html#cmdoption-arg-allowpointerconditions + - key: readability-implicit-bool-conversion.AllowPointerConditions + value: '1' + # https://clang.llvm.org/extra/clang-tidy/checks/readability-braces-around-statements.html + - key: readability-braces-around-statements.ShortStatementLines + value: '2' + - key: hicpp-signed-bitwise.IgnorePositiveIntegerLiterals + value: true +... diff --git a/client/logic/cmake/flatbuffersGeneration.cmake b/client/logic/cmake/flatbuffersGeneration.cmake new file mode 100644 index 000000000..951801bab --- /dev/null +++ b/client/logic/cmake/flatbuffersGeneration.cmake @@ -0,0 +1,71 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +# This CMake file creates a target FlatbufGen which re-generates flatbuffers +# header files from schemas and puts results back in the source tree +# +# Works like this: +# Case #1: with code generation (default) +# cmake # Creates FlatbufGen and FlatbufCheck targets based on *.fbs files +# make # generates *_gen.h files from *.fbs files and overwrites source tree. +# # If a new *.fbs file was added, has to re-run cmake (as with any other src files) +# # If existing *.fbs file changes, regenerates ALL *_gen.h files and rebuilds anything that depends on them +# # If no change in *.fbs files, does nothing +# +# Case #2: no code generation (explicitly disabled) +# cmake -Dramses-sdk_ENABLE_FLATBUFFERS_GENERATION=OFF +# make # Uses checked-in *_gen.h files and ignores any changes in *.fbs files +# make FlatbufGen # Fails - no such targets because disabled + +file(GLOB flatbuffers_schemas "${PROJECT_SOURCE_DIR}/client/logic/lib/flatbuffers/schemas/*.fbs") +set(flatbuffers_output_dir "${PROJECT_SOURCE_DIR}/client/logic/lib/flatbuffers/generated") + +# create list of *_gen.h out of *.fbs file list +foreach(schema ${flatbuffers_schemas}) + get_filename_component(filename ${schema} NAME_WE) + list(APPEND generated_headers "${flatbuffers_output_dir}/${filename}Gen.h") +endforeach() + +# TODO Violin/Tobias investigate the "--conform" option of flatc - looks very promising +# as a solution for backwards compatibility which can be automatically checked! + +# Additional interesting options to investigate in the future: +# --reflect-types/--reflect-names - maybe for type backwards compatibility +# --root-type - maybe not specify the root type in schemas, but from outside +# --force-empty-vectors - avoid nullptr +# --flexbuffers - schemaless buffers + +# Custom command which: +# - deletes all generated headers in source TARGET_FILE +# - regenerates headers from current *.fbs files in source tree +add_custom_command( + OUTPUT ${generated_headers} + COMMAND ${CMAKE_COMMAND} -E remove_directory ${flatbuffers_output_dir} + COMMAND ${CMAKE_COMMAND} -E make_directory ${flatbuffers_output_dir} + COMMAND $ + -o ${flatbuffers_output_dir}/ + --cpp ${flatbuffers_schemas} + --filename-suffix Gen + --filename-ext h + --scoped-enums + --reflect-names + --no-prefix + --cpp-std c++17 + DEPENDS ${flatbuffers_schemas} $ + COMMENT "Generating flatbuffers header files from schemas" + VERBATIM +) + +add_custom_target(FlatbufGen + DEPENDS ${generated_headers} +) + +set_target_properties(FlatbufGen PROPERTIES + FOLDER "CMakePredefinedTargets") + +message(STATUS " + FlatbufGen") diff --git a/client/logic/cmake/platformConfig.cmake b/client/logic/cmake/platformConfig.cmake new file mode 100644 index 000000000..b3d1a47ce --- /dev/null +++ b/client/logic/cmake/platformConfig.cmake @@ -0,0 +1,209 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +# let cmake know c++ version +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + + +# set additional flags dependent on used compiler and version + +# TODO Violin/Tobias which of this stuff can be modernized? + +# helper function to add flags +FUNCTION(ADD_FLAGS VAR) + SET(TMP "${${VAR}}") + FOREACH(flags ${ARGN}) + SET(TMP "${TMP} ${flags}") + ENDFOREACH() + SET(${VAR} ${TMP} PARENT_SCOPE) +ENDFUNCTION() + +FUNCTION(REMOVE_FROM_FLAGS flags toRemoveList outVar) + string(REGEX REPLACE " +" ";" flags_LIST "${flags}") # to list + list(REMOVE_ITEM flags_LIST ${toRemoveList}) # filter list + string(REPLACE ";" " " flags_filtered "${flags_LIST}") # to string + set(${outVar} "${flags_filtered}" PARENT_SCOPE) +ENDFUNCTION() + +# variables to fill +SET(RLOGIC_C_CXX_FLAGS) +SET(RLOGIC_C_FLAGS) +SET(RLOGIC_CXX_FLAGS) +SET(RLOGIC_DEBUG_FLAGS) +SET(RLOGIC_DEBUG_INFO_FLAGS) +SET(RLOGIC_RELEASE_FLAGS) + +# gcc OR clang (they share a lot) +IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-fPIC -pthread") + if (NOT ramses-logic_DISABLE_SYMBOL_VISIBILITY) + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-fvisibility=hidden") + endif() + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-Wall -Wextra -Wcast-align -Wshadow -Wformat -Wformat-security -Wvla -Wmissing-include-dirs") + ADD_FLAGS(RLOGIC_CXX_FLAGS "-std=c++17 -Wnon-virtual-dtor -Woverloaded-virtual -Wold-style-cast") + ADD_FLAGS(RLOGIC_C_FLAGS "-std=c11") + ADD_FLAGS(RLOGIC_DEBUG_FLAGS "-ggdb -D_DEBUG -fno-omit-frame-pointer") + ADD_FLAGS(RLOGIC_RELEASE_FLAGS "-O2 -DNDEBUG -fstack-protector-strong -D_FORTIFY_SOURCE=2") + ADD_FLAGS(RLOGIC_DEBUG_INFO_FLAGS "-ggdb -fno-omit-frame-pointer") + + if (ramses-logic_WARNINGS_AS_ERRORS) + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-Werror") + endif() +ENDIF() + +# gcc specific +IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + # optimize for debuggability + ADD_FLAGS(RLOGIC_DEBUG_FLAGS "-Og") + + # remap GOT readonly after resolving + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-Wl,-z,relro,-z,now") + + if (ramses-sdk_BUILD_WITH_LTO) + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-flto -Wodr -Wlto-type-mismatch") + endif() + + # gcc specific warnings + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-Wformat-signedness") + # disable too crazy optimizations causing problems + ADD_FLAGS(RLOGIC_RELEASE_FLAGS "-fno-ipa-cp-clone") + + IF(NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0) + # disable unfixed warnings from gcc 7 + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-Wno-stringop-overflow -Wno-implicit-fallthrough") + + # enable more warnings on newer gcc + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-Wformat-overflow -Wfree-nonheap-object") + ENDIF() + + # disable unfixed warnings from gcc 8 + IF(NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 8.0) + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-Wno-cast-function-type") + ENDIF() +ENDIF() + +# clang specific +IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 10.0) + message(FATAL_ERROR "Clang versions prior 10.0 are not supported") + endif() + + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-Wimplicit-fallthrough") + + # do not optimize debug build at all (-Og is wrong on clang) + ADD_FLAGS(RLOGIC_DEBUG_FLAGS "-O0") + + IF(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 3.5) + # suppress missing override keyword warning + ADD_FLAGS(RLOGIC_CXX_FLAGS "-Winconsistent-missing-override -Wmove") + ENDIF() + + # handle enable coverage + if (ramses-logic_ENABLE_TEST_COVERAGE) + message(STATUS "+ Test coverage (clang)") + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-fprofile-instr-generate") + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "-fcoverage-mapping") + endif() +ENDIF() + +IF(ramses-logic_ENABLE_TEST_COVERAGE AND NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + message(FATAL_ERROR "Can't enable test coverage for compilers different than clang!") +ENDIF() + +# flags for windows +IF(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + REMOVE_FROM_FLAGS("${CMAKE_CXX_FLAGS}" "/W1;/W2;/W3;/W4" CMAKE_CXX_FLAGS) + + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "/MP /DNOMINMAX") + ADD_FLAGS(RLOGIC_CXX_FLAGS "/std:c++17 /W4 /wd4503 /wd4265 /wd4201 /wd4127 /wd4996 /bigobj") + ADD_FLAGS(RLOGIC_RELEASE_FLAGS "/MD /O2 /Ob2 /DNDEBUG") + ADD_FLAGS(RLOGIC_DEBUG_FLAGS "/MDd /Zi /Od /RTC1 /D_DEBUG") + ADD_FLAGS(RLOGIC_DEBUG_INFO_FLAGS "/Zi") + + if (ramses-logic_WARNINGS_AS_ERRORS) + ADD_FLAGS(RLOGIC_C_CXX_FLAGS "/WX") + endif() + ADD_DEFINITIONS("-D_WIN32_WINNT=0x0600" "-DWINVER=0x0600") # enable 'modern' windows APIs +ENDIF() + +IF(${CMAKE_SYSTEM_NAME} MATCHES "Android") + SET(ENV{PKG_CONFIG_PATH} "") + SET(ENV{PKG_CONFIG_LIBDIR} "${CMAKE_SYSROOT}/usr/lib/pkgconfig:${CMAKE_SYSROOT}/usr/share/pkgconfig") + SET(ENV{PKG_CONFIG_SYSROOT_DIR} ${CMAKE_SYSROOT}) +ENDIF() + +# Special handling for C++17 filesystem +# Unfortunately not trivial, please keep this CMake config in one place! + +function(check_handle_special_filesystem_lib libname) + include(CheckCXXSourceCompiles) + set(CMAKE_REQUIRED_LIBRARIES ${libname}) + check_cxx_source_compiles("int main() {}" RLOGIC_HAS_STD_FS) + + if (RLOGIC_HAS_STD_FS) + link_libraries(${libname}) + else() + if (ramses-sdk_BUILD_TESTS) + message(FATAL_ERROR "std::filesystem libary not found. Cannot use emulation with tests enabled") + endif() + if (NOT CMAKE_SYSTEM_NAME STREQUAL Linux) + message(FATAL_ERROR "std::filesystem libary not found. Can use emulation only on Linux") + endif() + + message(STATUS "std::filesystem libary not found, enable emulation") + add_definitions("-DRLOGIC_STD_FILESYSTEM_EMULATION") + endif() +endfunction() + +# GCC prior version 9.1 puts filesystem in a separate static lib (stdc++fs) and potentially in the experimental namespace +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.1) + check_handle_special_filesystem_lib(stdc++fs) + # gcc prior version 8 puts symbols in the experimental namespace + if(RLOGIC_HAS_STD_FS AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 8) + add_definitions("-DRLOGIC_STD_FILESYSTEM_EXPERIMENTAL") + endif() +endif() + +# llvm prior version 9 puts filesystem in a separate static lib, similar but not exactly the same as GCC above +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9) + # libc++ requires different settings than stdlibc++ + # check if libc++ is used by inspecting global flags + string(FIND "${CMAKE_CXX_FLAGS}" "-stdlib=libc++" USES_LIBCXX) + if(USES_LIBCXX EQUAL -1) + # Link stdc++fs from the std lib + check_handle_special_filesystem_lib(stdc++fs) + # llvm prior version 7 puts symbols in the experimental namespace + if(RLOGIC_HAS_STD_FS AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7) + add_definitions("-DRLOGIC_STD_FILESYSTEM_EXPERIMENTAL") + endif() + else() + message(STATUS "Detected usage of libc++, using libc++ specific compiler flags") + # See docs for more details https://libcxx.llvm.org/docs/UsingLibcxx.html#using-libc-experimental-and-experimental + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7) + check_handle_special_filesystem_lib(c++experimental) + if (RLOGIC_HAS_STD_FS) + add_definitions("-DRLOGIC_STD_FILESYSTEM_EXPERIMENTAL") + endif() + else() + link_libraries(libc++fs) + endif() + endif() +endif() + +# distribute to the correct cmake variables +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${RLOGIC_CXX_FLAGS} ${RLOGIC_C_CXX_FLAGS} ") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${RLOGIC_C_FLAGS} ${RLOGIC_C_CXX_FLAGS} ") + +SET(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${RLOGIC_DEBUG_FLAGS}") +SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${RLOGIC_DEBUG_FLAGS}") +SET(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${RLOGIC_RELEASE_FLAGS}") +SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} ${RLOGIC_RELEASE_FLAGS}") +SET(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} ${RLOGIC_RELEASE_FLAGS} ${RLOGIC_DEBUG_INFO_FLAGS}") +SET(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} ${RLOGIC_RELEASE_FLAGS} ${RLOGIC_DEBUG_INFO_FLAGS}") diff --git a/client/logic/include/ramses-logic/AnchorPoint.h b/client/logic/include/ramses-logic/AnchorPoint.h new file mode 100644 index 000000000..7188dff56 --- /dev/null +++ b/client/logic/include/ramses-logic/AnchorPoint.h @@ -0,0 +1,100 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicNode.h" +#include + +namespace ramses +{ + class Node; + class Camera; +} + +namespace ramses::internal +{ + class AnchorPointImpl; +} + +namespace ramses +{ + /** + * Anchor point is a #ramses::LogicNode which calculates viewport coordinates (and depth) of a Ramses node's origin. + * The projected coordinates are accessible via its output property and can be linked to another logic node. + * Anchor point requires a #ramses::RamsesNodeBinding and a #ramses::RamsesCameraBinding at creation time, see #ramses::LogicEngine::createAnchorPoint. + * + * The update logic retrieves a model matrix from the provided Ramses node (via its binding), a projection and view matrix from the provided + * Ramses camera (via its binding) and projects a point (0, 0, 0) (which represents origin of the given node's local space) using those matrices: + * projectedPoint = projectionMatrix * viewMatrix * modelMatrix * (0, 0, 0, 1) + * Perspective correction will be applied (relevant only if using perspective camera) and then the result is transformed into viewport space of the given camera, + * i.e. the final values are coordinates within the camera viewport. + * + * This means that if the given Ramses node represents a transformation of a renderable mesh, the anchor point will calculate where in viewport + * (or on screen if viewport matches screen dimensions) the origin of the mesh would be rendered. This can be useful for example for 2D overlay + * graphical elements or text following a 3D object. + * + * - Property output: + * - viewportCoords (#ramses::EPropertyType::Vec2f) + * - provides [viewportCoordX, viewportCoordY] representing the [X,Y] coordinates of the projected node transformation in viewport space + * - note that it is up to user if and how he decides to round the floating point values to snap to discrete pixels + * - note that the coordinates will not be clamped to the viewport, so viewport coordinates can be negative + * or larger than viewport width/height if the tracked object is outside of frustum + * - depth (float) + * - non-linear depth in [0,1] range (if within frustum), 0 at near plane, 1 at far plane of the camera frustum + * (equivalent to the actual value stored in depth buffer for depth testing) + * - note that the depth can be outside of the [0,1] range if the tracked object is outside of frustum + * + * Important note on update order dependency: + * Unlike other logic nodes the calculation done inside #AnchorPoint does not depend on input properties but on states in Ramses scene instead + * - namely node transformations and camera settings. Imagine a case where #AnchorPoint tracks a Ramses node and this node's ancestor + * (in transformation topology) is being animated by a logic node (animation or script), i.e. the animation indirectly affects the result of #AnchorPoint. + * As this dependency is outside of Ramses logic network (it is in Ramses transformation topology) and identifying it in runtime + * (can change every frame) by querying Ramses is not feasible, the proper ordering of logic network update is NOT guaranteed in such case. + * It means the #AnchorPoint calculation might be executed first before the animation is and thus giving incorrect (old) result. + * If you end up using such setup, please use a workaround: UPDATE the #ramses::LogicEngine TWICE, right after each other in every frame/update iteration. + * + * Performance remark: + * Unlike other logic nodes #AnchorPoint does not use dirtiness mechanism monitoring node's inputs which then updates + * the outputs only if anything changed. Anchor point depends on Ramses objects and their states, which cannot be easily monitored + * and therefore it has to be updated every time #ramses::LogicEngine::update is called. For this reason it is highly recommended + * to keep the number of anchor nodes to a necessary minimum. + */ + class AnchorPoint : public LogicNode + { + public: + /** + * Returns given ramses node which is used to calculate coordinates. + * + * @return Ramses node to track + */ + [[nodiscard]] RAMSES_API const ramses::Node& getRamsesNode() const; + + /** + * Returns given ramses camera which is used to calculate coordinates. + * + * @return Ramses camera associated with node to track + */ + [[nodiscard]] RAMSES_API const ramses::Camera& getRamsesCamera() const; + + /** + * Implementation of AnchorPoint + */ + internal::AnchorPointImpl& m_anchorPointImpl; + + protected: + /** + * Constructor of AnchorPoint. User is not supposed to call this - AnchorPoints are created by other factory classes + * + * @param impl implementation details of the AnchorPoint + */ + explicit AnchorPoint(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/AnimationNode.h b/client/logic/include/ramses-logic/AnimationNode.h new file mode 100644 index 000000000..ce9a515e3 --- /dev/null +++ b/client/logic/include/ramses-logic/AnimationNode.h @@ -0,0 +1,97 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicNode.h" +#include "ramses-logic/AnimationTypes.h" +#include "ramses-logic/EPropertyType.h" +#include +#include + +namespace ramses::internal +{ + class AnimationNodeImpl; +} + +namespace ramses +{ + /** + * Animation node can be used to animate properties in logic network. + * Animation node itself is a logic node and has a set of input and output properties: + * - Fixed inputs: + * - progress (float) - point within [0;1] normalized range of where to jump to in the animation + * - in this range 0 marks the time 0 (regardless of timestamp of the first keyframe), + * and 1 marks the end of the animation determined by the duration of the animation + * - values outside of the [0;1] range are accepted and will be clamped + * - Fixed outputs: + * - duration (float) - total duration of the animation, determined by the highest timestamp from all its channels + * - is affected by modifications to timestamp data via properties + * (see #ramses::AnimationNodeConfig::setExposingOfChannelDataAsProperties) + * + * - Channel outputs: Each animation channel provided at creation time (#ramses::LogicEngine::createAnimationNode) + * will be represented as output property with name of the channel (#ramses::AnimationChannel::name) + * and a value of type matching element in #ramses::AnimationChannel::keyframes. + * If the data type of keyframes is #ramses::EPropertyType::Array (i.e. each keyframe is represented + * by an array of floats), the output property is also of array type and contains a corresponding + * number of children properties of type #ramses::EPropertyType::Float. + * Channel value output is a result of keyframes interpolation based on the 'progress' input above, + * it can be linked to another logic node input to use the animation result. + * + * - Channel data inputs (only if created with #ramses::AnimationNodeConfig::setExposingOfChannelDataAsProperties enabled): + * - channelsData (struct) - contains all channels and their data in a hierarchy. For each channel: + * - [channelName] (struct) + * - timestamps (array of float) - each element represents a timestamp value + * - keyframes (array of T) - each element represents a keyframe value + * - type T is data type matching this channel original keyframes + * + * During update when 'progress' input is set the following logic is executed: + * - calculate local animation time based on progress + * - for each channel: + * - lookup closest previous and next timestamp/keyframe pair according to the local animation time, + * - interpolate between them according to the interpolation type of that channel, + * - and finally set this value to the channel's output property. + * + * Note that all channel outputs will always have a value determined by corresponding keyframes, this includes + * also when the time falls outside of the first/last animation timestamps: + * - channel output value equals first keyframe for any time at or before the first keyframe timestamp + * - channel output value equals last keyframe for any time at or after the last keyframe timestamp + * This can be useful for example when needing to initialize the outputs before playing the animation yet, + * when updating the animation node with progress 0, the logic will execute and update outputs to their + * first keyframes. + */ + class AnimationNode : public LogicNode + { + public: + /** + * Returns channel data used in this animation (as provided at creation time #ramses::LogicEngine::createAnimationNode). + * + * Note that the retrieved data is not affected by any modifications via channel data input properties + * (see #ramses::AnimationNodeConfig::setExposingOfChannelDataAsProperties). If modifications were made, only corresponding + * properties hold the actual values used during animation. + * + * @return animation channels used in this animation. + */ + [[nodiscard]] RAMSES_API const AnimationChannels& getChannels() const; + + /** + * Implementation of AnimationNode + */ + internal::AnimationNodeImpl& m_animationNodeImpl; + + protected: + /** + * Constructor of AnimationNode. User is not supposed to call this - AnimationNodes are created by other factory classes + * + * @param impl implementation details of the AnimationNode + */ + explicit AnimationNode(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/AnimationNodeConfig.h b/client/logic/include/ramses-logic/AnimationNodeConfig.h new file mode 100644 index 000000000..32a5e5cc4 --- /dev/null +++ b/client/logic/include/ramses-logic/AnimationNodeConfig.h @@ -0,0 +1,115 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-logic/AnimationTypes.h" +#include + +namespace ramses::internal +{ + class AnimationNodeConfigImpl; +} + +namespace ramses +{ + /** + * Holds data and settings for #ramses::AnimationNode creation using #ramses::LogicEngine::createAnimationNode. + */ + class AnimationNodeConfig + { + public: + RAMSES_API AnimationNodeConfig() noexcept; + + /** + * Adds a data to animate. #ramses::AnimationNode can use multiple animation channels to animate, + * see #ramses::AnimationNode and #ramses::AnimationChannel for details. + * This call can fail, see #ramses::AnimationChannel for rules that channel data must follow. + * + * @param channelData channel data to add to configuration + * @return \c true if the channel was added successfully, \c false otherwise. + * In case of an error, check the logs. + */ + RAMSES_API bool addChannel(const AnimationChannel& channelData); + + /** + * Returns all channels data added to this #AnimationNodeConfig so far. + * + * @return animation channels in this #AnimationNodeConfig. + */ + [[nodiscard]] RAMSES_API const AnimationChannels& getChannels() const; + + /** + * If enabled, the created #ramses::AnimationNode will expose its basic channel data (timestamps and keyframes) in form of logic node properties. + * These properties can be set and linked allowing for control over actual animation data to be animated. The data can be adjusted at any point in time, + * also during playing the animation. + * + * This functionality comes at a price - there is performance overhead and strict limit of how many keyframes the animation can have. + * All timestamp/keyframe values are represented as child properties within an array, per channel, refer to #ramses::AnimationNode + * for the exact structure of these inputs. Processing the animation data in this form is not cheap compared to a regular animation node + * and there is a limit of 255 timestamps/keyframes any channel can have. Always try to minimize the amount of animation data when using + * this type of animation node. + * + * Note that all modifications to the animation data are considered temporary and will NOT be preserved when saving to a file. + * All values and corresponding properties will be reset to original values (provided in this #AnimationNodeConfig from when the #ramses::AnimationNode + * was created) when loading from a file. + * + * By default this feature is disabled. + * This feature cannot be enabled (this call will fail) if the #AnimationNodeConfig contains a channel with #ramses::DataArray using data type #ramses::EPropertyType::Array. + * + * @param enabled flag to enable or disable exposing of channels data as properties. + * @return \c true if enabled successfully, \c false otherwise. + * In case of an error, check the logs. + */ + RAMSES_API bool setExposingOfChannelDataAsProperties(bool enabled); + + /** + * Returns the currently set state of the exposing of channel data as properties. + * + * @return the currently set state of the exposing of channel data as properties. + */ + [[nodiscard]] RAMSES_API bool getExposingOfChannelDataAsProperties() const; + + /** + * Destructor of #AnimationNodeConfig + */ + RAMSES_API ~AnimationNodeConfig() noexcept; + + /** + * Copy Constructor of #AnimationNodeConfig + * @param other the other #AnimationNodeConfig to copy from + */ + RAMSES_API AnimationNodeConfig(const AnimationNodeConfig& other); + + /** + * Move Constructor of #AnimationNodeConfig + * @param other the other #AnimationNodeConfig to move from + */ + RAMSES_API AnimationNodeConfig(AnimationNodeConfig&& other) noexcept; + + /** + * Assignment operator of #AnimationNodeConfig + * @param other the other #AnimationNodeConfig to copy from + * @return self + */ + RAMSES_API AnimationNodeConfig& operator=(const AnimationNodeConfig& other); + + /** + * Move assignment operator of #AnimationNodeConfig + * @param other the other #AnimationNodeConfig to move from + * @return self + */ + RAMSES_API AnimationNodeConfig& operator=(AnimationNodeConfig&& other) noexcept; + + /** + * Implementation detail of #AnimationNodeConfig + */ + std::unique_ptr m_impl; + }; +} diff --git a/client/logic/include/ramses-logic/AnimationTypes.h b/client/logic/include/ramses-logic/AnimationTypes.h new file mode 100644 index 000000000..4dc3d4d31 --- /dev/null +++ b/client/logic/include/ramses-logic/AnimationTypes.h @@ -0,0 +1,70 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include +#include + +namespace ramses +{ + class DataArray; + + /** + * Interpolation types used for animations. + */ + enum class EInterpolationType : uint8_t + { + Step, ///< Switches the value in the middle between two keyframe values + Linear, ///< Interpolates using linear function + Cubic, ///< Interpolates using cubic function, see https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#interpolation-cubic + Linear_Quaternions, ///< Linear interpolation for vec4f values which are normalized for use as Quaternions after interpolation + Cubic_Quaternions, ///< Cubic interpolation for vec4f values which are normalized for use as Quaternions after interpolation + }; + + /** + * Animation channel data bundle. + * #timeStamps, #keyframes must always be provided, + * tangents #tangentsIn, #tangentsOut are mandatory only for #ramses::EInterpolationType::Cubic + * interpolation, ignored for other interpolation types. + * See GLTF 2.0 specification appendix C for details on tangents + * (https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#interpolation-cubic) + */ + struct AnimationChannel + { + /// Name of the channel for identification when linking + std::string name; + /// Timestamps, must be of type float and have strictly ascending order + const DataArray* timeStamps = nullptr; + /// Keyframes to interpolate from, must be same size as #timeStamps + const DataArray* keyframes = nullptr; + /// Type of interpolation for this channel + EInterpolationType interpolationType = EInterpolationType::Linear; + /// Tangents in (mandatory only for cubic interpolation), must be same type and size as #keyframes + const DataArray* tangentsIn = nullptr; + /// Tangents out (mandatory only for cubic interpolation), must be same type and size as #keyframes + const DataArray* tangentsOut = nullptr; + + /// Comparison operator + bool operator==(const AnimationChannel& rhs) const + { + return timeStamps == rhs.timeStamps + && keyframes == rhs.keyframes + && interpolationType == rhs.interpolationType + && tangentsIn == rhs.tangentsIn + && tangentsOut == rhs.tangentsOut; + } + /// Comparison operator + bool operator!=(const AnimationChannel& rhs) const + { + return !operator==(rhs); + } + }; + using AnimationChannels = std::vector; +} diff --git a/client/logic/include/ramses-logic/Collection.h b/client/logic/include/ramses-logic/Collection.h new file mode 100644 index 000000000..6708ff54b --- /dev/null +++ b/client/logic/include/ramses-logic/Collection.h @@ -0,0 +1,166 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-logic/Iterator.h" + +#include +#include +#include + +namespace ramses +{ + /** + * A Collection which allows STL-style algorithm with forward semantics to be executed (e.g. find_if, for_each etc.). + * A Collection cannot be instantiated directly, it can only be obtained using #ramses::LogicEngine::getCollection. + * + * See also #ramses::Iterator for more info on the iterator objects returned by #begin and #end. + * + * Template parameters: + * - T: the object type to iterate over, e.g. #ramses::LuaScript + */ + template + class Collection + { + private: + /** + * Internal container type. Not supposed to be used by user code in any way! + */ + using internal_container_type = std::vector; + public: + /** + * The iterator type returned by #begin() and #end() + */ + using iterator = Iterator; + + /** + * The iterator type returned by #cbegin() and #cend() + */ + using const_iterator = Iterator; + + /** + * The iterator type after dereferencing + */ + using value_type = typename iterator::value_type; + + /** + * Iterator value pointer type + */ + using pointer = typename iterator::pointer; + + /** + * Iterator value reference type + */ + using reference = typename iterator::reference; + + /** + * Return number of elements in the collection + * @return number of elements in the collection + */ + [[nodiscard]] size_t size() const + { + return m_container.get().size(); + } + + /** + * Return an iterator to the start of the collection + * @return iterator to the start of the collection + */ + [[nodiscard]] iterator begin() + { + return iterator(m_container.get().begin()); + } + + /** + * Return an const iterator to the start of the collection + * @return const iterator to the start of the collection + */ + [[nodiscard]] const_iterator begin() const + { + return iterator(m_container.get().begin()); + } + + /** + * Return an iterator to the end of the collection + * @return iterator to the end of the collection + */ + [[nodiscard]] iterator end() + { + return iterator(m_container.get().end()); + } + + /** + * Return a const iterator to the end of the collection + * @return const iterator to the end of the collection + */ + [[nodiscard]] const_iterator end() const + { + return const_iterator(m_container.get().cend()); + } + + /** + * Return a const iterator to the start of the collection + * @return const iterator to the of the collection + */ + [[nodiscard]] const_iterator cbegin() const + { + return const_iterator(m_container.get().cbegin()); + } + + /** + * Return a const iterator to the end of the collection + * @return const iterator to the end of the collection + */ + [[nodiscard]] const_iterator cend() const + { + return const_iterator(m_container.get().cend()); + } + + /** + * Default constructor is deleted because a collection is supposed to provide read-only access to + * internal data of the #ramses::LogicEngine class, therefore it can only be obtained by calling + * #ramses::LogicEngine::getCollection + */ + Collection() = delete; + + /** + * Default destructor + */ + ~Collection() noexcept = default; + + /** + * Default copy constructor + * @param other collection to copy + */ + Collection(const Collection& other) noexcept = default; + + /** + * Default assignment operator + * @param other collection to assign from + */ + Collection& operator=(const Collection& other) noexcept = default; + + /** + * Internal constructor. Not supposed to be called from user code! + * @param container internal container + */ + explicit Collection(internal_container_type& container) + : m_container(container) + { + } + + private: + /** + * Internal reference to the container holding the actual data held by the collection. + */ + std::reference_wrapper m_container; + }; + +} diff --git a/client/logic/include/ramses-logic/DataArray.h b/client/logic/include/ramses-logic/DataArray.h new file mode 100644 index 000000000..471435d2d --- /dev/null +++ b/client/logic/include/ramses-logic/DataArray.h @@ -0,0 +1,91 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicObject.h" +#include "ramses-logic/EPropertyType.h" + +#include +#include +#include + +namespace ramses::internal +{ + class DataArrayImpl; +} + +namespace ramses +{ + /** + * Storage for data - e.g. animation data like keyframes or timestamps. + */ + class DataArray : public LogicObject + { + public: + /** + * Returns the type of data stored in this #DataArray. + * + * @return the data type + */ + [[nodiscard]] RAMSES_API EPropertyType getDataType() const; + + /** + * Returns the data stored in this #DataArray. + * Make sure to use the right template type, query #getDataType + * to see what data type is stored (see also #ramses::PropertyTypeToEnum traits + * and #ramses::CanPropertyTypeBeStoredInDataArray). + * When called with an unsupported type, a compile-time assert is triggered. + * When called with a mismatching type (e.g. getData() when the type + * is vec4f) the method returns nullptr. + * + * @return vector of data or nullptr if incorrect template type is provided + */ + template + [[nodiscard]] const std::vector* getData() const; + + /** + * Returns the number of elements stored in this #DataArray. + * + * @return the number of elements + */ + [[nodiscard]] RAMSES_API size_t getNumElements() const; + + /** + * Implementation detail of DataArray + */ + internal::DataArrayImpl& m_impl; + + protected: + /** + * Constructor of DataArray. Use #ramses::LogicEngine::createDataArray. + * + * @param impl implementation details of the DataArray + */ + explicit DataArray(std::unique_ptr impl) noexcept; + + private: + /** + * Internal implementation of #getData. + * + * @return vector of data or nullptr if wrong template type provided + */ + template + [[nodiscard]] RAMSES_API const std::vector* getDataInternal() const; + + friend class internal::ApiObjects; + }; + + template + const std::vector* DataArray::getData() const + { + static_assert(CanPropertyTypeBeStoredInDataArray(PropertyTypeToEnum::TYPE), + "Unsupported data type, see createDataArray API doc to see supported types."); + return getDataInternal(); + } +} diff --git a/client/logic/include/ramses-logic/ELuaSavingMode.h b/client/logic/include/ramses-logic/ELuaSavingMode.h new file mode 100644 index 000000000..c27e074d4 --- /dev/null +++ b/client/logic/include/ramses-logic/ELuaSavingMode.h @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +namespace ramses +{ + /** + * Modes determining what data to store when serializing #ramses::LuaScript or #ramses::LuaModule. + * Each mode has different implications when it comes to exported file size, loading performance and compatibility. + */ + enum class ELuaSavingMode + { + /// Will store only source code as provided when script/module is created. + /// Will produce bigger file size and take longest to load (all code must be compiled from scratch) but does not rely on bytecode. + SourceCodeOnly, + /// Will store only bytecode generated from source code at creation time. + /// Will produce the smallest file size and fast loading speed but fully relies on bytecode being compatible on target platform. + ByteCodeOnly, + /// Will store both source code and generated bytecode. + /// Will produce the largest file size but allow fast loading speed if bytecode is compatible on target platform + /// and a fallback solution if not - Lua will be recompiled from source code. + SourceAndByteCode + }; +} diff --git a/client/logic/include/ramses-logic/EPropertyType.h b/client/logic/include/ramses-logic/EPropertyType.h new file mode 100644 index 000000000..bbb00808f --- /dev/null +++ b/client/logic/include/ramses-logic/EPropertyType.h @@ -0,0 +1,318 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-framework-api/DataTypes.h" +#include +#include +#include +#include + +namespace ramses +{ + /** + * #EPropertyType lists the types of properties created and managed by the #ramses::LogicNode + * class and its derivates. + */ + enum class EPropertyType : int + { + Float, ///< corresponds to float + Vec2f, ///< corresponds to [float, float] + Vec3f, ///< corresponds to [float, float, float] + Vec4f, ///< corresponds to [float, float, float, float] + Int32, ///< corresponds to int32_t + Int64, ///< corresponds to int64_t (note that Lua cannot handle 64bit integers in full range) + Vec2i, ///< corresponds to [int32_t, int32_t] + Vec3i, ///< corresponds to [int32_t, int32_t, int32_t] + Vec4i, ///< corresponds to [int32_t, int32_t, int32_t, int32_t] + Struct, ///< Has no value itself, but can have named child properties + String, ///< corresponds to std::string + Bool, ///< corresponds to bool + Array ///< Has no value itself, but can have unnamed child properties of homogeneous types (primitive or structs). + ///< When used in #ramses::DataArray this type refers to an array of float arrays (std::vector is the element type). + }; + + /** + * Type trait which converts C++ types to #ramses::EPropertyType enum for primitive types. + */ + template struct PropertyTypeToEnum; + + template <> struct PropertyTypeToEnum + { + static const EPropertyType TYPE = EPropertyType::Float; + }; + + template <> struct PropertyTypeToEnum + { + static const EPropertyType TYPE = EPropertyType::Vec2f; + }; + + template <> struct PropertyTypeToEnum + { + static const EPropertyType TYPE = EPropertyType::Vec3f; + }; + + template <> struct PropertyTypeToEnum + { + static const EPropertyType TYPE = EPropertyType::Vec4f; + }; + + template <> struct PropertyTypeToEnum + { + static const EPropertyType TYPE = EPropertyType::Int32; + }; + + template <> struct PropertyTypeToEnum + { + static const EPropertyType TYPE = EPropertyType::Int64; + }; + + template <> struct PropertyTypeToEnum + { + static const EPropertyType TYPE = EPropertyType::Vec2i; + }; + + template <> struct PropertyTypeToEnum + { + static const EPropertyType TYPE = EPropertyType::Vec3i; + }; + + template <> struct PropertyTypeToEnum + { + static const EPropertyType TYPE = EPropertyType::Vec4i; + }; + + template <> struct PropertyTypeToEnum + { + static const EPropertyType TYPE = EPropertyType::String; + }; + + template <> struct PropertyTypeToEnum + { + static const EPropertyType TYPE = EPropertyType::Bool; + }; + + template <> struct PropertyTypeToEnum> + { + static const EPropertyType TYPE = EPropertyType::Array; + }; + + /** + * Type trait which converts #ramses::EPropertyType enum to a C++ type. + */ + template struct PropertyEnumToType; + + template <> struct PropertyEnumToType + { + using TYPE = float; + }; + + template <> struct PropertyEnumToType + { + using TYPE = vec2f; + }; + + template <> struct PropertyEnumToType + { + using TYPE = vec3f; + }; + + template <> struct PropertyEnumToType + { + using TYPE = vec4f; + }; + + template <> struct PropertyEnumToType + { + using TYPE = int32_t; + }; + + template <> struct PropertyEnumToType + { + using TYPE = int64_t; + }; + + template <> struct PropertyEnumToType + { + using TYPE = vec2i; + }; + + template <> struct PropertyEnumToType + { + using TYPE = vec3i; + }; + + template <> struct PropertyEnumToType + { + using TYPE = vec4i; + }; + + template <> struct PropertyEnumToType + { + using TYPE = std::string; + }; + + template <> struct PropertyEnumToType + { + using TYPE = bool; + }; + + template <> struct PropertyEnumToType + { + using TYPE = std::vector; + }; + + /** + * Type trait which can be used to check if a type is primitive or not. + * "primitive" in this context means the type can be used as a template for + * #ramses::Property::set() and #ramses::Property::get(), i.e. it has a + * value which can be directly set/obtained, as opposed to non-primitive types + * like structs or arrays which don't have a singular settable value. + */ + template struct IsPrimitiveProperty + { + static const bool value = false; + }; + + template <> struct IsPrimitiveProperty + { + static const bool value = true; + }; + + template <> struct IsPrimitiveProperty + { + static const bool value = true; + }; + + template <> struct IsPrimitiveProperty + { + static const bool value = true; + }; + + template <> struct IsPrimitiveProperty + { + static const bool value = true; + }; + + template <> struct IsPrimitiveProperty + { + static const bool value = true; + }; + + template <> struct IsPrimitiveProperty + { + static const bool value = true; + }; + + template <> struct IsPrimitiveProperty + { + static const bool value = true; + }; + + template <> struct IsPrimitiveProperty + { + static const bool value = true; + }; + + template <> struct IsPrimitiveProperty + { + static const bool value = true; + }; + + template <> struct IsPrimitiveProperty + { + static const bool value = true; + }; + + template <> struct IsPrimitiveProperty + { + static const bool value = true; + }; + + /** + * Helper to determine if given property type can be stored in a #ramses::DataArray. + */ + constexpr bool CanPropertyTypeBeStoredInDataArray(EPropertyType type) + { + switch (type) + { + case EPropertyType::Float: + case EPropertyType::Vec2f: + case EPropertyType::Vec3f: + case EPropertyType::Vec4f: + case EPropertyType::Int32: + case EPropertyType::Vec2i: + case EPropertyType::Vec3i: + case EPropertyType::Vec4i: + /** + Arrays can be nested in other arrays, but only if their element type is Float + */ + case EPropertyType::Array: + return true; + case EPropertyType::Bool: + case EPropertyType::Struct: + case EPropertyType::String: + case EPropertyType::Int64: + return false; + } + + return false; + } + + /** + * Helper to determine if given property type can be animated using #ramses::AnimationNode. + */ + constexpr bool CanPropertyTypeBeAnimated(EPropertyType type) + { + // all data types that can be stored in DataArray can be animated + return CanPropertyTypeBeStoredInDataArray(type); + } + + /** + * Returns the string representation of a property type. This string corresponds to the syntax + * that has to be used in the Lua source code used to create scripts with properties with + * the corresponding type. If a type is declared as Type:T() in Lua, this function will return 'T' as string + */ + constexpr const char* GetLuaPrimitiveTypeName(EPropertyType type) + { + switch (type) + { + case EPropertyType::Float: + return "Float"; + case EPropertyType::Vec2f: + return "Vec2f"; + case EPropertyType::Vec3f: + return "Vec3f"; + case EPropertyType::Vec4f: + return "Vec4f"; + case EPropertyType::Int32: + return "Int32"; + case EPropertyType::Int64: + return "Int64"; + case EPropertyType::Vec2i: + return "Vec2i"; + case EPropertyType::Vec3i: + return "Vec3i"; + case EPropertyType::Vec4i: + return "Vec4i"; + case EPropertyType::Struct: + return "Struct"; + case EPropertyType::String: + return "String"; + case EPropertyType::Bool: + return "Bool"; + case EPropertyType::Array: + return "Array"; + } + return "Struct"; + } + + constexpr size_t MaxArrayPropertySize = 255u; +} diff --git a/client/logic/include/ramses-logic/EStandardModule.h b/client/logic/include/ramses-logic/EStandardModule.h new file mode 100644 index 000000000..50ba55a73 --- /dev/null +++ b/client/logic/include/ramses-logic/EStandardModule.h @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include + +namespace ramses +{ + /** + * Enum which represents the different Lua standard modules. Used in #ramses::LuaConfig::addStandardModuleDependency + */ + enum class EStandardModule : int32_t + { + Base=0, //< Basic functionality mapped to global space, e.g. ipairs(), pcall(), error(), assert() + String, //< The String module mapped to the Lua environment as a table named 'string' + Table, //< The Table module mapped to the Lua environment as a table named 'table' + Math, //< The Math module mapped to the Lua environment as a table named 'math' + Debug, //< The Debug module mapped to the Lua environment as a table named 'debug' + All, //< Use this to load all standard modules + }; +} diff --git a/client/logic/include/ramses-logic/ErrorData.h b/client/logic/include/ramses-logic/ErrorData.h new file mode 100644 index 000000000..5c5ae836b --- /dev/null +++ b/client/logic/include/ramses-logic/ErrorData.h @@ -0,0 +1,52 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include + +namespace ramses +{ + class LogicObject; + + /** + * #EErrorType helps distinguish between different types of errors in #ramses::ErrorData + */ + enum class EErrorType : int + { + BinaryDataAccessError, ///< Error during attempts to read or write files, non-existing paths, or data corruption (truncation, bit flips etc) + BinaryVersionMismatch, ///< The binary data was created with an incompatible version of the runtime(s) - either logic or ramses + ContentStateError, ///< The logic engine content is in an invalid state + RuntimeError, ///< There was an error during update(), e.g. a RamsesBinding failed to pass its values to Ramses, or Lua script's run() failed + IllegalArgument, ///< A call to the Ramses Logic API with missing arguments or incorrect values provided by user code + LuaSyntaxError, ///< Lua syntax error, e.g. when creating scripts from syntactically incorrect Lua source code + Other, ///< Error does not fit in any of the above clusters + }; + + /** + * Holds information about an error which occured during #ramses::LogicEngine API calls + */ + struct ErrorData + { + /** + * Error description as human-readable text. For Lua errors, an extra stack + * trace is contained in the error string with new-line separators. + */ + std::string message; + + /** + * Semantic type of the error + */ + EErrorType type; + + /** + * The #ramses::LogicObject which caused the issue. Can be nullptr if the issue was not originating from a specific object. + */ + const LogicObject* object; + }; +} diff --git a/client/logic/include/ramses-logic/Iterator.h b/client/logic/include/ramses-logic/Iterator.h new file mode 100644 index 000000000..976ec21e9 --- /dev/null +++ b/client/logic/include/ramses-logic/Iterator.h @@ -0,0 +1,208 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" + +#include +#include + +namespace ramses +{ + /** + * An STL-style iterator for various object types (T) with forward-semantics. See also: + * + * - #ramses::Collection + * - #ramses::LogicEngine::getCollection + * + * Template parameters: + * - T: the object type to iterate over, e.g. #ramses::LuaScript + * - internal_container: the internally wrapped container type. User code should *NOT* depend on this type + * to avoid API incompatibilities! + * - isConst: true for const-iterators, false for non-const iterators + * + * Dereferencing the iterator yields a pointer of type T* or const T*, depending if the iterator is a const iterator. + * The class is STL-compatible, meaning that the public type declarations may safely be used. + * + * Additional note: the Iterator class does not support swap() functionality as well as mutable assignments, because + * the internal data structures used to iterate over objects are not supposed to be changed by user code. The iterators + * are supposed to be used only in read-only scenarios. It is still possible to exercise write access on the + * pointers returned by the iterator, but it's not possible to assign a new value to the pointer itself or to swap the pointers. + */ + template + class Iterator + { + friend class Iterator; + friend class Iterator; + private: + /** + * Internal type trait (const iterator type) + */ + using const_iter = typename internal_container::const_iterator; + + /** + * Internal type trait (non-const iterator type) + */ + using non_const_iter = typename internal_container::iterator; + + /** + * Internal type trait (internally wrapped iterator type) + */ + using internal_iterator = typename std::conditional::type; + + /** + * Type returned when dereferencing the iterator (conditionally metaprogrammed for const-correctness when used inside const iterator) + */ + using maybe_const_T = typename std::conditional::type; + public: + /** + * Type traits as mandated by STL for custom iterators. + */ + using difference_type = typename internal_iterator::difference_type; + + /** + * The iterator type after dereferencing + */ + using value_type = typename internal_iterator::value_type; + + /** + * Iterator value pointer type + */ + using pointer = typename internal_iterator::pointer; + + /** + * Iterator value reference type + */ + using reference = typename internal_iterator::reference; + + /** + * Iterator type (refer to STL documentation for more information) + */ + using iterator_category = std::forward_iterator_tag; + + /** + * Operator dereferencing. Returns const T* if template argument isConst == true, otherwise returns T*. + * @return T* or const T* pointer to iterable object, depending on iterator constness + */ + [[nodiscard]] maybe_const_T operator*() noexcept + { + return *m_iterator; + } + + /** + * Member forwarding operator. Translates to '(const T*)->method' if template argument isConst == true, otherwise to '(T*)->method'. + * @return T* or const T* pointer to iterable object, depending on iterator constness + */ + [[nodiscard]] maybe_const_T operator->() noexcept + { + return *m_iterator; + } + + /** + * Pre-increment operator. + * @return reference to self + */ + Iterator& operator++() noexcept + { + ++m_iterator; + return *this; + } + + /** + * Post-increment operator. + * @return a copy of self before the increment was performed + */ + Iterator operator++(int) noexcept //NOLINT(cert-dcl21-cpp) false-positive clang tidy warning + { + Iterator retval = *this; + ++(*this); + return retval; + } + + /** + * Equality operator. + * @param other the other iterator to compare to + * @return true if the iterators point to the same object internally + */ + template + bool operator==(const Iterator& other) const noexcept + { + return m_iterator == other.m_iterator; + } + + /** + * Inequality operator. + * @param other the other iterator to compare to + * @return true if the iterators point to different objects internally + */ + template + bool operator!=(const Iterator& other) const noexcept + { + return !(*this == other); + } + + /** + * Default constructor. + */ + Iterator() noexcept = default; + + /** + * Constructor which allows const-iterator to be constructed from a non-const iterator, but not the other way around + * @param other iterator to construct from + */ + template> + Iterator(const Iterator & other) noexcept // NOLINT(google-explicit-constructor) needs conversion to work + : m_iterator(other.m_iterator) + { + } + + /** + * Assignment operator which allows const-iterator to be assigned from a non-const iterator, but not the other way around + * @param other iterator to be assign from + */ + template> + Iterator& operator=(const Iterator & other) noexcept + { + m_iterator = other.m_iterator; + return *this; + } + + /** + * Default copy constructor. This is redundant to the template version above, but is considered good style. + */ + Iterator(const Iterator&) noexcept = default; + + /** + * Default assignment operator. This is redundant to the template version above, but is considered good style. + */ + Iterator& operator=(const Iterator&) noexcept = default; + + /** + * Default destructor. + */ + ~Iterator() noexcept = default; + + /** + * Internal constructor which should not be called by user code to avoid API dependencies. Use + * #ramses::Collection::begin() and #ramses::Collection::end() or their const-counterparts to + * obtain iterators to desired #ramses::LogicEngine objects. + * @param iter internal iterator to be constructed from + */ + explicit Iterator(internal_iterator iter) noexcept + : m_iterator(iter) + { + } + + private: + /** + * Internal iterator. This implementation is not following the pimpl pattern for efficiency reasons. + */ + internal_iterator m_iterator = {}; + }; +} diff --git a/client/logic/include/ramses-logic/Logger.h b/client/logic/include/ramses-logic/Logger.h new file mode 100644 index 000000000..2aedc1cd7 --- /dev/null +++ b/client/logic/include/ramses-logic/Logger.h @@ -0,0 +1,73 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-framework-api/RamsesFrameworkTypes.h" + +#include +#include + +/** + * @ingroup LogicAPI + * Interface to interact with the internal logger. If you want to handle log messages by yourself, you can + * register your own log handler function with #ramses::Logger::SetLogHandler, which is called each time + * a log message is logged. In addition you can silence the standard output of the log messages + */ +namespace ramses::Logger +{ + /** + * The #LogHandlerFunc can be used to implement a custom log handler. The + * function is called for each log message separately. After the call to the function + * the string data behind std::string_view is deleted. If you want to keep it, you must + * copy it e.g. to a std::string. + * E.g. + * \code{.cpp} + * ramses::Logger::SetLogHandler([](ElogMessageType msgType, std::string_view message){ + * std::cout << message std::endl; + * }); + * \endcode + */ + using LogHandlerFunc = std::function; + + /** + * Controls how verbose the logging is. \p verbosityLimit has the following semantics: + * - if log message has message type with higher or equal priority as verbosityLimit, then it is logged + * - log priority is as documented by #ramses::ELogLevel (Errors are more important than Warnings, etc) + * - the default value is #ramses::ELogLevel::Info, meaning that log messages are processed if they + * have INFO priority or higher. + * + * @param verbosityLimit least priority a log message must have in order to be processed + */ + RAMSES_API void SetLogVerbosityLimit(ELogLevel verbosityLimit); + + /** + * Returns the current log verbosity limit of the logger. See #ramses::Logger::SetLogVerbosityLimit for + * more info on semantics. + * + * @return current log verbosity limit + */ + RAMSES_API ELogLevel GetLogVerbosityLimit(); + + /** + * Sets a custom log handler function, which is called each time a log message occurs. + * Note: setting a custom logger incurs a slight performance cost because log messages + * will be assembled and reported, even if default logging is disabled (#SetDefaultLogging). + * + * @ param logHandlerFunc function which is called for each log message + */ + RAMSES_API void SetLogHandler(const LogHandlerFunc& logHandlerFunc); + + /** + * Sets the default logging to std::out to enabled or disabled. Enabled by default. + * + * @param loggingEnabled true if you want to enable logging to std::out, false otherwise + */ + RAMSES_API void SetDefaultLogging(bool loggingEnabled); +} diff --git a/client/logic/include/ramses-logic/LogicEngine.h b/client/logic/include/ramses-logic/LogicEngine.h new file mode 100644 index 000000000..5bbb7988c --- /dev/null +++ b/client/logic/include/ramses-logic/LogicEngine.h @@ -0,0 +1,943 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-client-api/ERotationType.h" +#include "ramses-logic/AnimationTypes.h" +#include "ramses-logic/Collection.h" +#include "ramses-logic/EPropertyType.h" +#include "ramses-logic/ErrorData.h" +#include "ramses-logic/LogicEngineReport.h" +#include "ramses-logic/LuaConfig.h" +#include "ramses-logic/SaveFileConfig.h" +#include "ramses-logic/WarningData.h" +#include "ramses-logic/PropertyLink.h" +#include "ramses-framework-api/DataTypes.h" +#include "ramses-framework-api/EFeatureLevel.h" + +#include +#include + +namespace ramses +{ + class Scene; + class Node; + class Appearance; + class Camera; + class RenderPass; + class RenderGroup; + class UniformInput; + class MeshNode; +} + +namespace ramses::internal +{ + class LogicEngineImpl; +} + +/** + * @defgroup LogicAPI The Ramses Logic API + * This group contains all of the Ramses Logic API types. + */ + +namespace ramses +{ + class LogicNode; + class LuaScript; + class LuaInterface; + class LuaModule; + class Property; + class RamsesNodeBinding; + class RamsesAppearanceBinding; + class RamsesCameraBinding; + class RamsesRenderPassBinding; + class RamsesRenderGroupBinding; + class RamsesRenderGroupBindingElements; + class RamsesMeshNodeBinding; + class SkinBinding; + class DataArray; + class AnimationNode; + class AnimationNodeConfig; + class TimerNode; + class AnchorPoint; + enum class ELogLevel; + + /** + * @ingroup LogicAPI + * Central object which creates and manages the lifecycle and execution + * of scripts, bindings, and all other objects supported by the Ramses Logic library. + * All objects created by this class' methods must be destroyed with #destroy! + * + * - Use the create[Type] methods to create various objects, use #destroy() to delete them. + * - Use #link and #unlink to connect data properties between these objects + * - use #update() to trigger the execution of all objects + */ + class LogicEngine + { + public: + /** + * Constructor of #LogicEngine with a feature level specified. + * See #ramses::EFeatureLevel for more details what features are available. + * + * @param featureLevel Feature level to activate in this #LogicEngine instance. A feature level always includes + * a previous feature level if any, e.g. a feature level released after base level will contain + * all features from base level, however base level will include only base level features + * and none from a newer feature level. Use #ramses::EFeatureLevel_Latest to get all available features. + */ + RAMSES_API explicit LogicEngine(ramses::EFeatureLevel featureLevel) noexcept; + + /** + * Destructor of #LogicEngine + */ + RAMSES_API ~LogicEngine() noexcept; + + /** + * Returns the feature level this #LogicEngine instance was configured to use when created. + * See #LogicEngine(ramses::EFeatureLevel) and #ramses::EFeatureLevel for more details. + * + * @return feature level this #LogicEngine instance was configured to use + */ + [[nodiscard]] RAMSES_API ramses::EFeatureLevel getFeatureLevel() const; + + /** + * Returns an iterable #ramses::Collection of all instances of \c T created by this #LogicEngine. + * \c T must be a concrete logic object type (e.g. #ramses::LuaScript) or #ramses::LogicObject which will retrieve + * all logic objects created with this #LogicEngine (see #ramses::LogicObject::as to convert to concrete type). + * + * @return an iterable #ramses::Collection with all instances of \c T created by this #LogicEngine + */ + template + [[nodiscard]] Collection getCollection() const; + + /** + * Returns a pointer to the first occurrence of an object with a given \p name of the type \c T. + * \c T must be a concrete logic object type (e.g. #ramses::LuaScript) or #ramses::LogicObject which will search + * any object with given name regardless of its type (see #ramses::LogicObject::as to convert to concrete type). + * + * @param name the name of the logic object to search for + * @return a pointer to the logic object, or nullptr if none was found + */ + template + [[nodiscard]] const T* findByName(std::string_view name) const; + + /** @copydoc findByName(std::string_view) const */ + template + [[nodiscard]] T* findByName(std::string_view name); + + /** + * Returns a pointer to the first occurrence of an object with a given \p id regardless of its type. + * To convert the object to a concrete type (e.g. LuaScript) use #ramses::LogicObject::as() e.g.: + * auto myLuaScript = logicEngine.findLogicObjectById(1u)->as()); + * Be aware that this function behaves as \c dynamic_cast and will return nullptr (without error) if + * given type doesn't match the objects type. This can later lead to crash if ignored. + * + * @param id the id of the logic object to search for + * @return a pointer to the logic object, or nullptr if none was found + */ + [[nodiscard]] RAMSES_API const LogicObject* findLogicObjectById(uint64_t id) const; + /// @copydoc findLogicObjectById(uint64_t id) const + [[nodiscard]] RAMSES_API LogicObject* findLogicObjectById(uint64_t id); + + /** + * Creates a new Lua script from a source string. Refer to the #ramses::LuaScript class + * for requirements which Lua scripts must fulfill in order to be added to the #LogicEngine. + * You can optionally provide Lua module dependencies via the \p config, they will be accessible + * under their configured alias name for use by the script. The provided module dependencies + * must exactly match the declared dependencies in source code (see #extractLuaDependencies). + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param source the Lua source code + * @param config configuration options, e.g. for module dependencies + * @param scriptName name to assign to the script once it's created + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The script can be destroyed by calling the #destroy method + */ + RAMSES_API LuaScript* createLuaScript( + std::string_view source, + const LuaConfig& config = {}, + std::string_view scriptName = ""); + + /** + * Creates a new Lua interface from a source string. Refer to the #ramses::LuaInterface class + * for requirements which Lua interface must fulfill in order to be added to the #LogicEngine. + * Note: interfaces must have a non-empty name. + * #ramses::LuaInterface can be created with any non-empty name but if two or more instances + * share same name there will be a warning during validation (#validate) as it is advised + * for every Lua interface to have a unique name for clear identification from application logic. + * You can optionally provide Lua module dependencies via the \p config, they will be accessible + * under their configured alias name for use in the interface source. The provided module dependencies + * must exactly match the declared dependencies in source code (see #extractLuaDependencies). + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param source the Lua source code + * @param interfaceName name to assign to the interface once it's created. This name must be unique! + * @param config configuration options, e.g. for module dependencies + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The interface can be destroyed by calling the #destroy method + */ + RAMSES_API LuaInterface* createLuaInterface( + std::string_view source, + std::string_view interfaceName, + const LuaConfig& config); + + /** + * Deprecated! Use #createLuaInterface(std::string_view, std::string_view, const LuaConfig&) instead. + * + * Same as #createLuaInterface(std::string_view, std::string_view, const LuaConfig&) but without + * support for using #ramses::LuaModule in the interface, also will ignore any \c modules + * declaration within the provided interface source. + * + * @deprecated + * @param source the Lua source code + * @param interfaceName name to assign to the interface once it's created. This name must be unique! + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The interface can be destroyed by calling the #destroy method + */ + RAMSES_API LuaInterface* createLuaInterface( + std::string_view source, + std::string_view interfaceName); + + /** + * Creates a new #ramses::LuaModule from Lua source code. + * LuaModules can be used to share code and data constants across scripts or + * other modules. See also #createLuaScript and #ramses::LuaConfig for details. + * You can optionally provide Lua module dependencies via the \p config, they will be accessible + * under their configured alias name for use by the module. The provided module dependencies + * must exactly match the declared dependencies in source code (see #extractLuaDependencies). + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param source module source code + * @param config configuration options, e.g. for module dependencies + * @param moduleName name to assign to the module once it's created + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The script can be destroyed by calling the #destroy method + */ + RAMSES_API LuaModule* createLuaModule( + std::string_view source, + const LuaConfig& config = {}, + std::string_view moduleName = ""); + + /** + * Extracts dependencies from a Lua script, module or interface source code so that the corresponding + * modules can be provided when creating #ramses::LuaScript, #ramses::LuaModule or #ramses::LuaInterface. + * + * Any #ramses::LuaScript, #ramses::LuaModule or #ramses::LuaInterface which has a module dependency, + * i.e. it requires another #ramses::LuaModule for it to work, must explicitly declare these + * dependencies directly in their source code by calling function 'modules' in global space + * and pass list of module names it depends on, for example: + * \code{.lua} + * modules("foo", "bar") + * function interface(IN,OUT) + * OUT.x = foo.myType() + * end + * function run(IN,OUT) + * OUT.x = bar.doSth() + * end + * \endcode + * The 'modules' function does not affect any other part of the source code in any way, + * it is used only for the purpose of explicit declaration and extraction of its dependencies. + * + * Please note that script runtime errors are ignored during extraction. In case a runtime + * error prevents the 'modules' function to be called, this method will still succeed + * but will not extract any modules, i.e. will not call \c callbackFunc. It is therefore + * highly recommended to put the modules declaration always at the beginning of every script + * before any other code so it will get executed even if there is runtime error later in the code. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param source source code of module or script to parse for dependencies + * @param callbackFunc function callback will be called for each dependency found + * @return \c true if extraction succeeded (also if no dependencies found) or \c false if + * something went wrong. In that case, use #getErrors() to obtain errors. + */ + RAMSES_API bool extractLuaDependencies( + std::string_view source, + const std::function& callbackFunc); + + /** + * Creates a new #ramses::RamsesNodeBinding which can be used to set the properties of a Ramses Node object. + * The initial values of the binding's properties are loaded from the \p ramsesNode. Rotation values are + * taken over from the \p ramsesNode only if the conventions are compatible (see \ref ramses::ERotationType). + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param ramsesNode the ramses::Node object to control with the binding. + * @param rotationType the type of rotation to use (will affect the 'rotation' property semantics and type). + * @param name a name for the new #ramses::RamsesNodeBinding. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The binding can be destroyed by calling the #destroy method + */ + RAMSES_API RamsesNodeBinding* createRamsesNodeBinding(ramses::Node& ramsesNode, ramses::ERotationType rotationType = ramses::ERotationType::Euler_XYZ, std::string_view name = ""); + + /** + * Creates a new #ramses::RamsesAppearanceBinding which can be used to set the properties of a Ramses Appearance object. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param ramsesAppearance the ramses::Appearance object to control with the binding. + * @param name a name for the the new #ramses::RamsesAppearanceBinding. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The binding can be destroyed by calling the #destroy method + */ + RAMSES_API RamsesAppearanceBinding* createRamsesAppearanceBinding(ramses::Appearance& ramsesAppearance, std::string_view name = ""); + + /** + * Creates a new #ramses::RamsesCameraBinding which can be used to set the properties of a Ramses Camera object. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param ramsesCamera the ramses::Camera object to control with the binding. + * @param name a name for the the new #ramses::RamsesCameraBinding. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The binding can be destroyed by calling the #destroy method + */ + RAMSES_API RamsesCameraBinding* createRamsesCameraBinding(ramses::Camera& ramsesCamera, std::string_view name =""); + + /** + * Same as #createRamsesCameraBinding but the created #ramses::RamsesCameraBinding will have an input property + * for each frustum plane also for perspective camera. See #ramses::RamsesCameraBinding for details. + * Note that ramses::OrthographicCamera binding will always have frustum planes as properties whether #createRamsesCameraBinding + * or #createRamsesCameraBindingWithFrustumPlanes is used to create it. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param ramsesCamera the ramses::Camera object to control with the binding. + * @param name a name for the the new #ramses::RamsesCameraBinding. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The binding can be destroyed by calling the #destroy method + */ + RAMSES_API RamsesCameraBinding* createRamsesCameraBindingWithFrustumPlanes(ramses::Camera& ramsesCamera, std::string_view name =""); + + /** + * Creates a new #ramses::RamsesRenderPassBinding which can be used to set the properties of a ramses::RenderPass object. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param ramsesRenderPass the ramses::RenderPass object to control with the binding. + * @param name a name for the the new #ramses::RamsesRenderPassBinding. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The binding can be destroyed by calling the #destroy method + */ + RAMSES_API RamsesRenderPassBinding* createRamsesRenderPassBinding(ramses::RenderPass& ramsesRenderPass, std::string_view name =""); + + /** + * Creates a new #ramses::RamsesRenderGroupBinding which can be used to control some properties of a ramses::RenderGroup object. + * #ramses::RamsesRenderGroupBinding can be used to control render order of elements it contains on Ramses side - ramses::MeshNode or ramses::RenderGroup, + * the elements to control must be provided explicitly at creation time, see #ramses::RamsesRenderGroupBindingElements and #ramses::RamsesRenderGroupBinding + * to learn how these elements form the binding's input properties. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param ramsesRenderGroup the ramses::RenderGroup object to control with the binding. + * @param elements collection of elements (MeshNode or RenderGroup) to control with this #ramses::RamsesRenderGroupBinding. + * @param name a name for the the new #ramses::RamsesRenderGroupBinding. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The binding can be destroyed by calling the #destroy method + */ + RAMSES_API RamsesRenderGroupBinding* createRamsesRenderGroupBinding(ramses::RenderGroup& ramsesRenderGroup, const RamsesRenderGroupBindingElements& elements, std::string_view name =""); + + /** + * Creates a new #ramses::RamsesMeshNodeBinding which can be used to control some properties of a ramses::MeshNode object. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param ramsesMeshNode the ramses::MeshNode object to control with the binding. + * @param name a name for the the new #ramses::RamsesMeshNodeBinding. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The binding can be destroyed by calling the #destroy method + */ + RAMSES_API RamsesMeshNodeBinding* createRamsesMeshNodeBinding(ramses::MeshNode& ramsesMeshNode, std::string_view name =""); + + /** + * Creates a new #ramses::SkinBinding which can be used for vertex skinning (bone animations). + * Refer to #ramses::SkinBinding and examples for all the information needed how to use this object. + * + * These conditions must be met in order for the creation to succeed: + * - \c joints must contain at least 1 joint and no null pointers + * - \c inverseBindMatrices must be of equal size as \c joints (i.e. matrix per joint) + * - \c jointMatInput must point to a valid uniform input of the ramses::Effect used in \c appearanceBinding + * - the shader uniform that \c jointMatInput points to must be of type array of ramses::EDataType::Matrix44F + * with number of elements matching number of joints + * - \c jointMatInput must not be bound to any data object in its Ramses appearance + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param joints bindings to Ramses nodes which will act as skeleton nodes. + * @param inverseBindMatrices inverse transformation matrices (one for each joint node), values are expected ordered in column-major fashion. + * @param appearanceBinding binding to Ramses appearance which specifies the effect/shader for vertex skinning. + * @param jointMatInput Ramses appearance uniform input for the resulting joint matrices to be set. + * @param name a name for the the new #ramses::SkinBinding. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The binding can be destroyed by calling the #destroy method + */ + RAMSES_API SkinBinding* createSkinBinding( + const std::vector& joints, + const std::vector& inverseBindMatrices, + RamsesAppearanceBinding& appearanceBinding, + const ramses::UniformInput& jointMatInput, + std::string_view name = {}); + + /** + * Creates a new #ramses::DataArray to store data which can be used with animations. + * Provided data must not be empty otherwise creation will fail. + * See #ramses::CanPropertyTypeBeStoredInDataArray and #ramses::PropertyTypeToEnum + * to determine supported types that can be used to create a #ramses::DataArray. + * When using std::vector as element data type (corresponds to #ramses::EPropertyType::Array), + * the sizes of all the elements (std::vector instances) must be equal, otherwise creation will fail. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param data source data to move into #ramses::DataArray, must not be empty. + * @param name a name for the the new #ramses::DataArray. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + */ + template + DataArray* createDataArray(const std::vector& data, std::string_view name =""); + + /** + * Creates a new #ramses::AnimationNode for animating properties. + * Refer to #ramses::AnimationNode for more information about its use. + * There must be at least one channel provided in the #ramses::AnimationNodeConfig, + * please see #ramses::AnimationChannel requirements for all the data. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param config list of animation channels to be animated with this animation node and other configuration flags. + * @param name a name for the the new #ramses::AnimationNode. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + */ + RAMSES_API AnimationNode* createAnimationNode(const AnimationNodeConfig& config, std::string_view name = ""); + + /** + * Creates a new #ramses::TimerNode for generate and/or propagate timing information. + * Refer to #ramses::TimerNode for more information about its use. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param name a name for the the new #ramses::TimerNode. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + */ + RAMSES_API TimerNode* createTimerNode(std::string_view name = ""); + + /** + * Creates a new #ramses::AnchorPoint that can be used to calculate projected coordinates of given ramses::Node when viewed using given ramses::Camera. + * See #ramses::AnchorPoint for more details and usage of this special purpose logic node. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param nodeBinding binding referencing ramses::Node to use for model transformation when calculating projected coordinates. + * @param cameraBinding binding referencing ramses::Camera to use for view and projection transformation when calculating projected coordinates. + * @param name a name for the the new #ramses::AnchorPoint. + * @return a pointer to the created object or nullptr if + * something went wrong during creation. In that case, use #getErrors() to obtain errors. + * The #ramses::AnchorPoint can be destroyed by calling the #destroy method + */ + RAMSES_API AnchorPoint* createAnchorPoint(RamsesNodeBinding& nodeBinding, RamsesCameraBinding& cameraBinding, std::string_view name =""); + + /** + * Updates all #ramses::LogicNode's which were created by this #LogicEngine instance. + * The order in which #ramses::LogicNode's are executed is determined by the links created + * between them (see #link and #unlink). #ramses::LogicNode's which don't have any links + * between then are executed in arbitrary order, but the order is always the same between two + * invocations of #update without any calls to #link or #unlink between them. + * As an optimization #ramses::LogicNode's are only updated, if at least one input of a #ramses::LogicNode + * has changed since the last call to #update. If the links between logic nodes create a loop, + * this method will fail with an error and will not execute any of the logic nodes. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @return true if the update was successful, false otherwise + * In case of an error, use #getErrors() to obtain errors. + */ + RAMSES_API bool update(); + + /** + * Enables collecting of statistics during call to #update which can be obtained using #getLastUpdateReport. + * Once enabled every subsequent call to #update will be instructed to collect various statistical data + * which can be useful for profiling and optimizing the network of logic nodes. + * Note that when enabled there is a slight performance overhead to collect the data, it is recommended + * to use this only during a development phase. + * + * @param enable true or false to enable or disable update reports. + */ + RAMSES_API void enableUpdateReport(bool enable); + + /** + * Returns collection of statistics from last call to #update if reporting is enabled (#enableUpdateReport). + * The report contains lists of logic nodes that were executed and not executed and other useful data collected + * during last #update. See #ramses::LogicEngineReport for details. + * The report data is generated only if previously enabled using #enableUpdateReport and is empty otherwise. + * The data is only relevant for the last #update and is overwritten during next #update. + * Note that if #update fails the report contents are undefined. + * + * Attention! The #ramses::LogicEngineReport is returned by value and owns all the reported data. + * Make sure to keep the object as long as its data is referenced. + * + * @return collected statistics from last #update. + */ + [[nodiscard]] RAMSES_API LogicEngineReport getLastUpdateReport() const; + + /** + * Set the logging rate, i.e. how often statistics will be logged. Logging rate of \c N means + * every \c Nth call to #update statistics will be logged. + * Whether the the logs are actually logged is also influenced by the statistics log level that can be set with #setStatisticsLogLevel. + * The logging rate also determines how many collected sets will be used to calculate min/max and average. + * These statistics include: + * - \p Time since last log in seconds + * - \p Update execution time in microseconds (Avg, Min, Max) + * - \p Time between #update calls in microseconds (Avg, Min, Max) + * - \p Count of nodes executed in percentage of total count (Avg, Min, Max) + * - \p Links activated (Avg, Min, Max) + * When loggingRate is set to 0 the logging of statistics is disabled. + * Note that there is a slight performance overhead for collecting the statistics data, + * however on most platforms this should be marginal. + * To get more detailed information about update execution timings see #getLastUpdateReport. + * + * @param loggingRate rate of \c N means statistics will be logged every \c Nth call to #update. By default loggingRate is 60. + */ + RAMSES_API void setStatisticsLoggingRate(size_t loggingRate); + + /** + * Update statistics default logLevel is #ELogLevel::Debug. For the statistics to be logged the logLevel + * has to be <= then the result returned from #ramses::Logger::GetLogVerbosityLimit. + * For example if #ramses::Logger::GetLogVerbosityLimit returns #ELogLevel::Info you have to + * set statistics logLevel to #ELogLevel::Info or a smaller level (e.g. #ELogLevel::Warn) to display statistics. + * Note that setting the statistics log level only influences the periodic statistic logs. All other logs are not influenced + * by this method. + * To control the rate after how many updates a log is produced refer to #setStatisticsLoggingRate. + * + * @param logLevel the logLevel of statistics messages + */ + RAMSES_API void setStatisticsLogLevel(ELogLevel logLevel); + + /** + * Links a property of a #ramses::LogicNode to another #ramses::Property of another #ramses::LogicNode. + * After linking, calls to #update will propagate the value of \p sourceProperty to + * the \p targetProperty. Creating links influences the order in which scripts + * are executed - if node A provides data to node B, then node A will be executed + * before node B. A single output property (\p sourceProperty) can be linked to any number of input + * properties (\p targetProperty), but any input property can have at most one link to an output property + * (links are directional and support a 1-to-N relationships). + * + * The #link() method will fail when: + * - \p sourceProperty and \p targetProperty belong to the same #ramses::LogicNode + * - \p sourceProperty is not an output (see #ramses::LogicNode::getOutputs()) + * - \p targetProperty is not an input (see #ramses::LogicNode::getInputs()) + * - either \p sourceProperty or \p targetProperty is not a primitive property (you have to link sub-properties + * of structs and arrays individually) + * + * Creating links which form a cycle in the node dependency graph will not fail right away but during the next call + * to #update(). Links are directional, it is OK to have A->B, A->C and B->C, but is not OK to have + * A->B->C->A. Cycles are allowed only under special conditions when using a weak link (see #linkWeak). + * + * After calling #link, the value of the \p targetProperty will not change until the next call to #update. Creating + * and destroying links generally has no effect until #update is called. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param sourceProperty the output property which will provide data for \p targetProperty + * @param targetProperty the target property which will receive its value from \p sourceProperty + * @return true if linking was successful, false otherwise. To get more detailed + * error information use #getErrors() + */ + RAMSES_API bool link(const Property& sourceProperty, const Property& targetProperty); + + /** + * A weak link is for the most part equivalent to #link - it has the same requirements and data propagation behavior, + * but with one crucial difference. Weak link is not considered for node dependency graph (logic node topology) + * which determines the order of nodes execution during #update. This allows to form a cycle using weak link + * within the dependency graph, which can be helpful when some feedback data is required (e.g. a controller + * node managing a worker node using normal links, worker node reports progress back to controller via weak link). + * However there are important things to consider to avoid unexpected problems: + * 1. The value propagated from a weak link is old, it is the value that was calculated in previous #update. + * Example: consider A->B->C using normal links and a back loop C->A using weak link. When #update is called, + * the nodes are executed exactly in the order defined by normal link dependency: A, B, C (remember + * that weak links are not considered for execution order). This means however that when A is executed + * its input value from weak linked C is only from previous update because C is executed last. + * 2. During first update (after creation or load from file) there is no value applied from weak link, instead + * the initial input property value will be used at the weak link target. This is a logical consequence of the + * limitation described above - during first update there was no previous update and therefore no value from weak link. + * 3. Avoid infinite update loops - #update has an optimization to execute only nodes with modified inputs, + * this can greatly simplify update of complex node hierarchies. When using weak links there is a risk that + * there will be a never ending need of executions of nodes involved in a node graph cycle. Consider the last + * node providing new value via weak link to first node, which then generates again new value propagated + * to the last node, this will result in the need to execute those nodes in all upcoming #update calls. + * Try to avoid such case and if it is not possible at least try to limit this update loop to a minimum of nodes. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param sourceProperty the output property which will provide data for \p targetProperty + * @param targetProperty the target property which will receive its value from \p sourceProperty + * @return true if linking was successful, false otherwise. To get more detailed + * error information use #getErrors() + */ + RAMSES_API bool linkWeak(const Property& sourceProperty, const Property& targetProperty); + + /** + * Unlinks two properties which were linked with #link. After a link is destroyed, + * calls to #update will no longer propagate the output value from the \p sourceProperty to + * the input value of the \p targetProperty. The value of the \p targetProperty will remain as it was after the last call to #update - + * it will **not** be restored to a default value or to any value which was set manually with calls to #ramses::Property::set(). + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param sourceProperty the output property which is currently linked to \p targetProperty + * @param targetProperty the property which will no longer receive the value from \p sourceProperty + * @return true if unlinking was successful, false otherwise. To get more detailed + * error information use #getErrors(). + */ + RAMSES_API bool unlink(const Property& sourceProperty, const Property& targetProperty); + + /** + * Checks if an input or output of a given LogicNode is linked to another LogicNode + * @param logicNode the node to check for linkage. + * @return true if the given LogicNode is linked to any other LogicNode, false otherwise. + */ + [[nodiscard]] RAMSES_API bool isLinked(const LogicNode& logicNode) const; + + /** + * Collect and retrieve all existing links between properties of logic nodes. + * There will be a #ramses::PropertyLink in the returned container for every existing link created + * (using #link or #linkWeak). + * + * Note that the returned container will not be modified (even if new links are created or unlinked in #LogicEngine) + * until #getPropertyLinks is called again. + * + * @return all existing links between properties of logic nodes. + */ + [[nodiscard]] RAMSES_API const std::vector& getPropertyLinks() const; + + /** + * Returns the list of all errors which occurred during the last API call to a #LogicEngine method + * or any other method of its subclasses (scripts, bindings etc). Note that errors get wiped by all + * mutable methods of the #LogicEngine. + * + * This method can be used in two different ways: + * - To debug the correct usage of the Logic Engine API (e.g. by wrapping all API calls with a check + * for their return value and using this method to find out the cause of the error) + * - To check for runtime errors of scripts which come from a dynamic source, e.g. by calling the + * method after an unsuccessful call to #update() with a faulty script + * + * @return a list of errors + */ + [[nodiscard]] RAMSES_API const std::vector& getErrors() const; + + /** + * Performs a (potentially slow!) validation of the LogicEngine content and reports a list of warnings + * for things which could be fixed/changed for better performance, consistency or resource usage. + * + * @return a list of warnings + */ + [[nodiscard]] RAMSES_API const std::vector& validate() const; + + /** + * Destroys an instance of an object created with #LogicEngine. + * All objects created using #LogicEngine derive from a base class #ramses::LogicObject + * and can be destroyed using this method. + * + * In case of a #ramses::LogicNode and its derived classes, if any links are connected to this #ramses::LogicNode, + * they will be destroyed too. Note that after this call, the execution order of #ramses::LogicNode may change! See the + * docs of #link and #unlink for more information. + * + * In case of a #ramses::DataArray, destroy will fail if it is used in any #ramses::AnimationNode's #ramses::AnimationChannel. + * + * In case of a #ramses::LuaModule, destroy will fail if it is used in any #ramses::LuaScript. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param object the object instance to destroy + * @return true if object destroyed, false otherwise. Call #getErrors() for error details upon failure. + */ + RAMSES_API bool destroy(LogicObject& object); + + /** + * Writes the whole #LogicEngine and all of its objects to a binary file with the given filename. The RAMSES scene + * potentially referenced by #ramses::RamsesBinding objects is not saved - that is left to the application. + * #LogicEngine saves the references to those object, and restores them after loading. + * Thus, deleting Ramses objects which are being referenced from within the #LogicEngine + * will result in errors if the Logic Engine is loaded from the file again. Note that it is not sufficient + * to have objects with the same name, they have to be the exact same objects as during saving! + * For more in-depth information regarding saving and loading, refer to the online documentation at + * https://ramses-logic.readthedocs.io/en/latest/api.html#saving-loading-from-file + * + * Note: The method reports error and aborts if the #ramses::RamsesBinding objects reference more than one + * Ramses scene (this is acceptable during runtime, but not for saving to file). + * + * Note: This method fails and reports error if validation failed and was not disabled by + * calling #ramses::SaveFileConfig::setValidationEnabled with false + * + * Note: This method fails and reports error if there is any Lua script or module with debug log functions, + * (see #ramses::LuaConfig::enableDebugLogFunctions), these must be destroyed before saving to file. + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param filename path to file to save the data (relative or absolute). The file will be created or overwritten if it exists! + * @param config optional configuration object with exporter and asset metadata info, see #ramses::SaveFileConfig for details + * @return true if saving was successful, false otherwise. To get more detailed + * error information use #getErrors() + */ + RAMSES_API bool saveToFile(std::string_view filename, const SaveFileConfig& config = {}); + + /** + * Loads the whole LogicEngine data from the given file. See also #saveToFile(). + * After loading, the previous state of the #LogicEngine will be overwritten with the + * contents loaded from the file, i.e. all previously created objects (scripts, bindings, etc.) + * will be deleted and pointers to them will be invalid. The (optionally) provided ramsesScene + * will be used to resolve potential #ramses::RamsesBinding objects which point to Ramses objects. + * You can provide a nullptr if you know for sure that the + * #LogicEngine loaded from the file has no #ramses::RamsesBinding objects which point to a Ramses scene object. + * Otherwise, the call to #loadFromFile will fail with an error. In case of errors, the #LogicEngine + * may be left in an inconsistent state. + * For more in-depth information regarding saving and loading, refer to the online documentation at + * https://ramses-logic.readthedocs.io/en/latest/api.html#saving-loading-from-file + * + * Attention! This method clears all previous errors! See also docs of #getErrors() + * + * @param filename path to file from which to load content (relative or absolute) + * @param ramsesScene pointer to the Ramses Scene which holds the objects referenced in the Ramses Logic file + * @param enableMemoryVerification flag to enable memory verifier (a flatbuffers feature which checks bounds and ranges). + * Disable this only if the file comes from a trusted source and performance is paramount. + * @return true if deserialization was successful, false otherwise. To get more detailed + * error information use #getErrors() + */ + RAMSES_API bool loadFromFile(std::string_view filename, ramses::Scene* ramsesScene = nullptr, bool enableMemoryVerification = true); + + /** + * Loads the whole LogicEngine data from the given file descriptor. This method is equivalent to #loadFromFile(). + * + * The file descriptor must be opened for read access and must support seeking. + * It will be in closed state after this call. + * + * @param[in] fd Open and readable filedescriptor. + * @param[in] offset Absolute starting position of LogicEngine data within fd. + * @param[in] length Size of the data within fd. + * @param ramsesScene pointer to the Ramses Scene which holds the objects referenced in the Ramses Logic file + * @param enableMemoryVerification flag to enable memory verifier (a flatbuffers feature which checks bounds and ranges). + * Disable this only if the file comes from a trusted source and performance is paramount. + * @return true if deserialization was successful, false otherwise. To get more detailed + * error information use #getErrors() + */ + RAMSES_API bool loadFromFileDescriptor(int fd, size_t offset, size_t length, ramses::Scene* ramsesScene = nullptr, bool enableMemoryVerification = true); + + /** + * Loads the whole LogicEngine data from the given memory buffer. This method is equivalent to + * #loadFromFile but allows to have the file-opening + * logic done by the user and only pass the data as a buffer. The logic engine only reads the + * data, does not take ownership of it and does not modify it. The memory can be freed or + * modified after the call returns, the #LogicEngine keeps no references to it. + * + * @param rawBuffer pointer to the raw data in memory + * @param bufferSize size of the data (bytes) + * @param ramsesScene pointer to the Ramses Scene which holds the objects referenced in the Ramses Logic file + * @param enableMemoryVerification flag to enable memory verifier (a flatbuffers feature which checks bounds and ranges). + * Disable this only if the file comes from a trusted source and performance is paramount. + * @return true if deserialization was successful, false otherwise. To get more detailed + * error information use #getErrors() + */ + RAMSES_API bool loadFromBuffer(const void* rawBuffer, size_t bufferSize, ramses::Scene* ramsesScene = nullptr, bool enableMemoryVerification = true); + + /** + * Calculates the serialized size of all objects contained in this LogicEngine instance. + * Note that the returned size will differ from actual size when saved to a file but the difference should be no more than several bytes + * (file header, meta information, etc.). + * @param luaSavingMode calculate with Lua code saved as source string, binary or both, see #ramses::SaveFileConfig::setLuaSavingMode), + * default is #ramses::ELuaSavingMode::SourceAndByteCode. + * @return size in bytes of the serialized LogicEngine. + */ + [[nodiscard]] RAMSES_API size_t getTotalSerializedSize(ELuaSavingMode luaSavingMode = ELuaSavingMode::SourceAndByteCode) const; + + /** + * Calculates the serialized size of all objects of a specific type in this LogicEngine instance. + * \c T must be a concrete logic object type (e.g. #ramses::LuaScript). For the logic type LogicObject the size of all logic objects will be returned. + * + * @tparam T Logic object type to calculate size for + * @param luaSavingMode calculate with Lua code saved as source string, binary or both, see #ramses::SaveFileConfig::setLuaSavingMode), + * default is #ramses::ELuaSavingMode::SourceAndByteCode (relevant only for object types containing Lua code). + * @return size in bytes of the serialized objects. + */ + template + [[nodiscard]] size_t getSerializedSize(ELuaSavingMode luaSavingMode = ELuaSavingMode::SourceAndByteCode) const; + + /** + * Attempts to parse feature level from a Ramses Logic file. + * + * @param[in] filename file path to Ramses Logic file + * @param[out] detectedFeatureLevel feature level detected in given file (valid only if parsing successful!) + * @return true if parsing was successful, false otherwise. + */ + [[nodiscard]] RAMSES_API static bool GetFeatureLevelFromFile(std::string_view filename, ramses::EFeatureLevel& detectedFeatureLevel); + + /** + * Attempts to parse feature level from a Ramses Logic buffer. + * + * @param[in] logname additional name for errors logging (e.g. Ramses Logic file name) + * @param[in] buffer data buffer with Ramses Logic file content + * @param[in] bufferSize size of the data buffer + * @param[out] detectedFeatureLevel feature level detected in given file (valid only if parsing successful!) + * @return true if parsing was successful, false otherwise. + */ + [[nodiscard]] RAMSES_API static bool GetFeatureLevelFromBuffer(std::string_view logname, const void* buffer, size_t bufferSize, ramses::EFeatureLevel& detectedFeatureLevel); + + /** + * Copy Constructor of LogicEngine is deleted because logic engines hold named resources and are not supposed to be copied + * + * @param other logic engine to copy from + */ + LogicEngine(const LogicEngine& other) = delete; + + /** + * Move Constructor of LogicEngine + * + * @param other logic engine to move from + */ + RAMSES_API LogicEngine(LogicEngine&& other) noexcept; + + /** + * Assignment operator of LogicEngine is deleted because logic engines hold named resources and are not supposed to be copied + * + * @param other logic engine to assign from + */ + LogicEngine& operator=(const LogicEngine& other) = delete; + + /** + * Move assignment operator of LogicEngine + * + * @param other logic engine to move from + */ + RAMSES_API LogicEngine& operator=(LogicEngine&& other) noexcept; + + /** + * Implementation detail of LogicEngine + */ + std::unique_ptr m_impl; + + private: + /** + * Internal implementation of collection getter + * @return collection of objects + */ + template + [[nodiscard]] RAMSES_API Collection getLogicObjectsInternal() const; + + /** + * Internal implementation of type specific size getter + * @tparam T Logic object type + * @param luaSavingMode calculate with Lua code saved as source string, binary or both + * @return size in bytes of the serialized objects. + */ + template + [[nodiscard]] RAMSES_API size_t getSerializedSizeInternal(ELuaSavingMode luaSavingMode) const; + + /** + * Internal implementation of object finder + * @param name object name + * @return found object + */ + template + [[nodiscard]] RAMSES_API const T* findLogicObjectInternal(std::string_view name) const; + + /** + * Internal implementation of object finder + * @param name object name + * @return found object + */ + template + [[nodiscard]] RAMSES_API T* findLogicObjectInternal(std::string_view name); + + /** + * Internal implementation of #createDataArray + * + * @param data source data + * @param name name + * @return a pointer to the created object or nullptr on error + */ + template + [[nodiscard]] RAMSES_API DataArray* createDataArrayInternal(const std::vector& data, std::string_view name); + + /// Internal static helper to validate type + template + static void StaticTypeCheck(); + }; + + template + Collection LogicEngine::getCollection() const + { + StaticTypeCheck(); + return getLogicObjectsInternal(); + } + + template + const T* LogicEngine::findByName(std::string_view name) const + { + StaticTypeCheck(); + return findLogicObjectInternal(name); + } + + template + T* LogicEngine::findByName(std::string_view name) + { + StaticTypeCheck(); + return findLogicObjectInternal(name); + } + + template + DataArray* LogicEngine::createDataArray(const std::vector& data, std::string_view name) + { + static_assert(CanPropertyTypeBeStoredInDataArray(PropertyTypeToEnum::TYPE), + "Unsupported data type, see createDataArray API doc to see supported types."); + return createDataArrayInternal(data, name); + } + + template + void LogicEngine::StaticTypeCheck() + { + static_assert( + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v, + "Attempting to retrieve invalid type of object."); + } + + template + size_t LogicEngine::getSerializedSize(ELuaSavingMode luaSavingMode) const + { + StaticTypeCheck(); + return getSerializedSizeInternal(luaSavingMode); + } +} diff --git a/client/logic/include/ramses-logic/LogicEngineReport.h b/client/logic/include/ramses-logic/LogicEngineReport.h new file mode 100644 index 000000000..d7b3021f6 --- /dev/null +++ b/client/logic/include/ramses-logic/LogicEngineReport.h @@ -0,0 +1,129 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include +#include +#include + +namespace ramses::internal +{ + class LogicEngineReportImpl; +} + +namespace ramses +{ + class LogicNode; + + /** + * A collection of results from #ramses::LogicEngine::update which can be used + * for debugging or profiling logic node networks. + * Can be obtained using #ramses::LogicEngine::getLastUpdateReport after an update with enabled reporting + * (#ramses::LogicEngine::enableUpdateReport). + */ + class LogicEngineReport + { + public: + /// LogicNode with measured update execution + using LogicNodeTimed = std::pair; + + /** + * Gets list of logic nodes that were updated and the amount of time it took to execute their update logic. + * Typically nodes have to be updated due to one of their inputs being 'dirty' (modified) + * or some other internal trigger. + * The order of the nodes in the list matches the order of their execution, this order is determined + * by the logic network topology and is deterministic. + * + * @return list of updated nodes with update execution time, sorted by execution order + */ + [[nodiscard]] RAMSES_API const std::vector& getNodesExecuted() const; + + /** + * List of logic nodes that were not updated because their inputs did not change therefore there was no need to. + * Similar to #getNodesExecuted, the order of the nodes in the list matches the order of their + * (in this case skipped) execution. + * + * @return list of nodes which did not need update + */ + [[nodiscard]] RAMSES_API const std::vector& getNodesSkippedExecution() const; + + /** + * Time it took to sort logic nodes by their topology during update. + * Note that re-sorting is only needed if topology changed (node linked/unlinked), + * otherwise the result is cached and this should take negligible time. + * + * @return time to sort logic nodes + */ + [[nodiscard]] RAMSES_API std::chrono::microseconds getTopologySortExecutionTime() const; + + /** + * Time it took to update the whole logic nodes network. + * This is essentially the same as measuring how long it took to call #ramses::LogicEngine::update + * from application side. + * + * @return time to update all logic nodes + */ + [[nodiscard]] RAMSES_API std::chrono::microseconds getTotalUpdateExecutionTime() const; + + /** + * Obtain the number of links activated during update. + * + * @return the number of links activated during update + */ + [[nodiscard]] RAMSES_API size_t getTotalLinkActivations() const; + + /** + * Default constructor of LogicEngineReport. + */ + RAMSES_API LogicEngineReport() noexcept; + + /** + * Constructor of LogicEngineReport. Do not construct, use #ramses::LogicEngine::getLastUpdateReport to obtain. + * + * @param impl implementation details of the LogicEngineReport + */ + RAMSES_API explicit LogicEngineReport(std::unique_ptr impl) noexcept; + + /** + * Class destructor + */ + RAMSES_API ~LogicEngineReport(); + + /** + * Copying disabled, move instead. + */ + LogicEngineReport(const LogicEngineReport&) = delete; + + /** + * Move constructor + * + * @param other source + */ + RAMSES_API LogicEngineReport(LogicEngineReport&& other) noexcept; + + /** + * Copying disabled, move instead. + */ + LogicEngineReport& operator=(const LogicEngineReport&) = delete; + + /** + * Move assignment + * + * @param other source + */ + RAMSES_API LogicEngineReport& operator=(LogicEngineReport&& other) noexcept; + + private: + /** + * Implementation detail of LogicEngineReport + */ + std::unique_ptr m_impl; //NOLINT(modernize-use-default-member-init) fixing this would break pimpl pattern + }; +} diff --git a/client/logic/include/ramses-logic/LogicNode.h b/client/logic/include/ramses-logic/LogicNode.h new file mode 100644 index 000000000..49172ea62 --- /dev/null +++ b/client/logic/include/ramses-logic/LogicNode.h @@ -0,0 +1,80 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicObject.h" + +#include + +namespace ramses::internal +{ + class LogicNodeImpl; +} + +namespace ramses +{ + class Property; + + /** + * A base class for multiple logic classes which provides a unified interface to their + * inputs and outputs. Some subclasses don't have inputs or outputs - in that case the + * #getInputs or #getOutputs methods respectively will return nullptr. Some subclasses, + * like the #ramses::RamsesAppearanceBinding, will have their inputs depending on their + * current state (in this example the GLSL uniforms of the shader to which the bound ramses + * Appearance belongs). In those cases, #getInputs()/#getOutputs() will return a #ramses::Property + * which represents an empty struct (type Struct, but no child properties). + */ + class LogicNode : public LogicObject + { + public: + /** + * Returns a property of type Struct which holds the inputs of the #LogicNode. + * + * Returns the root Property of the LogicNode which contains potentially + * nested list of properties. Calling #ramses#Property#getName() on the returned object will always return "" + * regardless of the name used in the scripts. This applies only to the root input node, rest of the nodes + * in the tree structure follow standard behavior. + * The properties are different for the classes which derive from #LogicNode. Look at the documentation + * of each derived class for more information on the properties. + * @return a tree like structure with the inputs of the #LogicNode + */ + [[nodiscard]] RAMSES_API Property* getInputs(); + + /** + * @copydoc getInputs() + */ + [[nodiscard]] RAMSES_API const Property* getInputs() const; + + /** + * Returns a property of type Struct which holds the outputs of the #LogicNode + * + * Returns the root Property of the LogicNode which contains potentially + * nested list of properties. Calling #ramses#Property#getName() on the returned object will always return "" + * regardless of the name used in the scripts. This applies only to the root output node, rest of the nodes + * in the tree structure follow standard behavior. + * The properties are different for the classes which derive from #LogicNode. Look at the documentation + * of each derived class for more information on the properties. + * @return a tree like structure with the outputs of the LogicNode + */ + [[nodiscard]] RAMSES_API const Property* getOutputs() const; + + /** + * Implementation detail of LogicNode + */ + internal::LogicNodeImpl& m_impl; + + protected: + /** + * Constructor of LogicNode. User is not supposed to call this - LogcNodes are created by subclasses + * + * @param impl implementation details of the LogicNode + */ + explicit LogicNode(std::unique_ptr impl) noexcept; + }; +} diff --git a/client/logic/include/ramses-logic/LogicObject.h b/client/logic/include/ramses-logic/LogicObject.h new file mode 100644 index 000000000..61107d420 --- /dev/null +++ b/client/logic/include/ramses-logic/LogicObject.h @@ -0,0 +1,148 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include +#include + +namespace ramses::internal +{ + class LogicObjectImpl; + class ApiObjects; +} + +namespace ramses +{ + /** + * A base class for all rlogic API objects + */ + class LogicObject + { + public: + /** + * Returns the name of this object. + * + * @return the name of this object + */ + [[nodiscard]] RAMSES_API std::string_view getName() const; + + /** + * Sets the name of this object. + * + * @param name new name of the object + * @return true if setting the name was successful, false if the object name can't be changed (e.g. interface objects) + */ + RAMSES_API bool setName(std::string_view name); + + /** + * Returns the id of this object. Every object gets a unique, immutable id assigned on object creation. + * The id is serialized and thus persisted on load. + * + * @return the id of this object + */ + [[nodiscard]] RAMSES_API uint64_t getId() const; + + /** + * Set user ID for this object. + * User IDs are optional identifiers of logic objects stored as number of up to 128 bits. + * User IDs will be logged together with name (#getName) and unique ID (#getId) and are serialized, thus persistent. + * Note that user IDs do not have to be unique, it is user's choice and responsibility if uniqueness is desired. + * User ID is logged only if other than [0,0] was set and the format is hexadecimal 'highId|lowId' with all digits printed. + * + * @param highId high 64 bits of user ID to set + * @param lowId low 64 bits of user ID to set + * @return true if successful, false if failed + */ + RAMSES_API bool setUserId(uint64_t highId, uint64_t lowId); + + /** + * Returns the user ID set using #setUserId. + * + * @return the user ID [highId, lowId] or [0, 0] if no user ID was set + */ + [[nodiscard]] RAMSES_API std::pair getUserId() const; + + /** + * Casts this object to given type. + * Has same behavior as \c dynamic_cast, will return nullptr (without error) if given type does not match this object. + * + * @return logic object cast to given type or nullptr if wrong type provided + */ + template + [[nodiscard]] const T* as() const; + + /** + * @copydoc as() const + */ + template + [[nodiscard]] T* as(); + + /** + * Deleted copy constructor + */ + LogicObject(const LogicObject&) = delete; + + /** + * Deleted move constructor + */ + LogicObject(LogicObject&&) = delete; + + /** + * Deleted assignment operator + */ + LogicObject& operator=(const LogicObject&) = delete; + + /** + * Deleted move assignment operator + */ + LogicObject& operator=(LogicObject&&) = delete; + + std::unique_ptr m_impl; + + protected: + /** + * Constructor of #LogicObject. User is not supposed to call this - LogcNodes are created by subclasses + * + * @param impl implementation details of the #LogicObject + */ + explicit LogicObject(std::unique_ptr impl) noexcept; + + /** + * Destructor of #LogicObject + */ + virtual ~LogicObject() noexcept; + + /** + * Internal implementation of #as with check that T is the correct type + */ + template + [[nodiscard]] RAMSES_API const T* internalCast() const; + + /** + * @copydoc internalCast() const + */ + template + [[nodiscard]] RAMSES_API T* internalCast(); + + friend class internal::ApiObjects; + }; + + template const T* LogicObject::as() const + { + static_assert(std::is_base_of::value, "T in as must be a subclass of LogicObject!"); + return internalCast(); + } + + template T* LogicObject::as() + { + static_assert(std::is_base_of::value, "T in as must be a subclass of LogicObject!"); + return internalCast(); + } +} diff --git a/client/logic/include/ramses-logic/LuaConfig.h b/client/logic/include/ramses-logic/LuaConfig.h new file mode 100644 index 000000000..4e26271a4 --- /dev/null +++ b/client/logic/include/ramses-logic/LuaConfig.h @@ -0,0 +1,110 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-logic/EStandardModule.h" + +#include +#include + +namespace ramses::internal +{ + class LuaConfigImpl; +} + +namespace ramses +{ + class LuaModule; + + /** + * Holds configuration settings for Lua script and module creation. Can be default-constructed, moved + * and copied. + */ + class LuaConfig + { + public: + RAMSES_API LuaConfig() noexcept; + + /** + * Adds a #ramses::LuaModule as a dependency to be added when this config is used for script or module + * creation. The \p aliasName can be any valid Lua label which must obey following rules: + * - can't use the same label twice in the same #LuaConfig object + * - can't use standard module names (math, string etc.) + * + * The \p moduleInstance provided can be any module. You can't reference modules from + * different #ramses::LogicEngine instances and the referenced modules must be from the same instance + * on which the config is used for script creation. + * + * @param aliasName the alias name under which the dependency will be mapped into the parent script/module + * @param moduleInstance the dependency module to map + * @return true if the dependency was added successfully, false otherwise + * In case of an error, check the logs. + */ + RAMSES_API bool addDependency(std::string_view aliasName, const LuaModule& moduleInstance); + + /** + * Adds a standard module dependency. The module is mapped under a name as documented in #ramses::EStandardModule. + * + * @param stdModule the standard module which will be mapped into the parent script/module + * @return true if the standard module was added successfully, false otherwise + * In case of an error, check the logs. + */ + RAMSES_API bool addStandardModuleDependency(EStandardModule stdModule); + + /** + * Will expose these log functions in Lua: + * rl_logInfo + * rl_logWarn + * rl_logError + * Each with single string as argument. + * Call from Lua script or module to any of these will forward the string message to a corresponding internal Ramses Logic logger. + * + * IMPORTANT: These functions are meant for debug/prototype purposes only and attempt to save the project to file (#ramses::LogicEngine::saveToFile) + * with any script or module with these functions enabled will result in failure! + */ + RAMSES_API void enableDebugLogFunctions(); + + /** + * Destructor of #LuaConfig + */ + RAMSES_API ~LuaConfig() noexcept; + + /** + * Copy Constructor of #LuaConfig + * @param other the other #LuaConfig to copy from + */ + RAMSES_API LuaConfig(const LuaConfig& other); + + /** + * Move Constructor of #LuaConfig + * @param other the other #LuaConfig to move from + */ + RAMSES_API LuaConfig(LuaConfig&& other) noexcept; + + /** + * Assignment operator of #LuaConfig + * @param other the other #LuaConfig to copy from + * @return self + */ + RAMSES_API LuaConfig& operator=(const LuaConfig& other); + + /** + * Move assignment operator of #LuaConfig + * @param other the other #LuaConfig to move from + * @return self + */ + RAMSES_API LuaConfig& operator=(LuaConfig&& other) noexcept; + + /** + * Implementation detail of #LuaConfig + */ + std::unique_ptr m_impl; + }; +} diff --git a/client/logic/include/ramses-logic/LuaInterface.h b/client/logic/include/ramses-logic/LuaInterface.h new file mode 100644 index 000000000..8ec752ff8 --- /dev/null +++ b/client/logic/include/ramses-logic/LuaInterface.h @@ -0,0 +1,61 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicNode.h" + +#include +#include +#include + +namespace ramses::internal +{ + class LuaInterfaceImpl; +} + +namespace ramses +{ + class Property; + + /** + * LuaInterface encapsulates forms an additional layer to a logic network (whole or its parts) and it exposes only relevant + * inputs to the user of that network. Those input variables can be used to represent scenes in an abstract way. + * LuaInterface instances are created by the #ramses::LogicEngine class. + * + * A LuaInterface can be created from Lua source code which must fulfill same requirements as #ramses::LuaScript, except for the following: + * + * - A Lua interface contains only one global function: interface() + * - The interface() function takes only one parameter that represents both inputs and outputs, e.g., interface(INOUT_PARAMS) + * + * Violating any of these requirements will result in errors, which can be obtained by calling + * #ramses::LogicEngine::getErrors(). + * For other than interface definition purposes, see #ramses::LuaModule and #ramses::LuaScript for details. + * + * See also the full documentation at https://ramses-logic.readthedocs.io/en/latest/api.html for more details on Lua and + * its interaction with C++. + */ + class LuaInterface : public LogicNode + { + public: + /** + * Implementation detail of LuaInterface + */ + internal::LuaInterfaceImpl& m_interface; + + protected: + /** + * Constructor of LuaInterface. User is not supposed to call this - interface objects are created by other factory classes + * + * @param impl implementation details of the interface + */ + explicit LuaInterface(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/LuaModule.h b/client/logic/include/ramses-logic/LuaModule.h new file mode 100644 index 000000000..4f778622f --- /dev/null +++ b/client/logic/include/ramses-logic/LuaModule.h @@ -0,0 +1,79 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicObject.h" + +namespace ramses::internal +{ + class LuaModuleImpl; +} + +namespace ramses +{ + /** + * #LuaModule represents Lua source code in form of a reusable Lua module + * which can be used in Lua scripts of one or more #ramses::LuaScript instances. Lua modules can + * be also declared as dependency to other modules. + * Lua modules are expected to follow these guidelines https://www.tutorialspoint.com/lua/lua_modules.htm + * and have some limitations: + * - the name of the module given when creating it is only used to differentiate between #LuaModule + * instances on Ramses logic API, not to access them from a script or to resolve a file on a filesystem + * - the module is not supposed to be resolved with the 'require' keyword in Lua scripts where used + * (due to security concerns) but must be provided explicitly when creating #ramses::LuaScript instance + * - modules are read-only in order to prevent data races when accessed from different scripts with undefined + * order of execution + * #ramses::LuaModule source code is loaded into its own Lua environment and is accessible in other #ramses::LuaScript + * and/or #LuaModule instances in their own environments under the alias name given when creating those. + * + * #LuaModule can also be used to help provide property types for #ramses::LuaScript interface declarations, + * for example a module with a 'struct' type: + * \code{.lua} + * local mytypes = {} + * function mytypes.mystruct() + * return Type:Struct({ + * name = Type:String(), + * address = Type:Struct({ + * street = Type:String(), + * number = Type:Int32() + * }) + * }) + * end + * return mytypes + * \endcode + * And script using above module to define its interface: + * \code{.lua} + * modules("mytypes") -- must declare the dependency explicitly + * function interface(IN,OUT) + * IN.input_struct = mytypes.mystruct() + * OUT.output_struct = mytypes.mystruct() + * end + * \endcode + * + * The label 'Type' is a reserved keyword and must not be overwritten for other purposes (e.g. name of variable or function). + */ + class LuaModule : public LogicObject + { + public: + /** + * Implementation detail of #LuaModule + */ + internal::LuaModuleImpl& m_impl; + + protected: + /** + * Internal constructor of #LuaModule. Use #ramses::LogicEngine::createLuaModule for user code. + * + * @param impl implementation details of the #LuaModule + */ + explicit LuaModule(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/LuaScript.h b/client/logic/include/ramses-logic/LuaScript.h new file mode 100644 index 000000000..e5727ae8b --- /dev/null +++ b/client/logic/include/ramses-logic/LuaScript.h @@ -0,0 +1,91 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicNode.h" + +#include +#include +#include + +namespace ramses::internal +{ + class LuaScriptImpl; +} + +namespace ramses +{ + class Property; + + using LuaPrintFunction = std::function; + + /** + * The LuaScript class is the cornerstone of RAMSES Logic as it encapsulates + * a Lua script and the associated with it Lua environment. LuaScript instances are created + * by the #ramses::LogicEngine class. + * + * A LuaScript can be created from Lua source code which must fulfill following requirements: + * + * - valid Lua 5.1 syntax + * - contains two global functions - interface(IN,OUT) and run(IN,OUT) with exactly two parameters (for inputs and outputs respectively) and no return values + * - declares its inputs and outputs in the interface(IN,OUT) function, and its logic in the run(IN,OUT) function - you can use different names than IN/OUT + * - the interface(IN,OUT) function declares zero or more inputs and outputs to the IN and OUT variables passed to the function + * - inputs and outputs are declared like this: + * \code{.lua} + * function interface(IN,OUT) + * IN.input_name = Type:T() + * OUT.output_name = Type:T() + * end + * \endcode + * - T is one of [Int32|Int64|Float|Bool|String|Vec2f|Vec3f|Vec4f|Vec2i|Vec3i|Vec4i|Struct|Array] + * - For T=Struct: + * - The first and only argument to the function must be a table + * - The table keys must be strings, and the table values must be again declared with Type:T() + * - For T=Array, the function declaration is of the form Type:Array(n, E) where: + * - n is a positive integer + * - E must be declared with Type:T() + * - E can be a Type:Struct(), i.e. arrays of structs are supported + * - Each property must have a name (string) - other types like number, bool etc. are not supported as keys + * - T can also be defined in a module, see #ramses::LuaModule for details + * - as a convenience abbreviation, you can use {...} instead of Type:Struct({...}) to declare structs + * - You can optionally declare an init() function with no parameters + * - init() is allowed to write data or functions to a predefined GLOBAL table + * - init() will be executed exactly once after loading the script (also when loading from binaries) + * - the data from GLOBAL will be available to the interface() and run() functions + * - no other global data can be read or written in any of the functions or in the global scope of the script + * - you may declare module dependencies using the modules() function + * - modules() accepts a vararg of strings for each module + * - the string provided will be the name under which the module will be available in other functions + * - See also #ramses::LuaModule for more info on modules and their syntax + * + * Violating any of these requirements will result in errors, which can be obtained by calling + * #ramses::LogicEngine::getErrors(). + * + * See also the full documentation at https://ramses-logic.readthedocs.io/en/latest/api.html for more details on Lua and + * its interaction with C++. + */ + class LuaScript : public LogicNode + { + public: + /** + * Implementation detail of LuaScript + */ + internal::LuaScriptImpl& m_script; + + protected: + /** + * Constructor of LuaScript. User is not supposed to call this - script are created by other factory classes + * + * @param impl implementation details of the script + */ + explicit LuaScript(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/Property.h b/client/logic/include/ramses-logic/Property.h new file mode 100644 index 000000000..bf367f6b3 --- /dev/null +++ b/client/logic/include/ramses-logic/Property.h @@ -0,0 +1,270 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-logic/EPropertyType.h" +#include "ramses-logic/PropertyLink.h" + +#include +#include +#include +#include + +namespace ramses::internal +{ + class PropertyImpl; +} + +namespace ramses +{ + class LogicNode; + + /** + * Represents a generic property slot of the #ramses::LogicNode and its derived classes. + * Properties can have primitive types (string, integer, etc.) or complex types (structs, arrays). + * Complex types can have "children", i.e. nested properties: named fields (structs), or indexed + * fields (arrays). + */ + class Property + { + public: + /** + * Returns the type of this Property + * + * @return the type of this Property + */ + [[nodiscard]] RAMSES_API EPropertyType getType() const; + + /** + * Returns the name of this Property. Note that not all properties have a name - + * for example an array element does not have a name. In that case the result + * will be an empty string view. Struct fields always have a non-empty name. + * + * @return the name of this Property + */ + [[nodiscard]] RAMSES_API std::string_view getName() const; + + /** + * Returns the amount of available child (nested) properties. If the Property + * is of type #ramses::EPropertyType::Struct, the returned number will correspond + * to the number of named properties of the struct. If the Property is of type + * #ramses::EPropertyType::Array, the method will return the array size. For all + * other property types #getChildCount returns zero. + * + * @return the number of nested properties. + */ + [[nodiscard]] RAMSES_API size_t getChildCount() const; + + /** + * Return whether the #ramses::Property has a child with the given name + * + * @param name the name of the child that should be checked for existence + * @return whether the child with the given name exists + */ + [[nodiscard]] RAMSES_API bool hasChild(std::string_view name) const; + + /** + * Returns the child property with the given \p index. \p index must be < #getChildCount(). + * + * This method can be used to get nested properties of structs and arrays. For primitive types this will + * always return nullptr. + * + * Note that array indexing in #getChild follows C++ conventions and **not** Lua conventions! Inside the Lua scripts, you can + * and must use Lua conventions when indexing arrays (start at 1, end at N) while in C++ you must use [0, N-1]. + * + * Struct properties can also be retrieved by index. The ordering is **not** guaranteed to match the order of + * declaration inside Lua scripts (Lua explicitly warns to not rely on ordering of named table entries!). + * However, once a script is created, the index will not change, i.e. it is permitted that user code caches + * the property index for faster future access. + * + * @param[in] index zero based index of child property to retrieve + * @return the child with the given index, or nullptr if the property is primitive or the index is out of range + */ + [[nodiscard]] RAMSES_API const Property* getChild(size_t index) const; + + /** + * @copydoc getChild(size_t index) const + */ + [[nodiscard]] RAMSES_API Property* getChild(size_t index); + + /** + * Searches for a child with the given name. Only properties of type #ramses::EPropertyType::Struct can return a child by name. + * In case of a primitive property or array this method will return nullptr. + * + * Note that this method may be slower than #getChild(size_t index) as it must do a string-based search. + * + * @param[in] name name of child property to retrieve + * @return the child with the given name, or nullptr if property is not of type #ramses::EPropertyType::Struct + */ + [[nodiscard]] RAMSES_API Property* getChild(std::string_view name); + + /** + * @copydoc getChild(std::string_view name) + */ + [[nodiscard]] RAMSES_API const Property* getChild(std::string_view name) const; + + /** + * Returns the value of this property. The supported template types are defined by + * #ramses::IsPrimitiveProperty where IsPrimitiveProperty::value == true for a type T. + * + * Attention! We recommend always specifying the template argument T explicitly, and don't rely on the compiler's + * type deduction! If T is not one of the supported types, a static_assert will be triggered! + * + * Returns nullopt if the template type T does not match the internal type of the property. + * + * @return the value of this Property as std::optional or std::nullopt if T does not match. + */ + template [[nodiscard]] std::optional get() const; + + /** + * Sets the value of this Property. Same rules apply to template parameter T as in #get() + * + * Attention! We recommend always specifying the template argument T explicitly, and don't rely on the compiler's + * type deduction! If T is not one of the supported types, a static_assert will be triggered! + * + * #set() will report an error if the Property is linked. Use #hasIncomingLink() to check in advance. + * + * @param value the value to set for this Property + * @return true if setting the \p value was successful, false otherwise. + */ + template bool set(T value); + + /** + * Checks if this property is linked to a property of another node. + * + * Property can be either on inputs side or outputs side of a node (or both in case of #ramses::LuaInterface), + * which defines what type of link is applicable - an incoming or outgoing. + * This query checks for any links, regardless of data flow direction. + * See #hasIncomingLink() #hasOutgoingLink() for dedicated checks + * + * @return true if the property is linked, false otherwise. + */ + [[nodiscard]] RAMSES_API bool isLinked() const; + + /** + * Checks if there is any incoming link to this property, i.e. data is flowing to it from another node's property. + * There can be only one incoming link per input property. + * + * @return true if the property is an input and is linked, false otherwise. + */ + [[nodiscard]] RAMSES_API bool hasIncomingLink() const; + + /** + * Checks if there is any outgoing link from this property, i.e. data is flowing from it to another node's property. + * There can be multiple outgoing links per output property. + * + * @return true if the property is an output and is linked, false otherwise. + */ + [[nodiscard]] RAMSES_API bool hasOutgoingLink() const; + + /** + * Returns information about the incoming link to this property, namely which property is the source of the link. + * Only certain properties can have an incoming link, see #ramses::LogicEngine::link for details. + * + * @return incoming link data or std::nullopt if there is no link. + */ + [[nodiscard]] RAMSES_API std::optional getIncomingLink() const; + + /** + * Returns the number of outgoing links from this property. + * Only certain properties can have outgoing links, see #ramses::LogicEngine::link for details. + * + * @return number of outgoing links from this property. + */ + [[nodiscard]] RAMSES_API size_t getOutgoingLinksCount() const; + + /** + * Returns information about the outgoing link from this property, namely which property is the target of the link with given index. + * Use #getOutgoingLinksCount to query how many outgoing links there are. + * Only certain properties can have outgoing links, see #ramses::LogicEngine::link for details. + * + * @param[in] index zero based index of outgoing link to retrieve + * @return outgoing link data or std::nullopt if there is no link with given index. + */ + [[nodiscard]] RAMSES_API std::optional getOutgoingLink(size_t index) const; + + /** + * Get #ramses::LogicNode that owns this property. + * Every property is owned by a #ramses::LogicNode and represents either its input or output. + * + * @return #ramses::LogicNode that owns this property. + */ + [[nodiscard]] RAMSES_API const LogicNode& getOwningLogicNode() const; + + /// @copydoc getOwningLogicNode() const + [[nodiscard]] RAMSES_API LogicNode& getOwningLogicNode(); + + /** + * Copy Constructor of Property is deleted because properties are not supposed to be copied + * @param other property to copy from + */ + Property(const Property& other) = delete; + + /** + * Move Constructor of Property is deleted because properties are not supposed to be moved + * @param other property to move from + */ + Property(Property&& other) = delete; + + /** + * Assignment operator of Property is deleted because properties are not supposed to be copied + * @param other property to assign from + */ + Property& operator=(const Property& other) = delete; + + /** + * Move assignment operator of Property is deleted because properties are not supposed to be moved + * @param other property to move from + */ + Property& operator=(Property&& other) = delete; + + /** + * Implementation details of the Property class + */ + std::unique_ptr m_impl; + + protected: + /** + * Constructor of Property. User is not supposed to call this - properties are created by other factory classes + * + * @param impl implementation details of the property + */ + explicit Property(std::unique_ptr impl) noexcept; + + /** + * Destructor of Property. User is not supposed to call this - properties are destroyed by other factory classes + */ + ~Property() noexcept; + + private: + /** + * Internal implementation of #get + */ + template [[nodiscard]] RAMSES_API std::optional getInternal() const; + /** + * Internal implementation of #set + */ + template RAMSES_API bool setInternal(T value); + + friend class internal::PropertyImpl; + }; + + template std::optional Property::get() const + { + static_assert(IsPrimitiveProperty::value, "Call get only with types which have a value! Read the docs of the method!"); + return getInternal(); + } + + template bool Property::set(T value) + { + static_assert(IsPrimitiveProperty::value, "Call set only with types which have a value! Read the docs of the method!"); + return setInternal(value); + } +} diff --git a/client/logic/include/ramses-logic/PropertyLink.h b/client/logic/include/ramses-logic/PropertyLink.h new file mode 100644 index 000000000..d03db6aa6 --- /dev/null +++ b/client/logic/include/ramses-logic/PropertyLink.h @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +namespace ramses +{ + class Property; + + /** + * @ingroup LogicAPI + * PropertyLink describes a data link between two properties, each belonging to a #ramses::LogicNode. + * Data flows always from \c source (some #ramses::LogicNode's output) to \c target (another #ramses::LogicNode's input), + * see #ramses::LogicEngine::link and #ramses::LogicEngine::update for more details. + * PropertLink can be obtained using #ramses::Property::getIncomingLink, #ramses::Property::getOutgoingLink + * or #ramses::LogicEngine::getPropertyLinks. + */ + struct PropertyLink + { + /// data flow source (some #ramses::LogicNode's output) + const Property* source = nullptr; + /// data flow target (another #ramses::LogicNode's input) + const Property* target = nullptr; + /// \c true if this is a weak link - see #ramses::LogicEngine::linkWeak + bool isWeakLink = false; + }; +} diff --git a/client/logic/include/ramses-logic/RamsesAppearanceBinding.h b/client/logic/include/ramses-logic/RamsesAppearanceBinding.h new file mode 100644 index 000000000..9eab1a829 --- /dev/null +++ b/client/logic/include/ramses-logic/RamsesAppearanceBinding.h @@ -0,0 +1,74 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-logic/RamsesBinding.h" + +#include + +namespace ramses +{ + class Appearance; +} + +namespace ramses::internal +{ + class RamsesAppearanceBindingImpl; +} + +namespace ramses +{ + /** + * The RamsesAppearanceBinding is a type of #ramses::RamsesBinding which allows the #ramses::LogicEngine to control instances + * of ramses::Appearance. RamsesAppearanceBinding's can be created with #ramses::LogicEngine::createRamsesAppearanceBinding. + * + * The #RamsesAppearanceBinding has a static link to a ramses::Appearance. After creation, #ramses::LogicNode::getInputs will + * return a struct property with children equivalent to the uniform inputs of the provided ramses Appearance. + * + * Since the RamsesAppearanceBinding derives from #ramses::RamsesBinding, it also provides the #ramses::LogicNode::getInputs + * and #ramses::LogicNode::getOutputs method. For this particular implementation, the methods behave as follows: + * - #ramses::LogicNode::getInputs: returns the inputs corresponding to the available shader uniforms of the bound ramses::Appearance + * - #ramses::LogicNode::getOutputs: returns always nullptr, because a #RamsesAppearanceBinding does not have outputs, + * it implicitly controls the ramses Appearance + * - The values of this binding's inputs are initialized to default values (0, 0.0f, etc) and *not* loaded from the values in Ramses + * + * All shader uniforms are supported, except the following: + * - texture samplers of any kind + * - matrix types (e.g. mat4, mat23 etc.) + * - any uniform with attached semantics (e.g. display resolution) - see ramses::EEffectUniformSemantic docs + * + * Uniform types which are not supported are not available when queried over #ramses::LogicNode::getInputs. + * + */ + class RamsesAppearanceBinding : public RamsesBinding + { + public: + /** + * Returns the bound Ramses Appearance. + * @return the bound ramses appearance + */ + [[nodiscard]] RAMSES_API ramses::Appearance& getRamsesAppearance() const; + + /** + * Implementation detail of RamsesAppearanceBinding + */ + internal::RamsesAppearanceBindingImpl& m_appearanceBinding; + + protected: + /** + * Constructor of RamsesAppearanceBinding. User is not supposed to call this - RamsesAppearanceBinding are created by other factory classes + * + * @param impl implementation details of the RamsesAppearanceBinding + */ + explicit RamsesAppearanceBinding(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/RamsesBinding.h b/client/logic/include/ramses-logic/RamsesBinding.h new file mode 100644 index 000000000..1aa1aedc0 --- /dev/null +++ b/client/logic/include/ramses-logic/RamsesBinding.h @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicNode.h" + +namespace ramses::internal +{ + class RamsesBindingImpl; +} + +namespace ramses +{ + /** + * The RamsesBinding is a shared base class for bindings to Ramses objects. + * For details on each type of binding, look at the derived classes. + */ + class RamsesBinding : public LogicNode + { + protected: + /** + * Constructor of RamsesBinding. User is not supposed to call this - RamsesBinding are created by other factory classes + * + * @param impl implementation details of the RamsesBinding + */ + explicit RamsesBinding(std::unique_ptr impl) noexcept; + }; +} diff --git a/client/logic/include/ramses-logic/RamsesCameraBinding.h b/client/logic/include/ramses-logic/RamsesCameraBinding.h new file mode 100644 index 000000000..35f3c93bd --- /dev/null +++ b/client/logic/include/ramses-logic/RamsesCameraBinding.h @@ -0,0 +1,104 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-logic/RamsesBinding.h" + +#include + +namespace ramses +{ + class Camera; +} + +namespace ramses::internal +{ + class RamsesCameraBindingImpl; +} + +namespace ramses +{ + /** + * The RamsesCameraBinding is a type of #ramses::RamsesBinding which allows the #ramses::LogicEngine to control instances of ramses::Camera. + * RamsesCameraBinding's can be created with #ramses::LogicEngine::createRamsesCameraBinding or #ramses::LogicEngine::createRamsesCameraBindingWithFrustumPlanes, + * which affects the set of input properties that will be used to control camera frustum as described below. + * + * The #RamsesCameraBinding has a static link to a ramses::Camera. After creation, #ramses::LogicNode::getInputs will + * return a struct property with children equivalent to the camera settings of the provided ramses::Camera. + * + * There are two types of ramses::Camera: + * - ramses::PerspectiveCamera + * - ramses::OrthographicCamera + * + * Both camera types are defined through their viewport and their frustum properties. These are represented as two separate property structs + * in the RamsesCameraBinding. Be aware if you set one or more values to one of the structs on the binding and update the LogicEngine it will lead + * to all properties of this struct being set on the actual ramses::Camera. + * For example if you only set the 'offsetX' of the Camera viewport it will set + * all other viewport properties as well (offsetY, width and height) to whatever their state is at that moment. + * The values of the #RamsesCameraBinding inputs are initialized with the values of the provided ramses::Camera during creation. + * The frustum values of the ramses::Camera are not affected when setting viewport values, and vice-versa. + * Check the ramses::Camera API to see which values belong together. + * To avoid unexpected behavior, we highly recommend setting all viewport values together, and also setting all frustum planes together + * (either by link or by setting them directly). This way unwanted behavior can be avoided. + * + * Since the RamsesCameraBinding derives from #ramses::RamsesBinding, it also provides the #ramses::LogicNode::getInputs + * and #ramses::LogicNode::getOutputs method. For this class, the methods behave as follows: + * - #ramses::LogicNode::getInputs: returns inputs struct with two child properties: viewport and frustum. + * - 'viewport' (type struct) with these children: + * - 'offsetX' (type Int32) - viewport offset horizontal + * - 'offsetY' (type Int32) - viewport offset vertical + * - 'width' (type Int32) - viewport width + * - 'height' (type Int32) - viewport height + * - 'frustum' (type struct) children vary depending on frustum parameters to use, it will be one of the following sets of parameters: + * - full set of frustum plane properties: + * - 'nearPlane' (type Float) - frustum plane near + * - 'farPlane' (type Float) - frustum plane far + * - 'leftPlane' (type Float) - frustum plane left + * - 'rightPlane' (type Float) - frustum plane right + * - 'bottomPlane' (type Float) - frustum plane bottom + * - 'topPlane' (type Float) - frustum plane top + * - simplified set of frustum properties: + * - 'nearPlane' (type Float) - frustum plane near + * - 'farPlane' (type Float) - frustum plane far + * - 'fieldOfView' (type Float) - frustum field of view in degrees + * - 'aspectRatio' (type Float) - aspect ratio of frustum width / frustum height + * Full set of frustum planes properties will be present if camera is ramses::Orthographic (regardless of which create method was used) + * or camera is ramses::PerspectiveCamera and #ramses::LogicEngine::createRamsesCameraBindingWithFrustumPlanes was used to create it. + * Simplified set of frustum properties will be present if camera is ramses::PerspectiveCamera and #ramses::LogicEngine::createRamsesCameraBinding was used to create it. + * Refer to ramses::Camera, ramses::PerspectiveCamera and ramses::OrthographicCamera for meaning and constraints of all these inputs. + * + * - #ramses::LogicNode::getOutputs: returns always nullptr, because a #RamsesCameraBinding does not have outputs, + * it implicitly controls the ramses Camera + */ + class RamsesCameraBinding : public RamsesBinding + { + public: + /** + * Returns the bound ramses camera. + * @return the bound ramses camera + */ + [[nodiscard]] RAMSES_API ramses::Camera& getRamsesCamera() const; + + /** + * Implementation detail of RamsesCameraBinding + */ + internal::RamsesCameraBindingImpl& m_cameraBinding; + + protected: + /** + * Constructor of RamsesCameraBinding. User is not supposed to call this - RamsesCameraBindings are created by other factory classes + * + * @param impl implementation details of the RamsesCameraBinding + */ + explicit RamsesCameraBinding(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/RamsesLogicVersion.h b/client/logic/include/ramses-logic/RamsesLogicVersion.h new file mode 100644 index 000000000..b0ec0ba30 --- /dev/null +++ b/client/logic/include/ramses-logic/RamsesLogicVersion.h @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include +#include + +namespace ramses +{ + /** + * @brief Ramses Logic version information + */ + struct RamsesLogicVersion + { + /// Version information as string in format major.minor.patch with an optional arbitrary suffix + const std::string_view string; + + /// Major version + uint32_t major; + /// Minor version + uint32_t minor; + /// Patch version + uint32_t patch; + }; + + /** + * @brief Retrieve currently used Ramses Logic version information + * @returns the Ramses Logic version of the currently used build + */ + [[nodiscard]] RAMSES_API RamsesLogicVersion GetRamsesLogicVersion(); +} diff --git a/client/logic/include/ramses-logic/RamsesMeshNodeBinding.h b/client/logic/include/ramses-logic/RamsesMeshNodeBinding.h new file mode 100644 index 000000000..764818463 --- /dev/null +++ b/client/logic/include/ramses-logic/RamsesMeshNodeBinding.h @@ -0,0 +1,79 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-logic/RamsesBinding.h" + +namespace ramses +{ + class MeshNode; +} + +namespace ramses::internal +{ + class RamsesMeshNodeBindingImpl; +} + +namespace ramses +{ + /** + * The #RamsesMeshNodeBinding binds to a Ramses object instance of ramses::MeshNode and exposes a set of input properties + * allowing to control certain states the MeshNode. + * + * #RamsesMeshNodeBinding has these input properties: + * 'vertexOffset' (type int32) + * - binds to MeshNode's vertex offset, i.e. start offset into vertex arrays of the mesh (ramses::MeshNode::setStartVertex). + * 'indexOffset' (type int32) + * - binds to MeshNode's index offset, i.e. start offset into indices of the mesh (ramses::MeshNode::setStartIndex). + * 'indexCount' (type int32) + * - binds to MeshNode's index count, i.e. number of indices to use for rendering of the mesh (ramses::MeshNode::setIndexCount). + * 'instanceCount' (type int32) + * - binds to MeshNode's instance count, i.e. number of instances to render when using geometry instancing (ramses::MeshNode::setInstanceCount). + * + * The initial values of the input properties are taken from the bound ramses::MeshNode when the #RamsesMeshNodeBinding is created. + * + * The #RamsesMeshNodeBinding class has no output properties (thus #ramses::LogicNode::getOutputs() will return nullptr) because + * the outputs are implicitly forwarded to the bound ramses::MeshNode. + * + * The changes via binding objects are applied to the bound object right away when calling ramses::LogicEngine::update(), + * however keep in mind that Ramses has a mechanism for bundling scene changes and applying them at once using ramses::Scene::flush, + * so the changes will be applied all the way only after calling this method on the scene. + */ + class RamsesMeshNodeBinding : public RamsesBinding + { + public: + /** + * Returns the bound Ramses MeshNode. + * + * @return the bound Ramses MeshNode + */ + [[nodiscard]] RAMSES_API const ramses::MeshNode& getRamsesMeshNode() const; + + /** + * Returns the bound Ramses MeshNode. + * + * @return the bound Ramses MeshNode + */ + [[nodiscard]] RAMSES_API ramses::MeshNode& getRamsesMeshNode(); + + /// Implementation detail of RamsesMeshNodeBinding + internal::RamsesMeshNodeBindingImpl& m_meshNodeBinding; + + protected: + /** + * Constructor of RamsesMeshNodeBinding. User is not supposed to call this - RamsesMeshNodeBindings are created by other factory classes + * + * @param impl implementation details of the RamsesMeshNodeBinding + */ + explicit RamsesMeshNodeBinding(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/RamsesNodeBinding.h b/client/logic/include/ramses-logic/RamsesNodeBinding.h new file mode 100644 index 000000000..a9128051a --- /dev/null +++ b/client/logic/include/ramses-logic/RamsesNodeBinding.h @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-client-api/ERotationType.h" +#include "ramses-logic/RamsesBinding.h" + +#include + +namespace ramses +{ + class Node; +} + +namespace ramses::internal +{ + class RamsesNodeBindingImpl; +} + +namespace ramses +{ + /** + * The RamsesNodeBinding is a #ramses::RamsesBinding which allows manipulation of a Ramses node. + * RamsesNodeBinding can be created using #ramses::LogicEngine::createRamsesNodeBinding. + * + * The RamsesNodeBinding has a fixed set of inputs which correspond to the properties of a ramses::Node: + * 'visibility' (type bool) - binds to node's visibility mode to switch between visible and invisible, + * see also 'enabled' input for more options + * 'rotation' (type vec3f, or vec4f in case of quaternion) - binds to node's rotation, + * a vec3f represents the Euler angles v[0]:x, v[1]:y, v[2]:z + * a vec4f represents a quaternion with v[0]:x, v[1]:y, v[2]:z containing the quaternion's vector component + * and v[3]:w the quaternion's scalar component + * 'translation' (type vec3f) - binds to node's translation + * 'scaling' (type vec3f) - binds to node's scaling + * 'enabled' (type bool) - binds to node's visibility mode to be able to switch node to off mode + * (see ramses::EVisibilityMode). When 'enabled' is true (default) + * the visibility mode is determined by the 'visibility' input above. + * When 'enabled' is false, the visibility mode is Off, + * regardless of the 'visibility' input. + * + * The default values of the input properties are taken from the bound ramses::Node provided during construction. + * This also applies for rotations, if the ramses::ERotationType values of the + * ramses node match (both are either Quaternion or Euler with the same axis ordering). Otherwise a warning is + * issued and the rotation values are set to 0. + * + * The RamsesNodeBinding class has no output properties (thus getOutputs() will return nullptr) because + * the outputs are implicitly the properties of the bound Ramses node. + * + * \rst + .. note:: + + Not all states of the #ramses::Node must be managed via #RamsesNodeBinding, input properties which are not modified + via #RamsesNodeBinding (i.e. user never set a value explicitly nor linked the input) will not alter the corresponding Ramses object + state. This allows the user code to control some states via Ramses logic nodes and some directly using Ramses object. + + \endrst + * + * The changes via binding objects are applied to the bound object right away when calling ramses::LogicEngine::update(), + * however keep in mind that Ramses has a mechanism for bundling scene changes and applying them at once using ramses::Scene::flush, + * so the changes will be applied all the way only after calling this method on the scene. + */ + class RamsesNodeBinding : public RamsesBinding + { + public: + /** + * Returns the bound ramses node. + * + * @return the bound ramses node + */ + [[nodiscard]] RAMSES_API ramses::Node& getRamsesNode() const; + + /** + * Returns the statically configured rotation type for the node rotation property. + * + * @return the currently used rotation type + */ + [[nodiscard]] RAMSES_API ramses::ERotationType getRotationType() const; + + /** + * Implementation detail of RamsesNodeBinding + */ + internal::RamsesNodeBindingImpl& m_nodeBinding; + + protected: + /** + * Constructor of RamsesNodeBinding. User is not supposed to call this - RamsesNodeBindings are created by other factory classes + * + * @param impl implementation details of the RamsesNodeBinding + */ + explicit RamsesNodeBinding(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/RamsesRenderGroupBinding.h b/client/logic/include/ramses-logic/RamsesRenderGroupBinding.h new file mode 100644 index 000000000..8f39e6b7a --- /dev/null +++ b/client/logic/include/ramses-logic/RamsesRenderGroupBinding.h @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-logic/RamsesBinding.h" + +namespace ramses +{ + class RenderGroup; +} + +namespace ramses::internal +{ + class RamsesRenderGroupBindingImpl; +} + +namespace ramses +{ + /** + * The RamsesRenderGroupBinding binds to a Ramses object instance of ramses::RenderGroup. + * RamsesRenderGroupBinding allows controlling the rendering order of selected elements + * contained in the ramses::RenderGroup - these can be ramses::MeshNode and other (nested) ramses::RenderGroup objects. + * + * #RamsesRenderGroupBinding is initialized with a ramses::RenderGroup and a set of elements to expose for render order + * control through the binding's input properties. Not all the elements contained in the ramses::RenderGroup have to be provided, + * the binding can be used to control only a subset of the elements in the render group. The set of provided elements is then static + * for the lifetime of the #RamsesRenderGroupBinding. + * + * It is not possible to add or remove elements from the bound ramses::RenderGroup using this binding + * and this binding will not reflect changes made to the ramses::RenderGroup via its Ramses API. + * Therefore it is important to keep the binding valid with respect to the ramses::RenderGroup it is bound to, for example: + * - An element is added to the bound ramses::RenderGroup via Ramses API - the binding with its elements is valid + * and can be used but the newly added element will not be known to it (cannot control its render order). + * - An element is removed from the bound ramses::RenderGroup via Ramses API - if the removed element was part of the provided + * initial set at construction of the binding, the binding is invalidated and should not be used because any attempt to change + * render order of the removed element will result in update fail. + * If #RamsesRenderGroupBinding gets invalidated it needs to be destroyed and recreated with a valid set of elements contained in the render group. + * In this case property links (if any) get destroyed with the binding and need to be re-created. + * + * When #RamsesRenderGroupBinding is created using #ramses::LogicEngine::createRamsesRenderGroupBinding it will create + * property inputs based on the provided set of elements (see #ramses::RamsesRenderGroupBindingElements): + * 'renderOrders' (type struct) - contains all the elements provided as children: + * [element1Name] (type int32) - binds to render order of that element within the bound ramses::RenderGroup + * ... + * [elementNName] (type int32) + * The property name for each element can be specified when adding the element using #ramses::RamsesRenderGroupBindingElements::addElement. + * Note that the order of the render order input properties exactly matches the order of adding elements + * via #ramses::RamsesRenderGroupBindingElements::addElement. + * + * The render order is directly forwarded to Ramses and follows Ramses behavior of ordering, + * i.e. element with lower render order value gets rendered before element with higher value. + * If two elements have same number (default) their render order is not defined and the Ramses renderer decides their order + * based on its optimization criteria, it is therefore recommended to explicitly specify render order only if needed. + * + * The initial render order values of the input properties are taken from the bound ramses::RenderGroup's elements provided during construction. + * + * The #RamsesRenderGroupBinding class has no output properties (thus #ramses::LogicNode::getOutputs() will return nullptr) because + * the outputs are implicitly forwarded to the bound ramses::RenderGroup. + * + * The changes via binding objects are applied to the bound object right away when calling ramses::LogicEngine::update(), + * however keep in mind that Ramses has a mechanism for bundling scene changes and applying them at once using ramses::Scene::flush, + * so the changes will be applied all the way only after calling this method on the scene. + */ + class RamsesRenderGroupBinding : public RamsesBinding + { + public: + /** + * Returns the bound ramses render group. + * + * @return the bound ramses render group + */ + [[nodiscard]] RAMSES_API const ramses::RenderGroup& getRamsesRenderGroup() const; + + /** + * Returns the bound ramses render group. + * + * @return the bound ramses render group + */ + [[nodiscard]] RAMSES_API ramses::RenderGroup& getRamsesRenderGroup(); + + /// Implementation detail of RamsesRenderGroupBinding + internal::RamsesRenderGroupBindingImpl& m_renderGroupBinding; + + protected: + /** + * Constructor of RamsesRenderGroupBinding. User is not supposed to call this - RamsesRenderGroupBindings are created by other factory classes + * + * @param impl implementation details of the RamsesRenderGroupBinding + */ + explicit RamsesRenderGroupBinding(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/RamsesRenderGroupBindingElements.h b/client/logic/include/ramses-logic/RamsesRenderGroupBindingElements.h new file mode 100644 index 000000000..5121b8607 --- /dev/null +++ b/client/logic/include/ramses-logic/RamsesRenderGroupBindingElements.h @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include +#include + +namespace ramses +{ + class RenderGroup; + class MeshNode; +} + +namespace ramses::internal +{ + class RamsesRenderGroupBindingElementsImpl; +} + +namespace ramses +{ + /** + * RamsesRenderGroupBindingElements is a helper class holding a set of references to elements to be provided when constructing #ramses::RamsesRenderGroupBinding. + * These elements are either ramses::MeshNode or ramses::RenderGroup. + * Note that ramses::RenderGroup can contain other (nested) ramses::RenderGroup objects, in such case the parent ramses::RenderGroup corresponds + * to the #ramses::RamsesRenderGroupBinding to be created and the nested ramses::RenderGroup is the element provided here. + */ + class RamsesRenderGroupBindingElements + { + public: + /// Constructor of RamsesRenderGroupBindingElements. + RAMSES_API RamsesRenderGroupBindingElements() noexcept; + /// Destructor of RamsesRenderGroupBindingElements. + RAMSES_API ~RamsesRenderGroupBindingElements() noexcept; + /// Copy constructor + RAMSES_API RamsesRenderGroupBindingElements(const RamsesRenderGroupBindingElements& other); + /// Move constructor + RAMSES_API RamsesRenderGroupBindingElements(RamsesRenderGroupBindingElements&& other) noexcept; + /// Assignment operator + RAMSES_API RamsesRenderGroupBindingElements& operator=(const RamsesRenderGroupBindingElements& other); + /// Move assignment operator + RAMSES_API RamsesRenderGroupBindingElements& operator=(RamsesRenderGroupBindingElements&& other) noexcept; + + /** + * Add ramses::MeshNode element to control its render order when provided to #ramses::RamsesRenderGroupBinding. + * Will fail if given element is already contained. + * + * @param meshNode ramses::MeshNode element to add to be exposed for render order control. + * @param elementName This name will be used to name the input property in the created #ramses::RamsesRenderGroupBinding. + * If none provided, name of the ramses::MeshNode will be used. + * @return \c true if successful, \c false otherwise. + */ + RAMSES_API bool addElement(const ramses::MeshNode& meshNode, std::string_view elementName = {}); + + /** + * Add ramses::RenderGroup element to control its render order when provided to #ramses::RamsesRenderGroupBinding. + * Will fail if given element is already contained. + * + * @param nestedRenderGroup ramses::RenderGroup element to add to be exposed for render order control. + * @param elementName This name will be used to name the input property in the created #ramses::RamsesRenderGroupBinding. + * If none provided, name of the ramses::RenderGroup will be used. + * @return \c true if successful, \c false otherwise. + */ + RAMSES_API bool addElement(const ramses::RenderGroup& nestedRenderGroup, std::string_view elementName = {}); + + /// Implementation detail of RamsesRenderGroupBindingElements + std::unique_ptr m_impl; + }; +} diff --git a/client/logic/include/ramses-logic/RamsesRenderPassBinding.h b/client/logic/include/ramses-logic/RamsesRenderPassBinding.h new file mode 100644 index 000000000..1aafe7caf --- /dev/null +++ b/client/logic/include/ramses-logic/RamsesRenderPassBinding.h @@ -0,0 +1,86 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-logic/RamsesBinding.h" + +namespace ramses +{ + class RenderPass; +} + +namespace ramses::internal +{ + class RamsesRenderPassBindingImpl; +} + +namespace ramses +{ + /** + * The RamsesRenderPassBinding binds to a Ramses object instance like other types derived from + * #ramses::RamsesBinding, in this case it binds to a ramses::RenderPass. + * RamsesRenderPassBinding can be created using #ramses::LogicEngine::createRamsesRenderPassBinding + * after providing an instance of a ramses::RenderPass. + * + * The RamsesRenderPassBinding has a fixed set of inputs which correspond to some of the parameters in ramses::RenderPass: + * 'enabled' (type bool) - binds to enable/disable state (see ramses::RenderPass::setEnabled) + * 'renderOrder' (type int32) - binds to render order (see ramses::RenderPass::setRenderOrder) + * 'clearColor' (type vec4f) - binds to color used when clearing render pass (see ramses::RenderPass::setClearColor) + * 'renderOnce' (type bool) - binds to render once on/off state (see ramses::RenderPass::setRenderOnce) + * + * The initial values of the input properties are taken from the bound ramses::RenderPass provided during construction. + * + * The #RamsesRenderPassBinding class has no output properties (thus #ramses::LogicNode::getOutputs() will return nullptr) because + * the outputs are implicitly the properties of the bound ramses::RenderPass. + * + * \rst + .. note:: + + Not all states of the #ramses::RenderPass must be managed via #RamsesRenderPassBinding, input properties which are not modified + via #RamsesRenderPassBinding (i.e. user never set a value explicitly nor linked the input) will not alter the corresponding Ramses object + state. This allows the user code to control some states via Ramses logic nodes and some directly using Ramses object. + + \endrst + * + * The changes via binding objects are applied to the bound object right away when calling ramses::LogicEngine::update(), + * however keep in mind that Ramses has a mechanism for bundling scene changes and applying them at once using ramses::Scene::flush, + * so the changes will be applied all the way only after calling this method on the scene. + */ + class RamsesRenderPassBinding : public RamsesBinding + { + public: + /** + * Returns the bound ramses render pass. + * + * @return the bound ramses render pass + */ + [[nodiscard]] RAMSES_API const ramses::RenderPass& getRamsesRenderPass() const; + + /** + * Returns the bound ramses render pass. + * + * @return the bound ramses render pass + */ + [[nodiscard]] RAMSES_API ramses::RenderPass& getRamsesRenderPass(); + + /// Implementation detail of RamsesRenderPassBinding + internal::RamsesRenderPassBindingImpl& m_renderPassBinding; + + protected: + /** + * Constructor of RamsesRenderPassBinding. User is not supposed to call this - RamsesRenderPassBindings are created by other factory classes + * + * @param impl implementation details of the RamsesRenderPassBinding + */ + explicit RamsesRenderPassBinding(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/SaveFileConfig.h b/client/logic/include/ramses-logic/SaveFileConfig.h new file mode 100644 index 000000000..b9f154c8d --- /dev/null +++ b/client/logic/include/ramses-logic/SaveFileConfig.h @@ -0,0 +1,115 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-logic/ELuaSavingMode.h" + +#include +#include + +namespace ramses::internal +{ + class SaveFileConfigImpl; +} + +namespace ramses +{ + /** + * Holds configuration settings for saving #ramses::LogicEngine instances into a file. + * This config file is designed to work with the Ramses Composer editor, but you can use the metadata to store + * version and export information from any exporter (or script). Use the config to trace the origin and + * export environment of assets during runtime. + */ + class SaveFileConfig + { + public: + RAMSES_API SaveFileConfig() noexcept; + + /** + * Adds custom string metadata to the binary file saved by #ramses::LogicEngine. Can be any string. It is not + * used by anything other than logging info, particularly when loading a file and when errors occur. + * + * @param metadata the string to be written together with the saved binary data + */ + RAMSES_API void setMetadataString(std::string_view metadata); + + /** + * Specifies the semantic version of the exporter (binary, or script, or runtime logic). Use this to match a + * given asset later (e.g. when used in runtime) to the originating export tool to verify compatibility or + * diagnose errors and issues easier. + * + * @param major the major version number + * @param minor the minor version number + * @param patch the patch version number + * @param fileFormatVersion the file format used by the exporter itself (use 0 if not applicable) + */ + RAMSES_API void setExporterVersion(uint32_t major, uint32_t minor, uint32_t patch, uint32_t fileFormatVersion); + + /** + * By default, saving to file validates the content and issues warnings (see #ramses::LogicEngine::validate). + * This behavior can be disabled here. Calling this with validationEnabled=false will itself cause a INFO log, + * but will silence further warnings in the content. + * + * @param validationEnabled flag to disable/enable validation upon saving to file + */ + RAMSES_API void setValidationEnabled(bool validationEnabled); + + /** + * Sets saving mode for all #ramses::LuaScript and/or #ramses::LuaModule instances. + * See #ramses::ELuaSavingMode for the available options and their implications. + * Note that this is just a hint and the export logic will decide what to actually export, + * depending on availabilty of Lua source code or bytecode: + * - if only source code is available + * then only source code is exported regardless of the selected saving \c mode + * - if only byte code is available then only bytecode is exported regardless of the selected saving \c mode + * - if both source and bytecode are available then the selected saving \c mode is applied + * There will be no error produced if selected saving mode cannot be respected. + * + * @param mode selected saving mode, default is #ramses::ELuaSavingMode::SourceAndByteCode + */ + RAMSES_API void setLuaSavingMode(ELuaSavingMode mode); + + /** + * Destructor of #SaveFileConfig + */ + RAMSES_API ~SaveFileConfig() noexcept; + + /** + * Copy Constructor of #SaveFileConfig + * @param other the other #SaveFileConfig to copy from + */ + RAMSES_API SaveFileConfig(const SaveFileConfig& other); + + /** + * Move Constructor of #SaveFileConfig + * @param other the other #SaveFileConfig to move from + */ + RAMSES_API SaveFileConfig(SaveFileConfig&& other) noexcept; + + /** + * Assignment operator of #SaveFileConfig + * @param other the other #SaveFileConfig to copy from + * @return self + */ + RAMSES_API SaveFileConfig& operator=(const SaveFileConfig& other); + + /** + * Move assignment operator of #SaveFileConfig + * @param other the other #SaveFileConfig to move from + * @return self + */ + RAMSES_API SaveFileConfig& operator=(SaveFileConfig&& other) noexcept; + + /** + * Implementation detail of #SaveFileConfig + */ + std::unique_ptr m_impl; + }; +} diff --git a/client/logic/include/ramses-logic/SkinBinding.h b/client/logic/include/ramses-logic/SkinBinding.h new file mode 100644 index 000000000..0d009a73a --- /dev/null +++ b/client/logic/include/ramses-logic/SkinBinding.h @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/APIExport.h" +#include "ramses-logic/RamsesBinding.h" +#include + +namespace ramses +{ + class UniformInput; +} + +namespace ramses::internal +{ + class SkinBindingImpl; +} + +namespace ramses +{ + class RamsesNodeBinding; + class RamsesAppearanceBinding; + + /** + * #SkinBinding is a special kind of binding which holds all the data needed to calculate vertex skinning matrices + * which are then set on every update to the bound appearance using the provided uniform input. + * The data required for vertex skinning are: + * - joint nodes (also referred to as skeleton nodes) - these are transformation nodes which are typically animated + * as part of the skeleton structure. + * - inverse bind matrices - inverse transformation matrix for each joint node, these are static for the lifecycle + * of the #SkinBinding. + * - #ramses::RamsesAppearanceBinding - binds to ramses::Appearance with an ramses::Effect specifying a vertex shader + * which is expected to apply the final stage of vertex skinning calculation using the joint matrices calculated + * in this #SkinBinding. + * - ramses::UniformInput - specifies a vertex shader input (interface to the shader) where the joint matrices + * are expected. + * For more details on how this data is used please see the GLTF Skins tutorial + * https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_020_Skins.md + * the GLTF specification was used as reference for the #SkinBinding implementation in expectation to support most + * skinning use cases. + * + * Even though the #SkinBinding is a #ramses::LogicNode it does not have any input nor output properties. + * The joint matrices (output data) are internally passed to the ramses::Appearance and are therefore + * not exposed as output properties. Also all the input data described above is statically referenced and not exposed + * as input properties. + * + * Performance remark: + * Unlike other logic nodes #SkinBinding does not use dirtiness mechanism monitoring the input data which then calculates + * the output data only if anything changed. #SkinBinding depends on Ramses nodes which cannot be easily monitored + * and therefore it has to be updated every time #ramses::LogicEngine::update is called. For this reason it is highly recommended + * to keep the number of SkinBindings to a necessary minimum. + * + * The changes via binding objects are applied to the bound objects right away when calling ramses::LogicEngine::update(), + * however keep in mind that Ramses has a mechanism for bundling scene changes and applying them at once using ramses::Scene::flush, + * so the changes will be applied all the way only after calling this method on the ramses scene. + */ + class SkinBinding : public RamsesBinding + { + public: + /** + * Returns the appearance binding that this skin is bound to. + * + * @return the appearance binding that this skin is bound to + */ + [[nodiscard]] RAMSES_API const RamsesAppearanceBinding& getAppearanceBinding() const; + + /** + * Returns the uniform input used to set joint matrices to the bound appearance. + * + * @return the uniform input used to set joint matrices to the bound appearance + */ + [[nodiscard]] RAMSES_API const ramses::UniformInput& getAppearanceUniformInput() const; + + /// Implementation detail of SkinBinding + internal::SkinBindingImpl& m_skinBinding; + + protected: + /** + * Constructor of SkinBinding. User is not supposed to call this - SkinBindings are created by other factory classes + * + * @param impl implementation details of the SkinBinding + */ + explicit SkinBinding(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/TimerNode.h b/client/logic/include/ramses-logic/TimerNode.h new file mode 100644 index 000000000..37db0658e --- /dev/null +++ b/client/logic/include/ramses-logic/TimerNode.h @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicNode.h" +#include + +namespace ramses::internal +{ + class TimerNodeImpl; +} + +namespace ramses +{ + /** + * Timer node can be used to provide timing information to animation nodes (#ramses::AnimationNode) or any other logic nodes. + * - Property inputs: + * - ticker_us (int64) - (optional) user provided ticker in microseconds (by default, see below to learn how to use arbitrary time units). + * - if the input is 0 (default) then this TimerNode uses system clock to generate ticker by itself, + * this is recommended for simple use cases when application does not need more advanced timing control. + * - Property outputs: + * - ticker_us (int64) - in auto-generate mode ('ticker_us' input stays 0) this will output system clock time since epoch in microseconds + * - in case of user provided ticker (non-zero 'ticker_us' input) this output will contain the same value + * (user ticker is just passed through, this way time units are user defined). + * + * Timer node works in one of two modes - it generates ticker by itself or uses user provided ticker. This allows quick and easy switch + * between stages of the development, e.g. prototyping, testing or production, where for some use cases auto-generated time is desired + * and other require well specified timing provided by application. + * Due to the auto-generate mode both the input and output has defined time units, however timer node can also be used in a fully + * time unit agnostic mode, see inputs/outputs description above for details. + * Note that unlike other logic nodes a TimerNode is always updated on every #ramses::LogicEngine::update call regardless of if any of its + * inputs were modified or not. + */ + class TimerNode : public LogicNode + { + public: + /** + * Implementation of TimerNode + */ + internal::TimerNodeImpl& m_timerNodeImpl; + + protected: + /** + * Constructor of TimerNode. User is not supposed to call this - TimerNodes are created by other factory classes + * + * @param impl implementation details of the TimerNode + */ + explicit TimerNode(std::unique_ptr impl) noexcept; + + friend class internal::ApiObjects; + }; +} diff --git a/client/logic/include/ramses-logic/WarningData.h b/client/logic/include/ramses-logic/WarningData.h new file mode 100644 index 000000000..4b52a72b0 --- /dev/null +++ b/client/logic/include/ramses-logic/WarningData.h @@ -0,0 +1,78 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include + +namespace ramses +{ + class LogicObject; + + /** + * #EWarningType lists the types of content warnings issued by #ramses::LogicEngine::validate + */ + enum class EWarningType : int + { + Performance, ///< Warns about possibly optimize-able performance overhead + UnsafeDataState, ///< Warns about possible data races, potential data loss or otherwise unsafe data + UninitializedData, ///< Warns about uninitialized data which may result in unexpected behavior + PrecisionLoss, ///< Warns about possible precision issues, e.g. casting large types to smaller types + UnusedContent, ///< Warns about unused content which might be removed altogether + DuplicateContent, ///< Warns about duplicate content which might be possible to merge/optimize + Other, ///< Warning does not match any of the existing categories (this is used for new warnings for API compatibility) + }; + + /** + * Holds information about a warning returned by #ramses::LogicEngine::validate() + */ + struct WarningData + { + /** + * Error description as human-readable text. + */ + std::string message; + + /** + * Semantic type of the warning. + */ + EWarningType type; + + /** + * The #ramses::LogicObject which caused the warning. Can be nullptr if the warning was not originating from a specific object. + */ + const LogicObject* object; + }; + + /** + * Returns the string representation of a given warning type. Use this to display a human-readible text + * to content creators. + */ + constexpr const char* GetVerboseDescription(EWarningType warningType) + { + switch (warningType) + { + case EWarningType::Performance: + return "Performance"; + case EWarningType::UnsafeDataState: + return "Unsafe Data State"; + case EWarningType::UninitializedData: + return "Uninitialized Data"; + case EWarningType::PrecisionLoss: + return "Precision Loss"; + case EWarningType::UnusedContent: + return "Unused Content"; + case EWarningType::DuplicateContent: + return "Duplicate Content"; + case EWarningType::Other: + return "Other"; + } + return ""; + } + +} diff --git a/client/logic/lib/flatbuffers/generated/AnchorPointGen.h b/client/logic/lib/flatbuffers/generated/AnchorPointGen.h new file mode 100644 index 000000000..64931d61c --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/AnchorPointGen.h @@ -0,0 +1,140 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_ANCHORPOINT_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_ANCHORPOINT_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "PropertyGen.h" + +namespace rlogic_serialization { + +struct AnchorPoint; +struct AnchorPointBuilder; + +inline const flatbuffers::TypeTable *AnchorPointTypeTable(); + +struct AnchorPoint FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef AnchorPointBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return AnchorPointTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_NODEBINDINGID = 6, + VT_CAMERABINDINGID = 8, + VT_ROOTINPUT = 10, + VT_ROOTOUTPUT = 12 + }; + const rlogic_serialization::LogicObject *base() const { + return GetPointer(VT_BASE); + } + uint64_t nodeBindingId() const { + return GetField(VT_NODEBINDINGID, 0); + } + uint64_t cameraBindingId() const { + return GetField(VT_CAMERABINDINGID, 0); + } + const rlogic_serialization::Property *rootInput() const { + return GetPointer(VT_ROOTINPUT); + } + const rlogic_serialization::Property *rootOutput() const { + return GetPointer(VT_ROOTOUTPUT); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyField(verifier, VT_NODEBINDINGID) && + VerifyField(verifier, VT_CAMERABINDINGID) && + VerifyOffset(verifier, VT_ROOTINPUT) && + verifier.VerifyTable(rootInput()) && + VerifyOffset(verifier, VT_ROOTOUTPUT) && + verifier.VerifyTable(rootOutput()) && + verifier.EndTable(); + } +}; + +struct AnchorPointBuilder { + typedef AnchorPoint Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(AnchorPoint::VT_BASE, base); + } + void add_nodeBindingId(uint64_t nodeBindingId) { + fbb_.AddElement(AnchorPoint::VT_NODEBINDINGID, nodeBindingId, 0); + } + void add_cameraBindingId(uint64_t cameraBindingId) { + fbb_.AddElement(AnchorPoint::VT_CAMERABINDINGID, cameraBindingId, 0); + } + void add_rootInput(flatbuffers::Offset rootInput) { + fbb_.AddOffset(AnchorPoint::VT_ROOTINPUT, rootInput); + } + void add_rootOutput(flatbuffers::Offset rootOutput) { + fbb_.AddOffset(AnchorPoint::VT_ROOTOUTPUT, rootOutput); + } + explicit AnchorPointBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + AnchorPointBuilder &operator=(const AnchorPointBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateAnchorPoint( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + uint64_t nodeBindingId = 0, + uint64_t cameraBindingId = 0, + flatbuffers::Offset rootInput = 0, + flatbuffers::Offset rootOutput = 0) { + AnchorPointBuilder builder_(_fbb); + builder_.add_cameraBindingId(cameraBindingId); + builder_.add_nodeBindingId(nodeBindingId); + builder_.add_rootOutput(rootOutput); + builder_.add_rootInput(rootInput); + builder_.add_base(base); + return builder_.Finish(); +} + +struct AnchorPoint::Traits { + using type = AnchorPoint; + static auto constexpr Create = CreateAnchorPoint; +}; + +inline const flatbuffers::TypeTable *AnchorPointTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_ULONG, 0, -1 }, + { flatbuffers::ET_ULONG, 0, -1 }, + { flatbuffers::ET_SEQUENCE, 0, 1 }, + { flatbuffers::ET_SEQUENCE, 0, 1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::LogicObjectTypeTable, + rlogic_serialization::PropertyTypeTable + }; + static const char * const names[] = { + "base", + "nodeBindingId", + "cameraBindingId", + "rootInput", + "rootOutput" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 5, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_ANCHORPOINT_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/AnimationNodeGen.h b/client/logic/lib/flatbuffers/generated/AnimationNodeGen.h new file mode 100644 index 000000000..3ac226960 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/AnimationNodeGen.h @@ -0,0 +1,381 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_ANIMATIONNODE_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_ANIMATIONNODE_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "DataArrayGen.h" +#include "LogicObjectGen.h" +#include "PropertyGen.h" + +namespace rlogic_serialization { + +struct Channel; +struct ChannelBuilder; + +struct AnimationNode; +struct AnimationNodeBuilder; + +inline const flatbuffers::TypeTable *ChannelTypeTable(); + +inline const flatbuffers::TypeTable *AnimationNodeTypeTable(); + +enum class EInterpolationType : uint8_t { + Step = 0, + Linear = 1, + Cubic = 2, + Linear_Quaternions = 3, + Cubic_Quaternions = 4, + MIN = Step, + MAX = Cubic_Quaternions +}; + +inline const EInterpolationType (&EnumValuesEInterpolationType())[5] { + static const EInterpolationType values[] = { + EInterpolationType::Step, + EInterpolationType::Linear, + EInterpolationType::Cubic, + EInterpolationType::Linear_Quaternions, + EInterpolationType::Cubic_Quaternions + }; + return values; +} + +inline const char * const *EnumNamesEInterpolationType() { + static const char * const names[6] = { + "Step", + "Linear", + "Cubic", + "Linear_Quaternions", + "Cubic_Quaternions", + nullptr + }; + return names; +} + +inline const char *EnumNameEInterpolationType(EInterpolationType e) { + if (flatbuffers::IsOutRange(e, EInterpolationType::Step, EInterpolationType::Cubic_Quaternions)) return ""; + const size_t index = static_cast(e); + return EnumNamesEInterpolationType()[index]; +} + +struct Channel FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef ChannelBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return ChannelTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_NAME = 4, + VT_TIMESTAMPS = 6, + VT_KEYFRAMES = 8, + VT_INTERPOLATIONTYPE = 10, + VT_TANGENTSIN = 12, + VT_TANGENTSOUT = 14 + }; + const flatbuffers::String *name() const { + return GetPointer(VT_NAME); + } + const rlogic_serialization::DataArray *timestamps() const { + return GetPointer(VT_TIMESTAMPS); + } + const rlogic_serialization::DataArray *keyframes() const { + return GetPointer(VT_KEYFRAMES); + } + rlogic_serialization::EInterpolationType interpolationType() const { + return static_cast(GetField(VT_INTERPOLATIONTYPE, 0)); + } + const rlogic_serialization::DataArray *tangentsIn() const { + return GetPointer(VT_TANGENTSIN); + } + const rlogic_serialization::DataArray *tangentsOut() const { + return GetPointer(VT_TANGENTSOUT); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_NAME) && + verifier.VerifyString(name()) && + VerifyOffset(verifier, VT_TIMESTAMPS) && + verifier.VerifyTable(timestamps()) && + VerifyOffset(verifier, VT_KEYFRAMES) && + verifier.VerifyTable(keyframes()) && + VerifyField(verifier, VT_INTERPOLATIONTYPE) && + VerifyOffset(verifier, VT_TANGENTSIN) && + verifier.VerifyTable(tangentsIn()) && + VerifyOffset(verifier, VT_TANGENTSOUT) && + verifier.VerifyTable(tangentsOut()) && + verifier.EndTable(); + } +}; + +struct ChannelBuilder { + typedef Channel Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_name(flatbuffers::Offset name) { + fbb_.AddOffset(Channel::VT_NAME, name); + } + void add_timestamps(flatbuffers::Offset timestamps) { + fbb_.AddOffset(Channel::VT_TIMESTAMPS, timestamps); + } + void add_keyframes(flatbuffers::Offset keyframes) { + fbb_.AddOffset(Channel::VT_KEYFRAMES, keyframes); + } + void add_interpolationType(rlogic_serialization::EInterpolationType interpolationType) { + fbb_.AddElement(Channel::VT_INTERPOLATIONTYPE, static_cast(interpolationType), 0); + } + void add_tangentsIn(flatbuffers::Offset tangentsIn) { + fbb_.AddOffset(Channel::VT_TANGENTSIN, tangentsIn); + } + void add_tangentsOut(flatbuffers::Offset tangentsOut) { + fbb_.AddOffset(Channel::VT_TANGENTSOUT, tangentsOut); + } + explicit ChannelBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ChannelBuilder &operator=(const ChannelBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateChannel( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset name = 0, + flatbuffers::Offset timestamps = 0, + flatbuffers::Offset keyframes = 0, + rlogic_serialization::EInterpolationType interpolationType = rlogic_serialization::EInterpolationType::Step, + flatbuffers::Offset tangentsIn = 0, + flatbuffers::Offset tangentsOut = 0) { + ChannelBuilder builder_(_fbb); + builder_.add_tangentsOut(tangentsOut); + builder_.add_tangentsIn(tangentsIn); + builder_.add_keyframes(keyframes); + builder_.add_timestamps(timestamps); + builder_.add_name(name); + builder_.add_interpolationType(interpolationType); + return builder_.Finish(); +} + +struct Channel::Traits { + using type = Channel; + static auto constexpr Create = CreateChannel; +}; + +inline flatbuffers::Offset CreateChannelDirect( + flatbuffers::FlatBufferBuilder &_fbb, + const char *name = nullptr, + flatbuffers::Offset timestamps = 0, + flatbuffers::Offset keyframes = 0, + rlogic_serialization::EInterpolationType interpolationType = rlogic_serialization::EInterpolationType::Step, + flatbuffers::Offset tangentsIn = 0, + flatbuffers::Offset tangentsOut = 0) { + auto name__ = name ? _fbb.CreateString(name) : 0; + return rlogic_serialization::CreateChannel( + _fbb, + name__, + timestamps, + keyframes, + interpolationType, + tangentsIn, + tangentsOut); +} + +struct AnimationNode FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef AnimationNodeBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return AnimationNodeTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_CHANNELS = 6, + VT_CHANNELSASPROPERTIES = 8, + VT_ROOTINPUT = 10, + VT_ROOTOUTPUT = 12 + }; + const rlogic_serialization::LogicObject *base() const { + return GetPointer(VT_BASE); + } + const flatbuffers::Vector> *channels() const { + return GetPointer> *>(VT_CHANNELS); + } + bool channelsAsProperties() const { + return GetField(VT_CHANNELSASPROPERTIES, 0) != 0; + } + const rlogic_serialization::Property *rootInput() const { + return GetPointer(VT_ROOTINPUT); + } + const rlogic_serialization::Property *rootOutput() const { + return GetPointer(VT_ROOTOUTPUT); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyOffset(verifier, VT_CHANNELS) && + verifier.VerifyVector(channels()) && + verifier.VerifyVectorOfTables(channels()) && + VerifyField(verifier, VT_CHANNELSASPROPERTIES) && + VerifyOffset(verifier, VT_ROOTINPUT) && + verifier.VerifyTable(rootInput()) && + VerifyOffset(verifier, VT_ROOTOUTPUT) && + verifier.VerifyTable(rootOutput()) && + verifier.EndTable(); + } +}; + +struct AnimationNodeBuilder { + typedef AnimationNode Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(AnimationNode::VT_BASE, base); + } + void add_channels(flatbuffers::Offset>> channels) { + fbb_.AddOffset(AnimationNode::VT_CHANNELS, channels); + } + void add_channelsAsProperties(bool channelsAsProperties) { + fbb_.AddElement(AnimationNode::VT_CHANNELSASPROPERTIES, static_cast(channelsAsProperties), 0); + } + void add_rootInput(flatbuffers::Offset rootInput) { + fbb_.AddOffset(AnimationNode::VT_ROOTINPUT, rootInput); + } + void add_rootOutput(flatbuffers::Offset rootOutput) { + fbb_.AddOffset(AnimationNode::VT_ROOTOUTPUT, rootOutput); + } + explicit AnimationNodeBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + AnimationNodeBuilder &operator=(const AnimationNodeBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateAnimationNode( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + flatbuffers::Offset>> channels = 0, + bool channelsAsProperties = false, + flatbuffers::Offset rootInput = 0, + flatbuffers::Offset rootOutput = 0) { + AnimationNodeBuilder builder_(_fbb); + builder_.add_rootOutput(rootOutput); + builder_.add_rootInput(rootInput); + builder_.add_channels(channels); + builder_.add_base(base); + builder_.add_channelsAsProperties(channelsAsProperties); + return builder_.Finish(); +} + +struct AnimationNode::Traits { + using type = AnimationNode; + static auto constexpr Create = CreateAnimationNode; +}; + +inline flatbuffers::Offset CreateAnimationNodeDirect( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + const std::vector> *channels = nullptr, + bool channelsAsProperties = false, + flatbuffers::Offset rootInput = 0, + flatbuffers::Offset rootOutput = 0) { + auto channels__ = channels ? _fbb.CreateVector>(*channels) : 0; + return rlogic_serialization::CreateAnimationNode( + _fbb, + base, + channels__, + channelsAsProperties, + rootInput, + rootOutput); +} + +inline const flatbuffers::TypeTable *EInterpolationTypeTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::EInterpolationTypeTypeTable + }; + static const char * const names[] = { + "Step", + "Linear", + "Cubic", + "Linear_Quaternions", + "Cubic_Quaternions" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_ENUM, 5, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *ChannelTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_STRING, 0, -1 }, + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 1 }, + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 0, 0 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::DataArrayTypeTable, + rlogic_serialization::EInterpolationTypeTypeTable + }; + static const char * const names[] = { + "name", + "timestamps", + "keyframes", + "interpolationType", + "tangentsIn", + "tangentsOut" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 6, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *AnimationNodeTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 1, 1 }, + { flatbuffers::ET_BOOL, 0, -1 }, + { flatbuffers::ET_SEQUENCE, 0, 2 }, + { flatbuffers::ET_SEQUENCE, 0, 2 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::LogicObjectTypeTable, + rlogic_serialization::ChannelTypeTable, + rlogic_serialization::PropertyTypeTable + }; + static const char * const names[] = { + "base", + "channels", + "channelsAsProperties", + "rootInput", + "rootOutput" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 5, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_ANIMATIONNODE_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/ApiObjectsGen.h b/client/logic/lib/flatbuffers/generated/ApiObjectsGen.h new file mode 100644 index 000000000..ba562fd29 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/ApiObjectsGen.h @@ -0,0 +1,382 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_APIOBJECTS_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_APIOBJECTS_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "AnchorPointGen.h" +#include "AnimationNodeGen.h" +#include "DataArrayGen.h" +#include "LinkGen.h" +#include "LogicObjectGen.h" +#include "LuaInterfaceGen.h" +#include "LuaModuleGen.h" +#include "LuaScriptGen.h" +#include "PropertyGen.h" +#include "RamsesAppearanceBindingGen.h" +#include "RamsesBindingGen.h" +#include "RamsesCameraBindingGen.h" +#include "RamsesMeshNodeBindingGen.h" +#include "RamsesNodeBindingGen.h" +#include "RamsesReferenceGen.h" +#include "RamsesRenderGroupBindingGen.h" +#include "RamsesRenderPassBindingGen.h" +#include "SkinBindingGen.h" +#include "TimerNodeGen.h" + +namespace rlogic_serialization { + +struct ApiObjects; +struct ApiObjectsBuilder; + +inline const flatbuffers::TypeTable *ApiObjectsTypeTable(); + +struct ApiObjects FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef ApiObjectsBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return ApiObjectsTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_LUAMODULES = 4, + VT_LUASCRIPTS = 6, + VT_LUAINTERFACES = 8, + VT_NODEBINDINGS = 10, + VT_APPEARANCEBINDINGS = 12, + VT_CAMERABINDINGS = 14, + VT_DATAARRAYS = 16, + VT_ANIMATIONNODES = 18, + VT_TIMERNODES = 20, + VT_LINKS = 22, + VT_LASTOBJECTID = 24, + VT_RENDERPASSBINDINGS = 26, + VT_ANCHORPOINTS = 28, + VT_RENDERGROUPBINDINGS = 30, + VT_SKINBINDINGS = 32, + VT_MESHNODEBINDINGS = 34 + }; + const flatbuffers::Vector> *luaModules() const { + return GetPointer> *>(VT_LUAMODULES); + } + const flatbuffers::Vector> *luaScripts() const { + return GetPointer> *>(VT_LUASCRIPTS); + } + const flatbuffers::Vector> *luaInterfaces() const { + return GetPointer> *>(VT_LUAINTERFACES); + } + const flatbuffers::Vector> *nodeBindings() const { + return GetPointer> *>(VT_NODEBINDINGS); + } + const flatbuffers::Vector> *appearanceBindings() const { + return GetPointer> *>(VT_APPEARANCEBINDINGS); + } + const flatbuffers::Vector> *cameraBindings() const { + return GetPointer> *>(VT_CAMERABINDINGS); + } + const flatbuffers::Vector> *dataArrays() const { + return GetPointer> *>(VT_DATAARRAYS); + } + const flatbuffers::Vector> *animationNodes() const { + return GetPointer> *>(VT_ANIMATIONNODES); + } + const flatbuffers::Vector> *timerNodes() const { + return GetPointer> *>(VT_TIMERNODES); + } + const flatbuffers::Vector> *links() const { + return GetPointer> *>(VT_LINKS); + } + uint64_t lastObjectId() const { + return GetField(VT_LASTOBJECTID, 0); + } + const flatbuffers::Vector> *renderPassBindings() const { + return GetPointer> *>(VT_RENDERPASSBINDINGS); + } + const flatbuffers::Vector> *anchorPoints() const { + return GetPointer> *>(VT_ANCHORPOINTS); + } + const flatbuffers::Vector> *renderGroupBindings() const { + return GetPointer> *>(VT_RENDERGROUPBINDINGS); + } + const flatbuffers::Vector> *skinBindings() const { + return GetPointer> *>(VT_SKINBINDINGS); + } + const flatbuffers::Vector> *meshNodeBindings() const { + return GetPointer> *>(VT_MESHNODEBINDINGS); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_LUAMODULES) && + verifier.VerifyVector(luaModules()) && + verifier.VerifyVectorOfTables(luaModules()) && + VerifyOffset(verifier, VT_LUASCRIPTS) && + verifier.VerifyVector(luaScripts()) && + verifier.VerifyVectorOfTables(luaScripts()) && + VerifyOffset(verifier, VT_LUAINTERFACES) && + verifier.VerifyVector(luaInterfaces()) && + verifier.VerifyVectorOfTables(luaInterfaces()) && + VerifyOffset(verifier, VT_NODEBINDINGS) && + verifier.VerifyVector(nodeBindings()) && + verifier.VerifyVectorOfTables(nodeBindings()) && + VerifyOffset(verifier, VT_APPEARANCEBINDINGS) && + verifier.VerifyVector(appearanceBindings()) && + verifier.VerifyVectorOfTables(appearanceBindings()) && + VerifyOffset(verifier, VT_CAMERABINDINGS) && + verifier.VerifyVector(cameraBindings()) && + verifier.VerifyVectorOfTables(cameraBindings()) && + VerifyOffset(verifier, VT_DATAARRAYS) && + verifier.VerifyVector(dataArrays()) && + verifier.VerifyVectorOfTables(dataArrays()) && + VerifyOffset(verifier, VT_ANIMATIONNODES) && + verifier.VerifyVector(animationNodes()) && + verifier.VerifyVectorOfTables(animationNodes()) && + VerifyOffset(verifier, VT_TIMERNODES) && + verifier.VerifyVector(timerNodes()) && + verifier.VerifyVectorOfTables(timerNodes()) && + VerifyOffset(verifier, VT_LINKS) && + verifier.VerifyVector(links()) && + verifier.VerifyVectorOfTables(links()) && + VerifyField(verifier, VT_LASTOBJECTID) && + VerifyOffset(verifier, VT_RENDERPASSBINDINGS) && + verifier.VerifyVector(renderPassBindings()) && + verifier.VerifyVectorOfTables(renderPassBindings()) && + VerifyOffset(verifier, VT_ANCHORPOINTS) && + verifier.VerifyVector(anchorPoints()) && + verifier.VerifyVectorOfTables(anchorPoints()) && + VerifyOffset(verifier, VT_RENDERGROUPBINDINGS) && + verifier.VerifyVector(renderGroupBindings()) && + verifier.VerifyVectorOfTables(renderGroupBindings()) && + VerifyOffset(verifier, VT_SKINBINDINGS) && + verifier.VerifyVector(skinBindings()) && + verifier.VerifyVectorOfTables(skinBindings()) && + VerifyOffset(verifier, VT_MESHNODEBINDINGS) && + verifier.VerifyVector(meshNodeBindings()) && + verifier.VerifyVectorOfTables(meshNodeBindings()) && + verifier.EndTable(); + } +}; + +struct ApiObjectsBuilder { + typedef ApiObjects Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_luaModules(flatbuffers::Offset>> luaModules) { + fbb_.AddOffset(ApiObjects::VT_LUAMODULES, luaModules); + } + void add_luaScripts(flatbuffers::Offset>> luaScripts) { + fbb_.AddOffset(ApiObjects::VT_LUASCRIPTS, luaScripts); + } + void add_luaInterfaces(flatbuffers::Offset>> luaInterfaces) { + fbb_.AddOffset(ApiObjects::VT_LUAINTERFACES, luaInterfaces); + } + void add_nodeBindings(flatbuffers::Offset>> nodeBindings) { + fbb_.AddOffset(ApiObjects::VT_NODEBINDINGS, nodeBindings); + } + void add_appearanceBindings(flatbuffers::Offset>> appearanceBindings) { + fbb_.AddOffset(ApiObjects::VT_APPEARANCEBINDINGS, appearanceBindings); + } + void add_cameraBindings(flatbuffers::Offset>> cameraBindings) { + fbb_.AddOffset(ApiObjects::VT_CAMERABINDINGS, cameraBindings); + } + void add_dataArrays(flatbuffers::Offset>> dataArrays) { + fbb_.AddOffset(ApiObjects::VT_DATAARRAYS, dataArrays); + } + void add_animationNodes(flatbuffers::Offset>> animationNodes) { + fbb_.AddOffset(ApiObjects::VT_ANIMATIONNODES, animationNodes); + } + void add_timerNodes(flatbuffers::Offset>> timerNodes) { + fbb_.AddOffset(ApiObjects::VT_TIMERNODES, timerNodes); + } + void add_links(flatbuffers::Offset>> links) { + fbb_.AddOffset(ApiObjects::VT_LINKS, links); + } + void add_lastObjectId(uint64_t lastObjectId) { + fbb_.AddElement(ApiObjects::VT_LASTOBJECTID, lastObjectId, 0); + } + void add_renderPassBindings(flatbuffers::Offset>> renderPassBindings) { + fbb_.AddOffset(ApiObjects::VT_RENDERPASSBINDINGS, renderPassBindings); + } + void add_anchorPoints(flatbuffers::Offset>> anchorPoints) { + fbb_.AddOffset(ApiObjects::VT_ANCHORPOINTS, anchorPoints); + } + void add_renderGroupBindings(flatbuffers::Offset>> renderGroupBindings) { + fbb_.AddOffset(ApiObjects::VT_RENDERGROUPBINDINGS, renderGroupBindings); + } + void add_skinBindings(flatbuffers::Offset>> skinBindings) { + fbb_.AddOffset(ApiObjects::VT_SKINBINDINGS, skinBindings); + } + void add_meshNodeBindings(flatbuffers::Offset>> meshNodeBindings) { + fbb_.AddOffset(ApiObjects::VT_MESHNODEBINDINGS, meshNodeBindings); + } + explicit ApiObjectsBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ApiObjectsBuilder &operator=(const ApiObjectsBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateApiObjects( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset>> luaModules = 0, + flatbuffers::Offset>> luaScripts = 0, + flatbuffers::Offset>> luaInterfaces = 0, + flatbuffers::Offset>> nodeBindings = 0, + flatbuffers::Offset>> appearanceBindings = 0, + flatbuffers::Offset>> cameraBindings = 0, + flatbuffers::Offset>> dataArrays = 0, + flatbuffers::Offset>> animationNodes = 0, + flatbuffers::Offset>> timerNodes = 0, + flatbuffers::Offset>> links = 0, + uint64_t lastObjectId = 0, + flatbuffers::Offset>> renderPassBindings = 0, + flatbuffers::Offset>> anchorPoints = 0, + flatbuffers::Offset>> renderGroupBindings = 0, + flatbuffers::Offset>> skinBindings = 0, + flatbuffers::Offset>> meshNodeBindings = 0) { + ApiObjectsBuilder builder_(_fbb); + builder_.add_lastObjectId(lastObjectId); + builder_.add_meshNodeBindings(meshNodeBindings); + builder_.add_skinBindings(skinBindings); + builder_.add_renderGroupBindings(renderGroupBindings); + builder_.add_anchorPoints(anchorPoints); + builder_.add_renderPassBindings(renderPassBindings); + builder_.add_links(links); + builder_.add_timerNodes(timerNodes); + builder_.add_animationNodes(animationNodes); + builder_.add_dataArrays(dataArrays); + builder_.add_cameraBindings(cameraBindings); + builder_.add_appearanceBindings(appearanceBindings); + builder_.add_nodeBindings(nodeBindings); + builder_.add_luaInterfaces(luaInterfaces); + builder_.add_luaScripts(luaScripts); + builder_.add_luaModules(luaModules); + return builder_.Finish(); +} + +struct ApiObjects::Traits { + using type = ApiObjects; + static auto constexpr Create = CreateApiObjects; +}; + +inline flatbuffers::Offset CreateApiObjectsDirect( + flatbuffers::FlatBufferBuilder &_fbb, + const std::vector> *luaModules = nullptr, + const std::vector> *luaScripts = nullptr, + const std::vector> *luaInterfaces = nullptr, + const std::vector> *nodeBindings = nullptr, + const std::vector> *appearanceBindings = nullptr, + const std::vector> *cameraBindings = nullptr, + const std::vector> *dataArrays = nullptr, + const std::vector> *animationNodes = nullptr, + const std::vector> *timerNodes = nullptr, + const std::vector> *links = nullptr, + uint64_t lastObjectId = 0, + const std::vector> *renderPassBindings = nullptr, + const std::vector> *anchorPoints = nullptr, + const std::vector> *renderGroupBindings = nullptr, + const std::vector> *skinBindings = nullptr, + const std::vector> *meshNodeBindings = nullptr) { + auto luaModules__ = luaModules ? _fbb.CreateVector>(*luaModules) : 0; + auto luaScripts__ = luaScripts ? _fbb.CreateVector>(*luaScripts) : 0; + auto luaInterfaces__ = luaInterfaces ? _fbb.CreateVector>(*luaInterfaces) : 0; + auto nodeBindings__ = nodeBindings ? _fbb.CreateVector>(*nodeBindings) : 0; + auto appearanceBindings__ = appearanceBindings ? _fbb.CreateVector>(*appearanceBindings) : 0; + auto cameraBindings__ = cameraBindings ? _fbb.CreateVector>(*cameraBindings) : 0; + auto dataArrays__ = dataArrays ? _fbb.CreateVector>(*dataArrays) : 0; + auto animationNodes__ = animationNodes ? _fbb.CreateVector>(*animationNodes) : 0; + auto timerNodes__ = timerNodes ? _fbb.CreateVector>(*timerNodes) : 0; + auto links__ = links ? _fbb.CreateVector>(*links) : 0; + auto renderPassBindings__ = renderPassBindings ? _fbb.CreateVector>(*renderPassBindings) : 0; + auto anchorPoints__ = anchorPoints ? _fbb.CreateVector>(*anchorPoints) : 0; + auto renderGroupBindings__ = renderGroupBindings ? _fbb.CreateVector>(*renderGroupBindings) : 0; + auto skinBindings__ = skinBindings ? _fbb.CreateVector>(*skinBindings) : 0; + auto meshNodeBindings__ = meshNodeBindings ? _fbb.CreateVector>(*meshNodeBindings) : 0; + return rlogic_serialization::CreateApiObjects( + _fbb, + luaModules__, + luaScripts__, + luaInterfaces__, + nodeBindings__, + appearanceBindings__, + cameraBindings__, + dataArrays__, + animationNodes__, + timerNodes__, + links__, + lastObjectId, + renderPassBindings__, + anchorPoints__, + renderGroupBindings__, + skinBindings__, + meshNodeBindings__); +} + +inline const flatbuffers::TypeTable *ApiObjectsTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 1, 0 }, + { flatbuffers::ET_SEQUENCE, 1, 1 }, + { flatbuffers::ET_SEQUENCE, 1, 2 }, + { flatbuffers::ET_SEQUENCE, 1, 3 }, + { flatbuffers::ET_SEQUENCE, 1, 4 }, + { flatbuffers::ET_SEQUENCE, 1, 5 }, + { flatbuffers::ET_SEQUENCE, 1, 6 }, + { flatbuffers::ET_SEQUENCE, 1, 7 }, + { flatbuffers::ET_SEQUENCE, 1, 8 }, + { flatbuffers::ET_SEQUENCE, 1, 9 }, + { flatbuffers::ET_ULONG, 0, -1 }, + { flatbuffers::ET_SEQUENCE, 1, 10 }, + { flatbuffers::ET_SEQUENCE, 1, 11 }, + { flatbuffers::ET_SEQUENCE, 1, 12 }, + { flatbuffers::ET_SEQUENCE, 1, 13 }, + { flatbuffers::ET_SEQUENCE, 1, 14 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::LuaModuleTypeTable, + rlogic_serialization::LuaScriptTypeTable, + rlogic_serialization::LuaInterfaceTypeTable, + rlogic_serialization::RamsesNodeBindingTypeTable, + rlogic_serialization::RamsesAppearanceBindingTypeTable, + rlogic_serialization::RamsesCameraBindingTypeTable, + rlogic_serialization::DataArrayTypeTable, + rlogic_serialization::AnimationNodeTypeTable, + rlogic_serialization::TimerNodeTypeTable, + rlogic_serialization::LinkTypeTable, + rlogic_serialization::RamsesRenderPassBindingTypeTable, + rlogic_serialization::AnchorPointTypeTable, + rlogic_serialization::RamsesRenderGroupBindingTypeTable, + rlogic_serialization::SkinBindingTypeTable, + rlogic_serialization::RamsesMeshNodeBindingTypeTable + }; + static const char * const names[] = { + "luaModules", + "luaScripts", + "luaInterfaces", + "nodeBindings", + "appearanceBindings", + "cameraBindings", + "dataArrays", + "animationNodes", + "timerNodes", + "links", + "lastObjectId", + "renderPassBindings", + "anchorPoints", + "renderGroupBindings", + "skinBindings", + "meshNodeBindings" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 16, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_APIOBJECTS_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/DataArrayGen.h b/client/logic/lib/flatbuffers/generated/DataArrayGen.h new file mode 100644 index 000000000..1cc2f040f --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/DataArrayGen.h @@ -0,0 +1,493 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_DATAARRAY_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_DATAARRAY_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" + +namespace rlogic_serialization { + +struct floatArr; +struct floatArrBuilder; + +struct intArr; +struct intArrBuilder; + +struct DataArray; +struct DataArrayBuilder; + +inline const flatbuffers::TypeTable *floatArrTypeTable(); + +inline const flatbuffers::TypeTable *intArrTypeTable(); + +inline const flatbuffers::TypeTable *DataArrayTypeTable(); + +enum class EDataArrayType : uint8_t { + Float = 0, + Vec2f = 1, + Vec3f = 2, + Vec4f = 3, + Int32 = 4, + Vec2i = 5, + Vec3i = 6, + Vec4i = 7, + FloatArray = 8, + MIN = Float, + MAX = FloatArray +}; + +inline const EDataArrayType (&EnumValuesEDataArrayType())[9] { + static const EDataArrayType values[] = { + EDataArrayType::Float, + EDataArrayType::Vec2f, + EDataArrayType::Vec3f, + EDataArrayType::Vec4f, + EDataArrayType::Int32, + EDataArrayType::Vec2i, + EDataArrayType::Vec3i, + EDataArrayType::Vec4i, + EDataArrayType::FloatArray + }; + return values; +} + +inline const char * const *EnumNamesEDataArrayType() { + static const char * const names[10] = { + "Float", + "Vec2f", + "Vec3f", + "Vec4f", + "Int32", + "Vec2i", + "Vec3i", + "Vec4i", + "FloatArray", + nullptr + }; + return names; +} + +inline const char *EnumNameEDataArrayType(EDataArrayType e) { + if (flatbuffers::IsOutRange(e, EDataArrayType::Float, EDataArrayType::FloatArray)) return ""; + const size_t index = static_cast(e); + return EnumNamesEDataArrayType()[index]; +} + +enum class ArrayUnion : uint8_t { + NONE = 0, + floatArr = 1, + intArr = 2, + MIN = NONE, + MAX = intArr +}; + +inline const ArrayUnion (&EnumValuesArrayUnion())[3] { + static const ArrayUnion values[] = { + ArrayUnion::NONE, + ArrayUnion::floatArr, + ArrayUnion::intArr + }; + return values; +} + +inline const char * const *EnumNamesArrayUnion() { + static const char * const names[4] = { + "NONE", + "floatArr", + "intArr", + nullptr + }; + return names; +} + +inline const char *EnumNameArrayUnion(ArrayUnion e) { + if (flatbuffers::IsOutRange(e, ArrayUnion::NONE, ArrayUnion::intArr)) return ""; + const size_t index = static_cast(e); + return EnumNamesArrayUnion()[index]; +} + +template struct ArrayUnionTraits { + static const ArrayUnion enum_value = ArrayUnion::NONE; +}; + +template<> struct ArrayUnionTraits { + static const ArrayUnion enum_value = ArrayUnion::floatArr; +}; + +template<> struct ArrayUnionTraits { + static const ArrayUnion enum_value = ArrayUnion::intArr; +}; + +bool VerifyArrayUnion(flatbuffers::Verifier &verifier, const void *obj, ArrayUnion type); +bool VerifyArrayUnionVector(flatbuffers::Verifier &verifier, const flatbuffers::Vector> *values, const flatbuffers::Vector *types); + +struct floatArr FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef floatArrBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return floatArrTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_DATA = 4 + }; + const flatbuffers::Vector *data() const { + return GetPointer *>(VT_DATA); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_DATA) && + verifier.VerifyVector(data()) && + verifier.EndTable(); + } +}; + +struct floatArrBuilder { + typedef floatArr Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_data(flatbuffers::Offset> data) { + fbb_.AddOffset(floatArr::VT_DATA, data); + } + explicit floatArrBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + floatArrBuilder &operator=(const floatArrBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreatefloatArr( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset> data = 0) { + floatArrBuilder builder_(_fbb); + builder_.add_data(data); + return builder_.Finish(); +} + +struct floatArr::Traits { + using type = floatArr; + static auto constexpr Create = CreatefloatArr; +}; + +inline flatbuffers::Offset CreatefloatArrDirect( + flatbuffers::FlatBufferBuilder &_fbb, + const std::vector *data = nullptr) { + auto data__ = data ? _fbb.CreateVector(*data) : 0; + return rlogic_serialization::CreatefloatArr( + _fbb, + data__); +} + +struct intArr FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef intArrBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return intArrTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_DATA = 4 + }; + const flatbuffers::Vector *data() const { + return GetPointer *>(VT_DATA); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_DATA) && + verifier.VerifyVector(data()) && + verifier.EndTable(); + } +}; + +struct intArrBuilder { + typedef intArr Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_data(flatbuffers::Offset> data) { + fbb_.AddOffset(intArr::VT_DATA, data); + } + explicit intArrBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + intArrBuilder &operator=(const intArrBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateintArr( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset> data = 0) { + intArrBuilder builder_(_fbb); + builder_.add_data(data); + return builder_.Finish(); +} + +struct intArr::Traits { + using type = intArr; + static auto constexpr Create = CreateintArr; +}; + +inline flatbuffers::Offset CreateintArrDirect( + flatbuffers::FlatBufferBuilder &_fbb, + const std::vector *data = nullptr) { + auto data__ = data ? _fbb.CreateVector(*data) : 0; + return rlogic_serialization::CreateintArr( + _fbb, + data__); +} + +struct DataArray FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef DataArrayBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return DataArrayTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_TYPE = 6, + VT_DATA_TYPE = 8, + VT_DATA = 10, + VT_NUMELEMENTS = 12 + }; + const rlogic_serialization::LogicObject *base() const { + return GetPointer(VT_BASE); + } + rlogic_serialization::EDataArrayType type() const { + return static_cast(GetField(VT_TYPE, 0)); + } + rlogic_serialization::ArrayUnion data_type() const { + return static_cast(GetField(VT_DATA_TYPE, 0)); + } + const void *data() const { + return GetPointer(VT_DATA); + } + template const T *data_as() const; + const rlogic_serialization::floatArr *data_as_floatArr() const { + return data_type() == rlogic_serialization::ArrayUnion::floatArr ? static_cast(data()) : nullptr; + } + const rlogic_serialization::intArr *data_as_intArr() const { + return data_type() == rlogic_serialization::ArrayUnion::intArr ? static_cast(data()) : nullptr; + } + uint32_t numElements() const { + return GetField(VT_NUMELEMENTS, 0); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyField(verifier, VT_TYPE) && + VerifyField(verifier, VT_DATA_TYPE) && + VerifyOffset(verifier, VT_DATA) && + VerifyArrayUnion(verifier, data(), data_type()) && + VerifyField(verifier, VT_NUMELEMENTS) && + verifier.EndTable(); + } +}; + +template<> inline const rlogic_serialization::floatArr *DataArray::data_as() const { + return data_as_floatArr(); +} + +template<> inline const rlogic_serialization::intArr *DataArray::data_as() const { + return data_as_intArr(); +} + +struct DataArrayBuilder { + typedef DataArray Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(DataArray::VT_BASE, base); + } + void add_type(rlogic_serialization::EDataArrayType type) { + fbb_.AddElement(DataArray::VT_TYPE, static_cast(type), 0); + } + void add_data_type(rlogic_serialization::ArrayUnion data_type) { + fbb_.AddElement(DataArray::VT_DATA_TYPE, static_cast(data_type), 0); + } + void add_data(flatbuffers::Offset data) { + fbb_.AddOffset(DataArray::VT_DATA, data); + } + void add_numElements(uint32_t numElements) { + fbb_.AddElement(DataArray::VT_NUMELEMENTS, numElements, 0); + } + explicit DataArrayBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + DataArrayBuilder &operator=(const DataArrayBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateDataArray( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + rlogic_serialization::EDataArrayType type = rlogic_serialization::EDataArrayType::Float, + rlogic_serialization::ArrayUnion data_type = rlogic_serialization::ArrayUnion::NONE, + flatbuffers::Offset data = 0, + uint32_t numElements = 0) { + DataArrayBuilder builder_(_fbb); + builder_.add_numElements(numElements); + builder_.add_data(data); + builder_.add_base(base); + builder_.add_data_type(data_type); + builder_.add_type(type); + return builder_.Finish(); +} + +struct DataArray::Traits { + using type = DataArray; + static auto constexpr Create = CreateDataArray; +}; + +inline bool VerifyArrayUnion(flatbuffers::Verifier &verifier, const void *obj, ArrayUnion type) { + switch (type) { + case ArrayUnion::NONE: { + return true; + } + case ArrayUnion::floatArr: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case ArrayUnion::intArr: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + default: return true; + } +} + +inline bool VerifyArrayUnionVector(flatbuffers::Verifier &verifier, const flatbuffers::Vector> *values, const flatbuffers::Vector *types) { + if (!values || !types) return !values && !types; + if (values->size() != types->size()) return false; + for (flatbuffers::uoffset_t i = 0; i < values->size(); ++i) { + if (!VerifyArrayUnion( + verifier, values->Get(i), types->GetEnum(i))) { + return false; + } + } + return true; +} + +inline const flatbuffers::TypeTable *EDataArrayTypeTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::EDataArrayTypeTypeTable + }; + static const char * const names[] = { + "Float", + "Vec2f", + "Vec3f", + "Vec4f", + "Int32", + "Vec2i", + "Vec3i", + "Vec4i", + "FloatArray" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_ENUM, 9, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *ArrayUnionTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, -1 }, + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 0, 1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::floatArrTypeTable, + rlogic_serialization::intArrTypeTable + }; + static const char * const names[] = { + "NONE", + "floatArr", + "intArr" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_UNION, 3, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *floatArrTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_FLOAT, 1, -1 } + }; + static const char * const names[] = { + "data" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 1, type_codes, nullptr, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *intArrTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_INT, 1, -1 } + }; + static const char * const names[] = { + "data" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 1, type_codes, nullptr, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *DataArrayTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 1 }, + { flatbuffers::ET_UTYPE, 0, 2 }, + { flatbuffers::ET_SEQUENCE, 0, 2 }, + { flatbuffers::ET_UINT, 0, -1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::LogicObjectTypeTable, + rlogic_serialization::EDataArrayTypeTypeTable, + rlogic_serialization::ArrayUnionTypeTable + }; + static const char * const names[] = { + "base", + "type", + "data_type", + "data", + "numElements" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 5, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_DATAARRAY_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/LinkGen.h b/client/logic/lib/flatbuffers/generated/LinkGen.h new file mode 100644 index 000000000..f7abfc902 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/LinkGen.h @@ -0,0 +1,113 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_LINK_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_LINK_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "PropertyGen.h" + +namespace rlogic_serialization { + +struct Link; +struct LinkBuilder; + +inline const flatbuffers::TypeTable *LinkTypeTable(); + +struct Link FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef LinkBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return LinkTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_SOURCEPROPERTY = 4, + VT_TARGETPROPERTY = 6, + VT_ISWEAK = 8 + }; + const rlogic_serialization::Property *sourceProperty() const { + return GetPointer(VT_SOURCEPROPERTY); + } + const rlogic_serialization::Property *targetProperty() const { + return GetPointer(VT_TARGETPROPERTY); + } + bool isWeak() const { + return GetField(VT_ISWEAK, 0) != 0; + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_SOURCEPROPERTY) && + verifier.VerifyTable(sourceProperty()) && + VerifyOffset(verifier, VT_TARGETPROPERTY) && + verifier.VerifyTable(targetProperty()) && + VerifyField(verifier, VT_ISWEAK) && + verifier.EndTable(); + } +}; + +struct LinkBuilder { + typedef Link Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_sourceProperty(flatbuffers::Offset sourceProperty) { + fbb_.AddOffset(Link::VT_SOURCEPROPERTY, sourceProperty); + } + void add_targetProperty(flatbuffers::Offset targetProperty) { + fbb_.AddOffset(Link::VT_TARGETPROPERTY, targetProperty); + } + void add_isWeak(bool isWeak) { + fbb_.AddElement(Link::VT_ISWEAK, static_cast(isWeak), 0); + } + explicit LinkBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + LinkBuilder &operator=(const LinkBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateLink( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset sourceProperty = 0, + flatbuffers::Offset targetProperty = 0, + bool isWeak = false) { + LinkBuilder builder_(_fbb); + builder_.add_targetProperty(targetProperty); + builder_.add_sourceProperty(sourceProperty); + builder_.add_isWeak(isWeak); + return builder_.Finish(); +} + +struct Link::Traits { + using type = Link; + static auto constexpr Create = CreateLink; +}; + +inline const flatbuffers::TypeTable *LinkTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_BOOL, 0, -1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::PropertyTypeTable + }; + static const char * const names[] = { + "sourceProperty", + "targetProperty", + "isWeak" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 3, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_LINK_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/LogicEngineGen.h b/client/logic/lib/flatbuffers/generated/LogicEngineGen.h new file mode 100644 index 000000000..d55078d54 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/LogicEngineGen.h @@ -0,0 +1,433 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_LOGICENGINE_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_LOGICENGINE_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "AnchorPointGen.h" +#include "AnimationNodeGen.h" +#include "ApiObjectsGen.h" +#include "DataArrayGen.h" +#include "LinkGen.h" +#include "LogicObjectGen.h" +#include "LuaInterfaceGen.h" +#include "LuaModuleGen.h" +#include "LuaScriptGen.h" +#include "PropertyGen.h" +#include "RamsesAppearanceBindingGen.h" +#include "RamsesBindingGen.h" +#include "RamsesCameraBindingGen.h" +#include "RamsesMeshNodeBindingGen.h" +#include "RamsesNodeBindingGen.h" +#include "RamsesReferenceGen.h" +#include "RamsesRenderGroupBindingGen.h" +#include "RamsesRenderPassBindingGen.h" +#include "SkinBindingGen.h" +#include "TimerNodeGen.h" + +namespace rlogic_serialization { + +struct Version; +struct VersionBuilder; + +struct Metadata; +struct MetadataBuilder; + +struct LogicEngine; +struct LogicEngineBuilder; + +inline const flatbuffers::TypeTable *VersionTypeTable(); + +inline const flatbuffers::TypeTable *MetadataTypeTable(); + +inline const flatbuffers::TypeTable *LogicEngineTypeTable(); + +struct Version FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef VersionBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return VersionTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_V_MAJOR = 4, + VT_V_MINOR = 6, + VT_V_PATCH = 8, + VT_V_STRING = 10 + }; + uint32_t v_major() const { + return GetField(VT_V_MAJOR, 0); + } + uint32_t v_minor() const { + return GetField(VT_V_MINOR, 0); + } + uint32_t v_patch() const { + return GetField(VT_V_PATCH, 0); + } + const flatbuffers::String *v_string() const { + return GetPointer(VT_V_STRING); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_V_MAJOR) && + VerifyField(verifier, VT_V_MINOR) && + VerifyField(verifier, VT_V_PATCH) && + VerifyOffset(verifier, VT_V_STRING) && + verifier.VerifyString(v_string()) && + verifier.EndTable(); + } +}; + +struct VersionBuilder { + typedef Version Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_v_major(uint32_t v_major) { + fbb_.AddElement(Version::VT_V_MAJOR, v_major, 0); + } + void add_v_minor(uint32_t v_minor) { + fbb_.AddElement(Version::VT_V_MINOR, v_minor, 0); + } + void add_v_patch(uint32_t v_patch) { + fbb_.AddElement(Version::VT_V_PATCH, v_patch, 0); + } + void add_v_string(flatbuffers::Offset v_string) { + fbb_.AddOffset(Version::VT_V_STRING, v_string); + } + explicit VersionBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + VersionBuilder &operator=(const VersionBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateVersion( + flatbuffers::FlatBufferBuilder &_fbb, + uint32_t v_major = 0, + uint32_t v_minor = 0, + uint32_t v_patch = 0, + flatbuffers::Offset v_string = 0) { + VersionBuilder builder_(_fbb); + builder_.add_v_string(v_string); + builder_.add_v_patch(v_patch); + builder_.add_v_minor(v_minor); + builder_.add_v_major(v_major); + return builder_.Finish(); +} + +struct Version::Traits { + using type = Version; + static auto constexpr Create = CreateVersion; +}; + +inline flatbuffers::Offset CreateVersionDirect( + flatbuffers::FlatBufferBuilder &_fbb, + uint32_t v_major = 0, + uint32_t v_minor = 0, + uint32_t v_patch = 0, + const char *v_string = nullptr) { + auto v_string__ = v_string ? _fbb.CreateString(v_string) : 0; + return rlogic_serialization::CreateVersion( + _fbb, + v_major, + v_minor, + v_patch, + v_string__); +} + +struct Metadata FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef MetadataBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return MetadataTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_METADATASTRING = 4, + VT_EXPORTERVERSION = 6, + VT_EXPORTERFILEVERSION = 8 + }; + const flatbuffers::String *metadataString() const { + return GetPointer(VT_METADATASTRING); + } + const rlogic_serialization::Version *exporterVersion() const { + return GetPointer(VT_EXPORTERVERSION); + } + uint32_t exporterFileVersion() const { + return GetField(VT_EXPORTERFILEVERSION, 0); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_METADATASTRING) && + verifier.VerifyString(metadataString()) && + VerifyOffset(verifier, VT_EXPORTERVERSION) && + verifier.VerifyTable(exporterVersion()) && + VerifyField(verifier, VT_EXPORTERFILEVERSION) && + verifier.EndTable(); + } +}; + +struct MetadataBuilder { + typedef Metadata Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_metadataString(flatbuffers::Offset metadataString) { + fbb_.AddOffset(Metadata::VT_METADATASTRING, metadataString); + } + void add_exporterVersion(flatbuffers::Offset exporterVersion) { + fbb_.AddOffset(Metadata::VT_EXPORTERVERSION, exporterVersion); + } + void add_exporterFileVersion(uint32_t exporterFileVersion) { + fbb_.AddElement(Metadata::VT_EXPORTERFILEVERSION, exporterFileVersion, 0); + } + explicit MetadataBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + MetadataBuilder &operator=(const MetadataBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateMetadata( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset metadataString = 0, + flatbuffers::Offset exporterVersion = 0, + uint32_t exporterFileVersion = 0) { + MetadataBuilder builder_(_fbb); + builder_.add_exporterFileVersion(exporterFileVersion); + builder_.add_exporterVersion(exporterVersion); + builder_.add_metadataString(metadataString); + return builder_.Finish(); +} + +struct Metadata::Traits { + using type = Metadata; + static auto constexpr Create = CreateMetadata; +}; + +inline flatbuffers::Offset CreateMetadataDirect( + flatbuffers::FlatBufferBuilder &_fbb, + const char *metadataString = nullptr, + flatbuffers::Offset exporterVersion = 0, + uint32_t exporterFileVersion = 0) { + auto metadataString__ = metadataString ? _fbb.CreateString(metadataString) : 0; + return rlogic_serialization::CreateMetadata( + _fbb, + metadataString__, + exporterVersion, + exporterFileVersion); +} + +struct LogicEngine FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef LogicEngineBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return LogicEngineTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_RAMSESVERSION = 4, + VT_RLOGICVERSION = 6, + VT_APIOBJECTS = 8, + VT_ASSETMETADATA = 10, + VT_FEATURELEVEL = 12 + }; + const rlogic_serialization::Version *ramsesVersion() const { + return GetPointer(VT_RAMSESVERSION); + } + const rlogic_serialization::Version *rlogicVersion() const { + return GetPointer(VT_RLOGICVERSION); + } + const rlogic_serialization::ApiObjects *apiObjects() const { + return GetPointer(VT_APIOBJECTS); + } + const rlogic_serialization::Metadata *assetMetadata() const { + return GetPointer(VT_ASSETMETADATA); + } + uint32_t featureLevel() const { + return GetField(VT_FEATURELEVEL, 1); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffsetRequired(verifier, VT_RAMSESVERSION) && + verifier.VerifyTable(ramsesVersion()) && + VerifyOffsetRequired(verifier, VT_RLOGICVERSION) && + verifier.VerifyTable(rlogicVersion()) && + VerifyOffset(verifier, VT_APIOBJECTS) && + verifier.VerifyTable(apiObjects()) && + VerifyOffset(verifier, VT_ASSETMETADATA) && + verifier.VerifyTable(assetMetadata()) && + VerifyField(verifier, VT_FEATURELEVEL) && + verifier.EndTable(); + } +}; + +struct LogicEngineBuilder { + typedef LogicEngine Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_ramsesVersion(flatbuffers::Offset ramsesVersion) { + fbb_.AddOffset(LogicEngine::VT_RAMSESVERSION, ramsesVersion); + } + void add_rlogicVersion(flatbuffers::Offset rlogicVersion) { + fbb_.AddOffset(LogicEngine::VT_RLOGICVERSION, rlogicVersion); + } + void add_apiObjects(flatbuffers::Offset apiObjects) { + fbb_.AddOffset(LogicEngine::VT_APIOBJECTS, apiObjects); + } + void add_assetMetadata(flatbuffers::Offset assetMetadata) { + fbb_.AddOffset(LogicEngine::VT_ASSETMETADATA, assetMetadata); + } + void add_featureLevel(uint32_t featureLevel) { + fbb_.AddElement(LogicEngine::VT_FEATURELEVEL, featureLevel, 1); + } + explicit LogicEngineBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + LogicEngineBuilder &operator=(const LogicEngineBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + fbb_.Required(o, LogicEngine::VT_RAMSESVERSION); + fbb_.Required(o, LogicEngine::VT_RLOGICVERSION); + return o; + } +}; + +inline flatbuffers::Offset CreateLogicEngine( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset ramsesVersion = 0, + flatbuffers::Offset rlogicVersion = 0, + flatbuffers::Offset apiObjects = 0, + flatbuffers::Offset assetMetadata = 0, + uint32_t featureLevel = 1) { + LogicEngineBuilder builder_(_fbb); + builder_.add_featureLevel(featureLevel); + builder_.add_assetMetadata(assetMetadata); + builder_.add_apiObjects(apiObjects); + builder_.add_rlogicVersion(rlogicVersion); + builder_.add_ramsesVersion(ramsesVersion); + return builder_.Finish(); +} + +struct LogicEngine::Traits { + using type = LogicEngine; + static auto constexpr Create = CreateLogicEngine; +}; + +inline const flatbuffers::TypeTable *VersionTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_UINT, 0, -1 }, + { flatbuffers::ET_UINT, 0, -1 }, + { flatbuffers::ET_UINT, 0, -1 }, + { flatbuffers::ET_STRING, 0, -1 } + }; + static const char * const names[] = { + "v_major", + "v_minor", + "v_patch", + "v_string" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 4, type_codes, nullptr, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *MetadataTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_STRING, 0, -1 }, + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_UINT, 0, -1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::VersionTypeTable + }; + static const char * const names[] = { + "metadataString", + "exporterVersion", + "exporterFileVersion" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 3, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *LogicEngineTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 0, 1 }, + { flatbuffers::ET_SEQUENCE, 0, 2 }, + { flatbuffers::ET_UINT, 0, -1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::VersionTypeTable, + rlogic_serialization::ApiObjectsTypeTable, + rlogic_serialization::MetadataTypeTable + }; + static const char * const names[] = { + "ramsesVersion", + "rlogicVersion", + "apiObjects", + "assetMetadata", + "featureLevel" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 5, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +inline const rlogic_serialization::LogicEngine *GetLogicEngine(const void *buf) { + return flatbuffers::GetRoot(buf); +} + +inline const rlogic_serialization::LogicEngine *GetSizePrefixedLogicEngine(const void *buf) { + return flatbuffers::GetSizePrefixedRoot(buf); +} + +inline const char *LogicEngineIdentifier() { + return "rl28"; +} + +inline bool LogicEngineBufferHasIdentifier(const void *buf) { + return flatbuffers::BufferHasIdentifier( + buf, LogicEngineIdentifier()); +} + +inline bool VerifyLogicEngineBuffer( + flatbuffers::Verifier &verifier) { + return verifier.VerifyBuffer(LogicEngineIdentifier()); +} + +inline bool VerifySizePrefixedLogicEngineBuffer( + flatbuffers::Verifier &verifier) { + return verifier.VerifySizePrefixedBuffer(LogicEngineIdentifier()); +} + +inline void FinishLogicEngineBuffer( + flatbuffers::FlatBufferBuilder &fbb, + flatbuffers::Offset root) { + fbb.Finish(root, LogicEngineIdentifier()); +} + +inline void FinishSizePrefixedLogicEngineBuffer( + flatbuffers::FlatBufferBuilder &fbb, + flatbuffers::Offset root) { + fbb.FinishSizePrefixed(root, LogicEngineIdentifier()); +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_LOGICENGINE_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/LogicObjectGen.h b/client/logic/lib/flatbuffers/generated/LogicObjectGen.h new file mode 100644 index 000000000..4e5e444de --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/LogicObjectGen.h @@ -0,0 +1,134 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_LOGICOBJECT_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_LOGICOBJECT_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +namespace rlogic_serialization { + +struct LogicObject; +struct LogicObjectBuilder; + +inline const flatbuffers::TypeTable *LogicObjectTypeTable(); + +struct LogicObject FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef LogicObjectBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return LogicObjectTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_NAME = 4, + VT_ID = 6, + VT_USERIDHIGH = 8, + VT_USERIDLOW = 10 + }; + const flatbuffers::String *name() const { + return GetPointer(VT_NAME); + } + uint64_t id() const { + return GetField(VT_ID, 0); + } + uint64_t userIdHigh() const { + return GetField(VT_USERIDHIGH, 0); + } + uint64_t userIdLow() const { + return GetField(VT_USERIDLOW, 0); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_NAME) && + verifier.VerifyString(name()) && + VerifyField(verifier, VT_ID) && + VerifyField(verifier, VT_USERIDHIGH) && + VerifyField(verifier, VT_USERIDLOW) && + verifier.EndTable(); + } +}; + +struct LogicObjectBuilder { + typedef LogicObject Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_name(flatbuffers::Offset name) { + fbb_.AddOffset(LogicObject::VT_NAME, name); + } + void add_id(uint64_t id) { + fbb_.AddElement(LogicObject::VT_ID, id, 0); + } + void add_userIdHigh(uint64_t userIdHigh) { + fbb_.AddElement(LogicObject::VT_USERIDHIGH, userIdHigh, 0); + } + void add_userIdLow(uint64_t userIdLow) { + fbb_.AddElement(LogicObject::VT_USERIDLOW, userIdLow, 0); + } + explicit LogicObjectBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + LogicObjectBuilder &operator=(const LogicObjectBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateLogicObject( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset name = 0, + uint64_t id = 0, + uint64_t userIdHigh = 0, + uint64_t userIdLow = 0) { + LogicObjectBuilder builder_(_fbb); + builder_.add_userIdLow(userIdLow); + builder_.add_userIdHigh(userIdHigh); + builder_.add_id(id); + builder_.add_name(name); + return builder_.Finish(); +} + +struct LogicObject::Traits { + using type = LogicObject; + static auto constexpr Create = CreateLogicObject; +}; + +inline flatbuffers::Offset CreateLogicObjectDirect( + flatbuffers::FlatBufferBuilder &_fbb, + const char *name = nullptr, + uint64_t id = 0, + uint64_t userIdHigh = 0, + uint64_t userIdLow = 0) { + auto name__ = name ? _fbb.CreateString(name) : 0; + return rlogic_serialization::CreateLogicObject( + _fbb, + name__, + id, + userIdHigh, + userIdLow); +} + +inline const flatbuffers::TypeTable *LogicObjectTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_STRING, 0, -1 }, + { flatbuffers::ET_ULONG, 0, -1 }, + { flatbuffers::ET_ULONG, 0, -1 }, + { flatbuffers::ET_ULONG, 0, -1 } + }; + static const char * const names[] = { + "name", + "id", + "userIdHigh", + "userIdLow" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 4, type_codes, nullptr, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_LOGICOBJECT_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/LuaInterfaceGen.h b/client/logic/lib/flatbuffers/generated/LuaInterfaceGen.h new file mode 100644 index 000000000..a92e564bb --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/LuaInterfaceGen.h @@ -0,0 +1,103 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_LUAINTERFACE_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_LUAINTERFACE_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "PropertyGen.h" + +namespace rlogic_serialization { + +struct LuaInterface; +struct LuaInterfaceBuilder; + +inline const flatbuffers::TypeTable *LuaInterfaceTypeTable(); + +struct LuaInterface FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef LuaInterfaceBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return LuaInterfaceTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_ROOTPROPERTY = 6 + }; + const rlogic_serialization::LogicObject *base() const { + return GetPointer(VT_BASE); + } + const rlogic_serialization::Property *rootProperty() const { + return GetPointer(VT_ROOTPROPERTY); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyOffset(verifier, VT_ROOTPROPERTY) && + verifier.VerifyTable(rootProperty()) && + verifier.EndTable(); + } +}; + +struct LuaInterfaceBuilder { + typedef LuaInterface Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(LuaInterface::VT_BASE, base); + } + void add_rootProperty(flatbuffers::Offset rootProperty) { + fbb_.AddOffset(LuaInterface::VT_ROOTPROPERTY, rootProperty); + } + explicit LuaInterfaceBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + LuaInterfaceBuilder &operator=(const LuaInterfaceBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateLuaInterface( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + flatbuffers::Offset rootProperty = 0) { + LuaInterfaceBuilder builder_(_fbb); + builder_.add_rootProperty(rootProperty); + builder_.add_base(base); + return builder_.Finish(); +} + +struct LuaInterface::Traits { + using type = LuaInterface; + static auto constexpr Create = CreateLuaInterface; +}; + +inline const flatbuffers::TypeTable *LuaInterfaceTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 0, 1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::LogicObjectTypeTable, + rlogic_serialization::PropertyTypeTable + }; + static const char * const names[] = { + "base", + "rootProperty" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 2, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_LUAINTERFACE_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/LuaModuleGen.h b/client/logic/lib/flatbuffers/generated/LuaModuleGen.h new file mode 100644 index 000000000..3fc6ac8dc --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/LuaModuleGen.h @@ -0,0 +1,255 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_LUAMODULE_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_LUAMODULE_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" + +namespace rlogic_serialization { + +struct LuaModule; +struct LuaModuleBuilder; + +struct LuaModuleUsage; +struct LuaModuleUsageBuilder; + +inline const flatbuffers::TypeTable *LuaModuleTypeTable(); + +inline const flatbuffers::TypeTable *LuaModuleUsageTypeTable(); + +struct LuaModule FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef LuaModuleBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return LuaModuleTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_SOURCE = 6, + VT_DEPENDENCIES = 8, + VT_STANDARDMODULES = 10, + VT_LUABYTECODE = 12 + }; + const rlogic_serialization::LogicObject *base() const { + return GetPointer(VT_BASE); + } + const flatbuffers::String *source() const { + return GetPointer(VT_SOURCE); + } + const flatbuffers::Vector> *dependencies() const { + return GetPointer> *>(VT_DEPENDENCIES); + } + const flatbuffers::Vector *standardModules() const { + return GetPointer *>(VT_STANDARDMODULES); + } + const flatbuffers::Vector *luaByteCode() const { + return GetPointer *>(VT_LUABYTECODE); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyOffset(verifier, VT_SOURCE) && + verifier.VerifyString(source()) && + VerifyOffset(verifier, VT_DEPENDENCIES) && + verifier.VerifyVector(dependencies()) && + verifier.VerifyVectorOfTables(dependencies()) && + VerifyOffset(verifier, VT_STANDARDMODULES) && + verifier.VerifyVector(standardModules()) && + VerifyOffset(verifier, VT_LUABYTECODE) && + verifier.VerifyVector(luaByteCode()) && + verifier.EndTable(); + } +}; + +struct LuaModuleBuilder { + typedef LuaModule Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(LuaModule::VT_BASE, base); + } + void add_source(flatbuffers::Offset source) { + fbb_.AddOffset(LuaModule::VT_SOURCE, source); + } + void add_dependencies(flatbuffers::Offset>> dependencies) { + fbb_.AddOffset(LuaModule::VT_DEPENDENCIES, dependencies); + } + void add_standardModules(flatbuffers::Offset> standardModules) { + fbb_.AddOffset(LuaModule::VT_STANDARDMODULES, standardModules); + } + void add_luaByteCode(flatbuffers::Offset> luaByteCode) { + fbb_.AddOffset(LuaModule::VT_LUABYTECODE, luaByteCode); + } + explicit LuaModuleBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + LuaModuleBuilder &operator=(const LuaModuleBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateLuaModule( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + flatbuffers::Offset source = 0, + flatbuffers::Offset>> dependencies = 0, + flatbuffers::Offset> standardModules = 0, + flatbuffers::Offset> luaByteCode = 0) { + LuaModuleBuilder builder_(_fbb); + builder_.add_luaByteCode(luaByteCode); + builder_.add_standardModules(standardModules); + builder_.add_dependencies(dependencies); + builder_.add_source(source); + builder_.add_base(base); + return builder_.Finish(); +} + +struct LuaModule::Traits { + using type = LuaModule; + static auto constexpr Create = CreateLuaModule; +}; + +inline flatbuffers::Offset CreateLuaModuleDirect( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + const char *source = nullptr, + const std::vector> *dependencies = nullptr, + const std::vector *standardModules = nullptr, + const std::vector *luaByteCode = nullptr) { + auto source__ = source ? _fbb.CreateString(source) : 0; + auto dependencies__ = dependencies ? _fbb.CreateVector>(*dependencies) : 0; + auto standardModules__ = standardModules ? _fbb.CreateVector(*standardModules) : 0; + auto luaByteCode__ = luaByteCode ? _fbb.CreateVector(*luaByteCode) : 0; + return rlogic_serialization::CreateLuaModule( + _fbb, + base, + source__, + dependencies__, + standardModules__, + luaByteCode__); +} + +struct LuaModuleUsage FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef LuaModuleUsageBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return LuaModuleUsageTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_NAME = 4, + VT_MODULEID = 6 + }; + const flatbuffers::String *name() const { + return GetPointer(VT_NAME); + } + uint64_t moduleId() const { + return GetField(VT_MODULEID, 0); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_NAME) && + verifier.VerifyString(name()) && + VerifyField(verifier, VT_MODULEID) && + verifier.EndTable(); + } +}; + +struct LuaModuleUsageBuilder { + typedef LuaModuleUsage Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_name(flatbuffers::Offset name) { + fbb_.AddOffset(LuaModuleUsage::VT_NAME, name); + } + void add_moduleId(uint64_t moduleId) { + fbb_.AddElement(LuaModuleUsage::VT_MODULEID, moduleId, 0); + } + explicit LuaModuleUsageBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + LuaModuleUsageBuilder &operator=(const LuaModuleUsageBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateLuaModuleUsage( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset name = 0, + uint64_t moduleId = 0) { + LuaModuleUsageBuilder builder_(_fbb); + builder_.add_moduleId(moduleId); + builder_.add_name(name); + return builder_.Finish(); +} + +struct LuaModuleUsage::Traits { + using type = LuaModuleUsage; + static auto constexpr Create = CreateLuaModuleUsage; +}; + +inline flatbuffers::Offset CreateLuaModuleUsageDirect( + flatbuffers::FlatBufferBuilder &_fbb, + const char *name = nullptr, + uint64_t moduleId = 0) { + auto name__ = name ? _fbb.CreateString(name) : 0; + return rlogic_serialization::CreateLuaModuleUsage( + _fbb, + name__, + moduleId); +} + +inline const flatbuffers::TypeTable *LuaModuleTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_STRING, 0, -1 }, + { flatbuffers::ET_SEQUENCE, 1, 1 }, + { flatbuffers::ET_UCHAR, 1, -1 }, + { flatbuffers::ET_UCHAR, 1, -1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::LogicObjectTypeTable, + rlogic_serialization::LuaModuleUsageTypeTable + }; + static const char * const names[] = { + "base", + "source", + "dependencies", + "standardModules", + "luaByteCode" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 5, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *LuaModuleUsageTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_STRING, 0, -1 }, + { flatbuffers::ET_ULONG, 0, -1 } + }; + static const char * const names[] = { + "name", + "moduleId" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 2, type_codes, nullptr, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_LUAMODULE_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/LuaScriptGen.h b/client/logic/lib/flatbuffers/generated/LuaScriptGen.h new file mode 100644 index 000000000..ac6cc358f --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/LuaScriptGen.h @@ -0,0 +1,195 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_LUASCRIPT_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_LUASCRIPT_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "LuaModuleGen.h" +#include "PropertyGen.h" + +namespace rlogic_serialization { + +struct LuaScript; +struct LuaScriptBuilder; + +inline const flatbuffers::TypeTable *LuaScriptTypeTable(); + +struct LuaScript FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef LuaScriptBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return LuaScriptTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_LUASOURCECODE = 6, + VT_USERMODULES = 8, + VT_STANDARDMODULES = 10, + VT_ROOTINPUT = 12, + VT_ROOTOUTPUT = 14, + VT_LUABYTECODE = 16 + }; + const rlogic_serialization::LogicObject *base() const { + return GetPointer(VT_BASE); + } + const flatbuffers::String *luaSourceCode() const { + return GetPointer(VT_LUASOURCECODE); + } + const flatbuffers::Vector> *userModules() const { + return GetPointer> *>(VT_USERMODULES); + } + const flatbuffers::Vector *standardModules() const { + return GetPointer *>(VT_STANDARDMODULES); + } + const rlogic_serialization::Property *rootInput() const { + return GetPointer(VT_ROOTINPUT); + } + const rlogic_serialization::Property *rootOutput() const { + return GetPointer(VT_ROOTOUTPUT); + } + const flatbuffers::Vector *luaByteCode() const { + return GetPointer *>(VT_LUABYTECODE); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyOffset(verifier, VT_LUASOURCECODE) && + verifier.VerifyString(luaSourceCode()) && + VerifyOffset(verifier, VT_USERMODULES) && + verifier.VerifyVector(userModules()) && + verifier.VerifyVectorOfTables(userModules()) && + VerifyOffset(verifier, VT_STANDARDMODULES) && + verifier.VerifyVector(standardModules()) && + VerifyOffset(verifier, VT_ROOTINPUT) && + verifier.VerifyTable(rootInput()) && + VerifyOffset(verifier, VT_ROOTOUTPUT) && + verifier.VerifyTable(rootOutput()) && + VerifyOffset(verifier, VT_LUABYTECODE) && + verifier.VerifyVector(luaByteCode()) && + verifier.EndTable(); + } +}; + +struct LuaScriptBuilder { + typedef LuaScript Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(LuaScript::VT_BASE, base); + } + void add_luaSourceCode(flatbuffers::Offset luaSourceCode) { + fbb_.AddOffset(LuaScript::VT_LUASOURCECODE, luaSourceCode); + } + void add_userModules(flatbuffers::Offset>> userModules) { + fbb_.AddOffset(LuaScript::VT_USERMODULES, userModules); + } + void add_standardModules(flatbuffers::Offset> standardModules) { + fbb_.AddOffset(LuaScript::VT_STANDARDMODULES, standardModules); + } + void add_rootInput(flatbuffers::Offset rootInput) { + fbb_.AddOffset(LuaScript::VT_ROOTINPUT, rootInput); + } + void add_rootOutput(flatbuffers::Offset rootOutput) { + fbb_.AddOffset(LuaScript::VT_ROOTOUTPUT, rootOutput); + } + void add_luaByteCode(flatbuffers::Offset> luaByteCode) { + fbb_.AddOffset(LuaScript::VT_LUABYTECODE, luaByteCode); + } + explicit LuaScriptBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + LuaScriptBuilder &operator=(const LuaScriptBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateLuaScript( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + flatbuffers::Offset luaSourceCode = 0, + flatbuffers::Offset>> userModules = 0, + flatbuffers::Offset> standardModules = 0, + flatbuffers::Offset rootInput = 0, + flatbuffers::Offset rootOutput = 0, + flatbuffers::Offset> luaByteCode = 0) { + LuaScriptBuilder builder_(_fbb); + builder_.add_luaByteCode(luaByteCode); + builder_.add_rootOutput(rootOutput); + builder_.add_rootInput(rootInput); + builder_.add_standardModules(standardModules); + builder_.add_userModules(userModules); + builder_.add_luaSourceCode(luaSourceCode); + builder_.add_base(base); + return builder_.Finish(); +} + +struct LuaScript::Traits { + using type = LuaScript; + static auto constexpr Create = CreateLuaScript; +}; + +inline flatbuffers::Offset CreateLuaScriptDirect( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + const char *luaSourceCode = nullptr, + const std::vector> *userModules = nullptr, + const std::vector *standardModules = nullptr, + flatbuffers::Offset rootInput = 0, + flatbuffers::Offset rootOutput = 0, + const std::vector *luaByteCode = nullptr) { + auto luaSourceCode__ = luaSourceCode ? _fbb.CreateString(luaSourceCode) : 0; + auto userModules__ = userModules ? _fbb.CreateVector>(*userModules) : 0; + auto standardModules__ = standardModules ? _fbb.CreateVector(*standardModules) : 0; + auto luaByteCode__ = luaByteCode ? _fbb.CreateVector(*luaByteCode) : 0; + return rlogic_serialization::CreateLuaScript( + _fbb, + base, + luaSourceCode__, + userModules__, + standardModules__, + rootInput, + rootOutput, + luaByteCode__); +} + +inline const flatbuffers::TypeTable *LuaScriptTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_STRING, 0, -1 }, + { flatbuffers::ET_SEQUENCE, 1, 1 }, + { flatbuffers::ET_UCHAR, 1, -1 }, + { flatbuffers::ET_SEQUENCE, 0, 2 }, + { flatbuffers::ET_SEQUENCE, 0, 2 }, + { flatbuffers::ET_UCHAR, 1, -1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::LogicObjectTypeTable, + rlogic_serialization::LuaModuleUsageTypeTable, + rlogic_serialization::PropertyTypeTable + }; + static const char * const names[] = { + "base", + "luaSourceCode", + "userModules", + "standardModules", + "rootInput", + "rootOutput", + "luaByteCode" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 7, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_LUASCRIPT_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/PropertyGen.h b/client/logic/lib/flatbuffers/generated/PropertyGen.h new file mode 100644 index 000000000..1d84926f6 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/PropertyGen.h @@ -0,0 +1,1045 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_PROPERTY_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_PROPERTY_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +namespace rlogic_serialization { + +struct float_s; + +struct vec2f_s; + +struct vec3f_s; + +struct vec4f_s; + +struct int32_s; + +struct int64_s; + +struct vec2i_s; + +struct vec3i_s; + +struct vec4i_s; + +struct bool_s; + +struct string_s; +struct string_sBuilder; + +struct Property; +struct PropertyBuilder; + +inline const flatbuffers::TypeTable *float_sTypeTable(); + +inline const flatbuffers::TypeTable *vec2f_sTypeTable(); + +inline const flatbuffers::TypeTable *vec3f_sTypeTable(); + +inline const flatbuffers::TypeTable *vec4f_sTypeTable(); + +inline const flatbuffers::TypeTable *int32_sTypeTable(); + +inline const flatbuffers::TypeTable *int64_sTypeTable(); + +inline const flatbuffers::TypeTable *vec2i_sTypeTable(); + +inline const flatbuffers::TypeTable *vec3i_sTypeTable(); + +inline const flatbuffers::TypeTable *vec4i_sTypeTable(); + +inline const flatbuffers::TypeTable *bool_sTypeTable(); + +inline const flatbuffers::TypeTable *string_sTypeTable(); + +inline const flatbuffers::TypeTable *PropertyTypeTable(); + +enum class EPropertyRootType : uint8_t { + Primitive = 0, + Struct = 1, + Array = 2, + MIN = Primitive, + MAX = Array +}; + +inline const EPropertyRootType (&EnumValuesEPropertyRootType())[3] { + static const EPropertyRootType values[] = { + EPropertyRootType::Primitive, + EPropertyRootType::Struct, + EPropertyRootType::Array + }; + return values; +} + +inline const char * const *EnumNamesEPropertyRootType() { + static const char * const names[4] = { + "Primitive", + "Struct", + "Array", + nullptr + }; + return names; +} + +inline const char *EnumNameEPropertyRootType(EPropertyRootType e) { + if (flatbuffers::IsOutRange(e, EPropertyRootType::Primitive, EPropertyRootType::Array)) return ""; + const size_t index = static_cast(e); + return EnumNamesEPropertyRootType()[index]; +} + +enum class PropertyValue : uint8_t { + NONE = 0, + float_s = 1, + vec2f_s = 2, + vec3f_s = 3, + vec4f_s = 4, + int32_s = 5, + int64_s = 6, + vec2i_s = 7, + vec3i_s = 8, + vec4i_s = 9, + string_s = 10, + bool_s = 11, + MIN = NONE, + MAX = bool_s +}; + +inline const PropertyValue (&EnumValuesPropertyValue())[12] { + static const PropertyValue values[] = { + PropertyValue::NONE, + PropertyValue::float_s, + PropertyValue::vec2f_s, + PropertyValue::vec3f_s, + PropertyValue::vec4f_s, + PropertyValue::int32_s, + PropertyValue::int64_s, + PropertyValue::vec2i_s, + PropertyValue::vec3i_s, + PropertyValue::vec4i_s, + PropertyValue::string_s, + PropertyValue::bool_s + }; + return values; +} + +inline const char * const *EnumNamesPropertyValue() { + static const char * const names[13] = { + "NONE", + "float_s", + "vec2f_s", + "vec3f_s", + "vec4f_s", + "int32_s", + "int64_s", + "vec2i_s", + "vec3i_s", + "vec4i_s", + "string_s", + "bool_s", + nullptr + }; + return names; +} + +inline const char *EnumNamePropertyValue(PropertyValue e) { + if (flatbuffers::IsOutRange(e, PropertyValue::NONE, PropertyValue::bool_s)) return ""; + const size_t index = static_cast(e); + return EnumNamesPropertyValue()[index]; +} + +template struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::NONE; +}; + +template<> struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::float_s; +}; + +template<> struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::vec2f_s; +}; + +template<> struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::vec3f_s; +}; + +template<> struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::vec4f_s; +}; + +template<> struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::int32_s; +}; + +template<> struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::int64_s; +}; + +template<> struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::vec2i_s; +}; + +template<> struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::vec3i_s; +}; + +template<> struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::vec4i_s; +}; + +template<> struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::string_s; +}; + +template<> struct PropertyValueTraits { + static const PropertyValue enum_value = PropertyValue::bool_s; +}; + +bool VerifyPropertyValue(flatbuffers::Verifier &verifier, const void *obj, PropertyValue type); +bool VerifyPropertyValueVector(flatbuffers::Verifier &verifier, const flatbuffers::Vector> *values, const flatbuffers::Vector *types); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(4) float_s FLATBUFFERS_FINAL_CLASS { + private: + float v_; + + public: + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return float_sTypeTable(); + } + float_s() { + memset(static_cast(this), 0, sizeof(float_s)); + } + float_s(float _v) + : v_(flatbuffers::EndianScalar(_v)) { + } + float v() const { + return flatbuffers::EndianScalar(v_); + } +}; +FLATBUFFERS_STRUCT_END(float_s, 4); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(4) vec2f_s FLATBUFFERS_FINAL_CLASS { + private: + float x_; + float y_; + + public: + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return vec2f_sTypeTable(); + } + vec2f_s() { + memset(static_cast(this), 0, sizeof(vec2f_s)); + } + vec2f_s(float _x, float _y) + : x_(flatbuffers::EndianScalar(_x)), + y_(flatbuffers::EndianScalar(_y)) { + } + float x() const { + return flatbuffers::EndianScalar(x_); + } + float y() const { + return flatbuffers::EndianScalar(y_); + } +}; +FLATBUFFERS_STRUCT_END(vec2f_s, 8); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(4) vec3f_s FLATBUFFERS_FINAL_CLASS { + private: + float x_; + float y_; + float z_; + + public: + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return vec3f_sTypeTable(); + } + vec3f_s() { + memset(static_cast(this), 0, sizeof(vec3f_s)); + } + vec3f_s(float _x, float _y, float _z) + : x_(flatbuffers::EndianScalar(_x)), + y_(flatbuffers::EndianScalar(_y)), + z_(flatbuffers::EndianScalar(_z)) { + } + float x() const { + return flatbuffers::EndianScalar(x_); + } + float y() const { + return flatbuffers::EndianScalar(y_); + } + float z() const { + return flatbuffers::EndianScalar(z_); + } +}; +FLATBUFFERS_STRUCT_END(vec3f_s, 12); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(4) vec4f_s FLATBUFFERS_FINAL_CLASS { + private: + float x_; + float y_; + float z_; + float w_; + + public: + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return vec4f_sTypeTable(); + } + vec4f_s() { + memset(static_cast(this), 0, sizeof(vec4f_s)); + } + vec4f_s(float _x, float _y, float _z, float _w) + : x_(flatbuffers::EndianScalar(_x)), + y_(flatbuffers::EndianScalar(_y)), + z_(flatbuffers::EndianScalar(_z)), + w_(flatbuffers::EndianScalar(_w)) { + } + float x() const { + return flatbuffers::EndianScalar(x_); + } + float y() const { + return flatbuffers::EndianScalar(y_); + } + float z() const { + return flatbuffers::EndianScalar(z_); + } + float w() const { + return flatbuffers::EndianScalar(w_); + } +}; +FLATBUFFERS_STRUCT_END(vec4f_s, 16); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(4) int32_s FLATBUFFERS_FINAL_CLASS { + private: + int32_t v_; + + public: + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return int32_sTypeTable(); + } + int32_s() { + memset(static_cast(this), 0, sizeof(int32_s)); + } + int32_s(int32_t _v) + : v_(flatbuffers::EndianScalar(_v)) { + } + int32_t v() const { + return flatbuffers::EndianScalar(v_); + } +}; +FLATBUFFERS_STRUCT_END(int32_s, 4); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(8) int64_s FLATBUFFERS_FINAL_CLASS { + private: + int64_t v_; + + public: + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return int64_sTypeTable(); + } + int64_s() { + memset(static_cast(this), 0, sizeof(int64_s)); + } + int64_s(int64_t _v) + : v_(flatbuffers::EndianScalar(_v)) { + } + int64_t v() const { + return flatbuffers::EndianScalar(v_); + } +}; +FLATBUFFERS_STRUCT_END(int64_s, 8); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(4) vec2i_s FLATBUFFERS_FINAL_CLASS { + private: + int32_t x_; + int32_t y_; + + public: + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return vec2i_sTypeTable(); + } + vec2i_s() { + memset(static_cast(this), 0, sizeof(vec2i_s)); + } + vec2i_s(int32_t _x, int32_t _y) + : x_(flatbuffers::EndianScalar(_x)), + y_(flatbuffers::EndianScalar(_y)) { + } + int32_t x() const { + return flatbuffers::EndianScalar(x_); + } + int32_t y() const { + return flatbuffers::EndianScalar(y_); + } +}; +FLATBUFFERS_STRUCT_END(vec2i_s, 8); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(4) vec3i_s FLATBUFFERS_FINAL_CLASS { + private: + int32_t x_; + int32_t y_; + int32_t z_; + + public: + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return vec3i_sTypeTable(); + } + vec3i_s() { + memset(static_cast(this), 0, sizeof(vec3i_s)); + } + vec3i_s(int32_t _x, int32_t _y, int32_t _z) + : x_(flatbuffers::EndianScalar(_x)), + y_(flatbuffers::EndianScalar(_y)), + z_(flatbuffers::EndianScalar(_z)) { + } + int32_t x() const { + return flatbuffers::EndianScalar(x_); + } + int32_t y() const { + return flatbuffers::EndianScalar(y_); + } + int32_t z() const { + return flatbuffers::EndianScalar(z_); + } +}; +FLATBUFFERS_STRUCT_END(vec3i_s, 12); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(4) vec4i_s FLATBUFFERS_FINAL_CLASS { + private: + int32_t x_; + int32_t y_; + int32_t z_; + int32_t w_; + + public: + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return vec4i_sTypeTable(); + } + vec4i_s() { + memset(static_cast(this), 0, sizeof(vec4i_s)); + } + vec4i_s(int32_t _x, int32_t _y, int32_t _z, int32_t _w) + : x_(flatbuffers::EndianScalar(_x)), + y_(flatbuffers::EndianScalar(_y)), + z_(flatbuffers::EndianScalar(_z)), + w_(flatbuffers::EndianScalar(_w)) { + } + int32_t x() const { + return flatbuffers::EndianScalar(x_); + } + int32_t y() const { + return flatbuffers::EndianScalar(y_); + } + int32_t z() const { + return flatbuffers::EndianScalar(z_); + } + int32_t w() const { + return flatbuffers::EndianScalar(w_); + } +}; +FLATBUFFERS_STRUCT_END(vec4i_s, 16); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) bool_s FLATBUFFERS_FINAL_CLASS { + private: + uint8_t v_; + + public: + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return bool_sTypeTable(); + } + bool_s() { + memset(static_cast(this), 0, sizeof(bool_s)); + } + bool_s(bool _v) + : v_(flatbuffers::EndianScalar(static_cast(_v))) { + } + bool v() const { + return flatbuffers::EndianScalar(v_) != 0; + } +}; +FLATBUFFERS_STRUCT_END(bool_s, 1); + +struct string_s FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef string_sBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return string_sTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_V = 4 + }; + const flatbuffers::String *v() const { + return GetPointer(VT_V); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_V) && + verifier.VerifyString(v()) && + verifier.EndTable(); + } +}; + +struct string_sBuilder { + typedef string_s Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_v(flatbuffers::Offset v) { + fbb_.AddOffset(string_s::VT_V, v); + } + explicit string_sBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + string_sBuilder &operator=(const string_sBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset Createstring_s( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset v = 0) { + string_sBuilder builder_(_fbb); + builder_.add_v(v); + return builder_.Finish(); +} + +struct string_s::Traits { + using type = string_s; + static auto constexpr Create = Createstring_s; +}; + +inline flatbuffers::Offset Createstring_sDirect( + flatbuffers::FlatBufferBuilder &_fbb, + const char *v = nullptr) { + auto v__ = v ? _fbb.CreateString(v) : 0; + return rlogic_serialization::Createstring_s( + _fbb, + v__); +} + +struct Property FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef PropertyBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return PropertyTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_NAME = 4, + VT_ROOTTYPE = 6, + VT_CHILDREN = 8, + VT_VALUE_TYPE = 10, + VT_VALUE = 12 + }; + const flatbuffers::String *name() const { + return GetPointer(VT_NAME); + } + rlogic_serialization::EPropertyRootType rootType() const { + return static_cast(GetField(VT_ROOTTYPE, 0)); + } + const flatbuffers::Vector> *children() const { + return GetPointer> *>(VT_CHILDREN); + } + rlogic_serialization::PropertyValue value_type() const { + return static_cast(GetField(VT_VALUE_TYPE, 0)); + } + const void *value() const { + return GetPointer(VT_VALUE); + } + template const T *value_as() const; + const rlogic_serialization::float_s *value_as_float_s() const { + return value_type() == rlogic_serialization::PropertyValue::float_s ? static_cast(value()) : nullptr; + } + const rlogic_serialization::vec2f_s *value_as_vec2f_s() const { + return value_type() == rlogic_serialization::PropertyValue::vec2f_s ? static_cast(value()) : nullptr; + } + const rlogic_serialization::vec3f_s *value_as_vec3f_s() const { + return value_type() == rlogic_serialization::PropertyValue::vec3f_s ? static_cast(value()) : nullptr; + } + const rlogic_serialization::vec4f_s *value_as_vec4f_s() const { + return value_type() == rlogic_serialization::PropertyValue::vec4f_s ? static_cast(value()) : nullptr; + } + const rlogic_serialization::int32_s *value_as_int32_s() const { + return value_type() == rlogic_serialization::PropertyValue::int32_s ? static_cast(value()) : nullptr; + } + const rlogic_serialization::int64_s *value_as_int64_s() const { + return value_type() == rlogic_serialization::PropertyValue::int64_s ? static_cast(value()) : nullptr; + } + const rlogic_serialization::vec2i_s *value_as_vec2i_s() const { + return value_type() == rlogic_serialization::PropertyValue::vec2i_s ? static_cast(value()) : nullptr; + } + const rlogic_serialization::vec3i_s *value_as_vec3i_s() const { + return value_type() == rlogic_serialization::PropertyValue::vec3i_s ? static_cast(value()) : nullptr; + } + const rlogic_serialization::vec4i_s *value_as_vec4i_s() const { + return value_type() == rlogic_serialization::PropertyValue::vec4i_s ? static_cast(value()) : nullptr; + } + const rlogic_serialization::string_s *value_as_string_s() const { + return value_type() == rlogic_serialization::PropertyValue::string_s ? static_cast(value()) : nullptr; + } + const rlogic_serialization::bool_s *value_as_bool_s() const { + return value_type() == rlogic_serialization::PropertyValue::bool_s ? static_cast(value()) : nullptr; + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_NAME) && + verifier.VerifyString(name()) && + VerifyField(verifier, VT_ROOTTYPE) && + VerifyOffset(verifier, VT_CHILDREN) && + verifier.VerifyVector(children()) && + verifier.VerifyVectorOfTables(children()) && + VerifyField(verifier, VT_VALUE_TYPE) && + VerifyOffset(verifier, VT_VALUE) && + VerifyPropertyValue(verifier, value(), value_type()) && + verifier.EndTable(); + } +}; + +template<> inline const rlogic_serialization::float_s *Property::value_as() const { + return value_as_float_s(); +} + +template<> inline const rlogic_serialization::vec2f_s *Property::value_as() const { + return value_as_vec2f_s(); +} + +template<> inline const rlogic_serialization::vec3f_s *Property::value_as() const { + return value_as_vec3f_s(); +} + +template<> inline const rlogic_serialization::vec4f_s *Property::value_as() const { + return value_as_vec4f_s(); +} + +template<> inline const rlogic_serialization::int32_s *Property::value_as() const { + return value_as_int32_s(); +} + +template<> inline const rlogic_serialization::int64_s *Property::value_as() const { + return value_as_int64_s(); +} + +template<> inline const rlogic_serialization::vec2i_s *Property::value_as() const { + return value_as_vec2i_s(); +} + +template<> inline const rlogic_serialization::vec3i_s *Property::value_as() const { + return value_as_vec3i_s(); +} + +template<> inline const rlogic_serialization::vec4i_s *Property::value_as() const { + return value_as_vec4i_s(); +} + +template<> inline const rlogic_serialization::string_s *Property::value_as() const { + return value_as_string_s(); +} + +template<> inline const rlogic_serialization::bool_s *Property::value_as() const { + return value_as_bool_s(); +} + +struct PropertyBuilder { + typedef Property Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_name(flatbuffers::Offset name) { + fbb_.AddOffset(Property::VT_NAME, name); + } + void add_rootType(rlogic_serialization::EPropertyRootType rootType) { + fbb_.AddElement(Property::VT_ROOTTYPE, static_cast(rootType), 0); + } + void add_children(flatbuffers::Offset>> children) { + fbb_.AddOffset(Property::VT_CHILDREN, children); + } + void add_value_type(rlogic_serialization::PropertyValue value_type) { + fbb_.AddElement(Property::VT_VALUE_TYPE, static_cast(value_type), 0); + } + void add_value(flatbuffers::Offset value) { + fbb_.AddOffset(Property::VT_VALUE, value); + } + explicit PropertyBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + PropertyBuilder &operator=(const PropertyBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateProperty( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset name = 0, + rlogic_serialization::EPropertyRootType rootType = rlogic_serialization::EPropertyRootType::Primitive, + flatbuffers::Offset>> children = 0, + rlogic_serialization::PropertyValue value_type = rlogic_serialization::PropertyValue::NONE, + flatbuffers::Offset value = 0) { + PropertyBuilder builder_(_fbb); + builder_.add_value(value); + builder_.add_children(children); + builder_.add_name(name); + builder_.add_value_type(value_type); + builder_.add_rootType(rootType); + return builder_.Finish(); +} + +struct Property::Traits { + using type = Property; + static auto constexpr Create = CreateProperty; +}; + +inline flatbuffers::Offset CreatePropertyDirect( + flatbuffers::FlatBufferBuilder &_fbb, + const char *name = nullptr, + rlogic_serialization::EPropertyRootType rootType = rlogic_serialization::EPropertyRootType::Primitive, + const std::vector> *children = nullptr, + rlogic_serialization::PropertyValue value_type = rlogic_serialization::PropertyValue::NONE, + flatbuffers::Offset value = 0) { + auto name__ = name ? _fbb.CreateString(name) : 0; + auto children__ = children ? _fbb.CreateVector>(*children) : 0; + return rlogic_serialization::CreateProperty( + _fbb, + name__, + rootType, + children__, + value_type, + value); +} + +inline bool VerifyPropertyValue(flatbuffers::Verifier &verifier, const void *obj, PropertyValue type) { + switch (type) { + case PropertyValue::NONE: { + return true; + } + case PropertyValue::float_s: { + return verifier.Verify(static_cast(obj), 0); + } + case PropertyValue::vec2f_s: { + return verifier.Verify(static_cast(obj), 0); + } + case PropertyValue::vec3f_s: { + return verifier.Verify(static_cast(obj), 0); + } + case PropertyValue::vec4f_s: { + return verifier.Verify(static_cast(obj), 0); + } + case PropertyValue::int32_s: { + return verifier.Verify(static_cast(obj), 0); + } + case PropertyValue::int64_s: { + return verifier.Verify(static_cast(obj), 0); + } + case PropertyValue::vec2i_s: { + return verifier.Verify(static_cast(obj), 0); + } + case PropertyValue::vec3i_s: { + return verifier.Verify(static_cast(obj), 0); + } + case PropertyValue::vec4i_s: { + return verifier.Verify(static_cast(obj), 0); + } + case PropertyValue::string_s: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case PropertyValue::bool_s: { + return verifier.Verify(static_cast(obj), 0); + } + default: return true; + } +} + +inline bool VerifyPropertyValueVector(flatbuffers::Verifier &verifier, const flatbuffers::Vector> *values, const flatbuffers::Vector *types) { + if (!values || !types) return !values && !types; + if (values->size() != types->size()) return false; + for (flatbuffers::uoffset_t i = 0; i < values->size(); ++i) { + if (!VerifyPropertyValue( + verifier, values->Get(i), types->GetEnum(i))) { + return false; + } + } + return true; +} + +inline const flatbuffers::TypeTable *EPropertyRootTypeTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, 0 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::EPropertyRootTypeTypeTable + }; + static const char * const names[] = { + "Primitive", + "Struct", + "Array" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_ENUM, 3, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *PropertyValueTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, -1 }, + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 0, 1 }, + { flatbuffers::ET_SEQUENCE, 0, 2 }, + { flatbuffers::ET_SEQUENCE, 0, 3 }, + { flatbuffers::ET_SEQUENCE, 0, 4 }, + { flatbuffers::ET_SEQUENCE, 0, 5 }, + { flatbuffers::ET_SEQUENCE, 0, 6 }, + { flatbuffers::ET_SEQUENCE, 0, 7 }, + { flatbuffers::ET_SEQUENCE, 0, 8 }, + { flatbuffers::ET_SEQUENCE, 0, 9 }, + { flatbuffers::ET_SEQUENCE, 0, 10 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::float_sTypeTable, + rlogic_serialization::vec2f_sTypeTable, + rlogic_serialization::vec3f_sTypeTable, + rlogic_serialization::vec4f_sTypeTable, + rlogic_serialization::int32_sTypeTable, + rlogic_serialization::int64_sTypeTable, + rlogic_serialization::vec2i_sTypeTable, + rlogic_serialization::vec3i_sTypeTable, + rlogic_serialization::vec4i_sTypeTable, + rlogic_serialization::string_sTypeTable, + rlogic_serialization::bool_sTypeTable + }; + static const char * const names[] = { + "NONE", + "float_s", + "vec2f_s", + "vec3f_s", + "vec4f_s", + "int32_s", + "int64_s", + "vec2i_s", + "vec3i_s", + "vec4i_s", + "string_s", + "bool_s" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_UNION, 12, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *float_sTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_FLOAT, 0, -1 } + }; + static const int64_t values[] = { 0, 4 }; + static const char * const names[] = { + "v" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_STRUCT, 1, type_codes, nullptr, values, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *vec2f_sTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_FLOAT, 0, -1 }, + { flatbuffers::ET_FLOAT, 0, -1 } + }; + static const int64_t values[] = { 0, 4, 8 }; + static const char * const names[] = { + "x", + "y" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_STRUCT, 2, type_codes, nullptr, values, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *vec3f_sTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_FLOAT, 0, -1 }, + { flatbuffers::ET_FLOAT, 0, -1 }, + { flatbuffers::ET_FLOAT, 0, -1 } + }; + static const int64_t values[] = { 0, 4, 8, 12 }; + static const char * const names[] = { + "x", + "y", + "z" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_STRUCT, 3, type_codes, nullptr, values, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *vec4f_sTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_FLOAT, 0, -1 }, + { flatbuffers::ET_FLOAT, 0, -1 }, + { flatbuffers::ET_FLOAT, 0, -1 }, + { flatbuffers::ET_FLOAT, 0, -1 } + }; + static const int64_t values[] = { 0, 4, 8, 12, 16 }; + static const char * const names[] = { + "x", + "y", + "z", + "w" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_STRUCT, 4, type_codes, nullptr, values, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *int32_sTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_INT, 0, -1 } + }; + static const int64_t values[] = { 0, 4 }; + static const char * const names[] = { + "v" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_STRUCT, 1, type_codes, nullptr, values, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *int64_sTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_LONG, 0, -1 } + }; + static const int64_t values[] = { 0, 8 }; + static const char * const names[] = { + "v" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_STRUCT, 1, type_codes, nullptr, values, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *vec2i_sTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_INT, 0, -1 }, + { flatbuffers::ET_INT, 0, -1 } + }; + static const int64_t values[] = { 0, 4, 8 }; + static const char * const names[] = { + "x", + "y" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_STRUCT, 2, type_codes, nullptr, values, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *vec3i_sTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_INT, 0, -1 }, + { flatbuffers::ET_INT, 0, -1 }, + { flatbuffers::ET_INT, 0, -1 } + }; + static const int64_t values[] = { 0, 4, 8, 12 }; + static const char * const names[] = { + "x", + "y", + "z" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_STRUCT, 3, type_codes, nullptr, values, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *vec4i_sTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_INT, 0, -1 }, + { flatbuffers::ET_INT, 0, -1 }, + { flatbuffers::ET_INT, 0, -1 }, + { flatbuffers::ET_INT, 0, -1 } + }; + static const int64_t values[] = { 0, 4, 8, 12, 16 }; + static const char * const names[] = { + "x", + "y", + "z", + "w" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_STRUCT, 4, type_codes, nullptr, values, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *bool_sTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_BOOL, 0, -1 } + }; + static const int64_t values[] = { 0, 1 }; + static const char * const names[] = { + "v" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_STRUCT, 1, type_codes, nullptr, values, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *string_sTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_STRING, 0, -1 } + }; + static const char * const names[] = { + "v" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 1, type_codes, nullptr, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *PropertyTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_STRING, 0, -1 }, + { flatbuffers::ET_UCHAR, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 1, 1 }, + { flatbuffers::ET_UTYPE, 0, 2 }, + { flatbuffers::ET_SEQUENCE, 0, 2 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::EPropertyRootTypeTypeTable, + rlogic_serialization::PropertyTypeTable, + rlogic_serialization::PropertyValueTypeTable + }; + static const char * const names[] = { + "name", + "rootType", + "children", + "value_type", + "value" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 5, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_PROPERTY_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/RamsesAppearanceBindingGen.h b/client/logic/lib/flatbuffers/generated/RamsesAppearanceBindingGen.h new file mode 100644 index 000000000..9b8de6814 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/RamsesAppearanceBindingGen.h @@ -0,0 +1,149 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_RAMSESAPPEARANCEBINDING_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_RAMSESAPPEARANCEBINDING_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "PropertyGen.h" +#include "RamsesBindingGen.h" +#include "RamsesReferenceGen.h" + +namespace rlogic_serialization { + +struct ResourceId; + +struct RamsesAppearanceBinding; +struct RamsesAppearanceBindingBuilder; + +inline const flatbuffers::TypeTable *ResourceIdTypeTable(); + +inline const flatbuffers::TypeTable *RamsesAppearanceBindingTypeTable(); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(8) ResourceId FLATBUFFERS_FINAL_CLASS { + private: + uint64_t resourceIdLow_; + uint64_t resourceIdHigh_; + + public: + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return ResourceIdTypeTable(); + } + ResourceId() { + memset(static_cast(this), 0, sizeof(ResourceId)); + } + ResourceId(uint64_t _resourceIdLow, uint64_t _resourceIdHigh) + : resourceIdLow_(flatbuffers::EndianScalar(_resourceIdLow)), + resourceIdHigh_(flatbuffers::EndianScalar(_resourceIdHigh)) { + } + uint64_t resourceIdLow() const { + return flatbuffers::EndianScalar(resourceIdLow_); + } + uint64_t resourceIdHigh() const { + return flatbuffers::EndianScalar(resourceIdHigh_); + } +}; +FLATBUFFERS_STRUCT_END(ResourceId, 16); + +struct RamsesAppearanceBinding FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef RamsesAppearanceBindingBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return RamsesAppearanceBindingTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_PARENTEFFECTID = 6 + }; + const rlogic_serialization::RamsesBinding *base() const { + return GetPointer(VT_BASE); + } + const rlogic_serialization::ResourceId *parentEffectId() const { + return GetStruct(VT_PARENTEFFECTID); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyField(verifier, VT_PARENTEFFECTID) && + verifier.EndTable(); + } +}; + +struct RamsesAppearanceBindingBuilder { + typedef RamsesAppearanceBinding Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(RamsesAppearanceBinding::VT_BASE, base); + } + void add_parentEffectId(const rlogic_serialization::ResourceId *parentEffectId) { + fbb_.AddStruct(RamsesAppearanceBinding::VT_PARENTEFFECTID, parentEffectId); + } + explicit RamsesAppearanceBindingBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + RamsesAppearanceBindingBuilder &operator=(const RamsesAppearanceBindingBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateRamsesAppearanceBinding( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + const rlogic_serialization::ResourceId *parentEffectId = 0) { + RamsesAppearanceBindingBuilder builder_(_fbb); + builder_.add_parentEffectId(parentEffectId); + builder_.add_base(base); + return builder_.Finish(); +} + +struct RamsesAppearanceBinding::Traits { + using type = RamsesAppearanceBinding; + static auto constexpr Create = CreateRamsesAppearanceBinding; +}; + +inline const flatbuffers::TypeTable *ResourceIdTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_ULONG, 0, -1 }, + { flatbuffers::ET_ULONG, 0, -1 } + }; + static const int64_t values[] = { 0, 8, 16 }; + static const char * const names[] = { + "resourceIdLow", + "resourceIdHigh" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_STRUCT, 2, type_codes, nullptr, values, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *RamsesAppearanceBindingTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 0, 1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::RamsesBindingTypeTable, + rlogic_serialization::ResourceIdTypeTable + }; + static const char * const names[] = { + "base", + "parentEffectId" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 2, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_RAMSESAPPEARANCEBINDING_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/RamsesBindingGen.h b/client/logic/lib/flatbuffers/generated/RamsesBindingGen.h new file mode 100644 index 000000000..c14fa82f7 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/RamsesBindingGen.h @@ -0,0 +1,118 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_RAMSESBINDING_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_RAMSESBINDING_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "PropertyGen.h" +#include "RamsesReferenceGen.h" + +namespace rlogic_serialization { + +struct RamsesBinding; +struct RamsesBindingBuilder; + +inline const flatbuffers::TypeTable *RamsesBindingTypeTable(); + +struct RamsesBinding FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef RamsesBindingBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return RamsesBindingTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_BOUNDRAMSESOBJECT = 6, + VT_ROOTINPUT = 8 + }; + const rlogic_serialization::LogicObject *base() const { + return GetPointer(VT_BASE); + } + const rlogic_serialization::RamsesReference *boundRamsesObject() const { + return GetPointer(VT_BOUNDRAMSESOBJECT); + } + const rlogic_serialization::Property *rootInput() const { + return GetPointer(VT_ROOTINPUT); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyOffset(verifier, VT_BOUNDRAMSESOBJECT) && + verifier.VerifyTable(boundRamsesObject()) && + VerifyOffset(verifier, VT_ROOTINPUT) && + verifier.VerifyTable(rootInput()) && + verifier.EndTable(); + } +}; + +struct RamsesBindingBuilder { + typedef RamsesBinding Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(RamsesBinding::VT_BASE, base); + } + void add_boundRamsesObject(flatbuffers::Offset boundRamsesObject) { + fbb_.AddOffset(RamsesBinding::VT_BOUNDRAMSESOBJECT, boundRamsesObject); + } + void add_rootInput(flatbuffers::Offset rootInput) { + fbb_.AddOffset(RamsesBinding::VT_ROOTINPUT, rootInput); + } + explicit RamsesBindingBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + RamsesBindingBuilder &operator=(const RamsesBindingBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateRamsesBinding( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + flatbuffers::Offset boundRamsesObject = 0, + flatbuffers::Offset rootInput = 0) { + RamsesBindingBuilder builder_(_fbb); + builder_.add_rootInput(rootInput); + builder_.add_boundRamsesObject(boundRamsesObject); + builder_.add_base(base); + return builder_.Finish(); +} + +struct RamsesBinding::Traits { + using type = RamsesBinding; + static auto constexpr Create = CreateRamsesBinding; +}; + +inline const flatbuffers::TypeTable *RamsesBindingTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 0, 1 }, + { flatbuffers::ET_SEQUENCE, 0, 2 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::LogicObjectTypeTable, + rlogic_serialization::RamsesReferenceTypeTable, + rlogic_serialization::PropertyTypeTable + }; + static const char * const names[] = { + "base", + "boundRamsesObject", + "rootInput" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 3, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_RAMSESBINDING_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/RamsesCameraBindingGen.h b/client/logic/lib/flatbuffers/generated/RamsesCameraBindingGen.h new file mode 100644 index 000000000..743673d04 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/RamsesCameraBindingGen.h @@ -0,0 +1,91 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_RAMSESCAMERABINDING_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_RAMSESCAMERABINDING_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "PropertyGen.h" +#include "RamsesBindingGen.h" +#include "RamsesReferenceGen.h" + +namespace rlogic_serialization { + +struct RamsesCameraBinding; +struct RamsesCameraBindingBuilder; + +inline const flatbuffers::TypeTable *RamsesCameraBindingTypeTable(); + +struct RamsesCameraBinding FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef RamsesCameraBindingBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return RamsesCameraBindingTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4 + }; + const rlogic_serialization::RamsesBinding *base() const { + return GetPointer(VT_BASE); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + verifier.EndTable(); + } +}; + +struct RamsesCameraBindingBuilder { + typedef RamsesCameraBinding Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(RamsesCameraBinding::VT_BASE, base); + } + explicit RamsesCameraBindingBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + RamsesCameraBindingBuilder &operator=(const RamsesCameraBindingBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateRamsesCameraBinding( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0) { + RamsesCameraBindingBuilder builder_(_fbb); + builder_.add_base(base); + return builder_.Finish(); +} + +struct RamsesCameraBinding::Traits { + using type = RamsesCameraBinding; + static auto constexpr Create = CreateRamsesCameraBinding; +}; + +inline const flatbuffers::TypeTable *RamsesCameraBindingTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::RamsesBindingTypeTable + }; + static const char * const names[] = { + "base" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 1, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_RAMSESCAMERABINDING_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/RamsesMeshNodeBindingGen.h b/client/logic/lib/flatbuffers/generated/RamsesMeshNodeBindingGen.h new file mode 100644 index 000000000..006704cd1 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/RamsesMeshNodeBindingGen.h @@ -0,0 +1,91 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_RAMSESMESHNODEBINDING_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_RAMSESMESHNODEBINDING_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "PropertyGen.h" +#include "RamsesBindingGen.h" +#include "RamsesReferenceGen.h" + +namespace rlogic_serialization { + +struct RamsesMeshNodeBinding; +struct RamsesMeshNodeBindingBuilder; + +inline const flatbuffers::TypeTable *RamsesMeshNodeBindingTypeTable(); + +struct RamsesMeshNodeBinding FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef RamsesMeshNodeBindingBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return RamsesMeshNodeBindingTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4 + }; + const rlogic_serialization::RamsesBinding *base() const { + return GetPointer(VT_BASE); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + verifier.EndTable(); + } +}; + +struct RamsesMeshNodeBindingBuilder { + typedef RamsesMeshNodeBinding Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(RamsesMeshNodeBinding::VT_BASE, base); + } + explicit RamsesMeshNodeBindingBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + RamsesMeshNodeBindingBuilder &operator=(const RamsesMeshNodeBindingBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateRamsesMeshNodeBinding( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0) { + RamsesMeshNodeBindingBuilder builder_(_fbb); + builder_.add_base(base); + return builder_.Finish(); +} + +struct RamsesMeshNodeBinding::Traits { + using type = RamsesMeshNodeBinding; + static auto constexpr Create = CreateRamsesMeshNodeBinding; +}; + +inline const flatbuffers::TypeTable *RamsesMeshNodeBindingTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::RamsesBindingTypeTable + }; + static const char * const names[] = { + "base" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 1, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_RAMSESMESHNODEBINDING_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/RamsesNodeBindingGen.h b/client/logic/lib/flatbuffers/generated/RamsesNodeBindingGen.h new file mode 100644 index 000000000..abeee4901 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/RamsesNodeBindingGen.h @@ -0,0 +1,103 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_RAMSESNODEBINDING_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_RAMSESNODEBINDING_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "PropertyGen.h" +#include "RamsesBindingGen.h" +#include "RamsesReferenceGen.h" + +namespace rlogic_serialization { + +struct RamsesNodeBinding; +struct RamsesNodeBindingBuilder; + +inline const flatbuffers::TypeTable *RamsesNodeBindingTypeTable(); + +struct RamsesNodeBinding FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef RamsesNodeBindingBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return RamsesNodeBindingTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_ROTATIONTYPE = 6 + }; + const rlogic_serialization::RamsesBinding *base() const { + return GetPointer(VT_BASE); + } + uint8_t rotationType() const { + return GetField(VT_ROTATIONTYPE, 0); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyField(verifier, VT_ROTATIONTYPE) && + verifier.EndTable(); + } +}; + +struct RamsesNodeBindingBuilder { + typedef RamsesNodeBinding Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(RamsesNodeBinding::VT_BASE, base); + } + void add_rotationType(uint8_t rotationType) { + fbb_.AddElement(RamsesNodeBinding::VT_ROTATIONTYPE, rotationType, 0); + } + explicit RamsesNodeBindingBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + RamsesNodeBindingBuilder &operator=(const RamsesNodeBindingBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateRamsesNodeBinding( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + uint8_t rotationType = 0) { + RamsesNodeBindingBuilder builder_(_fbb); + builder_.add_base(base); + builder_.add_rotationType(rotationType); + return builder_.Finish(); +} + +struct RamsesNodeBinding::Traits { + using type = RamsesNodeBinding; + static auto constexpr Create = CreateRamsesNodeBinding; +}; + +inline const flatbuffers::TypeTable *RamsesNodeBindingTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_UCHAR, 0, -1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::RamsesBindingTypeTable + }; + static const char * const names[] = { + "base", + "rotationType" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 2, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_RAMSESNODEBINDING_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/RamsesReferenceGen.h b/client/logic/lib/flatbuffers/generated/RamsesReferenceGen.h new file mode 100644 index 000000000..e24710e5b --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/RamsesReferenceGen.h @@ -0,0 +1,94 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_RAMSESREFERENCE_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_RAMSESREFERENCE_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +namespace rlogic_serialization { + +struct RamsesReference; +struct RamsesReferenceBuilder; + +inline const flatbuffers::TypeTable *RamsesReferenceTypeTable(); + +struct RamsesReference FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef RamsesReferenceBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return RamsesReferenceTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_OBJECTID = 4, + VT_OBJECTTYPE = 6 + }; + uint64_t objectId() const { + return GetField(VT_OBJECTID, 0); + } + uint32_t objectType() const { + return GetField(VT_OBJECTTYPE, 0); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_OBJECTID) && + VerifyField(verifier, VT_OBJECTTYPE) && + verifier.EndTable(); + } +}; + +struct RamsesReferenceBuilder { + typedef RamsesReference Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_objectId(uint64_t objectId) { + fbb_.AddElement(RamsesReference::VT_OBJECTID, objectId, 0); + } + void add_objectType(uint32_t objectType) { + fbb_.AddElement(RamsesReference::VT_OBJECTTYPE, objectType, 0); + } + explicit RamsesReferenceBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + RamsesReferenceBuilder &operator=(const RamsesReferenceBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateRamsesReference( + flatbuffers::FlatBufferBuilder &_fbb, + uint64_t objectId = 0, + uint32_t objectType = 0) { + RamsesReferenceBuilder builder_(_fbb); + builder_.add_objectId(objectId); + builder_.add_objectType(objectType); + return builder_.Finish(); +} + +struct RamsesReference::Traits { + using type = RamsesReference; + static auto constexpr Create = CreateRamsesReference; +}; + +inline const flatbuffers::TypeTable *RamsesReferenceTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_ULONG, 0, -1 }, + { flatbuffers::ET_UINT, 0, -1 } + }; + static const char * const names[] = { + "objectId", + "objectType" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 2, type_codes, nullptr, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_RAMSESREFERENCE_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/RamsesRenderGroupBindingGen.h b/client/logic/lib/flatbuffers/generated/RamsesRenderGroupBindingGen.h new file mode 100644 index 000000000..7b53bfc1a --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/RamsesRenderGroupBindingGen.h @@ -0,0 +1,214 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_RAMSESRENDERGROUPBINDING_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_RAMSESRENDERGROUPBINDING_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "PropertyGen.h" +#include "RamsesBindingGen.h" +#include "RamsesReferenceGen.h" + +namespace rlogic_serialization { + +struct Element; +struct ElementBuilder; + +struct RamsesRenderGroupBinding; +struct RamsesRenderGroupBindingBuilder; + +inline const flatbuffers::TypeTable *ElementTypeTable(); + +inline const flatbuffers::TypeTable *RamsesRenderGroupBindingTypeTable(); + +struct Element FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef ElementBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return ElementTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_NAME = 4, + VT_RAMSESOBJECT = 6 + }; + const flatbuffers::String *name() const { + return GetPointer(VT_NAME); + } + const rlogic_serialization::RamsesReference *ramsesObject() const { + return GetPointer(VT_RAMSESOBJECT); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_NAME) && + verifier.VerifyString(name()) && + VerifyOffset(verifier, VT_RAMSESOBJECT) && + verifier.VerifyTable(ramsesObject()) && + verifier.EndTable(); + } +}; + +struct ElementBuilder { + typedef Element Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_name(flatbuffers::Offset name) { + fbb_.AddOffset(Element::VT_NAME, name); + } + void add_ramsesObject(flatbuffers::Offset ramsesObject) { + fbb_.AddOffset(Element::VT_RAMSESOBJECT, ramsesObject); + } + explicit ElementBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ElementBuilder &operator=(const ElementBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateElement( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset name = 0, + flatbuffers::Offset ramsesObject = 0) { + ElementBuilder builder_(_fbb); + builder_.add_ramsesObject(ramsesObject); + builder_.add_name(name); + return builder_.Finish(); +} + +struct Element::Traits { + using type = Element; + static auto constexpr Create = CreateElement; +}; + +inline flatbuffers::Offset CreateElementDirect( + flatbuffers::FlatBufferBuilder &_fbb, + const char *name = nullptr, + flatbuffers::Offset ramsesObject = 0) { + auto name__ = name ? _fbb.CreateString(name) : 0; + return rlogic_serialization::CreateElement( + _fbb, + name__, + ramsesObject); +} + +struct RamsesRenderGroupBinding FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef RamsesRenderGroupBindingBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return RamsesRenderGroupBindingTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_ELEMENTS = 6 + }; + const rlogic_serialization::RamsesBinding *base() const { + return GetPointer(VT_BASE); + } + const flatbuffers::Vector> *elements() const { + return GetPointer> *>(VT_ELEMENTS); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyOffset(verifier, VT_ELEMENTS) && + verifier.VerifyVector(elements()) && + verifier.VerifyVectorOfTables(elements()) && + verifier.EndTable(); + } +}; + +struct RamsesRenderGroupBindingBuilder { + typedef RamsesRenderGroupBinding Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(RamsesRenderGroupBinding::VT_BASE, base); + } + void add_elements(flatbuffers::Offset>> elements) { + fbb_.AddOffset(RamsesRenderGroupBinding::VT_ELEMENTS, elements); + } + explicit RamsesRenderGroupBindingBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + RamsesRenderGroupBindingBuilder &operator=(const RamsesRenderGroupBindingBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateRamsesRenderGroupBinding( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + flatbuffers::Offset>> elements = 0) { + RamsesRenderGroupBindingBuilder builder_(_fbb); + builder_.add_elements(elements); + builder_.add_base(base); + return builder_.Finish(); +} + +struct RamsesRenderGroupBinding::Traits { + using type = RamsesRenderGroupBinding; + static auto constexpr Create = CreateRamsesRenderGroupBinding; +}; + +inline flatbuffers::Offset CreateRamsesRenderGroupBindingDirect( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + const std::vector> *elements = nullptr) { + auto elements__ = elements ? _fbb.CreateVector>(*elements) : 0; + return rlogic_serialization::CreateRamsesRenderGroupBinding( + _fbb, + base, + elements__); +} + +inline const flatbuffers::TypeTable *ElementTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_STRING, 0, -1 }, + { flatbuffers::ET_SEQUENCE, 0, 0 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::RamsesReferenceTypeTable + }; + static const char * const names[] = { + "name", + "ramsesObject" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 2, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +inline const flatbuffers::TypeTable *RamsesRenderGroupBindingTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 1, 1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::RamsesBindingTypeTable, + rlogic_serialization::ElementTypeTable + }; + static const char * const names[] = { + "base", + "elements" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 2, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_RAMSESRENDERGROUPBINDING_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/RamsesRenderPassBindingGen.h b/client/logic/lib/flatbuffers/generated/RamsesRenderPassBindingGen.h new file mode 100644 index 000000000..43151a07f --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/RamsesRenderPassBindingGen.h @@ -0,0 +1,91 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_RAMSESRENDERPASSBINDING_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_RAMSESRENDERPASSBINDING_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "PropertyGen.h" +#include "RamsesBindingGen.h" +#include "RamsesReferenceGen.h" + +namespace rlogic_serialization { + +struct RamsesRenderPassBinding; +struct RamsesRenderPassBindingBuilder; + +inline const flatbuffers::TypeTable *RamsesRenderPassBindingTypeTable(); + +struct RamsesRenderPassBinding FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef RamsesRenderPassBindingBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return RamsesRenderPassBindingTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4 + }; + const rlogic_serialization::RamsesBinding *base() const { + return GetPointer(VT_BASE); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + verifier.EndTable(); + } +}; + +struct RamsesRenderPassBindingBuilder { + typedef RamsesRenderPassBinding Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(RamsesRenderPassBinding::VT_BASE, base); + } + explicit RamsesRenderPassBindingBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + RamsesRenderPassBindingBuilder &operator=(const RamsesRenderPassBindingBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateRamsesRenderPassBinding( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0) { + RamsesRenderPassBindingBuilder builder_(_fbb); + builder_.add_base(base); + return builder_.Finish(); +} + +struct RamsesRenderPassBinding::Traits { + using type = RamsesRenderPassBinding; + static auto constexpr Create = CreateRamsesRenderPassBinding; +}; + +inline const flatbuffers::TypeTable *RamsesRenderPassBindingTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::RamsesBindingTypeTable + }; + static const char * const names[] = { + "base" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 1, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_RAMSESRENDERPASSBINDING_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/SkinBindingGen.h b/client/logic/lib/flatbuffers/generated/SkinBindingGen.h new file mode 100644 index 000000000..be7e0ae55 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/SkinBindingGen.h @@ -0,0 +1,161 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_SKINBINDING_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_SKINBINDING_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "PropertyGen.h" +#include "RamsesBindingGen.h" +#include "RamsesReferenceGen.h" + +namespace rlogic_serialization { + +struct SkinBinding; +struct SkinBindingBuilder; + +inline const flatbuffers::TypeTable *SkinBindingTypeTable(); + +struct SkinBinding FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef SkinBindingBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return SkinBindingTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_JOINTNODEBINDINGIDS = 6, + VT_INVERSEBINDINGMATRICESDATA = 8, + VT_APPEARANCEBINDINGID = 10, + VT_JOINTMATUNIFORMINPUTNAME = 12 + }; + const rlogic_serialization::LogicObject *base() const { + return GetPointer(VT_BASE); + } + const flatbuffers::Vector *jointNodeBindingIds() const { + return GetPointer *>(VT_JOINTNODEBINDINGIDS); + } + const flatbuffers::Vector *inverseBindingMatricesData() const { + return GetPointer *>(VT_INVERSEBINDINGMATRICESDATA); + } + uint64_t appearanceBindingId() const { + return GetField(VT_APPEARANCEBINDINGID, 0); + } + const flatbuffers::String *jointMatUniformInputName() const { + return GetPointer(VT_JOINTMATUNIFORMINPUTNAME); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyOffset(verifier, VT_JOINTNODEBINDINGIDS) && + verifier.VerifyVector(jointNodeBindingIds()) && + VerifyOffset(verifier, VT_INVERSEBINDINGMATRICESDATA) && + verifier.VerifyVector(inverseBindingMatricesData()) && + VerifyField(verifier, VT_APPEARANCEBINDINGID) && + VerifyOffset(verifier, VT_JOINTMATUNIFORMINPUTNAME) && + verifier.VerifyString(jointMatUniformInputName()) && + verifier.EndTable(); + } +}; + +struct SkinBindingBuilder { + typedef SkinBinding Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(SkinBinding::VT_BASE, base); + } + void add_jointNodeBindingIds(flatbuffers::Offset> jointNodeBindingIds) { + fbb_.AddOffset(SkinBinding::VT_JOINTNODEBINDINGIDS, jointNodeBindingIds); + } + void add_inverseBindingMatricesData(flatbuffers::Offset> inverseBindingMatricesData) { + fbb_.AddOffset(SkinBinding::VT_INVERSEBINDINGMATRICESDATA, inverseBindingMatricesData); + } + void add_appearanceBindingId(uint64_t appearanceBindingId) { + fbb_.AddElement(SkinBinding::VT_APPEARANCEBINDINGID, appearanceBindingId, 0); + } + void add_jointMatUniformInputName(flatbuffers::Offset jointMatUniformInputName) { + fbb_.AddOffset(SkinBinding::VT_JOINTMATUNIFORMINPUTNAME, jointMatUniformInputName); + } + explicit SkinBindingBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + SkinBindingBuilder &operator=(const SkinBindingBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateSkinBinding( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + flatbuffers::Offset> jointNodeBindingIds = 0, + flatbuffers::Offset> inverseBindingMatricesData = 0, + uint64_t appearanceBindingId = 0, + flatbuffers::Offset jointMatUniformInputName = 0) { + SkinBindingBuilder builder_(_fbb); + builder_.add_appearanceBindingId(appearanceBindingId); + builder_.add_jointMatUniformInputName(jointMatUniformInputName); + builder_.add_inverseBindingMatricesData(inverseBindingMatricesData); + builder_.add_jointNodeBindingIds(jointNodeBindingIds); + builder_.add_base(base); + return builder_.Finish(); +} + +struct SkinBinding::Traits { + using type = SkinBinding; + static auto constexpr Create = CreateSkinBinding; +}; + +inline flatbuffers::Offset CreateSkinBindingDirect( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + const std::vector *jointNodeBindingIds = nullptr, + const std::vector *inverseBindingMatricesData = nullptr, + uint64_t appearanceBindingId = 0, + const char *jointMatUniformInputName = nullptr) { + auto jointNodeBindingIds__ = jointNodeBindingIds ? _fbb.CreateVector(*jointNodeBindingIds) : 0; + auto inverseBindingMatricesData__ = inverseBindingMatricesData ? _fbb.CreateVector(*inverseBindingMatricesData) : 0; + auto jointMatUniformInputName__ = jointMatUniformInputName ? _fbb.CreateString(jointMatUniformInputName) : 0; + return rlogic_serialization::CreateSkinBinding( + _fbb, + base, + jointNodeBindingIds__, + inverseBindingMatricesData__, + appearanceBindingId, + jointMatUniformInputName__); +} + +inline const flatbuffers::TypeTable *SkinBindingTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_ULONG, 1, -1 }, + { flatbuffers::ET_FLOAT, 1, -1 }, + { flatbuffers::ET_ULONG, 0, -1 }, + { flatbuffers::ET_STRING, 0, -1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::LogicObjectTypeTable + }; + static const char * const names[] = { + "base", + "jointNodeBindingIds", + "inverseBindingMatricesData", + "appearanceBindingId", + "jointMatUniformInputName" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 5, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_SKINBINDING_RLOGIC_SERIALIZATION_H_ diff --git a/client/logic/lib/flatbuffers/generated/TimerNodeGen.h b/client/logic/lib/flatbuffers/generated/TimerNodeGen.h new file mode 100644 index 000000000..8117d8df6 --- /dev/null +++ b/client/logic/lib/flatbuffers/generated/TimerNodeGen.h @@ -0,0 +1,116 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_TIMERNODE_RLOGIC_SERIALIZATION_H_ +#define FLATBUFFERS_GENERATED_TIMERNODE_RLOGIC_SERIALIZATION_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "LogicObjectGen.h" +#include "PropertyGen.h" + +namespace rlogic_serialization { + +struct TimerNode; +struct TimerNodeBuilder; + +inline const flatbuffers::TypeTable *TimerNodeTypeTable(); + +struct TimerNode FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef TimerNodeBuilder Builder; + struct Traits; + static const flatbuffers::TypeTable *MiniReflectTypeTable() { + return TimerNodeTypeTable(); + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BASE = 4, + VT_ROOTINPUT = 6, + VT_ROOTOUTPUT = 8 + }; + const rlogic_serialization::LogicObject *base() const { + return GetPointer(VT_BASE); + } + const rlogic_serialization::Property *rootInput() const { + return GetPointer(VT_ROOTINPUT); + } + const rlogic_serialization::Property *rootOutput() const { + return GetPointer(VT_ROOTOUTPUT); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_BASE) && + verifier.VerifyTable(base()) && + VerifyOffset(verifier, VT_ROOTINPUT) && + verifier.VerifyTable(rootInput()) && + VerifyOffset(verifier, VT_ROOTOUTPUT) && + verifier.VerifyTable(rootOutput()) && + verifier.EndTable(); + } +}; + +struct TimerNodeBuilder { + typedef TimerNode Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_base(flatbuffers::Offset base) { + fbb_.AddOffset(TimerNode::VT_BASE, base); + } + void add_rootInput(flatbuffers::Offset rootInput) { + fbb_.AddOffset(TimerNode::VT_ROOTINPUT, rootInput); + } + void add_rootOutput(flatbuffers::Offset rootOutput) { + fbb_.AddOffset(TimerNode::VT_ROOTOUTPUT, rootOutput); + } + explicit TimerNodeBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + TimerNodeBuilder &operator=(const TimerNodeBuilder &); + flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset(end); + return o; + } +}; + +inline flatbuffers::Offset CreateTimerNode( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset base = 0, + flatbuffers::Offset rootInput = 0, + flatbuffers::Offset rootOutput = 0) { + TimerNodeBuilder builder_(_fbb); + builder_.add_rootOutput(rootOutput); + builder_.add_rootInput(rootInput); + builder_.add_base(base); + return builder_.Finish(); +} + +struct TimerNode::Traits { + using type = TimerNode; + static auto constexpr Create = CreateTimerNode; +}; + +inline const flatbuffers::TypeTable *TimerNodeTypeTable() { + static const flatbuffers::TypeCode type_codes[] = { + { flatbuffers::ET_SEQUENCE, 0, 0 }, + { flatbuffers::ET_SEQUENCE, 0, 1 }, + { flatbuffers::ET_SEQUENCE, 0, 1 } + }; + static const flatbuffers::TypeFunction type_refs[] = { + rlogic_serialization::LogicObjectTypeTable, + rlogic_serialization::PropertyTypeTable + }; + static const char * const names[] = { + "base", + "rootInput", + "rootOutput" + }; + static const flatbuffers::TypeTable tt = { + flatbuffers::ST_TABLE, 3, type_codes, type_refs, nullptr, names + }; + return &tt; +} + +} // namespace rlogic_serialization + +#endif // FLATBUFFERS_GENERATED_TIMERNODE_RLOGIC_SERIALIZATION_H_ diff --git a/demo/ramses-dcsm-list/res/ramses-dcsm-list.vert b/client/logic/lib/flatbuffers/schemas/AnchorPoint.fbs similarity index 63% rename from demo/ramses-dcsm-list/res/ramses-dcsm-list.vert rename to client/logic/lib/flatbuffers/schemas/AnchorPoint.fbs index 72139cfb2..6cba4950d 100644 --- a/demo/ramses-dcsm-list/res/ramses-dcsm-list.vert +++ b/client/logic/lib/flatbuffers/schemas/AnchorPoint.fbs @@ -1,22 +1,21 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2019 BMW AG +// Copyright (C) 2022 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#version 100 +include "LogicObject.fbs"; +include "Property.fbs"; -uniform highp mat4 mvpMatrix; +namespace rlogic_serialization; -attribute vec3 a_position; -attribute vec2 a_texcoord; - -varying vec2 v_texcoord; - -void main() +table AnchorPoint { - gl_Position = mvpMatrix * vec4(a_position, 1.0); - v_texcoord = a_texcoord; + base:LogicObject; + nodeBindingId:uint64; + cameraBindingId:uint64; + rootInput:Property; + rootOutput:Property; } diff --git a/client/logic/lib/flatbuffers/schemas/AnimationNode.fbs b/client/logic/lib/flatbuffers/schemas/AnimationNode.fbs new file mode 100644 index 000000000..1a1dee0c9 --- /dev/null +++ b/client/logic/lib/flatbuffers/schemas/AnimationNode.fbs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +include "LogicObject.fbs"; +include "DataArray.fbs"; +include "Property.fbs"; + +namespace rlogic_serialization; + +enum EInterpolationType:uint8 +{ + Step = 0, + Linear = 1, + Cubic = 2, + Linear_Quaternions = 3, + Cubic_Quaternions = 4, +} + +table Channel +{ + name:string; + timestamps:DataArray; + keyframes:DataArray; + interpolationType:EInterpolationType; + tangentsIn:DataArray; + tangentsOut:DataArray; +} + +table AnimationNode +{ + base:LogicObject; + channels:[Channel]; + channelsAsProperties:bool; + rootInput:Property; + rootOutput:Property; +} diff --git a/client/logic/lib/flatbuffers/schemas/ApiObjects.fbs b/client/logic/lib/flatbuffers/schemas/ApiObjects.fbs new file mode 100644 index 000000000..812d53f10 --- /dev/null +++ b/client/logic/lib/flatbuffers/schemas/ApiObjects.fbs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +include "LuaModule.fbs"; +include "LuaScript.fbs"; +include "LuaInterface.fbs"; +include "RamsesNodeBinding.fbs"; +include "RamsesAppearanceBinding.fbs"; +include "RamsesCameraBinding.fbs"; +include "RamsesRenderPassBinding.fbs"; +include "RamsesRenderGroupBinding.fbs"; +include "RamsesMeshNodeBinding.fbs"; +include "SkinBinding.fbs"; +include "Link.fbs"; +include "DataArray.fbs"; +include "AnimationNode.fbs"; +include "TimerNode.fbs"; +include "AnchorPoint.fbs"; + +namespace rlogic_serialization; + +table ApiObjects +{ + luaModules:[LuaModule]; + luaScripts:[LuaScript]; + luaInterfaces:[LuaInterface]; + nodeBindings:[RamsesNodeBinding]; + appearanceBindings:[RamsesAppearanceBinding]; + cameraBindings:[RamsesCameraBinding]; + dataArrays:[DataArray]; + animationNodes:[AnimationNode]; + timerNodes:[TimerNode]; + links:[Link]; + lastObjectId:uint64 = 0; + renderPassBindings:[RamsesRenderPassBinding]; + anchorPoints:[AnchorPoint]; + renderGroupBindings:[RamsesRenderGroupBinding]; + skinBindings:[SkinBinding]; + meshNodeBindings:[RamsesMeshNodeBinding]; +} diff --git a/client/logic/lib/flatbuffers/schemas/DataArray.fbs b/client/logic/lib/flatbuffers/schemas/DataArray.fbs new file mode 100644 index 000000000..700d93b83 --- /dev/null +++ b/client/logic/lib/flatbuffers/schemas/DataArray.fbs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +include "LogicObject.fbs"; + +namespace rlogic_serialization; + +enum EDataArrayType:uint8 +{ + Float = 0, + Vec2f = 1, + Vec3f = 2, + Vec4f = 3, + Int32 = 4, + Vec2i = 5, + Vec3i = 6, + Vec4i = 7, + FloatArray = 8 +} + +table floatArr { data:[float]; } +table intArr { data:[int32]; } + +union ArrayUnion +{ + floatArr, + intArr +} + +table DataArray +{ + base:LogicObject; + type:EDataArrayType; + data:ArrayUnion; + numElements:uint32; // all vector and array types are stored as flattened arrays, store the total number of elements extra +} diff --git a/examples/ramses-example-basic-animation-realtime/res/ramses-example-basic-animation-realtime.vert b/client/logic/lib/flatbuffers/schemas/Link.fbs similarity index 71% rename from examples/ramses-example-basic-animation-realtime/res/ramses-example-basic-animation-realtime.vert rename to client/logic/lib/flatbuffers/schemas/Link.fbs index d36728575..408ae0222 100644 --- a/examples/ramses-example-basic-animation-realtime/res/ramses-example-basic-animation-realtime.vert +++ b/client/logic/lib/flatbuffers/schemas/Link.fbs @@ -1,18 +1,18 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2018 BMW Car IT GmbH +// Copyright (C) 2020 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#version 100 +include "Property.fbs"; -uniform highp mat4 mvpMatrix; +namespace rlogic_serialization; -attribute vec3 a_position; - -void main() +table Link { - gl_Position = mvpMatrix * vec4(a_position, 1.0); + sourceProperty:Property; + targetProperty:Property; + isWeak:bool = false; } diff --git a/client/logic/lib/flatbuffers/schemas/LogicEngine.fbs b/client/logic/lib/flatbuffers/schemas/LogicEngine.fbs new file mode 100644 index 000000000..cb440e09b --- /dev/null +++ b/client/logic/lib/flatbuffers/schemas/LogicEngine.fbs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +include "ApiObjects.fbs"; + +namespace rlogic_serialization; + +table Version +{ + // Uses v_ prefix because major/minor collides with system header symbols + v_major:uint32; + v_minor:uint32; + v_patch:uint32; + v_string:string; +} + +table Metadata +{ + metadataString:string; + exporterVersion:Version; + exporterFileVersion:uint32; +} + +table LogicEngine +{ + // Meta info + ramsesVersion:Version (required); + rlogicVersion:Version (required); + // Data objects + apiObjects:ApiObjects; + assetMetadata:Metadata; + featureLevel:uint32 = 1; +} + +root_type LogicEngine; + +// The identifier ensures a binary file/buffer can be identified as a ramses logic +// binary file; also, it is versioned so that breaking changes +// can be handled gracefully by a runtime. +// If we ever reach 99, can use the letters too (e.g. rl99 -> r100) +file_identifier "rl28"; diff --git a/client/logic/lib/flatbuffers/schemas/LogicObject.fbs b/client/logic/lib/flatbuffers/schemas/LogicObject.fbs new file mode 100644 index 000000000..029ec2451 --- /dev/null +++ b/client/logic/lib/flatbuffers/schemas/LogicObject.fbs @@ -0,0 +1,17 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +namespace rlogic_serialization; + +table LogicObject +{ + name:string; + id:uint64; + userIdHigh:uint64; + userIdLow:uint64; +} diff --git a/examples/ramses-example-basic-animation/res/ramses-example-basic-animation.vert b/client/logic/lib/flatbuffers/schemas/LuaInterface.fbs similarity index 71% rename from examples/ramses-example-basic-animation/res/ramses-example-basic-animation.vert rename to client/logic/lib/flatbuffers/schemas/LuaInterface.fbs index d36728575..44840d0c2 100644 --- a/examples/ramses-example-basic-animation/res/ramses-example-basic-animation.vert +++ b/client/logic/lib/flatbuffers/schemas/LuaInterface.fbs @@ -1,18 +1,18 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2018 BMW Car IT GmbH +// Copyright (C) 2022 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#version 100 +include "LogicObject.fbs"; +include "Property.fbs"; -uniform highp mat4 mvpMatrix; +namespace rlogic_serialization; -attribute vec3 a_position; - -void main() +table LuaInterface { - gl_Position = mvpMatrix * vec4(a_position, 1.0); + base:LogicObject; + rootProperty:Property; } diff --git a/client/logic/lib/flatbuffers/schemas/LuaModule.fbs b/client/logic/lib/flatbuffers/schemas/LuaModule.fbs new file mode 100644 index 000000000..9e4f254cc --- /dev/null +++ b/client/logic/lib/flatbuffers/schemas/LuaModule.fbs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +include "LogicObject.fbs"; + +namespace rlogic_serialization; + +table LuaModule +{ + base:LogicObject; + source:string; + dependencies:[LuaModuleUsage]; + standardModules:[uint8]; + luaByteCode:[uint8]; +} + +table LuaModuleUsage +{ + name:string; + moduleId:uint64; +} diff --git a/client/logic/lib/flatbuffers/schemas/LuaScript.fbs b/client/logic/lib/flatbuffers/schemas/LuaScript.fbs new file mode 100644 index 000000000..a60e972f4 --- /dev/null +++ b/client/logic/lib/flatbuffers/schemas/LuaScript.fbs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +include "LogicObject.fbs"; +include "Property.fbs"; +include "LuaModule.fbs"; + +namespace rlogic_serialization; + +table LuaScript +{ + base:LogicObject; + luaSourceCode:string; + userModules:[LuaModuleUsage]; + standardModules:[uint8]; + // These are cached because they hold the property values + rootInput:Property; + rootOutput:Property; + luaByteCode:[uint8]; +} diff --git a/client/logic/lib/flatbuffers/schemas/Property.fbs b/client/logic/lib/flatbuffers/schemas/Property.fbs new file mode 100644 index 000000000..5c8a3a786 --- /dev/null +++ b/client/logic/lib/flatbuffers/schemas/Property.fbs @@ -0,0 +1,104 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +namespace rlogic_serialization; + +// For primitives properties, the exact type is derived from the union type +enum EPropertyRootType:uint8 +{ + Primitive = 0, + Struct = 1, + Array = 2 +} + +// Because unions can't hold primitives directly, +// we need to define some dummy structs which only +// hold a single primitive +struct float_s +{ + v:float; +} +struct vec2f_s +{ + x:float; + y:float; +} +struct vec3f_s +{ + x:float; + y:float; + z:float; +} +struct vec4f_s +{ + x:float; + y:float; + z:float; + w:float; +} +struct int32_s +{ + v:int32; +} +struct int64_s +{ + v:int64; +} +struct vec2i_s +{ + x:int32; + y:int32; +} +struct vec3i_s +{ + x:int32; + y:int32; + z:int32; +} +struct vec4i_s +{ + x:int32; + y:int32; + z:int32; + w:int32; +} +struct bool_s +{ + v:bool; +} +// Strings can't be stored in structs +// Therefore we need a table +table string_s +{ + v:string; +} + +// Now we can build our union from our +// structs with primitives +union PropertyValue +{ + float_s, + vec2f_s, + vec3f_s, + vec4f_s, + int32_s, + int64_s, + vec2i_s, + vec3i_s, + vec4i_s, + string_s, + bool_s +} + +table Property +{ + name:string; + rootType:EPropertyRootType; + children:[Property]; + value:PropertyValue; +} diff --git a/demo/ramses-dcsm-scene-references-demo/res/ramses-demo-scene-references-text-effect.vert b/client/logic/lib/flatbuffers/schemas/RamsesAppearanceBinding.fbs similarity index 66% rename from demo/ramses-dcsm-scene-references-demo/res/ramses-demo-scene-references-text-effect.vert rename to client/logic/lib/flatbuffers/schemas/RamsesAppearanceBinding.fbs index bb5469796..e6aecbedf 100644 --- a/demo/ramses-dcsm-scene-references-demo/res/ramses-demo-scene-references-text-effect.vert +++ b/client/logic/lib/flatbuffers/schemas/RamsesAppearanceBinding.fbs @@ -6,19 +6,18 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#version 100 +include "RamsesBinding.fbs"; -precision highp float; +namespace rlogic_serialization; -uniform highp mat4 mvpMatrix; - -attribute vec2 a_position; -attribute vec2 a_texcoord; - -varying vec2 v_texcoord; +struct ResourceId +{ + resourceIdLow:uint64 = 0; + resourceIdHigh:uint64 = 0; +} -void main() +table RamsesAppearanceBinding { - v_texcoord = a_texcoord; - gl_Position = mvpMatrix * vec4(a_position, 0.0, 1.0); + base:RamsesBinding; + parentEffectId:ResourceId; } diff --git a/client/logic/lib/flatbuffers/schemas/RamsesBinding.fbs b/client/logic/lib/flatbuffers/schemas/RamsesBinding.fbs new file mode 100644 index 000000000..764ad2a6c --- /dev/null +++ b/client/logic/lib/flatbuffers/schemas/RamsesBinding.fbs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +include "LogicObject.fbs"; +include "Property.fbs"; +include "RamsesReference.fbs"; + +namespace rlogic_serialization; + +// Carries a reference to ramses object and additional data specific to bindings +table RamsesBinding +{ + base:LogicObject; + boundRamsesObject:RamsesReference; + // TODO Violin don't serialize rootInput, it is redundant! (inputs are uniquely defined by the binding type and reference) + // Storing property values is furthermore dangerous because they may override ramses values + rootInput:Property; +} diff --git a/framework/PlatformAbstraction/src/String.cpp b/client/logic/lib/flatbuffers/schemas/RamsesCameraBinding.fbs similarity index 80% rename from framework/PlatformAbstraction/src/String.cpp rename to client/logic/lib/flatbuffers/schemas/RamsesCameraBinding.fbs index d458baa63..b1904b627 100644 --- a/framework/PlatformAbstraction/src/String.cpp +++ b/client/logic/lib/flatbuffers/schemas/RamsesCameraBinding.fbs @@ -6,9 +6,11 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#include "Collections/String.h" +include "RamsesBinding.fbs"; -namespace ramses_internal +namespace rlogic_serialization; + +table RamsesCameraBinding { - const size_t String::npos; + base:RamsesBinding; } diff --git a/demo/ramses-text-layout-demo/res/ramses-layout-demo-colored-quad.frag b/client/logic/lib/flatbuffers/schemas/RamsesMeshNodeBinding.fbs similarity index 75% rename from demo/ramses-text-layout-demo/res/ramses-layout-demo-colored-quad.frag rename to client/logic/lib/flatbuffers/schemas/RamsesMeshNodeBinding.fbs index bf7e0ac83..9985c2655 100644 --- a/demo/ramses-text-layout-demo/res/ramses-layout-demo-colored-quad.frag +++ b/client/logic/lib/flatbuffers/schemas/RamsesMeshNodeBinding.fbs @@ -1,16 +1,16 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2018 BMW Car IT GmbH +// Copyright (C) 2022 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#version 100 +include "RamsesBinding.fbs"; -uniform highp vec4 u_color; +namespace rlogic_serialization; -void main(void) +table RamsesMeshNodeBinding { - gl_FragColor = u_color + vec4(0.1); + base:RamsesBinding; } diff --git a/client/logic/lib/flatbuffers/schemas/RamsesNodeBinding.fbs b/client/logic/lib/flatbuffers/schemas/RamsesNodeBinding.fbs new file mode 100644 index 000000000..8e34e43d1 --- /dev/null +++ b/client/logic/lib/flatbuffers/schemas/RamsesNodeBinding.fbs @@ -0,0 +1,17 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +include "RamsesBinding.fbs"; + +namespace rlogic_serialization; + +table RamsesNodeBinding +{ + base:RamsesBinding; + rotationType:uint8; +} diff --git a/examples/ramses-example-basic-animation/res/ramses-example-basic-animation.frag b/client/logic/lib/flatbuffers/schemas/RamsesReference.fbs similarity index 77% rename from examples/ramses-example-basic-animation/res/ramses-example-basic-animation.frag rename to client/logic/lib/flatbuffers/schemas/RamsesReference.fbs index fca1eceeb..601f32949 100644 --- a/examples/ramses-example-basic-animation/res/ramses-example-basic-animation.frag +++ b/client/logic/lib/flatbuffers/schemas/RamsesReference.fbs @@ -1,15 +1,15 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2018 BMW Car IT GmbH +// Copyright (C) 2020 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#version 100 +namespace rlogic_serialization; -uniform highp vec4 color; -void main(void) +table RamsesReference { - gl_FragColor = color; + objectId:uint64; + objectType:uint32; } diff --git a/demo/ramses-text-layout-demo/res/ramses-layout-demo-red-text.frag b/client/logic/lib/flatbuffers/schemas/RamsesRenderGroupBinding.fbs similarity index 65% rename from demo/ramses-text-layout-demo/res/ramses-layout-demo-red-text.frag rename to client/logic/lib/flatbuffers/schemas/RamsesRenderGroupBinding.fbs index 40685c7aa..f7ee157ab 100644 --- a/demo/ramses-text-layout-demo/res/ramses-layout-demo-red-text.frag +++ b/client/logic/lib/flatbuffers/schemas/RamsesRenderGroupBinding.fbs @@ -1,21 +1,23 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2018 BMW Car IT GmbH +// Copyright (C) 2022 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#version 100 +include "RamsesBinding.fbs"; -precision highp float; +namespace rlogic_serialization; -uniform sampler2D u_texture; - -varying vec2 v_texcoord; +table Element +{ + name:string; + ramsesObject:RamsesReference; +} -void main(void) +table RamsesRenderGroupBinding { - float a = texture2D(u_texture, v_texcoord).r; - gl_FragColor = vec4(1.0, 0.0, 0.0, a); + base:RamsesBinding; + elements:[Element]; } diff --git a/examples/ramses-example-basic-animation-realtime/res/ramses-example-basic-animation-realtime.frag b/client/logic/lib/flatbuffers/schemas/RamsesRenderPassBinding.fbs similarity index 75% rename from examples/ramses-example-basic-animation-realtime/res/ramses-example-basic-animation-realtime.frag rename to client/logic/lib/flatbuffers/schemas/RamsesRenderPassBinding.fbs index 2609211b0..3e694c325 100644 --- a/examples/ramses-example-basic-animation-realtime/res/ramses-example-basic-animation-realtime.frag +++ b/client/logic/lib/flatbuffers/schemas/RamsesRenderPassBinding.fbs @@ -1,14 +1,16 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2018 BMW Car IT GmbH +// Copyright (C) 2022 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#version 100 +include "RamsesBinding.fbs"; -void main(void) +namespace rlogic_serialization; + +table RamsesRenderPassBinding { - gl_FragColor = vec4(1,0,0,1); + base:RamsesBinding; } diff --git a/demo/ramses-dcsm-list/res/ramses-dcsm-list.frag b/client/logic/lib/flatbuffers/schemas/SkinBinding.fbs similarity index 61% rename from demo/ramses-dcsm-list/res/ramses-dcsm-list.frag rename to client/logic/lib/flatbuffers/schemas/SkinBinding.fbs index 95d1c0a5b..9208a25b5 100644 --- a/demo/ramses-dcsm-list/res/ramses-dcsm-list.frag +++ b/client/logic/lib/flatbuffers/schemas/SkinBinding.fbs @@ -1,20 +1,20 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2019 BMW AG +// Copyright (C) 2022 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#version 100 +include "RamsesBinding.fbs"; -uniform sampler2D textureSampler; +namespace rlogic_serialization; -varying lowp vec2 v_texcoord; - -uniform highp float transparency; - -void main(void) +table SkinBinding { - gl_FragColor = texture2D(textureSampler, v_texcoord) * vec4(1.0, 1.0, 1.0, transparency); + base:LogicObject; + jointNodeBindingIds:[uint64]; + inverseBindingMatricesData:[float]; + appearanceBindingId:uint64; + jointMatUniformInputName:string; } diff --git a/framework/Communication/TransportCommon/src/ConnectionSystemInitiatorResponder.cpp b/client/logic/lib/flatbuffers/schemas/TimerNode.fbs similarity index 72% rename from framework/Communication/TransportCommon/src/ConnectionSystemInitiatorResponder.cpp rename to client/logic/lib/flatbuffers/schemas/TimerNode.fbs index ed91fbd62..0499cb6de 100644 --- a/framework/Communication/TransportCommon/src/ConnectionSystemInitiatorResponder.cpp +++ b/client/logic/lib/flatbuffers/schemas/TimerNode.fbs @@ -6,4 +6,14 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#include "TransportCommon/ConnectionSystemInitiatorResponder.h" +include "LogicObject.fbs"; +include "Property.fbs"; + +namespace rlogic_serialization; + +table TimerNode +{ + base:LogicObject; + rootInput:Property; + rootOutput:Property; +} diff --git a/client/logic/lib/impl/AnchorPoint.cpp b/client/logic/lib/impl/AnchorPoint.cpp new file mode 100644 index 000000000..739c6c1bd --- /dev/null +++ b/client/logic/lib/impl/AnchorPoint.cpp @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/AnchorPoint.h" +#include "impl/AnchorPointImpl.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "impl/RamsesCameraBindingImpl.h" + +namespace ramses +{ + AnchorPoint::AnchorPoint(std::unique_ptr impl) noexcept + : LogicNode(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_anchorPointImpl{ static_cast(LogicNode::m_impl) } + { + } + + const ramses::Node& AnchorPoint::getRamsesNode() const + { + return m_anchorPointImpl.getRamsesNodeBinding().getRamsesNode(); + } + + const ramses::Camera& AnchorPoint::getRamsesCamera() const + { + return m_anchorPointImpl.getRamsesCameraBinding().getRamsesCamera(); + } +} diff --git a/client/logic/lib/impl/AnchorPointImpl.cpp b/client/logic/lib/impl/AnchorPointImpl.cpp new file mode 100644 index 000000000..83fb8a1df --- /dev/null +++ b/client/logic/lib/impl/AnchorPointImpl.cpp @@ -0,0 +1,155 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/AnchorPointImpl.h" + +#include "ramses-client-api/Node.h" +#include "ramses-client-api/Camera.h" + +#include "ramses-logic/Property.h" + +#include "impl/PropertyImpl.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "impl/RamsesCameraBindingImpl.h" + +#include "internals/ErrorReporting.h" + +#include "generated/AnchorPointGen.h" +#include "glm/gtc/type_ptr.hpp" + +namespace ramses::internal +{ + AnchorPointImpl::AnchorPointImpl(RamsesNodeBindingImpl& nodeBinding, RamsesCameraBindingImpl& cameraBinding, std::string_view name, uint64_t id) + : LogicNodeImpl{ name, id } + , m_nodeBinding{ nodeBinding } + , m_cameraBinding{ cameraBinding } + { + } + + void AnchorPointImpl::createRootProperties() + { + auto outputsType = MakeStruct("", { + TypeData{"viewportCoords", EPropertyType::Vec2f}, + TypeData{"depth", EPropertyType::Float}, + }); + auto outputs = std::make_unique(std::move(outputsType), EPropertySemantics::ScriptOutput); + + setRootProperties({}, std::move(outputs)); + } + + flatbuffers::Offset AnchorPointImpl::Serialize( + const AnchorPointImpl& anchorPoint, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap) + { + const auto fbLogicObject = LogicObjectImpl::Serialize(anchorPoint, builder); + const auto fbOutputs = PropertyImpl::Serialize(*anchorPoint.getOutputs()->m_impl, builder, serializationMap); + auto fbAnchorPoint = rlogic_serialization::CreateAnchorPoint(builder, + fbLogicObject, + anchorPoint.m_nodeBinding.getId(), + anchorPoint.m_cameraBinding.getId(), + 0, // no inputs + fbOutputs); + + builder.Finish(fbAnchorPoint); + + return fbAnchorPoint; + } + + std::unique_ptr AnchorPointImpl::Deserialize( + const rlogic_serialization::AnchorPoint& anchorPoint, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(anchorPoint.base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of AnchorPoint from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!anchorPoint.rootOutput()) + { + errorReporting.add("Fatal error during loading of AnchorPoint from serialized data: missing root output!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr deserializedRootOutput = PropertyImpl::Deserialize(*anchorPoint.rootOutput(), EPropertySemantics::ScriptOutput, errorReporting, deserializationMap); + if (!deserializedRootOutput) + return nullptr; + + if (deserializedRootOutput->getType() != EPropertyType::Struct) + { + errorReporting.add("Fatal error during loading of AnchorPoint from serialized data: root output has unexpected type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (deserializedRootOutput->getChildCount() != 2u || + !deserializedRootOutput->getChild("viewportCoords") || deserializedRootOutput->getChild("viewportCoords")->getType() != EPropertyType::Vec2f || + !deserializedRootOutput->getChild("depth") || deserializedRootOutput->getChild("depth")->getType() != EPropertyType::Float) + { + errorReporting.add("Fatal error during loading of AnchorPoint: missing or invalid properties!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto* nodeBinding = deserializationMap.resolveLogicObject(anchorPoint.nodeBindingId()); + auto* cameraBinding = deserializationMap.resolveLogicObject(anchorPoint.cameraBindingId()); + if (!nodeBinding || !cameraBinding) + { + errorReporting.add("Fatal error during loading of AnchorPoint: could not resolve NodeBinding and/or CameraBinding!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto binding = std::make_unique(*nodeBinding, *cameraBinding, name, id); + binding->setUserId(userIdHigh, userIdLow); + binding->setRootProperties({}, std::move(deserializedRootOutput)); + + return binding; + } + + std::optional AnchorPointImpl::update() + { + matrix44f projectionMatrix; + matrix44f cameraViewMatrix; + matrix44f modelMatrix; + + const auto& ramsesCam = m_cameraBinding.getRamsesCamera(); + if (ramsesCam.getProjectionMatrix(projectionMatrix) != ramses::StatusOK) + return LogicNodeRuntimeError{"Failed to retrieve projection matrix from Ramses camera!"}; + + if (ramsesCam.getInverseModelMatrix(cameraViewMatrix) != ramses::StatusOK) + return LogicNodeRuntimeError{ "Failed to retrieve view matrix from Ramses camera!" }; + + if (m_nodeBinding.getRamsesNode().getModelMatrix(modelMatrix) != ramses::StatusOK) + return LogicNodeRuntimeError{ "Failed to retrieve model matrix from Ramses node!" }; + + const vec4f localOrigin{ 0, 0, 0, 1 }; + const vec4f pointInClipSpace = projectionMatrix * cameraViewMatrix * modelMatrix * localOrigin; + const vec4f pointInNDS = pointInClipSpace / pointInClipSpace.w; // NOLINT(cppcoreguidelines-pro-type-union-access) + const vec4f pointNormalized = (pointInNDS + 1.f) / 2.f; + const vec4f pointViewport = pointNormalized * vec4f{ float(ramsesCam.getViewportWidth()), float(ramsesCam.getViewportHeight()), 1.f, 1.f }; + + getOutputs()->getChild(0u)->m_impl->setValue(vec2f{ pointViewport.x, pointViewport.y }); // NOLINT(cppcoreguidelines-pro-type-union-access) + getOutputs()->getChild(1u)->m_impl->setValue(pointViewport.z); // NOLINT(cppcoreguidelines-pro-type-union-access) + + return std::nullopt; + } + + RamsesNodeBindingImpl& AnchorPointImpl::getRamsesNodeBinding() + { + return m_nodeBinding; + } + + RamsesCameraBindingImpl& AnchorPointImpl::getRamsesCameraBinding() + { + return m_cameraBinding; + } +} diff --git a/client/logic/lib/impl/AnchorPointImpl.h b/client/logic/lib/impl/AnchorPointImpl.h new file mode 100644 index 000000000..0fc0057f0 --- /dev/null +++ b/client/logic/lib/impl/AnchorPointImpl.h @@ -0,0 +1,63 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/LogicNodeImpl.h" +#include + +namespace rlogic_serialization +{ + struct AnchorPoint; +} + +namespace flatbuffers +{ + class FlatBufferBuilder; + template struct Offset; +} + +namespace ramses::internal +{ + class RamsesNodeBindingImpl; + class RamsesCameraBindingImpl; + class ErrorReporting; + class SerializationMap; + class DeserializationMap; + + class AnchorPointImpl : public LogicNodeImpl + { + public: + // Move-able (noexcept); Not copy-able + explicit AnchorPointImpl(RamsesNodeBindingImpl& nodeBinding, RamsesCameraBindingImpl& cameraBinding, std::string_view name, uint64_t id); + ~AnchorPointImpl() noexcept override = default; + AnchorPointImpl(const AnchorPointImpl& other) = delete; + AnchorPointImpl& operator=(const AnchorPointImpl& other) = delete; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const AnchorPointImpl& anchorPoint, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::AnchorPoint& anchorPoint, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + [[nodiscard]] RamsesNodeBindingImpl& getRamsesNodeBinding(); + [[nodiscard]] RamsesCameraBindingImpl& getRamsesCameraBinding(); + + std::optional update() override; + + void createRootProperties() final; + + private: + RamsesNodeBindingImpl& m_nodeBinding; + RamsesCameraBindingImpl& m_cameraBinding; + }; +} diff --git a/client/logic/lib/impl/AnimationNode.cpp b/client/logic/lib/impl/AnimationNode.cpp new file mode 100644 index 000000000..a693416ba --- /dev/null +++ b/client/logic/lib/impl/AnimationNode.cpp @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/AnimationNode.h" +#include "impl/AnimationNodeImpl.h" + +namespace ramses +{ + AnimationNode::AnimationNode(std::unique_ptr impl) noexcept + : LogicNode(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_animationNodeImpl{ static_cast(LogicNode::m_impl) } + { + } + + const AnimationChannels& AnimationNode::getChannels() const + { + return m_animationNodeImpl.getChannels(); + } +} diff --git a/client/logic/lib/impl/AnimationNodeConfig.cpp b/client/logic/lib/impl/AnimationNodeConfig.cpp new file mode 100644 index 000000000..8111fda5f --- /dev/null +++ b/client/logic/lib/impl/AnimationNodeConfig.cpp @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/AnimationNodeConfig.h" +#include "impl/AnimationNodeConfigImpl.h" + +namespace ramses +{ + AnimationNodeConfig::AnimationNodeConfig() noexcept + : m_impl(std::make_unique()) + { + } + + AnimationNodeConfig::~AnimationNodeConfig() noexcept = default; + + AnimationNodeConfig& AnimationNodeConfig::operator=(const AnimationNodeConfig& other) + { + m_impl = std::make_unique(*other.m_impl); + return *this; + } + + AnimationNodeConfig::AnimationNodeConfig(const AnimationNodeConfig& other) + { + *this = other; + } + + AnimationNodeConfig::AnimationNodeConfig(AnimationNodeConfig&&) noexcept = default; + AnimationNodeConfig& AnimationNodeConfig::operator=(AnimationNodeConfig&&) noexcept = default; + + bool AnimationNodeConfig::addChannel(const AnimationChannel& channelData) + { + return m_impl->addChannel(channelData); + } + + const AnimationChannels& AnimationNodeConfig::getChannels() const + { + return m_impl->getChannels(); + } + + bool AnimationNodeConfig::setExposingOfChannelDataAsProperties(bool enabled) + { + return m_impl->setExposingOfChannelDataAsProperties(enabled); + } + + bool AnimationNodeConfig::getExposingOfChannelDataAsProperties() const + { + return m_impl->getExposingOfChannelDataAsProperties(); + } +} diff --git a/client/logic/lib/impl/AnimationNodeConfigImpl.cpp b/client/logic/lib/impl/AnimationNodeConfigImpl.cpp new file mode 100644 index 000000000..fc4ac6969 --- /dev/null +++ b/client/logic/lib/impl/AnimationNodeConfigImpl.cpp @@ -0,0 +1,164 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/AnimationNodeConfigImpl.h" + +#include "ramses-logic/EPropertyType.h" +#include "ramses-logic/DataArray.h" +#include "impl/LoggerImpl.h" + +namespace ramses::internal +{ + bool AnimationNodeConfigImpl::addChannel(const AnimationChannel& channelData) + { + if (!channelData.timeStamps || !channelData.keyframes) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', missing timestamps and/or keyframes.", channelData.name); + return false; + } + + if (!CanPropertyTypeBeAnimated(channelData.keyframes->getDataType())) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', keyframes data type cannot be animated.", channelData.name); + return false; + } + + if (channelData.timeStamps->getDataType() != EPropertyType::Float) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', timestamps must be of type Float.", channelData.name); + return false; + } + + if (channelData.timeStamps->getNumElements() != channelData.keyframes->getNumElements()) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', number of keyframes must be same as number of timestamps.", channelData.name); + return false; + } + + const auto& timestamps = *channelData.timeStamps->getData(); + if (std::adjacent_find(timestamps.cbegin(), timestamps.cend(), std::greater_equal()) != timestamps.cend()) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', timestamps have to be strictly in ascending order.", channelData.name); + return false; + } + + if (channelData.keyframes->getDataType() == EPropertyType::Array) + { + const size_t elementArraySize = channelData.keyframes->getData>()->front().size(); + if (elementArraySize > MaxArrayPropertySize) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}'," + " when using elements of data type float array, the float array size ({}) cannot exceed {}.", + channelData.name, elementArraySize, MaxArrayPropertySize); + return false; + } + } + + if ((channelData.interpolationType == EInterpolationType::Linear_Quaternions || channelData.interpolationType == EInterpolationType::Cubic_Quaternions) && + channelData.keyframes->getDataType() != EPropertyType::Vec4f) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', quaternion animation requires the keyframes to be of type vec4f.", channelData.name); + return false; + } + + if (channelData.interpolationType == EInterpolationType::Cubic || channelData.interpolationType == EInterpolationType::Cubic_Quaternions) + { + if (!channelData.tangentsIn || !channelData.tangentsOut) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', cubic interpolation requires tangents to be provided.", channelData.name); + return false; + } + if (channelData.tangentsIn->getDataType() != channelData.keyframes->getDataType() || + channelData.tangentsOut->getDataType() != channelData.keyframes->getDataType()) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', tangents must be of same data type as keyframes.", channelData.name); + return false; + } + if (channelData.tangentsIn->getNumElements() != channelData.keyframes->getNumElements() || + channelData.tangentsOut->getNumElements() != channelData.keyframes->getNumElements()) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', number of tangents in/out must be same as number of keyframes.", channelData.name); + return false; + } + if (channelData.keyframes->getDataType() == EPropertyType::Array) + { + const size_t elementArraySize = channelData.keyframes->getData>()->front().size(); + if (channelData.tangentsIn->getData>()->front().size() != elementArraySize || + channelData.tangentsOut->getData>()->front().size() != elementArraySize) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', tangents must have same array element size as keyframes.", channelData.name); + return false; + } + } + } + else if (channelData.tangentsIn || channelData.tangentsOut) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', tangents were provided for other than cubic interpolation type.", channelData.name); + return false; + } + + if (m_exposeChannelDataAsProperties) + { + if (channelData.keyframes->getNumElements() > MaxArrayPropertySize) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', number of keyframes ({}) cannot exceed {} when animation data exposed as properties.", + channelData.name, channelData.keyframes->getNumElements(), MaxArrayPropertySize); + return false; + } + + if (channelData.keyframes->getDataType() == EPropertyType::Array) + { + LOG_ERROR("AnimationNodeConfig::addChannel: Cannot add channelData data '{}', elements of data type float arrays cannot be exposed as properties.", channelData.name); + return false; + } + } + + m_channels.push_back(channelData); + return true; + } + + const AnimationChannels& AnimationNodeConfigImpl::getChannels() const + { + return m_channels; + } + + bool AnimationNodeConfigImpl::setExposingOfChannelDataAsProperties(bool enabled) + { + if (!enabled) + { + m_exposeChannelDataAsProperties = false; + return true; + } + + for (const auto& channelData : m_channels) + { + if (channelData.keyframes->getNumElements() > MaxArrayPropertySize) + { + LOG_ERROR("AnimationNodeConfig::setExposingOfChannelDataAsProperties: Cannot enable channel data properties for channel '{}'," + " number of keyframes ({}) cannot exceed {} when animation data exposed as properties.", + channelData.name, channelData.keyframes->getNumElements(), MaxArrayPropertySize); + return false; + } + + if (channelData.keyframes->getDataType() == EPropertyType::Array) + { + LOG_ERROR("AnimationNodeConfig::setExposingOfChannelDataAsProperties: Cannot enable channel data properties for channel '{}'," + " elements of data type float arrays cannot be exposed as properties.", channelData.name); + return false; + } + } + + m_exposeChannelDataAsProperties = true; + return true; + } + + bool AnimationNodeConfigImpl::getExposingOfChannelDataAsProperties() const + { + return m_exposeChannelDataAsProperties; + } +} diff --git a/client/logic/lib/impl/AnimationNodeConfigImpl.h b/client/logic/lib/impl/AnimationNodeConfigImpl.h new file mode 100644 index 000000000..7cd1cb13a --- /dev/null +++ b/client/logic/lib/impl/AnimationNodeConfigImpl.h @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/AnimationTypes.h" + +namespace ramses::internal +{ + class AnimationNodeConfigImpl + { + public: + bool addChannel(const AnimationChannel& channelData); + [[nodiscard]] const AnimationChannels& getChannels() const; + + bool setExposingOfChannelDataAsProperties(bool enabled); + [[nodiscard]] bool getExposingOfChannelDataAsProperties() const; + + private: + AnimationChannels m_channels; + bool m_exposeChannelDataAsProperties = false; + }; +} diff --git a/client/logic/lib/impl/AnimationNodeImpl.cpp b/client/logic/lib/impl/AnimationNodeImpl.cpp new file mode 100644 index 000000000..e9158b61b --- /dev/null +++ b/client/logic/lib/impl/AnimationNodeImpl.cpp @@ -0,0 +1,520 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/AnimationNodeImpl.h" +#include "impl/PropertyImpl.h" +#include "impl/DataArrayImpl.h" +#include "ramses-logic/EPropertyType.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/DataArray.h" +#include "internals/EPropertySemantics.h" +#include "internals/ErrorReporting.h" +#include "generated/AnimationNodeGen.h" +#include "fmt/format.h" +#include "glm/gtx/range.hpp" +#include + +namespace ramses::internal +{ + AnimationNodeImpl::AnimationNodeImpl(AnimationChannels channels, bool exposeDataAsProperties, std::string_view name, uint64_t id) noexcept + : LogicNodeImpl(name, id) + , m_channels{ std::move(channels) } + , m_hasChannelDataExposedViaProperties{ exposeDataAsProperties } + { + m_channelsWorkData.resize(m_channels.size()); + for (size_t i = 0u; i < m_channels.size(); ++i) + { + const auto& channel = m_channels[i]; + assert(channel.timeStamps && channel.keyframes); + assert(channel.timeStamps->getNumElements() == channel.keyframes->getNumElements()); + assert(!channel.tangentsIn || channel.timeStamps->getNumElements() == channel.tangentsIn->getNumElements()); + assert(!channel.tangentsOut || channel.timeStamps->getNumElements() == channel.tangentsOut->getNumElements()); + assert(!exposeDataAsProperties || channel.keyframes->getDataType() != EPropertyType::Array); + + // extract basic channel data to work containers, update logic operates with these containers instead of original channel data + // (for timestamps and keyframes at least), it also makes it possible to modify this data in runtime while keeping original data constant + assert(m_channels[i].timeStamps->getDataType() == EPropertyType::Float && m_channels[i].timeStamps->getNumElements() > 0); + m_channelsWorkData[i].timestamps = *m_channels[i].timeStamps->getData(); + m_channelsWorkData[i].keyframes = m_channels[i].keyframes->m_impl.getDataVariant(); + + // overall duration equals longest channel in animation + m_maxChannelDuration = std::max(m_maxChannelDuration, channel.timeStamps->getData()->back()); + } + } + + void AnimationNodeImpl::createRootProperties() + { + HierarchicalTypeData inputs = MakeStruct("", { + {"progress", EPropertyType::Float}, // EInputIdx_Progress + }); + if (m_hasChannelDataExposedViaProperties) + { + std::vector channelsData; + for (const auto& channel : m_channels) + { + assert(channel.timeStamps && channel.keyframes); + assert(channel.timeStamps->getNumElements() == channel.keyframes->getNumElements()); + assert(channel.timeStamps->getNumElements() <= MaxArrayPropertySize); + const std::initializer_list channelDataArrays = + { + MakeArray("timestamps", channel.timeStamps->getNumElements(), EPropertyType::Float), + MakeArray("keyframes", channel.keyframes->getNumElements(), channel.keyframes->getDataType()) + }; + + channelsData.push_back(HierarchicalTypeData({ std::string{ channel.name }, EPropertyType::Struct }, channelDataArrays)); + } + inputs.children.push_back(HierarchicalTypeData({ "channelsData", EPropertyType::Struct }, channelsData)); // EInputIdx_ChannelsData + } + auto inputsImpl = std::make_unique(std::move(inputs), EPropertySemantics::AnimationInput); + + HierarchicalTypeData outputs = MakeStruct("", { + {"duration", EPropertyType::Float}, // EOutputIdx_Duration + }); + for (const auto& channel : m_channels) + { + if (channel.keyframes->getDataType() == EPropertyType::Array) + { + const size_t elementArraySize = channel.keyframes->getData>()->front().size(); + assert(elementArraySize < MaxArrayPropertySize); + outputs.children.push_back(MakeArray(std::string{ channel.name }, elementArraySize, EPropertyType::Float)); + } + else + { + outputs.children.push_back(MakeType(std::string{ channel.name }, channel.keyframes->getDataType())); + } + } + auto outputsImpl = std::make_unique(std::move(outputs), EPropertySemantics::AnimationOutput); + + setRootProperties(std::move(inputsImpl), std::move(outputsImpl)); + + if (m_hasChannelDataExposedViaProperties) + initAnimationDataPropertyValues(); + + // initialize duration property, no need to set every update as it can change only if timestamps are modified + getOutputs()->getChild(EOutputIdx_Duration)->set(m_maxChannelDuration); + } + + float AnimationNodeImpl::getMaximumChannelDuration() const + { + return m_maxChannelDuration; + } + + const AnimationChannels& AnimationNodeImpl::getChannels() const + { + return m_channels; + } + + std::optional AnimationNodeImpl::update() + { + // propagate data from properties if this animation node has channel data properties + if (m_hasChannelDataExposedViaProperties) + updateAnimationDataFromProperties(); + + const float progress = *getInputs()->getChild(EInputIdx_Progress)->get(); + const float localAnimationTime = progress * m_maxChannelDuration; + + for (size_t i = 0u; i < m_channels.size(); ++i) + updateChannel(i, localAnimationTime); + + return std::nullopt; + } + + void AnimationNodeImpl::updateChannel(size_t channelIdx, float localAnimationTime) + { + const auto& channelWorkData = m_channelsWorkData[channelIdx]; + const auto& channel = m_channels[channelIdx]; + const auto& timeStamps = channelWorkData.timestamps; + + // find upper/lower timestamp neighbor of elapsed timestamp + auto tsUpperIt = std::upper_bound(timeStamps.cbegin(), timeStamps.cend(), localAnimationTime); + const auto tsLowerIt = (tsUpperIt == timeStamps.cbegin() ? timeStamps.cbegin() : tsUpperIt - 1); + tsUpperIt = (tsUpperIt == timeStamps.cend() ? tsUpperIt - 1 : tsUpperIt); + + // get index into corresponding keyframes + const auto lowerIdx = static_cast(std::distance(timeStamps.cbegin(), tsLowerIt)); + const auto upperIdx = static_cast(std::distance(timeStamps.cbegin(), tsUpperIt)); + assert(lowerIdx < channel.keyframes->getNumElements()); + assert(upperIdx < channel.keyframes->getNumElements()); + + // calculate interpolation ratio between the elapsed time and timestamp neighbors [0.0, 1.0] (0.0=lower, 1.0=upper) + float interpRatio = 0.f; + float timeBetweenKeys = *tsUpperIt - *tsLowerIt; + if (tsUpperIt != tsLowerIt) + interpRatio = (localAnimationTime - *tsLowerIt) / timeBetweenKeys; + // no clamping needed mathematically but to avoid float precision issues + interpRatio = std::clamp(interpRatio, 0.f, 1.f); + + using DataVariant = std::variant< + float, + vec2f, + vec3f, + vec4f, + int32_t, + vec2i, + vec3i, + vec4i, + std::vector + >; + DataVariant interpolatedValue; + std::visit([&](const auto& v) { + switch (channel.interpolationType) + { + case EInterpolationType::Step: + interpolatedValue = v[lowerIdx]; + break; + case EInterpolationType::Linear: + case EInterpolationType::Linear_Quaternions: + interpolatedValue = interpolateKeyframes_linear(v[lowerIdx], v[upperIdx], interpRatio); + break; + case EInterpolationType::Cubic: + case EInterpolationType::Cubic_Quaternions: + { + using ValueType = std::remove_const_t>; + const auto& tIn = *channel.tangentsIn->getData(); + const auto& tOut = *channel.tangentsOut->getData(); + interpolatedValue = interpolateKeyframes_cubic(v[lowerIdx], v[upperIdx], tOut[lowerIdx], tIn[upperIdx], interpRatio, timeBetweenKeys); + break; + } + } + }, channelWorkData.keyframes); + + if (channel.interpolationType == EInterpolationType::Linear_Quaternions || channel.interpolationType == EInterpolationType::Cubic_Quaternions) + { + auto& asQuaternion = std::get(interpolatedValue); + const float normalizationFactor = 1 / std::sqrt( + asQuaternion[0] * asQuaternion[0] + + asQuaternion[1] * asQuaternion[1] + + asQuaternion[2] * asQuaternion[2] + + asQuaternion[3] * asQuaternion[3]); + + asQuaternion[0] *= normalizationFactor; + asQuaternion[1] *= normalizationFactor; + asQuaternion[2] *= normalizationFactor; + asQuaternion[3] *= normalizationFactor; + } + + // 'progress' is at index 0, channel outputs are shifted by one + auto outputValueProp = getOutputs()->getChild(channelIdx + EOutputIdx_ChannelsBegin); + std::visit([outputValueProp](const auto& v) { + using ValueType = std::remove_const_t>; + if constexpr (std::is_same_v>) + { + // array data type requires each array element to be set to individual output property + for (size_t arrayIdx = 0u; arrayIdx < v.size(); ++arrayIdx) + outputValueProp->getChild(arrayIdx)->m_impl->setValue(v[arrayIdx]); + } + else + { + outputValueProp->m_impl->setValue(PropertyValue{ v }); + } + }, interpolatedValue); + } + + template + T AnimationNodeImpl::interpolateKeyframes_linear(T lowerVal, T upperVal, float interpRatio) + { + // this will be compiled for variant visitor but must not be executed + if constexpr (std::is_same_v) + assert(false); + + if constexpr (std::is_floating_point_v) + { + return lowerVal + interpRatio * (upperVal - lowerVal); + } + else if constexpr (std::is_integral_v) + { + return lowerVal + std::lround(interpRatio * static_cast(upperVal - lowerVal)); + } + else + { + T val = lowerVal; + auto valIt = begin(val); + auto lowerValIt = begin(lowerVal); + auto upperValIt = begin(upperVal); + + // decompose vecXy and interpolate each component separately + for (; valIt != end(val); ++valIt, ++lowerValIt, ++upperValIt) + *valIt = interpolateKeyframes_linear(*lowerValIt, *upperValIt, interpRatio); + + return val; + } + } + + template + T AnimationNodeImpl::interpolateKeyframes_cubic(T lowerVal, T upperVal, T lowerTangentOut, T upperTangentIn, float interpRatio, float timeBetweenKeys) + { + // this will be compiled for variant visitor but must not be executed + if constexpr (std::is_same_v) + assert(false); + + if constexpr (std::is_floating_point_v) + { + // GLTF v2 Appendix C (https://github.com/KhronosGroup/glTF/tree/master/specification/2.0?ts=4#appendix-c-spline-interpolation) + const float t = interpRatio; + const float t2 = t * t; + const float t3 = t2 * t; + const T p0 = lowerVal; + const T p1 = upperVal; + const T m0 = timeBetweenKeys * lowerTangentOut; + const T m1 = timeBetweenKeys * upperTangentIn; + return (2.f*t3 - 3.f*t2 + 1.f) * p0 + (t3 - 2.f*t2 + t) * m0 + (-2.f*t3 + 3.f*t2) * p1 + (t3 - t2) * m1; + } + else if constexpr (std::is_integral_v) + { + return std::lround(interpolateKeyframes_cubic( + static_cast(lowerVal), + static_cast(upperVal), + static_cast(lowerTangentOut), + static_cast(upperTangentIn), + interpRatio, + timeBetweenKeys)); + } + else + { + T val = lowerVal; + auto valIt = begin(val); + auto lowerValIt = begin(lowerVal); + auto upperValIt = begin(upperVal); + auto lowerTangentOutIt = begin(lowerTangentOut); + auto upperTangentInIt = begin(upperTangentIn); + assert((end(lowerVal) - begin(lowerVal)) == (end(lowerTangentOut) - begin(lowerTangentOut))); + assert((end(lowerVal) - begin(lowerVal)) == (end(upperTangentIn) - begin(upperTangentIn))); + + // decompose vecXy and interpolate each component separately + for (; valIt != end(val); ++valIt, ++lowerValIt, ++upperValIt, ++lowerTangentOutIt, ++upperTangentInIt) + *valIt = interpolateKeyframes_cubic(*lowerValIt, *upperValIt, *lowerTangentOutIt, *upperTangentInIt, interpRatio, timeBetweenKeys); + + return val; + } + } + + flatbuffers::Offset AnimationNodeImpl::Serialize( + const AnimationNodeImpl& animNode, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap) + { + std::vector> channelsFB; + channelsFB.reserve(animNode.m_channels.size()); + for (const auto& channel : animNode.m_channels) + { + rlogic_serialization::EInterpolationType interpTypeFB = rlogic_serialization::EInterpolationType::MAX; + switch (channel.interpolationType) + { + case EInterpolationType::Step: + interpTypeFB = rlogic_serialization::EInterpolationType::Step; + break; + case EInterpolationType::Linear: + interpTypeFB = rlogic_serialization::EInterpolationType::Linear; + break; + case EInterpolationType::Cubic: + interpTypeFB = rlogic_serialization::EInterpolationType::Cubic; + break; + case EInterpolationType::Linear_Quaternions: + interpTypeFB = rlogic_serialization::EInterpolationType::Linear_Quaternions; + break; + case EInterpolationType::Cubic_Quaternions: + interpTypeFB = rlogic_serialization::EInterpolationType::Cubic_Quaternions; + break; + } + + channelsFB.push_back(rlogic_serialization::CreateChannel( + builder, + builder.CreateString(channel.name), + serializationMap.resolveDataArrayOffset(channel.timeStamps->getId()), + serializationMap.resolveDataArrayOffset(channel.keyframes->getId()), + interpTypeFB, + channel.tangentsIn ? serializationMap.resolveDataArrayOffset(channel.tangentsIn->getId()) : 0, + channel.tangentsOut ? serializationMap.resolveDataArrayOffset(channel.tangentsOut->getId()) : 0 + )); + } + + const auto logicObject = LogicObjectImpl::Serialize(animNode, builder); + const auto inputPropertyObject = PropertyImpl::Serialize(*animNode.getInputs()->m_impl, builder, serializationMap); + const auto ouputPropertyObject = PropertyImpl::Serialize(*animNode.getOutputs()->m_impl, builder, serializationMap); + return rlogic_serialization::CreateAnimationNode( + builder, + logicObject, + builder.CreateVector(channelsFB), + animNode.m_hasChannelDataExposedViaProperties, + inputPropertyObject, + ouputPropertyObject + ); + } + + std::unique_ptr AnimationNodeImpl::Deserialize( + const rlogic_serialization::AnimationNode& animNodeFB, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(animNodeFB.base(), name, id, userIdHigh, userIdLow, errorReporting) || !animNodeFB.channels() || !animNodeFB.rootInput() || !animNodeFB.rootOutput()) + { + errorReporting.add("Fatal error during loading of AnimationNode from serialized data: missing name, id, channels or in/out property data!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + AnimationChannels channels; + channels.reserve(animNodeFB.channels()->size()); + for (const auto* channelFB : *animNodeFB.channels()) + { + if (!channelFB->name() || + !channelFB->timestamps() || + !channelFB->keyframes()) + { + errorReporting.add(fmt::format("Fatal error during loading of AnimationNode '{}' channel data: missing name, timestamps or keyframes!", name), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + AnimationChannel channel; + channel.name = channelFB->name()->string_view(); + channel.timeStamps = &deserializationMap.resolveDataArray(*channelFB->timestamps()); + channel.keyframes = &deserializationMap.resolveDataArray(*channelFB->keyframes()); + + switch (channelFB->interpolationType()) + { + case rlogic_serialization::EInterpolationType::Step: + channel.interpolationType = EInterpolationType::Step; + break; + case rlogic_serialization::EInterpolationType::Linear: + channel.interpolationType = EInterpolationType::Linear; + break; + case rlogic_serialization::EInterpolationType::Cubic: + channel.interpolationType = EInterpolationType::Cubic; + break; + case rlogic_serialization::EInterpolationType::Linear_Quaternions: + channel.interpolationType = EInterpolationType::Linear_Quaternions; + break; + case rlogic_serialization::EInterpolationType::Cubic_Quaternions: + channel.interpolationType = EInterpolationType::Cubic_Quaternions; + break; + default: + errorReporting.add(fmt::format("Fatal error during loading of AnimationNode '{}' channel '{}' data: missing or invalid interpolation type!", name, channel.name), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (channel.interpolationType == EInterpolationType::Cubic || channel.interpolationType == EInterpolationType::Cubic_Quaternions) + { + if (!channelFB->tangentsIn() || + !channelFB->tangentsOut()) + { + errorReporting.add(fmt::format("Fatal error during loading of AnimationNode '{}' channel '{}' data: missing tangents!", name, channel.name), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + channel.tangentsIn = &deserializationMap.resolveDataArray(*channelFB->tangentsIn()); + channel.tangentsOut = &deserializationMap.resolveDataArray(*channelFB->tangentsOut()); + } + + channels.push_back(std::move(channel)); + } + + const bool hasChannelDataProperties = animNodeFB.channelsAsProperties(); + + // deserialize and overwrite constructor generated properties + auto rootInProperty = PropertyImpl::Deserialize(*animNodeFB.rootInput(), EPropertySemantics::AnimationInput, errorReporting, deserializationMap); + auto rootOutProperty = PropertyImpl::Deserialize(*animNodeFB.rootOutput(), EPropertySemantics::AnimationOutput, errorReporting, deserializationMap); + + auto deserialized = std::make_unique(std::move(channels), hasChannelDataProperties, name, id); + deserialized->setUserId(userIdHigh, userIdLow); + + if (!rootInProperty->getChild(EInputIdx_Progress) || rootInProperty->getChild(EInputIdx_Progress)->getName() != "progress" || + rootOutProperty->getChildCount() != deserialized->getChannels().size() + EOutputIdx_ChannelsBegin || + !rootOutProperty->getChild(EOutputIdx_Duration) || rootOutProperty->getChild(EOutputIdx_Duration)->getName() != "duration") + { + errorReporting.add(fmt::format("Fatal error during loading of AnimationNode '{}': missing or invalid properties!", name), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (hasChannelDataProperties && (!rootInProperty->getChild(EInputIdx_ChannelsData) || rootInProperty->getChild(EInputIdx_ChannelsData)->getName() != "channelsData")) + { + errorReporting.add(fmt::format("Fatal error during loading of AnimationNode '{}': missing or invalid channels data property!", name), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + deserialized->setRootProperties(std::move(rootInProperty), std::move(rootOutProperty)); + + // reset property values of all channel data to original channel data provided at creation time + if (hasChannelDataProperties) + deserialized->initAnimationDataPropertyValues(); + + return deserialized; + } + + void AnimationNodeImpl::initAnimationDataPropertyValues() + { + Property* channelsData = getInputs()->getChild("channelsData"); + assert(channelsData && channelsData->getChildCount() == m_channelsWorkData.size()); + for (size_t channelIdx = 0u; channelIdx < channelsData->getChildCount(); ++channelIdx) + { + Property* channelDataProp = channelsData->getChild(channelIdx); + Property* timestampsProp = channelDataProp->getChild("timestamps"); + Property* keyframesProp = channelDataProp->getChild("keyframes"); + assert(timestampsProp && keyframesProp); + const auto& channelData = m_channelsWorkData[channelIdx]; + assert(timestampsProp->getChildCount() == channelData.timestamps.size()); + assert(keyframesProp->getChildCount() == timestampsProp->getChildCount()); + + const auto& timestamps = channelData.timestamps; + for (size_t i = 0u; i < timestamps.size(); ++i) + timestampsProp->getChild(i)->m_impl->setValue(timestamps[i]); + + std::visit([&](const auto& keyframes) { + using ValueType = std::remove_const_t>; + // array data type requires each array element of each keyframe element to be set to individual input property + if constexpr (std::is_same_v>) + { + assert(!"not supported"); + } + else + { + for (size_t i = 0u; i < keyframes.size(); ++i) + keyframesProp->getChild(i)->m_impl->setValue(keyframes[i]); + } + }, channelData.keyframes); + } + } + + void AnimationNodeImpl::updateAnimationDataFromProperties() + { + const auto channelsDataProp = getInputs()->getChild(EInputIdx_ChannelsData); + for (size_t ch = 0u; ch < channelsDataProp->getChildCount(); ++ch) + { + const auto channelDataProp = channelsDataProp->getChild(ch); + + const auto timestampsProp = channelDataProp->getChild(0u); + auto& timestamps = m_channelsWorkData[ch].timestamps; + assert(timestamps.size() == timestampsProp->getChildCount()); + for (size_t i = 0u; i < timestamps.size(); ++i) + { + timestamps[i] = *timestampsProp->getChild(i)->get(); + m_maxChannelDuration = std::max(m_maxChannelDuration, timestamps[i]); + } + + const auto keyframesProp = channelDataProp->getChild(1u); + auto& keyframesVariant = m_channelsWorkData[ch].keyframes; + std::visit([&](auto& keyframes) { + using ValueType = std::remove_const_t>; + // array data type requires each array element of each keyframe element to be read from individual input property + if constexpr (std::is_same_v>) + { + assert(!"not supported"); + } + else + { + for (size_t i = 0u; i < keyframes.size(); ++i) + keyframes[i] = *keyframesProp->getChild(i)->get(); + } + }, keyframesVariant); + } + + getOutputs()->getChild(EOutputIdx_Duration)->set(m_maxChannelDuration); + } +} diff --git a/client/logic/lib/impl/AnimationNodeImpl.h b/client/logic/lib/impl/AnimationNodeImpl.h new file mode 100644 index 000000000..f471eac51 --- /dev/null +++ b/client/logic/lib/impl/AnimationNodeImpl.h @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/AnimationTypes.h" + +#include "impl/LogicNodeImpl.h" +#include "impl/DataArrayImpl.h" +#include + +namespace rlogic_serialization +{ + struct AnimationNode; +} + +namespace flatbuffers +{ + template struct Offset; + class FlatBufferBuilder; +} + +namespace ramses::internal +{ + class SerializationMap; + class DeserializationMap; + class ErrorReporting; + + class AnimationNodeImpl : public LogicNodeImpl + { + public: + AnimationNodeImpl(AnimationChannels channels, bool exposeDataAsProperties, std::string_view name, uint64_t id) noexcept; + + [[nodiscard]] float getMaximumChannelDuration() const; + [[nodiscard]] const AnimationChannels& getChannels() const; + + std::optional update() override; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const AnimationNodeImpl& animNode, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::AnimationNode& animNodeFB, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + void createRootProperties() final; + + private: + void updateChannel(size_t channelIdx, float localAnimationTime); + + template + T interpolateKeyframes_linear(T lowerVal, T upperVal, float interpRatio); + template + T interpolateKeyframes_cubic(T lowerVal, T upperVal, T lowerTangentOut, T upperTangentIn, float interpRatio, float timeBetweenKeys); + + void initAnimationDataPropertyValues(); + void updateAnimationDataFromProperties(); + + // original channel data provided by user + AnimationChannels m_channels; + + // work data (extracted copy of subset of original data) + struct ChannelWorkData + { + std::vector timestamps; + DataArrayImpl::DataArrayVariant keyframes; + }; + std::vector m_channelsWorkData; + + float m_maxChannelDuration = 0.f; + + bool m_hasChannelDataExposedViaProperties = false; + + enum EInputIdx + { + EInputIdx_Progress = 0, + EInputIdx_ChannelsData // optional property for animation nodes with exposed channel data - should be always last! + }; + + enum EOutputIdx + { + EOutputIdx_Duration = 0, + EOutputIdx_ChannelsBegin // must be last + }; + }; +} diff --git a/client/logic/lib/impl/Collection.cpp b/client/logic/lib/impl/Collection.cpp new file mode 100644 index 000000000..a3a8576b6 --- /dev/null +++ b/client/logic/lib/impl/Collection.cpp @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/Collection.h" +#include "ramses-logic/LuaScript.h" +#include + +namespace ramses +{ + // Make sure collections have no hidden side effects + // Excluded on purpose: default-construct, direct assignment + static_assert(std::is_trivially_copy_assignable_v>); + static_assert(std::is_trivially_copy_constructible_v>); + static_assert(std::is_trivially_copyable_v>); + static_assert(std::is_trivially_destructible_v>); + static_assert(std::is_trivially_move_assignable_v>); + static_assert(std::is_trivially_move_constructible_v>); +} diff --git a/client/logic/lib/impl/DataArray.cpp b/client/logic/lib/impl/DataArray.cpp new file mode 100644 index 000000000..70995fc6a --- /dev/null +++ b/client/logic/lib/impl/DataArray.cpp @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/DataArray.h" +#include "impl/DataArrayImpl.h" + +namespace ramses +{ + DataArray::DataArray(std::unique_ptr impl) noexcept + : LogicObject(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_impl{ static_cast(*LogicObject::m_impl) } + { + } + + EPropertyType DataArray::getDataType() const + { + return m_impl.getDataType(); + } + + template + const std::vector* DataArray::getDataInternal() const + { + return m_impl.getData(); + } + + size_t DataArray::getNumElements() const + { + return m_impl.getNumElements(); + } + + template RAMSES_API const std::vector* DataArray::getDataInternal() const; + template RAMSES_API const std::vector* DataArray::getDataInternal() const; + template RAMSES_API const std::vector* DataArray::getDataInternal() const; + template RAMSES_API const std::vector* DataArray::getDataInternal() const; + template RAMSES_API const std::vector* DataArray::getDataInternal() const; + template RAMSES_API const std::vector* DataArray::getDataInternal() const; + template RAMSES_API const std::vector* DataArray::getDataInternal() const; + template RAMSES_API const std::vector* DataArray::getDataInternal() const; + template RAMSES_API const std::vector>* DataArray::getDataInternal() const; +} diff --git a/client/logic/lib/impl/DataArrayImpl.cpp b/client/logic/lib/impl/DataArrayImpl.cpp new file mode 100644 index 000000000..1de2a2a4e --- /dev/null +++ b/client/logic/lib/impl/DataArrayImpl.cpp @@ -0,0 +1,352 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/DataArrayImpl.h" +#include "generated/DataArrayGen.h" +#include "flatbuffers/flatbuffers.h" +#include "internals/ErrorReporting.h" +#include "LoggerImpl.h" +#include +#include "glm/gtx/range.hpp" + +namespace ramses::internal +{ + template + DataArrayImpl::DataArrayImpl(std::vector&& data, std::string_view name, uint64_t id) + : LogicObjectImpl(name, id) + , m_dataType{ PropertyTypeToEnum::TYPE } + , m_data{ std::move(data) } + { + } + + template + const std::vector* ramses::internal::DataArrayImpl::getData() const + { + if (PropertyTypeToEnum::TYPE != m_dataType) + { + LOG_ERROR("DataArray::getData failed for '{}', correct template that matches stored data type must be used.", getIdentificationString()); + return nullptr; + } + + return &std::get>(m_data); + } + + EPropertyType DataArrayImpl::getDataType() const + { + return m_dataType; + } + + template + constexpr size_t getNumComponents(const std::vector& data) + { + if constexpr (std::is_arithmetic_v) + { + (void)data; + return 1u; + } + else if constexpr (std::is_same_v>) + { + assert(!data.empty()); + return data.front().size(); + } + else + { + return T::length(); + } + } + + template + std::vector flattenArrayOfVec(const DataArrayImpl::DataArrayVariant& data) + { + using std::begin; + using std::end; + const auto& dataVec = std::get>(data); + std::vector dataFlattened; + dataFlattened.reserve(dataVec.size() * getNumComponents(dataVec)); + for (const auto& v : dataVec) + dataFlattened.insert(dataFlattened.end(), begin(v), end(v)); + return dataFlattened; + } + + flatbuffers::Offset DataArrayImpl::Serialize(const DataArrayImpl& data, flatbuffers::FlatBufferBuilder& builder, SerializationMap& /*serializationMap*/) + { + rlogic_serialization::ArrayUnion unionType = rlogic_serialization::ArrayUnion::NONE; + rlogic_serialization::EDataArrayType arrayType = rlogic_serialization::EDataArrayType::Float; + flatbuffers::Offset dataOffset; + + switch (data.m_dataType) + { + case EPropertyType::Float: + unionType = rlogic_serialization::ArrayUnion::floatArr; + arrayType = rlogic_serialization::EDataArrayType::Float; + dataOffset = rlogic_serialization::CreatefloatArr(builder, builder.CreateVector(std::get>(data.m_data))).Union(); + break; + case EPropertyType::Vec2f: + unionType = rlogic_serialization::ArrayUnion::floatArr; + arrayType = rlogic_serialization::EDataArrayType::Vec2f; + dataOffset = rlogic_serialization::CreatefloatArr(builder, builder.CreateVector(flattenArrayOfVec(data.m_data))).Union(); + break; + case EPropertyType::Vec3f: + unionType = rlogic_serialization::ArrayUnion::floatArr; + arrayType = rlogic_serialization::EDataArrayType::Vec3f; + dataOffset = rlogic_serialization::CreatefloatArr(builder, builder.CreateVector(flattenArrayOfVec(data.m_data))).Union(); + break; + case EPropertyType::Vec4f: + unionType = rlogic_serialization::ArrayUnion::floatArr; + arrayType = rlogic_serialization::EDataArrayType::Vec4f; + dataOffset = rlogic_serialization::CreatefloatArr(builder, builder.CreateVector(flattenArrayOfVec(data.m_data))).Union(); + break; + case EPropertyType::Int32: + unionType = rlogic_serialization::ArrayUnion::intArr; + arrayType = rlogic_serialization::EDataArrayType::Int32; + dataOffset = rlogic_serialization::CreateintArr(builder, builder.CreateVector(std::get>(data.m_data))).Union(); + break; + case EPropertyType::Vec2i: + unionType = rlogic_serialization::ArrayUnion::intArr; + arrayType = rlogic_serialization::EDataArrayType::Vec2i; + dataOffset = rlogic_serialization::CreateintArr(builder, builder.CreateVector(flattenArrayOfVec(data.m_data))).Union(); + break; + case EPropertyType::Vec3i: + unionType = rlogic_serialization::ArrayUnion::intArr; + arrayType = rlogic_serialization::EDataArrayType::Vec3i; + dataOffset = rlogic_serialization::CreateintArr(builder, builder.CreateVector(flattenArrayOfVec(data.m_data))).Union(); + break; + case EPropertyType::Vec4i: + unionType = rlogic_serialization::ArrayUnion::intArr; + arrayType = rlogic_serialization::EDataArrayType::Vec4i; + dataOffset = rlogic_serialization::CreateintArr(builder, builder.CreateVector(flattenArrayOfVec(data.m_data))).Union(); + break; + case EPropertyType::Array: + unionType = rlogic_serialization::ArrayUnion::floatArr; + arrayType = rlogic_serialization::EDataArrayType::FloatArray; + dataOffset = rlogic_serialization::CreatefloatArr(builder, builder.CreateVector(flattenArrayOfVec, float>(data.m_data))).Union(); + break; + case EPropertyType::Bool: + default: + assert(!"missing implementation"); + break; + } + + const auto logicObject = LogicObjectImpl::Serialize(data, builder); + auto animDataFB = rlogic_serialization::CreateDataArray( + builder, + logicObject, + arrayType, + unionType, + dataOffset, + static_cast(data.getNumElements()) + ); + + return animDataFB; + } + + template + bool checkFlatbufferVectorValidity(const rlogic_serialization::DataArray& data, ErrorReporting& errorReporting, uint32_t numComponents) + { + if (!data.data_as() || !data.data_as()->data()) + { + errorReporting.add("Fatal error during loading of DataArray from serialized data: unexpected data type!", nullptr, EErrorType::BinaryVersionMismatch); + return false; + } + const auto fbVec = data.data_as()->data(); + static_assert(std::is_arithmetic_v, "wrong base type used"); + if (numComponents == 0u || fbVec->size() == 0u || (fbVec->size() % numComponents != 0)) + { + errorReporting.add("Fatal error during loading of DataArray from serialized data: unexpected data size!", nullptr, EErrorType::BinaryVersionMismatch); + return false; + } + + return true; + } + + template + std::vector unflattenIntoArrayOfVec(const flatbuffers::Vector& fbDataFlattened, uint32_t numComponents) + { + using std::begin; + std::vector dataVec; + assert(fbDataFlattened.size() % numComponents == 0u); //checked in validation above + dataVec.resize(fbDataFlattened.size() / numComponents); + auto destIt = dataVec.begin(); + for (auto it = fbDataFlattened.cbegin(); it != fbDataFlattened.cend(); it += numComponents, ++destIt) + { + if constexpr(std::is_same_v>) + { + destIt->insert(destIt->end(), it, it + numComponents); + } + else + { + std::copy(it, it + numComponents, begin(*destIt)); + } + } + return dataVec; + } + + template + uint32_t determineNumComponentsPerDataElement(const rlogic_serialization::DataArray& data) + { + if constexpr (std::is_same_v>) + { + if (!data.data_as() || !data.data_as()->data() || data.numElements() == 0u) + return 0u; + + const auto numSerializedElements = data.data_as()->data()->size(); + // NOLINTNEXTLINE(clang-analyzer-core.DivideZero) wrong assumption from clang, tested right above for zero + return numSerializedElements / data.numElements(); + } + else + { + if constexpr (std::is_arithmetic_v) + { + return 1u; + } + else + { + return T::length(); + } + } + } + + std::unique_ptr DataArrayImpl::Deserialize(const rlogic_serialization::DataArray& data, ErrorReporting& errorReporting) + { + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(data.base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of DataArray from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr deserialized; + switch (data.type()) + { + case rlogic_serialization::EDataArrayType::Float: + { + const uint32_t numComponents = determineNumComponentsPerDataElement(data); + if (!checkFlatbufferVectorValidity(data, errorReporting, numComponents)) + return nullptr; + const auto& fbData = *data.data_as()->data(); + auto dataVec = std::vector{ fbData.cbegin(), fbData.cend() }; + deserialized = std::make_unique(std::move(dataVec), name, id); + break; + } + case rlogic_serialization::EDataArrayType::Vec2f: + { + const uint32_t numComponents = determineNumComponentsPerDataElement(data); + if (!checkFlatbufferVectorValidity(data, errorReporting, numComponents)) + return nullptr; + auto dataVec = unflattenIntoArrayOfVec(*data.data_as()->data(), numComponents); + return std::make_unique(std::move(dataVec), name, id); + } + case rlogic_serialization::EDataArrayType::Vec3f: + { + const uint32_t numComponents = determineNumComponentsPerDataElement(data); + if (!checkFlatbufferVectorValidity(data, errorReporting, numComponents)) + return nullptr; + auto dataVec = unflattenIntoArrayOfVec(*data.data_as()->data(), numComponents); + deserialized = std::make_unique(std::move(dataVec), name, id); + break; + } + case rlogic_serialization::EDataArrayType::Vec4f: + { + const uint32_t numComponents = determineNumComponentsPerDataElement(data); + if (!checkFlatbufferVectorValidity(data, errorReporting, numComponents)) + return nullptr; + auto dataVec = unflattenIntoArrayOfVec(*data.data_as()->data(), numComponents); + deserialized = std::make_unique(std::move(dataVec), name, id); + break; + } + case rlogic_serialization::EDataArrayType::Int32: + { + const uint32_t numComponents = determineNumComponentsPerDataElement(data); + if (!checkFlatbufferVectorValidity(data, errorReporting, numComponents)) + return nullptr; + const auto& fbData = *data.data_as()->data(); + auto dataVec = std::vector{ fbData.cbegin(), fbData.cend() }; + deserialized = std::make_unique(std::move(dataVec), name, id); + break; + } + case rlogic_serialization::EDataArrayType::Vec2i: + { + const uint32_t numComponents = determineNumComponentsPerDataElement(data); + if (!checkFlatbufferVectorValidity(data, errorReporting, numComponents)) + return nullptr; + auto dataVec = unflattenIntoArrayOfVec(*data.data_as()->data(), numComponents); + deserialized = std::make_unique(std::move(dataVec), name, id); + break; + } + case rlogic_serialization::EDataArrayType::Vec3i: + { + const uint32_t numComponents = determineNumComponentsPerDataElement(data); + if (!checkFlatbufferVectorValidity(data, errorReporting, numComponents)) + return nullptr; + auto dataVec = unflattenIntoArrayOfVec(*data.data_as()->data(), numComponents); + deserialized = std::make_unique(std::move(dataVec), name, id); + break; + } + case rlogic_serialization::EDataArrayType::Vec4i: + { + const uint32_t numComponents = determineNumComponentsPerDataElement(data); + if (!checkFlatbufferVectorValidity(data, errorReporting, numComponents)) + return nullptr; + auto dataVec = unflattenIntoArrayOfVec(*data.data_as()->data(), numComponents); + deserialized = std::make_unique(std::move(dataVec), name, id); + break; + } + case rlogic_serialization::EDataArrayType::FloatArray: + { + const uint32_t numComponents = determineNumComponentsPerDataElement, rlogic_serialization::floatArr>(data); + if (!checkFlatbufferVectorValidity, float, rlogic_serialization::floatArr>(data, errorReporting, numComponents)) + return nullptr; + auto dataVec = unflattenIntoArrayOfVec, float>(*data.data_as()->data(), numComponents); + deserialized = std::make_unique(std::move(dataVec), name, id); + break; + } + default: + errorReporting.add(fmt::format("Fatal error during loading of DataArray from serialized data: unsupported or corrupt data type '{}'!", data.type()), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + deserialized->setUserId(userIdHigh, userIdLow); + + return deserialized; + } + + size_t DataArrayImpl::getNumElements() const + { + size_t numElements = 0u; + std::visit([&numElements](const auto& v) { numElements = v.size(); }, m_data); + return numElements; + } + + const DataArrayImpl::DataArrayVariant& DataArrayImpl::getDataVariant() const + { + return m_data; + } + + template DataArrayImpl::DataArrayImpl(std::vector&& data, std::string_view name, uint64_t id); + template DataArrayImpl::DataArrayImpl(std::vector&& data, std::string_view name, uint64_t id); + template DataArrayImpl::DataArrayImpl(std::vector&& data, std::string_view name, uint64_t id); + template DataArrayImpl::DataArrayImpl(std::vector&& data, std::string_view name, uint64_t id); + template DataArrayImpl::DataArrayImpl(std::vector&& data, std::string_view name, uint64_t id); + template DataArrayImpl::DataArrayImpl(std::vector&& data, std::string_view name, uint64_t id); + template DataArrayImpl::DataArrayImpl(std::vector&& data, std::string_view name, uint64_t id); + template DataArrayImpl::DataArrayImpl(std::vector&& data, std::string_view name, uint64_t id); + template DataArrayImpl::DataArrayImpl(std::vector>&& data, std::string_view name, uint64_t id); + + template const std::vector* DataArrayImpl::getData() const; + template const std::vector* DataArrayImpl::getData() const; + template const std::vector* DataArrayImpl::getData() const; + template const std::vector* DataArrayImpl::getData() const; + template const std::vector* DataArrayImpl::getData() const; + template const std::vector* DataArrayImpl::getData() const; + template const std::vector* DataArrayImpl::getData() const; + template const std::vector* DataArrayImpl::getData() const; + template const std::vector>* DataArrayImpl::getData>() const; +} diff --git a/client/logic/lib/impl/DataArrayImpl.h b/client/logic/lib/impl/DataArrayImpl.h new file mode 100644 index 000000000..c88a004e9 --- /dev/null +++ b/client/logic/lib/impl/DataArrayImpl.h @@ -0,0 +1,71 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/EPropertyType.h" +#include "impl/LogicObjectImpl.h" +#include +#include +#include +#include + +namespace rlogic_serialization +{ + struct DataArray; +} + +namespace flatbuffers +{ + template struct Offset; + class FlatBufferBuilder; +} + +namespace ramses::internal +{ + class ErrorReporting; + class SerializationMap; + + class DataArrayImpl : public LogicObjectImpl + { + public: + template + DataArrayImpl(std::vector&& data, std::string_view name, uint64_t id); + + template + [[nodiscard]] const std::vector* getData() const; + [[nodiscard]] size_t getNumElements() const; + [[nodiscard]] EPropertyType getDataType() const; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const DataArrayImpl& data, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::DataArray& data, + ErrorReporting& errorReporting); + + using DataArrayVariant = std::variant< + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector> + >; + [[nodiscard]] const DataArrayVariant& getDataVariant() const; + + private: + EPropertyType m_dataType = EPropertyType::Float; + DataArrayVariant m_data; + }; +} diff --git a/client/logic/lib/impl/Iterator.cpp b/client/logic/lib/impl/Iterator.cpp new file mode 100644 index 000000000..34c93bfa9 --- /dev/null +++ b/client/logic/lib/impl/Iterator.cpp @@ -0,0 +1,35 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/Iterator.h" +#include "ramses-logic/LuaScript.h" + +#include + +namespace ramses +{ + using IteratorTypeForStaticAsserts = Iterator>, false>; + using ConstIteratorTypeForStaticAsserts = Iterator>, true>; + + // Assert that const type != non-const type + static_assert(!std::is_same_v); + + // Check standard properties of non-const iterator + static_assert(std::is_copy_assignable_v); + static_assert(std::is_copy_constructible_v); + static_assert(std::is_destructible_v); + static_assert(std::is_move_assignable_v); + static_assert(std::is_move_constructible_v); + + // Check standard properties of const iterator + static_assert(std::is_copy_assignable_v); + static_assert(std::is_copy_constructible_v); + static_assert(std::is_destructible_v); + static_assert(std::is_move_assignable_v); + static_assert(std::is_move_constructible_v); +} diff --git a/client/logic/lib/impl/Logger.cpp b/client/logic/lib/impl/Logger.cpp new file mode 100644 index 000000000..5074c7120 --- /dev/null +++ b/client/logic/lib/impl/Logger.cpp @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/Logger.h" +#include "impl/LoggerImpl.h" + +namespace ramses::Logger +{ + void SetLogVerbosityLimit(ELogLevel verbosityLimit) + { + internal::LoggerImpl::GetInstance().setLogVerbosityLimit(verbosityLimit); + } + + ELogLevel GetLogVerbosityLimit() + { + return internal::LoggerImpl::GetInstance().getLogVerbosityLimit(); + } + + void SetLogHandler(const LogHandlerFunc& logHandlerFunc) + { + internal::LoggerImpl::GetInstance().setLogHandler(logHandlerFunc); + } + + void SetDefaultLogging(bool loggingEnabled) + { + internal::LoggerImpl::GetInstance().setDefaultLogging(loggingEnabled); + } +} diff --git a/client/logic/lib/impl/LoggerImpl.cpp b/client/logic/lib/impl/LoggerImpl.cpp new file mode 100644 index 000000000..485ccb08c --- /dev/null +++ b/client/logic/lib/impl/LoggerImpl.cpp @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/LoggerImpl.h" + +namespace ramses::internal +{ + LoggerImpl::LoggerImpl() noexcept + : m_logHandler(nullptr) + { + } + + void LoggerImpl::setLogHandler(Logger::LogHandlerFunc logHandlerFunc) + { + m_logHandler = std::move(logHandlerFunc); + } + + void LoggerImpl::setDefaultLogging(bool loggingEnabled) + { + m_defaultLogging = loggingEnabled; + } + + void LoggerImpl::setLogVerbosityLimit(ELogLevel verbosityLimit) + { + m_logVerbosityLimit = verbosityLimit; + } + + ELogLevel LoggerImpl::getLogVerbosityLimit() const + { + return m_logVerbosityLimit; + } + +} diff --git a/client/logic/lib/impl/LoggerImpl.h b/client/logic/lib/impl/LoggerImpl.h new file mode 100644 index 000000000..09d5a1cc7 --- /dev/null +++ b/client/logic/lib/impl/LoggerImpl.h @@ -0,0 +1,184 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/RamsesFrameworkTypes.h" +#include "ramses-logic/Logger.h" + +#include "fmt/format.h" +#include + +#ifdef __ANDROID__ +#include +#else +#include +#endif + +#define LOG_FATAL(...) \ +internal::LoggerImpl::GetInstance().log(ramses::ELogLevel::Fatal, __VA_ARGS__) + +#define LOG_ERROR(...) \ +internal::LoggerImpl::GetInstance().log(ramses::ELogLevel::Error, __VA_ARGS__) + +#define LOG_WARN(...) \ +internal::LoggerImpl::GetInstance().log(ramses::ELogLevel::Warn, __VA_ARGS__) + +#define LOG_INFO(...) \ +internal::LoggerImpl::GetInstance().log(ramses::ELogLevel::Info, __VA_ARGS__) + +#define LOG_DEBUG(...) \ +internal::LoggerImpl::GetInstance().log(ramses::ELogLevel::Debug, __VA_ARGS__) + +#define LOG_TRACE(...) \ +internal::LoggerImpl::GetInstance().log(ramses::ELogLevel::Trace, __VA_ARGS__) + +namespace ramses::internal +{ + static inline const char* GetLogMessageTypeString(ELogLevel type) + { + switch (type) + { + case ELogLevel::Off: + assert(false && "Should never call this!"); + return ""; + case ELogLevel::Fatal: + return "FATAL "; + case ELogLevel::Error: + return "ERROR"; + case ELogLevel::Warn: + return "WARN "; + case ELogLevel::Info: + return "INFO "; + case ELogLevel::Debug: + return "DEBUG"; + case ELogLevel::Trace: + return "TRACE"; + } + assert(false); + return "INFO "; + } + + class LoggerImpl + { + public: + ~LoggerImpl() noexcept = default; + LoggerImpl(const LoggerImpl& other) = delete; + LoggerImpl(LoggerImpl&& other) = delete; + LoggerImpl& operator=(const LoggerImpl& other) = delete; + LoggerImpl& operator=(LoggerImpl&& other) = delete; + + template + // NOLINTNEXTLINE(modernize-avoid-c-arrays) need type representing string literals + void log(ELogLevel messageType, const char(&fmtString)[N], const ARGS&... args); + template + void log(ELogLevel messageType, std::string_view fmtString, const ARGS&... args); + + void setLogVerbosityLimit(ELogLevel verbosityLimit); + [[nodiscard]] ELogLevel getLogVerbosityLimit() const; + void setLogHandler(Logger::LogHandlerFunc logHandlerFunc); + void setDefaultLogging(bool loggingEnabled); + + static LoggerImpl& GetInstance(); + + private: + LoggerImpl() noexcept; + + [[nodiscard]] bool logMessageExceedsVerbosityLimit(ELogLevel messageType) const + { + return (messageType > m_logVerbosityLimit); + } + + static void PrintLogMessage(ELogLevel messageType, const std::string& message); + + Logger::LogHandlerFunc m_logHandler; + bool m_defaultLogging = true; + + ELogLevel m_logVerbosityLimit = ELogLevel::Info; + + }; + + // Note: we are forcing here format string to be literal to avoid issues. + // Otherwise a generated string could potentially contain content from user application + // (e.g. name, script code, malicious or invalid file) which if passed + // to Fmt directly as format string could cause undesired behavior (e.g. if contains curly brackets). + template + // NOLINTNEXTLINE(modernize-avoid-c-arrays) need type representing string literals + inline void LoggerImpl::log(ELogLevel messageType, const char(&fmtString)[N], const ARGS&... args) + { + // Early exit if log level exceeded, or no logger configured + if (logMessageExceedsVerbosityLimit(messageType) || (!m_defaultLogging && !m_logHandler)) + { + return; + } + + const std::string formattedMessage = fmt::format(fmtString, args...); + if (m_defaultLogging) + { + PrintLogMessage(messageType, formattedMessage); + } + if (nullptr != m_logHandler) + { + m_logHandler(messageType, formattedMessage); + } + } + + // workaround to make static assert below dependent on template argument + template + struct TemplatedFalse : std::false_type {}; + + template + void LoggerImpl::log(ELogLevel /*messageType*/, std::string_view /*fmtString*/, const ARGS&... /*args*/) + { + // See comment above for reasons + static_assert(TemplatedFalse::value, "Always use literal as format string when logging, e.g. 'LOG_ERROR(\"{}\", errorMsg)'"); + } + + inline void LoggerImpl::PrintLogMessage(ELogLevel messageType, const std::string& message) + { +#ifdef __ANDROID__ + + android_LogPriority logLevel; + + switch (messageType) + { + case ELogLevel::Trace: + logLevel = ANDROID_LOG_VERBOSE; + break; + case ELogLevel::Debug: + logLevel = ANDROID_LOG_DEBUG; + break; + case ELogLevel::Info: + logLevel = ANDROID_LOG_INFO; + break; + case ELogLevel::Warn: + logLevel = ANDROID_LOG_WARN; + break; + case ELogLevel::Error: + logLevel = ANDROID_LOG_ERROR; + break; + case ELogLevel::Fatal: + logLevel = ANDROID_LOG_FATAL; + break; + default: + logLevel = ANDROID_LOG_UNKNOWN; + break; + } + + __android_log_print(logLevel, "Ramses.Logic", "%s", message.c_str()); +#else + std::cout << "[ " << GetLogMessageTypeString(messageType) << " ] " << message << std::endl; +#endif + } + + inline LoggerImpl& LoggerImpl::GetInstance() + { + static LoggerImpl logger; + return logger; + } +} diff --git a/client/logic/lib/impl/LogicEngine.cpp b/client/logic/lib/impl/LogicEngine.cpp new file mode 100644 index 000000000..b4dfe0fa9 --- /dev/null +++ b/client/logic/lib/impl/LogicEngine.cpp @@ -0,0 +1,368 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/LogicEngine.h" + +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaModule.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/SkinBinding.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/AnchorPoint.h" + +#include "impl/LogicEngineImpl.h" +#include "impl/LuaConfigImpl.h" +#include "internals/ApiObjects.h" + +#include + +namespace ramses +{ + LogicEngine::LogicEngine(ramses::EFeatureLevel featureLevel) noexcept + : m_impl(std::make_unique(featureLevel)) + { + } + + LogicEngine::~LogicEngine() noexcept = default; + + ramses::EFeatureLevel LogicEngine::getFeatureLevel() const + { + return m_impl->getFeatureLevel(); + } + + LogicEngine::LogicEngine(LogicEngine&& other) noexcept = default; + + LogicEngine& LogicEngine::operator=(LogicEngine&& other) noexcept = default; + + template + Collection LogicEngine::getLogicObjectsInternal() const + { + return Collection(m_impl->getApiObjects().getApiObjectContainer()); + } + + template + const T* findObject(const internal::ApiObjects& apiObjects, std::string_view name) + { + const auto& container = apiObjects.getApiObjectContainer(); + const auto it = std::find_if(container.cbegin(), container.cend(), [name](const auto& o) { + return o->getName() == name; }); + + return (it == container.cend() ? nullptr : *it); + } + + template + const T* LogicEngine::findLogicObjectInternal(std::string_view name) const + { + auto& container = m_impl->getApiObjects().getApiObjectContainer(); + const auto it = std::find_if(container.begin(), container.end(), [name](const auto& o) { + return o->getName() == name; }); + + return (it == container.end() ? nullptr : *it); + } + + template + T* LogicEngine::findLogicObjectInternal(std::string_view name) + { + auto& container = m_impl->getApiObjects().getApiObjectContainer(); + const auto it = std::find_if(container.begin(), container.end(), [name](const auto& o) { + return o->getName() == name; }); + + return (it == container.end() ? nullptr : *it); + } + + const LogicObject* LogicEngine::findLogicObjectById(uint64_t id) const + { + return m_impl->getApiObjects().getApiObjectById(id); + } + + LogicObject* LogicEngine::findLogicObjectById(uint64_t id) + { + return m_impl->getApiObjects().getApiObjectById(id); + } + + LuaScript* LogicEngine::createLuaScript(std::string_view source, const LuaConfig& config, std::string_view scriptName) + { + return m_impl->createLuaScript(source, *config.m_impl, scriptName); + } + + LuaInterface* LogicEngine::createLuaInterface(std::string_view source, std::string_view interfaceName, const LuaConfig& config) + { + return m_impl->createLuaInterface(source, *config.m_impl, interfaceName, true); + } + + LuaInterface* LogicEngine::createLuaInterface(std::string_view source, std::string_view interfaceName) + { + // deprecated version of interface creation does not verify modules declared in script + return m_impl->createLuaInterface(source, {}, interfaceName, false); + } + + LuaModule* LogicEngine::createLuaModule(std::string_view source, const LuaConfig& config, std::string_view moduleName) + { + return m_impl->createLuaModule(source, *config.m_impl, moduleName); + } + + bool LogicEngine::extractLuaDependencies(std::string_view source, const std::function& callbackFunc) + { + return m_impl->extractLuaDependencies(source, callbackFunc); + } + + RamsesNodeBinding* LogicEngine::createRamsesNodeBinding(ramses::Node& ramsesNode, ramses::ERotationType rotationType /* = ramses::ERotationType::Euler_XYZ*/, std::string_view name) + { + return m_impl->createRamsesNodeBinding(ramsesNode, rotationType, name); + } + + bool LogicEngine::destroy(LogicObject& object) + { + return m_impl->destroy(object); + } + + RamsesAppearanceBinding* LogicEngine::createRamsesAppearanceBinding(ramses::Appearance& ramsesAppearance, std::string_view name) + { + return m_impl->createRamsesAppearanceBinding(ramsesAppearance, name); + } + + RamsesCameraBinding* LogicEngine::createRamsesCameraBinding(ramses::Camera& ramsesCamera, std::string_view name) + { + return m_impl->createRamsesCameraBinding(ramsesCamera, name); + } + + RamsesCameraBinding* LogicEngine::createRamsesCameraBindingWithFrustumPlanes(ramses::Camera& ramsesCamera, std::string_view name) + { + return m_impl->createRamsesCameraBindingWithFrustumPlanes(ramsesCamera, name); + } + + RamsesRenderPassBinding* LogicEngine::createRamsesRenderPassBinding(ramses::RenderPass& ramsesRenderPass, std::string_view name) + { + return m_impl->createRamsesRenderPassBinding(ramsesRenderPass, name); + } + + RamsesRenderGroupBinding* LogicEngine::createRamsesRenderGroupBinding(ramses::RenderGroup& ramsesRenderGroup, const RamsesRenderGroupBindingElements& elements, std::string_view name) + { + return m_impl->createRamsesRenderGroupBinding(ramsesRenderGroup, elements, name); + } + + RamsesMeshNodeBinding* LogicEngine::createRamsesMeshNodeBinding(ramses::MeshNode& ramsesMeshNode, std::string_view name) + { + return m_impl->createRamsesMeshNodeBinding(ramsesMeshNode, name); + } + + SkinBinding* LogicEngine::createSkinBinding( + const std::vector& joints, + const std::vector& inverseBindMatrices, + RamsesAppearanceBinding& appearanceBinding, + const ramses::UniformInput& jointMatInput, + std::string_view name) + { + return m_impl->createSkinBinding(joints, inverseBindMatrices, appearanceBinding, jointMatInput, name); + } + + template + DataArray* LogicEngine::createDataArrayInternal(const std::vector& data, std::string_view name) + { + static_assert(CanPropertyTypeBeStoredInDataArray(PropertyTypeToEnum::TYPE)); + return m_impl->createDataArray(data, name); + } + + AnimationNode* LogicEngine::createAnimationNode(const AnimationNodeConfig& config, std::string_view name) + { + return m_impl->createAnimationNode(config, name); + } + + TimerNode* LogicEngine::createTimerNode(std::string_view name) + { + return m_impl->createTimerNode(name); + } + + AnchorPoint* LogicEngine::createAnchorPoint(RamsesNodeBinding& nodeBinding, RamsesCameraBinding& cameraBinding, std::string_view name) + { + return m_impl->createAnchorPoint(nodeBinding, cameraBinding, name); + } + + const std::vector& LogicEngine::getErrors() const + { + return m_impl->getErrors(); + } + + const std::vector& LogicEngine::validate() const + { + return m_impl->validate(); + } + + bool LogicEngine::update() + { + return m_impl->update(); + } + + void LogicEngine::enableUpdateReport(bool enable) + { + m_impl->enableUpdateReport(enable); + } + + LogicEngineReport LogicEngine::getLastUpdateReport() const + { + return m_impl->getLastUpdateReport(); + } + + void LogicEngine::setStatisticsLoggingRate(size_t loggingRate) + { + m_impl->setStatisticsLoggingRate(loggingRate); + } + + void LogicEngine::setStatisticsLogLevel(ELogLevel logLevel) + { + m_impl->setStatisticsLogLevel(logLevel); + } + + bool LogicEngine::loadFromFile(std::string_view filename, ramses::Scene* ramsesScene /* = nullptr*/, bool enableMemoryVerification /* = true */) + { + return m_impl->loadFromFile(filename, ramsesScene, enableMemoryVerification); + } + + bool LogicEngine::loadFromFileDescriptor(int fd, size_t offset, size_t length, ramses::Scene* ramsesScene /* = nullptr*/, bool enableMemoryVerification /* = true */) + { + return m_impl->loadFromFileDescriptor(fd, offset, length, ramsesScene, enableMemoryVerification); + } + + bool LogicEngine::loadFromBuffer(const void* rawBuffer, size_t bufferSize, ramses::Scene* ramsesScene /* = nullptr*/, bool enableMemoryVerification /* = true */) + { + return m_impl->loadFromBuffer(rawBuffer, bufferSize, ramsesScene, enableMemoryVerification); + } + + bool LogicEngine::GetFeatureLevelFromFile(std::string_view filename, ramses::EFeatureLevel& detectedFeatureLevel) + { + return internal::LogicEngineImpl::GetFeatureLevelFromFile(filename, detectedFeatureLevel); + } + + bool LogicEngine::GetFeatureLevelFromBuffer(std::string_view logname, const void* buffer, size_t bufferSize, ramses::EFeatureLevel& detectedFeatureLevel) + { + return internal::LogicEngineImpl::GetFeatureLevelFromBuffer(logname, buffer, bufferSize, detectedFeatureLevel); + } + + bool LogicEngine::saveToFile(std::string_view filename, const SaveFileConfig& config) + { + return m_impl->saveToFile(filename, *config.m_impl); + } + + bool LogicEngine::link(const Property& sourceProperty, const Property& targetProperty) + { + return m_impl->link(sourceProperty, targetProperty); + } + + bool LogicEngine::linkWeak(const Property& sourceProperty, const Property& targetProperty) + { + return m_impl->linkWeak(sourceProperty, targetProperty); + } + + bool LogicEngine::unlink(const Property& sourceProperty, const Property& targetProperty) + { + return m_impl->unlink(sourceProperty, targetProperty); + } + + bool LogicEngine::isLinked(const LogicNode& logicNode) const + { + return m_impl->isLinked(logicNode); + } + + size_t LogicEngine::getTotalSerializedSize(ELuaSavingMode luaSavingMode) const + { + return m_impl->getTotalSerializedSize(luaSavingMode); + } + + template + size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode luaSavingMode) const + { + return m_impl->getSerializedSize(luaSavingMode); + } + + const std::vector& LogicEngine::getPropertyLinks() const + { + return m_impl->getApiObjects().getAllPropertyLinks(); + } + + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + template RAMSES_API Collection LogicEngine::getLogicObjectsInternal() const; + + template RAMSES_API const LogicObject* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const LuaScript* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const LuaModule* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const LuaInterface* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const RamsesNodeBinding* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const RamsesAppearanceBinding* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const RamsesCameraBinding* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const RamsesRenderPassBinding* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const RamsesRenderGroupBinding* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const RamsesMeshNodeBinding* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const SkinBinding* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const DataArray* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const AnimationNode* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const TimerNode* LogicEngine::findLogicObjectInternal(std::string_view) const; + template RAMSES_API const AnchorPoint* LogicEngine::findLogicObjectInternal(std::string_view) const; + + template RAMSES_API LogicObject* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API LuaScript* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API LuaModule* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API LuaInterface* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API RamsesNodeBinding* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API RamsesAppearanceBinding* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API RamsesCameraBinding* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API RamsesRenderPassBinding* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API RamsesRenderGroupBinding* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API RamsesMeshNodeBinding* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API SkinBinding* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API DataArray* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API AnimationNode* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API TimerNode* LogicEngine::findLogicObjectInternal(std::string_view); + template RAMSES_API AnchorPoint* LogicEngine::findLogicObjectInternal(std::string_view); + + template RAMSES_API DataArray* LogicEngine::createDataArrayInternal(const std::vector&, std::string_view); + template RAMSES_API DataArray* LogicEngine::createDataArrayInternal(const std::vector&, std::string_view); + template RAMSES_API DataArray* LogicEngine::createDataArrayInternal(const std::vector&, std::string_view); + template RAMSES_API DataArray* LogicEngine::createDataArrayInternal(const std::vector&, std::string_view); + template RAMSES_API DataArray* LogicEngine::createDataArrayInternal(const std::vector&, std::string_view); + template RAMSES_API DataArray* LogicEngine::createDataArrayInternal(const std::vector&, std::string_view); + template RAMSES_API DataArray* LogicEngine::createDataArrayInternal(const std::vector&, std::string_view); + template RAMSES_API DataArray* LogicEngine::createDataArrayInternal(const std::vector&, std::string_view); + template RAMSES_API DataArray* LogicEngine::createDataArrayInternal>(const std::vector>&, std::string_view); + + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; + template RAMSES_API size_t LogicEngine::getSerializedSizeInternal(ELuaSavingMode) const; +} diff --git a/client/logic/lib/impl/LogicEngineImpl.cpp b/client/logic/lib/impl/LogicEngineImpl.cpp new file mode 100644 index 000000000..4c89ed862 --- /dev/null +++ b/client/logic/lib/impl/LogicEngineImpl.cpp @@ -0,0 +1,857 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/LogicEngineImpl.h" + +#include "ramses-framework-api/RamsesVersion.h" +#include "ramses-logic/LogicNode.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaModule.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/AnchorPoint.h" +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesRenderGroupBindingElements.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/SkinBinding.h" + +#include "impl/LogicNodeImpl.h" +#include "impl/LoggerImpl.h" +#include "impl/LuaScriptImpl.h" +#include "impl/LuaModuleImpl.h" +#include "impl/LuaConfigImpl.h" +#include "impl/SaveFileConfigImpl.h" +#include "impl/LogicEngineReportImpl.h" +#include "impl/RamsesRenderGroupBindingElementsImpl.h" + +#include "internals/FileUtils.h" +#include "internals/TypeUtils.h" +#include "internals/RamsesObjectResolver.h" + +#include "ramses-client-api/RenderGroup.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/Effect.h" +#include "ramses-utils.h" + +#include "generated/LogicEngineGen.h" +#include "ramses-sdk-build-config.h" + +#include "fmt/format.h" + +#include +#include +#include + +namespace ramses::internal +{ + LogicEngineImpl::LogicEngineImpl(ramses::EFeatureLevel featureLevel) + : m_apiObjects{ std::make_unique(featureLevel) } + , m_featureLevel{ featureLevel } + { + if (std::find(ramses::AllFeatureLevels.cbegin(), ramses::AllFeatureLevels.cend(), m_featureLevel) == ramses::AllFeatureLevels.cend()) + { + LOG_ERROR("Unrecognized feature level '0{}' provided, falling back to feature level 01", m_featureLevel); + m_featureLevel = ramses::EFeatureLevel_01; + } + } + + LogicEngineImpl::~LogicEngineImpl() noexcept = default; + + LuaScript* LogicEngineImpl::createLuaScript(std::string_view source, const LuaConfigImpl& config, std::string_view scriptName) + { + m_errors.clear(); + return m_apiObjects->createLuaScript(source, config, scriptName, m_errors); + } + + LuaInterface* LogicEngineImpl::createLuaInterface(std::string_view source, const LuaConfigImpl& config, std::string_view interfaceName, bool verifyModules) + { + m_errors.clear(); + return m_apiObjects->createLuaInterface(source, config, interfaceName, m_errors, verifyModules); + } + + LuaModule* LogicEngineImpl::createLuaModule(std::string_view source, const LuaConfigImpl& config, std::string_view moduleName) + { + m_errors.clear(); + return m_apiObjects->createLuaModule(source, config, moduleName, m_errors); + } + + bool LogicEngineImpl::extractLuaDependencies(std::string_view source, const std::function& callbackFunc) + { + m_errors.clear(); + const std::optional> extractedDependencies = LuaCompilationUtils::ExtractModuleDependencies(source, m_errors); + if (!extractedDependencies) + return false; + + for (const auto& dep : *extractedDependencies) + callbackFunc(dep); + + return true; + } + + RamsesNodeBinding* LogicEngineImpl::createRamsesNodeBinding(ramses::Node& ramsesNode, ramses::ERotationType rotationType, std::string_view name) + { + m_errors.clear(); + return m_apiObjects->createRamsesNodeBinding(ramsesNode, rotationType, name); + } + + RamsesAppearanceBinding* LogicEngineImpl::createRamsesAppearanceBinding(ramses::Appearance& ramsesAppearance, std::string_view name) + { + m_errors.clear(); + return m_apiObjects->createRamsesAppearanceBinding(ramsesAppearance, name); + } + + RamsesCameraBinding* LogicEngineImpl::createRamsesCameraBinding(ramses::Camera& ramsesCamera, std::string_view name) + { + m_errors.clear(); + return m_apiObjects->createRamsesCameraBinding(ramsesCamera, false, name); + } + + RamsesCameraBinding* LogicEngineImpl::createRamsesCameraBindingWithFrustumPlanes(ramses::Camera& ramsesCamera, std::string_view name) + { + m_errors.clear(); + return m_apiObjects->createRamsesCameraBinding(ramsesCamera, true, name); + } + + RamsesRenderPassBinding* LogicEngineImpl::createRamsesRenderPassBinding(ramses::RenderPass& ramsesRenderPass, std::string_view name) + { + m_errors.clear(); + return m_apiObjects->createRamsesRenderPassBinding(ramsesRenderPass, name); + } + + RamsesRenderGroupBinding* LogicEngineImpl::createRamsesRenderGroupBinding(ramses::RenderGroup& ramsesRenderGroup, const RamsesRenderGroupBindingElements& elements, std::string_view name) + { + m_errors.clear(); + + if (elements.m_impl->getElements().empty()) + { + m_errors.add("Cannot create RamsesRenderGroupBinding, there were no elements provided.", nullptr, EErrorType::Other); + return nullptr; + } + + for (const auto& element : elements.m_impl->getElements()) + { + bool isContained = false; + if (element.second->isOfType(ramses::ERamsesObjectType::MeshNode)) + { + isContained = ramsesRenderGroup.containsMeshNode(*ramses::RamsesUtils::TryConvert(*element.second)); + } + else if (element.second->isOfType(ramses::ERamsesObjectType::RenderGroup)) + { + isContained = ramsesRenderGroup.containsRenderGroup(*ramses::RamsesUtils::TryConvert(*element.second)); + } + + if (!isContained) + { + m_errors.add("Cannot create RamsesRenderGroupBinding, one or more of the provided elements is not contained in the RenderGroup to bind.", nullptr, EErrorType::Other); + return nullptr; + } + } + + return m_apiObjects->createRamsesRenderGroupBinding(ramsesRenderGroup, elements, name); + } + + RamsesMeshNodeBinding* LogicEngineImpl::createRamsesMeshNodeBinding(ramses::MeshNode& ramsesMeshNode, std::string_view name) + { + m_errors.clear(); + return m_apiObjects->createRamsesMeshNodeBinding(ramsesMeshNode, name); + } + + SkinBinding* LogicEngineImpl::createSkinBinding( + const std::vector& joints, + const std::vector& inverseBindMatrices, + RamsesAppearanceBinding& appearanceBinding, + const ramses::UniformInput& jointMatInput, + std::string_view name) + { + m_errors.clear(); + + if (joints.empty() || std::find(joints.cbegin(), joints.cend(), nullptr) != joints.cend()) + { + m_errors.add("Cannot create SkinBinding, no or null joint node bindings provided.", nullptr, EErrorType::Other); + return nullptr; + } + + if (joints.size() != inverseBindMatrices.size()) + { + m_errors.add("Cannot create SkinBinding, number of inverse matrices must match the number of joints.", nullptr, EErrorType::Other); + return nullptr; + } + + for (const auto nodeBinding : joints) + { + const auto& nodeBindings = m_apiObjects->getApiObjectContainer(); + if (std::find(nodeBindings.cbegin(), nodeBindings.cend(), nodeBinding) == nodeBindings.cend()) + { + m_errors.add(fmt::format("Failed to create SkinBinding '{}': one or more of the provided Ramses node bindings was not found in this logic instance.", name), nullptr, EErrorType::IllegalArgument); + return nullptr; + } + } + + const auto& appearanceBindings = m_apiObjects->getApiObjectContainer(); + if (std::find(appearanceBindings.cbegin(), appearanceBindings.cend(), &appearanceBinding) == appearanceBindings.cend()) + { + m_errors.add(fmt::format("Failed to create SkinBinding '{}': provided Ramses appearance binding was not found in this logic instance.", name), nullptr, EErrorType::IllegalArgument); + return nullptr; + } + + ramses::UniformInput actualUniformInput; + appearanceBinding.getRamsesAppearance().getEffect().findUniformInput(jointMatInput.getName(), actualUniformInput); + if (!actualUniformInput.isValid() || appearanceBinding.getRamsesAppearance().isInputBound(actualUniformInput)) + { + m_errors.add("Cannot create SkinBinding, provided uniform input must be pointing to valid uniform of the provided appearance's effect and must not be bound.", + nullptr, EErrorType::Other); + return nullptr; + } + + if (*actualUniformInput.getDataType() != ramses::EDataType::Matrix44F + || actualUniformInput.getElementCount() != joints.size()) + { + m_errors.add("Cannot create SkinBinding, provided uniform input must be of type array of Matrix4x4 with element count matching number of joints.", nullptr, EErrorType::Other); + return nullptr; + } + + std::vector jointsAsImpls; + jointsAsImpls.reserve(joints.size()); + for (const auto j : joints) + jointsAsImpls.push_back(&j->m_nodeBinding); + + return m_apiObjects->createSkinBinding(std::move(jointsAsImpls), inverseBindMatrices, appearanceBinding.m_appearanceBinding, actualUniformInput, name); + } + + template + DataArray* LogicEngineImpl::createDataArray(const std::vector& data, std::string_view name) + { + static_assert(CanPropertyTypeBeStoredInDataArray(PropertyTypeToEnum::TYPE)); + m_errors.clear(); + + if (data.empty()) + { + m_errors.add(fmt::format("Cannot create DataArray '{}' with empty data.", name), nullptr, EErrorType::IllegalArgument); + return nullptr; + } + + if constexpr (std::is_same_v>) + { + for (const auto& vec : data) + { + if (vec.size() != data.front().size()) + { + m_errors.add("Failed to create DataArray of float arrays: all arrays must be of same size.", nullptr, EErrorType::Other); + return nullptr; + } + } + } + + // NOLINTNEXTLINE(readability-misleading-indentation) for some reason clang is confused about constexpr branch above + return m_apiObjects->createDataArray(data, name); + } + + ramses::AnimationNode* LogicEngineImpl::createAnimationNode(const AnimationNodeConfig& config, std::string_view name) + { + m_errors.clear(); + + auto containsDataArray = [this](const DataArray* da) { + const auto& dataArrays = m_apiObjects->getApiObjectContainer(); + const auto it = std::find_if(dataArrays.cbegin(), dataArrays.cend(), + [da](const auto& d) { return d == da; }); + return it != dataArrays.cend(); + }; + + if (config.getChannels().empty()) + { + m_errors.add(fmt::format("Failed to create AnimationNode '{}': must provide at least one channel.", name), nullptr, EErrorType::IllegalArgument); + return nullptr; + } + + for (const auto& channel : config.getChannels()) + { + if (!containsDataArray(channel.timeStamps) || + !containsDataArray(channel.keyframes)) + { + m_errors.add(fmt::format("Failed to create AnimationNode '{}': timestamps or keyframes were not found in this logic instance.", name), nullptr, EErrorType::IllegalArgument); + return nullptr; + } + + if ((channel.tangentsIn && !containsDataArray(channel.tangentsIn)) || + (channel.tangentsOut && !containsDataArray(channel.tangentsOut))) + { + m_errors.add(fmt::format("Failed to create AnimationNode '{}': tangents were not found in this logic instance.", name), nullptr, EErrorType::IllegalArgument); + return nullptr; + } + } + + return m_apiObjects->createAnimationNode(*config.m_impl, name); + } + + TimerNode* LogicEngineImpl::createTimerNode(std::string_view name) + { + m_errors.clear(); + return m_apiObjects->createTimerNode(name); + } + + AnchorPoint* LogicEngineImpl::createAnchorPoint(RamsesNodeBinding& nodeBinding, RamsesCameraBinding& cameraBinding, std::string_view name) + { + m_errors.clear(); + + const auto& nodeBindings = m_apiObjects->getApiObjectContainer(); + const auto& cameraBindings = m_apiObjects->getApiObjectContainer(); + if (std::find(nodeBindings.cbegin(), nodeBindings.cend(), &nodeBinding) == nodeBindings.cend() || + std::find(cameraBindings.cbegin(), cameraBindings.cend(), &cameraBinding) == cameraBindings.cend()) + { + m_errors.add(fmt::format("Failed to create AnchorPoint '{}': provided Ramses node binding and/or camera binding were not found in this logic instance.", name), nullptr, EErrorType::IllegalArgument); + return nullptr; + } + + return m_apiObjects->createAnchorPoint(nodeBinding.m_nodeBinding, cameraBinding.m_cameraBinding, name); + } + + bool LogicEngineImpl::destroy(LogicObject& object) + { + m_errors.clear(); + return m_apiObjects->destroy(object, m_errors); + } + + bool LogicEngineImpl::isLinked(const LogicNode& logicNode) const + { + return m_apiObjects->getLogicNodeDependencies().isLinked(logicNode.m_impl); + } + + ramses::EFeatureLevel LogicEngineImpl::getFeatureLevel() const + { + return m_featureLevel; + } + + size_t LogicEngineImpl::activateLinksRecursive(PropertyImpl& output) + { + size_t activatedLinks = 0u; + + const auto childCount = output.getChildCount(); + for (size_t i = 0; i < childCount; ++i) + { + PropertyImpl& child = *output.getChild(i)->m_impl; + + if (TypeUtils::CanHaveChildren(child.getType())) + { + activatedLinks += activateLinksRecursive(child); + } + else + { + const auto& outgoingLinks = child.getOutgoingLinks(); + for (const auto& outLink : outgoingLinks) + { + PropertyImpl* linkedProp = outLink.property; + const bool valueChanged = linkedProp->setValue(child.getValue()); + if (valueChanged || linkedProp->getPropertySemantics() == EPropertySemantics::AnimationInput) + { + linkedProp->getLogicNode().setDirty(true); + ++activatedLinks; + } + } + } + } + + return activatedLinks; + } + + bool LogicEngineImpl::update() + { + m_errors.clear(); + + if (m_statisticsEnabled || m_updateReportEnabled) + { + m_updateReport.clear(); + m_updateReport.sectionStarted(UpdateReport::ETimingSection::TotalUpdate); + } + if (m_updateReportEnabled) + { + m_updateReport.sectionStarted(UpdateReport::ETimingSection::TopologySort); + } + + const std::optional& sortedNodes = m_apiObjects->getLogicNodeDependencies().getTopologicallySortedNodes(); + if (!sortedNodes) + { + m_errors.add("Failed to sort logic nodes based on links between their properties. Create a loop-free link graph before calling update()!", nullptr, EErrorType::ContentStateError); + return false; + } + + if (m_updateReportEnabled) + m_updateReport.sectionFinished(UpdateReport::ETimingSection::TopologySort); + + // force dirty all timer nodes, anchor points and skinbindings + setNodeToBeAlwaysUpdatedDirty(); + + const bool success = updateNodes(*sortedNodes); + + if (m_statisticsEnabled || m_updateReportEnabled) + { + m_updateReport.sectionFinished(UpdateReport::ETimingSection::TotalUpdate); + m_statistics.collect(m_updateReport, sortedNodes->size()); + if (m_statistics.checkUpdateFrameFinished()) + m_statistics.calculateAndLog(); + } + + return success; + } + + bool LogicEngineImpl::updateNodes(const NodeVector& sortedNodes) + { + for (LogicNodeImpl* nodeIter : sortedNodes) + { + LogicNodeImpl& node = *nodeIter; + + if (!node.isDirty()) + { + if (m_updateReportEnabled) + m_updateReport.nodeSkippedExecution(node); + + if(m_nodeDirtyMechanismEnabled) + continue; + } + + if (m_updateReportEnabled) + m_updateReport.nodeExecutionStarted(node); + if (m_statisticsEnabled) + m_statistics.nodeExecuted(); + + const std::optional potentialError = node.update(); + if (potentialError) + { + m_errors.add(potentialError->message, &node.getLogicObject(), EErrorType::RuntimeError); + return false; + } + + Property* outputs = node.getOutputs(); + if (outputs != nullptr) + { + const size_t activatedLinks = activateLinksRecursive(*outputs->m_impl); + + if (m_statisticsEnabled || m_updateReportEnabled) + m_updateReport.linksActivated(activatedLinks); + } + + if (m_updateReportEnabled) + m_updateReport.nodeExecutionFinished(); + + node.setDirty(false); + } + + return true; + } + + void LogicEngineImpl::setNodeToBeAlwaysUpdatedDirty() + { + // force timer nodes dirty so they can update their ticker + for (TimerNode* timerNode : m_apiObjects->getApiObjectContainer()) + timerNode->m_impl.setDirty(true); + // force anchor points dirty because they depend on set of ramses states which cannot be monitored + for (AnchorPoint* anchorPoint : m_apiObjects->getApiObjectContainer()) + anchorPoint->m_impl.setDirty(true); + // force skinbindings dirty because they depend on set of ramses states which cannot be monitored + for (SkinBinding* skinBinding : m_apiObjects->getApiObjectContainer()) + skinBinding->m_impl.setDirty(true); + } + + const std::vector& LogicEngineImpl::getErrors() const + { + return m_errors.getErrors(); + } + + const std::vector& LogicEngineImpl::validate() const + { + m_validationResults.clear(); + + if (m_apiObjects->bindingsDirty()) + m_validationResults.add("Saving logic engine content with manually updated binding values without calling update() will result in those values being lost!", nullptr, EWarningType::UnsafeDataState); + + m_apiObjects->validateInterfaces(m_validationResults); + m_apiObjects->validateDanglingNodes(m_validationResults); + + return m_validationResults.getWarnings(); + } + + bool LogicEngineImpl::CheckRamsesVersionFromFile(const rlogic_serialization::Version& ramsesVersion) + { + // Only major version changes result in file incompatibilities + return static_cast(ramsesVersion.v_major()) == ramses::GetRamsesVersion().major; + } + + bool LogicEngineImpl::loadFromBuffer(const void* rawBuffer, size_t bufferSize, ramses::Scene* scene, bool enableMemoryVerification) + { + return loadFromByteData(rawBuffer, bufferSize, scene, enableMemoryVerification, fmt::format("data buffer '{}' (size: {})", rawBuffer, bufferSize)); + } + + bool LogicEngineImpl::loadFromFile(std::string_view filename, ramses::Scene* scene, bool enableMemoryVerification) + { + std::optional> maybeBytesFromFile = FileUtils::LoadBinary(std::string(filename)); + if (!maybeBytesFromFile) + { + m_errors.add(fmt::format("Failed to load file '{}'", filename), nullptr, EErrorType::BinaryDataAccessError); + return false; + } + + const size_t fileSize = (*maybeBytesFromFile).size(); + return loadFromByteData((*maybeBytesFromFile).data(), fileSize, scene, enableMemoryVerification, fmt::format("file '{}' (size: {})", filename, fileSize)); + } + + bool LogicEngineImpl::loadFromFileDescriptor(int fd, size_t offset, size_t size, ramses::Scene* scene, bool enableMemoryVerification) + { + if (fd <= 0) + { + m_errors.add(fmt::format("Invalid file descriptor: {}", fd), nullptr, EErrorType::BinaryDataAccessError); + return false; + } + if (size == 0) + { + m_errors.add("Failed to load from file descriptor: size may not be 0", nullptr, EErrorType::BinaryDataAccessError); + return false; + } + std::optional> maybeBytesFromFile = FileUtils::LoadBinary(fd, offset, size); + if (!maybeBytesFromFile) + { + m_errors.add(fmt::format("Failed to load from file descriptor: fd: {} offset: {} size: {}", fd, offset, size), nullptr, EErrorType::BinaryDataAccessError); + return false; + } + return loadFromByteData((*maybeBytesFromFile).data(), size, scene, enableMemoryVerification, fmt::format("fd: {} (offset: {}, size: {})", fd, offset, size)); + } + + bool LogicEngineImpl::checkFileIdentifierBytes(const std::string& dataSourceDescription, const std::string& fileIdBytes) + { + const std::string expected = rlogic_serialization::LogicEngineIdentifier(); + if (expected.substr(0, 2) != fileIdBytes.substr(0, 2)) + { + m_errors.add(fmt::format("{}: Tried loading a binary data which doesn't store Ramses Logic content! Expected file bytes 4-5 to be '{}', but found '{}' instead", + dataSourceDescription, + expected.substr(0, 2), + fileIdBytes.substr(0, 2) + ), nullptr, EErrorType::BinaryDataAccessError); + return false; + } + + if (expected.substr(2, 2) != fileIdBytes.substr(2, 2)) + { + m_errors.add(fmt::format("{}: Version mismatch while loading binary data! Expected version '{}', but found '{}'", + dataSourceDescription, + expected.substr(2, 2), + fileIdBytes.substr(2, 2) + ),nullptr, EErrorType::BinaryVersionMismatch); + return false; + } + + return true; + } + + bool LogicEngineImpl::loadFromByteData(const void* byteData, size_t byteSize, ramses::Scene* scene, bool enableMemoryVerification, const std::string& dataSourceDescription) + { + m_errors.clear(); + + if (byteSize < 8) + { + m_errors.add(fmt::format("{} contains corrupted data! Data should be at least 8 bytes", dataSourceDescription), nullptr, EErrorType::BinaryDataAccessError); + return false; + } + + auto* char8Data(static_cast(byteData)); + // file identifier bytes are always placed at bytes 4-7 in the buffer + const std::string fileIdBytes(&char8Data[4], 4); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) No better options + if (!checkFileIdentifierBytes(dataSourceDescription, fileIdBytes)) + { + return false; + } + + auto* uint8Data(static_cast(byteData)); + if (enableMemoryVerification) + { + flatbuffers::Verifier bufferVerifier(uint8Data, byteSize); + const bool bufferOK = bufferVerifier.VerifyBuffer(rlogic_serialization::LogicEngineIdentifier()); + + if (!bufferOK) + { + m_errors.add(fmt::format("{} contains corrupted data!", dataSourceDescription), nullptr, EErrorType::BinaryDataAccessError); + return false; + } + } + + const auto* logicEngine = rlogic_serialization::GetLogicEngine(byteData); + + if (nullptr == logicEngine) + { + m_errors.add(fmt::format("{} doesn't contain logic engine data with readable version specifiers", dataSourceDescription), nullptr, EErrorType::BinaryVersionMismatch); + return false; + } + + const auto& ramsesVersion = *logicEngine->ramsesVersion(); + const auto& rlogicVersion = *logicEngine->rlogicVersion(); + + LOG_INFO("Loading logic engine content from '{}' which was exported with Ramses {} and Logic Engine {}", dataSourceDescription, ramsesVersion.v_string()->string_view(), rlogicVersion.v_string()->string_view()); + + if (!CheckRamsesVersionFromFile(ramsesVersion)) + { + m_errors.add(fmt::format("Version mismatch while loading {}! Expected Ramses version {}.x.x but found {}", + dataSourceDescription, ramses::GetRamsesVersion().major, + ramsesVersion.v_string()->string_view()), nullptr, EErrorType::BinaryVersionMismatch); + return false; + } + + const auto featureLevel = static_cast(logicEngine->featureLevel()); + if (featureLevel != m_featureLevel) + { + m_errors.add(fmt::format("Feature level mismatch while loading {}! Loaded file with feature level {} but LogicEngine was instantiated with feature level {}", + dataSourceDescription, featureLevel, m_featureLevel), nullptr, EErrorType::BinaryVersionMismatch); + return false; + } + + if (nullptr == logicEngine->apiObjects()) + { + m_errors.add(fmt::format("Fatal error while loading {}: doesn't contain API objects!", dataSourceDescription), nullptr, EErrorType::BinaryVersionMismatch); + return false; + } + + if (logicEngine->assetMetadata()) + { + LogAssetMetadata(*logicEngine->assetMetadata()); + } + + std::unique_ptr ramsesResolver; + if (scene != nullptr) + ramsesResolver = std::make_unique(m_errors, *scene); + + std::unique_ptr deserializedObjects = ApiObjects::Deserialize(*logicEngine->apiObjects(), ramsesResolver.get(), dataSourceDescription, m_errors, m_featureLevel); + + if (!deserializedObjects) + { + return false; + } + + // No errors -> move data into member + m_apiObjects = std::move(deserializedObjects); + + return true; + } + + bool LogicEngineImpl::GetFeatureLevelFromFile(std::string_view filename, ramses::EFeatureLevel& detectedFeatureLevel) + { + std::optional> maybeBytesFromFile = FileUtils::LoadBinary(std::string(filename)); + if (!maybeBytesFromFile) + { + LOG_ERROR("Failed to load file '{}'", filename); + return false; + } + + return GetFeatureLevelFromBuffer(filename, maybeBytesFromFile->data(), maybeBytesFromFile->size(), detectedFeatureLevel); + } + + bool LogicEngineImpl::GetFeatureLevelFromBuffer(std::string_view logname, const void* buffer, size_t bufferSize, ramses::EFeatureLevel& detectedFeatureLevel) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) Safe here, not worth transforming whole vector + flatbuffers::Verifier bufferVerifier(reinterpret_cast(buffer), bufferSize); + if (!bufferVerifier.VerifyBuffer()) + { + LOG_ERROR("'{}' contains corrupted data", logname); + return false; + } + + const auto* logicEngine = rlogic_serialization::GetLogicEngine(buffer); + if (!logicEngine) + { + LOG_ERROR("File '{}' could not be parsed", logname); + return false; + } + + const uint32_t featureLevelInt = logicEngine->featureLevel(); + if (std::find(ramses::AllFeatureLevels.cbegin(), ramses::AllFeatureLevels.cend(), featureLevelInt) == ramses::AllFeatureLevels.cend()) + { + LOG_ERROR("Could not recognize feature level in file '{}'", logname); + return false; + } + + detectedFeatureLevel = static_cast(featureLevelInt); + return true; + } + + bool LogicEngineImpl::saveToFile(std::string_view filename, const SaveFileConfigImpl& config) + { + m_errors.clear(); + + if (!m_apiObjects->checkBindingsReferToSameRamsesScene(m_errors)) + { + m_errors.add("Can't save a logic engine to file while it has references to more than one Ramses scene!", nullptr, EErrorType::ContentStateError); + return false; + } + + // Refuse save() if logic graph has loops + if (!m_apiObjects->getLogicNodeDependencies().getTopologicallySortedNodes()) + { + m_errors.add("Failed to sort logic nodes based on links between their properties. Create a loop-free link graph before calling saveToFile()!", nullptr, EErrorType::ContentStateError); + return false; + } + + if (config.getValidationEnabled()) + { + const std::vector& warnings = validate(); + + if (!warnings.empty()) + { + m_errors.add( + "Failed to saveToFile() because validation warnings were encountered! " + "Refer to the documentation of saveToFile() for details how to address these gracefully.", nullptr, EErrorType::ContentStateError); + return false; + } + } + + const auto& scripts = m_apiObjects->getApiObjectContainer(); + const auto sIt = std::find_if(scripts.cbegin(), scripts.cend(), [](const LuaScript* s) { return s->m_script.hasDebugLogFunctions(); }); + if (sIt != scripts.cend()) + { + m_errors.add(fmt::format("Cannot save to file, Lua script '{}' has enabled debug log functions, remove this script before saving.", (*sIt)->m_impl.getIdentificationString()), *sIt, EErrorType::ContentStateError); + return false; + } + const auto& modules = m_apiObjects->getApiObjectContainer(); + const auto mIt = std::find_if(modules.cbegin(), modules.cend(), [](const LuaModule* m) { return m->m_impl.hasDebugLogFunctions(); }); + if (mIt != modules.cend()) + { + m_errors.add(fmt::format("Cannot save to file, Lua module '{}' has enabled debug log functions, remove this module before saving.", (*mIt)->m_impl.getIdentificationString()), *mIt, EErrorType::ContentStateError); + return false; + } + + flatbuffers::FlatBufferBuilder builder; + ramses::RamsesVersion ramsesVersion = ramses::GetRamsesVersion(); + + const auto ramsesVersionOffset = rlogic_serialization::CreateVersion(builder, + ramsesVersion.major, + ramsesVersion.minor, + ramsesVersion.patch, + builder.CreateString(ramsesVersion.string)); + builder.Finish(ramsesVersionOffset); + + const auto ramsesLogicVersionOffset = rlogic_serialization::CreateVersion(builder, + ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MAJOR_INT, + ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MINOR_INT, + ramses_sdk::RAMSES_SDK_PROJECT_VERSION_PATCH_INT, + builder.CreateString(ramses_sdk::RAMSES_SDK_RAMSES_VERSION)); + builder.Finish(ramsesLogicVersionOffset); + + const auto exporterVersionOffset = rlogic_serialization::CreateVersion(builder, + config.getExporterMajorVersion(), + config.getExporterMinorVersion(), + config.getExporterPatchVersion(), + builder.CreateString("")); + builder.Finish(exporterVersionOffset); + + const auto assetMetadataOffset = rlogic_serialization::CreateMetadata(builder, + builder.CreateString(config.getMetadataString()), + exporterVersionOffset, + config.getExporterFileFormatVersion()); + builder.Finish(assetMetadataOffset); + + const auto logicEngine = rlogic_serialization::CreateLogicEngine(builder, + ramsesVersionOffset, + ramsesLogicVersionOffset, + ApiObjects::Serialize(*m_apiObjects, builder, config.getLuaSavingMode()), + assetMetadataOffset, + m_featureLevel); + + builder.Finish(logicEngine, rlogic_serialization::LogicEngineIdentifier()); + + if (!FileUtils::SaveBinary(std::string(filename), builder.GetBufferPointer(), builder.GetSize())) + { + m_errors.add(fmt::format("Failed to save content to path '{}'!", filename), nullptr, EErrorType::BinaryDataAccessError); + return false; + } + + LOG_INFO("Saved logic engine to file: '{}'.", filename); + + return true; + } + + void LogicEngineImpl::LogAssetMetadata(const rlogic_serialization::Metadata& assetMetadata) + { + const std::string_view metadataString = assetMetadata.metadataString() ? assetMetadata.metadataString()->string_view() : "none"; + LOG_INFO("Logic Engine content metadata: '{}'", metadataString); + const std::string exporterVersion = assetMetadata.exporterVersion() ? + fmt::format("{}.{}.{} (file format version {})", + assetMetadata.exporterVersion()->v_major(), + assetMetadata.exporterVersion()->v_minor(), + assetMetadata.exporterVersion()->v_patch(), + assetMetadata.exporterFileVersion()) : "undefined"; + LOG_INFO("Exporter version: {}", exporterVersion); + } + + bool LogicEngineImpl::link(const Property& sourceProperty, const Property& targetProperty) + { + m_errors.clear(); + + return m_apiObjects->getLogicNodeDependencies().link(*sourceProperty.m_impl, *targetProperty.m_impl, false, m_errors); + } + + bool LogicEngineImpl::linkWeak(const Property& sourceProperty, const Property& targetProperty) + { + m_errors.clear(); + + return m_apiObjects->getLogicNodeDependencies().link(*sourceProperty.m_impl, *targetProperty.m_impl, true, m_errors); + } + + bool LogicEngineImpl::unlink(const Property& sourceProperty, const Property& targetProperty) + { + m_errors.clear(); + + return m_apiObjects->getLogicNodeDependencies().unlink(*sourceProperty.m_impl, *targetProperty.m_impl, m_errors); + } + + ApiObjects& LogicEngineImpl::getApiObjects() + { + return *m_apiObjects; + } + + void LogicEngineImpl::disableTrackingDirtyNodes() + { + m_nodeDirtyMechanismEnabled = false; + } + + void LogicEngineImpl::enableUpdateReport(bool enable) + { + m_updateReportEnabled = enable; + if (!m_updateReportEnabled) + m_updateReport.clear(); + } + + LogicEngineReport LogicEngineImpl::getLastUpdateReport() const + { + return LogicEngineReport{ std::make_unique(m_updateReport) }; + } + + void LogicEngineImpl::setStatisticsLoggingRate(size_t loggingRate) + { + m_statistics.setLoggingRate(loggingRate); + m_statisticsEnabled = (loggingRate != 0u); + } + + void LogicEngineImpl::setStatisticsLogLevel(ELogLevel logLevel) + { + m_statistics.setLogLevel(logLevel); + } + + size_t LogicEngineImpl::getTotalSerializedSize(ELuaSavingMode luaSavingMode) const + { + return ApiObjectsSerializedSize::GetTotalSerializedSize(*m_apiObjects, luaSavingMode); + } + + template DataArray* LogicEngineImpl::createDataArray(const std::vector&, std::string_view name); + template DataArray* LogicEngineImpl::createDataArray(const std::vector&, std::string_view name); + template DataArray* LogicEngineImpl::createDataArray(const std::vector&, std::string_view name); + template DataArray* LogicEngineImpl::createDataArray(const std::vector&, std::string_view name); + template DataArray* LogicEngineImpl::createDataArray(const std::vector&, std::string_view name); + template DataArray* LogicEngineImpl::createDataArray(const std::vector&, std::string_view name); + template DataArray* LogicEngineImpl::createDataArray(const std::vector&, std::string_view name); + template DataArray* LogicEngineImpl::createDataArray(const std::vector&, std::string_view name); + template DataArray* LogicEngineImpl::createDataArray>(const std::vector>&, std::string_view name); +} diff --git a/client/logic/lib/impl/LogicEngineImpl.h b/client/logic/lib/impl/LogicEngineImpl.h new file mode 100644 index 000000000..b61e165f9 --- /dev/null +++ b/client/logic/lib/impl/LogicEngineImpl.h @@ -0,0 +1,187 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/AnimationTypes.h" +#include "ramses-logic/LogicEngineReport.h" +#include "ramses-framework-api/DataTypes.h" +#include "ramses-framework-api/EFeatureLevel.h" +#include "internals/ApiObjects.h" +#include "internals/LogicNodeDependencies.h" +#include "internals/ErrorReporting.h" +#include "internals/ValidationResults.h" +#include "internals/UpdateReport.h" +#include "internals/LogicNodeUpdateStatistics.h" +#include "internals/ApiObjectsSerializedSize.h" + +#include "ramses-framework-api/RamsesFrameworkTypes.h" +#include "ramses-client-api/ERotationType.h" + +#include +#include +#include +#include + +namespace ramses +{ + class Scene; + class SceneObject; + class Node; + class Appearance; + class Camera; + class RenderPass; + class RenderGroup; + class UniformInput; + class MeshNode; +} + +namespace ramses +{ + class RamsesNodeBinding; + class RamsesAppearanceBinding; + class RamsesCameraBinding; + class RamsesRenderPassBinding; + class RamsesRenderGroupBinding; + class RamsesRenderGroupBindingElements; + class RamsesMeshNodeBinding; + class SkinBinding; + class DataArray; + class AnimationNode; + class AnimationNodeConfig; + class TimerNode; + class AnchorPoint; + class LuaScript; + class LuaInterface; + class LuaModule; + class LogicNode; + class Property; + enum class ELogLevel; +} + +namespace rlogic_serialization +{ + struct Version; + struct Metadata; +} + +namespace ramses::internal +{ + class LuaConfigImpl; + class SaveFileConfigImpl; + class LogicNodeImpl; + class RamsesBindingImpl; + class ApiObjects; + + class LogicEngineImpl + { + public: + // Move-able (noexcept); Not copy-able + + // can't be noexcept anymore because constructor of std::unordered_map can throw + explicit LogicEngineImpl(ramses::EFeatureLevel featureLevel); + ~LogicEngineImpl() noexcept; + + LogicEngineImpl(LogicEngineImpl&& other) noexcept = default; + LogicEngineImpl& operator=(LogicEngineImpl&& other) noexcept = default; + LogicEngineImpl(const LogicEngineImpl& other) = delete; + LogicEngineImpl& operator=(const LogicEngineImpl& other) = delete; + + // Public API + LuaScript* createLuaScript(std::string_view source, const LuaConfigImpl& config, std::string_view scriptName); + LuaInterface* createLuaInterface(std::string_view source, const LuaConfigImpl& config, std::string_view interfaceName, bool verifyModules); + LuaModule* createLuaModule(std::string_view source, const LuaConfigImpl& config, std::string_view moduleName); + bool extractLuaDependencies(std::string_view source, const std::function& callbackFunc); + RamsesNodeBinding* createRamsesNodeBinding(ramses::Node& ramsesNode, ramses::ERotationType rotationType, std::string_view name); + RamsesAppearanceBinding* createRamsesAppearanceBinding(ramses::Appearance& ramsesAppearance, std::string_view name); + RamsesCameraBinding* createRamsesCameraBinding(ramses::Camera& ramsesCamera, std::string_view name); + RamsesCameraBinding* createRamsesCameraBindingWithFrustumPlanes(ramses::Camera& ramsesCamera, std::string_view name); + RamsesRenderPassBinding* createRamsesRenderPassBinding(ramses::RenderPass& ramsesRenderPass, std::string_view name); + RamsesRenderGroupBinding* createRamsesRenderGroupBinding(ramses::RenderGroup& ramsesRenderGroup, const RamsesRenderGroupBindingElements& elements, std::string_view name); + RamsesMeshNodeBinding* createRamsesMeshNodeBinding(ramses::MeshNode& ramsesMeshNode, std::string_view name); + SkinBinding* createSkinBinding( + const std::vector& joints, + const std::vector& inverseBindMatrices, + RamsesAppearanceBinding& appearanceBinding, + const ramses::UniformInput& jointMatInput, + std::string_view name); + template + DataArray* createDataArray(const std::vector& data, std::string_view name); + AnimationNode* createAnimationNode(const AnimationNodeConfig& config, std::string_view name); + TimerNode* createTimerNode(std::string_view name); + AnchorPoint* createAnchorPoint(RamsesNodeBinding& nodeBinding, RamsesCameraBinding& cameraBinding, std::string_view name); + + bool destroy(LogicObject& object); + + bool update(); + + [[nodiscard]] const std::vector& getErrors() const; + const std::vector& validate() const; + + bool loadFromFile(std::string_view filename, ramses::Scene* scene, bool enableMemoryVerification); + bool loadFromFileDescriptor(int fd, size_t offset, size_t size, ramses::Scene* scene, bool enableMemoryVerification); + bool loadFromBuffer(const void* rawBuffer, size_t bufferSize, ramses::Scene* scene, bool enableMemoryVerification); + bool saveToFile(std::string_view filename, const SaveFileConfigImpl& config); + [[nodiscard]] static bool GetFeatureLevelFromFile(std::string_view filename, ramses::EFeatureLevel& detectedFeatureLevel); + [[nodiscard]] static bool GetFeatureLevelFromBuffer(std::string_view logname, const void* buffer, size_t bufferSize, ramses::EFeatureLevel& detectedFeatureLevel); + + bool link(const Property& sourceProperty, const Property& targetProperty); + bool linkWeak(const Property& sourceProperty, const Property& targetProperty); + bool unlink(const Property& sourceProperty, const Property& targetProperty); + + [[nodiscard]] bool isLinked(const LogicNode& logicNode) const; + + [[nodiscard]] ramses::EFeatureLevel getFeatureLevel() const; + + [[nodiscard]] ApiObjects& getApiObjects(); + + // for benchmarking purposes only + void disableTrackingDirtyNodes(); + + void enableUpdateReport(bool enable); + [[nodiscard]] LogicEngineReport getLastUpdateReport() const; + + void setStatisticsLoggingRate(size_t loggingRate); + void setStatisticsLogLevel(ELogLevel logLevel); + + [[nodiscard]] size_t getTotalSerializedSize(ELuaSavingMode luaSavingMode) const; + template + [[nodiscard]] size_t getSerializedSize(ELuaSavingMode luaSavingMode) const; + + private: + size_t activateLinksRecursive(PropertyImpl& output); + void setNodeToBeAlwaysUpdatedDirty(); + + static bool CheckRamsesVersionFromFile(const rlogic_serialization::Version& ramsesVersion); + + static void LogAssetMetadata(const rlogic_serialization::Metadata& assetMetadata); + + [[nodiscard]] bool updateNodes(const NodeVector& nodes); + + [[nodiscard]] bool loadFromByteData(const void* byteData, size_t byteSize, ramses::Scene* scene, bool enableMemoryVerification, const std::string& dataSourceDescription); + [[nodiscard]] bool checkFileIdentifierBytes(const std::string& dataSourceDescription, const std::string& fileIdBytes); + + std::unique_ptr m_apiObjects; + ErrorReporting m_errors; + mutable ValidationResults m_validationResults; + bool m_nodeDirtyMechanismEnabled = true; + + bool m_updateReportEnabled = false; + bool m_statisticsEnabled = true; + UpdateReport m_updateReport; + LogicNodeUpdateStatistics m_statistics; + + ramses::EFeatureLevel m_featureLevel; + }; + + template + size_t LogicEngineImpl::getSerializedSize(ELuaSavingMode luaSavingMode) const + { + return ApiObjectsSerializedSize::GetSerializedSize(*m_apiObjects, luaSavingMode); + } +} diff --git a/client/logic/lib/impl/LogicEngineReport.cpp b/client/logic/lib/impl/LogicEngineReport.cpp new file mode 100644 index 000000000..a9a37d00d --- /dev/null +++ b/client/logic/lib/impl/LogicEngineReport.cpp @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/LogicEngineReport.h" +#include "impl/LogicEngineReportImpl.h" + +namespace ramses +{ + LogicEngineReport::LogicEngineReport() noexcept + : m_impl{ std::make_unique() } + { + } + + LogicEngineReport::LogicEngineReport(std::unique_ptr impl) noexcept + : m_impl{ std::move(impl) } + { + } + + LogicEngineReport::LogicEngineReport(LogicEngineReport&& other) noexcept = default; + LogicEngineReport& LogicEngineReport::operator=(LogicEngineReport&& other) noexcept = default; + LogicEngineReport::~LogicEngineReport() = default; + + const std::vector& LogicEngineReport::getNodesExecuted() const + { + return m_impl->getNodesExecuted(); + } + + const std::vector& LogicEngineReport::getNodesSkippedExecution() const + { + return m_impl->getNodesSkippedExecution(); + } + + std::chrono::microseconds LogicEngineReport::getTopologySortExecutionTime() const + { + return m_impl->getTopologySortExecutionTime(); + } + + std::chrono::microseconds LogicEngineReport::getTotalUpdateExecutionTime() const + { + return m_impl->getTotalUpdateExecutionTime(); + } + + RAMSES_API size_t LogicEngineReport::getTotalLinkActivations() const + { + return m_impl->getTotalLinkActivations(); + } + +} diff --git a/client/logic/lib/impl/LogicEngineReportImpl.cpp b/client/logic/lib/impl/LogicEngineReportImpl.cpp new file mode 100644 index 000000000..128bec5f1 --- /dev/null +++ b/client/logic/lib/impl/LogicEngineReportImpl.cpp @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/LogicEngineReportImpl.h" +#include "LogicNodeImpl.h" + +namespace ramses::internal +{ + LogicEngineReportImpl::LogicEngineReportImpl() = default; + + LogicEngineReportImpl::LogicEngineReportImpl(const UpdateReport& reportData) + : m_totalUpdateExecutionTime{ reportData.getSectionExecutionTime(UpdateReport::ETimingSection::TotalUpdate) } + , m_topologySortExecutionTime{ reportData.getSectionExecutionTime(UpdateReport::ETimingSection::TopologySort) } + , m_activatedLinks{ reportData.getLinkActivations() } + { + m_nodesExecuted.reserve(reportData.getNodesExecuted().size()); + for (const auto& n : reportData.getNodesExecuted()) + m_nodesExecuted.push_back({ n.first->getLogicObject().as(), n.second }); + + m_nodesSkippedExecution.reserve(reportData.getNodesSkippedExecution().size()); + for (const auto& n : reportData.getNodesSkippedExecution()) + m_nodesSkippedExecution.push_back(n->getLogicObject().as()); + } + + const LogicEngineReportImpl::LogicNodesTimed& LogicEngineReportImpl::getNodesExecuted() const + { + return m_nodesExecuted; + } + + const LogicEngineReportImpl::LogicNodes& LogicEngineReportImpl::getNodesSkippedExecution() const + { + return m_nodesSkippedExecution; + } + + std::chrono::microseconds LogicEngineReportImpl::getTopologySortExecutionTime() const + { + return m_topologySortExecutionTime; + } + + std::chrono::microseconds LogicEngineReportImpl::getTotalUpdateExecutionTime() const + { + return m_totalUpdateExecutionTime; + } + + size_t LogicEngineReportImpl::getTotalLinkActivations() const + { + return m_activatedLinks; + } + +} diff --git a/client/logic/lib/impl/LogicEngineReportImpl.h b/client/logic/lib/impl/LogicEngineReportImpl.h new file mode 100644 index 000000000..feb7fe674 --- /dev/null +++ b/client/logic/lib/impl/LogicEngineReportImpl.h @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicNode.h" +#include "internals/UpdateReport.h" + +namespace ramses::internal +{ + class LogicEngineReportImpl + { + public: + using LogicNodesTimed = std::vector>; + using LogicNodes = std::vector; + + LogicEngineReportImpl(); + explicit LogicEngineReportImpl(const UpdateReport& reportData); + + [[nodiscard]] const LogicNodesTimed& getNodesExecuted() const; + [[nodiscard]] const LogicNodes& getNodesSkippedExecution() const; + [[nodiscard]] std::chrono::microseconds getTopologySortExecutionTime() const; + [[nodiscard]] std::chrono::microseconds getTotalUpdateExecutionTime() const; + [[nodiscard]] size_t getTotalLinkActivations() const; + + private: + LogicNodesTimed m_nodesExecuted; + LogicNodes m_nodesSkippedExecution; + UpdateReport::ReportTimeUnits m_totalUpdateExecutionTime{ 0 }; + UpdateReport::ReportTimeUnits m_topologySortExecutionTime{ 0 }; + size_t m_activatedLinks = 0u; + }; +} diff --git a/client/logic/lib/impl/LogicNode.cpp b/client/logic/lib/impl/LogicNode.cpp new file mode 100644 index 000000000..8350627c5 --- /dev/null +++ b/client/logic/lib/impl/LogicNode.cpp @@ -0,0 +1,35 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/LogicNode.h" +#include "impl/LogicNodeImpl.h" + +namespace ramses +{ + LogicNode::LogicNode(std::unique_ptr impl) noexcept + : LogicObject(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_impl{ static_cast(*LogicObject::m_impl) } + { + } + + Property* LogicNode::getInputs() + { + return m_impl.getInputs(); + } + + const Property* LogicNode::getInputs() const + { + return m_impl.getInputs(); + } + + const Property* LogicNode::getOutputs() const + { + return m_impl.getOutputs(); + } +} diff --git a/client/logic/lib/impl/LogicNodeImpl.cpp b/client/logic/lib/impl/LogicNodeImpl.cpp new file mode 100644 index 000000000..c7a2390b3 --- /dev/null +++ b/client/logic/lib/impl/LogicNodeImpl.cpp @@ -0,0 +1,71 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/LogicNodeImpl.h" + +#include "ramses-logic/Property.h" + +#include "impl/PropertyImpl.h" + +namespace ramses::internal +{ + LogicNodeImpl::LogicNodeImpl(std::string_view name, uint64_t id) noexcept + : LogicObjectImpl(name, id) + { + } + + LogicNodeImpl::~LogicNodeImpl() noexcept = default; + + Property* LogicNodeImpl::getInputs() + { + return m_inputs.get(); + } + + const Property* LogicNodeImpl::getInputs() const + { + return m_inputs.get(); + } + + const Property* LogicNodeImpl::getOutputs() const + { + return m_outputs.get(); + } + + Property* LogicNodeImpl::getOutputs() + { + return m_outputs.get(); + } + + void LogicNodeImpl::setDirty(bool dirty) + { + m_dirty = dirty; + } + + bool LogicNodeImpl::isDirty() const + { + return m_dirty; + } + + void LogicNodeImpl::setRootProperties(std::unique_ptr rootInput, std::unique_ptr rootOutput) + { + assert(!m_inputs); + assert(!m_outputs); + + if (rootInput) + { + m_inputs = PropertyImpl::CreateProperty(std::move(rootInput)); + m_inputs->m_impl->setLogicNode(*this); + } + + if (rootOutput) + { + m_outputs = PropertyImpl::CreateProperty(std::move(rootOutput)); + m_outputs->m_impl->setLogicNode(*this); + } + } +} diff --git a/client/logic/lib/impl/LogicNodeImpl.h b/client/logic/lib/impl/LogicNodeImpl.h new file mode 100644 index 000000000..bfaf7c0b7 --- /dev/null +++ b/client/logic/lib/impl/LogicNodeImpl.h @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + + +#pragma once + +#include "impl/LogicObjectImpl.h" +#include "impl/PropertyImpl.h" +#include +#include +#include +#include + +namespace ramses::internal +{ + struct LogicNodeRuntimeError { std::string message; }; + + class LogicNodeImpl : public LogicObjectImpl + { + public: + explicit LogicNodeImpl(std::string_view name, uint64_t id) noexcept; + LogicNodeImpl(const LogicNodeImpl& other) = delete; + LogicNodeImpl& operator=(const LogicNodeImpl& other) = delete; + ~LogicNodeImpl() noexcept override; + + [[nodiscard]] Property* getInputs(); + [[nodiscard]] const Property* getInputs() const; + + // Virtual because of LuaInterface + [[nodiscard]] virtual Property* getOutputs(); + [[nodiscard]] virtual const Property* getOutputs() const; + + virtual void createRootProperties() = 0; + virtual std::optional update() = 0; + + void setDirty(bool dirty); + [[nodiscard]] bool isDirty() const; + + protected: + void setRootProperties(std::unique_ptr rootInput, std::unique_ptr rootOutput); + + private: + PropertyUniquePtr m_inputs; + PropertyUniquePtr m_outputs; + + // Dirty after creation (every node gets executed at least once after creation) + bool m_dirty = true; + }; +} diff --git a/client/logic/lib/impl/LogicObject.cpp b/client/logic/lib/impl/LogicObject.cpp new file mode 100644 index 000000000..827383017 --- /dev/null +++ b/client/logic/lib/impl/LogicObject.cpp @@ -0,0 +1,107 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/LogicObject.h" +#include "ramses-logic/LuaModule.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/SkinBinding.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/AnchorPoint.h" +#include "impl/LogicObjectImpl.h" + +namespace ramses +{ + LogicObject::LogicObject(std::unique_ptr impl) noexcept + : m_impl{ std::move(impl) } + { + } + + LogicObject::~LogicObject() noexcept = default; + + std::string_view LogicObject::getName() const + { + return m_impl->getName(); + } + + uint64_t LogicObject::getId() const + { + return m_impl->getId(); + } + + bool LogicObject::setUserId(uint64_t highId, uint64_t lowId) + { + return m_impl->setUserId(highId, lowId); + } + + std::pair LogicObject::getUserId() const + { + return m_impl->getUserId(); + } + + template + const T* LogicObject::internalCast() const + { + return dynamic_cast(this); + } + + template + T* LogicObject::internalCast() + { + return dynamic_cast(this); + } + + bool LogicObject::setName(std::string_view name) + { + return m_impl->setName(name); + } + + template RAMSES_API const LogicObject* LogicObject::internalCast() const; + template RAMSES_API const LogicNode* LogicObject::internalCast() const; + template RAMSES_API const RamsesBinding* LogicObject::internalCast() const; + template RAMSES_API const LuaModule* LogicObject::internalCast() const; + template RAMSES_API const LuaScript* LogicObject::internalCast() const; + template RAMSES_API const LuaInterface* LogicObject::internalCast() const; + template RAMSES_API const RamsesNodeBinding* LogicObject::internalCast() const; + template RAMSES_API const RamsesAppearanceBinding* LogicObject::internalCast() const; + template RAMSES_API const RamsesCameraBinding* LogicObject::internalCast() const; + template RAMSES_API const RamsesRenderPassBinding* LogicObject::internalCast() const; + template RAMSES_API const RamsesRenderGroupBinding* LogicObject::internalCast() const; + template RAMSES_API const RamsesMeshNodeBinding* LogicObject::internalCast() const; + template RAMSES_API const SkinBinding* LogicObject::internalCast() const; + template RAMSES_API const DataArray* LogicObject::internalCast() const; + template RAMSES_API const AnimationNode* LogicObject::internalCast() const; + template RAMSES_API const TimerNode* LogicObject::internalCast() const; + template RAMSES_API const AnchorPoint* LogicObject::internalCast() const; + + template RAMSES_API LogicObject* LogicObject::internalCast(); + template RAMSES_API LogicNode* LogicObject::internalCast(); + template RAMSES_API RamsesBinding* LogicObject::internalCast(); + template RAMSES_API LuaModule* LogicObject::internalCast(); + template RAMSES_API LuaScript* LogicObject::internalCast(); + template RAMSES_API LuaInterface* LogicObject::internalCast(); + template RAMSES_API RamsesNodeBinding* LogicObject::internalCast(); + template RAMSES_API RamsesAppearanceBinding* LogicObject::internalCast(); + template RAMSES_API RamsesCameraBinding* LogicObject::internalCast(); + template RAMSES_API RamsesRenderPassBinding* LogicObject::internalCast(); + template RAMSES_API RamsesRenderGroupBinding* LogicObject::internalCast(); + template RAMSES_API RamsesMeshNodeBinding* LogicObject::internalCast(); + template RAMSES_API SkinBinding* LogicObject::internalCast(); + template RAMSES_API DataArray* LogicObject::internalCast(); + template RAMSES_API AnimationNode* LogicObject::internalCast(); + template RAMSES_API TimerNode* LogicObject::internalCast(); + template RAMSES_API AnchorPoint* LogicObject::internalCast(); +} diff --git a/client/logic/lib/impl/LogicObjectImpl.cpp b/client/logic/lib/impl/LogicObjectImpl.cpp new file mode 100644 index 000000000..c9fb140f8 --- /dev/null +++ b/client/logic/lib/impl/LogicObjectImpl.cpp @@ -0,0 +1,121 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/LogicObjectImpl.h" +#include "ramses-logic/LuaInterface.h" +#include "impl/LoggerImpl.h" +#include "internals/ErrorReporting.h" +#include "flatbuffers/flatbuffers.h" +#include "generated/LogicObjectGen.h" + +namespace ramses::internal +{ + LogicObjectImpl::LogicObjectImpl(std::string_view name, uint64_t id) noexcept + : m_name(name) + , m_id(id) + { + } + + LogicObjectImpl::~LogicObjectImpl() noexcept = default; + + std::string_view LogicObjectImpl::getName() const + { + return m_name; + } + + bool LogicObjectImpl::setName(std::string_view name) + { + m_name = name; + return true; + } + + uint64_t LogicObjectImpl::getId() const + { + return m_id; + } + + bool LogicObjectImpl::setUserId(uint64_t highId, uint64_t lowId) + { + m_userId = { highId, lowId }; + return true; + } + + std::pair LogicObjectImpl::getUserId() const + { + return m_userId; + } + + std::string LogicObjectImpl::getIdentificationString() const + { + if (m_userId.first != 0u || m_userId.second != 0u) + return fmt::format("{} [Id={} UserId={:016X}{:016X}]", m_name, m_id, m_userId.first, m_userId.second); + + return fmt::format("{} [Id={}]", m_name, m_id); + } + + flatbuffers::Offset LogicObjectImpl::Serialize(const LogicObjectImpl& object, flatbuffers::FlatBufferBuilder& builder) + { + return rlogic_serialization::CreateLogicObject(builder, + builder.CreateString(object.m_name), + object.m_id, + object.m_userId.first, + object.m_userId.second + ); + } + + bool LogicObjectImpl::Deserialize(const rlogic_serialization::LogicObject* object, + std::string& name, + uint64_t& id, + uint64_t& userIdHigh, + uint64_t& userIdLow, + ErrorReporting& errorReporting) + { + if (!object) + { + errorReporting.add("Fatal error during loading of LogicObject base from serialized data: missing base table!", nullptr, EErrorType::BinaryVersionMismatch); + return false; + } + + if (!object->name()) + { + errorReporting.add("Fatal error during loading of LogicObject base from serialized data: missing name!", nullptr, EErrorType::BinaryVersionMismatch); + return false; + } + + if (object->id() == 0u) + { + errorReporting.add("Fatal error during loading of LogicObject base from serialized data: missing or invalid ID!", nullptr, EErrorType::BinaryVersionMismatch); + return false; + } + + name = object->name()->string_view(); + id = object->id(); + userIdHigh = object->userIdHigh(); + userIdLow = object->userIdLow(); + + return true; + } + + void LogicObjectImpl::setLogicObject(LogicObject& obj) + { + assert(m_logicObject == nullptr); + m_logicObject = &obj; + } + + const LogicObject& LogicObjectImpl::getLogicObject() const + { + assert(m_logicObject != nullptr); + return *m_logicObject; + } + + LogicObject& LogicObjectImpl::getLogicObject() + { + assert(m_logicObject != nullptr); + return *m_logicObject; + } +} diff --git a/client/logic/lib/impl/LogicObjectImpl.h b/client/logic/lib/impl/LogicObjectImpl.h new file mode 100644 index 000000000..a4f82fa17 --- /dev/null +++ b/client/logic/lib/impl/LogicObjectImpl.h @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include + +namespace flatbuffers +{ + template struct Offset; + class FlatBufferBuilder; +} + +namespace rlogic_serialization +{ + struct LogicObject; +} + +namespace ramses +{ + class LogicObject; +} + +namespace ramses::internal +{ + class ErrorReporting; + + class LogicObjectImpl + { + public: + explicit LogicObjectImpl(std::string_view name, uint64_t id) noexcept; + virtual ~LogicObjectImpl() noexcept; + LogicObjectImpl(const LogicObjectImpl&) = delete; + LogicObjectImpl& operator=(const LogicObjectImpl&) = delete; + + [[nodiscard]] std::string_view getName() const; + [[nodiscard]] uint64_t getId() const; + bool setName(std::string_view name); + bool setUserId(uint64_t highId, uint64_t lowId); + [[nodiscard]] std::pair getUserId() const; + + [[nodiscard]] std::string getIdentificationString() const; + + void setLogicObject(LogicObject& obj); + [[nodiscard]] const LogicObject& getLogicObject() const; + [[nodiscard]] LogicObject& getLogicObject(); + + protected: + static flatbuffers::Offset Serialize(const LogicObjectImpl& object, flatbuffers::FlatBufferBuilder& builder); + static bool Deserialize(const rlogic_serialization::LogicObject* object, + std::string& name, + uint64_t& id, + uint64_t& userIdHigh, + uint64_t& userIdLow, + ErrorReporting& errorReporting); + + private: + std::string m_name; + uint64_t m_id; + std::pair m_userId{ 0u, 0u }; + LogicObject* m_logicObject = nullptr; + }; +} diff --git a/client/logic/lib/impl/LuaConfig.cpp b/client/logic/lib/impl/LuaConfig.cpp new file mode 100644 index 000000000..3d51ad228 --- /dev/null +++ b/client/logic/lib/impl/LuaConfig.cpp @@ -0,0 +1,50 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/LuaConfig.h" + +#include "impl/LuaConfigImpl.h" + +namespace ramses +{ + LuaConfig::LuaConfig() noexcept + : m_impl(std::make_unique()) + { + } + + void LuaConfig::enableDebugLogFunctions() + { + m_impl->enableDebugLogFunctions(); + } + + LuaConfig::~LuaConfig() noexcept = default; + + LuaConfig& LuaConfig::operator=(const LuaConfig& other) + { + m_impl = std::make_unique(*other.m_impl); + return *this; + } + + LuaConfig::LuaConfig(const LuaConfig& other) + { + *this = other; + } + + LuaConfig::LuaConfig(LuaConfig&&) noexcept = default; + LuaConfig& LuaConfig::operator=(LuaConfig&&) noexcept = default; + + bool LuaConfig::addDependency(std::string_view aliasName, const LuaModule& moduleInstance) + { + return m_impl->addDependency(aliasName, moduleInstance); + } + + bool LuaConfig::addStandardModuleDependency(EStandardModule stdModule) + { + return m_impl->addStandardModuleDependency(stdModule); + } +} diff --git a/client/logic/lib/impl/LuaConfigImpl.cpp b/client/logic/lib/impl/LuaConfigImpl.cpp new file mode 100644 index 000000000..291cc7986 --- /dev/null +++ b/client/logic/lib/impl/LuaConfigImpl.cpp @@ -0,0 +1,86 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/LuaConfigImpl.h" +#include "impl/LoggerImpl.h" + +#include "internals/LuaCompilationUtils.h" +#include "internals/SolState.h" + +namespace ramses::internal +{ + bool LuaConfigImpl::addDependency(std::string_view aliasName, const LuaModule& moduleInstance) + { + if (!LuaCompilationUtils::CheckModuleName(aliasName)) + { + LOG_ERROR("Failed to add dependency '{}'! The alias name should be a valid Lua label.", aliasName); + return false; + } + + if (SolState::IsReservedModuleName(aliasName)) + { + LOG_ERROR("Failed to add dependency '{}'! The alias collides with a standard library name!", aliasName); + return false; + } + + std::string aliasNameStr {aliasName}; + + if (m_modulesMapping.cend() != m_modulesMapping.find(aliasNameStr)) + { + LOG_ERROR("Module dependencies must be uniquely aliased! Alias '{}' is already used!", aliasName); + return false; + } + + m_modulesMapping.emplace(std::move(aliasNameStr), &moduleInstance); + return true; + } + + const ModuleMapping& LuaConfigImpl::getModuleMapping() const + { + return m_modulesMapping; + } + + bool LuaConfigImpl::addStandardModuleDependency(EStandardModule stdModule) + { + if (std::find(m_stdModules.cbegin(), m_stdModules.cend(), stdModule) != m_stdModules.cend()) + { + LOG_ERROR("Standard module {} already added, can't add twice!", stdModule); + return false; + } + + if (stdModule == EStandardModule::All) + { + for (EStandardModule m = EStandardModule::Base; m != EStandardModule::All;) + { + m_stdModules.push_back(m); + m = static_cast(static_cast(m) + 1); + } + } + else + { + m_stdModules.push_back(stdModule); + } + + return true; + } + + const StandardModules& LuaConfigImpl::getStandardModules() const + { + return m_stdModules; + } + + void LuaConfigImpl::enableDebugLogFunctions() + { + m_debugLogFunctionsEnabled = true; + } + + bool LuaConfigImpl::hasDebugLogFunctionsEnabled() const + { + return m_debugLogFunctionsEnabled; + } +} diff --git a/client/logic/lib/impl/LuaConfigImpl.h b/client/logic/lib/impl/LuaConfigImpl.h new file mode 100644 index 000000000..a6ec0ee12 --- /dev/null +++ b/client/logic/lib/impl/LuaConfigImpl.h @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/EStandardModule.h" + +#include +#include +#include +#include + +namespace ramses +{ + class LuaModule; +} + +namespace ramses::internal +{ + using ModuleMapping = std::unordered_map; + using StandardModules = std::vector; + + class LuaConfigImpl + { + public: + bool addDependency(std::string_view aliasName, const LuaModule& moduleInstance); + bool addStandardModuleDependency(EStandardModule stdModule); + void enableDebugLogFunctions(); + + [[nodiscard]] const ModuleMapping& getModuleMapping() const; + [[nodiscard]] const StandardModules& getStandardModules() const; + [[nodiscard]] bool hasDebugLogFunctionsEnabled() const; + + private: + ModuleMapping m_modulesMapping; + StandardModules m_stdModules; + bool m_debugLogFunctionsEnabled = false; + }; +} diff --git a/client/logic/lib/impl/LuaInterface.cpp b/client/logic/lib/impl/LuaInterface.cpp new file mode 100644 index 000000000..efa36438b --- /dev/null +++ b/client/logic/lib/impl/LuaInterface.cpp @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/LuaInterface.h" +#include "impl/LuaInterfaceImpl.h" + +namespace ramses +{ + LuaInterface::LuaInterface(std::unique_ptr impl) noexcept + : LogicNode(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_interface{ static_cast(LogicNode::m_impl) } + { + } +} diff --git a/client/logic/lib/impl/LuaInterfaceImpl.cpp b/client/logic/lib/impl/LuaInterfaceImpl.cpp new file mode 100644 index 000000000..dc5b3ec81 --- /dev/null +++ b/client/logic/lib/impl/LuaInterfaceImpl.cpp @@ -0,0 +1,118 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/LuaInterfaceImpl.h" + +#include "internals/ErrorReporting.h" +#include "internals/TypeUtils.h" + +#include "generated/LuaInterfaceGen.h" + +namespace ramses::internal +{ + LuaInterfaceImpl::LuaInterfaceImpl(LuaCompiledInterface compiledInterface, std::string_view name, uint64_t id) + : LogicNodeImpl(name, id) + { + setRootProperties(std::move(compiledInterface.rootProperty), nullptr); + } + + flatbuffers::Offset LuaInterfaceImpl::Serialize( + const LuaInterfaceImpl& luaInterface, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap) + { + const auto logicObject = LogicObjectImpl::Serialize(luaInterface, builder); + const auto propertyObject = PropertyImpl::Serialize(*luaInterface.getInputs()->m_impl, builder, serializationMap); + auto intf = rlogic_serialization::CreateLuaInterface(builder, logicObject, propertyObject); + builder.Finish(intf); + + return intf; + } + + std::unique_ptr LuaInterfaceImpl::Deserialize( + const rlogic_serialization::LuaInterface& luaInterface, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(luaInterface.base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of LuaInterface from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (name.length() == 0) + { + errorReporting.add("Fatal error during loading of LuaInterface from serialized data: empty name!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!luaInterface.rootProperty()) + { + errorReporting.add("Fatal error during loading of LuaInterface from serialized data: missing root property!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr rootProperty = PropertyImpl::Deserialize(*luaInterface.rootProperty(), EPropertySemantics::Interface, errorReporting, deserializationMap); + if (!rootProperty) + { + return nullptr; + } + + if (rootProperty->getType() != EPropertyType::Struct) + { + errorReporting.add("Fatal error during loading of LuaScript from serialized data: root property has unexpected type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto deserialized = std::make_unique( + LuaCompiledInterface{ std::move(rootProperty) }, + name, id); + deserialized->setUserId(userIdHigh, userIdLow); + + return deserialized; + } + + std::vector LuaInterfaceImpl::collectUnlinkedProperties() const + { + std::vector outputProperties = getOutputs()->m_impl->collectLeafChildren(); + + // filter for unlinked properties by removing linked ones + auto it = std::remove_if(outputProperties.begin(), outputProperties.end(), [](const auto* node) { return node->isLinked(); }); + outputProperties.erase(it, outputProperties.end()); + + return outputProperties; + } + + std::optional LuaInterfaceImpl::update() + { + // No-op, interfaces don't need any logic, they just hold proxy properties which are linked to other objects + return std::nullopt; + } + + void LuaInterfaceImpl::createRootProperties() + { + // unlike other logic objects, lua interface properties created outside of it (from script or deserialized) + } + + Property* LuaInterfaceImpl::getOutputs() + { + // For an interface, intputs == outputs + return getInputs(); + } + + const Property* LuaInterfaceImpl::getOutputs() const + { + // For an interface, intputs == outputs + return getInputs(); + } + +} diff --git a/client/logic/lib/impl/LuaInterfaceImpl.h b/client/logic/lib/impl/LuaInterfaceImpl.h new file mode 100644 index 000000000..40bc7667a --- /dev/null +++ b/client/logic/lib/impl/LuaInterfaceImpl.h @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/LogicNodeImpl.h" + +#include "internals/LuaCompilationUtils.h" +#include "internals/SerializationMap.h" +#include "internals/DeserializationMap.h" +#include "internals/WrappedLuaProperty.h" + +#include "ramses-logic/Property.h" +#include "ramses-logic/LuaInterface.h" + +#include +#include +#include + +namespace flatbuffers +{ + class FlatBufferBuilder; + + class FlatBufferBuilder; + template struct Offset; +} + +namespace rlogic_serialization +{ + struct LuaInterface; +} + +namespace ramses::internal +{ + class SolState; + + class LuaInterfaceImpl : public LogicNodeImpl + { + public: + explicit LuaInterfaceImpl(LuaCompiledInterface compiledInterface, std::string_view name, uint64_t id); + ~LuaInterfaceImpl() noexcept override = default; + LuaInterfaceImpl(const LuaInterfaceImpl & other) = delete; + LuaInterfaceImpl& operator=(const LuaInterfaceImpl & other) = delete; + + [[nodiscard]] Property* getOutputs() override; + [[nodiscard]] const Property* getOutputs() const override; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const LuaInterfaceImpl& luaInterface, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::LuaInterface& luaInterface, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + std::optional update() override; + void createRootProperties() final; + + [[nodiscard]] std::vector collectUnlinkedProperties() const; + }; +} diff --git a/client/ramses-client/ramses-client-api/AnimatedProperty.cpp b/client/logic/lib/impl/LuaModule.cpp similarity index 55% rename from client/ramses-client/ramses-client-api/AnimatedProperty.cpp rename to client/logic/lib/impl/LuaModule.cpp index 353411ed2..56c1b46f1 100644 --- a/client/ramses-client/ramses-client-api/AnimatedProperty.cpp +++ b/client/logic/lib/impl/LuaModule.cpp @@ -1,26 +1,20 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH +// Copyright (C) 2021 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -// API -#include "ramses-client-api/AnimatedProperty.h" - -// internal -#include "AnimatedPropertyImpl.h" +#include "ramses-logic/LuaModule.h" +#include "impl/LuaModuleImpl.h" namespace ramses { - AnimatedProperty::AnimatedProperty(AnimatedPropertyImpl& pimpl) - : AnimationObject(pimpl) - , impl(pimpl) - { - } - - AnimatedProperty::~AnimatedProperty() + LuaModule::LuaModule(std::unique_ptr impl) noexcept + : LogicObject(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_impl{ static_cast(*LogicObject::m_impl) } { } } diff --git a/client/logic/lib/impl/LuaModuleImpl.cpp b/client/logic/lib/impl/LuaModuleImpl.cpp new file mode 100644 index 000000000..3ceb0f6b3 --- /dev/null +++ b/client/logic/lib/impl/LuaModuleImpl.cpp @@ -0,0 +1,196 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/LuaModuleImpl.h" + +#include "ramses-logic/LuaModule.h" + +#include "internals/LuaCompilationUtils.h" +#include "generated/LuaModuleGen.h" +#include "flatbuffers/flatbuffers.h" +#include "internals/SolState.h" +#include "internals/ErrorReporting.h" +#include "internals/DeserializationMap.h" +#include "internals/SerializationMap.h" +#include "internals/EnvironmentProtection.h" +#include "internals/PropertyTypeExtractor.h" +#include + +namespace ramses::internal +{ + LuaModuleImpl::LuaModuleImpl(LuaCompiledModule module, std::string_view name, uint64_t id) + : LogicObjectImpl(name, id) + , m_sourceCode{ std::move(module.source.sourceCode) } + , m_byteCode{ std::move(module.source.byteCode) } + , m_module{ std::move(module.moduleTable) } + , m_dependencies{ std::move(module.source.userModules) } + , m_stdModules{ std::move(module.source.stdModules) } + , m_hasDebugLogFunctions{ module.source.hasDebugLogFunctions } + { + assert(m_module != sol::lua_nil); + } + + const sol::table& LuaModuleImpl::getModule() const + { + return m_module; + } + + flatbuffers::Offset LuaModuleImpl::Serialize( + const LuaModuleImpl& module, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap, + ELuaSavingMode luaSavingMode) + { + // serialization with debug logs is forbidden + assert(!module.hasDebugLogFunctions()); + + std::vector> modulesFB; + modulesFB.reserve(module.m_dependencies.size()); + for (const auto& dependency : module.m_dependencies) + { + modulesFB.push_back( + rlogic_serialization::CreateLuaModuleUsage(builder, + builder.CreateString(dependency.first), + dependency.second->getId())); + } + + std::vector stdModules; + stdModules.reserve(module.m_stdModules.size()); + for (const EStandardModule stdModule : module.m_stdModules) + { + stdModules.push_back(static_cast(stdModule)); + } + + const auto fbLogicObject = LogicObjectImpl::Serialize(module, builder); + const auto fbModulesVec = builder.CreateVector(modulesFB); + const auto fbStdModulesVec = builder.CreateVector(stdModules); + + const bool hasSourceCode = !module.m_sourceCode.empty(); + const bool hasByteCode = !module.m_byteCode.empty(); + assert(hasSourceCode || hasByteCode); + const bool serializeSourceCode = hasSourceCode && !((luaSavingMode == ELuaSavingMode::ByteCodeOnly) && hasByteCode); + const bool serializeByteCode = hasByteCode && !((luaSavingMode == ELuaSavingMode::SourceCodeOnly) && hasSourceCode); + assert(serializeSourceCode || serializeByteCode); + + const auto fbSrcCode = (serializeSourceCode ? builder.CreateString(module.m_sourceCode) : 0); + + flatbuffers::Offset> byteCodeOffset{}; + if (serializeByteCode) + { + std::string byteCodeString{ module.m_byteCode.as_string_view() }; + byteCodeOffset = serializationMap.resolveByteCodeOffsetIfFound(byteCodeString); + if (byteCodeOffset.IsNull()) + { + std::vector byteCodeAsVectorUInt8; + byteCodeAsVectorUInt8.reserve(module.m_byteCode.size()); + std::transform(module.m_byteCode.cbegin(), module.m_byteCode.cend(), std::back_inserter(byteCodeAsVectorUInt8), [](std::byte b) { return uint8_t(b); }); + + byteCodeOffset = builder.CreateVector(byteCodeAsVectorUInt8); + serializationMap.storeByteCodeOffset(std::move(byteCodeString), byteCodeOffset); + } + } + + return rlogic_serialization::CreateLuaModule(builder, + fbLogicObject, + fbSrcCode, + fbModulesVec, + fbStdModulesVec, + byteCodeOffset); + } + + std::unique_ptr LuaModuleImpl::Deserialize( + SolState& solState, + const rlogic_serialization::LuaModule& module, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(module.base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of LuaModule from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const bool hasSourceCode = (module.source() != nullptr && module.source()->size() > 0); + const bool hasBytecode = (module.luaByteCode() != nullptr && module.luaByteCode()->size() > 0); + if (!hasSourceCode && !hasBytecode) + { + errorReporting.add("Fatal error during loading of LuaModule from serialized data: has neither Lua source code nor bytecode!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!module.dependencies()) + { + errorReporting.add("Fatal error during loading of LuaModule from serialized data: missing dependencies!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + StandardModules stdModules; + stdModules.reserve(module.standardModules()->size()); + for (const uint8_t stdModule : *module.standardModules()) + { + stdModules.push_back(static_cast(stdModule)); + } + + ModuleMapping modulesUsed; + modulesUsed.reserve(module.dependencies()->size()); + for (const auto* mod : *module.dependencies()) + { + if (!mod->name()) + { + errorReporting.add(fmt::format("Fatal error during loading of LuaModule '{}' module data: missing name!", name), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + const auto* moduleUsed = deserializationMap.resolveLogicObject(mod->moduleId()); + if (!moduleUsed) + { + errorReporting.add(fmt::format("Fatal error during loading of LuaModule '{}' module data: could not resolve dependent module with id={}!", name, mod->moduleId()), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + modulesUsed.emplace(mod->name()->str(), moduleUsed->getLogicObject().as()); + } + + std::string source = (hasSourceCode ? module.source()->str() : ""); + sol::bytecode byteCode{}; + if (hasBytecode) + { + byteCode.reserve(module.luaByteCode()->size()); + std::transform(module.luaByteCode()->cbegin(), module.luaByteCode()->cend(), std::back_inserter(byteCode), [](uint8_t b) { return std::byte(b); }); + } + + auto compiledModule = LuaCompilationUtils::CompileModuleOrImportPrecompiled(solState, modulesUsed, stdModules, std::move(source), name, errorReporting, std::move(byteCode), false); + + if (!compiledModule) + { + errorReporting.add(fmt::format("Fatal error during loading of LuaModule '{}' from serialized data!", name), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto deserialized = std::make_unique( + std::move(*compiledModule), + name, id); + + deserialized->setUserId(userIdHigh, userIdLow); + + return deserialized; + } + + const ModuleMapping& LuaModuleImpl::getDependencies() const + { + return m_dependencies; + } + + bool LuaModuleImpl::hasDebugLogFunctions() const + { + return m_hasDebugLogFunctions; + } +} diff --git a/client/logic/lib/impl/LuaModuleImpl.h b/client/logic/lib/impl/LuaModuleImpl.h new file mode 100644 index 000000000..ffe2f4810 --- /dev/null +++ b/client/logic/lib/impl/LuaModuleImpl.h @@ -0,0 +1,69 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/LogicObjectImpl.h" +#include "internals/LuaCompilationUtils.h" +#include "internals/SolWrapper.h" +#include "ramses-logic/ELuaSavingMode.h" +#include + +namespace rlogic_serialization +{ + struct LuaModule; +} + +namespace flatbuffers +{ + template struct Offset; + class FlatBufferBuilder; +} + +namespace ramses +{ + class LuaModule; +} + +namespace ramses::internal +{ + class ErrorReporting; + class SolState; + class DeserializationMap; + class SerializationMap; + + class LuaModuleImpl : public LogicObjectImpl + { + public: + LuaModuleImpl(LuaCompiledModule module, std::string_view name, uint64_t id); + + [[nodiscard]] const sol::table& getModule() const; + [[nodiscard]] const ModuleMapping& getDependencies() const; + [[nodiscard]] bool hasDebugLogFunctions() const; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const LuaModuleImpl& module, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap, + ELuaSavingMode luaSavingMode); + + [[nodiscard]] static std::unique_ptr Deserialize( + SolState& solState, + const rlogic_serialization::LuaModule& module, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + private: + std::string m_sourceCode; + sol::bytecode m_byteCode; + sol::table m_module; + ModuleMapping m_dependencies; + StandardModules m_stdModules; + bool m_hasDebugLogFunctions; + }; +} diff --git a/client/ramses-client/ramses-client-api/Spline.cpp b/client/logic/lib/impl/LuaScript.cpp similarity index 56% rename from client/ramses-client/ramses-client-api/Spline.cpp rename to client/logic/lib/impl/LuaScript.cpp index 944c2ce54..f88e2f5a3 100644 --- a/client/ramses-client/ramses-client-api/Spline.cpp +++ b/client/logic/lib/impl/LuaScript.cpp @@ -1,28 +1,20 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH +// Copyright (C) 2020 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#include "ramses-client-api/Spline.h" -#include "SplineImpl.h" +#include "ramses-logic/LuaScript.h" +#include "impl/LuaScriptImpl.h" namespace ramses { - Spline::Spline(SplineImpl& pimpl) - : AnimationObject(pimpl) - , impl(pimpl) + LuaScript::LuaScript(std::unique_ptr impl) noexcept + : LogicNode(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_script{ static_cast(LogicNode::m_impl) } { } - - Spline::~Spline() - { - } - - uint32_t Spline::getNumberOfKeys() const - { - return impl.getNumKeys(); - } } diff --git a/client/logic/lib/impl/LuaScriptImpl.cpp b/client/logic/lib/impl/LuaScriptImpl.cpp new file mode 100644 index 000000000..7df68aa84 --- /dev/null +++ b/client/logic/lib/impl/LuaScriptImpl.cpp @@ -0,0 +1,270 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/LuaScriptImpl.h" + +#include "ramses-logic/LuaModule.h" +#include "internals/SolState.h" +#include "impl/PropertyImpl.h" +#include "impl/LuaModuleImpl.h" + +#include "internals/WrappedLuaProperty.h" +#include "internals/SolHelper.h" +#include "internals/ErrorReporting.h" +#include "internals/PropertyTypeExtractor.h" +#include "internals/EnvironmentProtection.h" +#include "internals/SerializationMap.h" + +#include "generated/LuaScriptGen.h" + +#include + +namespace ramses::internal +{ + LuaScriptImpl::LuaScriptImpl(LuaCompiledScript compiledScript, std::string_view name, uint64_t id) + : LogicNodeImpl(name, id) + , m_source(std::move(compiledScript.source.sourceCode)) + , m_byteCode(std::move(compiledScript.source.byteCode)) + , m_wrappedRootInput(*compiledScript.rootInput) + , m_wrappedRootOutput(*compiledScript.rootOutput) + , m_runFunction(std::move(compiledScript.runFunction)) + , m_modules(std::move(compiledScript.source.userModules)) + , m_stdModules(std::move(compiledScript.source.stdModules)) + , m_hasDebugLogFunctions{ compiledScript.source.hasDebugLogFunctions } + { + setRootProperties(std::move(compiledScript.rootInput), std::move(compiledScript.rootOutput)); + } + + void LuaScriptImpl::createRootProperties() + { + // unlike other logic objects, luascript properties created outside of it (from script or deserialized) + } + + flatbuffers::Offset LuaScriptImpl::Serialize( + const LuaScriptImpl& luaScript, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap, + ELuaSavingMode luaSavingMode) + { + // serialization with debug logs is forbidden + assert(!luaScript.hasDebugLogFunctions()); + + std::vector> userModules; + userModules.reserve(luaScript.m_modules.size()); + for (const auto& module : luaScript.m_modules) + { + userModules.push_back( + rlogic_serialization::CreateLuaModuleUsage(builder, + builder.CreateString(module.first), + module.second->getId())); + } + + std::vector stdModules; + stdModules.reserve(luaScript.m_stdModules.size()); + for (const EStandardModule stdModule : luaScript.m_stdModules) + { + stdModules.push_back(static_cast(stdModule)); + } + + const auto fbLogicObject = LogicObjectImpl::Serialize(luaScript, builder); + const auto fbModulesVec = builder.CreateVector(userModules); + const auto fbStdModulesVec = builder.CreateVector(stdModules); + const auto fbInputPropertyObject = PropertyImpl::Serialize(*luaScript.getInputs()->m_impl, builder, serializationMap); + const auto fbOuputPropertyObject = PropertyImpl::Serialize(*luaScript.getOutputs()->m_impl, builder, serializationMap); + + const bool hasSourceCode = !luaScript.m_source.empty(); + const bool hasByteCode = !luaScript.m_byteCode.empty(); + assert(hasSourceCode || hasByteCode); + const bool serializeSourceCode = hasSourceCode && !((luaSavingMode == ELuaSavingMode::ByteCodeOnly) && hasByteCode); + const bool serializeByteCode = hasByteCode && !((luaSavingMode == ELuaSavingMode::SourceCodeOnly) && hasSourceCode); + assert(serializeSourceCode || serializeByteCode); + + const auto fbSrcCode = (serializeSourceCode ? builder.CreateString(luaScript.m_source) : 0); + + flatbuffers::Offset> byteCodeOffset{}; + if (serializeByteCode) + { + std::string byteCodeString{ luaScript.m_byteCode.as_string_view() }; + byteCodeOffset = serializationMap.resolveByteCodeOffsetIfFound(byteCodeString); + if (byteCodeOffset.IsNull()) + { + std::vector byteCodeAsVectorUInt8; + byteCodeAsVectorUInt8.reserve(luaScript.m_byteCode.size()); + std::transform(luaScript.m_byteCode.cbegin(), luaScript.m_byteCode.cend(), std::back_inserter(byteCodeAsVectorUInt8), [](std::byte b) { return uint8_t(b); }); + + byteCodeOffset = builder.CreateVector(byteCodeAsVectorUInt8); + serializationMap.storeByteCodeOffset(std::move(byteCodeString), byteCodeOffset); + } + } + + auto script = rlogic_serialization::CreateLuaScript(builder, + fbLogicObject, + fbSrcCode, + fbModulesVec, + fbStdModulesVec, + fbInputPropertyObject, + fbOuputPropertyObject, + byteCodeOffset + ); + builder.Finish(script); + + return script; + } + + std::unique_ptr LuaScriptImpl::Deserialize( + SolState& solState, + const rlogic_serialization::LuaScript& luaScript, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(luaScript.base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of LuaScript from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const bool hasSourceCode = (luaScript.luaSourceCode() != nullptr && luaScript.luaSourceCode()->size() > 0); + const bool hasBytecode = (luaScript.luaByteCode() != nullptr && luaScript.luaByteCode()->size() > 0); + if (!hasSourceCode && !hasBytecode) + { + errorReporting.add("Fatal error during loading of LuaScript from serialized data: has neither Lua source code nor bytecode!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!luaScript.rootInput()) + { + errorReporting.add("Fatal error during loading of LuaScript from serialized data: missing root input!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr rootInput = PropertyImpl::Deserialize(*luaScript.rootInput(), EPropertySemantics::ScriptInput, errorReporting, deserializationMap); + if (!rootInput) + { + return nullptr; + } + + if (!luaScript.rootOutput()) + { + errorReporting.add("Fatal error during loading of LuaScript from serialized data: missing root output!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr rootOutput = PropertyImpl::Deserialize(*luaScript.rootOutput(), EPropertySemantics::ScriptOutput, errorReporting, deserializationMap); + if (!rootOutput) + { + return nullptr; + } + + if (rootInput->getType() != EPropertyType::Struct) + { + errorReporting.add("Fatal error during loading of LuaScript from serialized data: root input has unexpected type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (rootOutput->getType() != EPropertyType::Struct) + { + errorReporting.add("Fatal error during loading of LuaScript from serialized data: root output has unexpected type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!luaScript.userModules()) + { + errorReporting.add("Fatal error during loading of LuaScript from serialized data: missing user module dependencies!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + ModuleMapping userModules; + userModules.reserve(luaScript.userModules()->size()); + for (const auto* module : *luaScript.userModules()) + { + if (!module->name()) + { + errorReporting.add(fmt::format("Fatal error during loading of LuaScript '{}' module data: missing name!", name), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + const auto* moduleUsed = deserializationMap.resolveLogicObject(module->moduleId()); + if (!moduleUsed) + { + errorReporting.add(fmt::format("Fatal error during loading of LuaScript '{}' module data: could not resolve dependent module with id={}!", name, module->moduleId()), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + userModules.emplace(module->name()->str(), moduleUsed->getLogicObject().as()); + } + + if (!luaScript.standardModules()) + { + errorReporting.add("Fatal error during loading of LuaScript from serialized data: missing standard module dependencies!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + StandardModules stdModules; + stdModules.reserve(luaScript.standardModules()->size()); + for (const uint8_t stdModule : *luaScript.standardModules()) + stdModules.push_back(static_cast(stdModule)); + + std::string sourceCode = (hasSourceCode ? luaScript.luaSourceCode()->str() : ""); + sol::bytecode byteCode; + if (hasBytecode) + { + byteCode.reserve(luaScript.luaByteCode()->size()); + std::transform(luaScript.luaByteCode()->cbegin(), luaScript.luaByteCode()->cend(), std::back_inserter(byteCode), [](uint8_t b) { return std::byte(b); }); + } + + auto compiledScript = LuaCompilationUtils::CompileScriptOrImportPrecompiled( + solState, + userModules, + stdModules, + std::move(sourceCode), + name, + errorReporting, + std::move(byteCode), + std::move(rootInput), + std::move(rootOutput), + false); + + if (!compiledScript) + { + errorReporting.add(fmt::format("Fatal error during loading of LuaScript '{}' from serialized data!", name), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto deserialized = std::make_unique( + std::move(*compiledScript), + name, id); + + deserialized->setUserId(userIdHigh, userIdLow); + + return deserialized; + } + + std::optional LuaScriptImpl::update() + { + sol::protected_function_result result = m_runFunction(std::ref(m_wrappedRootInput), std::ref(m_wrappedRootOutput)); + + if (!result.valid()) + { + sol::error error = result; + return LogicNodeRuntimeError{error.what()}; + } + + return std::nullopt; + } + + const ModuleMapping& LuaScriptImpl::getModules() const + { + return m_modules; + } + + bool LuaScriptImpl::hasDebugLogFunctions() const + { + return m_hasDebugLogFunctions; + } +} diff --git a/client/logic/lib/impl/LuaScriptImpl.h b/client/logic/lib/impl/LuaScriptImpl.h new file mode 100644 index 000000000..4ff1a26a5 --- /dev/null +++ b/client/logic/lib/impl/LuaScriptImpl.h @@ -0,0 +1,85 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/LogicNodeImpl.h" + +#include "internals/LuaCompilationUtils.h" +#include "internals/DeserializationMap.h" +#include "internals/WrappedLuaProperty.h" + +#include "ramses-logic/Property.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/ELuaSavingMode.h" + +#include +#include +#include + +namespace flatbuffers +{ + class FlatBufferBuilder; + + class FlatBufferBuilder; + template struct Offset; +} + +namespace rlogic_serialization +{ + struct LuaScript; +} + +namespace ramses +{ + class LuaModule; +} + +namespace ramses::internal +{ + class SolState; + class SerializationMap; + + class LuaScriptImpl : public LogicNodeImpl + { + public: + explicit LuaScriptImpl(LuaCompiledScript compiledScript, std::string_view name, uint64_t id); + ~LuaScriptImpl() noexcept override = default; + LuaScriptImpl(const LuaScriptImpl & other) = delete; + LuaScriptImpl& operator=(const LuaScriptImpl & other) = delete; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const LuaScriptImpl& luaScript, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap, + ELuaSavingMode luaSavingMode); + + [[nodiscard]] static std::unique_ptr Deserialize( + SolState& solState, + const rlogic_serialization::LuaScript& luaScript, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + std::optional update() override; + + [[nodiscard]] const ModuleMapping& getModules() const; + [[nodiscard]] bool hasDebugLogFunctions() const; + + void createRootProperties() final; + + private: + std::string m_source; + sol::bytecode m_byteCode; + WrappedLuaProperty m_wrappedRootInput; + WrappedLuaProperty m_wrappedRootOutput; + sol::protected_function m_runFunction; + ModuleMapping m_modules; + StandardModules m_stdModules; + bool m_hasDebugLogFunctions; + }; +} diff --git a/client/logic/lib/impl/Property.cpp b/client/logic/lib/impl/Property.cpp new file mode 100644 index 000000000..353f39552 --- /dev/null +++ b/client/logic/lib/impl/Property.cpp @@ -0,0 +1,153 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/Property.h" +#include "ramses-logic/LogicNode.h" +#include "impl/PropertyImpl.h" +#include "impl/LogicNodeImpl.h" +#include "impl/LoggerImpl.h" + +namespace ramses +{ + Property::Property(std::unique_ptr impl) noexcept + : m_impl(std::move(impl)) + { + m_impl->setPropertyInstance(*this); + } + + Property::~Property() noexcept = default; + + size_t Property::getChildCount() const + { + return m_impl->getChildCount(); + } + + bool Property::hasChild(std::string_view name) const { + return m_impl->hasChild(name); + } + + EPropertyType Property::getType() const + { + return m_impl->getType(); + } + + std::string_view Property::getName() const + { + return m_impl->getName(); + } + + const Property* Property::getChild(size_t index) const + { + return m_impl->getChild(index); + } + + Property* Property::getChild(size_t index) + { + return m_impl->getChild(index); + } + + Property* Property::getChild(std::string_view name) + { + return m_impl->getChild(name); + } + + const Property* Property::getChild(std::string_view name) const + { + return m_impl->getChild(name); + } + + template std::optional Property::getInternal() const + { + return m_impl->getValue_PublicApi(); + } + + template + bool Property::setInternal(T value) + { + return m_impl->setValue_PublicApi(std::move(value)); + } + + // Lua works with int. The logic engine API uses int32_t. To ensure that the runtime has no side effects + // we assert the two types are equivalent on the platform/compiler + static_assert(std::is_same::value, "int32_t must be the same type as int"); + + template RAMSES_API std::optional Property::getInternal() const; + template RAMSES_API std::optional Property::getInternal() const; + template RAMSES_API std::optional Property::getInternal() const; + template RAMSES_API std::optional Property::getInternal() const; + template RAMSES_API std::optional Property::getInternal() const; + template RAMSES_API std::optional Property::getInternal() const; + template RAMSES_API std::optional Property::getInternal() const; + template RAMSES_API std::optional Property::getInternal() const; + template RAMSES_API std::optional Property::getInternal() const; + template RAMSES_API std::optional Property::getInternal() const; + template RAMSES_API std::optional Property::getInternal() const; + + template RAMSES_API bool Property::setInternal(float /*value*/); + template RAMSES_API bool Property::setInternal(vec2f /*value*/); + template RAMSES_API bool Property::setInternal(vec3f /*value*/); + template RAMSES_API bool Property::setInternal(vec4f /*value*/); + template RAMSES_API bool Property::setInternal(int32_t /*value*/); + template RAMSES_API bool Property::setInternal(int64_t /*value*/); + template RAMSES_API bool Property::setInternal(vec2i /*value*/); + template RAMSES_API bool Property::setInternal(vec3i /*value*/); + template RAMSES_API bool Property::setInternal(vec4i /*value*/); + template RAMSES_API bool Property::setInternal(std::string /*value*/); + template RAMSES_API bool Property::setInternal(bool /*value*/); + + bool Property::isLinked() const + { + return m_impl->isLinked(); + } + + bool Property::hasIncomingLink() const + { + return m_impl->hasIncomingLink(); + } + + bool Property::hasOutgoingLink() const + { + return m_impl->hasOutgoingLink(); + } + + std::optional Property::getIncomingLink() const + { + if (!m_impl->hasIncomingLink()) + return std::nullopt; + + const auto& link = m_impl->getIncomingLink(); + return PropertyLink{ &link.property->getPropertyInstance(), this, link.isWeakLink }; + } + + size_t Property::getOutgoingLinksCount() const + { + return m_impl->hasOutgoingLink() ? m_impl->getOutgoingLinks().size() : 0u; + } + + std::optional Property::getOutgoingLink(size_t index) const + { + if (index >= getOutgoingLinksCount()) + { + LOG_ERROR("Failed to get outgoing link: zero-based index #{} exceeds the total count of outgoing links which is {}.", index, getOutgoingLinksCount()); + return std::nullopt; + } + + const auto& link = m_impl->getOutgoingLinks()[index]; + return PropertyLink{ this, &link.property->getPropertyInstance(), link.isWeakLink }; + } + + const LogicNode& Property::getOwningLogicNode() const + { + return *m_impl->getLogicNode().getLogicObject().as(); + } + + LogicNode& Property::getOwningLogicNode() + { + return *m_impl->getLogicNode().getLogicObject().as(); + } +} diff --git a/client/logic/lib/impl/PropertyImpl.cpp b/client/logic/lib/impl/PropertyImpl.cpp new file mode 100644 index 000000000..c1890c66a --- /dev/null +++ b/client/logic/lib/impl/PropertyImpl.cpp @@ -0,0 +1,733 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/Property.h" + +#include "impl/PropertyImpl.h" +#include "impl/LogicNodeImpl.h" +#include "impl/LoggerImpl.h" + +#include "internals/SerializationHelper.h" +#include "internals/TypeUtils.h" +#include "internals/ErrorReporting.h" +#include "internals/TypeUtils.h" + +#include "generated/PropertyGen.h" + +#include +#include + +namespace ramses::internal +{ + PropertyImpl::PropertyImpl(HierarchicalTypeData type, EPropertySemantics semantics) + : m_typeData(std::move(type.typeData)) + , m_semantics(semantics) + { + if (TypeUtils::IsPrimitiveType(m_typeData.type)) + { + switch (m_typeData.type) + { + case EPropertyType::Float: + m_value = PropertyEnumToType::TYPE{ 0.0f }; + break; + case EPropertyType::Vec2f: + m_value = PropertyEnumToType::TYPE{ 0.0f, 0.0f }; + break; + case EPropertyType::Vec3f: + m_value = PropertyEnumToType::TYPE{ 0.0f, 0.0f, 0.0f }; + break; + case EPropertyType::Vec4f: + m_value = PropertyEnumToType::TYPE{ 0.0f, 0.0f, 0.0f, 0.0f }; + break; + case EPropertyType::Int32: + m_value = PropertyEnumToType::TYPE{ 0 }; + break; + case EPropertyType::Int64: + m_value = PropertyEnumToType::TYPE{ 0 }; + break; + case EPropertyType::Vec2i: + m_value = PropertyEnumToType::TYPE{ 0, 0 }; + break; + case EPropertyType::Vec3i: + m_value = PropertyEnumToType::TYPE{ 0, 0, 0 }; + break; + case EPropertyType::Vec4i: + m_value = PropertyEnumToType::TYPE{ 0, 0, 0, 0 }; + break; + case EPropertyType::String: + m_value = PropertyEnumToType::TYPE{}; + break; + case EPropertyType::Bool: + m_value = PropertyEnumToType::TYPE{ false }; + break; + case EPropertyType::Array: + case EPropertyType::Struct: + assert(false); + break; + } + } + else + { + for (const auto& childType : type.children) + { + m_children.emplace_back(CreateProperty(std::make_unique(childType, semantics))); + } + } + } + + PropertyImpl::PropertyImpl(HierarchicalTypeData type, EPropertySemantics semantics, PropertyValue initialValue) + : PropertyImpl(std::move(type), semantics) + { + assert(TypeUtils::IsPrimitiveType(m_typeData.type) && "Don't use this constructor with non-primitive types!"); + m_value = std::move(initialValue); + } + + PropertyImpl::~PropertyImpl() noexcept + { + // TODO Violin/Vaclav discuss if we want to handle this here + if (m_incomingLink.property != nullptr) + { + resetIncomingLink(); + } + + for (auto outgoingLink : m_outgoingLinks) + { + assert(outgoingLink.property->m_incomingLink.property == this); + outgoingLink.property->m_incomingLink = { nullptr, false }; + } + } + + flatbuffers::Offset PropertyImpl::Serialize(const PropertyImpl& prop, flatbuffers::FlatBufferBuilder& builder, SerializationMap& serializationMap) + { + auto result = SerializeRecursive(prop, builder, serializationMap); + builder.Finish(result); + return result; + } + + flatbuffers::Offset PropertyImpl::SerializeRecursive( + const PropertyImpl& prop, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap) + { + std::vector> child_vector; + child_vector.reserve(prop.m_children.size()); + + std::transform(prop.m_children.begin(), prop.m_children.end(), std::back_inserter(child_vector), [&builder, &serializationMap](auto& child) { + return SerializeRecursive(*child->m_impl, builder, serializationMap); + }); + + // Assume primitive property, override only for structs/arrays based on m_typeData.type + rlogic_serialization::EPropertyRootType propertyRootType = rlogic_serialization::EPropertyRootType::Primitive; + rlogic_serialization::PropertyValue valueType = rlogic_serialization::PropertyValue::NONE; + flatbuffers::Offset valueOffset; + + switch (prop.m_typeData.type) + { + case EPropertyType::Bool: + { + const rlogic_serialization::bool_s bool_struct(prop.getValueAs()); + valueOffset = builder.CreateStruct(bool_struct).Union(); + valueType = rlogic_serialization::PropertyValueTraits::enum_value; + } + break; + case EPropertyType::Float: + { + const rlogic_serialization::float_s float_struct(prop.getValueAs()); + valueOffset = builder.CreateStruct(float_struct).Union(); + valueType = rlogic_serialization::PropertyValueTraits::enum_value; + } + break; + case EPropertyType::Vec2f: + { + const auto& valueVec2f = prop.getValueAs(); + const rlogic_serialization::vec2f_s vec2f_struct(valueVec2f[0], valueVec2f[1]); + valueOffset = builder.CreateStruct(vec2f_struct).Union(); + valueType = rlogic_serialization::PropertyValueTraits::enum_value; + } + break; + case EPropertyType::Vec3f: + { + const auto& valueVec3f = prop.getValueAs(); + const rlogic_serialization::vec3f_s vec3f_struct(valueVec3f[0], valueVec3f[1], valueVec3f[2]); + valueOffset = builder.CreateStruct(vec3f_struct).Union(); + valueType = rlogic_serialization::PropertyValueTraits::enum_value; + } + break; + case EPropertyType::Vec4f: + { + const auto& valueVec4f = prop.getValueAs(); + const rlogic_serialization::vec4f_s vec4f_struct(valueVec4f[0], valueVec4f[1], valueVec4f[2], valueVec4f[3]); + valueOffset = builder.CreateStruct(vec4f_struct).Union(); + valueType = rlogic_serialization::PropertyValueTraits::enum_value; + } + break; + case EPropertyType::Int32: + { + const rlogic_serialization::int32_s int32_struct(prop.getValueAs()); + valueOffset = builder.CreateStruct(int32_struct).Union(); + valueType = rlogic_serialization::PropertyValueTraits::enum_value; + } + break; + case EPropertyType::Int64: + { + const rlogic_serialization::int64_s int64_struct(prop.getValueAs()); + valueOffset = builder.CreateStruct(int64_struct).Union(); + valueType = rlogic_serialization::PropertyValueTraits::enum_value; + } + break; + case EPropertyType::Vec2i: + { + const auto& valueVec2i = prop.getValueAs(); + const rlogic_serialization::vec2i_s vec2i_struct(valueVec2i[0], valueVec2i[1]); + valueOffset = builder.CreateStruct(vec2i_struct).Union(); + valueType = rlogic_serialization::PropertyValueTraits::enum_value; + } + break; + case EPropertyType::Vec3i: + { + const auto& valueVec3i = prop.getValueAs(); + const rlogic_serialization::vec3i_s vec3i_struct(valueVec3i[0], valueVec3i[1], valueVec3i[2]); + valueOffset = builder.CreateStruct(vec3i_struct).Union(); + valueType = rlogic_serialization::PropertyValueTraits::enum_value; + } + break; + case EPropertyType::Vec4i: + { + const auto& valueVec4i = prop.getValueAs(); + const rlogic_serialization::vec4i_s vec4i_struct(valueVec4i[0], valueVec4i[1], valueVec4i[2], valueVec4i[3]); + valueOffset = builder.CreateStruct(vec4i_struct).Union(); + valueType = rlogic_serialization::PropertyValueTraits::enum_value; + } + break; + case EPropertyType::String: + { + valueOffset = rlogic_serialization::Createstring_s(builder, builder.CreateString(prop.getValueAs())).Union(); + valueType = rlogic_serialization::PropertyValueTraits::enum_value; + } + break; + case EPropertyType::Array: + propertyRootType = rlogic_serialization::EPropertyRootType::Array; + break; + case EPropertyType::Struct: + propertyRootType = rlogic_serialization::EPropertyRootType::Struct; + break; + } + + const auto fbName = builder.CreateString(prop.m_typeData.name); + const auto fbChildrenVec = builder.CreateVector(child_vector); + + auto propertyFB = rlogic_serialization::CreateProperty(builder, + fbName, + propertyRootType, + fbChildrenVec, + valueType, + valueOffset + ); + + serializationMap.storePropertyOffset(prop, propertyFB); + + return propertyFB; + } + + std::unique_ptr PropertyImpl::Deserialize( + const rlogic_serialization::Property& prop, + EPropertySemantics semantics, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + // TODO Violin we can make name optional - e.g. array fields don't need a name, no need to serialize empty strings + if (!prop.name()) + { + errorReporting.add("Fatal error during loading of Property from serialized data: missing name!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const std::optional convertedType = ConvertSerializationTypeToEPropertyType(prop.rootType(), prop.value_type()); + + if (!convertedType) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr impl(new PropertyImpl(MakeType(std::string(prop.name()->string_view()), *convertedType), semantics)); + + // If primitive: set value; otherwise load children + if (prop.rootType() == rlogic_serialization::EPropertyRootType::Primitive) + { + // TODO Violin investigate possibilities to unit-test enum mismatches (and perhaps collapse + // the if-s in the switch below if possible) + switch (prop.value_type()) + { + case rlogic_serialization::PropertyValue::float_s: + if (!prop.value_as_float_s()) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid union!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + impl->m_value = prop.value_as_float_s()->v(); + break; + case rlogic_serialization::PropertyValue::vec2f_s: + { + auto vec2fValue = prop.value_as_vec2f_s(); + if (!vec2fValue) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid union!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + impl->m_value = vec2f{vec2fValue->x(), vec2fValue->y()}; + break; + } + case rlogic_serialization::PropertyValue::vec3f_s: + { + auto vec3fValue = prop.value_as_vec3f_s(); + if (!vec3fValue) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid union!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + impl->m_value = vec3f{vec3fValue->x(), vec3fValue->y(), vec3fValue->z()}; + break; + } + case rlogic_serialization::PropertyValue::vec4f_s: + { + auto vec4fValue = prop.value_as_vec4f_s(); + if (!vec4fValue) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid union!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + impl->m_value = vec4f{vec4fValue->x(), vec4fValue->y(), vec4fValue->z(), vec4fValue->w()}; + break; + } + case rlogic_serialization::PropertyValue::int32_s: + if (!prop.value_as_int32_s()) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid union!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + impl->m_value = prop.value_as_int32_s()->v(); + break; + case rlogic_serialization::PropertyValue::int64_s: + if (!prop.value_as_int64_s()) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid union!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + impl->m_value = prop.value_as_int64_s()->v(); + break; + case rlogic_serialization::PropertyValue::vec2i_s: + { + auto vec2iValue = prop.value_as_vec2i_s(); + if (!vec2iValue) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid union!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + impl->m_value = vec2i{vec2iValue->x(), vec2iValue->y()}; + break; + } + case rlogic_serialization::PropertyValue::vec3i_s: + { + auto vec3iValue = prop.value_as_vec3i_s(); + if (!vec3iValue) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid union!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + impl->m_value = vec3i{vec3iValue->x(), vec3iValue->y(), vec3iValue->z()}; + break; + } + case rlogic_serialization::PropertyValue::vec4i_s: + { + auto vec4iValue = prop.value_as_vec4i_s(); + if (!vec4iValue) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid union!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + impl->m_value = vec4i{vec4iValue->x(), vec4iValue->y(), vec4iValue->z(), vec4iValue->w()}; + break; + } + case rlogic_serialization::PropertyValue::string_s: + if (!prop.value_as_string_s()) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid union!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + impl->m_value = prop.value_as_string_s()->v()->str(); + break; + case rlogic_serialization::PropertyValue::bool_s: + if (!prop.value_as_bool_s()) + { + errorReporting.add("Fatal error during loading of Property from serialized data: invalid union!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + impl->m_value = prop.value_as_bool_s()->v(); + break; + case rlogic_serialization::PropertyValue::NONE: + default: + assert(false && "Should never reach this line - invalid types should be handled in ConvertSerializationTypeToEPropertyType above"); + return nullptr; + } + } + else + { + // Invalid types are handled above + assert (prop.rootType() == rlogic_serialization::EPropertyRootType::Struct || prop.rootType() == rlogic_serialization::EPropertyRootType::Array); + + if (!prop.children()) + { + errorReporting.add("Fatal error during loading of Property from serialized data: complex type has no child type info!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + for (const auto* child : *prop.children()) + { + if (!child) + { + // TODO Violin find ways to unit-test this case + errorReporting.add("Fatal error during loading of Property from serialized data: corrupt child data!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr deserializedChild = PropertyImpl::Deserialize(*child, semantics, errorReporting, deserializationMap); + + if (!deserializedChild) + { + return nullptr; + } + + impl->m_children.push_back(CreateProperty(std::move(deserializedChild))); + } + } + + deserializationMap.storePropertyImpl(prop, *impl); + + return impl; + } + + size_t PropertyImpl::getChildCount() const + { + return m_children.size(); + } + + EPropertyType PropertyImpl::getType() const + { + return m_typeData.type; + } + + std::string_view PropertyImpl::getName() const + { + return m_typeData.name; + } + + Property* PropertyImpl::getChild(size_t index) + { + if (index < m_children.size()) + { + return m_children[index].get(); + } + + LOG_ERROR("No child property with index '{}' found in '{}'", index, m_typeData.name); + return nullptr; + } + + const Property* PropertyImpl::getChild(size_t index) const + { + if (index < m_children.size()) + { + return m_children[index].get(); + } + + LOG_ERROR("No child property with index '{}' found in '{}'", index, m_typeData.name); + return nullptr; + } + + Property* PropertyImpl::getChild(std::string_view name) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) non-const version of getChild cast to its const version to avoid duplicating code + return const_cast((const_cast(*this)).getChild(name)); + } + + const Property* PropertyImpl::getChild(std::string_view name) const + { + auto it = std::find_if(m_children.begin(), m_children.end(), [&name](const auto& property) { + return property->getName() == name; + }); + if (it != m_children.end()) + { + return it->get(); + } + LOG_ERROR("No child property with name '{}' found in '{}'", name, m_typeData.name); + return nullptr; + } + + bool PropertyImpl::hasChild(std::string_view name) const + { + return m_children.end() != std::find_if(m_children.begin(), m_children.end(), [&name](const auto& property) { + return property->getName() == name; + }); + } + + std::vector PropertyImpl::collectLeafChildren() const + { + std::vector result; + + std::vector toTraverse; + toTraverse.push_back(m_propertyInstance); + while (!toTraverse.empty()) + { + const auto* current = toTraverse.back(); + toTraverse.pop_back(); + + if (TypeUtils::IsPrimitiveType(current->getType())) + { + result.push_back(current); + } + else + { + for (uint32_t i = 0; i < current->getChildCount(); ++i) + { + toTraverse.emplace_back(current->getChild(i)); + } + } + } + + return result; + } + + template std::optional PropertyImpl::getValue_PublicApi() const + { + if (PropertyTypeToEnum::TYPE == m_typeData.type) + { + assert(std::holds_alternative(m_value)); + return std::get(m_value); + } + LOG_ERROR("Invalid type '{}' when accessing property '{}', correct type is '{}'", + GetLuaPrimitiveTypeName(PropertyTypeToEnum::TYPE), m_typeData.name, GetLuaPrimitiveTypeName(m_typeData.type)); + return std::nullopt; + } + + template std::optional PropertyImpl::getValue_PublicApi() const; + template std::optional PropertyImpl::getValue_PublicApi() const; + template std::optional PropertyImpl::getValue_PublicApi() const; + template std::optional PropertyImpl::getValue_PublicApi() const; + template std::optional PropertyImpl::getValue_PublicApi() const; + template std::optional PropertyImpl::getValue_PublicApi() const; + template std::optional PropertyImpl::getValue_PublicApi() const; + template std::optional PropertyImpl::getValue_PublicApi() const; + template std::optional PropertyImpl::getValue_PublicApi() const; + template std::optional PropertyImpl::getValue_PublicApi() const; + template std::optional PropertyImpl::getValue_PublicApi() const; + + bool PropertyImpl::setValue_PublicApi(PropertyValue value) + { + if (m_semantics == EPropertySemantics::ScriptOutput) + { + LOG_ERROR("Cannot set property '{}' which is an output.", m_typeData.name); + return false; + } + + if (m_incomingLink.property != nullptr) + { + LOG_ERROR("Property '{}' is currently linked (to property '{}'). Unlink it first before setting its value!", m_typeData.name, m_incomingLink.property->getName()); + return false; + } + + if (!TypeUtils::IsPrimitiveType(m_typeData.type)) + { + LOG_ERROR("Property '{}' is not a primitive type, can't set its value directly!", m_typeData.name); + return false; + } + + if (value.index() != m_value.index()) + { + LOG_ERROR("Invalid type when setting property '{}', correct type is '{}'", m_typeData.name, GetLuaPrimitiveTypeName(m_typeData.type)); + return false; + } + + if (std::holds_alternative(value)) + { + // Lua uses (by default) double for internal storage of numerical values. + // IEEE 754 64-bit double can represent higher integers than this (DBL_MAX) but this is the maximum + // for which double can represent this value and all values below correctly + static constexpr auto maxIntegerAsDouble = static_cast(1LLU << 53u); + const auto int64Value = std::get(value); + if (int64Value > maxIntegerAsDouble || int64Value < -maxIntegerAsDouble) + { + LOG_ERROR("Invalid value when setting property '{}', Lua cannot handle full range of 64-bit integer, trying to set '{}' which is out of this range!", + m_typeData.name, int64Value); + return false; + } + } + + // Marks corresponding node dirty if value changed + const bool valueChanged = setValue(std::move(value)); + // TODO Violin possibly remove interface properties from this check, add tests first that interface objects dont need to be set + // dirty if their inputs were set + if (valueChanged || m_semantics == EPropertySemantics::AnimationInput || m_semantics == EPropertySemantics::BindingInput || m_semantics == EPropertySemantics::Interface) + { + m_logicNode->setDirty(true); + } + + return true; + } + + bool PropertyImpl::bindingInputHasNewValue() const + { + // TODO Violin can we make this assert the bindings semantics? + return m_bindingInputHasNewValue; + } + + bool PropertyImpl::checkForBindingInputNewValueAndReset() + { + // TODO Violin can we make this assert the bindings semantics? + const bool newValue = m_bindingInputHasNewValue; + m_bindingInputHasNewValue = false; + return newValue; + } + + bool PropertyImpl::setValue(PropertyValue value) + { + assert(m_value.index() == value.index()); + assert(TypeUtils::IsPrimitiveType(m_typeData.type)); + + if (m_semantics == EPropertySemantics::BindingInput) + { + m_bindingInputHasNewValue = true; + } + + const bool valueChanged = (m_value != value); + + m_value = std::move(value); + + return valueChanged; + } + + void PropertyImpl::setPropertyInstance(Property& property) + { + assert(m_propertyInstance == nullptr); + m_propertyInstance = &property; + } + + Property& PropertyImpl::getPropertyInstance() + { + assert(m_propertyInstance != nullptr); + return *m_propertyInstance; + } + + const Property& PropertyImpl::getPropertyInstance() const + { + assert(m_propertyInstance != nullptr); + return *m_propertyInstance; + } + + void PropertyImpl::setLogicNode(LogicNodeImpl& logicNode) + { + assert(m_logicNode == nullptr && "Properties are not transferrable across logic nodes!"); + m_logicNode = &logicNode; + for (auto& child : m_children) + { + child->m_impl->setLogicNode(logicNode); + } + } + + LogicNodeImpl& PropertyImpl::getLogicNode() + { + assert(m_logicNode != nullptr); + return *m_logicNode; + } + + const LogicNodeImpl& PropertyImpl::getLogicNode() const + { + assert(m_logicNode != nullptr); + return *m_logicNode; + } + + bool PropertyImpl::isInput() const + { + return m_semantics == EPropertySemantics::ScriptInput || m_semantics == EPropertySemantics::BindingInput || m_semantics == EPropertySemantics::AnimationInput || m_semantics == EPropertySemantics::Interface; + } + + bool PropertyImpl::isOutput() const + { + return m_semantics == EPropertySemantics::ScriptOutput || m_semantics == EPropertySemantics::AnimationOutput || m_semantics == EPropertySemantics::Interface; + } + + EPropertySemantics PropertyImpl::getPropertySemantics() const + { + return m_semantics; + } + + const PropertyValue& PropertyImpl::getValue() const + { + return m_value; + } + + bool PropertyImpl::isLinked() const + { + return (m_incomingLink.property != nullptr) || !m_outgoingLinks.empty(); + } + + bool PropertyImpl::hasIncomingLink() const + { + return (m_incomingLink.property != nullptr); + } + + bool PropertyImpl::hasOutgoingLink() const + { + return !m_outgoingLinks.empty(); + } + + const PropertyImpl::Link& PropertyImpl::getIncomingLink() const + { + assert(isInput()); + return m_incomingLink; + } + + const std::vector& PropertyImpl::getOutgoingLinks() const + { + assert(isOutput()); + return m_outgoingLinks; + } + + void PropertyImpl::setIncomingLink(PropertyImpl& output, bool isWeakLink) + { + assert(TypeUtils::IsPrimitiveType(getType())); + assert(TypeUtils::IsPrimitiveType(output.getType())); + assert(m_incomingLink.property == nullptr); + assert(std::find_if(output.m_outgoingLinks.begin(), output.m_outgoingLinks.end(), [this](const auto& p) { + return p.property == this; }) == output.m_outgoingLinks.end()); + + output.m_outgoingLinks.push_back({ this, isWeakLink }); + m_incomingLink = { &output, isWeakLink }; + } + + void PropertyImpl::resetIncomingLink() + { + assert(isInput() && m_incomingLink.property != nullptr); + auto& srcPropertyLinks = m_incomingLink.property->m_outgoingLinks; + auto linkIter = std::find_if(srcPropertyLinks.begin(), srcPropertyLinks.end(), [this](const auto& p) { return p.property == this; }); + assert(linkIter != srcPropertyLinks.end()); + srcPropertyLinks.erase(linkIter); + m_incomingLink = { nullptr, false }; + } + + void PropertyImpl::initializeBindingInputValue(PropertyValue value) + { + setValue(std::move(value)); + m_bindingInputHasNewValue = false; + } + + PropertyUniquePtr PropertyImpl::CreateProperty(std::unique_ptr impl) + { + assert(impl); + return PropertyUniquePtr{ new Property{ std::move(impl) }, [](Property* p) { delete p; } }; + } +} diff --git a/client/logic/lib/impl/PropertyImpl.h b/client/logic/lib/impl/PropertyImpl.h new file mode 100644 index 000000000..687e0f958 --- /dev/null +++ b/client/logic/lib/impl/PropertyImpl.h @@ -0,0 +1,160 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/EPropertyType.h" +#include "internals/EPropertySemantics.h" +#include "internals/SerializationMap.h" +#include "internals/DeserializationMap.h" +#include "internals/TypeData.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ramses +{ + class Property; +} +namespace rlogic_serialization +{ + struct Property; +} + +namespace flatbuffers +{ + template struct Offset; + class FlatBufferBuilder; +} + +namespace ramses::internal +{ + class LogicNodeImpl; + class ErrorReporting; + + using PropertyValue = std::variant; + using PropertyUniquePtr = std::unique_ptr>; + using PropertyList = std::vector; + + class PropertyImpl + { + public: + PropertyImpl(HierarchicalTypeData type, EPropertySemantics semantics); + PropertyImpl(HierarchicalTypeData type, EPropertySemantics semantics, PropertyValue initialValue); + + [[nodiscard]] static flatbuffers::Offset Serialize( + const PropertyImpl& prop, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::Property& prop, + EPropertySemantics semantics, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + + // Move-able (noexcept); Not copy-able + ~PropertyImpl() noexcept; + PropertyImpl& operator=(PropertyImpl&& other) noexcept = default; + PropertyImpl(PropertyImpl&& other) noexcept = default; + PropertyImpl& operator=(const PropertyImpl& other) = delete; + PropertyImpl(const PropertyImpl& other) = delete; + + [[nodiscard]] size_t getChildCount() const; + [[nodiscard]] EPropertyType getType() const; + [[nodiscard]] std::string_view getName() const; + + [[nodiscard]] bool bindingInputHasNewValue() const; + [[nodiscard]] bool checkForBindingInputNewValueAndReset(); + + [[nodiscard]] const Property* getChild(size_t index) const; + + // TODO Violin these 3 methods have redundancy, refactor + [[nodiscard]] bool isInput() const; + [[nodiscard]] bool isOutput() const; + [[nodiscard]] EPropertySemantics getPropertySemantics() const; + [[nodiscard]] bool isLinked() const; + [[nodiscard]] bool hasIncomingLink() const; + [[nodiscard]] bool hasOutgoingLink() const; + + [[nodiscard]] Property* getChild(size_t index); + [[nodiscard]] Property* getChild(std::string_view name); + [[nodiscard]] const Property* getChild(std::string_view name) const; + [[nodiscard]] bool hasChild(std::string_view name) const; + + [[nodiscard]] std::vector collectLeafChildren() const; + + // Public API access - only ever called by user, full error check and logs + template + [[nodiscard]] std::optional getValue_PublicApi() const; + [[nodiscard]] bool setValue_PublicApi(PropertyValue value); + + // Generic setter. Can optionally skip dirty-check + bool setValue(PropertyValue value); + // Special setter for binding value init + void initializeBindingInputValue(PropertyValue value); + + // Generic getter for use in other non-template code + [[nodiscard]] const PropertyValue& getValue() const; + // std::get wrapper for use in template code + template + [[nodiscard]] const T& getValueAs() const + { + return std::get(m_value); + } + + void setPropertyInstance(Property& property); + [[nodiscard]] Property& getPropertyInstance(); + [[nodiscard]] const Property& getPropertyInstance() const; + + void setLogicNode(LogicNodeImpl& logicNode); + [[nodiscard]] LogicNodeImpl& getLogicNode(); + [[nodiscard]] const LogicNodeImpl& getLogicNode() const; + + // Link handling + struct Link + { + PropertyImpl* property = nullptr; + bool isWeakLink = false; + }; + + [[nodiscard]] const Link& getIncomingLink() const; + [[nodiscard]] const std::vector& getOutgoingLinks() const; + + void setIncomingLink(PropertyImpl& output, bool isWeakLink); + void resetIncomingLink(); + + static PropertyUniquePtr CreateProperty(std::unique_ptr impl); + + private: + TypeData m_typeData; + PropertyList m_children; + PropertyValue m_value; + + Link m_incomingLink; + std::vector m_outgoingLinks; + + Property* m_propertyInstance = nullptr; + LogicNodeImpl* m_logicNode = nullptr; + + bool m_bindingInputHasNewValue = false; + EPropertySemantics m_semantics; + + [[nodiscard]] static flatbuffers::Offset SerializeRecursive( + const PropertyImpl& prop, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + }; +} diff --git a/client/logic/lib/impl/RamsesAppearanceBinding.cpp b/client/logic/lib/impl/RamsesAppearanceBinding.cpp new file mode 100644 index 000000000..f9722f419 --- /dev/null +++ b/client/logic/lib/impl/RamsesAppearanceBinding.cpp @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + + +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "impl/RamsesAppearanceBindingImpl.h" + +namespace ramses +{ + RamsesAppearanceBinding::RamsesAppearanceBinding(std::unique_ptr impl) noexcept + : RamsesBinding(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_appearanceBinding{ static_cast(RamsesBinding::m_impl) } + { + } + + ramses::Appearance& RamsesAppearanceBinding::getRamsesAppearance() const + { + return m_appearanceBinding.getRamsesAppearance(); + } +} diff --git a/client/logic/lib/impl/RamsesAppearanceBindingImpl.cpp b/client/logic/lib/impl/RamsesAppearanceBindingImpl.cpp new file mode 100644 index 000000000..10cf47cfd --- /dev/null +++ b/client/logic/lib/impl/RamsesAppearanceBindingImpl.cpp @@ -0,0 +1,271 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/RamsesAppearanceBindingImpl.h" + +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/Effect.h" +#include "ramses-client-api/UniformInput.h" + +#include "ramses-logic/EPropertyType.h" +#include "ramses-logic/Property.h" + +#include "impl/PropertyImpl.h" +#include "impl/LoggerImpl.h" + +#include "internals/RamsesHelper.h" +#include "internals/ErrorReporting.h" +#include "internals/TypeUtils.h" +#include "internals/RamsesObjectResolver.h" + +#include "generated/RamsesAppearanceBindingGen.h" + +namespace ramses::internal +{ + RamsesAppearanceBindingImpl::RamsesAppearanceBindingImpl(ramses::Appearance& ramsesAppearance, std::string_view name, uint64_t id) + : RamsesBindingImpl(name, id) + , m_ramsesAppearance(ramsesAppearance) + { + const auto& effect = m_ramsesAppearance.get().getEffect(); + const size_t uniformCount = effect.getUniformInputCount(); + m_uniformIndices.reserve(uniformCount); + + // create mapping from property children indices to uniform inputs, this must match properties (either created or deserialized) + for (size_t i = 0; i < uniformCount; ++i) + { + ramses::UniformInput uniformInput; + ramses::status_t result = effect.getUniformInput(i, uniformInput); + assert(result == ramses::StatusOK); + assert(uniformInput.isValid()); + (void)result; + + if (GetPropertyTypeForUniform(uniformInput)) + m_uniformIndices.push_back(i); + } + } + + void RamsesAppearanceBindingImpl::createRootProperties() + { + const auto& effect = m_ramsesAppearance.get().getEffect(); + const size_t uniformCount = effect.getUniformInputCount(); + + std::vector bindingInputs; + bindingInputs.reserve(uniformCount); + + for (size_t i = 0; i < uniformCount; ++i) + { + ramses::UniformInput uniformInput; + effect.getUniformInput(i, uniformInput); + const std::optional convertedType = GetPropertyTypeForUniform(uniformInput); + + // TODO Violin handle all types eventually (need some more breaking ramses features for that) + if (convertedType) + { + // Non-array case + if (uniformInput.getElementCount() == 1) + { + bindingInputs.emplace_back(MakeType(uniformInput.getName(), *convertedType)); + } + // Array case + else + { + bindingInputs.emplace_back(MakeArray(uniformInput.getName(), uniformInput.getElementCount(), *convertedType)); + } + } + } + + HierarchicalTypeData bindingInputsType(TypeData{ "", EPropertyType::Struct }, bindingInputs); + + setRootInputs(std::make_unique(bindingInputsType, EPropertySemantics::BindingInput)); + } + + flatbuffers::Offset RamsesAppearanceBindingImpl::Serialize( + const RamsesAppearanceBindingImpl& binding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap) + { + auto ramsesReference = RamsesBindingImpl::SerializeRamsesReference(binding.m_ramsesAppearance, builder); + + const auto logicObject = LogicObjectImpl::Serialize(binding, builder); + const auto propertyObject = PropertyImpl::Serialize(*binding.getInputs()->m_impl, builder, serializationMap); + auto ramsesBinding = rlogic_serialization::CreateRamsesBinding(builder, + logicObject, + ramsesReference, + propertyObject); + builder.Finish(ramsesBinding); + + rlogic_serialization::ResourceId parentEffectResourceId; + parentEffectResourceId = rlogic_serialization::ResourceId(binding.m_ramsesAppearance.get().getEffect().getResourceId().lowPart, binding.m_ramsesAppearance.get().getEffect().getResourceId().highPart); + + auto ramsesAppearanceBinding = rlogic_serialization::CreateRamsesAppearanceBinding(builder, + ramsesBinding, + &parentEffectResourceId + ); + builder.Finish(ramsesAppearanceBinding); + + return ramsesAppearanceBinding; + } + + std::unique_ptr RamsesAppearanceBindingImpl::Deserialize( + const rlogic_serialization::RamsesAppearanceBinding& appearanceBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + if (!appearanceBinding.base()) + { + errorReporting.add("Fatal error during loading of RamsesAppearanceBinding from serialized data: missing base class info!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(appearanceBinding.base()->base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of RamsesAppearanceBinding from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!appearanceBinding.base()->rootInput()) + { + errorReporting.add("Fatal error during loading of RamsesAppearanceBinding from serialized data: missing root input!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr deserializedRootInput = PropertyImpl::Deserialize(*appearanceBinding.base()->rootInput(), EPropertySemantics::BindingInput, errorReporting, deserializationMap); + + if (!deserializedRootInput) + { + return nullptr; + } + + if (deserializedRootInput->getType() != EPropertyType::Struct) + { + errorReporting.add("Fatal error during loading of RamsesAppearanceBinding from serialized data: root input has unexpected type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const auto* boundObject = appearanceBinding.base()->boundRamsesObject(); + if (!boundObject) + { + errorReporting.add("Fatal error during loading of RamsesAppearanceBinding from serialized data: no reference to appearance!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const ramses::sceneObjectId_t objectId(boundObject->objectId()); + ramses::Appearance* resolvedAppearance = ramsesResolver.findRamsesAppearanceInScene(name, objectId); + if (!resolvedAppearance) + { + // TODO Violin improve error reporting for this particular error (it's reported in ramsesResolver currently): provide better message and scene/app ids + return nullptr; + } + + const ramses::Effect& effect = resolvedAppearance->getEffect(); + const ramses::resourceId_t effectResourceId = effect.getResourceId(); + if (effectResourceId.lowPart != appearanceBinding.parentEffectId()->resourceIdLow() || effectResourceId.highPart != appearanceBinding.parentEffectId()->resourceIdHigh()) + { + errorReporting.add("Fatal error during loading of RamsesAppearanceBinding from serialized data: effect signature doesn't match after loading!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto binding = std::make_unique(*resolvedAppearance, name, id); + binding->setUserId(userIdHigh, userIdLow); + binding->setRootInputs(std::move(deserializedRootInput)); + + return binding; + } + + std::optional RamsesAppearanceBindingImpl::update() + { + const size_t childCount = getInputs()->getChildCount(); + for (size_t i = 0; i < childCount; ++i) + { + setInputValueToUniform(i); + } + + return std::nullopt; + } + + void RamsesAppearanceBindingImpl::setInputValueToUniform(size_t inputIndex) + { + PropertyImpl& inputProperty = *getInputs()->getChild(inputIndex)->m_impl; + const EPropertyType propertyType = inputProperty.getType(); + + if (TypeUtils::IsPrimitiveType(propertyType)) + { + if (inputProperty.checkForBindingInputNewValueAndReset()) + { + ramses::UniformInput uniform; + m_ramsesAppearance.get().getEffect().getUniformInput(m_uniformIndices[inputIndex], uniform); + std::visit([&](auto v) { + using RamsesValueType = typename RlogicTypeToRamsesType>>::TYPE; + if constexpr (ramses::IsUniformInputDataType()) + { + m_ramsesAppearance.get().setInputValue(uniform, RamsesValueType{ std::move(v) }); + } + else + { + assert(false && "This should never happen"); + } + }, inputProperty.getValue()); + } + } + else + { + assert(propertyType == EPropertyType::Array); + + // A new value on any of the array element causes the whole array to be updated + // Ramses does not allow partial updates so this is the only option here + bool anyArrayElementWasSet = false; + const size_t arraySize = inputProperty.getChildCount(); + for (size_t i = 0; i < arraySize; ++i) + { + if (inputProperty.getChild(i)->m_impl->checkForBindingInputNewValueAndReset()) + anyArrayElementWasSet = true; + } + + if (anyArrayElementWasSet) + { + ramses::UniformInput uniform; + m_ramsesAppearance.get().getEffect().getUniformInput(m_uniformIndices[inputIndex], uniform); + + std::visit([&](const auto& v) { + using ValueType = std::remove_const_t>; + using RamsesValueType = typename RlogicTypeToRamsesType>>::TYPE; + if constexpr (ramses::IsUniformInputDataType()) + { + std::vector values; + values.reserve(inputProperty.getChildCount()); + for (size_t i = 0u; i < inputProperty.getChildCount(); ++i) + values.push_back(RamsesValueType{ *inputProperty.getChild(i)->get() }); + m_ramsesAppearance.get().setInputValue(uniform, values.size(), values.data()); + } + else + assert(false && "This should never happen"); + }, inputProperty.getChild(0u)->m_impl->getValue()); // small trick to determine the element type so that the flattened array can be declared in templated code + } + } + } + + ramses::Appearance& RamsesAppearanceBindingImpl::getRamsesAppearance() const + { + return m_ramsesAppearance; + } + + std::optional RamsesAppearanceBindingImpl::GetPropertyTypeForUniform(const ramses::UniformInput& uniform) + { + assert(uniform.isValid()); + // Can't bind semantic uniforms + if (uniform.getSemantics() != ramses::EEffectUniformSemantic::Invalid) + return std::nullopt; + + return ConvertRamsesUniformTypeToPropertyType(*uniform.getDataType()); + } +} diff --git a/client/logic/lib/impl/RamsesAppearanceBindingImpl.h b/client/logic/lib/impl/RamsesAppearanceBindingImpl.h new file mode 100644 index 000000000..64d36dfed --- /dev/null +++ b/client/logic/lib/impl/RamsesAppearanceBindingImpl.h @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/RamsesBindingImpl.h" +#include "internals/SerializationMap.h" +#include "internals/DeserializationMap.h" + +#include "ramses-logic/EPropertyType.h" +#include "ramses-client-api/UniformInput.h" + +#include +#include +#include + +namespace ramses +{ + class Appearance; + class UniformInput; +} + +namespace rlogic_serialization +{ + struct RamsesAppearanceBinding; +} + +namespace flatbuffers +{ + class FlatBufferBuilder; + template struct Offset; +} + +namespace ramses::internal +{ + class PropertyImpl; + class ErrorReporting; + class IRamsesObjectResolver; + + class RamsesAppearanceBindingImpl : public RamsesBindingImpl + { + public: + explicit RamsesAppearanceBindingImpl(ramses::Appearance& ramsesAppearance, std::string_view name, uint64_t id); + + [[nodiscard]] static flatbuffers::Offset Serialize( + const RamsesAppearanceBindingImpl& binding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::RamsesAppearanceBinding& appearanceBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + [[nodiscard]] ramses::Appearance& getRamsesAppearance() const; + + std::optional update() override; + + void createRootProperties() final; + + private: + std::reference_wrapper m_ramsesAppearance; + std::vector m_uniformIndices; + + void setInputValueToUniform(size_t inputIndex); + + static std::optional GetPropertyTypeForUniform(const ramses::UniformInput& uniform); + }; +} diff --git a/client/ramses-client/ramses-client-api/DataObject.cpp b/client/logic/lib/impl/RamsesBinding.cpp similarity index 63% rename from client/ramses-client/ramses-client-api/DataObject.cpp rename to client/logic/lib/impl/RamsesBinding.cpp index 29bee3097..1fb02dcb1 100644 --- a/client/ramses-client/ramses-client-api/DataObject.cpp +++ b/client/logic/lib/impl/RamsesBinding.cpp @@ -1,26 +1,19 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH +// Copyright (C) 2020 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -// API -#include "ramses-client-api/DataObject.h" +#include "ramses-logic/RamsesBinding.h" -//internal -#include "DataObjectImpl.h" +#include "impl/RamsesBindingImpl.h" namespace ramses { - DataObject::DataObject(DataObjectImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - DataObject::~DataObject() + RamsesBinding::RamsesBinding(std::unique_ptr impl) noexcept + : LogicNode(std::move(impl)) { } } diff --git a/client/logic/lib/impl/RamsesBindingImpl.cpp b/client/logic/lib/impl/RamsesBindingImpl.cpp new file mode 100644 index 000000000..4c7319a31 --- /dev/null +++ b/client/logic/lib/impl/RamsesBindingImpl.cpp @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/RamsesBindingImpl.h" +#include "ramses-logic/Property.h" + +#include "ramses-client-api/SceneObject.h" + +#include "generated/RamsesReferenceGen.h" + +namespace ramses::internal +{ + RamsesBindingImpl::RamsesBindingImpl(std::string_view name, uint64_t id) noexcept + : LogicNodeImpl(name, id) + { + // Bindings are not supposed to do anything unless user set an actual value to them + // Thus, they are not dirty by default! + setDirty(false); + } + + flatbuffers::Offset RamsesBindingImpl::SerializeRamsesReference(const ramses::SceneObject& object, flatbuffers::FlatBufferBuilder& builder) + { + const ramses::sceneObjectId_t ramsesObjectId = object.getSceneObjectId(); + const ramses::ERamsesObjectType ramsesObjectType = object.getType(); + + auto ramsesRef = rlogic_serialization::CreateRamsesReference(builder, + ramsesObjectId.getValue(), + static_cast(ramsesObjectType) + ); + builder.Finish(ramsesRef); + + return ramsesRef; + } + + void RamsesBindingImpl::setRootInputs(std::unique_ptr rootInputs) + { + setRootProperties(std::move(rootInputs), {}); + } +} diff --git a/client/logic/lib/impl/RamsesBindingImpl.h b/client/logic/lib/impl/RamsesBindingImpl.h new file mode 100644 index 000000000..085093f1c --- /dev/null +++ b/client/logic/lib/impl/RamsesBindingImpl.h @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/LogicNodeImpl.h" + +namespace ramses +{ + class SceneObject; +} + +namespace flatbuffers +{ + template struct Offset; + class FlatBufferBuilder; +} + +namespace rlogic_serialization +{ + struct RamsesReference; +} + +namespace ramses::internal +{ + class RamsesBindingImpl : public LogicNodeImpl + { + public: + explicit RamsesBindingImpl(std::string_view name, uint64_t id) noexcept; + + protected: + // Used by subclasses to handle serialization + [[nodiscard]] static flatbuffers::Offset SerializeRamsesReference(const ramses::SceneObject& object, flatbuffers::FlatBufferBuilder& builder); + + void setRootInputs(std::unique_ptr rootInputs); + + private: + using LogicNodeImpl::setRootProperties; + }; +} diff --git a/client/logic/lib/impl/RamsesCameraBinding.cpp b/client/logic/lib/impl/RamsesCameraBinding.cpp new file mode 100644 index 000000000..ed0bf8535 --- /dev/null +++ b/client/logic/lib/impl/RamsesCameraBinding.cpp @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/RamsesCameraBinding.h" +#include "impl/RamsesCameraBindingImpl.h" + +namespace ramses +{ + RamsesCameraBinding::RamsesCameraBinding(std::unique_ptr impl) noexcept + : RamsesBinding(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_cameraBinding{ static_cast(RamsesBinding::m_impl) } + { + } + + ramses::Camera& RamsesCameraBinding::getRamsesCamera() const + { + return m_cameraBinding.getRamsesCamera(); + } +} diff --git a/client/logic/lib/impl/RamsesCameraBindingImpl.cpp b/client/logic/lib/impl/RamsesCameraBindingImpl.cpp new file mode 100644 index 000000000..a1b9221e5 --- /dev/null +++ b/client/logic/lib/impl/RamsesCameraBindingImpl.cpp @@ -0,0 +1,299 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/RamsesCameraBindingImpl.h" + +#include "ramses-utils.h" +#include "ramses-client-api/Camera.h" +#include "ramses-client-api/PerspectiveCamera.h" + +#include "ramses-logic/EPropertyType.h" +#include "ramses-logic/Property.h" + +#include "impl/PropertyImpl.h" +#include "impl/LoggerImpl.h" + +#include "internals/RamsesHelper.h" +#include "internals/ErrorReporting.h" +#include "internals/RamsesObjectResolver.h" + +#include "generated/RamsesCameraBindingGen.h" + +namespace ramses::internal +{ + RamsesCameraBindingImpl::RamsesCameraBindingImpl(ramses::Camera& ramsesCamera, bool withFrustumPlanes, std::string_view name, uint64_t id) + : RamsesBindingImpl(name, id) + , m_ramsesCamera(ramsesCamera) + , m_hasFrustumPlanesProperties{ ramsesCamera.isOfType(ramses::ERamsesObjectType::OrthographicCamera) || withFrustumPlanes } + { + } + + void RamsesCameraBindingImpl::createRootProperties() + { + std::vector frustumPlanes = { + TypeData{ "nearPlane", EPropertyType::Float }, + TypeData{ "farPlane", EPropertyType::Float }, + }; + + if (m_hasFrustumPlanesProperties) + { + // Attention! This order is important - it has to match the indices in ECameraFrustumPlanesPropertyStaticIndex + frustumPlanes.emplace_back("leftPlane", EPropertyType::Float); + frustumPlanes.emplace_back("rightPlane", EPropertyType::Float); + frustumPlanes.emplace_back("bottomPlane", EPropertyType::Float); + frustumPlanes.emplace_back("topPlane", EPropertyType::Float); + } + else + { + // Attention! This order is important - it has to match the indices in EPerspectiveCameraFrustumPropertyStaticIndex + frustumPlanes.emplace_back("fieldOfView", EPropertyType::Float); + frustumPlanes.emplace_back("aspectRatio", EPropertyType::Float); + } + + HierarchicalTypeData cameraBindingInputs( + TypeData{"", EPropertyType::Struct}, + { + MakeStruct("viewport", + { + // Attention! This order is important - it has to match the indices in ECameraViewportPropertyStaticIndex + TypeData{"offsetX", EPropertyType::Int32}, + TypeData{"offsetY", EPropertyType::Int32}, + TypeData{"width", EPropertyType::Int32}, + TypeData{"height", EPropertyType::Int32} + } + ), + MakeStruct("frustum", frustumPlanes), + } + ); + + setRootInputs(std::make_unique(cameraBindingInputs, EPropertySemantics::BindingInput)); + + ApplyRamsesValuesToInputProperties(*this); + } + + flatbuffers::Offset RamsesCameraBindingImpl::Serialize( + const RamsesCameraBindingImpl& cameraBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap) + { + auto ramsesReference = RamsesBindingImpl::SerializeRamsesReference(cameraBinding.m_ramsesCamera, builder); + + const auto logicObject = LogicObjectImpl::Serialize(cameraBinding, builder); + const auto propertyObject = PropertyImpl::Serialize(*cameraBinding.getInputs()->m_impl, builder, serializationMap); + auto ramsesBinding = rlogic_serialization::CreateRamsesBinding(builder, + logicObject, + ramsesReference, + propertyObject); + builder.Finish(ramsesBinding); + + auto ramsesCameraBinding = rlogic_serialization::CreateRamsesCameraBinding(builder, ramsesBinding); + builder.Finish(ramsesCameraBinding); + + return ramsesCameraBinding; + } + + std::unique_ptr RamsesCameraBindingImpl::Deserialize( + const rlogic_serialization::RamsesCameraBinding& cameraBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + if (!cameraBinding.base()) + { + errorReporting.add("Fatal error during loading of RamsesCameraBinding from serialized data: missing base class info!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(cameraBinding.base()->base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of RamsesCameraBinding from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!cameraBinding.base()->rootInput()) + { + errorReporting.add("Fatal error during loading of RamsesCameraBinding from serialized data: missing root input!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr deserializedRootInput = PropertyImpl::Deserialize(*cameraBinding.base()->rootInput(), EPropertySemantics::BindingInput, errorReporting, deserializationMap); + if (!deserializedRootInput) + return nullptr; + + if (deserializedRootInput->getType() != EPropertyType::Struct) + { + errorReporting.add("Fatal error during loading of RamsesCameraBinding from serialized data: root input has unexpected type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const auto frustumInputProp = deserializedRootInput->getChild("frustum"); + if (!frustumInputProp || !(frustumInputProp->getChildCount() == 4u || frustumInputProp->getChildCount() == 6u)) + { + errorReporting.add("Fatal error during loading of RamsesCameraBinding from serialized data: missing or invalid input properties!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + const bool hasFrustumPlanesProperties = (frustumInputProp->getChildCount() == 6u); + + const auto* boundObject = cameraBinding.base()->boundRamsesObject(); + if (!boundObject) + { + errorReporting.add("Fatal error during loading of RamsesCameraBinding from serialized data: no reference to ramses camera!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const ramses::sceneObjectId_t objectId(boundObject->objectId()); + + ramses::Camera* resolvedCamera = ramsesResolver.findRamsesCameraInScene(name, objectId); + if (!resolvedCamera) + return nullptr; + + if (static_cast(resolvedCamera->getType()) != boundObject->objectType()) + { + errorReporting.add("Fatal error during loading of RamsesCameraBinding from serialized data: loaded type does not match referenced camera type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto binding = std::make_unique(*resolvedCamera, hasFrustumPlanesProperties, name, id); + binding->setUserId(userIdHigh, userIdLow); + binding->setRootInputs(std::move(deserializedRootInput)); + + ApplyRamsesValuesToInputProperties(*binding); + + return binding; + } + + std::optional RamsesCameraBindingImpl::update() + { + ramses::status_t status = ramses::StatusOK; + PropertyImpl& vpProperties = *getInputs()->getChild(static_cast(ECameraPropertyStructStaticIndex::Viewport))->m_impl; + + PropertyImpl& vpOffsetX = *vpProperties.getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortOffsetX))->m_impl; + PropertyImpl& vpOffsetY = *vpProperties.getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortOffsetY))->m_impl; + PropertyImpl& vpWidth = *vpProperties.getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortWidth))->m_impl; + PropertyImpl& vpHeight = *vpProperties.getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortHeight))->m_impl; + if (vpOffsetX.checkForBindingInputNewValueAndReset() + || vpOffsetY.checkForBindingInputNewValueAndReset() + || vpWidth.checkForBindingInputNewValueAndReset() + || vpHeight.checkForBindingInputNewValueAndReset()) + { + const int32_t vpX = vpOffsetX.getValueAs(); + const int32_t vpY = vpOffsetY.getValueAs(); + const int32_t vpW = vpWidth.getValueAs(); + const int32_t vpH = vpHeight.getValueAs(); + + if (vpW <= 0 || vpH <= 0) + { + return LogicNodeRuntimeError{ fmt::format("Camera viewport size must be positive! (width: {}; height: {})", vpW, vpH) }; + } + + status = m_ramsesCamera.get().setViewport(vpX, vpY, vpW, vpH); + + if (status != ramses::StatusOK) + { + return LogicNodeRuntimeError{m_ramsesCamera.get().getStatusMessage(status)}; + } + } + + PropertyImpl& frustum = *getInputs()->getChild(static_cast(ECameraPropertyStructStaticIndex::Frustum))->m_impl; + + // Index of Perspective Frustum Properties is used, but wouldn't matter as Ortho Camera indeces are the same for these two properties + PropertyImpl& nearPlane = *frustum.getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::NearPlane))->m_impl; + PropertyImpl& farPlane = *frustum.getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::FarPlane))->m_impl; + + if (m_hasFrustumPlanesProperties) + { + PropertyImpl& leftPlane = *frustum.getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::LeftPlane))->m_impl; + PropertyImpl& rightPlane = *frustum.getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::RightPlane))->m_impl; + PropertyImpl& bottomPlane = *frustum.getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::BottomPlane))->m_impl; + PropertyImpl& topPlane = *frustum.getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::TopPlane))->m_impl; + + if (nearPlane.checkForBindingInputNewValueAndReset() + || farPlane.checkForBindingInputNewValueAndReset() + || leftPlane.checkForBindingInputNewValueAndReset() + || rightPlane.checkForBindingInputNewValueAndReset() + || bottomPlane.checkForBindingInputNewValueAndReset() + || topPlane.checkForBindingInputNewValueAndReset()) + { + status = m_ramsesCamera.get().setFrustum( + leftPlane.getValueAs(), + rightPlane.getValueAs(), + bottomPlane.getValueAs(), + topPlane.getValueAs(), + nearPlane.getValueAs(), + farPlane.getValueAs()); + + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesCamera.get().getStatusMessage(status) }; + } + } + else + { + PropertyImpl& fov = *frustum.getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::FieldOfView))->m_impl; + PropertyImpl& aR = *frustum.getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::AspectRatio))->m_impl; + + if (nearPlane.checkForBindingInputNewValueAndReset() + || farPlane.checkForBindingInputNewValueAndReset() + || fov.checkForBindingInputNewValueAndReset() + || aR.checkForBindingInputNewValueAndReset()) + { + assert(m_ramsesCamera.get().isOfType(ramses::ERamsesObjectType::PerspectiveCamera)); + auto* perspectiveCam = ramses::RamsesUtils::TryConvert(m_ramsesCamera.get()); + status = perspectiveCam->setFrustum(fov.getValueAs(), aR.getValueAs(), nearPlane.getValueAs(), farPlane.getValueAs()); + + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesCamera.get().getStatusMessage(status) }; + } + } + + return std::nullopt; + } + + bool RamsesCameraBindingImpl::hasFrustumPlanesProperties() const + { + return m_hasFrustumPlanesProperties; + } + + ramses::Camera& RamsesCameraBindingImpl::getRamsesCamera() const + { + return m_ramsesCamera; + } + + void RamsesCameraBindingImpl::ApplyRamsesValuesToInputProperties(RamsesCameraBindingImpl& binding) + { + const auto& ramsesCamera = binding.getRamsesCamera(); + + // Initializes input values with values from ramses camera silently (no dirty mechanism triggered) + PropertyImpl& viewport = *binding.getInputs()->getChild(static_cast(ECameraPropertyStructStaticIndex::Viewport))->m_impl; + viewport.getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortOffsetX))->m_impl->initializeBindingInputValue(PropertyValue{ ramsesCamera.getViewportX() }); + viewport.getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortOffsetY))->m_impl->initializeBindingInputValue(PropertyValue{ ramsesCamera.getViewportY() }); + viewport.getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortWidth))->m_impl->initializeBindingInputValue(PropertyValue{ static_cast(ramsesCamera.getViewportWidth()) }); + viewport.getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortHeight))->m_impl->initializeBindingInputValue(PropertyValue{ static_cast(ramsesCamera.getViewportHeight()) }); + + PropertyImpl& frustum = *binding.getInputs()->getChild(static_cast(ECameraPropertyStructStaticIndex::Frustum))->m_impl; + frustum.getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::NearPlane))->m_impl->initializeBindingInputValue(PropertyValue{ ramsesCamera.getNearPlane() }); + frustum.getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::FarPlane))->m_impl->initializeBindingInputValue(PropertyValue{ ramsesCamera.getFarPlane() }); + if (binding.hasFrustumPlanesProperties()) + { + frustum.getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::LeftPlane))->m_impl->initializeBindingInputValue(PropertyValue{ ramsesCamera.getLeftPlane() }); + frustum.getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::RightPlane))->m_impl->initializeBindingInputValue(PropertyValue{ ramsesCamera.getRightPlane() }); + frustum.getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::BottomPlane))->m_impl->initializeBindingInputValue(PropertyValue{ ramsesCamera.getBottomPlane() }); + frustum.getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::TopPlane))->m_impl->initializeBindingInputValue(PropertyValue{ ramsesCamera.getTopPlane() }); + } + else + { + assert(ramsesCamera.isOfType(ramses::ERamsesObjectType::PerspectiveCamera)); + const auto* perspectiveCam = ramses::RamsesUtils::TryConvert(ramsesCamera); + frustum.getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::FieldOfView))->m_impl->initializeBindingInputValue(PropertyValue{ perspectiveCam->getVerticalFieldOfView() }); + frustum.getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::AspectRatio))->m_impl->initializeBindingInputValue(PropertyValue{ perspectiveCam->getAspectRatio() }); + } + } +} diff --git a/client/logic/lib/impl/RamsesCameraBindingImpl.h b/client/logic/lib/impl/RamsesCameraBindingImpl.h new file mode 100644 index 000000000..e5d5ecfab --- /dev/null +++ b/client/logic/lib/impl/RamsesCameraBindingImpl.h @@ -0,0 +1,102 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/RamsesBindingImpl.h" +#include "internals/SerializationMap.h" +#include "internals/DeserializationMap.h" + +#include "ramses-client-api/RamsesObjectTypes.h" + +#include + +namespace ramses +{ + class Camera; +} + +namespace rlogic_serialization +{ + struct RamsesCameraBinding; +} + +namespace flatbuffers +{ + class FlatBufferBuilder; + template struct Offset; +} + +namespace ramses::internal +{ + class PropertyImpl; + class IRamsesObjectResolver; + class ErrorReporting; + + enum class ECameraPropertyStructStaticIndex : size_t + { + Viewport = 0, + Frustum = 1, + }; + + enum class ECameraViewportPropertyStaticIndex : size_t + { + ViewPortOffsetX = 0, + ViewPortOffsetY = 1, + ViewPortWidth = 2, + ViewPortHeight = 3, + }; + + enum class EPerspectiveCameraFrustumPropertyStaticIndex : size_t + { + NearPlane = 0, + FarPlane = 1, + FieldOfView = 2, + AspectRatio = 3, + }; + + enum class ECameraFrustumPlanesPropertyStaticIndex : size_t + { + NearPlane = 0, + FarPlane = 1, + LeftPlane = 2, + RightPlane = 3, + BottomPlane = 4, + TopPlane = 5, + }; + + class RamsesCameraBindingImpl : public RamsesBindingImpl + { + public: + explicit RamsesCameraBindingImpl(ramses::Camera& ramsesCamera, bool withFrustumPlanes, std::string_view name, uint64_t id); + + [[nodiscard]] static flatbuffers::Offset Serialize( + const RamsesCameraBindingImpl& cameraBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::RamsesCameraBinding& cameraBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + [[nodiscard]] ramses::Camera& getRamsesCamera() const; + [[nodiscard]] bool hasFrustumPlanesProperties() const; + + std::optional update() override; + + void createRootProperties() final; + + private: + std::reference_wrapper m_ramsesCamera; + bool m_hasFrustumPlanesProperties; + + static void ApplyRamsesValuesToInputProperties(RamsesCameraBindingImpl& binding); + }; +} diff --git a/client/logic/lib/impl/RamsesLogicVersion.cpp b/client/logic/lib/impl/RamsesLogicVersion.cpp new file mode 100644 index 000000000..5c109cbed --- /dev/null +++ b/client/logic/lib/impl/RamsesLogicVersion.cpp @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/RamsesLogicVersion.h" +#include "ramses-sdk-build-config.h" + +namespace ramses +{ + RamsesLogicVersion GetRamsesLogicVersion() + { + return RamsesLogicVersion + { + ramses_sdk::RAMSES_SDK_RAMSES_VERSION, + ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MAJOR_INT, + ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MINOR_INT, + ramses_sdk::RAMSES_SDK_PROJECT_VERSION_PATCH_INT, + }; + } +} diff --git a/client/logic/lib/impl/RamsesMeshNodeBinding.cpp b/client/logic/lib/impl/RamsesMeshNodeBinding.cpp new file mode 100644 index 000000000..4d814ddff --- /dev/null +++ b/client/logic/lib/impl/RamsesMeshNodeBinding.cpp @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "impl/RamsesMeshNodeBindingImpl.h" + +namespace ramses +{ + RamsesMeshNodeBinding::RamsesMeshNodeBinding(std::unique_ptr impl) noexcept + : RamsesBinding(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_meshNodeBinding{ static_cast(RamsesBinding::m_impl) } + { + } + + const ramses::MeshNode& RamsesMeshNodeBinding::getRamsesMeshNode() const + { + return m_meshNodeBinding.getRamsesMeshNode(); + } + + ramses::MeshNode& RamsesMeshNodeBinding::getRamsesMeshNode() + { + return m_meshNodeBinding.getRamsesMeshNode(); + } +} diff --git a/client/logic/lib/impl/RamsesMeshNodeBindingImpl.cpp b/client/logic/lib/impl/RamsesMeshNodeBindingImpl.cpp new file mode 100644 index 000000000..7ba844d4e --- /dev/null +++ b/client/logic/lib/impl/RamsesMeshNodeBindingImpl.cpp @@ -0,0 +1,189 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/RamsesMeshNodeBindingImpl.h" +#include "impl/PropertyImpl.h" +#include "ramses-client-api/MeshNode.h" +#include "ramses-logic/Property.h" +#include "ramses-utils.h" +#include "internals/ErrorReporting.h" +#include "internals/RamsesObjectResolver.h" +#include "generated/RamsesMeshNodeBindingGen.h" +#include "fmt/format.h" + +namespace ramses::internal +{ + RamsesMeshNodeBindingImpl::RamsesMeshNodeBindingImpl(ramses::MeshNode& ramsesMeshNode, std::string_view name, uint64_t id) + : RamsesBindingImpl{ name, id } + , m_ramsesMeshNode{ ramsesMeshNode } + { + } + + void RamsesMeshNodeBindingImpl::createRootProperties() + { + HierarchicalTypeData inputsType = MakeStruct("", { + TypeData{"vertexOffset", EPropertyType::Int32}, //EInputProperty::VertexOffset + TypeData{"indexOffset", EPropertyType::Int32}, //EInputProperty::IndexOffset + TypeData{"indexCount", EPropertyType::Int32}, //EInputProperty::IndexCount + TypeData{"instanceCount", EPropertyType::Int32} //EInputProperty::InstanceCount + }); + auto inputs = std::make_unique(std::move(inputsType), EPropertySemantics::BindingInput); + + setRootInputs(std::move(inputs)); + + ApplyRamsesValuesToInputProperties(*this, m_ramsesMeshNode); + } + + flatbuffers::Offset RamsesMeshNodeBindingImpl::Serialize( + const RamsesMeshNodeBindingImpl& meshNodeBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap) + { + const auto logicObject = LogicObjectImpl::Serialize(meshNodeBinding, builder); + const auto fbRamsesRef = RamsesBindingImpl::SerializeRamsesReference(meshNodeBinding.m_ramsesMeshNode, builder); + const auto propertyObject = PropertyImpl::Serialize(*meshNodeBinding.getInputs()->m_impl, builder, serializationMap); + auto fbRamsesBinding = rlogic_serialization::CreateRamsesBinding(builder, + logicObject, + fbRamsesRef, + propertyObject); + + auto fbMeshNodeBinding = rlogic_serialization::CreateRamsesMeshNodeBinding(builder, fbRamsesBinding); + builder.Finish(fbMeshNodeBinding); + + return fbMeshNodeBinding; + } + + std::unique_ptr RamsesMeshNodeBindingImpl::Deserialize( + const rlogic_serialization::RamsesMeshNodeBinding& meshNodeBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + if (!meshNodeBinding.base()) + { + errorReporting.add("Fatal error during loading of RamsesMeshNodeBinding from serialized data: missing base class info!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(meshNodeBinding.base()->base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of RamsesMeshNodeBinding from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!meshNodeBinding.base()->rootInput()) + { + errorReporting.add("Fatal error during loading of RamsesMeshNodeBinding from serialized data: missing root input!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr deserializedRootInput = PropertyImpl::Deserialize(*meshNodeBinding.base()->rootInput(), EPropertySemantics::BindingInput, errorReporting, deserializationMap); + if (!deserializedRootInput) + return nullptr; + + if (deserializedRootInput->getType() != EPropertyType::Struct || + deserializedRootInput->getChildCount() != size_t(EInputProperty::COUNT) || + deserializedRootInput->getChild(size_t(EInputProperty::VertexOffset))->getName() != "vertexOffset" || + deserializedRootInput->getChild(size_t(EInputProperty::IndexOffset))->getName() != "indexOffset" || + deserializedRootInput->getChild(size_t(EInputProperty::IndexCount))->getName() != "indexCount" || + deserializedRootInput->getChild(size_t(EInputProperty::InstanceCount))->getName() != "instanceCount") + { + errorReporting.add("Fatal error during loading of RamsesMeshNodeBinding from serialized data: corrupted root input!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const auto* boundObject = meshNodeBinding.base()->boundRamsesObject(); + if (!boundObject) + { + errorReporting.add("Fatal error during loading of RamsesMeshNodeBinding from serialized data: missing ramses object reference!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const ramses::sceneObjectId_t objectId{ boundObject->objectId() }; + ramses::SceneObject* ramsesObject = ramsesResolver.findRamsesSceneObjectInScene(name, objectId); + if (!ramsesObject) + return nullptr; + + if (ramsesObject->getType() != ramses::ERamsesObjectType::MeshNode || ramsesObject->getType() != static_cast(boundObject->objectType())) + { + errorReporting.add("Fatal error during loading of RamsesMeshNodeBinding from serialized data: loaded object type does not match referenced object type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto* ramsesMeshNode = ramses::RamsesUtils::TryConvert(*ramsesObject); + assert(ramsesMeshNode); + auto binding = std::make_unique(*ramsesMeshNode, name, id); + binding->setUserId(userIdHigh, userIdLow); + binding->setRootInputs(std::move(deserializedRootInput)); + + ApplyRamsesValuesToInputProperties(*binding, *ramsesMeshNode); + + return binding; + } + + std::optional RamsesMeshNodeBindingImpl::update() + { + auto prop = getInputs()->getChild(size_t(EInputProperty::VertexOffset))->m_impl.get(); + if (prop->checkForBindingInputNewValueAndReset()) + { + const auto status = m_ramsesMeshNode.get().setStartVertex(prop->getValueAs()); + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesMeshNode.get().getStatusMessage(status) }; + } + + prop = getInputs()->getChild(size_t(EInputProperty::IndexOffset))->m_impl.get(); + if (prop->checkForBindingInputNewValueAndReset()) + { + const auto status = m_ramsesMeshNode.get().setStartIndex(prop->getValueAs()); + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesMeshNode.get().getStatusMessage(status) }; + } + + prop = getInputs()->getChild(size_t(EInputProperty::IndexCount))->m_impl.get(); + if (prop->checkForBindingInputNewValueAndReset()) + { + const auto status = m_ramsesMeshNode.get().setIndexCount(prop->getValueAs()); + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesMeshNode.get().getStatusMessage(status) }; + } + + prop = getInputs()->getChild(size_t(EInputProperty::InstanceCount))->m_impl.get(); + if (prop->checkForBindingInputNewValueAndReset()) + { + const auto status = m_ramsesMeshNode.get().setInstanceCount(prop->getValueAs()); + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesMeshNode.get().getStatusMessage(status) }; + } + + return std::nullopt; + } + + const ramses::MeshNode& RamsesMeshNodeBindingImpl::getRamsesMeshNode() const + { + return m_ramsesMeshNode; + } + + ramses::MeshNode& RamsesMeshNodeBindingImpl::getRamsesMeshNode() + { + return m_ramsesMeshNode; + } + + // Overwrites binding value cache silently (without triggering dirty check) - this code is only executed at initialization, + // should not overwrite values unless set() or link explicitly called + void RamsesMeshNodeBindingImpl::ApplyRamsesValuesToInputProperties(RamsesMeshNodeBindingImpl& binding, ramses::MeshNode& ramsesMeshNode) + { + binding.getInputs()->getChild(size_t(EInputProperty::VertexOffset))->m_impl->initializeBindingInputValue(static_cast(ramsesMeshNode.getStartVertex())); + binding.getInputs()->getChild(size_t(EInputProperty::IndexOffset))->m_impl->initializeBindingInputValue(static_cast(ramsesMeshNode.getStartIndex())); + binding.getInputs()->getChild(size_t(EInputProperty::IndexCount))->m_impl->initializeBindingInputValue(static_cast(ramsesMeshNode.getIndexCount())); + binding.getInputs()->getChild(size_t(EInputProperty::InstanceCount))->m_impl->initializeBindingInputValue(static_cast(ramsesMeshNode.getInstanceCount())); + } +} diff --git a/client/logic/lib/impl/RamsesMeshNodeBindingImpl.h b/client/logic/lib/impl/RamsesMeshNodeBindingImpl.h new file mode 100644 index 000000000..541aff979 --- /dev/null +++ b/client/logic/lib/impl/RamsesMeshNodeBindingImpl.h @@ -0,0 +1,79 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/RamsesBindingImpl.h" +#include + +namespace ramses +{ + class MeshNode; +} + +namespace rlogic_serialization +{ + struct RamsesMeshNodeBinding; +} + +namespace flatbuffers +{ + class FlatBufferBuilder; + template struct Offset; +} + +namespace ramses::internal +{ + class IRamsesObjectResolver; + class ErrorReporting; + class SerializationMap; + class DeserializationMap; + + class RamsesMeshNodeBindingImpl : public RamsesBindingImpl + { + public: + // Move-able (noexcept); Not copy-able + explicit RamsesMeshNodeBindingImpl(ramses::MeshNode& ramsesMeshNode, std::string_view name, uint64_t id); + ~RamsesMeshNodeBindingImpl() noexcept override = default; + RamsesMeshNodeBindingImpl(const RamsesMeshNodeBindingImpl& other) = delete; + RamsesMeshNodeBindingImpl& operator=(const RamsesMeshNodeBindingImpl& other) = delete; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const RamsesMeshNodeBindingImpl& meshNodeBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::RamsesMeshNodeBinding& meshNodeBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + [[nodiscard]] const ramses::MeshNode& getRamsesMeshNode() const; + [[nodiscard]] ramses::MeshNode& getRamsesMeshNode(); + + std::optional update() override; + + void createRootProperties() final; + + enum class EInputProperty + { + VertexOffset = 0, + IndexOffset, + IndexCount, + InstanceCount, + + COUNT + }; + + private: + static void ApplyRamsesValuesToInputProperties(RamsesMeshNodeBindingImpl& binding, ramses::MeshNode& ramsesMeshNode); + + std::reference_wrapper m_ramsesMeshNode; + }; +} diff --git a/client/logic/lib/impl/RamsesNodeBinding.cpp b/client/logic/lib/impl/RamsesNodeBinding.cpp new file mode 100644 index 000000000..8696819c1 --- /dev/null +++ b/client/logic/lib/impl/RamsesNodeBinding.cpp @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/RamsesNodeBindingImpl.h" + +#include "ramses-logic/RamsesNodeBinding.h" + +namespace ramses +{ + RamsesNodeBinding::RamsesNodeBinding(std::unique_ptr impl) noexcept + : RamsesBinding(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_nodeBinding{ static_cast(RamsesBinding::m_impl) } + { + } + + ramses::Node& RamsesNodeBinding::getRamsesNode() const + { + return m_nodeBinding.getRamsesNode(); + } + + ramses::ERotationType RamsesNodeBinding::getRotationType() const + { + return m_nodeBinding.getRotationType(); + } +} diff --git a/client/logic/lib/impl/RamsesNodeBindingImpl.cpp b/client/logic/lib/impl/RamsesNodeBindingImpl.cpp new file mode 100644 index 000000000..8aa1e5fda --- /dev/null +++ b/client/logic/lib/impl/RamsesNodeBindingImpl.cpp @@ -0,0 +1,290 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/RamsesNodeBindingImpl.h" + +#include "ramses-client-api/Node.h" + +#include "ramses-logic/Property.h" + +#include "impl/PropertyImpl.h" +#include "impl/LoggerImpl.h" + +#include "internals/ErrorReporting.h" +#include "internals/RamsesObjectResolver.h" + +#include "generated/RamsesNodeBindingGen.h" +#include "glm/gtc/type_ptr.hpp" + +namespace ramses::internal +{ + RamsesNodeBindingImpl::RamsesNodeBindingImpl(ramses::Node& ramsesNode, ramses::ERotationType rotationType, std::string_view name, uint64_t id) + : RamsesBindingImpl{ name, id } + , m_ramsesNode{ ramsesNode } + , m_rotationType{ rotationType } + { + } + + void RamsesNodeBindingImpl::createRootProperties() + { + // Attention! This order is important - it has to match the indices in ENodePropertyStaticIndex! + auto inputsType = MakeStruct("", { + TypeData{"visibility", EPropertyType::Bool}, + TypeData{"rotation", m_rotationType == ramses::ERotationType::Quaternion ? EPropertyType::Vec4f : EPropertyType::Vec3f}, + TypeData{"translation", EPropertyType::Vec3f}, + TypeData{"scaling", EPropertyType::Vec3f}, + TypeData{"enabled", EPropertyType::Bool} + }); + auto inputs = std::make_unique(inputsType, EPropertySemantics::BindingInput); + + setRootInputs(std::move(inputs)); + + ApplyRamsesValuesToInputProperties(*this, m_ramsesNode); + } + + flatbuffers::Offset RamsesNodeBindingImpl::Serialize( + const RamsesNodeBindingImpl& nodeBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap) + { + auto ramsesReference = RamsesBindingImpl::SerializeRamsesReference(nodeBinding.m_ramsesNode, builder); + + const auto logicObject = LogicObjectImpl::Serialize(nodeBinding, builder); + const auto propertyObject = PropertyImpl::Serialize(*nodeBinding.getInputs()->m_impl, builder, serializationMap); + auto ramsesBinding = rlogic_serialization::CreateRamsesBinding(builder, + logicObject, + ramsesReference, + propertyObject); + builder.Finish(ramsesBinding); + + auto ramsesNodeBinding = rlogic_serialization::CreateRamsesNodeBinding(builder, + ramsesBinding, + static_cast(nodeBinding.m_rotationType) + ); + builder.Finish(ramsesNodeBinding); + + return ramsesNodeBinding; + } + + std::unique_ptr RamsesNodeBindingImpl::Deserialize( + const rlogic_serialization::RamsesNodeBinding& nodeBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + if (!nodeBinding.base()) + { + errorReporting.add("Fatal error during loading of RamsesNodeBinding from serialized data: missing base class info!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(nodeBinding.base()->base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of RamsesNodeBinding from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!nodeBinding.base()->rootInput()) + { + errorReporting.add("Fatal error during loading of RamsesNodeBinding from serialized data: missing root input!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr deserializedRootInput = PropertyImpl::Deserialize(*nodeBinding.base()->rootInput(), EPropertySemantics::BindingInput, errorReporting, deserializationMap); + + if (!deserializedRootInput) + { + return nullptr; + } + + if (deserializedRootInput->getType() != EPropertyType::Struct) + { + errorReporting.add("Fatal error during loading of RamsesNodeBinding from serialized data: root input has unexpected type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const auto* boundObject = nodeBinding.base()->boundRamsesObject(); + if (!boundObject) + { + errorReporting.add("Fatal error during loading of RamsesNodeBinding from serialized data: missing ramses object reference!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const ramses::sceneObjectId_t objectId(boundObject->objectId()); + + ramses::Node* ramsesNode = ramsesResolver.findRamsesNodeInScene(name, objectId); + if (!ramsesNode) + { + // TODO Violin improve error reporting for this particular error (it's reported in ramsesResolver currently): provide better message and scene/node ids + return nullptr; + } + + if (ramsesNode->getType() != static_cast(boundObject->objectType())) + { + errorReporting.add("Fatal error during loading of RamsesNodeBinding from serialized data: loaded node type does not match referenced node type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const auto rotationType (static_cast(nodeBinding.rotationType())); + + auto binding = std::make_unique(*ramsesNode, rotationType, name, id); + binding->setUserId(userIdHigh, userIdLow); + binding->setRootInputs(std::move(deserializedRootInput)); + + ApplyRamsesValuesToInputProperties(*binding, *ramsesNode); + + return binding; + } + + std::optional RamsesNodeBindingImpl::update() + { + ramses::status_t status = ramses::StatusOK; + + PropertyImpl& visibility = *getInputs()->getChild(static_cast(ENodePropertyStaticIndex::Visibility))->m_impl; + PropertyImpl& enabled = *getInputs()->getChild(static_cast(ENodePropertyStaticIndex::Enabled))->m_impl; + + // Ramses uses 3-state visibility mode, transform the 2 bool properties 'visibility' and 'enabled' into 3-state + const bool visibilityChanged = visibility.checkForBindingInputNewValueAndReset(); + const bool enabledChanged = enabled.checkForBindingInputNewValueAndReset(); + if (visibilityChanged || enabledChanged) + { + ramses::EVisibilityMode visibilityMode = (visibility.getValueAs() ? ramses::EVisibilityMode::Visible : ramses::EVisibilityMode::Invisible); + // if 'enabled' false it overrides visibility mode to OFF, otherwise (enabled==true) the mode is determined by 'visibility' only + if (!enabled.getValueAs()) + visibilityMode = ramses::EVisibilityMode::Off; + + status = m_ramsesNode.get().setVisibility(visibilityMode); + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesNode.get().getStatusMessage(status) }; + } + + PropertyImpl& rotation = *getInputs()->getChild(static_cast(ENodePropertyStaticIndex::Rotation))->m_impl; + if (rotation.checkForBindingInputNewValueAndReset()) + { + if (m_rotationType == ramses::ERotationType::Quaternion) + { + const auto& value = rotation.getValueAs(); + status = m_ramsesNode.get().setRotation(quat(value[3], value[0], value[1], value[2])); + } + else + { + const auto& valuesEuler = rotation.getValueAs(); + status = m_ramsesNode.get().setRotation(valuesEuler, m_rotationType); + } + + if (status != ramses::StatusOK) + { + return LogicNodeRuntimeError{m_ramsesNode.get().getStatusMessage(status)}; + } + } + + PropertyImpl& translation = *getInputs()->getChild(static_cast(ENodePropertyStaticIndex::Translation))->m_impl; + if (translation.checkForBindingInputNewValueAndReset()) + { + const auto& value = translation.getValueAs(); + status = m_ramsesNode.get().setTranslation(value); + + if (status != ramses::StatusOK) + { + return LogicNodeRuntimeError{ m_ramsesNode.get().getStatusMessage(status) }; + } + } + + PropertyImpl& scaling = *getInputs()->getChild(static_cast(ENodePropertyStaticIndex::Scaling))->m_impl; + if (scaling.checkForBindingInputNewValueAndReset()) + { + const auto& value = scaling.getValueAs(); + status = m_ramsesNode.get().setScaling(value); + + if (status != ramses::StatusOK) + { + return LogicNodeRuntimeError{ m_ramsesNode.get().getStatusMessage(status) }; + } + } + + return std::nullopt; + } + + ramses::Node& RamsesNodeBindingImpl::getRamsesNode() const + { + return m_ramsesNode; + } + + ramses::ERotationType RamsesNodeBindingImpl::getRotationType() const + { + return m_rotationType; + } + + // Overwrites binding value cache silently (without triggering dirty check) - this code is only executed at initialization, + // should not overwrite values unless set() or link explicitly called + void RamsesNodeBindingImpl::ApplyRamsesValuesToInputProperties(RamsesNodeBindingImpl& binding, ramses::Node& ramsesNode) + { + // The 3-state ramses visibility mode is transformed into 2 bool properties in node binding - 'visibility' and 'enabled'. + const ramses::EVisibilityMode visibilityMode = ramsesNode.getVisibility(); + const bool visible = (visibilityMode == ramses::EVisibilityMode::Visible); + binding.getInputs()->getChild(static_cast(ENodePropertyStaticIndex::Visibility))->m_impl->initializeBindingInputValue(PropertyValue{ visible }); + const bool enabled = (visibilityMode == ramses::EVisibilityMode::Visible || visibilityMode == ramses::EVisibilityMode::Invisible); + binding.getInputs()->getChild(static_cast(ENodePropertyStaticIndex::Enabled))->m_impl->initializeBindingInputValue(PropertyValue{ enabled }); + + vec3f translationValue; + ramsesNode.getTranslation(translationValue); + binding.getInputs()->getChild(static_cast(ENodePropertyStaticIndex::Translation))->m_impl->initializeBindingInputValue(PropertyValue{ translationValue }); + + vec3f scalingValue; + ramsesNode.getScaling(scalingValue); + binding.getInputs()->getChild(static_cast(ENodePropertyStaticIndex::Scaling))->m_impl->initializeBindingInputValue(PropertyValue{ scalingValue }); + + const ramses::ERotationType rotationType = ramsesNode.getRotationType(); + if (binding.m_rotationType == ramses::ERotationType::Quaternion) + { + vec4f rotationValue = {0.f, 0.f, 0.f, 1.f}; + if (rotationType == ramses::ERotationType::Quaternion) + { + quat quaternion; + ramsesNode.getRotation(quaternion); + rotationValue = {quaternion.x, quaternion.y, quaternion.z, quaternion.w}; + } + else + { + vec3f euler; + ramsesNode.getRotation(euler); + // Allow special case where rotation is not set (i.e. zero) -> mismatching rotationType is OK in this case + // Otherwise issue a warning + if (euler[0] != 0.f || euler[1] != 0.f || euler[2] != 0.f) + { + LOG_WARN("Initial rotation values for RamsesNodeBinding '{}' will not be imported from bound Ramses node due to mismatching rotation type. Expected Quaternion, got Euler.", + binding.getIdentificationString()); + } + } + binding.getInputs()->getChild(static_cast(ENodePropertyStaticIndex::Rotation))->m_impl->initializeBindingInputValue(rotationValue); + } + else + { + vec3f rotationValue; + ramsesNode.getRotation(rotationValue); + + if (binding.m_rotationType != rotationType) + { + // Allow special case where rotation is not set (i.e. zero) -> mismatching convention is OK in this case + // Otherwise issue a warning + if (rotationValue[0] != 0.f || rotationValue[1] != 0.f || rotationValue[2] != 0.f) + { + LOG_WARN("Initial rotation values for RamsesNodeBinding '{}' will not be imported from bound Ramses node due to mismatching rotation type.", binding.getIdentificationString()); + } + } + else + { + binding.getInputs()->getChild(static_cast(ENodePropertyStaticIndex::Rotation))->m_impl->initializeBindingInputValue(PropertyValue{ rotationValue }); + } + } + } +} diff --git a/client/logic/lib/impl/RamsesNodeBindingImpl.h b/client/logic/lib/impl/RamsesNodeBindingImpl.h new file mode 100644 index 000000000..9b82e24bc --- /dev/null +++ b/client/logic/lib/impl/RamsesNodeBindingImpl.h @@ -0,0 +1,84 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/RamsesBindingImpl.h" +#include "ramses-logic/EPropertyType.h" +#include "internals/SerializationMap.h" +#include "internals/DeserializationMap.h" +#include "ramses-client-api/ERotationType.h" + +#include + +namespace ramses +{ + class Scene; + class Node; +} + +namespace rlogic_serialization +{ + struct RamsesNodeBinding; +} + +namespace flatbuffers +{ + class FlatBufferBuilder; + template struct Offset; +} + +namespace ramses::internal +{ + class IRamsesObjectResolver; + class ErrorReporting; + + enum class ENodePropertyStaticIndex : size_t + { + Visibility = 0, + Rotation = 1, + Translation = 2, + Scaling = 3, + Enabled = 4 + }; + + class RamsesNodeBindingImpl : public RamsesBindingImpl + { + public: + // Move-able (noexcept); Not copy-able + explicit RamsesNodeBindingImpl(ramses::Node& ramsesNode, ramses::ERotationType rotationType, std::string_view name, uint64_t id); + ~RamsesNodeBindingImpl() noexcept override = default; + RamsesNodeBindingImpl(const RamsesNodeBindingImpl& other) = delete; + RamsesNodeBindingImpl& operator=(const RamsesNodeBindingImpl& other) = delete; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const RamsesNodeBindingImpl& nodeBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::RamsesNodeBinding& nodeBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + [[nodiscard]] ramses::Node& getRamsesNode() const; + + [[nodiscard]] ramses::ERotationType getRotationType() const; + + std::optional update() override; + + void createRootProperties() final; + + private: + static void ApplyRamsesValuesToInputProperties(RamsesNodeBindingImpl& binding, ramses::Node& ramsesNode); + + std::reference_wrapper m_ramsesNode; + ramses::ERotationType m_rotationType; + }; +} diff --git a/client/logic/lib/impl/RamsesRenderGroupBinding.cpp b/client/logic/lib/impl/RamsesRenderGroupBinding.cpp new file mode 100644 index 000000000..97532263e --- /dev/null +++ b/client/logic/lib/impl/RamsesRenderGroupBinding.cpp @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "impl/RamsesRenderGroupBindingImpl.h" + +namespace ramses +{ + RamsesRenderGroupBinding::RamsesRenderGroupBinding(std::unique_ptr impl) noexcept + : RamsesBinding(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_renderGroupBinding{ static_cast(RamsesBinding::m_impl) } + { + } + + const ramses::RenderGroup& RamsesRenderGroupBinding::getRamsesRenderGroup() const + { + return m_renderGroupBinding.getRamsesRenderGroup(); + } + + ramses::RenderGroup& RamsesRenderGroupBinding::getRamsesRenderGroup() + { + return m_renderGroupBinding.getRamsesRenderGroup(); + } +} diff --git a/client/logic/lib/impl/RamsesRenderGroupBindingElements.cpp b/client/logic/lib/impl/RamsesRenderGroupBindingElements.cpp new file mode 100644 index 000000000..0264cac35 --- /dev/null +++ b/client/logic/lib/impl/RamsesRenderGroupBindingElements.cpp @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/RamsesRenderGroupBindingElements.h" +#include "impl/RamsesRenderGroupBindingElementsImpl.h" +#include "ramses-client-api/MeshNode.h" +#include "ramses-client-api/RenderGroup.h" + +namespace ramses +{ + RamsesRenderGroupBindingElements::RamsesRenderGroupBindingElements() noexcept + : m_impl{ std::make_unique() } + { + } + RamsesRenderGroupBindingElements::~RamsesRenderGroupBindingElements() noexcept = default; + + RamsesRenderGroupBindingElements::RamsesRenderGroupBindingElements(const RamsesRenderGroupBindingElements& other) + : m_impl{ std::make_unique(*other.m_impl) } + { + } + + RamsesRenderGroupBindingElements::RamsesRenderGroupBindingElements(RamsesRenderGroupBindingElements&& other) noexcept = default; + + RamsesRenderGroupBindingElements& RamsesRenderGroupBindingElements::operator=(const RamsesRenderGroupBindingElements& other) + { + m_impl = std::make_unique(*other.m_impl); + return *this; + } + + RamsesRenderGroupBindingElements& RamsesRenderGroupBindingElements::operator=(RamsesRenderGroupBindingElements&& other) noexcept = default; + + bool RamsesRenderGroupBindingElements::addElement(const ramses::MeshNode& meshNode, std::string_view elementName) + { + return m_impl->addElement(meshNode, elementName); + } + + bool RamsesRenderGroupBindingElements::addElement(const ramses::RenderGroup& nestedRenderGroup, std::string_view elementName) + { + return m_impl->addElement(nestedRenderGroup, elementName); + } +} diff --git a/client/logic/lib/impl/RamsesRenderGroupBindingElementsImpl.cpp b/client/logic/lib/impl/RamsesRenderGroupBindingElementsImpl.cpp new file mode 100644 index 000000000..30a8f0cd6 --- /dev/null +++ b/client/logic/lib/impl/RamsesRenderGroupBindingElementsImpl.cpp @@ -0,0 +1,42 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/RamsesRenderGroupBindingElementsImpl.h" +#include "impl/LoggerImpl.h" +#include "ramses-client-api/SceneObject.h" + +namespace ramses::internal +{ + bool RamsesRenderGroupBindingElementsImpl::addElement(const ramses::SceneObject& ramsesObject, std::string_view elementName) + { + assert(ramsesObject.isOfType(ramses::ERamsesObjectType::MeshNode) || ramsesObject.isOfType(ramses::ERamsesObjectType::RenderGroup)); + + const std::string name = (elementName.empty() ? ramsesObject.getName() : std::string{ elementName }); + if (name.empty()) + { + LOG_ERROR("RamsesRenderGroupBindingElements: Failed to add element, object has no name and provided element name is empty."); + return false; + } + + const auto it = std::find_if(m_elements.cbegin(), m_elements.cend(), [&ramsesObject](const auto& e) { return e.second == &ramsesObject; }); + if (it != m_elements.cend()) + { + LOG_ERROR("RamsesRenderGroupBindingElements: Failed to add element '{}', it is already contained under name '{}'.", name, it->first); + return false; + } + + m_elements.push_back({ name, &ramsesObject }); + + return true; + } + + const RamsesRenderGroupBindingElementsImpl::Elements& RamsesRenderGroupBindingElementsImpl::getElements() const + { + return m_elements; + } +} diff --git a/client/logic/lib/impl/RamsesRenderGroupBindingElementsImpl.h b/client/logic/lib/impl/RamsesRenderGroupBindingElementsImpl.h new file mode 100644 index 000000000..52aad54f1 --- /dev/null +++ b/client/logic/lib/impl/RamsesRenderGroupBindingElementsImpl.h @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include +#include + +namespace ramses +{ + class SceneObject; +} + +namespace ramses::internal +{ + class RamsesRenderGroupBindingElementsImpl + { + public: + [[nodiscard]] bool addElement(const ramses::SceneObject& ramsesObject, std::string_view elementName); + + using ElementEntry = std::pair; + using Elements = std::vector; + [[nodiscard]] const Elements& getElements() const; + + private: + Elements m_elements; + }; +} diff --git a/client/logic/lib/impl/RamsesRenderGroupBindingImpl.cpp b/client/logic/lib/impl/RamsesRenderGroupBindingImpl.cpp new file mode 100644 index 000000000..bd7f245ad --- /dev/null +++ b/client/logic/lib/impl/RamsesRenderGroupBindingImpl.cpp @@ -0,0 +1,261 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/RamsesRenderGroupBindingImpl.h" +#include "impl/PropertyImpl.h" +#include "ramses-client-api/RenderGroup.h" +#include "ramses-logic/Property.h" +#include "ramses-utils.h" +#include "internals/ErrorReporting.h" +#include "internals/RamsesObjectResolver.h" +#include "generated/RamsesRenderGroupBindingGen.h" +#include "fmt/format.h" + +namespace ramses::internal +{ + RamsesRenderGroupBindingImpl::RamsesRenderGroupBindingImpl(ramses::RenderGroup& ramsesRenderGroup, const RamsesRenderGroupBindingElementsImpl& elements, std::string_view name, uint64_t id) + : RamsesBindingImpl{ name, id } + , m_ramsesRenderGroup{ ramsesRenderGroup } + , m_elements{ elements.getElements() } + { + assert(!m_elements.empty()); + assert(std::none_of(m_elements.cbegin(), m_elements.cend(), [](const auto& e) { return e.second == nullptr; })); + } + + void RamsesRenderGroupBindingImpl::createRootProperties() + { + HierarchicalTypeData inputsType = MakeStruct("", { + TypeData{"renderOrders", EPropertyType::Struct} + }); + + auto& elementInputs = inputsType.children.front().children; + elementInputs.reserve(m_elements.size()); + for (const auto& e : m_elements) + elementInputs.push_back(MakeType(e.first, EPropertyType::Int32)); + + auto inputs = std::make_unique(std::move(inputsType), EPropertySemantics::BindingInput); + + setRootInputs(std::move(inputs)); + + ApplyRamsesValuesToInputProperties(*this, m_ramsesRenderGroup); + } + + flatbuffers::Offset RamsesRenderGroupBindingImpl::Serialize( + const RamsesRenderGroupBindingImpl& renderGroupBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap) + { + const auto logicObject = LogicObjectImpl::Serialize(renderGroupBinding, builder); + const auto fbRamsesRef = RamsesBindingImpl::SerializeRamsesReference(renderGroupBinding.m_ramsesRenderGroup, builder); + const auto propertyObject = PropertyImpl::Serialize(*renderGroupBinding.getInputs()->m_impl, builder, serializationMap); + auto fbRamsesBinding = rlogic_serialization::CreateRamsesBinding(builder, + logicObject, + fbRamsesRef, + propertyObject); + + std::vector> elementsFB; + elementsFB.reserve(renderGroupBinding.m_elements.size()); + for (const auto& element : renderGroupBinding.m_elements) + { + const auto ramsesReferenceFB = RamsesBindingImpl::SerializeRamsesReference(*element.second, builder); + elementsFB.push_back(rlogic_serialization::CreateElement( + builder, + builder.CreateString(element.first), + ramsesReferenceFB)); + } + + auto fbRenderGroupBinding = rlogic_serialization::CreateRamsesRenderGroupBinding(builder, fbRamsesBinding, builder.CreateVector(elementsFB)); + builder.Finish(fbRenderGroupBinding); + + return fbRenderGroupBinding; + } + + std::unique_ptr RamsesRenderGroupBindingImpl::Deserialize( + const rlogic_serialization::RamsesRenderGroupBinding& renderGroupBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + if (!renderGroupBinding.base()) + { + errorReporting.add("Fatal error during loading of RamsesRenderGroupBinding from serialized data: missing base class info!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(renderGroupBinding.base()->base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of RamsesRenderGroupBinding from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!renderGroupBinding.base()->rootInput()) + { + errorReporting.add("Fatal error during loading of RamsesRenderGroupBinding from serialized data: missing root input!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr deserializedRootInput = PropertyImpl::Deserialize(*renderGroupBinding.base()->rootInput(), EPropertySemantics::BindingInput, errorReporting, deserializationMap); + if (!deserializedRootInput) + return nullptr; + + if (deserializedRootInput->getType() != EPropertyType::Struct) + { + errorReporting.add("Fatal error during loading of RamsesRenderGroupBinding from serialized data: root input has unexpected type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const auto* boundObject = renderGroupBinding.base()->boundRamsesObject(); + if (!boundObject) + { + errorReporting.add("Fatal error during loading of RamsesRenderGroupBinding from serialized data: missing ramses object reference!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const ramses::sceneObjectId_t objectId{ boundObject->objectId() }; + ramses::RenderGroup* ramsesRenderGroup = ramsesResolver.findRamsesRenderGroupInScene(name, objectId); + if (!ramsesRenderGroup) + return nullptr; + + if (ramsesRenderGroup->getType() != static_cast(boundObject->objectType())) + { + errorReporting.add("Fatal error during loading of RamsesRenderGroupBinding from serialized data: loaded object type does not match referenced object type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!deserializedRootInput->getChild("renderOrders") || deserializedRootInput->getChild("renderOrders")->getChildCount() != renderGroupBinding.elements()->size()) + { + errorReporting.add("Fatal error during loading of RamsesRenderGroupBinding from serialized data: input properties do not match RenderGroup's elements!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + RamsesRenderGroupBindingElementsImpl elements; + for (const auto* elementFB : *renderGroupBinding.elements()) + { + if (!elementFB->name() || !elementFB->ramsesObject()) + { + errorReporting.add(fmt::format("Fatal error during loading of RamsesRenderGroupBinding '{}' elements data: missing name or Ramses reference!", name), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto elementObject = ramsesResolver.findRamsesSceneObjectInScene(name, ramses::sceneObjectId_t{ elementFB->ramsesObject()->objectId() }); + if (!elementObject) + return nullptr; + + if (elementObject->getType() != static_cast(elementFB->ramsesObject()->objectType())) + { + errorReporting.add(fmt::format("Fatal error during loading of RamsesRenderGroupBinding '{}' elements data: loaded element object type does not match referenced object type!", name), + nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!elements.addElement(*elementObject, elementFB->name()->string_view())) + { + errorReporting.add(fmt::format("Fatal error during loading of RamsesRenderGroupBinding '{}' elements data '{}' corrupted!", name, elementFB->name()->string_view()), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + } + + auto binding = std::make_unique(*ramsesRenderGroup, elements, name, id); + binding->setUserId(userIdHigh, userIdLow); + binding->setRootInputs(std::move(deserializedRootInput)); + + ApplyRamsesValuesToInputProperties(*binding, *ramsesRenderGroup); + + return binding; + } + + std::optional RamsesRenderGroupBindingImpl::update() + { + // The input properties for render order must match the ramses object references stored in m_elements. + // This is asserted in ApplyRamsesValuesToInputProperties which is always executed once at creation/deserialization, + // asserting everything here again would be redundant. + + auto renderOrdersProps = getInputs()->getChild(0u); + for (std::size_t i = 0u; i < renderOrdersProps->getChildCount(); ++i) + { + auto& elementPropImpl = *renderOrdersProps->getChild(i)->m_impl; + if (elementPropImpl.checkForBindingInputNewValueAndReset()) + { + const int32_t renderOrder = elementPropImpl.getValueAs(); + const auto& obj = *m_elements[i].second; + assert(obj.isOfType(ramses::ERamsesObjectType::MeshNode) || obj.isOfType(ramses::ERamsesObjectType::RenderGroup)); + + ramses::status_t status = ramses::StatusOK; + if (obj.isOfType(ramses::ERamsesObjectType::MeshNode)) + { + const auto meshNode = ramses::RamsesUtils::TryConvert(obj); + if (!m_ramsesRenderGroup.get().containsMeshNode(*meshNode)) + return LogicNodeRuntimeError{ "Cannot set render order of MeshNode which is not contained in bound RenderGroup." }; + status = m_ramsesRenderGroup.get().addMeshNode(*meshNode, renderOrder); // we are not adding it, this is ramses way to change render order of already contained element + } + else + { + const auto rg = ramses::RamsesUtils::TryConvert(obj); + if (!m_ramsesRenderGroup.get().containsRenderGroup(*rg)) + return LogicNodeRuntimeError{ "Cannot set render order of RenderGroup which is not contained in bound RenderGroup." }; + status = m_ramsesRenderGroup.get().addRenderGroup(*rg, renderOrder); // we are not adding it, this is ramses way to change render order of already contained element + } + + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesRenderGroup.get().getStatusMessage(status) }; + } + } + + return std::nullopt; + } + + const ramses::RenderGroup& RamsesRenderGroupBindingImpl::getRamsesRenderGroup() const + { + return m_ramsesRenderGroup; + } + + ramses::RenderGroup& RamsesRenderGroupBindingImpl::getRamsesRenderGroup() + { + return m_ramsesRenderGroup; + } + + // Overwrites binding value cache silently (without triggering dirty check) - this code is only executed at initialization, + // should not overwrite values unless set() or link explicitly called + void RamsesRenderGroupBindingImpl::ApplyRamsesValuesToInputProperties(RamsesRenderGroupBindingImpl& binding, ramses::RenderGroup& ramsesRenderGroup) + { + auto renderOrdersProps = binding.getInputs()->getChild(0u); + assert(renderOrdersProps && binding.m_elements.size() == renderOrdersProps->getChildCount()); + + for (std::size_t i = 0u; i < renderOrdersProps->getChildCount(); ++i) + { + int32_t renderOrder = 0; + const auto& obj = *binding.m_elements[i].second; + + ramses::status_t status = ramses::StatusOK; + if (obj.isOfType(ramses::ERamsesObjectType::MeshNode)) + { + const auto meshNode = ramses::RamsesUtils::TryConvert(obj); + assert(meshNode); + assert(ramsesRenderGroup.containsMeshNode(*meshNode)); + status = ramsesRenderGroup.getMeshNodeOrder(*meshNode, renderOrder); + } + else + { + const auto rg = ramses::RamsesUtils::TryConvert(obj); + assert(rg); + assert(ramsesRenderGroup.containsRenderGroup(*rg)); + status = ramsesRenderGroup.getRenderGroupOrder(*rg, renderOrder); + } + assert(status == ramses::StatusOK); + (void)status; + + auto& elementPropImpl = *renderOrdersProps->getChild(i)->m_impl; + assert(elementPropImpl.getName() == binding.m_elements[i].first); + elementPropImpl.initializeBindingInputValue(PropertyValue{ renderOrder }); + } + } +} diff --git a/client/logic/lib/impl/RamsesRenderGroupBindingImpl.h b/client/logic/lib/impl/RamsesRenderGroupBindingImpl.h new file mode 100644 index 000000000..2116ee8b5 --- /dev/null +++ b/client/logic/lib/impl/RamsesRenderGroupBindingImpl.h @@ -0,0 +1,71 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/RamsesBindingImpl.h" +#include "impl/RamsesRenderGroupBindingElementsImpl.h" +#include + +namespace ramses +{ + class RenderGroup; +} + +namespace rlogic_serialization +{ + struct RamsesRenderGroupBinding; +} + +namespace flatbuffers +{ + class FlatBufferBuilder; + template struct Offset; +} + +namespace ramses::internal +{ + class IRamsesObjectResolver; + class ErrorReporting; + class SerializationMap; + class DeserializationMap; + + class RamsesRenderGroupBindingImpl : public RamsesBindingImpl + { + public: + // Move-able (noexcept); Not copy-able + explicit RamsesRenderGroupBindingImpl(ramses::RenderGroup& ramsesRenderGroup, const RamsesRenderGroupBindingElementsImpl& elements, std::string_view name, uint64_t id); + ~RamsesRenderGroupBindingImpl() noexcept override = default; + RamsesRenderGroupBindingImpl(const RamsesRenderGroupBindingImpl& other) = delete; + RamsesRenderGroupBindingImpl& operator=(const RamsesRenderGroupBindingImpl& other) = delete; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const RamsesRenderGroupBindingImpl& renderGroupBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::RamsesRenderGroupBinding& renderGroupBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + [[nodiscard]] const ramses::RenderGroup& getRamsesRenderGroup() const; + [[nodiscard]] ramses::RenderGroup& getRamsesRenderGroup(); + + std::optional update() override; + + void createRootProperties() final; + + private: + static void ApplyRamsesValuesToInputProperties(RamsesRenderGroupBindingImpl& binding, ramses::RenderGroup& ramsesRenderGroup); + + std::reference_wrapper m_ramsesRenderGroup; + RamsesRenderGroupBindingElementsImpl::Elements m_elements; + }; +} diff --git a/client/logic/lib/impl/RamsesRenderPassBinding.cpp b/client/logic/lib/impl/RamsesRenderPassBinding.cpp new file mode 100644 index 000000000..062adf8e9 --- /dev/null +++ b/client/logic/lib/impl/RamsesRenderPassBinding.cpp @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "impl/RamsesRenderPassBindingImpl.h" + +namespace ramses +{ + RamsesRenderPassBinding::RamsesRenderPassBinding(std::unique_ptr impl) noexcept + : RamsesBinding(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_renderPassBinding{ static_cast(RamsesBinding::m_impl) } + { + } + + const ramses::RenderPass& RamsesRenderPassBinding::getRamsesRenderPass() const + { + return m_renderPassBinding.getRamsesRenderPass(); + } + + ramses::RenderPass& RamsesRenderPassBinding::getRamsesRenderPass() + { + return m_renderPassBinding.getRamsesRenderPass(); + } +} diff --git a/client/logic/lib/impl/RamsesRenderPassBindingImpl.cpp b/client/logic/lib/impl/RamsesRenderPassBindingImpl.cpp new file mode 100644 index 000000000..ffaf16f4f --- /dev/null +++ b/client/logic/lib/impl/RamsesRenderPassBindingImpl.cpp @@ -0,0 +1,192 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/RamsesRenderPassBindingImpl.h" + +#include "ramses-client-api/RenderPass.h" + +#include "ramses-logic/Property.h" + +#include "impl/PropertyImpl.h" + +#include "internals/ErrorReporting.h" +#include "internals/RamsesObjectResolver.h" + +#include "generated/RamsesRenderPassBindingGen.h" + +namespace ramses::internal +{ + RamsesRenderPassBindingImpl::RamsesRenderPassBindingImpl(ramses::RenderPass& ramsesRenderPass, std::string_view name, uint64_t id) + : RamsesBindingImpl{ name, id } + , m_ramsesRenderPass{ ramsesRenderPass } + { + } + + void RamsesRenderPassBindingImpl::createRootProperties() + { + // Attention! This order is important - it has to match the indices in EPropertyIndex! + auto inputsType = MakeStruct("", { + TypeData{"enabled", EPropertyType::Bool}, + TypeData{"renderOrder", EPropertyType::Int32}, + TypeData{"clearColor", EPropertyType::Vec4f}, + TypeData{"renderOnce", EPropertyType::Bool} + }); + auto inputs = std::make_unique(std::move(inputsType), EPropertySemantics::BindingInput); + + setRootInputs(std::move(inputs)); + + ApplyRamsesValuesToInputProperties(*this, m_ramsesRenderPass); + } + + flatbuffers::Offset RamsesRenderPassBindingImpl::Serialize( + const RamsesRenderPassBindingImpl& renderPassBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap) + { + const auto logicObject = LogicObjectImpl::Serialize(renderPassBinding, builder); + const auto fbRamsesRef = RamsesBindingImpl::SerializeRamsesReference(renderPassBinding.m_ramsesRenderPass, builder); + const auto propertyObject = PropertyImpl::Serialize(*renderPassBinding.getInputs()->m_impl, builder, serializationMap); + auto fbRamsesBinding = rlogic_serialization::CreateRamsesBinding(builder, + logicObject, + fbRamsesRef, + propertyObject); + + auto fbRenderPassBinding = rlogic_serialization::CreateRamsesRenderPassBinding(builder, fbRamsesBinding); + builder.Finish(fbRenderPassBinding); + + return fbRenderPassBinding; + } + + std::unique_ptr RamsesRenderPassBindingImpl::Deserialize( + const rlogic_serialization::RamsesRenderPassBinding& renderPassBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + if (!renderPassBinding.base()) + { + errorReporting.add("Fatal error during loading of RamsesRenderPassBinding from serialized data: missing base class info!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(renderPassBinding.base()->base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of RamsesRenderPassBinding from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!renderPassBinding.base()->rootInput()) + { + errorReporting.add("Fatal error during loading of RamsesRenderPassBinding from serialized data: missing root input!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::unique_ptr deserializedRootInput = PropertyImpl::Deserialize(*renderPassBinding.base()->rootInput(), EPropertySemantics::BindingInput, errorReporting, deserializationMap); + if (!deserializedRootInput) + return nullptr; + + if (deserializedRootInput->getType() != EPropertyType::Struct) + { + errorReporting.add("Fatal error during loading of RamsesRenderPassBinding from serialized data: root input has unexpected type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const auto* boundObject = renderPassBinding.base()->boundRamsesObject(); + if (!boundObject) + { + errorReporting.add("Fatal error during loading of RamsesRenderPassBinding from serialized data: missing ramses object reference!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const ramses::sceneObjectId_t objectId{ boundObject->objectId() }; + ramses::RenderPass* ramsesRenderPass = ramsesResolver.findRamsesRenderPassInScene(name, objectId); + if (!ramsesRenderPass) + return nullptr; + + if (ramsesRenderPass->getType() != static_cast(boundObject->objectType())) + { + errorReporting.add("Fatal error during loading of RamsesRenderPassBinding from serialized data: loaded object type does not match referenced object type!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto binding = std::make_unique(*ramsesRenderPass, name, id); + binding->setUserId(userIdHigh, userIdLow); + binding->setRootInputs(std::move(deserializedRootInput)); + + ApplyRamsesValuesToInputProperties(*binding, *ramsesRenderPass); + + return binding; + } + + std::optional RamsesRenderPassBindingImpl::update() + { + ramses::status_t status = ramses::StatusOK; + + PropertyImpl& enabled = *getInputs()->getChild(EPropertyIndex_Enabled)->m_impl; + if (enabled.checkForBindingInputNewValueAndReset()) + { + status = m_ramsesRenderPass.get().setEnabled(enabled.getValueAs()); + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesRenderPass.get().getStatusMessage(status) }; + } + + PropertyImpl& renderOrder = *getInputs()->getChild(EPropertyIndex_RenderOrder)->m_impl; + if (renderOrder.checkForBindingInputNewValueAndReset()) + { + status = m_ramsesRenderPass.get().setRenderOrder(renderOrder.getValueAs()); + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesRenderPass.get().getStatusMessage(status) }; + } + + PropertyImpl& clearColor = *getInputs()->getChild(EPropertyIndex_ClearColor)->m_impl; + if (clearColor.checkForBindingInputNewValueAndReset()) + { + status = m_ramsesRenderPass.get().setClearColor(clearColor.getValueAs()); + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesRenderPass.get().getStatusMessage(status) }; + } + + PropertyImpl& renderOnce = *getInputs()->getChild(EPropertyIndex_RenderOnce)->m_impl; + if (renderOnce.checkForBindingInputNewValueAndReset()) + { + status = m_ramsesRenderPass.get().setRenderOnce(renderOnce.getValueAs()); + if (status != ramses::StatusOK) + return LogicNodeRuntimeError{ m_ramsesRenderPass.get().getStatusMessage(status) }; + } + + return std::nullopt; + } + + const ramses::RenderPass& RamsesRenderPassBindingImpl::getRamsesRenderPass() const + { + return m_ramsesRenderPass; + } + + ramses::RenderPass& RamsesRenderPassBindingImpl::getRamsesRenderPass() + { + return m_ramsesRenderPass; + } + + // Overwrites binding value cache silently (without triggering dirty check) - this code is only executed at initialization, + // should not overwrite values unless set() or link explicitly called + void RamsesRenderPassBindingImpl::ApplyRamsesValuesToInputProperties(RamsesRenderPassBindingImpl& binding, ramses::RenderPass& ramsesRenderPass) + { + binding.getInputs()->getChild(EPropertyIndex_Enabled)->m_impl->initializeBindingInputValue(PropertyValue{ ramsesRenderPass.isEnabled() }); + + binding.getInputs()->getChild(EPropertyIndex_RenderOrder)->m_impl->initializeBindingInputValue(PropertyValue{ ramsesRenderPass.getRenderOrder() }); + + vec4f clearColor = ramsesRenderPass.getClearColor(); + binding.getInputs()->getChild(EPropertyIndex_ClearColor)->m_impl->initializeBindingInputValue(PropertyValue{ clearColor }); + + binding.getInputs()->getChild(EPropertyIndex_RenderOnce)->m_impl->initializeBindingInputValue(PropertyValue{ ramsesRenderPass.isRenderOnce() }); + } +} diff --git a/client/logic/lib/impl/RamsesRenderPassBindingImpl.h b/client/logic/lib/impl/RamsesRenderPassBindingImpl.h new file mode 100644 index 000000000..605bb0817 --- /dev/null +++ b/client/logic/lib/impl/RamsesRenderPassBindingImpl.h @@ -0,0 +1,79 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/RamsesBindingImpl.h" +#include + +namespace ramses +{ + class RenderPass; +} + +namespace rlogic_serialization +{ + struct RamsesRenderPassBinding; +} + +namespace flatbuffers +{ + class FlatBufferBuilder; + template struct Offset; +} + +namespace ramses::internal +{ + class IRamsesObjectResolver; + class ErrorReporting; + class SerializationMap; + class DeserializationMap; + + class RamsesRenderPassBindingImpl : public RamsesBindingImpl + { + public: + enum EPropertyIndex + { + EPropertyIndex_Enabled = 0, + EPropertyIndex_RenderOrder, + EPropertyIndex_ClearColor, + EPropertyIndex_RenderOnce, + + EPropertyIndex_COUNT + }; + + // Move-able (noexcept); Not copy-able + explicit RamsesRenderPassBindingImpl(ramses::RenderPass& ramsesRenderPass, std::string_view name, uint64_t id); + ~RamsesRenderPassBindingImpl() noexcept override = default; + RamsesRenderPassBindingImpl(const RamsesRenderPassBindingImpl& other) = delete; + RamsesRenderPassBindingImpl& operator=(const RamsesRenderPassBindingImpl& other) = delete; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const RamsesRenderPassBindingImpl& renderPassBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::RamsesRenderPassBinding& renderPassBinding, + const IRamsesObjectResolver& ramsesResolver, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + [[nodiscard]] const ramses::RenderPass& getRamsesRenderPass() const; + [[nodiscard]] ramses::RenderPass& getRamsesRenderPass(); + + std::optional update() override; + + void createRootProperties() final; + + private: + static void ApplyRamsesValuesToInputProperties(RamsesRenderPassBindingImpl& binding, ramses::RenderPass& ramsesRenderPass); + + std::reference_wrapper m_ramsesRenderPass; + }; +} diff --git a/client/logic/lib/impl/SaveFileConfig.cpp b/client/logic/lib/impl/SaveFileConfig.cpp new file mode 100644 index 000000000..282e2778c --- /dev/null +++ b/client/logic/lib/impl/SaveFileConfig.cpp @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/SaveFileConfig.h" + +#include "impl/SaveFileConfigImpl.h" + +namespace ramses +{ + SaveFileConfig::SaveFileConfig() noexcept + : m_impl(std::make_unique()) + { + } + + SaveFileConfig::~SaveFileConfig() noexcept = default; + + SaveFileConfig& SaveFileConfig::operator=(const SaveFileConfig& other) + { + m_impl = std::make_unique(*other.m_impl); + return *this; + } + + SaveFileConfig::SaveFileConfig(const SaveFileConfig& other) + { + *this = other; + } + + SaveFileConfig::SaveFileConfig(SaveFileConfig&&) noexcept = default; + SaveFileConfig& SaveFileConfig::operator=(SaveFileConfig&&) noexcept = default; + + void SaveFileConfig::setMetadataString(std::string_view metadata) + { + m_impl->setMetadataString(metadata); + } + + void SaveFileConfig::setExporterVersion(uint32_t major, uint32_t minor, uint32_t patch, uint32_t fileFormatVersion) + { + m_impl->setExporterVersion(major, minor, patch, fileFormatVersion); + } + + void SaveFileConfig::setValidationEnabled(bool validationEnabled) + { + m_impl->setValidationEnabled(validationEnabled); + } + + void SaveFileConfig::setLuaSavingMode(ELuaSavingMode mode) + { + m_impl->setLuaSavingMode(mode); + } +} diff --git a/client/logic/lib/impl/SaveFileConfigImpl.cpp b/client/logic/lib/impl/SaveFileConfigImpl.cpp new file mode 100644 index 000000000..a39f202ec --- /dev/null +++ b/client/logic/lib/impl/SaveFileConfigImpl.cpp @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/SaveFileConfigImpl.h" +#include "impl/LoggerImpl.h" + +namespace ramses::internal +{ + void SaveFileConfigImpl::setMetadataString(std::string_view metadata) + { + m_metadata = metadata; + } + + void SaveFileConfigImpl::setExporterVersion(uint32_t major, uint32_t minor, uint32_t patch, uint32_t fileFormatVersion) + { + m_exporterMajorVersion = major; + m_exporterMinorVersion = minor; + m_exporterPatchVersion = patch; + m_exporterFileFormatVersion = fileFormatVersion; + } + + void SaveFileConfigImpl::setValidationEnabled(bool validationEnabled) + { + if (validationEnabled == false) + { + LOG_INFO("Validation before saving was disabled during save*() calls! Possible content issues will not yield further warnings."); + } + m_validationEnabled = validationEnabled; + } + + bool SaveFileConfigImpl::getValidationEnabled() const + { + return m_validationEnabled; + } + + uint32_t SaveFileConfigImpl::getExporterMajorVersion() const + { + return m_exporterMajorVersion; + } + + uint32_t SaveFileConfigImpl::getExporterMinorVersion() const + { + return m_exporterMinorVersion; + } + + uint32_t SaveFileConfigImpl::getExporterPatchVersion() const + { + return m_exporterPatchVersion; + } + + uint32_t SaveFileConfigImpl::getExporterFileFormatVersion() const + { + return m_exporterFileFormatVersion; + } + + const std::string& SaveFileConfigImpl::getMetadataString() const + { + return m_metadata; + } + + void SaveFileConfigImpl::setLuaSavingMode(ELuaSavingMode mode) + { + m_luaSavingMode = mode; + } + + ELuaSavingMode SaveFileConfigImpl::getLuaSavingMode() const + { + return m_luaSavingMode; + } +} diff --git a/client/logic/lib/impl/SaveFileConfigImpl.h b/client/logic/lib/impl/SaveFileConfigImpl.h new file mode 100644 index 000000000..057ac2991 --- /dev/null +++ b/client/logic/lib/impl/SaveFileConfigImpl.h @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include "ramses-logic/ELuaSavingMode.h" + +namespace ramses +{ + class LuaModule; +} + +namespace ramses::internal +{ + class SaveFileConfigImpl + { + public: + void setMetadataString(std::string_view metadata); + void setExporterVersion(uint32_t major, uint32_t minor, uint32_t patch, uint32_t fileFormatVersion); + void setValidationEnabled(bool validationEnabled); + void setLuaSavingMode(ELuaSavingMode mode); + + [[nodiscard]] const std::string& getMetadataString() const; + [[nodiscard]] uint32_t getExporterMajorVersion() const; + [[nodiscard]] uint32_t getExporterMinorVersion() const; + [[nodiscard]] uint32_t getExporterPatchVersion() const; + [[nodiscard]] uint32_t getExporterFileFormatVersion() const; + [[nodiscard]] bool getValidationEnabled() const; + [[nodiscard]] ELuaSavingMode getLuaSavingMode() const; + + private: + std::string m_metadata; + uint32_t m_exporterMajorVersion = 0u; + uint32_t m_exporterMinorVersion = 0u; + uint32_t m_exporterPatchVersion = 0u; + uint32_t m_exporterFileFormatVersion = 0u; + bool m_validationEnabled = true; + ELuaSavingMode m_luaSavingMode = ELuaSavingMode::SourceAndByteCode; + }; +} diff --git a/client/logic/lib/impl/SkinBinding.cpp b/client/logic/lib/impl/SkinBinding.cpp new file mode 100644 index 000000000..f3e4164fb --- /dev/null +++ b/client/logic/lib/impl/SkinBinding.cpp @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/SkinBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "impl/SkinBindingImpl.h" +#include "impl/RamsesAppearanceBindingImpl.h" + +namespace ramses +{ + SkinBinding::SkinBinding(std::unique_ptr impl) noexcept + : RamsesBinding(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_skinBinding{ static_cast(RamsesBinding::m_impl) } + { + } + + const RamsesAppearanceBinding& SkinBinding::getAppearanceBinding() const + { + return *m_skinBinding.getAppearanceBinding().getLogicObject().as(); + } + + const ramses::UniformInput& SkinBinding::getAppearanceUniformInput() const + { + return m_skinBinding.getAppearanceUniformInput(); + } +} diff --git a/client/logic/lib/impl/SkinBindingImpl.cpp b/client/logic/lib/impl/SkinBindingImpl.cpp new file mode 100644 index 000000000..bbab66176 --- /dev/null +++ b/client/logic/lib/impl/SkinBindingImpl.cpp @@ -0,0 +1,190 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/SkinBindingImpl.h" +#include "impl/RamsesAppearanceBindingImpl.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "ramses-client-api/Node.h" +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/Effect.h" +#include "internals/ErrorReporting.h" +#include "internals/DeserializationMap.h" +#include "generated/SkinBindingGen.h" +#include "fmt/format.h" +#include "glm/gtc/type_ptr.hpp" +#include "glm/gtx/range.hpp" + +namespace ramses::internal +{ + SkinBindingImpl::SkinBindingImpl( + std::vector joints, + const std::vector& inverseBindMatrices, + RamsesAppearanceBindingImpl& appearanceBinding, + const ramses::UniformInput& jointMatInput, + std::string_view name, + uint64_t id) + : RamsesBindingImpl{ name, id } + , m_joints{ std::move(joints) } + , m_inverseBindMatrices(inverseBindMatrices) + , m_appearanceBinding{ appearanceBinding } + { + assert(!m_joints.empty()); + assert(m_joints.size() == inverseBindMatrices.size()); + assert(std::find(m_joints.cbegin(), m_joints.cend(), nullptr) == m_joints.cend()); + + // ramses::UniformInput cannot be copied, to avoid referencing user provided instance, get it from effect again + assert(jointMatInput.isValid()); + appearanceBinding.getRamsesAppearance().getEffect().findUniformInput(jointMatInput.getName(), m_jointMatInput); + assert(m_jointMatInput.isValid()); + + assert(!m_appearanceBinding.getRamsesAppearance().isInputBound(m_jointMatInput)); + assert(*m_jointMatInput.getDataType() == ramses::EDataType::Matrix44F); + assert(m_jointMatInput.getElementCount() == m_joints.size()); + } + + void SkinBindingImpl::createRootProperties() + { + // no inputs or outputs + setRootInputs({}); + } + + flatbuffers::Offset SkinBindingImpl::Serialize( + const SkinBindingImpl& skinBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& /*serializationMap*/) + { + const auto fbLogicObject = LogicObjectImpl::Serialize(skinBinding, builder); + + std::vector jointIds; + jointIds.reserve(skinBinding.m_joints.size()); + for (const auto* joint : skinBinding.m_joints) + jointIds.push_back(joint->getId()); + const auto fbJoints = builder.CreateVector(jointIds); + + std::vector inverseBindMatData; + inverseBindMatData.reserve(skinBinding.m_inverseBindMatrices.size() * 16u); + for (const auto& mat : skinBinding.m_inverseBindMatrices) + { + inverseBindMatData.insert(inverseBindMatData.begin(), begin(mat), end(mat)); + } + const auto fbInverseBindMatData = builder.CreateVector(inverseBindMatData); + + auto fbSkinBinding = rlogic_serialization::CreateSkinBinding(builder, + fbLogicObject, + fbJoints, + fbInverseBindMatData, + skinBinding.m_appearanceBinding.getId(), + builder.CreateString(skinBinding.m_jointMatInput.getName())); + builder.Finish(fbSkinBinding); + + return fbSkinBinding; + } + + std::unique_ptr SkinBindingImpl::Deserialize( + const rlogic_serialization::SkinBinding& skinBinding, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(skinBinding.base(), name, id, userIdHigh, userIdLow, errorReporting)) + { + errorReporting.add("Fatal error during loading of SkinBinding from serialized data: missing name and/or ID!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!skinBinding.jointNodeBindingIds() || !skinBinding.inverseBindingMatricesData() || skinBinding.jointNodeBindingIds()->size() == 0u || + skinBinding.jointNodeBindingIds()->size() * 16u != skinBinding.inverseBindingMatricesData()->size()) + { + errorReporting.add("Fatal error during loading of SkinBinding from serialized data: missing or corrupted joints and/or inverse matrices data!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + std::vector joints; + joints.reserve(skinBinding.jointNodeBindingIds()->size()); + for (const uint64_t nodeId : *skinBinding.jointNodeBindingIds()) + { + const auto nodeBinding = deserializationMap.resolveLogicObject(nodeId); + if (!nodeBinding) + { + errorReporting.add("Fatal error during loading of SkinBinding from serialized data: could not resolve referenced node binding!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + joints.push_back(nodeBinding); + } + + std::vector inverseMats; + inverseMats.resize(skinBinding.inverseBindingMatricesData()->size() / 16u); + auto fbDataBegin = skinBinding.inverseBindingMatricesData()->cbegin(); + for (auto& mat : inverseMats) + { + const auto fbDataEnd = fbDataBegin + 16u; + std::copy(fbDataBegin, fbDataEnd, glm::value_ptr(mat)); + fbDataBegin = fbDataEnd; + } + + auto appearanceBinding = deserializationMap.resolveLogicObject(skinBinding.appearanceBindingId()); + if (!appearanceBinding) + { + errorReporting.add("Fatal error during loading of SkinBinding from serialized data: could not resolve referenced appearance binding!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + ramses::UniformInput jointMatInput; + if (skinBinding.jointMatUniformInputName()) + appearanceBinding->getRamsesAppearance().getEffect().findUniformInput(skinBinding.jointMatUniformInputName()->c_str(), jointMatInput); + if (!jointMatInput.isValid() || *jointMatInput.getDataType() != ramses::EDataType::Matrix44F || jointMatInput.getElementCount() != joints.size()) + { + errorReporting.add("Fatal error during loading of SkinBinding from serialized data: invalid or mismatching uniform input!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto binding = std::make_unique(joints, inverseMats, *appearanceBinding, jointMatInput, name, id); + binding->setUserId(userIdHigh, userIdLow); + binding->setRootInputs({}); + + return binding; + } + + std::optional SkinBindingImpl::update() + { + m_jointMatricesArray.clear(); + for (size_t i = 0u; i < m_joints.size(); ++i) + { + matrix44f jointNodeWorld; + if (m_joints[i]->getRamsesNode().getModelMatrix(jointNodeWorld) != ramses::StatusOK) + return LogicNodeRuntimeError{ "Failed to retrieve model matrix from Ramses node!" }; + const auto& inverseBindMatForJoint = m_inverseBindMatrices[i]; + const auto jointMat = jointNodeWorld * inverseBindMatForJoint; + + m_jointMatricesArray.emplace_back(jointMat); + } + + if (m_appearanceBinding.getRamsesAppearance().setInputValue(m_jointMatInput, uint32_t(m_jointMatricesArray.size()), m_jointMatricesArray.data()) != ramses::StatusOK) + return LogicNodeRuntimeError{ "Failed to set matrix array uniform to Ramses appearance!" }; + + return std::nullopt; + } + + const std::vector& SkinBindingImpl::getJoints() const + { + return m_joints; + } + + const RamsesAppearanceBindingImpl& SkinBindingImpl::getAppearanceBinding() const + { + return m_appearanceBinding; + } + + const ramses::UniformInput& SkinBindingImpl::getAppearanceUniformInput() const + { + return m_jointMatInput; + } +} diff --git a/client/logic/lib/impl/SkinBindingImpl.h b/client/logic/lib/impl/SkinBindingImpl.h new file mode 100644 index 000000000..6f39c8118 --- /dev/null +++ b/client/logic/lib/impl/SkinBindingImpl.h @@ -0,0 +1,78 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/RamsesBindingImpl.h" +#include "ramses-logic/Property.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-framework-api/DataTypes.h" +#include + +namespace rlogic_serialization +{ + struct SkinBinding; +} + +namespace flatbuffers +{ + class FlatBufferBuilder; + template struct Offset; +} + +namespace ramses::internal +{ + class RamsesNodeBindingImpl; + class RamsesAppearanceBindingImpl; + class ErrorReporting; + class SerializationMap; + class DeserializationMap; + + class SkinBindingImpl : public RamsesBindingImpl + { + public: + // Move-able (noexcept); Not copy-able + explicit SkinBindingImpl( + std::vector joints, + const std::vector& inverseBindMatrices, + RamsesAppearanceBindingImpl& appearanceBinding, + const ramses::UniformInput& jointMatInput, + std::string_view name, + uint64_t id); + ~SkinBindingImpl() noexcept override = default; + SkinBindingImpl(const SkinBindingImpl& other) = delete; + SkinBindingImpl& operator=(const SkinBindingImpl& other) = delete; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const SkinBindingImpl& skinBinding, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::SkinBinding& skinBinding, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + + [[nodiscard]] const std::vector& getJoints() const; + [[nodiscard]] const RamsesAppearanceBindingImpl& getAppearanceBinding() const; + [[nodiscard]] const ramses::UniformInput& getAppearanceUniformInput() const; + + std::optional update() override; + + void createRootProperties() final; + + private: + std::vector m_joints; + std::vector m_inverseBindMatrices; + RamsesAppearanceBindingImpl& m_appearanceBinding; + ramses::UniformInput m_jointMatInput; + + // temp variable used only in update kept as member to avoid reallocs every update call + std::vector m_jointMatricesArray; + }; +} diff --git a/client/ramses-client/ramses-client-api/AnimationObject.cpp b/client/logic/lib/impl/TimerNode.cpp similarity index 55% rename from client/ramses-client/ramses-client-api/AnimationObject.cpp rename to client/logic/lib/impl/TimerNode.cpp index 7476c71bc..a1b1c7689 100644 --- a/client/ramses-client/ramses-client-api/AnimationObject.cpp +++ b/client/logic/lib/impl/TimerNode.cpp @@ -1,26 +1,20 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH +// Copyright (C) 2021 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -// API -#include "ramses-client-api/AnimationObject.h" - -// internal -#include "AnimationObjectImpl.h" +#include "ramses-logic/TimerNode.h" +#include "impl/TimerNodeImpl.h" namespace ramses { - AnimationObject::AnimationObject(AnimationObjectImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - AnimationObject::~AnimationObject() + TimerNode::TimerNode(std::unique_ptr impl) noexcept + : LogicNode(std::move(impl)) + /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) */ + , m_timerNodeImpl{ static_cast(LogicNode::m_impl) } { } } diff --git a/client/logic/lib/impl/TimerNodeImpl.cpp b/client/logic/lib/impl/TimerNodeImpl.cpp new file mode 100644 index 000000000..5328de04b --- /dev/null +++ b/client/logic/lib/impl/TimerNodeImpl.cpp @@ -0,0 +1,123 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "impl/TimerNodeImpl.h" +#include "ramses-logic/Property.h" +#include "impl/PropertyImpl.h" +#include "internals/ErrorReporting.h" +#include "generated/TimerNodeGen.h" +#include "flatbuffers/flatbuffers.h" +#include "fmt/format.h" + +namespace ramses::internal +{ + TimerNodeImpl::TimerNodeImpl(std::string_view name, uint64_t id) noexcept + : LogicNodeImpl(name, id) + { + } + + void TimerNodeImpl::createRootProperties() + { + HierarchicalTypeData inputs = MakeStruct("", { + {"ticker_us", EPropertyType::Int64} + }); + auto inputsImpl = std::make_unique(std::move(inputs), EPropertySemantics::ScriptInput); + + HierarchicalTypeData outputs = MakeStruct("", { + {"ticker_us", EPropertyType::Int64} + }); + auto outputsImpl = std::make_unique(std::move(outputs), EPropertySemantics::ScriptOutput); + + setRootProperties(std::move(inputsImpl), std::move(outputsImpl)); + } + + std::optional TimerNodeImpl::update() + { + const int64_t ticker = *getInputs()->getChild(0u)->get(); + + int64_t outTicker_us = 0; + if (ticker == 0) // built-in ticker using system clock + { + outTicker_us = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + } + else // user provided ticker + { + outTicker_us = ticker; + } + + getOutputs()->getChild(0u)->m_impl->setValue(outTicker_us); + + return std::nullopt; + } + + flatbuffers::Offset TimerNodeImpl::Serialize( + const TimerNodeImpl& timerNode, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap) + { + // Timer nodes require special serialization logic. We don't want to store system time + // in the files (this makes their content undeterministic). Instead, we write zeroes, and + // cache the current values and restore them again after serialization is done + + // 1. Cache current output values + const PropertyValue outTickerTmp = timerNode.getOutputs()->getChild(0u)->m_impl->getValue(); + + // 2. Set to 0 + timerNode.getOutputs()->getChild(0u)->m_impl->setValue(int64_t(0)); + + // 3. Serialize + const auto logicObject = LogicObjectImpl::Serialize(timerNode, builder); + const auto inputPropertyObject = PropertyImpl::Serialize(*timerNode.getInputs()->m_impl, builder, serializationMap); + const auto outputPropertyObject = PropertyImpl::Serialize(*timerNode.getOutputs()->m_impl, builder, serializationMap); + auto timerNodeOffset = rlogic_serialization::CreateTimerNode( + builder, + logicObject, + inputPropertyObject, + outputPropertyObject + ); + + // 4. Restore values + timerNode.getOutputs()->getChild(0u)->m_impl->setValue(outTickerTmp); + + return timerNodeOffset; + } + + std::unique_ptr TimerNodeImpl::Deserialize( + const rlogic_serialization::TimerNode& timerNodeFB, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap) + { + std::string name; + uint64_t id = 0u; + uint64_t userIdHigh = 0u; + uint64_t userIdLow = 0u; + if (!LogicObjectImpl::Deserialize(timerNodeFB.base(), name, id, userIdHigh, userIdLow, errorReporting) || !timerNodeFB.rootInput() || !timerNodeFB.rootOutput()) + { + errorReporting.add("Fatal error during loading of TimerNode from serialized data: missing name, id or in/out property data!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + auto deserialized = std::make_unique(name, id); + deserialized->setUserId(userIdHigh, userIdLow); + + // deserialize and overwrite constructor generated properties + auto rootInProperty = PropertyImpl::Deserialize(*timerNodeFB.rootInput(), EPropertySemantics::ScriptInput, errorReporting, deserializationMap); + auto rootOutProperty = PropertyImpl::Deserialize(*timerNodeFB.rootOutput(), EPropertySemantics::ScriptOutput, errorReporting, deserializationMap); + if (rootInProperty->getChildCount() != 1u || + rootOutProperty->getChildCount() != 1u || + !rootInProperty->getChild(0u) || rootInProperty->getChild(0u)->getName() != "ticker_us" || + !rootOutProperty->getChild(0u) || rootOutProperty->getChild(0u)->getName() != "ticker_us") + { + errorReporting.add(fmt::format("Fatal error during loading of TimerNode '{}': missing or invalid properties!", name), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + deserialized->setRootProperties(std::move(rootInProperty), std::move(rootOutProperty)); + + return deserialized; + } +} diff --git a/client/logic/lib/impl/TimerNodeImpl.h b/client/logic/lib/impl/TimerNodeImpl.h new file mode 100644 index 000000000..d1b4dcb50 --- /dev/null +++ b/client/logic/lib/impl/TimerNodeImpl.h @@ -0,0 +1,52 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/LogicNodeImpl.h" +#include +#include +#include + +namespace rlogic_serialization +{ + struct TimerNode; +} + +namespace flatbuffers +{ + template struct Offset; + class FlatBufferBuilder; +} + +namespace ramses::internal +{ + class SerializationMap; + class DeserializationMap; + class ErrorReporting; + + class TimerNodeImpl : public LogicNodeImpl + { + public: + TimerNodeImpl(std::string_view name, uint64_t id) noexcept; + + std::optional update() override; + + void createRootProperties() final; + + [[nodiscard]] static flatbuffers::Offset Serialize( + const TimerNodeImpl& timerNode, + flatbuffers::FlatBufferBuilder& builder, + SerializationMap& serializationMap); + + [[nodiscard]] static std::unique_ptr Deserialize( + const rlogic_serialization::TimerNode& timerNodeFB, + ErrorReporting& errorReporting, + DeserializationMap& deserializationMap); + }; +} diff --git a/client/logic/lib/internals/ApiObjects.cpp b/client/logic/lib/internals/ApiObjects.cpp new file mode 100644 index 000000000..a0136a9e6 --- /dev/null +++ b/client/logic/lib/internals/ApiObjects.cpp @@ -0,0 +1,1550 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/ApiObjects.h" +#include "internals/ErrorReporting.h" + +#include "ramses-sdk-build-config.h" + +#include "ramses-logic/LogicObject.h" +#include "ramses-logic/LogicNode.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaModule.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesRenderGroupBindingElements.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/SkinBinding.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/AnimationTypes.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/AnchorPoint.h" + +#include "impl/PropertyImpl.h" +#include "impl/LuaScriptImpl.h" +#include "impl/LuaInterfaceImpl.h" +#include "impl/LuaModuleImpl.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "impl/RamsesAppearanceBindingImpl.h" +#include "impl/RamsesCameraBindingImpl.h" +#include "impl/RamsesRenderPassBindingImpl.h" +#include "impl/RamsesRenderGroupBindingImpl.h" +#include "impl/RamsesMeshNodeBindingImpl.h" +#include "impl/SkinBindingImpl.h" +#include "impl/DataArrayImpl.h" +#include "impl/AnimationNodeImpl.h" +#include "impl/AnimationNodeConfigImpl.h" +#include "impl/TimerNodeImpl.h" +#include "impl/AnchorPointImpl.h" + +#include "ramses-client-api/Node.h" +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/Camera.h" +#include "ramses-client-api/RenderPass.h" +#include "ramses-client-api/RenderGroup.h" +#include "ramses-client-api/MeshNode.h" + +#include "generated/ApiObjectsGen.h" +#include "generated/RamsesAppearanceBindingGen.h" +#include "generated/RamsesBindingGen.h" +#include "generated/RamsesCameraBindingGen.h" +#include "generated/RamsesNodeBindingGen.h" +#include "generated/LinkGen.h" +#include "generated/DataArrayGen.h" +#include "generated/AnimationNodeGen.h" +#include "generated/TimerNodeGen.h" + +#include "fmt/format.h" +#include "TypeUtils.h" +#include "ValidationResults.h" +#include + +namespace ramses::internal +{ + ApiObjects::ApiObjects(ramses::EFeatureLevel featureLevel) + : m_featureLevel{ featureLevel } + { + (void)m_featureLevel; // maybe unused if not affecting any internal objects but kept for future levels + } + + ApiObjects::~ApiObjects() noexcept = default; + + bool ApiObjects::checkLuaModules(const ModuleMapping& moduleMapping, ErrorReporting& errorReporting) + { + for (const auto& module : moduleMapping) + { + if (m_luaModules.cend() == std::find_if(m_luaModules.cbegin(), m_luaModules.cend(), + [&module](const auto& m) { return m == module.second; })) + { + errorReporting.add(fmt::format("Failed to map Lua module '{}'! It was created on a different instance of LogicEngine.", module.first), module.second, EErrorType::IllegalArgument); + return false; + } + } + + return true; + } + + LuaScript* ApiObjects::createLuaScript( + std::string_view source, + const LuaConfigImpl& config, + std::string_view scriptName, + ErrorReporting& errorReporting) + { + const ModuleMapping& modules = config.getModuleMapping(); + if (!checkLuaModules(modules, errorReporting)) + return nullptr; + + std::optional compiledScript = LuaCompilationUtils::CompileScriptOrImportPrecompiled( + *m_solState, + modules, + config.getStandardModules(), + std::string{ source }, + scriptName, + errorReporting, + {}, {}, {}, + config.hasDebugLogFunctionsEnabled()); + + if (!compiledScript) + return nullptr; + + auto impl = std::make_unique(std::move(*compiledScript), scriptName, getNextLogicObjectId()); + impl->createRootProperties(); + + return &createAndRegisterObject(std::move(impl)); + } + + LuaInterface* ApiObjects::createLuaInterface( + std::string_view source, + const LuaConfigImpl& config, + std::string_view interfaceName, + ErrorReporting& errorReporting, + bool verifyModules) + { + if (interfaceName.empty()) + { + errorReporting.add("Can't create interface with empty name!", nullptr, EErrorType::IllegalArgument); + return nullptr; + } + + const ModuleMapping& modules = config.getModuleMapping(); + if (!checkLuaModules(modules, errorReporting)) + return nullptr; + + std::optional compiledInterface = LuaCompilationUtils::CompileInterface( + *m_solState, + modules, + config.getStandardModules(), + verifyModules, + std::string{ source }, + interfaceName, + errorReporting); + + if (!compiledInterface) + return nullptr; + + auto impl = std::make_unique(std::move(*compiledInterface), interfaceName, getNextLogicObjectId()); + impl->createRootProperties(); + + return &createAndRegisterObject(std::move(impl)); + } + + LuaModule* ApiObjects::createLuaModule( + std::string_view source, + const LuaConfigImpl& config, + std::string_view moduleName, + ErrorReporting& errorReporting) + { + const ModuleMapping& modules = config.getModuleMapping(); + if (!checkLuaModules(modules, errorReporting)) + return nullptr; + + std::optional compiledModule = LuaCompilationUtils::CompileModuleOrImportPrecompiled( + *m_solState, + modules, + config.getStandardModules(), + std::string{source}, + moduleName, + errorReporting, + {}, + config.hasDebugLogFunctionsEnabled()); + + if (!compiledModule) + return nullptr; + + auto impl = std::make_unique(std::move(*compiledModule), moduleName, getNextLogicObjectId()); + return &createAndRegisterObject(std::move(impl)); + } + + RamsesNodeBinding* ApiObjects::createRamsesNodeBinding(ramses::Node& ramsesNode, ramses::ERotationType rotationType, std::string_view name) + { + auto impl = std::make_unique(ramsesNode, rotationType, name, getNextLogicObjectId()); + impl->createRootProperties(); + return &createAndRegisterObject(std::move(impl)); + } + + RamsesAppearanceBinding* ApiObjects::createRamsesAppearanceBinding(ramses::Appearance& ramsesAppearance, std::string_view name) + { + auto impl = std::make_unique(ramsesAppearance, name, getNextLogicObjectId()); + impl->createRootProperties(); + return &createAndRegisterObject(std::move(impl)); + } + + RamsesCameraBinding* ApiObjects::createRamsesCameraBinding(ramses::Camera& ramsesCamera, bool withFrustumPlanes, std::string_view name) + { + auto impl = std::make_unique(ramsesCamera, withFrustumPlanes, name, getNextLogicObjectId()); + impl->createRootProperties(); + return &createAndRegisterObject(std::move(impl)); + } + + RamsesRenderPassBinding* ApiObjects::createRamsesRenderPassBinding(ramses::RenderPass& ramsesRenderPass, std::string_view name) + { + auto impl = std::make_unique(ramsesRenderPass, name, getNextLogicObjectId()); + impl->createRootProperties(); + return &createAndRegisterObject(std::move(impl)); + } + + RamsesRenderGroupBinding* ApiObjects::createRamsesRenderGroupBinding(ramses::RenderGroup& ramsesRenderGroup, const RamsesRenderGroupBindingElements& elements, std::string_view name) + { + auto impl = std::make_unique(ramsesRenderGroup, *elements.m_impl, name, getNextLogicObjectId()); + impl->createRootProperties(); + return &createAndRegisterObject(std::move(impl)); + } + + RamsesMeshNodeBinding* ApiObjects::createRamsesMeshNodeBinding(ramses::MeshNode& ramsesMeshNode, std::string_view name) + { + auto impl = std::make_unique(ramsesMeshNode, name, getNextLogicObjectId()); + impl->createRootProperties(); + return &createAndRegisterObject(std::move(impl)); + } + + SkinBinding* ApiObjects::createSkinBinding( + std::vector joints, + const std::vector& inverseBindMatrices, + RamsesAppearanceBindingImpl& appearanceBinding, + const ramses::UniformInput& jointMatInput, + std::string_view name) + { + auto impl = std::make_unique(std::move(joints), inverseBindMatrices, appearanceBinding, jointMatInput, name, getNextLogicObjectId()); + impl->createRootProperties(); + return &createAndRegisterObject(std::move(impl)); + } + + template + DataArray* ApiObjects::createDataArray(const std::vector& data, std::string_view name) + { + static_assert(CanPropertyTypeBeStoredInDataArray(PropertyTypeToEnum::TYPE)); + // make copy of users data and move into data array + std::vector dataCopy = data; + auto impl = std::make_unique(std::move(dataCopy), name, getNextLogicObjectId()); + return &createAndRegisterObject(std::move(impl)); + } + + AnimationNode* ApiObjects::createAnimationNode(const AnimationNodeConfigImpl& config, std::string_view name) + { + auto impl = std::make_unique(config.getChannels(), config.getExposingOfChannelDataAsProperties(), name, getNextLogicObjectId()); + impl->createRootProperties(); + return &createAndRegisterObject(std::move(impl)); + } + + TimerNode* ApiObjects::createTimerNode(std::string_view name) + { + auto impl = std::make_unique(name, getNextLogicObjectId()); + impl->createRootProperties(); + return &createAndRegisterObject(std::move(impl)); + } + + AnchorPoint* ApiObjects::createAnchorPoint(RamsesNodeBindingImpl& nodeBinding, RamsesCameraBindingImpl& cameraBinding, std::string_view name) + { + auto impl = std::make_unique(nodeBinding, cameraBinding, name, getNextLogicObjectId()); + impl->createRootProperties(); + auto& anchor = createAndRegisterObject(std::move(impl)); + + m_logicNodeDependencies.addBindingDependency(nodeBinding, anchor.m_impl); + m_logicNodeDependencies.addBindingDependency(cameraBinding, anchor.m_impl); + + return &anchor; + } + + bool ApiObjects::destroy(LogicObject& object, ErrorReporting& errorReporting) + { + auto luaScript = dynamic_cast(&object); + if (luaScript) + return destroyAndUnregisterObject(*luaScript, errorReporting); + + auto luaInterface = dynamic_cast(&object); + if (luaInterface) + return destroyAndUnregisterObject(*luaInterface, errorReporting); + + auto luaModule = dynamic_cast(&object); + if (luaModule) + return destroyInternal(*luaModule, errorReporting); + + auto ramsesNodeBinding = dynamic_cast(&object); + if (ramsesNodeBinding) + return destroyInternal(*ramsesNodeBinding, errorReporting); + + auto ramsesAppearanceBinding = dynamic_cast(&object); + if (ramsesAppearanceBinding) + return destroyInternal(*ramsesAppearanceBinding, errorReporting); + + auto ramsesCameraBinding = dynamic_cast(&object); + if (ramsesCameraBinding) + return destroyInternal(*ramsesCameraBinding, errorReporting); + + auto ramsesRenderPassBinding = dynamic_cast(&object); + if (ramsesRenderPassBinding) + return destroyAndUnregisterObject(*ramsesRenderPassBinding, errorReporting); + + auto ramsesRenderGroupBinding = dynamic_cast(&object); + if (ramsesRenderGroupBinding) + return destroyAndUnregisterObject(*ramsesRenderGroupBinding, errorReporting); + + auto ramsesMeshNodeBinding = dynamic_cast(&object); + if (ramsesMeshNodeBinding) + return destroyAndUnregisterObject(*ramsesMeshNodeBinding, errorReporting); + + auto skinBinding = dynamic_cast(&object); + if (skinBinding) + return destroyAndUnregisterObject(*skinBinding, errorReporting); + + auto animNode = dynamic_cast(&object); + if (animNode) + return destroyAndUnregisterObject(*animNode, errorReporting); + + auto dataArray = dynamic_cast(&object); + if (dataArray) + return destroyInternal(*dataArray, errorReporting); + + auto timer = dynamic_cast(&object); + if (timer) + return destroyAndUnregisterObject(*timer, errorReporting); + + auto anchor = dynamic_cast(&object); + if (anchor) + return destroyInternal(*anchor, errorReporting); + + errorReporting.add(fmt::format("Tried to destroy object '{}' with unknown type", object.getName()), &object, EErrorType::IllegalArgument); + + return false; + } + + bool ApiObjects::destroyInternal(DataArray& dataArray, ErrorReporting& errorReporting) + { + for (const auto& animNode : m_animationNodes) + { + for (const auto& channel : animNode->getChannels()) + { + if (channel.timeStamps == &dataArray || + channel.keyframes == &dataArray || + channel.tangentsIn == &dataArray || + channel.tangentsOut == &dataArray) + { + errorReporting.add(fmt::format("Failed to destroy data array '{}', it is used in animation node '{}' channel '{}'", dataArray.getName(), animNode->getName(), channel.name), &dataArray, EErrorType::IllegalArgument); + return false; + } + } + } + + return destroyAndUnregisterObject(dataArray, errorReporting); + } + + bool ApiObjects::destroyInternal(LuaModule& luaModule, ErrorReporting& errorReporting) + { + for (const auto& script : m_scripts) + { + for (const auto& moduleInUse : script->m_script.getModules()) + { + if (moduleInUse.second == &luaModule) + { + errorReporting.add(fmt::format("Failed to destroy LuaModule '{}', it is used in LuaScript '{}'", luaModule.getName(), script->getName()), &luaModule, EErrorType::IllegalArgument); + return false; + } + } + } + + return destroyAndUnregisterObject(luaModule, errorReporting); + } + + bool ApiObjects::destroyInternal(RamsesNodeBinding& ramsesNodeBinding, ErrorReporting& errorReporting) + { + for (const auto& anchor : m_anchorPoints) + { + if (anchor->m_anchorPointImpl.getRamsesNodeBinding().getId() == ramsesNodeBinding.getId()) + { + errorReporting.add(fmt::format("Failed to destroy Ramses node binding '{}', it is used in anchor point '{}'", ramsesNodeBinding.getName(), anchor->getName()), &ramsesNodeBinding, EErrorType::Other); + return false; + } + } + + for (const auto& skin : m_skinBindings) + { + for (const auto node : skin->m_skinBinding.getJoints()) + { + if (node->getId() == ramsesNodeBinding.getId()) + { + errorReporting.add(fmt::format("Failed to destroy Ramses node binding '{}', it is used in skin binding '{}'", ramsesNodeBinding.getName(), skin->getName()), &ramsesNodeBinding, EErrorType::Other); + return false; + } + } + } + + return destroyAndUnregisterObject(ramsesNodeBinding, errorReporting); + } + + bool ApiObjects::destroyInternal(RamsesAppearanceBinding& ramsesAppearanceBinding, ErrorReporting& errorReporting) + { + for (const auto& skin : m_skinBindings) + { + if (skin->m_skinBinding.getAppearanceBinding().getId() == ramsesAppearanceBinding.getId()) + { + errorReporting.add(fmt::format("Failed to destroy Ramses appearance binding '{}', it is used in skin binding '{}'", ramsesAppearanceBinding.getName(), skin->getName()), &ramsesAppearanceBinding, EErrorType::Other); + return false; + } + } + + return destroyAndUnregisterObject(ramsesAppearanceBinding, errorReporting); + } + + bool ApiObjects::destroyInternal(RamsesCameraBinding& ramsesCameraBinding, ErrorReporting& errorReporting) + { + for (const auto& anchor : m_anchorPoints) + { + if (anchor->m_anchorPointImpl.getRamsesCameraBinding().getId() == ramsesCameraBinding.getId()) + { + errorReporting.add(fmt::format("Failed to destroy Ramses camera binding '{}', it is used in anchor point '{}'", ramsesCameraBinding.getName(), anchor->getName()), &ramsesCameraBinding, EErrorType::Other); + return false; + } + } + + return destroyAndUnregisterObject(ramsesCameraBinding, errorReporting); + } + + bool ApiObjects::destroyInternal(AnchorPoint& node, ErrorReporting& errorReporting) + { + if (std::find(m_anchorPoints.cbegin(), m_anchorPoints.cend(), &node) != m_anchorPoints.end()) + { + m_logicNodeDependencies.removeBindingDependency(node.m_anchorPointImpl.getRamsesNodeBinding(), node.m_impl); + m_logicNodeDependencies.removeBindingDependency(node.m_anchorPointImpl.getRamsesCameraBinding(), node.m_impl); + } + + return destroyAndUnregisterObject(node, errorReporting); + } + + template + T& ApiObjects::createAndRegisterObject(std::unique_ptr impl) + { + static_assert(std::is_base_of_v, "Meant for LogicObject instances only"); + std::unique_ptr> object{ new T{ std::move(impl) }, [](LogicObject* obj_) { delete obj_; } }; + + T& objRaw = *object; + + // move to owning pool + assert(std::find_if(m_objectsOwningContainer.cbegin(), m_objectsOwningContainer.cend(), [&](const auto& obj) { return obj.get() == &objRaw; }) == m_objectsOwningContainer.cend()); + m_objectsOwningContainer.push_back(std::move(object)); + + // set reference to HL object so impl can access its HL instance + objRaw.LogicObject::m_impl->setLogicObject(objRaw); + + // store in general pool of logic objects + assert(std::find(m_logicObjects.cbegin(), m_logicObjects.cend(), &objRaw) == m_logicObjects.cend()); + m_logicObjects.push_back(&objRaw); + // store in map to retrieve object by ID + assert(m_logicObjectIdMapping.count(objRaw.getId()) == 0u); + m_logicObjectIdMapping.emplace(objRaw.getId(), &objRaw); + + // put into own scope as clang-tidy is confused with constexpr branches and reports indentation warning + { + // store in its typed pool + if constexpr (std::is_same_v) + { + this->m_scripts.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_interfaces.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_luaModules.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_ramsesNodeBindings.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_ramsesAppearanceBindings.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_ramsesCameraBindings.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_ramsesRenderPassBindings.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_ramsesRenderGroupBindings.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_ramsesMeshNodeBindings.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_skinBindings.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_dataArrays.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_animationNodes.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_timerNodes.push_back(&objRaw); + } + else if constexpr (std::is_same_v) + { + this->m_anchorPoints.push_back(&objRaw); + } + else + { + assert(!"unhandled type"); + } + + // types derived from LogicNode are added to node dependency graph + if constexpr (std::is_base_of_v) + m_logicNodeDependencies.addNode(objRaw.LogicNode::m_impl); + } + + return objRaw; + } + + template + bool ApiObjects::destroyAndUnregisterObject(T& objToDelete, ErrorReporting& errorReporting) + { + static_assert(std::is_base_of_v, "Meant for LogicObject instances only"); + + const auto findOwnedObj = std::find_if(m_objectsOwningContainer.cbegin(), m_objectsOwningContainer.cend(), [&](const auto& obj) { return obj.get() == &objToDelete; }); + if (findOwnedObj == m_objectsOwningContainer.cend()) + { + errorReporting.add(fmt::format("Failed to destroy object '{}', cannot find it in this LogicEngine instance.", objToDelete.LogicObject::m_impl->getIdentificationString()), &objToDelete, EErrorType::IllegalArgument); + return false; + } + + auto eraseFromPool = [](auto& obj, auto& pool) { pool.erase(std::find(pool.begin(), pool.end(), &obj)); }; + + // put into own scope as clang-tidy is confused with constexpr branches and reports indentation warning + { + if constexpr (std::is_base_of_v) + { + LogicNodeImpl& logicNodeImpl = objToDelete.LogicNode::m_impl; + m_logicNodeDependencies.removeNode(logicNodeImpl); + } + } + + // put into own scope as clang-tidy is confused with constexpr branches and reports indentation warning + { + if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_scripts); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_interfaces); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_luaModules); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_ramsesNodeBindings); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_ramsesAppearanceBindings); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_ramsesCameraBindings); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_ramsesRenderPassBindings); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_ramsesRenderGroupBindings); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_ramsesMeshNodeBindings); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_skinBindings); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_dataArrays); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_animationNodes); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_timerNodes); + } + else if constexpr (std::is_same_v) + { + eraseFromPool(objToDelete, this->m_anchorPoints); + } + else + { + assert(!"unhandled type"); + } + } + + assert(m_logicObjectIdMapping.count(objToDelete.getId()) != 0u); + m_logicObjectIdMapping.erase(objToDelete.getId()); + + const auto findLogicObj = std::find(m_logicObjects.cbegin(), m_logicObjects.cend(), &objToDelete); + assert(findLogicObj != m_logicObjects.cend() && "Can't find LogicObject in logic objects!"); + m_logicObjects.erase(findLogicObj); + + m_objectsOwningContainer.erase(findOwnedObj); + + return true; + } + + bool ApiObjects::checkBindingsReferToSameRamsesScene(ErrorReporting& errorReporting) const + { + // Optional because it's OK that no Ramses object is referenced at all (and thus no ramses scene) + std::optional sceneId; + + for (const auto& binding : m_ramsesNodeBindings) + { + const ramses::Node& node = binding->m_nodeBinding.getRamsesNode(); + const ramses::sceneId_t nodeSceneId = node.getSceneId(); + if (!sceneId) + { + sceneId = nodeSceneId; + } + + if (*sceneId != nodeSceneId) + { + errorReporting.add(fmt::format("Ramses node '{}' is from scene with id:{} but other objects are from scene with id:{}!", + node.getName(), nodeSceneId.getValue(), sceneId->getValue()), binding, EErrorType::IllegalArgument); + return false; + } + } + + for (const auto& binding : m_ramsesAppearanceBindings) + { + const ramses::Appearance& appearance = binding->m_appearanceBinding.getRamsesAppearance(); + const ramses::sceneId_t appearanceSceneId = appearance.getSceneId(); + if (!sceneId) + { + sceneId = appearanceSceneId; + } + + if (*sceneId != appearanceSceneId) + { + errorReporting.add(fmt::format("Ramses appearance '{}' is from scene with id:{} but other objects are from scene with id:{}!", + appearance.getName(), appearanceSceneId.getValue(), sceneId->getValue()), binding, EErrorType::IllegalArgument); + return false; + } + } + + for (const auto& binding : m_ramsesCameraBindings) + { + const ramses::Camera& camera = binding->m_cameraBinding.getRamsesCamera(); + const ramses::sceneId_t cameraSceneId = camera.getSceneId(); + if (!sceneId) + { + sceneId = cameraSceneId; + } + + if (*sceneId != cameraSceneId) + { + errorReporting.add(fmt::format("Ramses camera '{}' is from scene with id:{} but other objects are from scene with id:{}!", + camera.getName(), cameraSceneId.getValue(), sceneId->getValue()), binding, EErrorType::IllegalArgument); + return false; + } + } + + for (const auto& binding : m_ramsesRenderPassBindings) + { + const ramses::RenderPass& rp = binding->m_renderPassBinding.getRamsesRenderPass(); + const ramses::sceneId_t rpSceneId = rp.getSceneId(); + if (!sceneId) + sceneId = rpSceneId; + + if (*sceneId != rpSceneId) + { + errorReporting.add(fmt::format("Ramses render pass '{}' is from scene with id:{} but other objects are from scene with id:{}!", + rp.getName(), rpSceneId.getValue(), sceneId->getValue()), binding, EErrorType::IllegalArgument); + return false; + } + } + + for (const auto& binding : m_ramsesRenderGroupBindings) + { + const auto& rg = binding->m_renderGroupBinding.getRamsesRenderGroup(); + const ramses::sceneId_t rgSceneId = rg.getSceneId(); + if (!sceneId) + sceneId = rgSceneId; + + if (*sceneId != rgSceneId) + { + errorReporting.add(fmt::format("Ramses render group '{}' is from scene with id:{} but other objects are from scene with id:{}!", + rg.getName(), rgSceneId.getValue(), sceneId->getValue()), binding, EErrorType::IllegalArgument); + return false; + } + } + + for (const auto& binding : m_ramsesMeshNodeBindings) + { + const auto& mn = binding->m_meshNodeBinding.getRamsesMeshNode(); + const ramses::sceneId_t mnSceneId = mn.getSceneId(); + if (!sceneId) + sceneId = mnSceneId; + + if (*sceneId != mnSceneId) + { + errorReporting.add(fmt::format("Ramses mesh node '{}' is from scene with id:{} but other objects are from scene with id:{}!", + mn.getName(), mnSceneId.getValue(), sceneId->getValue()), binding, EErrorType::IllegalArgument); + return false; + } + } + + return true; + } + + void ApiObjects::validateInterfaces(ValidationResults& validationResults) const + { + // check if there are any outputs without link + // Note: this is different from the check for dangling nodes/content, where nodes are checked if they have ANY + // linked inputs and/or outputs depending on node type. Interfaces on the other hand must have ALL of their outputs + // linked to be valid. + + for (const auto* intf : m_interfaces) + { + std::vector unlinkedProperties = intf->m_interface.collectUnlinkedProperties(); + for (const auto* output : unlinkedProperties) + validationResults.add(::fmt::format("Interface [{}] has unlinked output [{}]", intf->getName(), output->getName()), intf, EWarningType::UnusedContent); + } + + // check if there are any name conflicts + auto interfacesByName = m_interfaces; + std::sort(interfacesByName.begin(), interfacesByName.end(), [](const auto i1, const auto i2) { return i1->getName() < i2->getName(); }); + const auto duplicateIt = std::adjacent_find(interfacesByName.cbegin(), interfacesByName.cend(), [](const auto i1, const auto i2) { return i1->getName() == i2->getName(); }); + if (duplicateIt != interfacesByName.cend()) + validationResults.add(fmt::format("Interface [{}] does not have a unique name", (*duplicateIt)->getName()), *duplicateIt, EWarningType::Other); + } + + void ApiObjects::validateDanglingNodes(ValidationResults& validationResults) const + { + //nodes with no outputs linked + for (const auto* logicObj : m_logicObjects) + { + const auto* objAsNode = dynamic_cast(logicObj); + if (objAsNode != nullptr) + { + if (dynamic_cast(logicObj) || // interfaces have their own validation logic in ApiObjects::validateInterfaces + dynamic_cast(logicObj) || // bindings have no outputs + dynamic_cast(logicObj)) // anchor points are being used in special way which sometimes involves reading output value directly by application only + continue; + + assert(objAsNode->getOutputs() != nullptr); + // only check for unlinked outputs if there are any + if (objAsNode->getOutputs()->getChildCount() != 0u) + { + bool anyOutputLinked = false; + for (const auto* output : objAsNode->getOutputs()->m_impl->collectLeafChildren()) + anyOutputLinked |= (output->hasOutgoingLink()); + + if (!anyOutputLinked) + validationResults.add(fmt::format("Node [{}] has no outgoing links! Node should be deleted or properly linked!", objAsNode->getName()), objAsNode, EWarningType::UnusedContent); + } + } + } + + // collect node bindings used in anchor points and skin bindings + // - these are allowed to have no incoming links because they are being read from + std::unordered_set bindingsInUse; + for (const auto* anchor : m_anchorPoints) + { + bindingsInUse.insert(anchor->m_anchorPointImpl.getRamsesNodeBinding().getLogicObject().as()); + bindingsInUse.insert(anchor->m_anchorPointImpl.getRamsesCameraBinding().getLogicObject().as()); + } + for (const auto* skin : m_skinBindings) + { + bindingsInUse.insert(skin->m_skinBinding.getAppearanceBinding().getLogicObject().as()); + for (const auto* joint : skin->m_skinBinding.getJoints()) + bindingsInUse.insert(joint->getLogicObject().as()); + } + + //nodes with no linked inputs + for (const auto* logicObj : m_logicObjects) + { + const auto* objAsNode = dynamic_cast(logicObj); + if (objAsNode != nullptr) + { + if (dynamic_cast(logicObj) || // interfaces have their own validation logic in ApiObjects::validateInterfaces + dynamic_cast(logicObj) || // timer not having input is valid use case which enables internal clock ticker + dynamic_cast(logicObj) || // anchor points have no inputs + dynamic_cast(logicObj)) // skinbinding has no inputs + continue; + + if (bindingsInUse.count(dynamic_cast(logicObj)) != 0) // bindings in use by other rlogic objects can be without incoming links + continue; + + assert(objAsNode->getInputs() != nullptr); + // only check for unlinked inputs if there are any + if (objAsNode->getInputs()->getChildCount() != 0u) + { + bool anyInputLinked = false; + for (const auto* input : objAsNode->getInputs()->m_impl->collectLeafChildren()) + anyInputLinked |= (input->hasIncomingLink()); + + if (!anyInputLinked) + validationResults.add(fmt::format("Node [{}] has no ingoing links! Node should be deleted or properly linked!", objAsNode->getName()), objAsNode, EWarningType::UnusedContent); + } + } + } + } + + template + const ApiObjectContainer& ApiObjects::getApiObjectContainer() const + { + if constexpr (std::is_same_v) + { + return m_logicObjects; + } + else if constexpr (std::is_same_v) + { + return m_scripts; + } + else if constexpr (std::is_same_v) + { + return m_interfaces; + } + else if constexpr (std::is_same_v) + { + return m_luaModules; + } + else if constexpr (std::is_same_v) + { + return m_ramsesNodeBindings; + } + else if constexpr (std::is_same_v) + { + return m_ramsesAppearanceBindings; + } + else if constexpr (std::is_same_v) + { + return m_ramsesCameraBindings; + } + else if constexpr (std::is_same_v) + { + return m_ramsesRenderPassBindings; + } + else if constexpr (std::is_same_v) + { + return m_ramsesRenderGroupBindings; + } + else if constexpr (std::is_same_v) + { + return m_ramsesMeshNodeBindings; + } + else if constexpr (std::is_same_v) + { + return m_skinBindings; + } + else if constexpr (std::is_same_v) + { + return m_dataArrays; + } + else if constexpr (std::is_same_v) + { + return m_animationNodes; + } + else if constexpr (std::is_same_v) + { + return m_timerNodes; + } + else if constexpr (std::is_same_v) + { + return m_anchorPoints; + } + } + + template + ApiObjectContainer& ApiObjects::getApiObjectContainer() + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) non-const version of getApiObjectContainer cast to its const version to avoid duplicating code + return const_cast&>((const_cast(*this)).getApiObjectContainer()); + } + + const ApiObjectOwningContainer& ApiObjects::getApiObjectOwningContainer() const + { + return m_objectsOwningContainer; + } + + LogicNodeDependencies& ApiObjects::getLogicNodeDependencies() + { + return m_logicNodeDependencies; + } + + const LogicNodeDependencies& ApiObjects::getLogicNodeDependencies() const + { + return m_logicNodeDependencies; + } + + LogicObject* ApiObjects::getApiObjectById(uint64_t id) const + { + auto apiObjectIter = m_logicObjectIdMapping.find(id); + if (apiObjectIter != m_logicObjectIdMapping.end()) + { + assert(apiObjectIter->second->getId() == id); + return apiObjectIter->second; + } + return nullptr; + } + + flatbuffers::Offset ApiObjects::Serialize(const ApiObjects& apiObjects, flatbuffers::FlatBufferBuilder& builder, ELuaSavingMode luaSavingMode) + { + SerializationMap serializationMap; + + std::vector> luaModules; + luaModules.reserve(apiObjects.m_luaModules.size()); + for (const auto& luaModule : apiObjects.m_luaModules) + luaModules.push_back(LuaModuleImpl::Serialize(luaModule->m_impl, builder, serializationMap, luaSavingMode)); + + std::vector> luascripts; + luascripts.reserve(apiObjects.m_scripts.size()); + std::transform(apiObjects.m_scripts.begin(), apiObjects.m_scripts.end(), std::back_inserter(luascripts), + [&builder, &serializationMap, luaSavingMode](const std::vector::value_type& it) { + return LuaScriptImpl::Serialize(it->m_script, builder, serializationMap, luaSavingMode); + }); + + std::vector> luaInterfaces; + luascripts.reserve(apiObjects.m_interfaces.size()); + std::transform(apiObjects.m_interfaces.cbegin(), apiObjects.m_interfaces.cend(), std::back_inserter(luaInterfaces), + [&builder, &serializationMap](const std::vector::value_type& it) { + return LuaInterfaceImpl::Serialize(it->m_interface, builder, serializationMap); + }); + + std::vector> ramsesnodebindings; + ramsesnodebindings.reserve(apiObjects.m_ramsesNodeBindings.size()); + std::transform(apiObjects.m_ramsesNodeBindings.begin(), + apiObjects.m_ramsesNodeBindings.end(), + std::back_inserter(ramsesnodebindings), + [&builder, &serializationMap](const std::vector::value_type& it) { + return RamsesNodeBindingImpl::Serialize(it->m_nodeBinding, builder, serializationMap); + }); + + std::vector> ramsesappearancebindings; + ramsesappearancebindings.reserve(apiObjects.m_ramsesAppearanceBindings.size()); + std::transform(apiObjects.m_ramsesAppearanceBindings.begin(), + apiObjects.m_ramsesAppearanceBindings.end(), + std::back_inserter(ramsesappearancebindings), + [&builder, &serializationMap](const std::vector::value_type& it) { + return RamsesAppearanceBindingImpl::Serialize(it->m_appearanceBinding, builder, serializationMap); + }); + + std::vector> ramsescamerabindings; + ramsescamerabindings.reserve(apiObjects.m_ramsesCameraBindings.size()); + std::transform(apiObjects.m_ramsesCameraBindings.begin(), + apiObjects.m_ramsesCameraBindings.end(), + std::back_inserter(ramsescamerabindings), + [&builder, &serializationMap](const std::vector::value_type& it) { + return RamsesCameraBindingImpl::Serialize(it->m_cameraBinding, builder, serializationMap); + }); + + std::vector> ramsesrenderpassbindings; + ramsesrenderpassbindings.reserve(apiObjects.m_ramsesRenderPassBindings.size()); + std::transform(apiObjects.m_ramsesRenderPassBindings.begin(), + apiObjects.m_ramsesRenderPassBindings.end(), + std::back_inserter(ramsesrenderpassbindings), + [&builder, &serializationMap](const std::vector::value_type& it) { + return RamsesRenderPassBindingImpl::Serialize(it->m_renderPassBinding, builder, serializationMap); + }); + + std::vector> dataArrays; + dataArrays.reserve(apiObjects.m_dataArrays.size()); + for (const auto& da : apiObjects.m_dataArrays) + { + dataArrays.push_back(DataArrayImpl::Serialize(da->m_impl, builder, serializationMap)); + serializationMap.storeDataArray(da->getId(), dataArrays.back()); + } + + // animation nodes must go after data arrays because they reference them + std::vector> animationNodes; + animationNodes.reserve(apiObjects.m_animationNodes.size()); + for (const auto& animNode : apiObjects.m_animationNodes) + animationNodes.push_back(AnimationNodeImpl::Serialize(animNode->m_animationNodeImpl, builder, serializationMap)); + + std::vector> timerNodes; + timerNodes.reserve(apiObjects.m_timerNodes.size()); + for (const auto& timerNode : apiObjects.m_timerNodes) + timerNodes.push_back(TimerNodeImpl::Serialize(timerNode->m_timerNodeImpl, builder, serializationMap)); + + // anchor points must go after node and camera bindings because they reference them + std::vector> anchorPoints; + anchorPoints.reserve(apiObjects.m_anchorPoints.size()); + for (const auto& anchorPoint : apiObjects.m_anchorPoints) + anchorPoints.push_back(AnchorPointImpl::Serialize(anchorPoint->m_anchorPointImpl, builder, serializationMap)); + + std::vector> ramsesRenderGroupBindings; + ramsesRenderGroupBindings.reserve(apiObjects.m_ramsesRenderGroupBindings.size()); + for (const auto& rgBinding : apiObjects.m_ramsesRenderGroupBindings) + ramsesRenderGroupBindings.push_back(RamsesRenderGroupBindingImpl::Serialize(rgBinding->m_renderGroupBinding, builder, serializationMap)); + + std::vector> ramsesMeshNodeBindings; + ramsesMeshNodeBindings.reserve(apiObjects.m_ramsesMeshNodeBindings.size()); + for (const auto& mnBinding : apiObjects.m_ramsesMeshNodeBindings) + ramsesMeshNodeBindings.push_back(RamsesMeshNodeBindingImpl::Serialize(mnBinding->m_meshNodeBinding, builder, serializationMap)); + + std::vector> skinBindings; + skinBindings.reserve(apiObjects.m_skinBindings.size()); + for (const auto& skinBinding : apiObjects.m_skinBindings) + skinBindings.push_back(SkinBindingImpl::Serialize(skinBinding->m_skinBinding, builder, serializationMap)); + + // links must go last due to dependency on serialized properties + const auto collectedLinks = apiObjects.collectPropertyLinks(); + std::vector> links; + links.reserve(collectedLinks.size()); + for (const auto& link : collectedLinks) + { + links.push_back(rlogic_serialization::CreateLink(builder, + serializationMap.resolvePropertyOffset(*link.source->m_impl), + serializationMap.resolvePropertyOffset(*link.target->m_impl), + link.isWeakLink)); + } + + const auto fbModules = builder.CreateVector(luaModules); + const auto fbScripts = builder.CreateVector(luascripts); + const auto fbInterfaces = builder.CreateVector(luaInterfaces); + const auto fbNodeBindings = builder.CreateVector(ramsesnodebindings); + const auto fbAppearanceBindings = builder.CreateVector(ramsesappearancebindings); + const auto fbCameraBindings = builder.CreateVector(ramsescamerabindings); + const auto fbDataArrays = builder.CreateVector(dataArrays); + const auto fbAnimations = builder.CreateVector(animationNodes); + const auto fbTimers = builder.CreateVector(timerNodes); + const auto fbLinks = builder.CreateVector(links); + const auto fbRenderPasses = builder.CreateVector(ramsesrenderpassbindings); + const auto fbAnchorPoints = builder.CreateVector(anchorPoints); + const auto fbRenderGroupBindings = builder.CreateVector(ramsesRenderGroupBindings); + const auto fbMeshNodeBindings = builder.CreateVector(ramsesMeshNodeBindings); + const auto fbSkinBindings = builder.CreateVector(skinBindings); + + const auto logicEngine = rlogic_serialization::CreateApiObjects( + builder, + fbModules, + fbScripts, + fbInterfaces, + fbNodeBindings, + fbAppearanceBindings, + fbCameraBindings, + fbDataArrays, + fbAnimations, + fbTimers, + fbLinks, + apiObjects.m_lastObjectId, + fbRenderPasses, + fbAnchorPoints, + fbRenderGroupBindings, + fbSkinBindings, + fbMeshNodeBindings + ); + + builder.Finish(logicEngine); + + return logicEngine; + } + + std::unique_ptr ApiObjects::Deserialize( + const rlogic_serialization::ApiObjects& apiObjects, + const IRamsesObjectResolver* ramsesResolver, + const std::string& dataSourceDescription, + ErrorReporting& errorReporting, + ramses::EFeatureLevel featureLevel) + { + // Collect data here, only return if no error occurred + auto deserialized = std::make_unique(featureLevel); + + // Collect deserialized object mappings to resolve dependencies + DeserializationMap deserializationMap; + + if (!apiObjects.luaModules()) + { + errorReporting.add("Fatal error during loading from serialized data: missing Lua modules container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.luaScripts()) + { + errorReporting.add("Fatal error during loading from serialized data: missing Lua scripts container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.luaInterfaces()) + { + errorReporting.add("Fatal error during loading from serialized data: missing Lua interfaces container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.nodeBindings()) + { + errorReporting.add("Fatal error during loading from serialized data: missing node bindings container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.appearanceBindings()) + { + errorReporting.add("Fatal error during loading from serialized data: missing appearance bindings container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.cameraBindings()) + { + errorReporting.add("Fatal error during loading from serialized data: missing camera bindings container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.renderPassBindings()) + { + errorReporting.add("Fatal error during loading from serialized data: missing renderpass bindings container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.links()) + { + errorReporting.add("Fatal error during loading from serialized data: missing links container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.dataArrays()) + { + errorReporting.add("Fatal error during loading from serialized data: missing data arrays container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.animationNodes()) + { + errorReporting.add("Fatal error during loading from serialized data: missing animation nodes container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.timerNodes()) + { + errorReporting.add("Fatal error during loading from serialized data: missing timer nodes container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.anchorPoints()) + { + errorReporting.add("Fatal error during loading from serialized data: missing anchor points container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.renderGroupBindings()) + { + errorReporting.add("Fatal error during loading from serialized data: missing rendergroup bindings container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.meshNodeBindings()) + { + errorReporting.add("Fatal error during loading from serialized data: missing meshnode bindings container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!apiObjects.skinBindings()) + { + errorReporting.add("Fatal error during loading from serialized data: missing skin bindings container!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + deserialized->m_lastObjectId = apiObjects.lastObjectId(); + + const size_t logicObjectsTotalSize = + static_cast(apiObjects.luaModules()->size()) + + static_cast(apiObjects.luaScripts()->size()) + + static_cast(apiObjects.luaInterfaces()->size()) + + static_cast(apiObjects.nodeBindings()->size()) + + static_cast(apiObjects.appearanceBindings()->size()) + + static_cast(apiObjects.cameraBindings()->size()) + + static_cast(apiObjects.renderPassBindings()->size()) + + static_cast(apiObjects.dataArrays()->size()) + + static_cast(apiObjects.animationNodes()->size()) + + static_cast(apiObjects.timerNodes()->size()) + + static_cast(apiObjects.anchorPoints()->size()) + + static_cast(apiObjects.renderGroupBindings()->size()) + + static_cast(apiObjects.meshNodeBindings()->size()) + + static_cast(apiObjects.skinBindings()->size()); + + deserialized->m_objectsOwningContainer.reserve(logicObjectsTotalSize); + deserialized->m_logicObjects.reserve(logicObjectsTotalSize); + + const auto& luaModules = *apiObjects.luaModules(); + deserialized->m_luaModules.reserve(luaModules.size()); + for (const auto* module : luaModules) + { + assert(module); + std::unique_ptr deserializedModule = LuaModuleImpl::Deserialize(*deserialized->m_solState, *module, errorReporting, deserializationMap); + if (!deserializedModule) + return nullptr; + + auto& obj = deserialized->createAndRegisterObject(std::move(deserializedModule)); + deserializationMap.storeLogicObject(obj.getId(), obj.m_impl); + } + + const auto& luascripts = *apiObjects.luaScripts(); + deserialized->m_scripts.reserve(luascripts.size()); + for (const auto* script : luascripts) + { + assert(script); + std::unique_ptr deserializedScript = LuaScriptImpl::Deserialize(*deserialized->m_solState, *script, errorReporting, deserializationMap); + if (!deserializedScript) + return nullptr; + + deserialized->createAndRegisterObject(std::move(deserializedScript)); + } + + const auto& luaInterfaces = *apiObjects.luaInterfaces(); + deserialized->m_interfaces.reserve(luaInterfaces.size()); + for (const auto* intf : luaInterfaces) + { + assert(intf); + std::unique_ptr deserializedInterface = LuaInterfaceImpl::Deserialize(*intf, errorReporting, deserializationMap); + if (!deserializedInterface) + return nullptr; + + deserialized->createAndRegisterObject(std::move(deserializedInterface)); + } + + if (apiObjects.nodeBindings()->size() != 0u || + apiObjects.appearanceBindings()->size() != 0u || + apiObjects.cameraBindings()->size() != 0u || + apiObjects.renderPassBindings()->size() != 0u || + apiObjects.renderGroupBindings()->size() != 0u || + apiObjects.meshNodeBindings()->size() != 0u) + { + if (ramsesResolver == nullptr) + { + errorReporting.add("Fatal error during loading from file! File contains references to Ramses objects but no Ramses scene was provided!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + } + + const auto& ramsesNodeBindings = *apiObjects.nodeBindings(); + deserialized->m_ramsesNodeBindings.reserve(ramsesNodeBindings.size()); + for (const auto* binding : ramsesNodeBindings) + { + assert(binding); + assert(ramsesResolver); + std::unique_ptr deserializedBinding = RamsesNodeBindingImpl::Deserialize(*binding, *ramsesResolver, errorReporting, deserializationMap); + if (!deserializedBinding) + return nullptr; + + auto& obj = deserialized->createAndRegisterObject(std::move(deserializedBinding)); + deserializationMap.storeLogicObject(obj.getId(), obj.m_impl); + } + + const auto& ramsesAppearanceBindings = *apiObjects.appearanceBindings(); + deserialized->m_ramsesAppearanceBindings.reserve(ramsesAppearanceBindings.size()); + for (const auto* binding : ramsesAppearanceBindings) + { + assert(binding); + assert(ramsesResolver); + std::unique_ptr deserializedBinding = RamsesAppearanceBindingImpl::Deserialize(*binding, *ramsesResolver, errorReporting, deserializationMap); + if (!deserializedBinding) + return nullptr; + + auto& obj = deserialized->createAndRegisterObject(std::move(deserializedBinding)); + deserializationMap.storeLogicObject(obj.getId(), obj.m_impl); + } + + const auto& ramsesCameraBindings = *apiObjects.cameraBindings(); + deserialized->m_ramsesCameraBindings.reserve(ramsesCameraBindings.size()); + for (const auto* binding : ramsesCameraBindings) + { + assert(binding); + assert(ramsesResolver); + std::unique_ptr deserializedBinding = RamsesCameraBindingImpl::Deserialize(*binding, *ramsesResolver, errorReporting, deserializationMap); + if (!deserializedBinding) + return nullptr; + + auto& obj = deserialized->createAndRegisterObject(std::move(deserializedBinding)); + deserializationMap.storeLogicObject(obj.getId(), obj.m_impl); + } + + const auto& ramsesRenderPassBindings = *apiObjects.renderPassBindings(); + deserialized->m_ramsesRenderPassBindings.reserve(ramsesRenderPassBindings.size()); + for (const auto* binding : ramsesRenderPassBindings) + { + assert(binding); + assert(ramsesResolver); + std::unique_ptr deserializedBinding = RamsesRenderPassBindingImpl::Deserialize(*binding, *ramsesResolver, errorReporting, deserializationMap); + if (!deserializedBinding) + return nullptr; + + auto& obj = deserialized->createAndRegisterObject(std::move(deserializedBinding)); + deserializationMap.storeLogicObject(obj.getId(), obj.m_impl); + } + + const auto& dataArrays = *apiObjects.dataArrays(); + deserialized->m_dataArrays.reserve(dataArrays.size()); + for (const auto* fbData : dataArrays) + { + assert(fbData); + auto deserializedDataArray = DataArrayImpl::Deserialize(*fbData, errorReporting); + if (!deserializedDataArray) + return nullptr; + + auto& obj = deserialized->createAndRegisterObject(std::move(deserializedDataArray)); + deserializationMap.storeDataArray(*fbData, obj); + } + + // animation nodes must go after data arrays because they need to resolve references + const auto& animNodes = *apiObjects.animationNodes(); + deserialized->m_animationNodes.reserve(animNodes.size()); + for (const auto* fbData : animNodes) + { + assert(fbData); + auto deserializedAnimNode = AnimationNodeImpl::Deserialize(*fbData, errorReporting, deserializationMap); + if (!deserializedAnimNode) + return nullptr; + + auto& obj = deserialized->createAndRegisterObject(std::move(deserializedAnimNode)); + deserializationMap.storeLogicObject(obj.getId(), obj.m_impl); + } + + const auto& timerNodes = *apiObjects.timerNodes(); + deserialized->m_timerNodes.reserve(timerNodes.size()); + for (const auto* fbData : timerNodes) + { + assert(fbData); + auto deserializedTimer = TimerNodeImpl::Deserialize(*fbData, errorReporting, deserializationMap); + if (!deserializedTimer) + return nullptr; + + deserialized->createAndRegisterObject(std::move(deserializedTimer)); + } + + // anchor points must go after node and camera bindings because they need to resolve references + const auto& anchorPoints = *apiObjects.anchorPoints(); + deserialized->m_anchorPoints.reserve(anchorPoints.size()); + for (const auto* fbAnchor : anchorPoints) + { + assert(fbAnchor); + assert(ramsesResolver); + std::unique_ptr deserializedAnchor = AnchorPointImpl::Deserialize(*fbAnchor, errorReporting, deserializationMap); + if (!deserializedAnchor) + return nullptr; + + deserialized->createAndRegisterObject(std::move(deserializedAnchor)); + } + + const auto& ramsesRenderGroupBindings = *apiObjects.renderGroupBindings(); + deserialized->m_ramsesRenderGroupBindings.reserve(ramsesRenderGroupBindings.size()); + for (const auto* binding : ramsesRenderGroupBindings) + { + assert(binding); + assert(ramsesResolver); + std::unique_ptr deserializedBinding = RamsesRenderGroupBindingImpl::Deserialize(*binding, *ramsesResolver, errorReporting, deserializationMap); + if (!deserializedBinding) + return nullptr; + + deserialized->createAndRegisterObject(std::move(deserializedBinding)); + } + + // skin bindings must go after node and appearance bindings because they need to resolve references + const auto& skinBindings = *apiObjects.skinBindings(); + deserialized->m_skinBindings.reserve(skinBindings.size()); + for (const auto* binding : skinBindings) + { + assert(binding); + std::unique_ptr deserializedBinding = SkinBindingImpl::Deserialize(*binding, errorReporting, deserializationMap); + if (!deserializedBinding) + return nullptr; + + deserialized->createAndRegisterObject(std::move(deserializedBinding)); + } + + const auto& ramsesMeshNodeBindings = *apiObjects.meshNodeBindings(); + deserialized->m_ramsesMeshNodeBindings.reserve(ramsesMeshNodeBindings.size()); + for (const auto* binding : ramsesMeshNodeBindings) + { + assert(binding); + assert(ramsesResolver); + std::unique_ptr deserializedBinding = RamsesMeshNodeBindingImpl::Deserialize(*binding, *ramsesResolver, errorReporting, deserializationMap); + if (!deserializedBinding) + return nullptr; + + deserialized->createAndRegisterObject(std::move(deserializedBinding)); + } + + // links must go last due to dependency on deserialized properties + const auto& links = *apiObjects.links(); + // TODO Violin move this code (serialization parts too) to LogicNodeDependencies + for (const auto* rLink : links) + { + assert(rLink); + + if (!rLink->sourceProperty()) + { + errorReporting.add("Fatal error during loading from serialized data: missing link source property!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + if (!rLink->targetProperty()) + { + errorReporting.add("Fatal error during loading from serialized data: missing link target property!", nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + + const rlogic_serialization::Property* sourceProp = rLink->sourceProperty(); + const rlogic_serialization::Property* targetProp = rLink->targetProperty(); + + const bool success = deserialized->m_logicNodeDependencies.link( + deserializationMap.resolvePropertyImpl(*sourceProp), + deserializationMap.resolvePropertyImpl(*targetProp), + rLink->isWeak(), + errorReporting); + if (!success) + { + errorReporting.add( + fmt::format("Fatal error during loading from {}! Could not link property '{}' to property '{}'!", + dataSourceDescription, + sourceProp->name()->string_view(), + targetProp->name()->string_view() + ), nullptr, EErrorType::BinaryVersionMismatch); + return nullptr; + } + } + + return deserialized; + } + + bool ApiObjects::bindingsDirty() const + { + return + std::any_of(m_ramsesNodeBindings.cbegin(), m_ramsesNodeBindings.cend(), [](const auto& b) { return b->m_impl.isDirty(); }) || + std::any_of(m_ramsesAppearanceBindings.cbegin(), m_ramsesAppearanceBindings.cend(), [](const auto& b) { return b->m_impl.isDirty(); }) || + std::any_of(m_ramsesCameraBindings.cbegin(), m_ramsesCameraBindings.cend(), [](const auto& b) { return b->m_impl.isDirty(); }) || + std::any_of(m_ramsesRenderPassBindings.cbegin(), m_ramsesRenderPassBindings.cend(), [](const auto& b) { return b->m_impl.isDirty(); }) || + std::any_of(m_ramsesRenderGroupBindings.cbegin(), m_ramsesRenderGroupBindings.cend(), [](const auto& b) { return b->m_impl.isDirty(); }) || + std::any_of(m_ramsesMeshNodeBindings.cbegin(), m_ramsesMeshNodeBindings.cend(), [](const auto& b) { return b->m_impl.isDirty(); }) || + std::any_of(m_skinBindings.cbegin(), m_skinBindings.cend(), [](const auto& b) { return b->m_impl.isDirty(); }); + } + + uint64_t ApiObjects::getNextLogicObjectId() + { + return ++m_lastObjectId; + } + + int ApiObjects::getNumElementsInLuaStack() const + { + return m_solState->getNumElementsInLuaStack(); + } + + const std::vector& ApiObjects::getAllPropertyLinks() const + { + m_collectedLinks = collectPropertyLinks(); + return m_collectedLinks; + } + + std::vector ApiObjects::collectPropertyLinks() const + { + std::vector links; + + std::deque propsStack; + for (const auto& obj : m_logicObjects) + { + const auto logicNode = obj->as(); + if (!logicNode) + continue; + + propsStack.clear(); + propsStack.push_back(logicNode->getInputs()); + propsStack.push_back(logicNode->getOutputs()); + while (!propsStack.empty()) + { + const auto prop = propsStack.back(); + propsStack.pop_back(); + if (prop == nullptr) + continue; + + const auto incomingLink = prop->getIncomingLink(); + if (incomingLink) + links.push_back(*incomingLink); + + for (size_t i = 0u; i < prop->getChildCount(); ++i) + propsStack.push_back(prop->getChild(i)); + } + } + + return links; + } + + template DataArray* ApiObjects::createDataArray(const std::vector&, std::string_view); + template DataArray* ApiObjects::createDataArray(const std::vector&, std::string_view); + template DataArray* ApiObjects::createDataArray(const std::vector&, std::string_view); + template DataArray* ApiObjects::createDataArray(const std::vector&, std::string_view); + template DataArray* ApiObjects::createDataArray(const std::vector&, std::string_view); + template DataArray* ApiObjects::createDataArray(const std::vector&, std::string_view); + template DataArray* ApiObjects::createDataArray(const std::vector&, std::string_view); + template DataArray* ApiObjects::createDataArray(const std::vector&, std::string_view); + template DataArray* ApiObjects::createDataArray>(const std::vector>&, std::string_view); + + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + template ApiObjectContainer& ApiObjects::getApiObjectContainer(); + + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; + template const ApiObjectContainer& ApiObjects::getApiObjectContainer() const; +} diff --git a/client/logic/lib/internals/ApiObjects.h b/client/logic/lib/internals/ApiObjects.h new file mode 100644 index 000000000..3100043fa --- /dev/null +++ b/client/logic/lib/internals/ApiObjects.h @@ -0,0 +1,220 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/AnimationTypes.h" +#include "ramses-logic/PropertyLink.h" +#include "ramses-framework-api/DataTypes.h" +#include "ramses-logic/ELuaSavingMode.h" +#include "ramses-framework-api/EFeatureLevel.h" + +#include "impl/LuaConfigImpl.h" + +#include "internals/LuaCompilationUtils.h" +#include "internals/SolState.h" +#include "internals/LogicNodeDependencies.h" + +#include "ramses-client-api/ERotationType.h" + +#include +#include +#include + +namespace ramses +{ + class Scene; + class Node; + class Appearance; + class Camera; + class RenderPass; + class RenderGroup; + class UniformInput; + class MeshNode; +} + +namespace rlogic_serialization +{ + struct ApiObjects; +} + +namespace flatbuffers +{ + template struct Offset; + class FlatBufferBuilder; +} + +namespace ramses +{ + class LogicObject; + class LogicNode; + class LuaScript; + class LuaInterface; + class LuaModule; + class RamsesNodeBinding; + class RamsesAppearanceBinding; + class RamsesCameraBinding; + class RamsesRenderPassBinding; + class RamsesRenderGroupBinding; + class RamsesRenderGroupBindingElements; + class RamsesMeshNodeBinding; + class SkinBinding; + class DataArray; + class AnimationNode; + class TimerNode; + class AnchorPoint; +} + +namespace ramses::internal +{ + class SolState; + class IRamsesObjectResolver; + class AnimationNodeConfigImpl; + class ValidationResults; + class SerializationMap; + class RamsesNodeBindingImpl; + class RamsesCameraBindingImpl; + class RamsesAppearanceBindingImpl; + + template + using ApiObjectContainer = std::vector; + using ApiObjectOwningContainer = std::vector>>; + + class ApiObjects + { + public: + // Not move-able and non-copyable + explicit ApiObjects(ramses::EFeatureLevel featureLevel); + ~ApiObjects() noexcept; + // Not move-able because of the dependency between sol objects and their parent sol state + // Moving those would require a custom move assignment operator which keeps both sol states alive + // until the objects have been moved, and only then also moves the sol state - we don't need this + // complexity because we never move-assign ApiObjects + ApiObjects(ApiObjects&& other) = delete; + ApiObjects& operator=(ApiObjects&& other) = delete; + ApiObjects(const ApiObjects& other) = delete; + ApiObjects& operator=(const ApiObjects& other) = delete; + + // Serialization/Deserialization + static flatbuffers::Offset Serialize( + const ApiObjects& apiObjects, + flatbuffers::FlatBufferBuilder& builder, + ELuaSavingMode luaSavingMode); + static std::unique_ptr Deserialize( + const rlogic_serialization::ApiObjects& apiObjects, + const IRamsesObjectResolver* ramsesResolver, + const std::string& dataSourceDescription, + ErrorReporting& errorReporting, + ramses::EFeatureLevel featureLevel); + + // Create/destroy API objects + LuaScript* createLuaScript( + std::string_view source, + const LuaConfigImpl& config, + std::string_view scriptName, + ErrorReporting& errorReporting); + LuaInterface* createLuaInterface( + std::string_view source, + const LuaConfigImpl& config, + std::string_view interfaceName, + ErrorReporting& errorReporting, + bool verifyModules); + LuaModule* createLuaModule( + std::string_view source, + const LuaConfigImpl& config, + std::string_view moduleName, + ErrorReporting& errorReporting); + RamsesNodeBinding* createRamsesNodeBinding(ramses::Node& ramsesNode, ramses::ERotationType rotationType, std::string_view name); + RamsesAppearanceBinding* createRamsesAppearanceBinding(ramses::Appearance& ramsesAppearance, std::string_view name); + RamsesCameraBinding* createRamsesCameraBinding(ramses::Camera& ramsesCamera, bool withFrustumPlanes, std::string_view name); + RamsesRenderPassBinding* createRamsesRenderPassBinding(ramses::RenderPass& renderPass, std::string_view name); + RamsesRenderGroupBinding* createRamsesRenderGroupBinding(ramses::RenderGroup& ramsesRenderGroup, const RamsesRenderGroupBindingElements& elements, std::string_view name); + RamsesMeshNodeBinding* createRamsesMeshNodeBinding(ramses::MeshNode& ramsesMeshNode, std::string_view name); + SkinBinding* createSkinBinding( + std::vector joints, + const std::vector& inverseBindMatrices, + RamsesAppearanceBindingImpl& appearanceBinding, + const ramses::UniformInput& jointMatInput, + std::string_view name); + template + DataArray* createDataArray(const std::vector& data, std::string_view name); + AnimationNode* createAnimationNode(const AnimationNodeConfigImpl& config, std::string_view name); + TimerNode* createTimerNode(std::string_view name); + AnchorPoint* createAnchorPoint(RamsesNodeBindingImpl& nodeBinding, RamsesCameraBindingImpl& cameraBinding, std::string_view name); + bool destroy(LogicObject& object, ErrorReporting& errorReporting); + + // Invariance checks + [[nodiscard]] bool checkBindingsReferToSameRamsesScene(ErrorReporting& errorReporting) const; + void validateInterfaces(ValidationResults& validationResults) const; + void validateDanglingNodes(ValidationResults& validationResults) const; + + // Getters + template + [[nodiscard]] const ApiObjectContainer& getApiObjectContainer() const; + template + [[nodiscard]] ApiObjectContainer& getApiObjectContainer(); + [[nodiscard]] const ApiObjectOwningContainer& getApiObjectOwningContainer() const; + [[nodiscard]] const LogicNodeDependencies& getLogicNodeDependencies() const; + [[nodiscard]] LogicNodeDependencies& getLogicNodeDependencies(); + + [[nodiscard]] LogicObject* getApiObjectById(uint64_t id) const; + + // Internally used + [[nodiscard]] bool bindingsDirty() const; + [[nodiscard]] uint64_t getNextLogicObjectId(); + + [[nodiscard]] int getNumElementsInLuaStack() const; + + [[nodiscard]] const std::vector& getAllPropertyLinks() const; + + private: + template + T& createAndRegisterObject(std::unique_ptr impl); + template + [[nodiscard]] bool destroyAndUnregisterObject(T& objToDelete, ErrorReporting& errorReporting); + + // Type-specific destruction logic + [[nodiscard]] bool destroyInternal(RamsesNodeBinding& ramsesNodeBinding, ErrorReporting& errorReporting); + [[nodiscard]] bool destroyInternal(LuaModule& luaModule, ErrorReporting& errorReporting); + [[nodiscard]] bool destroyInternal(RamsesAppearanceBinding& ramsesAppearanceBinding, ErrorReporting& errorReporting); + [[nodiscard]] bool destroyInternal(RamsesCameraBinding& ramsesCameraBinding, ErrorReporting& errorReporting); + [[nodiscard]] bool destroyInternal(DataArray& dataArray, ErrorReporting& errorReporting); + [[nodiscard]] bool destroyInternal(AnchorPoint& node, ErrorReporting& errorReporting); + + [[nodiscard]] bool checkLuaModules(const ModuleMapping& moduleMapping, ErrorReporting& errorReporting); + [[nodiscard]] std::vector collectPropertyLinks() const; + + std::unique_ptr m_solState {std::make_unique()}; + + ApiObjectContainer m_scripts; + ApiObjectContainer m_interfaces; + ApiObjectContainer m_luaModules; + ApiObjectContainer m_ramsesNodeBindings; + ApiObjectContainer m_ramsesAppearanceBindings; + ApiObjectContainer m_ramsesCameraBindings; + ApiObjectContainer m_ramsesRenderPassBindings; + ApiObjectContainer m_ramsesRenderGroupBindings; + ApiObjectContainer m_ramsesMeshNodeBindings; + ApiObjectContainer m_skinBindings; + ApiObjectContainer m_dataArrays; + ApiObjectContainer m_animationNodes; + ApiObjectContainer m_timerNodes; + ApiObjectContainer m_anchorPoints; + ApiObjectContainer m_logicObjects; + ApiObjectOwningContainer m_objectsOwningContainer; + + LogicNodeDependencies m_logicNodeDependencies; + uint64_t m_lastObjectId = 0; + std::unordered_map m_logicObjectIdMapping; + + // persistent storage for links to be given out via public API getPropertyLinks() + mutable std::vector m_collectedLinks; + + ramses::EFeatureLevel m_featureLevel; + }; +} diff --git a/client/logic/lib/internals/ApiObjectsSerializedSize.cpp b/client/logic/lib/internals/ApiObjectsSerializedSize.cpp new file mode 100644 index 000000000..ff61c9b82 --- /dev/null +++ b/client/logic/lib/internals/ApiObjectsSerializedSize.cpp @@ -0,0 +1,212 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/ApiObjects.h" +#include "internals/ApiObjectsSerializedSize.h" + +#include "ramses-logic/AnchorPoint.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaModule.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/SkinBinding.h" +#include "ramses-logic/TimerNode.h" + +#include "generated/ApiObjectsGen.h" + +#include "impl/AnchorPointImpl.h" +#include "impl/AnimationNodeImpl.h" +#include "impl/DataArrayImpl.h" +#include "impl/LuaInterfaceImpl.h" +#include "impl/LuaModuleImpl.h" +#include "impl/LuaScriptImpl.h" +#include "impl/RamsesAppearanceBindingImpl.h" +#include "impl/RamsesCameraBindingImpl.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "impl/RamsesRenderPassBindingImpl.h" +#include "impl/RamsesRenderGroupBindingImpl.h" +#include "impl/RamsesMeshNodeBindingImpl.h" +#include "impl/SkinBindingImpl.h" +#include "impl/TimerNodeImpl.h" + +namespace ramses::internal +{ + // Helper functions for GetSerializedSize specializations + template + size_t calculateSerializedSize(const ApiObjectContainer& container, ELuaSavingMode /*unused*/) + { + flatbuffers::FlatBufferBuilder builder{}; + SerializationMap serializationMap{}; + for (const auto& element : container) + { + (void)I::Serialize(static_cast(element->m_impl), builder, serializationMap); + } + return static_cast(builder.GetSize()); + } + + // Since the channels (DataArrays) of an animation are not stored within the Serialize method we need this in this specialization + template<> + size_t calculateSerializedSize(const ApiObjectContainer& container, ELuaSavingMode /*unused*/) + { + auto insertIds = [](const DataArray* data, std::unordered_set& ids) + { + if (data) + { + ids.insert(data->getId()); + } + }; + + flatbuffers::FlatBufferBuilder builder{}; + for (const auto& element : container) + { + std::unordered_set ids{}; + for (const auto& channel : element->getChannels()) + { + insertIds(channel.timeStamps, ids); + insertIds(channel.keyframes, ids); + insertIds(channel.tangentsIn, ids); + insertIds(channel.tangentsOut, ids); + } + SerializationMap serializationMap{}; + for (auto& id : ids) + { + serializationMap.storeDataArray(id, 0u); + } + (void)AnimationNodeImpl::Serialize(element->m_animationNodeImpl, builder, serializationMap); + } + return static_cast(builder.GetSize()); + } + + template<> + size_t calculateSerializedSize(const ApiObjectContainer& container, ELuaSavingMode luaSavingMode) + { + flatbuffers::FlatBufferBuilder builder{}; + SerializationMap serializationMap{}; + for (const auto& element : container) + { + (void)LuaScriptImpl::Serialize(element->m_script, builder, serializationMap, luaSavingMode); + } + return static_cast(builder.GetSize()); + } + + template<> + size_t calculateSerializedSize(const ApiObjectContainer& container, ELuaSavingMode luaSavingMode) + { + flatbuffers::FlatBufferBuilder builder{}; + SerializationMap serializationMap{}; + for (const auto& element : container) + { + (void)LuaModuleImpl::Serialize(element->m_impl, builder, serializationMap, luaSavingMode); + } + return static_cast(builder.GetSize()); + } + + size_t ApiObjectsSerializedSize::GetTotalSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + flatbuffers::FlatBufferBuilder builder{}; + (void)ApiObjects::Serialize(apiObjects, builder, luaSavingMode); + return static_cast(builder.GetSize()); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return calculateSerializedSize(apiObjects.getApiObjectContainer(), luaSavingMode); + } + + template<> + size_t ApiObjectsSerializedSize::GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode) + { + return GetTotalSerializedSize(apiObjects, luaSavingMode); + } +} diff --git a/client/logic/lib/internals/ApiObjectsSerializedSize.h b/client/logic/lib/internals/ApiObjectsSerializedSize.h new file mode 100644 index 000000000..85a7758c3 --- /dev/null +++ b/client/logic/lib/internals/ApiObjectsSerializedSize.h @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2023 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/ELuaSavingMode.h" + +namespace ramses::internal +{ + class ApiObjects; + + class ApiObjectsSerializedSize + { + public: + [[nodiscard]] static std::size_t GetTotalSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode = ELuaSavingMode::SourceAndByteCode); + + template + [[nodiscard]] static std::size_t GetSerializedSize(const ApiObjects& apiObjects, ELuaSavingMode luaSavingMode = ELuaSavingMode::SourceAndByteCode); + }; +} diff --git a/client/logic/lib/internals/DeserializationMap.h b/client/logic/lib/internals/DeserializationMap.h new file mode 100644 index 000000000..96b7da850 --- /dev/null +++ b/client/logic/lib/internals/DeserializationMap.h @@ -0,0 +1,92 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include + +namespace rlogic_serialization +{ + struct Property; + struct DataArray; +} + +namespace ramses +{ + class DataArray; +} + +namespace ramses::internal +{ + class PropertyImpl; + class LogicObjectImpl; + + // Remembers flatbuffers pointers to deserialized objects temporarily during deserialization + class DeserializationMap + { + public: + void storePropertyImpl(const rlogic_serialization::Property& flatbufferObject, PropertyImpl& impl) + { + Store(&flatbufferObject, &impl, m_properties); + } + + PropertyImpl& resolvePropertyImpl(const rlogic_serialization::Property& flatbufferObject) const + { + return *Get(&flatbufferObject, m_properties); + } + + void storeDataArray(const rlogic_serialization::DataArray& flatbufferObject, const DataArray& dataArray) + { + Store(&flatbufferObject, &dataArray, m_dataArrays); + } + + const DataArray& resolveDataArray(const rlogic_serialization::DataArray& flatbufferObject) const + { + return *Get(&flatbufferObject, m_dataArrays); + } + + void storeLogicObject(uint64_t id, LogicObjectImpl& obj) + { + Store(id, &obj, m_logicObjects); + } + + template + ImplT* resolveLogicObject(uint64_t id) const + { + // fail queries using IDs gracefully if given ID not found + // file can be OK on flatbuffer schema level but might still contain corrupted ID value + const auto it = m_logicObjects.find(id); + if (it != m_logicObjects.cend()) + return dynamic_cast(it->second); + + return nullptr; + } + + private: + template + static void Store(Key key, Value value, std::unordered_map& container) + { + static_assert(std::is_trivially_copyable_v && std::is_trivially_copyable_v, "Performance warning"); + assert(container.count(key) == 0 && "one time store only"); + container.emplace(std::move(key), std::move(value)); + } + + template + [[nodiscard]] static Value Get(Key key, const std::unordered_map& container) + { + const auto it = container.find(key); + assert(it != container.cend()); + return it->second; + } + + std::unordered_map m_properties; + std::unordered_map m_dataArrays; + std::unordered_map m_logicObjects; + }; + +} diff --git a/client/logic/lib/internals/DirectedAcyclicGraph.cpp b/client/logic/lib/internals/DirectedAcyclicGraph.cpp new file mode 100644 index 000000000..b977d2254 --- /dev/null +++ b/client/logic/lib/internals/DirectedAcyclicGraph.cpp @@ -0,0 +1,244 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/DirectedAcyclicGraph.h" + +#include +#include +#include +#include +#include + +namespace ramses::internal +{ + void DirectedAcyclicGraph::addNode(Node& node) + { + assert(m_nodeOutgoingEdges.count(&node) == 0); + assert(m_nodeIncomingEdges.count(&node) == 0); + m_nodeOutgoingEdges.insert({ &node, {} }); + m_nodeIncomingEdges.insert({ &node, {} }); + } + + void DirectedAcyclicGraph::removeNode(Node& nodeToRemove) + { + assert(m_nodeOutgoingEdges.count(&nodeToRemove) != 0); + assert(m_nodeIncomingEdges.count(&nodeToRemove) != 0); + + // remove node from all edge lists pointing to it from source nodes + const auto srcNodesIt = m_nodeIncomingEdges.find(&nodeToRemove); + for (const auto srcNode : srcNodesIt->second) + { + EdgeList& srcNodeOutgoingEdges = m_nodeOutgoingEdges.find(srcNode)->second; + const auto it = std::remove_if(srcNodeOutgoingEdges.begin(), srcNodeOutgoingEdges.end(), + [&nodeToRemove](const auto& e) { return e.target == &nodeToRemove; }); + srcNodeOutgoingEdges.erase(it, srcNodeOutgoingEdges.end()); + } + + // remove node from all edge lists pointing to it from its target nodes + const auto tgtNodesIt = m_nodeOutgoingEdges.find(&nodeToRemove); + for (const auto& tgtNode : tgtNodesIt->second) + { + auto& tgtNodeEdges = m_nodeIncomingEdges.find(tgtNode.target)->second; + tgtNodeEdges.erase(std::find(tgtNodeEdges.begin(), tgtNodeEdges.end(), &nodeToRemove)); + } + + // remove node from both maps + m_nodeIncomingEdges.erase(srcNodesIt); + m_nodeOutgoingEdges.erase(&nodeToRemove); + } + + // This is a slightly exotic sorting algorithm for DAGs + // It works based on these general principles: + // - Traverse the DAG starting from the root nodes + // - Keep the nodes in a sparsely sorted queue (with more slots than actual nodes, and some empty slots) + // - Any time a new 'edge' is traversed, moves the 'target' node of the edge to the last position of the queue + // - If number of iterations exceeds N^2, there was a loop in the graph -> abort + // This is supposed to work fast, because the queue is never re-allocated or re-sorted, only grows incrementally, and + // we only need to run a second time and remove the 'empty slots' to get the final order. + std::optional DirectedAcyclicGraph::getTopologicallySortedNodes() const + { + const size_t totalNodeCount = m_nodeOutgoingEdges.size(); + + // This remembers temporarily the position of node N in 'nodeQueue' (see below) + // This index can change in different loops of the code below + std::unordered_map nodeIndexIntoQueue; + nodeIndexIntoQueue.reserve(totalNodeCount); + + // This is a queue of nodes which is: + // - partially sorted (at any given time during the loops, the first X entries are sorted, while the rest is not sorted yet) + // - sparse (some entries can be nullptr) - of nodes which were moved during the algorithm, see below + // - sorted at the end of the loop (by their topological rank) + // - starts with the root nodes (they are always at the beginning) + NodeVector sparseNodeQueue = collectRootNodes(); + + // Cycle condition - can't find root nodes among a non-empty set of nodes + if (sparseNodeQueue.empty() && !m_nodeOutgoingEdges.empty()) + { + return std::nullopt; + } + + // sparseNodeQueue grows here all the time + // TODO Violin rework algorithm to not grow and loop over the same container + for (size_t i = 0; i < sparseNodeQueue.size(); ++i) + { + if (i > totalNodeCount * totalNodeCount) + { + // TODO Violin this is a primitive loop detection. Replace with a proper graph-based solution! + // The inner and the outer loop are bound by N(number of nodes), so exceeding that is a + // sufficient condition that there was a loop + return std::nullopt; + } + + // Get the next node in the queue and process based on its outgoing edges + Node* nextNode = sparseNodeQueue[i]; + // sparseNodeQueue has nullptr holes - skip those + if (nextNode != nullptr) + { + const EdgeList& nextNodeEdges = m_nodeOutgoingEdges.find(nextNode)->second; + + // For each edge, put the 'target' node to the end of the queue (this order may be temporarily wrong, + // because we don't know if those nodes have also edges between them which would affect this order) + // What happens if it's wrong? see the if() inside the loop + for (const auto& outgoingEdge : nextNodeEdges) + { + // Put the node at the end of the 'sparseNodeQueue' and remember the index + Node* outgoingEdgeTarget = outgoingEdge.target; + sparseNodeQueue.emplace_back(outgoingEdgeTarget); + const size_t targetNodeIndex = sparseNodeQueue.size() - 1; + + const auto potentiallyAlreadyProcessedNode = nodeIndexIntoQueue.find(outgoingEdgeTarget); + // target node not processed yet? + if (potentiallyAlreadyProcessedNode == nodeIndexIntoQueue.end()) + { + // => insert to processed queue, with current index from 'queue' + nodeIndexIntoQueue.insert({ outgoingEdgeTarget, targetNodeIndex }); + } + // target node processed already? + else + { + // => move the node from its last computed index to the current one + // (and set to nullptr on its last position so that it does not occur twice in the queue) + // Why do we do this? Because it makes sure that any time there is a 'new edge' to a node, + // it is moved to the last position in the queue, unless it has no 'incoming edges' (root node) + // or it has exactly one incoming edge (and never needs to be re-sorted) + sparseNodeQueue[potentiallyAlreadyProcessedNode->second] = nullptr; + potentiallyAlreadyProcessedNode->second = targetNodeIndex; + } + } + } + } + + // Some nodes are nullptr because of the special 'bubble sort' sorting method + sparseNodeQueue.erase(std::remove(sparseNodeQueue.begin(), sparseNodeQueue.end(), nullptr), sparseNodeQueue.end()); + + return sparseNodeQueue; + } + + bool DirectedAcyclicGraph::addEdge(Node& source, Node& target) + { + assert(m_nodeOutgoingEdges.count(&source) != 0); + assert(m_nodeOutgoingEdges.count(&target) != 0); + assert(m_nodeIncomingEdges.count(&source) != 0); + assert(m_nodeIncomingEdges.count(&target) != 0); + + auto& nodeEdges = m_nodeOutgoingEdges.find(&source)->second; + auto edgeBetweenNodes = FindEdgeToNode(nodeEdges, target); + const bool isNewConnection = (edgeBetweenNodes == nodeEdges.end()); + if (isNewConnection) + { + nodeEdges.push_back({ &target, 1u }); + auto& tgtToSourcesList = m_nodeIncomingEdges.find(&target)->second; + assert(std::find(tgtToSourcesList.cbegin(), tgtToSourcesList.cend(), &source) == tgtToSourcesList.cend()); + tgtToSourcesList.push_back(&source); + } + else + { + edgeBetweenNodes->multiplicity++; + } + + return isNewConnection; + } + + void DirectedAcyclicGraph::removeEdge(Node& source, Node& target) + { + assert(m_nodeOutgoingEdges.count(&source) != 0); + assert(m_nodeOutgoingEdges.count(&target) != 0); + assert(m_nodeIncomingEdges.count(&source) != 0); + assert(m_nodeIncomingEdges.count(&target) != 0); + + auto& srcNodeEdges = m_nodeOutgoingEdges.find(&source)->second; + auto outgoingEdge = FindEdgeToNode(srcNodeEdges, target); + assert(outgoingEdge != srcNodeEdges.end()); + assert(outgoingEdge->multiplicity > 0u); + --outgoingEdge->multiplicity; + if (outgoingEdge->multiplicity == 0) + { + srcNodeEdges.erase(outgoingEdge); + auto& tgtToSourcesList = m_nodeIncomingEdges.find(&target)->second; + assert(std::find(tgtToSourcesList.cbegin(), tgtToSourcesList.cend(), &source) != tgtToSourcesList.cend()); + tgtToSourcesList.erase(std::find(tgtToSourcesList.begin(), tgtToSourcesList.end(), &source)); + } + } + + size_t DirectedAcyclicGraph::getInDegree(Node& node) const + { + assert(m_nodeOutgoingEdges.count(&node) != 0); + assert(m_nodeIncomingEdges.count(&node) != 0); + + size_t edgeCount = 0u; + const auto srcNodesList = m_nodeIncomingEdges.find(&node)->second; + for (const auto srcNode : srcNodesList) + { + const EdgeList& srcNodeOutgoingEdges = m_nodeOutgoingEdges.find(srcNode)->second; + const auto edgeIt = FindEdgeToNode(srcNodeOutgoingEdges, node); + assert(edgeIt != srcNodeOutgoingEdges.cend()); + edgeCount += edgeIt->multiplicity; + } + + return edgeCount; + } + + size_t DirectedAcyclicGraph::getOutDegree(Node& node) const + { + assert(containsNode(node)); + const auto it = m_nodeOutgoingEdges.find(&node); + return std::accumulate(it->second.cbegin(), it->second.cend(), size_t(0u), [](size_t sum, const Edge& e) { + return sum + e.multiplicity; + }); + } + + NodeVector DirectedAcyclicGraph::collectRootNodes() const + { + NodeVector rootNodes; + // reserve to all nodes count because it will be used to store all sorted nodes later + rootNodes.reserve(m_nodeIncomingEdges.size()); + + for (const auto& nodeIngoingEdges : m_nodeIncomingEdges) + { + if (nodeIngoingEdges.second.empty()) + rootNodes.push_back(nodeIngoingEdges.first); + } + + return rootNodes; + } + + bool DirectedAcyclicGraph::containsNode(Node& node) const + { + return m_nodeOutgoingEdges.find(&node) != m_nodeOutgoingEdges.end(); + } + + DirectedAcyclicGraph::EdgeList::const_iterator DirectedAcyclicGraph::FindEdgeToNode(const EdgeList& vec, const Node& node) + { + return std::find_if(vec.begin(), vec.end(), [&node](const auto& e) { return e.target == &node; }); + } + + DirectedAcyclicGraph::EdgeList::iterator DirectedAcyclicGraph::FindEdgeToNode(EdgeList& vec, const Node& node) + { + return std::find_if(vec.begin(), vec.end(), [&node](const auto& e) { return e.target == &node; }); + } +} diff --git a/client/logic/lib/internals/DirectedAcyclicGraph.h b/client/logic/lib/internals/DirectedAcyclicGraph.h new file mode 100644 index 000000000..263141b34 --- /dev/null +++ b/client/logic/lib/internals/DirectedAcyclicGraph.h @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include +#include + +namespace ramses::internal +{ + // We don't depend on any functionality of LogicNodeImpl here, just using the + // pointer as a unique identifier of the node + class LogicNodeImpl; + + // TODO narrow down the scope of this typedef + using NodeVector = std::vector; + + // For short: DAG + // This DAG is used to represent the "property links" in LogicNodes, but abstracts the individual links and only + // counts the number of links between two nodes, not the actual properties which are linked (this info is stored in PropertyImpl). + // Edge direction is equivalent to direction of data flow + // inside the logic engine (outputs -> inputs). Node outgoing degree represent the + // number of total links of node properties to other nodes' properties, i.e. if two nodes A and B have three connected + // properties, and node A and C have two connected properties, then addEdge(A, B) will have been called 3 times, + // addEdge(A, C) two times, and A will have outDegree=5. + // Topological sort result is cached because it's sensitive for performance (and is only + // executed once before update() + class DirectedAcyclicGraph + { + public: + // this could be template parameter for this class, for now it is the only type used with it + using Node = LogicNodeImpl; + + void addNode(Node& node); + void removeNode(Node& node); + [[nodiscard]] bool containsNode(Node& node) const; + + bool addEdge(Node& source, Node& target); + void removeEdge(Node& source, Node& target); + + [[nodiscard]] std::optional getTopologicallySortedNodes() const; + + // For testing only + [[nodiscard]] size_t getInDegree(Node& node) const; + [[nodiscard]] size_t getOutDegree(Node& node) const; + + private: + struct Edge + { + Node* target = nullptr; + size_t multiplicity = 0u; + }; + using EdgeList = std::vector; + + NodeVector collectRootNodes() const; + + static EdgeList::const_iterator FindEdgeToNode(const EdgeList& vec, const Node& node); + static EdgeList::iterator FindEdgeToNode(EdgeList& vec, const Node& node); + + // Stores both nodes and their edges in one hashmap + // If a node has no outgoing links, the 'EdgeList' is empty + // Each entry in 'EdgeList' represents an edge to another node + + // Edges from source node to target nodes (edge can have more than 1 instance represented by multiplicity) + std::unordered_map m_nodeOutgoingEdges; + // Reverse relation from target node to all its source nodes (here without keeping edge multiplicity count) + std::unordered_map m_nodeIncomingEdges; + }; +} diff --git a/doc/developer/60_SomeIPConnectionLogic.dox b/client/logic/lib/internals/EPropertySemantics.h similarity index 64% rename from doc/developer/60_SomeIPConnectionLogic.dox rename to client/logic/lib/internals/EPropertySemantics.h index 0767e3bbb..4d64ced58 100644 --- a/doc/developer/60_SomeIPConnectionLogic.dox +++ b/client/logic/lib/internals/EPropertySemantics.h @@ -6,14 +6,19 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -/** +#pragma once -@page RAMSESSOIPDev SomeIP Connection Logic +#include -SomeIP Connection System implemented in -framework/Communication/TransportCommon/include/TransportCommon/ConnectionSystemInitiatorResponder.h - -\dotfile responder_with_old.dot -\dotfile initiator_with_old.dot - -*/ +namespace ramses::internal +{ + enum class EPropertySemantics : uint8_t + { + ScriptInput, + BindingInput, + ScriptOutput, + AnimationInput, + AnimationOutput, + Interface, + }; +} diff --git a/client/logic/lib/internals/EnvironmentProtection.cpp b/client/logic/lib/internals/EnvironmentProtection.cpp new file mode 100644 index 000000000..dd2b55c7d --- /dev/null +++ b/client/logic/lib/internals/EnvironmentProtection.cpp @@ -0,0 +1,307 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/EnvironmentProtection.h" + +#include "ramses-logic/EPropertyType.h" + +#include "internals/SolHelper.h" + +namespace ramses::internal +{ + sol::table EnvironmentProtection::GetProtectedEnvironmentTable(const sol::environment& environmentTable) noexcept + { + sol::object protectedTable = environmentTable[sol::metatable_key]["__sensitive"]; + assert(protectedTable != sol::lua_nil && "No protection added!"); + return protectedTable; + } + + void EnvironmentProtection::AddProtectedEnvironmentTable(sol::environment& env, sol::state& state) + { + assert(env[sol::metatable_key] == sol::lua_nil && "Already has protection!"); + sol::table sensitiveTable(state, sol::create); + sol::table metatable = state.create_table(); + metatable["__sensitive"] = sensitiveTable; + env[sol::metatable_key] = metatable; + } + + void EnvironmentProtection::SetEnvironmentProtectionLevel(sol::environment& env, EEnvProtectionFlag protectionFlag) + { + sol::table protectedMetatable = env.traverse_raw_get("_G", sol::metatable_key); + + switch (protectionFlag) + { + case EEnvProtectionFlag::None: + protectedMetatable[sol::meta_function::new_index] = sol::lua_nil; + protectedMetatable[sol::meta_function::index] = sol::lua_nil; + break; + case EEnvProtectionFlag::LoadScript: + protectedMetatable[sol::meta_function::new_index] = EnvironmentProtection::protectedNewIndex_LoadScript; + protectedMetatable[sol::meta_function::index] = EnvironmentProtection::protectedIndex_LoadScript; + break; + case EEnvProtectionFlag::LoadInterface: + protectedMetatable[sol::meta_function::new_index] = EnvironmentProtection::protectedNewIndex_LoadInterface; + protectedMetatable[sol::meta_function::index] = EnvironmentProtection::protectedIndex_LoadInterface; + break; + case EEnvProtectionFlag::InitFunction: + protectedMetatable[sol::meta_function::new_index] = EnvironmentProtection::protectedNewIndex_InitializeFunction; + protectedMetatable[sol::meta_function::index] = EnvironmentProtection::protectedIndex_InitializeFunction; + break; + case EEnvProtectionFlag::InterfaceFunctionInScript: + protectedMetatable[sol::meta_function::new_index] = EnvironmentProtection::protectedNewIndex_InterfaceFunctionInScript; + protectedMetatable[sol::meta_function::index] = EnvironmentProtection::protectedIndex_InterfaceFunctionInScript; + break; + case EEnvProtectionFlag::InterfaceFunctionInInterface: + protectedMetatable[sol::meta_function::new_index] = EnvironmentProtection::protectedNewIndex_InterfaceFunctionInInterface; + protectedMetatable[sol::meta_function::index] = EnvironmentProtection::protectedIndex_InterfaceFunctionInInterface; + break; + case EEnvProtectionFlag::RunFunction: + protectedMetatable[sol::meta_function::new_index] = EnvironmentProtection::protectedNewIndex_RunFunction; + protectedMetatable[sol::meta_function::index] = EnvironmentProtection::protectedIndex_RunFunction; + break; + case EEnvProtectionFlag::Module: + protectedMetatable[sol::meta_function::new_index] = EnvironmentProtection::protectedNewIndex_Module; + protectedMetatable[sol::meta_function::index] = EnvironmentProtection::protectedIndex_Module; + break; + } + } + + void EnvironmentProtection::EnsureStringKey(const sol::object& key) + { + const sol::type keyType = key.get_type(); + if (keyType != sol::type::string) + { + sol_helper::throwSolException("Assigning global variables with a non-string index is prohibited! (key type used '{}')", sol_helper::GetSolTypeName(keyType)); + } + } + + void EnvironmentProtection::protectedNewIndex_LoadScript(const sol::lua_table& tbl, const sol::object& key, const sol::object& value) + { + EnsureStringKey(key); + const std::string keyStr(key.as()); + const sol::type valueType = value.get_type(); + + if (valueType != sol::type::function) + { + sol_helper::throwSolException("Declaring global variables is forbidden (exceptions: the functions 'init', 'interface' and 'run')! (found value of type '{}')", + sol_helper::GetSolTypeName(valueType)); + } + + if (keyStr != "init" && keyStr != "interface" && keyStr != "run") + { + sol_helper::throwSolException("Unexpected function name '{}'! Allowed names: 'init', 'interface', 'run'", keyStr); + } + + if (GetProtectedEnvironmentTable(tbl).raw_get(key) != sol::lua_nil) + { + sol_helper::throwSolException("Function '{}' can only be declared once!", keyStr); + } + + GetProtectedEnvironmentTable(tbl).raw_set(key, value); + }; + + sol::object EnvironmentProtection::protectedIndex_LoadScript(const sol::lua_table& tbl, const sol::object& key) + { + EnsureStringKey(key); + + const std::string keyStr(key.as()); + if (keyStr != "modules") + { + sol_helper::throwSolException( + "Trying to read global variable '{}' outside the scope of init(), interface() and run() functions! This can cause undefined behavior and is forbidden!", keyStr); + } + + return GetProtectedEnvironmentTable(tbl).raw_get(key); + }; + + void EnvironmentProtection::protectedNewIndex_LoadInterface(const sol::lua_table& tbl, const sol::object& key, const sol::object& value) + { + EnsureStringKey(key); + const std::string keyStr(key.as()); + const sol::type valueType = value.get_type(); + + if (valueType != sol::type::function) + { + sol_helper::throwSolException("Declaring global variables is forbidden (exception: the 'interface' function)! (found value of type '{}')", + sol_helper::GetSolTypeName(valueType)); + } + + if (keyStr != "interface") + { + sol_helper::throwSolException("Unexpected function name '{}'! Only 'interface' function can be declared!", keyStr); + } + + if (GetProtectedEnvironmentTable(tbl).raw_get(key) != sol::lua_nil) + { + sol_helper::throwSolException("Function '{}' can only be declared once!", keyStr); + } + + GetProtectedEnvironmentTable(tbl).raw_set(key, value); + }; + + sol::object EnvironmentProtection::protectedIndex_LoadInterface(const sol::lua_table& /*tbl*/, const sol::object& key) + { + EnsureStringKey(key); + const std::string keyStr(key.as()); + sol_helper::throwSolException("Trying to read global variable '{}' in an interface!", keyStr); + + return {}; + }; + + void EnvironmentProtection::protectedNewIndex_InitializeFunction(const sol::lua_table& /*tbl*/, const sol::object& key, const sol::object& /*value*/) + { + EnsureStringKey(key); + const std::string keyStr(key.as()); + + if (keyStr == "GLOBAL") + { + sol_helper::throwSolException("Trying to override the GLOBAL table in init()! You can only add data, but not overwrite the table!"); + } + else if (keyStr == "Type") + { + sol_helper::throwSolException("Can't override the Type special table in init()!"); + } + else + { + sol_helper::throwSolException("Unexpected global variable definition '{}' in init()! Please use the GLOBAL table to declare global data and functions, or use modules!", keyStr); + } + } + + sol::object EnvironmentProtection::protectedIndex_InitializeFunction(const sol::lua_table& tbl, const sol::object& key) + { + EnsureStringKey(key); + const std::string keyStr(key.as()); + if (keyStr != "GLOBAL" && keyStr != "Type") + { + sol_helper::throwSolException( + "Trying to read global variable '{}' in the init() function! This can cause undefined behavior and is forbidden! Use the GLOBAL table to read/write global data!", keyStr); + } + return GetProtectedEnvironmentTable(tbl).raw_get(key); + } + + void EnvironmentProtection::protectedNewIndex_InterfaceFunctionInScript(const sol::lua_table& /*tbl*/, const sol::object& key, const sol::object& /*value*/) + { + EnsureStringKey(key); + const std::string keyStr(key.as()); + if (keyStr == "GLOBAL") + { + sol_helper::throwSolException("Trying to override the GLOBAL table in interface()! You can only read data, but not overwrite the GLOBAL table!"); + } + + if (keyStr == "Type") + { + sol_helper::throwSolException("Can't override the 'Type' symbol in interface()!"); + } + + sol_helper::throwSolException( + "Unexpected global variable definition '{}' in interface()! Use the GLOBAL table inside the init() function to declare global data and functions, or use modules!", keyStr); + } + + void EnvironmentProtection::protectedNewIndex_InterfaceFunctionInInterface(const sol::lua_table& /*tbl*/, const sol::object& key, const sol::object& /*value*/) + { + EnsureStringKey(key); + const std::string keyStr(key.as()); + + if (keyStr == "Type") + { + sol_helper::throwSolException("Special global '{}' symbol should not be overwritten in the interface() function!", keyStr); + } + + sol_helper::throwSolException("Unexpected variable definition '{}' in interface()!", keyStr); + } + + sol::object EnvironmentProtection::protectedIndex_InterfaceFunctionInScript(const sol::lua_table& tbl, const sol::object& key) + { + EnsureStringKey(key); + const std::string keyStr(key.as()); + if (keyStr != "GLOBAL" && keyStr != "Type") + { + sol_helper::throwSolException("Unexpected global access to key '{}' in interface()! Only 'GLOBAL' and 'Type' are allowed as a key", keyStr); + } + return GetProtectedEnvironmentTable(tbl).raw_get(key); + } + + sol::object EnvironmentProtection::protectedIndex_InterfaceFunctionInInterface(const sol::lua_table& tbl, const sol::object& key) + { + EnsureStringKey(key); + const std::string keyStr(key.as()); + if (keyStr != "Type") + { + sol_helper::throwSolException("Unexpected global access to key '{}' in interface()! Only 'Type' is allowed as a key", keyStr); + } + return GetProtectedEnvironmentTable(tbl).raw_get(key); + } + + void EnvironmentProtection::protectedNewIndex_RunFunction(const sol::lua_table& /*tbl*/, const sol::object& key, const sol::object& /*value*/) + { + EnsureStringKey(key); + const std::string keyStr(key.as()); + if (keyStr == "GLOBAL") + { + sol_helper::throwSolException("Trying to override the GLOBAL table in run()! You can only read data, but not overwrite the table!"); + } + else + { + sol_helper::throwSolException("Unexpected global variable definition '{}' in run()! Use the init() function to declare global data and functions, or use modules!", keyStr); + } + } + + sol::object EnvironmentProtection::protectedIndex_RunFunction(const sol::lua_table& tbl, const sol::object& key) + { + EnsureStringKey(key); + const std::string_view keyStr(key.as()); + if (keyStr != "GLOBAL") + { + sol_helper::throwSolException("Unexpected global access to key '{}' in run()! Only 'GLOBAL' is allowed as a key", keyStr); + } + return GetProtectedEnvironmentTable(tbl).raw_get(key); + } + + void EnvironmentProtection::protectedNewIndex_Module(const sol::lua_table& /*tbl*/, const sol::object& key, const sol::object& value) + { + EnsureStringKey(key); + const std::string keyStr(key.as()); + const sol::type valueType = value.get_type(); + + if (keyStr == "Type") + { + sol_helper::throwSolException("Special global 'Type' symbol should not be overwritten in modules!"); + } + + sol_helper::throwSolException("Declaring global variables is forbidden in modules! (found value of type '{}' assigned to variable '{}')", + sol_helper::GetSolTypeName(valueType), keyStr); + }; + + sol::object EnvironmentProtection::protectedIndex_Module(const sol::lua_table& tbl, const sol::object& key) + { + EnsureStringKey(key); + + const std::string keyStr(key.as()); + + // Allows reading existing symbols (e.g. standard modules), but reports error if accessing anything else + sol::object existingObject = GetProtectedEnvironmentTable(tbl).raw_get(key); + if (!existingObject.valid()) + { + sol_helper::throwSolException( + "Trying to read global variable '{}' in module! This can cause undefined behavior and is forbidden!", keyStr); + } + + return existingObject; + }; + + ScopedEnvironmentProtection::ScopedEnvironmentProtection(sol::environment& env, EEnvProtectionFlag protectionFlag) + : m_env(env) + { + EnvironmentProtection::SetEnvironmentProtectionLevel(m_env, protectionFlag); + } + + ScopedEnvironmentProtection::~ScopedEnvironmentProtection() + { + EnvironmentProtection::SetEnvironmentProtectionLevel(m_env, EEnvProtectionFlag::None); + } +} diff --git a/client/logic/lib/internals/EnvironmentProtection.h b/client/logic/lib/internals/EnvironmentProtection.h new file mode 100644 index 000000000..6f8071528 --- /dev/null +++ b/client/logic/lib/internals/EnvironmentProtection.h @@ -0,0 +1,73 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "internals/SolWrapper.h" + +namespace ramses::internal +{ + enum class EEnvProtectionFlag + { + None, + LoadScript, + LoadInterface, + InitFunction, + InterfaceFunctionInScript, + InterfaceFunctionInInterface, + RunFunction, + Module, + }; + + class EnvironmentProtection + { + public: + static void AddProtectedEnvironmentTable(sol::environment& env, sol::state& state); + static void SetEnvironmentProtectionLevel(sol::environment& env, EEnvProtectionFlag protectionFlag); + + // Used by logic engine internals to bypass environment protection + [[nodiscard]] static sol::table GetProtectedEnvironmentTable(const sol::environment& environmentTable) noexcept; + + private: + static void protectedNewIndex_LoadScript(const sol::lua_table& tbl, const sol::object& key, const sol::object& value); + [[nodiscard]] static sol::object protectedIndex_LoadScript(const sol::lua_table& tbl, const sol::object& key); + + static void protectedNewIndex_LoadInterface(const sol::lua_table& tbl, const sol::object& key, const sol::object& value); + [[nodiscard]] static sol::object protectedIndex_LoadInterface(const sol::lua_table& tbl, const sol::object& key); + + static void protectedNewIndex_InitializeFunction(const sol::lua_table& tbl, const sol::object& key, const sol::object& value); + [[nodiscard]] static sol::object protectedIndex_InitializeFunction(const sol::lua_table& tbl, const sol::object& key); + + static void protectedNewIndex_InterfaceFunctionInScript(const sol::lua_table& tbl, const sol::object& key, const sol::object& value); + [[nodiscard]] static sol::object protectedIndex_InterfaceFunctionInScript(const sol::lua_table& tbl, const sol::object& key); + + static void protectedNewIndex_InterfaceFunctionInInterface(const sol::lua_table& tbl, const sol::object& key, const sol::object& value); + [[nodiscard]] static sol::object protectedIndex_InterfaceFunctionInInterface(const sol::lua_table& tbl, const sol::object& key); + + static void protectedNewIndex_RunFunction(const sol::lua_table& tbl, const sol::object& key, const sol::object& value); + [[nodiscard]] static sol::object protectedIndex_RunFunction(const sol::lua_table& tbl, const sol::object& key); + + static void protectedNewIndex_Module(const sol::lua_table& tbl, const sol::object& key, const sol::object& value); + [[nodiscard]] static sol::object protectedIndex_Module(const sol::lua_table& tbl, const sol::object& key); + + static void EnsureStringKey(const sol::object& key); + }; + + class ScopedEnvironmentProtection + { + public: + ScopedEnvironmentProtection(sol::environment& env, EEnvProtectionFlag protectionFlag); + ~ScopedEnvironmentProtection(); + + ScopedEnvironmentProtection(const ScopedEnvironmentProtection&) = delete; + ScopedEnvironmentProtection& operator=(const ScopedEnvironmentProtection&) = delete; + + private: + sol::environment& m_env; + }; +} diff --git a/client/logic/lib/internals/ErrorReporting.cpp b/client/logic/lib/internals/ErrorReporting.cpp new file mode 100644 index 000000000..dd6bdd82a --- /dev/null +++ b/client/logic/lib/internals/ErrorReporting.cpp @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/ErrorReporting.h" +#include "ramses-logic/LogicNode.h" +#include "impl/LoggerImpl.h" +#include "impl/LogicObjectImpl.h" + +namespace ramses::internal +{ + void ErrorReporting::add(std::string errorMessage, const LogicObject* logicObject, EErrorType type) + { + if (logicObject) + { + LOG_ERROR("[{}] {}", logicObject->m_impl->getIdentificationString(), errorMessage); + } + else + { + LOG_ERROR("{}", errorMessage); + } + + m_errors.emplace_back(ErrorData{ std::move(errorMessage), type, logicObject }); + } + + void ErrorReporting::clear() + { + m_errors.clear(); + } + + const std::vector& ErrorReporting::getErrors() const + { + return m_errors; + } + +} diff --git a/framework/Components/include/Components/LogDcsmInfo.h b/client/logic/lib/internals/ErrorReporting.h similarity index 54% rename from framework/Components/include/Components/LogDcsmInfo.h rename to client/logic/lib/internals/ErrorReporting.h index 2d36321c2..71bd1a82f 100644 --- a/framework/Components/include/Components/LogDcsmInfo.h +++ b/client/logic/lib/internals/ErrorReporting.h @@ -1,29 +1,29 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2019 BMW AG +// Copyright (C) 2020 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#ifndef RAMSES_LOGDCSMINFO_H -#define RAMSES_LOGDCSMINFO_H +#pragma once -#include "Ramsh/RamshCommand.h" +#include +#include +#include "ramses-logic/ErrorData.h" -namespace ramses_internal +namespace ramses::internal { - class DcsmComponent; - - class LogDcsmInfo : public RamshCommand + class ErrorReporting { public: - explicit LogDcsmInfo(DcsmComponent& dcsmComponent); - virtual bool executeInput(const std::vector& input) override; + + void clear(); + void add(std::string errorMessage, const LogicObject* logicObject, EErrorType type); + + [[nodiscard]] const std::vector& getErrors() const; private: - DcsmComponent& m_dcsmComponent; + std::vector m_errors; }; } - -#endif diff --git a/client/logic/lib/internals/FileUtils.cpp b/client/logic/lib/internals/FileUtils.cpp new file mode 100644 index 000000000..1ca208459 --- /dev/null +++ b/client/logic/lib/internals/FileUtils.cpp @@ -0,0 +1,74 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "FileUtils.h" +#include "StdFilesystemWrapper.h" +#include + +namespace ramses::internal +{ + bool FileUtils::SaveBinary(const std::string& filename, const void* binaryBuffer, size_t bufferLength) + { + std::ofstream fileStream(filename, std::ofstream::binary); + if (!fileStream.is_open()) + { + return false; + } + fileStream.write(static_cast(binaryBuffer), static_cast(bufferLength)); + return !fileStream.bad(); + } + + std::optional> FileUtils::LoadBinary(const std::string& filename) + { + // ifstream does not prevent opening directories (and crashes in some cases), have to use filesystem to check + if(fs::is_directory(filename)) + { + return std::nullopt; + } + + std::ifstream fileStream(filename, std::ifstream::binary); + if (!fileStream.is_open()) + { + return std::nullopt; + } + fileStream.seekg(0, std::ios::end); + std::vector byteBuffer(static_cast(fileStream.tellg())); + fileStream.seekg(0, std::ios::beg); + fileStream.read(byteBuffer.data(), static_cast(byteBuffer.size())); + if (fileStream.bad()) + { + return std::nullopt; + } + + return byteBuffer; + } + + std::optional> FileUtils::LoadBinary(int fd, size_t offset, size_t size) + { + std::unique_ptr file(fdopen(fd, "rb"), &fclose); + if (!file) + { + return std::nullopt; + } + + // NOLINTNEXTLINE(google-runtime-int) long cast required to match with fseek declaration + if (fseek(file.get(), static_cast(offset), SEEK_SET) != 0) + { + return std::nullopt; + } + + std::vector byteBuffer(size); + + const auto bytesRead = fread(byteBuffer.data(), 1, size, file.get()); + if (bytesRead != size) + { + return std::nullopt; + } + return byteBuffer; + } +} diff --git a/client/logic/lib/internals/FileUtils.h b/client/logic/lib/internals/FileUtils.h new file mode 100644 index 000000000..27e602350 --- /dev/null +++ b/client/logic/lib/internals/FileUtils.h @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include + +namespace ramses::internal +{ + class FileUtils + { + public: + static bool SaveBinary(const std::string& filename, const void* binaryBuffer, size_t bufferLength); + static std::optional> LoadBinary(const std::string& filename); + static std::optional> LoadBinary(int fd, size_t offset, size_t size); + }; +} diff --git a/client/logic/lib/internals/InterfaceTypeFunctions.cpp b/client/logic/lib/internals/InterfaceTypeFunctions.cpp new file mode 100644 index 000000000..b62520e0c --- /dev/null +++ b/client/logic/lib/internals/InterfaceTypeFunctions.cpp @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/InterfaceTypeFunctions.h" +#include "internals/SolHelper.h" + +namespace ramses::internal +{ + + sol::object InterfaceTypeFunctions::CreateArray(sol::this_state state, const sol::object& /*unused*/, const sol::object& size, std::optional arrayType) + { + const DataOrError potentialUInt = LuaTypeConversions::ExtractSpecificType(size); + if (potentialUInt.hasError()) + { + sol_helper::throwSolException("Type:Array(N, T) invoked with bad size argument! {}", potentialUInt.getError()); + } + // TODO Violin revisit max array size + // Putting a "sane" number here, but maybe worth reconsidering the limit again + const size_t arraySize = potentialUInt.getData(); + if (arraySize == 0u || arraySize > MaxArrayPropertySize) + { + sol_helper::throwSolException("Type:Array(N, T) invoked with invalid size parameter N={} (must be in the range [1, {}])!", arraySize, MaxArrayPropertySize); + } + if (!arrayType) + { + sol_helper::throwSolException("Type:Array(N, T) invoked with invalid type parameter T!"); + } + return sol::object(state, sol::in_place_type, InterfaceTypeInfo{ EPropertyType::Array, arraySize, *arrayType }); + } + + sol::object InterfaceTypeFunctions::CreateStruct(sol::this_state state, const sol::object& /*unused*/, std::optional structType) + { + if (!structType) + { + sol_helper::throwSolException("Type:Struct(T) invoked with invalid type parameter T!"); + } + return sol::object(state, sol::in_place_type, InterfaceTypeInfo{ EPropertyType::Struct, 0, *structType }); + } +} diff --git a/client/logic/lib/internals/InterfaceTypeFunctions.h b/client/logic/lib/internals/InterfaceTypeFunctions.h new file mode 100644 index 000000000..34640343d --- /dev/null +++ b/client/logic/lib/internals/InterfaceTypeFunctions.h @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "internals/InterfaceTypeInfo.h" +#include "internals/LuaTypeConversions.h" + +namespace ramses::internal +{ + // This class holds the functions used to declare types in Lua, e.g. Type:Int32(), Type:Array() etc. + // The Lua syntax Table:Method(args) converts to Table.Method(Table, args) and therefore we have an + // unused argument in the methods because Type is a singleton instance and we don't use it for anything currently + class InterfaceTypeFunctions + { + public: + [[nodiscard]] static sol::object CreateArray( + sol::this_state state, + const sol::object& /*unused*/, + const sol::object& size, + std::optional arrayType); + + [[nodiscard]] static sol::object CreateStruct( + sol::this_state state, + const sol::object& /*unused*/, + std::optional structType); + + template + [[nodiscard]] static sol::object CreatePrimitiveType(sol::this_state state, const sol::object& /*unused*/) + { + return sol::object(state, sol::in_place_type, InterfaceTypeInfo{ type, 0u, sol::lua_nil }); + } + }; +} diff --git a/framework/Core/Utils/test/DataBindTest.h b/client/logic/lib/internals/InterfaceTypeInfo.h similarity index 54% rename from framework/Core/Utils/test/DataBindTest.h rename to client/logic/lib/internals/InterfaceTypeInfo.h index 7cf809ad4..8751e617f 100644 --- a/framework/Core/Utils/test/DataBindTest.h +++ b/client/logic/lib/internals/InterfaceTypeInfo.h @@ -1,30 +1,23 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2012 BMW Car IT GmbH +// Copyright (C) 2022 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#ifndef RAMSES_DATABINDTEST_H -#define RAMSES_DATABINDTEST_H +#pragma once -#include "framework_common_gmock_header.h" -#include "gtest/gtest.h" -#include "Utils/DataBind.h" -#include "DataBindTestUtils.h" +#include "internals/SolWrapper.h" +#include "ramses-logic/EPropertyType.h" -namespace ramses_internal +namespace ramses::internal { - class DataBindTest: public testing::Test + // TODO Violin consider reworking this to use HierarchicalTypeData instead + struct InterfaceTypeInfo { - public: - DataBindTest() - { - } - - protected: + EPropertyType typeId = EPropertyType::Struct; + size_t arraySize = 0u; + sol::object complexType = sol::lua_nil; }; } - -#endif diff --git a/client/logic/lib/internals/LogicNodeDependencies.cpp b/client/logic/lib/internals/LogicNodeDependencies.cpp new file mode 100644 index 000000000..55efaa1e9 --- /dev/null +++ b/client/logic/lib/internals/LogicNodeDependencies.cpp @@ -0,0 +1,227 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/LogicNodeDependencies.h" + +#include "ramses-logic/Property.h" + +#include "impl/LogicNodeImpl.h" +#include "impl/PropertyImpl.h" +#include "impl/RamsesBindingImpl.h" + +#include "internals/ErrorReporting.h" +#include "internals/TypeUtils.h" + +#include +#include "fmt/format.h" + +namespace ramses::internal +{ + + void LogicNodeDependencies::addNode(LogicNodeImpl& node) + { + assert(!m_logicNodeDAG.containsNode(node)); + m_logicNodeDAG.addNode(node); + m_nodeTopologyChanged = true; + } + + void LogicNodeDependencies::removeNode(LogicNodeImpl& node) + { + assert(m_logicNodeDAG.containsNode(node)); + m_logicNodeDAG.removeNode(node); + + // Remove the node from the cache without reordering the rest (unless there is no cache yet) + // Removing nodes does not require topology update (we don't guarantee specific ordering when + // nodes are not related, we only guarantee relative ordering when nodes are linked) + if (m_cachedTopologicallySortedNodes) + { + NodeVector& cachedNodes = *m_cachedTopologicallySortedNodes; + cachedNodes.erase(std::remove(cachedNodes.begin(), cachedNodes.end(), &node), cachedNodes.end()); + } + } + + bool LogicNodeDependencies::isLinked(const LogicNodeImpl& logicNode) const + { + auto inputs = logicNode.getInputs(); + if (isLinked(*inputs->m_impl)) + { + return true; + } + + const auto outputs = logicNode.getOutputs(); + if (nullptr != outputs) + { + return isLinked(*outputs->m_impl); + } + return false; + } + + bool LogicNodeDependencies::isLinked(PropertyImpl& input) const + { + const auto inputCount = input.getChildCount(); + // check if an input of this node is a target of another node + for (size_t i = 0; i < inputCount; ++i) + { + const auto child = input.getChild(i); + if (TypeUtils::CanHaveChildren(child->getType())) + { + if (isLinked(*child->m_impl)) + { + return true; + } + } + else + { + assert(TypeUtils::IsPrimitiveType(child->getType())); + if (child->m_impl->isLinked()) + { + return true; + } + } + } + return false; + } + + const std::optional& LogicNodeDependencies::getTopologicallySortedNodes() + { + if (m_nodeTopologyChanged) + { + m_cachedTopologicallySortedNodes = m_logicNodeDAG.getTopologicallySortedNodes(); + m_nodeTopologyChanged = false; + } + + return m_cachedTopologicallySortedNodes; + } + + bool LogicNodeDependencies::link(PropertyImpl& output, PropertyImpl& input, bool isWeakLink, ErrorReporting& errorReporting) + { + if (!m_logicNodeDAG.containsNode(output.getLogicNode())) + { + errorReporting.add(fmt::format("LogicNode '{}' is not an instance of this LogicEngine", output.getLogicNode().getName()), nullptr, EErrorType::IllegalArgument); + return false; + } + + if (!m_logicNodeDAG.containsNode(input.getLogicNode())) + { + errorReporting.add(fmt::format("LogicNode '{}' is not an instance of this LogicEngine", input.getLogicNode().getName()), nullptr, EErrorType::IllegalArgument); + return false; + } + + if (&output.getLogicNode() == &input.getLogicNode()) + { + errorReporting.add(fmt::format("Link source and target can't belong to the same node! ('{}')", input.getLogicNode().getName()), nullptr, EErrorType::IllegalArgument); + return false; + } + + if (!(output.isOutput() && input.isInput())) + { + std::string_view lhsType = output.isOutput() ? "output" : "input"; + std::string_view rhsType = input.isOutput() ? "output" : "input"; + errorReporting.add(fmt::format("Failed to link {} property '{}' to {} property '{}'. Only outputs can be linked to inputs", lhsType, output.getName(), rhsType, input.getName()), nullptr, EErrorType::IllegalArgument); + return false; + } + + if (output.getType() != input.getType()) + { + errorReporting.add(fmt::format("Types of source property '{}:{}' does not match target property '{}:{}'", + output.getName(), + GetLuaPrimitiveTypeName(output.getType()), + input.getName(), + GetLuaPrimitiveTypeName(input.getType())), nullptr, EErrorType::IllegalArgument); + return false; + } + + // No need to also test input type, above check already makes sure output and input are of the same type + if (!TypeUtils::IsPrimitiveType(output.getType())) + { + errorReporting.add(fmt::format("Can't link properties of complex types directly, currently only primitive properties can be linked"), nullptr, EErrorType::IllegalArgument); + return false; + } + + const PropertyImpl* linkedIncomingProperty = input.getIncomingLink().property; + if (linkedIncomingProperty != nullptr) + { + errorReporting.add(fmt::format("The property '{}' of LogicNode '{}' is already linked (to property '{}' of LogicNode '{}')", + input.getName(), + input.getLogicNode().getName(), + linkedIncomingProperty->getName(), + linkedIncomingProperty->getLogicNode().getName() + ), nullptr, EErrorType::IllegalArgument); + return false; + } + + input.setIncomingLink(output, isWeakLink); + + if (!isWeakLink) + { + const bool isNewEdge = m_logicNodeDAG.addEdge(output.getLogicNode(), input.getLogicNode()); + if (isNewEdge) + { + m_nodeTopologyChanged = true; + } + } + + // TODO Violin don't set anything dirty here, handle dirtiness purely in update() + input.getLogicNode().setDirty(true); + output.getLogicNode().setDirty(true); + + return true; + } + + bool LogicNodeDependencies::unlink(PropertyImpl& output, PropertyImpl& input, ErrorReporting& errorReporting) + { + if (TypeUtils::CanHaveChildren(input.getType())) + { + errorReporting.add(fmt::format("Can't unlink properties of complex types directly!"), nullptr, EErrorType::IllegalArgument); + return false; + } + + const PropertyImpl* linkedIncomingProperty = input.getIncomingLink().property; + if (linkedIncomingProperty == nullptr) + { + errorReporting.add(fmt::format("Input property '{}' is not currently linked!", input.getName()), nullptr, EErrorType::IllegalArgument); + return false; + } + + if (linkedIncomingProperty != &output) + { + errorReporting.add(fmt::format("Input property '{}' is currently linked to another property '{}'", linkedIncomingProperty->getName(), input.getName()), nullptr, EErrorType::IllegalArgument); + return false; + } + + if (!input.getIncomingLink().isWeakLink) + { + auto& node = output.getLogicNode(); + auto& targetNode = input.getLogicNode(); + m_logicNodeDAG.removeEdge(node, targetNode); + } + + input.resetIncomingLink(); + + return true; + } + + void LogicNodeDependencies::addBindingDependency(RamsesBindingImpl& binding, LogicNodeImpl& node) + { + assert(m_logicNodeDAG.containsNode(node)); + assert(m_logicNodeDAG.containsNode(binding)); + assert(&node != &binding); + + if (m_logicNodeDAG.addEdge(binding, node)) + m_nodeTopologyChanged = true; + } + + void LogicNodeDependencies::removeBindingDependency(RamsesBindingImpl& binding, LogicNodeImpl& node) + { + assert(m_logicNodeDAG.containsNode(node)); + assert(m_logicNodeDAG.containsNode(binding)); + assert(&node != &binding); + + m_logicNodeDAG.removeEdge(binding, node); + } +} diff --git a/client/logic/lib/internals/LogicNodeDependencies.h b/client/logic/lib/internals/LogicNodeDependencies.h new file mode 100644 index 000000000..d90b52840 --- /dev/null +++ b/client/logic/lib/internals/LogicNodeDependencies.h @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "internals/DirectedAcyclicGraph.h" + +#include + +namespace ramses::internal +{ + class LogicNodeImpl; + class ErrorReporting; + class PropertyImpl; + class RamsesBindingImpl; + + using NodeSet = std::unordered_set; + + // Tracks the links between logic nodes and orders them based on the topological structure derived + // from those links. + class LogicNodeDependencies + { + public: + // The primary purpose of this class + [[nodiscard]] const std::optional& getTopologicallySortedNodes(); + + // Nodes management + void addNode(LogicNodeImpl& node); + void removeNode(LogicNodeImpl& node); + + // Link management + bool link(PropertyImpl& output, PropertyImpl& input, bool isWeakLink, ErrorReporting& errorReporting); + bool unlink(PropertyImpl& output, PropertyImpl& input, ErrorReporting& errorReporting); + [[nodiscard]] bool isLinked(const LogicNodeImpl& node) const; + + // Dependency between binding and node, i.e. node depends on binding + void addBindingDependency(RamsesBindingImpl& binding, LogicNodeImpl& node); + void removeBindingDependency(RamsesBindingImpl& binding, LogicNodeImpl& node); + + private: + DirectedAcyclicGraph m_logicNodeDAG; + + [[nodiscard]] bool isLinked(PropertyImpl& input) const; + + // Initial state: no nodes and no need to re-compute node topology + std::optional m_cachedTopologicallySortedNodes = NodeVector{}; + bool m_nodeTopologyChanged = false; + }; +} diff --git a/client/logic/lib/internals/LogicNodeUpdateStatistics.cpp b/client/logic/lib/internals/LogicNodeUpdateStatistics.cpp new file mode 100644 index 000000000..c360ebde8 --- /dev/null +++ b/client/logic/lib/internals/LogicNodeUpdateStatistics.cpp @@ -0,0 +1,143 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/LogicNode.h" +#include "internals/UpdateReport.h" +#include "internals/LogicNodeUpdateStatistics.h" +#include "impl/LoggerImpl.h" + +namespace ramses::internal +{ + void LogicNodeUpdateStatistics::clear() + { + m_updateExecutionTime.clear(); + m_timeSinceLastUpdate.clear(); + m_nodesExecuted.clear(); + m_activatedLinks.clear(); + m_currentStatisticsFrame = 0u; + m_totalNodesCount = 0u; + m_lastTimeUpdateDataAdded = std::nullopt; + } + + void LogicNodeUpdateStatistics::setLogLevel(ELogLevel logLevel) + { + m_logLevel = logLevel; + } + + void LogicNodeUpdateStatistics::collect(const UpdateReport& report, size_t totalNodesCount) + { + m_totalNodesCount = totalNodesCount; + + collectTimeSinceLastUpdate(); + m_updateExecutionTime.add(report.getSectionExecutionTime(UpdateReport::ETimingSection::TotalUpdate).count()); + m_nodesExecuted.add(static_cast(m_nodesExecutedCurrentUpdate)); + m_nodesExecutedCurrentUpdate = 0u; + m_activatedLinks.add(static_cast(report.getLinkActivations())); + + m_currentStatisticsFrame++; + } + + bool LogicNodeUpdateStatistics::checkUpdateFrameFinished() const + { + return m_currentStatisticsFrame == m_loggingRate; + } + + void LogicNodeUpdateStatistics::nodeExecuted() + { + m_nodesExecutedCurrentUpdate++; + } + + void LogicNodeUpdateStatistics::collectTimeSinceLastUpdate() + { + const auto now = Clock::now(); + if (m_lastTimeUpdateDataAdded.has_value()) + { + const auto timeSinceLastFrame = std::chrono::duration_cast((now - m_lastTimeUpdateDataAdded.value())).count(); + m_timeSinceLastUpdate.add(timeSinceLastFrame); + } + m_lastTimeUpdateDataAdded = now; + } + + void LogicNodeUpdateStatistics::setLoggingRate(size_t loggingRate) + { + m_loggingRate = loggingRate; + } + + void LogicNodeUpdateStatistics::logTimeSinceLastLog() + { + const auto now = Clock::now(); + if (!m_lastTimeLogged.has_value()) + { + log("First Statistics Log"); + } + else + { + const auto timeSinceLastLog = std::chrono::duration_cast(now - m_lastTimeLogged.value()); + log("Time since last log: {:.2f} sec", static_cast(timeSinceLastLog.count()) / 1000.f); + } + m_lastTimeLogged = now; + } + + void LogicNodeUpdateStatistics::logUpdateExecutionTime() + { + log("Update Execution time (min/max/avg): {}/{}/{} [u]sec", + m_updateExecutionTime.min, + m_updateExecutionTime.max, + m_updateExecutionTime.acc / m_currentStatisticsFrame); + } + + void LogicNodeUpdateStatistics::logTimeBetweenUpdates() + { + if (m_currentStatisticsFrame == 1) + { + log("Time between Update calls cannot be measured with loggingRate = 1"); + } + else + { + log("Time between Update calls (min/max/avg): {:.2f}/{:.2f}/{:.2f} [m]sec", + static_cast(m_timeSinceLastUpdate.min) / 1000.f, + static_cast(m_timeSinceLastUpdate.max) / 1000.f, + (static_cast(m_timeSinceLastUpdate.acc) / static_cast(m_currentStatisticsFrame - 1)) / 1000.f); + } + } + + void LogicNodeUpdateStatistics::logNodesExecuted() + { + const size_t totalNodesNotZero = std::max(size_t(1), m_totalNodesCount); + log("Nodes Executed (min/max/avg): {}%/{}%/{}% of {} nodes total", + (m_nodesExecuted.min / totalNodesNotZero) * 100, + (m_nodesExecuted.max / totalNodesNotZero) * 100, + ((m_nodesExecuted.acc / m_currentStatisticsFrame) / totalNodesNotZero) * 100, + m_totalNodesCount); + } + + void LogicNodeUpdateStatistics::logActivatedLinks() + { + log("Activated links (min/max/avg): {}/{}/{}", + m_activatedLinks.min, + m_activatedLinks.max, + m_activatedLinks.acc / m_currentStatisticsFrame); + } + + void LogicNodeUpdateStatistics::calculateAndLog() + { + assert(m_currentStatisticsFrame != 0u); + + logTimeSinceLastLog(); + + logUpdateExecutionTime(); + + logTimeBetweenUpdates(); + + logNodesExecuted(); + + logActivatedLinks(); + + clear(); + } +} diff --git a/client/logic/lib/internals/LogicNodeUpdateStatistics.h b/client/logic/lib/internals/LogicNodeUpdateStatistics.h new file mode 100644 index 000000000..69a644308 --- /dev/null +++ b/client/logic/lib/internals/LogicNodeUpdateStatistics.h @@ -0,0 +1,79 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once +#include "ramses-framework-api/RamsesFrameworkTypes.h" +#include "impl/LoggerImpl.h" + +namespace ramses::internal +{ + using Clock = std::chrono::steady_clock; + using TimePoint = std::chrono::time_point; + + class UpdateReport; + + class LogicNodeUpdateStatistics + { + public: + + void nodeExecuted(); + void collect(const UpdateReport& report, size_t totalNodesCount); + void calculateAndLog(); + void setLogLevel(ELogLevel logLevel); + void setLoggingRate(size_t loggingRate); + [[nodiscard]] bool checkUpdateFrameFinished() const; + + private: + struct StatisticProperty + { + int64_t min = std::numeric_limits::max(); + int64_t max = 0; + int64_t acc = 0; + + void add(int64_t value) + { + min = std::min(min, value); + max = std::max(max, value); + acc += value; + } + + void clear() + { + min = std::numeric_limits::max(); + max = 0; + acc = 0; + } + }; + + template void log(const ARGS&... args) + { + LoggerImpl::GetInstance().log(m_logLevel, args...); + } + void clear(); + void collectTimeSinceLastUpdate(); + + void logTimeSinceLastLog(); + void logUpdateExecutionTime(); + void logTimeBetweenUpdates(); + void logNodesExecuted(); + void logActivatedLinks(); + + size_t m_loggingRate = 60u; + size_t m_currentStatisticsFrame = 0u; + size_t m_nodesExecutedCurrentUpdate = 0u; + size_t m_totalNodesCount = 0u; + std::optional m_lastTimeLogged = std::nullopt; + std::optional m_lastTimeUpdateDataAdded = std::nullopt; + ELogLevel m_logLevel = ELogLevel::Debug; + + StatisticProperty m_timeSinceLastUpdate; + StatisticProperty m_updateExecutionTime; + StatisticProperty m_nodesExecuted; + StatisticProperty m_activatedLinks; + }; +} diff --git a/client/logic/lib/internals/LuaCompilationUtils.cpp b/client/logic/lib/internals/LuaCompilationUtils.cpp new file mode 100644 index 000000000..b37bf2ef0 --- /dev/null +++ b/client/logic/lib/internals/LuaCompilationUtils.cpp @@ -0,0 +1,540 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/LuaCompilationUtils.h" + +#include "ramses-logic/LuaModule.h" +#include "impl/PropertyImpl.h" +#include "impl/LuaModuleImpl.h" +#include "impl/LoggerImpl.h" +#include "internals/SolState.h" +#include "internals/InterfaceTypeInfo.h" +#include "internals/ErrorReporting.h" +#include "internals/PropertyTypeExtractor.h" +#include "internals/EPropertySemantics.h" +#include "internals/EnvironmentProtection.h" +#include "fmt/format.h" +#include "SolHelper.h" + +namespace ramses::internal +{ + std::optional LuaCompilationUtils::CompileScriptOrImportPrecompiled( + SolState& solState, + const ModuleMapping& userModules, + const StandardModules& stdModules, + std::string source, + std::string_view name, + ErrorReporting& errorReporting, + sol::bytecode byteCodeFromPrecompiledScript, + std::unique_ptr inputsFromPrecompiledScript, + std::unique_ptr outputsFromPrecompiledScript, + bool enableDebugLogFunctions) + { + sol::environment env = solState.createEnvironment(stdModules, userModules, enableDebugLogFunctions); + sol::table internalEnv = EnvironmentProtection::GetProtectedEnvironmentTable(env); + + internalEnv["GLOBAL"] = solState.createTable(); + + sol::load_result load_result{}; + sol::protected_function_result main_result{}; + sol::protected_function mainFunction{}; + const std::string debuggingName = "RL_lua_script"; + + if (!byteCodeFromPrecompiledScript.empty()) + { + ScopedEnvironmentProtection p(env, EEnvProtectionFlag::LoadScript); + main_result = solState.loadScriptByteCode(byteCodeFromPrecompiledScript.as_string_view(), debuggingName, env); + if (!main_result.valid()) + { + sol::error error = main_result; + if (source.empty()) + { + errorReporting.add(fmt::format("Fatal error during loading of LuaScript '{}': failed loading pre-compiled byte code and no source available to recompile:\n{}!", name, error.what()), + nullptr, EErrorType::BinaryVersionMismatch); + return std::nullopt; + } + + LOG_WARN("Performance warning! Error during loading of LuaScript '{}' from pre-compiled byte code, will try to recompile script from source code. Error:\n{}!", name, error.what()); + byteCodeFromPrecompiledScript.clear(); + } + } + + if (byteCodeFromPrecompiledScript.empty()) + { + load_result = solState.loadScript(source, debuggingName); + if (!load_result.valid()) + { + sol::error error = load_result; + errorReporting.add(fmt::format("[{}] Error while loading script. Lua stack trace:\n{}", name, error.what()), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + if (!CrossCheckDeclaredAndProvidedModules(source, userModules, name, errorReporting)) + return std::nullopt; + + mainFunction = load_result; + env.set_on(mainFunction); + + { + ScopedEnvironmentProtection p(env, EEnvProtectionFlag::LoadScript); + main_result = mainFunction(); + } + + if (!main_result.valid()) + { + sol::error error = main_result; + errorReporting.add(error.what(), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + } + + if (main_result.get_type() != sol::type::none) + { + errorReporting.add(fmt::format("[{}] Expected no return value in script source, but a value of type '{}' was returned!", + name, sol_helper::GetSolTypeName(main_result.get_type())), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + sol::protected_function init = internalEnv["init"]; + if (init.valid()) + { + // in order to support interface definitions in globals we need to register the symbols for init section + sol::protected_function_result initResult {}; + { + PropertyTypeExtractor::RegisterTypes(internalEnv); + ScopedEnvironmentProtection p(env, EEnvProtectionFlag::InitFunction); + initResult = init(); + PropertyTypeExtractor::UnregisterTypes(internalEnv); + } + + if (!initResult.valid()) + { + sol::error error = initResult; + errorReporting.add(fmt::format("[{}] Error while initializing script. Lua stack trace:\n{}", name, error.what()), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + } + + sol::protected_function run = internalEnv["run"]; + + if (!run.valid()) + { + errorReporting.add(fmt::format("[{}] No 'run' function defined!", name), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + std::unique_ptr resultInputs; + std::unique_ptr resultOutputs; + + if (inputsFromPrecompiledScript) + { + assert(outputsFromPrecompiledScript); + resultInputs = std::move(inputsFromPrecompiledScript); + resultOutputs = std::move(outputsFromPrecompiledScript); + } + else + { + sol::protected_function intf = internalEnv["interface"]; + if (!intf.valid()) + { + errorReporting.add(fmt::format("[{}] No 'interface' function defined!", name), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + // Names used only for better error messages! + PropertyTypeExtractor inputsExtractor("inputs", EPropertyType::Struct); + PropertyTypeExtractor outputsExtractor("outputs", EPropertyType::Struct); + + sol::environment interfaceEnv = solState.createEnvironment(stdModules, userModules, false); + sol::table internalInterfaceEnv = EnvironmentProtection::GetProtectedEnvironmentTable(interfaceEnv); + PropertyTypeExtractor::RegisterTypes(internalInterfaceEnv); + // Expose globals to interface function + internalInterfaceEnv.set("GLOBAL", internalEnv["GLOBAL"]); + + interfaceEnv.set_on(intf); + sol::protected_function_result intfResult{}; + { + ScopedEnvironmentProtection p(interfaceEnv, EEnvProtectionFlag::InterfaceFunctionInScript); + intfResult = intf(std::ref(inputsExtractor), std::ref(outputsExtractor)); + } + + for (const auto& module : userModules) + interfaceEnv[module.first] = sol::lua_nil; + PropertyTypeExtractor::UnregisterTypes(internalInterfaceEnv); + + if (!intfResult.valid()) + { + sol::error error = intfResult; + errorReporting.add(fmt::format("[{}] Error while loading script. Lua stack trace:\n{}", name, error.what()), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + HierarchicalTypeData extractedInputsType = inputsExtractor.getExtractedTypeData(); + HierarchicalTypeData extractedOutputsType = outputsExtractor.getExtractedTypeData(); + // Remove names + extractedInputsType.typeData.name = ""; + extractedOutputsType.typeData.name = ""; + + resultInputs = std::make_unique(extractedInputsType, EPropertySemantics::ScriptInput); + resultOutputs = std::make_unique(extractedOutputsType, EPropertySemantics::ScriptOutput); + } + + sol::bytecode resultByteCode = (byteCodeFromPrecompiledScript.empty() ? mainFunction.dump() : std::move(byteCodeFromPrecompiledScript)); + + EnvironmentProtection::SetEnvironmentProtectionLevel(env, EEnvProtectionFlag::RunFunction); + + return LuaCompiledScript{ + LuaCompiledSource{ + std::move(source), + std::move(resultByteCode), + solState, + stdModules, + userModules, + enableDebugLogFunctions + }, + std::move(run), + std::move(resultInputs), + std::move(resultOutputs) + }; + } + + std::optional LuaCompilationUtils::CompileInterface( + SolState& solState, + const ModuleMapping& userModules, + const StandardModules& stdModules, + bool verifyModules, + const std::string& source, + std::string_view name, + ErrorReporting& errorReporting) + { + sol::load_result load_result = solState.loadScript(source, name); + if (!load_result.valid()) + { + sol::error error = load_result; + errorReporting.add(fmt::format("[{}] Error while loading interface. Lua stack trace:\n{}", name, error.what()), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + if (verifyModules && !CrossCheckDeclaredAndProvidedModules(source, userModules, name, errorReporting)) + return std::nullopt; + + sol::environment env = solState.createEnvironment(stdModules, userModules, false); + sol::table internalEnv = EnvironmentProtection::GetProtectedEnvironmentTable(env); + + sol::protected_function mainFunction = load_result; + env.set_on(mainFunction); + + sol::protected_function_result main_result{}; + { + ScopedEnvironmentProtection p(env, EEnvProtectionFlag::LoadInterface); + main_result = mainFunction(); + } + + if (!main_result.valid()) + { + sol::error error = main_result; + errorReporting.add(error.what(), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + if (main_result.get_type() != sol::type::none) + { + errorReporting.add(fmt::format("[{}] Expected no return value in interface source, but a value of type '{}' was returned!", + name, sol_helper::GetSolTypeName(main_result.get_type())), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + sol::protected_function intf = internalEnv["interface"]; + if (!intf.valid()) + { + errorReporting.add(fmt::format("[{}] No 'interface' function defined!", name), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + // Name used for better error messages and discarded later + PropertyTypeExtractor inputsExtractor("inputs", EPropertyType::Struct); + + sol::environment interfaceEnv = solState.createEnvironment(stdModules, userModules, false); + sol::table internalInterfaceEnv = EnvironmentProtection::GetProtectedEnvironmentTable(interfaceEnv); + PropertyTypeExtractor::RegisterTypes(internalInterfaceEnv); + + interfaceEnv.set_on(intf); + sol::protected_function_result interfaceResult{}; + { + ScopedEnvironmentProtection p(interfaceEnv, EEnvProtectionFlag::InterfaceFunctionInInterface); + interfaceResult = intf(std::ref(inputsExtractor)); + } + + if (!interfaceResult.valid()) + { + sol::error error = interfaceResult; + errorReporting.add(fmt::format("[{}] Error while loading interface. Lua stack trace:\n{}", name, error.what()), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + PropertyTypeExtractor::UnregisterTypes(internalInterfaceEnv); + + // Discard name + HierarchicalTypeData extractedInputsType = inputsExtractor.getExtractedTypeData(); + extractedInputsType.typeData.name = ""; + + auto rootProperty = std::make_unique(extractedInputsType, EPropertySemantics::Interface); + + return LuaCompiledInterface + { + std::move(rootProperty) + }; + } + + std::optional LuaCompilationUtils::CompileModuleOrImportPrecompiled( + SolState& solState, + const ModuleMapping& userModules, + const StandardModules& stdModules, + std::string source, + std::string_view name, + ErrorReporting& errorReporting, + sol::bytecode byteCodeFromPrecompiledModule, + bool enableDebugLogFunctions) + { + sol::environment env = solState.createEnvironment(stdModules, userModules, enableDebugLogFunctions); + sol::table internalEnv = EnvironmentProtection::GetProtectedEnvironmentTable(env); + // interface definitions can be provided within module, in order to be able to extract them + // when used in LuaScript interface the necessary user types need to be provided + PropertyTypeExtractor::RegisterTypes(internalEnv); + + sol::load_result load_result{}; + sol::protected_function mainFunction{}; + sol::protected_function_result main_result{}; + + const std::string debuggingName = "RL_lua_module"; + if (!byteCodeFromPrecompiledModule.empty()) + { + ScopedEnvironmentProtection p(env, EEnvProtectionFlag::Module); + main_result = solState.loadScriptByteCode(byteCodeFromPrecompiledModule.as_string_view(), debuggingName, env); + + if (!main_result.valid()) + { + sol::error error = main_result; + if (source.empty()) + { + errorReporting.add(fmt::format("Fatal error during loading of LuaModule '{}': failed loading pre-compiled byte code and no source available to recompile:\n{}!", name, error.what()), + nullptr, EErrorType::BinaryVersionMismatch); + return std::nullopt; + } + + LOG_WARN("Performance warning! Error during loading of LuaScript '{}' from pre-compiled byte code, will try to recompile script from source code. Error:\n{}!", name, error.what()); + byteCodeFromPrecompiledModule.clear(); + } + } + + if (byteCodeFromPrecompiledModule.empty()) + { + load_result = solState.loadScript(source, debuggingName); + if (!load_result.valid()) + { + sol::error error = load_result; + errorReporting.add(fmt::format("[{}] Error while loading module. Lua stack trace:\n{}", name, error.what()), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + if (!CrossCheckDeclaredAndProvidedModules(source, userModules, name, errorReporting)) + return std::nullopt; + + mainFunction = load_result; + env.set_on(mainFunction); + + { + ScopedEnvironmentProtection p(env, EEnvProtectionFlag::Module); + main_result = mainFunction(); + } + + if (!main_result.valid()) + { + sol::error error = main_result; + errorReporting.add(error.what(), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + } + + PropertyTypeExtractor::UnregisterTypes(internalEnv); + + sol::object resultObj = main_result; + // TODO Violin check and test for abuse: yield, more than one result + if (!resultObj.is()) + { + errorReporting.add(fmt::format("[{}] Error while loading module. Module script must return a table!", name), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + sol::table moduleTable = resultObj; + + //for serialization + sol::bytecode resultByteCode = (byteCodeFromPrecompiledModule.empty() ? mainFunction.dump() : std::move(byteCodeFromPrecompiledModule)); + + auto compiledModule = LuaCompiledModule{ + LuaCompiledSource{ + std::move(source), + std::move(resultByteCode), + solState, + stdModules, + userModules, + enableDebugLogFunctions + }, + LuaCompilationUtils::MakeTableReadOnly(solState, moduleTable) + }; + + // Applies environment protection to the module until it's destroyed + // There is no difference between compilation time and runtime protection rules for modules + // (the rules are the same) + EnvironmentProtection::SetEnvironmentProtectionLevel(env, EEnvProtectionFlag::Module); + + return compiledModule; + } + + // Implements https://www.lua.org/pil/13.4.4.html + sol::table LuaCompilationUtils::MakeTableReadOnly(SolState& solState, sol::table table) + { + auto denyAccess = []() { + sol_helper::throwSolException("Modifying module data is not allowed!"); + }; + + for (auto [childKey, childObject] : table) + { + // TODO Violin remove this special case again; currently it's needed because applying read protection + // to type info objects makes them unusable during interface() + if (childObject.is()) + { + continue; + } + + if (childObject.is()) + { + table[childKey] = LuaCompilationUtils::MakeTableReadOnly(solState, childObject); + } + } + + // create metatable which denies write access but allows reading + sol::table metatable = solState.createTable(); + metatable[sol::meta_function::new_index] = denyAccess; + metatable[sol::meta_function::index] = table; + + // overwrite assigned module with new table and point its metatable to read-only table + sol::table readOnlyTable = solState.createTable(); + readOnlyTable[sol::metatable_key] = metatable; + + return readOnlyTable; + } + + bool LuaCompilationUtils::CheckModuleName(std::string_view name) + { + if (name.empty()) + return false; + + // Check if any non-alpha-numeric char is contained + const bool onlyAlphanumChars = std::all_of(name.cbegin(), name.cend(), [](char c) { return c == '_' || (0 != std::isalnum(c)); }); + if (!onlyAlphanumChars) + return false; + + // First letter is a digit -> error + if (0 != std::isdigit(name[0])) + return false; + + return true; + } + + bool LuaCompilationUtils::CrossCheckDeclaredAndProvidedModules(std::string_view source, const ModuleMapping& modules, std::string_view name, ErrorReporting& errorReporting) + { + std::optional> declaredModules = LuaCompilationUtils::ExtractModuleDependencies(source, errorReporting); + if (!declaredModules) // failed extraction + return false; + if (modules.empty() && declaredModules->empty()) // early out if no modules + return true; + + std::vector providedModules; + providedModules.reserve(modules.size()); + for (const auto& m : modules) + providedModules.push_back(m.first); + std::sort(declaredModules->begin(), declaredModules->end()); + std::sort(providedModules.begin(), providedModules.end()); + if (providedModules != declaredModules) + { + std::string errMsg = fmt::format("[{}] Error while loading script/module. Module dependencies declared in source code do not match those provided by LuaConfig.\n", name); + errMsg += fmt::format(" Module dependencies declared in source code: {}\n", fmt::join(*declaredModules, ", ")); + errMsg += fmt::format(" Module dependencies provided on create API: {}", fmt::join(providedModules, ", ")); + errorReporting.add(errMsg, nullptr, EErrorType::IllegalArgument); + return false; + } + + return true; + } + + std::optional> LuaCompilationUtils::ExtractModuleDependencies(std::string_view source, ErrorReporting& errorReporting) + { + sol::state tempLuaState; + + std::vector extractedModules; + bool success = true; + int timesCalled = 0; + tempLuaState.set_function("modules", [&](sol::variadic_args va) { + ++timesCalled; + int argIdx = 0; + for (const auto& v : va) + { + if (v.is()) + { + extractedModules.push_back(v.as()); + } + else + { + const auto argTypeName = sol::type_name(v.lua_state(), v.get_type()); + errorReporting.add( + fmt::format(R"(Error while extracting module dependencies: argument {} is of type '{}', string must be provided: ex. 'modules("moduleA", "moduleB")')", + argIdx, argTypeName), nullptr, EErrorType::LuaSyntaxError); + success = false; + } + ++argIdx; + } + }); + + const sol::load_result load_result = tempLuaState.load(source, "temp"); + if (!load_result.valid()) + { + sol::error error = load_result; + errorReporting.add(fmt::format("Error while extracting module dependencies:\n{}", error.what()), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + sol::protected_function scriptFunc = load_result; + const sol::protected_function_result scriptFuncResult = scriptFunc(); + if (!scriptFuncResult.valid()) + { + const sol::error error = scriptFuncResult; + LOG_DEBUG("Lua runtime error while extracting module dependencies, this is ignored for the actual extraction but might affect its result:\n{}", error.what()); + } + + if (!success) + return std::nullopt; + + if (timesCalled > 1) + { + errorReporting.add("Error while extracting module dependencies: 'modules' function was executed more than once", nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + auto sortedDependencies = extractedModules; + std::sort(sortedDependencies.begin(), sortedDependencies.end()); + const auto duplicateIt = std::adjacent_find(sortedDependencies.begin(), sortedDependencies.end()); + if (duplicateIt != sortedDependencies.end()) + { + errorReporting.add(fmt::format("Error while extracting module dependencies: '{}' appears more than once in dependency list", *duplicateIt), nullptr, EErrorType::LuaSyntaxError); + return std::nullopt; + } + + return extractedModules; + } +} diff --git a/client/logic/lib/internals/LuaCompilationUtils.h b/client/logic/lib/internals/LuaCompilationUtils.h new file mode 100644 index 000000000..18ac11c0f --- /dev/null +++ b/client/logic/lib/internals/LuaCompilationUtils.h @@ -0,0 +1,112 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/LuaConfigImpl.h" +#include "internals/SolWrapper.h" + +#include +#include +#include + +namespace ramses +{ + class LuaModule; +} + +namespace ramses::internal +{ + class SolState; + class ErrorReporting; + class PropertyImpl; + + struct LuaCompiledSource + { + // Metadata + std::string sourceCode; + sol::bytecode byteCode; + std::reference_wrapper solState; + StandardModules stdModules; + ModuleMapping userModules; + bool hasDebugLogFunctions = false; + }; + + struct LuaCompiledScript + { + LuaCompiledSource source; + + // The run() function + sol::protected_function runFunction; + + // Parsed interface properties + std::unique_ptr rootInput; + std::unique_ptr rootOutput; + }; + + struct LuaCompiledInterface + { + std::unique_ptr rootProperty; + }; + + struct LuaCompiledModule + { + LuaCompiledSource source; + sol::table moduleTable; + }; + + class LuaCompilationUtils + { + public: + [[nodiscard]] static std::optional CompileScriptOrImportPrecompiled( + SolState& solState, + const ModuleMapping& userModules, + const StandardModules& stdModules, + std::string source, + std::string_view name, + ErrorReporting& errorReporting, + sol::bytecode byteCodeFromPrecompiledScript, + std::unique_ptr inputsFromPrecompiledScript, + std::unique_ptr outputsFromPrecompiledScript, + bool enableDebugLogFunctions); + + [[nodiscard]] static std::optional CompileInterface( + SolState& solState, + const ModuleMapping& userModules, + const StandardModules& stdModules, + bool verifyModules, + const std::string& source, + std::string_view name, + ErrorReporting& errorReporting); + + [[nodiscard]] static std::optional CompileModuleOrImportPrecompiled( + SolState& solState, + const ModuleMapping& userModules, + const StandardModules& stdModules, + std::string source, + std::string_view name, + ErrorReporting& errorReporting, + sol::bytecode byteCodeFromPrecompiledModule, + bool enableDebugLogFunctions); + + [[nodiscard]] static bool CheckModuleName(std::string_view name); + + [[nodiscard]] static std::optional> ExtractModuleDependencies( + std::string_view source, + ErrorReporting& errorReporting); + + [[nodiscard]] static sol::table MakeTableReadOnly(SolState& solState, sol::table table); + + private: + [[nodiscard]] static bool CrossCheckDeclaredAndProvidedModules( + std::string_view source, + const ModuleMapping& modules, + std::string_view chunkname, + ErrorReporting& errorReporting); + }; +} diff --git a/client/logic/lib/internals/LuaCustomizations.cpp b/client/logic/lib/internals/LuaCustomizations.cpp new file mode 100644 index 000000000..22dbd629b --- /dev/null +++ b/client/logic/lib/internals/LuaCustomizations.cpp @@ -0,0 +1,380 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/LuaCustomizations.h" + +#include "ramses-logic/Property.h" +#include "ramses-logic/EPropertyType.h" + +#include "impl/LoggerImpl.h" + +#include "internals/SolHelper.h" +#include "internals/WrappedLuaProperty.h" +#include "internals/PropertyTypeExtractor.h" +#include "internals/LuaTypeConversions.h" +#include "internals/TypeUtils.h" + +namespace ramses::internal +{ + void LuaCustomizations::RegisterTypes(sol::state& state) + { + state["rl_len"] = rl_len; + state["rl_next"] = rl_next; + state["rl_pairs"] = rl_pairs; + state["rl_ipairs"] = rl_ipairs; + } + + void LuaCustomizations::MapToEnvironment(sol::state& state, sol::environment& env) + { + env["rl_len"] = state["rl_len"]; + env["rl_next"] = state["rl_next"]; + env["rl_pairs"] = state["rl_pairs"]; + env["rl_ipairs"] = state["rl_ipairs"]; + } + + void LuaCustomizations::MapDebugLogFunctions(sol::environment& env) + { + auto rl_logInfo = [](const std::string& msg) + { + LOG_INFO("LuaDebugLog: {}", msg); + }; + auto rl_logWarn = [](const std::string& msg) + { + LOG_WARN("LuaDebugLog: {}", msg); + }; + auto rl_logError = [](const std::string& msg) + { + LOG_ERROR("LuaDebugLog: {}", msg); + }; + + env["rl_logInfo"] = rl_logInfo; + env["rl_logWarn"] = rl_logWarn; + env["rl_logError"] = rl_logError; + } + + size_t LuaCustomizations::rl_len(const sol::object& obj) + { + // Check if normal lua table, or read-only module table + const std::optional potentialTable = LuaTypeConversions::ExtractLuaTable(obj); + if (potentialTable) + { + return potentialTable->size(); + } + + // Check for custom types (registered by logic engine) + if (obj.get_type() == sol::type::userdata) + { + if (obj.is()) + { + return obj.as().size(); + } + + if (obj.is()) + { + const auto& extractor = obj.as(); + const EPropertyType rootType = extractor.getRootTypeData().type; + if (TypeUtils::CanHaveChildren(rootType)) + return extractor.getNestedExtractors().size(); + + // all other custom types are not countable during type extraction + sol_helper::throwSolException("rl_len() called on an unsupported type '{}'", GetLuaPrimitiveTypeName(rootType)); + } + } + + // Other type (unsupported) -> report usage error + sol_helper::throwSolException("rl_len() called on an unsupported type '{}'", sol_helper::GetSolTypeName(obj.get_type())); + + return 0u; + } + + std::tuple LuaCustomizations::rl_next(sol::this_state state, const sol::object& container, const sol::object& indexObject) + { + // Runtime property (checked first because performance-wise highest priority) + if (container.is()) + { + const WrappedLuaProperty& wrappedProperty = container.as(); + const EPropertyType propType = wrappedProperty.getWrappedProperty().getType(); + + if (TypeUtils::CanHaveChildren(propType)) + { + assert(propType == EPropertyType::Struct || propType == EPropertyType::Array); + + // Valid case for struct and array! If container empty, next() is required to return a tuple of nil's + if (wrappedProperty.size() == 0u) + return std::make_tuple(sol::lua_nil, sol::lua_nil); + + if (propType == EPropertyType::Array) + return rl_next_runtime_array(state, wrappedProperty, indexObject); + if (propType == EPropertyType::Struct) + return rl_next_runtime_struct(state, wrappedProperty, indexObject); + } + + if (TypeUtils::IsPrimitiveVectorType(propType)) + return rl_next_runtime_vector(state, wrappedProperty, indexObject); + + sol_helper::throwSolException("rl_next() called on an unsupported type '{}'", GetLuaPrimitiveTypeName(propType)); + } + + // Standard Lua table or a read-only module + std::optional potentialModuleTable = LuaTypeConversions::ExtractLuaTable(container); + if (potentialModuleTable) + { + sol::function stdNext = sol::state_view(state)["next"]; + std::tuple resultOfStdNext = stdNext(*potentialModuleTable, indexObject); + + return resultOfStdNext; + } + + // Property extractor - this is not executed during runtime, only during interface(), so it's ok to check last + if (container.is()) + { + const PropertyTypeExtractor& typeExtractor = container.as(); + const EPropertyType rootType = typeExtractor.getRootTypeData().type; + + // Not possible to get non-iteratable types during extraction, safe to assert here + if (TypeUtils::CanHaveChildren(rootType)) + { + // Valid case! If container empty, next() is required to return a tuple of nil's + if (typeExtractor.getNestedExtractors().empty()) + return std::make_tuple(sol::lua_nil, sol::lua_nil); + + if (rootType == EPropertyType::Array) + return rl_next_array_extractor(state, typeExtractor, indexObject); + + return rl_next_struct_extractor(state, typeExtractor, indexObject); + } + + // all other custom types are not iteratable during type extraction + sol_helper::throwSolException("rl_next() called on an unsupported type '{}'", GetLuaPrimitiveTypeName(rootType)); + } + + sol_helper::throwSolException("rl_next() called on an unsupported type '{}'", sol_helper::GetSolTypeName(container.get_type())); + + return std::make_tuple(sol::lua_nil, sol::lua_nil); + } + + std::tuple LuaCustomizations::rl_next_runtime_array(sol::this_state state, const WrappedLuaProperty& wrappedArray, const sol::object& indexObject) + { + assert(wrappedArray.getWrappedProperty().getType() == EPropertyType::Array); + + // If index is nil, return the first element + if (indexObject.get_type() == sol::type::lua_nil) + { + // in Lua counting starts at 1; this returns the first tuple of index + value + return std::make_tuple(sol::make_object(state, 1), wrappedArray.resolveChild(state, 0u)); + } + + const DataOrError potentiallyIndex = LuaTypeConversions::ExtractSpecificType(indexObject); + + if (potentiallyIndex.hasError()) + { + sol_helper::throwSolException("Invalid key to rl_next() of type: {}", potentiallyIndex.getError()); + } + + const size_t index = potentiallyIndex.getData(); + + if (index == 0 || index > wrappedArray.size()) + { + sol_helper::throwSolException("Invalid key value '{}' for rl_next(). Expected a number in the range [1, {}]!", index, wrappedArray.size()); + } + + // This is valid - when index is the last element, the 'next' one is idx=nil, value=nil + if (index == wrappedArray.size()) + { + return std::make_tuple(sol::lua_nil, sol::lua_nil); + } + + return std::make_tuple(sol::make_object(state, index + 1), wrappedArray.resolveChild(state, index)); + } + + std::tuple LuaCustomizations::rl_next_runtime_struct(sol::this_state state, const WrappedLuaProperty& wrappedStruct, const sol::object& indexObject) + { + assert(wrappedStruct.getWrappedProperty().getType() == EPropertyType::Struct); + + // If index is nil, return the first element + if (indexObject.get_type() == sol::type::lua_nil) + { + // return name of first element as key + return std::make_tuple(sol::make_object(state, wrappedStruct.getWrappedProperty().getChild(0)->getName()), wrappedStruct.resolveChild(state, 0u)); + } + + // TODO Violin rework the error message here so that it's clear where the error comes from + const size_t structFieldIndex = wrappedStruct.resolvePropertyIndex(indexObject); + + // This is valid - when index is the last element, the 'next' one is idx=nil, value=nil + if (structFieldIndex == wrappedStruct.size() - 1) + { + return std::make_tuple(sol::lua_nil, sol::lua_nil); + } + + return std::make_tuple( + sol::make_object(state, wrappedStruct.getWrappedProperty().getChild(structFieldIndex + 1)->getName()), + wrappedStruct.resolveChild(state, structFieldIndex + 1)); + } + + std::tuple LuaCustomizations::rl_next_runtime_vector(sol::this_state state, const WrappedLuaProperty& wrappedVec, const sol::object& indexObject) + { + assert(TypeUtils::IsPrimitiveVectorType(wrappedVec.getWrappedProperty().getType())); + + // If index is nil, return the first element + if (indexObject.get_type() == sol::type::lua_nil) + return std::make_tuple(sol::make_object(state, 1u), wrappedVec.resolveVectorElement(state, 1u)); + + const size_t elementIndex = wrappedVec.resolvePropertyIndex(indexObject); + const size_t numElements = TypeUtils::ComponentsSizeForPropertyType(wrappedVec.getWrappedProperty().getType()); + + // This is valid - when index is the last element, the 'next' one is idx=nil, value=nil + if (elementIndex == numElements) + return std::make_tuple(sol::lua_nil, sol::lua_nil); + + return std::make_tuple(sol::make_object(state, elementIndex + 1), wrappedVec.resolveVectorElement(state, elementIndex + 1)); + } + + std::tuple LuaCustomizations::rl_next_array_extractor(sol::this_state state, const PropertyTypeExtractor& arrayExtractor, const sol::object& indexObject) + { + // If index is nil, return the first element + if (indexObject.get_type() == sol::type::lua_nil) + { + // in Lua counting starts at 1; this returns the first tuple of index + value + return ResolveExtractorField(state, arrayExtractor, 0u); + } + + const DataOrError potentiallyIndex = LuaTypeConversions::ExtractSpecificType(indexObject); + + if (potentiallyIndex.hasError()) + { + sol_helper::throwSolException("Invalid key to rl_next() of type: {}", potentiallyIndex.getError()); + } + + const size_t arrayElementCount = arrayExtractor.getNestedExtractors().size(); + + const size_t index = potentiallyIndex.getData(); + + if (index == 0 || index > arrayElementCount) + { + sol_helper::throwSolException("Invalid key value '{}' for rl_next(). Expected a number in the range [1, {}]!", index, arrayElementCount); + } + + // This is valid - when index is the last element, the 'next' one is idx=nil, value=nil + if (index == arrayElementCount) + { + return std::make_tuple(sol::lua_nil, sol::lua_nil); + } + + // Return next element which is 'index + 1', but because Lua starts at 1, it's just 'index' + return ResolveExtractorField(state, arrayExtractor, index); + } + + std::tuple LuaCustomizations::rl_next_struct_extractor(sol::this_state state, const PropertyTypeExtractor& structExtractor, const sol::object& indexObject) + { + // If index is nil, return the first element + if (indexObject.get_type() == sol::type::lua_nil) + { + return ResolveExtractorField(state, structExtractor, 0); + } + + const DataOrError potentiallyStrIndex = LuaTypeConversions::ExtractSpecificType(indexObject); + + if (potentiallyStrIndex.hasError()) + { + sol_helper::throwSolException("Invalid key to rl_next(): {}", potentiallyStrIndex.getError()); + } + + const std::string_view strIndex = potentiallyStrIndex.getData(); + + const std::vector& fields = structExtractor.getNestedExtractors(); + const auto fieldIter = std::find_if(fields.cbegin(), fields.cend(), + [&strIndex](const PropertyTypeExtractor& field) { + return field.getRootTypeData().name == strIndex; + }); + + if (fieldIter == fields.cend()) + { + sol_helper::throwSolException("Could not find field named '{}' in struct object '{}'!", + strIndex, structExtractor.getRootTypeData().name); + } + + const size_t structFieldIndex = fieldIter - fields.cbegin(); + + // This is valid - when index is the last element, the 'next' one is idx=nil, value=nil + if (structFieldIndex == fields.size() - 1) + { + return std::make_tuple(sol::lua_nil, sol::lua_nil); + } + + return ResolveExtractorField(state, structExtractor, structFieldIndex + 1); + } + + std::tuple LuaCustomizations::rl_ipairs(sol::this_state s, sol::object iterableObject) + { + if (iterableObject.get_type() == sol::type::userdata) + { + auto wrappedProperty = iterableObject.as>(); + auto propertyExtractor = iterableObject.as>(); + // Assert that one of the types were found + assert(wrappedProperty || propertyExtractor); + + const EPropertyType propertyType = (wrappedProperty ? wrappedProperty->getWrappedProperty().getType() : propertyExtractor->getRootTypeData().type); + if (propertyType == EPropertyType::Array || // Array types can be iterated both in runtime and extraction + (TypeUtils::IsPrimitiveVectorType(propertyType) && wrappedProperty)) // VEC types can be iterated only in runtime, not during property extraction + { + return std::make_tuple(sol::state_view(s)["rl_next"], std::move(iterableObject), sol::lua_nil); + } + + // no other custom types can be iterated using rl_ipairs + sol_helper::throwSolException("rl_ipairs() called on an unsupported type '{}'. Use only with array-like built-in types or modules!", GetLuaPrimitiveTypeName(propertyType)); + } + + std::optional potentialModuleTable = LuaTypeConversions::ExtractLuaTable(iterableObject); + if (potentialModuleTable) + { + return std::make_tuple(sol::state_view(s)["next"], std::move(*potentialModuleTable), sol::lua_nil); + } + + sol_helper::throwSolException("rl_ipairs() called on an unsupported type '{}'. Use only with user types like IN/OUT, modules etc.!", sol_helper::GetSolTypeName(iterableObject.get_type())); + return std::make_tuple(sol::lua_nil, sol::lua_nil, sol::lua_nil); + } + + std::tuple LuaCustomizations::rl_pairs(sol::this_state s, sol::object iterableObject) + { + if (iterableObject.get_type() == sol::type::userdata) + { + return std::make_tuple(sol::state_view(s)["rl_next"], std::move(iterableObject), sol::lua_nil); + } + + std::optional potentialModuleTable = LuaTypeConversions::ExtractLuaTable(iterableObject); + if (potentialModuleTable) + { + return std::make_tuple(sol::state_view(s)["next"], std::move(*potentialModuleTable), sol::lua_nil); + } + + sol_helper::throwSolException("rl_pairs() called on an unsupported type '{}'. Use only with user types like IN/OUT, modules etc.!", sol_helper::GetSolTypeName(iterableObject.get_type())); + return std::make_tuple(sol::lua_nil, sol::lua_nil, sol::lua_nil); + } + + std::tuple LuaCustomizations::ResolveExtractorField(sol::this_state s, const PropertyTypeExtractor& typeExtractor, size_t fieldId) + { + const EPropertyType rootType = typeExtractor.getRootTypeData().type; + std::reference_wrapper field = typeExtractor.getChildReference(fieldId); + const EPropertyType fieldType = field.get().getRootTypeData().type; + + // Provide name as key for structs, index for arrays + sol::object key = + rootType == EPropertyType::Struct ? + sol::make_object(s, field.get().getRootTypeData().name) : + sol::make_object(s, fieldId + 1); // Convert to Lua numeric convention (starts at 1) + + // Cast to label, e.g. EPropertyType::Int32, EPropertyType:Float etc., for primitive types; Otherwise, return the container object for further iteration if needed + sol::object value = + TypeUtils::IsPrimitiveType(fieldType) ? + sol::make_object(s, static_cast(fieldType)) : + sol::make_object(s, field); + + return std::make_tuple(std::move(key), std::move(value)); + } +} diff --git a/client/logic/lib/internals/LuaCustomizations.h b/client/logic/lib/internals/LuaCustomizations.h new file mode 100644 index 000000000..d29c538de --- /dev/null +++ b/client/logic/lib/internals/LuaCustomizations.h @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "internals/SolWrapper.h" + +namespace ramses::internal +{ + class PropertyTypeExtractor; + class WrappedLuaProperty; + + class LuaCustomizations + { + public: + static void RegisterTypes(sol::state& state); + static void MapToEnvironment(sol::state& state, sol::environment& env); + static void MapDebugLogFunctions(sol::environment& env); + + private: + [[nodiscard]] static size_t rl_len(const sol::object& obj); + + [[nodiscard]] static std::tuple rl_next(sol::this_state state, const sol::object& container, const sol::object& indexObject); + [[nodiscard]] static std::tuple rl_next_runtime_array(sol::this_state state, const WrappedLuaProperty& wrappedArray, const sol::object& indexObject); + [[nodiscard]] static std::tuple rl_next_runtime_struct(sol::this_state state, const WrappedLuaProperty& wrappedStruct, const sol::object& indexObject); + [[nodiscard]] static std::tuple rl_next_runtime_vector(sol::this_state state, const WrappedLuaProperty& wrappedVec, const sol::object& indexObject); + [[nodiscard]] static std::tuple rl_next_array_extractor(sol::this_state state, const PropertyTypeExtractor& arrayExtractor, const sol::object& indexObject); + [[nodiscard]] static std::tuple rl_next_struct_extractor(sol::this_state state, const PropertyTypeExtractor& structExtractor, const sol::object& indexObject); + + [[nodiscard]] static std::tuple rl_pairs(sol::this_state s, sol::object iterableObject); + [[nodiscard]] static std::tuple rl_ipairs(sol::this_state s, sol::object iterableObject); + + [[nodiscard]] static std::tuple ResolveExtractorField(sol::this_state s, const PropertyTypeExtractor& typeExtractor, size_t fieldId); + }; +} diff --git a/client/logic/lib/internals/LuaTypeConversions.cpp b/client/logic/lib/internals/LuaTypeConversions.cpp new file mode 100644 index 000000000..01e3b24d5 --- /dev/null +++ b/client/logic/lib/internals/LuaTypeConversions.cpp @@ -0,0 +1,279 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/LuaTypeConversions.h" +#include "internals/SolHelper.h" + +#include + +namespace ramses::internal +{ + // extracts a plain Lua table, or one which was made read-only (e.g. module data tables) + std::optional LuaTypeConversions::ExtractLuaTable(const sol::object& object) + { + auto potentialTable = object.as>(); + + // The missing else() statements below mean that if any of the conditions fail, the + // result of the function will be the first cast (potentialTable) + if (potentialTable) + { + // Identify read-only data (e.g. logic engine read-only module tables) + // This is basically a reverse-test for this: https://www.lua.org/pil/13.4.4.html + // See also LuaCompilationUtils::MakeTableReadOnly for the counterpart + sol::lua_table table = *potentialTable; + sol::object metaContent = table[sol::metatable_key]; + + if (metaContent.valid()) + { + auto potentialMetatable = metaContent.as>(); + if (potentialMetatable) + { + sol::object metaIndex = (*potentialMetatable)[sol::meta_function::index]; + + if (metaIndex.valid()) + { + auto potentialModuleTable = metaIndex.as>(); + if (potentialModuleTable) + { + potentialTable = potentialModuleTable; + } + } + } + } + } + + return potentialTable; + } + + size_t LuaTypeConversions::GetMaxIndexForVectorType(ramses::EPropertyType type) + { + switch (type) + { + case EPropertyType::Vec2i: + case EPropertyType::Vec2f: + return 2u; + case EPropertyType::Vec3i: + case EPropertyType::Vec3f: + return 3u; + case EPropertyType::Vec4f: + case EPropertyType::Vec4i: + return 4u; + case EPropertyType::Struct: + case EPropertyType::Array: + case EPropertyType::Float: + case EPropertyType::Int32: + case EPropertyType::Int64: + case EPropertyType::String: + case EPropertyType::Bool: + assert(false && "Should not ask about size of non-vector types!"); + } + + assert(false && "Vector type not handled!"); + return 0; + } + + template <> DataOrError LuaTypeConversions::ExtractSpecificType(const sol::object& solObject) + { + if (!solObject.valid() || solObject.get_type() != sol::type::number) + { + return DataOrError( + fmt::format("Error while extracting floating point number: expected a number, received '{}'", + sol_helper::GetSolTypeName(solObject.get_type()))); + } + + // Extract Lua number (==double) + const double asDouble = solObject.as(); + + // Integral part out of range of float type + if (asDouble > std::numeric_limits::max() || asDouble < std::numeric_limits::lowest()) + { + return DataOrError(fmt::format("Error while extracting floating point number: value would cause overflow in float ('{}')", asDouble)); + } + + return DataOrError(static_cast(asDouble)); + } + + template <> + DataOrError LuaTypeConversions::ExtractSpecificType(const sol::object& solObject) + { + if (!solObject.valid() || solObject.get_type() != sol::type::number) + { + return DataOrError( + fmt::format("Error while extracting integer: expected a number, received '{}'", + sol_helper::GetSolTypeName(solObject.get_type()))); + } + + // Get Lua number as double (internal format of Lua) + const double asDouble = solObject.as(); + // Rounds to closest signed integer + const double rounded = std::round(asDouble); + + // fractional part too large -> rounding error + const double fractPart = std::abs(asDouble - rounded); + if (fractPart > std::numeric_limits::epsilon()) + { + return DataOrError( + fmt::format("Error while extracting integer: implicit rounding (fractional part '{}' is not negligible)", + fractPart)); + } + + // Integral part out of range + if (rounded > std::numeric_limits::max() || rounded < std::numeric_limits::lowest()) + { + return DataOrError( + fmt::format("Error while extracting integer: integral part too large to fit in a signed 32-bit integer ('{}')", rounded)); + } + + // Static cast is well defined now + return DataOrError(static_cast(rounded)); + } + + template <> + DataOrError LuaTypeConversions::ExtractSpecificType(const sol::object& solObject) + { + if (!solObject.valid() || solObject.get_type() != sol::type::number) + { + return DataOrError( + fmt::format("Error while extracting integer: expected a number, received '{}'", + sol_helper::GetSolTypeName(solObject.get_type()))); + } + + // Get Lua number as double (internal format of Lua) + const double asDouble = solObject.as(); + // Rounds to closest signed integer + const double rounded = std::round(asDouble); + + // fractional part too large -> rounding error + const double fractPart = std::abs(asDouble - rounded); + if (fractPart > std::numeric_limits::epsilon()) + { + return DataOrError( + fmt::format("Error while extracting integer: implicit rounding (fractional part '{}' is not negligible)", + fractPart)); + } + + // Integral part out of range - have to compare in double due to narrowing + if (rounded > static_cast(std::numeric_limits::max()) + || rounded < static_cast(std::numeric_limits::lowest())) + { + return DataOrError( + fmt::format("Error while extracting integer: integral part too large to fit in a signed 64-bit integer ('{}')", rounded)); + } + + // Static cast is well defined now + return DataOrError(static_cast(rounded)); + } + + + template <> + DataOrError LuaTypeConversions::ExtractSpecificType(const sol::object& solObject) + { + if (!solObject.valid() || solObject.get_type() != sol::type::number) + { + return DataOrError( + fmt::format("Error while extracting integer: expected a number, received '{}'", + sol_helper::GetSolTypeName(solObject.get_type()))); + } + + // Get Lua number as double (internal format of Lua) + const double asDouble = solObject.as(); + + // Check that number is >= 0, with some tolerance + if (asDouble < -std::numeric_limits::epsilon()) + { + return DataOrError( + fmt::format("Error while extracting integer: expected non-negative number, received '{}'", asDouble)); + } + + // Rounds to closest signed integer + const double rounded = std::round(asDouble); + + // Integral part too large + if (rounded > double(std::numeric_limits::max())) + { + return DataOrError( + fmt::format("Error while extracting integer: integral part too large to fit in 64-bit unsigned integer ('{}')", rounded)); + } + + // fractional part too large -> rounding error + const double fractPart = std::abs(asDouble - rounded); + if (fractPart > std::numeric_limits::epsilon()) + { + return DataOrError( + fmt::format("Error while extracting integer: implicit rounding (fractional part '{}' is not negligible)", + fractPart)); + } + + // Static cast is well defined now + return DataOrError(static_cast(rounded)); + } + + template <> + DataOrError LuaTypeConversions::ExtractSpecificType(const sol::object& solObject) + { + if (!solObject.is()) + { + return DataOrError( + fmt::format("Expected a string but got object of type {} instead!", + sol_helper::GetSolTypeName(solObject.get_type()))); + } + + return DataOrError(solObject.as()); + } + + template + DataOrError< std::array > LuaTypeConversions::ExtractArray(const sol::object& solObject) + { + const std::optional potentialLuaTable = ExtractLuaTable(solObject); + if (!potentialLuaTable) + { + return DataOrError>(fmt::format("Expected a Lua table with {} entries but got object of type {} instead!", + size, + sol_helper::GetSolTypeName(solObject.get_type()))); + } + + const sol::lua_table& solTable = *potentialLuaTable; + + const size_t tableFieldCount = solTable.size(); + + if (tableFieldCount != size) + { + return DataOrError>( + fmt::format("Error while extracting array: expected {} array components in table but got {} instead!", + size, tableFieldCount)); + } + + std::array data{}; + for (size_t i = 1; i <= size; ++i) + { + const sol::object& tableEntry = solTable[i]; + + const DataOrError maybeValue = ExtractSpecificType(tableEntry); + + if (maybeValue.hasError()) + { + return DataOrError>( + fmt::format("Error while extracting array: unexpected value (type: '{}') at array element # {}! Reason: {}", + sol_helper::GetSolTypeName(tableEntry.get_type()), i, maybeValue.getError()) + ); + } + + data[i-1] = maybeValue.getData(); + } + + return DataOrError(std::move(data)); + } + + // Explicitly instantiate types we use + template DataOrError> LuaTypeConversions::ExtractArray(const sol::object& solObject); + template DataOrError> LuaTypeConversions::ExtractArray(const sol::object& solObject); + template DataOrError> LuaTypeConversions::ExtractArray(const sol::object& solObject); + template DataOrError> LuaTypeConversions::ExtractArray(const sol::object& solObject); + template DataOrError> LuaTypeConversions::ExtractArray(const sol::object& solObject); + template DataOrError> LuaTypeConversions::ExtractArray(const sol::object& solObject); +} diff --git a/client/logic/lib/internals/LuaTypeConversions.h b/client/logic/lib/internals/LuaTypeConversions.h new file mode 100644 index 000000000..1d076960a --- /dev/null +++ b/client/logic/lib/internals/LuaTypeConversions.h @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "internals/SolWrapper.h" +#include "ramses-logic/EPropertyType.h" + +#include +#include + +namespace ramses::internal +{ + // Simple implementation of std::expected for the purpose of type conversion + template + class DataOrError + { + public: + explicit DataOrError(T data) + : m_data(std::move(data)) + { + } + + explicit DataOrError(const std::string& errorMessage) + : m_data(Error{errorMessage}) + { + } + + [[nodiscard]] bool hasError() const + { + return std::holds_alternative(m_data); + } + + [[nodiscard]] T getData() const + { + assert(std::holds_alternative(m_data)); + return std::get(m_data); + } + + [[nodiscard]] const std::string& getError() const + { + assert(std::holds_alternative(m_data)); + return std::get(m_data).message; + } + + private: + struct Error + { + std::string message; + }; + + std::variant m_data; + }; + + + class LuaTypeConversions + { + public: + [[nodiscard]] static std::optional ExtractLuaTable(const sol::object& object); + + template + [[nodiscard]] static DataOrError ExtractSpecificType(const sol::object& solObject); + + [[nodiscard]] static size_t GetMaxIndexForVectorType(ramses::EPropertyType type); + + template + [[nodiscard]] static DataOrError> ExtractArray(const sol::object& solObject); + + static_assert(std::is_same::value, "This class assumes that Lua-internal numbers are double precision floats"); + }; + +} diff --git a/client/logic/lib/internals/PropertyTypeExtractor.cpp b/client/logic/lib/internals/PropertyTypeExtractor.cpp new file mode 100644 index 000000000..92eb11f4d --- /dev/null +++ b/client/logic/lib/internals/PropertyTypeExtractor.cpp @@ -0,0 +1,286 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/PropertyTypeExtractor.h" +#include "internals/SolHelper.h" +#include "internals/TypeUtils.h" +#include "internals/InterfaceTypeInfo.h" +#include "internals/InterfaceTypeFunctions.h" +#include "internals/LuaTypeConversions.h" + +#include "fmt/format.h" +#include + +namespace ramses::internal +{ + PropertyTypeExtractor::PropertyTypeExtractor(std::string rootName, EPropertyType rootType) + : m_typeData(std::move(rootName), rootType) + { + } + + std::reference_wrapper PropertyTypeExtractor::index(const sol::object& propertyIndex) + { + auto childIter = m_children.end(); + if (m_typeData.type == EPropertyType::Struct) + { + const DataOrError childName = LuaTypeConversions::ExtractSpecificType(propertyIndex); + if (childName.hasError()) + { + sol_helper::throwSolException("Bad index access to struct '{}': {}", m_typeData.name, childName.getError()); + } + + childIter = findChild(childName.getData()); + + if (childIter == m_children.end()) + { + sol_helper::throwSolException("Field '{}' does not exist in struct '{}'!", childName.getData(), m_typeData.name); + } + } + else if(m_typeData.type == EPropertyType::Array) + { + const DataOrError childIndex = LuaTypeConversions::ExtractSpecificType(propertyIndex); + + if (childIndex.hasError()) + { + sol_helper::throwSolException("Invalid index access in array '{}': {}", m_typeData.name, childIndex.getError()); + } + + if (childIndex.getData() >= m_children.size()) + { + sol_helper::throwSolException("Invalid index access in array '{}'. Expected index in the range [0, {}] but got {} instead!", + m_typeData.name, m_children.size(), childIndex.getData()); + } + + childIter = m_children.begin() + static_cast(childIndex.getData()); + } + + PropertyTypeExtractor& refToChild = *childIter; + return refToChild; + } + + void PropertyTypeExtractor::newIndex(const sol::object& idx, const sol::object& value) + { + const DataOrError potentiallyIndex = LuaTypeConversions::ExtractSpecificType(idx); + + if (potentiallyIndex.hasError()) + { + sol_helper::throwSolException("Invalid index for new field on struct '{}': {}", m_typeData.name, potentiallyIndex.getError()); + } + + const std::string idxAsStr(potentiallyIndex.getData()); + + auto childIter = findChild(idxAsStr); + if (childIter != m_children.end()) + { + sol_helper::throwSolException("Field '{}' already exists! Can't declare the same field twice!", idxAsStr); + } + + // TODO Violin consider if we want to forbid this + if (value.get_type() == sol::type::table) + { + const std::optional potentiallyTable = LuaTypeConversions::ExtractLuaTable(value); + assert (potentiallyTable); + PropertyTypeExtractor structProperty(idxAsStr, EPropertyType::Struct); + structProperty.extractPropertiesFromTable(*potentiallyTable); + m_children.emplace_back(std::move(structProperty)); + } + else + { + if (value.is()) + { + const InterfaceTypeInfo& typeInfo = value.as(); + addField(idxAsStr, typeInfo); + } + else + { + // TODO Violin maybe list all available types here in the error msg? + sol_helper::throwSolException("Invalid type of field '{}'! Expected Type:T() syntax where T=Float,Int32,... Found a value of type '{}' instead", + idxAsStr, sol_helper::GetSolTypeName(value.get_type())); + } + } + } + + void PropertyTypeExtractor::addField(const std::string& name, const InterfaceTypeInfo& typeInfo) + { + const EPropertyType typeId = typeInfo.typeId; + + if (!TypeUtils::IsValidType(typeId)) + { + // TODO Violin can we sandbox this properly so it can never happen? + sol_helper::throwSolException("Internal error: Invalid type id '{}'!", static_cast(typeId)); + } + + switch (typeId) + { + case EPropertyType::Float: + case EPropertyType::Vec2f: + case EPropertyType::Vec3f: + case EPropertyType::Vec4f: + case EPropertyType::Int32: + case EPropertyType::Int64: + case EPropertyType::Vec2i: + case EPropertyType::Vec3i: + case EPropertyType::Vec4i: + case EPropertyType::Bool: + case EPropertyType::String: + m_children.emplace_back(PropertyTypeExtractor{ std::string(name), typeId }); + break; + case EPropertyType::Struct: + { + const std::optional structFieldsTable = LuaTypeConversions::ExtractLuaTable(typeInfo.complexType); + if (!structFieldsTable) + { + sol_helper::throwSolException("Invalid type of struct field '{}'! Expected Type:Struct(T) Where T is a table with named fields. Found a value of type '{}' instead!", + name, sol_helper::GetSolTypeName(typeInfo.complexType.get_type())); + } + + PropertyTypeExtractor structProperty(name, EPropertyType::Struct); + structProperty.extractPropertiesFromTable(*structFieldsTable); + m_children.emplace_back(std::move(structProperty)); + break; + } + case EPropertyType::Array: + { + sol::optional potentiallyArrayElementTypeInfo = typeInfo.complexType.as>(); + + // Special case for struct/tables. TODO Violin forbid? + if (!potentiallyArrayElementTypeInfo) + { + const std::optional maybeTable = LuaTypeConversions::ExtractLuaTable(typeInfo.complexType); + if (maybeTable) + { + potentiallyArrayElementTypeInfo = InterfaceTypeInfo{ EPropertyType::Struct, 0, *maybeTable }; + } + } + + if (!potentiallyArrayElementTypeInfo) + { + sol_helper::throwSolException("Invalid element type T of array field '{}'! Found a value of type T='{}' instead of T=Type:() in call {} = Type:Array(N, T)!", + name, sol_helper::GetSolTypeName(typeInfo.complexType.get_type()), name); + } + + const InterfaceTypeInfo& elementTypeInfo = *potentiallyArrayElementTypeInfo; + + if (!TypeUtils::IsValidType(elementTypeInfo.typeId)) + { + // TODO Violin can we sandbox this properly so it can never happen? + sol_helper::throwSolException("Internal error: Invalid type id '{}'!", static_cast(elementTypeInfo.typeId)); + } + + PropertyTypeExtractor arrayContainer(name, EPropertyType::Array); + + if (TypeUtils::IsPrimitiveType(elementTypeInfo.typeId)) + { + arrayContainer.m_children.resize(typeInfo.arraySize, PropertyTypeExtractor("", elementTypeInfo.typeId)); + } + else + { + // TODO Violin add tests for nested arrays + assert(elementTypeInfo.typeId == EPropertyType::Array || elementTypeInfo.typeId == EPropertyType::Struct); + + // TODO Violin rework this hack to more elegant sol code + PropertyTypeExtractor tmp("", EPropertyType::Struct); + tmp.addField("", elementTypeInfo); + + arrayContainer.m_children.resize(typeInfo.arraySize, tmp.m_children[0]); + } + + m_children.emplace_back(std::move(arrayContainer)); + break; + } + } + } + + PropertyTypeExtractor::ChildProperties::iterator PropertyTypeExtractor::findChild(const std::string_view name) + { + return std::find_if(m_children.begin(), m_children.end(), [&name](const PropertyTypeExtractor& child) {return child.m_typeData.name == name; }); + } + + void PropertyTypeExtractor::extractPropertiesFromTable(const sol::lua_table& table) + { + for (const auto& tableEntry : table) + { + newIndex(tableEntry.first, tableEntry.second); + } + } + + void PropertyTypeExtractor::RegisterTypes(sol::table& environment) + { + environment.new_usertype("LuaScriptPropertyExtractor", + sol::meta_method::new_index, &PropertyTypeExtractor::newIndex, + sol::meta_method::index, &PropertyTypeExtractor::index); + environment.new_usertype("InterfaceTypeInfo"); + sol::usertype usertypeTypeInfoFuncs = environment.new_usertype("Type"); + + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Float)] = &InterfaceTypeFunctions::CreatePrimitiveType; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Vec2f)] = &InterfaceTypeFunctions::CreatePrimitiveType; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Vec3f)] = &InterfaceTypeFunctions::CreatePrimitiveType; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Vec4f)] = &InterfaceTypeFunctions::CreatePrimitiveType; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Int32)] = &InterfaceTypeFunctions::CreatePrimitiveType; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Int64)] = &InterfaceTypeFunctions::CreatePrimitiveType; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Vec2i)] = &InterfaceTypeFunctions::CreatePrimitiveType; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Vec3i)] = &InterfaceTypeFunctions::CreatePrimitiveType; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Vec4i)] = &InterfaceTypeFunctions::CreatePrimitiveType; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::String)] = &InterfaceTypeFunctions::CreatePrimitiveType; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Bool)] = &InterfaceTypeFunctions::CreatePrimitiveType; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Array)] = &InterfaceTypeFunctions::CreateArray; + usertypeTypeInfoFuncs[GetLuaPrimitiveTypeName(EPropertyType::Struct)] = &InterfaceTypeFunctions::CreateStruct; + } + + void PropertyTypeExtractor::UnregisterTypes(sol::table& environment) + { + auto propertyExtractorUserType = environment.get("LuaScriptPropertyExtractor"); + assert(propertyExtractorUserType.valid()); + propertyExtractorUserType.unregister(); + + auto typeInfo = environment.get("InterfaceTypeInfo"); + assert(typeInfo.valid()); + typeInfo.unregister(); + + auto usertypeTypeInfoFuncs = environment.get("Type"); + assert(usertypeTypeInfoFuncs.valid()); + usertypeTypeInfoFuncs.unregister(); + } + + HierarchicalTypeData PropertyTypeExtractor::getExtractedTypeData() const + { + // extract children type info first + std::vector children; + children.reserve(m_children.size()); + std::transform(m_children.begin(), m_children.end(), std::back_inserter(children), + [](const PropertyTypeExtractor& childExtractor) { return childExtractor.getExtractedTypeData(); }); + + // sort struct children by name lexicographically + if (m_typeData.type == EPropertyType::Struct) + { + std::sort(children.begin(), children.end(), [](const HierarchicalTypeData& child1, const HierarchicalTypeData& child2) + { + assert(!child1.typeData.name.empty() && !child2.typeData.name.empty()); + return child1.typeData.name < child2.typeData.name; + }); + } + + return HierarchicalTypeData(m_typeData, std::move(children)); + } + + std::reference_wrapper PropertyTypeExtractor::getChildReference(size_t childIndex) const + { + assert(childIndex < m_children.size()); + return m_children.at(childIndex); + } + + TypeData PropertyTypeExtractor::getRootTypeData() const + { + return m_typeData; + } + + const std::vector& PropertyTypeExtractor::getNestedExtractors() const + { + return m_children; + } +} diff --git a/client/logic/lib/internals/PropertyTypeExtractor.h b/client/logic/lib/internals/PropertyTypeExtractor.h new file mode 100644 index 000000000..56b93dc0d --- /dev/null +++ b/client/logic/lib/internals/PropertyTypeExtractor.h @@ -0,0 +1,52 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "internals/SolWrapper.h" +#include "ramses-logic/EPropertyType.h" +#include "internals/TypeData.h" + +namespace ramses::internal +{ + struct InterfaceTypeInfo; + + class PropertyTypeExtractor + { + public: + PropertyTypeExtractor(std::string rootName, EPropertyType rootType); + + // Obtain collected type info + [[nodiscard]] HierarchicalTypeData getExtractedTypeData() const; + + // Used for iteration from rl_(i)pairs functions + [[nodiscard]] std::reference_wrapper getChildReference(size_t childIndex) const; + [[nodiscard]] TypeData getRootTypeData() const; + [[nodiscard]] const std::vector& getNestedExtractors() const; + + + // Lua overloads + [[nodiscard]] std::reference_wrapper index(const sol::object& propertyIndex); + void newIndex(const sol::object& idx, const sol::object& value); + + // Register symbols for type extraction to sol protected environment + static void RegisterTypes(sol::table& environment); + static void UnregisterTypes(sol::table& environment); + + private: + TypeData m_typeData; + + using ChildProperties = std::vector; + + ChildProperties m_children; + + ChildProperties::iterator findChild(const std::string_view name); + void extractPropertiesFromTable(const sol::lua_table& table); + void addField(const std::string& name, const InterfaceTypeInfo& typeInfo); + }; +} diff --git a/client/logic/lib/internals/RamsesHelper.h b/client/logic/lib/internals/RamsesHelper.h new file mode 100644 index 000000000..87806cce1 --- /dev/null +++ b/client/logic/lib/internals/RamsesHelper.h @@ -0,0 +1,56 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/EPropertyType.h" +#include "ramses-framework-api/EDataType.h" +#include + +namespace ramses::internal +{ + static constexpr std::optional ConvertRamsesUniformTypeToPropertyType(ramses::EDataType type) + { + switch (type) + { + case ramses::EDataType::Int32: + return EPropertyType::Int32; + case ramses::EDataType::Float: + return EPropertyType::Float; + case ramses::EDataType::Vector2F: + return EPropertyType::Vec2f; + case ramses::EDataType::Vector3F: + return EPropertyType::Vec3f; + case ramses::EDataType::Vector4F: + return EPropertyType::Vec4f; + case ramses::EDataType::Vector2I: + return EPropertyType::Vec2i; + case ramses::EDataType::Vector3I: + return EPropertyType::Vec3i; + case ramses::EDataType::Vector4I: + return EPropertyType::Vec4i; + + // unsupported property types + case ramses::EDataType::UInt16: + case ramses::EDataType::UInt32: + case ramses::EDataType::Matrix22F: + case ramses::EDataType::Matrix33F: + case ramses::EDataType::Matrix44F: + case ramses::EDataType::TextureSampler2D: + case ramses::EDataType::TextureSampler3D: + case ramses::EDataType::TextureSamplerCube: + case ramses::EDataType::TextureSampler2DMS: + case ramses::EDataType::TextureSamplerExternal: + case ramses::EDataType::ByteBlob: + return std::nullopt; + } + + assert(false); + return std::nullopt; + } +} diff --git a/client/logic/lib/internals/RamsesObjectResolver.cpp b/client/logic/lib/internals/RamsesObjectResolver.cpp new file mode 100644 index 000000000..82d75310d --- /dev/null +++ b/client/logic/lib/internals/RamsesObjectResolver.cpp @@ -0,0 +1,89 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/RamsesObjectResolver.h" + +#include "internals/ErrorReporting.h" + +#include "ramses-client-api/SceneObject.h" +#include "ramses-client-api/Scene.h" +#include "ramses-utils.h" + +#include "fmt/format.h" + +namespace ramses::internal +{ + RamsesObjectResolver::RamsesObjectResolver(ErrorReporting& errorReporting, ramses::Scene& scene) + : m_errors(errorReporting) + , m_scene(scene) + { + } + + ramses::Node* RamsesObjectResolver::findRamsesNodeInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const + { + return findRamsesObjectInScene(logicNodeName, objectId); + } + + ramses::Appearance* RamsesObjectResolver::findRamsesAppearanceInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const + { + return findRamsesObjectInScene(logicNodeName, objectId); + } + + ramses::Camera* RamsesObjectResolver::findRamsesCameraInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const + { + return findRamsesObjectInScene(logicNodeName, objectId); + } + + ramses::RenderPass* RamsesObjectResolver::findRamsesRenderPassInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const + { + return findRamsesObjectInScene(logicNodeName, objectId); + } + + ramses::RenderGroup* RamsesObjectResolver::findRamsesRenderGroupInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const + { + return findRamsesObjectInScene(logicNodeName, objectId); + } + + ramses::SceneObject* RamsesObjectResolver::findRamsesSceneObjectInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const + { + return findRamsesObjectInScene(logicNodeName, objectId); + } + + template + T* RamsesObjectResolver::findRamsesObjectInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const + { + ramses::SceneObject* sceneObject = m_scene.findObjectById(objectId); + + if (sceneObject == nullptr) + { + m_errors.add( + fmt::format("Fatal error during loading from file! Serialized Ramses Logic object '{}' points to a Ramses object (id: {}) which couldn't be found in the provided scene!", + logicNodeName, objectId.getValue()), + nullptr, EErrorType::ContentStateError); + return nullptr; + } + + T* concreteObject = ramses::RamsesUtils::TryConvert(*sceneObject); + if (concreteObject == nullptr) + { + m_errors.add(fmt::format("Fatal error during loading from file! Ramses binding '{}' points to a Ramses scene object (id: {}) which is not of the same type!", + logicNodeName, objectId.getValue()), + nullptr, EErrorType::ContentStateError); + return nullptr; + } + + return concreteObject; + } + + template ramses::Node* RamsesObjectResolver::findRamsesObjectInScene(std::string_view, ramses::sceneObjectId_t) const; + template ramses::Appearance* RamsesObjectResolver::findRamsesObjectInScene(std::string_view, ramses::sceneObjectId_t) const; + template ramses::Camera* RamsesObjectResolver::findRamsesObjectInScene(std::string_view, ramses::sceneObjectId_t) const; + template ramses::RenderPass* RamsesObjectResolver::findRamsesObjectInScene(std::string_view, ramses::sceneObjectId_t) const; + template ramses::RenderGroup* RamsesObjectResolver::findRamsesObjectInScene(std::string_view, ramses::sceneObjectId_t) const; + template ramses::SceneObject* RamsesObjectResolver::findRamsesObjectInScene(std::string_view, ramses::sceneObjectId_t) const; +} diff --git a/client/logic/lib/internals/RamsesObjectResolver.h b/client/logic/lib/internals/RamsesObjectResolver.h new file mode 100644 index 000000000..f755bb1c5 --- /dev/null +++ b/client/logic/lib/internals/RamsesObjectResolver.h @@ -0,0 +1,61 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/RamsesFrameworkTypes.h" +#include + +namespace ramses +{ + class SceneObject; + class Scene; + class Node; + class Appearance; + class Camera; + class RenderPass; + class RenderGroup; +} + +namespace ramses::internal +{ + class ErrorReporting; + + class IRamsesObjectResolver + { + public: + virtual ~IRamsesObjectResolver() = default; + + [[nodiscard]] virtual ramses::Node* findRamsesNodeInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const = 0; + [[nodiscard]] virtual ramses::Appearance* findRamsesAppearanceInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const = 0; + [[nodiscard]] virtual ramses::Camera* findRamsesCameraInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const = 0; + [[nodiscard]] virtual ramses::RenderPass* findRamsesRenderPassInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const = 0; + [[nodiscard]] virtual ramses::RenderGroup* findRamsesRenderGroupInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const = 0; + [[nodiscard]] virtual ramses::SceneObject* findRamsesSceneObjectInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const = 0; + }; + + class RamsesObjectResolver final : public IRamsesObjectResolver + { + public: + explicit RamsesObjectResolver(ErrorReporting& errorReporting, ramses::Scene& scene); + + [[nodiscard]] ramses::Node* findRamsesNodeInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const override; + [[nodiscard]] ramses::Appearance* findRamsesAppearanceInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const override; + [[nodiscard]] ramses::Camera* findRamsesCameraInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const override; + [[nodiscard]] ramses::RenderPass* findRamsesRenderPassInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const override; + [[nodiscard]] ramses::RenderGroup* findRamsesRenderGroupInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const override; + [[nodiscard]] ramses::SceneObject* findRamsesSceneObjectInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const override; + + private: + template + [[nodiscard]] T* findRamsesObjectInScene(std::string_view logicNodeName, ramses::sceneObjectId_t objectId) const; + + ErrorReporting& m_errors; + ramses::Scene& m_scene; + }; +} diff --git a/client/logic/lib/internals/SerializationHelper.h b/client/logic/lib/internals/SerializationHelper.h new file mode 100644 index 000000000..bdd2795e9 --- /dev/null +++ b/client/logic/lib/internals/SerializationHelper.h @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/EPropertyType.h" + +#include "generated/PropertyGen.h" + +#include + +namespace ramses::internal +{ + static std::optional ConvertSerializationTypeToEPropertyType(rlogic_serialization::EPropertyRootType propertyRootType, rlogic_serialization::PropertyValue valueType) + { + switch (propertyRootType) + { + case rlogic_serialization::EPropertyRootType::Struct: + return EPropertyType::Struct; + case rlogic_serialization::EPropertyRootType::Array: + return EPropertyType::Array; + case rlogic_serialization::EPropertyRootType::Primitive: + switch (valueType) + { + case rlogic_serialization::PropertyValue::bool_s: + return EPropertyType::Bool; + case rlogic_serialization::PropertyValue::float_s: + return EPropertyType::Float; + case rlogic_serialization::PropertyValue::vec2f_s: + return EPropertyType::Vec2f; + case rlogic_serialization::PropertyValue::vec3f_s: + return EPropertyType::Vec3f; + case rlogic_serialization::PropertyValue::vec4f_s: + return EPropertyType::Vec4f; + case rlogic_serialization::PropertyValue::int32_s: + return EPropertyType::Int32; + case rlogic_serialization::PropertyValue::int64_s: + return EPropertyType::Int64; + case rlogic_serialization::PropertyValue::vec2i_s: + return EPropertyType::Vec2i; + case rlogic_serialization::PropertyValue::vec3i_s: + return EPropertyType::Vec3i; + case rlogic_serialization::PropertyValue::vec4i_s: + return EPropertyType::Vec4i; + case rlogic_serialization::PropertyValue::string_s: + return EPropertyType::String; + case rlogic_serialization::PropertyValue::NONE: + return std::nullopt; + } + } + return std::nullopt; + } +} diff --git a/client/logic/lib/internals/SerializationMap.h b/client/logic/lib/internals/SerializationMap.h new file mode 100644 index 000000000..6772a7ab5 --- /dev/null +++ b/client/logic/lib/internals/SerializationMap.h @@ -0,0 +1,80 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "generated/PropertyGen.h" +#include "generated/DataArrayGen.h" +#include +#include + +namespace ramses::internal +{ + class PropertyImpl; + + // Remembers flatbuffer offsets for select objects during serialization + class SerializationMap + { + public: + void storePropertyOffset(const PropertyImpl& prop, flatbuffers::Offset offset) + { + Store(&prop, offset, m_properties); + } + + [[nodiscard]] flatbuffers::Offset resolvePropertyOffset(const PropertyImpl& prop) const + { + return Get(&prop, m_properties); + } + + void storeDataArray(uint64_t id, flatbuffers::Offset offset) + { + Store(id, offset, m_dataArrays); + } + + [[nodiscard]] flatbuffers::Offset resolveDataArrayOffset(uint64_t dataArrayId) const + { + return Get(dataArrayId, m_dataArrays); + } + + void storeByteCodeOffset(std::string byteCode, flatbuffers::Offset> offset) + { + m_byteCodeOffsets.emplace(std::move(byteCode), offset); + } + + [[nodiscard]] flatbuffers::Offset> resolveByteCodeOffsetIfFound(const std::string& v) + { + const auto it = m_byteCodeOffsets.find(v); + if (it != m_byteCodeOffsets.cend()) + { + return it->second; + } + return 0; + } + + private: + template + static void Store(Key key, Value value, std::unordered_map& container) + { + static_assert(std::is_trivially_copyable_v && std::is_trivially_copyable_v, "Performance warning"); + assert(container.count(key) == 0 && "one time store only"); + container.emplace(std::move(key), std::move(value)); + } + + template + [[nodiscard]] static Value Get(Key key, const std::unordered_map& container) + { + const auto it = container.find(key); + assert(it != container.cend()); + return it->second; + } + + std::unordered_map> m_properties; + std::unordered_map> m_dataArrays; + std::unordered_map>> m_byteCodeOffsets; + }; +} diff --git a/client/logic/lib/internals/SolHelper.h b/client/logic/lib/internals/SolHelper.h new file mode 100644 index 000000000..046e4674e --- /dev/null +++ b/client/logic/lib/internals/SolHelper.h @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "fmt/format.h" +#include "ramses-logic/EStandardModule.h" +#include "internals/SolWrapper.h" + +#include + +namespace sol_helper +{ + template + static void throwSolException(const S& format_str, ARGS ...args) + { + throw sol::error(fmt::format(format_str, args...)); + } + + static constexpr std::string_view GetSolTypeName(sol::type type) + { + switch (type) + { + case sol::type::none: + return "none"; + case sol::type::lua_nil: + return "nil"; + case sol::type::string: + return "string"; + case sol::type::number: + return "number"; + case sol::type::thread: + return "thread"; + case sol::type::boolean: + return "bool"; + case sol::type::function: + return "function"; + case sol::type::userdata: + return "userdata"; + case sol::type::lightuserdata: + return "lightuserdata"; + case sol::type::table: + return "table"; + case sol::type::poly: + return "poly"; + }; + return "none"; + } +} diff --git a/client/logic/lib/internals/SolState.cpp b/client/logic/lib/internals/SolState.cpp new file mode 100644 index 000000000..c65f3be4d --- /dev/null +++ b/client/logic/lib/internals/SolState.cpp @@ -0,0 +1,192 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/SolState.h" + +#include "ramses-logic/LuaModule.h" +#include "impl/LuaModuleImpl.h" + +#include "internals/LuaCustomizations.h" +#include "internals/WrappedLuaProperty.h" +#include "internals/SolHelper.h" +#include "internals/LuaTypeConversions.h" +#include "internals/EnvironmentProtection.h" + +#include + +namespace ramses::internal +{ + // NOLINTNEXTLINE(performance-unnecessary-value-param) The signature is forced by SOL. Therefore we have to disable this warning. + static int solExceptionHandler(lua_State* L, sol::optional maybe_exception, sol::string_view description) + { + if (maybe_exception) + { + return sol::stack::push(L, description); + } + return sol::stack::top(L); + } + + SolState::SolState() + { + m_safeBaselibSymbols = { + "assert", + "error", + "ipairs", + "next", + "pairs", + "print", + "select", + "tonumber", + "tostring", + "type", + "unpack", + "pcall", + "xpcall", + "_VERSION", + + // Potentially less safe, but allows for advanced Lua use cases + "rawequal", + "rawget", + "rawset", + "setmetatable", + "getmetatable", + }; + + // TODO Violin try to load standard modules on demand + for (auto solLib : SolLibs) + { + m_solState.open_libraries(solLib); + } + + m_solState.set_exception_handler(&solExceptionHandler); + + // TODO Violin only register wrappers to runtime environments, not in the global environment + WrappedLuaProperty::RegisterTypes(m_solState); + + LuaCustomizations::RegisterTypes(m_solState); + } + + sol::load_result SolState::loadScript(std::string_view source, std::string_view scriptName) + { + return m_solState.load(source, std::string(scriptName)); + } + + sol::protected_function_result SolState::loadScriptByteCode(std::string_view byteCode, std::string_view scriptName, sol::environment& env) + { + return m_solState.safe_script(byteCode, env, sol::script_pass_on_error, std::string(scriptName)); + } + + sol::environment SolState::createEnvironment(const StandardModules& stdModules, const ModuleMapping& userModules, bool exposeDebugLogFunctions) + { + sol::environment protectedEnv(m_solState, sol::create); + + protectedEnv["_G"] = protectedEnv; + + mapStandardModules(stdModules, protectedEnv); + + for (const auto& module : userModules) + { + assert(!SolState::IsReservedModuleName(module.first)); + protectedEnv[module.first] = module.second->m_impl.getModule(); + } + + // TODO Violin take a closer look at this, should not be needed + // if using 'modules' call to declare dependencies - resolve it to noop + auto dummyFunc = [](const std::string& /*unused*/) {}; + protectedEnv["modules"] = dummyFunc; + + LuaCustomizations::MapToEnvironment(m_solState, protectedEnv); + + if (exposeDebugLogFunctions) + LuaCustomizations::MapDebugLogFunctions(protectedEnv); + + EnvironmentProtection::AddProtectedEnvironmentTable(protectedEnv, m_solState); + + return protectedEnv; + } + + void SolState::mapStandardModules(const StandardModules& stdModules, sol::environment& env) + { + // Copy base libraries' tables into environment to sandbox them, so that scripts cannot affect each other by modifying them + for (const auto& stdModule : stdModules) + { + // The base module needs special handling because it's not in a named table + if (stdModule == EStandardModule::Base) + { + + for (const auto& name : m_safeBaselibSymbols) + { + env[name] = m_solState[name]; + } + + } + else + { + std::string_view moduleTableName = *GetStdModuleName(stdModule); + const sol::table& moduleAsTable = m_solState[moduleTableName]; + copyTableIntoEnvironment(moduleAsTable, moduleTableName, env); + } + } + } + + void SolState::copyTableIntoEnvironment(const sol::table& table, std::string_view name, sol::environment& env) + { + sol::table copy(m_solState, sol::create); + for (const auto& pair : table) + { + // first is the name of a function in module, second is the function + copy[pair.first] = pair.second; + } + env[name] = std::move(copy); + } + + std::optional SolState::GetStdModuleName(ramses::EStandardModule m) + { + switch (m) + { + case EStandardModule::Base: + // Standard module has no name because it's not loaded in a table but global space + return std::nullopt; + case EStandardModule::String: + return "string"; + case EStandardModule::Table: + return "table"; + case EStandardModule::Math: + return "math"; + case EStandardModule::Debug: + return "debug"; + case EStandardModule::All: + return std::nullopt; + } + return std::nullopt; + } + + bool SolState::IsReservedModuleName(std::string_view name) + { + for (auto m : StdModules) + { + std::optional potentialName = GetStdModuleName(m); + if (potentialName && *potentialName == name) + { + return true; + } + } + + return false; + } + + sol::table SolState::createTable() + { + return m_solState.create_table(); + } + + int SolState::getNumElementsInLuaStack() const + { + return lua_gettop(m_solState.lua_state()); + } +} diff --git a/client/logic/lib/internals/SolState.h b/client/logic/lib/internals/SolState.h new file mode 100644 index 000000000..28e9f8a23 --- /dev/null +++ b/client/logic/lib/internals/SolState.h @@ -0,0 +1,70 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/LuaConfigImpl.h" +#include "internals/SolWrapper.h" + +#include +#include + +namespace ramses::internal +{ + constexpr std::array StdModules = { + ramses::EStandardModule::Base, + ramses::EStandardModule::String, + ramses::EStandardModule::Table, + ramses::EStandardModule::Math, + ramses::EStandardModule::Debug + }; + constexpr std::array SolLibs = { + sol::lib::base, + sol::lib::string, + sol::lib::table, + sol::lib::math, + sol::lib::debug + }; + + static_assert(StdModules.size() == SolLibs.size()); + + class SolState + { + public: + SolState(); + + // Move-able (noexcept); Not copy-able + ~SolState() noexcept = default; + // Not move-able because of the dependency of sol environments to the + // underlying sol state. Enabling move would require to write a custom + // move code which first moves the dependent objects, then the sol state + // (in inverse order of creation) + SolState(SolState&& other) noexcept = delete; + SolState& operator=(SolState&& other) noexcept = delete; + SolState(const SolState& other) = delete; + SolState& operator=(const SolState& other) = delete; + + sol::load_result loadScript(std::string_view source, std::string_view scriptName); + sol::protected_function_result loadScriptByteCode(std::string_view byteCode, std::string_view scriptName, sol::environment& env); + sol::environment createEnvironment(const StandardModules& stdModules, const ModuleMapping& userModules, bool exposeDebugLogFunctions); + void copyTableIntoEnvironment(const sol::table& table, std::string_view name, sol::environment& env); + sol::table createTable(); + + [[nodiscard]] int getNumElementsInLuaStack() const; + + [[nodiscard]] static bool IsReservedModuleName(std::string_view name); + + private: + sol::state m_solState; + // Cached to avoid unnecessary heap allocations + std::vector m_safeBaselibSymbols; + + void mapStandardModules(const StandardModules& stdModules, sol::environment& env); + [[nodiscard]] static std::optional GetStdModuleName(ramses::EStandardModule m); + }; +} diff --git a/client/logic/lib/internals/SolWrapper.h b/client/logic/lib/internals/SolWrapper.h new file mode 100644 index 000000000..ca77c7024 --- /dev/null +++ b/client/logic/lib/internals/SolWrapper.h @@ -0,0 +1,143 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + + +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#pragma GCC diagnostic ignored "-Wdeprecated" +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#pragma GCC diagnostic ignored "-Wcast-align" +#pragma GCC diagnostic ignored "-Wempty-body" +#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wsuggest-override" +#endif + +#if defined(_MSC_VER) +__pragma(warning(push)) +__pragma(warning(disable: 4100)) +#endif + +// TODO (Violin) Workaround for a compiler issue fixed in newer sol. Remove once upgraded +#include +#include "sol/sol.hpp" + +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif + +#if defined(_MSC_VER) +__pragma(warning(pop)) +#endif + +// Make sure sol defines are configured correctly and work uniformly on all compile units which use Sol +// Uses Sol-internal macros to ensure the exact same compiler output, independent of user-configured settings +// Write a comment in the error message why we need (or don't need) the corresponding safety setting + +#if SOL_IS_OFF(SOL_EXCEPTIONS_I_) +#error "We use the default setting (ON)" +#endif + +#if SOL_IS_OFF(SOL_RTTI_I_) +#error "We use the default setting (ON)" +#endif + +#if SOL_IS_OFF(SOL_USE_THREAD_LOCAL_I_) +#error "We use the default setting (ON) - none of the platforms supported by Logic engine should be affected by this" +#endif + +#if SOL_IS_ON(SOL_SAFE_USERTYPE_I_) +#error "We use the default setting (OFF)" +#endif + +#if SOL_IS_ON(SOL_SAFE_REFERENCES_I_) +#error "We use the default setting (OFF)" +#endif + +#if SOL_IS_ON(SOL_SAFE_FUNCTION_CALLS_I_) +#error "We use the default setting (OFF)" +#endif + +#if SOL_IS_ON(SOL_SAFE_FUNCTION_OBJECTS_I_) +#error "We use the default setting (OFF)" +#endif + +#if SOL_IS_OFF(SOL_SAFE_NUMERICS_I_) +#error "Safe numerics are required so that Lua/Sol numbers behave correctly in the logic engine!" +#endif + +#if SOL_IS_ON(SOL_SAFE_GETTER_I_) +#error "We use the default setting (OFF)" +#endif + +// Configured with SOL_SAFE_STACK_CHECK=1 +// this is not documented in Sol, but it looks like it should be +#if SOL_IS_OFF(SOL_SAFE_STACK_CHECK_I_) +#error "Safe stack checks are required so that Lua stack grows correctly when used!" +#endif + +#if SOL_IS_ON(SOL_USE_CXX_LUAJIT_I_) +#error "LuaJIT is not tested yet, thus we expect that SOL_USE_CXX_LUAJIT is off currently" +#endif + +#if SOL_IS_ON(SOL_USE_LUA_HPP_I_) +#error "We use the default setting (OFF)" +#endif + +// Configured with SOL_EXCEPTIONS_ALWAYS_UNSAFE=1 +#if SOL_IS_ON(SOL_PROPAGATE_EXCEPTIONS_I_) +#error "We want to catch and redirect exceptions and pass to to user handler func instead of prapagating them directly through Lua" +#endif + +#if SOL_IS_OFF(SOL_ALIGN_MEMORY_I_) +#error "We use the default setting (ON)" +#endif + +#if SOL_IS_OFF(SOL_USE_COMPATIBILITY_LAYER_I_) +#error "We run in compatibility mode (because of Lua v5.1)" +#endif + +#if SOL_IS_ON(SOL_GET_FUNCTION_POINTER_UNSAFE_I_) +#error "We use the default setting (OFF)" +#endif + +#if SOL_IS_OFF(SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_) +#error "We use the default setting (ON)" +#endif + +#if SOL_IS_OFF(SOL_STD_VARIANT_I_) +#error "We use the default setting (ON)" +#endif + +#if SOL_IS_OFF(SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_) +#error "We use the default setting (ON)" +#endif + +#if SOL_IS_OFF(SOL_USERTYPE_TYPE_BINDING_INFO_I_) +#error "We use the default setting (ON)" +#endif + +#if SOL_IS_ON(SOL_STRINGS_ARE_NUMBERS_I_) +#error "We use the default setting (OFF)" +#endif + +#if SOL_IS_OFF(SOL_NUMBER_PRECISION_CHECKS_I_) +#error "We use the default setting (turned on automatically when SOL_SAFE_NUMERICS is ON)" +#endif + +// this is not documented in Sol, but it looks like it should be +#if SOL_IS_ON(SOL_SAFE_PROXIES_I_) +#error "We use the default setting (OFF)" +#endif + + + diff --git a/client/logic/lib/internals/StdFilesystemWrapper.h b/client/logic/lib/internals/StdFilesystemWrapper.h new file mode 100644 index 000000000..45feae694 --- /dev/null +++ b/client/logic/lib/internals/StdFilesystemWrapper.h @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#if defined(RLOGIC_STD_FILESYSTEM_EXPERIMENTAL) +#include +namespace fs = std::experimental::filesystem; + +#elif defined(RLOGIC_STD_FILESYSTEM_EMULATION) + +// add more emulation methods when needed +#include +#include +#include +#include + +namespace fs +{ + bool is_directory(const std::string& path) + { + struct stat tmp; + return stat(path.c_str(), &tmp) == 0 && S_ISDIR(tmp.st_mode); + } +} + +#else +#include +namespace fs = std::filesystem; +#endif + diff --git a/client/logic/lib/internals/TypeData.cpp b/client/logic/lib/internals/TypeData.cpp new file mode 100644 index 000000000..d782077c6 --- /dev/null +++ b/client/logic/lib/internals/TypeData.cpp @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/TypeData.h" + +namespace ramses::internal +{ + TypeData::TypeData(std::string _name, EPropertyType _type) + : name(std::move(_name)) + , type(_type) + { + } + + bool TypeData::operator!=(const TypeData& other) const + { + return !operator==(other); + } + + bool TypeData::operator==(const TypeData& other) const + { + return name == other.name && type == other.type; + } + + bool HierarchicalTypeData::operator!=(const HierarchicalTypeData& other) const + { + return !operator==(other); + } + + bool HierarchicalTypeData::operator==(const HierarchicalTypeData& other) const + { + return typeData == other.typeData && children == other.children; + } + + HierarchicalTypeData::HierarchicalTypeData(TypeData _typeData, std::vector _children) + : typeData(std::move(_typeData)) + , children(std::move(_children)) + { + + } + +} diff --git a/client/logic/lib/internals/TypeData.h b/client/logic/lib/internals/TypeData.h new file mode 100644 index 000000000..8f319d7d6 --- /dev/null +++ b/client/logic/lib/internals/TypeData.h @@ -0,0 +1,69 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/EPropertyType.h" + +#include +#include +#include +#include +#include + +namespace ramses::internal +{ + struct TypeData + { + TypeData(std::string _name, EPropertyType _type); + + bool operator==(const TypeData& other) const; + bool operator!=(const TypeData& other) const; + + std::string name; + EPropertyType type; + }; + + struct HierarchicalTypeData + { + HierarchicalTypeData(TypeData _typeData, std::vector _children); + + // Treats different ordering of child types as different types + bool operator==(const HierarchicalTypeData& other) const; + bool operator!=(const HierarchicalTypeData& other) const; + + TypeData typeData; + std::vector children; + }; + + inline HierarchicalTypeData MakeType(std::string name, EPropertyType type) + { + return HierarchicalTypeData(TypeData(std::move(name), type), {}); + } + + inline HierarchicalTypeData MakeArray(std::string name, size_t size, EPropertyType type) + { + return HierarchicalTypeData(TypeData(std::move(name), EPropertyType::Array), std::vector(size, HierarchicalTypeData(TypeData("", type), {}))); + } + + inline HierarchicalTypeData MakeStruct(std::string name, std::initializer_list properties) + { + std::vector children; + children.reserve(properties.size()); + std::transform(properties.begin(), properties.end(), std::back_inserter(children), [](const TypeData& propData){return HierarchicalTypeData(propData, {});}); + return HierarchicalTypeData(TypeData(std::move(name), EPropertyType::Struct), std::move(children)); + } + + inline HierarchicalTypeData MakeStruct(std::string name, const std::vector& properties) + { + std::vector children; + children.reserve(properties.size()); + std::transform(properties.begin(), properties.end(), std::back_inserter(children), [](const TypeData& propData) {return HierarchicalTypeData(propData, {}); }); + return HierarchicalTypeData(TypeData(std::move(name), EPropertyType::Struct), std::move(children)); + } +} diff --git a/client/logic/lib/internals/TypeUtils.h b/client/logic/lib/internals/TypeUtils.h new file mode 100644 index 000000000..7a19afc45 --- /dev/null +++ b/client/logic/lib/internals/TypeUtils.h @@ -0,0 +1,138 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/Property.h" +#include "impl/PropertyImpl.h" +#include "ramses-framework-api/DataTypes.h" +#include +#include + +namespace ramses::internal +{ + class TypeUtils + { + public: + // Enum classes should not require range checks, but EPropertyType is marshalled from Lua and from files + // this method should be used to check if the enum is in range + static bool IsValidType(EPropertyType type) + { + switch (type) + { + case EPropertyType::Float: + case EPropertyType::Vec2f: + case EPropertyType::Vec3f: + case EPropertyType::Vec4f: + case EPropertyType::Int32: + case EPropertyType::Int64: + case EPropertyType::Vec2i: + case EPropertyType::Vec3i: + case EPropertyType::Vec4i: + case EPropertyType::String: + case EPropertyType::Bool: + case EPropertyType::Struct: + case EPropertyType::Array: + return true; + default: + return false; + } + } + + static bool IsPrimitiveType(EPropertyType type) + { + assert(IsValidType(type)); + + switch (type) + { + case EPropertyType::Float: + case EPropertyType::Vec2f: + case EPropertyType::Vec3f: + case EPropertyType::Vec4f: + case EPropertyType::Int32: + case EPropertyType::Int64: + case EPropertyType::Vec2i: + case EPropertyType::Vec3i: + case EPropertyType::Vec4i: + case EPropertyType::String: + case EPropertyType::Bool: + return true; + case EPropertyType::Struct: + case EPropertyType::Array: + default: + return false; + } + } + + static bool IsPrimitiveVectorType(EPropertyType type) + { + assert(IsValidType(type)); + + switch (type) + { + case EPropertyType::Vec2f: + case EPropertyType::Vec3f: + case EPropertyType::Vec4f: + case EPropertyType::Vec2i: + case EPropertyType::Vec3i: + case EPropertyType::Vec4i: + return true; + case EPropertyType::Float: + case EPropertyType::Int32: + case EPropertyType::Int64: + case EPropertyType::String: + case EPropertyType::Bool: + case EPropertyType::Struct: + case EPropertyType::Array: + default: + return false; + } + } + + // This method is for better readability in code + static bool CanHaveChildren(EPropertyType type) + { + return !IsPrimitiveType(type); + } + + static size_t ComponentsSizeForPropertyType(EPropertyType propertyType) + { + switch (propertyType) + { + case EPropertyType::Float: + case EPropertyType::Int32: + case EPropertyType::Int64: + return 1u; + case EPropertyType::Vec2f: + case EPropertyType::Vec2i: + return 2u; + case EPropertyType::Vec3f: + case EPropertyType::Vec3i: + return 3u; + case EPropertyType::Vec4f: + case EPropertyType::Vec4i: + return 4u; + case EPropertyType::String: + case EPropertyType::Array: + case EPropertyType::Struct: + case EPropertyType::Bool: + assert(false && "This should never happen"); + } + return 0u; + } + }; + + /// TODO vaclav temporary till these are unified + template struct RlogicTypeToRamsesType { using TYPE = T; }; + template <> struct RlogicTypeToRamsesType { using TYPE = ramses::vec2f; }; + template <> struct RlogicTypeToRamsesType { using TYPE = ramses::vec3f; }; + template <> struct RlogicTypeToRamsesType { using TYPE = ramses::vec4f; }; + template <> struct RlogicTypeToRamsesType { using TYPE = ramses::vec2i; }; + template <> struct RlogicTypeToRamsesType { using TYPE = ramses::vec3i; }; + template <> struct RlogicTypeToRamsesType { using TYPE = ramses::vec4i; }; +} diff --git a/client/logic/lib/internals/UpdateReport.cpp b/client/logic/lib/internals/UpdateReport.cpp new file mode 100644 index 000000000..ffe12b3f3 --- /dev/null +++ b/client/logic/lib/internals/UpdateReport.cpp @@ -0,0 +1,83 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/UpdateReport.h" +#include + +namespace ramses::internal +{ + void UpdateReport::sectionStarted(ETimingSection section) + { + std::optional& sectionTiming = m_sectionStarted[static_cast(section)]; + assert(!sectionTiming); + sectionTiming = Clock::now(); + } + + void UpdateReport::sectionFinished(ETimingSection section) + { + const auto sectionIdx = static_cast(section); + std::optional& sectionTiming = m_sectionStarted[sectionIdx]; + assert(sectionTiming); + m_sectionExecutionTime[sectionIdx] = std::chrono::duration_cast(Clock::now() - *sectionTiming); + sectionTiming.reset(); + } + + void UpdateReport::nodeExecutionStarted(LogicNodeImpl& node) + { + assert(!m_nodeExecutionStarted); + m_nodeExecutionStarted = Clock::now(); + m_nodesExecuted.push_back({ &node, ReportTimeUnits{ 0u } }); + } + + void UpdateReport::nodeExecutionFinished() + { + assert(m_nodeExecutionStarted); + m_nodesExecuted.back().second = std::chrono::duration_cast(Clock::now() - *m_nodeExecutionStarted); + m_nodeExecutionStarted.reset(); + } + + void UpdateReport::nodeSkippedExecution(LogicNodeImpl& node) + { + m_nodesSkippedExecution.push_back(&node); + } + + void UpdateReport::clear() + { + m_nodesExecuted.clear(); + m_nodesSkippedExecution.clear(); + for (auto& s : m_sectionExecutionTime) + s = ReportTimeUnits{ 0u }; + m_activatedLinks = 0u; + + // clear also internals in case update/measure was interrupted due to error + m_nodeExecutionStarted.reset(); + for (auto& s : m_sectionStarted) + s.reset(); + } + + const UpdateReport::LogicNodesTimed& UpdateReport::getNodesExecuted() const + { + return m_nodesExecuted; + } + + const UpdateReport::LogicNodes& UpdateReport::getNodesSkippedExecution() const + { + return m_nodesSkippedExecution; + } + + UpdateReport::ReportTimeUnits UpdateReport::getSectionExecutionTime(ETimingSection section) const + { + return m_sectionExecutionTime[static_cast(section)]; + } + + size_t UpdateReport::getLinkActivations() const + { + return m_activatedLinks; + } + +} diff --git a/client/logic/lib/internals/UpdateReport.h b/client/logic/lib/internals/UpdateReport.h new file mode 100644 index 000000000..726e30b86 --- /dev/null +++ b/client/logic/lib/internals/UpdateReport.h @@ -0,0 +1,63 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include +#include + +namespace ramses::internal +{ + class LogicNodeImpl; + + class UpdateReport + { + public: + using ReportTimeUnits = std::chrono::microseconds; + using LogicNodesTimed = std::vector>; + using LogicNodes = std::vector; + + enum class ETimingSection + { + TotalUpdate = 0, + TopologySort + }; + + void sectionStarted(ETimingSection section); + void sectionFinished(ETimingSection section); + void nodeExecutionStarted(LogicNodeImpl& node); + void nodeExecutionFinished(); + void nodeSkippedExecution(LogicNodeImpl& node); + void linksActivated(size_t activatedLinks); + void clear(); + + [[nodiscard]] const LogicNodesTimed& getNodesExecuted() const; + [[nodiscard]] const LogicNodes& getNodesSkippedExecution() const; + [[nodiscard]] ReportTimeUnits getSectionExecutionTime(ETimingSection section) const; + [[nodiscard]] size_t getLinkActivations() const; + + private: + using Clock = std::chrono::steady_clock; + using TimePoint = std::chrono::time_point; + + LogicNodesTimed m_nodesExecuted; + LogicNodes m_nodesSkippedExecution; + std::array m_sectionExecutionTime = { ReportTimeUnits{ 0 } }; + size_t m_activatedLinks {0u}; + + std::optional m_nodeExecutionStarted; + std::array, 2u> m_sectionStarted; + }; + + inline void UpdateReport::linksActivated(size_t activatedLinks) + { + m_activatedLinks += activatedLinks; + } +} diff --git a/client/logic/lib/internals/ValidationResults.cpp b/client/logic/lib/internals/ValidationResults.cpp new file mode 100644 index 000000000..a7513274d --- /dev/null +++ b/client/logic/lib/internals/ValidationResults.cpp @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/ValidationResults.h" +#include "ramses-logic/LogicNode.h" +#include "impl/LoggerImpl.h" +#include "impl/LogicObjectImpl.h" + +namespace ramses::internal +{ + void ValidationResults::add(std::string warningMessage, const LogicObject* logicObject, EWarningType type) + { + if (logicObject) + { + LOG_WARN("[{}] {}", logicObject->m_impl->getIdentificationString(), warningMessage); + } + else + { + LOG_WARN("{}", warningMessage); + } + + m_warnings.emplace_back(WarningData{ std::move(warningMessage), type, logicObject }); + } + + void ValidationResults::clear() + { + m_warnings.clear(); + } + + const std::vector& ValidationResults::getWarnings() const + { + return m_warnings; + } +} diff --git a/framework/Animation/Animation/test/AnimationDataBindTest.h b/client/logic/lib/internals/ValidationResults.h similarity index 51% rename from framework/Animation/Animation/test/AnimationDataBindTest.h rename to client/logic/lib/internals/ValidationResults.h index 1e9cb2654..08e0248d4 100644 --- a/framework/Animation/Animation/test/AnimationDataBindTest.h +++ b/client/logic/lib/internals/ValidationResults.h @@ -1,29 +1,28 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2012 BMW Car IT GmbH +// Copyright (C) 2022 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#ifndef RAMSES_ANIMATIONDATABINDTEST_H -#define RAMSES_ANIMATIONDATABINDTEST_H +#pragma once -#include "framework_common_gmock_header.h" -#include "gtest/gtest.h" -#include "AnimationDataBindTestUtils.h" +#include "ramses-logic/WarningData.h" -namespace ramses_internal +#include +#include + +namespace ramses::internal { - class AnimationDataBindTest: public testing::Test + class ValidationResults { public: - AnimationDataBindTest() - { - } + void clear(); + void add(std::string warningMessage, const LogicObject* logicObject, EWarningType type); + [[nodiscard]] const std::vector& getWarnings() const; - protected: + private: + std::vector m_warnings; }; } - -#endif diff --git a/client/logic/lib/internals/WrappedLuaProperty.cpp b/client/logic/lib/internals/WrappedLuaProperty.cpp new file mode 100644 index 000000000..2976911a5 --- /dev/null +++ b/client/logic/lib/internals/WrappedLuaProperty.cpp @@ -0,0 +1,600 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/WrappedLuaProperty.h" +#include "internals/SolHelper.h" +#include "internals/LuaTypeConversions.h" +#include "internals/TypeUtils.h" +#include "glm/gtc/type_ptr.hpp" + +#include "impl/PropertyImpl.h" + +namespace ramses::internal +{ + class BadStructAccess : public sol::error + { + public: + BadStructAccess(std::string _fieldName, std::string solError) + : sol::error(std::move(solError)) + , fieldName(std::move(_fieldName)) + { + } + + std::string fieldName; + }; + + WrappedLuaProperty::WrappedLuaProperty(PropertyImpl& propertyToWrap) + : m_wrappedProperty(propertyToWrap) + { + m_wrappedChildProperties.reserve(propertyToWrap.getChildCount()); + + for (size_t i = 0; i < propertyToWrap.getChildCount(); ++i) + { + m_wrappedChildProperties.emplace_back(*propertyToWrap.getChild(i)->m_impl); + } + } + + sol::object WrappedLuaProperty::index(sol::this_state solState, const sol::object& index) const + { + switch (m_wrappedProperty.get().getType()) + { + // Primitive types don't have sub-types and thus can't be indexed (we never return a wrapper of primitive + // type to Lua, instead we resolve it first to a Lua built-in (e.g. string or number) + case EPropertyType::Float: + case EPropertyType::Int32: + case EPropertyType::Int64: + case EPropertyType::String: + case EPropertyType::Bool: + sol_helper::throwSolException("Implementation error!"); + break; + case EPropertyType::Vec2f: + return extractVectorComponent(solState, index); + case EPropertyType::Vec3f: + return extractVectorComponent(solState, index); + case EPropertyType::Vec4f: + return extractVectorComponent(solState, index); + case EPropertyType::Vec2i: + return extractVectorComponent(solState, index); + case EPropertyType::Vec3i: + return extractVectorComponent(solState, index); + case EPropertyType::Vec4i: + return extractVectorComponent(solState, index); + case EPropertyType::Array: + case EPropertyType::Struct: + return resolveChild(solState, resolvePropertyIndex(index)); + } + + assert(false && "Missing type implementation!"); + return sol::lua_nil; + } + + sol::object WrappedLuaProperty::resolveChild(sol::this_state solState, size_t childIndex) const + { + const WrappedLuaProperty& child = m_wrappedChildProperties[childIndex]; + const PropertyImpl& childProperty = child.m_wrappedProperty; + + switch (childProperty.getType()) + { + case EPropertyType::Float: + return sol::make_object(solState, childProperty.getValueAs()); + case EPropertyType::Int32: + return sol::make_object(solState, childProperty.getValueAs()); + case EPropertyType::Int64: + return sol::make_object(solState, childProperty.getValueAs()); + case EPropertyType::String: + return sol::make_object(solState, childProperty.getValueAs()); + case EPropertyType::Bool: + return sol::make_object(solState, childProperty.getValueAs()); + case EPropertyType::Vec2f: + case EPropertyType::Vec3f: + case EPropertyType::Vec4f: + case EPropertyType::Vec2i: + case EPropertyType::Vec3i: + case EPropertyType::Vec4i: + case EPropertyType::Array: + case EPropertyType::Struct: + return sol::make_object(solState, std::ref(child)); + } + + assert(false && "Missing type implementation!"); + return sol::lua_nil; + } + + sol::object WrappedLuaProperty::resolveVectorElement(sol::this_state solState, size_t elementIndex) const + { + const EPropertyType vecType = m_wrappedProperty.get().getType(); + assert(TypeUtils::IsPrimitiveVectorType(vecType)); + assert(elementIndex > 0 && elementIndex <= TypeUtils::ComponentsSizeForPropertyType(vecType)); + const auto index = static_cast(elementIndex) - 1; + + switch (vecType) + { + case EPropertyType::Vec2f: + return sol::make_object(solState, m_wrappedProperty.get().getValueAs::TYPE>()[index]); + case EPropertyType::Vec3f: + return sol::make_object(solState, m_wrappedProperty.get().getValueAs::TYPE>()[index]); + case EPropertyType::Vec4f: + return sol::make_object(solState, m_wrappedProperty.get().getValueAs::TYPE>()[index]); + case EPropertyType::Vec2i: + return sol::make_object(solState, m_wrappedProperty.get().getValueAs::TYPE>()[index]); + case EPropertyType::Vec3i: + return sol::make_object(solState, m_wrappedProperty.get().getValueAs::TYPE>()[index]); + case EPropertyType::Vec4i: + return sol::make_object(solState, m_wrappedProperty.get().getValueAs::TYPE>()[index]); + default: + assert(false && "unexpected"); + return sol::lua_nil; + } + } + + void WrappedLuaProperty::newIndex(const sol::object& index, const sol::object& rhs) + { + if (TypeUtils::IsPrimitiveVectorType(m_wrappedProperty.get().getType())) + { + sol_helper::throwSolException("Error while writing to '{}'. Can't assign individual components of vector types, must assign the whole vector!", m_wrappedProperty.get().getName()); + } + + const size_t childIndex = resolvePropertyIndex(index); + + if (m_wrappedProperty.get().getPropertySemantics() != EPropertySemantics::ScriptOutput) + { + sol_helper::throwSolException("Error while writing to '{}'. Writing input values is not allowed, only outputs!", getChildDebugName(childIndex)); + } + + setChildValue(childIndex, rhs); + } + + void WrappedLuaProperty::setChildValue(size_t index, const sol::object& rhs) + { + WrappedLuaProperty& childProperty = m_wrappedChildProperties[index]; + + if (rhs.get_type() == sol::type::userdata) + { + if (!rhs.is()) + { + // If we ever add other user data objects, should modify this block + // For now, we check the type explicitly before converting for a better user message + sol_helper::throwSolException("Implementation error: Unexpected userdata"); + } + + childProperty.setComplex(rhs.as()); + } + else + { + switch (childProperty.m_wrappedProperty.get().getType()) + { + case EPropertyType::Array: + childProperty.setArray(rhs); + break; + case EPropertyType::Struct: + childProperty.setStruct(rhs); + break; + case EPropertyType::Vec2f: + childProperty.setVectorComponents(rhs); + break; + case EPropertyType::Vec3f: + childProperty.setVectorComponents(rhs); + break; + case EPropertyType::Vec4f: + childProperty.setVectorComponents(rhs); + break; + case EPropertyType::Vec2i: + childProperty.setVectorComponents(rhs); + break; + case EPropertyType::Vec3i: + childProperty.setVectorComponents(rhs); + break; + case EPropertyType::Vec4i: + childProperty.setVectorComponents(rhs); + break; + case EPropertyType::String: + childProperty.setString(rhs); + break; + case EPropertyType::Bool: + childProperty.setBool(rhs); + break; + case EPropertyType::Float: + childProperty.setFloat(rhs); + break; + case EPropertyType::Int32: + childProperty.setInt32(rhs); + break; + case EPropertyType::Int64: + childProperty.setInt64(rhs); + break; + default: + assert(false && "Missing implementation"); + } + } + } + + size_t WrappedLuaProperty::resolvePropertyIndex(const sol::object& propertyIndex) const + { + const EPropertyType propertyType = m_wrappedProperty.get().getType(); + if (propertyType == EPropertyType::Struct) + { + const DataOrError structFieldName = LuaTypeConversions::ExtractSpecificType(propertyIndex); + + if (structFieldName.hasError()) + { + sol_helper::throwSolException("Bad access to property '{}'! {}", m_wrappedProperty.get().getName(), structFieldName.getError()); + } + + for (size_t i = 0; i < m_wrappedChildProperties.size(); ++i) + { + if (m_wrappedChildProperties[i].m_wrappedProperty.get().getName() == structFieldName.getData()) + { + return i; + } + } + + throw BadStructAccess(std::string(structFieldName.getData()), fmt::format("Tried to access undefined struct property '{}'", structFieldName.getData())); + } + + if (propertyType == EPropertyType::Array) + { + const DataOrError maybeUInt = LuaTypeConversions::ExtractSpecificType(propertyIndex); + if (maybeUInt.hasError()) + { + sol_helper::throwSolException("Bad access to property '{}'! {}", m_wrappedProperty.get().getName(), maybeUInt.getError()); + } + const size_t childCount = m_wrappedChildProperties.size(); + const size_t indexAsUInt = maybeUInt.getData(); + if (indexAsUInt == 0 || indexAsUInt > childCount) + { + sol_helper::throwSolException("Index out of range! Expected 0 < index <= {} but received index == {}", childCount, indexAsUInt); + } + return indexAsUInt - 1; + } + + if (TypeUtils::IsPrimitiveVectorType(propertyType)) + { + const DataOrError elementIdx = LuaTypeConversions::ExtractSpecificType(propertyIndex); + if (elementIdx.hasError()) + sol_helper::throwSolException("Bad access to property '{}'! {}", m_wrappedProperty.get().getName(), elementIdx.getError()); + + const size_t numElements = TypeUtils::ComponentsSizeForPropertyType(propertyType); + const size_t indexAsUInt = elementIdx.getData(); + if (indexAsUInt == 0 || indexAsUInt > numElements) + sol_helper::throwSolException("Index out of range! Expected 0 < index <= {} but received index == {}", numElements, indexAsUInt); + + return indexAsUInt; + } + + sol_helper::throwSolException("Implementation error"); + return 0; + } + + void WrappedLuaProperty::setComplex(const WrappedLuaProperty& other) + { + verifyTypeCompatibility(other); + + if (TypeUtils::IsPrimitiveType(m_wrappedProperty.get().getType())) + { + m_wrappedProperty.get().setValue(other.m_wrappedProperty.get().getValue()); + } + else + { + for (size_t i = 0; i < m_wrappedChildProperties.size(); ++i) + { + m_wrappedChildProperties[i].setComplex(other.m_wrappedChildProperties[i]); + } + } + } + + template + sol::object WrappedLuaProperty::extractVectorComponent(sol::this_state solState, const sol::object& index) const + { + assert(TypeUtils::IsPrimitiveVectorType(m_wrappedProperty.get().getType())); + + const DataOrError potentiallyIndex = LuaTypeConversions::ExtractSpecificType(index); + if (potentiallyIndex.hasError()) + { + sol_helper::throwSolException("Only non-negative integers supported as array index type! {}", potentiallyIndex.getError()); + } + const auto indexAsInt = potentiallyIndex.getData(); + static_assert(T::length() > 0); + if (indexAsInt == 0 || indexAsInt > static_cast(T::length())) + { + sol_helper::throwSolException("Bad index '{}', expected 1 <= i <= {}!", indexAsInt, T::length()); + } + + return sol::make_object(solState, m_wrappedProperty.get().getValueAs()[static_cast(indexAsInt) - 1]); + } + + template + void WrappedLuaProperty::setVectorComponents(const sol::object& rhs) + { + static_assert(std::is_same_v || std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || std::is_same_v); + + const DataOrError potentialArrayData = LuaTypeConversions::ExtractArray(rhs); + + if (potentialArrayData.hasError()) + { + sol_helper::throwSolException("Error while assigning output Vec{} property '{}'. {}", + T::length(), m_wrappedProperty.get().getName(), + potentialArrayData.getError()); + } + + static_assert(T::length() >= 2 && T::length() <= 4); + + if constexpr (T::length() == 2) + { + m_wrappedProperty.get().setValue(glm::make_vec2(potentialArrayData.getData().data())); + } + else if constexpr (T::length() == 3) + { + m_wrappedProperty.get().setValue(glm::make_vec3(potentialArrayData.getData().data())); + } + else if constexpr (T::length() == 4) + { + m_wrappedProperty.get().setValue(glm::make_vec4(potentialArrayData.getData().data())); + } + } + + // Overrides the '#' operator in Lua (sol3 template substitution) + size_t WrappedLuaProperty::size() const + { + switch (m_wrappedProperty.get().getType()) + { + case EPropertyType::Array: + case EPropertyType::Struct: + return m_wrappedChildProperties.size(); + case EPropertyType::Vec2f: + case EPropertyType::Vec2i: + return 2u; + case EPropertyType::Vec3f: + case EPropertyType::Vec3i: + return 3u; + case EPropertyType::Vec4f: + case EPropertyType::Vec4i: + return 4u; + // This is unreachable code (Lua handles size of primitive types) + case EPropertyType::Float: + case EPropertyType::Int32: + case EPropertyType::Int64: + case EPropertyType::Bool: + case EPropertyType::String: + break; + } + + assert(false && "Unreachable code!"); + return 0u; + } + + void WrappedLuaProperty::badTypeAssignment(const sol::type rhsType) + { + sol_helper::throwSolException("Assigning {} to '{}' output '{}'!", + sol_helper::GetSolTypeName(rhsType), + GetLuaPrimitiveTypeName(m_wrappedProperty.get().getType()), + m_wrappedProperty.get().getName()); + } + + void WrappedLuaProperty::setInt32(const sol::object& rhs) + { + if (rhs.get_type() != sol::type::number) + { + badTypeAssignment(rhs.get_type()); + } + + const DataOrError potentiallyInt32 = LuaTypeConversions::ExtractSpecificType(rhs); + if (potentiallyInt32.hasError()) + { + sol_helper::throwSolException("Error during assignment of property '{}'! {}", + m_wrappedProperty.get().getName(), + potentiallyInt32.getError()); + } + m_wrappedProperty.get().setValue(potentiallyInt32.getData()); + } + + void WrappedLuaProperty::setInt64(const sol::object& rhs) + { + if (rhs.get_type() != sol::type::number) + { + badTypeAssignment(rhs.get_type()); + } + + const DataOrError potentiallyInt64 = LuaTypeConversions::ExtractSpecificType(rhs); + if (potentiallyInt64.hasError()) + { + sol_helper::throwSolException("Error during assignment of property '{}'! {}", + m_wrappedProperty.get().getName(), + potentiallyInt64.getError()); + } + m_wrappedProperty.get().setValue(potentiallyInt64.getData()); + } + + void WrappedLuaProperty::setFloat(const sol::object& rhs) + { + if (rhs.get_type() != sol::type::number) + { + badTypeAssignment(rhs.get_type()); + } + + const DataOrError potentiallyFloat = LuaTypeConversions::ExtractSpecificType(rhs); + if (potentiallyFloat.hasError()) + { + sol_helper::throwSolException("Error during assignment of property '{}'! {}", + m_wrappedProperty.get().getName(), + potentiallyFloat.getError()); + } + m_wrappedProperty.get().setValue(potentiallyFloat.getData()); + } + + void WrappedLuaProperty::setString(const sol::object& rhs) + { + if (!rhs.is()) + { + badTypeAssignment(rhs.get_type()); + } + + m_wrappedProperty.get().setValue(rhs.as()); + } + + void WrappedLuaProperty::setBool(const sol::object& rhs) + { + if (!rhs.is()) + { + badTypeAssignment(rhs.get_type()); + } + + m_wrappedProperty.get().setValue(rhs.as()); + } + + void WrappedLuaProperty::setStruct(const sol::object& rhs) + { + const std::optional potentialLuaTable = LuaTypeConversions::ExtractLuaTable(rhs); + if (!potentialLuaTable) + { + sol_helper::throwSolException("Unexpected type ({}) while assigning value of struct field '{}' (expected a table or another struct)!", + sol_helper::GetSolTypeName(rhs.get_type()), + m_wrappedProperty.get().getName()); + } + + const sol::lua_table& solTable = *potentialLuaTable; + + const size_t expectedTableEntries = m_wrappedChildProperties.size(); + + // Collect values first before applying to avoid modify-on-iteration (causes stack overflows) + std::vector childValuesOrderedByIndex(expectedTableEntries); + + size_t actualTableEntries = 0u; + for (const auto& tableEntry : solTable) + { + size_t childIndex = 0; + try { + childIndex = resolvePropertyIndex(tableEntry.first); + } + catch(const BadStructAccess& badAccess){ + sol_helper::throwSolException("Unexpected property '{}' while assigning values to struct '{}'!", badAccess.fieldName, m_wrappedProperty.get().getName()); + } + + // Sanity check, not possible have two properties resolve to the same field index + assert (childValuesOrderedByIndex[childIndex] == sol::lua_nil); + childValuesOrderedByIndex[childIndex] = tableEntry.second; + ++actualTableEntries; + + if (actualTableEntries > expectedTableEntries) + { + sol_helper::throwSolException("Element size mismatch when assigning struct property '{}'! Expected: {} entries, received more", + m_wrappedProperty.get().getName(), + expectedTableEntries); + } + } + + for (size_t i = 0; i < childValuesOrderedByIndex.size(); ++i) + { + if (childValuesOrderedByIndex[i] == sol::lua_nil) + { + sol_helper::throwSolException("Error while assigning struct '{}', expected a value for property '{}' but found none!", + m_wrappedProperty.get().getName(), + m_wrappedChildProperties[i].getWrappedProperty().getName() + ); + } + + setChildValue(i, childValuesOrderedByIndex[i]); + } + } + + void WrappedLuaProperty::setArray(const sol::object& rhs) + { + if (!rhs.is()) + { + sol_helper::throwSolException("Unexpected type ({}) while assigning value of array field '{}' (expected a table or another array)!", + sol_helper::GetSolTypeName(rhs.get_type()), + m_wrappedProperty.get().getName()); + } + + const sol::lua_table& table = rhs.as(); + + for (size_t i = 1u; i <= m_wrappedChildProperties.size(); ++i) + { + const sol::object& field = table[i]; + + if (field == sol::lua_nil) + { + sol_helper::throwSolException("Error during assignment of array property '{}'! Expected a value at index {}", + m_wrappedProperty.get().getName(), i); + } + + // Convert to C+style index by subtracting 1 + setChildValue(i-1, field); + } + + // According to Lua semantics, table size is N iff table[N+1] is nil -> this check mimics that semantics + const sol::object potentiallySuperfluousField = table[m_wrappedChildProperties.size()+1]; + if (potentiallySuperfluousField != sol::lua_nil) + { + sol_helper::throwSolException("Element size mismatch when assigning array property '{}'! Expected array size: {}", + m_wrappedProperty.get().getName(), + m_wrappedChildProperties.size()); + } + + } + + void WrappedLuaProperty::RegisterTypes(sol::state& state) + { + state.new_usertype("WrappedLuaProperty", + sol::meta_method::new_index, &WrappedLuaProperty::newIndex, + sol::meta_method::index, &WrappedLuaProperty::index); + } + + std::string WrappedLuaProperty::getChildDebugName(size_t childIndex) const + { + if (m_wrappedProperty.get().getType() == EPropertyType::Struct) + { + return std::string(m_wrappedChildProperties[childIndex].m_wrappedProperty.get().getName()); + } + + // Convert to Lua-style index (+1) + return fmt::format("idx: {}", childIndex + 1); + } + + void WrappedLuaProperty::verifyTypeCompatibility(const WrappedLuaProperty& other) const + { + const EPropertyType myType = m_wrappedProperty.get().getType() ; + if (myType != other.m_wrappedProperty.get().getType()) + { + sol_helper::throwSolException("Can't assign property '{}' (type {}) to property '{}' (type {})!", + other.m_wrappedProperty.get().getName(), + GetLuaPrimitiveTypeName(other.m_wrappedProperty.get().getType()), + m_wrappedProperty.get().getName(), + GetLuaPrimitiveTypeName(myType)); + } + + if (m_wrappedChildProperties.size() != other.m_wrappedChildProperties.size()) + { + sol_helper::throwSolException("Can't assign property '{}' (#fields={}) to property '{}' (#fields={})!", + other.m_wrappedProperty.get().getName(), + other.m_wrappedChildProperties.size(), + m_wrappedProperty.get().getName(), + m_wrappedChildProperties.size()); + } + + // Verify struct fields recursively + if (myType == EPropertyType::Struct) + { + for (size_t i = 0; i < m_wrappedChildProperties.size(); ++i) + { + m_wrappedChildProperties[i].verifyTypeCompatibility(other.m_wrappedChildProperties[i]); + } + } + + // Verify first array element, assuming arrays are homogeneous (ensured during creation) + if (myType == EPropertyType::Array) + { + m_wrappedChildProperties[0].verifyTypeCompatibility(other.m_wrappedChildProperties[0]); + } + } + + const PropertyImpl& WrappedLuaProperty::getWrappedProperty() const + { + return m_wrappedProperty.get(); + } +} diff --git a/client/logic/lib/internals/WrappedLuaProperty.h b/client/logic/lib/internals/WrappedLuaProperty.h new file mode 100644 index 000000000..f58efffae --- /dev/null +++ b/client/logic/lib/internals/WrappedLuaProperty.h @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "internals/SolWrapper.h" + +#include "impl/PropertyImpl.h" +#include "internals/SolState.h" + +namespace ramses +{ + class Property; +} + +namespace ramses::internal +{ + // This class provides a Lua-like interface to the logic engine types + class WrappedLuaProperty + { + public: + explicit WrappedLuaProperty(PropertyImpl& propertyToWrap); + ~WrappedLuaProperty() = default; + + // Non-copyable, move-able + WrappedLuaProperty(WrappedLuaProperty && other) noexcept = default; + WrappedLuaProperty& operator=(WrappedLuaProperty && other) noexcept = default; + WrappedLuaProperty(const WrappedLuaProperty& other) = delete; + WrappedLuaProperty& operator=(const WrappedLuaProperty& other) = delete; + + // Interface metamethods used by Lua + // Called on 'obj.index = rhs' + void newIndex(const sol::object& index, const sol::object& rhs); + // Called on 'X = obj.index' + [[nodiscard]] sol::object index(sol::this_state solState, const sol::object& index) const; + // Called on '#obj' + [[nodiscard]] size_t size() const; + [[nodiscard]] sol::object resolveChild(sol::this_state solState, size_t childIndex) const; + [[nodiscard]] sol::object resolveVectorElement(sol::this_state solState, size_t elementIndex) const; + [[nodiscard]] size_t resolvePropertyIndex(const sol::object& propertyIndex) const; + + [[nodiscard]] const PropertyImpl& getWrappedProperty() const; + + // Register symbols for type extraction to sol state globally + static void RegisterTypes(sol::state& state); + + private: + std::reference_wrapper m_wrappedProperty; + std::vector m_wrappedChildProperties; + + template + [[nodiscard]] sol::object extractVectorComponent(sol::this_state solState, const sol::object& index) const; + template + void setVectorComponents(const sol::object& rhs); + + void setChildValue(size_t index, const sol::object& rhs); + void setComplex(const WrappedLuaProperty& other); + + void setStruct(const sol::object& rhs); + void setArray(const sol::object& rhs); + void setInt32(const sol::object& rhs); + void setInt64(const sol::object& rhs); + void setFloat(const sol::object& rhs); + void setString(const sol::object& rhs); + void setBool(const sol::object& rhs); + + [[nodiscard]] std::string getChildDebugName(size_t childIndex) const; + void verifyTypeCompatibility(const WrappedLuaProperty& other) const; + void badTypeAssignment(const sol::type rhsType); + }; +} diff --git a/scripts/integration_tests/framework-self-tests/test_foo.py b/client/logic/tools/CMakeLists.txt similarity index 79% rename from scripts/integration_tests/framework-self-tests/test_foo.py rename to client/logic/tools/CMakeLists.txt index 68fe64bc3..278665d1c 100644 --- a/scripts/integration_tests/framework-self-tests/test_foo.py +++ b/client/logic/tools/CMakeLists.txt @@ -6,6 +6,8 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. # ------------------------------------------------------------------------- -# dummy test to ensure tests work -def test_dummy(): - assert 1 == 1 +add_subdirectory(ramses-logic-viewer) + +if(ramses-sdk_BUILD_TESTS) + add_subdirectory(ramses-logic-viewer-test) +endif() diff --git a/client/logic/tools/ramses-logic-viewer-test/CMakeLists.txt b/client/logic/tools/ramses-logic-viewer-test/CMakeLists.txt new file mode 100644 index 000000000..3c46d05e7 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer-test/CMakeLists.txt @@ -0,0 +1,68 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2022 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +find_package(ImageMagick COMPONENTS compare) + +if(ramses-sdk_USE_IMAGEMAGICK AND NOT ImageMagick_FOUND) + message(FATAL_ERROR "ImageMagick compare not found, but required (ramses-sdk_USE_IMAGEMAGICK=ON)") +endif() + +add_executable(ramses-logic-viewer-unittests + LogicViewerTestBase.h + LogicViewerTestBase.cpp + LogicViewerTest.cpp + LogicViewerLuaTest.cpp +) + +target_link_libraries(ramses-logic-viewer-unittests + PRIVATE + ramses-logic-viewer-lib + ramses-client + ramses-gmock + ramses-gmock-main + ) + +target_include_directories(ramses-logic-viewer-unittests + PRIVATE + $ + $ +) + +MakeTestFromTarget( + TARGET ramses-logic-viewer-unittests + SUFFIX UNITTEST) + +folderizeTarget(ramses-logic-viewer-unittests) + +if(ramses-sdk_USE_IMAGEMAGICK AND ImageMagick_FOUND) + createModule( + NAME ramses-logic-viewer-swrast-tests + TYPE BINARY + ENABLE_INSTALL ${ramses-sdk_ENABLE_INSTALL} + INCLUDE_PATHS ${PROJECT_SOURCE_DIR}/client/logic/tools/ramses-logic-viewer + ${PROJECT_SOURCE_DIR}/client/logic/unittests/shared + SRC_FILES LogicViewerAppTest.cpp + DEPENDENCIES ramses-logic-viewer-gui-lib + ramses-shared-lib + ramses-gmock + ramses-gmock-main + ) + + if(TARGET ramses-logic-viewer-swrast-tests) + target_compile_definitions(ramses-logic-viewer-swrast-tests + PRIVATE MAGICK_COMPARE="${ImageMagick_compare_EXECUTABLE}" + ) + + MakeTestFromTarget( + TARGET ramses-logic-viewer-swrast-tests + SUFFIX RNDSANDWICHTEST_SWRAST) + + add_custom_command(TARGET ramses-logic-viewer-swrast-tests PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/res/ ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/unittests) + endif() +endif() diff --git a/client/logic/tools/ramses-logic-viewer-test/LogicViewerAppTest.cpp b/client/logic/tools/ramses-logic-viewer-test/LogicViewerAppTest.cpp new file mode 100644 index 000000000..86bf40b1f --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer-test/LogicViewerAppTest.cpp @@ -0,0 +1,1358 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-sdk-build-config.h" +#include "LogicViewerGuiApp.h" +#include "LogicViewerHeadlessApp.h" +#include "LogicViewer.h" +#include "LogicViewerSettings.h" +#include "gmock/gmock.h" +#include "RamsesTestUtils.h" +#include "WithTempDirectory.h" +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesRenderGroupBindingElements.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/SkinBinding.h" +#include "ramses-client.h" +#include "ImguiClientHelper.h" +#include "fmt/format.h" +#include "CLI/CLI.hpp" +#include +#include +#include + +const auto defaultLuaFile = R"(function default() + --Interfaces + rlogic.interfaces["myInterface"]["IN"]["paramFloat"].value = 0 + --Scripts + rlogic.scripts["allTypesScript"]["IN"]["paramBool"].value = false + rlogic.scripts["allTypesScript"]["IN"]["paramFloat"].value = 0 + rlogic.scripts["allTypesScript"]["IN"]["paramInt32"].value = 0 + rlogic.scripts["allTypesScript"]["IN"]["paramInt64"].value = 0 + rlogic.scripts["allTypesScript"]["IN"]["paramString"].value = '' + rlogic.scripts["allTypesScript"]["IN"]["paramVec2f"].value = { 0, 0 } + rlogic.scripts["allTypesScript"]["IN"]["paramVec2i"].value = { 0, 0 } + rlogic.scripts["allTypesScript"]["IN"]["paramVec3f"].value = { 0, 0, 0 } + rlogic.scripts["allTypesScript"]["IN"]["paramVec3i"].value = { 0, 0, 0 } + rlogic.scripts["allTypesScript"]["IN"]["paramVec4f"].value = { 0, 0, 0, 0 } + rlogic.scripts["allTypesScript"]["IN"]["paramVec4i"].value = { 0, 0, 0, 0 } + rlogic.scripts["allTypesScript"]["IN"]["struct"]["nested"]["data1"].value = '' + rlogic.scripts["allTypesScript"]["IN"]["struct"]["nested"]["data2"].value = 0 + --Node bindings + rlogic.nodeBindings["myNode"]["IN"]["visibility"].value = true + rlogic.nodeBindings["myNode"]["IN"]["translation"].value = { 0, 0, 0 } + rlogic.nodeBindings["myNode"]["IN"]["scaling"].value = { 1, 1, 1 } + rlogic.nodeBindings["myNode"]["IN"]["enabled"].value = true + --Appearance bindings + rlogic.appearanceBindings["myAppearance"]["IN"]["green"].value = 0 + rlogic.appearanceBindings["myAppearance"]["IN"]["blue"].value = 0 + --Camera bindings + rlogic.cameraBindings["myCamera"]["IN"]["viewport"]["offsetX"].value = 0 + rlogic.cameraBindings["myCamera"]["IN"]["viewport"]["offsetY"].value = 0 + rlogic.cameraBindings["myCamera"]["IN"]["viewport"]["width"].value = 800 + rlogic.cameraBindings["myCamera"]["IN"]["viewport"]["height"].value = 800 + rlogic.cameraBindings["myCamera"]["IN"]["frustum"]["nearPlane"].value = 0.1 + rlogic.cameraBindings["myCamera"]["IN"]["frustum"]["farPlane"].value = 100 + rlogic.cameraBindings["myCamera"]["IN"]["frustum"]["fieldOfView"].value = 20 + rlogic.cameraBindings["myCamera"]["IN"]["frustum"]["aspectRatio"].value = 1 + --RenderPass bindings + rlogic.renderPassBindings["myRenderPass"]["IN"]["enabled"].value = true + rlogic.renderPassBindings["myRenderPass"]["IN"]["renderOrder"].value = 0 + rlogic.renderPassBindings["myRenderPass"]["IN"]["clearColor"].value = { 0, 0, 0, 1 } + rlogic.renderPassBindings["myRenderPass"]["IN"]["renderOnce"].value = false + --RenderGroup bindings + rlogic.renderGroupBindings["myRenderGroup"]["IN"]["renderOrders"]["myMeshNode"].value = 0 + --MeshNode bindings + rlogic.meshNodeBindings["myMeshNode"]["IN"]["vertexOffset"].value = 0 + rlogic.meshNodeBindings["myMeshNode"]["IN"]["indexOffset"].value = 0 + rlogic.meshNodeBindings["myMeshNode"]["IN"]["indexCount"].value = 3 + rlogic.meshNodeBindings["myMeshNode"]["IN"]["instanceCount"].value = 1 + --Anchor points + --Skin bindings +end + + +defaultView = { + name = "Default", + description = "", + update = function(time_ms) + default() + end +} + +rlogic.views = {defaultView} + +-- sample test function for automated image base tests +-- can be executed by command line parameter --exec=test_default +function test_default() + -- modify properties + default() + -- stores a screenshot (relative to the working directory) + rlogic.screenshot("test_default.png") +end +)"; + +const auto iniFile = R"( +[Window][Logic Viewer (FeatureLevel 01)] +Pos=0,0 +Size=540,720 +Collapsed=0 + +[LogicViewerGui][Settings] +ShowWindow=1 +ShowInterfaces=1 +ShowScripts=1 +ShowAnimationNodes=1 +ShowTimerNodes=1 +ShowDataArrays=1 +ShowRamsesBindings=1 +ShowUpdateReport=0 +ShowLinkedInputs=1 +ShowOutputs=1 +LuaPreferObjectIds=0 +LuaPreferIdentifiers=0 +ShowDisplaySettings=0 +)"; + +class UI +{ +public: + // ui positions / sizes (in pixels) + const int32_t fontHeight = 13; + const int32_t padding = 3; + const int32_t spacing = 4; + const int32_t titleBar = fontHeight + 2 * padding; + const int32_t buttonHeight = fontHeight + 2 * padding + spacing; + const int32_t smallButtonHeight = fontHeight + spacing; + const int32_t hline = 4; + const int32_t yMiddle = padding + fontHeight / 2; // half height of a button + + void setup(const ramses::LogicViewerSettings* settings) + { + m_settings = settings; + } + + [[nodiscard]] int32_t interfaces() const + { + // there's an extra line in the view section if update report is enabled + const bool showUpdateReport = m_settings ? m_settings->showUpdateReport : false; + return 86 + yMiddle + (showUpdateReport ? 20 : 0); + } + + [[nodiscard]] int32_t scripts() const + { + return interfaces() + buttonHeight; + } + + [[nodiscard]] int32_t animationNodes() const + { + return scripts() + buttonHeight; + } + + [[nodiscard]] int32_t timerNodes() const + { + return animationNodes() + buttonHeight; + } + + [[nodiscard]] int32_t dataArrays() const + { + return timerNodes() + buttonHeight; + } + + [[nodiscard]] int32_t appearanceBindings() const + { + return dataArrays() + buttonHeight; + } + + [[nodiscard]] int32_t nodeBindings() const + { + return appearanceBindings() + buttonHeight; + } + + [[nodiscard]] int32_t cameraBindings() const + { + return nodeBindings() + buttonHeight; + } + + [[nodiscard]] int32_t renderPassBindings() const + { + return cameraBindings() + buttonHeight; + } + + [[nodiscard]] int32_t renderGroupBindings() const + { + return renderPassBindings() + buttonHeight; + } + + [[nodiscard]] int32_t meshNodeBindings() const + { + return renderGroupBindings() + buttonHeight; + } + + [[nodiscard]] int32_t anchorPoints() const + { + return meshNodeBindings() + buttonHeight; + } + + [[nodiscard]] int32_t skinBindings() const + { + return anchorPoints() + buttonHeight; + } + + [[nodiscard]] int32_t displaySettings() const + { + const bool showUpdateReport = m_settings ? m_settings->showUpdateReport : false; + return updateReport() + (showUpdateReport ? buttonHeight : 0); + } + + [[nodiscard]] int32_t updateReport() const + { + return skinBindings() + buttonHeight; + } + +private: + const ramses::LogicViewerSettings* m_settings = nullptr; +}; + +std::string rtrim(const std::string& str) +{ + const auto it = std::find_if(str.rbegin(), str.rend(), [](unsigned char ch) { return std::isspace(ch) == 0; }); + return std::string(str.begin(), it.base()); +} + + +namespace ramses::internal +{ + const char* const logicFile = "ALogicViewerAppTest.rlogic"; + const char* const ramsesFile = "ALogicViewerAppTest.ramses"; + const char* const luaFile = "ALogicViewerAppTest.lua"; + + class LogHandler + { + public: + void add(ramses::ELogLevel level, const std::string& context, const std::string& msg) + { + std::lock_guard guard(m_mutex); + m_log.push_back({level, context, msg}); + } + + void clear() + { + std::lock_guard guard(m_mutex); + m_log.clear(); + } + + [[nodiscard]] size_t find(const std::string& token) + { + std::lock_guard guard(m_mutex); + size_t count = 0u; + for (const auto& entry : m_log) + { + if (entry.msg.find(token) != std::string::npos) + { + ++count; + } + } + return count; + } + + private: + struct LogEntry + { + ramses::ELogLevel level; + std::string context; + std::string msg; + }; + std::mutex m_mutex; + std::list m_log; + }; + + + template + class ALogicViewerAppBase + { + public: + ALogicViewerAppBase() + { + createLogicFile(); + [[maybe_unused]] auto status = m_scene.scene->saveToFile(ramsesFile, true); + auto handler = [&](ramses::ELogLevel level, const std::string& context, const std::string& msg) { m_log.add(level, context, msg); }; + ramses::RamsesFramework::SetLogHandler(handler); + } + + virtual ~ALogicViewerAppBase() + { + ramses::RamsesFramework::SetLogHandler(ramses::LogHandlerFunc()); + } + + void createLogicFile() + { + LogicEngine engine{ ramses::EFeatureLevel_Latest }; + + auto* interface = engine.createLuaInterface(R"( + function interface(IN,OUT) + IN.paramFloat = Type:Float() + end + )", "myInterface"); + + auto* script = engine.createLuaScript(R"( + function interface(IN,OUT) + IN.paramFloat = Type:Float() + OUT.paramVec3f = Type:Vec3f() + end + + function run(IN,OUT) + OUT.paramVec3f = {0, 0, IN.paramFloat} + end + )", {}, "myScript"); + + engine.createLuaScript(R"( + function interface(IN,OUT) + IN.paramBool = Type:Bool() + IN.paramInt32 = Type:Int32() + IN.paramInt64 = Type:Int64() + IN.paramFloat = Type:Float() + IN.paramString = Type:String() + IN.paramVec2f = Type:Vec2f() + IN.paramVec3f = Type:Vec3f() + IN.paramVec4f = Type:Vec4f() + IN.paramVec2i = Type:Vec2i() + IN.paramVec3i = Type:Vec3i() + IN.paramVec4i = Type:Vec4i() + IN.array = Type:Array(5, Type:Float()) + IN.struct = { + nested = { + data1 = Type:String(), + data2 = Type:Int32() + } + } + OUT.paramBool = Type:Bool() + OUT.paramInt32 = Type:Int32() + OUT.paramInt64 = Type:Int64() + OUT.paramFloat = Type:Float() + OUT.paramString = Type:String() + OUT.paramVec2f = Type:Vec2f() + OUT.paramVec3f = Type:Vec3f() + OUT.paramVec4f = Type:Vec4f() + OUT.paramVec2i = Type:Vec2i() + OUT.paramVec3i = Type:Vec3i() + OUT.paramVec4i = Type:Vec4i() + OUT.array = Type:Array(5, Type:Float()) + OUT.struct = { + nested = { + data1 = Type:String(), + data2 = Type:Int32() + } + } + end + function run(IN,OUT) + OUT.paramBool = IN.paramBool + OUT.paramInt32 = IN.paramInt32 + OUT.paramInt64 = IN.paramInt64 + OUT.paramFloat = IN.paramFloat + OUT.paramString = IN.paramString + OUT.paramVec2f = IN.paramVec2f + OUT.paramVec3f = IN.paramVec3f + OUT.paramVec4f = IN.paramVec4f + OUT.paramVec2i = IN.paramVec2i + OUT.paramVec3i = IN.paramVec3i + OUT.paramVec4i = IN.paramVec4i + OUT.array = IN.array + OUT.struct = IN.struct + end + )", {}, "allTypesScript"); + + auto* nodeBinding = engine.createRamsesNodeBinding(*m_scene.meshNode, ramses::ERotationType::Euler_XYZ, "myNode"); + + engine.createRamsesAppearanceBinding(*m_scene.appearance, "myAppearance"); + engine.createRamsesCameraBinding(*m_scene.camera, "myCamera"); + engine.createRamsesRenderPassBinding(*m_scene.renderPass, "myRenderPass"); + ramses::RamsesRenderGroupBindingElements rgElements; + rgElements.addElement(*m_scene.meshNode, "myMeshNode"); + engine.createRamsesRenderGroupBinding(*m_scene.renderGroup, rgElements, "myRenderGroup"); + engine.createRamsesMeshNodeBinding(*m_scene.meshNode, "myMeshNode"); + + engine.createTimerNode("myTimer"); + + ramses::DataArray* animTimestamps = engine.createDataArray(std::vector{ 0.f, 0.5f, 1.f, 1.5f }); // will be interpreted as seconds + ramses::DataArray* animKeyframes = engine.createDataArray(std::vector{ {0.f, 0.f, 0.f}, {0.f, 0.f, 180.f}, {0.f, 0.f, 100.f}, {0.f, 0.f, 360.f} }); + const ramses::AnimationChannel stepAnimChannel { "rotationZstep", animTimestamps, animKeyframes, ramses::EInterpolationType::Step }; + ramses::AnimationNodeConfig config; + config.addChannel(stepAnimChannel); + engine.createAnimationNode(config, "myAnimation"); + + engine.link(*interface->getOutputs()->getChild("paramFloat"), *script->getInputs()->getChild("paramFloat")); + engine.link(*script->getOutputs()->getChild("paramVec3f"), *nodeBinding->getInputs()->getChild("rotation")); + + engine.update(); + + ramses::SaveFileConfig noValidationConfig; + noValidationConfig.setValidationEnabled(false); + engine.saveToFile(logicFile, noValidationConfig); + } + + void createApp(const std::vector& argsList = {}) + { + std::vector args; + args.resize(argsList.size()); + std::transform(argsList.begin(), argsList.end(), args.begin(), [](const auto& str) { return str.c_str(); }); + args.insert(args.begin(), "viewer"); // 1st is executable name to comply with standard application args + m_app = std::make_unique(static_cast(args.size()), args.data()); + } + + [[nodiscard]] bool runUntil(const std::string& message) + { + const size_t maxCycles = 200u; // timeout:3.2s (200 * 16ms) + bool running = true; + for (size_t i = 0u; running && i < maxCycles; ++i) + { + running = m_app->doOneLoop(); + const auto count = m_log.find(message); + m_log.clear(); + if (count > 0) + { + return true; + } + } + return false; + } + + static void SaveFile(std::string_view text, std::string_view filename = luaFile) + { + std::ofstream fileStream(filename.data(), std::ofstream::binary); + if (!fileStream.is_open()) + { + throw std::runtime_error("filestream not open"); + } + fileStream << text; + if (fileStream.bad()) + { + throw std::runtime_error("bad filestream"); + } + } + + [[nodiscard]] static testing::AssertionResult CompareImage(std::string_view actual, std::string_view expected, const float tolerance = 0.1f) + { + const std::string diffDir = "../res/unittests/diff/"; + const std::string resourceDir = "../res/unittests/"; + const std::string expectedPath = resourceDir + expected.data(); + const std::string filenameOut = diffDir + "OUT_" + expected.data(); + const std::string filenameDiff = diffDir + "DIFF_" + expected.data(); + const std::string filenameStderr = diffDir + "STDERR_" + expected.data(); + const std::string tempDiff = "diff.png"; + const std::string tempStderr = "diff.txt"; + + if (!fs::exists(actual)) + { + return testing::AssertionFailure() << "file does not exist: " << actual; + } + + if (!fs::exists(expectedPath)) + { + return testing::AssertionFailure() << "file does not exist: " << expectedPath; + } + + if (!fs::exists(MAGICK_COMPARE)) + { + return testing::AssertionFailure() << "image magick compare not found: " << MAGICK_COMPARE; + } + + const auto cmd = fmt::format(R"("{}" -metric AE -fuzz {}% {} {} {} 2> {})", MAGICK_COMPARE, tolerance, actual, expectedPath, tempDiff, tempStderr); + // NOLINTNEXTLINE(cert-env33-c) inputs are known at compile time + const auto retval = std::system(cmd.c_str()); + if (retval != 0) + { + auto result = testing::AssertionFailure() << "image compare failed - cmd:" << cmd; + fs::create_directory(diffDir); + fs::copy(actual, filenameOut, fs::copy_options::overwrite_existing); + if (fs::exists(tempDiff)) + { + fs::copy(tempDiff, filenameDiff, fs::copy_options::overwrite_existing); + } + if (fs::exists(tempStderr)) + { + std::ifstream ifs(tempStderr); + result << std::endl << "stderr:" << ifs.rdbuf(); + fs::copy(tempStderr, filenameStderr, fs::copy_options::overwrite_existing); + } + return result; + } + + return testing::AssertionSuccess(); + } + + protected: + WithTempDirectory m_withTempDirectory; + RamsesTestSetup m_ramses; + TriangleTestScene m_scene = {m_ramses.createTriangleTestScene()}; + LogHandler m_log; + std::unique_ptr m_app; + }; + + class ALogicViewerGuiApp : public ALogicViewerAppBase, public ::testing::Test + { + }; + + class ALogicViewerHeadlessApp : public ALogicViewerAppBase, public ::testing::Test + { + }; + + template + class ALogicViewerApp_T : public ALogicViewerAppBase, public ::testing::Test + { + }; + + using LogicViewerAppTypes = ::testing::Types< LogicViewerGuiApp, LogicViewerHeadlessApp>; + + TYPED_TEST_SUITE(ALogicViewerApp_T, LogicViewerAppTypes); + + class ALogicViewerAppUIBase : public ALogicViewerAppBase + { + public: + void setup(std::string_view ini, std::vector args = {}) + { + SaveFile(ini, "imgui.ini"); + args.emplace_back(ramsesFile); + this->createApp(args); + + ui.setup(m_app->getSettings()); + ImGui::GetIO().GetClipboardTextFn = GetClipboardText; + ImGui::GetIO().SetClipboardTextFn = SetClipboardText; + EXPECT_TRUE(runUntil("is in state RENDERED caused by command SHOW")); + EXPECT_TRUE(m_app->doOneLoop()); + } + + [[nodiscard]] bool click(int32_t x, int32_t y) + { + auto imgui = m_app->getImguiClientHelper(); + imgui->mouseEvent(ramses::displayId_t(0), ramses::EMouseEvent::LeftButtonDown, x, y); + EXPECT_TRUE(m_app->doOneLoop()); + EXPECT_TRUE(m_app->doOneLoop()); + imgui->mouseEvent(ramses::displayId_t(0), ramses::EMouseEvent::LeftButtonUp, x, y); + EXPECT_TRUE(m_app->doOneLoop()); + return m_app->doOneLoop(); + } + + [[nodiscard]] bool rightClick(int32_t x, int32_t y) + { + auto imgui = m_app->getImguiClientHelper(); + imgui->mouseEvent(ramses::displayId_t(0), ramses::EMouseEvent::RightButtonDown, x, y); + EXPECT_TRUE(m_app->doOneLoop()); + EXPECT_TRUE(m_app->doOneLoop()); + imgui->mouseEvent(ramses::displayId_t(0), ramses::EMouseEvent::RightButtonUp, x, y); + EXPECT_TRUE(m_app->doOneLoop()); + return m_app->doOneLoop(); + } + + [[nodiscard]] bool keyPress(ramses::EKeyCode keyCode) + { + auto imgui = m_app->getImguiClientHelper(); + imgui->keyEvent(ramses::displayId_t(0), ramses::EKeyEvent::Pressed, 0, keyCode); + EXPECT_TRUE(m_app->doOneLoop()); + imgui->keyEvent(ramses::displayId_t(0), ramses::EKeyEvent::Released, 0, keyCode); + return m_app->doOneLoop(); + } + + [[nodiscard]] bool contextMenuSelect(int32_t x, int32_t y, int32_t item = 0) + { + EXPECT_TRUE(rightClick(x, y)); + return click(x + 9, y + (item * ui.smallButtonHeight) + 9); + } + + [[nodiscard]] bool dragX(int32_t x1, int32_t x2, int32_t y) + { + auto imgui = m_app->getImguiClientHelper(); + imgui->mouseEvent(ramses::displayId_t(0), ramses::EMouseEvent::LeftButtonDown, x1, y); + EXPECT_TRUE(m_app->doOneLoop()); + EXPECT_TRUE(m_app->doOneLoop()); + imgui->mouseEvent(ramses::displayId_t(0), ramses::EMouseEvent::LeftButtonDown, x2, y); + EXPECT_TRUE(m_app->doOneLoop()); + EXPECT_TRUE(m_app->doOneLoop()); + imgui->mouseEvent(ramses::displayId_t(0), ramses::EMouseEvent::LeftButtonUp, x2, y); + EXPECT_TRUE(m_app->doOneLoop()); + return m_app->doOneLoop(); + } + + static void SetClipboardText(void* /*unused*/, const char* str) + { + s_clipboard = str; + } + + [[nodiscard]] static const char* GetClipboardText(void* /*unused*/) + { + return s_clipboard.c_str(); + } + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) must be static and non-const + static std::string s_clipboard; + + UI ui; + }; + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) must be static and non-const + std::string ALogicViewerAppUIBase::s_clipboard; + + class ALogicViewerAppUI : public ALogicViewerAppUIBase, public ::testing::Test + { + public: + ALogicViewerAppUI() + { + setup(iniFile); + } + }; + + TEST_F(ALogicViewerGuiApp, ImageCompareSelfTest) + { + EXPECT_TRUE(CompareImage("../res/unittests/ALogicViewerApp_red.png", "ALogicViewerApp_red.png")); + EXPECT_TRUE(CompareImage("../res/unittests/ALogicViewerApp_red.png", "ALogicViewerApp_white.png", 100)); + EXPECT_FALSE(CompareImage("../res/unittests/ALogicViewerApp_red.png", "ALogicViewerApp_white.png")); + const std::string diffDir = "../res/unittests/diff/"; + EXPECT_TRUE(fs::exists(diffDir + "OUT_ALogicViewerApp_white.png")); + EXPECT_TRUE(fs::exists(diffDir + "DIFF_ALogicViewerApp_white.png")); + EXPECT_TRUE(fs::exists(diffDir + "STDERR_ALogicViewerApp_white.png")); + std::ifstream stream(diffDir + "STDERR_ALogicViewerApp_white.png"); + std::string str((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); + EXPECT_EQ("143262", str); // image differences + } + + TYPED_TEST(ALogicViewerApp_T, nullparameter) + { + TypeParam app(0, nullptr); + EXPECT_EQ(-1, app.run()); + EXPECT_EQ(-1, app.exitCode()); + } + + TYPED_TEST(ALogicViewerApp_T, emptyParam) + { + this->createApp(); + EXPECT_EQ(static_cast(CLI::ExitCodes::RequiredError), this->m_app->run()); + EXPECT_EQ(static_cast(CLI::ExitCodes::RequiredError), this->m_app->exitCode()); + } + + TYPED_TEST(ALogicViewerApp_T, version) + { + testing::internal::CaptureStdout(); + this->createApp({ "--version" }); + EXPECT_THAT(testing::internal::GetCapturedStdout(), testing::StartsWith(ramses_sdk::RAMSES_SDK_RAMSES_VERSION)); + EXPECT_EQ(0, this->m_app->run()); + } + + TYPED_TEST(ALogicViewerApp_T, ramsesFileDoesNotExist) + { + testing::internal::CaptureStderr(); + this->createApp({ "notExisting.ramses" }); + EXPECT_EQ(static_cast(CLI::ExitCodes::ValidationError), this->m_app->run()); + EXPECT_THAT(testing::internal::GetCapturedStderr(), testing::HasSubstr("File does not exist: notExisting.ramses")); + } + + TYPED_TEST(ALogicViewerApp_T, logicFileDoesNotExist) + { + testing::internal::CaptureStderr(); + this->createApp({ ramsesFile, "notExisting.rlogic" }); + EXPECT_EQ(static_cast(CLI::ExitCodes::ValidationError), this->m_app->run()); + EXPECT_THAT(testing::internal::GetCapturedStderr(), testing::HasSubstr("File does not exist: notExisting.rlogic")); + } + + TYPED_TEST(ALogicViewerApp_T, luaFileDoesNotExist) + { + testing::internal::CaptureStderr(); + this->createApp({ ramsesFile, logicFile, "notExisting.lua" }); + EXPECT_EQ(static_cast(CLI::ExitCodes::ValidationError), this->m_app->run()); + EXPECT_THAT(testing::internal::GetCapturedStderr(), testing::HasSubstr("File does not exist: notExisting.lua")); + } + + TYPED_TEST(ALogicViewerApp_T, writeDefaultLuaConfiguration) + { + this->createApp({ "--write-config", ramsesFile }); + auto* viewer = this->m_app->getViewer(); + ASSERT_TRUE(viewer != nullptr); + EXPECT_EQ(Result(), viewer->update()); + EXPECT_EQ(0, this->m_app->run()); + EXPECT_TRUE(fs::exists(luaFile)); + std::ifstream str(luaFile); + std::string genfile((std::istreambuf_iterator(str)), std::istreambuf_iterator()); + EXPECT_EQ(defaultLuaFile, genfile); + } + + TYPED_TEST(ALogicViewerApp_T, writeDefaultLuaConfigurationHeadless) + { + this->createApp({ "--write-config", "--headless", ramsesFile }); + auto* viewer = this->m_app->getViewer(); + ASSERT_TRUE(viewer != nullptr); + EXPECT_EQ(Result(), viewer->update()); + EXPECT_EQ(0, this->m_app->run()); + EXPECT_TRUE(fs::exists(luaFile)); + std::ifstream str(luaFile); + std::string genfile((std::istreambuf_iterator(str)), std::istreambuf_iterator()); + EXPECT_EQ(defaultLuaFile, genfile); + } + + TYPED_TEST(ALogicViewerApp_T, writeDefaultLuaConfigurationToOtherFile) + { + this->createApp({ "--write-config=foobar.lua", ramsesFile }); + auto* viewer = this->m_app->getViewer(); + ASSERT_TRUE(viewer != nullptr); + EXPECT_EQ(Result(), viewer->update()); + EXPECT_EQ(0, this->m_app->run()); + EXPECT_TRUE(fs::exists("foobar.lua")); + std::ifstream str("foobar.lua"); + std::string genfile((std::istreambuf_iterator(str)), std::istreambuf_iterator()); + EXPECT_EQ(defaultLuaFile, genfile); + } + + TEST_F(ALogicViewerGuiApp, runInteractive) + { + createApp({ ramsesFile }); + EXPECT_TRUE(runUntil("is in state RENDERED caused by command SHOW")); + EXPECT_TRUE(m_app->doOneLoop()); + EXPECT_TRUE(m_app->doOneLoop()); + EXPECT_TRUE(m_app->doOneLoop()); + auto imgui = m_app->getImguiClientHelper(); + imgui->windowClosed(ramses::displayId_t()); + EXPECT_FALSE(m_app->doOneLoop()); + EXPECT_EQ(0, m_app->exitCode()); + } + + TEST_F(ALogicViewerHeadlessApp, runInteractive) + { + createApp({ ramsesFile }); + EXPECT_FALSE(m_app->doOneLoop()); + EXPECT_EQ(-1, m_app->run()); + } + + TYPED_TEST(ALogicViewerApp_T, exec_luaFileMissing) + { + // implicit filename + testing::internal::CaptureStderr(); + this->createApp({ "--exec=test_default", ramsesFile }); + EXPECT_EQ(static_cast(LogicViewerApp::ExitCode::ErrorLoadLua), this->m_app->run()); + EXPECT_THAT(testing::internal::GetCapturedStderr(), testing::HasSubstr("cannot open ALogicViewerAppTest.lua: No such file or directory")); + // explicit filename + testing::internal::CaptureStderr(); + this->createApp({ "--exec=test_default", "--lua=NotExistingLuaFile.lua", ramsesFile }); + EXPECT_EQ(static_cast(CLI::ExitCodes::ValidationError), this->m_app->run()); + EXPECT_THAT(testing::internal::GetCapturedStderr(), testing::HasSubstr("File does not exist: NotExistingLuaFile.lua")); + } + + TEST_F(ALogicViewerGuiApp, exec_screenshot) + { + SaveFile(R"( + function test_default() + -- stores a screenshot (relative to the working directory) + rlogic.screenshot("test_red.png") + rlogic.appearanceBindings.myAppearance.IN.green.value = 1 + rlogic.screenshot("test_yellow.png") + end + )"); + createApp({ "--exec=test_default", ramsesFile }); + EXPECT_EQ(0, m_app->run()); + EXPECT_TRUE(CompareImage("test_red.png", "ALogicViewerApp_red.png")); + EXPECT_TRUE(CompareImage("test_yellow.png", "ALogicViewerApp_yellow.png")); + } + + TYPED_TEST(ALogicViewerApp_T, exec_luaError) + { + this->SaveFile(R"( + function test_default() + -- stores a screenshot (relative to the working directory) + rlogic.screenshot("test_red.png") + rlogic.appearanceBindings.myAppearance.IN.green.value = 1 + rlogic.screenshot("test_yellow.png") + )"); + this->createApp({ "--exec=test_default", ramsesFile }); + EXPECT_EQ(static_cast(LogicViewerApp::ExitCode::ErrorLoadLua), this->m_app->run()); + } + + TEST_F(ALogicViewerGuiApp, exec_lua_function) + { + SaveFile(R"( + function test_default(filename, greenVal) + rlogic.appearanceBindings.myAppearance.IN.green.value = greenVal + rlogic.screenshot(filename) + end + )"); + createApp({ R"(--exec-lua=test_default('almost_yellow.png', 0.9))", ramsesFile }); + EXPECT_EQ(0, m_app->run()); + auto appearance = m_app->getViewer()->getEngine().findByName("myAppearance"); + ASSERT_TRUE(appearance != nullptr); + auto prop = appearance->getInputs()->getChild("green"); + ASSERT_TRUE(prop != nullptr); + EXPECT_FLOAT_EQ(0.9f, prop->get().value()); + EXPECT_TRUE(CompareImage("almost_yellow.png", "ALogicViewerApp_yellow.png", 15.f)); + } + + TYPED_TEST(ALogicViewerApp_T, exec_lua_code) + { + this->createApp({ R"(--exec-lua=rlogic.appearanceBindings.myAppearance.IN.green.value = 0.44)", ramsesFile }); + EXPECT_EQ(0, this->m_app->run()); + auto appearance = this->m_app->getViewer()->getEngine().template findByName("myAppearance"); + ASSERT_TRUE(appearance != nullptr); + auto prop = appearance->getInputs()->getChild("green"); + ASSERT_TRUE(prop != nullptr); + EXPECT_FLOAT_EQ(0.44f, prop->template get().value()); + } + + TYPED_TEST(ALogicViewerApp_T, exec_lua_error) + { + testing::internal::CaptureStderr(); + this->createApp({ R"(--exec-lua=rlogic.appearanceBindings.myAppearance.IN.green = 0.44)", ramsesFile }); + EXPECT_THAT(testing::internal::GetCapturedStderr(), testing::HasSubstr("sol: cannot set (new_index) into this object")); + EXPECT_EQ(static_cast(LogicViewerApp::ExitCode::ErrorLoadLua), this->m_app->run()); + } + + TEST_F(ALogicViewerGuiApp, exec_lua_headless) + { + createApp({ R"(--exec-lua=rlogic.appearanceBindings.myAppearance.IN.green.value = 0.24)", "--headless", ramsesFile }); + ASSERT_EQ(0, m_app->run()); + auto appearance = m_app->getViewer()->getEngine().findByName("myAppearance"); + ASSERT_TRUE(appearance != nullptr); + auto prop = appearance->getInputs()->getChild("green"); + ASSERT_TRUE(prop != nullptr); + EXPECT_FLOAT_EQ(0.24f, prop->get().value()); + } + + TYPED_TEST(ALogicViewerApp_T, exec_lua_screenshot_headless) + { + testing::internal::CaptureStderr(); + this->createApp({ R"(--exec-lua=rlogic.screenshot("screenshot.png"))", "--headless", ramsesFile }); + EXPECT_EQ(static_cast(LogicViewerApp::ExitCode::ErrorLoadLua), this->m_app->run()); + EXPECT_THAT(testing::internal::GetCapturedStderr(), testing::HasSubstr("No screenshots available in current configuration")); + } + + TEST_F(ALogicViewerGuiApp, interactive_luaError) + { + SaveFile(R"( + function test_default() + -- stores a screenshot (relative to the working directory) + rlogic.screenshot("test_red.png") + rlogic.appearanceBindings.myAppearance.IN.green.value = 1 + rlogic.screenshot("test_yellow.png") + )"); + createApp({ ramsesFile }); + EXPECT_TRUE(runUntil("is in state RENDERED caused by command SHOW")); + EXPECT_THAT(m_app->getViewer()->getLastResult().getMessage(), testing::HasSubstr("ALogicViewerAppTest.lua:7: 'end' expected")); + EXPECT_TRUE(m_app->doOneLoop()); + EXPECT_TRUE(m_app->doOneLoop()); + // does not terminate + } + + TEST_F(ALogicViewerGuiApp, no_offscreen) + { + SaveFile(R"( + function test_default() + -- stores a screenshot (relative to the working directory) + rlogic.screenshot("test_red.png") + end + )"); + createApp({ "--exec=test_default", "--no-offscreen", ramsesFile }); + EXPECT_EQ(0, m_app->run()); + EXPECT_TRUE(CompareImage("test_red.png", "ALogicViewerApp_red.png")); + } + + TEST_F(ALogicViewerGuiApp, windowSize) + { + SaveFile(R"( + function test_default() + -- stores a screenshot (relative to the working directory) + rlogic.screenshot("test_red.png") + end + )"); + createApp({ "--exec=test_default", "--width", "500", "--height", "700", ramsesFile }); + EXPECT_EQ(0, m_app->run()); + EXPECT_TRUE(CompareImage("test_red.png", "ALogicViewerApp_red_500x700.png")); + } + + TEST_F(ALogicViewerAppUI, modifyInterfaceInput) + { + const int32_t mouseX = 100; + EXPECT_TRUE(click(mouseX, ui.interfaces())); + EXPECT_TRUE(click(mouseX, ui.interfaces() + ui.smallButtonHeight)); + EXPECT_TRUE(dragX(mouseX, mouseX + 30, ui.interfaces() + ui.buttonHeight + 2 * ui.smallButtonHeight)); + + auto* script = m_app->getViewer()->getEngine().findByName("myScript"); + ASSERT_TRUE(script != nullptr); + auto* prop = script->getOutputs()->getChild("paramVec3f"); + ASSERT_TRUE(prop != nullptr); + const auto value = prop->get().value(); + EXPECT_FLOAT_EQ(0.f, value[0]); + EXPECT_FLOAT_EQ(0.f, value[1]); + EXPECT_FLOAT_EQ(3.f, value[2]); + } + + TEST_F(ALogicViewerAppUI, modifyScript) + { + const int32_t mouseX = 100; + EXPECT_TRUE(click(mouseX, ui.scripts())); + EXPECT_TRUE(click(mouseX, ui.scripts() + 2 * ui.smallButtonHeight)); + EXPECT_TRUE(dragX(mouseX, mouseX + 30, ui.animationNodes() + ui.buttonHeight + 4 * ui.smallButtonHeight)); + + auto* script = m_app->getViewer()->getEngine().findByName("allTypesScript"); + ASSERT_TRUE(script != nullptr); + auto* prop = script->getOutputs()->getChild("paramFloat"); + ASSERT_TRUE(prop != nullptr); + EXPECT_FLOAT_EQ(3.f, prop->get().value()); + } + + TEST_F(ALogicViewerAppUI, modifyAnimation) + { + const int32_t mouseX = 100; + EXPECT_TRUE(click(mouseX, ui.animationNodes())); + EXPECT_TRUE(click(mouseX, ui.animationNodes() + ui.smallButtonHeight)); + EXPECT_TRUE(dragX(mouseX, mouseX + 10, ui.animationNodes() + ui.buttonHeight + 4 * ui.smallButtonHeight)); + + auto* animation = m_app->getViewer()->getEngine().findByName("myAnimation"); + ASSERT_TRUE(animation != nullptr); + auto* prop = animation->getInputs()->getChild("progress"); + ASSERT_TRUE(prop != nullptr); + EXPECT_FLOAT_EQ(1.f, prop->get().value()); + } + + TEST_F(ALogicViewerAppUI, modifyTimer) + { + const int32_t mouseX = 100; + EXPECT_TRUE(click(mouseX, ui.timerNodes())); + EXPECT_TRUE(click(mouseX, ui.timerNodes() + ui.smallButtonHeight)); + EXPECT_TRUE(dragX(mouseX, mouseX + 30, ui.timerNodes() + ui.buttonHeight + 2 * ui.smallButtonHeight)); + + auto* timer = m_app->getViewer()->getEngine().findByName("myTimer"); + ASSERT_TRUE(timer != nullptr); + auto* prop = timer->getInputs()->getChild("ticker_us"); + ASSERT_TRUE(prop != nullptr); + EXPECT_EQ(3, prop->get().value()); + } + + TEST_F(ALogicViewerAppUI, modifyAppearance) + { + const int32_t mouseX = 100; + EXPECT_TRUE(click(mouseX, ui.appearanceBindings())); + EXPECT_TRUE(click(mouseX, ui.appearanceBindings() + ui.smallButtonHeight)); + EXPECT_TRUE(dragX(mouseX, mouseX + 10, ui.appearanceBindings() + ui.buttonHeight + 3 * ui.smallButtonHeight)); + + auto* appearance = m_app->getViewer()->getEngine().findByName("myAppearance"); + ASSERT_TRUE(appearance != nullptr); + auto* prop = appearance->getInputs()->getChild("green"); + ASSERT_TRUE(prop != nullptr); + EXPECT_FLOAT_EQ(1.f, prop->get().value()); + } + + TEST_F(ALogicViewerAppUI, modifyNodeBinding) + { + const int32_t mouseX = 100; + EXPECT_TRUE(click(mouseX, ui.nodeBindings())); + EXPECT_TRUE(click(mouseX, ui.nodeBindings() + ui.smallButtonHeight)); + EXPECT_TRUE(dragX(mouseX, mouseX + 20, ui.nodeBindings() + 2 * ui.buttonHeight + 5 * ui.smallButtonHeight)); + + auto* node = m_app->getViewer()->getEngine().findByName("myNode"); + ASSERT_TRUE(node != nullptr); + auto* prop = node->getInputs()->getChild("translation"); + ASSERT_TRUE(prop != nullptr); + const auto value = prop->get().value(); + EXPECT_FLOAT_EQ(2.f, value[0]); + EXPECT_FLOAT_EQ(0.f, value[1]); + EXPECT_FLOAT_EQ(0.f, value[2]); + } + + TEST_F(ALogicViewerAppUI, modifyCameraBinding) + { + const int32_t mouseX = 100; + EXPECT_TRUE(click(mouseX, ui.cameraBindings())); + EXPECT_TRUE(click(mouseX, ui.cameraBindings() + ui.smallButtonHeight)); + EXPECT_TRUE(click(mouseX, ui.cameraBindings() + 4 * ui.smallButtonHeight)); + EXPECT_TRUE(dragX(mouseX, mouseX - 30, ui.cameraBindings() + 3 * ui.buttonHeight + 4 * ui.smallButtonHeight)); + + auto* camera = m_app->getViewer()->getEngine().findByName("myCamera"); + ASSERT_TRUE(camera != nullptr); + auto* prop = camera->getInputs()->getChild("viewport"); + ASSERT_TRUE(prop != nullptr); + prop = prop->getChild("width"); + ASSERT_TRUE(prop != nullptr); + EXPECT_EQ(797, prop->get().value()); + } + + TEST_F(ALogicViewerAppUI, modifyRenderPass) + { + const int32_t mouseX = 100; + EXPECT_TRUE(click(mouseX, ui.renderPassBindings())); + EXPECT_TRUE(click(mouseX, ui.renderPassBindings() + ui.smallButtonHeight)); + EXPECT_TRUE(dragX(mouseX, mouseX + 30, ui.renderPassBindings() + 2 * ui.buttonHeight + 3 * ui.smallButtonHeight)); + + auto* renderPass = m_app->getViewer()->getEngine().findByName("myRenderPass"); + ASSERT_TRUE(renderPass != nullptr); + auto* prop = renderPass->getInputs()->getChild("renderOrder"); + ASSERT_TRUE(prop != nullptr); + EXPECT_EQ(3, prop->get().value()); + } + + TEST_F(ALogicViewerAppUI, modifyRenderGroup) + { + const int32_t mouseX = 100; + EXPECT_TRUE(click(mouseX, ui.renderGroupBindings())); + EXPECT_TRUE(click(mouseX, ui.renderGroupBindings() + ui.smallButtonHeight)); + EXPECT_TRUE(click(mouseX, ui.renderGroupBindings() + 4 * ui.smallButtonHeight)); + EXPECT_TRUE(dragX(mouseX, mouseX + 30, ui.renderGroupBindings() + ui.buttonHeight + 4 * ui.smallButtonHeight)); + + auto* renderGroup = m_app->getViewer()->getEngine().findByName("myRenderGroup"); + ASSERT_TRUE(renderGroup != nullptr); + auto* prop = renderGroup->getInputs()->getChild("renderOrders")->getChild("myMeshNode"); + ASSERT_TRUE(prop != nullptr); + EXPECT_EQ(3, prop->get().value()); + } + + TEST_F(ALogicViewerAppUI, modifyMeshNode) + { + const int32_t mouseX = 100; + EXPECT_TRUE(click(mouseX, ui.meshNodeBindings())); + EXPECT_TRUE(click(mouseX, ui.meshNodeBindings() + ui.smallButtonHeight)); + EXPECT_TRUE(dragX(mouseX, mouseX + 30, ui.meshNodeBindings() + 4 * ui.smallButtonHeight)); + + auto* meshNode = m_app->getViewer()->getEngine().findByName("myMeshNode"); + ASSERT_TRUE(meshNode != nullptr); + auto* prop = meshNode->getInputs()->getChild("vertexOffset"); + ASSERT_TRUE(prop != nullptr); + EXPECT_EQ(3, prop->get().value()); + } + + TEST_F(ALogicViewerAppUI, reloadConfiguration) + { + const int32_t xFile = 25; + auto* script = m_app->getViewer()->getEngine().findByName("allTypesScript"); + ASSERT_TRUE(script != nullptr); + auto* prop = script->getOutputs()->getChild("paramString"); + ASSERT_TRUE(prop != nullptr); + + SaveFile(R"(rlogic.scripts.allTypesScript.IN.paramString.value = "Hello")"); + EXPECT_TRUE(m_app->doOneLoop()); + EXPECT_EQ("", prop->get().value()); // not reloaded automatically + + EXPECT_TRUE(keyPress(ramses::EKeyCode_F5)); // reload configuration + EXPECT_EQ("Hello", prop->get().value()); + + SaveFile(R"(rlogic.scripts.allTypesScript.IN.paramString.value = "World")"); + EXPECT_TRUE(click(xFile, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xFile, ui.titleBar + ui.buttonHeight + ui.yMiddle)); + EXPECT_EQ("World", prop->get().value()); + + SaveFile(R"(rlogic.scripts.allTypesScript.IN.paramString.value = "Foo")"); + EXPECT_TRUE(contextMenuSelect(700, 350, 1)); + EXPECT_EQ("Foo", prop->get().value()); + } + + TEST_F(ALogicViewerAppUI, clipboard) + { + const int32_t mouseX = 100; + EXPECT_TRUE(contextMenuSelect(mouseX, ui.interfaces())); + EXPECT_EQ(R"(rlogic.interfaces["myInterface"]["IN"]["paramFloat"].value = 0)", rtrim(ImGui::GetClipboardText())); + + EXPECT_TRUE(contextMenuSelect(mouseX, ui.scripts())); + EXPECT_EQ(R"(rlogic.scripts["allTypesScript"]["IN"]["paramBool"].value = false +rlogic.scripts["allTypesScript"]["IN"]["paramFloat"].value = 0 +rlogic.scripts["allTypesScript"]["IN"]["paramInt32"].value = 0 +rlogic.scripts["allTypesScript"]["IN"]["paramInt64"].value = 0 +rlogic.scripts["allTypesScript"]["IN"]["paramString"].value = '' +rlogic.scripts["allTypesScript"]["IN"]["paramVec2f"].value = { 0, 0 } +rlogic.scripts["allTypesScript"]["IN"]["paramVec2i"].value = { 0, 0 } +rlogic.scripts["allTypesScript"]["IN"]["paramVec3f"].value = { 0, 0, 0 } +rlogic.scripts["allTypesScript"]["IN"]["paramVec3i"].value = { 0, 0, 0 } +rlogic.scripts["allTypesScript"]["IN"]["paramVec4f"].value = { 0, 0, 0, 0 } +rlogic.scripts["allTypesScript"]["IN"]["paramVec4i"].value = { 0, 0, 0, 0 } +rlogic.scripts["allTypesScript"]["IN"]["struct"]["nested"]["data1"].value = '' +rlogic.scripts["allTypesScript"]["IN"]["struct"]["nested"]["data2"].value = 0)", rtrim(ImGui::GetClipboardText())); + + EXPECT_TRUE(contextMenuSelect(mouseX, ui.animationNodes())); + EXPECT_EQ(R"(rlogic.animationNodes["myAnimation"]["IN"]["progress"].value = 0)", rtrim(ImGui::GetClipboardText())); + + EXPECT_TRUE(contextMenuSelect(mouseX, ui.timerNodes())); + EXPECT_EQ(R"(rlogic.timerNodes["myTimer"]["IN"]["ticker_us"].value = 0)", rtrim(ImGui::GetClipboardText())); + + EXPECT_TRUE(contextMenuSelect(mouseX, ui.appearanceBindings())); + EXPECT_EQ(R"(rlogic.appearanceBindings["myAppearance"]["IN"]["green"].value = 0 +rlogic.appearanceBindings["myAppearance"]["IN"]["blue"].value = 0)", rtrim(ImGui::GetClipboardText())); + + EXPECT_TRUE(contextMenuSelect(mouseX, ui.nodeBindings())); + EXPECT_EQ(R"(rlogic.nodeBindings["myNode"]["IN"]["visibility"].value = true +rlogic.nodeBindings["myNode"]["IN"]["translation"].value = { 0, 0, 0 } +rlogic.nodeBindings["myNode"]["IN"]["scaling"].value = { 1, 1, 1 } +rlogic.nodeBindings["myNode"]["IN"]["enabled"].value = true)", rtrim(ImGui::GetClipboardText())); + + EXPECT_TRUE(contextMenuSelect(mouseX, ui.cameraBindings())); + EXPECT_EQ(R"(rlogic.cameraBindings["myCamera"]["IN"]["viewport"]["offsetX"].value = 0 +rlogic.cameraBindings["myCamera"]["IN"]["viewport"]["offsetY"].value = 0 +rlogic.cameraBindings["myCamera"]["IN"]["viewport"]["width"].value = 800 +rlogic.cameraBindings["myCamera"]["IN"]["viewport"]["height"].value = 800 +rlogic.cameraBindings["myCamera"]["IN"]["frustum"]["nearPlane"].value = 0.1 +rlogic.cameraBindings["myCamera"]["IN"]["frustum"]["farPlane"].value = 100 +rlogic.cameraBindings["myCamera"]["IN"]["frustum"]["fieldOfView"].value = 20 +rlogic.cameraBindings["myCamera"]["IN"]["frustum"]["aspectRatio"].value = 1)", rtrim(ImGui::GetClipboardText())); + + EXPECT_TRUE(contextMenuSelect(mouseX, ui.renderPassBindings())); + EXPECT_EQ(R"(rlogic.renderPassBindings["myRenderPass"]["IN"]["enabled"].value = true +rlogic.renderPassBindings["myRenderPass"]["IN"]["renderOrder"].value = 0 +rlogic.renderPassBindings["myRenderPass"]["IN"]["clearColor"].value = { 0, 0, 0, 1 } +rlogic.renderPassBindings["myRenderPass"]["IN"]["renderOnce"].value = false)", rtrim(ImGui::GetClipboardText())); + + EXPECT_TRUE(contextMenuSelect(mouseX, ui.renderGroupBindings())); + EXPECT_EQ(R"(rlogic.renderGroupBindings["myRenderGroup"]["IN"]["renderOrders"]["myMeshNode"].value = 0)", + rtrim(ImGui::GetClipboardText())); + + EXPECT_TRUE(contextMenuSelect(mouseX, ui.meshNodeBindings())); + EXPECT_EQ(R"(rlogic.meshNodeBindings["myMeshNode"]["IN"]["vertexOffset"].value = 0 +rlogic.meshNodeBindings["myMeshNode"]["IN"]["indexOffset"].value = 0 +rlogic.meshNodeBindings["myMeshNode"]["IN"]["indexCount"].value = 3 +rlogic.meshNodeBindings["myMeshNode"]["IN"]["instanceCount"].value = 1)", rtrim(ImGui::GetClipboardText())); + } + + TEST_F(ALogicViewerAppUI, changeView) + { + SaveFile(R"( + red = { + name = "Red", + description = "Shows a red triangle", + update = function(time_ms) + rlogic.appearanceBindings["myAppearance"]["IN"]["green"].value = 0 + rlogic.appearanceBindings.myAppearance.IN.blue.value = 0 + end + } + + yellow = { + name = "Yellow", + description = "Shows a yellow triangle", + update = function(time_ms) + rlogic.appearanceBindings["myAppearance"]["IN"]["green"].value = 1 + end, + inputs = {rlogic.appearanceBindings.myAppearance.IN.blue} + } + + rlogic.views = {red, yellow} + + function screenshot() + rlogic.screenshot("screenshot.png") + end + )"); + + EXPECT_TRUE(keyPress(ramses::EKeyCode_F5)); // reload configuration + EXPECT_EQ(2u, m_app->getViewer()->getViewCount()); + EXPECT_EQ(1u, m_app->getViewer()->getCurrentView()); + EXPECT_EQ(Result(), m_app->getViewer()->call("screenshot")); + EXPECT_TRUE(CompareImage("screenshot.png", "ALogicViewerApp_red.png")); + + EXPECT_TRUE(keyPress(ramses::EKeyCode_Right)); // next view + EXPECT_EQ(2u, m_app->getViewer()->getCurrentView()); + EXPECT_EQ(Result(), m_app->getViewer()->call("screenshot")); + EXPECT_TRUE(CompareImage("screenshot.png", "ALogicViewerApp_yellow.png")); + + // Modify the "blue" input 0 -> 1 + EXPECT_TRUE(dragX(100, 100 + 10, ui.titleBar + 2 * ui.buttonHeight + 2 * ui.smallButtonHeight + ui.yMiddle)); + EXPECT_EQ(Result(), m_app->getViewer()->call("screenshot")); + EXPECT_TRUE(CompareImage("screenshot.png", "ALogicViewerApp_white.png")); + + EXPECT_TRUE(keyPress(ramses::EKeyCode_Left)); // previous view + EXPECT_EQ(1u, m_app->getViewer()->getCurrentView()); + EXPECT_EQ(Result(), m_app->getViewer()->call("screenshot")); + EXPECT_TRUE(CompareImage("screenshot.png", "ALogicViewerApp_red.png")); + } + + TEST_F(ALogicViewerAppUI, settings) + { + const int xSettings = 75; + + auto* settings = m_app->getSettings(); + EXPECT_TRUE(settings->showWindow); + EXPECT_TRUE(contextMenuSelect(700, 350)); + EXPECT_FALSE(settings->showWindow); + EXPECT_TRUE(keyPress(ramses::EKeyCode_F11)); + EXPECT_TRUE(settings->showWindow); + + EXPECT_FALSE(settings->showDisplaySettings); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 12 * ui.smallButtonHeight + 3 * ui.hline + ui.yMiddle)); + EXPECT_TRUE(settings->showDisplaySettings); + + EXPECT_FALSE(settings->luaPreferObjectIds); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 11 * ui.smallButtonHeight + 3 * ui.hline + ui.yMiddle)); + EXPECT_TRUE(settings->luaPreferObjectIds); + + EXPECT_FALSE(settings->luaPreferIdentifiers); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 10 * ui.smallButtonHeight + 3 * ui.hline + ui.yMiddle)); + EXPECT_TRUE(settings->luaPreferIdentifiers); + + EXPECT_TRUE(settings->showOutputs); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 9 * ui.smallButtonHeight + 2 * ui.hline + ui.yMiddle)); + EXPECT_FALSE(settings->showOutputs); + + EXPECT_TRUE(settings->showLinkedInputs); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 8 * ui.smallButtonHeight + 2 * ui.hline + ui.yMiddle)); + EXPECT_FALSE(settings->showLinkedInputs); + + EXPECT_FALSE(settings->showUpdateReport); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 7 * ui.smallButtonHeight + ui.hline + ui.yMiddle)); + EXPECT_TRUE(settings->showUpdateReport); + + EXPECT_TRUE(settings->showRamsesBindings); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 6 * ui.smallButtonHeight + ui.hline + ui.yMiddle)); + EXPECT_FALSE(settings->showRamsesBindings); + + EXPECT_TRUE(settings->showDataArrays); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 5 * ui.smallButtonHeight + ui.hline + ui.yMiddle)); + EXPECT_FALSE(settings->showDataArrays); + + EXPECT_TRUE(settings->showTimerNodes); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 4 * ui.smallButtonHeight + ui.hline + ui.yMiddle)); + EXPECT_FALSE(settings->showTimerNodes); + + EXPECT_TRUE(settings->showAnimationNodes); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 3 * ui.smallButtonHeight + ui.hline + ui.yMiddle)); + EXPECT_FALSE(settings->showAnimationNodes); + + EXPECT_TRUE(settings->showScripts); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 2 * ui.smallButtonHeight + ui.hline + ui.yMiddle)); + EXPECT_FALSE(settings->showScripts); + + EXPECT_TRUE(settings->showInterfaces); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + 1 * ui.smallButtonHeight + ui.hline + ui.yMiddle)); + EXPECT_FALSE(settings->showInterfaces); + + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(xSettings, ui.titleBar + ui.buttonHeight + ui.yMiddle)); + EXPECT_FALSE(settings->showWindow); + } + + class ALogicViewerAppUIClearColor : public ALogicViewerAppUIBase, public ::testing::TestWithParam + { + public: + ALogicViewerAppUIClearColor() + { + if (GetParam()) + { + setup(iniFile, { "--clear-color", "0", "0", "0.5", "1" }); + } + else + { + setup(iniFile, { "--no-offscreen", "--clear-color", "0", "0", "0.5", "1" }); + } + } + }; + + INSTANTIATE_TEST_SUITE_P( + ALogicViewerAppUIClearColor_TestInstances, + ALogicViewerAppUIClearColor, + ::testing::Values( + false, // no offscreen + true) // offscreen + ); + + TEST_P(ALogicViewerAppUIClearColor, usesClearColorFromCommandLine) + { + SaveFile(R"( + function screenshot() + rlogic.screenshot("screenshot.png") + end + )"); + EXPECT_TRUE(keyPress(ramses::EKeyCode_F5)); // reload configuration + EXPECT_TRUE(keyPress(ramses::EKeyCode_F11)); // hide UI + + EXPECT_EQ(Result(), m_app->getViewer()->call("screenshot")); + EXPECT_TRUE(CompareImage("screenshot.png", "ALogicViewerApp_clearColorCmdLine.png", 0.5f)); // increased tolerance due to some platforms being 1/255 off covering large area (as background) + } + + TEST_P(ALogicViewerAppUIClearColor, changeClearColor) + { + SaveFile(R"( + function screenshot() + rlogic.screenshot("screenshot.png") + end + )"); + EXPECT_TRUE(keyPress(ramses::EKeyCode_F5)); // reload configuration + + const int mouseX = 75; + // show display settings + EXPECT_TRUE(click(mouseX, ui.titleBar + ui.yMiddle)); + EXPECT_TRUE(click(mouseX, ui.titleBar + ui.buttonHeight + 12 * ui.smallButtonHeight + 3 * ui.hline + ui.yMiddle)); + EXPECT_TRUE(m_app->getSettings()->showDisplaySettings); + // change clear color + const int displaySettingsY = ui.displaySettings() - ui.buttonHeight; + EXPECT_TRUE(click(mouseX, displaySettingsY)); + EXPECT_TRUE(dragX(mouseX + 50, mouseX + 55, displaySettingsY + ui.buttonHeight)); + EXPECT_TRUE(keyPress(ramses::EKeyCode_F11)); // hide UI + + EXPECT_EQ(Result(), m_app->getViewer()->call("screenshot")); + EXPECT_TRUE(CompareImage("screenshot.png", "ALogicViewerApp_clearColor.png", 0.5f)); // increased tolerance due to some platforms being 1/255 off covering large area (as background) + } + + class ALogicViewerAppUIUpdateReport : public ALogicViewerAppUIBase, public ::testing::Test + { + }; + + TEST_F(ALogicViewerAppUIUpdateReport, updateReport) + { + setup(R"([Window][Logic Viewer (FeatureLevel 01)] +Pos=0,0 +Size=540,720 +Collapsed=0 + +[LogicViewerGui][Settings] +ShowWindow=1 +ShowInterfaces=1 +ShowScripts=1 +ShowAnimationNodes=1 +ShowTimerNodes=1 +ShowDataArrays=1 +ShowRamsesBindings=1 +ShowUpdateReport=1 +ShowLinkedInputs=1 +ShowOutputs=1 +LuaPreferObjectIds=0 +LuaPreferIdentifiers=0 +ShowDisplaySettings=0)"); + const int32_t mouseX = 100; + EXPECT_TRUE(click(mouseX, ui.updateReport())); + EXPECT_TRUE(click(mouseX, ui.updateReport() + 2 * ui.buttonHeight + 7 * ui.smallButtonHeight + ui.hline)); + EXPECT_TRUE(click(mouseX, ui.updateReport() + 2 * ui.buttonHeight + 6 * ui.smallButtonHeight + ui.hline)); + + const auto& report = m_app->getViewer()->getUpdateReport(); + + // set update interval to 1 to avoid random test failures + // (only the longest update is reported for an interval) + EXPECT_EQ(60u, report.getInterval()); + EXPECT_TRUE(dragX(mouseX, mouseX - 120, ui.updateReport() + 2 * ui.buttonHeight)); + EXPECT_EQ(1u, report.getInterval()); + + EXPECT_EQ(1, report.getNodesExecuted().size()); + EXPECT_EQ(10, report.getNodesSkippedExecution().size()); + + auto* interface = m_app->getViewer()->getEngine().findByName("myInterface"); + ASSERT_TRUE(interface != nullptr); + auto* prop = interface->getInputs()->getChild("paramFloat"); + ASSERT_TRUE(prop != nullptr); + prop->set(2.1f); + EXPECT_TRUE(m_app->doOneLoop()); + EXPECT_EQ(4, report.getNodesExecuted().size()); + EXPECT_EQ(7, report.getNodesSkippedExecution().size()); + } +} diff --git a/client/logic/tools/ramses-logic-viewer-test/LogicViewerLuaTest.cpp b/client/logic/tools/ramses-logic-viewer-test/LogicViewerLuaTest.cpp new file mode 100644 index 000000000..1790dbd5c --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer-test/LogicViewerLuaTest.cpp @@ -0,0 +1,1085 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewerTestBase.h" +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/RamsesRenderGroupBindingElements.h" + +const char* const logicFile = "test.rlogic"; + +namespace ramses::internal +{ + /** + * Tests for the lua configuration file handling + */ + class ALogicViewerLua : public ALogicViewerBase + { + public: + ALogicViewerLua() + { + createLogicFile(); + EXPECT_TRUE(viewer.loadRamsesLogic(logicFile, m_scene)); + } + + void createLogicFile() + { + LogicEngine engine{ ramses::EFeatureLevel_Latest }; + auto *interface = engine.createLuaInterface(R"( + function interface(IN,OUT) + IN.paramInt32 = Type:Int32() + end + )", "foo"); + + auto* script = engine.createLuaScript(R"( + function interface(IN,OUT) + IN.paramBool = Type:Bool() + IN.paramInt32 = Type:Int32() + IN.paramInt32_2 = Type:Int32() + IN.paramInt64 = Type:Int64() + IN.paramInt64_2 = Type:Int64() + IN.paramFloat = Type:Float() + IN.paramFloat_2 = Type:Float() + IN.paramString = Type:String() + IN.paramVec2f = Type:Vec2f() + IN.paramVec3f = Type:Vec3f() + IN.paramVec4f = Type:Vec4f() + IN.paramVec2i = Type:Vec2i() + IN.paramVec3i = Type:Vec3i() + IN.paramVec4i = Type:Vec4i() + IN.array = Type:Array(5, Type:Float()) + IN.struct = { + nested = { + data1 = Type:String(), + data2 = Type:Int32() + } + } + IN.anchorData1 = Type:Vec2f() + IN.anchorData2 = Type:Float() + + OUT.paramBool = Type:Bool() + OUT.paramInt32 = Type:Int32() + OUT.paramInt64 = Type:Int64() + OUT.paramFloat = Type:Float() + OUT.paramString = Type:String() + OUT.paramVec2f = Type:Vec2f() + OUT.paramVec3f = Type:Vec3f() + OUT.paramVec4f = Type:Vec4f() + OUT.paramVec2i = Type:Vec2i() + OUT.paramVec3i = Type:Vec3i() + OUT.paramVec4i = Type:Vec4i() + OUT.array = Type:Array(5, Type:Float()) + OUT.struct = { + nested = { + data1 = Type:String(), + data2 = Type:Int32() + } + } + OUT.anchorData1 = Type:Vec2f() + OUT.anchorData2 = Type:Float() + end + function run(IN,OUT) + OUT.paramBool = not IN.paramBool + OUT.paramInt32 = 2 * IN.paramInt32 + OUT.paramInt64 = 1 + IN.paramInt64 + OUT.paramFloat = 3 * IN.paramFloat + OUT.paramString = IN.paramString.."foo" + OUT.paramVec2f = {0, IN.paramFloat} + OUT.paramVec3f = {0, 0, IN.paramFloat} + OUT.paramVec4f = {0, 0, 0, IN.paramFloat} + OUT.paramVec2i = {0, IN.paramInt32} + OUT.paramVec3i = {0, 0, IN.paramInt32} + OUT.paramVec4i = {0, 0, 0, IN.paramInt32} + OUT.array = {10,20,30,44,50} + OUT.struct.nested.data1 = IN.paramString + OUT.struct.nested.data2 = IN.paramInt32 + OUT.anchorData1 = IN.anchorData1 + OUT.anchorData2 = IN.anchorData2 + end + )", {}, "foo"); + + ASSERT_TRUE(script != nullptr); + + auto* nodeBinding = engine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "foo"); + + // make camera valid + m_camera->setFrustum(-1.f, 1.f, -1.f, 1.f, 0.1f, 10.f); + + auto* appearanceBind = engine.createRamsesAppearanceBinding(*m_appearance, "foo"); + auto* cameraBinding = engine.createRamsesCameraBinding(*m_camera, "foo"); + auto* passBinding = engine.createRamsesRenderPassBinding(*m_renderPass, "foo"); + auto* timer = engine.createTimerNode("foo"); + auto* anchor = engine.createAnchorPoint(*nodeBinding, *cameraBinding, "foo"); + m_renderGroup->addRenderGroup(*m_nestedRenderGroup); + ramses::RamsesRenderGroupBindingElements elements; + elements.addElement(*m_nestedRenderGroup, "nestedRG"); + auto rgBinding = engine.createRamsesRenderGroupBinding(*m_renderGroup, elements, "rg"); + auto meshBinding = engine.createRamsesMeshNodeBinding(*m_meshNode, "mn"); + + ramses::DataArray* animTimestamps = engine.createDataArray(std::vector{ 0.f, 0.5f, 1.f, 1.5f }); // will be interpreted as seconds + ramses::DataArray* animKeyframes = engine.createDataArray(std::vector{ {0.f, 0.f, 0.f}, {0.f, 0.f, 180.f}, {0.f, 0.f, 100.f}, {0.f, 0.f, 360.f} }); + const ramses::AnimationChannel stepAnimChannel { "rotationZstep", animTimestamps, animKeyframes, ramses::EInterpolationType::Step }; + ramses::AnimationNodeConfig config; + config.addChannel(stepAnimChannel); + auto* animation = engine.createAnimationNode(config, "foo"); + + // link some of the created objects' inputs and outputs, so that none of the scripts + // generate a warning for having unlinked inputs or outputs on saving to file + engine.link( + *interface->getOutputs()->getChild("paramInt32"), + *script->getInputs()->getChild("paramInt32")); + + engine.link( + *script->getOutputs()->getChild("paramVec3f"), + *nodeBinding->getInputs()->getChild("rotation")); + + engine.link( + *animation->getOutputs()->getChild("duration"), + *script->getInputs()->getChild("paramFloat_2")); + + // use weak link because of circular dependency. The link has no meaning, it is just + // needed to make the setup valid (fee of dangling content) + engine.linkWeak( + *script->getOutputs()->getChild("paramFloat"), + *animation->getInputs()->getChild("progress")); + + engine.link( + *script->getOutputs()->getChild("paramBool"), + *passBinding->getInputs()->getChild("enabled")); + + engine.link( + *script->getOutputs()->getChild("paramInt32"), + *rgBinding->getInputs()->getChild("renderOrders")->getChild("nestedRG")); + + engine.link( + *script->getOutputs()->getChild("paramInt32"), + *meshBinding->getInputs()->getChild("vertexOffset")); + + engine.link( + *timer->getOutputs()->getChild("ticker_us"), + *script->getInputs()->getChild("paramInt64_2")); + + engine.link( + *script->getOutputs()->getChild("paramFloat"), + *appearanceBind->getInputs()->getChild("floatUniform")); + + engine.link( + *script->getOutputs()->getChild("paramFloat"), + *cameraBinding->getInputs()->getChild("frustum")->getChild("leftPlane")); + + // use weak link because of circular dependency + engine.linkWeak( + *anchor->getOutputs()->getChild("viewportCoords"), + *script->getInputs()->getChild("anchorData1")); + engine.linkWeak( + *anchor->getOutputs()->getChild("depth"), + *script->getInputs()->getChild("anchorData2")); + + engine.update(); + + engine.saveToFile(logicFile); + } + + void unlinkInput(const ramses::Property& inputProperty) + { + assert(inputProperty.hasIncomingLink()); + const ramses::Property& sourceProperty = *inputProperty.getIncomingLink()->source; + EXPECT_TRUE(viewer.getEngine().unlink(sourceProperty, inputProperty)); + } + }; + + TEST_F(ALogicViewerLua, loadLuaFileEmpty) + { + EXPECT_EQ(Result(), loadLua("")); + } + + TEST_F(ALogicViewerLua, setInputBool) + { + const Result ok; + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.paramBool.value = true)")); + auto* property = getInput("foo", "paramBool"); + EXPECT_TRUE(property->get().value()); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.paramBool.value = false)")); + EXPECT_FALSE(property->get().value()); + } + + TEST_F(ALogicViewerLua, setInputInt32) + { + const Result ok; + EXPECT_EQ(ok, loadLua(R"(rlogic.interfaces.foo.IN.paramInt32.value = 42)")); + auto* property = getInput("foo", "paramInt32"); + EXPECT_EQ(42, property->get()); + } + + TEST_F(ALogicViewerLua, setInputInt64) + { + const Result ok; + auto* property = getInput("foo", "paramInt64"); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.paramInt64.value = 42)")); + EXPECT_EQ(42, property->get()); + } + + TEST_F(ALogicViewerLua, setInputFloat) + { + const Result ok; + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.paramFloat.value = 42.5)")); + auto* property = getInput("foo", "paramFloat"); + EXPECT_FLOAT_EQ(42.5f, property->get().value()); + } + + TEST_F(ALogicViewerLua, setInputString) + { + const Result ok; + auto* property = getInput("foo", "paramString"); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.paramString.value = "Hello World")")); + EXPECT_EQ("Hello World", property->get()); + } + + TEST_F(ALogicViewerLua, setInputVec2f) + { + const Result ok; + auto* property = getInput("foo", "paramVec2f"); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.paramVec2f.value = {42.5, 1.3})")); + EXPECT_FLOAT_EQ(42.5f, property->get().value()[0]); + EXPECT_FLOAT_EQ(1.3f, property->get().value()[1]); + } + + TEST_F(ALogicViewerLua, setInputVec3f) + { + const Result ok; + auto* property = getInput("foo", "paramVec3f"); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.paramVec3f.value = {42.5, 1.3, 100000})")); + EXPECT_FLOAT_EQ(42.5f, property->get().value()[0]); + EXPECT_FLOAT_EQ(1.3f, property->get().value()[1]); + EXPECT_FLOAT_EQ(100000.f, property->get().value()[2]); + } + + TEST_F(ALogicViewerLua, setInputVec4f) + { + const Result ok; + auto* property = getInput("foo", "paramVec4f"); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.paramVec4f.value = {42.5, 1.3, -8.2, 0.0001})")); + EXPECT_FLOAT_EQ(42.5f, property->get().value()[0]); + EXPECT_FLOAT_EQ(1.3f, property->get().value()[1]); + EXPECT_FLOAT_EQ(-8.2f, property->get().value()[2]); + EXPECT_FLOAT_EQ(0.0001f, property->get().value()[3]); + } + + TEST_F(ALogicViewerLua, setInputVec2i) + { + const Result ok; + auto* property = getInput("foo", "paramVec2i"); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.paramVec2i.value = {5, -18})")); + EXPECT_EQ(ramses::vec2i({5, -18}), property->get()); + } + + TEST_F(ALogicViewerLua, setInputVec3i) + { + const Result ok; + auto* property = getInput("foo", "paramVec3i"); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.paramVec3i.value = {5, 0xffad, -10000})")); + EXPECT_EQ(ramses::vec3i({5, 0xffad, -10000}), property->get()); + } + + TEST_F(ALogicViewerLua, setInputVec4i) + { + const Result ok; + auto* property = getInput("foo", "paramVec4i"); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.paramVec4i.value = {2147483647, -2147483647, 5, -18})")); + EXPECT_EQ(ramses::vec4i({2147483647, -2147483647, 5, -18}), property->get()); + } + + TEST_F(ALogicViewerLua, setInputArrayFloat) + { + const Result ok; + auto* property = getInput("foo", "array"); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.array.value = {99, 118.119, 1.3, -8.2, 0.0001})")); + EXPECT_EQ(5u, property->getChildCount()); + EXPECT_FLOAT_EQ(99.f, property->getChild(0)->get().value()); + EXPECT_FLOAT_EQ(118.119f, property->getChild(1)->get().value()); + EXPECT_FLOAT_EQ(1.3f, property->getChild(2)->get().value()); + EXPECT_FLOAT_EQ(-8.2f, property->getChild(3)->get().value()); + EXPECT_FLOAT_EQ(0.0001f, property->getChild(4)->get().value()); + } + + TEST_F(ALogicViewerLua, setInputArrayByIndex) + { + auto* property = getInput("foo", "array"); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.array.value = {99, 42, 11, 13, 0.1} + rlogic.scripts.foo.IN.array[1].value = rlogic.scripts.foo.IN.array[5].value + )")); + EXPECT_FLOAT_EQ(0.1f, property->getChild(0)->get().value()); + } + + TEST_F(ALogicViewerLua, setInputArrayFloatOutOfBounds) + { + EXPECT_THAT(loadLua(R"(rlogic.scripts.foo.IN.array.value = {99, 118.119, 1.3, -8.2, 0.0001, 12})").getMessage(), + testing::HasSubstr("index 6 out of bounds for array array[5]")); + } + + TEST_F(ALogicViewerLua, setInputArrayByIndexOutOfBounds) + { + EXPECT_THAT(loadLua(R"(rlogic.scripts.foo.IN.array[6].value = 14)").getMessage(), + testing::HasSubstr("attempt to index field '?' (a nil value)")); + } + + TEST_F(ALogicViewerLua, setInputArrayByIndexBadSyntax) + { + EXPECT_THAT(loadLua(R"(rlogic.scripts.foo.IN.array.value[6] = 14)").getMessage(), + testing::HasSubstr("attempt to index field 'value' (a nil value)")); + } + + TEST_F(ALogicViewerLua, setInputStruct) + { + const Result ok; + auto* property = getInput("foo", "struct"); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.struct.value = { nested = {data1 = "Baz", data2 = 400}})")); + EXPECT_EQ("Baz", property->getChild("nested")->getChild("data1")->get()); + EXPECT_EQ(400, property->getChild("nested")->getChild("data2")->get()); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.struct.nested.data1.value = "Foo")")); + EXPECT_EQ("Foo", property->getChild("nested")->getChild("data1")->get()); + EXPECT_EQ(400, property->getChild("nested")->getChild("data2")->get()); + EXPECT_EQ(ok, loadLua(R"(rlogic.scripts.foo.IN.struct.nested.data2.value = -12)")); + EXPECT_EQ("Foo", property->getChild("nested")->getChild("data1")->get()); + EXPECT_EQ(-12, property->getChild("nested")->getChild("data2")->get()); + } + + TEST_F(ALogicViewerLua, setInputStructNotAKey) + { + EXPECT_THAT(loadLua(R"(rlogic.scripts.foo.IN.struct.value = { notAKey = {data1 = "Baz", data2 = 400}})").getMessage(), + testing::HasSubstr("Property not found in struct: notAKey")); + } + + TEST_F(ALogicViewerLua, getOutputBool) + { + const auto lua = R"( + if rlogic.scripts.foo.OUT.paramBool.value then + rlogic.scripts.foo.IN.paramString.value = "true" + else + rlogic.scripts.foo.IN.paramString.value = "false" + end + )"; + EXPECT_TRUE(getInput("foo", "paramBool")->set(false)); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(lua)); + EXPECT_EQ("true", getInput("foo", "paramString")->get().value()); + EXPECT_TRUE(getInput("foo", "paramBool")->set(true)); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(lua)); + EXPECT_EQ("false", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, getOutputInt32) + { + EXPECT_TRUE(getInput("foo", "paramInt32")->set(static_cast(43))); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.paramInt32.value + )")); + EXPECT_EQ("86", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, getOutputInt64) + { + EXPECT_TRUE(getInput("foo", "paramInt64")->set(static_cast(12))); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.paramInt64.value + )")); + EXPECT_EQ("13", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, getOutputFloat) + { + EXPECT_TRUE(getInput("foo", "paramFloat")->set(-3.2f)); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.paramFloat.value + )")); + EXPECT_THAT(getInput("foo", "paramString")->get().value(), testing::StartsWith("-9.6")); + } + + TEST_F(ALogicViewerLua, getOutputString) + { + EXPECT_TRUE(getInput("foo", "paramString")->set(std::string("Hello"))); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.paramString.value + )")); + EXPECT_EQ("Hellofoo", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, getOutputVec2f) + { + EXPECT_TRUE(getInput("foo", "paramFloat")->set(1.0f)); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.paramVec2f.value[2] + )")); + EXPECT_EQ("1", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, getOutputVec3f) + { + EXPECT_TRUE(getInput("foo", "paramFloat")->set(2.0f)); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.paramVec3f.value[3] + )")); + EXPECT_EQ("2", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, getOutputVec4f) + { + EXPECT_TRUE(getInput("foo", "paramFloat")->set(-1.0f)); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.paramVec4f.value[4] + )")); + EXPECT_EQ("-1", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, getOutputVec2i) + { + EXPECT_TRUE(getInput("foo", "paramInt32")->set(17)); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.paramVec2i.value[2] + )")); + EXPECT_EQ("17", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, getOutputVec3i) + { + EXPECT_TRUE(getInput("foo", "paramInt32")->set(18)); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.paramVec3i.value[3] + )")); + EXPECT_EQ("18", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, getOutputVec4i) + { + EXPECT_TRUE(getInput("foo", "paramInt32")->set(19)); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.paramVec4i.value[4] + )")); + EXPECT_EQ("19", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, getOutputArray) + { + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.array[5].value + )")); + EXPECT_EQ("50", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, getOutputArrayOutOfBounds) + { + EXPECT_THAT(loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.array[6].value + )").getMessage(), testing::HasSubstr("attempt to index field '?' (a nil value)")); + } + + TEST_F(ALogicViewerLua, getOutputStruct) + { + EXPECT_TRUE(getInput("foo", "paramInt32")->set(15)); + EXPECT_TRUE(getInput("foo", "paramString")->set(std::string("Hello"))); + EXPECT_EQ(Result(), viewer.update()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = rlogic.scripts.foo.OUT.struct.nested.data1.value..rlogic.scripts.foo.OUT.struct.nested.data2.value + )")); + EXPECT_EQ("Hello15", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, outputPropertyToString) + { + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = tostring(rlogic.scripts.foo.OUT.paramInt32) + )")); + EXPECT_EQ("ConstProperty: paramInt32", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, inputPropertyToString) + { + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = tostring(rlogic.scripts.foo.IN.paramInt32) + )")); + EXPECT_EQ("Property: paramInt32", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, nodeToString) + { + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.paramString.value = tostring(rlogic.scripts.foo) + )")); + EXPECT_EQ("LogicNode: foo", getInput("foo", "paramString")->get().value()); + } + + TEST_F(ALogicViewerLua, invalidView) + { + EXPECT_EQ(0u, viewer.getViewCount()); + auto view = viewer.getView(0u); + EXPECT_FALSE(view.isValid()); + EXPECT_EQ("", view.name()); + EXPECT_EQ("", view.description()); + EXPECT_EQ(nullptr, view.getInput(0)); + EXPECT_EQ(0u, view.getInputCount()); + } + + TEST_F(ALogicViewerLua, simpleView) + { + const Result ok; + auto* property = getInput("foo", "paramInt32"); + EXPECT_EQ(0u, viewer.getViewCount()); + EXPECT_FALSE(viewer.getView(0u).isValid()); + EXPECT_FALSE(viewer.getView(1u).isValid()); + EXPECT_EQ(0, property->get().value()); + + EXPECT_EQ(ok, loadLua(R"( + defaultView = { + name = "Default View", + description = "Description for the default view", + update = function(time_ms) + rlogic.interfaces.foo.IN.paramInt32.value = 1773 + end + } + + rlogic.views = {defaultView} + )")); + + EXPECT_EQ(1u, viewer.getViewCount()); + EXPECT_EQ(1u, viewer.getCurrentView()); + auto view = viewer.getView(1u); + EXPECT_TRUE(view.isValid()); + EXPECT_EQ("Default View", view.name()); + EXPECT_EQ("Description for the default view", view.description()); + EXPECT_EQ(0u, view.getInputCount()); + EXPECT_EQ(nullptr, view.getInput(0u)); + + EXPECT_EQ(ok, viewer.update()); + EXPECT_EQ(1773, property->get().value()); + } + + TEST_F(ALogicViewerLua, viewMissingUpdate) + { + EXPECT_THAT(loadLua(R"( + defaultView = { + name = "View with missing update() function", + } + + rlogic.views = {defaultView} + )").getMessage(), testing::HasSubstr("update() function is missing for current view")); + } + + TEST_F(ALogicViewerLua, viewReportsErrorOnUpdate) + { + EXPECT_THAT(loadLua(R"( + defaultView = { + name = "View with error in update", + update = function(time_ms) + error("view error during update") + end + } + + rlogic.views = {defaultView} + )").getMessage(), testing::HasSubstr("view error during update")); + } + + TEST_F(ALogicViewerLua, viewWithInput) + { + const Result ok; + auto* property = getInput("foo", "paramInt32"); + ASSERT_TRUE(property != nullptr); + + EXPECT_EQ(ok, loadLua(R"( + defaultView = { + name = "View1", + description = "View with 1 user input", + update = function(time_ms) + end, + inputs = {rlogic.interfaces.foo.IN.paramInt32} + } + + rlogic.views = {defaultView} + )")); + + EXPECT_EQ(1u, viewer.getViewCount()); + EXPECT_EQ(1u, viewer.getCurrentView()); + auto view = viewer.getView(1u); + EXPECT_TRUE(view.isValid()); + EXPECT_EQ("View1", view.name()); + EXPECT_EQ("View with 1 user input", view.description()); + EXPECT_EQ(1u, view.getInputCount()); + auto* input1 = view.getInput(0u); + EXPECT_EQ(property, input1); + } + + TEST_F(ALogicViewerLua, changeView) + { + const Result ok; + auto* property1 = getInput("foo", "paramInt32"); + auto* property2 = getInput("foo", "paramString"); + ASSERT_TRUE(property1 != nullptr); + ASSERT_TRUE(property2 != nullptr); + + EXPECT_EQ(ok, loadLua(R"( + view1 = { + name = "View1", + description = "View1 with 2 user inputs", + update = function(time_ms) + end, + inputs = { + rlogic.interfaces.foo.IN.paramInt32, + rlogic.scripts.foo.IN.paramString, + } + } + + view2 = { + name = "View2", + description = "View2 with no inputs", + update = function(time_ms) + rlogic.interfaces.foo.IN.paramInt32.value = 1773 + end, + } + + rlogic.views = {view1, view2} + )")); + + EXPECT_EQ(2u, viewer.getViewCount()); + EXPECT_EQ(1u, viewer.getCurrentView()); + auto view1 = viewer.getView(1u); + EXPECT_TRUE(view1.isValid()); + EXPECT_EQ("View1", view1.name()); + EXPECT_EQ("View1 with 2 user inputs", view1.description()); + EXPECT_EQ(2u, view1.getInputCount()); + EXPECT_EQ(property1, view1.getInput(0u)); + EXPECT_EQ(property2, view1.getInput(1u)); + + EXPECT_EQ(ok, viewer.update()); + EXPECT_EQ(0, property1->get().value()); + + viewer.setCurrentView(2u); + + EXPECT_EQ(2u, viewer.getCurrentView()); + auto view2 = viewer.getView(2u); + EXPECT_EQ("View2", view2.name()); + EXPECT_EQ("View2 with no inputs", view2.description()); + EXPECT_EQ(0u, view2.getInputCount()); + + EXPECT_EQ(ok, viewer.update()); + EXPECT_EQ(1773, property1->get().value()); + } + + TEST_F(ALogicViewerLua, callByName) + { + const Result ok; + auto* property1 = getInput("foo", "paramInt32"); + + EXPECT_EQ(ok, loadLua(R"( + function f1() + rlogic.interfaces.foo.IN.paramInt32.value = -91 + end + + function f2() + rlogic.interfaces.foo.IN.paramInt32.value = 908 + end + )")); + + EXPECT_EQ(0, property1->get().value()); + EXPECT_EQ(ok, viewer.call("f1")); + EXPECT_EQ(-91, property1->get().value()); + EXPECT_EQ(ok, viewer.call("f2")); + EXPECT_EQ(908, property1->get().value()); + EXPECT_THAT(viewer.call("fNotExisting").getMessage(), testing::StartsWith("attempt to call a nil value")); + } + + TEST_F(ALogicViewerLua, screenshot) + { + const Result ok; + MockScreenshot mock; + EXPECT_CALL(mock, screenshot("foo.png")); + EXPECT_EQ(ok, loadLua(R"( + rlogic.screenshot("foo.png") + )")); + } + + TEST_F(ALogicViewerLua, update) + { + EXPECT_TRUE(getInput("foo", "paramInt32")->set(static_cast(43))); + EXPECT_EQ(0, getOutput("foo", "paramInt32")->get()); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.update() + )")); + EXPECT_EQ(86, getOutput("foo", "paramInt32")->get()); + } + + TEST_F(ALogicViewerLua, link_unlink) + { + auto* in = getInput("foo", "ticker_us"); + auto* out = getOutput("foo", "paramInt64"); + EXPECT_FALSE(in->isLinked()); + EXPECT_FALSE(in->hasIncomingLink()); + EXPECT_FALSE(in->hasOutgoingLink()); + EXPECT_FALSE(out->isLinked()); + EXPECT_FALSE(out->hasIncomingLink()); + EXPECT_FALSE(out->hasOutgoingLink()); + + EXPECT_EQ(Result(), loadLua(R"( + rlogic.link(rlogic.scripts.foo.OUT.paramInt64, rlogic.timerNodes.foo.IN.ticker_us) + )")); + + EXPECT_TRUE(in->isLinked()); + EXPECT_TRUE(in->hasIncomingLink()); + EXPECT_FALSE(in->hasOutgoingLink()); + EXPECT_TRUE(out->isLinked()); + EXPECT_FALSE(out->hasIncomingLink()); + EXPECT_TRUE(out->hasOutgoingLink()); + + EXPECT_EQ(Result(), loadLua(R"( + rlogic.unlink(rlogic.scripts.foo.OUT.paramInt64, rlogic.timerNodes.foo.IN.ticker_us) + )")); + EXPECT_FALSE(in->isLinked()); + EXPECT_FALSE(in->hasIncomingLink()); + EXPECT_FALSE(in->hasOutgoingLink()); + EXPECT_FALSE(out->isLinked()); + EXPECT_FALSE(out->hasIncomingLink()); + EXPECT_FALSE(out->hasOutgoingLink()); + } + + TEST_F(ALogicViewerLua, nodeBindingByName) + { + auto* translation = getInput("foo", "translation"); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.nodeBindings.foo.IN.translation.value = {1,2,3} + )")); + EXPECT_FLOAT_EQ(1.f, translation->get().value()[0]); + EXPECT_FLOAT_EQ(2.f, translation->get().value()[1]); + EXPECT_FLOAT_EQ(3.f, translation->get().value()[2]); + } + + TEST_F(ALogicViewerLua, appearanceBindingByName) + { + auto* floatUniform = getInput("foo", "floatUniform"); + //unlink input to avoid generating error for setting value for a linked input + unlinkInput(*floatUniform); + + EXPECT_EQ(Result(), loadLua(R"( + rlogic.appearanceBindings.foo.IN.floatUniform.value = 9.1 + )")); + EXPECT_FLOAT_EQ(9.1f, floatUniform->get().value()); + } + + TEST_F(ALogicViewerLua, cameraBindingByName) + { + auto* frustum = getInput("foo", "frustum"); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.cameraBindings.foo.IN.frustum.nearPlane.value = 0.93 + )")); + EXPECT_FLOAT_EQ(0.93f, frustum->getChild("nearPlane")->get().value()); + } + + TEST_F(ALogicViewerLua, renderPassBindingByName) + { + auto* renderOrder = getInput("foo", "renderOrder"); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.renderPassBindings.foo.IN.renderOrder.value = 42 + )")); + EXPECT_EQ(42, renderOrder->get().value()); + } + + TEST_F(ALogicViewerLua, renderGroupBindingByName) + { + auto* renderOrder = getInput("rg", "renderOrders")->getChild("nestedRG"); + //unlink input to avoid generating error for setting value for a linked input + unlinkInput(*renderOrder); + + EXPECT_EQ(Result(), loadLua(R"( + rlogic.renderGroupBindings.rg.IN.renderOrders.nestedRG.value = 42 + )")); + EXPECT_EQ(42, renderOrder->get().value()); + } + + TEST_F(ALogicViewerLua, meshNodeBindingByName) + { + auto* vertexOffset = getInput("mn", "vertexOffset"); + auto* indexOffset = getInput("mn", "indexOffset"); + auto* indexCount = getInput("mn", "indexCount"); + auto* instanceCount = getInput("mn", "instanceCount"); + //unlink input to avoid generating error for setting value for a linked input + unlinkInput(*vertexOffset); + + EXPECT_EQ(Result(), loadLua(R"( + rlogic.meshNodeBindings.mn.IN.vertexOffset.value = 42 + rlogic.meshNodeBindings.mn.IN.indexOffset.value = 43 + rlogic.meshNodeBindings.mn.IN.indexCount.value = 44 + rlogic.meshNodeBindings.mn.IN.instanceCount.value = 45 + )")); + EXPECT_EQ(42, vertexOffset->get().value()); + EXPECT_EQ(43, indexOffset->get().value()); + EXPECT_EQ(44, indexCount->get().value()); + EXPECT_EQ(45, instanceCount->get().value()); + } + + TEST_F(ALogicViewerLua, timerNodeByName) + { + auto* ticker = getInput("foo", "ticker_us"); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.timerNodes.foo.IN.ticker_us.value = 19083 + )")); + EXPECT_EQ(19083, ticker->get().value()); + } + + TEST_F(ALogicViewerLua, animationNodeByName) + { + auto* progress = getInput("foo", "progress"); + //unlink input to avoid generating error for setting value for a linked input + unlinkInput(*progress); + + EXPECT_EQ(Result(), loadLua(R"( + rlogic.animationNodes.foo.IN.progress.value = 198 + )")); + EXPECT_FLOAT_EQ(198.f, progress->get().value()); + } + + TEST_F(ALogicViewerLua, anchorPointByName) + { + // unlink anchor from script inputs so that we can overwrite values + unlinkInput(*getInput("foo", "anchorData1")); + unlinkInput(*getInput("foo", "anchorData2")); + + // first overwrite with some dummy values to 'reset' them + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.anchorData1.value = { 1, 2 } + rlogic.scripts.foo.IN.anchorData2.value = 3 + )")); + auto* outScript = getNode("foo"); + EXPECT_FLOAT_EQ(1.f, GetOutput(outScript, "anchorData1")->get().value()[0]); + EXPECT_FLOAT_EQ(2.f, GetOutput(outScript, "anchorData1")->get().value()[1]); + EXPECT_FLOAT_EQ(3.f, GetOutput(outScript, "anchorData2")->get().value()); + + // now access values from anchor point and assign them to script inputs, overwriting the dummy values from before + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.anchorData1.value = { rlogic.anchorPoints.foo.OUT.viewportCoords.value[1], rlogic.anchorPoints.foo.OUT.viewportCoords.value[2] } + rlogic.scripts.foo.IN.anchorData2.value = rlogic.anchorPoints.foo.OUT.depth.value + )")); + + EXPECT_FLOAT_EQ(0.f, GetOutput(outScript, "anchorData1")->get().value()[0]); + EXPECT_FLOAT_EQ(8.f, GetOutput(outScript, "anchorData1")->get().value()[1]); + EXPECT_FLOAT_EQ(-0.01010102f, GetOutput(outScript, "anchorData2")->get().value()); + } + + TEST_F(ALogicViewerLua, interfaceById) + { + auto* node = getNode("foo"); + ASSERT_EQ(1u, node->getId()); + EXPECT_EQ(Result(), loadLua(R"(rlogic.interfaces[1].IN.paramInt32.value = 42)")); + EXPECT_EQ(42, GetInput(node, "paramInt32")->get().value()); + } + + TEST_F(ALogicViewerLua, scriptById) + { + auto* node = getNode("foo"); + ASSERT_EQ(2u, node->getId()); + EXPECT_EQ(Result(), loadLua(R"(rlogic.scripts[2].IN.paramInt64.value = 99)")); + EXPECT_EQ(99, GetInput(node, "paramInt64")->get().value()); + } + + TEST_F(ALogicViewerLua, nodeBindingById) + { + auto* node = getNode("foo"); + ASSERT_EQ(3u, node->getId()); + auto* translation = GetInput(node, "translation"); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.nodeBindings[3].IN.translation.value = {1,2,3} + )")); + EXPECT_FLOAT_EQ(1.f, translation->get().value()[0]); + EXPECT_FLOAT_EQ(2.f, translation->get().value()[1]); + EXPECT_FLOAT_EQ(3.f, translation->get().value()[2]); + } + + TEST_F(ALogicViewerLua, appearanceBindingById) + { + auto* node = getNode("foo"); + ASSERT_EQ(4u, node->getId()); + auto* floatUniform = GetInput(node, "floatUniform"); + //unlink input to avoid generating error for setting value for a linked input + unlinkInput(*floatUniform); + + EXPECT_EQ(Result(), loadLua(R"( + rlogic.appearanceBindings[4].IN.floatUniform.value = 9.1 + )")); + EXPECT_FLOAT_EQ(9.1f, floatUniform->get().value()); + } + + TEST_F(ALogicViewerLua, cameraBindingById) + { + auto* node = getNode("foo"); + ASSERT_EQ(5u, node->getId()); + auto* frustum = GetInput(node, "frustum"); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.cameraBindings[5].IN.frustum.nearPlane.value = 0.93 + )")); + EXPECT_FLOAT_EQ(0.93f, frustum->getChild("nearPlane")->get().value()); + } + + TEST_F(ALogicViewerLua, renderPassBindingById) + { + auto* rp = getNode("foo"); + ASSERT_EQ(6u, rp->getId()); + auto* renderOrder = GetInput(rp, "renderOrder"); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.renderPassBindings[6].IN.renderOrder.value = 42 + )")); + EXPECT_EQ(42, renderOrder->get().value()); + } + + TEST_F(ALogicViewerLua, renderGroupBindingById) + { + auto* rg = getNode("rg"); + ASSERT_EQ(9u, rg->getId()); + auto* renderOrder = GetInput(rg, "renderOrders")->getChild("nestedRG"); + //unlink input to avoid generating error for setting value for a linked input + unlinkInput(*renderOrder); + + EXPECT_EQ(Result(), loadLua(R"( + rlogic.renderGroupBindings[9].IN.renderOrders.nestedRG.value = 42 + )")); + EXPECT_EQ(42, renderOrder->get().value()); + } + + TEST_F(ALogicViewerLua, meshNodeBindingById) + { + auto* mn = getNode("mn"); + ASSERT_EQ(10u, mn->getId()); + auto* vertexOffset = GetInput(mn, "vertexOffset"); + auto* indexOffset = GetInput(mn, "indexOffset"); + auto* indexCount = GetInput(mn, "indexCount"); + auto* instanceCount = GetInput(mn, "instanceCount"); + + //unlink input to avoid generating error for setting value for a linked input + unlinkInput(*vertexOffset); + + EXPECT_EQ(Result(), loadLua(R"( + rlogic.meshNodeBindings[10].IN.vertexOffset.value = 42 + rlogic.meshNodeBindings[10].IN.indexOffset.value = 43 + rlogic.meshNodeBindings[10].IN.indexCount.value = 44 + rlogic.meshNodeBindings[10].IN.instanceCount.value = 45 + )")); + EXPECT_EQ(42, vertexOffset->get().value()); + EXPECT_EQ(43, indexOffset->get().value()); + EXPECT_EQ(44, indexCount->get().value()); + EXPECT_EQ(45, instanceCount->get().value()); + } + + TEST_F(ALogicViewerLua, timerNodeById) + { + auto* node = getNode("foo"); + ASSERT_EQ(7u, node->getId()); + auto* ticker = GetInput(node, "ticker_us"); + EXPECT_EQ(Result(), loadLua(R"( + rlogic.timerNodes[7].IN.ticker_us.value = 19083 + )")); + EXPECT_EQ(19083, ticker->get().value()); + } + + TEST_F(ALogicViewerLua, anchorPointById) + { + // unlink anchor from script inputs so that we can overwrite values + unlinkInput(*getInput("foo", "anchorData1")); + unlinkInput(*getInput("foo", "anchorData2")); + + EXPECT_EQ(Result(), loadLua(R"( + rlogic.scripts.foo.IN.anchorData1.value = { rlogic.anchorPoints[8].OUT.viewportCoords.value[1], rlogic.anchorPoints[8].OUT.viewportCoords.value[2] } + rlogic.scripts.foo.IN.anchorData2.value = rlogic.anchorPoints[8].OUT.depth.value + )")); + + auto* outScript = getNode("foo"); + EXPECT_FLOAT_EQ(0.f, GetOutput(outScript, "anchorData1")->get().value()[0]); + EXPECT_FLOAT_EQ(8.f, GetOutput(outScript, "anchorData1")->get().value()[1]); + EXPECT_FLOAT_EQ(-0.01010102f, GetOutput(outScript, "anchorData2")->get().value()); + } + + TEST_F(ALogicViewerLua, animationNodeById) + { + auto* node = getNode("foo"); + ASSERT_EQ(13u, node->getId()); + auto* progress = GetInput(node, "progress"); + //unlink input to avoid generating error for setting value for a linked input + unlinkInput(*progress); + + EXPECT_EQ(Result(), loadLua(R"( + rlogic.animationNodes[13].IN.progress.value = 198 + )")); + EXPECT_FLOAT_EQ(198.f, progress->get().value()); + } + + TEST_F(ALogicViewerLua, animationNodeWrongId) + { + auto* node = getNode("foo"); + ASSERT_EQ(13u, node->getId()); + EXPECT_THAT(loadLua(R"( + rlogic.animationNodes[89032].IN.progress.value = 198 + )").getMessage(), testing::HasSubstr("attempt to index field '?' (a nil value)")); + } + + TEST_F(ALogicViewerLua, iterateAnimationNodes) + { + auto* progress = getInput("foo", "progress"); + //unlink input to avoid generating error for setting value for a linked input + unlinkInput(*progress); + + EXPECT_EQ(Result(), loadLua(R"( + for node in rlogic.animationNodes() do + node.IN.progress.value = 78.6 + end + )")); + EXPECT_FLOAT_EQ(78.6f, progress->get().value()); + } + + TEST_F(ALogicViewerLua, reportsUserError) + { + EXPECT_THAT(loadLua(R"( + error("custom lua exception") + )").getMessage(), testing::HasSubstr("custom lua exception")); + } + + TEST_F(ALogicViewerLua, reportsParseError) + { + EXPECT_THAT(loadLua(R"( + foobar) + )").getMessage(), testing::HasSubstr("'=' expected near")); + } + + TEST_F(ALogicViewerLua, enableUpdateReport) + { + // set update interval to 1 to avoid random test failures + // (only the longest update is reported for an interval) + const size_t updateInterval = 1u; // in frames + EXPECT_TRUE(viewer.loadRamsesLogic(logicFile, m_scene)); + EXPECT_FALSE(viewer.isUpdateReportEnabled()); + auto& summary = viewer.getUpdateReport(); + + EXPECT_EQ(0u, summary.getTotalTime().maxValue.count()); + EXPECT_EQ(0u, summary.getSortTime().maxValue.count()); + EXPECT_EQ(0u, summary.getLinkActivations().maxValue); + EXPECT_EQ(0u, summary.getNodesExecuted().size()); + EXPECT_EQ(0u, summary.getNodesSkippedExecution().size()); + viewer.enableUpdateReport(true, updateInterval); + EXPECT_TRUE(viewer.isUpdateReportEnabled()); + + EXPECT_EQ(Result(), viewer.update()); + + EXPECT_EQ(2u, summary.getLinkActivations().maxValue); + EXPECT_EQ(11u, summary.getNodesExecuted().size()); + EXPECT_EQ(0u, summary.getNodesSkippedExecution().size()); + } +} diff --git a/client/logic/tools/ramses-logic-viewer-test/LogicViewerTest.cpp b/client/logic/tools/ramses-logic-viewer-test/LogicViewerTest.cpp new file mode 100644 index 000000000..d5ba398c5 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer-test/LogicViewerTest.cpp @@ -0,0 +1,59 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewerTestBase.h" + +const char* const logicFile = "test.rlogic"; + +namespace ramses::internal +{ + class ALogicViewer : public ALogicViewerBase + { + public: + ALogicViewer() + { + createLogicFile(); + } + + static void createLogicFile() + { + LogicEngine engine{ ramses::EFeatureLevel_Latest }; + engine.createLuaScript(R"( + function interface(IN,OUT) + IN.paramInt32 = Type:Int32() + end + )", + {} , "foo"); + engine.saveToFile(logicFile); + } + }; + + TEST_F(ALogicViewer, loadRamsesLogicNotAFile) + { + EXPECT_FALSE(viewer.loadRamsesLogic("notAFile", m_scene)); + } + + TEST_F(ALogicViewer, loadRamsesLogic) + { + EXPECT_TRUE(viewer.loadRamsesLogic(logicFile, m_scene)); + EXPECT_EQ(logicFile, viewer.getLogicFilename()); + EXPECT_EQ("", viewer.getLuaFilename()); + EXPECT_EQ(Result(), viewer.getLastResult()); + } + + TEST_F(ALogicViewer, loadLuaFileNotAFile) + { + EXPECT_TRUE(viewer.loadRamsesLogic(logicFile, m_scene)); + auto result = viewer.loadLuaFile("notAFile"); + EXPECT_FALSE(result.ok()); + EXPECT_EQ("cannot open notAFile: No such file or directory", result.getMessage()); + EXPECT_EQ(logicFile, viewer.getLogicFilename()); + EXPECT_EQ("notAFile", viewer.getLuaFilename()); + EXPECT_EQ(result, viewer.getLastResult()); + } +} diff --git a/client/logic/tools/ramses-logic-viewer-test/LogicViewerTestBase.cpp b/client/logic/tools/ramses-logic-viewer-test/LogicViewerTestBase.cpp new file mode 100644 index 000000000..9fe49397e --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer-test/LogicViewerTestBase.cpp @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewerTestBase.h" + +namespace ramses::internal +{ + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) must be static and non-const (see SetMockScreenshot) + ALogicViewerBase::MockScreenshot* ALogicViewerBase::m_mockScreenshot = nullptr; +} diff --git a/client/logic/tools/ramses-logic-viewer-test/LogicViewerTestBase.h b/client/logic/tools/ramses-logic-viewer-test/LogicViewerTestBase.h new file mode 100644 index 000000000..9612aa3fb --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer-test/LogicViewerTestBase.h @@ -0,0 +1,152 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "gmock/gmock.h" +#include "LogicViewer.h" +#include "RamsesTestUtils.h" +#include "WithTempDirectory.h" +#include +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/AnchorPoint.h" +#include "ramses-logic/SkinBinding.h" +#include "ramses-client.h" +#include + + +namespace ramses::internal +{ + class ALogicViewerBase : public ::testing::Test + { + public: + + class MockScreenshot + { + public: + MockScreenshot() + { + SetMockScreenshot(this); + } + + ~MockScreenshot() + { + SetMockScreenshot(nullptr); + } + + MOCK_METHOD(bool, screenshot, (const std::string&)); + }; + + ALogicViewerBase() + { + m_mockScreenshot = nullptr; + } + + Result loadLua(std::string_view lua) + { + const char* filename = "logicviewertest.lua"; + { + std::ofstream fileStream(filename, std::ofstream::binary); + if (!fileStream.is_open()) + { + throw std::runtime_error("filestream not open"); + } + fileStream << lua; + if (fileStream.bad()) + { + throw std::runtime_error("bad filestream"); + } + } + auto result = viewer.loadLuaFile(filename); + if (result.ok()) + { + result = viewer.update(); + } + return result; + } + + static bool doScreenshot(const std::string& filename) + { + return (m_mockScreenshot != nullptr) ? m_mockScreenshot->screenshot(filename) : false; + } + + template + T* getNode(std::string_view nodeName) + { + auto* node = viewer.getEngine().findByName(nodeName); + if (node == nullptr) + { + throw(std::runtime_error(fmt::format("Node not found: '{}'", nodeName))); + } + return node; + } + + static ramses::Property* GetInput(ramses::LogicNode* node, std::string_view propertyName) + { + auto* property = node->getInputs()->getChild(propertyName); + if (property == nullptr) + { + throw(std::runtime_error(fmt::format("Input property '{}' not found in node '{}'", propertyName, node->getName()))); + } + return property; + } + + template + ramses::Property* getInput(std::string_view nodeName, std::string_view propertyName) + { + return GetInput(getNode(nodeName), propertyName); + } + + static const ramses::Property* GetOutput(const ramses::LogicNode* node, std::string_view propertyName) + { + const auto* property = node->getOutputs()->getChild(propertyName); + if (property == nullptr) + { + throw(std::runtime_error(fmt::format("Output property '{}' not found in node '{}'", propertyName, node->getName()))); + } + return property; + } + + template + const ramses::Property* getOutput(std::string_view nodeName, std::string_view propertyName) + { + return GetOutput(getNode(nodeName), propertyName); + } + + static void SetMockScreenshot(MockScreenshot* mock) + { + m_mockScreenshot = mock; + } + + protected: + WithTempDirectory m_withTempDirectory; + LogicViewer viewer{ ramses::EFeatureLevel_Latest, doScreenshot }; + RamsesTestSetup m_ramses; + ramses::Scene* m_scene = { m_ramses.createScene() }; + ramses::Node* m_node = { m_scene->createNode() }; + ramses::OrthographicCamera* m_camera = { m_scene->createOrthographicCamera() }; + ramses::Appearance* m_appearance = { &RamsesTestSetup::CreateTrivialTestAppearance(*m_scene) }; + ramses::RenderPass* m_renderPass = { m_scene->createRenderPass() }; + ramses::RenderGroup* m_renderGroup = { m_scene->createRenderGroup() }; + ramses::RenderGroup* m_nestedRenderGroup = { m_scene->createRenderGroup() }; + ramses::MeshNode* m_meshNode = { m_scene->createMeshNode() }; + + private: + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) must be static and non-const (see SetMockScreenshot) + static MockScreenshot* m_mockScreenshot; + }; +} diff --git a/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_clearColor.png b/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_clearColor.png new file mode 100644 index 0000000000000000000000000000000000000000..64a2faa204654250f6ca2fa58561ef63dfdcf922 GIT binary patch literal 1730 zcmbW&Z%7ky7zgm1x1H{CIlC*Cw@UkSTxN9}r3L-jrdbP=)+_6M`X=Br*kZ+hVKdmg^e``;5pYgZe{ z6cWQQgL6$zK8E3`<1~0I8EBN|#TtPG_bTkrp)m|wpXT}*3-&p4?Cx^SV9(v_<_)Ik zPPf&QuJxboTA(JAX<{KVpDOOC1&xt&+roFb$uJ>GG6F8RA(qVnvk^AccZ^eAriwlmOmW!aTrO%Zm z5mtKJFQJqw@HIUClP95^9Pg1tEGa9V<(_R^>_ zI|B1$%|zV-Y=}9?=~&oF!$N5i1?#U(a)pAhJrD7Q&M2(0AX~#~Xr2qUZ4x#l*E|MdU$Wc|oHP!>Bs7?#@hy27tX dA0PkKn+Z|(jV&85UVS;9IajUCx#AFge*ymsj=TT> literal 0 HcmV?d00001 diff --git a/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_clearColorCmdLine.png b/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_clearColorCmdLine.png new file mode 100644 index 0000000000000000000000000000000000000000..fd99702f61bf37f96bcd7d956dc03f6cf6961125 GIT binary patch literal 1730 zcmbW&Z%h++7zgkx*B-9;-VWe zqNPx3jVN7Db4`|br;=#mKa7bQ9e*nVBaDzi5SBQmFEp~KI1^+NR$lq)dE=9Oe$SKd z^ZxgQ@y;D4I)f$%!sPy@u#_Ok%q2Q9kxX~UpCuZFMelZkIJQU-=tBOpMDU}#(CMw! z&5Ye^x9+mU4|?ssY<=+j$QN2Voh6lH>)CxThhv*hzZ}b_#uoLH^Sc($%cY);Z@0bp z?RwtH`h|a*BL_Mk3$qKSxuT=*kM#Ec-nrlMZtlP4$oL%h-JT}}??v=3$G~HQIdzR=;1+?#EzOZVOt?tdUO{(>xEG zlG`eJ8b@u@&gaT2^^jPL=S?eRhJn4Ik_UrE*x^RqKB>oV7s?!~6hTcDEtuj{5y?<; zgOMD((-5+k1z=N-f|%V2J0NK}3hYX5D?Tg2#<@+1<6-wqto%6xTcD^D-FotY6lf?H zx*aw}QZ1r@Q+W%bm{his)IIP8{N;A1!xqa_714}EJ_YfNo{bulPW)OD`dcc6J-XyN zXuAx0UHyl;DF&;}%f^3-u=m^%q~l?cmK@m}g-sMK_q4iUht`oBhStM6(nCK>D`1a~ zyWF-gXrMZ-u8+Zdd28{g2%F`OA_EUQWBO8F!N4Y)(>#$d>~JsHHWY_-lojZNc8yRb zFd*8{0dCX z{Q*lNtcI&Z4j$Iu@(GKgu$ew9tqj2GDk#IS57udOWeKY^vvLxrh($=xuVa&<>2$+D zO0dh<)H89%r3rUI(^bD{tqSZ$ZXNoH&u25DYv>ktTCne8$W<{~pxK^lBg((HmP=;W zMoBs{pbPdt^}BrEJF@JXRddXWkBiIEP)jlL^5^g+aehaMXJgyaPybO*-rM4zY7*y% kATs+O*9CRiOXG+7YgycTbMLp8uTL#y?(I7Zueqea|3k`-c>n+a literal 0 HcmV?d00001 diff --git a/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_red.png b/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_red.png new file mode 100644 index 0000000000000000000000000000000000000000..76a69b3f2a6cfaf8f175d4ebb3803359836a6da3 GIT binary patch literal 1730 zcmbW&Z%7ki90%~5x1H{CIlC*Cw@R&!%dBprw4guRG;8C?Cs`Chbn1GQmPLQeB<6Ik zMJv~?xIxfeB^30ZprX>WWol9(DP}=gy@`y<3e1us`-p^H^o?Oj8T08!^F}$<& z4L>q|oGCi==fK0xXRSL-zeayIhWkgEt()H${2I)d)qSgf6t&#y2#lOtvQj#8`I}mM zvm?-(c&OLITA2#2n1#hFtJHfktab;l-iyM_%yMo9HB&mKPjfwPNNz4~ukW=C*+0r( zl|4c=8Z%6jC<^wSNbdIQVSDQ}+r)OSl`pkTmw6>sFd>3X<#39S>hyT`?Ye-q)CYTI zD~MX{u$`isE5k13HK9`ite;uRF)Zw^ftD6fuyKMo(x$~9iN3lrzRhNlMa3lWD3vvF z1eHo}$2E^UK5v=TZnH!)6c%$`o=rBRh>wp(c?HL7lEW=1VFqv$s_Yt6mwVde^+>iJ$(zuKb_H2dfh62g5Qu-_-auITPc* Z^LA3qePheUi&tMwbk5c53a>as-(UZSjne=C literal 0 HcmV?d00001 diff --git a/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_red_500x700.png b/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_red_500x700.png new file mode 100644 index 0000000000000000000000000000000000000000..9624cc9b599c464bd1146a87745d3e35bce1dead GIT binary patch literal 774 zcmeAS@N?(olHy`uVBq!ia0y~yVEn?sz_f>v87QLs`+g{pVhivIaRt)<85pGh_bLIU zcYC@xhEy=Vy|nSHg1J!3Mb(T8DI7el3%|COE!1F{X~eRX>Bi4SN!#sq|06^9-wXR2 z{r}Ie3-9;*W|sH=SO5NR&8PqM@2^k(|9J0x|9|Jd|FikI|K9K4PyW}vmw)!Zq(S}P zLx%p(^%e&n|B>hWVDZ15+2-7T45820*?%Pd!yqhP=>KxM|BU@tXWhd4FZh3T@4K-5 zXJg%>_!s8Cn*S}*|B_p`VEzm7U%mep?0>;sJ6ryR`mffy#rC7xhC+}8ducA65=1WX ce`7u4>ZmdKI;Vst0Ie{v`~Uy| literal 0 HcmV?d00001 diff --git a/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_white.png b/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_white.png new file mode 100644 index 0000000000000000000000000000000000000000..11828f8a627a15b1a29fd4180b5e571800e3ec45 GIT binary patch literal 1712 zcmbW&Z%7ky7zgm1+fH}7oZc17Tc%dWC04glTJ&$5W^Ek#OOzsrPMxn(v#>IgnA5oy zTCT0QLC{?#81$b&BlBO=)TBaEOoOs|6B(rym?fsKdiA{NfzQLk_j&(3UbK6smWU%T z4Aa_nWn^QRGGT#YKES?5Ztlh~{DdvTl2fjj7(X%)iMIBNqx*aZOzR6-X z1`{Na*CC#XU>Q(*p+O!tMW1HX4D6zI zT_Bc(jn+oneO}nHC(6cw5UedPUBTUxF>wT8d=ZN=sT4-igwv~4cZ6zPBX=dUxDO@p zDXflJLPd~~M2%Oi?h3iW^71X6MH8}9gh{E#k-)=B=|ilUf%VoerdU60qQ^jpeXxoG zT>abuYd2aGxuvpBtV0oeCaYprP+|TUqN*>#-QlR|8fmf0+(XcK(Pgg@1r6v7zla^E z&th|!E2xQ|*}2EQx^dys|5r=k?7eYkY$3Dl L*qw3PD!Bdvq*94( literal 0 HcmV?d00001 diff --git a/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_yellow.png b/client/logic/tools/ramses-logic-viewer-test/res/ALogicViewerApp_yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..b2c2e75516eacb1c19848d12dc3dc31721b592f9 GIT binary patch literal 1730 zcmbW&Z%7ki90%~5x1H{CIlC*Cw@UkSTxN9}r3L-jrdbHPIK-4vue*os$Y(3xBTt^z>TJ@v4c>b)h}2 zZ}`!PlT7~6KZhT6K5yM^`Ze~uF4RB9?AZ1`_t#+BobKEGW2ohJM_}~)@-@=gE8o#^tsM*p9eTwTzZDMmtdrhxp*#1%e zs_Yjk(70iSL{YHsL}IsJ4?9q!*(tVrt$dMfrpznJf(a39GKW)yRISIm?^Fk@MLyV& zEjMDd!}f@3mJGX`-H4h5SU7Ml%&1 +#include +#include +#include +#include "ramses-framework-api/RamsesFrameworkTypes.h" +#include "ramses-framework-api/RamsesFrameworkTypes.h" +#include "ramses-sdk-build-config.h" + +class Arguments +{ +public: + void registerOptions(CLI::App& cli) + { + cli.description(R"( +Loads and shows a ramses scene from the . + and are auto-resolved if matching files with *.rlogic and *.lua extensions are found in the same path as . (Explicit arguments override autodetection.) +)"); + cli.add_option("ramsesfile", m_sceneFile, "Ramses scene file")->required()->check(CLI::ExistingFile); + cli.add_option("logicfile", m_logicFile, "Ramses Logic file")->check(CLI::ExistingFile); + cli.add_option("luafile,--lua", m_luaFile, "Lua configuration file")->check(CLI::ExistingFile); + auto exec = cli.add_option("--exec", m_luaFunction, "Calls the given lua function and exits."); + cli.add_option("--exec-lua", m_exec, "Calls the given lua code and exits.")->excludes(exec); + auto setWriteConfig = [&](const std::string& filename) { + m_luaFile = filename; + m_writeConfig = true; + }; + cli.add_option_function("--write-config", setWriteConfig, "Writes the default lua configuration file and exits") + ->expected(0, 1) + ->type_name("[FILE]") + ->excludes(exec); + cli.add_flag("--no-offscreen", m_noOffscreen, "Renders the scene directly to the window's framebuffer. Screenshot size will be the current window size."); + cli.set_version_flag("--version", ramses_sdk::RAMSES_SDK_RAMSES_VERSION); + + std::vector> loglevelMap{{"off", ramses::ELogLevel::Off}, + {"fatal", ramses::ELogLevel::Fatal}, + {"error", ramses::ELogLevel::Error}, + {"warn", ramses::ELogLevel::Warn}, + {"info", ramses::ELogLevel::Info}, + {"debug", ramses::ELogLevel::Debug}, + {"trace", ramses::ELogLevel::Trace}}; + cli.add_option("--log-level-console", m_ramsesLogLevel, "Sets log level for console messages.")->transform(CLI::CheckedTransformer(loglevelMap))->default_val(m_ramsesLogLevel); + } + + const std::string& sceneFile() const + { + return m_sceneFile; + } + + const std::string& logicFile() const + { + if (m_logicFile.empty()) + { + m_logicFile = ReplaceExtension(m_sceneFile, "rlogic"); + } + return m_logicFile; + } + + const std::string& luaFile() const + { + if (m_luaFile.empty()) + { + m_luaFile = ReplaceExtension(m_sceneFile, "lua"); + } + return m_luaFile; + } + + const std::string& luaFunction() const + { + return m_luaFunction; + } + + const std::string& exec() const + { + return m_exec; + } + + bool noOffscreen() const + { + return m_noOffscreen; + } + + bool writeConfig() const + { + return m_writeConfig; + } + + ramses::ELogLevel ramsesLogLevel() const + { + return m_ramsesLogLevel; + } + +private: + [[nodiscard]] static std::string ReplaceExtension(fs::path filename, const std::string& extension) + { + std::string retval; + filename.replace_extension(extension); + return filename.string(); + } + + std::string m_sceneFile; + mutable std::string m_logicFile; + mutable std::string m_luaFile; + std::string m_luaFunction; + std::string m_exec; + bool m_noOffscreen = false; + bool m_writeConfig = false; + ramses::ELogLevel m_ramsesLogLevel = ramses::ELogLevel::Error; +}; + diff --git a/client/logic/tools/ramses-logic-viewer/CMakeLists.txt b/client/logic/tools/ramses-logic-viewer/CMakeLists.txt new file mode 100644 index 000000000..a43916b18 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/CMakeLists.txt @@ -0,0 +1,67 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2021 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +set(logic_viewer_header + LogicViewerHeadlessApp.h + LogicViewerApp.h + LogicViewer.h + LogicViewerLog.h + LogicViewerSettings.h + LogicViewerLuaTypes.h + UpdateReportSummary.h + Arguments.h + SceneSetup.h) + +set(logic_viewer_src + LogicViewerHeadlessApp.cpp + LogicViewerApp.cpp + LogicViewer.cpp + LogicViewerLog.cpp + LogicViewerSettings.cpp + LogicViewerLuaTypes.cpp) + +add_library(ramses-logic-viewer-lib INTERFACE) +target_sources(ramses-logic-viewer-lib INTERFACE ${logic_viewer_header}) +target_sources(ramses-logic-viewer-lib INTERFACE ${logic_viewer_src}) +target_include_directories(ramses-logic-viewer-lib INTERFACE ${PROJECT_SOURCE_DIR}/client/logic/lib) +target_link_libraries(ramses-logic-viewer-lib INTERFACE imgui fmt::fmt sol2::sol2 lua::lua ramses-framework-cli) + +add_library(ramses-logic-viewer-gui-lib INTERFACE) +target_sources(ramses-logic-viewer-gui-lib INTERFACE + LogicViewerGuiApp.h + LogicViewerGui.h + LogicViewerGuiApp.cpp + LogicViewerGui.cpp + ImguiClientHelper.h + ImguiClientHelper.cpp) +target_link_libraries(ramses-logic-viewer-gui-lib INTERFACE ramses-logic-viewer-lib) + +createModule( + NAME ramses-logic-viewer + TYPE BINARY + ENABLE_INSTALL ${ramses-sdk_ENABLE_INSTALL} + SRC_FILES main.cpp + DEPENDENCIES ramses-logic-viewer-gui-lib + ramses-shared-lib +) + +if(ramses-sdk_BUILD_HEADLESS_SHARED_LIB) + createModule( + NAME ramses-logic-viewer-headless + TYPE BINARY + ENABLE_INSTALL ON + SRC_FILES main_headless.cpp + DEPENDENCIES ramses-logic-viewer-lib ramses-shared-lib-headless + ) + + if (ramses-sdk_ENABLE_INSTALL) + install( + TARGETS ramses-logic-viewer-headless + DESTINATION ${RAMSES_INSTALL_RUNTIME_PATH}) + endif() +endif() diff --git a/client/logic/tools/ramses-logic-viewer/ImguiClientHelper.cpp b/client/logic/tools/ramses-logic-viewer/ImguiClientHelper.cpp new file mode 100644 index 000000000..869237829 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/ImguiClientHelper.cpp @@ -0,0 +1,541 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ImguiClientHelper.h" + +namespace ramses +{ + namespace + { + const char* const ImguiFragShader = R"GLSL( + #version 100 + uniform sampler2D textureSampler; + + varying lowp vec2 v_texcoord; + varying lowp vec4 Frag_Color; + void main(void) + { + gl_FragColor = texture2D(textureSampler, v_texcoord.st)*Frag_Color; + })GLSL"; + + const char* const ImguiVertShader = R"GLSL( + #version 100 + uniform highp mat4 mvpMatrix; + attribute vec2 a_position; + attribute vec2 a_texcoord; + attribute vec4 Color; + varying vec2 v_texcoord; + varying vec4 Frag_Color; + void main(void) + { + gl_Position = mvpMatrix*vec4(a_position.xy, 0.0, 1.0); + v_texcoord = a_texcoord; + Frag_Color = Color; + })GLSL"; + + uint32_t GetSpecialChar(ramses::EKeyCode keyCode, const bool isShift) + { + switch (keyCode) + { + case ramses::EKeyCode_Space: + return ' '; + case ramses::EKeyCode_Numpad_Subtract: + case ramses::EKeyCode_Minus: + return isShift ? '_' : '-'; + case ramses::EKeyCode_Slash: + return isShift ? '?' : '/'; + case ramses::EKeyCode_Numpad_Decimal: + return '.'; + case ramses::EKeyCode_Period: + return isShift ? '>' : '.'; + case ramses::EKeyCode_Comma: + return isShift ? '<' : ','; + case ramses::EKeyCode_Backslash: + return isShift ? '|' : '\\'; + case ramses::EKeyCode_LeftBracket: + return isShift ? '{' : '['; + case ramses::EKeyCode_RightBracket: + return isShift ? '}' : ']'; + case ramses::EKeyCode_Equals: + return isShift ? '+' : '='; + case ramses::EKeyCode_Semicolon: + return isShift ? ':' : ';'; + case ramses::EKeyCode_Apostrophe: + return isShift ? '@' : '\''; + case ramses::EKeyCode_Grave: + return '`'; + case ramses::EKeyCode_NumberSign: + return isShift ? '~' : '#'; + default: + break; + } + + return 0U; + } + + // emulates an English keyboard for the available ramses key events + // (ramses (on Windows) does not consider keymaps, but sends virtual key events + uint32_t GetCharCode(uint32_t keyModifiers, ramses::EKeyCode keyCode) + { + const bool isShift = (keyModifiers & ramses::EKeyModifier_Shift) != 0u; + uint32_t retval = 0U; + if (ramses::EKeyCode_0 <= keyCode && keyCode <= ramses::EKeyCode_9) + { + const char* shift = ")!\"$$%^&*("; + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + retval = (isShift) ? shift[keyCode - ramses::EKeyCode_0] : ('0' + keyCode - ramses::EKeyCode_0); + } + else if (ramses::EKeyCode_A <= keyCode && keyCode <= ramses::EKeyCode_Z) + { + retval = (isShift) ? ('A' + keyCode - ramses::EKeyCode_A) : ('a' + keyCode - ramses::EKeyCode_A); + } + else if (ramses::EKeyCode_Numpad_0 <= keyCode && keyCode <= ramses::EKeyCode_Numpad_9) + { + retval = ('0' + keyCode - ramses::EKeyCode_Numpad_0); + } + else + { + retval = GetSpecialChar(keyCode, isShift); + } + return retval; + } + } // namespace + + ImguiClientHelper::ImguiClientHelper(ramses::RamsesClient& client, uint32_t width, uint32_t height, ramses::sceneId_t sceneid) + { + assert(ImGui::GetCurrentContext() != nullptr); // context should be already created + ramses::SceneConfig local; + m_imguiscene = client.createScene(sceneid, local, "imgui scene"); + + imguicamera = m_imguiscene->createOrthographicCamera("imgui camera"); + + imguicamera->setFrustum(0.0f, float(width), -float(height), 0.0f, 0.1f, 1.0f); + imguicamera->setViewport(0, 0, width, height); + imguicamera->translate({0.0f, 0.0f, 0.5f}); + imguicamera->scale({1.0, -1.0f, 1.0f}); + + ramses::RenderPass* renderPass = m_imguiscene->createRenderPass("imgui render pass"); + renderPass->setClearFlags(ramses::EClearFlags_None); + renderPass->setCamera(*imguicamera); + renderGroup = m_imguiscene->createRenderGroup(); + renderPass->addRenderGroup(*renderGroup); + ramses::EffectDescription effectDescImgui; + effectDescImgui.setFragmentShader(ImguiFragShader); + effectDescImgui.setVertexShader(ImguiVertShader); + effectDescImgui.setUniformSemantic("mvpMatrix", ramses::EEffectUniformSemantic::ModelViewProjectionMatrix); + + effect = m_imguiscene->createEffect(effectDescImgui); + + effect->findUniformInput("textureSampler", textureInput); + + effect->findAttributeInput("a_position", inputPosition); + effect->findAttributeInput("a_texcoord", inputUV); + effect->findAttributeInput("Color", inputColor); + + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize.x = static_cast(width); + io.DisplaySize.y = static_cast(height); + io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX); + io.MouseDown[0] = false; + io.MouseDown[1] = false; + io.MouseDown[2] = false; + io.MouseDown[3] = false; + io.MouseDown[4] = false; + io.KeyMap[ImGuiKey_Tab] = ramses::EKeyCode_Tab; + io.KeyMap[ImGuiKey_LeftArrow] = ramses::EKeyCode_Left; + io.KeyMap[ImGuiKey_RightArrow] = ramses::EKeyCode_Right; + io.KeyMap[ImGuiKey_UpArrow] = ramses::EKeyCode_Up; + io.KeyMap[ImGuiKey_DownArrow] = ramses::EKeyCode_Down; + io.KeyMap[ImGuiKey_PageUp] = ramses::EKeyCode_PageUp; + io.KeyMap[ImGuiKey_PageDown] = ramses::EKeyCode_PageDown; + io.KeyMap[ImGuiKey_Home] = ramses::EKeyCode_Home; + io.KeyMap[ImGuiKey_End] = ramses::EKeyCode_End; + io.KeyMap[ImGuiKey_Insert] = ramses::EKeyCode_Insert; + io.KeyMap[ImGuiKey_Delete] = ramses::EKeyCode_Delete; + io.KeyMap[ImGuiKey_Backspace] = ramses::EKeyCode_Backspace; + io.KeyMap[ImGuiKey_Space] = ramses::EKeyCode_Space; + io.KeyMap[ImGuiKey_Enter] = ramses::EKeyCode_Return; + io.KeyMap[ImGuiKey_Escape] = ramses::EKeyCode_Escape; + io.KeyMap[ImGuiKey_KeyPadEnter] = ramses::EKeyCode_Numpad_Enter; + io.KeyMap[ImGuiKey_A] = ramses::EKeyCode_A; + io.KeyMap[ImGuiKey_C] = ramses::EKeyCode_C; + io.KeyMap[ImGuiKey_V] = ramses::EKeyCode_V; + io.KeyMap[ImGuiKey_X] = ramses::EKeyCode_X; + io.KeyMap[ImGuiKey_Y] = ramses::EKeyCode_Y; + io.KeyMap[ImGuiKey_Z] = ramses::EKeyCode_Z; + + int texturewidth = 0; + int textureheight = 0; + unsigned char* pixels = nullptr; + io.Fonts->GetTexDataAsRGBA32(&pixels, &texturewidth, &textureheight); + ramses::MipLevelData mipLevelData(static_cast(texturewidth * textureheight * 4), pixels); + auto texture = m_imguiscene->createTexture2D(ramses::ETextureFormat::RGBA8, texturewidth, textureheight, 1, &mipLevelData); + sampler = m_imguiscene->createTextureSampler( + ramses::ETextureAddressMode::Repeat, + ramses::ETextureAddressMode::Repeat, + ramses::ETextureSamplingMethod::Linear, + ramses::ETextureSamplingMethod::Linear, + *texture); + + // At this point you've got the texture data and you need to upload that your your graphic system: + // After we have created the texture, store its pointer/identifier (_in whichever format your engine uses_) in 'io.Fonts->TexID'. + // This will be passed back to your via the renderer. Basically ImTextureID == void*. Read FAQ below for details about ImTextureID. + io.Fonts->TexID = sampler; + m_imguiscene->publish(ramses::EScenePublicationMode::LocalOnly); + m_imguiscene->flush(); + } + + ImguiClientHelper::~ImguiClientHelper() = default; + + void ImguiClientHelper::setDisplayId(ramses::displayId_t displayId) + { + m_displayId = displayId; + } + + void ImguiClientHelper::draw() + { + // cleanup previous frame/objects + for (auto m : todeleteMeshes) + { + m_imguiscene->destroy(*m); + } + todeleteMeshes.clear(); + for (auto m : todeleteRes) + { + m_imguiscene->destroy(*m); + } + todeleteRes.clear(); + renderGroup->removeAllRenderables(); + + // let imgui update itself + ImGui::Render(); + + // convert all imgui data to ramses objects + int meshnr = 1; + ImDrawData* draw_data = ImGui::GetDrawData(); + for (int n = 0; n < draw_data->CmdListsCount; n++) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) C-style array in 3rd party code. Bounds are checked by loop condition + const ImDrawList* cmd_list = draw_data->CmdLists[n]; + const ImDrawIdx* idx_buffer = cmd_list->IdxBuffer.Data; + auto ramsesind = m_imguiscene->createArrayResource(cmd_list->IdxBuffer.Size, idx_buffer); + todeleteRes.push_back(ramsesind); + unsigned int idx = 0U; + std::vector positions; + std::vector uv; + std::vector color; + for (auto& v : cmd_list->VtxBuffer) + { + positions.emplace_back(v.pos.x, v.pos.y); + uv.emplace_back(v.uv.x, v.uv.y); + + const auto alpha = (v.col >> 24U) & 0xFFU; + const auto blue = (v.col >> 16U) & 0xFFU; + const auto green = (v.col >> 8U) & 0xFFU; + const auto red = v.col & 0xFFU; + color.emplace_back( + static_cast(red) / 255.0f, + static_cast(green) / 255.0f, + static_cast(blue) / 255.0f, + static_cast(alpha) / 255.0f); + } + auto ramsespositions = m_imguiscene->createArrayResource(cmd_list->VtxBuffer.Size, positions.data()); + auto ramsesuv = m_imguiscene->createArrayResource(cmd_list->VtxBuffer.Size, uv.data()); + auto ramsescolor = m_imguiscene->createArrayResource(cmd_list->VtxBuffer.Size, color.data()); + todeleteRes.push_back(ramsespositions); + todeleteRes.push_back(ramsesuv); + todeleteRes.push_back(ramsescolor); + auto geobinding = m_imguiscene->createGeometryBinding(*effect); + todeleteMeshes.push_back(geobinding); + + geobinding->setInputBuffer(inputPosition, *ramsespositions); + geobinding->setInputBuffer(inputUV, *ramsesuv); + geobinding->setInputBuffer(inputColor, *ramsescolor); + geobinding->setIndices(*ramsesind); + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + { + const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; + ramses::Appearance* appearance = m_imguiscene->createAppearance(*effect, "triangle appearance"); + todeleteMeshes.push_back(appearance); + appearance->setBlendingFactors(ramses::EBlendFactor::SrcAlpha, ramses::EBlendFactor::OneMinusSrcAlpha, ramses::EBlendFactor::One, ramses::EBlendFactor::One); + appearance->setBlendingOperations(ramses::EBlendOperation::Add, ramses::EBlendOperation::Add); + appearance->setDepthFunction(ramses::EDepthFunc::Disabled); + appearance->setDepthWrite(ramses::EDepthWrite::Disabled); + appearance->setCullingMode(ramses::ECullMode::Disabled); + appearance->setInputTexture(textureInput, *static_cast(pcmd->TextureId)); + auto mesh = m_imguiscene->createMeshNode(); + todeleteMeshes.push_back(mesh); + mesh->setGeometryBinding(*geobinding); + mesh->setAppearance(*appearance); + const ImVec2 pos = draw_data->DisplayPos; + const ImVec2 displaysize = draw_data->DisplaySize; + appearance->setScissorTest(ramses::EScissorTest::Enabled, + static_cast(pcmd->ClipRect.x - pos.x), + static_cast(displaysize.y - pcmd->ClipRect.w - pos.y), + static_cast(pcmd->ClipRect.z - pos.x - pcmd->ClipRect.x - pos.x), + static_cast(pcmd->ClipRect.w - pos.y - pcmd->ClipRect.y - pos.y)); + mesh->setStartIndex(idx); + mesh->setIndexCount(pcmd->ElemCount); + renderGroup->addMeshNode(*mesh, meshnr++); + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) C-style array in 3rd party code. + idx_buffer += pcmd->ElemCount; + idx += pcmd->ElemCount; + } + } + + // apply new version of imgui scene + m_imguiscene->flush(); + } + + void ImguiClientHelper::sceneStateChanged(ramses::sceneId_t sceneId, ramses::RendererSceneState state) + { + m_scenes[sceneId].state = state; + } + + void ImguiClientHelper::sceneFlushed(ramses::sceneId_t sceneId, ramses::sceneVersionTag_t sceneVersion) + { + m_scenes[sceneId].version = sceneVersion; + } + + void ImguiClientHelper::offscreenBufferCreated(ramses::displayId_t /*displayId_t*/, ramses::displayBufferId_t offscreenBufferId, ramses::ERendererEventResult result) + { + if (ramses::ERendererEventResult::Failed != result) + { + m_offscreenBuffers.insert(offscreenBufferId); + } + } + + void ImguiClientHelper::offscreenBufferLinked(ramses::displayBufferId_t /*offscreenBufferId*/, ramses::sceneId_t consumerScene, ramses::dataConsumerId_t /*consumerId*/, bool success) + { + if (success) + { + m_scenesConsumingOffscreenBuffer[consumerScene].state = ramses::RendererSceneState::Unavailable; + } + } + + void ImguiClientHelper::displayCreated(ramses::displayId_t displayId, ramses::ERendererEventResult result) + { + if (ramses::ERendererEventResult::Failed != result) + { + m_displays.insert(displayId); + } + else + { + m_isRunning = false; + } + } + + void ImguiClientHelper::displayDestroyed(ramses::displayId_t displayId, ramses::ERendererEventResult result) + { + if (ramses::ERendererEventResult::Failed != result) + { + m_displays.erase(displayId); + } + else + { + m_isRunning = false; + } + } + + void ImguiClientHelper::mouseEvent(ramses::displayId_t displayId, ramses::EMouseEvent eventType, int32_t mousePosX, int32_t mousePosY) + { + if (!m_displayId.isValid() || displayId == m_displayId) + { + ImGuiIO& io = ImGui::GetIO(); + io.MousePos = {static_cast(mousePosX), static_cast(mousePosY)}; + switch (eventType) + { + case ramses::EMouseEvent::LeftButtonUp: + io.MouseDown[0] = false; + m_clickEvent = {mousePosX, mousePosY}; + break; + case ramses::EMouseEvent::LeftButtonDown: + io.MouseDown[0] = true; + break; + case ramses::EMouseEvent::WheelUp: + io.MouseWheel = 1; + break; + case ramses::EMouseEvent::WheelDown: + io.MouseWheel = -1; + break; + case ramses::EMouseEvent::RightButtonDown: + io.MouseDown[1] = true; + break; + case ramses::EMouseEvent::RightButtonUp: + io.MouseDown[1] = false; + break; + + default: + break; + } + } + } + + void ImguiClientHelper::keyEvent(ramses::displayId_t displayId, ramses::EKeyEvent eventType, uint32_t keyModifiers, ramses::EKeyCode keyCode) + { + if (!m_displayId.isValid() || displayId == m_displayId) + { + ImGuiIO& io = ImGui::GetIO(); + const bool pressed = (eventType == ramses::EKeyEvent::Pressed); + io.KeysDown[keyCode] = pressed; + + switch (keyCode) + { + case ramses::EKeyCode_ControlLeft: + case ramses::EKeyCode_ControlRight: + io.KeyCtrl = pressed; + return; + case ramses::EKeyCode_ShiftLeft: + case ramses::EKeyCode_ShiftRight: + io.KeyShift = pressed; + return; + case ramses::EKeyCode_AltLeft: + case ramses::EKeyCode_AltRight: + io.KeyAlt = pressed; + return; + case ramses::EKeyCode_WindowsLeft: + case ramses::EKeyCode_WindowsRight: + io.KeySuper = pressed; + return; + default: + break; + } + + if (pressed) + { + const uint32_t charCode = GetCharCode(keyModifiers, keyCode); + if (charCode != 0U) + io.AddInputCharacter(charCode); + } + } + } + + void ImguiClientHelper::windowResized(ramses::displayId_t displayId, uint32_t width, uint32_t height) + { + if (!m_displayId.isValid() || displayId == m_displayId) + { + imguicamera->setFrustum(0.0f, float(width), -float(height), 0.0f, 0.1f, 1.0f); + imguicamera->setViewport(0, 0, width, height); + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize.x = static_cast(width); + io.DisplaySize.y = static_cast(height); + } + } + + void ImguiClientHelper::windowClosed(ramses::displayId_t /*displayId*/) + { + m_isRunning = false; + } + + void ImguiClientHelper::framebufferPixelsRead(const uint8_t* pixelData, + const uint32_t pixelDataSize, + ramses::displayId_t displayId, + ramses::displayBufferId_t displayBuffer, + ramses::ERendererEventResult result) + { + static_cast(displayId); + static_cast(displayBuffer); + if (!m_screenshot.empty()) + { + m_screenshotSaved = false; + if (result == ramses::ERendererEventResult::Ok) + { + std::vector buffer; + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + buffer.insert(buffer.end(), &pixelData[0], &pixelData[pixelDataSize]); + m_screenshotSaved = ramses::RamsesUtils::SaveImageBufferToPng(m_screenshot, buffer, m_screenshotWidth, m_screenshotHeight, true); + } + m_screenshot.clear(); + } + } + + void ImguiClientHelper::dispatchClickEvent(std::pair& clickEventOut) + { + clickEventOut = m_clickEvent; + m_clickEvent = {std::numeric_limits::max(), std::numeric_limits::max()}; + } + + void ImguiClientHelper::dispatchEvents() + { + if (m_renderer) + { + m_renderer->dispatchEvents(*this); + m_renderer->getSceneControlAPI()->dispatchEvents(*this); + } + } + + bool ImguiClientHelper::saveScreenshot(const std::string& filename) + { + return saveScreenshot(filename, ramses::displayBufferId_t(), 0, 0, imguicamera->getViewportWidth(), imguicamera->getViewportHeight()); + } + + bool ImguiClientHelper::saveScreenshot(const std::string& filename, ramses::displayBufferId_t screenshotBuf, uint32_t x, uint32_t y, uint32_t width, uint32_t height) + { + if (m_renderer && m_screenshot.empty() && !filename.empty()) + { + m_screenshotSaved = false; + m_screenshot = filename; + m_screenshotWidth = width - x; + m_screenshotHeight = height - x; + m_renderer->readPixels(m_displayId, screenshotBuf, x, y, width, height); + m_renderer->flush(); + return true; + } + return false; + } + + bool ImguiClientHelper::waitForDisplay(ramses::displayId_t displayId) + { + return waitUntil([&] { return m_displays.find(displayId) != m_displays.end(); }); + } + + bool ImguiClientHelper:: waitForSceneState(ramses::sceneId_t sceneId, ramses::RendererSceneState state) + { + return waitUntil([&] { return m_scenes[sceneId].state == state; }); + } + + bool ImguiClientHelper::waitForSceneVersion(ramses::sceneId_t sceneId, ramses::sceneVersionTag_t version) + { + return waitUntil([&] { return m_scenes[sceneId].version == version; }); + } + + bool ImguiClientHelper::waitForOffscreenBufferCreated(const ramses::displayBufferId_t offscreenBufferId) + { + return waitUntil([&] { return m_offscreenBuffers.find(offscreenBufferId) != m_offscreenBuffers.end(); }); + } + + bool ImguiClientHelper::waitForOffscreenBufferLinked(const ramses::sceneId_t sceneId) + { + return waitUntil([&] { return m_scenesConsumingOffscreenBuffer.count(sceneId) > 0; }); + } + + bool ImguiClientHelper::waitForScreenshot() + { + waitUntil([&] { return m_screenshot.empty(); }); + return m_screenshotSaved; + } + + bool ImguiClientHelper::waitUntil(const std::function& conditionFunction) + { + if (m_renderer) + { + const std::chrono::steady_clock::time_point timeoutTS = std::chrono::steady_clock::now() + std::chrono::seconds{5}; + while (m_isRunning && !conditionFunction() && std::chrono::steady_clock::now() < timeoutTS) + { + std::this_thread::sleep_for(std::chrono::milliseconds{5}); // will give the renderer time to process changes + m_renderer->dispatchEvents(*this); + auto* sceneControl = m_renderer->getSceneControlAPI(); + sceneControl->dispatchEvents(*this); + } + } + + return conditionFunction(); + } + +} + diff --git a/client/logic/tools/ramses-logic-viewer/ImguiClientHelper.h b/client/logic/tools/ramses-logic-viewer/ImguiClientHelper.h new file mode 100644 index 000000000..7944e8b30 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/ImguiClientHelper.h @@ -0,0 +1,145 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ImguiWrapper.h" +#include "ramses-client.h" +#include "ramses-utils.h" +#include "ramses-renderer-api/RamsesRenderer.h" +#include "ramses-renderer-api/RendererSceneControl.h" +#include "ramses-renderer-api/IRendererEventHandler.h" +#include "ramses-renderer-api/IRendererSceneControlEventHandler.h" + +#include +#include +#include +#include + +namespace ramses +{ + class ImguiClientHelper : public ramses::RendererEventHandlerEmpty, public ramses::RendererSceneControlEventHandlerEmpty + { + public: + ImguiClientHelper(ramses::RamsesClient& client, uint32_t width, uint32_t height, ramses::sceneId_t sceneid); + ImguiClientHelper(const ImguiClientHelper&) = delete; + ImguiClientHelper& operator=(const ImguiClientHelper&) = delete; + ~ImguiClientHelper() override; + + /** + * @brief Used to filter input events for a certain display only + * @param[in] displayId The display to receive events for - invalid displayId handles all events + */ + void setDisplayId(ramses::displayId_t displayId); + + void setRenderer(ramses::RamsesRenderer* renderer); + + void dispatchEvents(); + + void dispatchClickEvent(std::pair& clickEventOut); + + void draw(); + + bool isRunning() const; + ramses::Scene* getScene(); + uint32_t getViewportWidth() const; + uint32_t getViewportHeight() const; + + bool saveScreenshot(const std::string& filename); + bool saveScreenshot(const std::string& filename, ramses::displayBufferId_t screenshotBuf, uint32_t x, uint32_t y, uint32_t width, uint32_t height); + + bool waitForDisplay(ramses::displayId_t displayId); + bool waitForSceneState(ramses::sceneId_t sceneId, ramses::RendererSceneState state); + bool waitForSceneVersion(ramses::sceneId_t sceneId, ramses::sceneVersionTag_t version); + bool waitForOffscreenBufferCreated(const ramses::displayBufferId_t offscreenBufferId); + bool waitForOffscreenBufferLinked(const ramses::sceneId_t sceneId); + bool waitForScreenshot(); + + // SceneControlEvents + void sceneStateChanged(ramses::sceneId_t sceneId, ramses::RendererSceneState state) override; + void sceneFlushed(ramses::sceneId_t sceneId, ramses::sceneVersionTag_t sceneVersion) override; + void offscreenBufferCreated(ramses::displayId_t displayId_t, ramses::displayBufferId_t offscreenBufferId, ramses::ERendererEventResult result) override; + void offscreenBufferLinked(ramses::displayBufferId_t offscreenBufferId, ramses::sceneId_t consumerScene, ramses::dataConsumerId_t consumerId, bool success) override; + + // Renderer events + void displayCreated(ramses::displayId_t displayId, ramses::ERendererEventResult result) override; + void displayDestroyed(ramses::displayId_t displayId, ramses::ERendererEventResult result) override; + void mouseEvent(ramses::displayId_t displayId, ramses::EMouseEvent eventType, int32_t mousePosX, int32_t mousePosY) override; + void keyEvent(ramses::displayId_t displayId, ramses::EKeyEvent eventType, uint32_t keyModifiers, ramses::EKeyCode keyCode) override; + void windowResized(ramses::displayId_t displayId, uint32_t width, uint32_t height) override; + void windowClosed(ramses::displayId_t displayId) override; + void framebufferPixelsRead(const uint8_t* pixelData, + const uint32_t pixelDataSize, + ramses::displayId_t displayId, + ramses::displayBufferId_t displayBuffer, + ramses::ERendererEventResult result) override; + + private: + bool waitUntil(const std::function& conditionFunction); + + struct SceneInfo + { + ramses::RendererSceneState state = ramses::RendererSceneState::Unavailable; + ramses::sceneVersionTag_t version = ramses::InvalidSceneVersionTag; + }; + + using SceneSet = std::unordered_map; + using OffscreenBufferSet = std::unordered_set; + using DisplaySet = std::unordered_set; + + SceneSet m_scenes; + SceneSet m_scenesAssignedToOffscreenBuffer; + SceneSet m_scenesConsumingOffscreenBuffer; + DisplaySet m_displays; + OffscreenBufferSet m_offscreenBuffers; + ramses::RamsesRenderer* m_renderer = nullptr; + ramses::displayId_t m_displayId; + ramses::Scene* m_imguiscene = nullptr; + ramses::OrthographicCamera* imguicamera = nullptr; + ramses::TextureSampler* sampler = nullptr; + ramses::Effect* effect = nullptr; + ramses::RenderGroup* renderGroup = nullptr; + ramses::UniformInput textureInput; + ramses::AttributeInput inputPosition; + ramses::AttributeInput inputUV; + ramses::AttributeInput inputColor; + std::vector todeleteMeshes; + std::vector todeleteRes; + std::pair m_clickEvent; + std::string m_screenshot; + uint32_t m_screenshotWidth = 0U; + uint32_t m_screenshotHeight = 0U; + bool m_screenshotSaved = false; + bool m_isRunning = true; + }; + + inline ramses::Scene* ImguiClientHelper::getScene() + { + return m_imguiscene; + } + + inline bool ImguiClientHelper::isRunning() const + { + return m_isRunning; + } + + inline void ImguiClientHelper::setRenderer(ramses::RamsesRenderer* renderer) + { + m_renderer = renderer; + } + + inline uint32_t ImguiClientHelper::getViewportWidth() const + { + return imguicamera->getViewportWidth(); + } + + inline uint32_t ImguiClientHelper::getViewportHeight() const + { + return imguicamera->getViewportHeight(); + } +} diff --git a/demo/ramses-dcsm-scene-references-demo/res/ramses-demo-scene-references-text-effect.frag b/client/logic/tools/ramses-logic-viewer/ImguiWrapper.h similarity index 68% rename from demo/ramses-dcsm-scene-references-demo/res/ramses-demo-scene-references-text-effect.frag rename to client/logic/tools/ramses-logic-viewer/ImguiWrapper.h index 8b1e0bc6d..833f06844 100644 --- a/demo/ramses-dcsm-scene-references-demo/res/ramses-demo-scene-references-text-effect.frag +++ b/client/logic/tools/ramses-logic-viewer/ImguiWrapper.h @@ -6,16 +6,17 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#version 100 +#pragma once -precision highp float; +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif -uniform sampler2D u_texture; +#include "imgui.h" +#include "misc/cpp/imgui_stdlib.h" -varying vec2 v_texcoord; +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif -void main(void) -{ - float a = texture2D(u_texture, v_texcoord).r; - gl_FragColor = vec4(1.0, 1.0, 1.0, a); -} diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewer.cpp b/client/logic/tools/ramses-logic-viewer/LogicViewer.cpp new file mode 100644 index 000000000..c7c6b58cb --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewer.cpp @@ -0,0 +1,374 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewer.h" +#include "LogicViewerLuaTypes.h" +#include "LogicViewerLog.h" +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/AnchorPoint.h" +#include "ramses-logic/SkinBinding.h" +#include "fmt/format.h" +#include "internals/SolHelper.h" +#include +#include + +namespace ramses +{ + namespace + { + // NOLINTNEXTLINE(performance-unnecessary-value-param) The signature is forced by SOL. Therefore we have to disable this warning. + int solExceptionHandler(lua_State* L, sol::optional maybe_exception, sol::string_view description) + { + if (maybe_exception) + { + return sol::stack::push(L, description); + } + return sol::stack::top(L); + } + + template + void registerNodeListType(sol::state& sol, const char* name) { + sol.new_usertype>(name, + sol::no_constructor, + sol::meta_function::index, + &NodeListWrapper::get, + sol::meta_function::call, + &NodeListWrapper::iterator, + sol::meta_function::to_string, + [=]() { return name; }); + std::string iteratorName = name; + iteratorName += "Iterator"; + sol.new_usertype>(iteratorName, sol::no_constructor, sol::meta_function::call, &NodeListIterator::call); + } + } // namespace + + struct LogicWrapper + { + explicit LogicWrapper(LogicEngine& logicEngine, sol::state& sol) + : views(sol.create_table()) + , interfaces(logicEngine) + , scripts(logicEngine) + , animations(logicEngine) + , timers(logicEngine) + , nodeBindings(logicEngine) + , appearanceBindings(logicEngine) + , cameraBindings(logicEngine) + , renderPassBindings(logicEngine) + , renderGroupBindings(logicEngine) + , meshNodeBindings(logicEngine) + , anchorPoints(logicEngine) + , skinBindings(logicEngine) + { + } + + sol::table views; + + NodeListWrapper interfaces; + NodeListWrapper scripts; + NodeListWrapper animations; + NodeListWrapper timers; + NodeListWrapper nodeBindings; + NodeListWrapper appearanceBindings; + NodeListWrapper cameraBindings; + NodeListWrapper renderPassBindings; + NodeListWrapper renderGroupBindings; + NodeListWrapper meshNodeBindings; + NodeListWrapper anchorPoints; + NodeListWrapper skinBindings; + }; + + const char* const LogicViewer::ltnModule = "rlogic"; + const char* const LogicViewer::ltnScript = "scripts"; + const char* const LogicViewer::ltnInterface = "interfaces"; + const char* const LogicViewer::ltnAnimation = "animationNodes"; + const char* const LogicViewer::ltnTimer = "timerNodes"; + const char* const LogicViewer::ltnNode = "nodeBindings"; + const char* const LogicViewer::ltnAppearance = "appearanceBindings"; + const char* const LogicViewer::ltnCamera = "cameraBindings"; + const char* const LogicViewer::ltnRenderPass = "renderPassBindings"; + const char* const LogicViewer::ltnRenderGroup = "renderGroupBindings"; + const char* const LogicViewer::ltnMeshNode = "meshNodeBindings"; + const char* const LogicViewer::ltnAnchorPoint = "anchorPoints"; + const char* const LogicViewer::ltnSkinBinding = "skinBindings"; + const char* const LogicViewer::ltnScreenshot = "screenshot"; + const char* const LogicViewer::ltnViews = "views"; + const char* const LogicViewer::ltnLink = "link"; + const char* const LogicViewer::ltnUnlink = "unlink"; + const char* const LogicViewer::ltnUpdate = "update"; + const char* const LogicViewer::ltnIN = "IN"; + const char* const LogicViewer::ltnOUT = "OUT"; + + const char* const LogicViewer::ltnPropertyValue = "value"; + const char* const LogicViewer::ltnViewUpdate = "update"; + const char* const LogicViewer::ltnViewInputs = "inputs"; + const char* const LogicViewer::ltnViewName = "name"; + const char* const LogicViewer::ltnViewDescription = "description"; + + LogicViewer::LogicViewer(ramses::EFeatureLevel engineFeatureLevel, ScreenshotFunc screenshotFunc) + : m_logicEngine{ engineFeatureLevel } + , m_screenshotFunc(std::move(screenshotFunc)) + { + m_startTime = std::chrono::steady_clock::now(); + } + + bool LogicViewer::loadRamsesLogic(const std::string& filename, ramses::Scene* scene) + { + m_logicFilename = filename; + return m_logicEngine.loadFromFile(filename, scene); + } + + Result LogicViewer::loadLuaFile(const std::string& filename) + { + m_result = Result(); + m_sol = sol::state(); + m_sol.open_libraries(sol::lib::base, sol::lib::string, sol::lib::math, sol::lib::table, sol::lib::debug); + m_sol.set_exception_handler(&solExceptionHandler); + registerNodeListType(m_sol, "Interfaces"); + registerNodeListType(m_sol, "LuaScripts"); + registerNodeListType(m_sol, "AnimationNodes"); + registerNodeListType(m_sol, "TimerNodes"); + registerNodeListType(m_sol, "NodeBindings"); + registerNodeListType(m_sol, "AppearanceBindings"); + registerNodeListType(m_sol, "CameraBindings"); + registerNodeListType(m_sol, "RenderPassBindings"); + registerNodeListType(m_sol, "RenderGroupBindings"); + registerNodeListType(m_sol, "MeshNodeBindings"); + registerNodeListType(m_sol, "AnchorPoints"); + registerNodeListType(m_sol, "SkinBindings"); + m_sol.new_usertype("LogicNode", sol::meta_function::index, &LogicNodeWrapper::get, sol::meta_function::to_string, &LogicNodeWrapper::toString); + m_sol.new_usertype("LogicProperty", + ltnPropertyValue, + sol::property(&PropertyWrapper::getValue, &PropertyWrapper::setValue), + sol::meta_function::index, + &PropertyWrapper::get, + sol::meta_function::to_string, + &PropertyWrapper::toString); + m_sol.new_usertype("ConstLogicProperty", + ltnPropertyValue, + sol::readonly_property(&ConstPropertyWrapper::getValue), + sol::meta_function::index, + &ConstPropertyWrapper::get, + sol::meta_function::to_string, + &ConstPropertyWrapper::toString); + m_sol.new_usertype( + "RamsesLogic", + sol::no_constructor, + ltnInterface, + sol::readonly(&LogicWrapper::interfaces), + ltnScript, + sol::readonly(&LogicWrapper::scripts), + ltnAnimation, + sol::readonly(&LogicWrapper::animations), + ltnTimer, + sol::readonly(&LogicWrapper::timers), + ltnNode, + sol::readonly(&LogicWrapper::nodeBindings), + ltnAppearance, + sol::readonly(&LogicWrapper::appearanceBindings), + ltnCamera, + sol::readonly(&LogicWrapper::cameraBindings), + ltnRenderPass, + sol::readonly(&LogicWrapper::renderPassBindings), + ltnRenderGroup, + sol::readonly(&LogicWrapper::renderGroupBindings), + ltnMeshNode, + sol::readonly(&LogicWrapper::meshNodeBindings), + ltnAnchorPoint, + sol::readonly(&LogicWrapper::anchorPoints), + ltnSkinBinding, + sol::readonly(&LogicWrapper::skinBindings), + ltnViews, + &LogicWrapper::views, + ltnScreenshot, + [&](const std::string& screenshotFile) { + updateEngine(); + if (!m_screenshotFunc) + { + throw sol::error("No screenshots available in current configuration"); + } + return m_screenshotFunc(screenshotFile); + }, + ltnUpdate, + [&]() { updateEngine(); }, + ltnLink, + [&](const ConstPropertyWrapper& src, const PropertyWrapper& target) { return m_logicEngine.link(src.m_property, target.m_property); }, + ltnUnlink, + [&](const ConstPropertyWrapper& src, const PropertyWrapper& target) { return m_logicEngine.unlink(src.m_property, target.m_property); }); + + m_sol[ltnModule] = LogicWrapper(m_logicEngine, m_sol); + + m_luaFilename = filename; + if (!filename.empty()) + { + load(m_sol.load_file(filename)); + } + return m_result; + } + + void LogicViewer::load(sol::load_result&& loadResult) + { + if (!loadResult.valid()) + { + sol::error err = loadResult; + m_result = Result(err.what()); + } + else + { + sol::protected_function mainFunction = loadResult; + auto mainResult = mainFunction(); + if (!mainResult.valid()) + { + sol::error err = mainResult; + m_result = Result(err.what()); + } + } + } + + Result LogicViewer::exec(const std::string& lua) + { + load(m_sol.load_buffer(lua.c_str(), lua.size())); + return m_result; + } + + Result LogicViewer::call(const std::string& functionName) + { + sol::protected_function protectedFunction = m_sol[functionName]; + auto result = protectedFunction(); + if (!result.valid()) + { + sol::error err = result; + m_result = Result(err.what()); + } + return m_result; + } + + Result LogicViewer::update() + { + updateEngine(); + // don't update if there's already an error + if (m_result.ok()) + { + sol::optional view = m_sol[ltnModule][ltnViews][m_view]; + if (view) + { + const auto elapsed = std::chrono::steady_clock::now() - m_startTime; + const auto millisecs = std::chrono::duration_cast(elapsed).count(); + sol::optional func = (*view)[ltnViewUpdate]; + if (func) + { + auto result = (*func)(millisecs); + if (!result.valid()) + { + sol::error err = result; + std::cerr << err.what() << std::endl; + m_result = Result(err.what()); + return m_result; + } + } + else + { + m_result = Result("update() function is missing for current view"); + return m_result; + } + } + } + return Result(); + } + + size_t LogicViewer::getViewCount() const + { + sol::optional tbl = m_sol[ltnModule][ltnViews]; + return tbl ? tbl->size() : 0U; + } + + void LogicViewer::setCurrentView(size_t viewId) + { + if (viewId >= 1U && viewId <= getViewCount()) + m_view = viewId; + } + + LogicViewer::View LogicViewer::getView(size_t viewId) const + { + sol::optional tbl = m_sol[ltnModule][ltnViews][viewId]; + return View(std::move(tbl)); + } + + void LogicViewer::updateEngine() + { + m_logicEngine.update(); + if (m_updateReportEnabled) + { + m_updateReportSummary.add(m_logicEngine.getLastUpdateReport()); + } + } + + Result LogicViewer::saveDefaultLuaFile(const std::string& filename, const LogicViewerSettings& settings) + { + std::ofstream outfile; + outfile.open(filename, std::ios::out | std::ios::binary); + if (!outfile.is_open()) + { + return Result(fmt::format("Could not open file: {}", filename)); + } + LogicViewerLog log(m_logicEngine, settings); + log.logText("function default()\n"); + + log.logAllInputs("--Interfaces\n", LogicViewer::ltnInterface); + log.logAllInputs("--Scripts\n", LogicViewer::ltnScript); + log.logAllInputs("--Node bindings\n", LogicViewer::ltnNode); + log.logAllInputs("--Appearance bindings\n", LogicViewer::ltnAppearance); + log.logAllInputs("--Camera bindings\n", LogicViewer::ltnCamera); + log.logAllInputs("--RenderPass bindings\n", LogicViewer::ltnRenderPass); + log.logAllInputs("--RenderGroup bindings\n", LogicViewer::ltnRenderGroup); + log.logAllInputs("--MeshNode bindings\n", LogicViewer::ltnMeshNode); + log.logAllInputs("--Anchor points\n", LogicViewer::ltnAnchorPoint); + log.logAllInputs("--Skin bindings\n", LogicViewer::ltnSkinBinding); + + log.logText("end\n\n"); + const char* code = R"( +defaultView = { + name = "Default", + description = "", + update = function(time_ms) + default() + end +} + +rlogic.views = {defaultView} + +-- sample test function for automated image base tests +-- can be executed by command line parameter --exec=test_default +function test_default() + -- modify properties + default() + -- stores a screenshot (relative to the working directory) + rlogic.screenshot("test_default.png") +end +)"; + log.logText(code); + outfile << log.getText(); + if (outfile.bad()) + { + return Result(fmt::format("Could not write to file: {}", filename)); + } + outfile.close(); + return Result(); + } +} + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewer.h b/client/logic/tools/ramses-logic-viewer/LogicViewer.h new file mode 100644 index 000000000..cf3695683 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewer.h @@ -0,0 +1,216 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "LogicViewerLuaTypes.h" +#include "UpdateReportSummary.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/LogicEngine.h" +#include "Result.h" +#include +#include + +namespace ramses +{ + class LogicEngine; + struct LogicViewerSettings; + class Property; + + class LogicViewer + { + public: + using ScreenshotFunc = std::function; + + // global tokens + static const char* const ltnModule; + static const char* const ltnScript; + static const char* const ltnInterface; + static const char* const ltnAnimation; + static const char* const ltnTimer; + static const char* const ltnNode; + static const char* const ltnAppearance; + static const char* const ltnCamera; + static const char* const ltnRenderPass; + static const char* const ltnRenderGroup; + static const char* const ltnMeshNode; + static const char* const ltnAnchorPoint; + static const char* const ltnSkinBinding; + static const char* const ltnScreenshot; + static const char* const ltnViews; + static const char* const ltnLink; + static const char* const ltnUnlink; + static const char* const ltnUpdate; + static const char* const ltnIN; + static const char* const ltnOUT; + // property type + static const char* const ltnPropertyValue; + // view type + static const char* const ltnViewUpdate; + static const char* const ltnViewInputs; + static const char* const ltnViewName; + static const char* const ltnViewDescription; + + class View + { + public: + explicit View(sol::optional&& tbl) + : m_tbl(std::move(tbl)) + { + } + + [[nodiscard]] bool isValid() const + { + return m_tbl ? true : false; + } + + [[nodiscard]] std::string name() const + { + if (m_tbl) + return (*m_tbl).get_or(ltnViewName, std::string()); + return std::string(); + } + + [[nodiscard]] size_t getInputCount() const + { + if (m_tbl) + { + sol::optional inputs = (*m_tbl)[ltnViewInputs]; + if (inputs) + { + return (*inputs).size(); + } + } + return 0U; + } + + [[nodiscard]] Property* getInput(size_t index) const + { + if (m_tbl) + { + sol::optional input = (*m_tbl)[ltnViewInputs][index + 1]; + if (input) + { + return &input->m_property; + } + } + return nullptr; + } + + [[nodiscard]] std::string description() const + { + if (m_tbl) + return (*m_tbl).get_or(ltnViewDescription, std::string()); + return std::string(); + } + + private: + sol::optional m_tbl; + }; + + LogicViewer(ramses::EFeatureLevel engineFeatureLevel, ScreenshotFunc screenshotFunc); + + [[nodiscard]] bool loadRamsesLogic(const std::string& filename, ramses::Scene* scene); + + [[nodiscard]] const std::string& getLogicFilename() const; + + [[nodiscard]] Result loadLuaFile(const std::string& filename); + + [[nodiscard]] Result exec(const std::string& lua); + + [[nodiscard]] Result call(const std::string& functionName); + + [[nodiscard]] const std::string& getLuaFilename() const; + + [[nodiscard]] ramses::LogicEngine& getEngine(); + + [[nodiscard]] Result update(); + + [[nodiscard]] size_t getViewCount() const; + + void setCurrentView(size_t viewId); + + [[nodiscard]] size_t getCurrentView() const; + + [[nodiscard]] View getView(size_t viewId) const; + + [[nodiscard]] const Result& getLastResult() const; + + void enableUpdateReport(bool enable, size_t interval); + + [[nodiscard]] bool isUpdateReportEnabled() const; + + [[nodiscard]] const UpdateReportSummary& getUpdateReport() const; + + /** + * saves a simple default lua configuration that can be used as a starting point + */ + [[nodiscard]] Result saveDefaultLuaFile(const std::string& filename, const LogicViewerSettings& settings); + + private: + void updateEngine(); + + void load(sol::load_result&& loadResult); + + ramses::LogicEngine m_logicEngine; + ScreenshotFunc m_screenshotFunc; + std::string m_logicFilename; + std::string m_luaFilename; + sol::state m_sol; + size_t m_view = 1U; + Result m_result; + + std::chrono::steady_clock::time_point m_startTime; + + bool m_updateReportEnabled = false; + UpdateReportSummary m_updateReportSummary; + }; + + inline LogicEngine& LogicViewer::getEngine() + { + return m_logicEngine; + } + + inline const std::string& LogicViewer::getLuaFilename() const + { + return m_luaFilename; + } + + inline const std::string& LogicViewer::getLogicFilename() const + { + return m_logicFilename; + } + + inline const Result& LogicViewer::getLastResult() const + { + return m_result; + } + + inline void LogicViewer::enableUpdateReport(bool enable, size_t interval) + { + m_logicEngine.enableUpdateReport(enable); + m_updateReportSummary.setInterval(interval); + m_updateReportEnabled = enable; + } + + inline bool LogicViewer::isUpdateReportEnabled() const + { + return m_updateReportEnabled; + } + + inline const UpdateReportSummary& LogicViewer::getUpdateReport() const + { + return m_updateReportSummary; + } + + inline size_t LogicViewer::getCurrentView() const + { + return m_view; + } +} + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerApp.cpp b/client/logic/tools/ramses-logic-viewer/LogicViewerApp.cpp new file mode 100644 index 000000000..28e9cad6a --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerApp.cpp @@ -0,0 +1,145 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewerApp.h" + +#include "Arguments.h" +#include "LogicViewer.h" +#include "LogicViewerSettings.h" +#include "ramses-logic/Logger.h" +#include "ImguiWrapper.h" + +namespace ramses +{ + LogicViewerApp::LogicViewerApp() + { + m_imguiContext = ImGui::CreateContext(); + } + + LogicViewerApp::~LogicViewerApp() + { + ImGui::DestroyContext(m_imguiContext); + } + + int LogicViewerApp::GetFeatureLevelFromFiles(const std::string& sceneFilename, const std::string& logicFilename, EFeatureLevel& featureLevel) + { + EFeatureLevel tmpLevel{EFeatureLevel_01}; + if (!RamsesClient::GetFeatureLevelFromFile(sceneFilename, tmpLevel)) + { + std::cerr << "Could not parse feature level from scene file " << sceneFilename << std::endl; + return static_cast(ExitCode::ErrorLoadScene); + } + if (!LogicEngine::GetFeatureLevelFromFile(logicFilename, featureLevel)) + { + std::cerr << "Could not parse feature level from logic file " << logicFilename << std::endl; + return static_cast(ExitCode::ErrorLoadLogic); + } + if (tmpLevel != featureLevel) + { + std::cerr << "Feature levels of the scene file and the logic file do not match!" << std::endl; + return static_cast(ExitCode::ErrorLoadScene); + } + return 0; + } + + int LogicViewerApp::loadScene(const Arguments& args, EFeatureLevel featureLevel) + { + ramses::RamsesFrameworkConfig frameworkConfig{featureLevel}; + frameworkConfig.setPeriodicLogInterval(std::chrono::seconds(0)); + frameworkConfig.setLogLevelConsole(args.ramsesLogLevel()); + ramses::Logger::SetLogVerbosityLimit(args.ramsesLogLevel()); + m_framework = std::make_unique(frameworkConfig); + m_client = m_framework->createClient("ramses-logic-viewer"); + if (!m_client) + { + std::cerr << "Could not create ramses client" << std::endl; + return static_cast(ExitCode::ErrorRamsesClient); + } + + m_scene = m_client->loadSceneFromFile(args.sceneFile()); + if (!m_scene) + { + std::cerr << "Failed to load scene: " << args.sceneFile() << std::endl; + return static_cast(ExitCode::ErrorLoadScene); + } + m_scene->publish(); + m_scene->flush(); + + m_settings = std::make_unique(); + return 0; + } + + int LogicViewerApp::createViewer(const Arguments& args, EFeatureLevel featureLevel, LogicViewer::ScreenshotFunc&& fScreenshot) + { + if (!fs::exists(args.logicFile())) + { + std::cerr << "Logic file does not exist: " << args.logicFile() << std::endl; + return static_cast(ExitCode::ErrorLoadLogic); + } + + m_viewer = std::make_unique(featureLevel, fScreenshot); + + if (!m_viewer->loadRamsesLogic(args.logicFile(), m_scene)) + { + std::cerr << "Failed to load logic file: " << args.logicFile() << std::endl; + return static_cast(ExitCode::ErrorLoadLogic); + } + + if (args.writeConfig()) + { + ImGui::NewFrame(); + const auto status = m_viewer->saveDefaultLuaFile(args.luaFile(), *m_settings); + ImGui::EndFrame(); + if (!status.ok()) + { + std::cerr << status.getMessage() << std::endl; + return static_cast(ExitCode::ErrorUnknown); + } + } + else if (!args.luaFunction().empty()) + { + m_loadLuaStatus = m_viewer->loadLuaFile(args.luaFile()); + + if (m_loadLuaStatus.ok()) + { + m_loadLuaStatus = m_viewer->call(args.luaFunction()); + } + if (!m_loadLuaStatus.ok()) + { + std::cerr << m_loadLuaStatus.getMessage() << std::endl; + return static_cast(ExitCode::ErrorLoadLua); + } + } + else if (!args.exec().empty()) + { + // default lua file may be missing (explicit lua file is checked by CLI11 before) + m_loadLuaStatus = m_viewer->loadLuaFile(fs::exists(args.luaFile()) ? args.luaFile() : ""); + if (m_loadLuaStatus.ok()) + { + m_loadLuaStatus = m_viewer->exec(args.exec()); + } + if (!m_loadLuaStatus.ok()) + { + std::cerr << m_loadLuaStatus.getMessage() << std::endl; + return static_cast(ExitCode::ErrorLoadLua); + } + } + else + { + // interactive mode + m_interactive = true; + // default lua file may be missing (explicit lua file is checked by CLI11 before) + if (fs::exists(args.luaFile())) + { + m_loadLuaStatus = m_viewer->loadLuaFile(args.luaFile()); + } + } + return 0; + } +} + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerApp.h b/client/logic/tools/ramses-logic-viewer/LogicViewerApp.h new file mode 100644 index 000000000..66b09b122 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerApp.h @@ -0,0 +1,98 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-client.h" +#include "ramses-framework-api/RamsesFrameworkTypes.h" +#include "Result.h" +#include "LogicViewer.h" +#include + +class Arguments; +struct ImGuiContext; + +namespace ramses +{ + struct LogicViewerSettings; + + class LogicViewerApp + { + public: + enum class ExitCode + { + Ok = 0, + ErrorRamsesClient = 1, + ErrorRamsesRenderer = 2, + ErrorSceneControl = 3, + ErrorLoadScene = 4, + ErrorLoadLogic = 5, + ErrorLoadLua = 6, + ErrorNoDisplay = 7, + ErrorUnknown = -1, + }; + + LogicViewerApp(); + + virtual ~LogicViewerApp(); + + LogicViewerApp(const LogicViewerApp&) = delete; + + LogicViewerApp& operator=(const LogicViewerApp&) = delete; + + [[nodiscard]] virtual bool doOneLoop() = 0; + + [[nodiscard]] int exitCode() const; + + [[nodiscard]] int run(); + + [[nodiscard]] ramses::LogicViewer* getViewer(); + + [[nodiscard]] const ramses::LogicViewerSettings* getSettings() const; + + protected: + [[nodiscard]] static int GetFeatureLevelFromFiles(const std::string& sceneFilename, const std::string& logicFilename, EFeatureLevel& featureLevel); + [[nodiscard]] int loadScene(const Arguments& args, EFeatureLevel featureLevel); + [[nodiscard]] int createViewer(const Arguments& args, EFeatureLevel featureLevel, LogicViewer::ScreenshotFunc&& fScreenshot); + + ImGuiContext* m_imguiContext = nullptr; + std::unique_ptr m_framework; + std::unique_ptr m_settings; + std::unique_ptr m_viewer; + + ramses::RamsesClient* m_client = nullptr; + ramses::Scene* m_scene = nullptr; + ramses::Result m_loadLuaStatus; + + int m_exitCode = -1; + bool m_interactive = false; + }; + + inline int LogicViewerApp::exitCode() const + { + return m_exitCode; + } + + inline int LogicViewerApp::run() + { + while (doOneLoop()) + ; + return m_exitCode; + } + + inline ramses::LogicViewer* LogicViewerApp::getViewer() + { + return m_viewer.get(); + } + + inline const ramses::LogicViewerSettings* LogicViewerApp::getSettings() const + { + return m_settings.get(); + } +} + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerGui.cpp b/client/logic/tools/ramses-logic-viewer/LogicViewerGui.cpp new file mode 100644 index 000000000..bd01672a5 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerGui.cpp @@ -0,0 +1,1364 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewerGui.h" +#include "LogicViewer.h" +#include "LogicViewerLog.h" +#include "LogicViewerSettings.h" +#include "ramses-client-api/Scene.h" +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/AnchorPoint.h" +#include "ramses-logic/SkinBinding.h" +#include "internals/StdFilesystemWrapper.h" +#include "fmt/format.h" +#include "glm/gtc/type_ptr.hpp" + +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "imgui_internal.h" // for ImGuiSettingsHandler +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif + +template <> struct fmt::formatter +{ + template constexpr auto parse(ParseContext& ctx) + { + return ctx.begin(); + } + + template constexpr auto format(const std::chrono::microseconds value, FormatContext& ctx) + { + const auto c = value.count(); + if (c == 0) + return fmt::format_to(ctx.out(), "0"); + return fmt::format_to(ctx.out(), "{}.{:03}", c / 1000, c % 1000); + } +}; + + +namespace ramses +{ + namespace + { + const char* EnumToString(ramses::ERotationType t) + { + switch (t) + { + case ramses::ERotationType::Euler_XYZ: + return "Euler_XYZ"; + case ramses::ERotationType::Euler_XZY: + return "Euler_XZY"; + case ramses::ERotationType::Euler_YXZ: + return "Euler_YXZ"; + case ramses::ERotationType::Euler_YZX: + return "Euler_YZX"; + case ramses::ERotationType::Euler_ZXY: + return "Euler_ZXY"; + case ramses::ERotationType::Euler_ZYX: + return "Euler_ZYX"; + case ramses::ERotationType::Euler_XYX: + return "Euler_XYX"; + case ramses::ERotationType::Euler_XZX: + return "Euler_XZX"; + case ramses::ERotationType::Euler_YXY: + return "Euler_YXY"; + case ramses::ERotationType::Euler_YZY: + return "Euler_YZY"; + case ramses::ERotationType::Euler_ZXZ: + return "Euler_ZXZ"; + case ramses::ERotationType::Euler_ZYZ: + return "Euler_ZYZ"; + case ramses::ERotationType::Quaternion: + return "Quaternion"; + } + return ""; + } + + const char* EnumToString(EInterpolationType t) + { + switch (t) + { + case EInterpolationType::Step: + return "Step"; + case EInterpolationType::Linear: + return "Linear"; + case EInterpolationType::Cubic: + return "Cubic"; + case EInterpolationType::Linear_Quaternions: + return "Linear_Quaternions"; + case EInterpolationType::Cubic_Quaternions: + return "Cubic_Quaternions"; + } + return ""; + } + + bool TreeNode(const void* ptr_id, const std::string_view& text) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + return ImGui::TreeNode(ptr_id, "%s", text.data()); + } + + bool TreeNode(const void* ptr_id, const std::string& text) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + return ImGui::TreeNode(ptr_id, "%s", text.c_str()); + } + + const char* TypeName(const LogicNode* node) + { + const char* name = "Unknown"; + if (node->as() != nullptr) + { + name = "LuaInterface"; + } + else if (node->as() != nullptr) + { + name = "LuaScript"; + } + else if (node->as() != nullptr) + { + name = "Animation"; + } + else if (node->as() != nullptr) + { + name = "NodeBinding"; + } + else if (node->as() != nullptr) + { + name = "AppearanceBinding"; + } + else if (node->as() != nullptr) + { + name = "CameraBinding"; + } + else if (node->as() != nullptr) + { + name = "RenderPassBinding"; + } + else if (node->as() != nullptr) + { + name = "RenderGroupBinding"; + } + else if (node->as() != nullptr) + { + name = "MeshNodeBinding"; + } + else if (node->as() != nullptr) + { + name = "Timer"; + } + else if (node->as() != nullptr) + { + name = "AnchorPoint"; + } + else if (node->as() != nullptr) + { + name = "SkinBinding"; + } + return name; + } + + template + void HelpMarker(const char* desc, Args&& ... args) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(fmt::format(desc, args...).c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + } + } + + LogicViewerGui::LogicViewerGui(ramses::LogicViewer& viewer, LogicViewerSettings& settings, std::string luafile) + : m_settings(settings) + , m_viewer(viewer) + , m_logicEngine(viewer.getEngine()) + , m_filename(std::move(luafile)) + { + m_viewer.enableUpdateReport(m_settings.showUpdateReport, m_updateReportInterval); + } + + void LogicViewerGui::draw() + { + if (ImGui::IsKeyPressed(ramses::EKeyCode_Left)) + { + m_viewer.setCurrentView(m_viewer.getCurrentView() - 1); + } + else if (ImGui::IsKeyPressed(ramses::EKeyCode_Right)) + { + m_viewer.setCurrentView(m_viewer.getCurrentView() + 1); + } + else if (ImGui::IsKeyPressed(ramses::EKeyCode_F11)) + { + m_settings.showWindow = !m_settings.showWindow; + ImGui::MarkIniSettingsDirty(); + } + else if (ImGui::IsKeyPressed(ramses::EKeyCode_F5)) + { + reloadConfiguration(); + } + else if (ImGui::IsKeyPressed(ramses::EKeyCode_C) && ImGui::GetIO().KeyCtrl) + { + copyScriptInputs(); + } + else + { + } + + drawGlobalContextMenu(); + drawSceneTexture(); + drawErrorPopup(); + + if (m_settings.showWindow) + { + // ImGui::ShowDemoWindow(); + drawWindow(); + } + } + + void LogicViewerGui::openErrorPopup(const std::string& message) + { + // ImGui::OpenPopup("Error") does not work in all cases (The calculated ID seems to be context related) + // The popup will be opened in LogicViewerGui::draw() instead + m_lastErrorMessage = message; + } + + void LogicViewerGui::setSceneTexture(ramses::TextureSampler* sampler, uint32_t width, uint32_t height) + { + m_sampler = sampler; + m_samplerSize.x = static_cast(width); + m_samplerSize.y = static_cast(height); + } + + void LogicViewerGui::setRendererInfo(ramses::RamsesRenderer& renderer, ramses::displayId_t displayId, ramses::displayBufferId_t displayBufferId, const std::array& initialClearColor) + { + m_renderer = &renderer; + m_displayId = displayId; + m_displayBufferId = displayBufferId; + m_clearColor = initialClearColor; + } + + void LogicViewerGui::drawMenuItemShowWindow() + { + if (ImGui::MenuItem("Show Logic Viewer Window", "F11", &m_settings.showWindow)) + { + ImGui::MarkIniSettingsDirty(); + } + } + + void LogicViewerGui::drawMenuItemReload() + { + if (ImGui::MenuItem("Reload configuration", "F5")) + { + reloadConfiguration(); + } + } + + void LogicViewerGui::drawMenuItemCopy() + { + if (ImGui::MenuItem("Copy script inputs", "Ctrl+C")) + { + copyScriptInputs(); + } + } + + void LogicViewerGui::reloadConfiguration() + { + if (fs::exists(m_filename)) + { + loadLuaFile(m_filename); + } + } + + void LogicViewerGui::loadLuaFile(const std::string& filename) + { + auto result = m_viewer.loadLuaFile(filename); + if (!result.ok()) + { + openErrorPopup(result.getMessage()); + } + } + + template + void LogicViewerGui::copyInputs(const std::string_view& ns, Collection collection) + { + LogicViewerLog::PathVector path; + path.push_back(LogicViewer::ltnModule); + path.push_back(ns); + LogicViewerLog log(m_logicEngine, m_settings); + for (auto* node : collection) + { + log.logInputs(node, path); + } + ImGui::SetClipboardText(log.getText().c_str()); + } + + void LogicViewerGui::copyScriptInputs() + { + copyInputs(LogicViewer::ltnScript, m_logicEngine.getCollection()); + } + + void LogicViewerGui::drawGlobalContextMenu() + { + if (ImGui::BeginPopupContextVoid("GlobalContextMenu")) + { + drawMenuItemShowWindow(); + drawMenuItemReload(); + drawMenuItemCopy(); + if (ImGui::MenuItem("Next view", "Arrow Right", nullptr, (m_viewer.getCurrentView() < m_viewer.getViewCount()))) + m_viewer.setCurrentView(m_viewer.getCurrentView() + 1); + if (ImGui::MenuItem("Previous view", "Arrow Left", nullptr, m_viewer.getCurrentView() > 1)) + m_viewer.setCurrentView(m_viewer.getCurrentView() - 1); + ImGui::EndPopup(); + } + } + + void LogicViewerGui::drawSceneTexture() + { + if (m_sampler) + { + ImGui::GetBackgroundDrawList()->AddImage(m_sampler, ImVec2(0, 0), m_samplerSize, ImVec2(0, 1), ImVec2(1, 0)); + } + } + + void LogicViewerGui::drawErrorPopup() + { + if (!m_lastErrorMessage.empty()) + ImGui::OpenPopup("Error"); + + if (ImGui::BeginPopupModal("Error", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::TextUnformatted(m_lastErrorMessage.c_str()); + ImGui::Separator(); + + if (ImGui::Button("OK", ImVec2(120, 0))) + { + m_lastErrorMessage.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Copy Message", ImVec2(120, 0))) + { + ImGui::SetClipboardText(m_lastErrorMessage.c_str()); + } + ImGui::EndPopup(); + } + } + + void LogicViewerGui::drawWindow() + { + if (!ImGui::Begin(fmt::format("Logic Viewer (FeatureLevel 0{})", m_logicEngine.getFeatureLevel()).c_str(), &m_settings.showWindow, ImGuiWindowFlags_MenuBar)) + { + ImGui::End(); + return; + } + + drawMenuBar(); + drawCurrentView(); + + if (m_settings.showInterfaces) + { + drawInterfaces(); + } + + if (m_settings.showScripts) + { + drawScripts(); + } + + if (m_settings.showAnimationNodes) + { + drawAnimationNodes(); + } + + if (m_settings.showTimerNodes) + { + drawTimerNodes(); + } + + if (m_settings.showDataArrays && ImGui::CollapsingHeader("Data Arrays")) + { + for (auto* obj : m_logicEngine.getCollection()) + { + DrawDataArray(obj); + } + } + + if (m_settings.showRamsesBindings) + { + drawAppearanceBindings(); + drawNodeBindings(); + drawCameraBindings(); + drawRenderPassBindings(); + drawRenderGroupBindings(); + drawMeshNodeBindings(); + drawAnchorPoints(); + drawSkinBindings(); + } + + if (m_settings.showUpdateReport) + { + drawUpdateReport(); + } + + if (m_settings.showDisplaySettings && m_renderer) + { + drawDisplaySettings(); + } + + ImGui::End(); + } + + void LogicViewerGui::drawMenuBar() + { + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + drawMenuItemReload(); + drawMenuItemCopy(); + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Settings")) + { + drawMenuItemShowWindow(); + ImGui::Separator(); + bool changed = ImGui::MenuItem("Show Interfaces", nullptr, &m_settings.showInterfaces); + changed = ImGui::MenuItem("Show Scripts", nullptr, &m_settings.showScripts) || changed; + changed = ImGui::MenuItem("Show Animation Nodes", nullptr, &m_settings.showAnimationNodes) || changed; + changed = ImGui::MenuItem("Show Timer Nodes", nullptr, &m_settings.showTimerNodes) || changed; + changed = ImGui::MenuItem("Show Data Arrays", nullptr, &m_settings.showDataArrays) || changed; + changed = ImGui::MenuItem("Show Ramses Bindings", nullptr, &m_settings.showRamsesBindings) || changed; + if (ImGui::MenuItem("Show Update Report", nullptr, &m_settings.showUpdateReport)) + { + m_viewer.enableUpdateReport(m_settings.showUpdateReport, m_updateReportInterval); + ImGui::MarkIniSettingsDirty(); + } + ImGui::Separator(); + changed = ImGui::MenuItem("Show Linked Inputs", nullptr, &m_settings.showLinkedInputs) || changed; + changed = ImGui::MenuItem("Show Outputs", nullptr, &m_settings.showOutputs) || changed; + ImGui::Separator(); + changed = ImGui::MenuItem("Lua: prefer identifiers (scripts.foo)", nullptr, &m_settings.luaPreferIdentifiers) || changed; + changed = ImGui::MenuItem("Lua: prefer object ids (scripts[1])", nullptr, &m_settings.luaPreferObjectIds) || changed; + ImGui::Separator(); + changed = ImGui::MenuItem("Show Display Settings", nullptr, &m_settings.showDisplaySettings) || changed; + ImGui::EndMenu(); + + if (changed) + { + ImGui::MarkIniSettingsDirty(); + } + } + ImGui::EndMenuBar(); + } + } + + void LogicViewerGui::drawCurrentView() + { + const auto viewCount = static_cast(m_viewer.getViewCount()); + const auto& status = m_viewer.getLastResult(); + if (!status.ok()) + { + ImGui::TextUnformatted(fmt::format("Error occurred in {}", m_viewer.getLuaFilename()).c_str()); + ImGui::TextUnformatted(status.getMessage().c_str()); + } + else if (m_viewer.getLuaFilename().empty()) + { + drawSaveDefaultLuaFile(); + } + else if (viewCount > 0) + { + auto viewId = static_cast(m_viewer.getCurrentView()); + const auto view = m_viewer.getView(viewId); + ImGui::TextUnformatted(!view.name().empty() ? view.name().c_str() : ""); + ImGui::SetNextItemWidth(100); + if (ImGui::InputInt("##View", &viewId)) + m_viewer.setCurrentView(viewId); + ImGui::SameLine(); + ImGui::TextUnformatted(fmt::format("of {}", viewCount).c_str()); + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + ImGui::TextWrapped("%s", view.description().c_str()); + + for (size_t i = 0U; i < view.getInputCount(); ++i) + { + auto* prop = view.getInput(i); + if (prop) + drawProperty(view.getInput(i), i); + } + } + else + { + ImGui::TextUnformatted("no views defined in configuration file"); + } + + if (m_settings.showUpdateReport) + { + ImGui::Separator(); + ImGui::TextUnformatted(fmt::format("Average Update Time: {} ms", m_viewer.getUpdateReport().getTotalTime().average).c_str()); + ImGui::SameLine(); + HelpMarker("Time it took to update the whole logic nodes network (LogicEngine::update())."); + } + } + + bool LogicViewerGui::DrawTreeNode(ramses::LogicObject* obj) + { + return TreeNode(obj, fmt::format("[{}]: {}", obj->getId(), obj->getName())); + } + + void LogicViewerGui::drawScripts() + { + const bool openScripts = ImGui::CollapsingHeader("Scripts"); + if (ImGui::BeginPopupContextItem("ScriptsContextMenu")) + { + if (ImGui::MenuItem("Copy all Script inputs")) + { + copyScriptInputs(); + } + ImGui::EndPopup(); + } + if (openScripts) + { + for (auto* script : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(script); + drawNodeContextMenu(script, LogicViewer::ltnScript); + if (open) + { + drawNode(script); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawInterfaces() + { + const bool openInterfaces = ImGui::CollapsingHeader("Interfaces"); + if (ImGui::BeginPopupContextItem("InterfacesContextMenu")) + { + if (ImGui::MenuItem("Copy all Interface inputs")) + { + copyInputs(LogicViewer::ltnInterface, m_logicEngine.getCollection()); + } + ImGui::EndPopup(); + } + if (openInterfaces) + { + for (auto* script : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(script); + drawNodeContextMenu(script, LogicViewer::ltnInterface); + if (open) + { + drawNode(script); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawAnimationNodes() + { + const bool openAnimationNodes = ImGui::CollapsingHeader("Animation Nodes"); + if (ImGui::BeginPopupContextItem("AnimationNodesContextMenu")) + { + if (ImGui::MenuItem("Copy all Animation Node inputs")) + { + copyInputs(LogicViewer::ltnAnimation, m_logicEngine.getCollection()); + } + ImGui::EndPopup(); + } + if (openAnimationNodes) + { + for (auto* obj : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(obj); + drawNodeContextMenu(obj, LogicViewer::ltnAnimation); + if (open) + { + ImGui::TextUnformatted(fmt::format("Duration: {}", *obj->getOutputs()->getChild("duration")->get()).c_str()); + if (ImGui::TreeNode("Channels")) + { + for (auto& ch : obj->getChannels()) + { + if (TreeNode(&ch, ch.name)) + { + ImGui::TextUnformatted(fmt::format("InterpolationType: {}", EnumToString(ch.interpolationType)).c_str()); + DrawDataArray(ch.keyframes, "Keyframes"); + DrawDataArray(ch.tangentsIn, "TangentsIn"); + DrawDataArray(ch.tangentsOut, "TangentsOut"); + DrawDataArray(ch.timeStamps, "TimeStamps"); + ImGui::TreePop(); + } + } + ImGui::TreePop(); + } + drawNode(obj); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawTimerNodes() + { + const bool openTimerNodes = ImGui::CollapsingHeader("Timer Nodes"); + if (ImGui::BeginPopupContextItem("TimerNodesContextMenu")) + { + if (ImGui::MenuItem("Copy all Timer Node inputs")) + { + copyInputs(LogicViewer::ltnTimer, m_logicEngine.getCollection()); + } + ImGui::EndPopup(); + } + if (openTimerNodes) + { + for (auto* obj : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(obj); + drawNodeContextMenu(obj, LogicViewer::ltnTimer); + if (open) + { + drawNode(obj); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawNodeBindings() + { + const bool openBindings = ImGui::CollapsingHeader("Node Bindings"); + if (ImGui::BeginPopupContextItem("NodeBindingsContextMenu")) + { + if (ImGui::MenuItem("Copy all Node Binding inputs")) + { + copyInputs(LogicViewer::ltnNode, m_logicEngine.getCollection()); + } + ImGui::EndPopup(); + } + if (openBindings) + { + for (auto* obj : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(obj); + drawNodeContextMenu(obj, LogicViewer::ltnNode); + if (open) + { + ImGui::TextUnformatted(fmt::format("Ramses Node: {}", obj->getRamsesNode().getName()).c_str()); + ImGui::TextUnformatted(fmt::format("Rotation Mode: {}", EnumToString(obj->getRotationType())).c_str()); + drawNode(obj); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawCameraBindings() + { + const bool openBindings = ImGui::CollapsingHeader("Camera Bindings"); + if (ImGui::BeginPopupContextItem("CameraBindingsContextMenu")) + { + if (ImGui::MenuItem("Copy all Camera Binding inputs")) + { + copyInputs(LogicViewer::ltnCamera, m_logicEngine.getCollection()); + } + ImGui::EndPopup(); + } + if (openBindings) + { + for (auto* obj : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(obj); + drawNodeContextMenu(obj, LogicViewer::ltnCamera); + if (open) + { + ImGui::TextUnformatted(fmt::format("Ramses Camera: {}", obj->getRamsesCamera().getName()).c_str()); + drawNode(obj); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawRenderPassBindings() + { + const bool openBindings = ImGui::CollapsingHeader("RenderPass Bindings"); + if (ImGui::BeginPopupContextItem("RenderPassBindingsContextMenu")) + { + if (ImGui::MenuItem("Copy all RenderPass Binding inputs")) + { + copyInputs(LogicViewer::ltnRenderPass, m_logicEngine.getCollection()); + } + ImGui::EndPopup(); + } + if (openBindings) + { + for (auto* obj : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(obj); + drawNodeContextMenu(obj, LogicViewer::ltnRenderPass); + if (open) + { + ImGui::TextUnformatted(fmt::format("Ramses RenderPass: {}", obj->getRamsesRenderPass().getName()).c_str()); + drawNode(obj); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawRenderGroupBindings() + { + const bool openBindings = ImGui::CollapsingHeader("RenderGroup Bindings"); + if (ImGui::BeginPopupContextItem("RenderGroupBindingsContextMenu")) + { + if (ImGui::MenuItem("Copy all RenderGroup Binding inputs")) + { + copyInputs(LogicViewer::ltnRenderGroup, m_logicEngine.getCollection()); + } + ImGui::EndPopup(); + } + if (openBindings) + { + for (auto* obj : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(obj); + drawNodeContextMenu(obj, LogicViewer::ltnRenderGroup); + if (open) + { + ImGui::TextUnformatted(fmt::format("Ramses RenderGroup: {}", obj->getRamsesRenderGroup().getName()).c_str()); + drawNode(obj); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawMeshNodeBindings() + { + const bool openBindings = ImGui::CollapsingHeader("MeshNode Bindings"); + if (ImGui::BeginPopupContextItem("MeshNodeBindingsContextMenu")) + { + if (ImGui::MenuItem("Copy all MeshNode Binding inputs")) + { + copyInputs(LogicViewer::ltnMeshNode, m_logicEngine.getCollection()); + } + ImGui::EndPopup(); + } + if (openBindings) + { + for (auto* obj : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(obj); + drawNodeContextMenu(obj, LogicViewer::ltnMeshNode); + if (open) + { + ImGui::TextUnformatted(fmt::format("Ramses MeshNode: {}", obj->getRamsesMeshNode().getName()).c_str()); + drawNode(obj); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawAnchorPoints() + { + const bool openAnchors = ImGui::CollapsingHeader("Anchor Points"); + if (ImGui::BeginPopupContextItem("AnchorPointsContextMenu")) + { + if (ImGui::MenuItem("Copy all Anchor Point inputs")) + { + copyInputs(LogicViewer::ltnAnchorPoint, m_logicEngine.getCollection()); + } + ImGui::EndPopup(); + } + if (openAnchors) + { + for (auto* obj : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(obj); + drawNodeContextMenu(obj, LogicViewer::ltnAnchorPoint); + if (open) + { + ImGui::TextUnformatted(fmt::format("Ramses Node: {}", obj->getRamsesNode().getName()).c_str()); + ImGui::TextUnformatted(fmt::format("Ramses Camera: {}", obj->getRamsesCamera().getName()).c_str()); + drawNode(obj); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawSkinBindings() + { + const bool openSkinBindings = ImGui::CollapsingHeader("Skin Bindings"); + if (ImGui::BeginPopupContextItem("SkinBindingsContextMenu")) + { + if (ImGui::MenuItem("Copy all Skin Binding inputs")) + { + copyInputs(LogicViewer::ltnSkinBinding, m_logicEngine.getCollection()); + } + ImGui::EndPopup(); + } + if (openSkinBindings) + { + for (auto* obj : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(obj); + drawNodeContextMenu(obj, LogicViewer::ltnSkinBinding); + if (open) + { + ImGui::TextUnformatted(fmt::format("Ramses Appearance: {}", obj->getAppearanceBinding().getRamsesAppearance().getName()).c_str()); + ImGui::TextUnformatted(fmt::format("Ramses Uniform input: {}", obj->getAppearanceUniformInput().getName()).c_str()); + drawNode(obj); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawUpdateReport() + { + const bool open = ImGui::CollapsingHeader("Update Report"); + if (open) + { + auto interval = static_cast(m_updateReportInterval); + bool refresh = m_viewer.isUpdateReportEnabled(); + if (ImGui::Checkbox("Auto Refresh", &refresh)) + { + m_viewer.enableUpdateReport(refresh, m_updateReportInterval); + } + ImGui::SetNextItemWidth(100); + if (ImGui::DragInt("Refresh Interval", &interval, 0.5f, 1, 1000, "%d Frames")) + { + m_updateReportInterval = static_cast(interval); + m_viewer.enableUpdateReport(refresh, m_updateReportInterval); + } + const auto& report = m_viewer.getUpdateReport(); + const auto& executed = report.getNodesExecuted(); + const auto& skipped = report.getNodesSkippedExecution(); + const auto longest = report.getTotalTime().maxValue; + + ImGui::Separator(); + ImGui::TextUnformatted("Summary:"); + ImGui::SameLine(); + HelpMarker("Timing data is collected and summarized for {} frames.\n'min', 'max', 'avg' show the minimum, maximum, and average value for the measured interval.", m_updateReportInterval); + ImGui::Indent(); + + const auto& updateTime = report.getTotalTime(); + ImGui::TextUnformatted(fmt::format("Total Update Time (ms): max:{} min:{} avg:{}", updateTime.maxValue, updateTime.minValue, updateTime.average).c_str()); + ImGui::SameLine(); + HelpMarker("Time it took to update the whole logic nodes network (LogicEngine::update())."); + + const auto& sortTime = report.getSortTime(); + ImGui::TextUnformatted(fmt::format("Topology Sort Time (ms): max:{} min:{} avg:{}", sortTime.maxValue, sortTime.minValue, sortTime.average).c_str()); + ImGui::SameLine(); + HelpMarker("Time it took to sort logic nodes by their topology during update (see ramses::LogicEngineReport::getTopologySortExecutionTime()"); + + const auto& links = report.getLinkActivations(); + ImGui::TextUnformatted(fmt::format("Activated Links: max:{} min:{} avg:{}", links.maxValue, links.minValue, links.average).c_str()); + ImGui::SameLine(); + HelpMarker("Number of input properties that had been updated by an output property (see ramses::LogicEngineReport::getTotalLinkActivations())."); + ImGui::Unindent(); + + ImGui::TextUnformatted(fmt::format("Details for the longest update ({} ms):", longest).c_str()); + if (TreeNode("Executed", fmt::format("Executed Nodes ({}):", executed.size()))) + { + for (auto& timedNode : executed) + { + auto* node = timedNode.first; + const auto percentage = (longest.count() > 0u) ? (100u * timedNode.second / longest) : 0u; + if (TreeNode(node, fmt::format("{}[{}]: {} [time:{} ms, {}%]", TypeName(node), node->getId(), node->getName(), timedNode.second, percentage))) + { + drawNode(node); + ImGui::TreePop(); + } + } + ImGui::TreePop(); + } + + if (TreeNode("Skipped", fmt::format("Skipped Nodes ({}):", skipped.size()))) + { + for (auto& node : skipped) + { + if (TreeNode(node, fmt::format("{}[{}]: {}", TypeName(node), node->getId(), node->getName()))) + { + drawNode(node); + ImGui::TreePop(); + } + } + ImGui::TreePop(); + } + } + } + + void LogicViewerGui::drawAppearanceBindings() + { + const bool openBindings = ImGui::CollapsingHeader("Appearance Bindings"); + if (ImGui::BeginPopupContextItem("AppearanceBindingsContextMenu")) + { + if (ImGui::MenuItem("Copy all Appearance Binding inputs")) + { + copyInputs(LogicViewer::ltnAppearance, m_logicEngine.getCollection()); + } + ImGui::EndPopup(); + } + if (openBindings) + { + for (auto* obj : m_logicEngine.getCollection()) + { + const bool open = DrawTreeNode(obj); + drawNodeContextMenu(obj, LogicViewer::ltnAppearance); + if (open) + { + ImGui::TextUnformatted(fmt::format("Ramses Appearance: {}", obj->getRamsesAppearance().getName()).c_str()); + drawNode(obj); + ImGui::TreePop(); + } + } + } + } + + void LogicViewerGui::drawDisplaySettings() + { + assert(m_renderer); + const bool openDisplaySettings = ImGui::CollapsingHeader("Display Settings"); + if (openDisplaySettings) + { + if (ImGui::DragFloat4("Clear color", m_clearColor.data(), 0.1f, 0.f, 1.f)) + { + m_renderer->setDisplayBufferClearColor(m_displayId, m_displayBufferId, {m_clearColor[0], m_clearColor[1], m_clearColor[2], m_clearColor[3]}); + m_renderer->flush(); + } + + float fps = m_renderer->getFramerateLimit(m_displayId); + if (ImGui::DragFloat("Maximum FPS", &fps, 1.f, 1.f, 1000.f)) + { + m_renderer->setFramerateLimit(m_displayId, fps); + m_renderer->flush(); + } + + if (ImGui::Checkbox("Skip rendering of unmodified buffers", &m_skipUnmodifiedBuffers)) + { + m_renderer->setSkippingOfUnmodifiedBuffers(m_skipUnmodifiedBuffers); + m_renderer->flush(); + } + } + } + + void LogicViewerGui::drawSaveDefaultLuaFile() + { + ImGui::TextUnformatted("No lua configuration file found."); + ImGui::InputText("##filename", &m_filename); + ImGui::SameLine(); + if (ImGui::Button("Save default")) + { + fs::path luafile(m_filename); + if (fs::exists(luafile)) + { + ImGui::OpenPopup("Overwrite?"); + } + else if (!luafile.empty()) + { + saveDefaultLuaFile(); + } + } + ImGui::SameLine(); + if (ImGui::Button("Open")) + { + fs::path luafile(m_filename); + if (fs::exists(luafile)) + { + loadLuaFile(m_filename); + } + else if (!luafile.empty()) + { + m_lastErrorMessage = "File does not exist: " + m_filename; + } + } + + if (ImGui::BeginPopupModal("Overwrite?", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::TextUnformatted(fmt::format("File exists:\n{}\nOverwrite default lua configuration?", m_filename).c_str()); + ImGui::Separator(); + + if (ImGui::Button("OK", ImVec2(120, 0))) + { + saveDefaultLuaFile(); + ImGui::CloseCurrentPopup(); + } + ImGui::SetItemDefaultFocus(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) + { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + + + void LogicViewerGui::drawNodeContextMenu(ramses::LogicNode* obj, const std::string_view& ns) + { + if (ImGui::BeginPopupContextItem(obj->getName().data())) + { + if (ImGui::MenuItem(fmt::format("Copy {} inputs", obj->getName()).c_str())) + { + LogicViewerLog::PathVector path; + path.push_back(LogicViewer::ltnModule); + path.push_back(ns); + LogicViewerLog log(m_logicEngine, m_settings); + log.logInputs(obj, path); + ImGui::SetClipboardText(log.getText().c_str()); + } + ImGui::EndPopup(); + } + } + + void LogicViewerGui::drawNode(ramses::LogicNode* obj) + { + auto* in = obj->getInputs(); + const auto* out = obj->getOutputs(); + ImGui::SetNextItemOpen(true, ImGuiCond_Always); + if (in != nullptr) + { + if (TreeNode(in, std::string_view("Inputs"))) + { + for (size_t i = 0; i < in->getChildCount(); ++i) + { + drawProperty(in->getChild(i), i); + } + ImGui::TreePop(); + } + } + if (out != nullptr && m_settings.showOutputs) + { + ImGui::SetNextItemOpen(true, ImGuiCond_Always); + if (TreeNode(out, std::string_view("Outputs"))) + { + for (size_t i = 0; i < out->getChildCount(); ++i) + { + drawOutProperty(out->getChild(i), i); + } + ImGui::TreePop(); + } + } + } + + void LogicViewerGui::drawProperty(ramses::Property* prop, size_t index) + { + const bool isLinked = prop->hasIncomingLink(); + if (isLinked && !m_settings.showLinkedInputs) + return; + + std::string strName = prop->getName().data(); + if (prop->getName().empty()) + strName = fmt::format("[{}]", index); + const char* name = strName.c_str(); + + switch (prop->getType()) + { + case ramses::EPropertyType::Int32: { + auto value = prop->get().value(); + if (isLinked) + { + ImGui::TextUnformatted(fmt::format("{}: {}", name, value).c_str()); + } + else if (ImGui::DragInt(name, &value, 0.1f)) + { + prop->set(value); + } + break; + } + case ramses::EPropertyType::Int64: { + auto value = prop->get().value(); + if (isLinked) + { + ImGui::TextUnformatted(fmt::format("{}: {}", name, value).c_str()); + } + else if (ImGui::DragScalar(name, ImGuiDataType_S64, &value, 0.1f, nullptr, nullptr)) + { + prop->set(value); + } + break; + } + case ramses::EPropertyType::Float: { + auto value = prop->get().value(); + if (isLinked) + { + ImGui::TextUnformatted(fmt::format("{}: {}", name, value).c_str()); + } + else if (ImGui::DragFloat(name, &value, 0.1f)) + { + prop->set(value); + } + break; + } + case ramses::EPropertyType::Vec2f: { + auto value = prop->get().value(); + if (isLinked) + { + ImGui::TextUnformatted(fmt::format("{}: ({}, {})", name, value[0], value[1]).c_str()); + } + else if (ImGui::DragFloat2(name, glm::value_ptr(value), 0.1f)) + { + prop->set(value); + } + break; + } + case ramses::EPropertyType::Vec3f: { + auto value = prop->get().value(); + if (isLinked) + { + ImGui::TextUnformatted(fmt::format("{}: ({}, {}, {})", name, value[0], value[1], value[2]).c_str()); + } + else if (ImGui::DragFloat3(name, glm::value_ptr(value), 0.1f)) + { + prop->set(value); + } + break; + } + case ramses::EPropertyType::Vec4f: { + auto value = prop->get().value(); + if (isLinked) + { + ImGui::TextUnformatted(fmt::format("{}: ({}, {}, {}, {})", name, value[0], value[1], value[2], value[3]).c_str()); + } + else if (ImGui::DragFloat4(name, glm::value_ptr(value), 0.1f)) + { + prop->set(value); + } + break; + } + case ramses::EPropertyType::Vec2i: { + auto value = prop->get().value(); + if (isLinked) + { + ImGui::TextUnformatted(fmt::format("{}: ({}, {})", name, value[0], value[1]).c_str()); + } + else if (ImGui::DragInt2(name, glm::value_ptr(value), 0.1f)) + { + prop->set(value); + } + break; + } + case ramses::EPropertyType::Vec3i: { + auto value = prop->get().value(); + if (isLinked) + { + ImGui::TextUnformatted(fmt::format("{}: ({}, {}, {})", name, value[0], value[1], value[2]).c_str()); + } + else if (ImGui::DragInt3(name, glm::value_ptr(value), 0.1f)) + { + prop->set(value); + } + break; + } + case ramses::EPropertyType::Vec4i: { + auto value = prop->get().value(); + if (isLinked) + { + ImGui::TextUnformatted(fmt::format("{}: ({}, {}, {}, {})", name, value[0], value[1], value[2], value[3]).c_str()); + } + else if (ImGui::DragInt4(name, glm::value_ptr(value), 0.1f)) + { + prop->set(value); + } + break; + } + case ramses::EPropertyType::Struct: + if (TreeNode(prop, fmt::format("Struct {}", name))) + { + for (size_t i = 0U; i < prop->getChildCount(); ++i) + { + auto* child = prop->getChild(i); + drawProperty(child, i); + } + ImGui::TreePop(); + } + break; + case ramses::EPropertyType::Bool: { + auto value = prop->get().value(); + if (isLinked) + { + ImGui::TextUnformatted(fmt::format("{}: {}", name, value ? "true" : "false").c_str()); + } + else if (ImGui::Checkbox(name, &value)) + { + prop->set(value); + } + break; + } + case ramses::EPropertyType::String: { + auto value = prop->get().value(); + if (isLinked) + { + ImGui::TextUnformatted(fmt::format("{}: {}", name, value).c_str()); + } + else if (ImGui::InputText(name, &value)) + { + prop->set(value); + } + break; + } + case ramses::EPropertyType::Array: + if (TreeNode(prop, fmt::format("Array {}", name))) + { + for (size_t i = 0U; i < prop->getChildCount(); ++i) + { + auto* child = prop->getChild(i); + drawProperty(child, i); + } + ImGui::TreePop(); + } + break; + } + } + + void LogicViewerGui::drawOutProperty(const ramses::Property* prop, size_t index) + { + std::string strName = prop->getName().data(); + if (prop->getName().empty()) + strName = fmt::format("[{}]", index); + const char* name = strName.c_str(); + + switch (prop->getType()) + { + case ramses::EPropertyType::Int32: { + auto value = prop->get().value(); + ImGui::TextUnformatted(fmt::format("{}: {}", name, value).c_str()); + break; + } + case ramses::EPropertyType::Int64: { + auto value = prop->get().value(); + ImGui::TextUnformatted(fmt::format("{}: {}", name, value).c_str()); + break; + } + case ramses::EPropertyType::Float: { + auto value = prop->get().value(); + ImGui::TextUnformatted(fmt::format("{}: {}", name, value).c_str()); + break; + } + case ramses::EPropertyType::Vec2f: { + auto value = prop->get().value(); + ImGui::TextUnformatted(fmt::format("{}: ({}, {})", name, value[0], value[1]).c_str()); + break; + } + case ramses::EPropertyType::Vec3f: { + auto value = prop->get().value(); + ImGui::TextUnformatted(fmt::format("{}: ({}, {}, {})", name, value[0], value[1], value[2]).c_str()); + break; + } + case ramses::EPropertyType::Vec4f: { + auto value = prop->get().value(); + ImGui::TextUnformatted(fmt::format("{}: ({}, {}, {}, {})", name, value[0], value[1], value[2], value[3]).c_str()); + break; + } + case ramses::EPropertyType::Vec2i: { + auto value = prop->get().value(); + ImGui::TextUnformatted(fmt::format("{}: ({}, {})", name, value[0], value[1]).c_str()); + break; + } + case ramses::EPropertyType::Vec3i: { + auto value = prop->get().value(); + ImGui::TextUnformatted(fmt::format("{}: ({}, {}, {})", name, value[0], value[1], value[2]).c_str()); + break; + } + case ramses::EPropertyType::Vec4i: { + auto value = prop->get().value(); + ImGui::TextUnformatted(fmt::format("{}: ({}, {}, {}, {})", name, value[0], value[1], value[2], value[3]).c_str()); + break; + } + case ramses::EPropertyType::Struct: + if (TreeNode(prop, fmt::format("Struct {}", name))) + { + for (size_t i = 0U; i < prop->getChildCount(); ++i) + { + const auto* child = prop->getChild(i); + drawOutProperty(child, i); + } + ImGui::TreePop(); + } + break; + case ramses::EPropertyType::String: { + auto value = prop->get().value(); + ImGui::TextUnformatted(fmt::format("{}: {}", name, value).c_str()); + break; + } + case ramses::EPropertyType::Bool: { + auto value = prop->get().value(); + ImGui::TextUnformatted(fmt::format("{}: {}", name, value ? "true" : "false").c_str()); + break; + } + case ramses::EPropertyType::Array: + if (TreeNode(prop, fmt::format("Array {}", name))) + { + for (size_t i = 0U; i < prop->getChildCount(); ++i) + { + const auto* child = prop->getChild(i); + drawOutProperty(child, i); + } + ImGui::TreePop(); + } + break; + } + } + + void LogicViewerGui::DrawDataArray(const ramses::DataArray* obj, std::string_view context) + { + if (obj != nullptr) + { + if (!context.empty()) + { + ImGui::TextUnformatted( + fmt::format("{}: [{}]: {} Type:{}[{}]", context.data(), obj->getId(), obj->getName(), GetLuaPrimitiveTypeName(obj->getDataType()), obj->getNumElements()).c_str()); + } + else + { + ImGui::TextUnformatted(fmt::format("[{}]: {} Type:{}[{}]", obj->getId(), obj->getName(), GetLuaPrimitiveTypeName(obj->getDataType()), obj->getNumElements()).c_str()); + } + } + } + + void LogicViewerGui::saveDefaultLuaFile() + { + const auto result = m_viewer.saveDefaultLuaFile(m_filename, m_settings); + if (result.ok()) + { + loadLuaFile(m_filename); + } + else + { + m_lastErrorMessage = result.getMessage(); + } + } +} + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerGui.h b/client/logic/tools/ramses-logic-viewer/LogicViewerGui.h new file mode 100644 index 000000000..58ba20b36 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerGui.h @@ -0,0 +1,110 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ImguiClientHelper.h" +#include "ramses-logic/Collection.h" +#include + +struct ImGuiSettingsHandler; + +namespace ramses +{ + class Scene; + class RamsesRenderer; +} + +namespace ramses +{ + class LogicEngine; + class Property; + class LogicObject; + class LogicNode; + class LogicViewer; + class DataArray; + struct LogicViewerSettings; + + class LogicViewerGui + { + public: + /** + * @param luafile the luafile argument provided by command line + * If it does not exist the LogicViewerGui will provide a UI to save the default settings + */ + explicit LogicViewerGui(ramses::LogicViewer& viewer, LogicViewerSettings& settings, std::string luafile); + void draw(); + void openErrorPopup(const std::string& message); + void setSceneTexture(ramses::TextureSampler* sampler, uint32_t width, uint32_t height); + void setRendererInfo(ramses::RamsesRenderer& renderer, ramses::displayId_t displayId, ramses::displayBufferId_t displayBufferId, const std::array& initialClearColor); + + private: + void drawMenuItemShowWindow(); + void drawMenuItemReload(); + void drawMenuItemCopy(); + + void drawGlobalContextMenu(); + void drawSceneTexture(); + void drawErrorPopup(); + void drawWindow(); + void drawMenuBar(); + void drawCurrentView(); + void drawInterfaces(); + void drawScripts(); + void drawAnimationNodes(); + void drawTimerNodes(); + void drawNodeBindings(); + void drawCameraBindings(); + void drawRenderPassBindings(); + void drawRenderGroupBindings(); + void drawMeshNodeBindings(); + void drawAnchorPoints(); + void drawSkinBindings(); + void drawUpdateReport(); + void drawAppearanceBindings(); + void drawDisplaySettings(); + void drawSaveDefaultLuaFile(); + + static bool DrawTreeNode(ramses::LogicObject* obj); + + void drawNodeContextMenu(ramses::LogicNode* obj, const std::string_view& ns); + void drawNode(ramses::LogicNode* obj); + void drawProperty(ramses::Property* prop, size_t index); + void drawOutProperty(const ramses::Property* prop, size_t index); + static void DrawDataArray(const ramses::DataArray* obj, std::string_view context = std::string_view()); + + void reloadConfiguration(); + void loadLuaFile(const std::string& filename); + void saveDefaultLuaFile(); + + /** + * copies all node inputs to clipboard (lua syntax) + */ + template + void copyInputs(const std::string_view& ns, Collection collection); + void copyScriptInputs(); + + LogicViewerSettings& m_settings; + ramses::LogicViewer& m_viewer; + ramses::LogicEngine& m_logicEngine; + + std::string m_lastErrorMessage; + std::string m_filename; + size_t m_updateReportInterval = 60u; + + ramses::TextureSampler* m_sampler = nullptr; + ImVec2 m_samplerSize; + + ramses::RamsesRenderer* m_renderer = nullptr; + ramses::displayId_t m_displayId; + ramses::displayBufferId_t m_displayBufferId; + std::array m_clearColor{ 0, 0, 0, 1 }; + bool m_skipUnmodifiedBuffers = true; + }; +} + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerGuiApp.cpp b/client/logic/tools/ramses-logic-viewer/LogicViewerGuiApp.cpp new file mode 100644 index 000000000..55ad0f9ba --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerGuiApp.cpp @@ -0,0 +1,226 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewerGuiApp.h" + +#include "Arguments.h" +#include "SceneSetup.h" +#include "ImguiClientHelper.h" +#include "LogicViewerGui.h" +#include "LogicViewer.h" +#include "LogicViewerSettings.h" +#include "ramses-logic/Logger.h" +#include "ramses-cli.h" + +namespace ramses +{ + namespace + { + bool getPreferredSize(const ramses::Scene& scene, uint32_t& width, uint32_t& height) + { + ramses::SceneObjectIterator it(scene, ramses::ERamsesObjectType::RenderPass); + ramses::RamsesObject* obj = nullptr; + while (nullptr != (obj = it.getNext())) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) + auto* rp = static_cast(obj); + if (!rp->getRenderTarget()) + { + auto* camera = rp->getCamera(); + if (camera) + { + width = camera->getViewportWidth(); + height = camera->getViewportHeight(); + return true; + } + } + } + return false; + } + } // namespace + + LogicViewerGuiApp::LogicViewerGuiApp(int argc, const char * const * argv) + { + m_exitCode = init(argc, argv); + } + + LogicViewerGuiApp::~LogicViewerGuiApp() = default; + + int LogicViewerGuiApp::init(int argc, char const* const* argv) + { + if (argc == 0 || argv == nullptr) + { + return static_cast(ExitCode::ErrorUnknown); + } + + CLI::App cli; + Arguments args; + ramses::DisplayConfig displayConfig; + displayConfig.setResizable(true); + + try + { + args.registerOptions(cli); + cli.add_flag("--headless", m_headless, "Runs without user interface and renderer. Disables screenshots."); + auto setClearColor = [&](const auto& clearColor) { m_defaultClearColor = clearColor; }; + cli.add_option_function>("--clear-color", setClearColor, "Background clear color as RGBA (ex. --clearColor 0 0.5 0.8 1"); + ramses::registerOptions(cli, displayConfig); + } + catch (const CLI::Error& err) + { + // catch configuration errors + std::cerr << err.what() << std::endl; + return -1; + } + + CLI11_PARSE(cli, argc, argv); + + const bool customWidth = cli.get_option("--width")->count() > 0; + const bool customHeight = cli.get_option("--width")->count() > 0; + const bool autoDetectViewportSize = !customHeight && !customWidth; + + ramses::EFeatureLevel featureLevel = ramses::EFeatureLevel_01; + auto exitCode = GetFeatureLevelFromFiles(args.sceneFile(), args.logicFile(), featureLevel); + if (exitCode != 0) + { + return exitCode; + } + + exitCode = LogicViewerApp::loadScene(args, featureLevel); + if (exitCode != 0) + { + return exitCode; + } + + const auto guiSceneId = ramses::sceneId_t(m_scene->getSceneId().getValue() + 1); + + if (autoDetectViewportSize && getPreferredSize(*m_scene, m_width, m_height)) + { + displayConfig.setWindowRectangle(0, 0, m_width, m_height); + } + + int32_t winX = 0; + int32_t winY = 0; + displayConfig.getWindowRectangle(winX, winY, m_width, m_height); + m_imguiHelper = std::make_unique(*m_client, m_width, m_height, guiSceneId); + + ramses::RamsesRenderer* renderer = nullptr; + ramses::displayId_t displayId; + if (!m_headless) + { + renderer = m_framework->createRenderer({}); + if (!renderer) + { + std::cerr << "Could not create ramses renderer" << std::endl; + return static_cast(ExitCode::ErrorRamsesRenderer); + } + + displayId = initDisplay(args, *renderer, displayConfig); + if (!displayId.isValid()) + { + std::cerr << "Could not create ramses display" << std::endl; + return static_cast(ExitCode::ErrorNoDisplay); + } + } + + auto takeScreenshot = [&](const std::string& filename) { + static ramses::sceneVersionTag_t ver = 42; + ++ver; + m_scene->flush(ver); + if (m_imguiHelper->waitForSceneVersion(m_scene->getSceneId(), ver)) + { + if (m_imguiHelper->saveScreenshot(filename, m_sceneSetup->getOffscreenBuffer(), 0, 0, m_sceneSetup->getWidth(), m_sceneSetup->getHeight())) + { + if (m_imguiHelper->waitForScreenshot()) + { + return true; + } + } + } + return false; + }; + + if (m_headless) + { + exitCode = LogicViewerApp::createViewer(args, featureLevel, LogicViewer::ScreenshotFunc()); + } + else + { + m_sceneSetup->apply(); + exitCode = LogicViewerApp::createViewer(args, featureLevel, takeScreenshot); + } + + if (exitCode != 0) + { + return exitCode; + } + + m_gui = std::make_unique(*m_viewer, *m_settings, args.luaFile()); + if (!m_headless) + { + m_gui->setSceneTexture(m_sceneSetup->getTextureSampler(), m_width, m_height); + m_gui->setRendererInfo(*renderer, displayId, m_sceneSetup->getOffscreenBuffer(), m_defaultClearColor); + } + + return 0; + } + + ramses::displayId_t LogicViewerGuiApp::initDisplay(const Arguments& args, ramses::RamsesRenderer& renderer, const ramses::DisplayConfig& displayConfig) + { + renderer.startThread(); + m_imguiHelper->setRenderer(&renderer); + + const auto display = renderer.createDisplay(displayConfig); + m_imguiHelper->setDisplayId(display); + renderer.flush(); + + if (!m_imguiHelper->waitForDisplay(display)) + { + return {}; + } + + if (args.noOffscreen()) + { + m_sceneSetup = std::make_unique(*m_imguiHelper, renderer, m_scene, display); + } + else + { + m_sceneSetup = std::make_unique(*m_imguiHelper, renderer, m_scene, display, m_width, m_height); + } + + renderer.setDisplayBufferClearColor(display, m_sceneSetup->getOffscreenBuffer(), {m_defaultClearColor[0], m_defaultClearColor[1], m_defaultClearColor[2], m_defaultClearColor[3]}); + renderer.flush(); + + return display; + } + + bool LogicViewerGuiApp::doOneLoop() + { + const auto isRunning = m_interactive && (m_exitCode == 0) && m_imguiHelper && m_imguiHelper->isRunning(); + if (isRunning) + { + const auto updateStatus = m_viewer->update(); + m_scene->flush(); + m_imguiHelper->dispatchEvents(); + ImGui::NewFrame(); + m_gui->draw(); + if (!m_loadLuaStatus.ok()) + { + m_gui->openErrorPopup(m_loadLuaStatus.getMessage()); + m_loadLuaStatus = ramses::Result(); + } + if (!updateStatus.ok()) + m_gui->openErrorPopup(updateStatus.getMessage()); + ImGui::EndFrame(); + m_imguiHelper->draw(); + std::this_thread::sleep_for(std::chrono::milliseconds(16)); + } + return isRunning; + } +} + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerGuiApp.h b/client/logic/tools/ramses-logic-viewer/LogicViewerGuiApp.h new file mode 100644 index 000000000..6d3ef0f6a --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerGuiApp.h @@ -0,0 +1,74 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "LogicViewerApp.h" + +class ISceneSetup; + +namespace ramses +{ + class DisplayConfig; + class RendererConfig; +} + +namespace ramses +{ + class ImguiClientHelper; + class LogicViewerGui; + + class LogicViewerGuiApp : public LogicViewerApp + { + public: + enum class ExitCode + { + Ok = 0, + ErrorRamsesClient = 1, + ErrorRamsesRenderer = 2, + ErrorSceneControl = 3, + ErrorLoadScene = 4, + ErrorLoadLogic = 5, + ErrorLoadLua = 6, + ErrorNoDisplay = 7, + ErrorUnknown = -1, + }; + + LogicViewerGuiApp(int argc, char const* const* argv); + + ~LogicViewerGuiApp() override; + + LogicViewerGuiApp(const LogicViewerGuiApp&) = delete; + + LogicViewerGuiApp& operator=(const LogicViewerGuiApp&) = delete; + + [[nodiscard]] bool doOneLoop() override; + + [[nodiscard]] ramses::ImguiClientHelper* getImguiClientHelper(); + + private: + [[nodiscard]] int init(int argc, char const* const* argv); + [[nodiscard]] ramses::displayId_t initDisplay(const Arguments& args, ramses::RamsesRenderer& renderer, const ramses::DisplayConfig& displayConfig); + + + std::unique_ptr m_imguiHelper; + std::unique_ptr m_gui; + std::unique_ptr m_sceneSetup; + + uint32_t m_width = 0u; + uint32_t m_height = 0u; + std::array m_defaultClearColor{ 0, 0, 0, 1 }; + bool m_headless = false; + }; + + inline ramses::ImguiClientHelper* LogicViewerGuiApp::getImguiClientHelper() + { + return m_imguiHelper.get(); + } +} + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerHeadlessApp.cpp b/client/logic/tools/ramses-logic-viewer/LogicViewerHeadlessApp.cpp new file mode 100644 index 000000000..666a87098 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerHeadlessApp.cpp @@ -0,0 +1,88 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewerHeadlessApp.h" +#include "ImguiWrapper.h" +#include "Arguments.h" +#include "LogicViewer.h" +#include "LogicViewerSettings.h" +#include "ramses-logic/Logger.h" + +namespace ramses +{ + LogicViewerHeadlessApp::LogicViewerHeadlessApp(int argc, const char * const * argv) + { + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize.x = 400.f; + io.DisplaySize.y = 320.f; + int texturewidth = 0; + int textureheight = 0; + unsigned char* pixels = nullptr; + io.Fonts->GetTexDataAsRGBA32(&pixels, &texturewidth, &textureheight); + m_exitCode = init(argc, argv); + } + + LogicViewerHeadlessApp::~LogicViewerHeadlessApp() = default; + + int LogicViewerHeadlessApp::init(int argc, char const* const* argv) + { + if (argc == 0 || argv == nullptr) + { + return static_cast(ExitCode::ErrorUnknown); + } + + CLI::App cli; + Arguments args; + + try + { + CLI::deprecate_option(cli.add_flag("--headless", "headless by default - flag will be ignored"), ""); + args.registerOptions(cli); + } + catch (const CLI::Error& err) + { + // catch configuration errors + std::cerr << err.what() << std::endl; + return -1; + } + + CLI11_PARSE(cli, argc, argv); + + EFeatureLevel featureLevel{EFeatureLevel_01}; + auto exitCode = GetFeatureLevelFromFiles(args.sceneFile(), args.logicFile(), featureLevel); + if (exitCode != 0) + { + return exitCode; + } + + exitCode = LogicViewerApp::loadScene(args, featureLevel); + if (exitCode != 0) + { + return exitCode; + } + + exitCode = LogicViewerApp::createViewer(args, featureLevel, LogicViewer::ScreenshotFunc()); + if (exitCode != 0) + { + return exitCode; + } + + if (m_interactive) + { + std::cerr << "no interactive mode in headless viewer" << std::endl; + return static_cast(ExitCode::ErrorUnknown); + } + return 0; + } + + bool LogicViewerHeadlessApp::doOneLoop() + { + return false; // never interactive + } +} + diff --git a/framework/ramses-framework-api/include/ramses-framework-api/EDcsmOfferingMode.h b/client/logic/tools/ramses-logic-viewer/LogicViewerHeadlessApp.h similarity index 55% rename from framework/ramses-framework-api/include/ramses-framework-api/EDcsmOfferingMode.h rename to client/logic/tools/ramses-logic-viewer/LogicViewerHeadlessApp.h index 6377cdc7d..23cc745df 100644 --- a/framework/ramses-framework-api/include/ramses-framework-api/EDcsmOfferingMode.h +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerHeadlessApp.h @@ -1,26 +1,28 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2019 BMW AG +// Copyright (C) 2022 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#ifndef RAMSES_EDCSMOFFERINGMODE_H -#define RAMSES_EDCSMOFFERINGMODE_H +#pragma once + +#include "LogicViewerApp.h" namespace ramses { - /** - * Specifies mode of offering content - * LocalOnly: Offer Content only to Consumers within the same process - * LocalAndRemote: Offer Content to all local and remote Consumers - */ - enum class EDcsmOfferingMode + class LogicViewerHeadlessApp : public LogicViewerApp { - LocalOnly = 0, - LocalAndRemote + public: + LogicViewerHeadlessApp(int argc, char const* const* argv); + + ~LogicViewerHeadlessApp() override; + + [[nodiscard]] bool doOneLoop() override; + + private: + [[nodiscard]] int init(int argc, char const* const* argv); }; } -#endif diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerLog.cpp b/client/logic/tools/ramses-logic-viewer/LogicViewerLog.cpp new file mode 100644 index 000000000..f876ba381 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerLog.cpp @@ -0,0 +1,137 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewerLog.h" +#include "LogicViewer.h" +#include "LogicViewerSettings.h" +#include "ramses-client-api/Scene.h" +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LogicNode.h" +#include "ramses-logic/Property.h" +#include "fmt/format.h" + +namespace ramses +{ + LogicViewerLog::LogicViewerLog(ramses::LogicEngine& engine, const LogicViewerSettings& settings) + : m_settings(settings) + , m_logicEngine(engine) + { + } + + void LogicViewerLog::logText(const std::string& text) + { + m_text.append(text); + } + + void LogicViewerLog::logInputs(ramses::LogicNode* obj, const PathVector& path) + { + const auto joinedPath = fmt::format("{}", fmt::join(path.begin(), path.end(), ".")); + std::string prefix; + if ((m_settings.luaPreferObjectIds) || obj->getName().empty()) + { + prefix = fmt::format("{}[{}]", joinedPath, obj->getId()); + } + else if (m_settings.luaPreferIdentifiers) + { + prefix = fmt::format("{}.{}", joinedPath, obj->getName()); + } + else + { + prefix = fmt::format("{}[\"{}\"]", joinedPath, obj->getName()); + } + PathVector propertyPath; + propertyPath.push_back(LogicViewer::ltnIN); + auto prop = obj->getInputs(); + if (prop) + { + for (size_t i = 0U; i < prop->getChildCount(); ++i) + { + logProperty(prop->getChild(i), prefix, propertyPath); + } + } + } + + void LogicViewerLog::logProperty(ramses::Property* prop, const std::string& prefix, PathVector& path) + { + if (prop->hasIncomingLink()) + return; + + path.push_back(prop->getName()); + + std::string strPath; + if (m_settings.luaPreferIdentifiers) + { + strPath = fmt::format("{}.{}.value", prefix, fmt::join(path.begin(), path.end(), ".")); + } + else + { + strPath = fmt::format("{}[\"{}\"].value", prefix, fmt::join(path.begin(), path.end(), "\"][\"")); + } + + switch (prop->getType()) + { + case ramses::EPropertyType::Int32: + logText(fmt::format("{} = {}\n", strPath, prop->get().value())); + break; + case ramses::EPropertyType::Int64: + logText(fmt::format("{} = {}\n", strPath, prop->get().value())); + break; + case ramses::EPropertyType::Float: + logText(fmt::format("{} = {}\n", strPath, prop->get().value())); + break; + case ramses::EPropertyType::Vec2f: { + auto val = prop->get().value(); + logText(fmt::format("{} = {{ {}, {} }}\n", strPath, val[0], val[1])); + break; + } + case ramses::EPropertyType::Vec3f: { + auto val = prop->get().value(); + logText(fmt::format("{} = {{ {}, {}, {} }}\n", strPath, val[0], val[1], val[2])); + break; + } + case ramses::EPropertyType::Vec4f: { + auto val = prop->get().value(); + logText(fmt::format("{} = {{ {}, {}, {}, {} }}\n", strPath, val[0], val[1], val[2], val[3])); + break; + } + case ramses::EPropertyType::Vec2i: { + auto val = prop->get().value(); + logText(fmt::format("{} = {{ {}, {} }}\n", strPath, val[0], val[1])); + break; + } + case ramses::EPropertyType::Vec3i: { + auto val = prop->get().value(); + logText(fmt::format("{} = {{ {}, {}, {} }}\n", strPath, val[0], val[1], val[2])); + break; + } + case ramses::EPropertyType::Vec4i: { + auto val = prop->get().value(); + logText(fmt::format("{} = {{ {}, {}, {}, {} }}\n", strPath, val[0], val[1], val[2], val[3])); + break; + } + case ramses::EPropertyType::Struct: + for (size_t i = 0U; i < prop->getChildCount(); ++i) + { + logProperty(prop->getChild(i), prefix, path); + } + break; + case ramses::EPropertyType::Bool: { + logText(fmt::format("{} = {}\n", strPath, prop->get().value())); + break; + } + case ramses::EPropertyType::String: + logText(fmt::format("{} = '{}'\n", strPath, prop->get().value())); + break; + case ramses::EPropertyType::Array: + break; + } + path.pop_back(); + } + +} + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerLog.h b/client/logic/tools/ramses-logic-viewer/LogicViewerLog.h new file mode 100644 index 000000000..a8eb52ffc --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerLog.h @@ -0,0 +1,61 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/Collection.h" +#include "LogicViewer.h" +#include + +namespace ramses +{ + class LogicEngine; + class Property; + class LogicNode; + struct LogicViewerSettings; + + class LogicViewerLog + { + public: + explicit LogicViewerLog(ramses::LogicEngine& engine, const LogicViewerSettings& settings); + + void logText(const std::string& text); + + template void logAllInputs(std::string_view headline, std::string_view ltn); + + using PathVector = std::vector; + void logInputs(ramses::LogicNode* obj, const PathVector& path); + void logProperty(ramses::Property* prop, const std::string& prefix, PathVector& path); + + [[nodiscard]] const std::string& getText() const + { + return m_text; + } + + private: + const LogicViewerSettings& m_settings; + ramses::LogicEngine& m_logicEngine; + std::string m_text; + }; + + template + inline void LogicViewerLog::logAllInputs(std::string_view headline, std::string_view ltn) + { + PathVector path; + const std::string indent = " "; + std::string name = indent + LogicViewer::ltnModule + "." + ltn.data(); + path.push_back(name); + logText(indent + headline.data()); + for (auto* node : m_logicEngine.getCollection()) + { + logInputs(node, path); + } + } +} + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerLuaTypes.cpp b/client/logic/tools/ramses-logic-viewer/LogicViewerLuaTypes.cpp new file mode 100644 index 000000000..2ce4448a6 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerLuaTypes.cpp @@ -0,0 +1,347 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewerLuaTypes.h" +#include "LogicViewer.h" +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LogicNode.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/AnchorPoint.h" +#include "ramses-logic/SkinBinding.h" +#include "ramses-logic/Property.h" +#include "internals/SolHelper.h" +#include "fmt/format.h" +#include "glm/gtx/range.hpp" + +namespace glm +{ + template + int sol_lua_push(sol::types> /*unused*/, lua_State* L, const vec& value) + { + auto vectable = sol::table::create(L, N); + for (const auto& v : value) + { + vectable.add(v); + } + int amount = sol::stack::push(L, vectable); + return amount; + } + + template + vec sol_lua_get(sol::types> /*unused*/, lua_State* L, int index, sol::stack::record& tracking) + { + sol::lua_table vectable = sol::stack::get(L, index, tracking); + vec result{}; + for (glm::length_t i = 0; i < N; ++i) + { + result[i] = vectable[i + 1]; + } + return result; + } +} + +namespace ramses +{ + namespace + { + void setPropertyValue(Property& prop, const sol::object& val) + { + switch (prop.getType()) + { + case ramses::EPropertyType::Int32: { + prop.set(val.as()); + break; + } + case ramses::EPropertyType::Int64: { + prop.set(val.as()); + break; + } + case ramses::EPropertyType::Struct: { + auto tbl = val.as(); + for (const auto& item : tbl) + { + auto ki = item.first.as(); + Property* sub = prop.getChild(ki); + if (sub == nullptr) + sol_helper::throwSolException("Property not found in struct: {}", ki); + setPropertyValue(*sub, item.second); + } + break; + } + case ramses::EPropertyType::Float: + prop.set(val.as()); + break; + case ramses::EPropertyType::Vec2f: + prop.set(val.as()); + break; + case ramses::EPropertyType::Vec3f: + prop.set(val.as()); + break; + case ramses::EPropertyType::Vec4f: + prop.set(val.as()); + break; + case ramses::EPropertyType::Vec2i: + prop.set(val.as()); + break; + case ramses::EPropertyType::Vec3i: + prop.set(val.as()); + break; + case ramses::EPropertyType::Vec4i: + prop.set(val.as()); + break; + case ramses::EPropertyType::Bool: + prop.set(val.as()); + break; + case ramses::EPropertyType::String: + prop.set(val.as()); + break; + case ramses::EPropertyType::Array: + auto tbl = val.as(); + for (const auto& item : tbl) + { + auto ki = item.first.as(); + Property* sub = prop.getChild(ki - 1); + if (sub == nullptr) + sol_helper::throwSolException("index {} out of bounds for array {}[{}]", ki, prop.getName(), prop.getChildCount()); + setPropertyValue(*sub, item.second); + } + break; + } + } + + sol::object getPropertyValue(const ramses::Property& prop, sol::this_state L) + { + switch (prop.getType()) + { + case ramses::EPropertyType::Int32: + return sol::object(L, sol::in_place, prop.get()); + case ramses::EPropertyType::Int64: + return sol::object(L, sol::in_place, prop.get()); + case ramses::EPropertyType::Float: + return sol::object(L, sol::in_place, prop.get()); + case ramses::EPropertyType::Vec2f: + return sol::object(L, sol::in_place, prop.get()); + case ramses::EPropertyType::Vec3f: + return sol::object(L, sol::in_place, prop.get()); + case ramses::EPropertyType::Vec4f: + return sol::object(L, sol::in_place, prop.get()); + case ramses::EPropertyType::Vec2i: + return sol::object(L, sol::in_place, prop.get()); + case ramses::EPropertyType::Vec3i: + return sol::object(L, sol::in_place, prop.get()); + case ramses::EPropertyType::Vec4i: + return sol::object(L, sol::in_place, prop.get()); + case ramses::EPropertyType::Bool: + return sol::object(L, sol::in_place, prop.get()); + case ramses::EPropertyType::String: + return sol::object(L, sol::in_place, prop.get()); + case ramses::EPropertyType::Array: + case ramses::EPropertyType::Struct: + // no values for array and struct + break; + } + return sol::object(L, sol::in_place, sol::lua_nil); + } + + template + sol::object getChildProperty(ramses::Property& prop, sol::stack_object key, sol::this_state L) + { + using ChildWrapper = std::conditional_t; + auto strKey = key.as>(); + Property* child = nullptr; + if (strKey) + { + child = prop.getChild(*strKey); + } + else + { + auto intKey = key.as>(); + if (intKey) + { + // expect 1-based index by lua convention + auto intKeyValue = static_cast(*intKey); + if (intKeyValue != 0u) + { + child = prop.getChild(intKeyValue - 1u); + } + } + } + if (child) + { + return sol::object(L, sol::in_place, ChildWrapper(*child)); + } + return sol::object(L, sol::in_place, sol::lua_nil); + } + } // namespace + + ConstPropertyWrapper::ConstPropertyWrapper(const ramses::Property& property) + : m_property(property) + { + } + + sol::object ConstPropertyWrapper::toString(sol::this_state L) + { + std::string name = "ConstProperty: "; + name += m_property.getName(); + return sol::object(L, sol::in_place, name); + } + + sol::object ConstPropertyWrapper::getValue(sol::this_state L) + { + return getPropertyValue(m_property, L); + } + + sol::object ConstPropertyWrapper::get(sol::stack_object key, sol::this_state L) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) avoid code duplication + return getChildProperty(const_cast(m_property), key, L); + } + + PropertyWrapper::PropertyWrapper(ramses::Property& property) + : m_property(property) + { + } + + sol::object PropertyWrapper::toString(sol::this_state L) + { + std::string name = "Property: "; + name += m_property.getName(); + return sol::object(L, sol::in_place, name); + } + + sol::object PropertyWrapper::getValue(sol::this_state L) + { + return getPropertyValue(m_property, L); + } + + void PropertyWrapper::setValue(sol::stack_object value, sol::this_state /*L*/) + { + setPropertyValue(m_property, value); + } + + sol::object PropertyWrapper::get(sol::stack_object key, sol::this_state L) + { + return getChildProperty(m_property, key, L); + } + + + LogicNodeWrapper::LogicNodeWrapper(LogicNode& logicNode) + : m_logicNode(logicNode) + { + } + + sol::object LogicNodeWrapper::toString(sol::this_state L) + { + std::string name = "LogicNode: "; + name += m_logicNode.getName(); + return sol::object(L, sol::in_place, name); + } + + sol::object LogicNodeWrapper::get(sol::stack_object key, sol::this_state L) + { + auto strKey = key.as>(); + sol::object retval; + if (strKey) + { + auto* inputs = m_logicNode.getInputs(); + if ((inputs != nullptr) && (strKey == LogicViewer::ltnIN)) + { + retval = sol::object(L, sol::in_place, PropertyWrapper(*inputs)); + } + else + { + auto* outputs = m_logicNode.getOutputs(); + if ((outputs != nullptr) && (strKey == LogicViewer::ltnOUT)) + { + retval = sol::object(L, sol::in_place, ConstPropertyWrapper(*outputs)); + } + } + } + if (!retval.valid()) + { + retval = sol::object(L, sol::in_place, sol::lua_nil); + } + return retval; + } + + template + NodeListWrapper::NodeListWrapper(LogicEngine& logicEngine) + : m_logicEngine(logicEngine) + { + } + + template + sol::object NodeListWrapper::get(sol::stack_object key, sol::this_state L) + { + auto strKey = key.as>(); + ramses::LogicNode* node = nullptr; + if (strKey) + { + node = find(*strKey); + } + else + { + auto intKey = key.as>(); + if (intKey) + { + auto* obj = m_logicEngine.findLogicObjectById(static_cast(*intKey)); + if (obj != nullptr) + { + node = obj->template as(); + } + } + } + + if (node != nullptr) + { + return sol::object(L, sol::in_place, LogicNodeWrapper(*node)); + } + return sol::object(L, sol::in_place, sol::lua_nil); + } + + template + LogicNode* NodeListWrapper::find(std::string_view key) + { + return m_logicEngine.findByName(key); + } + + template + NodeListIterator NodeListWrapper::iterator() + { + return NodeListIterator(collection()); + } + + template + Collection NodeListWrapper::collection() + { + return m_logicEngine.getCollection(); + } + + template struct NodeListWrapper; + template struct NodeListWrapper; + template struct NodeListWrapper; + template struct NodeListWrapper; + template struct NodeListWrapper; + template struct NodeListWrapper; + template struct NodeListWrapper; + template struct NodeListWrapper; + template struct NodeListWrapper; + template struct NodeListWrapper; + template struct NodeListWrapper; + template struct NodeListWrapper; +} // namespace ramses + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerLuaTypes.h b/client/logic/tools/ramses-logic-viewer/LogicViewerLuaTypes.h new file mode 100644 index 000000000..adcd8d821 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerLuaTypes.h @@ -0,0 +1,98 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "internals/SolWrapper.h" +#include "ramses-logic/Collection.h" + +namespace ramses +{ + class Property; + class LogicNode; + class LogicEngine; + + struct ConstPropertyWrapper + { + explicit ConstPropertyWrapper(const ramses::Property& property); + + sol::object getValue(sol::this_state L); + + sol::object toString(sol::this_state L); + + sol::object get(sol::stack_object key, sol::this_state L); + + const Property& m_property; + }; + + struct PropertyWrapper + { + explicit PropertyWrapper(ramses::Property& property); + + sol::object toString(sol::this_state L); + + sol::object getValue(sol::this_state L); + + void setValue(sol::stack_object value, sol::this_state L); + + sol::object get(sol::stack_object key, sol::this_state L); + + Property& m_property; + }; + + struct LogicNodeWrapper + { + explicit LogicNodeWrapper(LogicNode& logicNode); + + sol::object toString(sol::this_state L); + + sol::object get(sol::stack_object key, sol::this_state L); + + LogicNode& m_logicNode; + }; + + template + struct NodeListIterator + { + using Container = Collection; + explicit NodeListIterator(Container collection) + : m_it(collection.begin()) + , m_end(collection.end()) + { + } + + sol::object call(sol::this_state L) + { + if (m_it != m_end) + { + auto obj = sol::object(L, sol::in_place, LogicNodeWrapper(**m_it)); + ++m_it; + return obj; + } + return sol::object(L, sol::in_place, sol::lua_nil); + } + + typename Container::iterator m_it; + typename Container::iterator m_end; + }; + + template + struct NodeListWrapper + { + explicit NodeListWrapper(LogicEngine& logicEngine); + + sol::object get(sol::stack_object key, sol::this_state L); + NodeListIterator iterator(); + + LogicNode* find(std::string_view key); + Collection collection(); + + LogicEngine& m_logicEngine; + }; +} + diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerSettings.cpp b/client/logic/tools/ramses-logic-viewer/LogicViewerSettings.cpp new file mode 100644 index 000000000..5efbbfd92 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerSettings.cpp @@ -0,0 +1,144 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewerSettings.h" +#include "ImguiWrapper.h" + +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "imgui_internal.h" // for ImGuiSettingsHandler +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif + +namespace ramses +{ + namespace + { + bool IniReadFlag(const char* line, const char* fmt, int* flag) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg, cert-err34-c) no suitable replacement + return (sscanf(line, fmt, flag) == 1); + } + } + + LogicViewerSettings::LogicViewerSettings() + { + auto* ctx = ImGui::GetCurrentContext(); + assert(ctx != nullptr); + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "LogicViewerGui"; + ini_handler.TypeHash = ImHashStr("LogicViewerGui"); + ini_handler.ReadOpenFn = IniReadOpen; + ini_handler.ReadLineFn = IniReadLine; + ini_handler.WriteAllFn = IniWriteAll; + ini_handler.UserData = this; + ctx->SettingsHandlers.push_back(ini_handler); + ImGui::LoadIniSettingsFromDisk(ImGui::GetIO().IniFilename); + } + + void* LogicViewerSettings::IniReadOpen(ImGuiContext* /*context*/, ImGuiSettingsHandler* handler, const char* /*name*/) + { + return handler->UserData; + } + + void LogicViewerSettings::IniReadLine(ImGuiContext* /*context*/, ImGuiSettingsHandler* handler, void* /*entry*/, const char* line) + { + auto* gui = static_cast(handler->UserData); + int flag = 0; + if (IniReadFlag(line, "ShowWindow=%d", &flag)) + { + gui->showWindow = (flag != 0); + } + else if (IniReadFlag(line, "ShowInterfaces=%d", &flag)) + { + gui->showInterfaces = (flag != 0); + } + else if (IniReadFlag(line, "ShowScripts=%d", &flag)) + { + gui->showScripts = (flag != 0); + } + else if (IniReadFlag(line, "ShowAnimationNodes=%d", &flag)) + { + gui->showAnimationNodes = (flag != 0); + } + else if (IniReadFlag(line, "ShowTimerNodes=%d", &flag)) + { + gui->showTimerNodes = (flag != 0); + } + else if (IniReadFlag(line, "ShowDataArrays=%d", &flag)) + { + gui->showDataArrays = (flag != 0); + } + else if (IniReadFlag(line, "ShowRamsesBindings=%d", &flag)) + { + gui->showRamsesBindings = (flag != 0); + } + else if (IniReadFlag(line, "ShowUpdateReport=%d", &flag)) + { + gui->showUpdateReport = (flag != 0); + } + else if (IniReadFlag(line, "ShowLinkedInputs=%d", &flag)) + { + gui->showLinkedInputs = (flag != 0); + } + else if (IniReadFlag(line, "ShowOutputs=%d", &flag)) + { + gui->showOutputs = (flag != 0); + } + else if (IniReadFlag(line, "LuaPreferObjectIds=%d", &flag)) + { + gui->luaPreferObjectIds = (flag != 0); + } + else if (IniReadFlag(line, "LuaPreferIdentifiers=%d", &flag)) + { + gui->luaPreferIdentifiers = (flag != 0); + } + else if (IniReadFlag(line, "ShowDisplaySettings=%d", &flag)) + { + gui->showDisplaySettings = (flag != 0); + } + } + + void LogicViewerSettings::IniWriteAll(ImGuiContext* /*context*/, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) + { + auto* gui = static_cast(handler->UserData); + buf->reserve(buf->size() + 400); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("[%s][Settings]\n", handler->TypeName); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("ShowWindow=%d\n", gui->showWindow ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("ShowInterfaces=%d\n", gui->showInterfaces ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("ShowScripts=%d\n", gui->showScripts ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("ShowAnimationNodes=%d\n", gui->showAnimationNodes ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("ShowTimerNodes=%d\n", gui->showTimerNodes ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("ShowDataArrays=%d\n", gui->showDataArrays ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("ShowRamsesBindings=%d\n", gui->showRamsesBindings ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("ShowUpdateReport=%d\n", gui->showUpdateReport ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("ShowLinkedInputs=%d\n", gui->showLinkedInputs ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("ShowOutputs=%d\n", gui->showOutputs ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("LuaPreferObjectIds=%d\n", gui->luaPreferObjectIds ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("LuaPreferIdentifiers=%d\n", gui->luaPreferIdentifiers ? 1 : 0); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) 3rd party interface + buf->appendf("ShowDisplaySettings=%d\n", gui->showDisplaySettings ? 1 : 0); + buf->append("\n"); + } +} diff --git a/client/logic/tools/ramses-logic-viewer/LogicViewerSettings.h b/client/logic/tools/ramses-logic-viewer/LogicViewerSettings.h new file mode 100644 index 000000000..510c013fb --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/LogicViewerSettings.h @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +struct ImGuiSettingsHandler; +struct ImGuiContext; +struct ImGuiTextBuffer; + +namespace ramses +{ + struct LogicViewerSettings + { + bool showWindow = true; + bool showOutputs = true; + bool showLinkedInputs = true; + bool showInterfaces = true; + bool showScripts = false; + bool showAnimationNodes = false; + bool showTimerNodes = false; + bool showDataArrays = false; + bool showRamsesBindings = false; + bool showUpdateReport = true; + + bool luaPreferObjectIds = false; + bool luaPreferIdentifiers = false; + + bool showDisplaySettings = false; + + LogicViewerSettings(); + + static void* IniReadOpen(ImGuiContext* context, ImGuiSettingsHandler* handler, const char* name); + static void IniReadLine(ImGuiContext* context, ImGuiSettingsHandler* handler, void* entry, const char* line); + static void IniWriteAll(ImGuiContext* context, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf); + }; +} diff --git a/client/logic/tools/ramses-logic-viewer/Result.h b/client/logic/tools/ramses-logic-viewer/Result.h new file mode 100644 index 000000000..67606f604 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/Result.h @@ -0,0 +1,57 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- +#pragma once + +#include +#include + +namespace ramses +{ + /** + * @brief Result class as a return object for functions + */ + class Result + { + public: + Result() = default; + + explicit Result(std::string msg) + : m_message(std::move(msg)) + { + } + + [[nodiscard]] bool ok() const + { + return m_message.empty(); + } + + [[nodiscard]] const std::string& getMessage() const + { + return m_message; + } + + [[nodiscard]] bool operator==(const Result& rhs) const + { + return rhs.m_message == m_message; + } + + [[nodiscard]] bool operator!=(const Result& rhs) const + { + return !(rhs == *this); + } + + private: + std::string m_message; + }; + + inline std::ostream& operator<<(std::ostream& os, const Result& result) + { + os << result.getMessage(); + return os; + } +} diff --git a/client/logic/tools/ramses-logic-viewer/SceneSetup.h b/client/logic/tools/ramses-logic-viewer/SceneSetup.h new file mode 100644 index 000000000..3be4a7d1c --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/SceneSetup.h @@ -0,0 +1,167 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ImguiClientHelper.h" + +#include "ramses-client.h" + +#include "ramses-renderer-api/RamsesRenderer.h" +#include "ramses-renderer-api/DisplayConfig.h" +#include "ramses-renderer-api/RendererSceneControl.h" + +#include + +class ISceneSetup +{ +public: + virtual ~ISceneSetup() = default; + + virtual void apply() = 0; + + [[nodiscard]] virtual uint32_t getWidth() const = 0; + + [[nodiscard]] virtual uint32_t getHeight() const = 0; + + [[nodiscard]] virtual ramses::displayBufferId_t getOffscreenBuffer() const = 0; + + [[nodiscard]] virtual ramses::TextureSampler* getTextureSampler() = 0; +}; + +class OffscreenSetup : public ISceneSetup +{ +public: + OffscreenSetup(ramses::ImguiClientHelper& imguiHelper, ramses::RamsesRenderer& renderer, ramses::Scene* scene, ramses::displayId_t display, uint32_t width, uint32_t height) + : m_imguiHelper(imguiHelper) + , m_sceneControl(renderer.getSceneControlAPI()) + , m_scene(scene) + , m_width(width) + , m_height(height) + { + m_ob = renderer.createOffscreenBuffer(display, width, height); + renderer.flush(); + + static const std::array imgbuf = {255, 255, 255, 255}; + ramses::MipLevelData mipLevelData(4, imgbuf.data()); + auto* texture = imguiHelper.getScene()->createTexture2D(ramses::ETextureFormat::RGBA8, 1, 1, 1, &mipLevelData); + m_sampler = imguiHelper.getScene()->createTextureSampler( + ramses::ETextureAddressMode::Clamp, ramses::ETextureAddressMode::Clamp, ramses::ETextureSamplingMethod::Linear, ramses::ETextureSamplingMethod::Linear, *texture); + + const auto guiSceneId = imguiHelper.getScene()->getSceneId(); + m_sceneControl->setSceneMapping(scene->getSceneId(), display); + m_sceneControl->setSceneMapping(guiSceneId, display); + m_sceneControl->setSceneState(scene->getSceneId(), ramses::RendererSceneState::Ready); + m_sceneControl->setSceneState(guiSceneId, ramses::RendererSceneState::Ready); + m_sceneControl->flush(); + } + + void apply() override + { + const auto guiSceneId = m_imguiHelper.getScene()->getSceneId(); + const ramses::dataConsumerId_t consumerId(519); + + m_imguiHelper.getScene()->createTextureConsumer(*m_sampler, consumerId); + m_imguiHelper.getScene()->flush(42); + m_imguiHelper.waitForSceneVersion(guiSceneId, 42); + m_imguiHelper.waitForOffscreenBufferCreated(m_ob); + + m_imguiHelper.waitForSceneState(guiSceneId, ramses::RendererSceneState::Ready); + m_imguiHelper.waitForSceneState(m_scene->getSceneId(), ramses::RendererSceneState::Ready); + + m_sceneControl->setSceneDisplayBufferAssignment(m_scene->getSceneId(), m_ob); + m_sceneControl->linkOffscreenBuffer(m_ob, guiSceneId, consumerId); + m_sceneControl->flush(); + m_imguiHelper.waitForOffscreenBufferLinked(guiSceneId); + m_sceneControl->setSceneState(guiSceneId, ramses::RendererSceneState::Rendered); + m_sceneControl->setSceneState(m_scene->getSceneId(), ramses::RendererSceneState::Rendered); + m_sceneControl->flush(); + + m_imguiHelper.waitForSceneState(m_scene->getSceneId(), ramses::RendererSceneState::Rendered); + m_imguiHelper.waitForSceneState(guiSceneId, ramses::RendererSceneState::Rendered); + } + + [[nodiscard]] uint32_t getWidth() const override + { + return m_width; + } + + [[nodiscard]] uint32_t getHeight() const override + { + return m_height; + } + + [[nodiscard]] ramses::displayBufferId_t getOffscreenBuffer() const override + { + return m_ob; + } + + [[nodiscard]] ramses::TextureSampler* getTextureSampler() override + { + return m_sampler; + } + +private: + ramses::ImguiClientHelper& m_imguiHelper; + ramses::RendererSceneControl* m_sceneControl; + ramses::Scene* m_scene; + uint32_t m_width; + uint32_t m_height; + ramses::displayBufferId_t m_ob; + ramses::TextureSampler* m_sampler = nullptr; +}; + +class FramebufferSetup : public ISceneSetup +{ +public: + FramebufferSetup(ramses::ImguiClientHelper& imguiHelper, ramses::RamsesRenderer& renderer, ramses::Scene* scene, ramses::displayId_t display) + : m_imguiHelper(imguiHelper) + , m_sceneControl(renderer.getSceneControlAPI()) + , m_scene(scene) + { + const auto guiSceneId = imguiHelper.getScene()->getSceneId(); + m_sceneControl->setSceneMapping(scene->getSceneId(), display); + m_sceneControl->setSceneMapping(guiSceneId, display); + m_sceneControl->setSceneState(scene->getSceneId(), ramses::RendererSceneState::Rendered); + m_sceneControl->flush(); + } + + void apply() override + { + m_imguiHelper.waitForSceneState(m_scene->getSceneId(), ramses::RendererSceneState::Rendered); + const auto guiSceneId = m_imguiHelper.getScene()->getSceneId(); + m_sceneControl->setSceneState(guiSceneId, ramses::RendererSceneState::Rendered); + m_sceneControl->flush(); + } + + [[nodiscard]] uint32_t getWidth() const override + { + return m_imguiHelper.getViewportWidth(); + } + + [[nodiscard]] uint32_t getHeight() const override + { + return m_imguiHelper.getViewportHeight(); + } + + [[nodiscard]] ramses::displayBufferId_t getOffscreenBuffer() const override + { + return ramses::displayBufferId_t(); + } + + [[nodiscard]] ramses::TextureSampler* getTextureSampler() override + { + return nullptr; + } + +private: + ramses::ImguiClientHelper& m_imguiHelper; + ramses::RendererSceneControl* m_sceneControl; + ramses::Scene* m_scene; +}; + diff --git a/client/logic/tools/ramses-logic-viewer/UpdateReportSummary.h b/client/logic/tools/ramses-logic-viewer/UpdateReportSummary.h new file mode 100644 index 000000000..1a0cc0824 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/UpdateReportSummary.h @@ -0,0 +1,152 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-logic/LogicEngineReport.h" +#include +#include +#include + +namespace ramses +{ + class UpdateReportSummary + { + public: + template + struct StatisticEntry + { + T minValue = std::numeric_limits::max(); + T maxValue = std::numeric_limits::min(); + T sum = T(); + + void add(const T& value) + { + sum += value; + minValue = std::min(minValue, value); + maxValue = std::max(maxValue, value); + } + + void reset() + { + minValue = std::numeric_limits::max(); + maxValue = std::numeric_limits::min(); + sum = T(); + } + }; + + template + struct Summary + { + Summary() + : minValue() + , maxValue() + , average() + { + } + + Summary(const StatisticEntry& stat, size_t count) + : minValue(stat.minValue) + , maxValue(stat.maxValue) + , average((count != 0u) ? T(stat.sum / count) : T()) + { + } + + T minValue; + T maxValue; + T average; + }; + + [[nodiscard]] const auto& getTotalTime() const + { + return m_totalTimeSummary; + } + + [[nodiscard]] const auto& getSortTime() const + { + return m_sortTimeSummary; + } + + [[nodiscard]] const auto& getLinkActivations() const + { + return m_linkActivationsSummary; + } + + [[nodiscard]] const auto& getNodesExecuted() const + { + return m_nodesExecuted; + } + + [[nodiscard]] const auto& getNodesSkippedExecution() const + { + return m_nodesSkipped; + } + + void add(ramses::LogicEngineReport&& report) + { + m_sortTime.add(report.getTopologySortExecutionTime()); + m_linkActivations.add(report.getTotalLinkActivations()); + const auto totalTime = report.getTotalUpdateExecutionTime(); + if (totalTime > m_totalTime.maxValue) + { + m_report = std::move(report); + } + m_totalTime.add(totalTime); + ++m_measureCount; + if (m_measureCount == m_measureInterval) + { + applySummary(); + } + } + + void setInterval(size_t interval) + { + if (interval >= 1u) + { + if (interval <= m_measureCount) + { + applySummary(); + } + m_measureInterval = interval; + } + } + + [[nodiscard]] size_t getInterval() const + { + return m_measureInterval; + } + + private: + void applySummary() + { + m_nodesExecuted = m_report.getNodesExecuted(); + std::sort(m_nodesExecuted.begin(), m_nodesExecuted.end(), [](const auto& a, const auto& b) { return a.second > b.second; }); + m_nodesSkipped = m_report.getNodesSkippedExecution(); + m_totalTimeSummary = {m_totalTime, m_measureCount}; + m_sortTimeSummary = {m_sortTime, m_measureCount}; + m_linkActivationsSummary = {m_linkActivations, m_measureCount}; + m_totalTime.reset(); + m_sortTime.reset(); + m_linkActivations.reset(); + m_measureCount = 0; + } + + ramses::LogicEngineReport m_report; + StatisticEntry m_totalTime; + StatisticEntry m_sortTime; + StatisticEntry m_linkActivations; + Summary m_totalTimeSummary; + Summary m_sortTimeSummary; + Summary m_linkActivationsSummary; + std::vector m_nodesExecuted; + std::vector m_nodesSkipped; + size_t m_measureInterval = 60u; + size_t m_measureCount = 0u; + }; +} + diff --git a/demo/ramses-text-layout-demo/src/main.cpp b/client/logic/tools/ramses-logic-viewer/main.cpp similarity index 78% rename from demo/ramses-text-layout-demo/src/main.cpp rename to client/logic/tools/ramses-logic-viewer/main.cpp index 69b4d889f..4512289fb 100644 --- a/demo/ramses-text-layout-demo/src/main.cpp +++ b/client/logic/tools/ramses-logic-viewer/main.cpp @@ -1,15 +1,15 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH +// Copyright (C) 2021 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#include "TextLayoutDemo.h" +#include "LogicViewerGuiApp.h" int main(int argc, char* argv[]) { - TextLayoutDemo test(argc, argv); - return 0; + ramses::LogicViewerGuiApp app(argc, argv); + return app.run(); } diff --git a/client/logic/tools/ramses-logic-viewer/main_headless.cpp b/client/logic/tools/ramses-logic-viewer/main_headless.cpp new file mode 100644 index 000000000..781164150 --- /dev/null +++ b/client/logic/tools/ramses-logic-viewer/main_headless.cpp @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicViewerHeadlessApp.h" + +int main(int argc, char* argv[]) +{ + ramses::LogicViewerHeadlessApp app(argc, argv); + return app.run(); +} diff --git a/client/logic/unittests/api/AnchorPointTest.cpp b/client/logic/unittests/api/AnchorPointTest.cpp new file mode 100644 index 000000000..f234dcad9 --- /dev/null +++ b/client/logic/unittests/api/AnchorPointTest.cpp @@ -0,0 +1,479 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "RamsesTestUtils.h" +#include "SerializationTestUtils.h" +#include "WithTempDirectory.h" + +#include "impl/LogicEngineImpl.h" +#include "impl/AnchorPointImpl.h" +#include "impl/PropertyImpl.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "impl/RamsesCameraBindingImpl.h" +#include "generated/AnchorPointGen.h" + +#include "ramses-logic/AnchorPoint.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/LogicEngine.h" + +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/OrthographicCamera.h" + +namespace ramses::internal +{ + class AnAnchorPoint : public ALogicEngine + { + protected: + RamsesNodeBinding& m_nodeBinding{ *m_logicEngine.createRamsesNodeBinding(*m_node) }; + RamsesCameraBinding& m_cameraBinding{ *m_logicEngine.createRamsesCameraBinding(*m_camera) }; + }; + + TEST_F(AnAnchorPoint, HasANameAndIdAfterCreation) + { + const auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_cameraBinding, "anchor"); + EXPECT_EQ("anchor", anchorPoint.getName()); + EXPECT_EQ(3u, anchorPoint.getId()); + } + + TEST_F(AnAnchorPoint, ReferencesBindings) + { + const auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_cameraBinding, "anchor"); + EXPECT_EQ(&m_nodeBinding.getRamsesNode(), &anchorPoint.getRamsesNode()); + EXPECT_EQ(&m_cameraBinding.getRamsesCamera(), &anchorPoint.getRamsesCamera()); + EXPECT_EQ(&m_nodeBinding.m_nodeBinding, &anchorPoint.m_anchorPointImpl.getRamsesNodeBinding()); + EXPECT_EQ(&m_cameraBinding.m_cameraBinding, &anchorPoint.m_anchorPointImpl.getRamsesCameraBinding()); + } + + TEST_F(AnAnchorPoint, HasNoInputsAfterCreation) + { + auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_cameraBinding, "anchor"); + EXPECT_EQ(nullptr, anchorPoint.getInputs()); + } + + TEST_F(AnAnchorPoint, HasOutputsAfterCreation) + { + auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_cameraBinding, "anchor"); + ASSERT_NE(nullptr, anchorPoint.getOutputs()); + ASSERT_EQ(2u, anchorPoint.getOutputs()->getChildCount()); + EXPECT_EQ(anchorPoint.getOutputs()->getChild(0u), anchorPoint.getOutputs()->getChild("viewportCoords")); + EXPECT_EQ(EPropertyType::Vec2f, anchorPoint.getOutputs()->getChild(0u)->getType()); + EXPECT_EQ(anchorPoint.getOutputs()->getChild(1u), anchorPoint.getOutputs()->getChild("depth")); + EXPECT_EQ(EPropertyType::Float, anchorPoint.getOutputs()->getChild(1u)->getType()); + } + + TEST_F(AnAnchorPoint, ProducesErrorOnUpdateIfCameraNotInitialized) + { + const auto uninitializedCamera = m_scene->createOrthographicCamera(); + m_logicEngine.createAnchorPoint(m_nodeBinding, *m_logicEngine.createRamsesCameraBinding(*uninitializedCamera), "anchor"); + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Failed to retrieve projection matrix from Ramses camera!"); + } + + class AnAnchorPoint_Serialization : public AnAnchorPoint + { + protected: + enum class ESerializationIssue + { + AllValid, + MissingName, + MissingRootOutput, + RootOutputNotStruct, + OutputPropertyInvalid, + CannotResolveNodeBinding, + CannotResolveCameraBinding + }; + + std::unique_ptr deserializeSerializedDataWithIssue(ESerializationIssue issue) + { + { + const EPropertyType propType = (issue == ESerializationIssue::OutputPropertyInvalid ? EPropertyType::Float : EPropertyType::Vec2f); + auto outputsType = MakeStruct("", { + TypeData{"viewportCoords", propType}, + TypeData{"depth", EPropertyType::Float}, + }); + + HierarchicalTypeData outputs = (issue == ESerializationIssue::RootOutputNotStruct ? MakeType("", EPropertyType::Bool) : outputsType); + auto outputsImpl = std::make_unique(std::move(outputs), EPropertySemantics::ScriptOutput); + + auto fbAnchorPoint = rlogic_serialization::CreateAnchorPoint( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, (issue == ESerializationIssue::MissingName ? 0 : m_flatBufferBuilder.CreateString("name")), 1u, 0u, 0u), + m_nodeBinding.getId(), + m_cameraBinding.getId(), + 0, // no inputs + (issue == ESerializationIssue::MissingRootOutput ? 0 : PropertyImpl::Serialize(*outputsImpl, m_flatBufferBuilder, m_serializationMap))); + m_flatBufferBuilder.Finish(fbAnchorPoint); + } + + if (issue != ESerializationIssue::CannotResolveNodeBinding) + m_deserializationMap.storeLogicObject(m_nodeBinding.getId(), m_nodeBinding.m_nodeBinding); + if (issue != ESerializationIssue::CannotResolveCameraBinding) + m_deserializationMap.storeLogicObject(m_cameraBinding.getId(), m_cameraBinding.m_cameraBinding); + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + return AnchorPointImpl::Deserialize(serialized, m_errorReporting, m_deserializationMap); + } + + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + ErrorReporting m_errorReporting; + SerializationMap m_serializationMap; + DeserializationMap m_deserializationMap; + }; + + TEST_F(AnAnchorPoint_Serialization, DeserializesAllData) + { + { + AnchorPointImpl anchor(m_nodeBinding.m_nodeBinding, m_cameraBinding.m_cameraBinding, "name", 1u); + anchor.createRootProperties(); + (void)AnchorPointImpl::Serialize(anchor, m_flatBufferBuilder, m_serializationMap); + } + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + m_deserializationMap.storeLogicObject(m_nodeBinding.getId(), m_nodeBinding.m_nodeBinding); + m_deserializationMap.storeLogicObject(m_cameraBinding.getId(), m_cameraBinding.m_cameraBinding); + + std::unique_ptr deserialized = AnchorPointImpl::Deserialize(serialized, m_errorReporting, m_deserializationMap); + ASSERT_TRUE(deserialized); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + + EXPECT_EQ(deserialized->getName(), "name"); + EXPECT_EQ(deserialized->getId(), 1u); + EXPECT_FALSE(deserialized->getInputs()); + ASSERT_TRUE(deserialized->getOutputs()); + ASSERT_EQ(2u, deserialized->getOutputs()->getChildCount()); + EXPECT_EQ("viewportCoords", deserialized->getOutputs()->getChild(0u)->getName()); + EXPECT_EQ(EPropertyType::Vec2f, deserialized->getOutputs()->getChild(0u)->getType()); + EXPECT_EQ("depth", deserialized->getOutputs()->getChild(1u)->getName()); + EXPECT_EQ(EPropertyType::Float, deserialized->getOutputs()->getChild(1u)->getType()); + EXPECT_EQ(&m_nodeBinding.m_nodeBinding, &deserialized->getRamsesNodeBinding()); + EXPECT_EQ(&m_cameraBinding.m_cameraBinding, &deserialized->getRamsesCameraBinding()); + } + + TEST_F(AnAnchorPoint_Serialization, CanSerializeWithNoIssue) + { + EXPECT_TRUE(deserializeSerializedDataWithIssue(AnAnchorPoint_Serialization::ESerializationIssue::AllValid)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + + TEST_F(AnAnchorPoint_Serialization, ReportsSerializationError_MissingName) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(AnAnchorPoint_Serialization::ESerializationIssue::MissingName)); + ASSERT_EQ(2u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LogicObject base from serialized data: missing name!"); + EXPECT_EQ(m_errorReporting.getErrors()[1].message, "Fatal error during loading of AnchorPoint from serialized data: missing name and/or ID!"); + } + + TEST_F(AnAnchorPoint_Serialization, ReportsSerializationError_MissingRootOutput) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(AnAnchorPoint_Serialization::ESerializationIssue::MissingRootOutput)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of AnchorPoint from serialized data: missing root output!"); + } + + TEST_F(AnAnchorPoint_Serialization, ReportsSerializationError_RootOutputNotStruct) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(AnAnchorPoint_Serialization::ESerializationIssue::RootOutputNotStruct)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of AnchorPoint from serialized data: root output has unexpected type!"); + } + + TEST_F(AnAnchorPoint_Serialization, ReportsSerializationError_PropertyInvalid) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(AnAnchorPoint_Serialization::ESerializationIssue::OutputPropertyInvalid)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of AnchorPoint: missing or invalid properties!"); + } + + TEST_F(AnAnchorPoint_Serialization, ReportsSerializationError_UnresolvedNodeBinding) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(AnAnchorPoint_Serialization::ESerializationIssue::CannotResolveNodeBinding)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of AnchorPoint: could not resolve NodeBinding and/or CameraBinding!"); + } + + TEST_F(AnAnchorPoint_Serialization, ReportsSerializationError_UnresolvedCameraBinding) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(AnAnchorPoint_Serialization::ESerializationIssue::CannotResolveCameraBinding)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of AnchorPoint: could not resolve NodeBinding and/or CameraBinding!"); + } + + class AnAnchorPoint_Math : public AnAnchorPoint + { + protected: + void SetUp() override + { + m_node->setTranslation({1.f, 2.f, 3.f}); + m_node->setRotation({-1.f, -2.f, -3.f}); + m_node->setScaling({1.f, 2.f, 3.f}); + + auto nodeTransNode = m_scene->createNode(); + nodeTransNode->setTranslation({1.f, 2.f, -3.f}); + nodeTransNode->addChild(*m_node); + + m_perspCamera.setTranslation({1.f, 2.f, -3.f}); + m_perspCamera.setRotation({-1.f, -2.f, 3.f}); + m_perspCamera.setScaling({1.f, 2.f, -3.f}); + m_perspCamera.setFrustum(75.f, 0.8f, 0.01f, 100.f); + m_perspCamera.setViewport(10, 20, 30u, 40u); + ASSERT_EQ(ramses::StatusOK, m_perspCamera.validate()); + + m_orthoCamera.setTranslation({1.f, 2.f, -3.f}); + m_orthoCamera.setRotation({-1.f, -2.f, 3.f}); + m_orthoCamera.setScaling({1.f, 2.f, -3.f}); + m_orthoCamera.setFrustum(-0.5f, 0.5f, -1.f, 2.f, 0.01f, 100.f); + m_orthoCamera.setViewport(10, 20, 30u, 40u); + ASSERT_EQ(ramses::StatusOK, m_orthoCamera.validate()); + + auto camTransNode = m_scene->createNode(); + camTransNode->setTranslation({1.f, 2.f, -3.f}); + camTransNode->addChild(m_perspCamera); + camTransNode->addChild(m_orthoCamera); + } + + ramses::PerspectiveCamera& m_perspCamera{ *m_scene->createPerspectiveCamera() }; + ramses::OrthographicCamera& m_orthoCamera{ *m_scene->createOrthographicCamera() }; + RamsesCameraBinding& m_perspCameraBinding{ *m_logicEngine.createRamsesCameraBinding(m_perspCamera) }; + RamsesCameraBinding& m_orthoCameraBinding{ *m_logicEngine.createRamsesCameraBinding(m_orthoCamera) }; + }; + + TEST_F(AnAnchorPoint_Math, CalculatesCoords_PerspCamera) + { + const auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_perspCameraBinding, "anchor"); + EXPECT_TRUE(m_logicEngine.update()); + const auto coords = *anchorPoint.getOutputs()->getChild(0u)->get(); + EXPECT_FLOAT_EQ(17.560308f, coords[0]); + EXPECT_FLOAT_EQ(19.317562f, coords[1]); + EXPECT_FLOAT_EQ(0.99509573f, *anchorPoint.getOutputs()->getChild(1u)->get()); + } + + TEST_F(AnAnchorPoint_Math, CalculatesCoords_OrthoCamera) + { + const auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_orthoCameraBinding, "anchor"); + EXPECT_TRUE(m_logicEngine.update()); + const auto coords = *anchorPoint.getOutputs()->getChild(0u)->get(); + EXPECT_FLOAT_EQ(21.281908f, coords[0]); + EXPECT_FLOAT_EQ(12.63566f, coords[1]); + EXPECT_FLOAT_EQ(0.019886762f, *anchorPoint.getOutputs()->getChild(1u)->get()); + } + + class AnAnchorPoint_Dirtiness : public AnAnchorPoint_Math + { + protected: + void SetUp() override + { + AnAnchorPoint_Math::SetUp(); + m_logicEngine.enableUpdateReport(true); + } + + void expectNodeExecuted(const LogicNode& node) const + { + const auto report = m_logicEngine.getLastUpdateReport(); + const auto& executedNodes = report.getNodesExecuted(); + const auto it = std::find_if(executedNodes.cbegin(), executedNodes.cend(), [&node](const auto& timedNode) { return timedNode.first == &node; }); + EXPECT_NE(it, executedNodes.cend()); + } + + void expectNodeSkipped(const LogicNode& node) const + { + const auto report = m_logicEngine.getLastUpdateReport(); + const auto& skippedNodes = report.getNodesSkippedExecution(); + const auto it = std::find(skippedNodes.cbegin(), skippedNodes.cend(), &node); + EXPECT_NE(it, skippedNodes.cend()); + } + }; + + TEST_F(AnAnchorPoint_Dirtiness, RecalculatesWhenModelMatrixChanges) + { + const auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_perspCameraBinding, "anchor"); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + + m_node->setTranslation({123.f, 231.f, 321.f}); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + } + + TEST_F(AnAnchorPoint_Dirtiness, RecalculatesWhenViewMatrixChanges) + { + const auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_perspCameraBinding, "anchor"); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + + m_perspCamera.setTranslation({123.f, 231.f, 321.f}); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + } + + TEST_F(AnAnchorPoint_Dirtiness, RecalculatesWhenProjectionMatrixChanges) + { + const auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_perspCameraBinding, "anchor"); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + + m_perspCamera.setFrustum(10.f, 1.f, 1000.f, 2000.f); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + } + + TEST_F(AnAnchorPoint_Dirtiness, RecalculatesWhenNodeParentTransChanges) + { + auto parentNode = m_scene->createNode(); + parentNode->addChild(*m_node); + + const auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_perspCameraBinding, "anchor"); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + + parentNode->setTranslation({123.f, 231.f, 321.f}); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + } + + TEST_F(AnAnchorPoint_Dirtiness, RecalculatesWhenCameraParentTransChanges) + { + auto parentNode = m_scene->createNode(); + parentNode->addChild(m_perspCamera); + + const auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_perspCameraBinding, "anchor"); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + + parentNode->setTranslation({123.f, 231.f, 321.f}); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + } + + TEST_F(AnAnchorPoint_Dirtiness, RecalculatesWhenViewportChanges) + { + const auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_perspCameraBinding, "anchor"); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + + m_perspCamera.setViewport(666, -100, 333u, 444u); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(anchorPoint); + } + + TEST_F(AnAnchorPoint_Dirtiness, OutputIsNotDirtyIfNothingChanged) + { + const auto& anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBinding, m_perspCameraBinding, "anchor"); + const auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.coords = Type:Vec2f() + OUT.y = Type:Float() + end + + function run(IN,OUT) + OUT.y = IN.coords[2]; + end + )"); + + ASSERT_TRUE(m_logicEngine.link(*anchorPoint.getOutputs()->getChild("viewportCoords"), *script->getInputs()->getChild("coords"))); + EXPECT_TRUE(m_logicEngine.update()); + expectNodeExecuted(*script); + + EXPECT_TRUE(m_logicEngine.update()); + expectNodeSkipped(*script); + + EXPECT_TRUE(m_logicEngine.update()); + expectNodeSkipped(*script); + } + + // This test is to cover update order of anchor point with unknown dependencies. + // Anchor point is special in sense it depends on ramses node (via binding), however those depend on other ramses nodes + // (transformation topology), e.g. node ancestor, which can be affected by another node binding via script for example. + // Workaround for case of unknown dependency is dual update, which is tested here. + // + // Test setup consists of script bound to nodeA and anchor point attached to nodeB. nodeA and nodeB are interdependent + // inside ramses, unknown to rlogic. + // There is one test for each order of creation which affects also order of execution, result is expected same after dual update. + class AnAnchorPoint_UpdateOrder : public AnAnchorPoint_Math + { + protected: + void SetUp() override + { + AnAnchorPoint_Math::SetUp(); + + m_nodeAnchor.setParent(m_nodeScript); + } + + ramses::Node& m_nodeAnchor = *m_scene->createNode(); + RamsesNodeBinding& m_nodeBindingAnchor = *m_logicEngine.createRamsesNodeBinding(m_nodeAnchor); + + const std::string_view m_scriptSrc = R"( + function interface(IN,OUT) + IN.trans = Type:Vec3f() + OUT.trans = Type:Vec3f() + end + + function run(IN,OUT) + OUT.trans = IN.trans; + end + )"; + ramses::Node& m_nodeScript = *m_scene->createNode(); + RamsesNodeBinding& m_nodeBindingScript = *m_logicEngine.createRamsesNodeBinding(m_nodeScript); + }; + + TEST_F(AnAnchorPoint_UpdateOrder, OutputIsCalculatedWithDoubleUpdateIfDependencyUnknown_createAnchorFirst) + { + // create anchor then script + AnchorPoint& m_anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBindingAnchor, m_perspCameraBinding); + LuaScript& m_passThruScript = *m_logicEngine.createLuaScript(m_scriptSrc); + m_logicEngine.link(*m_passThruScript.getOutputs()->getChild("trans"), *m_nodeBindingScript.getInputs()->getChild("translation")); + + const vec2f initialCoordsExpected{ -9.3664684f, -6.0138535f }; + const float initialDepthExpected = 0.99510324f; + EXPECT_TRUE(m_logicEngine.update()); + const auto coordsInitial = *m_anchorPoint.getOutputs()->getChild(0u)->get(); + EXPECT_EQ(initialCoordsExpected, coordsInitial); + EXPECT_EQ(initialDepthExpected, *m_anchorPoint.getOutputs()->getChild(1u)->get()); + + m_passThruScript.getInputs()->getChild("trans")->set(vec3f{ 666.f, 333.f, 111.f }); + const vec2f coordsAfterChangeExpected{ 525.07275f, 136.18787f }; + const float depthAfterChangeExpected = 0.99979484f; + + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.update()); + + const auto coordsAfterChange = *m_anchorPoint.getOutputs()->getChild(0u)->get(); + EXPECT_EQ(coordsAfterChangeExpected, coordsAfterChange); + EXPECT_EQ(depthAfterChangeExpected, *m_anchorPoint.getOutputs()->getChild(1u)->get()); + } + + TEST_F(AnAnchorPoint_UpdateOrder, OutputIsCalculatedWithDoubleUpdateIfDependencyUnknown_createAnchorLast) + { + // create script then anchor + LuaScript& m_passThruScript = *m_logicEngine.createLuaScript(m_scriptSrc); + m_logicEngine.link(*m_passThruScript.getOutputs()->getChild("trans"), *m_nodeBindingScript.getInputs()->getChild("translation")); + AnchorPoint& m_anchorPoint = *m_logicEngine.createAnchorPoint(m_nodeBindingAnchor, m_perspCameraBinding); + + const vec2f initialCoordsExpected{ -9.3664684f, -6.0138535f }; + const float initialDepthExpected = 0.99510324f; + EXPECT_TRUE(m_logicEngine.update()); + const auto coordsInitial = *m_anchorPoint.getOutputs()->getChild(0u)->get(); + EXPECT_EQ(initialCoordsExpected, coordsInitial); + EXPECT_EQ(initialDepthExpected, *m_anchorPoint.getOutputs()->getChild(1u)->get()); + + m_passThruScript.getInputs()->getChild("trans")->set(vec3f{ 666.f, 333.f, 111.f }); + const vec2f coordsAfterChangeExpected{ 525.07275f, 136.18787f }; + const float depthAfterChangeExpected = 0.99979484f; + + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.update()); + + const auto coordsAfterChange = *m_anchorPoint.getOutputs()->getChild(0u)->get(); + EXPECT_EQ(coordsAfterChangeExpected, coordsAfterChange); + EXPECT_EQ(depthAfterChangeExpected, *m_anchorPoint.getOutputs()->getChild(1u)->get()); + } +} diff --git a/client/logic/unittests/api/AnimationNodeConfigTest.cpp b/client/logic/unittests/api/AnimationNodeConfigTest.cpp new file mode 100644 index 000000000..deb83da04 --- /dev/null +++ b/client/logic/unittests/api/AnimationNodeConfigTest.cpp @@ -0,0 +1,269 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/DataArray.h" +#include + +namespace ramses::internal +{ + class AnAnimationNodeConfig : public ::testing::Test + { + public: + void SetUp() override + { + m_dataFloat = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }); + m_dataVec2 = m_logicEngine.createDataArray(std::vector{ { 1.f, 2.f }, { 3.f, 4.f }, { 5.f, 6.f } }); + m_dataVecVec = m_logicEngine.createDataArray(std::vector>{ { 1.f, 2.f }, { 3.f, 4.f }, { 5.f, 6.f } }); + } + + static bool createConfig(const std::initializer_list channels) + { + AnimationNodeConfig config; + for (const auto& channel : channels) + { + if (!config.addChannel(channel)) + return false; + } + + return true; + } + + protected: + LogicEngine m_logicEngine{ ramses::EFeatureLevel_Latest }; + DataArray* m_dataFloat = nullptr; + DataArray* m_dataVec2 = nullptr; + DataArray* m_dataVecVec = nullptr; + }; + + TEST_F(AnAnimationNodeConfig, CanBeCreatedWithValidChannels) + { + const AnimationChannel validChannel1{ "ok1", m_dataFloat, m_dataVec2 }; + const AnimationChannel validChannel2{ "ok2", m_dataFloat, m_dataVecVec }; + EXPECT_TRUE(createConfig({ validChannel1, validChannel2 })); + } + + TEST_F(AnAnimationNodeConfig, CanBeCopiedAndMoved) + { + const AnimationChannel validChannel1{ "ok1", m_dataFloat, m_dataVec2 }; + const AnimationChannel validChannel2{ "ok2", m_dataFloat, m_dataVecVec }; + AnimationNodeConfig config; + EXPECT_TRUE(config.addChannel(validChannel1)); + EXPECT_TRUE(config.addChannel(validChannel2)); + + // assign + AnimationNodeConfig config2; + config2 = config; + EXPECT_EQ(config.getChannels(), config2.getChannels()); + + // copy ctor + AnimationNodeConfig config3{ config }; + EXPECT_EQ(config.getChannels(), config3.getChannels()); + + // move assign + AnimationNodeConfig config4; + config4 = std::move(config2); + EXPECT_EQ(config.getChannels(), config4.getChannels()); + + // move ctor + AnimationNodeConfig config5{ std::move(config3) }; + EXPECT_EQ(config.getChannels(), config5.getChannels()); + } + + TEST_F(AnAnimationNodeConfig, CanGetAddedChannels) + { + AnimationNodeConfig config; + const AnimationChannel validChannel{ "ok", m_dataFloat, m_dataVec2 }; + + EXPECT_TRUE(config.addChannel(validChannel)); + AnimationChannels expected{ validChannel }; + EXPECT_EQ(expected, config.getChannels()); + + EXPECT_TRUE(config.addChannel(validChannel)); + expected = { validChannel, validChannel }; + EXPECT_EQ(expected, config.getChannels()); + } + + TEST_F(AnAnimationNodeConfig, FailsToAddChannelIfMissingTimestampsOrKeyframes) + { + const AnimationChannel validChannel{ "ok", m_dataFloat, m_dataVec2 }; + + EXPECT_FALSE(createConfig({ validChannel, { "channel", nullptr, m_dataVec2 } })); + EXPECT_FALSE(createConfig({ { "channel", m_dataFloat, nullptr }, validChannel })); + } + + TEST_F(AnAnimationNodeConfig, FailsToAddChannelIfTimestampsOrKeyframesTypeInvalid) + { + const AnimationChannel validChannel{ "ok", m_dataFloat, m_dataVec2 }; + const auto dataVec2OtherSize = m_logicEngine.createDataArray(std::vector{ { 1.f, 2.f } }); // single element only + + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataVec2, m_dataVec2 } })); + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, dataVec2OtherSize } })); + } + + TEST_F(AnAnimationNodeConfig, FailsToAddChannelIfTimestampsNotStrictlyAscending) + { + const AnimationChannel validChannel{ "ok", m_dataFloat, m_dataVec2 }; + + const auto timeStampsDescending = m_logicEngine.createDataArray(std::vector{ { 1.f, 3.f, 2.f } }); + EXPECT_FALSE(createConfig({ validChannel, { "channel", timeStampsDescending, m_dataVec2 } })); + + const auto timeStampsNotStrictAscend = m_logicEngine.createDataArray(std::vector{ { 1.f, 2.f, 2.f } }); + EXPECT_FALSE(createConfig({ validChannel, { "channel", timeStampsNotStrictAscend, m_dataVec2 } })); + } + + TEST_F(AnAnimationNodeConfig, FailsToAddChannelIfTangentsProvidedForNonCubicInterpolation) + { + const AnimationChannel validChannel{ "ok", m_dataFloat, m_dataVec2 }; + + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, m_dataVec2, EInterpolationType::Linear, m_dataVec2, nullptr } })); + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, m_dataVec2, EInterpolationType::Linear, nullptr, m_dataVec2 } })); + } + + TEST_F(AnAnimationNodeConfig, FailsToAddChannelIfQuaternionInterpolationWithNonVec4fKeyframes) + { + const AnimationChannel validChannel{ "ok", m_dataFloat, m_dataVec2 }; + + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, m_dataVec2, EInterpolationType::Linear_Quaternions } })); + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, m_dataVec2, EInterpolationType::Cubic_Quaternions, m_dataVec2, m_dataVec2 } })); + EXPECT_FALSE(createConfig({ { "channel", m_dataFloat, m_dataVec2, EInterpolationType::Cubic_Quaternions, m_dataVec2, m_dataVec2 }, validChannel })); + } + + TEST_F(AnAnimationNodeConfig, FailsToAddChannelIfInputRequirementsNotMet_specificToCubicInerpolation) + { + const AnimationChannel validChannel{ "ok", m_dataFloat, m_dataVec2, EInterpolationType::Cubic, m_dataVec2, m_dataVec2 }; + EXPECT_TRUE(createConfig({ validChannel })); + + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, m_dataVec2, EInterpolationType::Cubic, m_dataVec2, nullptr } })); + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, m_dataVec2, EInterpolationType::Cubic, nullptr, m_dataVec2 } })); + + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, m_dataVec2, EInterpolationType::Cubic, m_dataVec2, m_dataFloat } })); + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, m_dataVec2, EInterpolationType::Cubic, m_dataFloat, m_dataVec2 } })); + + const auto dataVec2OtherSize = m_logicEngine.createDataArray(std::vector{ { 1.f, 2.f } }); // single element only + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, m_dataVec2, EInterpolationType::Cubic, m_dataVec2, dataVec2OtherSize } })); + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, m_dataVec2, EInterpolationType::Cubic, dataVec2OtherSize, m_dataVec2 } })); + } + + TEST_F(AnAnimationNodeConfig, FailsToAddChannelIfElementArraysTooBig) + { + const AnimationChannel validChannel{ "ok", m_dataFloat, m_dataVec2 }; + + std::vector> tooBigArrays; + tooBigArrays.resize(3u); + for (auto& elementArray : tooBigArrays) + elementArray.resize(MaxArrayPropertySize + 1u, 0.f); + const auto invalidData = m_logicEngine.createDataArray(tooBigArrays, "invalid"); + + EXPECT_FALSE(createConfig({ validChannel, { "channel", m_dataFloat, invalidData } })); + EXPECT_FALSE(createConfig({ { "channel", m_dataFloat, invalidData }, validChannel })); + } + + TEST_F(AnAnimationNodeConfig, FailsToAddChannelIfElementArraysSizeMismatchBetweenKeyframesAndTangents) + { + auto dataWithLargerElementArrays = *m_dataVecVec->getData>(); + for (auto& elementArray : dataWithLargerElementArrays) + elementArray.push_back(0.f); + const auto largerData = m_logicEngine.createDataArray(dataWithLargerElementArrays, "invalid"); + + const AnimationChannel validChannel{ "ok", m_dataFloat, m_dataVecVec, EInterpolationType::Cubic, m_dataVecVec, m_dataVecVec }; + const AnimationChannel invalidChannel1{ "invalid", m_dataFloat, m_dataVecVec, EInterpolationType::Cubic, m_dataVecVec, largerData }; + const AnimationChannel invalidChannel2{ "invalid", m_dataFloat, largerData, EInterpolationType::Cubic, m_dataVecVec, m_dataVecVec }; + + EXPECT_FALSE(createConfig({ validChannel, invalidChannel1 })); + EXPECT_FALSE(createConfig({ invalidChannel2, validChannel })); + } + + TEST_F(AnAnimationNodeConfig, CanEnableAndDisableDataExposingAsPropeties) + { + const AnimationChannel validChannel{ "ok", m_dataFloat, m_dataVec2, EInterpolationType::Cubic, m_dataVec2, m_dataVec2 }; + + AnimationNodeConfig config; + EXPECT_TRUE(config.setExposingOfChannelDataAsProperties(true)); + EXPECT_TRUE(config.getExposingOfChannelDataAsProperties()); + EXPECT_TRUE(config.setExposingOfChannelDataAsProperties(false)); + EXPECT_FALSE(config.getExposingOfChannelDataAsProperties()); + + EXPECT_TRUE(config.addChannel(validChannel)); + EXPECT_TRUE(config.setExposingOfChannelDataAsProperties(true)); + EXPECT_TRUE(config.getExposingOfChannelDataAsProperties()); + EXPECT_TRUE(config.setExposingOfChannelDataAsProperties(false)); + EXPECT_FALSE(config.getExposingOfChannelDataAsProperties()); + + EXPECT_TRUE(config.addChannel(validChannel)); + EXPECT_TRUE(config.setExposingOfChannelDataAsProperties(true)); + EXPECT_TRUE(config.getExposingOfChannelDataAsProperties()); + EXPECT_TRUE(config.setExposingOfChannelDataAsProperties(false)); + EXPECT_FALSE(config.getExposingOfChannelDataAsProperties()); + } + + TEST_F(AnAnimationNodeConfig, FailsToAddChannelIfDataExposeEnabledAndNumberOfKeyframesExceedsMaximum) + { + const auto dataOk = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }); + std::vector vecDataExceeding; + vecDataExceeding.resize(MaxArrayPropertySize + 1u); + std::iota(vecDataExceeding.begin(), vecDataExceeding.end(), 1.f); + const auto dataExceeding = m_logicEngine.createDataArray(vecDataExceeding); + + const AnimationChannel channelOk{ "channel1", dataOk, dataOk }; + const AnimationChannel channelExceeding{ "channel2", dataExceeding, dataExceeding }; + + AnimationNodeConfig config; + EXPECT_TRUE(config.setExposingOfChannelDataAsProperties(true)); + EXPECT_TRUE(config.getExposingOfChannelDataAsProperties()); + + EXPECT_TRUE(config.addChannel(channelOk)); + EXPECT_FALSE(config.addChannel(channelExceeding)); + + // stays enabled + EXPECT_TRUE(config.getExposingOfChannelDataAsProperties()); + } + + TEST_F(AnAnimationNodeConfig, FailsToEnableDataExposeIfNumberOfKeyframesExceedsMaximum) + { + const auto dataOk = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }); + std::vector vecDataExceeding; + vecDataExceeding.resize(MaxArrayPropertySize + 1u); + std::iota(vecDataExceeding.begin(), vecDataExceeding.end(), 1.f); + const auto dataExceeding = m_logicEngine.createDataArray(vecDataExceeding); + + const AnimationChannel channelOk{ "channel1", dataOk, dataOk }; + const AnimationChannel channelExceeding{ "channel2", dataExceeding, dataExceeding }; + + AnimationNodeConfig config; + EXPECT_TRUE(config.addChannel(channelOk)); + EXPECT_TRUE(config.addChannel(channelExceeding)); + + EXPECT_FALSE(config.setExposingOfChannelDataAsProperties(true)); + // stays disabled + EXPECT_FALSE(config.getExposingOfChannelDataAsProperties()); + } + + TEST_F(AnAnimationNodeConfig, FailsToAddChannelIfDataExposeEnabledAndAddingElementsOfArrayType) + { + AnimationNodeConfig config; + EXPECT_TRUE(config.setExposingOfChannelDataAsProperties(true)); + EXPECT_TRUE(config.getExposingOfChannelDataAsProperties()); + + EXPECT_FALSE(config.addChannel({ "channel", m_dataFloat, m_dataVecVec })); + + // stays enabled + EXPECT_TRUE(config.getExposingOfChannelDataAsProperties()); + } + + TEST_F(AnAnimationNodeConfig, FailsToEnableDataExposeIfContainingElementsOfArrayType) + { + AnimationNodeConfig config; + EXPECT_TRUE(config.addChannel({ "channel", m_dataFloat, m_dataVecVec })); + + EXPECT_FALSE(config.setExposingOfChannelDataAsProperties(true)); + EXPECT_FALSE(config.getExposingOfChannelDataAsProperties()); + } +} diff --git a/client/logic/unittests/api/AnimationNodeTest.cpp b/client/logic/unittests/api/AnimationNodeTest.cpp new file mode 100644 index 000000000..b9960c468 --- /dev/null +++ b/client/logic/unittests/api/AnimationNodeTest.cpp @@ -0,0 +1,893 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "WithTempDirectory.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/Property.h" +#include "impl/AnimationNodeImpl.h" +#include "impl/DataArrayImpl.h" +#include "impl/PropertyImpl.h" +#include "internals/ErrorReporting.h" +#include "internals/SerializationMap.h" +#include "internals/DeserializationMap.h" +#include "internals/TypeData.h" +#include "internals/EPropertySemantics.h" +#include "generated/AnimationNodeGen.h" +#include "flatbuffers/flatbuffers.h" +#include + +namespace ramses::internal +{ + class AnAnimationNode : public ::testing::TestWithParam + { + public: + void SetUp() override + { + m_dataFloat = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }); + m_dataVec2 = m_logicEngine.createDataArray(std::vector{ { 1.f, 2.f }, { 3.f, 4.f }, { 5.f, 6.f } }); + // Quaternions which are not normalized (ie. not of unit length). Used for tests to check they are normalized correctly + m_dataVec4 = m_logicEngine.createDataArray(std::vector{ { 2.f, 0.f, 0.f, 0.f }, { 0.f, 2.f, 0.f, 0.f } , { 0.f, 0.f, 2.f, 0.f } }); + m_dataVecVec = m_logicEngine.createDataArray(std::vector>{ { 1.f, 2.f, 3.f, 4.f, 5.f }, { 3.f, 4.f, 5.f, 6.f, 7.f }, { 5.f, 6.f, 7.f, 8.f, 9.f } }); + + m_saveFileConfigNoValidation.setValidationEnabled(false); + } + + protected: + AnimationNode* createAnimationNode(const AnimationChannels& channels, std::string_view name = "") + { + AnimationNodeConfig config; + for (const auto& ch : channels) + { + EXPECT_TRUE(config.addChannel(ch)); + } + + if (!config.setExposingOfChannelDataAsProperties(GetParam())) + return nullptr; + + return m_logicEngine.createAnimationNode(config, name); + } + + template + void advanceAnimationAndExpectValues(AnimationNode& animNode, float progress, const T& expectedValue) + { + EXPECT_TRUE(animNode.getInputs()->getChild("progress")->set(progress)); + EXPECT_TRUE(m_logicEngine.update()); + + if constexpr (std::is_same_v>) + { + const auto outputArrayProp = animNode.getOutputs()->getChild("channel"); + ASSERT_EQ(outputArrayProp->getChildCount(), expectedValue.size()); + for (size_t i = 0u; i < outputArrayProp->getChildCount(); ++i) + EXPECT_FLOAT_EQ(expectedValue[i], *outputArrayProp->getChild(i)->get()); + } + else + { + const auto val = *animNode.getOutputs()->getChild("channel")->get(); + if constexpr (std::is_same_v) + { + EXPECT_FLOAT_EQ(expectedValue[0], val[0]); + EXPECT_FLOAT_EQ(expectedValue[1], val[1]); + } + else if constexpr (std::is_same_v) + { + EXPECT_EQ(expectedValue[0], val[0]); + EXPECT_EQ(expectedValue[1], val[1]); + } + else if constexpr (std::is_same_v) + { + EXPECT_FLOAT_EQ(expectedValue[0], val[0]); + EXPECT_FLOAT_EQ(expectedValue[1], val[1]); + EXPECT_FLOAT_EQ(expectedValue[2], val[2]); + EXPECT_FLOAT_EQ(expectedValue[3], val[3]); + } + else if constexpr (std::is_arithmetic_v) + { + EXPECT_EQ(expectedValue, val); + } + else + { + ASSERT_TRUE(false) << "test missing for type"; + } + } + } + + void advanceAnimationAndExpectValues_twoChannels(AnimationNode& animNode, float progress, const vec2f& expectedValue1, const vec2f& expectedValue2) + { + animNode.getInputs()->getChild("progress")->set(progress); + m_logicEngine.update(); + const auto val1 = *animNode.getOutputs()->getChild("channel1")->get(); + EXPECT_FLOAT_EQ(expectedValue1[0], val1[0]); + EXPECT_FLOAT_EQ(expectedValue1[1], val1[1]); + const auto val2 = *animNode.getOutputs()->getChild("channel2")->get(); + EXPECT_FLOAT_EQ(expectedValue2[0], val2[0]); + EXPECT_FLOAT_EQ(expectedValue2[1], val2[1]); + } + + LogicEngine m_logicEngine{ ramses::EFeatureLevel_Latest }; + DataArray* m_dataFloat = nullptr; + DataArray* m_dataVec2 = nullptr; + DataArray* m_dataVec4 = nullptr; + DataArray* m_dataVecVec = nullptr; + SaveFileConfig m_saveFileConfigNoValidation; + }; + + INSTANTIATE_TEST_SUITE_P( + AnAnimationNode_TestInstances, + AnAnimationNode, + ::testing::Values( + false, // without animation data exposed as properties + true) // with animation data exposed as properties + ); + + TEST_P(AnAnimationNode, IsCreated) + { + const AnimationChannel channel{ "channel", m_dataFloat, m_dataVec2 }; + const AnimationChannels channels{ channel, channel }; + const auto animNode = createAnimationNode(channels, "animNode"); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + ASSERT_NE(nullptr, animNode); + EXPECT_EQ(animNode, m_logicEngine.findByName("animNode")); + + EXPECT_EQ("animNode", animNode->getName()); + EXPECT_EQ(channels, animNode->getChannels()); + } + + TEST_P(AnAnimationNode, IsDestroyed) + { + const auto animNode = createAnimationNode({ { "channel", m_dataFloat, m_dataVec2 } }, "animNode"); + EXPECT_TRUE(m_logicEngine.destroy(*animNode)); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + EXPECT_EQ(nullptr, m_logicEngine.findByName("animNode")); + } + + TEST_P(AnAnimationNode, FailsToBeDestroyedIfFromOtherLogicInstance) + { + auto animNode = createAnimationNode({ { "channel", m_dataFloat, m_dataVec2 } }, "animNode"); + + LogicEngine otherEngine{ m_logicEngine.getFeatureLevel() }; + EXPECT_FALSE(otherEngine.destroy(*animNode)); + ASSERT_FALSE(otherEngine.getErrors().empty()); + EXPECT_EQ("Failed to destroy object 'animNode [Id=5]', cannot find it in this LogicEngine instance.", otherEngine.getErrors().front().message); + } + + TEST_P(AnAnimationNode, ChangesName) + { + const auto animNode = createAnimationNode({ { "channel", m_dataFloat, m_dataVec2 } }, "animNode"); + + animNode->setName("an"); + EXPECT_EQ("an", animNode->getName()); + EXPECT_EQ(animNode, m_logicEngine.findByName("an")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + } + + TEST_P(AnAnimationNode, CanContainVariousAnimationChannels) + { + const auto timeStamps1 = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f }); + const auto timeStamps2 = m_logicEngine.createDataArray(std::vector{ 3.f, 4.f, 5.f }); + const auto data1 = m_logicEngine.createDataArray(std::vector{ { 11.f, 22.f }, { 33.f, 44.f } }); + const auto data2 = m_logicEngine.createDataArray(std::vector{ { 11, 22 }, { 44, 55 }, { 66, 77 } }); + + const AnimationChannel channel1{ "channel1", timeStamps1, data1, EInterpolationType::Step }; + const AnimationChannel channel2{ "channel2", timeStamps1, data1, EInterpolationType::Linear }; + const AnimationChannel channel3{ "channel3", timeStamps2, data2, EInterpolationType::Linear }; + const AnimationChannel channel4{ "channel4", timeStamps1, data1, EInterpolationType::Cubic, data1, data1 }; + const AnimationChannels channels{ channel1, channel2, channel3, channel4 }; + + const auto animNode = createAnimationNode(channels, "animNode"); + + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + ASSERT_NE(nullptr, animNode); + EXPECT_EQ(animNode, m_logicEngine.findByName("animNode")); + + EXPECT_EQ("animNode", animNode->getName()); + EXPECT_FLOAT_EQ(5.f, *animNode->getOutputs()->getChild("duration")->get()); + EXPECT_EQ(channels, animNode->getChannels()); + } + + TEST_P(AnAnimationNode, HasFixedProperties) + { + const AnimationChannel channel{ "channel", m_dataFloat, m_dataVec2 }; + const AnimationChannels channels{ channel, channel }; + const auto animNode = createAnimationNode(channels, "animNode"); + ASSERT_NE(nullptr, animNode); + + const auto rootIn = animNode->getInputs(); + ASSERT_TRUE(rootIn); + EXPECT_EQ("", rootIn->getName()); + ASSERT_EQ(rootIn->getChildCount(), GetParam() ? 2u : 1u); // with/out animation data properties + ASSERT_TRUE(rootIn->getChild("progress")); + EXPECT_EQ(EPropertyType::Float, rootIn->getChild("progress")->getType()); + + const auto rootOut = animNode->getOutputs(); + ASSERT_TRUE(rootOut); + EXPECT_EQ("", rootOut->getName()); + ASSERT_EQ(3u, rootOut->getChildCount()); + ASSERT_TRUE(rootOut && rootOut->getChild("duration")); + EXPECT_EQ(EPropertyType::Float, rootOut->getChild("duration")->getType()); + } + + TEST_P(AnAnimationNode, HasPropertiesMatchingChannels) + { + const AnimationChannel channel1{ "channel1", m_dataFloat, m_dataFloat }; + const AnimationChannel channel2{ "channel2", m_dataFloat, m_dataVec4, EInterpolationType::Linear_Quaternions }; + const auto animNode = createAnimationNode({ channel1, channel2 }, "animNode"); + + const auto rootOut = animNode->getOutputs(); + ASSERT_EQ(3u, rootOut->getChildCount()); + EXPECT_EQ("channel1", rootOut->getChild(1u)->getName()); + EXPECT_EQ("channel2", rootOut->getChild(2u)->getName()); + EXPECT_EQ(EPropertyType::Float, rootOut->getChild(1u)->getType()); + EXPECT_EQ(EPropertyType::Vec4f, rootOut->getChild(2u)->getType()); + } + + TEST_P(AnAnimationNode, HasPropertiesMatchingChannelsWithFloatArrays) + { + if (GetParam()) // cannot use float arrays exposed as input properties + GTEST_SKIP(); + + const AnimationChannel channel1{ "channel1", m_dataFloat, m_dataFloat }; + const AnimationChannel channel2{ "channel2", m_dataFloat, m_dataVecVec }; + const auto animNode = createAnimationNode({ channel1, channel2 }, "animNode"); + + const auto rootOut = animNode->getOutputs(); + ASSERT_EQ(3u, rootOut->getChildCount()); + EXPECT_EQ("channel1", rootOut->getChild(1u)->getName()); + EXPECT_EQ("channel2", rootOut->getChild(2u)->getName()); + EXPECT_EQ(EPropertyType::Float, rootOut->getChild(1u)->getType()); + EXPECT_EQ(EPropertyType::Array, rootOut->getChild(2u)->getType()); + + const auto arrayOutProp = rootOut->getChild(2u); + ASSERT_EQ(5u, arrayOutProp->getChildCount()); + for (size_t i = 0u; i < 5u; ++i) + EXPECT_EQ(EPropertyType::Float, arrayOutProp->getChild(i)->getType()); + } + + TEST_P(AnAnimationNode, DeterminesDurationFromHighestTimestamp) + { + const auto timeStamps1 = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }); + const auto timeStamps2 = m_logicEngine.createDataArray(std::vector{ 4.f, 5.f, 6.f }); + + const auto animNode1 = createAnimationNode({ { "channel", timeStamps1, m_dataVec2 } }, "animNode1"); + EXPECT_FLOAT_EQ(3.f, *animNode1->getOutputs()->getChild("duration")->get()); + const auto animNode2 = createAnimationNode({ { "channel1", timeStamps1, m_dataVec2 }, { "channel2", timeStamps2, m_dataVec2 } }, "animNode2"); + EXPECT_FLOAT_EQ(6.f, *animNode2->getOutputs()->getChild("duration")->get()); + } + + TEST_P(AnAnimationNode, FailsToBeCreatedWithNoChannels) + { + EXPECT_EQ(nullptr, createAnimationNode({}, "animNode")); + EXPECT_EQ("Failed to create AnimationNode 'animNode': must provide at least one channel.", m_logicEngine.getErrors().front().message); + } + + TEST_P(AnAnimationNode, FailsToBeCreatedIfDataArrayFromOtherLogicInstance) + { + LogicEngine otherInstance{ m_logicEngine.getFeatureLevel() }; + auto otherInstanceData = otherInstance.createDataArray(std::vector{ 1.f, 2.f, 3.f }); + + EXPECT_EQ(nullptr, createAnimationNode({ { "channel", otherInstanceData, m_dataFloat } }, "animNode")); + EXPECT_EQ("Failed to create AnimationNode 'animNode': timestamps or keyframes were not found in this logic instance.", m_logicEngine.getErrors().front().message); + EXPECT_EQ(nullptr, createAnimationNode({ { "channel", m_dataFloat, otherInstanceData } }, "animNode")); + EXPECT_EQ("Failed to create AnimationNode 'animNode': timestamps or keyframes were not found in this logic instance.", m_logicEngine.getErrors().front().message); + EXPECT_EQ(nullptr, createAnimationNode({ { "channel", m_dataFloat, m_dataFloat, EInterpolationType::Cubic, otherInstanceData, m_dataFloat } }, "animNode")); + EXPECT_EQ("Failed to create AnimationNode 'animNode': tangents were not found in this logic instance.", m_logicEngine.getErrors().front().message); + EXPECT_EQ(nullptr, createAnimationNode({ { "channel", m_dataFloat, m_dataFloat, EInterpolationType::Cubic, m_dataFloat, otherInstanceData } }, "animNode")); + EXPECT_EQ("Failed to create AnimationNode 'animNode': tangents were not found in this logic instance.", m_logicEngine.getErrors().front().message); + } + + TEST_P(AnAnimationNode, CanBeSerializedAndDeserialized) + { + WithTempDirectory tempDir; + + { + LogicEngine otherEngine{ m_logicEngine.getFeatureLevel() }; + + const auto timeStamps1 = otherEngine.createDataArray(std::vector{ 1.f, 2.f }, "ts1"); + const auto timeStamps2 = otherEngine.createDataArray(std::vector{ 3.f, 4.f, 5.f }, "ts2"); + const auto data1 = otherEngine.createDataArray(std::vector{ { 11, 22 }, { 33, 44 } }, "data1"); + const auto data2 = otherEngine.createDataArray(std::vector{ { 11, 22 }, { 44, 55 }, { 66, 77 } }, "data2"); + const auto data3 = otherEngine.createDataArray(*m_dataVecVec->getData>(), "data3"); + + const AnimationChannel channel1{ "channel1", timeStamps1, data1, EInterpolationType::Step }; + const AnimationChannel channel2{ "channel2", timeStamps1, data1, EInterpolationType::Linear }; + const AnimationChannel channel3{ "channel3", timeStamps2, data2, EInterpolationType::Linear }; + const AnimationChannel channel4{ "channel4", timeStamps1, data1, EInterpolationType::Cubic, data1, data1 }; + const AnimationChannel channel5{ "channel5", timeStamps2, data3, EInterpolationType::Cubic, data3, data3 }; + + AnimationNodeConfig config1; + EXPECT_TRUE(config1.addChannel(channel1)); + EXPECT_TRUE(config1.addChannel(channel2)); + EXPECT_TRUE(config1.addChannel(channel3)); + EXPECT_TRUE(config1.addChannel(channel4)); + if (!GetParam()) + { + EXPECT_TRUE(config1.addChannel(channel5)); + } + EXPECT_TRUE(config1.setExposingOfChannelDataAsProperties(GetParam())); + + AnimationNodeConfig config2; + EXPECT_TRUE(config2.addChannel(channel4)); + EXPECT_TRUE(config2.addChannel(channel3)); + EXPECT_TRUE(config2.addChannel(channel2)); + EXPECT_TRUE(config2.addChannel(channel1)); + if (!GetParam()) + { + EXPECT_TRUE(config2.addChannel(channel5)); + } + EXPECT_TRUE(config2.setExposingOfChannelDataAsProperties(GetParam())); + + otherEngine.createAnimationNode(config1, "animNode1"); + otherEngine.createAnimationNode(config2, "animNode2"); + + ASSERT_TRUE(otherEngine.saveToFile("logic_animNodes.bin", m_saveFileConfigNoValidation)); + } + + ASSERT_TRUE(m_logicEngine.loadFromFile("logic_animNodes.bin")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + EXPECT_EQ(2u, m_logicEngine.getCollection().size()); + const auto animNode1 = m_logicEngine.findByName("animNode1"); + const auto animNode2 = m_logicEngine.findByName("animNode2"); + ASSERT_TRUE(animNode1 && animNode2); + + EXPECT_EQ("animNode1", animNode1->getName()); + EXPECT_EQ("animNode2", animNode2->getName()); + EXPECT_FLOAT_EQ(5.f, *animNode1->getOutputs()->getChild("duration")->get()); + EXPECT_FLOAT_EQ(5.f, *animNode2->getOutputs()->getChild("duration")->get()); + + // ptrs are different after loading, find data arrays to verify the references in loaded anim nodes match + const auto ts1 = m_logicEngine.findByName("ts1"); + const auto ts2 = m_logicEngine.findByName("ts2"); + const auto data1 = m_logicEngine.findByName("data1"); + const auto data2 = m_logicEngine.findByName("data2"); + const auto data3 = m_logicEngine.findByName("data3"); + const AnimationChannel channel1{ "channel1", ts1, data1, EInterpolationType::Step }; + const AnimationChannel channel2{ "channel2", ts1, data1, EInterpolationType::Linear }; + const AnimationChannel channel3{ "channel3", ts2, data2, EInterpolationType::Linear }; + const AnimationChannel channel4{ "channel4", ts1, data1, EInterpolationType::Cubic, data1, data1 }; + const AnimationChannel channel5{ "channel5", ts2, data3, EInterpolationType::Cubic, data3, data3 }; + AnimationChannels expectedChannels1{ channel1, channel2, channel3, channel4 }; + AnimationChannels expectedChannels2{ channel4, channel3, channel2, channel1 }; + if (!GetParam()) + { + expectedChannels1.push_back(channel5); + expectedChannels2.push_back(channel5); + } + + EXPECT_EQ(expectedChannels1, animNode1->getChannels()); + EXPECT_EQ(expectedChannels2, animNode2->getChannels()); + + // verify properties after loading + // check properties same for both anim nodes + for (const auto animNode : { animNode1, animNode2 }) + { + const auto rootIn = animNode->getInputs(); + EXPECT_EQ("", rootIn->getName()); + ASSERT_EQ(rootIn->getChildCount(), GetParam() ? 2u : 1u); // with/out animation data properties + EXPECT_EQ("progress", rootIn->getChild(0u)->getName()); + EXPECT_EQ(EPropertyType::Float, rootIn->getChild(0u)->getType()); + + const auto rootOut = animNode->getOutputs(); + EXPECT_EQ("", rootOut->getName()); + ASSERT_EQ((GetParam() ? 5u : 6u), rootOut->getChildCount()); + ASSERT_EQ("duration", rootOut->getChild(0u)->getName()); + ASSERT_EQ(EPropertyType::Float, rootOut->getChild(0u)->getType()); + EXPECT_FLOAT_EQ(5.f, *animNode->getOutputs()->getChild("duration")->get()); + + EXPECT_EQ(EPropertyType::Vec2i, rootOut->getChild(1u)->getType()); + EXPECT_EQ(EPropertyType::Vec2i, rootOut->getChild(2u)->getType()); + EXPECT_EQ(EPropertyType::Vec2i, rootOut->getChild(3u)->getType()); + EXPECT_EQ(EPropertyType::Vec2i, rootOut->getChild(4u)->getType()); + + if (!GetParam()) + { + const auto arrayOutProp = rootOut->getChild(5u); + EXPECT_EQ(EPropertyType::Array, arrayOutProp->getType()); + ASSERT_EQ(5u, arrayOutProp->getChildCount()); + for (size_t i = 0u; i < 5u; ++i) + EXPECT_EQ(EPropertyType::Float, arrayOutProp->getChild(i)->getType()); + } + } + // check output names separately + const auto rootOut1 = animNode1->getOutputs(); + EXPECT_EQ("channel1", rootOut1->getChild(1u)->getName()); + EXPECT_EQ("channel2", rootOut1->getChild(2u)->getName()); + EXPECT_EQ("channel3", rootOut1->getChild(3u)->getName()); + EXPECT_EQ("channel4", rootOut1->getChild(4u)->getName()); + if (!GetParam()) + { + EXPECT_EQ("channel5", rootOut1->getChild(5u)->getName()); + } + + const auto rootOut2 = animNode2->getOutputs(); + EXPECT_EQ("channel4", rootOut2->getChild(1u)->getName()); + EXPECT_EQ("channel3", rootOut2->getChild(2u)->getName()); + EXPECT_EQ("channel2", rootOut2->getChild(3u)->getName()); + EXPECT_EQ("channel1", rootOut2->getChild(4u)->getName()); + if (!GetParam()) + { + EXPECT_EQ("channel5", rootOut2->getChild(5u)->getName()); + } + } + + TEST_P(AnAnimationNode, WillSerializeAnimationIncludingProgress) + { + WithTempDirectory tempDir; + + { + LogicEngine otherEngine{ m_logicEngine.getFeatureLevel() }; + + const auto timeStamps = otherEngine.createDataArray(std::vector{ 1.f, 2.f }, "ts"); + const auto data = otherEngine.createDataArray(std::vector{ 10, 20 }, "data"); + const AnimationChannel channel{ "channel", timeStamps, data, EInterpolationType::Linear }; + AnimationNodeConfig config; + config.addChannel(channel); + config.setExposingOfChannelDataAsProperties(GetParam()); + const auto animNode = otherEngine.createAnimationNode(config, "animNode"); + + EXPECT_TRUE(animNode->getInputs()->getChild("progress")->set(0.75f)); + EXPECT_TRUE(otherEngine.update()); + EXPECT_EQ(15, *animNode->getOutputs()->getChild("channel")->get()); + + ASSERT_TRUE(otherEngine.saveToFile("logic_animNodes.bin", m_saveFileConfigNoValidation)); + } + + ASSERT_TRUE(m_logicEngine.loadFromFile("logic_animNodes.bin")); + const auto animNode = m_logicEngine.findByName("animNode"); + ASSERT_TRUE(animNode); + + // update node with no change to progress to check its state + EXPECT_TRUE(m_logicEngine.update()); + + // progress same as when saved + EXPECT_EQ(15, *animNode->getOutputs()->getChild("channel")->get()); + + // can play again + advanceAnimationAndExpectValues(*animNode, 0.f, 10); + advanceAnimationAndExpectValues(*animNode, 0.5f, 10); + advanceAnimationAndExpectValues(*animNode, 0.75f, 15); + advanceAnimationAndExpectValues(*animNode, 1.f, 20); + } + + TEST_P(AnAnimationNode, CanHandleProgressOutOfNormalizedRange) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + const auto data = m_logicEngine.createDataArray(std::vector{ 10.f, 20.f }); + const auto animNode = createAnimationNode({ { "channel", timeStamps, data, EInterpolationType::Linear } }); + + advanceAnimationAndExpectValues(*animNode, 0.0f, 10.f); + advanceAnimationAndExpectValues(*animNode, 0.5f, 15.f); + advanceAnimationAndExpectValues(*animNode, 1.0f, 20.f); + advanceAnimationAndExpectValues(*animNode, -999.f, 10.f); + advanceAnimationAndExpectValues(*animNode, 999.f, 20.f); + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_step_vec2f) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + const auto data = m_logicEngine.createDataArray(std::vector{ { 0.f, 10.f }, { 1.f, 20.f } }); + const auto animNode = createAnimationNode({ { "channel", timeStamps, data, EInterpolationType::Step } }); + + advanceAnimationAndExpectValues(*animNode, 0.f, { 0.f, 10.f }); + advanceAnimationAndExpectValues(*animNode, 0.99f, { 0.f, 10.f }); // still no change + advanceAnimationAndExpectValues(*animNode, 1.000001f, { 1.f, 20.f }); // step to next keyframe value at its timestamp + advanceAnimationAndExpectValues(*animNode, 100.f, { 1.f, 20.f }); // no change pass end of animation + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_step_vec2i) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + const auto data = m_logicEngine.createDataArray(std::vector{ { 0, 10 }, { 1, 20 } }); + const auto animNode = createAnimationNode({ { "channel", timeStamps, data, EInterpolationType::Step } }); + + advanceAnimationAndExpectValues(*animNode, 0.f, { 0, 10 }); + advanceAnimationAndExpectValues(*animNode, 0.99f, { 0, 10 }); // still no change + advanceAnimationAndExpectValues(*animNode, 1.000001f, { 1, 20 }); // step to next keyframe value at its timestamp + advanceAnimationAndExpectValues(*animNode, 100.f, { 1, 20 }); // no change pass end of animation + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_step_vecvec) + { + if (GetParam()) + GTEST_SKIP(); + + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + const auto data = m_logicEngine.createDataArray(std::vector>{ { 0.f, 10.f }, { 1.f, 20.f } }); + const auto animNode = createAnimationNode({ { "channel", timeStamps, data, EInterpolationType::Step } }); + + advanceAnimationAndExpectValues>(*animNode, 0.f, { 0.f, 10.f }); + advanceAnimationAndExpectValues>(*animNode, 0.99f, { 0.f, 10.f }); // still no change + advanceAnimationAndExpectValues>(*animNode, 1.000001f, { 1.f, 20.f }); // step to next keyframe value at its timestamp + advanceAnimationAndExpectValues>(*animNode, 100.f, { 1.f, 20.f }); // no change pass end of animation + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_linear_vec2f) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + const auto data = m_logicEngine.createDataArray(std::vector{ { 0.f, 10.f }, { 1.f, 20.f } }); + const auto animNode = createAnimationNode({ { "channel", timeStamps, data, EInterpolationType::Linear } }); + + advanceAnimationAndExpectValues(*animNode, 0.f, { 0.f, 10.f }); + advanceAnimationAndExpectValues(*animNode, 0.1f, { 0.1f, 11.f }); // time 0.1 + advanceAnimationAndExpectValues(*animNode, 0.5f, { 0.5f, 15.f }); // time 0.5 + advanceAnimationAndExpectValues(*animNode, 0.9f, { 0.9f, 19.f }); // time 0.9 + advanceAnimationAndExpectValues(*animNode, 1.0f, { 1.f, 20.f }); // time 1.0 + advanceAnimationAndExpectValues(*animNode, 100.f, { 1.f, 20.f }); // stays at last keyframe after animation end + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_linear_vec2i) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + const auto data = m_logicEngine.createDataArray(std::vector{ { 0, 10 }, { 1, 20 } }); + const auto animNode = createAnimationNode({ { "channel", timeStamps, data, EInterpolationType::Linear } }); + + advanceAnimationAndExpectValues(*animNode, 0.f, { 0, 10 }); + advanceAnimationAndExpectValues(*animNode, 0.1f, { 0, 11 }); // time 0.1 + advanceAnimationAndExpectValues(*animNode, 0.5f, { 1, 15 }); // time 0.5 + advanceAnimationAndExpectValues(*animNode, 0.9f, { 1, 19 }); // time 0.9 + advanceAnimationAndExpectValues(*animNode, 1.0f, { 1, 20 }); // time 1.0 + advanceAnimationAndExpectValues(*animNode, 100.f, { 1, 20 }); // stays at last keyframe after animation end + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_linear_vecvec) + { + if (GetParam()) + GTEST_SKIP(); + + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + const auto data = m_logicEngine.createDataArray(std::vector>{ { 0.f, 10.f }, { 1.f, 20.f } }); + const auto animNode = createAnimationNode({ { "channel", timeStamps, data, EInterpolationType::Linear } }); + + advanceAnimationAndExpectValues>(*animNode, 0.f, { 0.f, 10.f }); + advanceAnimationAndExpectValues>(*animNode, 0.1f, { 0.1f, 11.f }); // time 0.1 + advanceAnimationAndExpectValues>(*animNode, 0.5f, { 0.5f, 15.f }); // time 0.5 + advanceAnimationAndExpectValues>(*animNode, 0.9f, { 0.9f, 19.f }); // time 0.9 + advanceAnimationAndExpectValues>(*animNode, 1.0f, { 1.f, 20.f }); // time 1.0 + advanceAnimationAndExpectValues>(*animNode, 100.f, { 1.f, 20.f }); // stays at last keyframe after animation end + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_linear_quaternions) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f, 2.f }); + const auto animNode = createAnimationNode({ { "channel", timeStamps, m_dataVec4, EInterpolationType::Linear_Quaternions } }); + + advanceAnimationAndExpectValues(*animNode, 0.f, { 1, 0, 0, 0 }); + advanceAnimationAndExpectValues(*animNode, 0.25f, { .70710677f, .70710677f, 0, 0 }); // time 0.5 + advanceAnimationAndExpectValues(*animNode, 0.5f, { 0, 1, 0, 0 }); // time 1.0 + advanceAnimationAndExpectValues(*animNode, 0.75f, { 0, .70710677f, .70710677f, 0 }); // time 1.5 + advanceAnimationAndExpectValues(*animNode, 100.f, { 0, 0, 1, 0 }); // stays at last keyframe after animation end + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_cubic_vec2f) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + const auto data = m_logicEngine.createDataArray(std::vector{ { 0.f, 10.f }, { 1.f, 20.f } }); + const auto tangentsZero = m_logicEngine.createDataArray(std::vector{ { 0.f, 0.f }, { 0.f, 0.f } }); + const auto tangentsIn = m_logicEngine.createDataArray(std::vector{ { 0.f, 0.f }, { -1.f, -2.f } }); + const auto tangentsOut = m_logicEngine.createDataArray(std::vector{ { 2.f, 5.f }, { 0.f, 0.f } }); + // animation with one channel using zero tangents and another channel with non-zero tangents + const auto animNode = createAnimationNode({ + { "channel1", timeStamps, data, EInterpolationType::Cubic, tangentsZero, tangentsZero }, + { "channel2", timeStamps, data, EInterpolationType::Cubic, tangentsIn, tangentsOut }, + }); + + advanceAnimationAndExpectValues_twoChannels(*animNode, 0.f, { 0.f, 10.f }, { 0.f, 10.f }); + advanceAnimationAndExpectValues_twoChannels(*animNode, 0.1f, { 0.028f, 10.28f }, { 0.199f, 10.703f }); // time 0.1 + advanceAnimationAndExpectValues_twoChannels(*animNode, 0.5f, { 0.5f, 15.f }, { 0.875f, 15.875f }); // time 0.5 + advanceAnimationAndExpectValues_twoChannels(*animNode, 0.9f, { 0.972f, 19.72f }, { 1.071f, 19.927f }); // time 0.9 + advanceAnimationAndExpectValues_twoChannels(*animNode, 1.0f, { 1.f, 20.f }, { 1.f, 20.f }); // time 1.0 + advanceAnimationAndExpectValues_twoChannels(*animNode, 100.f, { 1.f, 20.f }, { 1.f, 20.f }); // stays at last keyframe after animation end + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_cubic_vecvec) + { + if (GetParam()) + GTEST_SKIP(); + + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + const auto data = m_logicEngine.createDataArray(std::vector>{ { 0.f, 10.f }, { 1.f, 20.f } }); + const auto tangentsIn = m_logicEngine.createDataArray(std::vector>{ { 0.f, 0.f }, { -1.f, -2.f } }); + const auto tangentsOut = m_logicEngine.createDataArray(std::vector>{ { 2.f, 5.f }, { 0.f, 0.f } }); + const auto animNode = createAnimationNode({{ "channel", timeStamps, data, EInterpolationType::Cubic, tangentsIn, tangentsOut }}); + + advanceAnimationAndExpectValues>(*animNode, 0.f, { 0.f, 10.f }); + advanceAnimationAndExpectValues>(*animNode, 0.1f, { 0.199f, 10.703f }); // time 0.1 + advanceAnimationAndExpectValues>(*animNode, 0.5f, { 0.875f, 15.875f }); // time 0.5 + advanceAnimationAndExpectValues>(*animNode, 0.9f, { 1.071f, 19.927f }); // time 0.9 + advanceAnimationAndExpectValues>(*animNode, 1.0f, { 1.f, 20.f }); // time 1.0 + advanceAnimationAndExpectValues>(*animNode, 100.f, { 1.f, 20.f }); // stays at last keyframe after animation end + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_cubic_quaternions) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f, 2.f }); + const auto tangentsZero = m_logicEngine.createDataArray(std::vector{ { 0.f, 0.f, 0.f, 0.f }, { 0.f, 0.f, 0.f, 0.f }, { 0.f, 0.f, 0.f, 0.f } }); + const auto animNode = createAnimationNode({{ "channel", timeStamps, m_dataVec4, EInterpolationType::Cubic_Quaternions, tangentsZero, tangentsZero }}); + + advanceAnimationAndExpectValues(*animNode, 0.f, { 1, 0, 0, 0 }); + advanceAnimationAndExpectValues(*animNode, 0.125f, { .98328203f, .18208927f, 0, 0 }); // time 0.25 + advanceAnimationAndExpectValues(*animNode, 0.25f, { .70710677f, .70710677f, 0, 0 }); // time 0.5 + advanceAnimationAndExpectValues(*animNode, 0.375f, { .18208927f, .98328203f, 0, 0 }); // time 0.75 + advanceAnimationAndExpectValues(*animNode, 0.5f, { 0, 1, 0, 0 }); // time 1.0 + advanceAnimationAndExpectValues(*animNode, 0.875f, { 0, .18208927f, .98328203f, 0 }); // time 1.75 + advanceAnimationAndExpectValues(*animNode, 100.f, { 0, 0, 1, 0 }); // stays at last keyframe after animation end + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_cubic_quaternions_withTangents) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f, 2.f }); + const auto tangentsIn = m_logicEngine.createDataArray(std::vector{ { 0.f, 0.f, 0.f, 0.f }, { 1.f, 1.f, 0.f, 0.f }, { 1.f, 1.f, 0.f, 0.f } }); + const auto tangentsOut = m_logicEngine.createDataArray(std::vector{ { 1.f, 1.f, 0.f, 0.f}, { 1.f, 1.f, 0.f, 0.f }, { 0.f, 0.f, 0.f, 0.f } }); + const auto animNode = createAnimationNode({ { "channel", timeStamps, m_dataVec4, EInterpolationType::Cubic_Quaternions, tangentsIn, tangentsOut } }); + + advanceAnimationAndExpectValues(*animNode, 0.f, { 1, 0, 0, 0 }); + advanceAnimationAndExpectValues(*animNode, 0.125f, { 0.9749645f, 0.22236033f, 0, 0 }); // time 0.25 + advanceAnimationAndExpectValues(*animNode, 0.25f, { .70710677f, .70710677f, 0, 0 }); // time 0.5 + advanceAnimationAndExpectValues(*animNode, 0.375f, { 0.13598002f, 0.99071163f, 0, 0 }); // time 0.75 + advanceAnimationAndExpectValues(*animNode, 0.5f, { 0, 1, 0, 0 }); // time 1.0 + advanceAnimationAndExpectValues(*animNode, 0.875f, { -0.055011157f, 0.12835936f, 0.99020082f, 0 }); // time 1.75 + advanceAnimationAndExpectValues(*animNode, 100.f, { 0, 0, 1, 0 }); // stays at last keyframe after animation end + } + + TEST_P(AnAnimationNode, InterpolatesKeyframeValues_cubic_vec2i) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + const auto data = m_logicEngine.createDataArray(std::vector{ { 0, 10 }, { 1, 20 } }); + const auto tangentsIn = m_logicEngine.createDataArray(std::vector{ { 0, 0 }, { -1, -2 } }); + const auto tangentsOut = m_logicEngine.createDataArray(std::vector{ { 2, 5 }, { 0, 0 } }); + const auto animNode = createAnimationNode({ + { "channel", timeStamps, data, EInterpolationType::Cubic, tangentsIn, tangentsOut }, + }); + + advanceAnimationAndExpectValues(*animNode, 0.f, { 0, 10 }); + advanceAnimationAndExpectValues(*animNode, 0.1f, { 0, 11 }); // time 0.1 + advanceAnimationAndExpectValues(*animNode, 0.5f, { 1, 16 }); // time 0.5 + advanceAnimationAndExpectValues(*animNode, 0.9f, { 1, 20 }); // time 0.9 + advanceAnimationAndExpectValues(*animNode, 1.0f, { 1, 20 }); // time 1.0 + advanceAnimationAndExpectValues(*animNode, 100.f, { 1, 20 }); // stays at last keyframe after animation end + } + + TEST_P(AnAnimationNode, InterpolatedValueBeforeFirstTimestampIsFirstKeyframe) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f }); + const auto data = m_logicEngine.createDataArray(std::vector{ { 1.f, 20.f }, { 2.f, 30.f } }); + const auto animNode = createAnimationNode({ { "channel", timeStamps, data, EInterpolationType::Linear } }); + + advanceAnimationAndExpectValues(*animNode, 0.f, { 1.f, 20.f }); + advanceAnimationAndExpectValues(*animNode, 0.25f, { 1.f, 20.f }); // time 0.5 + advanceAnimationAndExpectValues(*animNode, 0.5f, { 1.f, 20.f }); // time 1.0 + advanceAnimationAndExpectValues(*animNode, 0.75f, { 1.5f, 25.f }); // time 1.5 + advanceAnimationAndExpectValues(*animNode, 100.f, { 2.f, 30.f }); // stays at last keyframe after animation end + } + + TEST_P(AnAnimationNode, CanJumpAnywhereInAnimation) + { + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + const auto data = m_logicEngine.createDataArray(std::vector{ { 10.f, 20.f } }); + const auto animNode = createAnimationNode({ { "channel", timeStamps, data, EInterpolationType::Linear } }); + + advanceAnimationAndExpectValues(*animNode, 0.f, 10.f); + advanceAnimationAndExpectValues(*animNode, 1.f, 20.f); + advanceAnimationAndExpectValues(*animNode, -10.f, 10.f); + advanceAnimationAndExpectValues(*animNode, 10.f, 20.f); + advanceAnimationAndExpectValues(*animNode, 0.5f, 15.f); + advanceAnimationAndExpectValues(*animNode, 0.25f, 12.5f); + advanceAnimationAndExpectValues(*animNode, 0.75f, 17.5f); + } + + TEST_P(AnAnimationNode, GivesStableResultsWithExtremelySmallTimestamps) + { + constexpr float Eps = std::numeric_limits::epsilon(); + const auto timeStamps = m_logicEngine.createDataArray(std::vector{ Eps * 100, Eps * 200 }); + const auto data = m_logicEngine.createDataArray(std::vector{ { 1.f, 2.f } }); + auto& animNode = *createAnimationNode({ { "channel", timeStamps, data, EInterpolationType::Linear } }); + + // initialize output value by updating with zero progress and expect first keyframe value + advanceAnimationAndExpectValues(animNode, 0.f, 1.f); + + float lastValue = 0.f; + for (int i = 0; i < 110; ++i) + { + const float progress = 0.01f * float(i); + + animNode.getInputs()->getChild("progress")->set(progress); + m_logicEngine.update(); + const auto val = *animNode.getOutputs()->getChild("channel")->get(); + + // expect interpolated value to go through keyframes in stable manner + EXPECT_TRUE(val >= lastValue); + lastValue = val; + } + // expect last keyframe value + EXPECT_FLOAT_EQ(2.f, lastValue); + } + + TEST_P(AnAnimationNode, CanBeCreatedWithMoreThanMaximumArraySizeKeyframesIfNotExposedViaProperties) + { + std::vector vecDataExceeding; + vecDataExceeding.resize(MaxArrayPropertySize + 1u); + std::iota(vecDataExceeding.begin(), vecDataExceeding.end(), 1.f); + const auto dataExceeding = m_logicEngine.createDataArray(vecDataExceeding); + + const AnimationChannel channelExceeding{ "channel2", dataExceeding, dataExceeding }; + AnimationNodeConfig config; + config.addChannel(channelExceeding); + EXPECT_NE(nullptr, m_logicEngine.createAnimationNode(config, "animNode")); + } + + class AnAnimationNode_SerializationLifecycle : public AnAnimationNode + { + protected: + enum class ESerializationIssue + { + AllValid, + NameMissing, + IdMissing, + ChannelsMissing, + RootInMissing, + RootOutMissing, + ChannelNameMissing, + ChannelTimestampsMissing, + ChannelKeyframesMissing, + ChannelTangentsInMissing, + ChannelTangentsOutMissing, + InvalidInterpolationType, + PropertyInMissing, + PropertyOutMissing, + PropertyInWrongName, + PropertyOutWrongName, + PropertyChannelsDataInvalid + }; + + std::unique_ptr deserializeSerializedDataWithIssue(ESerializationIssue issue) + { + flatbuffers::FlatBufferBuilder flatBufferBuilder; + SerializationMap serializationMap; + DeserializationMap deserializationMap; + + { + const auto data = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }); + + HierarchicalTypeData inputs = MakeStruct("", {}); + if (issue != ESerializationIssue::PropertyInMissing) + { + if (issue == ESerializationIssue::PropertyInWrongName) + { + inputs.children.push_back(MakeType("wrongInput", EPropertyType::Float)); + } + else + { + inputs.children.push_back(MakeType("progress", EPropertyType::Float)); + } + } + if (issue == ESerializationIssue::PropertyChannelsDataInvalid) + inputs.children.push_back(MakeType("invalidChannelsData", EPropertyType::Array)); + + auto inputsImpl = std::make_unique(std::move(inputs), EPropertySemantics::AnimationInput); + + HierarchicalTypeData outputs = MakeStruct("", {}); + if (issue == ESerializationIssue::PropertyOutWrongName) + { + outputs.children.push_back(MakeType("wrongOutput", EPropertyType::Float)); + } + else + { + outputs.children.push_back(MakeType("duration", EPropertyType::Float)); + } + if (issue != ESerializationIssue::PropertyOutMissing) + outputs.children.push_back(MakeType("channel", EPropertyType::Float)); + auto outputsImpl = std::make_unique(std::move(outputs), EPropertySemantics::AnimationOutput); + + const auto dataFb = DataArrayImpl::Serialize(data->m_impl, flatBufferBuilder, serializationMap); + flatBufferBuilder.Finish(dataFb); + const auto dataFbSerialized = flatbuffers::GetRoot(flatBufferBuilder.GetBufferPointer()); + deserializationMap.storeDataArray(*dataFbSerialized, *data); + + std::vector> channelsFB; + channelsFB.push_back(rlogic_serialization::CreateChannel( + flatBufferBuilder, + issue == ESerializationIssue::ChannelNameMissing ? 0 : flatBufferBuilder.CreateString("channel"), + issue == ESerializationIssue::ChannelTimestampsMissing ? 0 : dataFb, + issue == ESerializationIssue::ChannelKeyframesMissing ? 0 : dataFb, + issue == ESerializationIssue::InvalidInterpolationType ? static_cast(10) : rlogic_serialization::EInterpolationType::Cubic, + issue == ESerializationIssue::ChannelTangentsInMissing ? 0 : dataFb, + issue == ESerializationIssue::ChannelTangentsOutMissing ? 0 : dataFb + )); + + const auto animNodeFB = rlogic_serialization::CreateAnimationNode( + flatBufferBuilder, + rlogic_serialization::CreateLogicObject(flatBufferBuilder, + issue == ESerializationIssue::NameMissing ? 0 : flatBufferBuilder.CreateString("animNode"), + issue == ESerializationIssue::IdMissing ? 0 : 1u), + issue == ESerializationIssue::ChannelsMissing ? 0 : flatBufferBuilder.CreateVector(channelsFB), + issue == ESerializationIssue::PropertyChannelsDataInvalid, + issue == ESerializationIssue::RootInMissing ? 0 : PropertyImpl::Serialize(*inputsImpl, flatBufferBuilder, serializationMap), + issue == ESerializationIssue::RootOutMissing ? 0 : PropertyImpl::Serialize(*outputsImpl, flatBufferBuilder, serializationMap) + ); + + flatBufferBuilder.Finish(animNodeFB); + } + + const auto& serialized = *flatbuffers::GetRoot(flatBufferBuilder.GetBufferPointer()); + return AnimationNodeImpl::Deserialize(serialized, m_errorReporting, deserializationMap); + } + + ErrorReporting m_errorReporting; + }; + + INSTANTIATE_TEST_SUITE_P( + AnAnimationNode_SerializationLifecycle_TestInstances, + AnAnimationNode_SerializationLifecycle, + ::testing::Values(false, true)); + + TEST_P(AnAnimationNode_SerializationLifecycle, FailsDeserializationIfEssentialDataMissing) + { + EXPECT_TRUE(deserializeSerializedDataWithIssue(AnAnimationNode_SerializationLifecycle::ESerializationIssue::AllValid)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + + for (const auto issue : { ESerializationIssue::NameMissing, ESerializationIssue::IdMissing, ESerializationIssue::ChannelsMissing, ESerializationIssue::RootInMissing, ESerializationIssue::RootOutMissing }) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(issue)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of AnimationNode from serialized data: missing name, id, channels or in/out property data!", m_errorReporting.getErrors().back().message); + m_errorReporting.clear(); + } + } + + TEST_P(AnAnimationNode_SerializationLifecycle, FailsDeserializationIfChannelDataMissing) + { + for (const auto issue : { ESerializationIssue::ChannelTimestampsMissing, ESerializationIssue::ChannelKeyframesMissing }) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(issue)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of AnimationNode 'animNode' channel data: missing name, timestamps or keyframes!", m_errorReporting.getErrors().front().message); + m_errorReporting.clear(); + } + } + + TEST_P(AnAnimationNode_SerializationLifecycle, FailsDeserializationIfTangentsMissing) + { + for (const auto issue : { ESerializationIssue::ChannelTangentsInMissing, ESerializationIssue::ChannelTangentsOutMissing }) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(issue)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of AnimationNode 'animNode' channel 'channel' data: missing tangents!", m_errorReporting.getErrors().front().message); + m_errorReporting.clear(); + } + } + + TEST_P(AnAnimationNode_SerializationLifecycle, FailsDeserializationIfInvalidInterpolationType) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ESerializationIssue::InvalidInterpolationType)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of AnimationNode 'animNode' channel 'channel' data: missing or invalid interpolation type!", m_errorReporting.getErrors().front().message); + m_errorReporting.clear(); + } + + TEST_P(AnAnimationNode_SerializationLifecycle, FailsDeserializationIfInvalidOrMissingProperties) + { + for (const auto issue : { ESerializationIssue::PropertyInMissing, ESerializationIssue::PropertyOutMissing, ESerializationIssue::PropertyInWrongName, ESerializationIssue::PropertyOutWrongName }) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(issue)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of AnimationNode 'animNode': missing or invalid properties!", m_errorReporting.getErrors().front().message); + m_errorReporting.clear(); + } + } + + TEST_P(AnAnimationNode_SerializationLifecycle, FailsDeserializationIfInvalidChannelsData) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ESerializationIssue::PropertyChannelsDataInvalid)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of AnimationNode 'animNode': missing or invalid channels data property!", m_errorReporting.getErrors().front().message); + } +} diff --git a/client/logic/unittests/api/AnimationNodeWithDataPropertiesTest.cpp b/client/logic/unittests/api/AnimationNodeWithDataPropertiesTest.cpp new file mode 100644 index 000000000..a61ad18c5 --- /dev/null +++ b/client/logic/unittests/api/AnimationNodeWithDataPropertiesTest.cpp @@ -0,0 +1,336 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/Property.h" +#include "WithTempDirectory.h" +#include + +namespace ramses::internal +{ + // all operations and behavior not specific to node 'with data properties' is tested in AnimationNodeTest.cpp + class AnAnimationNodeWithDataProperties : public ::testing::Test + { + public: + void SetUp() override + { + m_dataFloat1 = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f, 2.f }); + m_dataFloat2 = m_logicEngine.createDataArray(std::vector{ 0.f, 10.f, 20.f }); + + m_configSaveFileWithoutValidation.setValidationEnabled(false); + } + + protected: + AnimationNode* createAnimationNodeWithDataProperties(const AnimationChannels& channels, std::string_view name = "") + { + AnimationNodeConfig config; + for (const auto& ch : channels) + { + EXPECT_TRUE(config.addChannel(ch)); + } + + if (!config.setExposingOfChannelDataAsProperties(true)) + return nullptr; + + return m_logicEngine.createAnimationNode(config, name); + } + + static void ExpectArrayDataProperty(const Property& arrayDataProp, const std::vector& arrayValues) + { + ASSERT_EQ(EPropertyType::Array, arrayDataProp.getType()); + ASSERT_EQ(arrayValues.size(), arrayDataProp.getChildCount()); + + std::vector actualValues; + for (size_t i = 0u; i < arrayDataProp.getChildCount(); ++i) + { + const auto val = arrayDataProp.getChild(i)->get(); + ASSERT_TRUE(val); + actualValues.push_back(*val); + } + + EXPECT_EQ(arrayValues, actualValues); + } + + static void ExpectChannelDataProperties(const Property& channelDataProp, const std::vector& timestamps, const std::vector& keyframes) + { + EXPECT_EQ(EPropertyType::Struct, channelDataProp.getType()); + ASSERT_EQ(2u, channelDataProp.getChildCount()); + + const auto timestampsProp = channelDataProp.getChild(0u); + EXPECT_EQ("timestamps", timestampsProp->getName()); + ExpectArrayDataProperty(*timestampsProp, timestamps); + + const auto keyframesProp = channelDataProp.getChild(1u); + EXPECT_EQ("keyframes", keyframesProp->getName()); + ExpectArrayDataProperty(*keyframesProp, keyframes); + } + + void advanceAnimationAndExpectValues(AnimationNode& animNode, float progress, float expectedValue) + { + EXPECT_TRUE(animNode.getInputs()->getChild("progress")->set(progress)); + EXPECT_TRUE(m_logicEngine.update()); + const auto val = *animNode.getOutputs()->getChild("channel")->get(); + EXPECT_FLOAT_EQ(expectedValue, val); + } + + LogicEngine m_logicEngine{ ramses::EFeatureLevel_Latest }; + DataArray* m_dataFloat1 = nullptr; + DataArray* m_dataFloat2 = nullptr; + SaveFileConfig m_configSaveFileWithoutValidation; + }; + + TEST_F(AnAnimationNodeWithDataProperties, HasAnimationDataProperties) + { + const AnimationChannel channel1{ "channel1", m_dataFloat1, m_dataFloat2 }; + const AnimationChannel channel2{ "channel2", m_dataFloat2, m_dataFloat1 }; + const auto animNode = createAnimationNodeWithDataProperties({ channel1, channel2 }, "animNode"); + + const auto rootIn = animNode->getInputs(); + EXPECT_EQ("", rootIn->getName()); + ASSERT_EQ(2u, rootIn->getChildCount()); + const auto channelsData = rootIn->getChild(1u); + EXPECT_EQ("channelsData", channelsData->getName()); + EXPECT_EQ(EPropertyType::Struct, channelsData->getType()); + + ASSERT_EQ(2u, channelsData->getChildCount()); + + const auto channelData1 = channelsData->getChild(0u); + EXPECT_EQ("channel1", channelData1->getName()); + ExpectChannelDataProperties(*channelData1, *m_dataFloat1->getData(), *m_dataFloat2->getData()); + + const auto channelData2 = channelsData->getChild(1u); + EXPECT_EQ("channel2", channelData2->getName()); + ExpectChannelDataProperties(*channelData2, *m_dataFloat2->getData(), *m_dataFloat1->getData()); + } + + TEST_F(AnAnimationNodeWithDataProperties, AnimatesAccordinglyWhenKeyframesModified) + { + const auto animNode = createAnimationNodeWithDataProperties({ { "channel", m_dataFloat1, m_dataFloat2, EInterpolationType::Linear } }); + + // keyframes (0/0, 1/10, 2/20) + advanceAnimationAndExpectValues(*animNode, 0.0f, 0.f); + advanceAnimationAndExpectValues(*animNode, 0.05f, 1.f); // time 0.1 + advanceAnimationAndExpectValues(*animNode, 0.25f, 5.f); // time 0.5 + + // modify 2nd keyframe -> keyframes (0/0, 1/30, 2/20) + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(1u)->set(30.f); + // will jump to newly interpolated value + advanceAnimationAndExpectValues(*animNode, 0.25f, 15.f); // time 0.5 + // arrive to 2nd keyframe with new value + advanceAnimationAndExpectValues(*animNode, 0.5f, 30.f); // time 1.0 + + // modify 2nd keyframe again -> keyframes (0/0, 1/100, 2/20) + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(1u)->set(100.f); + // jump to new value right after update, no time change applied + advanceAnimationAndExpectValues(*animNode, 0.5f, 100.f); // time 1.0 + + // modify 1st keyframe -> keyframes (0/-1000, 1/100, 2/20) + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(0u)->set(-1000.f); + // has no effect as we are pass that keyframe + advanceAnimationAndExpectValues(*animNode, 0.5f, 100.f); // time 1.0 + + // modify last keyframe -> keyframes (0/-1000, 1/100, 2/200) + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(2u)->set(200.f); + // has no effect yet we are at 2nd keyframe + advanceAnimationAndExpectValues(*animNode, 0.5f, 100.f); // time 1.0 + // last keyframe with new value in effect when progressing + advanceAnimationAndExpectValues(*animNode, 0.75f, 150.f); // time 1.5 + advanceAnimationAndExpectValues(*animNode, 1.f, 200.f); // time 2.0 + + // now jump around and test all the new values (0/-1000, 1/100, 2/200) + advanceAnimationAndExpectValues(*animNode, 0.0f, -1000.f); // time 0.0 + advanceAnimationAndExpectValues(*animNode, 1.0f, 200.f); // time 2.0 + advanceAnimationAndExpectValues(*animNode, 0.25f, -450.f); // time 0.5 + advanceAnimationAndExpectValues(*animNode, 0.75f, 150.f); // time 1.5 + advanceAnimationAndExpectValues(*animNode, 0.5f, 100.f); // time 1.0 + } + + TEST_F(AnAnimationNodeWithDataProperties, AnimatesToNewKeyframeSmoothlyByModifyingBothKeyframesAndTimestamps) + { + const auto animNode = createAnimationNodeWithDataProperties({ { "channel", m_dataFloat1, m_dataFloat2, EInterpolationType::Linear } }); + + // keyframes (0/0, 1/10, 2/20) + // for simplicity test will move only between 2 keys in the [1/10, 2/20] range + advanceAnimationAndExpectValues(*animNode, 0.5f, 10.f); // time 1.0 + + // progress towards 20 + advanceAnimationAndExpectValues(*animNode, 0.6f, 12.f); // time 1.2 + + // new value target 1000 + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(2u)->set(1000.f); + // to avoid jump which would be caused by lerp between [1/10, 2/1000] at time 1.2 we need to modify previous keyframe as well + // if we 'move' the previous keyframe to current time and current value, animation will smoothly continue from 'here' to new target + const float currTime = 1.2f; + const float currValue = 12.f; + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("timestamps")->getChild(1u)->set(currTime); + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(1u)->set(currValue); + // we modified both previous and next keyframes plus previous timestamp, update with no progress will give same value as before + advanceAnimationAndExpectValues(*animNode, 0.6f, currValue); // time 1.2 + // now progress to new target 1000 + advanceAnimationAndExpectValues(*animNode, 0.625f, 73.749947f); // time 1.25 + advanceAnimationAndExpectValues(*animNode, 0.65f, 135.49989f); // time 1.3 + advanceAnimationAndExpectValues(*animNode, 0.7f, 259.f); // time 1.4 + + // repeat the same again in the middle of ongoing animation -> new value target -1000 + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(2u)->set(-1000.f); + const float currTime2 = 1.4f; + const float currValue2 = 259.f; + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("timestamps")->getChild(1u)->set(currTime2); + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(1u)->set(currValue2); + advanceAnimationAndExpectValues(*animNode, 0.7f, currValue2); // time 1.4 + // now progress to new target -1000 + advanceAnimationAndExpectValues(*animNode, 0.75f, 49.166626f); // time 1.5 + advanceAnimationAndExpectValues(*animNode, 0.8f, -160.66675f); // time 1.6 + advanceAnimationAndExpectValues(*animNode, 0.9f, -580.3335f); // time 1.8 + advanceAnimationAndExpectValues(*animNode, 1.0f, -1000.f); // time 2.0 + } + + TEST_F(AnAnimationNodeWithDataProperties, ModifyingLastTimestampExtendsWholeAnimation) + { + const auto animNode = createAnimationNodeWithDataProperties({ { "channel", m_dataFloat1, m_dataFloat2, EInterpolationType::Linear } }); + + // keyframes (0/0, 1/10, 2/20) + // jump right at the end + advanceAnimationAndExpectValues(*animNode, 1.0f, 20.f); // time 2 + + // modify last timestamp and keyframe to extend animation -> keyframes (0/0, 1/10, 50/1000) + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("timestamps")->getChild(2u)->set(50.f); + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(2u)->set(1000.f); + // will jump to newly interpolated value because animation progress is suddenly before its last keyframe + advanceAnimationAndExpectValues(*animNode, 0.04f, 30.204081f); // time 2 + advanceAnimationAndExpectValues(*animNode, 1.f, 1000.f); // time 50 + EXPECT_FLOAT_EQ(50.f, *animNode->getOutputs()->getChild("duration")->get()); + + // extend again -> keyframes (0/0, 1/10, 100/2000) + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("timestamps")->getChild(2u)->set(100.f); + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(2u)->set(2000.f); + // will jump to newly interpolated value because animation progress is suddenly before its last keyframe + advanceAnimationAndExpectValues(*animNode, 0.5f, 994.94946f); // time 50 + advanceAnimationAndExpectValues(*animNode, 1.f, 2000.f); // time 100 + EXPECT_FLOAT_EQ(100.f, *animNode->getOutputs()->getChild("duration")->get()); + } + + TEST_F(AnAnimationNodeWithDataProperties, ModifyingKeyframeNorTimestampDoesNotAffectChannelDataRetrievedViaGetter) + { + const auto animNode = createAnimationNodeWithDataProperties({ { "channel", m_dataFloat1, m_dataFloat2, EInterpolationType::Linear } }); + + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("timestamps")->getChild(2u)->set(50.f); + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(2u)->set(1000.f); + EXPECT_TRUE(m_logicEngine.update()); + + const AnimationChannels& channels = animNode->getChannels(); + ASSERT_EQ(1u, channels.size()); + EXPECT_EQ(m_dataFloat1, channels[0].timeStamps); + EXPECT_EQ(m_dataFloat2, channels[0].keyframes); + EXPECT_THAT(*channels[0].timeStamps->getData(), ::testing::ElementsAre(0.f, 1.f, 2.f)); + EXPECT_THAT(*channels[0].keyframes->getData(), ::testing::ElementsAre(0.f, 10.f, 20.f)); + } + + TEST_F(AnAnimationNodeWithDataProperties, CanBeSerializedAndDeserialized) + { + WithTempDirectory tempDir; + + { + LogicEngine otherEngine{ m_logicEngine.getFeatureLevel() }; + + const auto data1 = otherEngine.createDataArray(std::vector{ 0.f, 1.f, 2.f }, "data1"); + const auto data2 = otherEngine.createDataArray(std::vector{ 0.f, 10.f, 20.f }, "data2"); + const AnimationChannel channel1{ "channel1", data1, data2, EInterpolationType::Linear }; + const AnimationChannel channel2{ "channel2", data2, data1, EInterpolationType::Linear }; + AnimationNodeConfig config; + ASSERT_TRUE(config.setExposingOfChannelDataAsProperties(true)); + ASSERT_TRUE(config.addChannel(channel1)); + ASSERT_TRUE(config.addChannel(channel2)); + ASSERT_TRUE(otherEngine.createAnimationNode(config, "animNode")); + + ASSERT_TRUE(otherEngine.saveToFile("logic_animNodes.bin", m_configSaveFileWithoutValidation)); + } + + ASSERT_TRUE(m_logicEngine.loadFromFile("logic_animNodes.bin")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + const auto data1 = m_logicEngine.findByName("data1"); + const auto data2 = m_logicEngine.findByName("data2"); + const auto animNode = m_logicEngine.findByName("animNode"); + ASSERT_TRUE(data1 && data2 && animNode); + + EXPECT_EQ("animNode", animNode->getName()); + EXPECT_FLOAT_EQ(20.f, *animNode->getOutputs()->getChild("duration")->get()); + + const auto rootIn = animNode->getInputs(); + EXPECT_EQ("", rootIn->getName()); + ASSERT_EQ(2u, rootIn->getChildCount()); + const auto channelsData = rootIn->getChild(1u); + EXPECT_EQ("channelsData", channelsData->getName()); + EXPECT_EQ(EPropertyType::Struct, channelsData->getType()); + + ASSERT_EQ(2u, channelsData->getChildCount()); + + const auto channelData1 = channelsData->getChild(0u); + EXPECT_EQ("channel1", channelData1->getName()); + ExpectChannelDataProperties(*channelData1, *data1->getData(), *data2->getData()); + + const auto channelData2 = channelsData->getChild(1u); + EXPECT_EQ("channel2", channelData2->getName()); + ExpectChannelDataProperties(*channelData2, *data2->getData(), *data1->getData()); + } + + TEST_F(AnAnimationNodeWithDataProperties, ResetsChannelDataToOriginalValuesWhenLoadedFromFile) + { + WithTempDirectory tempDir; + + { + LogicEngine otherEngine{ m_logicEngine.getFeatureLevel() }; + + const auto data1 = otherEngine.createDataArray(std::vector{ 0.f, 1.f, 2.f }, "data1"); + const auto data2 = otherEngine.createDataArray(std::vector{ 0.f, 10.f, 20.f }, "data2"); + const AnimationChannel channel{ "channel", data1, data2, EInterpolationType::Linear }; + AnimationNodeConfig config; + ASSERT_TRUE(config.setExposingOfChannelDataAsProperties(true)); + ASSERT_TRUE(config.addChannel(channel)); + const auto animNode = otherEngine.createAnimationNode(config, "animNode"); + ASSERT_TRUE(animNode); + + // modify animation data and update + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("timestamps")->getChild(2u)->set(50.f); + animNode->getInputs()->getChild("channelsData")->getChild("channel")->getChild("keyframes")->getChild(2u)->set(1000.f); + EXPECT_TRUE(otherEngine.update()); + + ASSERT_TRUE(otherEngine.saveToFile("logic_animNodes.bin", m_configSaveFileWithoutValidation)); + } + + ASSERT_TRUE(m_logicEngine.loadFromFile("logic_animNodes.bin")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + EXPECT_TRUE(m_logicEngine.update()); + + const auto data1 = m_logicEngine.findByName("data1"); + const auto data2 = m_logicEngine.findByName("data2"); + const auto animNode = m_logicEngine.findByName("animNode"); + ASSERT_TRUE(data1 && data2 && animNode); + + EXPECT_EQ("animNode", animNode->getName()); + EXPECT_FLOAT_EQ(2.f, *animNode->getOutputs()->getChild("duration")->get()); + + const auto rootIn = animNode->getInputs(); + EXPECT_EQ("", rootIn->getName()); + ASSERT_EQ(2u, rootIn->getChildCount()); + const auto channelsData = rootIn->getChild(1u); + EXPECT_EQ("channelsData", channelsData->getName()); + EXPECT_EQ(EPropertyType::Struct, channelsData->getType()); + + ASSERT_EQ(1u, channelsData->getChildCount()); + + const auto channelData = channelsData->getChild(0u); + EXPECT_EQ("channel", channelData->getName()); + ExpectChannelDataProperties(*channelData, *data1->getData(), *data2->getData()); + } +} diff --git a/client/logic/unittests/api/CollectionTest.cpp b/client/logic/unittests/api/CollectionTest.cpp new file mode 100644 index 000000000..d49a50e81 --- /dev/null +++ b/client/logic/unittests/api/CollectionTest.cpp @@ -0,0 +1,182 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "ramses-logic/Collection.h" + +namespace ramses +{ + class TestCollectionItem + { + public: + explicit TestCollectionItem(size_t id) + : m_id(id) + { + } + + [[nodiscard]] size_t getId() const + { + return m_id; + } + + void setId(size_t newId) + { + m_id = newId; + } + + private: + size_t m_id; + }; + + using TestCollectionType = Collection; + + class ACollection : public ::testing::Test + { + protected: + ACollection() + : m_testCollection(m_internalContainer) + { + auto testCollectionItem1 = std::make_unique(1); + auto testCollectionItem2 = std::make_unique(2); + auto testCollectionItem3 = std::make_unique(3); + m_internalContainer.push_back(testCollectionItem1.get()); + m_internalContainer.push_back(testCollectionItem2.get()); + m_internalContainer.push_back(testCollectionItem3.get()); + m_objectsOwningVector.push_back(std::move(testCollectionItem1)); + m_objectsOwningVector.push_back(std::move(testCollectionItem2)); + m_objectsOwningVector.push_back(std::move(testCollectionItem3)); + } + + std::vector> m_objectsOwningVector; + std::vector m_internalContainer; + TestCollectionType m_testCollection; + }; + + TEST_F(ACollection, ReturnsSize) + { + EXPECT_EQ(3u, m_testCollection.size()); + } + + TEST_F(ACollection, CanBeIteratedInRangeLoops) + { + size_t id = 1; + for (auto item : m_testCollection) + { + EXPECT_EQ(id, item->getId()); + id++; + } + } + + TEST_F(ACollection, CanBeIteratedInRangeLoops_AsConstRef) + { + const TestCollectionType& collectionConstRef = m_testCollection; + + size_t id = 1; + auto constIter = collectionConstRef.cbegin(); + auto constIterEnd = collectionConstRef.cend(); + for (; constIter != constIterEnd; constIter++) + { + const TestCollectionItem* itemPtr = *constIter; + EXPECT_EQ(id, itemPtr->getId()); + ++id; + } + } + + TEST_F(ACollection, CanBeCopied) + { + TestCollectionType collectionCopy = m_testCollection; + + size_t id = 1; + for (auto item : collectionCopy) + { + EXPECT_EQ(id, item->getId()); + id++; + } + } + + TEST_F(ACollection, CanBeAssigned) + { + TestCollectionType collectionToBeAssigned(m_internalContainer); + collectionToBeAssigned = m_testCollection; + + size_t id = 1; + for (auto item : collectionToBeAssigned) + { + EXPECT_EQ(id, item->getId()); + id++; + } + } + + // Shallow because changing underlying data is visible in both the original and the copy + TEST_F(ACollection, CopyIsShallow) + { + TestCollectionType collectionCopy = m_testCollection; + + m_testCollection.begin()->setId(15); + EXPECT_EQ(15u, collectionCopy.begin()->getId()); + } + + TEST_F(ACollection, CanBeUsedWithStdBeginAndStdEnd) + { + EXPECT_EQ(std::begin(m_testCollection), std::begin(m_testCollection)); + EXPECT_EQ(std::end(m_testCollection), std::end(m_testCollection)); + EXPECT_NE(std::end(m_testCollection), std::begin(m_testCollection)); + } + + TEST_F(ACollection, CanBePassedAsArgumentToStdAlgorithms) + { + TestCollectionType collectionCopy(m_testCollection); + const TestCollectionType& collectionConstRef = m_testCollection; + + size_t i = 1; + auto checkNames = [&i](const TestCollectionItem* item) + { + ASSERT_EQ(i++, item->getId()); + }; + std::for_each(m_testCollection.begin(), m_testCollection.end(), checkNames); + + i = 1; + std::for_each(m_testCollection.cbegin(), m_testCollection.cend(), checkNames); + + i = 1; + std::for_each(collectionConstRef.cbegin(), collectionConstRef.cend(), checkNames); + + i = 1; + std::for_each(collectionConstRef.begin(), collectionConstRef.end(), checkNames); + + EXPECT_EQ(m_internalContainer[1], *std::find_if(m_testCollection.begin(), m_testCollection.end(), [](TestCollectionItem* item) {return item->getId() == 2; })); + EXPECT_EQ(m_internalContainer[1], *std::find_if(collectionCopy.cbegin(), collectionCopy.cend(), [](const TestCollectionItem* item) {return item->getId() == 2; })); + EXPECT_EQ(m_testCollection.end(), std::find_if(m_testCollection.begin(), m_testCollection.end(), [](TestCollectionItem* item) {return item->getId() == 999; })); + } + + TEST_F(ACollection, ProvidesIncrementableIterators) + { + TestCollectionType collectionCopy(m_testCollection); + auto iter = collectionCopy.begin(); + auto otherIter = iter++; + EXPECT_NE(iter, otherIter); + ++otherIter; + EXPECT_EQ(otherIter, iter); + } + + TEST_F(ACollection, ProvidesComparableIterators) + { + EXPECT_EQ(m_testCollection.begin(), m_testCollection.begin()); + EXPECT_EQ(m_testCollection.end(), m_testCollection.end()); + auto iter0_a = m_testCollection.begin(); + auto iter0_b = m_testCollection.begin(); + auto iter1 = ++m_testCollection.begin(); + EXPECT_EQ(iter0_a, iter0_a); + EXPECT_NE(iter0_a, iter1); + EXPECT_NE(iter0_b, iter1); + EXPECT_EQ(iter1, iter1); + } +} + diff --git a/client/logic/unittests/api/DataArrayTest.cpp b/client/logic/unittests/api/DataArrayTest.cpp new file mode 100644 index 000000000..c13927d1b --- /dev/null +++ b/client/logic/unittests/api/DataArrayTest.cpp @@ -0,0 +1,464 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "WithTempDirectory.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/DataArray.h" +#include "impl/DataArrayImpl.h" +#include "internals/ErrorReporting.h" +#include "generated/DataArrayGen.h" + +#include + +namespace ramses::internal +{ + template + class ADataArray : public ::testing::Test + { + protected: + LogicEngine m_logicEngine{ ramses::EFeatureLevel_Latest }; + }; + + using DataTypes = ::testing::Types < + float, + vec2f, + vec3f, + vec4f, + int32_t, + vec2i, + vec3i, + vec4i, + std::vector + >; + TYPED_TEST_SUITE(ADataArray, DataTypes); + + template + std::vector SomeDataVector(); + template <> std::vector SomeDataVector() { return { 1.f, 2.f, 3.f }; } + template <> std::vector SomeDataVector() { return { {1.f, 2.f}, {3.f, 4.f} }; } + template <> std::vector SomeDataVector() { return { {1.f, 2.f, 3.f}, {3.f, 4.f, 5.f} }; } + template <> std::vector SomeDataVector() { return { {1.f, 2.f, 3.f, 4.f}, {3.f, 4.f, 5.f, 6.f} }; } + template <> std::vector SomeDataVector() { return { 1, 2, 3 }; } + template <> std::vector SomeDataVector() { return { {1, 2}, {3, 4} }; } + template <> std::vector SomeDataVector() { return { {1, 2, 3}, {3, 4, 5} }; } + template <> std::vector SomeDataVector() { return { {1, 2, 3, 4}, {3, 4, 5, 6} }; } + template <> std::vector> SomeDataVector() { return { {1.f, 2.f, 3.f, 4.f, 5.f}, {3.f, 4.f, 5.f, 6.f, 7.f} }; } + + TYPED_TEST(ADataArray, IsCreated) + { + const auto data = SomeDataVector(); + const auto dataArray = this->m_logicEngine.createDataArray(data, "dataarray"); + EXPECT_TRUE(this->m_logicEngine.getErrors().empty()); + ASSERT_NE(nullptr, dataArray); + EXPECT_EQ(dataArray, this->m_logicEngine.template findByName("dataarray")); + + EXPECT_EQ("dataarray", dataArray->getName()); + EXPECT_EQ(EPropertyType(PropertyTypeToEnum::TYPE), dataArray->getDataType()); + ASSERT_NE(nullptr, dataArray->template getData()); + EXPECT_EQ(data, *dataArray->template getData()); + EXPECT_EQ(data.size(), dataArray->getNumElements()); + } + + TYPED_TEST(ADataArray, FailsToCreateIfEmptyDataProvided) + { + EXPECT_EQ(nullptr, this->m_logicEngine.createDataArray(std::vector{}, "dataarray")); + ASSERT_FALSE(this->m_logicEngine.getErrors().empty()); + EXPECT_EQ("Cannot create DataArray 'dataarray' with empty data.", this->m_logicEngine.getErrors().front().message); + } + + TYPED_TEST(ADataArray, IsDestroyed) + { + const auto data = SomeDataVector(); + auto dataArray = this->m_logicEngine.createDataArray(data, "dataarray"); + + EXPECT_TRUE(this->m_logicEngine.destroy(*dataArray)); + EXPECT_TRUE(this->m_logicEngine.getErrors().empty()); + EXPECT_EQ(nullptr, this->m_logicEngine.template findByName("dataarray")); + } + + TYPED_TEST(ADataArray, FailsToBeDestroyedIfFromOtherLogicInstance) + { + LogicEngine otherEngine{ this->m_logicEngine.getFeatureLevel() }; + auto dataArray = otherEngine.createDataArray(SomeDataVector(), "dataarray"); + + EXPECT_FALSE(this->m_logicEngine.destroy(*dataArray)); + ASSERT_FALSE(this->m_logicEngine.getErrors().empty()); + EXPECT_EQ("Failed to destroy object 'dataarray [Id=1]', cannot find it in this LogicEngine instance.", this->m_logicEngine.getErrors().front().message); + } + + TYPED_TEST(ADataArray, ChangesName) + { + auto dataArray = this->m_logicEngine.createDataArray(SomeDataVector(), "dataarray"); + + dataArray->setName("da"); + EXPECT_EQ("da", dataArray->getName()); + EXPECT_EQ(dataArray, this->m_logicEngine.template findByName("da")); + EXPECT_TRUE(this->m_logicEngine.getErrors().empty()); + } + + TYPED_TEST(ADataArray, ReturnsNullIfWrongDataTypeQueried) + { + const auto data = SomeDataVector(); + auto dataArray = this->m_logicEngine.createDataArray(data, "dataarray"); + if (dataArray->getDataType() != EPropertyType::Vec3f) + { + EXPECT_EQ(nullptr, dataArray->template getData()); + } + } + + TYPED_TEST(ADataArray, CanBeSerializedAndDeserialized) + { + WithTempDirectory tempDir; + + const auto data1 = SomeDataVector(); + const auto data2 = SomeDataVector(); + const auto data3 = SomeDataVector(); + const auto data4 = SomeDataVector(); + const auto data5 = SomeDataVector>(); + + { + LogicEngine otherEngine{ this->m_logicEngine.getFeatureLevel() }; + + otherEngine.createDataArray(data1, "dataarray1"); + otherEngine.createDataArray(data2, "dataarray2"); + otherEngine.createDataArray(data3, "dataarray3"); + otherEngine.createDataArray(data4, "dataarray4"); + otherEngine.createDataArray(data5, "dataarray5"); + + ASSERT_TRUE(otherEngine.saveToFile("LogicEngine.bin")); + } + + ASSERT_TRUE(this->m_logicEngine.loadFromFile("LogicEngine.bin")); + EXPECT_TRUE(this->m_logicEngine.getErrors().empty()); + + EXPECT_EQ(5u, this->m_logicEngine.template getCollection().size()); + const auto dataArray1 = this->m_logicEngine.template findByName("dataarray1"); + const auto dataArray2 = this->m_logicEngine.template findByName("dataarray2"); + const auto dataArray3 = this->m_logicEngine.template findByName("dataarray3"); + const auto dataArray4 = this->m_logicEngine.template findByName("dataarray4"); + const auto dataArray5 = this->m_logicEngine.template findByName("dataarray5"); + ASSERT_TRUE(dataArray1 && dataArray2 && dataArray3 && dataArray4 && dataArray5); + + EXPECT_EQ(data1.size(), dataArray1->getNumElements()); + EXPECT_EQ(data2.size(), dataArray2->getNumElements()); + EXPECT_EQ(data3.size(), dataArray3->getNumElements()); + EXPECT_EQ(data4.size(), dataArray4->getNumElements()); + EXPECT_EQ(data5.size(), dataArray5->getNumElements()); + + const auto loadedData1 = dataArray1->template getData(); + const auto loadedData2 = dataArray2->template getData(); + const auto loadedData3 = dataArray3->template getData(); + const auto loadedData4 = dataArray4->template getData(); + const auto loadedData5 = dataArray5->template getData>(); + ASSERT_TRUE(loadedData1 && loadedData2 && loadedData3 && loadedData4 && loadedData5); + + EXPECT_EQ(data1, *loadedData1); + EXPECT_EQ(data2, *loadedData2); + EXPECT_EQ(data3, *loadedData3); + EXPECT_EQ(data4, *loadedData4); + EXPECT_EQ(data5, *loadedData5); + } + + TEST(ADataArrayOfFloatArrays, FailsToBeCreatedIfArraysSizesNotEqual) + { + const std::vector> dataVector = { + { 1.f, 2.f, 3.f }, + { 1.f, 2.f, 3.f, 4.f }, + { 1.f, 2.f, 3.f, 4.f, 5.f }, + { 1.f, 2.f, 3.f } + }; + + LogicEngine logicEngine{ ramses::EFeatureLevel_Latest }; + EXPECT_EQ(nullptr, logicEngine.createDataArray(dataVector, "dataarray")); + ASSERT_EQ(1u, logicEngine.getErrors().size()); + EXPECT_EQ(logicEngine.getErrors()[0].message, "Failed to create DataArray of float arrays: all arrays must be of same size."); + } + + class ADataArray_SerializationLifecycle : public ::testing::Test + { + protected: + LogicEngine m_logicEngine{ ramses::EFeatureLevel_Latest }; + + enum class ESerializationIssue + { + AllValid, + NameMissing, + IdMissing, + NoData, + WrongDataType, + CorruptArrayDataType, + WrongDataSize, + }; + + std::unique_ptr deserializeSerializedData(rlogic_serialization::ArrayUnion unionType, rlogic_serialization::EDataArrayType dataType, bool wrongElementCount = false) + { + flatbuffers::FlatBufferBuilder builder; + + flatbuffers::Offset dataOffset; + if (unionType == rlogic_serialization::ArrayUnion::floatArr) + { + std::vector dummyData{ 0.1f, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; //12 elements works for all vecX types + if (wrongElementCount) + { + dummyData.push_back(13); + } + + dataOffset = rlogic_serialization::CreatefloatArr(builder, builder.CreateVector(dummyData)).Union(); + } + else + { + std::vector dummyData{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; //12 elements works for all vecX types + if (wrongElementCount) + { + dummyData.push_back(13); + } + + dataOffset = rlogic_serialization::CreateintArr(builder, builder.CreateVector(dummyData)).Union(); + } + + uint32_t numElements = 0u; + switch (dataType) + { + case rlogic_serialization::EDataArrayType::Float: + case rlogic_serialization::EDataArrayType::Int32: + numElements = 12u; + break; + case rlogic_serialization::EDataArrayType::Vec2f: + case rlogic_serialization::EDataArrayType::Vec2i: + numElements = 6u; + break; + case rlogic_serialization::EDataArrayType::Vec3f: + case rlogic_serialization::EDataArrayType::Vec3i: + numElements = 4u; + break; + case rlogic_serialization::EDataArrayType::Vec4f: + case rlogic_serialization::EDataArrayType::Vec4i: + numElements = 3u; + break; + case rlogic_serialization::EDataArrayType::FloatArray: + numElements = 2u; + break; + default: + assert(false); + break; + } + + auto dataArrayFB = rlogic_serialization::CreateDataArray( + builder, + rlogic_serialization::CreateLogicObject(builder, builder.CreateString("dataarray"), 1u), + dataType, + unionType, + dataOffset, + numElements + ); + + builder.Finish(dataArrayFB); + + const auto& serialized = *flatbuffers::GetRoot(builder.GetBufferPointer()); + return DataArrayImpl::Deserialize(serialized, m_errorReporting); + } + + std::unique_ptr deserializeSerializedDataWithIssue(ESerializationIssue issue) + { + flatbuffers::FlatBufferBuilder flatBufferBuilder; + + auto dataOffset = issue == ESerializationIssue::WrongDataSize ? flatBufferBuilder.CreateVector(std::vector{ 0.f, 1.f, 2.f }) : flatBufferBuilder.CreateVector(std::vector{ 0.f, 1.f }); + + rlogic_serialization::ArrayUnion unionType = rlogic_serialization::ArrayUnion::floatArr; + + if (issue == ESerializationIssue::NoData) + { + unionType = rlogic_serialization::ArrayUnion::NONE; + } + else if (issue == ESerializationIssue::WrongDataType) + { + unionType = rlogic_serialization::ArrayUnion::intArr; + } + + const auto dataArrayFB = rlogic_serialization::CreateDataArray( + flatBufferBuilder, + rlogic_serialization::CreateLogicObject(flatBufferBuilder, + issue == ESerializationIssue::NameMissing ? 0 : flatBufferBuilder.CreateString("dataArray"), + issue == ESerializationIssue::IdMissing ? 0 : 1u), + issue == ESerializationIssue::CorruptArrayDataType ? static_cast(128) : rlogic_serialization::EDataArrayType::Vec2f, + unionType, + issue == ESerializationIssue::NoData ? 0 : rlogic_serialization::CreatefloatArr(flatBufferBuilder, dataOffset).Union() + ); + + flatBufferBuilder.Finish(dataArrayFB); + + const auto& serialized = *flatbuffers::GetRoot(flatBufferBuilder.GetBufferPointer()); + return DataArrayImpl::Deserialize(serialized, m_errorReporting); + } + + ErrorReporting m_errorReporting; + }; + + TEST_F(ADataArray_SerializationLifecycle, ReportsNoDeserializationErrorsWhenAllDataCorrect) + { + EXPECT_TRUE(deserializeSerializedDataWithIssue(ADataArray_SerializationLifecycle::ESerializationIssue::AllValid)); + EXPECT_TRUE(this->m_errorReporting.getErrors().empty()); + } + + TEST_F(ADataArray_SerializationLifecycle, ReportsErrorWhenDeserializedWithoutName) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ADataArray_SerializationLifecycle::ESerializationIssue::NameMissing)); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing name!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of DataArray from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ADataArray_SerializationLifecycle, ReportsErrorWhenDeserializedWithoutId) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ADataArray_SerializationLifecycle::ESerializationIssue::IdMissing)); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing or invalid ID!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of DataArray from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ADataArray_SerializationLifecycle, ReportsErrorWhenDeserializedWithoutData) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ADataArray_SerializationLifecycle::ESerializationIssue::NoData)); + EXPECT_FALSE(this->m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of DataArray from serialized data: unexpected data type!", this->m_errorReporting.getErrors().front().message); + } + + TEST_F(ADataArray_SerializationLifecycle, ReportsErrorWhenDeserializedWithWrongDataType) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ADataArray_SerializationLifecycle::ESerializationIssue::WrongDataType)); + EXPECT_FALSE(this->m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of DataArray from serialized data: unexpected data type!", this->m_errorReporting.getErrors().front().message); + } + + TEST_F(ADataArray_SerializationLifecycle, ReportsErrorWhenDeserializedWithCorruptedDataUnion) + { + std::vector> invalidDataUnionPairs = { + {rlogic_serialization::ArrayUnion::intArr, rlogic_serialization::EDataArrayType::Float}, + {rlogic_serialization::ArrayUnion::intArr, rlogic_serialization::EDataArrayType::Vec2f}, + {rlogic_serialization::ArrayUnion::intArr, rlogic_serialization::EDataArrayType::Vec3f}, + {rlogic_serialization::ArrayUnion::intArr, rlogic_serialization::EDataArrayType::Vec4f}, + {rlogic_serialization::ArrayUnion::floatArr, rlogic_serialization::EDataArrayType::Int32}, + {rlogic_serialization::ArrayUnion::floatArr, rlogic_serialization::EDataArrayType::Vec2i}, + {rlogic_serialization::ArrayUnion::floatArr, rlogic_serialization::EDataArrayType::Vec3i}, + {rlogic_serialization::ArrayUnion::floatArr, rlogic_serialization::EDataArrayType::Vec4i}, + {rlogic_serialization::ArrayUnion::intArr, rlogic_serialization::EDataArrayType::FloatArray} + }; + + for(const auto& [unionType, dataArrayType] : invalidDataUnionPairs) + { + EXPECT_FALSE(deserializeSerializedData(unionType, dataArrayType)); + EXPECT_FALSE(this->m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of DataArray from serialized data: unexpected data type!", this->m_errorReporting.getErrors().front().message); + this->m_errorReporting.clear(); + } + } + + TEST_F(ADataArray_SerializationLifecycle, ReportsErrorWhenDeserializedWithCorruptedElementSizes) + { + std::vector> dataUnionPairs = { + {rlogic_serialization::ArrayUnion::intArr, rlogic_serialization::EDataArrayType::Vec2i}, + {rlogic_serialization::ArrayUnion::intArr, rlogic_serialization::EDataArrayType::Vec3i}, + {rlogic_serialization::ArrayUnion::intArr, rlogic_serialization::EDataArrayType::Vec4i}, + {rlogic_serialization::ArrayUnion::floatArr, rlogic_serialization::EDataArrayType::Vec2f}, + {rlogic_serialization::ArrayUnion::floatArr, rlogic_serialization::EDataArrayType::Vec3f}, + {rlogic_serialization::ArrayUnion::floatArr, rlogic_serialization::EDataArrayType::Vec4f}, + {rlogic_serialization::ArrayUnion::floatArr, rlogic_serialization::EDataArrayType::FloatArray} + }; + + for (const auto& [unionType, dataArrayType] : dataUnionPairs) + { + EXPECT_FALSE(deserializeSerializedData(unionType, dataArrayType, true)); + EXPECT_FALSE(this->m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of DataArray from serialized data: unexpected data size!", this->m_errorReporting.getErrors().front().message); + this->m_errorReporting.clear(); + } + } + + TEST_F(ADataArray_SerializationLifecycle, ReportsErrorWhenDeserializedWithCorruptDataType) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ADataArray_SerializationLifecycle::ESerializationIssue::CorruptArrayDataType)); + EXPECT_FALSE(this->m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of DataArray from serialized data: unsupported or corrupt data type '128'!", this->m_errorReporting.getErrors().front().message); + } + + TEST_F(ADataArray_SerializationLifecycle, ReportsErrorWhenDeserializedWithWrongDataSize) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ADataArray_SerializationLifecycle::ESerializationIssue::WrongDataSize)); + EXPECT_FALSE(this->m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of DataArray from serialized data: unexpected data size!", this->m_errorReporting.getErrors().front().message); + } + + TEST(AnimationChannel, EqualityOperatorsTests) + { + LogicEngine engine{ ramses::EFeatureLevel_Latest }; + + const auto dataVector1 = SomeDataVector(); + const auto dataVector2 = SomeDataVector>(); + + DataArray* dataArray1 = engine.createDataArray(dataVector1, "dataarray1"); + DataArray* dataArray2 = engine.createDataArray(dataVector2, "dataarray2"); + + AnimationChannel emptyChannel; + AnimationChannel channel1 { + /// Name of the channel for identification when linking + "channel1", + dataArray1, + dataArray1, + EInterpolationType::Linear, + dataArray2, + dataArray2 + }; + AnimationChannel channel1_differentName{ + /// Name of the channel for identification when linking + "differentName", + dataArray1, + dataArray1, + EInterpolationType::Linear, + dataArray2, + dataArray2 + }; + AnimationChannel channel1_differentInterpType{ + /// Name of the channel for identification when linking + "differentName", + dataArray1, + dataArray1, + EInterpolationType::Cubic, + dataArray2, + dataArray2 + }; + AnimationChannel channel2{ + /// Name of the channel for identification when linking + "differentName", + dataArray2, + dataArray2, + EInterpolationType::Cubic, + dataArray1, + dataArray1 + }; + + // All objects are equal to itself + EXPECT_EQ(emptyChannel, emptyChannel); + EXPECT_EQ(channel1, channel1); + EXPECT_EQ(channel1_differentName, channel1_differentName); + EXPECT_EQ(channel1_differentInterpType, channel1_differentInterpType); + EXPECT_EQ(channel2, channel2); + + // Empty channel is not equal to non-empty channels + EXPECT_NE(emptyChannel, channel1); + EXPECT_NE(emptyChannel, channel2); + + // Name is not take into account in equality check + EXPECT_EQ(channel1, channel1_differentName); + + // Different data -> not equal + EXPECT_NE(channel1, channel1_differentInterpType); + EXPECT_NE(channel1, channel2); + } +} diff --git a/client/logic/unittests/api/IteratorTest.cpp b/client/logic/unittests/api/IteratorTest.cpp new file mode 100644 index 000000000..aa8276e3d --- /dev/null +++ b/client/logic/unittests/api/IteratorTest.cpp @@ -0,0 +1,160 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "ramses-logic/Iterator.h" + +namespace ramses +{ + class TestIteratorItem + { + public: + explicit TestIteratorItem(size_t id) + : m_id(id) + { + } + + [[nodiscard]] size_t getId() const + { + return m_id; + } + + void setId(size_t newId) + { + m_id = newId; + } + + private: + size_t m_id; + }; + + using TestIternalType = std::vector; + using TestIteratorType = Iterator; + using TestConstIteratorType = Iterator; + + class AIterator : public ::testing::Test + { + protected: + AIterator() + { + auto testIteratorItem1 = std::make_unique(1); + auto testIteratorItem2 = std::make_unique(2); + auto testIteratorItem3 = std::make_unique(3); + + m_internalContainer.push_back(testIteratorItem1.get()); + m_internalContainer.push_back(testIteratorItem2.get()); + m_internalContainer.push_back(testIteratorItem3.get()); + m_objectsOwningVector.push_back(std::move(testIteratorItem1)); + m_objectsOwningVector.push_back(std::move(testIteratorItem2)); + m_objectsOwningVector.push_back(std::move(testIteratorItem3)); + + m_begin = TestIteratorType(m_internalContainer.begin()); + m_end = TestIteratorType(m_internalContainer.end()); + m_cbegin = TestConstIteratorType(m_internalContainer.begin()); + m_cend = TestConstIteratorType(m_internalContainer.end()); + } + + std::vector> m_objectsOwningVector; + std::vector m_internalContainer; + TestIteratorType m_begin; + TestIteratorType m_end; + TestConstIteratorType m_cbegin; + TestConstIteratorType m_cend; + }; + + TEST_F(AIterator, CanBeDereferenced) + { + EXPECT_EQ(*m_internalContainer.begin(), *m_begin); + EXPECT_EQ(*m_internalContainer.begin(), *m_cbegin); + } + + TEST_F(AIterator, CanBePostIncremented) + { + auto beforeIncrement = m_begin++; + EXPECT_EQ(m_begin->getId(), 2u); + EXPECT_EQ(beforeIncrement->getId(), 1u); + } + + TEST_F(AIterator, CanBePreIncremented) + { + auto incremented = ++m_begin; + EXPECT_EQ(m_begin->getId(), 2u); + EXPECT_EQ(incremented->getId(), 2u); + } + + TEST_F(AIterator, CanBeComparedToOtherIterators) + { + EXPECT_EQ(m_begin, m_begin); + EXPECT_EQ(m_end, m_end); + EXPECT_NE(m_begin, m_end); + } + + TEST_F(AIterator, CanBeAssignedToOtherIterator) + { + TestIteratorType toBeAssigned; + + toBeAssigned = ++m_begin; + EXPECT_EQ(toBeAssigned->getId(), 2u); + } + + TEST_F(AIterator, CanBeAssignedToConstIterator) + { + TestIteratorType nonConstIter(m_begin); + TestConstIteratorType constIter; + + constIter = nonConstIter; + EXPECT_EQ(constIter->getId(), 1u); + } + + TEST_F(AIterator, CanBeCopiedIntoConstIterator) + { + TestIteratorType nonConstIter(m_begin); + TestConstIteratorType constIter = nonConstIter; + EXPECT_EQ(constIter->getId(), 1u); + } + + TEST_F(AIterator, CanBeCopied) + { + auto iteratorCopy1 = m_begin; + EXPECT_EQ(iteratorCopy1, m_begin); + auto iteratorCopy2(m_begin); + EXPECT_EQ(iteratorCopy2, m_begin); + + EXPECT_EQ(iteratorCopy1->getId(), 1u); + EXPECT_EQ(iteratorCopy2->getId(), 1u); + } + + TEST_F(AIterator, CanBeDefaultInitialized) + { + TestIteratorType defaultInitializedIterator1; + TestIteratorType defaultInitializedIterator2; + EXPECT_EQ(defaultInitializedIterator1, defaultInitializedIterator2); + TestConstIteratorType defaultInitializedConstIterator1; + TestConstIteratorType defaultInitializedConstIterator2; + EXPECT_EQ(defaultInitializedConstIterator1, defaultInitializedConstIterator2); + } + + TEST_F(AIterator, ProvidesNonConstAccessToIterable) + { + m_begin->setId(15); + EXPECT_EQ(15u, m_begin->getId()); + } + + TEST_F(AIterator, CanCompareConstAndMutableIteratorsInAnyDirection) + { + EXPECT_EQ(m_cbegin, m_begin); + EXPECT_EQ(m_begin, m_cbegin); + EXPECT_EQ(m_cend, m_end); + + EXPECT_NE(m_cbegin, m_end); + EXPECT_NE(m_cend, m_begin); + EXPECT_EQ(m_cend, m_end); + } +} diff --git a/client/logic/unittests/api/LoggerTest.cpp b/client/logic/unittests/api/LoggerTest.cpp new file mode 100644 index 000000000..d95b84399 --- /dev/null +++ b/client/logic/unittests/api/LoggerTest.cpp @@ -0,0 +1,102 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" + +#include "ramses-logic/Logger.h" +#include "ramses-framework-api/RamsesFrameworkTypes.h" + +#include "impl/LoggerImpl.h" + +#include "LogTestUtils.h" + +namespace ramses +{ + // Test default state without fixture + TEST(ALogger_ByDefault, HasInfoAsVerbosityLimit) + { + EXPECT_EQ(ELogLevel::Info, Logger::GetLogVerbosityLimit()); + } + + class ALogger : public ::testing::Test + { + protected: + std::vector m_logTypes; + std::vector m_logMessages; + ScopedLogContextLevel m_logCollector{ ELogLevel::Trace, [this](ELogLevel type, std::string_view message) + { + m_logTypes.emplace_back(type); + m_logMessages.emplace_back(message); + } + }; + }; + + TEST_F(ALogger, LogsDifferentLogLevelsSequentially) + { + LOG_FATAL("Fatal"); + LOG_ERROR("Error"); + LOG_WARN("Warn"); + LOG_INFO("Info"); + LOG_DEBUG("Debug"); + LOG_TRACE("Trace"); + + EXPECT_THAT(m_logTypes, ::testing::ElementsAre(ELogLevel::Fatal, ELogLevel::Error, ELogLevel::Warn, ELogLevel::Info, ELogLevel::Debug, ELogLevel::Trace)); + EXPECT_THAT(m_logMessages, ::testing::ElementsAre("Fatal", "Error", "Warn", "Info", "Debug", "Trace")); + } + + TEST_F(ALogger, LogsFormattedMessage) + { + LOG_INFO("Info Message {}", 42); + + EXPECT_EQ(m_logMessages[0], "Info Message 42"); + } + + TEST_F(ALogger, LogsFormattedMessageWithMultipleArgumentsAndTypes) + { + LOG_INFO("Info Message {} {} {} {}", 42, 0.5f, "bool:", true); + + EXPECT_EQ(m_logMessages[0], "Info Message 42 0.5 bool: true"); + } + + TEST_F(ALogger, SetsDefaultLoggingOffAndOnAgain) + { + Logger::SetDefaultLogging(false); + LOG_INFO("Info Message {} {} {}", 42, 42.0f, "42"); + Logger::SetDefaultLogging(true); + LOG_INFO("Info Message {} {} {}", 42, 42.0f, "43"); + + // Can't expect anything because default logging goes to stdout + } + + TEST_F(ALogger, SetsDefaultLoggingOff_DoesNotAffectCustomLogHandler) + { + Logger::SetDefaultLogging(false); + + LOG_INFO("info"); + EXPECT_EQ(m_logMessages[0], "info"); + + // Reset to not affect other tests + Logger::SetDefaultLogging(true); + } + + TEST_F(ALogger, ChangesLogVerbosityAffectsWhichMessagesAreProcessed) + { + Logger::SetLogVerbosityLimit(ELogLevel::Error); + + // Simulate logs of all types. Only error and fatal error should be logged + LOG_TRACE("trace"); + LOG_DEBUG("debug"); + LOG_FATAL("fatal"); + LOG_WARN("warn"); + LOG_INFO("info"); + LOG_DEBUG("debug"); + LOG_ERROR("error"); + + EXPECT_THAT(m_logTypes, ::testing::ElementsAre(ELogLevel::Fatal, ELogLevel::Error)); + } +} diff --git a/client/logic/unittests/api/LogicEngineTest_Animations.cpp b/client/logic/unittests/api/LogicEngineTest_Animations.cpp new file mode 100644 index 000000000..99d697ce4 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_Animations.cpp @@ -0,0 +1,301 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-framework-api/RamsesFramework.h" +#include "ramses-client-api/RamsesClient.h" +#include "ramses-client-api/Scene.h" +#include "ramses-client-api/Node.h" +#include + +namespace ramses::internal +{ + class ALogicEngine_Animations : public ::testing::Test + { + public: + void SetUp() override + { + const auto dataArray = m_logicEngine.createDataArray(std::vector{ 0.f, 1.f }, "dataarray"); + const AnimationChannel channel{ "channel", dataArray, dataArray, ramses::EInterpolationType::Linear }; + AnimationNodeConfig config; + config.addChannel(channel); + + m_animation1 = m_logicEngine.createAnimationNode(config, "animNode1"); + m_animation2 = m_logicEngine.createAnimationNode(config, "animNode2"); + m_timer = m_logicEngine.createTimerNode(); + } + + protected: + void setTickerAndUpdate(int64_t tick) + { + m_timer->getInputs()->getChild("ticker_us")->set(tick); + m_logicEngine.update(); + } + + void expectNodeValues(float expectedTranslate, float expectedRotate) + { + // we lose more than just single float epsilon precision in timer+animation logic + // but this error is still negligible considering input data <0, 1> + static constexpr float maxError = 1e-5f; + + vec3f vals; + m_node->getTranslation(vals); + EXPECT_NEAR(expectedTranslate, vals[0], maxError); + EXPECT_NEAR(expectedTranslate, vals[1], maxError); + EXPECT_NEAR(expectedTranslate, vals[2], maxError); + m_node->getRotation(vals); + EXPECT_NEAR(expectedRotate, vals[0], maxError); + EXPECT_NEAR(expectedRotate, vals[1], maxError); + EXPECT_NEAR(expectedRotate, vals[2], maxError); + EXPECT_EQ(ramses::ERotationType::Euler_XYZ, m_node->getRotationType()); + } + + ramses::RamsesFramework m_ramsesFramework{ ramses::RamsesFrameworkConfig{ramses::EFeatureLevel_Latest} }; + ramses::RamsesClient* m_ramsesClient{ m_ramsesFramework.createClient("client") }; + ramses::Scene* m_scene{ m_ramsesClient->createScene(ramses::sceneId_t{123u}) }; + ramses::Node* m_node{ m_scene->createNode() }; + + LogicEngine m_logicEngine{ ramses::EFeatureLevel_Latest }; + AnimationNode* m_animation1 = nullptr; + AnimationNode* m_animation2 = nullptr; + TimerNode* m_timer = nullptr; + + const std::string_view ScriptScalarToVecSrc = R"( + function interface(IN,OUT) + IN.scalar = Type:Float() + OUT.vec = Type:Vec3f() + end + function run(IN,OUT) + OUT.vec = { IN.scalar, IN.scalar, IN.scalar } + end + )"; + }; + + TEST_F(ALogicEngine_Animations, ScriptsControllingAnimationsLinkedToScene_UsingTimerNodeWithUserProvidedTicker) + { + const auto scriptSrc = R"( + function init() + GLOBAL.startTick = 0 + end + + function interface(IN,OUT) + IN.ticker = Type:Int64() + IN.anim1Duration = Type:Float() + IN.anim2Duration = Type:Float() + + OUT.anim1Progress = Type:Float() + OUT.anim2Progress = Type:Float() + end + + function run(IN,OUT) + if GLOBAL.startTick == 0 then + GLOBAL.startTick = IN.ticker + end + + local elapsedTime = IN.ticker - GLOBAL.startTick + -- ticker from TimerNode is in microseconds, our animation timestamps are in seconds, conversion is needed + elapsedTime = elapsedTime / 1000000 + + -- play anim1 right away + OUT.anim1Progress = elapsedTime / IN.anim1Duration + -- play anim2 after anim1 + OUT.anim2Progress = (elapsedTime - IN.anim1Duration) / IN.anim2Duration + end + )"; + + const auto script = m_logicEngine.createLuaScript(scriptSrc); + const auto scriptScalarToVec1 = m_logicEngine.createLuaScript(ScriptScalarToVecSrc); + const auto scriptScalarToVec2 = m_logicEngine.createLuaScript(ScriptScalarToVecSrc); + + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node); + + script->getInputs()->getChild("anim1Duration")->set(*m_animation1->getOutputs()->getChild("duration")->get()); + script->getInputs()->getChild("anim2Duration")->set(*m_animation2->getOutputs()->getChild("duration")->get()); + + // controlScript -> anim1/channel -> scalarToVec -> nodeRotation + // -> anim2/channel -> scalarToVec -> nodeScaling + m_logicEngine.link(*script->getOutputs()->getChild("anim1Progress"), *m_animation1->getInputs()->getChild("progress")); + m_logicEngine.link(*script->getOutputs()->getChild("anim2Progress"), *m_animation2->getInputs()->getChild("progress")); + // link anim outputs to node bindings via helper to convert scalar to vec3 + m_logicEngine.link(*m_animation1->getOutputs()->getChild("channel"), *scriptScalarToVec1->getInputs()->getChild("scalar")); + m_logicEngine.link(*m_animation2->getOutputs()->getChild("channel"), *scriptScalarToVec2->getInputs()->getChild("scalar")); + m_logicEngine.link(*scriptScalarToVec1->getOutputs()->getChild("vec"), *nodeBinding->getInputs()->getChild("translation")); + m_logicEngine.link(*scriptScalarToVec2->getOutputs()->getChild("vec"), *nodeBinding->getInputs()->getChild("rotation")); + + // link timer + m_logicEngine.link(*m_timer->getOutputs()->getChild("ticker_us"), *script->getInputs()->getChild("ticker")); + + setTickerAndUpdate(1); // don't use 0 because it triggers timer auto generated time + expectNodeValues(0.f, 0.f); + + setTickerAndUpdate(100000); + expectNodeValues(0.1f, 0.f); + + setTickerAndUpdate(500000); + expectNodeValues(0.5f, 0.f); + + setTickerAndUpdate(1000000); + expectNodeValues(1.f, 0.f); + + // anim2 plays when anim1 finished + setTickerAndUpdate(1100000); + expectNodeValues(1.f, 0.1f); + + setTickerAndUpdate(1500000); + expectNodeValues(1.f, 0.5f); + + setTickerAndUpdate(2000000); + expectNodeValues(1.f, 1.f); + + setTickerAndUpdate(9999999); + expectNodeValues(1.f, 1.f); + } + + TEST_F(ALogicEngine_Animations, AnimationProgressesWhenUsingTimerWithAutogeneratedTicker) + { + const auto scriptSrc = R"( + function init() + GLOBAL.startTick = 0 + end + + function interface(IN,OUT) + IN.ticker = Type:Int64() + IN.anim1Duration = Type:Float() + IN.anim2Duration = Type:Float() + + OUT.anim1Progress = Type:Float() + OUT.anim2Progress = Type:Float() + end + + function run(IN,OUT) + if GLOBAL.startTick == 0 then + GLOBAL.startTick = IN.ticker + end + + local elapsedTime = IN.ticker - GLOBAL.startTick + -- ticker from TimerNode is in microseconds, our animation timestamps are in seconds, conversion is needed + elapsedTime = elapsedTime / 1000000 + + -- play both animations right away + OUT.anim1Progress = elapsedTime / IN.anim1Duration + OUT.anim2Progress = elapsedTime / IN.anim2Duration + end + )"; + + const auto script = m_logicEngine.createLuaScript(scriptSrc); + const auto scriptScalarToVec1 = m_logicEngine.createLuaScript(ScriptScalarToVecSrc); + const auto scriptScalarToVec2 = m_logicEngine.createLuaScript(ScriptScalarToVecSrc); + + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node); + + script->getInputs()->getChild("anim1Duration")->set(*m_animation1->getOutputs()->getChild("duration")->get()); + script->getInputs()->getChild("anim2Duration")->set(*m_animation2->getOutputs()->getChild("duration")->get()); + + // controlScript -> anim1/channel -> scalarToVec -> nodeRotation + // -> anim2/channel -> scalarToVec -> nodeScaling + m_logicEngine.link(*script->getOutputs()->getChild("anim1Progress"), *m_animation1->getInputs()->getChild("progress")); + m_logicEngine.link(*script->getOutputs()->getChild("anim2Progress"), *m_animation2->getInputs()->getChild("progress")); + // link anim outputs to node bindings via helper to convert scalar to vec3 + m_logicEngine.link(*m_animation1->getOutputs()->getChild("channel"), *scriptScalarToVec1->getInputs()->getChild("scalar")); + m_logicEngine.link(*m_animation2->getOutputs()->getChild("channel"), *scriptScalarToVec2->getInputs()->getChild("scalar")); + m_logicEngine.link(*scriptScalarToVec1->getOutputs()->getChild("vec"), *nodeBinding->getInputs()->getChild("translation")); + m_logicEngine.link(*scriptScalarToVec2->getOutputs()->getChild("vec"), *nodeBinding->getInputs()->getChild("rotation")); + + // link timer + m_logicEngine.link(*m_timer->getOutputs()->getChild("ticker_us"), *script->getInputs()->getChild("ticker")); + + setTickerAndUpdate(0); + expectNodeValues(0.f, 0.f); + + // loop for at most 2 secs + // break when one of animated outputs reaches 0.1 sec progress + for (int i = 0; i < 100; ++i) + { + std::this_thread::sleep_for(std::chrono::milliseconds{ 20 }); + m_logicEngine.update(); + + vec3f vals; + m_node->getTranslation(vals); + if (vals[0] >= 0.1f) + break; + } + + vec3f vals; + m_node->getTranslation(vals); + EXPECT_GE(vals[0], 0.1f); + EXPECT_GE(vals[1], 0.1f); + EXPECT_GE(vals[2], 0.1f); + m_node->getRotation(vals); + EXPECT_GE(vals[0], 0.1f); + EXPECT_GE(vals[1], 0.1f); + EXPECT_GE(vals[2], 0.1f); + } + + TEST_F(ALogicEngine_Animations, ScriptsControllingAnimationsLinkedToScene_WithWeakLinkedDuration) + { + const auto scriptSrc = R"( + function init() + GLOBAL.startTick = 0 + end + + function interface(IN,OUT) + IN.ticker = Type:Int64() + IN.animDuration = Type:Float() + + OUT.animProgress = Type:Float() + end + + function run(IN,OUT) + if GLOBAL.startTick == 0 then + GLOBAL.startTick = IN.ticker + end + + local elapsedTime = IN.ticker - GLOBAL.startTick + -- ticker from TimerNode is in microseconds, our animation timestamps are in seconds, conversion is needed + elapsedTime = elapsedTime / 1000000 + + OUT.animProgress = elapsedTime / IN.animDuration + end + )"; + + const auto script = m_logicEngine.createLuaScript(scriptSrc); + const auto scriptScalarToVec = m_logicEngine.createLuaScript(ScriptScalarToVecSrc); + + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node); + + // weak link duration - weak because it goes against the data flow direction + m_logicEngine.linkWeak(*m_animation1->getOutputs()->getChild("duration"), *script->getInputs()->getChild("animDuration")); + + // link animation to control script and to destination node property + m_logicEngine.link(*script->getOutputs()->getChild("animProgress"), *m_animation1->getInputs()->getChild("progress")); + m_logicEngine.link(*m_animation1->getOutputs()->getChild("channel"), *scriptScalarToVec->getInputs()->getChild("scalar")); + m_logicEngine.link(*scriptScalarToVec->getOutputs()->getChild("vec"), *nodeBinding->getInputs()->getChild("translation")); + + // link timer + m_logicEngine.link(*m_timer->getOutputs()->getChild("ticker_us"), *script->getInputs()->getChild("ticker")); + + // weak link limitation is undefined value during 1st update, we need to add extra update before expecting valid output + setTickerAndUpdate(1); // don't use 0 because it triggers timer auto generated time + + // check begin-middle-end + setTickerAndUpdate(1); // don't use 0 because it triggers timer auto generated time + expectNodeValues(0.f, 0.f); + setTickerAndUpdate(500000); + expectNodeValues(0.5f, 0.f); + setTickerAndUpdate(1000000); + expectNodeValues(1.f, 0.f); + } +} diff --git a/client/logic/unittests/api/LogicEngineTest_Compatibility.cpp b/client/logic/unittests/api/LogicEngineTest_Compatibility.cpp new file mode 100644 index 000000000..84044ca47 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_Compatibility.cpp @@ -0,0 +1,450 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "RamsesTestUtils.h" +#include "WithTempDirectory.h" +#include "FeatureLevelTestValues.h" +#include "PropertyLinkTestUtils.h" + +#include "ramses-logic/RamsesLogicVersion.h" +#include "ramses-logic/Property.h" + +#include "ramses-client-api/EffectDescription.h" +#include "ramses-client-api/Effect.h" +#include "ramses-client-api/Scene.h" +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-framework-api/RamsesVersion.h" + +#include "internals/ApiObjects.h" +#include "internals/FileUtils.h" + +#include "generated/LogicEngineGen.h" +#include "fmt/format.h" + +#include + +namespace ramses::internal +{ + class ALogicEngine_Compatibility : public ALogicEngineBase, public ::testing::TestWithParam + { + public: + ALogicEngine_Compatibility() : ALogicEngineBase{ GetParam() } + { + } + + protected: + static const char* GetFileIdentifier() + { + return rlogic_serialization::LogicEngineIdentifier(); + } + + void createFlatLogicEngineData( + ramses::RamsesVersion ramsesVersion, + ramses::RamsesLogicVersion logicVersion, + const char* fileId = GetFileIdentifier(), + ramses::EFeatureLevel featureLevel = GetParam()) + { + ApiObjects emptyApiObjects{ featureLevel }; + + auto logicEngine = rlogic_serialization::CreateLogicEngine( + m_fbBuilder, + rlogic_serialization::CreateVersion(m_fbBuilder, + ramsesVersion.major, ramsesVersion.minor, ramsesVersion.patch, m_fbBuilder.CreateString(ramsesVersion.string)), + rlogic_serialization::CreateVersion(m_fbBuilder, + logicVersion.major, logicVersion.minor, logicVersion.patch, m_fbBuilder.CreateString(logicVersion.string)), + ApiObjects::Serialize(emptyApiObjects, m_fbBuilder, ELuaSavingMode::ByteCodeOnly), + 0, + featureLevel + ); + + m_fbBuilder.Finish(logicEngine, fileId); + } + + static ramses::RamsesVersion FakeRamsesVersion() + { + ramses::RamsesVersion version{ + "10.20.900-suffix", + 10, + 20, + 900 + }; + return version; + } + + flatbuffers::FlatBufferBuilder m_fbBuilder; + WithTempDirectory m_tempDir; + }; + + INSTANTIATE_TEST_SUITE_P( + ALogicEngine_CompatibilityTests, + ALogicEngine_Compatibility, + GetFeatureLevelTestValues()); + + TEST_P(ALogicEngine_Compatibility, CreatesLogicEngineWithFeatureLevel) + { + LogicEngine logicEngine{ GetParam() }; + EXPECT_EQ(GetParam(), logicEngine.getFeatureLevel()); + } + + TEST_P(ALogicEngine_Compatibility, FallsBackToFeatureLevel01IfUnknownFeatureLevelRequested) + { + LogicEngine logicEngine{ static_cast(999) }; + EXPECT_EQ(ramses::EFeatureLevel_01, logicEngine.getFeatureLevel()); + } + + TEST_P(ALogicEngine_Compatibility, ProducesErrorIfDeserilizedFromFileReferencingIncompatibleRamsesVersion) + { + createFlatLogicEngineData(FakeRamsesVersion(), GetRamsesLogicVersion()); + + ASSERT_TRUE(FileUtils::SaveBinary("wrong_ramses_version.bin", m_fbBuilder.GetBufferPointer(), m_fbBuilder.GetSize())); + + EXPECT_FALSE(m_logicEngine.loadFromFile("wrong_ramses_version.bin")); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_THAT(errors[0].message, ::testing::HasSubstr("Version mismatch while loading file 'wrong_ramses_version.bin' (size: ")); + EXPECT_THAT(errors[0].message, ::testing::HasSubstr(fmt::format("Expected Ramses version {}.x.x but found 10.20.900-suffix", ramses::GetRamsesVersion().major))); + + //Also test with buffer version of the API + EXPECT_FALSE(m_logicEngine.loadFromBuffer(m_fbBuilder.GetBufferPointer(), m_fbBuilder.GetSize())); + errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_THAT(errors[0].message, ::testing::HasSubstr("Version mismatch while loading data buffer")); + EXPECT_THAT(errors[0].message, ::testing::HasSubstr(fmt::format("Expected Ramses version {}.x.x but found 10.20.900-suffix", ramses::GetRamsesVersion().major))); + } + + TEST_P(ALogicEngine_Compatibility, ProducesErrorIfDeserilizedFromDifferentTypeOfFile) + { + const char* badFileIdentifier = "xyWW"; + createFlatLogicEngineData(ramses::GetRamsesVersion(), GetRamsesLogicVersion(), badFileIdentifier); + + ASSERT_TRUE(FileUtils::SaveBinary("temp.bin", m_fbBuilder.GetBufferPointer(), m_fbBuilder.GetSize())); + + EXPECT_FALSE(m_logicEngine.loadFromFile("temp.bin")); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_THAT(errors[0].message, ::testing::HasSubstr(fmt::format("Tried loading a binary data which doesn't store Ramses Logic content! Expected file bytes 4-5 to be 'rl', but found 'xy' instead"))); + } + + TEST_P(ALogicEngine_Compatibility, ProducesErrorIfDeserilizedFromIncompatibleFileVersion) + { + // Format was changed + const char* versionFromFuture = "rl99"; + createFlatLogicEngineData(ramses::GetRamsesVersion(), GetRamsesLogicVersion(), versionFromFuture); + + ASSERT_TRUE(FileUtils::SaveBinary("temp.bin", m_fbBuilder.GetBufferPointer(), m_fbBuilder.GetSize())); + + EXPECT_FALSE(m_logicEngine.loadFromFile("temp.bin")); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_THAT(errors[0].message, ::testing::HasSubstr(fmt::format("Version mismatch while loading binary data! Expected version '28', but found '99'"))); + } + + TEST_P(ALogicEngine_Compatibility, CanDeserializeSameFeatureLevelVersion) + { + const ramses::EFeatureLevel featureLevel = GetParam(); + createFlatLogicEngineData(ramses::GetRamsesVersion(), GetRamsesLogicVersion(), GetFileIdentifier(), featureLevel); + ASSERT_TRUE(FileUtils::SaveBinary("temp.bin", m_fbBuilder.GetBufferPointer(), m_fbBuilder.GetSize())); + + LogicEngine logicEngine(featureLevel); + EXPECT_TRUE(logicEngine.loadFromFile("temp.bin")); + EXPECT_TRUE(logicEngine.getErrors().empty()); + } + + TEST_P(ALogicEngine_Compatibility, ProducesErrorIfDeserializedFromIncompatibleFeatureLevelVersion) + { + // this test is not parametrized + if (GetParam() != ramses::EFeatureLevel_01) + GTEST_SKIP(); + + // test all mismatching combinations + for (ramses::EFeatureLevel fileFeatureLevel : ramses::AllFeatureLevels) + { + for (ramses::EFeatureLevel engineFeatureLevel : ramses::AllFeatureLevels) + { + if (fileFeatureLevel == engineFeatureLevel) + continue; + + // note - use file identifier matching engine otherwise load fails already when checking file identifier (tested above) + createFlatLogicEngineData(ramses::GetRamsesVersion(), GetRamsesLogicVersion(), GetFileIdentifier(), fileFeatureLevel); + ASSERT_TRUE(FileUtils::SaveBinary("temp.bin", m_fbBuilder.GetBufferPointer(), m_fbBuilder.GetSize())); + + LogicEngine logicEngine{ engineFeatureLevel }; + EXPECT_FALSE(logicEngine.loadFromFile("temp.bin")); + const auto& errors = logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_THAT(errors[0].message, ::testing::HasSubstr(fmt::format("Feature level mismatch while loading file 'temp.bin' (size:"))); + EXPECT_THAT(errors[0].message, ::testing::HasSubstr(fmt::format("Loaded file with feature level {} but LogicEngine was instantiated with feature level {}", fileFeatureLevel, engineFeatureLevel))); + } + } + } + + TEST_P(ALogicEngine_Compatibility, CanParseFeatureLevelFromFile) + { + const ramses::EFeatureLevel featureLevel = GetParam(); + createFlatLogicEngineData(ramses::GetRamsesVersion(), GetRamsesLogicVersion(), GetFileIdentifier(), featureLevel); + ASSERT_TRUE(FileUtils::SaveBinary("temp.bin", m_fbBuilder.GetBufferPointer(), m_fbBuilder.GetSize())); + + ramses::EFeatureLevel detectedFeatureLevel = ramses::EFeatureLevel_01; + ASSERT_TRUE(LogicEngine::GetFeatureLevelFromFile("temp.bin", detectedFeatureLevel)); + EXPECT_EQ(featureLevel, detectedFeatureLevel); + } + + TEST_P(ALogicEngine_Compatibility, FailsToParseFeatureLevelFromNotExistingFile) + { + ramses::EFeatureLevel detectedFeatureLevel = ramses::EFeatureLevel_01; + EXPECT_FALSE(LogicEngine::GetFeatureLevelFromFile("doesntexist", detectedFeatureLevel)); + } + + TEST_P(ALogicEngine_Compatibility, FailsToParseFeatureLevelFromCorruptedFile) + { + const std::string invalidData{"invaliddata"}; + ASSERT_TRUE(FileUtils::SaveBinary("temp.bin", invalidData.data(), invalidData.size())); + + ramses::EFeatureLevel detectedFeatureLevel = ramses::EFeatureLevel_01; + EXPECT_FALSE(LogicEngine::GetFeatureLevelFromFile("temp.bin", detectedFeatureLevel)); + } + + TEST_P(ALogicEngine_Compatibility, FailsToParseFeatureLevelFromValidFileButUnknownFeatureLevel) + { + createFlatLogicEngineData(ramses::GetRamsesVersion(), GetRamsesLogicVersion(), GetFileIdentifier(), static_cast(999)); + ASSERT_TRUE(FileUtils::SaveBinary("temp.bin", m_fbBuilder.GetBufferPointer(), m_fbBuilder.GetSize())); + + ramses::EFeatureLevel detectedFeatureLevel = ramses::EFeatureLevel_01; + EXPECT_FALSE(LogicEngine::GetFeatureLevelFromFile("temp.bin", detectedFeatureLevel)); + } + + TEST_P(ALogicEngine_Compatibility, CanParseFeatureLevelFromBuffer) + { + const ramses::EFeatureLevel featureLevel = GetParam(); + createFlatLogicEngineData(ramses::GetRamsesVersion(), GetRamsesLogicVersion(), GetFileIdentifier(), featureLevel); + ramses::EFeatureLevel detectedFeatureLevel = ramses::EFeatureLevel_01; + ASSERT_TRUE(LogicEngine::GetFeatureLevelFromBuffer("temp.bin", m_fbBuilder.GetBufferPointer(), m_fbBuilder.GetSize(), detectedFeatureLevel)); + EXPECT_EQ(featureLevel, detectedFeatureLevel); + } + + // These tests will always break on incompatible file format changes. + class ALogicEngine_Binary_Compatibility : public ::testing::Test + { + protected: + static void checkBaseContents(LogicEngine& logicEngine, ramses::Scene& ramsesScene) + { + ASSERT_NE(nullptr, logicEngine.findByName("nestedModuleMath")); + ASSERT_NE(nullptr, logicEngine.findByName("moduleMath")); + ASSERT_NE(nullptr, logicEngine.findByName("moduleTypes")); + const auto script1 = logicEngine.findByName("script1"); + ASSERT_NE(nullptr, script1); + EXPECT_NE(nullptr, script1->getInputs()->getChild("intInput")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("int64Input")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("vec2iInput")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("vec3iInput")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("vec4iInput")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("floatInput")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("vec2fInput")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("vec3fInput")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("vec4fInput")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("boolInput")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("stringInput")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("structInput")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("arrayInput")); + EXPECT_NE(nullptr, script1->getOutputs()->getChild("floatOutput")); + EXPECT_NE(nullptr, script1->getOutputs()->getChild("nodeTranslation")); + EXPECT_NE(nullptr, script1->getInputs()->getChild("floatInput")); + const auto script2 = logicEngine.findByName("script2"); + ASSERT_NE(nullptr, script2); + EXPECT_NE(nullptr, script2->getInputs()->getChild("floatInput")); + EXPECT_NE(nullptr, script2->getOutputs()->getChild("cameraViewport")->getChild("offsetX")); + EXPECT_NE(nullptr, script2->getOutputs()->getChild("cameraViewport")->getChild("offsetY")); + EXPECT_NE(nullptr, script2->getOutputs()->getChild("cameraViewport")->getChild("width")); + EXPECT_NE(nullptr, script2->getOutputs()->getChild("cameraViewport")->getChild("height")); + EXPECT_NE(nullptr, script2->getOutputs()->getChild("floatUniform")); + const auto animNode = logicEngine.findByName("animNode"); + ASSERT_NE(nullptr, animNode); + EXPECT_EQ(1u, animNode->getInputs()->getChildCount()); + ASSERT_EQ(2u, animNode->getOutputs()->getChildCount()); + EXPECT_NE(nullptr, animNode->getOutputs()->getChild("channel")); + ASSERT_NE(nullptr, logicEngine.findByName("animNodeWithDataProperties")); + EXPECT_EQ(2u, logicEngine.findByName("animNodeWithDataProperties")->getInputs()->getChildCount()); + EXPECT_NE(nullptr, logicEngine.findByName("timerNode")); + + const auto nodeBinding = logicEngine.findByName("nodebinding"); + ASSERT_NE(nullptr, nodeBinding); + EXPECT_NE(nullptr, nodeBinding->getInputs()->getChild("enabled")); + EXPECT_TRUE(logicEngine.isLinked(*nodeBinding)); + const auto cameraBinding = logicEngine.findByName("camerabinding"); + ASSERT_NE(nullptr, cameraBinding); + const auto appearanceBinding = logicEngine.findByName("appearancebinding"); + ASSERT_NE(nullptr, appearanceBinding); + EXPECT_NE(nullptr, logicEngine.findByName("dataarray")); + + std::vector expectedLinks; + + const auto intf = logicEngine.findByName("intf"); + ASSERT_NE(nullptr, intf); + intf->getInputs()->getChild("struct")->getChild("floatInput")->set(42.5f); + expectedLinks.push_back({ intf->getOutputs()->getChild("struct")->getChild("floatInput"), script1->getInputs()->getChild("floatInput"), false }); + expectedLinks.push_back({ script1->getOutputs()->getChild("boolOutput"), nodeBinding->getInputs()->getChild("enabled"), false }); + expectedLinks.push_back({ script1->getOutputs()->getChild("floatOutput"), script2->getInputs()->getChild("floatInput"), false }); + expectedLinks.push_back({ script1->getOutputs()->getChild("nodeTranslation"), nodeBinding->getInputs()->getChild("translation"), false }); + expectedLinks.push_back({ script2->getOutputs()->getChild("cameraViewport")->getChild("offsetX"), cameraBinding->getInputs()->getChild("viewport")->getChild("offsetX"), false }); + expectedLinks.push_back({ script2->getOutputs()->getChild("cameraViewport")->getChild("offsetY"), cameraBinding->getInputs()->getChild("viewport")->getChild("offsetY"), false }); + expectedLinks.push_back({ script2->getOutputs()->getChild("cameraViewport")->getChild("width"), cameraBinding->getInputs()->getChild("viewport")->getChild("width"), false }); + expectedLinks.push_back({ script2->getOutputs()->getChild("cameraViewport")->getChild("height"), cameraBinding->getInputs()->getChild("viewport")->getChild("height"), false }); + expectedLinks.push_back({ script2->getOutputs()->getChild("floatUniform"), appearanceBinding->getInputs()->getChild("floatUniform"), false }); + expectedLinks.push_back({ animNode->getOutputs()->getChild("channel"), appearanceBinding->getInputs()->getChild("animatedFloatUniform"), false }); + + const auto triLogicIntf = logicEngine.findByName("Interface_CameraCrane"); + ASSERT_TRUE(triLogicIntf); + const auto triLogicScript = logicEngine.findByName("CameraCrane"); + ASSERT_TRUE(triLogicScript); + const auto triCamNode = logicEngine.findByName("triangleCamNodeBinding"); + ASSERT_TRUE(triCamNode); + const auto triCamBinding = logicEngine.findByName("triangleCamBinding"); + ASSERT_TRUE(triCamBinding); + expectedLinks.push_back({ triLogicIntf->getOutputs()->getChild("CraneGimbal")->getChild("Yaw"), triLogicScript->getInputs()->getChild("yaw"), false }); + expectedLinks.push_back({ triLogicIntf->getOutputs()->getChild("CraneGimbal")->getChild("Pitch"), triLogicScript->getInputs()->getChild("pitch"), false }); + expectedLinks.push_back({ triLogicIntf->getOutputs()->getChild("Viewport")->getChild("Width"), triCamBinding->getInputs()->getChild("viewport")->getChild("width"), false }); + expectedLinks.push_back({ triLogicIntf->getOutputs()->getChild("Viewport")->getChild("Height"), triCamBinding->getInputs()->getChild("viewport")->getChild("height"), false }); + expectedLinks.push_back({ triLogicScript->getOutputs()->getChild("translation"), triCamNode->getInputs()->getChild("translation"), false }); + + PropertyLinkTestUtils::ExpectLinks(logicEngine, expectedLinks); + + EXPECT_TRUE(logicEngine.update()); + + // Values on Ramses are updated according to expectations + vec3f translation; + auto node = ramses::RamsesUtils::TryConvert(*ramsesScene.findObjectByName("test node")); + auto camera = ramses::RamsesUtils::TryConvert(*ramsesScene.findObjectByName("test camera")); + node->getTranslation(translation); + EXPECT_EQ(translation, vec3f(42.5f, 2.f, 3.f)); + // test that linked value from script propagated to ramses scene + EXPECT_EQ(ramses::EVisibilityMode::Off, node->getVisibility()); + + EXPECT_EQ(camera->getViewportX(), 45); + EXPECT_EQ(camera->getViewportY(), 47); + EXPECT_EQ(camera->getViewportWidth(), 143u); + EXPECT_EQ(camera->getViewportHeight(), 243u); + + // Animation node is linked and can be animated + EXPECT_FLOAT_EQ(2.f, *animNode->getOutputs()->getChild("duration")->get()); + animNode->getInputs()->getChild("progress")->set(0.75f); + EXPECT_TRUE(logicEngine.update()); + + ramses::UniformInput uniform; + auto appearance = ramses::RamsesUtils::TryConvert(*ramsesScene.findObjectByName("test appearance")); + appearance->getEffect().getUniformInput(1, uniform); + float floatValue = 0.f; + appearance->getInputValue(uniform, floatValue); + EXPECT_FLOAT_EQ(1.5f, floatValue); + + EXPECT_EQ(957, *logicEngine.findByName("script2")->getOutputs()->getChild("nestedModulesResult")->get()); + + EXPECT_TRUE(logicEngine.findByName("renderpassbinding")); + EXPECT_TRUE(logicEngine.findByName("anchorpoint")); + + const auto cameraBindingPersp = logicEngine.findByName("camerabindingPersp"); + const auto cameraBindingPerspWithFrustumPlanes = logicEngine.findByName("camerabindingPerspWithFrustumPlanes"); + ASSERT_TRUE(cameraBindingPersp && cameraBindingPerspWithFrustumPlanes); + EXPECT_EQ(4u, cameraBindingPersp->getInputs()->getChild("frustum")->getChildCount()); + EXPECT_EQ(6u, cameraBindingPerspWithFrustumPlanes->getInputs()->getChild("frustum")->getChildCount()); + + EXPECT_TRUE(logicEngine.findByName("rendergroupbinding")); + EXPECT_TRUE(logicEngine.findByName("skin")); + const auto dataArray = logicEngine.findByName("dataarrayOfArrays"); + ASSERT_TRUE(dataArray); + EXPECT_EQ(EPropertyType::Array, dataArray->getDataType()); + EXPECT_EQ(2u, dataArray->getNumElements()); + const auto data = dataArray->getData>(); + ASSERT_TRUE(data); + const std::vector> expectedData{ { 1.f, 2.f, 3.f, 4.f, 5.f }, { 6.f, 7.f, 8.f, 9.f, 10.f } }; + EXPECT_EQ(expectedData, *data); + EXPECT_TRUE(logicEngine.findByName("meshnodebinding")); + } + + static void expectFeatureLevel02Content(const LogicEngine& /*logicEngine*/, ramses::Scene& /*ramsesScene*/) + { + // features added in future feature level expected to be present + } + + static void expectFeatureLevel02ContentNotPresent(const LogicEngine& /*logicEngine*/) + { + // features added in future feature level expected NOT to be present in previous feature levels + } + + static void checkContents(LogicEngine& logicEngine, ramses::Scene& scene) + { + // check for content expected to exist + // higher feature level always contains content supported by lower level + switch (logicEngine.getFeatureLevel()) + { + //case ramses::EFeatureLevel_02: + // expectFeatureLevel02Content(logicEngine, scene); + // [[fallthrough]]; + case ramses::EFeatureLevel_01: + checkBaseContents(logicEngine, scene); + break; + } + + // check for content expected to not exist + // lower feature level never contains content supported only in higher level + switch (logicEngine.getFeatureLevel()) + { + case ramses::EFeatureLevel_01: + expectFeatureLevel02ContentNotPresent(logicEngine); + // [[fallthrough]]; + //case EFeatureLevel_02: + // break; + } + } + + static void saveAndReloadAndCheckContents(LogicEngine& logicEngine, ramses::Scene& scene) + { + WithTempDirectory tempDir; + + ramses::SaveFileConfig noValidationConfig; + noValidationConfig.setValidationEnabled(false); + logicEngine.saveToFile("temp.rlogic", noValidationConfig); + + LogicEngine logicEngineAnother{ logicEngine.getFeatureLevel() }; + ASSERT_TRUE(logicEngineAnother.loadFromFile("temp.rlogic", &scene)); + EXPECT_TRUE(logicEngineAnother.update()); + + checkContents(logicEngineAnother, scene); + } + + ramses::Scene* loadRamsesScene(ramses::EFeatureLevel featureLevel) + { + switch(featureLevel) + { + case ramses::EFeatureLevel_01: + return &m_ramses.loadSceneFromFile("res/testScene_01.ramses"); + } + return nullptr; + } + + RamsesTestSetup m_ramses; + }; + + TEST_F(ALogicEngine_Binary_Compatibility, CanLoadAndUpdateABinaryFileExportedWithLastCompatibleVersionOfEngine_FeatureLevel01) + { + ramses::EFeatureLevel featureLevel = ramses::EFeatureLevel_Latest; + EXPECT_TRUE(LogicEngine::GetFeatureLevelFromFile("res/testLogic_01.rlogic", featureLevel)); + EXPECT_EQ(ramses::EFeatureLevel_01, featureLevel); + + ramses::Scene* scene = loadRamsesScene(ramses::EFeatureLevel_01); + ASSERT_TRUE(scene); + LogicEngine logicEngine{ featureLevel }; + ASSERT_TRUE(logicEngine.loadFromFile("res/testLogic_01.rlogic", scene)); + EXPECT_TRUE(logicEngine.update()); + + checkContents(logicEngine, *scene); + saveAndReloadAndCheckContents(logicEngine, *scene); + } +} diff --git a/client/logic/unittests/api/LogicEngineTest_DependencyExtraction.cpp b/client/logic/unittests/api/LogicEngineTest_DependencyExtraction.cpp new file mode 100644 index 000000000..865d24b87 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_DependencyExtraction.cpp @@ -0,0 +1,198 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +namespace ramses +{ + class ALogicEngine_DependencyExtraction : public ALogicEngine + { + protected: + void extractAndExpect(std::string_view src, const std::vector& expected) + { + std::vector extractedDependencies; + std::function callbackFunc = [&extractedDependencies](const std::string& dep) { + extractedDependencies.push_back(dep); + }; + + ASSERT_TRUE(m_logicEngine.extractLuaDependencies(src, callbackFunc)); + EXPECT_EQ(expected, extractedDependencies); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + } + + void extractAndExpectError(std::string_view src, std::string_view errorMsgSubstr) + { + std::function callbackFunc = [](const std::string& /*unused*/) {}; + EXPECT_FALSE(m_logicEngine.extractLuaDependencies(src, callbackFunc)); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr(errorMsgSubstr)); + } + }; + + TEST_F(ALogicEngine_DependencyExtraction, extractsModules) + { + constexpr std::string_view src = R"( + modules("foo", "bar") + function interface(IN,OUT) + bla.bla = Type:Int32() + end + function run(IN,OUT) + bla.bla = bla + end + )"; + + extractAndExpect(src, { "foo", "bar" }); + } + + TEST_F(ALogicEngine_DependencyExtraction, extractsEmptyModulesIfModulesNotSpecifiedInSource) + { + constexpr std::string_view src = R"( + function interface(IN,OUT) + bla.bla = Type:Int32() + end + function run(IN,OUT) + bla.bla = bla + end + )"; + + extractAndExpect(src, {}); + } + + TEST_F(ALogicEngine_DependencyExtraction, extractsEmptyModulesIfModulesSpecifiedInScriptButEmpty) + { + constexpr std::string_view src = R"( + modules() + function interface(IN,OUT) + bla.bla = Type:Int32() + end + function run(IN,OUT) + bla.bla = bla + end + )"; + + extractAndExpect(src, {}); + } + + TEST_F(ALogicEngine_DependencyExtraction, failsExtractModulesIfWrongArguments) + { + constexpr std::string_view src = R"( + modules("foo", 5, "bar") + function interface(IN,OUT) + bla.bla = Type:Int32() + end + function run(IN,OUT) + bla.bla = bla + end + )"; + + extractAndExpectError(src, R"(Error while extracting module dependencies: argument 1 is of type 'number', string must be provided: ex. 'modules("moduleA", "moduleB")')"); + } + + TEST_F(ALogicEngine_DependencyExtraction, failsExtractModulesIfWrongSyntax) + { + constexpr std::string_view src = R"( + modules("foo", + function interface(IN,OUT) + bla.bla = Type:Int32() + end + function run(IN,OUT) + bla.bla = bla + end + )"; + + extractAndExpectError(src, "'(' expected near"); + } + + TEST_F(ALogicEngine_DependencyExtraction, extractsModulesEvenIfUnresolvedLabelUsedInGlobalSpace) + { + constexpr std::string_view src = R"( + modules("foo", "bar") + + foo.bla(bar.bla) -- ILLEGAL + + function interface(IN,OUT) + bla.bla = Type:Int32() + end + function run(IN,OUT) + bla.bla = bla + end + )"; + + extractAndExpect(src, { "foo", "bar" }); + } + + TEST_F(ALogicEngine_DependencyExtraction, extractsEmptyModulesIfUnresolvedLabelUsedBeforeModulesDeclaration) + { + constexpr std::string_view src = R"( + foo.bla(bar.bla) -- ILLEGAL + + modules("foo", "bar") -- modules should be declared before code + + function interface(IN,OUT) + bla.bla = Type:Int32() + end + function run(IN,OUT) + bla.bla = bla + end + )"; + + // will not fail but will not extract modules + extractAndExpect(src, {}); + } + + TEST_F(ALogicEngine_DependencyExtraction, failsExtractModulesIfDupliciteModulesUsed) + { + extractAndExpectError(R"(modules("foo", "foo"))", "Error while extracting module dependencies: 'foo' appears more than once in dependency list"); + extractAndExpectError(R"(modules("foo", "bar", "foo"))", "Error while extracting module dependencies: 'foo' appears more than once in dependency list"); + extractAndExpectError(R"(modules("bar", "foo", "duck", "foo", "pigeon"))", "Error while extracting module dependencies: 'foo' appears more than once in dependency list"); + } + + TEST_F(ALogicEngine_DependencyExtraction, failsExtractModulesIfModulesMoreThanOnceInExecutedCode) + { + constexpr std::string_view src = R"( + modules("foo", "bar") + function interface(IN,OUT) + bla.bla = Type:Int32() + end + modules("foo2", "bar2") + function run(IN,OUT) + bla.bla = bla + end + modules("foo3", "bar3") + )"; + + extractAndExpectError(src, "Error while extracting module dependencies: 'modules' function was executed more than once"); + } + + TEST_F(ALogicEngine_DependencyExtraction, extractsModulesFromModuleUsingAnotherModule) + { + constexpr std::string_view src = R"( + modules("foo") + local mymath = {} + function mymath.doSth(x) + return foo.compute(x) + end + mymath.pi = foo.sqrt(2) + return mymath + )"; + + extractAndExpect(src, { "foo" }); + } + + TEST_F(ALogicEngine_DependencyExtraction, extractsModulesFromLuaInterfaceSource) + { + constexpr std::string_view src = R"( + modules("foo", "bar") + function interface(IN) + IN.bla = foo.type + end + )"; + + extractAndExpect(src, { "foo", "bar" }); + } +} diff --git a/client/logic/unittests/api/LogicEngineTest_Dirtiness.cpp b/client/logic/unittests/api/LogicEngineTest_Dirtiness.cpp new file mode 100644 index 000000000..9f9da9a43 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_Dirtiness.cpp @@ -0,0 +1,569 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include + +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/Property.h" + +#include "LogicEngineTest_Base.h" +#include "impl/LogicEngineImpl.h" +#include "impl/LogicNodeImpl.h" +#include "internals/ApiObjects.h" + +#include "RamsesTestUtils.h" + +namespace ramses::internal +{ + class ALogicEngine_DirtinessBase : public ALogicEngineBase + { + protected: + ApiObjects& m_apiObjects = { m_logicEngine.m_impl->getApiObjects() }; + + const std::string_view m_minimal_script = R"( + function interface(IN,OUT) + IN.data = Type:Int32() + OUT.data = Type:Int32() + end + function run(IN,OUT) + OUT.data = IN.data + end + )"; + + const std::string_view m_nested_properties_script = R"( + function interface(IN,OUT) + IN.data = { + nested = Type:Int32() + } + OUT.data = { + nested = Type:Int32() + } + end + function run(IN,OUT) + OUT.data.nested = IN.data.nested + end + )"; + + const std::string_view m_valid_empty_interface = R"( + function interface(IN,OUT) + end + )"; + + const std::string_view m_minimal_interface = R"( + function interface(IN) + IN.data = Type:Int32() + end + )"; + }; + + class ALogicEngine_Dirtiness : public ALogicEngine_DirtinessBase, public ::testing::Test + { + }; + + TEST_F(ALogicEngine_Dirtiness, CreatedObjectsAreDirtyAfterCreating) + { + const auto obj1 = m_logicEngine.createLuaScript(m_valid_empty_script); + EXPECT_TRUE(obj1->m_impl.isDirty()); + const auto obj2 = m_logicEngine.createLuaInterface(m_valid_empty_interface, "iface name"); + EXPECT_TRUE(obj2->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, CreatedBindingsAreNotDirtyAfterCreating) + { + const auto obj3 = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + EXPECT_FALSE(obj3->m_impl.isDirty()); + const auto obj4 = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, ""); + EXPECT_FALSE(obj4->m_impl.isDirty()); + const auto obj5 = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, ""); + EXPECT_FALSE(obj5->m_impl.isDirty()); + const auto obj6 = createRenderGroupBinding(); + EXPECT_FALSE(obj6->m_impl.isDirty()); + const auto obj = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode); + EXPECT_FALSE(obj->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, DirtyAfterCreatingNodeBinding_AndChangingInput) + { + RamsesNodeBinding* nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + nodeBinding->getInputs()->getChild("scaling")->set(vec3f{1.5f, 1.f, 1.f}); + EXPECT_TRUE(nodeBinding->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, DirtyAfterCreatingAppearanceBinding_AndChangingInput) + { + RamsesAppearanceBinding* appBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, ""); + appBinding->getInputs()->getChild("floatUniform")->set(15.f); + EXPECT_TRUE(appBinding->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, DirtyAfterCreatingCameraBinding_AndChangingInput) + { + RamsesCameraBinding* camBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, ""); + camBinding->getInputs()->getChild("viewport")->getChild("width")->set(15); + EXPECT_TRUE(camBinding->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, DirtyAfterCreatingRenderPassBinding_AndChangingInput) + { + RamsesRenderPassBinding* binding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, ""); + binding->getInputs()->getChild("enabled")->set(false); + EXPECT_TRUE(binding->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, DirtyAfterCreatingRenderGroupBinding_AndChangingInput) + { + auto binding = createRenderGroupBinding(); + binding->getInputs()->getChild("renderOrders")->getChild("mesh")->set(42); + EXPECT_TRUE(binding->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, DirtyAfterCreatingMeshNodeBinding_AndChangingInput) + { + auto binding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode); + binding->getInputs()->getChild("vertexOffset")->set(42); + EXPECT_TRUE(binding->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, NotDirty_AfterCreatingObjectsAndCallingUpdate) + { + const auto obj1 = m_logicEngine.createLuaScript(m_valid_empty_script); + const auto obj2 = m_logicEngine.createLuaInterface(m_valid_empty_interface, "iface name"); + const auto obj3 = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + const auto obj4 = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, ""); + const auto obj5 = m_logicEngine.createRamsesCameraBinding(*m_camera, ""); + const auto obj6 = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, ""); + const auto obj7 = createRenderGroupBinding(); + const auto obj8 = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode); + + m_logicEngine.update(); + EXPECT_FALSE(obj1->m_impl.isDirty()); + EXPECT_FALSE(obj2->m_impl.isDirty()); + EXPECT_FALSE(obj3->m_impl.isDirty()); + EXPECT_FALSE(obj4->m_impl.isDirty()); + EXPECT_FALSE(obj5->m_impl.isDirty()); + EXPECT_FALSE(obj6->m_impl.isDirty()); + EXPECT_FALSE(obj7->m_impl.isDirty()); + EXPECT_FALSE(obj8->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, Dirty_AfterSettingScriptInput) + { + LuaScript* script = m_logicEngine.createLuaScript(m_minimal_script); + m_logicEngine.update(); + + script->getInputs()->getChild("data")->set(5); + + EXPECT_TRUE(script->m_impl.isDirty()); + m_logicEngine.update(); + EXPECT_FALSE(script->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, Dirty_AfterSettingNestedScriptInput) + { + LuaScript* script = m_logicEngine.createLuaScript(m_nested_properties_script); + m_logicEngine.update(); + + script->getInputs()->getChild("data")->getChild("nested")->set(5); + + EXPECT_TRUE(script->m_impl.isDirty()); + m_logicEngine.update(); + EXPECT_FALSE(script->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, Dirty_AfterSettingInterfaceInput) + { + LuaInterface* intf = m_logicEngine.createLuaInterface(m_minimal_interface, "iface name"); + m_logicEngine.update(); + + intf->getInputs()->getChild("data")->set(5); + + EXPECT_TRUE(intf->m_impl.isDirty()); + m_logicEngine.update(); + EXPECT_FALSE(intf->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, Dirty_WhenSettingBindingInputToDefaultValue) + { + RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + m_logicEngine.update(); + + // zeroes is the default value + binding->getInputs()->getChild("translation")->set({0, 0, 0}); + EXPECT_TRUE(binding->m_impl.isDirty()); + m_logicEngine.update(); + + // Set different value, and then set again + binding->getInputs()->getChild("translation")->set({1, 2, 3}); + m_logicEngine.update(); + binding->getInputs()->getChild("translation")->set({1, 2, 3}); + EXPECT_TRUE(binding->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, Dirty_WhenSettingBindingInputToDifferentValue) + { + RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + m_logicEngine.update(); + + // Set non-default value, and then set again to different value + binding->getInputs()->getChild("translation")->set({ 1, 2, 3 }); + m_logicEngine.update(); + EXPECT_FALSE(binding->m_impl.isDirty()); + binding->getInputs()->getChild("translation")->set({ 11, 12, 13 }); + EXPECT_TRUE(binding->m_impl.isDirty()); + } + + class ALogicEngine_DirtinessViaLink : public ALogicEngine_DirtinessBase, public ::testing::TestWithParam + { + protected: + void link(const Property& src, const Property& dst) + { + if (GetParam()) + { + EXPECT_TRUE(m_logicEngine.linkWeak(src, dst)); + } + else + { + EXPECT_TRUE(m_logicEngine.link(src, dst)); + } + } + }; + + INSTANTIATE_TEST_SUITE_P( + ALogicEngine_DirtinessViaLink_TestInstances, + ALogicEngine_DirtinessViaLink, + ::testing::Values(false, true)); + + TEST_P(ALogicEngine_DirtinessViaLink, Dirty_WhenAddingLink) + { + LuaScript* script1 = m_logicEngine.createLuaScript(m_minimal_script); + LuaScript* script2 = m_logicEngine.createLuaScript(m_minimal_script); + m_logicEngine.update(); + + link(*script1->getOutputs()->getChild("data"), *script2->getInputs()->getChild("data")); + EXPECT_TRUE(script1->m_impl.isDirty()); + EXPECT_TRUE(script2->m_impl.isDirty()); + m_logicEngine.update(); + EXPECT_FALSE(script1->m_impl.isDirty()); + EXPECT_FALSE(script2->m_impl.isDirty()); + } + + TEST_P(ALogicEngine_DirtinessViaLink, NotDirty_WhenRemovingLink) + { + LuaScript* script1 = m_logicEngine.createLuaScript(m_minimal_script); + LuaScript* script2 = m_logicEngine.createLuaScript(m_minimal_script); + link(*script1->getOutputs()->getChild("data"), *script2->getInputs()->getChild("data")); + m_logicEngine.update(); + + EXPECT_FALSE(script1->m_impl.isDirty()); + EXPECT_FALSE(script2->m_impl.isDirty()); + m_logicEngine.unlink(*script1->getOutputs()->getChild("data"), *script2->getInputs()->getChild("data")); + + EXPECT_FALSE(script1->m_impl.isDirty()); + EXPECT_FALSE(script2->m_impl.isDirty()); + m_logicEngine.update(); + EXPECT_FALSE(script1->m_impl.isDirty()); + EXPECT_FALSE(script2->m_impl.isDirty()); + } + + TEST_P(ALogicEngine_DirtinessViaLink, NotDirty_WhenRemovingNestedLink) + { + LuaScript* script1 = m_logicEngine.createLuaScript(m_nested_properties_script); + LuaScript* script2 = m_logicEngine.createLuaScript(m_nested_properties_script); + link(*script1->getOutputs()->getChild("data")->getChild("nested"), *script2->getInputs()->getChild("data")->getChild("nested")); + m_logicEngine.update(); + + EXPECT_FALSE(script1->m_impl.isDirty()); + EXPECT_FALSE(script2->m_impl.isDirty()); + m_logicEngine.unlink(*script1->getOutputs()->getChild("data")->getChild("nested"), *script2->getInputs()->getChild("data")->getChild("nested")); + + EXPECT_FALSE(script1->m_impl.isDirty()); + EXPECT_FALSE(script2->m_impl.isDirty()); + m_logicEngine.update(); + EXPECT_FALSE(script1->m_impl.isDirty()); + EXPECT_FALSE(script2->m_impl.isDirty()); + } + + // Removing link does not mark things dirty, but setting value does + TEST_P(ALogicEngine_DirtinessViaLink, Dirty_WhenRemovingLink_AndSettingValueByCallingSetAfterwards) + { + LuaScript* script1 = m_logicEngine.createLuaScript(m_nested_properties_script); + LuaScript* script2 = m_logicEngine.createLuaScript(m_nested_properties_script); + m_logicEngine.update(); + + link(*script1->getOutputs()->getChild("data")->getChild("nested"), *script2->getInputs()->getChild("data")->getChild("nested")); + m_logicEngine.update(); + EXPECT_FALSE(script1->m_impl.isDirty()); + EXPECT_FALSE(script2->m_impl.isDirty()); + + m_logicEngine.unlink(*script1->getOutputs()->getChild("data")->getChild("nested"), *script2->getInputs()->getChild("data")->getChild("nested")); + script2->getInputs()->getChild("data")->getChild("nested")->set(5); + EXPECT_FALSE(script1->m_impl.isDirty()); + EXPECT_TRUE(script2->m_impl.isDirty()); + } + + TEST_P(ALogicEngine_DirtinessViaLink, Dirty_WhenAddingLinkToInterfaceInput) + { + LuaScript* script = m_logicEngine.createLuaScript(m_minimal_script); + LuaInterface* intf = m_logicEngine.createLuaInterface(m_minimal_interface, "iface name"); + m_logicEngine.update(); + + link(*script->getOutputs()->getChild("data"), *intf->getInputs()->getChild("data")); + EXPECT_TRUE(script->m_impl.isDirty()); + EXPECT_TRUE(intf->m_impl.isDirty()); + m_logicEngine.update(); + EXPECT_FALSE(script->m_impl.isDirty()); + EXPECT_FALSE(intf->m_impl.isDirty()); + } + + TEST_P(ALogicEngine_DirtinessViaLink, Dirty_WhenAddingLinkToInterfaceOutput) + { + LuaScript* script = m_logicEngine.createLuaScript(m_minimal_script); + LuaInterface* intf = m_logicEngine.createLuaInterface(m_minimal_interface, "iface name"); + m_logicEngine.update(); + + link(*intf->getOutputs()->getChild("data"), *script->getInputs()->getChild("data")); + EXPECT_TRUE(script->m_impl.isDirty()); + EXPECT_TRUE(intf->m_impl.isDirty()); + m_logicEngine.update(); + EXPECT_FALSE(script->m_impl.isDirty()); + EXPECT_FALSE(intf->m_impl.isDirty()); + } + + TEST_P(ALogicEngine_DirtinessViaLink, NotDirty_WhenRemovingLinkToInterface) + { + LuaScript* script = m_logicEngine.createLuaScript(m_minimal_script); + LuaInterface* intf = m_logicEngine.createLuaInterface(m_minimal_interface, "iface name"); + link(*script->getOutputs()->getChild("data"), *intf->getInputs()->getChild("data")); + m_logicEngine.update(); + + EXPECT_FALSE(script->m_impl.isDirty()); + EXPECT_FALSE(intf->m_impl.isDirty()); + m_logicEngine.unlink(*script->getOutputs()->getChild("data"), *intf->getInputs()->getChild("data")); + + EXPECT_FALSE(script->m_impl.isDirty()); + EXPECT_FALSE(intf->m_impl.isDirty()); + m_logicEngine.update(); + EXPECT_FALSE(script->m_impl.isDirty()); + EXPECT_FALSE(intf->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Dirtiness, Dirty_WhenScriptHadRuntimeError) + { + const std::string_view scriptWithError = R"( + function interface(IN,OUT) + end + function run(IN,OUT) + error("Snag!") + end + )"; + + const auto script = m_logicEngine.createLuaScript(scriptWithError); + EXPECT_FALSE(m_logicEngine.update()); + + EXPECT_TRUE(script->m_impl.isDirty()); + } + + // This is a bit of a special case, but an important one. If script A provides a value for script B and script A has an error + // AFTER it set a new value to B, then script B will be dirty (and not executed!) until the error in script A was fixed + TEST_F(ALogicEngine_Dirtiness, KeepsDirtynessStateOfDependentScript_UntilErrorInSourceScriptIsFixed) + { + const std::string_view scriptWithFixableError = R"( + function interface(IN,OUT) + IN.triggerError = Type:Bool() + IN.data = Type:Int32() + OUT.data = Type:Int32() + end + function run(IN,OUT) + OUT.data = IN.data + if IN.triggerError then + error("Snag!") + end + end + )"; + + LuaScript* script1 = m_logicEngine.createLuaScript(scriptWithFixableError); + LuaScript* script2 = m_logicEngine.createLuaScript(m_minimal_script); + + // No error -> have normal run -> nothing is dirty + script1->getInputs()->getChild("triggerError")->set(false); + m_logicEngine.link(*script1->getOutputs()->getChild("data"), *script2->getInputs()->getChild("data")); + m_logicEngine.update(); + EXPECT_FALSE(script1->m_impl.isDirty()); + EXPECT_FALSE(script2->m_impl.isDirty()); + + // Trigger error -> keep in dirty state + script1->getInputs()->getChild("triggerError")->set(true); + EXPECT_FALSE(m_logicEngine.update()); + EXPECT_FALSE(m_logicEngine.update()); + EXPECT_TRUE(script1->m_impl.isDirty()); + EXPECT_FALSE(script2->m_impl.isDirty()); + // "fix" the error and set a value -> expect nothing is dirty and value was propagated + script1->getInputs()->getChild("triggerError")->set(false); + script1->getInputs()->getChild("data")->set(15); + + m_logicEngine.update(); + EXPECT_FALSE(script1->m_impl.isDirty()); + EXPECT_FALSE(script2->m_impl.isDirty()); + EXPECT_EQ(15, *script2->getOutputs()->getChild("data")->get()); + } + + class ALogicEngine_BindingDirtiness : public ALogicEngine_Dirtiness + { + protected: + const std::string_view m_bindningDataScript = R"( + function interface(IN,OUT) + OUT.vec3f = Type:Vec3f() + end + function run(IN,OUT) + OUT.vec3f = {1, 2, 3} + end + )"; + }; + + TEST_F(ALogicEngine_BindingDirtiness, NotDirtyAfterConstruction) + { + EXPECT_FALSE(m_apiObjects.bindingsDirty()); + } + + TEST_F(ALogicEngine_BindingDirtiness, NotDirtyAfterCreatingScript) + { + m_logicEngine.createLuaScript(m_valid_empty_script); + EXPECT_FALSE(m_apiObjects.bindingsDirty()); + } + + TEST_F(ALogicEngine_BindingDirtiness, NotDirtyAfterCreatingNodeBinding) + { + m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + EXPECT_FALSE(m_apiObjects.bindingsDirty()); + } + + TEST_F(ALogicEngine_BindingDirtiness, NotDirtyAfterCreatingAppearanceBinding) + { + m_logicEngine.createRamsesAppearanceBinding(*m_appearance, ""); + EXPECT_FALSE(m_apiObjects.bindingsDirty()); + } + + TEST_F(ALogicEngine_BindingDirtiness, Dirty_WhenSettingBindingInputToDefaultValue) + { + RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + m_logicEngine.update(); + + // zeroes is the default value + binding->getInputs()->getChild("translation")->set({ 0, 0, 0 }); + EXPECT_TRUE(m_apiObjects.bindingsDirty()); + m_logicEngine.update(); + + // Set different value, and then set again + binding->getInputs()->getChild("translation")->set({ 1, 2, 3 }); + m_logicEngine.update(); + binding->getInputs()->getChild("translation")->set({ 1, 2, 3 }); + EXPECT_TRUE(m_apiObjects.bindingsDirty()); + } + + TEST_F(ALogicEngine_BindingDirtiness, Dirty_WhenSettingBindingInputToDifferentValue) + { + RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + m_logicEngine.update(); + + // Set non-default value, and then set again to different value + binding->getInputs()->getChild("translation")->set({ 1, 2, 3 }); + m_logicEngine.update(); + EXPECT_FALSE(m_apiObjects.bindingsDirty()); + binding->getInputs()->getChild("translation")->set({ 11, 12, 13 }); + EXPECT_TRUE(m_apiObjects.bindingsDirty()); + } + + TEST_F(ALogicEngine_BindingDirtiness, Dirty_WhenAddingLink) + { + LuaScript* script = m_logicEngine.createLuaScript(m_bindningDataScript); + RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + m_logicEngine.update(); + + m_logicEngine.link(*script->getOutputs()->getChild("vec3f"), *binding->getInputs()->getChild("rotation")); + EXPECT_TRUE(m_apiObjects.bindingsDirty()); + + // After update - not dirty + m_logicEngine.update(); + EXPECT_FALSE(m_apiObjects.bindingsDirty()); + } + + TEST_F(ALogicEngine_BindingDirtiness, NotDirty_WhenRemovingLink) + { + LuaScript* script = m_logicEngine.createLuaScript(m_bindningDataScript); + RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + m_logicEngine.link(*script->getOutputs()->getChild("vec3f"), *binding->getInputs()->getChild("rotation")); + m_logicEngine.update(); + + EXPECT_FALSE(script->m_impl.isDirty()); + EXPECT_FALSE(binding->m_impl.isDirty()); + m_logicEngine.unlink(*script->getOutputs()->getChild("vec3f"), *binding->getInputs()->getChild("rotation")); + + EXPECT_FALSE(script->m_impl.isDirty()); + EXPECT_FALSE(binding->m_impl.isDirty()); + m_logicEngine.update(); + EXPECT_FALSE(script->m_impl.isDirty()); + EXPECT_FALSE(binding->m_impl.isDirty()); + } + + // Special case, but worth testing as we want that bindings are always + // executed when adding link, even if the link was just "re-added" + TEST_F(ALogicEngine_BindingDirtiness, Dirty_WhenReAddingLink) + { + LuaScript* script = m_logicEngine.createLuaScript(m_bindningDataScript); + RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("vec3f"), *binding->getInputs()->getChild("rotation"))); + m_logicEngine.update(); + + EXPECT_FALSE(script->m_impl.isDirty()); + EXPECT_FALSE(binding->m_impl.isDirty()); + m_logicEngine.unlink(*script->getOutputs()->getChild("vec3f"), *binding->getInputs()->getChild("rotation")); + m_logicEngine.link(*script->getOutputs()->getChild("vec3f"), *binding->getInputs()->getChild("rotation")); + + EXPECT_TRUE(script->m_impl.isDirty()); + EXPECT_TRUE(binding->m_impl.isDirty()); + m_logicEngine.update(); + EXPECT_FALSE(script->m_impl.isDirty()); + EXPECT_FALSE(binding->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_BindingDirtiness, Dirty_WhenSettingDataToNestedAppearanceBindingInputs) + { + // Vertex shader with array -> results in nested binding inputs + const std::string_view vertShader_array = R"( + #version 300 es + + uniform highp vec4 vec4Array[2]; + + void main() + { + gl_Position = vec4Array[1]; + })"; + + const std::string_view fragShader_trivial = R"( + #version 300 es + + out lowp vec4 color; + void main(void) + { + color = vec4(1.0, 0.0, 0.0, 1.0); + })"; + + RamsesAppearanceBinding* binding = m_logicEngine.createRamsesAppearanceBinding(RamsesTestSetup::CreateTestAppearance(*m_scene, vertShader_array, fragShader_trivial), ""); + + m_logicEngine.update(); + EXPECT_FALSE(m_apiObjects.bindingsDirty()); + + EXPECT_TRUE(binding->getInputs()->getChild("vec4Array")->getChild(0)->set({ .1f, .2f, .3f, .4f })); + EXPECT_TRUE(m_apiObjects.bindingsDirty()); + + m_logicEngine.update(); + EXPECT_FALSE(m_apiObjects.bindingsDirty()); + } +} + diff --git a/client/logic/unittests/api/LogicEngineTest_ErrorHandling.cpp b/client/logic/unittests/api/LogicEngineTest_ErrorHandling.cpp new file mode 100644 index 000000000..bffa67e98 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_ErrorHandling.cpp @@ -0,0 +1,113 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/Property.h" + +#include "WithTempDirectory.h" + +namespace ramses +{ + class ALogicEngine_ErrorHandling : public ALogicEngine + { + protected: + const std::string_view m_linkable_script = R"( + function interface(IN,OUT) + IN.input = Type:Bool() + OUT.output = Type:Bool() + end + function run(IN,OUT) + end + )"; + }; + + TEST_F(ALogicEngine_ErrorHandling, ClearsErrorsOnCreateNewLuaScript) + { + auto script = m_logicEngine.createLuaScript("somefile.txt"); + ASSERT_EQ(nullptr, script); + EXPECT_FALSE(m_logicEngine.getErrors().empty()); + + script = m_logicEngine.createLuaScript(m_valid_empty_script); + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + } + + TEST_F(ALogicEngine_ErrorHandling, ReturnsOnFirstError) + { + auto script = m_logicEngine.createLuaScript(m_invalid_empty_script); + ASSERT_EQ(nullptr, script); + EXPECT_EQ(m_logicEngine.getErrors().size(), 1u); + } + + TEST_F(ALogicEngine_ErrorHandling, ClearsErrorsOnUpdate) + { + auto script = m_logicEngine.createLuaScript(m_invalid_empty_script); + ASSERT_EQ(nullptr, script); + EXPECT_EQ(m_logicEngine.getErrors().size(), 1u); + + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(m_logicEngine.getErrors().size(), 0u); + } + + TEST_F(ALogicEngine_ErrorHandling, ClearsErrorsOnCreateNewRamsesNodeBinding) + { + LogicEngine otherLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto ramsesNodeBinding = otherLogicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + + ASSERT_FALSE(m_logicEngine.destroy(*ramsesNodeBinding)); + + EXPECT_FALSE(m_logicEngine.getErrors().empty()); + auto anotherNodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + EXPECT_NE(nullptr, anotherNodeBinding); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + } + + TEST_F(ALogicEngine_ErrorHandling, ClearsErrorsOnSaveAndLoadFromFile) + { + WithTempDirectory tempFolder; + + m_logicEngine.createLuaScript(m_valid_empty_script); + + // Generate error, so that we can test it's cleared by saveToFile() + m_logicEngine.createLuaScript(m_invalid_empty_script); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + + EXPECT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "logic.bin")); + EXPECT_EQ(m_logicEngine.getErrors().size(), 0u); + + // Generate error, so that we can test it's cleared by loadFromFile() + m_logicEngine.createLuaScript(m_invalid_empty_script); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + + EXPECT_TRUE(m_logicEngine.loadFromFile("logic.bin")); + EXPECT_EQ(m_logicEngine.getErrors().size(), 0u); + } + + TEST_F(ALogicEngine_ErrorHandling, ClearsErrorsOnLinkAndUnlink) + { + LuaScript* script1 = m_logicEngine.createLuaScript(m_linkable_script); + LuaScript* script2 = m_logicEngine.createLuaScript(m_linkable_script); + + // Generate error, so that we can test it's cleared by link() + m_logicEngine.createLuaScript(m_invalid_empty_script); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + + EXPECT_TRUE(m_logicEngine.link(*script1->getOutputs()->getChild("output"), *script2->getInputs()->getChild("input"))); + EXPECT_EQ(m_logicEngine.getErrors().size(), 0u); + + // Generate error, so that we can test it's cleared by unlink() + m_logicEngine.createLuaScript(m_invalid_empty_script); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + + EXPECT_TRUE(m_logicEngine.unlink(*script1->getOutputs()->getChild("output"), *script2->getInputs()->getChild("input"))); + EXPECT_EQ(m_logicEngine.getErrors().size(), 0u); + } +} + diff --git a/client/logic/unittests/api/LogicEngineTest_Factory.cpp b/client/logic/unittests/api/LogicEngineTest_Factory.cpp new file mode 100644 index 000000000..e7b303a95 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_Factory.cpp @@ -0,0 +1,683 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/LuaModule.h" +#include "ramses-logic/AnimationNodeConfig.h" + +#include "ramses-client-api/DataObject.h" + +#include "impl/LogicNodeImpl.h" + +#include "FeatureLevelTestValues.h" +#include "WithTempDirectory.h" + +#include +#include + +namespace ramses +{ + // There are more specific "create/destroy" tests in ApiObjects unit tests! + class ALogicEngine_Factory : public ALogicEngineBase, public ::testing::TestWithParam + { + public: + ALogicEngine_Factory() : ALogicEngineBase{ GetParam() } + { + } + + protected: + WithTempDirectory tempFolder; + }; + + INSTANTIATE_TEST_SUITE_P( + ALogicEngine_FactoryTests, + ALogicEngine_Factory, + ramses::internal::GetFeatureLevelTestValues()); + + TEST_P(ALogicEngine_Factory, ProducesErrorWhenCreatingEmptyScript) + { + const LuaScript* script = m_logicEngine.createLuaScript(""); + ASSERT_EQ(nullptr, script); + EXPECT_FALSE(m_logicEngine.getErrors().empty()); + } + + TEST_P(ALogicEngine_Factory, CreatesScriptFromValidLuaWithoutErrors) + { + const LuaScript* script = m_logicEngine.createLuaScript(m_valid_empty_script); + ASSERT_TRUE(nullptr != script); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + } + + TEST_P(ALogicEngine_Factory, DestroysScriptWithoutErrors) + { + LuaScript* script = m_logicEngine.createLuaScript(m_valid_empty_script); + ASSERT_TRUE(script); + ASSERT_TRUE(m_logicEngine.destroy(*script)); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorsWhenDestroyingScriptFromAnotherEngineInstance) + { + LogicEngine otherLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto script = otherLogicEngine.createLuaScript(m_valid_empty_script); + ASSERT_TRUE(script); + ASSERT_FALSE(m_logicEngine.destroy(*script)); + EXPECT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Failed to destroy object ' [Id=1]', cannot find it in this LogicEngine instance."); + } + + TEST_P(ALogicEngine_Factory, CreatesLuaModule) + { + const auto module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymodule"); + ASSERT_NE(nullptr, module); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + EXPECT_EQ(module, m_logicEngine.findByName("mymodule")); + ASSERT_EQ(1u, m_logicEngine.getCollection().size()); + EXPECT_EQ(module, *m_logicEngine.getCollection().cbegin()); + + const auto& constLogicEngine = m_logicEngine; + EXPECT_EQ(module, constLogicEngine.findByName("mymodule")); + } + + TEST_P(ALogicEngine_Factory, AllowsCreatingLuaModuleWithEmptyName) + { + EXPECT_NE(nullptr, m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + } + + TEST_P(ALogicEngine_Factory, AllowsCreatingLuaModuleWithNameContainingNonAlphanumericChars) + { + EXPECT_NE(nullptr, m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "!@#$")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + } + + TEST_P(ALogicEngine_Factory, AllowsCreatingLuaModuleWithDupliciteNameEvenIfSourceDiffers) + { + ASSERT_TRUE(m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymodule")); + // same name and same source is OK + EXPECT_TRUE(m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymodule")); + + // same name and different source is also OK + EXPECT_TRUE(m_logicEngine.createLuaModule("return {}", {}, "mymodule")); + } + + TEST_P(ALogicEngine_Factory, CanDestroyLuaModule) + { + LuaModule* module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymodule"); + ASSERT_NE(nullptr, module); + EXPECT_TRUE(m_logicEngine.destroy(*module)); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + EXPECT_FALSE(m_logicEngine.findByName("mymodule")); + } + + TEST_P(ALogicEngine_Factory, FailsToDestroyLuaModuleIfFromOtherLogicInstance) + { + LogicEngine otherLogic{ m_logicEngine.getFeatureLevel() }; + LuaModule* module = otherLogic.createLuaModule(m_moduleSourceCode); + ASSERT_NE(nullptr, module); + + EXPECT_FALSE(m_logicEngine.destroy(*module)); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_EQ(m_logicEngine.getErrors().front().message, "Failed to destroy object ' [Id=1]', cannot find it in this LogicEngine instance."); + } + + TEST_P(ALogicEngine_Factory, FailsToDestroyLuaModuleIfUsedInLuaScript) + { + LuaModule* module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymodule"); + ASSERT_NE(nullptr, module); + + constexpr std::string_view valid_empty_script = R"( + modules("mymodule") + function interface(IN,OUT) + end + function run(IN,OUT) + end + )"; + EXPECT_TRUE(m_logicEngine.createLuaScript(valid_empty_script, CreateDeps({ { "mymodule", module } }), "script")); + + EXPECT_FALSE(m_logicEngine.destroy(*module)); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_EQ(m_logicEngine.getErrors().front().message, "Failed to destroy LuaModule 'mymodule', it is used in LuaScript 'script'"); + } + + TEST_P(ALogicEngine_Factory, CanDestroyModuleAfterItIsNotUsedAnymore) + { + LuaModule* module = m_logicEngine.createLuaModule(m_moduleSourceCode); + ASSERT_NE(nullptr, module); + + constexpr std::string_view valid_empty_script = R"( + modules("mymodule") + function interface(IN,OUT) + end + function run(IN,OUT) + end + )"; + auto script = m_logicEngine.createLuaScript(valid_empty_script, CreateDeps({ { "mymodule", module } })); + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.destroy(*module)); + + EXPECT_TRUE(m_logicEngine.destroy(*script)); + EXPECT_TRUE(m_logicEngine.destroy(*module)); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorWhenCreatingLuaScriptUsingModuleFromAnotherLogicInstance) + { + LogicEngine other{ m_logicEngine.getFeatureLevel() }; + const auto module = other.createLuaModule(m_moduleSourceCode); + ASSERT_NE(nullptr, module); + + EXPECT_EQ(nullptr, m_logicEngine.createLuaScript(m_valid_empty_script, CreateDeps({ { "name", module } }))); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_EQ(m_logicEngine.getErrors().front().message, + "Failed to map Lua module 'name'! It was created on a different instance of LogicEngine."); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorWhenCreatingLuaModuleUsingModuleFromAnotherLogicInstance) + { + LogicEngine other{ m_logicEngine.getFeatureLevel() }; + const auto module = other.createLuaModule(m_moduleSourceCode); + ASSERT_NE(nullptr, module); + + LuaConfig config; + config.addDependency("name", *module); + EXPECT_EQ(nullptr, m_logicEngine.createLuaModule(m_valid_empty_script, config)); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_EQ(m_logicEngine.getErrors().front().message, + "Failed to map Lua module 'name'! It was created on a different instance of LogicEngine."); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorsWhenDestroyingRamsesNodeBindingFromAnotherEngineInstance) + { + LogicEngine otherLogicEngine{ m_logicEngine.getFeatureLevel() }; + + auto ramsesNodeBinding = otherLogicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + ASSERT_TRUE(ramsesNodeBinding); + ASSERT_FALSE(m_logicEngine.destroy(*ramsesNodeBinding)); + EXPECT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Failed to destroy object 'NodeBinding [Id=1]', cannot find it in this LogicEngine instance."); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorsWhenDestroyingRamsesAppearanceBindingFromAnotherEngineInstance) + { + LogicEngine otherLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto binding = otherLogicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + ASSERT_TRUE(binding); + ASSERT_FALSE(m_logicEngine.destroy(*binding)); + EXPECT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Failed to destroy object 'AppearanceBinding [Id=1]', cannot find it in this LogicEngine instance."); + } + + TEST_P(ALogicEngine_Factory, DestroysRamsesCameraBindingWithoutErrors) + { + auto binding = m_logicEngine.createRamsesCameraBinding(*m_camera, "CameraBinding"); + ASSERT_TRUE(binding); + ASSERT_TRUE(m_logicEngine.destroy(*binding)); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorsWhenDestroyingRamsesCameraBindingFromAnotherEngineInstance) + { + LogicEngine otherLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto binding = otherLogicEngine.createRamsesCameraBinding(*m_camera, "CameraBinding"); + ASSERT_TRUE(binding); + ASSERT_FALSE(m_logicEngine.destroy(*binding)); + EXPECT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Failed to destroy object 'CameraBinding [Id=1]', cannot find it in this LogicEngine instance."); + } + + TEST_P(ALogicEngine_Factory, DestroysRamsesRenderPassBindingWithoutErrors) + { + auto binding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rp"); + ASSERT_TRUE(binding); + ASSERT_TRUE(m_logicEngine.destroy(*binding)); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorsWhenDestroyingRamsesRenderPassBindingFromAnotherEngineInstance) + { + LogicEngine otherLogicEngine{ GetParam() }; + auto binding = otherLogicEngine.createRamsesRenderPassBinding(*m_renderPass, "rp"); + ASSERT_TRUE(binding); + ASSERT_FALSE(m_logicEngine.destroy(*binding)); + EXPECT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Failed to destroy object 'rp [Id=1]', cannot find it in this LogicEngine instance."); + } + + TEST_P(ALogicEngine_Factory, DestroysRamsesRenderGroupBindingWithoutErrors) + { + auto binding = createRenderGroupBinding(); + ASSERT_TRUE(binding); + ASSERT_TRUE(m_logicEngine.destroy(*binding)); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorsWhenDestroyingRamsesRenderGroupBindingFromAnotherEngineInstance) + { + auto binding = createRenderGroupBinding(); + ASSERT_TRUE(binding); + + LogicEngine otherLogicEngine{ GetParam() }; + ASSERT_FALSE(otherLogicEngine.destroy(*binding)); + EXPECT_EQ(otherLogicEngine.getErrors().size(), 1u); + EXPECT_EQ(otherLogicEngine.getErrors()[0].message, "Failed to destroy object 'renderGroupBinding [Id=1]', cannot find it in this LogicEngine instance."); + } + + TEST_P(ALogicEngine_Factory, DestroysRamsesMeshNodeBindingWithoutErrors) + { + auto binding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode); + ASSERT_TRUE(binding); + ASSERT_TRUE(m_logicEngine.destroy(*binding)); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorsWhenDestroyingRamsesMeshNodeBindingFromAnotherEngineInstance) + { + auto binding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode); + ASSERT_TRUE(binding); + + LogicEngine otherLogicEngine{ GetParam() }; + ASSERT_FALSE(otherLogicEngine.destroy(*binding)); + EXPECT_EQ(otherLogicEngine.getErrors().size(), 1u); + EXPECT_EQ(otherLogicEngine.getErrors()[0].message, "Failed to destroy object ' [Id=1]', cannot find it in this LogicEngine instance."); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorWhenCreatingAnchorPointAndNodeOrCameraFromAnotherInstance) + { + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node); + const auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera); + + LogicEngine otherEngine{ m_logicEngine.getFeatureLevel() }; + const auto nodeBindingOther = otherEngine.createRamsesNodeBinding(*m_node); + const auto cameraBindingOther = otherEngine.createRamsesCameraBinding(*m_camera); + + EXPECT_EQ(nullptr, m_logicEngine.createAnchorPoint(*nodeBindingOther, *cameraBinding, "anchor")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Failed to create AnchorPoint 'anchor': provided Ramses node binding and/or camera binding were not found in this logic instance."); + + EXPECT_EQ(nullptr, m_logicEngine.createAnchorPoint(*nodeBinding, *cameraBindingOther, "anchor")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Failed to create AnchorPoint 'anchor': provided Ramses node binding and/or camera binding were not found in this logic instance."); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorWhenCreatingSkinBindingAndNodeOrAppearanceFromAnotherInstance) + { + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node); + const auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance); + + LogicEngine otherEngine{ m_logicEngine.getFeatureLevel() }; + const auto nodeBindingOther = otherEngine.createRamsesNodeBinding(*m_node); + const auto appearanceBindingOther = otherEngine.createRamsesAppearanceBinding(*m_appearance); + + EXPECT_EQ(nullptr, createSkinBinding(*nodeBindingOther, *appearanceBinding, m_logicEngine)); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Failed to create SkinBinding 'skin': one or more of the provided Ramses node bindings was not found in this logic instance."); + + EXPECT_EQ(nullptr, createSkinBinding(*nodeBinding, *appearanceBindingOther, m_logicEngine)); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Failed to create SkinBinding 'skin': provided Ramses appearance binding was not found in this logic instance."); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorWhenCreatingSkinBindingAndNodesEmptyOrNull) + { + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node); + auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance); + ramses::UniformInput uniform; + m_appearance->getEffect().findUniformInput("jointMat", uniform); + EXPECT_TRUE(uniform.isValid()); + + EXPECT_FALSE(m_logicEngine.createSkinBinding({}, {}, *appearanceBinding, uniform, "skin")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot create SkinBinding, no or null joint node bindings provided."); + + EXPECT_FALSE(m_logicEngine.createSkinBinding({ nodeBinding, nullptr }, { matrix44f{ 0.f }, matrix44f{ 0.f } }, *appearanceBinding, uniform, "skin")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot create SkinBinding, no or null joint node bindings provided."); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorWhenCreatingSkinBindingAndNodesCountDifferentFromMatricesCount) + { + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node); + auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance); + ramses::UniformInput uniform; + m_appearance->getEffect().findUniformInput("jointMat", uniform); + EXPECT_TRUE(uniform.isValid()); + + EXPECT_FALSE(m_logicEngine.createSkinBinding({ nodeBinding }, { matrix44f{ 0.f }, matrix44f{ 0.f } }, *appearanceBinding, uniform, "skin")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot create SkinBinding, number of inverse matrices must match the number of joints."); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorWhenCreatingSkinBindingAndUniformInvalidOrFromAnotherEffect) + { + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node); + auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance); + + // invalid uniform + ramses::UniformInput uniform; + EXPECT_FALSE(uniform.isValid()); + + EXPECT_FALSE(m_logicEngine.createSkinBinding({ nodeBinding }, { matrix44f{ 0.f } }, *appearanceBinding, uniform, "skin")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot create SkinBinding, provided uniform input must be pointing to valid uniform of the provided appearance's effect and must not be bound."); + + // valid uniform but from other effect + const std::string_view vertShader = R"( + #version 100 + uniform highp float someUniform; + attribute vec3 a_position; + void main() + { + gl_Position = someUniform * vec4(a_position, 1.0); + })"; + const std::string_view fragShader = R"( + #version 100 + void main(void) + { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + })"; + ramses::EffectDescription effectDesc; + effectDesc.setVertexShader(vertShader.data()); + effectDesc.setFragmentShader(fragShader.data()); + const auto otherEffect = m_scene->createEffect(effectDesc); + otherEffect->findUniformInput("someUniform", uniform); + EXPECT_TRUE(uniform.isValid()); + + EXPECT_FALSE(m_logicEngine.createSkinBinding({ nodeBinding }, { matrix44f{ 0.f } }, *appearanceBinding, uniform, "skin")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot create SkinBinding, provided uniform input must be pointing to valid uniform of the provided appearance's effect and must not be bound."); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorWhenCreatingSkinBindingAndUniformIsBoundInRamses) + { + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node); + auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance); + + ramses::UniformInput uniform; + m_appearance->getEffect().findUniformInput("floatUniform", uniform); + EXPECT_TRUE(uniform.isValid()); + EXPECT_EQ(ramses::StatusOK, m_appearance->bindInput(uniform, *m_scene->createDataObject(ramses::EDataType::Float))); + + EXPECT_FALSE(m_logicEngine.createSkinBinding({ nodeBinding }, { matrix44f{ 0.f } }, *appearanceBinding, uniform, "skin")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot create SkinBinding, provided uniform input must be pointing to valid uniform of the provided appearance's effect and must not be bound."); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorWhenCreatingSkinBindingAndUniformDataTypeWrong) + { + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node); + auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance); + + ramses::UniformInput uniform; + m_appearance->getEffect().findUniformInput("floatUniform", uniform); + EXPECT_TRUE(uniform.isValid()); + + EXPECT_FALSE(m_logicEngine.createSkinBinding({ nodeBinding }, { matrix44f{ 0.f } }, *appearanceBinding, uniform, "skin")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot create SkinBinding, provided uniform input must be of type array of Matrix4x4 with element count matching number of joints."); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorWhenCreatingSkinBindingAndUniformPointsToArrayWithMismatchedSize) + { + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node); + + const std::string_view vertShader = R"( + #version 100 + uniform highp mat4 someArray[3]; + attribute vec3 a_position; + void main() + { + gl_Position = someArray[0] * vec4(a_position, 1.0); + })"; + const std::string_view fragShader = R"( + #version 100 + void main(void) + { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + })"; + ramses::EffectDescription effectDesc; + effectDesc.setVertexShader(vertShader.data()); + effectDesc.setFragmentShader(fragShader.data()); + const auto otherEffect = m_scene->createEffect(effectDesc); + ramses::UniformInput uniform; + otherEffect->findUniformInput("someArray", uniform); + EXPECT_TRUE(uniform.isValid()); + auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_scene->createAppearance(*otherEffect)); + + EXPECT_FALSE(m_logicEngine.createSkinBinding({ nodeBinding }, { matrix44f{ 0.f } }, *appearanceBinding, uniform, "skin")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot create SkinBinding, provided uniform input must be of type array of Matrix4x4 with element count matching number of joints."); + } + + TEST_P(ALogicEngine_Factory, RenamesObjectsAfterCreation) + { + auto script = m_logicEngine.createLuaScript(m_valid_empty_script); + auto ramsesNodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + auto ramsesAppearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + auto ramsesCameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "CameraBinding"); + + script->setName("same name twice"); + ramsesNodeBinding->setName("same name twice"); + ramsesAppearanceBinding->setName(""); + ramsesCameraBinding->setName(""); + + EXPECT_EQ("same name twice", script->getName()); + EXPECT_EQ("same name twice", ramsesNodeBinding->getName()); + EXPECT_EQ("", ramsesAppearanceBinding->getName()); + EXPECT_EQ("", ramsesCameraBinding->getName()); + + auto ramsesRenderPassBinding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rp"); + ramsesRenderPassBinding->setName(""); + EXPECT_EQ("", ramsesRenderPassBinding->getName()); + + auto ramsesRenderGroupBinding = createRenderGroupBinding(); + ramsesRenderGroupBinding->setName(""); + EXPECT_EQ("", ramsesRenderGroupBinding->getName()); + } + + TEST_P(ALogicEngine_Factory, CanCastObjectsToValidTypes) + { + LogicObject* luaModule = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + LogicObject* luaScript = m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + LogicObject* nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + LogicObject* appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + LogicObject* cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + LogicObject* dataArray = m_logicEngine.createDataArray(std::vector{1.f, 2.f, 3.f}, "dataarray"); + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray->as(), dataArray->as(), EInterpolationType::Linear }); + LogicObject* animNode = m_logicEngine.createAnimationNode(config, "animNode"); + LogicObject* renderPassBinding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rp"); + LogicObject* renderGroupBinding = createRenderGroupBinding(); + LogicObject* skin = createSkinBinding(m_logicEngine); + LogicObject* meshBinding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode); + + EXPECT_TRUE(luaModule->as()); + EXPECT_TRUE(luaScript->as()); + EXPECT_TRUE(nodeBinding->as()); + EXPECT_TRUE(appearanceBinding->as()); + EXPECT_TRUE(cameraBinding->as()); + EXPECT_TRUE(dataArray->as()); + EXPECT_TRUE(animNode->as()); + EXPECT_TRUE(renderPassBinding->as()); + EXPECT_TRUE(renderGroupBinding->as()); + EXPECT_TRUE(skin->as()); + EXPECT_TRUE(meshBinding->as()); + + EXPECT_FALSE(luaModule->as()); + EXPECT_FALSE(luaScript->as()); + EXPECT_FALSE(nodeBinding->as()); + EXPECT_FALSE(appearanceBinding->as()); + EXPECT_FALSE(cameraBinding->as()); + EXPECT_FALSE(dataArray->as()); + EXPECT_FALSE(animNode->as()); + EXPECT_FALSE(renderPassBinding->as()); + EXPECT_FALSE(renderGroupBinding->as()); + EXPECT_FALSE(skin->as()); + EXPECT_FALSE(meshBinding->as()); + + //cast obj -> node -> binding -> appearanceBinding + auto* nodeCastFromObject = appearanceBinding->as(); + EXPECT_TRUE(nodeCastFromObject); + auto* bindingCastFromNode = nodeCastFromObject->as(); + EXPECT_TRUE(bindingCastFromNode); + auto* appearanceBindingCastFromBinding = bindingCastFromNode->as(); + EXPECT_TRUE(appearanceBindingCastFromBinding); + + //cast appearanceBinding -> binding -> node -> obj + EXPECT_TRUE(appearanceBindingCastFromBinding->as()); + EXPECT_TRUE(bindingCastFromNode->as()); + EXPECT_TRUE(nodeCastFromObject->as()); + + //cast obj -> node -> animationnode + auto* anNodeCastFromObject = animNode->as(); + EXPECT_TRUE(anNodeCastFromObject); + auto* animationCastFromNode = anNodeCastFromObject->as(); + EXPECT_TRUE(animationCastFromNode); + + //cast animationnode -> node -> obj + EXPECT_TRUE(animationCastFromNode->as()); + EXPECT_TRUE(anNodeCastFromObject->as()); + } + + TEST_P(ALogicEngine_Factory, CanCastObjectsToValidTypes_Const) + { + m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + const LogicObject* dataArray = m_logicEngine.createDataArray(std::vector{1.f, 2.f, 3.f}, "dataarray"); + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray->as(), dataArray->as(), EInterpolationType::Linear }); + m_logicEngine.createAnimationNode(config, "animNode"); + m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + createRenderGroupBinding(); + createSkinBinding(m_logicEngine); + m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "meshBinding"); + + const auto& immutableLogicEngine = m_logicEngine; + const auto* luaModuleConst = immutableLogicEngine.findByName("luaModule"); + const auto* luaScriptConst = immutableLogicEngine.findByName("script"); + const auto* nodeBindingConst = immutableLogicEngine.findByName("nodebinding"); + const auto* appearanceBindingConst = immutableLogicEngine.findByName("appbinding"); + const auto* cameraBindingConst = immutableLogicEngine.findByName("camerabinding"); + const auto* dataArrayConst = immutableLogicEngine.findByName("dataarray"); + const auto* animNodeConst = immutableLogicEngine.findByName("animNode"); + const auto* renderPassBindingConst = immutableLogicEngine.findByName("renderPass"); + const auto* renderGroupBindingConst = immutableLogicEngine.findByName("renderGroupBinding"); + const auto* skinConst = immutableLogicEngine.findByName("skin"); + const auto* meshBindingConst = immutableLogicEngine.findByName("meshBinding"); + + EXPECT_TRUE(luaModuleConst->as()); + EXPECT_TRUE(luaScriptConst->as()); + EXPECT_TRUE(nodeBindingConst->as()); + EXPECT_TRUE(appearanceBindingConst->as()); + EXPECT_TRUE(cameraBindingConst->as()); + EXPECT_TRUE(dataArrayConst->as()); + EXPECT_TRUE(animNodeConst->as()); + EXPECT_TRUE(renderPassBindingConst->as()); + EXPECT_TRUE(renderGroupBindingConst->as()); + EXPECT_TRUE(skinConst->as()); + EXPECT_TRUE(meshBindingConst->as()); + + EXPECT_FALSE(luaModuleConst->as()); + EXPECT_FALSE(luaScriptConst->as()); + EXPECT_FALSE(nodeBindingConst->as()); + EXPECT_FALSE(appearanceBindingConst->as()); + EXPECT_FALSE(cameraBindingConst->as()); + EXPECT_FALSE(dataArrayConst->as()); + EXPECT_FALSE(animNodeConst->as()); + EXPECT_FALSE(renderPassBindingConst->as()); + EXPECT_FALSE(renderGroupBindingConst->as()); + EXPECT_FALSE(skinConst->as()); + EXPECT_FALSE(meshBindingConst->as()); + + // cast obj -> node -> binding -> appearanceBinding + const auto* nodeCastFromObject = appearanceBindingConst->as(); + EXPECT_TRUE(nodeCastFromObject); + const auto* bindingCastFromNode = nodeCastFromObject->as(); + EXPECT_TRUE(bindingCastFromNode); + const auto* appearanceBindingCastFromBinding = bindingCastFromNode->as(); + EXPECT_TRUE(appearanceBindingCastFromBinding); + + // cast appearanceBinding -> binding -> node -> obj + EXPECT_TRUE(appearanceBindingCastFromBinding->as()); + EXPECT_TRUE(bindingCastFromNode->as()); + EXPECT_TRUE(nodeCastFromObject->as()); + + // cast obj -> node -> animationnode + const auto* anNodeCastFromObject = animNodeConst->as(); + EXPECT_TRUE(anNodeCastFromObject); + const auto* animationCastFromNode = anNodeCastFromObject->as(); + EXPECT_TRUE(animationCastFromNode); + + // cast animationnode -> node -> obj + EXPECT_TRUE(animationCastFromNode->as()); + EXPECT_TRUE(anNodeCastFromObject->as()); + } + + TEST_P(ALogicEngine_Factory, ProducesErrorIfWrongObjectTypeIsDestroyed) + { + struct UnknownObjectImpl: internal::LogicNodeImpl + { + UnknownObjectImpl() + : LogicNodeImpl("name", 1u) + { + } + + std::optional update() override { return std::nullopt; } + void createRootProperties() final {} + }; + + struct UnknownObject : LogicNode + { + explicit UnknownObject(std::unique_ptr impl) + : LogicNode(std::move(impl)) + { + } + }; + + UnknownObject unknownObject(std::make_unique()); + EXPECT_FALSE(m_logicEngine.destroy(unknownObject)); + const auto& errors = m_logicEngine.getErrors(); + EXPECT_EQ(1u, errors.size()); + EXPECT_EQ(errors[0].message, "Tried to destroy object 'name' with unknown type"); + } + + TEST_P(ALogicEngine_Factory, CanBeMoved) + { + LuaScript* script = m_logicEngine.createLuaScript(m_valid_empty_script, {}, "Script"); + RamsesNodeBinding* ramsesNodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + RamsesAppearanceBinding* appBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + RamsesCameraBinding* camBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "CameraBinding"); + const RamsesRenderPassBinding* rpBinding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "RenderPass"); + const RamsesRenderGroupBinding* rgBinding = createRenderGroupBinding(); + + LogicEngine movedLogicEngine(std::move(m_logicEngine)); + EXPECT_EQ(script, movedLogicEngine.findByName("Script")); + EXPECT_EQ(ramsesNodeBinding, movedLogicEngine.findByName("NodeBinding")); + EXPECT_EQ(appBinding, movedLogicEngine.findByName("AppearanceBinding")); + EXPECT_EQ(camBinding, movedLogicEngine.findByName("CameraBinding")); + EXPECT_EQ(rpBinding, movedLogicEngine.findByName("RenderPass")); + EXPECT_EQ(rgBinding, movedLogicEngine.findByName("renderGroupBinding")); + + movedLogicEngine.update(); + + LogicEngine moveAssignedLogicEngine{ GetParam() }; + moveAssignedLogicEngine = std::move(movedLogicEngine); + + EXPECT_EQ(script, moveAssignedLogicEngine.findByName("Script")); + EXPECT_EQ(ramsesNodeBinding, moveAssignedLogicEngine.findByName("NodeBinding")); + EXPECT_EQ(appBinding, moveAssignedLogicEngine.findByName("AppearanceBinding")); + EXPECT_EQ(camBinding, moveAssignedLogicEngine.findByName("CameraBinding")); + EXPECT_EQ(rpBinding, moveAssignedLogicEngine.findByName("RenderPass")); + EXPECT_EQ(rgBinding, moveAssignedLogicEngine.findByName("renderGroupBinding")); + + moveAssignedLogicEngine.update(); + } +} diff --git a/client/logic/unittests/api/LogicEngineTest_Linking.cpp b/client/logic/unittests/api/LogicEngineTest_Linking.cpp new file mode 100644 index 000000000..14daaf962 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_Linking.cpp @@ -0,0 +1,2453 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "RamsesTestUtils.h" +#include "WithTempDirectory.h" +#include "PropertyLinkTestUtils.h" + +#include "ramses-client-api/EffectDescription.h" +#include "ramses-client-api/Effect.h" +#include "ramses-client-api/Node.h" +#include "ramses-client-api/Scene.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/RenderPass.h" + +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/EStandardModule.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" + +#include "impl/LogicEngineImpl.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "impl/LogicNodeImpl.h" +#include "impl/PropertyImpl.h" +#include "internals/ApiObjects.h" + +#include "fmt/format.h" +#include + +namespace ramses +{ + class ALogicEngine_Linking : public ALogicEngine + { + protected: + ALogicEngine_Linking() + : m_sourceScript(*m_logicEngine.createLuaScript(m_minimalLinkScript, {}, "SourceScript")) + , m_targetScript(*m_logicEngine.createLuaScript(m_minimalLinkScript, {}, "TargetScript")) + , m_sourceProperty(*m_sourceScript.getOutputs()->getChild("source")) + , m_targetProperty(*m_targetScript.getInputs()->getChild("target")) + { + } + + const std::string_view m_minimalLinkScript = R"( + function interface(IN,OUT) + IN.target = Type:Bool() + OUT.source = Type:Bool() + end + function run(IN,OUT) + end + )"; + + const std::string_view m_linkScriptMultipleTypes = R"( + function interface(IN,OUT) + IN.target_int = Type:Int32() + OUT.source_int = Type:Int32() + IN.target_vec3f = Type:Vec3f() + OUT.source_vec3f = Type:Vec3f() + end + function run(IN,OUT) + OUT.source_int = IN.target_int + OUT.source_vec3f = IN.target_vec3f + end + )"; + + LuaScript& m_sourceScript; + LuaScript& m_targetScript; + + const Property& m_sourceProperty; + Property& m_targetProperty; + }; + + TEST_F(ALogicEngine_Linking, ProducesErrorIfPropertiesWithMismatchedTypesAreLinked) + { + const char* errorString = "Types of source property 'outParam:{}' does not match target property 'inParam:{}'"; + + std::array, 7> errorCases = { + std::make_tuple("Type:Float()", "Type:Int32()", fmt::format(errorString, "Float", "Int32")), + std::make_tuple("Type:Int32()", "Type:Int64()", fmt::format(errorString, "Int32", "Int64")), + std::make_tuple("Type:Int64()", "Type:Int32()", fmt::format(errorString, "Int64", "Int32")), + std::make_tuple("Type:Vec3f()", "Type:Vec3i()", fmt::format(errorString, "Vec3f", "Vec3i")), + std::make_tuple("Type:Vec2f()", "Type:Vec4i()", fmt::format(errorString, "Vec2f", "Vec4i")), + std::make_tuple("Type:Vec2i()", "Type:Float()", fmt::format(errorString, "Vec2i", "Float")), + std::make_tuple("Type:Int32()", + R"({ + param1 = Type:Int32(), + param2 = Type:Float() + })", fmt::format(errorString, "Int32", "Struct")) + }; + + for (const auto& errorCase : errorCases) + { + const auto luaScriptSource = fmt::format(R"( + function interface(IN,OUT) + IN.inParam = {} + OUT.outParam = {} + end + function run(IN,OUT) + end + )", std::get<1>(errorCase), std::get<0>(errorCase)); + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource); + auto targetScript = m_logicEngine.createLuaScript(luaScriptSource); + + const auto sourceProperty = sourceScript->getOutputs()->getChild("outParam"); + const auto targetProperty = targetScript->getInputs()->getChild("inParam"); + + EXPECT_FALSE(m_logicEngine.link( + *sourceProperty, + *targetProperty + )); + + const auto& errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ(errors[0].message, std::get<2>(errorCase)); + } + } + + TEST_F(ALogicEngine_Linking, ProducesErrorIfLogicNodeIsLinkedToItself) + { + const auto targetPropertyFromTheSameScript = m_sourceScript.getInputs()->getChild("target"); + + EXPECT_FALSE(m_logicEngine.link(m_sourceProperty, *targetPropertyFromTheSameScript)); + + const auto& errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ(errors[0].message, "Link source and target can't belong to the same node! ('SourceScript')"); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorIfInputIsLinkedToOutput) + { + const auto sourceProperty = m_sourceScript.getOutputs()->getChild("source"); + const auto targetProperty = m_targetScript.getInputs()->getChild("target"); + + EXPECT_FALSE(m_logicEngine.link(*targetProperty, *sourceProperty)); + const auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ("Failed to link input property 'target' to output property 'source'. Only outputs can be linked to inputs", errors[0].message); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorIfInputIsLinkedToInput) + { + const auto sourceInput = m_sourceScript.getInputs()->getChild("target"); + const auto targetInput = m_targetScript.getInputs()->getChild("target"); + + EXPECT_FALSE(m_logicEngine.link(*sourceInput, *targetInput)); + const auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ("Failed to link input property 'target' to input property 'target'. Only outputs can be linked to inputs", errors[0].message); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorIfOutputIsLinkedToOutput) + { + auto sourceScript = m_logicEngine.createLuaScript(m_linkScriptMultipleTypes); + auto targetScript = m_logicEngine.createLuaScript(m_linkScriptMultipleTypes); + + const auto sourceOuput = sourceScript->getOutputs()->getChild("source_int"); + const auto targetOutput = targetScript->getOutputs()->getChild("source_int"); + + EXPECT_FALSE(m_logicEngine.link(*sourceOuput, *targetOutput)); + + const auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ("Failed to link output property 'source_int' to output property 'source_int'. Only outputs can be linked to inputs", errors[0].message); + } + + TEST_F(ALogicEngine_Linking, ProducesNoErrorIfMatchingPropertiesAreLinked) + { + EXPECT_TRUE(m_logicEngine.link(m_sourceProperty, m_targetProperty)); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorIfPropertyIsLinkedTwiceToSameProperty_LuaScript) + { + EXPECT_TRUE(m_logicEngine.link(m_sourceProperty, m_targetProperty)); + EXPECT_FALSE(m_logicEngine.link(m_sourceProperty, m_targetProperty)); + + const auto& errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ(errors[0].message, "The property 'target' of LogicNode 'TargetScript' is already linked (to property 'source' of LogicNode 'SourceScript')"); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorIfPropertyIsLinkedTwice_RamsesBinding) + { + auto ramsesBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "RamsesBinding"); + + const auto visibilityProperty = ramsesBinding->getInputs()->getChild("visibility"); + + EXPECT_TRUE(m_logicEngine.link(m_sourceProperty, *visibilityProperty)); + EXPECT_FALSE(m_logicEngine.link(m_sourceProperty, *visibilityProperty)); + + const auto& errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ(errors[0].message, "The property 'visibility' of LogicNode 'RamsesBinding' is already linked (to property 'source' of LogicNode 'SourceScript')"); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorIfNotLinkedPropertyIsUnlinked_LuaScript) + { + EXPECT_FALSE(m_logicEngine.unlink(m_sourceProperty, m_targetProperty)); + + const auto& errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ(errors[0].message, "Input property 'target' is not currently linked!"); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorIfNotLinkedPropertyIsUnlinked_RamsesNodeBinding) + { + auto ramsesBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "RamsesBinding"); + + const auto visibilityProperty = ramsesBinding->getInputs()->getChild("visibility"); + + EXPECT_FALSE(m_logicEngine.unlink(m_sourceProperty, *visibilityProperty)); + + const auto& errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ(errors[0].message, "Input property 'visibility' is not currently linked!"); + } + + TEST_F(ALogicEngine_Linking, ProducesNoErrorIfLinkedToMatchingType) + { + const auto luaScriptSource = R"( + function interface(IN,OUT) + IN.boolTarget = Type:Bool() + IN.intTarget = Type:Int32() + IN.int64Target = Type:Int64() + IN.floatTarget = Type:Float() + IN.vec2Target = Type:Vec2f() + IN.vec3Target = Type:Vec3f() + OUT.boolSource = Type:Bool() + OUT.intSource = Type:Int32() + OUT.int64Source = Type:Int64() + OUT.floatSource = Type:Float() + OUT.vec2Source = Type:Vec2f() + OUT.vec3Source = Type:Vec3f() + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource); + auto targetScript = m_logicEngine.createLuaScript(luaScriptSource); + + const auto outputs = sourceScript->getOutputs(); + const auto inputs = targetScript->getInputs(); + + auto boolTarget = inputs->getChild("boolTarget"); + auto intTarget = inputs->getChild("intTarget"); + auto int64Target = inputs->getChild("int64Target"); + auto floatTarget = inputs->getChild("floatTarget"); + auto vec2Target = inputs->getChild("vec2Target"); + auto vec3Target = inputs->getChild("vec3Target"); + + auto boolSource = outputs->getChild("boolSource"); + auto intSource = outputs->getChild("intSource"); + auto int64Source = outputs->getChild("int64Source"); + auto floatSource = outputs->getChild("floatSource"); + auto vec2Source = outputs->getChild("vec2Source"); + auto vec3Source = outputs->getChild("vec3Source"); + + EXPECT_TRUE(m_logicEngine.link(*boolSource, *boolTarget)); + EXPECT_TRUE(m_logicEngine.link(*intSource, *intTarget)); + EXPECT_TRUE(m_logicEngine.link(*int64Source, *int64Target)); + EXPECT_TRUE(m_logicEngine.link(*floatSource, *floatTarget)); + EXPECT_TRUE(m_logicEngine.link(*vec2Source, *vec2Target)); + EXPECT_TRUE(m_logicEngine.link(*vec3Source, *vec3Target)); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorOnNextUpdateIfLinkCycleWasCreated) + { + LuaScript& loopScript = *m_logicEngine.createLuaScript(m_minimalLinkScript); + const Property* sourceInput = m_sourceScript.getInputs()->getChild("target"); + const Property* sourceOutput = m_sourceScript.getOutputs()->getChild("source"); + const Property* targetInput = m_targetScript.getInputs()->getChild("target"); + const Property* targetOutput = m_targetScript.getOutputs()->getChild("source"); + const Property* loopInput = loopScript.getInputs()->getChild("target"); + const Property* loopOutput = loopScript.getOutputs()->getChild("source"); + + EXPECT_TRUE(m_logicEngine.link(*sourceOutput, *targetInput)); + EXPECT_TRUE(m_logicEngine.link(*targetOutput, *loopInput)); + EXPECT_TRUE(m_logicEngine.link(*loopOutput, *sourceInput)); + EXPECT_FALSE(m_logicEngine.update()); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ("Failed to sort logic nodes based on links between their properties. Create a loop-free link graph before calling update()!", errors[0].message); + + // Also refuse to save to file + EXPECT_FALSE(m_logicEngine.saveToFile("will_not_write")); + errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ("Failed to sort logic nodes based on links between their properties. Create a loop-free link graph before calling saveToFile()!", errors[0].message); + } + + TEST_F(ALogicEngine_Linking, PropagatesValuesAcrossMultipleLinksInAChain) + { + auto scriptSource = R"( + function interface(IN,OUT) + IN.inString1 = Type:String() + IN.inString2 = Type:String() + OUT.outString = Type:String() + end + function run(IN,OUT) + OUT.outString = IN.inString1 .. IN.inString2 + end + )"; + + auto script1 = m_logicEngine.createLuaScript(scriptSource); + auto script2 = m_logicEngine.createLuaScript(scriptSource); + auto script3 = m_logicEngine.createLuaScript(scriptSource); + + auto script1Input2 = script1->getInputs()->getChild("inString2"); + auto script2Input1 = script2->getInputs()->getChild("inString1"); + auto script2Input2 = script2->getInputs()->getChild("inString2"); + auto script3Input1 = script3->getInputs()->getChild("inString1"); + auto script3Input2 = script3->getInputs()->getChild("inString2"); + auto script1Output = script1->getOutputs()->getChild("outString"); + auto script2Output = script2->getOutputs()->getChild("outString"); + auto script3Output = script3->getOutputs()->getChild("outString"); + + m_logicEngine.link(*script1Output, *script2Input1); + m_logicEngine.link(*script2Output, *script3Input1); + + script1Input2->set(std::string("Script1")); + script2Input2->set(std::string("Script2")); + script3Input2->set(std::string("Script3")); + + m_logicEngine.update(); + + EXPECT_EQ("Script1Script2Script3", script3Output->get()); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorOnLinkingStructs) + { + const auto luaScriptSource = R"( + function interface(IN,OUT) + IN.intTarget = Type:Int32() + IN.structTarget = { + intTarget = Type:Int32(), + floatTarget = Type:Float() + } + OUT.intSource = Type:Int32() + OUT.structSource = { + intTarget = Type:Int32(), + floatTarget = Type:Float() + } + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource); + auto targetScript = m_logicEngine.createLuaScript(luaScriptSource); + + const auto outputs = sourceScript->getOutputs(); + const auto inputs = targetScript->getInputs(); + + auto structTarget = inputs->getChild("structTarget"); + auto structSource = outputs->getChild("structSource"); + + EXPECT_FALSE(m_logicEngine.link(*structSource, *structTarget)); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ("Can't link properties of complex types directly, currently only primitive properties can be linked", errors[0].message); + + EXPECT_FALSE(m_logicEngine.link(*outputs, *inputs)); + errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ("Can't link properties of complex types directly, currently only primitive properties can be linked", errors[0].message); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorOnLinkingArrays) + { + const auto luaScriptSource = R"( + function interface(IN,OUT) + IN.array = Type:Array(2, Type:Int32()) + OUT.array = Type:Array(2, Type:Int32()) + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource); + auto targetScript = m_logicEngine.createLuaScript(luaScriptSource); + + auto arrayTarget = targetScript->getInputs()->getChild("array"); + auto arraySource = sourceScript->getOutputs()->getChild("array"); + + EXPECT_FALSE(m_logicEngine.link(*arraySource, *arrayTarget)); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ("Can't link properties of complex types directly, currently only primitive properties can be linked", errors[0].message); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorIfNotLinkedPropertyIsUnlinked_WhenAnotherLinkFromTheSameScriptExists) + { + const auto luaScriptSource = R"( + function interface(IN,OUT) + IN.intTarget1 = Type:Int32() + IN.intTarget2 = Type:Int32() + OUT.intSource = Type:Int32() + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource); + auto targetScript = m_logicEngine.createLuaScript(luaScriptSource); + + const auto sourceProperty = sourceScript->getOutputs()->getChild("intSource"); + const auto targetProperty1 = targetScript->getInputs()->getChild("intTarget1"); + const auto targetProperty2 = targetScript->getInputs()->getChild("intTarget2"); + + EXPECT_TRUE(m_logicEngine.link(*sourceProperty, *targetProperty1)); + + EXPECT_FALSE(m_logicEngine.unlink(*sourceProperty, *targetProperty2)); + + const auto& errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ(errors[0].message, "Input property 'intTarget2' is not currently linked!"); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorIfNotLinkedPropertyIsUnlinked_RamsesBinding) + { + auto targetBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + const auto visibilityProperty = targetBinding->getInputs()->getChild("visibility"); + const auto unlinkedTargetProperty = targetBinding->getInputs()->getChild("translation"); + + EXPECT_TRUE(m_logicEngine.link(m_sourceProperty, *visibilityProperty)); + + EXPECT_FALSE(m_logicEngine.unlink(m_sourceProperty, *unlinkedTargetProperty)); + + const auto& errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ(errors[0].message, "Input property 'translation' is not currently linked!"); + } + + TEST_F(ALogicEngine_Linking, UnlinksPropertiesWhichAreLinked) + { + ASSERT_TRUE(m_logicEngine.link( + m_sourceProperty, + m_targetProperty + )); + + EXPECT_TRUE(m_logicEngine.unlink( + m_sourceProperty, + m_targetProperty + )); + } + + TEST_F(ALogicEngine_Linking, ProducesNoErrorsIfMultipleLinksFromSameSourceAreUnlinked) + { + auto targetScript2 = m_logicEngine.createLuaScript(m_minimalLinkScript); + + const auto targetProperty2 = targetScript2->getInputs()->getChild("target"); + + m_logicEngine.link( + m_sourceProperty, + m_targetProperty + ); + + m_logicEngine.link( + m_sourceProperty, + *targetProperty2 + ); + + EXPECT_TRUE(m_logicEngine.unlink( + m_sourceProperty, + m_targetProperty + )); + + EXPECT_TRUE(m_logicEngine.unlink( + m_sourceProperty, + *targetProperty2 + )); + } + + TEST_F(ALogicEngine_Linking, PropagatesOutputsToInputsIfLinked) + { + auto sourceScript = m_logicEngine.createLuaScript(m_linkScriptMultipleTypes); + auto targetScript = m_logicEngine.createLuaScript(m_linkScriptMultipleTypes); + + auto output = sourceScript->getOutputs()->getChild("source_int"); + auto input = targetScript->getInputs()->getChild("target_int"); + + EXPECT_TRUE(m_logicEngine.link(*output, *input)); + + sourceScript->getInputs()->getChild("target_int")->set(42); + + m_logicEngine.update(); + + EXPECT_EQ(42, *targetScript->getOutputs()->getChild("source_int")->get()); + } + + TEST_F(ALogicEngine_Linking, PropagatesOutputsToInputsIfLinked_Int64) + { + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + IN.data = Type:Int64() + OUT.data = Type:Int64() + end + function run(IN,OUT) + OUT.data = IN.data + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(scriptSrc); + auto targetScript = m_logicEngine.createLuaScript(scriptSrc); + + auto output = sourceScript->getOutputs()->getChild("data"); + auto input = targetScript->getInputs()->getChild("data"); + + EXPECT_TRUE(m_logicEngine.link(*output, *input)); + + const int64_t value = static_cast(std::numeric_limits::max()) + 1; + sourceScript->getInputs()->getChild("data")->set(value); + + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_EQ(value, *targetScript->getOutputs()->getChild("data")->get()); + } + + TEST_F(ALogicEngine_Linking, PropagatesOutputsToInputsIfLinked_ArraysOfStructs) + { + const std::string_view scriptArrayOfStructs = R"( + function interface(IN,OUT) + IN.data = Type:Array(3, + { + one = Type:Int32(), + two = Type:Int32() + } + ) + OUT.data = Type:Array(3, + { + one = Type:Int32(), + two = Type:Int32() + } + ) + end + function run(IN,OUT) + OUT.data = IN.data + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(scriptArrayOfStructs); + auto targetScript = m_logicEngine.createLuaScript(scriptArrayOfStructs); + + auto output = sourceScript->getOutputs()->getChild("data")->getChild(1)->getChild("one"); + auto input = targetScript->getInputs()->getChild("data")->getChild(1)->getChild("two"); + + EXPECT_TRUE(m_logicEngine.link(*output, *input)); + + sourceScript->getInputs()->getChild("data")->getChild(1)->getChild("one")->set(42); + + m_logicEngine.update(); + + EXPECT_EQ(42, *targetScript->getOutputs()->getChild("data")->getChild(1)->getChild("two")->get()); + } + + TEST_F(ALogicEngine_Linking, PropagatesOutputsToInputsIfLinked_StructOfArrays) + { + const std::string_view scriptArrayOfStructs = R"( + function interface(IN,OUT) + IN.data = + { + one = Type:Array(3, Type:Int32()), + two = Type:Array(3, Type:Int32()) + } + OUT.data = + { + one = Type:Array(3, Type:Int32()), + two = Type:Array(3, Type:Int32()) + } + end + function run(IN,OUT) + OUT.data = IN.data + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(scriptArrayOfStructs); + auto targetScript = m_logicEngine.createLuaScript(scriptArrayOfStructs); + + auto output = sourceScript->getOutputs()->getChild("data")->getChild("one")->getChild(1); + auto input = targetScript->getInputs()->getChild("data")->getChild("two")->getChild(1); + + EXPECT_TRUE(m_logicEngine.link(*output, *input)); + + sourceScript->getInputs()->getChild("data")->getChild("one")->getChild(1)->set(42); + + m_logicEngine.update(); + + EXPECT_EQ(42, *targetScript->getOutputs()->getChild("data")->getChild("two")->getChild(1)->get()); + } + + TEST_F(ALogicEngine_Linking, DoesNotPropagateOutputsToInputsAfterUnlink) + { + const auto luaScriptSource = R"( + function interface(IN,OUT) + IN.intTarget = Type:Int32() + OUT.intSource = Type:Int32() + end + function run(IN,OUT) + OUT.intSource = IN.intTarget + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource); + auto targetScript = m_logicEngine.createLuaScript(luaScriptSource); + + auto output = sourceScript->getOutputs()->getChild("intSource"); + auto input = targetScript->getInputs()->getChild("intTarget"); + + EXPECT_TRUE(m_logicEngine.link(*output, *input)); + sourceScript->getInputs()->getChild("intTarget")->set(42); + + EXPECT_TRUE(m_logicEngine.unlink( + *output, + *input + )); + + m_logicEngine.update(); + + EXPECT_EQ(0, *targetScript->getOutputs()->getChild("intSource")->get()); + } + + // TODO Violin add test with 2 scripts , one input in each + TEST_F(ALogicEngine_Linking, PropagatesOneOutputToMultipleInputs) + { + const auto luaScriptSource1 = R"( + function interface(IN,OUT) + OUT.intSource = Type:Int32() + end + function run(IN,OUT) + OUT.intSource = 5 + end + )"; + + const auto luaScriptSource2 = R"( + function interface(IN,OUT) + IN.intTarget1 = Type:Int32() + IN.intTarget2 = Type:Int32() + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource1); + auto targetScript = m_logicEngine.createLuaScript(luaScriptSource2); + + auto output = sourceScript->getOutputs()->getChild("intSource"); + auto input1 = targetScript->getInputs()->getChild("intTarget1"); + auto input2 = targetScript->getInputs()->getChild("intTarget2"); + + EXPECT_TRUE(m_logicEngine.link(*output, *input1)); + EXPECT_TRUE(m_logicEngine.link(*output, *input2)); + + m_logicEngine.update(); + + EXPECT_EQ(5, *targetScript->getInputs()->getChild("intTarget1")->get()); + EXPECT_EQ(5, *targetScript->getInputs()->getChild("intTarget2")->get()); + + EXPECT_TRUE(m_logicEngine.unlink(*output, *input1)); + input1->set(6); + + m_logicEngine.update(); + + EXPECT_EQ(6, *input1->get()); + EXPECT_EQ(5, *input2->get()); + } + + TEST_F(ALogicEngine_Linking, DoesNotOverwriteTargetValue_WhenUnlinked_WithoutLinkActivated) + { + const auto luaScriptSource1 = R"( + function interface(IN,OUT) + OUT.output = Type:Int32() + end + function run(IN,OUT) + OUT.output = 5 + end + )"; + + const auto luaScriptSource2 = R"( + function interface(IN,OUT) + IN.input = Type:Int32() + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource1); + auto targetScript = m_logicEngine.createLuaScript(luaScriptSource2); + + auto sourceOutput = sourceScript->getOutputs()->getChild("output"); + auto targetInput = targetScript->getInputs()->getChild("input"); + + // Set target value + targetInput->set(100); + ASSERT_TRUE(m_logicEngine.update()); + + m_logicEngine.link(*sourceOutput, *targetInput); + m_logicEngine.unlink(*sourceOutput, *targetInput); + m_logicEngine.update(); + + // Value not overwritten by 5 from the link + EXPECT_EQ(100, *targetInput->get()); + } + + TEST_F(ALogicEngine_Linking, PropagatesOutputsToInputsIfLinkedForRamsesAppearanceBindings) + { + const auto luaScriptSource = R"( + function interface(IN,OUT) + IN.floatInput = Type:Float() + OUT.floatOutput = Type:Float() + end + function run(IN,OUT) + OUT.floatOutput = IN.floatInput + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource); + auto targetBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "TargetBinding"); + + auto sourceInput = sourceScript->getInputs()->getChild("floatInput"); + auto sourceOutput = sourceScript->getOutputs()->getChild("floatOutput"); + auto targetInput = targetBinding->getInputs()->getChild("floatUniform"); + + m_logicEngine.link(*sourceOutput, *targetInput); + + sourceInput->set(47.11f); + m_logicEngine.update(); + + ramses::UniformInput floatUniform; + m_appearance->getEffect().findUniformInput("floatUniform", floatUniform); + float result = 0.0f; + m_appearance->getInputValue(floatUniform, result); + EXPECT_FLOAT_EQ(47.11f, result); + } + + TEST_F(ALogicEngine_Linking, PropagatesOutputsToInputsIfLinkedForRamsesCameraBindings) + { + const auto luaScriptSource = R"( + function interface(IN,OUT) + IN.floatInput = Type:Float() + OUT.floatOutput = Type:Float() + end + function run(IN,OUT) + OUT.floatOutput = IN.floatInput + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource); + auto targetBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "TargetBinding"); + + auto sourceInput = sourceScript->getInputs()->getChild("floatInput"); + auto sourceOutput = sourceScript->getOutputs()->getChild("floatOutput"); + auto targetInput = targetBinding->getInputs()->getChild("frustum")->getChild("farPlane"); + + m_logicEngine.link(*sourceOutput, *targetInput); + + sourceInput->set(47.11f); + m_logicEngine.update(); + + EXPECT_FLOAT_EQ(47.11f, m_camera->getFarPlane()); + } + + TEST_F(ALogicEngine_Linking, PropagatesOutputsToInputsIfLinkedForRamsesRenderPassBindings) + { + const auto luaScriptSource = R"( + function interface(IN,OUT) + IN.val = Type:Int32() + OUT.val = Type:Int32() + end + function run(IN,OUT) + OUT.val = IN.val + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource); + auto targetBinding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "TargetBinding"); + + auto sourceInput = sourceScript->getInputs()->getChild("val"); + auto sourceOutput = sourceScript->getOutputs()->getChild("val"); + auto targetInput = targetBinding->getInputs()->getChild("renderOrder"); + + m_logicEngine.link(*sourceOutput, *targetInput); + + sourceInput->set(-11); + m_logicEngine.update(); + + EXPECT_EQ(-11, m_renderPass->getRenderOrder()); + } + + TEST_F(ALogicEngine_Linking, PropagatesOutputsToInputsIfLinkedForRamsesRenderGroupBindings) + { + const auto luaScriptSource = R"( + function interface(IN,OUT) + IN.val = Type:Int32() + OUT.val = Type:Int32() + end + function run(IN,OUT) + OUT.val = IN.val + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource); + auto targetBinding = createRenderGroupBinding(); + + auto sourceInput = sourceScript->getInputs()->getChild("val"); + auto sourceOutput = sourceScript->getOutputs()->getChild("val"); + auto targetInput = targetBinding->getInputs()->getChild("renderOrders")->getChild("mesh"); + + m_logicEngine.link(*sourceOutput, *targetInput); + + sourceInput->set(-11); + m_logicEngine.update(); + + int32_t actualRenderOrder = 0; + m_renderGroup->getMeshNodeOrder(*m_meshNode, actualRenderOrder); + EXPECT_EQ(-11, actualRenderOrder); + } + + TEST_F(ALogicEngine_Linking, NewLinkTransfersValue_SourceValueSet) + { + const auto luaScriptSource1 = R"( + function interface(IN,OUT) + OUT.output = Type:Int32() + end + function run(IN,OUT) + OUT.output = 5 + end + )"; + + const auto luaScriptSource2 = R"( + function interface(IN,OUT) + IN.input = Type:Int32() + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource1); + auto targetScript = m_logicEngine.createLuaScript(luaScriptSource2); + + auto sourceOutput = sourceScript->getOutputs()->getChild("output"); + auto targetInput = targetScript->getInputs()->getChild("input"); + + // Execute run -> sets output to 5 + ASSERT_TRUE(m_logicEngine.update()); + ASSERT_EQ(5, *sourceOutput->get()); + + ASSERT_TRUE(m_logicEngine.link(*sourceOutput, *targetInput)); + m_logicEngine.update(); + + EXPECT_EQ(5, *targetInput->get()); + } + + TEST_F(ALogicEngine_Linking, NewLinkTransfersValue_SourceValueNotZero_OutputNotChanged) + { + const auto luaScriptSource1 = R"( + function interface(IN,OUT) + IN.setOutput = Type:Bool() + OUT.output = Type:Int32() + end + function run(IN,OUT) + if IN.setOutput then + OUT.output = 5 + end + end + )"; + + const auto luaScriptSource2 = R"( + function interface(IN,OUT) + IN.input = Type:Int32() + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource1); + auto targetScript = m_logicEngine.createLuaScript(luaScriptSource2); + + auto setOutput = sourceScript->getInputs()->getChild("setOutput"); + setOutput->set(true); + auto sourceOutput = sourceScript->getOutputs()->getChild("output"); + auto targetInput = targetScript->getInputs()->getChild("input"); + + // Execute run -> sets output to 5 + ASSERT_TRUE(m_logicEngine.update()); + ASSERT_EQ(5, *sourceOutput->get()); + + // Disable output writing and add link + setOutput->set(false); + ASSERT_TRUE(m_logicEngine.link(*sourceOutput, *targetInput)); + m_logicEngine.update(); + + EXPECT_EQ(5, *targetInput->get()); + } + + TEST_F(ALogicEngine_Linking, PropagatesValueIfLinkIsCreatedAndInputValueIsSetBeforehand) + { + const auto luaScriptSource1 = R"( + function interface(IN,OUT) + OUT.output = Type:Int32() + end + function run(IN,OUT) + OUT.output = 5 + end + )"; + + const auto luaScriptSource2 = R"( + function interface(IN,OUT) + IN.input = Type:Int32() + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource1); + auto targetScript = m_logicEngine.createLuaScript(luaScriptSource2); + + auto sourceOutput = sourceScript->getOutputs()->getChild("output"); + auto targetInput = targetScript->getInputs()->getChild("input"); + + targetInput->set(100); + ASSERT_TRUE(m_logicEngine.update()); + + ASSERT_EQ(5, *sourceOutput->get()); + ASSERT_EQ(100, *targetInput->get()); + + m_logicEngine.link(*sourceOutput, *targetInput); + m_logicEngine.update(); + + EXPECT_EQ(5, *targetInput->get()); + + m_logicEngine.unlink(*sourceOutput, *targetInput); + m_logicEngine.update(); + + // Value was overwritten after link + update + EXPECT_EQ(5, *targetInput->get()); + } + + TEST_F(ALogicEngine_Linking, ProducesErrorIfLinkIsCreatedBetweenDifferentLogicEngines) + { + LogicEngine otherLogicEngine{ m_logicEngine.getFeatureLevel() }; + const auto luaScriptSource = R"( + function interface(IN,OUT) + IN.floatInput = Type:Float() + OUT.floatOutput = Type:Float() + end + function run(IN,OUT) + OUT.floatOutput = IN.floatInput + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(luaScriptSource, {}, "SourceScript"); + auto targetScript = otherLogicEngine.createLuaScript(luaScriptSource, {}, "TargetScript"); + + const auto sourceOutput = sourceScript->getOutputs()->getChild("floatOutput"); + const auto targetInput = targetScript->getInputs()->getChild("floatInput"); + + EXPECT_FALSE(m_logicEngine.link(*sourceOutput, *targetInput)); + { + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ("LogicNode 'TargetScript' is not an instance of this LogicEngine", errors[0].message); + } + + EXPECT_FALSE(otherLogicEngine.link(*sourceOutput, *targetInput)); + { + auto errors = otherLogicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_EQ("LogicNode 'SourceScript' is not an instance of this LogicEngine", errors[0].message); + } + } + + TEST_F(ALogicEngine_Linking, PropagatesValuesFromMultipleOutputScriptsToOneInputScript) + { + const auto sourceScript = R"( + function interface(IN,OUT) + IN.floatInput = Type:Float() + OUT.floatOutput = Type:Float() + end + function run(IN,OUT) + OUT.floatOutput = IN.floatInput + end + )"; + const auto targetScript = R"( + function interface(IN,OUT) + IN.floatInput1 = Type:Float() + IN.floatInput2 = Type:Float() + OUT.floatOutput1 = Type:Float() + OUT.floatOutput2 = Type:Float() + end + function run(IN,OUT) + OUT.floatOutput1 = IN.floatInput1 + OUT.floatOutput2 = IN.floatInput2 + end + )"; + + auto scriptA = m_logicEngine.createLuaScript(sourceScript); + auto scriptB = m_logicEngine.createLuaScript(sourceScript); + auto scriptC = m_logicEngine.createLuaScript(targetScript); + + auto inputA = scriptA->getInputs()->getChild("floatInput"); + auto outputA = scriptA->getOutputs()->getChild("floatOutput"); + auto inputB = scriptB->getInputs()->getChild("floatInput"); + auto outputB = scriptB->getOutputs()->getChild("floatOutput"); + + auto inputC1 = scriptC->getInputs()->getChild("floatInput1"); + auto inputC2 = scriptC->getInputs()->getChild("floatInput2"); + auto outputC1 = scriptC->getOutputs()->getChild("floatOutput1"); + auto outputC2 = scriptC->getOutputs()->getChild("floatOutput2"); + + m_logicEngine.link(*outputA, *inputC1); + m_logicEngine.link(*outputB, *inputC2); + + inputA->set(42.f); + inputB->set(24.f); + + m_logicEngine.update(); + + EXPECT_FLOAT_EQ(42.f, *outputC1->get()); + EXPECT_FLOAT_EQ(24.f, *outputC2->get()); + } + + TEST_F(ALogicEngine_Linking, PropagatesValuesFromOutputScriptToMultipleInputScripts) + { + const auto scriptSource = R"( + function interface(IN,OUT) + IN.floatInput = Type:Float() + OUT.floatOutput = Type:Float() + end + function run(IN,OUT) + OUT.floatOutput = IN.floatInput + end + )"; + + auto scriptA = m_logicEngine.createLuaScript(scriptSource); + auto scriptB = m_logicEngine.createLuaScript(scriptSource); + auto scriptC = m_logicEngine.createLuaScript(scriptSource); + + auto inputA = scriptA->getInputs()->getChild("floatInput"); + auto outputA = scriptA->getOutputs()->getChild("floatOutput"); + auto inputB = scriptB->getInputs()->getChild("floatInput"); + auto outputB = scriptB->getOutputs()->getChild("floatOutput"); + auto inputC = scriptC->getInputs()->getChild("floatInput"); + auto outputC = scriptC->getOutputs()->getChild("floatOutput"); + + m_logicEngine.link(*outputA, *inputB); + m_logicEngine.link(*outputA, *inputC); + + inputA->set(42.f); + + m_logicEngine.update(); + + EXPECT_FLOAT_EQ(42.f, *outputB->get()); + EXPECT_FLOAT_EQ(42.f, *outputC->get()); + } + + TEST_F(ALogicEngine_Linking, PropagatesOutputToMultipleScriptsWithMultipleInputs) + { + const auto sourceScript = R"( + function interface(IN,OUT) + IN.floatInput = Type:Float() + OUT.floatOutput = Type:Float() + end + function run(IN,OUT) + OUT.floatOutput = IN.floatInput + end + )"; + const auto targetScript = R"( + function interface(IN,OUT) + IN.floatInput1 = Type:Float() + IN.floatInput2 = Type:Float() + OUT.floatOutput1 = Type:Float() + OUT.floatOutput2 = Type:Float() + end + function run(IN,OUT) + OUT.floatOutput1 = IN.floatInput1 + OUT.floatOutput2 = IN.floatInput2 + end + )"; + + auto scriptA = m_logicEngine.createLuaScript(sourceScript); + auto scriptB = m_logicEngine.createLuaScript(targetScript); + auto scriptC = m_logicEngine.createLuaScript(targetScript); + + auto inputA = scriptA->getInputs()->getChild("floatInput"); + auto outputA = scriptA->getOutputs()->getChild("floatOutput"); + + auto inputB1 = scriptB->getInputs()->getChild("floatInput1"); + auto inputB2 = scriptB->getInputs()->getChild("floatInput2"); + auto outputB1 = scriptB->getOutputs()->getChild("floatOutput1"); + auto outputB2 = scriptB->getOutputs()->getChild("floatOutput2"); + auto inputC1 = scriptC->getInputs()->getChild("floatInput1"); + auto inputC2 = scriptC->getInputs()->getChild("floatInput2"); + auto outputC1 = scriptC->getOutputs()->getChild("floatOutput1"); + auto outputC2 = scriptC->getOutputs()->getChild("floatOutput2"); + + m_logicEngine.link(*outputA, *inputB1); + m_logicEngine.link(*outputA, *inputB2); + m_logicEngine.link(*outputA, *inputC1); + m_logicEngine.link(*outputA, *inputC2); + + inputA->set(42.f); + + m_logicEngine.update(); + + EXPECT_FLOAT_EQ(42.f, *outputB1->get()); + EXPECT_FLOAT_EQ(42.f, *outputB2->get()); + EXPECT_FLOAT_EQ(42.f, *outputC1->get()); + EXPECT_FLOAT_EQ(42.f, *outputC2->get()); + } + + TEST_F(ALogicEngine_Linking, DoesNotPropagateValuesIfScriptIsDestroyed) + { + const auto scriptSource = R"( + function interface(IN,OUT) + IN.floatInput = Type:Float() + OUT.floatOutput = Type:Float() + end + function run(IN,OUT) + OUT.floatOutput = IN.floatInput + end + )"; + + auto scriptA = m_logicEngine.createLuaScript(scriptSource); + auto scriptB = m_logicEngine.createLuaScript(scriptSource); + auto scriptC = m_logicEngine.createLuaScript(scriptSource); + + auto inputA = scriptA->getInputs()->getChild("floatInput"); + auto outputA = scriptA->getOutputs()->getChild("floatOutput"); + auto inputB = scriptB->getInputs()->getChild("floatInput"); + auto outputB = scriptB->getOutputs()->getChild("floatOutput"); + auto inputC = scriptC->getInputs()->getChild("floatInput"); + auto outputC = scriptC->getOutputs()->getChild("floatOutput"); + + m_logicEngine.link(*outputA, *inputB); + m_logicEngine.link(*outputB, *inputC); + + m_logicEngine.destroy(*scriptB); + + inputA->set(42.f); + + m_logicEngine.update(); + + EXPECT_FLOAT_EQ(42.f, *outputA->get()); + EXPECT_FLOAT_EQ(0.f, *inputC->get()); + EXPECT_FLOAT_EQ(0.f, *outputC->get()); + } + + TEST_F(ALogicEngine_Linking, LinksNestedPropertiesBetweenScripts) + { + const auto srcScriptA = R"( + function interface(IN,OUT) + OUT.output = Type:String() + OUT.nested = { + str1 = Type:String(), + str2 = Type:String() + } + end + function run(IN,OUT) + OUT.output = "foo" + OUT.nested = {str1 = "str1", str2 = "str2"} + end + )"; + const auto srcScriptB = R"( + function interface(IN,OUT) + IN.input = Type:String() + IN.nested = { + str1 = Type:String(), + str2 = Type:String() + } + OUT.concat_all = Type:String() + end + function run(IN,OUT) + OUT.concat_all = IN.input .. " {" .. IN.nested.str1 .. ", " .. IN.nested.str2 .. "}" + end + )"; + + // Create scripts in reversed order to make it more likely that order will be wrong unless ordered by dependencies + auto scriptB = m_logicEngine.createLuaScript(srcScriptB); + auto scriptA = m_logicEngine.createLuaScript(srcScriptA); + + auto scriptAOutput = scriptA->getOutputs()->getChild("output"); + auto scriptAnested_str1 = scriptA->getOutputs()->getChild("nested")->getChild("str1"); + auto scriptAnested_str2 = scriptA->getOutputs()->getChild("nested")->getChild("str2"); + + auto scriptBInput = scriptB->getInputs()->getChild("input"); + auto scriptBnested_str1 = scriptB->getInputs()->getChild("nested")->getChild("str1"); + auto scriptBnested_str2 = scriptB->getInputs()->getChild("nested")->getChild("str2"); + + // Do a crossover link between nested property and non-nested property + EXPECT_TRUE(m_logicEngine.link(*scriptAOutput, *scriptBnested_str1)); + EXPECT_TRUE(m_logicEngine.link(*scriptAnested_str1, *scriptBInput)); + EXPECT_TRUE(m_logicEngine.link(*scriptAnested_str2, *scriptBnested_str2)); + + EXPECT_TRUE(m_logicEngine.update()); + + auto scriptB_concatenated = scriptB->getOutputs()->getChild("concat_all"); + EXPECT_EQ(std::string("str1 {foo, str2}"), *scriptB_concatenated->get()); + } + + TEST_F(ALogicEngine_Linking, LinksNestedScriptPropertiesToBindingInputs) + { + const auto scriptSrc = R"( + function interface(IN,OUT) + OUT.nested = { + bool = Type:Bool(), + vec3f = Type:Vec3f() + } + end + function run(IN,OUT) + OUT.nested = {bool = false, vec3f = {0.1, 0.2, 0.3}} + end + )"; + + auto script = m_logicEngine.createLuaScript(scriptSrc); + // TODO Violin add appearance binding here too, once test PR #305 is merged + auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + + auto nestedOutput_bool = script->getOutputs()->getChild("nested")->getChild("bool"); + auto nestedOutput_vec3f = script->getOutputs()->getChild("nested")->getChild("vec3f"); + + auto nodeBindingInput_bool = nodeBinding->getInputs()->getChild("visibility"); + auto nodeBindingInput_vec3f = nodeBinding->getInputs()->getChild("translation"); + + ASSERT_TRUE(m_logicEngine.link(*nestedOutput_bool, *nodeBindingInput_bool)); + ASSERT_TRUE(m_logicEngine.link(*nestedOutput_vec3f, *nodeBindingInput_vec3f)); + + ASSERT_TRUE(m_logicEngine.update()); + + EXPECT_EQ(false, *nodeBindingInput_bool->get()); + EXPECT_EQ(*nodeBindingInput_vec3f->get(), vec3f(0.1f, 0.2f, 0.3f)); + } + + TEST_F(ALogicEngine_Linking, PropagatesValuesCorrectlyAfterUnlink) + { + /* + * --> ScriptB + * / \ + * ScriptA ---------------->ScriptC + */ + + const auto sourceScript = R"( + function interface(IN,OUT) + IN.floatInput = Type:Float() + OUT.floatOutput = Type:Float() + end + function run(IN,OUT) + OUT.floatOutput = IN.floatInput + end + )"; + const auto targetScript = R"( + function interface(IN,OUT) + IN.floatInput1 = Type:Float() + IN.floatInput2 = Type:Float() + OUT.floatOutput1 = Type:Float() + OUT.floatOutput2 = Type:Float() + end + function run(IN,OUT) + OUT.floatOutput1 = IN.floatInput1 + OUT.floatOutput2 = IN.floatInput2 + end + )"; + + auto scriptA = m_logicEngine.createLuaScript(sourceScript); + auto scriptB = m_logicEngine.createLuaScript(sourceScript); + auto scriptC = m_logicEngine.createLuaScript(targetScript); + + auto scriptAInput = scriptA->getInputs()->getChild("floatInput"); + auto scriptAOutput = scriptA->getOutputs()->getChild("floatOutput"); + + auto scriptBInput = scriptB->getInputs()->getChild("floatInput"); + auto scriptBOutput = scriptB->getOutputs()->getChild("floatOutput"); + + auto scriptCInput1 = scriptC->getInputs()->getChild("floatInput1"); + auto scriptCInput2 = scriptC->getInputs()->getChild("floatInput2"); + auto scriptCOutput1 = scriptC->getOutputs()->getChild("floatOutput1"); + auto scriptCOutput2 = scriptC->getOutputs()->getChild("floatOutput2"); + + m_logicEngine.link(*scriptAOutput, *scriptBInput); + m_logicEngine.link(*scriptAOutput, *scriptCInput1); + m_logicEngine.link(*scriptBOutput, *scriptCInput2); + + scriptAInput->set(42.f); + + m_logicEngine.update(); + + EXPECT_FLOAT_EQ(42.f, *scriptCOutput1->get()); + EXPECT_FLOAT_EQ(42.f, *scriptCOutput2->get()); + + /* + * ScriptB + * \ + * ScriptA ----------->ScriptC + */ + m_logicEngine.unlink(*scriptAOutput, *scriptBInput); + + scriptBInput->set(23.f); + + m_logicEngine.update(); + + EXPECT_FLOAT_EQ(42.f, *scriptCOutput1->get()); + EXPECT_FLOAT_EQ(23.f, *scriptCOutput2->get()); + } + + TEST_F(ALogicEngine_Linking, canDestroyBindingAfterUnlinkingFromScript) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + + auto* outScript = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.out_vec3f = Type:Vec3f() + end + + function run(IN,OUT) + OUT.out_vec3f = { 0.0, 0.0, 0.0 } + end + )"); + + const Property* nodeTranslation = nodeBinding.getInputs()->getChild("translation"); + const Property* scriptOutVec3f = outScript->getOutputs()->getChild("out_vec3f"); + + EXPECT_TRUE(m_logicEngine.link(*scriptOutVec3f, *nodeTranslation)); + EXPECT_TRUE(m_logicEngine.unlink(*scriptOutVec3f, *nodeTranslation)); + + EXPECT_FALSE(m_logicEngine.m_impl->getApiObjects().getLogicNodeDependencies().isLinked(nodeBinding.m_impl)); + EXPECT_FALSE(m_logicEngine.m_impl->getApiObjects().getLogicNodeDependencies().isLinked(outScript->m_impl)); + EXPECT_FALSE(m_logicEngine.m_impl->getApiObjects().getLogicNodeDependencies().isLinked(nodeBinding.m_impl)); + EXPECT_FALSE(m_logicEngine.m_impl->getApiObjects().getLogicNodeDependencies().isLinked(outScript->m_impl)); + EXPECT_FALSE(m_logicEngine.m_impl->isLinked(*outScript)); + EXPECT_FALSE(m_logicEngine.m_impl->isLinked(nodeBinding)); + + EXPECT_TRUE(m_logicEngine.destroy(nodeBinding)); + + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALogicEngine_Linking, WHEN_ScriptWasUnlinkedFromBindingAndMultipleLinksDestroyed_THEN_UpdateDoesNotOverwriteBindingInputsByDanglingLinks) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + + auto* outScript = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.translation = Type:Vec3f() + OUT.visibility = Type:Bool() + end + + function run(IN,OUT) + end + )"); + + ramses::Property* nodeTranslation = nodeBinding.getInputs()->getChild("translation"); + ramses::Property* nodeVisibility = nodeBinding.getInputs()->getChild("visibility"); + + const ramses::Property* scriptTranslation = outScript->getOutputs()->getChild("translation"); + const ramses::Property* scriptVisiblity = outScript->getOutputs()->getChild("visibility"); + + EXPECT_TRUE(m_logicEngine.link(*scriptVisiblity, *nodeVisibility)); + EXPECT_TRUE(m_logicEngine.link(*scriptTranslation, *nodeTranslation)); + + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_TRUE(m_logicEngine.unlink(*scriptVisiblity, *nodeVisibility)); + EXPECT_TRUE(m_logicEngine.unlink(*scriptTranslation, *nodeTranslation)); + + EXPECT_FALSE(m_logicEngine.m_impl->getApiObjects().getLogicNodeDependencies().isLinked(nodeBinding.m_impl)); + EXPECT_FALSE(m_logicEngine.m_impl->getApiObjects().getLogicNodeDependencies().isLinked(outScript->m_impl)); + EXPECT_FALSE(m_logicEngine.m_impl->getApiObjects().getLogicNodeDependencies().isLinked(nodeBinding.m_impl)); + EXPECT_FALSE(m_logicEngine.m_impl->getApiObjects().getLogicNodeDependencies().isLinked(outScript->m_impl)); + EXPECT_FALSE(m_logicEngine.m_impl->isLinked(*outScript)); + EXPECT_FALSE(m_logicEngine.m_impl->isLinked(nodeBinding)); + + EXPECT_TRUE(m_logicEngine.destroy(*outScript)); + + // Set some custom values after unlink + const vec3f translationValues{ 11.f, 12.0f, 13.0f }; + nodeTranslation->set(translationValues); + nodeVisibility->set(false); + + EXPECT_TRUE(m_logicEngine.update()); + + // Check that custom values are kept + EXPECT_EQ(*nodeTranslation->get(), translationValues); + EXPECT_FALSE(*nodeVisibility->get()); + } + + TEST_F(ALogicEngine_Linking, PropagatesValuesInACycleAcrossMultipleLinksAndWeakLink) + { + auto scriptSource = R"( + function interface(IN,OUT) + IN.inString1 = Type:String() + IN.inString2 = Type:String() + OUT.outString = Type:String() + end + function run(IN,OUT) + OUT.outString = IN.inString1 .. IN.inString2 + end + )"; + + auto script1 = m_logicEngine.createLuaScript(scriptSource); + auto script2 = m_logicEngine.createLuaScript(scriptSource); + auto script3 = m_logicEngine.createLuaScript(scriptSource); + + auto script1Input1 = script1->getInputs()->getChild("inString1"); + auto script1Input2 = script1->getInputs()->getChild("inString2"); + auto script2Input1 = script2->getInputs()->getChild("inString1"); + auto script2Input2 = script2->getInputs()->getChild("inString2"); + auto script3Input1 = script3->getInputs()->getChild("inString1"); + auto script3Input2 = script3->getInputs()->getChild("inString2"); + auto script1Output = script1->getOutputs()->getChild("outString"); + auto script2Output = script2->getOutputs()->getChild("outString"); + auto script3Output = script3->getOutputs()->getChild("outString"); + + m_logicEngine.link(*script1Output, *script2Input1); + m_logicEngine.link(*script2Output, *script3Input1); + m_logicEngine.linkWeak(*script3Output, *script1Input1); + + script1Input2->set(std::string("A")); + script2Input2->set(std::string("B")); + script3Input2->set(std::string("C")); + + // during 1st update the weak link has no value yet but will mark script1 dirty + m_logicEngine.update(); + EXPECT_EQ("ABC", script3Output->get()); + // every upcoming update will concatenate result from previous update with this update + m_logicEngine.update(); + EXPECT_EQ("ABCABC", script3Output->get()); + m_logicEngine.update(); + EXPECT_EQ("ABCABCABC", script3Output->get()); + m_logicEngine.update(); + EXPECT_EQ("ABCABCABCABC", script3Output->get()); + } + + class ALogicEngine_Linking_WithFiles : public ALogicEngine_Linking + { + protected: + WithTempDirectory tempFolder; + }; + + TEST_F(ALogicEngine_Linking_WithFiles, PreservesLinksBetweenScriptsAfterSavingAndLoading) + { + { + /* + * -> ScriptB -- + * / \ + * ScriptA ------------------> ScriptC + */ + + LogicEngine tmpLogicEngine{ m_logicEngine.getFeatureLevel() }; + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Base); + const auto srcScriptAB = R"( + function interface(IN,OUT) + IN.input = Type:String() + OUT.output = Type:String() + end + function run(IN,OUT) + OUT.output = "forward " .. tostring(IN.input) + end + )"; + const auto srcScriptCsrc = R"( + function interface(IN,OUT) + IN.fromA = Type:String() + IN.fromB = Type:String() + OUT.concatenate_AB = Type:String() + end + function run(IN,OUT) + OUT.concatenate_AB = "A: " .. IN.fromA .. " & B: " .. IN.fromB + end + )"; + + // Create them in reversed order to make sure they are ordered wrongly if not ordered explicitly + auto scriptC = tmpLogicEngine.createLuaScript(srcScriptCsrc, config, "ScriptC"); + auto scriptB = tmpLogicEngine.createLuaScript(srcScriptAB, config, "ScriptB"); + auto scriptA = tmpLogicEngine.createLuaScript(srcScriptAB, config, "ScriptA"); + + auto scriptAInput = scriptA->getInputs()->getChild("input"); + auto scriptAOutput = scriptA->getOutputs()->getChild("output"); + + auto scriptBInput = scriptB->getInputs()->getChild("input"); + auto scriptBOutput = scriptB->getOutputs()->getChild("output"); + + auto scriptC_fromA = scriptC->getInputs()->getChild("fromA"); + auto scriptC_fromB = scriptC->getInputs()->getChild("fromB"); + auto scriptC_concatenate_AB = scriptC->getOutputs()->getChild("concatenate_AB"); + + tmpLogicEngine.link(*scriptAOutput, *scriptBInput); + tmpLogicEngine.linkWeak(*scriptAOutput, *scriptC_fromA); + tmpLogicEngine.link(*scriptBOutput, *scriptC_fromB); + + scriptAInput->set("'From A'"); + + tmpLogicEngine.update(); + + ASSERT_EQ(std::string("A: forward 'From A' & B: forward forward 'From A'"), *scriptC_concatenate_AB->get()); + + ASSERT_TRUE(SaveToFileWithoutValidation(tmpLogicEngine, "links.bin")); + } + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("links.bin")); + + // Load all scripts and their properties + auto scriptC = m_logicEngine.findByName("ScriptC"); + auto scriptB = m_logicEngine.findByName("ScriptB"); + auto scriptA = m_logicEngine.findByName("ScriptA"); + + auto scriptAInput = scriptA->getInputs()->getChild("input"); + auto scriptAOutput = scriptA->getOutputs()->getChild("output"); + + auto scriptBInput = scriptB->getInputs()->getChild("input"); + auto scriptBOutput = scriptB->getOutputs()->getChild("output"); + + auto scriptC_fromA = scriptC->getInputs()->getChild("fromA"); + auto scriptC_fromB = scriptC->getInputs()->getChild("fromB"); + auto scriptC_concatenate_AB = scriptC->getOutputs()->getChild("concatenate_AB"); + + // Internal check that deserialization did not result in more link copies + PropertyLinkTestUtils::ExpectLinks(m_logicEngine, { + { scriptAOutput, scriptBInput, false }, + { scriptAOutput, scriptC_fromA, true }, + { scriptBOutput, scriptC_fromB, false } + }); + + // Before update, values should be still as before saving + EXPECT_EQ(std::string("forward 'From A'"), *scriptAOutput->get()); + EXPECT_EQ(std::string("forward forward 'From A'"), *scriptBOutput->get()); + EXPECT_EQ(std::string("A: forward 'From A' & B: forward forward 'From A'"), *scriptC_concatenate_AB->get()); + + EXPECT_TRUE(m_logicEngine.update()); + + // Values should be still the same - because the data didn't change + EXPECT_EQ(std::string("forward 'From A'"), *scriptAOutput->get()); + EXPECT_EQ(std::string("forward forward 'From A'"), *scriptBOutput->get()); + EXPECT_EQ(std::string("A: forward 'From A' & B: forward forward 'From A'"), *scriptC_concatenate_AB->get()); + + // Set different data manually + EXPECT_TRUE(scriptAInput->set("'A++'")); + // linked inputs cannot be set manually, so this will fail + EXPECT_FALSE(scriptBInput->set("xxx")); + EXPECT_FALSE(scriptC_fromA->set("yyy")); + EXPECT_FALSE(scriptC_fromB->set("zzz")); + + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_EQ(std::string("forward 'A++'"), *scriptAOutput->get()); + EXPECT_EQ(std::string("forward forward 'A++'"), *scriptBOutput->get()); + EXPECT_EQ(std::string("A: forward 'A++' & B: forward forward 'A++'"), *scriptC_concatenate_AB->get()); + } + } + + TEST_F(ALogicEngine_Linking_WithFiles, PreservesNestedLinksBetweenScriptsAfterSavingAndLoading) + { + { + LogicEngine tmpLogicEngine{ m_logicEngine.getFeatureLevel() }; + const auto srcScriptA = R"( + function interface(IN,OUT) + IN.appendixNestedStr2 = Type:String() + OUT.output = Type:String() + OUT.nested = { + str1 = Type:String(), + str2 = Type:String() + } + end + function run(IN,OUT) + OUT.output = "foo" + OUT.nested = {str1 = "str1", str2 = "str2" .. IN.appendixNestedStr2} + end + )"; + const auto srcScriptB = R"( + function interface(IN,OUT) + IN.input = Type:String() + IN.nested = { + str1 = Type:String(), + str2 = Type:String() + } + OUT.concat_all = Type:String() + end + function run(IN,OUT) + OUT.concat_all = IN.input .. " {" .. IN.nested.str1 .. ", " .. IN.nested.str2 .. "}" + end + )"; + + // Create scripts in reversed order to make it more likely that order will be wrong unless ordered by dependencies + auto scriptB = tmpLogicEngine.createLuaScript(srcScriptB, {}, "ScriptB"); + auto scriptA = tmpLogicEngine.createLuaScript(srcScriptA, {}, "ScriptA"); + + auto scriptAOutput = scriptA->getOutputs()->getChild("output"); + auto scriptAnested_str1 = scriptA->getOutputs()->getChild("nested")->getChild("str1"); + auto scriptAnested_str2 = scriptA->getOutputs()->getChild("nested")->getChild("str2"); + + auto scriptBInput = scriptB->getInputs()->getChild("input"); + auto scriptBnested_str1 = scriptB->getInputs()->getChild("nested")->getChild("str1"); + auto scriptBnested_str2 = scriptB->getInputs()->getChild("nested")->getChild("str2"); + + // Do a crossover link between nested property and non-nested property + ASSERT_TRUE(tmpLogicEngine.link(*scriptAOutput, *scriptBnested_str1)); + ASSERT_TRUE(tmpLogicEngine.link(*scriptAnested_str1, *scriptBInput)); + ASSERT_TRUE(tmpLogicEngine.link(*scriptAnested_str2, *scriptBnested_str2)); + + ASSERT_TRUE(tmpLogicEngine.update()); + + auto scriptB_concatenated = scriptB->getOutputs()->getChild("concat_all"); + ASSERT_EQ(std::string("str1 {foo, str2}"), *scriptB_concatenated->get()); + + ASSERT_TRUE(SaveToFileWithoutValidation(tmpLogicEngine, "nested_links.bin")); + } + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("nested_links.bin")); + + // Load all scripts and their properties + auto scriptA = m_logicEngine.findByName("ScriptA"); + auto scriptB = m_logicEngine.findByName("ScriptB"); + + auto scriptAOutput = scriptA->getOutputs()->getChild("output"); + auto scriptAnested_str1 = scriptA->getOutputs()->getChild("nested")->getChild("str1"); + auto scriptAnested_str2 = scriptA->getOutputs()->getChild("nested")->getChild("str2"); + + auto scriptBInput = scriptB->getInputs()->getChild("input"); + auto scriptBnested_str1 = scriptB->getInputs()->getChild("nested")->getChild("str1"); + auto scriptBnested_str2 = scriptB->getInputs()->getChild("nested")->getChild("str2"); + auto scriptB_concatenated = scriptB->getOutputs()->getChild("concat_all"); + + // Internal check that deserialization did not result in more link copies + PropertyLinkTestUtils::ExpectLinks(m_logicEngine, { + { scriptAOutput, scriptBnested_str1, false }, + { scriptAnested_str1, scriptBInput, false }, + { scriptAnested_str2, scriptBnested_str2, false } + }); + + // Before update, values should be still as before saving + EXPECT_EQ(std::string("foo"), *scriptAOutput->get()); + EXPECT_EQ(std::string("str1"), *scriptAnested_str1->get()); + EXPECT_EQ(std::string("str2"), *scriptAnested_str2->get()); + EXPECT_EQ(std::string("str1"), *scriptBInput->get()); + EXPECT_EQ(std::string("foo"), *scriptBnested_str1->get()); + EXPECT_EQ(std::string("str2"), *scriptBnested_str2->get()); + EXPECT_EQ(std::string("str1 {foo, str2}"), *scriptB_concatenated->get()); + + EXPECT_TRUE(m_logicEngine.update()); + + // Values should be still the same - because the data didn't change + EXPECT_EQ(std::string("str1 {foo, str2}"), *scriptB_concatenated->get()); + + // Set different data manually + auto scriptAappendix = scriptA->getInputs()->getChild("appendixNestedStr2"); + EXPECT_TRUE(scriptAappendix->set("!bar")); + // linked inputs cannot be set manually, so this will fail + EXPECT_FALSE(scriptBInput->set("xxx")); + EXPECT_FALSE(scriptBnested_str1->set("yyy")); + EXPECT_FALSE(scriptBnested_str2->set("zzz")); + + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_EQ(std::string("str1 {foo, str2!bar}"), *scriptB_concatenated->get()); + } + } + + TEST_F(ALogicEngine_Linking_WithFiles, PropagatesValuesInACycleAcrossMultipleLinksAndWeakLinkAfterSavingAndLoading) + { + { + constexpr auto scriptSource = R"( + function interface(IN,OUT) + IN.inString1 = Type:String() + IN.inString2 = Type:String() + OUT.outString = Type:String() + end + function run(IN,OUT) + OUT.outString = IN.inString1 .. IN.inString2 + end)"; + + LogicEngine tmpLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto script1 = tmpLogicEngine.createLuaScript(scriptSource, {}, "scriptA"); + auto script2 = tmpLogicEngine.createLuaScript(scriptSource, {}, "scriptB"); + auto script3 = tmpLogicEngine.createLuaScript(scriptSource, {}, "scriptC"); + + auto script1Input1 = script1->getInputs()->getChild("inString1"); + auto script1Input2 = script1->getInputs()->getChild("inString2"); + auto script2Input1 = script2->getInputs()->getChild("inString1"); + auto script2Input2 = script2->getInputs()->getChild("inString2"); + auto script3Input1 = script3->getInputs()->getChild("inString1"); + auto script3Input2 = script3->getInputs()->getChild("inString2"); + auto script1Output = script1->getOutputs()->getChild("outString"); + auto script2Output = script2->getOutputs()->getChild("outString"); + auto script3Output = script3->getOutputs()->getChild("outString"); + + tmpLogicEngine.link(*script1Output, *script2Input1); + tmpLogicEngine.link(*script2Output, *script3Input1); + tmpLogicEngine.linkWeak(*script3Output, *script1Input1); + + script1Input2->set(std::string("A")); + script2Input2->set(std::string("B")); + script3Input2->set(std::string("C")); + + tmpLogicEngine.update(); + // during 1st update the weak link has no value yet so there is no concatenation from previous update + EXPECT_STREQ("ABC", script3Output->get()->c_str()); + + ASSERT_TRUE(SaveToFileWithoutValidation(tmpLogicEngine, "weaklinks.bin")); + } + + ASSERT_TRUE(m_logicEngine.loadFromFile("weaklinks.bin")); + const auto script3 = m_logicEngine.findByName("scriptC"); + const auto script3Output = script3->getOutputs()->getChild("outString"); + EXPECT_STREQ("ABC", script3Output->get()->c_str()); + + // right after deserialization the weak link will propagate whatever value was serialized + // therefore already 1st update after loading concatenates + m_logicEngine.update(); + EXPECT_STREQ("ABCABC", script3Output->get()->c_str()); + // every upcoming update will concatenate result from previous update with this update + m_logicEngine.update(); + EXPECT_STREQ("ABCABCABC", script3Output->get()->c_str()); + m_logicEngine.update(); + EXPECT_STREQ("ABCABCABCABC", script3Output->get()->c_str()); + } + + class ALogicEngine_Linking_WithBindings : public ALogicEngine_Linking_WithFiles + { + protected: + using ENodePropertyStaticIndex = ramses::internal::ENodePropertyStaticIndex; + + static void ExpectValues(ramses::Node& node, ENodePropertyStaticIndex prop, vec3f expectedValues) + { + vec3f values = { 0.0f, 0.0f, 0.0f }; + if (prop == ENodePropertyStaticIndex::Rotation) + { + node.getRotation(values); + } + if (prop == ENodePropertyStaticIndex::Translation) + { + node.getTranslation(values); + } + if (prop == ENodePropertyStaticIndex::Scaling) + { + node.getScaling(values); + } + EXPECT_EQ(values, expectedValues); + } + + static void ExpectVec3f(ramses::Appearance& appearance, const char* uniformName, vec3f expectedValue) + { + ramses::vec3f value{ 0.0f, 0.0f, 0.0f }; + ramses::UniformInput uniform; + appearance.getEffect().findUniformInput(uniformName, uniform); + appearance.getInputValue(uniform, value); + EXPECT_EQ(expectedValue, value); + } + + ramses::Effect& createTestEffect(std::string_view vertShader, std::string_view fragShader) + { + ramses::EffectDescription effectDesc; + effectDesc.setVertexShader(vertShader.data()); + effectDesc.setFragmentShader(fragShader.data()); + return *m_scene->createEffect(effectDesc); + } + + ramses::Appearance& createTestAppearance(ramses::Effect& effect) + { + return *m_scene->createAppearance(effect); + } + + const std::string_view m_vertShader = R"( + #version 300 es + + uniform highp vec3 uniform1; + uniform highp vec3 uniform2; + + void main() + { + gl_Position = vec4(uniform1 + uniform2, 1.0); + })"; + + const std::string_view m_fragShader = R"( + #version 300 es + + out lowp vec4 color; + void main(void) + { + color = vec4(1.0, 0.0, 0.0, 1.0); + })"; + }; + + TEST_F(ALogicEngine_Linking_WithBindings, PreservesLinksToNodeBindingsAfterSavingAndLoadingFromFile) + { + auto ramsesNode1 = m_scene->createNode(); + auto ramsesNode2 = m_scene->createNode(); + + ramsesNode1->setTranslation({1.1f, 1.2f, 1.3f}); + ramsesNode1->setRotation({2.1f, 2.2f, 2.3f}, ramses::ERotationType::Euler_ZYX); + ramsesNode1->setScaling({3.1f, 3.2f, 3.3f}); + + ramsesNode2->setTranslation({11.1f, 11.2f, 11.3f}); + + { + LogicEngine tmpLogicEngine{ m_logicEngine.getFeatureLevel() }; + const auto scriptSrc = R"( + function interface(IN,OUT) + OUT.vec3f = Type:Vec3f() + OUT.visibility = Type:Bool() + end + function run(IN,OUT) + OUT.vec3f = {100.0, 200.0, 300.0} + OUT.visibility = false + end + )"; + + auto script = tmpLogicEngine.createLuaScript(scriptSrc); + auto nodeBinding1 = tmpLogicEngine.createRamsesNodeBinding(*ramsesNode1, ramses::ERotationType::Euler_XYZ, "NodeBinding1"); + auto nodeBinding2 = tmpLogicEngine.createRamsesNodeBinding(*ramsesNode2, ramses::ERotationType::Euler_XYZ, "NodeBinding2"); + + auto scriptOutputVec3f = script->getOutputs()->getChild("vec3f"); + auto scriptOutputBool = script->getOutputs()->getChild("visibility"); + auto binding1TranslationInput = nodeBinding1->getInputs()->getChild("translation"); + auto binding2RotationInput = nodeBinding2->getInputs()->getChild("rotation"); + auto binding1VisibilityInput = nodeBinding1->getInputs()->getChild("visibility"); + + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutputBool, *binding1VisibilityInput)); + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutputVec3f, *binding1TranslationInput)); + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutputVec3f, *binding2RotationInput)); + + ASSERT_TRUE(tmpLogicEngine.update()); + + ASSERT_EQ(*binding1TranslationInput->get(), vec3f(100.0f, 200.0f, 300.0f)); + ASSERT_EQ(*binding2RotationInput->get(), vec3f(100.0f, 200.0f, 300.0f)); + ASSERT_EQ(false, *binding1VisibilityInput->get()); + + ExpectValues(*ramsesNode1, ENodePropertyStaticIndex::Rotation, { 2.1f, 2.2f, 2.3f }); + ExpectValues(*ramsesNode1, ENodePropertyStaticIndex::Scaling, { 3.1f, 3.2f, 3.3f }); + ExpectValues(*ramsesNode1, ENodePropertyStaticIndex::Translation, { 100.0f, 200.0f, 300.0f }); + EXPECT_EQ(ramsesNode1->getVisibility(), ramses::EVisibilityMode::Invisible); + + ExpectValues(*ramsesNode2, ENodePropertyStaticIndex::Rotation, { 100.0f, 200.0f, 300.0f }); + ExpectValues(*ramsesNode2, ENodePropertyStaticIndex::Scaling, { 1.0f, 1.0f, 1.0f }); + ExpectValues(*ramsesNode2, ENodePropertyStaticIndex::Translation, { 11.1f, 11.2f, 11.3f }); + EXPECT_EQ(ramsesNode2->getVisibility(), ramses::EVisibilityMode::Visible); + + ASSERT_TRUE(SaveToFileWithoutValidation(tmpLogicEngine, "binding_links.bin")); + } + + // Make sure loading of bindings doesn't do anything to the node until update() is called + // To test that, we reset one node's properties to default + ramsesNode1->setTranslation({0.0f, 0.0f, 0.0f}); + ramsesNode1->setRotation({0.0f, 0.0f, 0.0f}); + ramsesNode1->setScaling({1.0f, 1.0f, 1.0f}); + ramsesNode1->setVisibility(ramses::EVisibilityMode::Visible); + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("binding_links.bin", m_scene)); + + ExpectValues(*ramsesNode1, ENodePropertyStaticIndex::Rotation, { 0.0f, 0.0f, 0.0f }); + ExpectValues(*ramsesNode1, ENodePropertyStaticIndex::Scaling, { 1.0f, 1.0f, 1.0f }); + ExpectValues(*ramsesNode1, ENodePropertyStaticIndex::Translation, { 0.0f, 0.0f, 0.0f }); + EXPECT_EQ(ramsesNode1->getVisibility(), ramses::EVisibilityMode::Visible); + + auto nodeBinding1 = m_logicEngine.findByName("NodeBinding1"); + auto nodeBinding2 = m_logicEngine.findByName("NodeBinding2"); + + auto binding1TranslationInput = nodeBinding1->getInputs()->getChild("translation"); + auto binding2RotationInput = nodeBinding2->getInputs()->getChild("rotation"); + auto notLinkedManualInputProperty = nodeBinding2->getInputs()->getChild("translation"); + auto bindingVisibilityInput = nodeBinding1->getInputs()->getChild("visibility"); + + // These values should be overwritten by the link - set them to a different value to make sure that happens + binding1TranslationInput->m_impl->setValue(vec3f{ 99.0f, 99.0f, 99.0f }); + binding2RotationInput->m_impl->setValue(vec3f{ 99.0f, 99.0f, 99.0f }); + bindingVisibilityInput->m_impl->setValue(true); + // This should not be overwritten, but should keep the manual value instead + ASSERT_TRUE(notLinkedManualInputProperty->set({100.0f, 101.0f, 102.0f})); + EXPECT_TRUE(m_logicEngine.update()); + + // These have default values + ExpectValues(*ramsesNode1, ENodePropertyStaticIndex::Rotation, { 0.0f, 0.0f, 0.0f }); + ExpectValues(*ramsesNode1, ENodePropertyStaticIndex::Scaling, { 1.0f, 1.0f, 1.0f }); + // These came over the link + ExpectValues(*ramsesNode1, ENodePropertyStaticIndex::Translation, { 100.0f, 200.0f, 300.0f }); + EXPECT_EQ(ramsesNode1->getVisibility(), ramses::EVisibilityMode::Invisible); + + // These came over the link + ExpectValues(*ramsesNode2, ENodePropertyStaticIndex::Rotation, { 100.0f, 200.0f, 300.0f }); + // These came over manual set after loading + ExpectValues(*ramsesNode2, ENodePropertyStaticIndex::Translation, { 100.0f, 101.0f, 102.0f }); + // These have default values + ExpectValues(*ramsesNode2, ENodePropertyStaticIndex::Scaling, { 1.0f, 1.0f, 1.0f }); + EXPECT_EQ(ramsesNode2->getVisibility(), ramses::EVisibilityMode::Visible); + } + } + + TEST_F(ALogicEngine_Linking_WithBindings, PreservesLinksToAppearanceBindingsAfterSavingAndLoadingFromFile) + { + ramses::Effect& effect = createTestEffect(m_vertShader, m_fragShader); + auto& appearance1 = createTestAppearance(effect); + auto& appearance2 = createTestAppearance(effect); + ramses::UniformInput uniform1; + ramses::UniformInput uniform2; + appearance1.getEffect().findUniformInput("uniform1", uniform1); + appearance1.getEffect().findUniformInput("uniform2", uniform2); + + appearance1.setInputValue(uniform1, ramses::vec3f{1.1f, 1.2f, 1.3f}); + appearance1.setInputValue(uniform2, ramses::vec3f{2.1f, 2.2f, 2.3f}); + appearance2.setInputValue(uniform1, ramses::vec3f{3.1f, 3.2f, 3.3f}); + appearance2.setInputValue(uniform2, ramses::vec3f{4.1f, 4.2f, 4.3f}); + + { + LogicEngine tmpLogicEngine{ m_logicEngine.getFeatureLevel() }; + const auto scriptSrc = R"( + function interface(IN,OUT) + OUT.uniform = Type:Vec3f() + end + function run(IN,OUT) + OUT.uniform = {100.0, 200.0, 300.0} + end + )"; + + auto script = tmpLogicEngine.createLuaScript(scriptSrc); + auto appBinding1 = tmpLogicEngine.createRamsesAppearanceBinding(appearance1, "AppBinding1"); + auto appBinding2 = tmpLogicEngine.createRamsesAppearanceBinding(appearance2, "AppBinding2"); + + auto scriptOutput = script->getOutputs()->getChild("uniform"); + auto binding1uniform1 = appBinding1->getInputs()->getChild("uniform1"); + auto binding2uniform1 = appBinding2->getInputs()->getChild("uniform1"); + auto binding2uniform2 = appBinding2->getInputs()->getChild("uniform2"); + + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutput, *binding1uniform1)); + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutput, *binding2uniform1)); + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutput, *binding2uniform2)); + + ASSERT_TRUE(tmpLogicEngine.update()); + + ExpectVec3f(appearance1, "uniform1", { 100.0f, 200.0f, 300.0f }); + ExpectVec3f(appearance1, "uniform2", { 2.1f, 2.2f, 2.3f }); + ExpectVec3f(appearance2, "uniform1", { 100.0f, 200.0f, 300.0f }); + ExpectVec3f(appearance2, "uniform2", { 100.0f, 200.0f, 300.0f }); + + ASSERT_TRUE(SaveToFileWithoutValidation(tmpLogicEngine, "binding_links.bin")); + } + + // Make sure loading of bindings doesn't do anything to the appearance until update() is called + // To test that, we reset both appearances' properties to zeroes + appearance1.setInputValue(uniform1, ramses::vec3f{0.0f, 0.0f, 0.0f}); + appearance1.setInputValue(uniform2, ramses::vec3f{0.0f, 0.0f, 0.0f}); + appearance2.setInputValue(uniform1, ramses::vec3f{0.0f, 0.0f, 0.0f}); + appearance2.setInputValue(uniform2, ramses::vec3f{0.0f, 0.0f, 0.0f}); + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("binding_links.bin", m_scene)); + + ExpectVec3f(appearance1, "uniform1", { 0.0f, 0.0f, 0.0f }); + ExpectVec3f(appearance1, "uniform2", { 0.0f, 0.0f, 0.0f }); + ExpectVec3f(appearance2, "uniform1", { 0.0f, 0.0f, 0.0f }); + ExpectVec3f(appearance2, "uniform2", { 0.0f, 0.0f, 0.0f }); + + auto appBinding1 = m_logicEngine.findByName("AppBinding1"); + auto appBinding2 = m_logicEngine.findByName("AppBinding2"); + + auto binding1uniform1 = appBinding1->getInputs()->getChild("uniform1"); + auto binding1uniform2 = appBinding1->getInputs()->getChild("uniform2"); + auto binding2uniform1 = appBinding2->getInputs()->getChild("uniform1"); + auto binding2uniform2 = appBinding2->getInputs()->getChild("uniform2"); + + // These values should be overwritten by the link - set them to a different value to make sure that happens + binding1uniform1->m_impl->setValue(vec3f{ 99.0f, 99.0f, 99.0f }); + // This should not be overwritten, but should keep the manual value instead, because no link points to it + ASSERT_TRUE(binding1uniform2->set({ 100.0f, 101.0f, 102.0f })); + // These values should be overwritten by the link - set them to a different value to make sure that happens + binding2uniform1->m_impl->setValue(vec3f{ 99.0f, 99.0f, 99.0f }); + binding2uniform2->m_impl->setValue(vec3f{ 99.0f, 99.0f, 99.0f }); + EXPECT_TRUE(m_logicEngine.update()); + + ExpectVec3f(appearance1, "uniform1", { 100.0f, 200.0f, 300.0f }); + ExpectVec3f(appearance1, "uniform2", { 100.0f, 101.0f, 102.0f }); + ExpectVec3f(appearance2, "uniform1", { 100.0f, 200.0f, 300.0f }); + ExpectVec3f(appearance2, "uniform2", { 100.0f, 200.0f, 300.0f }); + } + } + + TEST_F(ALogicEngine_Linking_WithBindings, PreservesLinksToCameraBindingsAfterSavingAndLoadingFromFile) + { + auto camera1 = m_scene->createPerspectiveCamera(); + auto camera2 = m_scene->createPerspectiveCamera(); + + camera1->setViewport(1, 2, 3u, 4u); + camera1->setFrustum(30.f, 640.f / 480.f, 1.f, 2.f); + camera2->setViewport(5, 6, 7u, 8u); + camera2->setFrustum(15.f, 320.f / 240.f, 3.f, 4.f); + + { + LogicEngine tmpLogicEngine{ m_logicEngine.getFeatureLevel() }; + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.vpOffsetX = Type:Int32() + OUT.farPlane = Type:Float() + end + function run(IN,OUT) + OUT.vpOffsetX = 19 + OUT.farPlane = 7.8 + end + )"; + + auto script = tmpLogicEngine.createLuaScript(scriptSrc); + auto cameraBinding1 = tmpLogicEngine.createRamsesCameraBinding(*camera1, "CameraBinding1"); + auto cameraBinding2 = tmpLogicEngine.createRamsesCameraBinding(*camera2, "CameraBinding2"); + + auto scriptOutputOffset = script->getOutputs()->getChild("vpOffsetX"); + auto scriptOutputFarPlane = script->getOutputs()->getChild("farPlane"); + auto binding1cameraProperty1 = cameraBinding1->getInputs()->getChild("viewport")->getChild("offsetX"); + auto binding2cameraProperty1 = cameraBinding2->getInputs()->getChild("viewport")->getChild("offsetX"); + auto binding2cameraProperty2 = cameraBinding2->getInputs()->getChild("frustum")->getChild("farPlane"); + + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutputOffset, *binding1cameraProperty1)); + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutputOffset, *binding2cameraProperty1)); + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutputFarPlane, *binding2cameraProperty2)); + + ASSERT_TRUE(tmpLogicEngine.update()); + + EXPECT_EQ(camera1->getViewportX(), 19); + EXPECT_EQ(camera1->getFarPlane(), 2.f); + EXPECT_EQ(camera2->getViewportX(), 19); + EXPECT_EQ(camera2->getFarPlane(), 7.8f); + + ASSERT_TRUE(SaveToFileWithoutValidation(tmpLogicEngine, "binding_links.bin")); + } + + // Make sure loading of bindings doesn't do anything to the camera until update() is called + // To test that, we reset both cameras' properties + camera1->setViewport(0, 0, 1u, 1u); + camera1->setFrustum(0.1f, 0.1f, .1f, .2f); + camera2->setViewport(0, 0, 1u, 1u); + camera2->setFrustum(0.1f, 0.1f, .1f, .2f); + { + ASSERT_TRUE(m_logicEngine.loadFromFile("binding_links.bin", m_scene)); + + EXPECT_EQ(camera1->getViewportX(), 0); + EXPECT_EQ(camera1->getFarPlane(), .2f); + EXPECT_EQ(camera2->getViewportX(), 0); + EXPECT_EQ(camera2->getFarPlane(), .2f); + + auto cameraBinding1 = m_logicEngine.findByName("CameraBinding1"); + auto cameraBinding2 = m_logicEngine.findByName("CameraBinding2"); + + auto binding1cameraProperty1 = cameraBinding1->getInputs()->getChild("viewport")->getChild("offsetX"); + auto binding1cameraProperty2 = cameraBinding1->getInputs()->getChild("frustum")->getChild("farPlane"); + auto binding2cameraProperty1 = cameraBinding2->getInputs()->getChild("viewport")->getChild("offsetX"); + auto binding2cameraProperty2 = cameraBinding2->getInputs()->getChild("frustum")->getChild("farPlane"); + + // These values should be overwritten by the link - set them to a different value to make sure that happens + binding1cameraProperty1->m_impl->setValue(100); + // This should not be overwritten, but should keep the manual value instead, because no link points to it + ASSERT_TRUE(binding1cameraProperty2->set(100.f)); + // These values should be overwritten by the link - set them to a different value to make sure that happens + binding2cameraProperty1->m_impl->setValue(100); + binding2cameraProperty2->m_impl->setValue(100.f); + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_EQ(camera1->getViewportX(), 19); + EXPECT_EQ(camera1->getFarPlane(), 100.f); + EXPECT_EQ(camera2->getViewportX(), 19); + EXPECT_EQ(camera2->getFarPlane(), 7.8f); + } + } + + TEST_F(ALogicEngine_Linking_WithBindings, PreservesLinksToRenderPassBindingsAfterSavingAndLoadingFromFile) + { + auto rp1 = m_scene->createRenderPass(); + auto rp2 = m_scene->createRenderPass(); + + rp1->setEnabled(true); + rp1->setRenderOrder(11); + rp2->setEnabled(false); + rp2->setRenderOrder(22); + + { + LogicEngine tmpLogicEngine{ m_logicEngine.getFeatureLevel() }; + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.enabled = Type:Bool() + OUT.order = Type:Int32() + end + function run(IN,OUT) + OUT.enabled = false + OUT.order = 33 + end + )"; + + auto script = tmpLogicEngine.createLuaScript(scriptSrc); + auto binding1 = tmpLogicEngine.createRamsesRenderPassBinding(*rp1, "RPBinding1"); + auto binding2 = tmpLogicEngine.createRamsesRenderPassBinding(*rp2, "RPBinding2"); + + auto scriptOutputEnabled = script->getOutputs()->getChild("enabled"); + auto scriptOutputOrder = script->getOutputs()->getChild("order"); + auto binding1Property1 = binding1->getInputs()->getChild("enabled"); + auto binding2Property1 = binding2->getInputs()->getChild("enabled"); + auto binding2Property2 = binding2->getInputs()->getChild("renderOrder"); + + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutputEnabled, *binding1Property1)); + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutputEnabled, *binding2Property1)); + ASSERT_TRUE(tmpLogicEngine.link(*scriptOutputOrder, *binding2Property2)); + + ASSERT_TRUE(tmpLogicEngine.update()); + + EXPECT_FALSE(rp1->isEnabled()); + EXPECT_EQ(rp1->getRenderOrder(), 11); + EXPECT_FALSE(rp2->isEnabled()); + EXPECT_EQ(rp2->getRenderOrder(), 33); + + ASSERT_TRUE(SaveToFileWithoutValidation(tmpLogicEngine, "binding_links.bin")); + } + + // Make sure loading of bindings doesn't do anything to the render pass until update() is called + // To test that, we reset both render passes' properties + rp1->setEnabled(true); + rp1->setRenderOrder(0); + rp2->setEnabled(true); + rp2->setRenderOrder(0); + { + ASSERT_TRUE(m_logicEngine.loadFromFile("binding_links.bin", m_scene)); + + EXPECT_TRUE(rp1->isEnabled()); + EXPECT_EQ(rp1->getRenderOrder(), 0); + EXPECT_TRUE(rp2->isEnabled()); + EXPECT_EQ(rp2->getRenderOrder(), 0); + + auto binding1 = m_logicEngine.findByName("RPBinding1"); + auto binding2 = m_logicEngine.findByName("RPBinding2"); + + auto binding1Property1 = binding1->getInputs()->getChild("enabled"); + auto binding1Property2 = binding1->getInputs()->getChild("renderOrder"); + auto binding2Property1 = binding2->getInputs()->getChild("enabled"); + auto binding2Property2 = binding2->getInputs()->getChild("renderOrder"); + + // These values should be overwritten by the link - set them to a different value to make sure that happens + binding1Property1->m_impl->setValue(false); + // This should not be overwritten, but should keep the manual value instead, because no link points to it + ASSERT_TRUE(binding1Property2->set(100)); + // These values should be overwritten by the link - set them to a different value to make sure that happens + binding2Property1->m_impl->setValue(false); + binding2Property2->m_impl->setValue(100); + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_FALSE(rp1->isEnabled()); + EXPECT_EQ(rp1->getRenderOrder(), 100); + EXPECT_FALSE(rp2->isEnabled()); + EXPECT_EQ(rp2->getRenderOrder(), 33); + } + } + + TEST_F(ALogicEngine_Linking_WithBindings, PreservesLinksToRenderGroupBindingsAfterSavingAndLoadingFromFile) + { + m_renderGroup->addMeshNode(*m_meshNode, 42); + + { + LogicEngine tmpLogicEngine{ m_logicEngine.getFeatureLevel() }; + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.order = Type:Int32() + end + function run(IN,OUT) + OUT.order = 33 + end + )"; + + auto script = tmpLogicEngine.createLuaScript(scriptSrc); + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*m_meshNode)); + auto binding = tmpLogicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements, "binding"); + ASSERT_TRUE(tmpLogicEngine.link(*script->getOutputs()->getChild("order"), *binding->getInputs()->getChild("renderOrders")->getChild("meshNode"))); + + ASSERT_TRUE(tmpLogicEngine.update()); + + int32_t renderOrder = -1; + m_renderGroup->getMeshNodeOrder(*m_meshNode, renderOrder); + EXPECT_EQ(33, renderOrder); + + // script has no inputs linked, validatation would fail + ASSERT_TRUE(SaveToFileWithoutValidation(tmpLogicEngine, "binding_links.bin")); + } + + // Make sure loading of bindings doesn't do anything to the render group until update() is called + // To test that, we reset render group property + m_renderGroup->addMeshNode(*m_meshNode, 100); + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("binding_links.bin", m_scene)); + + int32_t renderOrder = -1; + m_renderGroup->getMeshNodeOrder(*m_meshNode, renderOrder); + EXPECT_EQ(100, renderOrder); + + auto binding = m_logicEngine.findByName("binding"); + + // This value should be overwritten by the link - set it to a different value to make sure that happens + binding->getInputs()->getChild("renderOrders")->getChild("meshNode")->set(222); + EXPECT_TRUE(m_logicEngine.update()); + + m_renderGroup->getMeshNodeOrder(*m_meshNode, renderOrder); + EXPECT_EQ(33, renderOrder); + } + } + + TEST_F(ALogicEngine_Linking, ReportsNodeAsLinked_IFF_ItHasIncomingOrOutgoingLinks) + { + auto scriptSource = R"( + function interface(IN,OUT) + IN.input = { + inBool = Type:Bool() + } + OUT.output = { + outBool = Type:Bool() + } + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(scriptSource); + auto middleScript = m_logicEngine.createLuaScript(scriptSource); + auto targetBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + + auto sourceOutputBool = sourceScript->getOutputs()->getChild("output")->getChild("outBool"); + auto middleInputBool = middleScript->getInputs()->getChild("input")->getChild("inBool"); + auto middleOutputBool = middleScript->getOutputs()->getChild("output")->getChild("outBool"); + auto targetInputBool = targetBinding->getInputs()->getChild("visibility"); + + m_logicEngine.link(*sourceOutputBool, *middleInputBool); + m_logicEngine.link(*middleOutputBool, *targetInputBool); + + EXPECT_TRUE(m_logicEngine.isLinked(*sourceScript)); + EXPECT_TRUE(m_logicEngine.isLinked(*middleScript)); + EXPECT_TRUE(m_logicEngine.isLinked(*targetBinding)); + + m_logicEngine.unlink(*middleOutputBool, *targetInputBool); + + EXPECT_TRUE(m_logicEngine.isLinked(*sourceScript)); + EXPECT_TRUE(m_logicEngine.isLinked(*middleScript)); + EXPECT_FALSE(m_logicEngine.isLinked(*targetBinding)); + + m_logicEngine.unlink(*sourceOutputBool, *middleInputBool); + + EXPECT_FALSE(m_logicEngine.isLinked(*sourceScript)); + EXPECT_FALSE(m_logicEngine.isLinked(*middleScript)); + EXPECT_FALSE(m_logicEngine.isLinked(*targetBinding)); + } + + TEST_F(ALogicEngine_Linking, SetsAffectedNodesToDirtyAfterLinking) + { + auto scriptSource = R"( + function interface(IN,OUT) + IN.input = Type:Bool() + OUT.output = Type:Bool() + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(scriptSource); + auto targetBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "RamsesBinding"); + + m_logicEngine.update(); + + EXPECT_FALSE(sourceScript->m_impl.isDirty()); + EXPECT_FALSE(targetBinding->m_impl.isDirty()); + + auto output = sourceScript->getOutputs()->getChild("output"); + auto input = targetBinding->getInputs()->getChild("visibility"); + + m_logicEngine.link(*output, *input); + + EXPECT_TRUE(sourceScript->m_impl.isDirty()); + EXPECT_TRUE(targetBinding->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Linking, SetsAffectedNodesToDirtyAfterLinkingWithStructs) + { + auto scriptSource = R"( + function interface(IN,OUT) + IN.struct = { + inBool = Type:Bool() + } + OUT.struct = { + outBool = Type:Bool() + } + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(scriptSource); + auto targetScript = m_logicEngine.createLuaScript(scriptSource); + + m_logicEngine.update(); + + EXPECT_FALSE(sourceScript->m_impl.isDirty()); + EXPECT_FALSE(targetScript->m_impl.isDirty()); + + auto output = sourceScript->getOutputs()->getChild("struct")->getChild("outBool"); + auto input = targetScript->getInputs()->getChild("struct")->getChild("inBool"); + + m_logicEngine.link(*output, *input); + + EXPECT_TRUE(sourceScript->m_impl.isDirty()); + EXPECT_TRUE(targetScript->m_impl.isDirty()); + } + + TEST_F(ALogicEngine_Linking, SetsNeitherTargetNodeNorSourceNodeToDirtyAfterUnlink) + { + auto scriptSource = R"( + function interface(IN,OUT) + IN.input = Type:Bool() + OUT.output = Type:Bool() + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(scriptSource); + auto targetBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "RamsesBinding"); + + auto output = sourceScript->getOutputs()->getChild("output"); + auto input = targetBinding->getInputs()->getChild("visibility"); + + m_logicEngine.link(*output, *input); + + m_logicEngine.update(); + + EXPECT_FALSE(sourceScript->m_impl.isDirty()); + EXPECT_FALSE(targetBinding->m_impl.isDirty()); + + m_logicEngine.unlink(*output, *input); + + EXPECT_FALSE(sourceScript->m_impl.isDirty()); + EXPECT_FALSE(targetBinding->m_impl.isDirty()); + } + + class ALogicEngine_Linking_Confidence : public ALogicEngine + { + protected: + const std::string_view m_scriptNestedStructs = R"( + function interface(IN,OUT) + IN.struct = { + nested = { + vec3f = Type:Vec3f() + } + } + + OUT.struct = { + nested = { + vec3f = Type:Vec3f() + } + } + end + + function run(IN,OUT) + end + )"; + + const std::string_view m_scriptArrayOfStructs = R"( + function interface(IN,OUT) + IN.array = Type:Array(2, { + vec3f = Type:Vec3f() + }) + + OUT.array = Type:Array(2, { + vec3f = Type:Vec3f() + }) + end + + function run(IN,OUT) + end + )"; + }; + + TEST_F(ALogicEngine_Linking_Confidence, CanDestroyLinkedScriptsWithComplexTypes_WithLinkStillActive_DestroySourceFirst) + { + LuaScript& sourceScript(*m_logicEngine.createLuaScript(m_scriptNestedStructs)); + LuaScript& targetScript(*m_logicEngine.createLuaScript(m_scriptNestedStructs)); + + const Property& sourceVec(*sourceScript.getOutputs()->getChild("struct")->getChild("nested")->getChild("vec3f")); + Property& targetVec(*targetScript.getInputs()->getChild("struct")->getChild("nested")->getChild("vec3f")); + + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.link(sourceVec, targetVec)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.destroy(sourceScript)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.destroy(targetScript)); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALogicEngine_Linking_Confidence, CanDestroyLinkedScriptsWithComplexTypes_WithLinkStillActive_DestroyTargetFirst) + { + LuaScript& sourceScript(*m_logicEngine.createLuaScript(m_scriptNestedStructs)); + LuaScript& targetScript(*m_logicEngine.createLuaScript(m_scriptNestedStructs)); + + const Property& sourceVec(*sourceScript.getOutputs()->getChild("struct")->getChild("nested")->getChild("vec3f")); + Property& targetVec(*targetScript.getInputs()->getChild("struct")->getChild("nested")->getChild("vec3f")); + + // Update in-between each step, to make sure no crashes + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.link(sourceVec, targetVec)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.destroy(targetScript)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.destroy(sourceScript)); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALogicEngine_Linking_Confidence, CanDestroyLinkedScriptsWithComplexTypes_WithLinkStillActive_ArrayOfStructs) + { + LuaScript& sourceScript(*m_logicEngine.createLuaScript(m_scriptArrayOfStructs)); + LuaScript& targetScript(*m_logicEngine.createLuaScript(m_scriptNestedStructs)); + + const Property& sourceVec(*sourceScript.getOutputs()->getChild("array")->getChild(0)->getChild("vec3f")); + Property& targetVec(*targetScript.getInputs()->getChild("struct")->getChild("nested")->getChild("vec3f")); + + // Update in-between each step, to make sure no crashes + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.link(sourceVec, targetVec)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.destroy(sourceScript)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.destroy(targetScript)); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALogicEngine_Linking_Confidence, CanDestroyLinkedBinding_WithNestedLinkStillActive) + { + LuaScript& sourceScript(*m_logicEngine.createLuaScript(m_scriptNestedStructs)); + + auto targetBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + auto translationProperty = targetBinding->getInputs()->getChild("translation"); + + const Property& sourceVec(*sourceScript.getOutputs()->getChild("struct")->getChild(0)->getChild("vec3f")); + + // Update in-between each step, to make sure no crashes + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.link(sourceVec, *translationProperty)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.destroy(sourceScript)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.destroy(*targetBinding)); + EXPECT_TRUE(m_logicEngine.update()); + } +} diff --git a/client/logic/unittests/api/LogicEngineTest_LogicNodeUpdateStatistics.cpp b/client/logic/unittests/api/LogicEngineTest_LogicNodeUpdateStatistics.cpp new file mode 100644 index 000000000..22a1e01fa --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_LogicNodeUpdateStatistics.cpp @@ -0,0 +1,143 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "LogicEngineTest_Base.h" +#include "ramses-logic/Property.h" + +#include "LogTestUtils.h" + +namespace ramses +{ + class ALogicEngine_LogicObjectStatistics : public ALogicEngine + { + protected: + std::vector m_logTypes; + std::vector m_logMessages; + ScopedLogContextLevel m_logCollector{ELogLevel::Debug, [this](ELogLevel type, std::string_view message) + { + m_logTypes.emplace_back(type); + m_logMessages.emplace_back(message); + }}; + }; + + TEST_F(ALogicEngine_LogicObjectStatistics, LogsAllMessages) + { + m_logicEngine.setStatisticsLoggingRate(2u); + + constexpr auto scriptSource = R"( + function interface(IN,OUT) + IN.param = Type:Int32() + OUT.param = Type:Int32() + end + function run(IN,OUT) + OUT.param = IN.param + end + )"; + + auto node1 = m_logicEngine.createLuaScript(scriptSource); + auto node2 = m_logicEngine.createLuaScript(scriptSource); + auto node3 = m_logicEngine.createLuaScript(scriptSource); + auto node4 = m_logicEngine.createLuaScript(scriptSource); + auto node5 = m_logicEngine.createLuaScript(scriptSource); + + m_logicEngine.link(*node1->getOutputs()->getChild(0u), *node2->getInputs()->getChild(0u)); + m_logicEngine.link(*node2->getOutputs()->getChild(0u), *node3->getInputs()->getChild(0u)); + m_logicEngine.link(*node3->getOutputs()->getChild(0u), *node4->getInputs()->getChild(0u)); + m_logicEngine.link(*node4->getOutputs()->getChild(0u), *node5->getInputs()->getChild(0u)); + + for (int32_t i = 0; i < 4; ++i) + { + node1->getInputs()->getChild(0u)->set(i); + EXPECT_TRUE(m_logicEngine.update()); + } + + //5 log lines per frame and 2 frames + EXPECT_EQ(10u, m_logMessages.size()); + + EXPECT_TRUE(m_logMessages[0].find("First Statistics Log") != std::string::npos); + EXPECT_TRUE(m_logMessages[1].find("Update Execution time (min/max/avg):") != std::string::npos); + EXPECT_TRUE(m_logMessages[2].find("Time between Update calls (min/max/avg):") != std::string::npos); + EXPECT_TRUE(m_logMessages[3].find("Nodes Executed (min/max/avg):") != std::string::npos); + EXPECT_TRUE(m_logMessages[4].find("Activated links (min/max/avg):") != std::string::npos); + EXPECT_TRUE(m_logMessages[5].find("Time since last log:") != std::string::npos); + EXPECT_TRUE(m_logMessages[6].find("Update Execution time (min/max/avg):") != std::string::npos); + EXPECT_TRUE(m_logMessages[7].find("Time between Update calls (min/max/avg):") != std::string::npos); + EXPECT_TRUE(m_logMessages[8].find("Nodes Executed (min/max/avg):") != std::string::npos); + EXPECT_TRUE(m_logMessages[9].find("Activated links (min/max/avg):") != std::string::npos); + } + + TEST_F(ALogicEngine_LogicObjectStatistics, NoLogsWhenLoggingRateZero) + { + m_logicEngine.setStatisticsLoggingRate(0u); + + for (int32_t i = 0; i < 4; ++i) + { + EXPECT_TRUE(m_logicEngine.update()); + } + + EXPECT_EQ(0u, m_logMessages.size()); + } + + TEST_F(ALogicEngine_LogicObjectStatistics, OnlyLogWhenLogLevelSmallerOrEqualToLogVerbosityLimit) + { + m_logicEngine.setStatisticsLoggingRate(2u); + m_logicEngine.setStatisticsLogLevel(ELogLevel::Trace); + + for (int32_t i = 0; i < 4; ++i) + { + EXPECT_TRUE(m_logicEngine.update()); + } + EXPECT_TRUE(m_logMessages.empty()); + + m_logicEngine.setStatisticsLogLevel(ELogLevel::Info); + for (int32_t i = 0; i < 4; ++i) + { + EXPECT_TRUE(m_logicEngine.update()); + } + EXPECT_FALSE(m_logMessages.empty()); + + m_logMessages.clear(); + EXPECT_TRUE(m_logMessages.empty()); + + m_logicEngine.setStatisticsLogLevel(ELogLevel::Warn); + for (int32_t i = 0; i < 4; ++i) + { + EXPECT_TRUE(m_logicEngine.update()); + } + EXPECT_FALSE(m_logMessages.empty()); + } + + TEST_F(ALogicEngine_LogicObjectStatistics, LogsAccordingToLoggingRate) + { + m_logicEngine.setStatisticsLoggingRate(2u); + + m_logicEngine.update(); + EXPECT_TRUE(m_logMessages.empty()); + + m_logicEngine.update(); + EXPECT_EQ(5u ,m_logMessages.size()); + + m_logicEngine.update(); + EXPECT_EQ(5u, m_logMessages.size()); + + m_logicEngine.update(); + EXPECT_EQ(10u, m_logMessages.size()); + } + + TEST_F(ALogicEngine_LogicObjectStatistics, NoTimeBetweenUpdateCallsCalculationWithLoggingRateOne) + { + m_logicEngine.setStatisticsLoggingRate(1u); + + m_logicEngine.update(); + EXPECT_TRUE(m_logMessages[2].find("Time between Update calls cannot be measured with loggingRate = 1") != std::string::npos); + + m_logicEngine.update(); + EXPECT_TRUE(m_logMessages[7].find("Time between Update calls cannot be measured with loggingRate = 1") != std::string::npos); + } +} diff --git a/client/logic/unittests/api/LogicEngineTest_Lookup.cpp b/client/logic/unittests/api/LogicEngineTest_Lookup.cpp new file mode 100644 index 000000000..11b9adec7 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_Lookup.cpp @@ -0,0 +1,550 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" +#include "ramses-logic/AnimationNodeConfig.h" + +#include "impl/LuaModuleImpl.h" +#include "impl/LuaScriptImpl.h" +#include "impl/LuaInterfaceImpl.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "impl/RamsesAppearanceBindingImpl.h" +#include "impl/RamsesCameraBindingImpl.h" +#include "impl/RamsesRenderPassBindingImpl.h" +#include "impl/RamsesRenderGroupBindingImpl.h" +#include "impl/RamsesMeshNodeBindingImpl.h" +#include "impl/DataArrayImpl.h" +#include "impl/AnimationNodeImpl.h" +#include "impl/TimerNodeImpl.h" +#include "impl/AnchorPointImpl.h" +#include "impl/SkinBindingImpl.h" + +namespace ramses +{ + class ALogicEngine_Lookup : public ALogicEngine + { + public: + ALogicEngine_Lookup() : ALogicEngine{ ramses::EFeatureLevel_Latest } // test with latest feature level so all possible API objects are available + { + } + + protected: + AnimationNode* createAnimationNode(const DataArray* dataArray) + { + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray, dataArray }); + return m_logicEngine.createAnimationNode(config, "animNode"); + } + }; + + TEST_F(ALogicEngine_Lookup, FindsObjectsByTheirName) + { + const auto luaModule = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + const auto script = m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + const auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + const auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + const auto renderPassBinding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpbinding"); + const auto renderGroupBinding = createRenderGroupBinding(); + const auto meshNodeBinding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + const auto dataArray = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "dataarray"); + const auto animNode = createAnimationNode(dataArray); + const auto timerNode = m_logicEngine.createTimerNode("timerNode"); + const auto intf = m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + const auto anchor = m_logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + const auto skin = createSkinBinding(*nodeBinding, *appearanceBinding, m_logicEngine); + + EXPECT_EQ(luaModule, m_logicEngine.findByName("luaModule")); + EXPECT_EQ(script, m_logicEngine.findByName("script")); + EXPECT_EQ(nodeBinding, m_logicEngine.findByName("nodebinding")); + EXPECT_EQ(appearanceBinding, m_logicEngine.findByName("appbinding")); + EXPECT_EQ(cameraBinding, m_logicEngine.findByName("camerabinding")); + EXPECT_EQ(renderPassBinding, m_logicEngine.findByName("rpbinding")); + EXPECT_EQ(renderGroupBinding, m_logicEngine.findByName("renderGroupBinding")); + EXPECT_EQ(meshNodeBinding, m_logicEngine.findByName("mb")); + EXPECT_EQ(dataArray, m_logicEngine.findByName("dataarray")); + EXPECT_EQ(animNode, m_logicEngine.findByName("animNode")); + EXPECT_EQ(timerNode, m_logicEngine.findByName("timerNode")); + EXPECT_EQ(intf, m_logicEngine.findByName("intf")); + EXPECT_EQ(anchor, m_logicEngine.findByName("anchor")); + EXPECT_EQ(skin, m_logicEngine.findByName("skin")); + + EXPECT_EQ(luaModule, m_logicEngine.findByName("luaModule")); + EXPECT_EQ(script, m_logicEngine.findByName("script")); + EXPECT_EQ(nodeBinding, m_logicEngine.findByName("nodebinding")); + EXPECT_EQ(appearanceBinding, m_logicEngine.findByName("appbinding")); + EXPECT_EQ(cameraBinding, m_logicEngine.findByName("camerabinding")); + EXPECT_EQ(renderPassBinding, m_logicEngine.findByName("rpbinding")); + EXPECT_EQ(renderGroupBinding, m_logicEngine.findByName("renderGroupBinding")); + EXPECT_EQ(meshNodeBinding, m_logicEngine.findByName("mb")); + EXPECT_EQ(dataArray, m_logicEngine.findByName("dataarray")); + EXPECT_EQ(animNode, m_logicEngine.findByName("animNode")); + EXPECT_EQ(timerNode, m_logicEngine.findByName("timerNode")); + EXPECT_EQ(intf, m_logicEngine.findByName("intf")); + EXPECT_EQ(anchor, m_logicEngine.findByName("anchor")); + EXPECT_EQ(skin, m_logicEngine.findByName("skin")); + + auto it = m_logicEngine.getCollection().cbegin(); + EXPECT_EQ(*it++, luaModule); + EXPECT_EQ(*it++, script); + EXPECT_EQ(*it++, nodeBinding); + EXPECT_EQ(*it++, appearanceBinding); + EXPECT_EQ(*it++, cameraBinding); + EXPECT_EQ(*it++, renderPassBinding); + EXPECT_EQ(*it++, renderGroupBinding); + EXPECT_EQ(*it++, meshNodeBinding); + EXPECT_EQ(*it++, dataArray); + EXPECT_EQ(*it++, animNode); + EXPECT_EQ(*it++, timerNode); + EXPECT_EQ(*it++, intf); + EXPECT_EQ(*it++, anchor); + EXPECT_EQ(*it++, skin); + } + + TEST_F(ALogicEngine_Lookup, FindsObjectsByTheirName_Const) + { + const auto luaModule = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + const auto script = m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + const auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + const auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + const auto renderPassBinding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpbinding"); + const auto renderGroupBinding = createRenderGroupBinding(); + const auto meshNodeBinding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + const auto dataArray = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "dataarray"); + const auto animNode = createAnimationNode(dataArray); + const auto timerNode = m_logicEngine.createTimerNode("timerNode"); + const auto intf = m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + const auto anchor = m_logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + const auto skin = createSkinBinding(*nodeBinding, *appearanceBinding, m_logicEngine); + + const LogicEngine& immutableLogicEngine = m_logicEngine; + EXPECT_EQ(luaModule, immutableLogicEngine.findByName("luaModule")); + EXPECT_EQ(script, immutableLogicEngine.findByName("script")); + EXPECT_EQ(nodeBinding, immutableLogicEngine.findByName("nodebinding")); + EXPECT_EQ(appearanceBinding, immutableLogicEngine.findByName("appbinding")); + EXPECT_EQ(cameraBinding, immutableLogicEngine.findByName("camerabinding")); + EXPECT_EQ(renderPassBinding, immutableLogicEngine.findByName("rpbinding")); + EXPECT_EQ(renderGroupBinding, immutableLogicEngine.findByName("renderGroupBinding")); + EXPECT_EQ(meshNodeBinding, immutableLogicEngine.findByName("mb")); + EXPECT_EQ(dataArray, immutableLogicEngine.findByName("dataarray")); + EXPECT_EQ(animNode, immutableLogicEngine.findByName("animNode")); + EXPECT_EQ(timerNode, immutableLogicEngine.findByName("timerNode")); + EXPECT_EQ(intf, immutableLogicEngine.findByName("intf")); + EXPECT_EQ(anchor, immutableLogicEngine.findByName("anchor")); + EXPECT_EQ(skin, immutableLogicEngine.findByName("skin")); + + EXPECT_EQ(luaModule, immutableLogicEngine.findByName("luaModule")); + EXPECT_EQ(script, immutableLogicEngine.findByName("script")); + EXPECT_EQ(nodeBinding, immutableLogicEngine.findByName("nodebinding")); + EXPECT_EQ(appearanceBinding, immutableLogicEngine.findByName("appbinding")); + EXPECT_EQ(cameraBinding, immutableLogicEngine.findByName("camerabinding")); + EXPECT_EQ(renderPassBinding, immutableLogicEngine.findByName("rpbinding")); + EXPECT_EQ(renderGroupBinding, immutableLogicEngine.findByName("renderGroupBinding")); + EXPECT_EQ(meshNodeBinding, immutableLogicEngine.findByName("mb")); + EXPECT_EQ(dataArray, immutableLogicEngine.findByName("dataarray")); + EXPECT_EQ(animNode, immutableLogicEngine.findByName("animNode")); + EXPECT_EQ(timerNode, immutableLogicEngine.findByName("timerNode")); + EXPECT_EQ(intf, immutableLogicEngine.findByName("intf")); + EXPECT_EQ(anchor, immutableLogicEngine.findByName("anchor")); + EXPECT_EQ(skin, immutableLogicEngine.findByName("skin")); + } + + TEST_F(ALogicEngine_Lookup, FindsObjectsByTheirName_CanBeUsedWithRealType) + { + m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + const auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + const auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpbinding"); + createRenderGroupBinding(); + m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + const auto dataArray = m_logicEngine.createDataArray(std::vector{1.f, 2.f, 3.f}, "dataarray"); + createAnimationNode(dataArray); + m_logicEngine.createTimerNode("timerNode"); + m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + m_logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + createSkinBinding(*nodeBinding, *appearanceBinding, m_logicEngine); + + const auto* luaModuleFound = m_logicEngine.findByName("luaModule")->as(); + const auto* luaScriptFound = m_logicEngine.findByName("script")->as(); + const auto* nodeBindingFound = m_logicEngine.findByName("nodebinding")->as(); + const auto* appearanceBindingFound = m_logicEngine.findByName("appbinding")->as(); + const auto* cameraBindingFound = m_logicEngine.findByName("camerabinding")->as(); + const auto* renderPassBindingFound = m_logicEngine.findByName("rpbinding")->as(); + const auto* renderGroupBindingFound = m_logicEngine.findByName("renderGroupBinding")->as(); + const auto* meshNodeBindingFound = m_logicEngine.findByName("mb")->as(); + const auto* dataArrayFound = m_logicEngine.findByName("dataarray")->as(); + const auto* animNodeFound = m_logicEngine.findByName("animNode")->as(); + const auto* timerNodeFound = m_logicEngine.findByName("timerNode")->as(); + const auto* intfFound = m_logicEngine.findByName("intf")->as(); + const auto* anchorFound = m_logicEngine.findByName("anchor")->as(); + const auto* skinFound = m_logicEngine.findByName("skin")->as(); + + ASSERT_NE(nullptr, luaModuleFound); + ASSERT_NE(nullptr, luaScriptFound); + ASSERT_NE(nullptr, nodeBindingFound); + ASSERT_NE(nullptr, appearanceBindingFound); + ASSERT_NE(nullptr, cameraBindingFound); + ASSERT_NE(nullptr, renderPassBindingFound); + ASSERT_NE(nullptr, renderGroupBindingFound); + ASSERT_NE(nullptr, meshNodeBindingFound); + ASSERT_NE(nullptr, dataArrayFound); + ASSERT_NE(nullptr, animNodeFound); + ASSERT_NE(nullptr, timerNodeFound); + ASSERT_NE(nullptr, intfFound); + ASSERT_NE(nullptr, anchorFound); + ASSERT_NE(nullptr, skinFound); + + EXPECT_EQ(luaModuleFound->getName(), "luaModule"); + EXPECT_EQ(luaScriptFound->getName(), "script"); + EXPECT_EQ(nodeBindingFound->getName(), "nodebinding"); + EXPECT_EQ(appearanceBindingFound->getName(), "appbinding"); + EXPECT_EQ(cameraBindingFound->getName(), "camerabinding"); + EXPECT_EQ(renderPassBindingFound->getName(), "rpbinding"); + EXPECT_EQ(renderGroupBindingFound->getName(), "renderGroupBinding"); + EXPECT_EQ(meshNodeBindingFound->getName(), "mb"); + EXPECT_EQ(dataArrayFound->getName(), "dataarray"); + EXPECT_EQ(animNodeFound->getName(), "animNode"); + EXPECT_EQ(timerNodeFound->getName(), "timerNode"); + EXPECT_EQ(intfFound->getName(), "intf"); + EXPECT_EQ(anchorFound->getName(), "anchor"); + EXPECT_EQ(skinFound->getName(), "skin"); + } + + TEST_F(ALogicEngine_Lookup, FindsObjectsByTheirName_CanBeUsedAsRealType_Const) + { + m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + const auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + const auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpbinding"); + createRenderGroupBinding(); + m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + const auto dataArray = m_logicEngine.createDataArray(std::vector{1.f, 2.f, 3.f}, "dataarray"); + createAnimationNode(dataArray); + m_logicEngine.createTimerNode("timerNode"); + m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + m_logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + createSkinBinding(*nodeBinding, *appearanceBinding, m_logicEngine); + + const LogicEngine& immutableLogicEngine = m_logicEngine; + const auto* luaModuleFound = immutableLogicEngine.findByName("luaModule")->as(); + const auto* luaScriptFound = immutableLogicEngine.findByName("script")->as(); + const auto* nodeBindingFound = immutableLogicEngine.findByName("nodebinding")->as(); + const auto* appearanceBindingFound = immutableLogicEngine.findByName("appbinding")->as(); + const auto* cameraBindingFound = immutableLogicEngine.findByName("camerabinding")->as(); + const auto* renderPassBindingFound = immutableLogicEngine.findByName("rpbinding")->as(); + const auto* renderGroupBindingFound = immutableLogicEngine.findByName("renderGroupBinding")->as(); + const auto* meshNodeBindingFound = immutableLogicEngine.findByName("mb")->as(); + const auto* dataArrayFound = immutableLogicEngine.findByName("dataarray")->as(); + const auto* animNodeFound = immutableLogicEngine.findByName("animNode")->as(); + const auto* timerNodeFound = immutableLogicEngine.findByName("timerNode")->as(); + const auto* intfFound = immutableLogicEngine.findByName("intf")->as(); + const auto* anchorFound = immutableLogicEngine.findByName("anchor")->as(); + const auto* skinFound = immutableLogicEngine.findByName("skin")->as(); + + ASSERT_NE(nullptr, luaModuleFound); + ASSERT_NE(nullptr, luaScriptFound); + ASSERT_NE(nullptr, nodeBindingFound); + ASSERT_NE(nullptr, appearanceBindingFound); + ASSERT_NE(nullptr, cameraBindingFound); + ASSERT_NE(nullptr, renderPassBindingFound); + ASSERT_NE(nullptr, renderGroupBindingFound); + ASSERT_NE(nullptr, meshNodeBindingFound); + ASSERT_NE(nullptr, dataArrayFound); + ASSERT_NE(nullptr, animNodeFound); + ASSERT_NE(nullptr, timerNodeFound); + ASSERT_NE(nullptr, intfFound); + ASSERT_NE(nullptr, anchorFound); + ASSERT_NE(nullptr, skinFound); + + EXPECT_EQ(luaModuleFound->getName(), "luaModule"); + EXPECT_EQ(luaScriptFound->getName(), "script"); + EXPECT_EQ(nodeBindingFound->getName(), "nodebinding"); + EXPECT_EQ(appearanceBindingFound->getName(), "appbinding"); + EXPECT_EQ(cameraBindingFound->getName(), "camerabinding"); + EXPECT_EQ(renderPassBindingFound->getName(), "rpbinding"); + EXPECT_EQ(renderGroupBindingFound->getName(), "renderGroupBinding"); + EXPECT_EQ(meshNodeBindingFound->getName(), "mb"); + EXPECT_EQ(dataArrayFound->getName(), "dataarray"); + EXPECT_EQ(animNodeFound->getName(), "animNode"); + EXPECT_EQ(timerNodeFound->getName(), "timerNode"); + EXPECT_EQ(intfFound->getName(), "intf"); + EXPECT_EQ(anchorFound->getName(), "anchor"); + EXPECT_EQ(skinFound->getName(), "skin"); + } + + TEST_F(ALogicEngine_Lookup, FindsObjectsByTheirId) + { + const auto luaModule = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + const auto script = m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + const auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + const auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + const auto renderPassBinding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpbinding"); + const auto renderGroupBinding = createRenderGroupBinding(); + const auto meshBinding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + const auto dataArray = m_logicEngine.createDataArray(std::vector{1.f, 2.f, 3.f}, "dataarray"); + const auto animNode = createAnimationNode(dataArray); + const auto timerNode = m_logicEngine.createTimerNode("timerNode"); + const auto intf = m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + const auto anchor = m_logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + const auto skin = createSkinBinding(*nodeBinding, *appearanceBinding, m_logicEngine); + + EXPECT_EQ(luaModule, m_logicEngine.findLogicObjectById(1u)); + EXPECT_EQ(script, m_logicEngine.findLogicObjectById(2u)); + EXPECT_EQ(nodeBinding, m_logicEngine.findLogicObjectById(3u)); + EXPECT_EQ(appearanceBinding, m_logicEngine.findLogicObjectById(4u)); + EXPECT_EQ(cameraBinding, m_logicEngine.findLogicObjectById(5u)); + EXPECT_EQ(renderPassBinding, m_logicEngine.findLogicObjectById(6u)); + EXPECT_EQ(renderGroupBinding, m_logicEngine.findLogicObjectById(7u)); + EXPECT_EQ(meshBinding, m_logicEngine.findLogicObjectById(8u)); + EXPECT_EQ(dataArray, m_logicEngine.findLogicObjectById(9u)); + EXPECT_EQ(animNode, m_logicEngine.findLogicObjectById(10u)); + EXPECT_EQ(timerNode, m_logicEngine.findLogicObjectById(11u)); + EXPECT_EQ(intf, m_logicEngine.findLogicObjectById(12u)); + EXPECT_EQ(anchor, m_logicEngine.findLogicObjectById(13u)); + EXPECT_EQ(skin, m_logicEngine.findLogicObjectById(14u)); + } + + TEST_F(ALogicEngine_Lookup, FindsObjectsByTheirId_Const) + { + const auto luaModule = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + const auto script = m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + const auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + const auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + const auto renderPassBinding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpbinding"); + const auto renderGroupBinding = createRenderGroupBinding(); + const auto meshBinding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + const auto dataArray = m_logicEngine.createDataArray(std::vector{1.f, 2.f, 3.f}, "dataarray"); + const auto animNode = createAnimationNode(dataArray); + const auto timerNode = m_logicEngine.createTimerNode("timerNode"); + const auto intf = m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + const auto anchor = m_logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + const auto skin = createSkinBinding(*nodeBinding, *appearanceBinding, m_logicEngine); + + const LogicEngine& immutableLogicEngine = m_logicEngine; + EXPECT_EQ(luaModule, immutableLogicEngine.findLogicObjectById(1u)); + EXPECT_EQ(script, immutableLogicEngine.findLogicObjectById(2u)); + EXPECT_EQ(nodeBinding, immutableLogicEngine.findLogicObjectById(3u)); + EXPECT_EQ(appearanceBinding, immutableLogicEngine.findLogicObjectById(4u)); + EXPECT_EQ(cameraBinding, immutableLogicEngine.findLogicObjectById(5u)); + EXPECT_EQ(renderPassBinding, immutableLogicEngine.findLogicObjectById(6u)); + EXPECT_EQ(renderGroupBinding, immutableLogicEngine.findLogicObjectById(7u)); + EXPECT_EQ(meshBinding, immutableLogicEngine.findLogicObjectById(8u)); + EXPECT_EQ(dataArray, immutableLogicEngine.findLogicObjectById(9u)); + EXPECT_EQ(animNode, immutableLogicEngine.findLogicObjectById(10u)); + EXPECT_EQ(timerNode, immutableLogicEngine.findLogicObjectById(11u)); + EXPECT_EQ(intf, immutableLogicEngine.findLogicObjectById(12u)); + EXPECT_EQ(anchor, immutableLogicEngine.findLogicObjectById(13u)); + EXPECT_EQ(skin, immutableLogicEngine.findLogicObjectById(14u)); + } + + TEST_F(ALogicEngine_Lookup, FindsObjectsByTheirName_CutsNameAtNullTermination) + { + RamsesAppearanceBinding* appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + EXPECT_EQ(appearanceBinding, m_logicEngine.findByName("appbinding\0withsurprise")); + } + + TEST_F(ALogicEngine_Lookup, FindsObjectsAfterRenaming_ByNewNameOnly) + { + LuaModule* luaModule = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + LuaScript* script = m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + RamsesNodeBinding* nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + RamsesAppearanceBinding* appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + RamsesCameraBinding* cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + RamsesRenderPassBinding* renderPassBinding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpbinding"); + auto renderGroupBinding = createRenderGroupBinding(); + auto meshBinding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "meshbinding"); + auto dataArray = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "dataarray"); + auto animNode = createAnimationNode(dataArray); + auto timerNode = m_logicEngine.createTimerNode("timerNode"); + auto intf = m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + auto anchor = m_logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + auto skin = createSkinBinding(*nodeBinding, *appearanceBinding, m_logicEngine); + + // Rename + luaModule->setName("L"); + script->setName("S"); + nodeBinding->setName("NB"); + appearanceBinding->setName("AB"); + cameraBinding->setName("CB"); + renderPassBinding->setName("RPB"); + renderGroupBinding->setName("RGB"); + meshBinding->setName("MB"); + dataArray->setName("DA"); + animNode->setName("AN"); + timerNode->setName("TN"); + intf->setName("I"); + anchor->setName("A"); + skin->setName("SB"); + + // Can't find by old name + EXPECT_EQ(nullptr, m_logicEngine.findByName("luaModule")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("script")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("nodebinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("appbinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("camerabinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("rpbinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("renderGroupBinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("meshbinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("dataarray")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("animNode")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("timerNode")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("intf")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("anchor")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("skin")); + + // Found by new name + EXPECT_EQ(luaModule, m_logicEngine.findByName("L")); + EXPECT_EQ(script, m_logicEngine.findByName("S")); + EXPECT_EQ(nodeBinding, m_logicEngine.findByName("NB")); + EXPECT_EQ(appearanceBinding, m_logicEngine.findByName("AB")); + EXPECT_EQ(cameraBinding, m_logicEngine.findByName("CB")); + EXPECT_EQ(renderPassBinding, m_logicEngine.findByName("RPB")); + EXPECT_EQ(renderGroupBinding, m_logicEngine.findByName("RGB")); + EXPECT_EQ(meshBinding, m_logicEngine.findByName("MB")); + EXPECT_EQ(dataArray, m_logicEngine.findByName("DA")); + EXPECT_EQ(animNode, m_logicEngine.findByName("AN")); + EXPECT_EQ(timerNode, m_logicEngine.findByName("TN")); + EXPECT_EQ(intf, m_logicEngine.findByName("I")); + EXPECT_EQ(anchor, m_logicEngine.findByName("A")); + EXPECT_EQ(skin, m_logicEngine.findByName("SB")); + } + + TEST_F(ALogicEngine_Lookup, FindsObjectByNameOnlyIfTypeMatches) + { + m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + const auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + const auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpbinding"); + createRenderGroupBinding(); + m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "meshbinding"); + const auto dataArray = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "dataarray"); + createAnimationNode(dataArray); + m_logicEngine.createTimerNode("timerNode"); + m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + m_logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + createSkinBinding(*nodeBinding, *appearanceBinding, m_logicEngine); + + EXPECT_EQ(nullptr, m_logicEngine.findByName("dataarray")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("nodebinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("appbinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("camerabinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("animNode")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("script")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("luaModule")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("appbinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("meshbinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("renderGroupBinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("anchor")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("timerNode")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("intf")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("rpbinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("skin")); + } + + TEST_F(ALogicEngine_Lookup, FindsObjectByNameOnlyStringMatchesExactly) + { + m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + + EXPECT_EQ(nullptr, m_logicEngine.findByName("Nodebinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("node")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("binding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("Xnodebinding")); + EXPECT_EQ(nullptr, m_logicEngine.findByName("nodebindinY")); + } + + TEST_F(ALogicEngine_Lookup, GetHLObjectFromImpl) + { + const auto module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + const auto script = m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + const auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + const auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + const auto rpBinding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpbinding"); + const auto rgBinding = createRenderGroupBinding(); + const auto meshBinding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "meshbinding"); + const auto dataArray = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "dataarray"); + const auto animNode = createAnimationNode(dataArray); + const auto timer = m_logicEngine.createTimerNode("timerNode"); + const auto intf = m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + const auto anchor = m_logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + const auto skin = createSkinBinding(*nodeBinding, *appearanceBinding, m_logicEngine); + + EXPECT_EQ(module, &module->m_impl.getLogicObject()); + EXPECT_EQ(script, &script->m_script.getLogicObject()); + EXPECT_EQ(nodeBinding, &nodeBinding->m_nodeBinding.getLogicObject()); + EXPECT_EQ(appearanceBinding, &appearanceBinding->m_appearanceBinding.getLogicObject()); + EXPECT_EQ(cameraBinding, &cameraBinding->m_cameraBinding.getLogicObject()); + EXPECT_EQ(rpBinding, &rpBinding->m_renderPassBinding.getLogicObject()); + EXPECT_EQ(rgBinding, &rgBinding->m_renderGroupBinding.getLogicObject()); + EXPECT_EQ(meshBinding, &meshBinding->m_meshNodeBinding.getLogicObject()); + EXPECT_EQ(dataArray, &dataArray->m_impl.getLogicObject()); + EXPECT_EQ(animNode, &animNode->m_animationNodeImpl.getLogicObject()); + EXPECT_EQ(timer, &timer->m_timerNodeImpl.getLogicObject()); + EXPECT_EQ(intf, &intf->m_interface.getLogicObject()); + EXPECT_EQ(anchor, &anchor->m_anchorPointImpl.getLogicObject()); + EXPECT_EQ(skin, &skin->m_skinBinding.getLogicObject()); + } + + TEST_F(ALogicEngine_Lookup, GetHLObjectFromImpl_const) + { + const auto module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "luaModule"); + const auto script = m_logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + const auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + const auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appbinding"); + const auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "camerabinding"); + const auto rpBinding = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpbinding"); + const auto rgBinding = createRenderGroupBinding(); + const auto meshBinding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "meshbinding"); + const auto dataArray = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "dataarray"); + const auto animNode = createAnimationNode(dataArray); + const auto timer = m_logicEngine.createTimerNode("timerNode"); + const auto intf = m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + const auto anchor = m_logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + const auto skin = createSkinBinding(*nodeBinding, *appearanceBinding, m_logicEngine); + + const auto& moduleImpl = module->m_impl; + const auto& scriptImpl = script->m_script; + const auto& nodeBindingImpl = nodeBinding->m_nodeBinding; + const auto& appearanceBindingImpl = appearanceBinding->m_appearanceBinding; + const auto& cameraBindingImpl = cameraBinding->m_cameraBinding; + const auto& rpBindingImpl = rpBinding->m_renderPassBinding; + const auto& rgBindingImpl = rgBinding->m_renderGroupBinding; + const auto& meshBindingImpl = meshBinding->m_meshNodeBinding; + const auto& dataArrayImpl = dataArray->m_impl; + const auto& animNodeImpl = animNode->m_animationNodeImpl; + const auto& timerImpl = timer->m_timerNodeImpl; + const auto& intfImpl = intf->m_interface; + const auto& anchorImpl = anchor->m_anchorPointImpl; + const auto& skinImpl = skin->m_skinBinding; + + EXPECT_EQ(module, &moduleImpl.getLogicObject()); + EXPECT_EQ(script, &scriptImpl.getLogicObject()); + EXPECT_EQ(nodeBinding, &nodeBindingImpl.getLogicObject()); + EXPECT_EQ(appearanceBinding, &appearanceBindingImpl.getLogicObject()); + EXPECT_EQ(cameraBinding, &cameraBindingImpl.getLogicObject()); + EXPECT_EQ(rpBinding, &rpBindingImpl.getLogicObject()); + EXPECT_EQ(rgBinding, &rgBindingImpl.getLogicObject()); + EXPECT_EQ(meshBinding, &meshBindingImpl.getLogicObject()); + EXPECT_EQ(dataArray, &dataArrayImpl.getLogicObject()); + EXPECT_EQ(animNode, &animNodeImpl.getLogicObject()); + EXPECT_EQ(timer, &timerImpl.getLogicObject()); + EXPECT_EQ(intf, &intfImpl.getLogicObject()); + EXPECT_EQ(anchor, &anchorImpl.getLogicObject()); + EXPECT_EQ(skin, &skinImpl.getLogicObject()); + } +} + diff --git a/client/logic/unittests/api/LogicEngineTest_Serialization.cpp b/client/logic/unittests/api/LogicEngineTest_Serialization.cpp new file mode 100644 index 000000000..9b4c40143 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_Serialization.cpp @@ -0,0 +1,1216 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "RamsesTestUtils.h" +#include "WithTempDirectory.h" +#include "FeatureLevelTestValues.h" +#include "PropertyLinkTestUtils.h" + +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/Logger.h" +#include "ramses-logic/RamsesLogicVersion.h" + +#include "ramses-client-api/EffectDescription.h" +#include "ramses-client-api/Effect.h" +#include "ramses-client-api/Scene.h" +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/RenderPass.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-framework-api/RamsesVersion.h" + +#include "impl/LogicNodeImpl.h" +#include "impl/LogicEngineImpl.h" +#include "impl/DataArrayImpl.h" +#include "impl/PropertyImpl.h" +#include "internals/ApiObjects.h" +#include "internals/FileUtils.h" +#include "LogTestUtils.h" +#include "FileDescriptorHelper.h" + +#include "generated/LogicEngineGen.h" +#include "ramses-sdk-build-config.h" +#include "fmt/format.h" + +#include +#include + +namespace ramses::internal +{ + class ALogicEngine_Serialization : public ALogicEngineBase, public ::testing::TestWithParam + { + public: + ALogicEngine_Serialization() : ALogicEngineBase{ GetParam() } + { + } + + protected: + static std::vector CreateTestBuffer(const SaveFileConfig& config = {}) + { + LogicEngine logicEngineForSaving{ GetParam() }; + + const std::string src = R"( + function interface(IN,OUT) + IN.param = Type:Int32() + OUT.param2 = Type:Int32() + end + function run(IN,OUT) + end + )"; + + // Create simple (and compact) valid setup, where two (identical) scripts are created + // and their inputs and outputs are cross linked, so that none of the scripts + // generate a warning for having unlinked inputs or outputs + + auto* script1 = logicEngineForSaving.createLuaScript(src, {}, "luascript"); + auto* script2 = logicEngineForSaving.createLuaScript(src, {}, "luascript2"); + + //link output of 1st script to input of 2nd script + logicEngineForSaving.link(*script1->getOutputs()->getChild("param2"), *script2->getInputs()->getChild("param")); + //link output of 2nd script to input of 1st script, use weak link to avoid circular dependancy + logicEngineForSaving.linkWeak(*script2->getOutputs()->getChild("param2"), *script1->getInputs()->getChild("param")); + + EXPECT_TRUE(logicEngineForSaving.saveToFile("tempfile.bin", config)); + + return *FileUtils::LoadBinary("tempfile.bin"); + } + + static void SaveBufferToFile(const std::vector& bufferData, const std::string& file) + { + FileUtils::SaveBinary(file, static_cast(bufferData.data()), bufferData.size()); + } + + std::vector saveAndLoadAllTypesOfObjects() + { + LogicEngine logicEngine{ GetParam() }; + logicEngine.createLuaModule(m_moduleSourceCode, {}, "module"); + logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + auto* nodeBinding = logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodeBinding"); + auto* appearanceBinding = logicEngine.createRamsesAppearanceBinding(*m_appearance, "appearanceBinding"); + auto* cameraBinding = logicEngine.createRamsesCameraBinding(*m_camera, "cameraBinding"); + const auto* dataArray = logicEngine.createDataArray(std::vector{1.f, 2.f, 3.f}, "dataArray"); + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear }); + logicEngine.createAnimationNode(config, "animNode"); + logicEngine.createTimerNode("timerNode"); + logicEngine.createLuaInterface(R"( + function interface(IN, OUT) + end + )", "intf"); + + logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpBinding"); + logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + createRenderGroupBinding(logicEngine); + createSkinBinding(*nodeBinding, *appearanceBinding, logicEngine); + logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + + EXPECT_TRUE(logicEngine.update()); + EXPECT_TRUE(SaveToFileWithoutValidation(logicEngine, "LogicEngine.bin")); + + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin", m_scene)); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + const std::vector names{ "module", "script", "nodeBinding", "appearanceBinding", "cameraBinding", "dataArray", "animNode", + "timerNode", "intf", "rpBinding", "anchor", "renderGroupBinding", "skin", "mb" }; + + std::vector objects; + for (const auto& name : names) + { + auto obj = m_logicEngine.findByName(name); + EXPECT_NE(nullptr, obj); + objects.push_back(obj); + } + + return objects; + } + + WithTempDirectory m_tempDirectory; + }; + + INSTANTIATE_TEST_SUITE_P( + ALogicEngine_SerializationTests, + ALogicEngine_Serialization, + ramses::internal::GetFeatureLevelTestValues()); + + TEST_P(ALogicEngine_Serialization, ProducesErrorIfDeserilizedFromInvalidFile) + { + EXPECT_FALSE(m_logicEngine.loadFromFile("invalid")); + const auto& errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_THAT(errors[0].message, ::testing::HasSubstr("Failed to load file 'invalid'")); + } + + TEST_P(ALogicEngine_Serialization, ProducesErrorIfDeserilizedFromFileWithoutApiObjects) + { + { + ramses::RamsesVersion ramsesVersion = ramses::GetRamsesVersion(); + flatbuffers::FlatBufferBuilder builder; + auto logicEngine = rlogic_serialization::CreateLogicEngine( + builder, + rlogic_serialization::CreateVersion(builder, + ramsesVersion.major, + ramsesVersion.minor, + ramsesVersion.patch, + builder.CreateString(ramsesVersion.string)), + rlogic_serialization::CreateVersion(builder, + ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MAJOR_INT, + ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MINOR_INT, + ramses_sdk::RAMSES_SDK_PROJECT_VERSION_PATCH_INT, + builder.CreateString(ramses_sdk::RAMSES_SDK_RAMSES_VERSION)), + 0, // missing api objects + 0, + GetParam() + ); + + builder.Finish(logicEngine, rlogic_serialization::LogicEngineIdentifier()); + + ASSERT_TRUE(FileUtils::SaveBinary("no_api_objects.bin", builder.GetBufferPointer(), builder.GetSize())); + } + + EXPECT_FALSE(m_logicEngine.loadFromFile("no_api_objects.bin")); + const auto& errors = m_logicEngine.getErrors(); + ASSERT_EQ(1u, errors.size()); + EXPECT_THAT(errors[0].message, ::testing::HasSubstr("doesn't contain API objects")); + } + + TEST_P(ALogicEngine_Serialization, ProducesErrorWhenProvidingAFolderAsTargetForSaving) + { + fs::create_directories("folder"); + EXPECT_FALSE(m_logicEngine.saveToFile("folder")); + EXPECT_EQ("Failed to save content to path 'folder'!", m_logicEngine.getErrors()[0].message); + } + + TEST_P(ALogicEngine_Serialization, ProducesErrorIfDeserilizedFromFolder) + { + fs::create_directories("folder"); + EXPECT_FALSE(m_logicEngine.loadFromFile("folder")); + EXPECT_EQ("Failed to load file 'folder'", m_logicEngine.getErrors()[0].message); + } + + TEST_P(ALogicEngine_Serialization, LoadFromFileDescriptor) + { + std::vector bufferData = CreateTestBuffer(); + SaveBufferToFile(bufferData, "LogicEngine.bin"); + const int fd = ramses_internal::FileDescriptorHelper::OpenFileDescriptorBinary("LogicEngine.bin"); + EXPECT_LT(0, fd); + EXPECT_TRUE(m_logicEngine.loadFromFileDescriptor(fd, 0, bufferData.size())); + } + + TEST_P(ALogicEngine_Serialization, LoadFromFileDescriptorWithOffset) + { + const size_t offset = 10; + std::vector bufferData = CreateTestBuffer(); + bufferData.insert(bufferData.begin(), offset, 'x'); + SaveBufferToFile(bufferData, "LogicEngine.bin"); + const int fd = ramses_internal::FileDescriptorHelper::OpenFileDescriptorBinary("LogicEngine.bin"); + EXPECT_LT(0, fd); + EXPECT_TRUE(m_logicEngine.loadFromFileDescriptor(fd, offset, bufferData.size()- offset)); + } + + TEST_P(ALogicEngine_Serialization, LoadFromFileDescriptor_InvalidFileDescriptor) + { + EXPECT_FALSE(m_logicEngine.loadFromFileDescriptor(0, 0, 1000)); + EXPECT_EQ("Invalid file descriptor: 0", m_logicEngine.getErrors()[0].message); + } + + TEST_P(ALogicEngine_Serialization, LoadFromFileDescriptor_ZeroSize) + { + EXPECT_FALSE(m_logicEngine.loadFromFileDescriptor(42, 0, 0)); + EXPECT_EQ("Failed to load from file descriptor: size may not be 0", m_logicEngine.getErrors()[0].message); + } + + TEST_P(ALogicEngine_Serialization, LoadFromFileDescriptor_InvalidOffset) + { + std::vector bufferData = CreateTestBuffer(); + SaveBufferToFile(bufferData, "LogicEngine.bin"); + const int fd = ramses_internal::FileDescriptorHelper::OpenFileDescriptorBinary("LogicEngine.bin"); + EXPECT_FALSE(m_logicEngine.loadFromFileDescriptor(fd, bufferData.size(), bufferData.size())); + EXPECT_EQ(fmt::format("Failed to load from file descriptor: fd: {} offset: {} size: {}", fd, bufferData.size(), bufferData.size()), + m_logicEngine.getErrors()[0].message); + } + + TEST_P(ALogicEngine_Serialization, LoadFromFileDescriptor_InvalidSize) + { + std::vector bufferData = CreateTestBuffer(); + SaveBufferToFile(bufferData, "LogicEngine.bin"); + const int fd = ramses_internal::FileDescriptorHelper::OpenFileDescriptorBinary("LogicEngine.bin"); + EXPECT_FALSE(m_logicEngine.loadFromFileDescriptor(fd, 0, bufferData.size() + 1)); + EXPECT_EQ(fmt::format("Failed to load from file descriptor: fd: {} offset: {} size: {}", fd, 0, bufferData.size() + 1), + m_logicEngine.getErrors()[0].message); + } + + TEST_P(ALogicEngine_Serialization, DeserializesFromMemoryBuffer) + { + const std::vector bufferData = CreateTestBuffer(); + + EXPECT_TRUE(m_logicEngine.loadFromBuffer(bufferData.data(), bufferData.size())); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + { + auto script = m_logicEngine.findByName("luascript"); + ASSERT_NE(nullptr, script); + const auto inputs = script->getInputs(); + ASSERT_NE(nullptr, inputs); + EXPECT_EQ(1u, inputs->getChildCount()); + } + } + + TEST_P(ALogicEngine_Serialization, ProducesErrorIfDeserializedFromCorruptedData) + { + // Emulate data corruption + { + std::vector bufferData = CreateTestBuffer(); + ASSERT_GT(bufferData.size(), 62u); + // Do a random byte corruption + // byte 62 happens to break the format - found out by trial and error + bufferData[62] = 42; + SaveBufferToFile(bufferData, "LogicEngine.bin"); + } + + // Test with file API + { + ASSERT_FALSE(m_logicEngine.loadFromFile("LogicEngine.bin")); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("contains corrupted data!")); + } + + // Test with buffer API + { + std::vector corruptedMemory = *FileUtils::LoadBinary("LogicEngine.bin"); + ASSERT_FALSE(m_logicEngine.loadFromBuffer(corruptedMemory.data(), corruptedMemory.size())); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("contains corrupted data!")); + } + } + + TEST_P(ALogicEngine_Serialization, PrintsMetadataInfoOnLoad) + { + SaveFileConfig config; + config.setMetadataString("This is a scene exported for tests"); + config.setExporterVersion(3, 1, 2, 42); + + // Test different constructor variations + SaveFileConfig config2(config); + config2 = config; + config2 = std::move(config); + + SaveBufferToFile(CreateTestBuffer(config2), "LogicEngine.bin"); + + TestLogCollector logCollector(ELogLevel::Info); + + // Test with file API + { + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin")); + + ASSERT_EQ(logCollector.logs.size(), 3u); + EXPECT_THAT(logCollector.logs[0].message, ::testing::HasSubstr("Loading logic engine content from 'file 'LogicEngine.bin'")); + EXPECT_THAT(logCollector.logs[1].message, ::testing::HasSubstr("Logic Engine content metadata: 'This is a scene exported for tests'")); + EXPECT_THAT(logCollector.logs[2].message, ::testing::HasSubstr("Exporter version: 3.1.2 (file format version 42)")); + } + + logCollector.logs.clear(); + + // Test with buffer API + { + const std::vector byteBuffer = *FileUtils::LoadBinary("LogicEngine.bin"); + EXPECT_TRUE(m_logicEngine.loadFromBuffer(byteBuffer.data(), byteBuffer.size())); + + ASSERT_EQ(logCollector.logs.size(), 3u); + EXPECT_THAT(logCollector.logs[0].message, ::testing::HasSubstr("Loading logic engine content from 'data buffer")); + EXPECT_THAT(logCollector.logs[1].message, ::testing::HasSubstr("Logic Engine content metadata: 'This is a scene exported for tests'")); + EXPECT_THAT(logCollector.logs[2].message, ::testing::HasSubstr("Exporter version: 3.1.2 (file format version 42)")); + } + } + + TEST_P(ALogicEngine_Serialization, PrintsMetadataInfoOnLoad_NoVersionInfoProvided) + { + SaveFileConfig config; + SaveBufferToFile(CreateTestBuffer(config), "LogicEngine.bin"); + + TestLogCollector logCollector(ELogLevel::Info); + + // Test with file API + { + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin")); + + ASSERT_EQ(logCollector.logs.size(), 3u); + EXPECT_THAT(logCollector.logs[0].message, ::testing::HasSubstr("Loading logic engine content from 'file 'LogicEngine.bin'")); + EXPECT_THAT(logCollector.logs[1].message, ::testing::HasSubstr("Logic Engine content metadata: ''")); + EXPECT_THAT(logCollector.logs[2].message, ::testing::HasSubstr("Exporter version: 0.0.0 (file format version 0)")); + } + + logCollector.logs.clear(); + + // Test with buffer API + { + const std::vector byteBuffer = *FileUtils::LoadBinary("LogicEngine.bin"); + EXPECT_TRUE(m_logicEngine.loadFromBuffer(byteBuffer.data(), byteBuffer.size())); + + ASSERT_EQ(logCollector.logs.size(), 3u); + EXPECT_THAT(logCollector.logs[0].message, ::testing::HasSubstr("Loading logic engine content from 'data buffer")); + EXPECT_THAT(logCollector.logs[1].message, ::testing::HasSubstr("Logic Engine content metadata: ''")); + EXPECT_THAT(logCollector.logs[2].message, ::testing::HasSubstr("Exporter version: 0.0.0 (file format version 0)")); + } + } + + // the special file identifiers in flatbuffers are at bytes 4-7, so a file smaller than 8 bytes is a special case of broken + TEST_P(ALogicEngine_Serialization, ProducesErrorIfDeserializedFromFileSmallerThan8Bytes) + { + // Emulate data truncation + { + std::vector bufferData = CreateTestBuffer(); + ASSERT_GT(bufferData.size(), 60u); + std::vector truncated(bufferData.begin(), bufferData.begin() + 7); + SaveBufferToFile(truncated, "LogicEngine.bin"); + } + + // Test with file API + { + EXPECT_FALSE(m_logicEngine.loadFromFile("LogicEngine.bin")); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("(size: 7) contains corrupted data! Data should be at least 8 bytes")); + } + + // Test with buffer API + { + std::vector truncatedMemory = *FileUtils::LoadBinary("LogicEngine.bin"); + EXPECT_FALSE(m_logicEngine.loadFromBuffer(truncatedMemory.data(), truncatedMemory.size())); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("(size: 7) contains corrupted data! Data should be at least 8 bytes")); + } + } + + TEST_P(ALogicEngine_Serialization, ProducesErrorIfDeserializedFromTruncatedData) + { + // Emulate data truncation + { + std::vector bufferData = CreateTestBuffer(); + ASSERT_GT(bufferData.size(), 60u); + + // Cutting off the data at byte 60 breaks deserialization (found by trial and error) + std::vector truncated(bufferData.begin(), bufferData.begin() + 60); + SaveBufferToFile(truncated, "LogicEngine.bin"); + } + + // Test with file API + { + EXPECT_FALSE(m_logicEngine.loadFromFile("LogicEngine.bin")); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("(size: 60) contains corrupted data!")); + } + + // Test with buffer API + { + std::vector truncatedMemory = *FileUtils::LoadBinary("LogicEngine.bin"); + EXPECT_FALSE(m_logicEngine.loadFromBuffer(truncatedMemory.data(), truncatedMemory.size())); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("(size: 60) contains corrupted data!")); + } + } + +// The Windows API doesn't allow non-admin access to symlinks, this breaks on dev machines +#ifndef _WIN32 + TEST_P(ALogicEngine_Serialization, CanBeDeserializedFromHardLink) + { + EXPECT_TRUE(m_logicEngine.saveToFile("testfile.bin")); + fs::create_hard_link("testfile.bin", "hardlink"); + EXPECT_TRUE(m_logicEngine.loadFromFile("hardlink")); + } + + TEST_P(ALogicEngine_Serialization, CanBeDeserializedFromSymLink) + { + EXPECT_TRUE(m_logicEngine.saveToFile("testfile.bin")); + fs::create_symlink("testfile.bin", "symlink"); + EXPECT_TRUE(m_logicEngine.loadFromFile("symlink")); + } + + TEST_P(ALogicEngine_Serialization, FailsGracefullyWhenTryingToOpenFromDanglingSymLink) + { + EXPECT_TRUE(m_logicEngine.saveToFile("testfile.bin")); + fs::create_symlink("testfile.bin", "dangling_symlink"); + fs::remove("testfile.bin"); + EXPECT_FALSE(m_logicEngine.loadFromFile("dangling_symlink")); + EXPECT_EQ("Failed to load file 'dangling_symlink'", m_logicEngine.getErrors()[0].message); + } +#endif + + TEST_P(ALogicEngine_Serialization, ProducesNoErrorIfDeserializedWithNoScriptsAndNoNodeBindings) + { + { + LogicEngine logicEngine{ GetParam() }; + ASSERT_TRUE(logicEngine.saveToFile("LogicEngine.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + } + } + + TEST_P(ALogicEngine_Serialization, ProducesNoErrorIfDeserializedWithNoScripts) + { + { + LogicEngine logicEngine{ GetParam() }; + logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "binding"); + + ASSERT_TRUE(SaveToFileWithoutValidation(logicEngine, "LogicEngine.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin", m_scene)); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + { + auto rNodeBinding = m_logicEngine.findByName("binding"); + ASSERT_NE(nullptr, rNodeBinding); + } + } + } + + TEST_P(ALogicEngine_Serialization, ProducesNoErrorIfDeserializedWithoutNodeBindings) + { + { + LogicEngine logicEngine{ GetParam() }; + logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.param = Type:Int32() + end + function run(IN,OUT) + end + )", {}, "luascript"); + + ASSERT_TRUE(SaveToFileWithoutValidation(logicEngine, "LogicEngine.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + { + auto script = m_logicEngine.findByName("luascript"); + ASSERT_NE(nullptr, script); + const auto inputs = script->getInputs(); + ASSERT_NE(nullptr, inputs); + EXPECT_EQ(1u, inputs->getChildCount()); + } + } + } + + TEST_P(ALogicEngine_Serialization, ProducesErrorIfSavedWithValidationWarning) + { + // Put logic engine to a dirty state (create new object and don't call update) + RamsesNodeBinding* nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "binding"); + + std::vector messages; + std::vector messageTypes; + ScopedLogContextLevel scopedLogs(ELogLevel::Warn, [&](ELogLevel msgType, std::string_view message) { + messages.emplace_back(message); + messageTypes.emplace_back(msgType); + }); + + // Set a value and save -> causes warning + nodeBinding->getInputs()->getChild("visibility")->set(false); + ASSERT_TRUE(nodeBinding->m_impl.isDirty()); + ASSERT_FALSE(m_logicEngine.saveToFile("LogicEngine.bin")); + + ASSERT_EQ(3u, messages.size()); + EXPECT_EQ("Saving logic engine content with manually updated binding values without calling update() will result in those values being lost!", messages[0]); + EXPECT_EQ("[binding [Id=1]] Node [binding] has no ingoing links! Node should be deleted or properly linked!", messages[1]); + EXPECT_EQ("Failed to saveToFile() because validation warnings were encountered! Refer to the documentation of saveToFile() for details how to address these gracefully.", messages[2]); + EXPECT_EQ(ELogLevel::Warn, messageTypes[0]); + EXPECT_EQ(ELogLevel::Warn, messageTypes[1]); + EXPECT_EQ(ELogLevel::Error, messageTypes[2]); + + // Unset custom log handler + Logger::SetLogHandler([](ELogLevel msgType, std::string_view message) { + (void)message; + (void)msgType; + }); + } + + TEST_P(ALogicEngine_Serialization, RefusesToSaveTwoNodeBindingsWhichPointToDifferentScenes) + { + RamsesTestSetup testSetup; + ramses::Scene* scene1 = testSetup.createScene(ramses::sceneId_t(1)); + ramses::Scene* scene2 = testSetup.createScene(ramses::sceneId_t(2)); + + ramses::Node* node1 = scene1->createNode("node1"); + ramses::Node* node2 = scene2->createNode("node2"); + + m_logicEngine.createRamsesNodeBinding(*node1, ramses::ERotationType::Euler_XYZ, "binding1"); + ramses::RamsesNodeBinding* binding2 = m_logicEngine.createRamsesNodeBinding(*node2, ramses::ERotationType::Euler_XYZ, "binding2"); + + EXPECT_FALSE(m_logicEngine.saveToFile("will_not_be_written.logic")); + ASSERT_EQ(2u, m_logicEngine.getErrors().size()); + EXPECT_EQ("Ramses node 'node2' is from scene with id:2 but other objects are from scene with id:1!", m_logicEngine.getErrors()[0].message); + EXPECT_EQ(binding2, m_logicEngine.getErrors()[0].object); + EXPECT_EQ("Can't save a logic engine to file while it has references to more than one Ramses scene!", m_logicEngine.getErrors()[1].message); + EXPECT_EQ(nullptr, m_logicEngine.getErrors()[1].object); + } + + TEST_P(ALogicEngine_Serialization, RefusesToSaveTwoCameraBindingsWhichPointToDifferentScenes) + { + RamsesTestSetup testSetup; + ramses::Scene* scene1 = testSetup.createScene(ramses::sceneId_t(1)); + ramses::Scene* scene2 = testSetup.createScene(ramses::sceneId_t(2)); + + ramses::PerspectiveCamera* camera1 = scene1->createPerspectiveCamera("camera1"); + ramses::PerspectiveCamera* camera2 = scene2->createPerspectiveCamera("camera2"); + + m_logicEngine.createRamsesCameraBinding(*camera1, "binding1"); + ramses::RamsesCameraBinding* binding2 = m_logicEngine.createRamsesCameraBinding(*camera2, "binding2"); + + EXPECT_FALSE(m_logicEngine.saveToFile("will_not_be_written.logic")); + ASSERT_EQ(2u, m_logicEngine.getErrors().size()); + EXPECT_EQ("Ramses camera 'camera2' is from scene with id:2 but other objects are from scene with id:1!", m_logicEngine.getErrors()[0].message); + EXPECT_EQ(binding2, m_logicEngine.getErrors()[0].object); + EXPECT_EQ("Can't save a logic engine to file while it has references to more than one Ramses scene!", m_logicEngine.getErrors()[1].message); + EXPECT_EQ(nullptr, m_logicEngine.getErrors()[1].object); + } + + TEST_P(ALogicEngine_Serialization, RefusesToSaveTwoRenderPassBindingsWhichPointToDifferentScenes) + { + RamsesTestSetup testSetup; + ramses::Scene* scene1 = testSetup.createScene(ramses::sceneId_t(1)); + ramses::Scene* scene2 = testSetup.createScene(ramses::sceneId_t(2)); + + auto* rp1 = scene1->createRenderPass("rp1"); + auto* rp2 = scene2->createRenderPass("rp2"); + + m_logicEngine.createRamsesRenderPassBinding(*rp1, "binding1"); + auto* binding2 = m_logicEngine.createRamsesRenderPassBinding(*rp2, "binding2"); + + EXPECT_FALSE(m_logicEngine.saveToFile("will_not_be_written.logic")); + ASSERT_EQ(2u, m_logicEngine.getErrors().size()); + EXPECT_EQ("Ramses render pass 'rp2' is from scene with id:2 but other objects are from scene with id:1!", m_logicEngine.getErrors()[0].message); + EXPECT_EQ(binding2, m_logicEngine.getErrors()[0].object); + EXPECT_EQ("Can't save a logic engine to file while it has references to more than one Ramses scene!", m_logicEngine.getErrors()[1].message); + EXPECT_EQ(nullptr, m_logicEngine.getErrors()[1].object); + } + + TEST_P(ALogicEngine_Serialization, RefusesToSaveTwoRenderGroupBindingsWhichPointToDifferentScenes) + { + RamsesTestSetup testSetup; + ramses::Scene* scene1 = testSetup.createScene(ramses::sceneId_t(1)); + ramses::Scene* scene2 = testSetup.createScene(ramses::sceneId_t(2)); + + const auto meshNode1 = scene1->createMeshNode(); + auto* rg1 = scene1->createRenderGroup("rg1"); + rg1->addMeshNode(*meshNode1); + + const auto meshNode2 = scene2->createMeshNode(); + auto* rg2 = scene2->createRenderGroup("rg2"); + rg2->addMeshNode(*meshNode2); + + RamsesRenderGroupBindingElements elements1; + EXPECT_TRUE(elements1.addElement(*meshNode1, "mesh")); + m_logicEngine.createRamsesRenderGroupBinding(*rg1, elements1, "binding1"); + + RamsesRenderGroupBindingElements elements2; + EXPECT_TRUE(elements2.addElement(*meshNode2, "mesh")); + const auto binding2 = m_logicEngine.createRamsesRenderGroupBinding(*rg2, elements2, "binding2"); + + EXPECT_FALSE(m_logicEngine.saveToFile("will_not_be_written.logic")); + ASSERT_EQ(2u, m_logicEngine.getErrors().size()); + EXPECT_EQ("Ramses render group 'rg2' is from scene with id:2 but other objects are from scene with id:1!", m_logicEngine.getErrors()[0].message); + EXPECT_EQ(binding2, m_logicEngine.getErrors()[0].object); + EXPECT_EQ("Can't save a logic engine to file while it has references to more than one Ramses scene!", m_logicEngine.getErrors()[1].message); + EXPECT_EQ(nullptr, m_logicEngine.getErrors()[1].object); + } + + TEST_P(ALogicEngine_Serialization, RefusesToSaveTwoMeshNodeBindingsWhichPointToDifferentScenes) + { + RamsesTestSetup testSetup; + ramses::Scene* scene1 = testSetup.createScene(ramses::sceneId_t(1)); + ramses::Scene* scene2 = testSetup.createScene(ramses::sceneId_t(2)); + + const auto meshNode1 = scene1->createMeshNode("mesh1"); + const auto meshNode2 = scene2->createMeshNode("mesh2"); + + m_logicEngine.createRamsesMeshNodeBinding(*meshNode1, "binding1"); + const auto binding2 = m_logicEngine.createRamsesMeshNodeBinding(*meshNode2, "binding2"); + + EXPECT_FALSE(m_logicEngine.saveToFile("will_not_be_written.logic")); + ASSERT_EQ(2u, m_logicEngine.getErrors().size()); + EXPECT_EQ("Ramses mesh node 'mesh2' is from scene with id:2 but other objects are from scene with id:1!", m_logicEngine.getErrors()[0].message); + EXPECT_EQ(binding2, m_logicEngine.getErrors()[0].object); + EXPECT_EQ("Can't save a logic engine to file while it has references to more than one Ramses scene!", m_logicEngine.getErrors()[1].message); + EXPECT_EQ(nullptr, m_logicEngine.getErrors()[1].object); + } + + TEST_P(ALogicEngine_Serialization, RefusesToSaveAppearanceBindingWhichIsFromDifferentSceneThanNodeBinding) + { + ramses::Scene* scene2 = m_ramses.createScene(ramses::sceneId_t(2)); + + m_logicEngine.createRamsesNodeBinding(*scene2->createNode(), ramses::ERotationType::Euler_XYZ, "node binding"); + ramses::RamsesAppearanceBinding* appBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "app binding"); + + EXPECT_FALSE(m_logicEngine.saveToFile("will_not_be_written.logic")); + EXPECT_EQ(2u, m_logicEngine.getErrors().size()); + EXPECT_EQ("Ramses appearance 'test appearance' is from scene with id:1 but other objects are from scene with id:2!", m_logicEngine.getErrors()[0].message); + EXPECT_EQ(appBinding, m_logicEngine.getErrors()[0].object); + EXPECT_EQ("Can't save a logic engine to file while it has references to more than one Ramses scene!", m_logicEngine.getErrors()[1].message); + EXPECT_EQ(nullptr, m_logicEngine.getErrors()[1].object); + } + + TEST_P(ALogicEngine_Serialization, ProducesNoErrorIfDeserilizedSuccessfully) + { + saveAndLoadAllTypesOfObjects(); + { + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin", m_scene)); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + { + auto moduleByName = m_logicEngine.findByName("module"); + auto moduleById = m_logicEngine.findLogicObjectById(1u); + ASSERT_NE(nullptr, moduleByName); + ASSERT_EQ(moduleById, moduleByName); + } + { + auto scriptByName = m_logicEngine.findByName("script"); + auto scriptById = m_logicEngine.findLogicObjectById(2u); + ASSERT_NE(nullptr, scriptByName); + ASSERT_EQ(scriptById, scriptByName); + const auto inputs = scriptByName->getInputs(); + ASSERT_NE(nullptr, inputs); + EXPECT_EQ(0u, inputs->getChildCount()); + EXPECT_TRUE(scriptByName->m_impl.isDirty()); + } + { + auto rNodeBindingByName = m_logicEngine.findByName("nodeBinding"); + auto rNodeBindingById = m_logicEngine.findLogicObjectById(3u); + ASSERT_NE(nullptr, rNodeBindingByName); + ASSERT_EQ(rNodeBindingById, rNodeBindingByName); + const auto inputs = rNodeBindingByName->getInputs(); + ASSERT_NE(nullptr, inputs); + EXPECT_EQ(5u, inputs->getChildCount()); + EXPECT_FALSE(rNodeBindingByName->m_impl.isDirty()); + } + { + auto rCameraBindingByName = m_logicEngine.findByName("cameraBinding"); + auto rCameraBindingById = m_logicEngine.findLogicObjectById(5u); + ASSERT_NE(nullptr, rCameraBindingByName); + ASSERT_EQ(rCameraBindingById, rCameraBindingByName); + const auto inputs = rCameraBindingByName->getInputs(); + ASSERT_NE(nullptr, inputs); + EXPECT_EQ(2u, inputs->getChildCount()); + EXPECT_FALSE(rCameraBindingByName->m_impl.isDirty()); + } + { + auto rAppearanceBindingByName = m_logicEngine.findByName("appearanceBinding"); + auto rAppearanceBindingById = m_logicEngine.findLogicObjectById(4u); + ASSERT_NE(nullptr, rAppearanceBindingByName); + ASSERT_EQ(rAppearanceBindingById, rAppearanceBindingByName); + const auto inputs = rAppearanceBindingByName->getInputs(); + ASSERT_NE(nullptr, inputs); + + ASSERT_EQ(1u, inputs->getChildCount()); + auto floatUniform = inputs->getChild(0); + ASSERT_NE(nullptr, floatUniform); + EXPECT_EQ("floatUniform", floatUniform->getName()); + EXPECT_EQ(EPropertyType::Float, floatUniform->getType()); + EXPECT_FALSE(rAppearanceBindingByName->m_impl.isDirty()); + } + { + const auto dataArrayByName = m_logicEngine.findByName("dataArray"); + const auto dataArrayById = m_logicEngine.findLogicObjectById(6u); + ASSERT_NE(nullptr, dataArrayByName); + ASSERT_EQ(dataArrayById, dataArrayByName); + EXPECT_EQ(EPropertyType::Float, dataArrayByName->getDataType()); + ASSERT_NE(nullptr, dataArrayByName->getData()); + const std::vector expectedData{ 1.f, 2.f, 3.f }; + EXPECT_EQ(expectedData, *m_logicEngine.findByName("dataArray")->getData()); + + const auto animNodeByName = m_logicEngine.findByName("animNode"); + const auto animNodeById = m_logicEngine.findLogicObjectById(7u); + ASSERT_NE(nullptr, animNodeByName); + ASSERT_EQ(animNodeById, animNodeByName); + ASSERT_EQ(1u, animNodeByName->getChannels().size()); + EXPECT_EQ(dataArrayByName, animNodeByName->getChannels().front().timeStamps); + EXPECT_EQ(dataArrayByName, animNodeByName->getChannels().front().keyframes); + } + { + auto rpBindingByName = m_logicEngine.findByName("rpBinding"); + auto rpBindingById = m_logicEngine.findLogicObjectById(10u); + ASSERT_NE(nullptr, rpBindingByName); + ASSERT_EQ(rpBindingById, rpBindingByName); + const auto inputs = rpBindingByName->getInputs(); + ASSERT_NE(nullptr, inputs); + EXPECT_EQ(4u, inputs->getChildCount()); + EXPECT_FALSE(rpBindingByName->m_impl.isDirty()); + + auto anchorByName = m_logicEngine.findByName("anchor"); + auto anchorById = m_logicEngine.findLogicObjectById(11u); + ASSERT_NE(nullptr, anchorByName); + ASSERT_EQ(anchorById, anchorByName); + const auto outputs = anchorByName->getOutputs(); + ASSERT_NE(nullptr, outputs); + EXPECT_EQ(2u, outputs->getChildCount()); + } + { + auto rgBindingByName = m_logicEngine.findByName("renderGroupBinding"); + auto rgBindingById = m_logicEngine.findLogicObjectById(12u); + ASSERT_NE(nullptr, rgBindingByName); + ASSERT_EQ(rgBindingById, rgBindingByName); + const auto inputs = rgBindingByName->getInputs(); + ASSERT_NE(nullptr, inputs); + EXPECT_EQ(1u, inputs->getChildCount()); + EXPECT_EQ(1u, inputs->getChild("renderOrders")->getChildCount()); + EXPECT_FALSE(rgBindingByName->m_impl.isDirty()); + } + { + auto skinByName = m_logicEngine.findByName("skin"); + auto skinById = m_logicEngine.findLogicObjectById(13u); + ASSERT_NE(nullptr, skinByName); + ASSERT_EQ(skinById, skinByName); + } + { + auto meshBindingByName = m_logicEngine.findByName("mb"); + auto meshBindingById = m_logicEngine.findLogicObjectById(14u); + ASSERT_NE(nullptr, meshBindingByName); + ASSERT_EQ(meshBindingById, meshBindingByName); + const auto inputs = meshBindingByName->getInputs(); + ASSERT_NE(nullptr, inputs); + EXPECT_EQ(4u, inputs->getChildCount()); + EXPECT_FALSE(meshBindingByName->m_impl.isDirty()); + } + } + } + + TEST_P(ALogicEngine_Serialization, ReplacesCurrentStateWithStateFromFile) + { + { + LogicEngine logicEngine{ GetParam() }; + logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.param = Type:Int32() + end + function run(IN,OUT) + end + )", {}, "luascript"); + + logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "binding"); + ASSERT_TRUE(SaveToFileWithoutValidation(logicEngine, "LogicEngine.bin")); + } + { + m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.param2 = Type:Float() + end + function run(IN,OUT) + end + )", {}, "luascript2"); + + m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "binding2"); + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin", m_scene)); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + { + ASSERT_EQ(nullptr, m_logicEngine.findByName("luascript2")); + ASSERT_EQ(nullptr, m_logicEngine.findByName("binding2")); + + auto script = m_logicEngine.findByName("luascript"); + ASSERT_NE(nullptr, script); + auto rNodeBinding = m_logicEngine.findByName("binding"); + ASSERT_NE(nullptr, rNodeBinding); + EXPECT_EQ(m_node, &rNodeBinding->getRamsesNode()); + } + } + } + + TEST_P(ALogicEngine_Serialization, DeserializesLinks) + { + { + std::string_view scriptSource = R"( + function interface(IN,OUT) + IN.input = Type:Int32() + OUT.output = Type:Int32() + end + function run(IN,OUT) + end + )"; + + LogicEngine logicEngine{ GetParam() }; + auto sourceScript1 = logicEngine.createLuaScript(scriptSource, {}, "SourceScript1"); + auto targetScript1 = logicEngine.createLuaScript(scriptSource, {}, "TargetScript1"); + auto sourceScript2 = logicEngine.createLuaScript(scriptSource, {}, "SourceScript2"); + auto targetScript2 = logicEngine.createLuaScript(scriptSource, {}, "TargetScript2"); + logicEngine.createLuaScript(scriptSource, {}, "NotLinkedScript"); + + auto srcOutput1 = sourceScript1->getOutputs()->getChild("output"); + auto tgtInput1 = targetScript1->getInputs()->getChild("input"); + auto srcOutput2 = sourceScript2->getOutputs()->getChild("output"); + auto tgtInput2 = targetScript2->getInputs()->getChild("input"); + + EXPECT_TRUE(logicEngine.link(*srcOutput1, *tgtInput1)); + EXPECT_TRUE(logicEngine.linkWeak(*srcOutput2, *tgtInput2)); + + ASSERT_TRUE(SaveToFileWithoutValidation(logicEngine, "LogicEngine.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + auto sourceScript1 = m_logicEngine.findByName("SourceScript1"); + auto targetScript1 = m_logicEngine.findByName("TargetScript1"); + auto sourceScript2 = m_logicEngine.findByName("SourceScript2"); + auto targetScript2 = m_logicEngine.findByName("TargetScript2"); + auto notLinkedScript = m_logicEngine.findByName("NotLinkedScript"); + + EXPECT_TRUE(m_logicEngine.isLinked(*sourceScript1)); + EXPECT_TRUE(m_logicEngine.isLinked(*targetScript1)); + EXPECT_TRUE(m_logicEngine.isLinked(*sourceScript2)); + EXPECT_TRUE(m_logicEngine.isLinked(*targetScript2)); + EXPECT_FALSE(m_logicEngine.isLinked(*notLinkedScript)); + + const internal::LogicNodeDependencies& internalNodeDependencies = m_logicEngine.m_impl->getApiObjects().getLogicNodeDependencies(); + EXPECT_TRUE(internalNodeDependencies.isLinked(sourceScript1->m_impl)); + EXPECT_TRUE(internalNodeDependencies.isLinked(targetScript1->m_impl)); + EXPECT_TRUE(internalNodeDependencies.isLinked(sourceScript2->m_impl)); + EXPECT_TRUE(internalNodeDependencies.isLinked(targetScript2->m_impl)); + + const auto srcOutput1 = sourceScript1->getOutputs()->getChild("output"); + const auto tgtInput1 = targetScript1->getInputs()->getChild("input"); + const auto srcOutput2 = sourceScript2->getOutputs()->getChild("output"); + const auto tgtInput2 = targetScript2->getInputs()->getChild("input"); + PropertyLinkTestUtils::ExpectLinks(m_logicEngine, { + { srcOutput1, tgtInput1, false }, + { srcOutput2, tgtInput2, true } + }); + + const auto& srcOutLinks1 = srcOutput1->m_impl->getOutgoingLinks(); + const auto& srcOutLinks2 = srcOutput2->m_impl->getOutgoingLinks(); + ASSERT_EQ(1u, srcOutLinks1.size()); + ASSERT_EQ(1u, srcOutLinks2.size()); + EXPECT_EQ(tgtInput1->m_impl.get(), srcOutLinks1[0].property); + EXPECT_EQ(tgtInput2->m_impl.get(), srcOutLinks2[0].property); + EXPECT_FALSE(srcOutLinks1[0].isWeakLink); + EXPECT_TRUE(srcOutLinks2[0].isWeakLink); + EXPECT_EQ(srcOutput1->m_impl.get(), tgtInput1->m_impl->getIncomingLink().property); + EXPECT_EQ(srcOutput2->m_impl.get(), tgtInput2->m_impl->getIncomingLink().property); + EXPECT_FALSE(tgtInput1->m_impl->getIncomingLink().isWeakLink); + EXPECT_TRUE(tgtInput2->m_impl->getIncomingLink().isWeakLink); + } + } + + TEST_P(ALogicEngine_Serialization, InternalLinkDataIsDeletedAfterDeserialization) + { + std::string_view scriptSource = R"( + function interface(IN,OUT) + IN.input = Type:Int32() + OUT.output = Type:Int32() + end + function run(IN,OUT) + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(scriptSource, {}, "SourceScript"); + auto targetScript = m_logicEngine.createLuaScript(scriptSource, {}, "TargetScript"); + + // Save logic engine state without links to file + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "LogicEngine.bin")); + + // Create link (should be wiped after loading from file) + auto output = sourceScript->getOutputs()->getChild("output"); + auto input = targetScript->getInputs()->getChild("input"); + m_logicEngine.link(*output, *input); + + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin")); + + auto sourceScriptAfterLoading = m_logicEngine.findByName("SourceScript"); + auto targetScriptAfterLoading = m_logicEngine.findByName("TargetScript"); + + // Make a copy of the object so that we can call non-const methods on it too (getTopologicallySortedNodes()) + // This can't happen in user code, we only do this to test internal data + internal::LogicNodeDependencies internalNodeDependencies = m_logicEngine.m_impl->getApiObjects().getLogicNodeDependencies(); + ASSERT_TRUE(internalNodeDependencies.getTopologicallySortedNodes().has_value()); + + // New objects are not linked (because they weren't before saving) + EXPECT_FALSE(m_logicEngine.isLinked(*sourceScriptAfterLoading)); + EXPECT_FALSE(m_logicEngine.isLinked(*targetScriptAfterLoading)); + EXPECT_FALSE(internalNodeDependencies.isLinked(sourceScriptAfterLoading->m_impl)); + EXPECT_FALSE(internalNodeDependencies.isLinked(sourceScriptAfterLoading->m_impl)); + + // Internal topological graph has two unsorted nodes, before and after update() + EXPECT_EQ(2u, (*internalNodeDependencies.getTopologicallySortedNodes()).size()); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(2u, (*internalNodeDependencies.getTopologicallySortedNodes()).size()); + } + + TEST_P(ALogicEngine_Serialization, PreviouslyCreatedModulesAreDeletedInSolStateAfterDeserialization) + { + { + LogicEngine logicEngineForSaving{ GetParam() }; + const std::string_view moduleSrc = R"( + local mymath = {} + mymath.PI=3.1415 + return mymath + )"; + + std::string_view script = R"( + modules("mymath") + function interface(IN,OUT) + OUT.pi = Type:Float() + end + function run(IN,OUT) + OUT.pi = mymath.PI + end + )"; + + LuaModule* mymath = logicEngineForSaving.createLuaModule(moduleSrc, {}, "mymath"); + LuaConfig config; + config.addDependency("mymath", *mymath); + logicEngineForSaving.createLuaScript(script, config, "script"); + + ASSERT_TRUE(SaveToFileWithoutValidation(logicEngineForSaving, "LogicEngine.bin")); + } + + // Create a module with name colliding with the one from file - it should be deleted + const std::string_view moduleToBeWipedSrc = R"( + local mymath = {} + mymath.PI=4 + return mymath + )"; + + // This module will be overwritten when loading the file below. The logic engine should not + // keep any leftovers from modules or scripts when loading from file - all content should be + // taken from the file! + LuaModule* moduleToBeWiped = m_logicEngine.createLuaModule(moduleToBeWipedSrc, {}, "mymath"); + EXPECT_NE(nullptr, moduleToBeWiped); + + ASSERT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin")); + + m_logicEngine.update(); + + auto script = m_logicEngine.findByName("script"); + + // This is the PI from the loaded module, not from 'moduleToBeWiped' + EXPECT_FLOAT_EQ(3.1415f, *script->getOutputs()->getChild("pi")->get()); + } + + TEST_P(ALogicEngine_Serialization, IDsAfterDeserializationAreUnique) + { + uint64_t serializedId = 0u; + { + LogicEngine logicEngine{ GetParam() }; + const auto script = logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.param = Type:Int32() + end + function run(IN,OUT) + end + )", {}, "luascript"); + serializedId = script->getId(); + + ASSERT_TRUE(SaveToFileWithoutValidation(logicEngine, "LogicEngine.bin")); + } + + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + auto script = m_logicEngine.findLogicObjectById(serializedId); + ASSERT_NE(nullptr, script); + EXPECT_EQ("luascript", script->getName()); + + const auto anotherScript = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + end + )"); + EXPECT_GT(anotherScript->getId(), script->getId()); + } + + TEST_P(ALogicEngine_Serialization, persistsUserIds) + { + { + LogicEngine logicEngine{ GetParam() }; + auto* luaModule = logicEngine.createLuaModule(m_moduleSourceCode, {}, "module"); + auto* luaScript = logicEngine.createLuaScript(m_valid_empty_script, {}, "script"); + auto* nodeBinding = logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodeBinding"); + auto* appearanceBinding = logicEngine.createRamsesAppearanceBinding(*m_appearance, "appearanceBinding"); + auto* cameraBinding = logicEngine.createRamsesCameraBinding(*m_camera, "cameraBinding"); + auto* dataArray = logicEngine.createDataArray(std::vector{1.f, 2.f, 3.f}, "dataArray"); + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear }); + auto* animationNode = logicEngine.createAnimationNode(config, "animNode"); + auto* timerNode = logicEngine.createTimerNode("timerNode"); + auto* luaInterface = logicEngine.createLuaInterface(R"( + function interface(IN, OUT) + end + )", "intf"); + auto* rpBinding = logicEngine.createRamsesRenderPassBinding(*m_renderPass, "rpBinding"); + auto* anchor = logicEngine.createAnchorPoint(*nodeBinding, *cameraBinding, "anchor"); + auto* rgBinding = createRenderGroupBinding(logicEngine); + auto* skin = createSkinBinding(logicEngine); + auto* meshBinding = logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + + EXPECT_TRUE(luaModule->setUserId(1u, 2u)); + EXPECT_TRUE(luaScript->setUserId(3u, 4u)); + EXPECT_TRUE(nodeBinding->setUserId(5u, 6u)); + EXPECT_TRUE(appearanceBinding->setUserId(7u, 8u)); + EXPECT_TRUE(cameraBinding->setUserId(9u, 10u)); + EXPECT_TRUE(dataArray->setUserId(11u, 12u)); + EXPECT_TRUE(animationNode->setUserId(13u, 14u)); + EXPECT_TRUE(timerNode->setUserId(15u, 16u)); + EXPECT_TRUE(luaInterface->setUserId(17u, 18u)); + EXPECT_TRUE(rpBinding->setUserId(19u, 20u)); + EXPECT_TRUE(anchor->setUserId(21u, 22u)); + EXPECT_TRUE(rgBinding->setUserId(23u, 24u)); + EXPECT_TRUE(skin->setUserId(25u, 26u)); + EXPECT_TRUE(meshBinding->setUserId(27u, 28u)); + + ASSERT_TRUE(SaveToFileWithoutValidation(logicEngine, "LogicEngine.bin")); + } + + EXPECT_TRUE(m_logicEngine.loadFromFile("LogicEngine.bin", m_scene)); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + auto obj = m_logicEngine.findByName("module"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(1u, obj->getUserId().first); + EXPECT_EQ(2u, obj->getUserId().second); + + obj = m_logicEngine.findByName("script"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(3u, obj->getUserId().first); + EXPECT_EQ(4u, obj->getUserId().second); + + obj = m_logicEngine.findByName("nodeBinding"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(5u, obj->getUserId().first); + EXPECT_EQ(6u, obj->getUserId().second); + + obj = m_logicEngine.findByName("appearanceBinding"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(7u, obj->getUserId().first); + EXPECT_EQ(8u, obj->getUserId().second); + + obj = m_logicEngine.findByName("cameraBinding"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(9u, obj->getUserId().first); + EXPECT_EQ(10u, obj->getUserId().second); + + obj = m_logicEngine.findByName("dataArray"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(11u, obj->getUserId().first); + EXPECT_EQ(12u, obj->getUserId().second); + + obj = m_logicEngine.findByName("animNode"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(13u, obj->getUserId().first); + EXPECT_EQ(14u, obj->getUserId().second); + + obj = m_logicEngine.findByName("timerNode"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(15u, obj->getUserId().first); + EXPECT_EQ(16u, obj->getUserId().second); + + obj = m_logicEngine.findByName("intf"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(17u, obj->getUserId().first); + EXPECT_EQ(18u, obj->getUserId().second); + + obj = m_logicEngine.findByName("rpBinding"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(19u, obj->getUserId().first); + EXPECT_EQ(20u, obj->getUserId().second); + + obj = m_logicEngine.findByName("anchor"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(21u, obj->getUserId().first); + EXPECT_EQ(22u, obj->getUserId().second); + + obj = m_logicEngine.findByName("renderGroupBinding"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(23u, obj->getUserId().first); + EXPECT_EQ(24u, obj->getUserId().second); + + obj = m_logicEngine.findByName("skin"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(25u, obj->getUserId().first); + EXPECT_EQ(26u, obj->getUserId().second); + + obj = m_logicEngine.findByName("mb"); + ASSERT_NE(nullptr, obj); + EXPECT_EQ(27u, obj->getUserId().first); + EXPECT_EQ(28u, obj->getUserId().second); + } + + TEST_P(ALogicEngine_Serialization, persistsLogicObjectImplToHLObjectMapping) + { + const auto objects = saveAndLoadAllTypesOfObjects(); + for (const auto& obj : objects) + { + EXPECT_EQ(obj, &obj->m_impl->getLogicObject()); + } + } + + TEST_P(ALogicEngine_Serialization, persistsOwnershipOfAllPropertiesByTheirLogicNode) + { + const auto objects = saveAndLoadAllTypesOfObjects(); + + int propsCount = 0; + for (const auto& obj : objects) + { + const auto logicNode = obj->as(); + if (!logicNode) + continue; + + std::deque props{ logicNode->getInputs(), logicNode->getOutputs() }; + while (!props.empty()) + { + const auto prop = props.back(); + props.pop_back(); + if (prop == nullptr) + continue; + + propsCount++; + EXPECT_EQ(obj, &prop->getOwningLogicNode()); + + for (size_t i = 0u; i < prop->getChildCount(); ++i) + props.push_back(prop->getChild(i)); + } + } + + // just check that the iterating over all props works + EXPECT_EQ(50, propsCount); + } + + TEST_P(ALogicEngine_Serialization, persistsPropertyImplToHLObjectMapping) + { + const auto objects = saveAndLoadAllTypesOfObjects(); + + int propsCount = 0; + for (const auto& obj : objects) + { + const auto logicNode = obj->as(); + if (!logicNode) + continue; + + std::deque props{ logicNode->getInputs(), logicNode->getOutputs() }; + while (!props.empty()) + { + const auto prop = props.back(); + props.pop_back(); + if (prop == nullptr) + continue; + + propsCount++; + EXPECT_EQ(prop, &prop->m_impl->getPropertyInstance()); + + for (size_t i = 0u; i < prop->getChildCount(); ++i) + props.push_back(prop->getChild(i)); + } + } + + // just check that the iterating over all props works + EXPECT_EQ(50, propsCount); + } +} diff --git a/client/logic/unittests/api/LogicEngineTest_SerializedSize.cpp b/client/logic/unittests/api/LogicEngineTest_SerializedSize.cpp new file mode 100644 index 000000000..abf2a9b60 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_SerializedSize.cpp @@ -0,0 +1,397 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2023 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/LuaModule.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesRenderGroupBindingElements.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/AnchorPoint.h" +#include "ramses-logic/SkinBinding.h" +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/RenderPass.h" +#include "ramses-client-api/UniformInput.h" + +#include "RamsesTestUtils.h" +#include "FeatureLevelTestValues.h" + +namespace ramses::internal +{ + class ALogicEngine_SerializedSize : public ALogicEngineBase, public ::testing::TestWithParam + { + protected: + ALogicEngine_SerializedSize() + { + m_renderGroup->addMeshNode(*m_meshNode); + } + + LogicEngine m_logicEngine{ GetParam() }; + + RamsesTestSetup m_ramses; + ramses::Scene* m_scene = { m_ramses.createScene() }; + ramses::Node* m_node = { m_scene->createNode() }; + ramses::PerspectiveCamera* m_camera = { m_scene->createPerspectiveCamera() }; + ramses::Appearance* m_appearance = { &RamsesTestSetup::CreateTrivialTestAppearance(*m_scene) }; + ramses::RenderPass* m_renderPass = { m_scene->createRenderPass() }; + ramses::RenderGroup* m_renderGroup = { m_scene->createRenderGroup() }; + ramses::MeshNode* m_meshNode = { m_scene->createMeshNode("meshNode") }; + + const std::string_view m_valid_empty_script = R"( + function interface(IN,OUT) + end + function run(IN,OUT) + end + )"; + + const std::string_view m_valid_empty_interface = R"( + function interface(IN,OUT) + end + )"; + + const std::string_view m_module_source_code = R"( + local mymath = {} + function mymath.add(a,b) + return a+b + end + mymath.PI=3.1415 + return mymath + )"; + + const std::string_view m_script_source_code = R"( + function interface(IN,OUT) + IN.a = Type:Float() + IN.b = Type:Float() + OUT.value = Type:Float() + end + + function run(IN,OUT) + OUT.value = IN.a + IN.b + end + )"; + + const std::string_view m_script_with_module_source_code = R"( + modules("mymath") + + function interface(IN,OUT) + OUT.v = Type:Int32() + OUT.pi = Type:Float() + end + + function run(IN,OUT) + OUT.v = mymath.add(1,2) + OUT.pi = mymath.PI + end + )"; + + LuaScript* createScript(std::string_view source) + { + auto script = m_logicEngine.createLuaScript(source, {}, "script"); + EXPECT_NE(nullptr, script); + return script; + } + + LuaInterface* createInterface() + { + auto intf = m_logicEngine.createLuaInterface(m_valid_empty_interface, "intf"); + EXPECT_NE(nullptr, intf); + return intf; + } + + SkinBinding* createSkinBinding() + { + const auto node = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodeForSkin"); + const auto appearance = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appearanceForSkin"); + ramses::UniformInput uniform; + appearance->getRamsesAppearance().getEffect().findUniformInput("jointMat", uniform); + EXPECT_TRUE(uniform.isValid()); + return m_logicEngine.createSkinBinding({ node }, { matrix44f{ 0.f } }, *appearance, uniform, "skin"); + } + }; + + static constexpr size_t EmptySerializedSizeTotal{ 164u }; + + INSTANTIATE_TEST_SUITE_P( + ALogicEngine_SerializedSizeTests, + ALogicEngine_SerializedSize, + ramses::internal::GetFeatureLevelTestValues()); + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithoutContent) + { + EXPECT_EQ(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), EmptySerializedSizeTotal); + + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 0u); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithInterface) + { + createInterface(); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 112u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithModule) + { + this->m_logicEngine.createLuaModule(m_module_source_code, {}, "module"); + const auto result = this->m_logicEngine.getSerializedSize(ELuaSavingMode::SourceCodeOnly); + EXPECT_EQ(result, 260u); + } + + // TODO figure out how to deal with difference in exported size on different platforms + // byte code has different size on 32bit arch (currently known) + TEST_P(ALogicEngine_SerializedSize, DISABLED_ChecksSerializedSizeWithModule_withByteCode) + { + this->m_logicEngine.createLuaModule(m_module_source_code, {}, "module"); + + auto result = this->m_logicEngine.getSerializedSize(ELuaSavingMode::ByteCodeOnly); + EXPECT_EQ(result, 366u); + + result = this->m_logicEngine.getSerializedSize(ELuaSavingMode::SourceAndByteCode); + EXPECT_EQ(result, 550u); + + // default is source and bytecode + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), + this->m_logicEngine.getSerializedSize(ELuaSavingMode::SourceAndByteCode)); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithEmptyScript) + { + createScript(m_valid_empty_script); + const auto result = this->m_logicEngine.getSerializedSize(ELuaSavingMode::SourceCodeOnly); + EXPECT_EQ(result, 312u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(ELuaSavingMode::SourceCodeOnly), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithScript) + { + this->m_logicEngine.createLuaScript(m_script_source_code, {}, "script"); + const auto result = this->m_logicEngine.getSerializedSize(ELuaSavingMode::SourceCodeOnly); + EXPECT_EQ(result, 616u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(ELuaSavingMode::SourceCodeOnly), EmptySerializedSizeTotal); + } + + // TODO figure out how to deal with difference in exported size on different platforms + // byte code has different size on 32bit arch (currently known) + TEST_P(ALogicEngine_SerializedSize, DISABLED_ChecksSerializedSizeWithScript_withByteCode) + { + this->m_logicEngine.createLuaScript(m_script_source_code, {}, "script"); + + auto result = this->m_logicEngine.getSerializedSize(ELuaSavingMode::ByteCodeOnly); + EXPECT_EQ(result, 912u); + + result = this->m_logicEngine.getSerializedSize(ELuaSavingMode::SourceAndByteCode); + EXPECT_EQ(result, 1184u); + + // default is source and bytecode + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), + this->m_logicEngine.getSerializedSize(ELuaSavingMode::SourceAndByteCode)); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithScriptAndModuleDependency) + { + LuaConfig config; + const auto module = this->m_logicEngine.createLuaModule(m_module_source_code, config, "module"); + config.addDependency("mymath", *module); + this->m_logicEngine.createLuaScript(m_script_with_module_source_code, config, "script"); + + const auto result = this->m_logicEngine.getSerializedSize(ELuaSavingMode::SourceCodeOnly); + EXPECT_EQ(result, 624u); + } + + // TODO figure out how to deal with difference in exported size on different platforms + // byte code has different size on 32bit arch (currently known) + TEST_P(ALogicEngine_SerializedSize, DISABLED_ChecksSerializedSizeWithScriptAndModuleDependency_withByteCode) + { + LuaConfig config; + const auto module = this->m_logicEngine.createLuaModule(m_module_source_code, config, "module"); + config.addDependency("mymath", *module); + this->m_logicEngine.createLuaScript(m_script_with_module_source_code, config, "script"); + + auto result = this->m_logicEngine.getSerializedSize(ELuaSavingMode::ByteCodeOnly); + EXPECT_EQ(result, 1000u); + + result = this->m_logicEngine.getSerializedSize(ELuaSavingMode::SourceAndByteCode); + EXPECT_EQ(result, 1304u); + + // default is source and bytecode + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), + this->m_logicEngine.getSerializedSize(ELuaSavingMode::SourceAndByteCode)); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithNodeBinding) + { + this->m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "node"); + const auto result = this->m_logicEngine.getSerializedSize(); + EXPECT_EQ(result, 440u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithAppearanceBinding) + { + this->m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "appearance"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 256u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithCameraBinding) + { + this->m_logicEngine.createRamsesCameraBinding(*m_camera, "camera"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 632u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithCameraBindingWithFrustumPlanes) + { + this->m_logicEngine.createRamsesCameraBindingWithFrustumPlanes(*m_camera, "camera"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 728u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithRenderPassBinding) + { + this->m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderpass"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 376u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithRenderGroupBinding) + { + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*m_meshNode)); + this->m_logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements, "rg"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 336u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithMeshNodeBinding) + { + this->m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 376u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + // Since there is a template specialization for animations collecting data arrays + // we test with different data arrays and reusing one single data array. + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithLinearAnimation_differentArrays) + { + // Test animation channel with different data arrays + auto dataArray1 = this->m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data1"); + auto dataArray2 = this->m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data2"); + ASSERT_NE(nullptr, dataArray1); + ASSERT_NE(nullptr, dataArray2); + + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray1, dataArray2, EInterpolationType::Linear }); + this->m_logicEngine.createAnimationNode(config, "animation"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 378u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithLinearAnimation_sameArray) + { + // Test animation channel with the same data arrays + auto data = this->m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data"); + ASSERT_NE(nullptr, data); + + AnimationNodeConfig config; + config.addChannel({ "channel", data, data, EInterpolationType::Linear }); + this->m_logicEngine.createAnimationNode(config, "animation"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 378u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + // Since there is a template specialization for animations collecting data arrays + // we test with different data arrays and reusing one single data array. + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithCubicAnimation_differentArrays) + { + // Test animation channel with different data arrays + auto dataArray1 = this->m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data1"); + auto dataArray2 = this->m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data2"); + auto dataArray3 = this->m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data3"); + auto dataArray4 = this->m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data4"); + ASSERT_NE(nullptr, dataArray1); + ASSERT_NE(nullptr, dataArray2); + ASSERT_NE(nullptr, dataArray3); + ASSERT_NE(nullptr, dataArray4); + + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray1, dataArray2, EInterpolationType::Cubic, dataArray3, dataArray4 }); + this->m_logicEngine.createAnimationNode(config, "animation"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 378u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithCubicAnimation_sameArray) + { + // Test animation channel with the same data arrays + auto data = this->m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data"); + ASSERT_NE(nullptr, data); + + AnimationNodeConfig config; + config.addChannel({ "channel", data, data, EInterpolationType::Cubic, data, data }); + this->m_logicEngine.createAnimationNode(config, "animation"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 378u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithDataArray) + { + this->m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 98u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithTimer) + { + this->m_logicEngine.createTimerNode("timer"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 290u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithAnchorPoint) + { + const auto node = this->m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "node"); + const auto camera = this->m_logicEngine.createRamsesCameraBinding(*m_camera, "camera"); + ASSERT_TRUE(node && camera); + this->m_logicEngine.createAnchorPoint(*node, *camera, "timer"); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 248u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } + + TEST_P(ALogicEngine_SerializedSize, ChecksSerializedSizeWithSkinBinding) + { + createSkinBinding(); + EXPECT_EQ(this->m_logicEngine.getSerializedSize(), 184u); + EXPECT_GT(this->m_logicEngine.getTotalSerializedSize(), EmptySerializedSizeTotal); + } +} diff --git a/client/logic/unittests/api/LogicEngineTest_Update.cpp b/client/logic/unittests/api/LogicEngineTest_Update.cpp new file mode 100644 index 000000000..b3c549e64 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_Update.cpp @@ -0,0 +1,474 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include + +#include "LogicEngineTest_Base.h" + +#include "RamsesTestUtils.h" + +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" + +#include "ramses-logic/Property.h" + +#include "ramses-client-api/EffectDescription.h" +#include "ramses-client-api/Effect.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/Node.h" + +#include "impl/LogicNodeImpl.h" +#include "impl/LogicEngineImpl.h" + +#include "fmt/format.h" + +namespace ramses +{ + class ALogicEngine_Update : public ALogicEngine + { + }; + + TEST_F(ALogicEngine_Update, UpdatesRamsesNodeBindingValuesOnUpdate) + { + auto luaScript = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.param = Type:Bool() + OUT.param = Type:Bool() + end + function run(IN,OUT) + OUT.param = IN.param + end + )"); + + auto ramsesNodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + + auto scriptInput = luaScript->getInputs()->getChild("param"); + auto scriptOutput = luaScript->getOutputs()->getChild("param"); + auto nodeInput = ramsesNodeBinding->getInputs()->getChild("visibility"); + scriptInput->set(true); + nodeInput->set(false); + + m_logicEngine.link(*scriptOutput, *nodeInput); + + EXPECT_FALSE(*nodeInput->get()); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(*nodeInput->get()); + } + + TEST_F(ALogicEngine_Update, UpdatesRamsesCameraBindingValuesOnUpdate) + { + RamsesTestSetup testSetup; + ramses::Scene* scene = testSetup.createScene(); + + auto luaScript = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.param = Type:Int32() + OUT.param = Type:Int32() + end + function run(IN,OUT) + OUT.param = IN.param + end + )"); + + auto ramsesCameraBinding = m_logicEngine.createRamsesCameraBinding(*scene->createPerspectiveCamera(), "CameraBinding"); + + auto scriptInput = luaScript->getInputs()->getChild("param"); + auto scriptOutput = luaScript->getOutputs()->getChild("param"); + auto cameraInput = ramsesCameraBinding->getInputs()->getChild("viewport")->getChild("offsetX"); + scriptInput->set(34); + cameraInput->set(21); + + m_logicEngine.link(*scriptOutput, *cameraInput); + + EXPECT_EQ(21, *cameraInput->get()); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(34, *cameraInput->get()); + } + + TEST_F(ALogicEngine_Update, UpdatesARamsesAppearanceBinding) + { + RamsesTestSetup testSetup; + ramses::Scene* scene = testSetup.createScene(); + + ramses::EffectDescription effectDesc; + effectDesc.setFragmentShader(R"( + #version 100 + + void main(void) + { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + })"); + + effectDesc.setVertexShader(R"( + #version 100 + + uniform highp float floatUniform; + attribute vec3 a_position; + + void main() + { + gl_Position = floatUniform * vec4(a_position, 1.0); + })"); + + const ramses::Effect* effect = scene->createEffect(effectDesc); + ramses::Appearance* appearance = scene->createAppearance(*effect); + + auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*appearance, "appearancebinding"); + + auto floatUniform = appearanceBinding->getInputs()->getChild("floatUniform"); + floatUniform->set(47.11f); + + m_logicEngine.update(); + + ramses::UniformInput floatInput; + effect->findUniformInput("floatUniform", floatInput); + float result = 0.0f; + appearance->getInputValue(floatInput, result); + EXPECT_FLOAT_EQ(47.11f, result); + } + + TEST_F(ALogicEngine_Update, ProducesErrorIfLinkedScriptHasRuntimeError) + { + auto scriptSource = R"( + function interface(IN,OUT) + IN.param = Type:Bool() + OUT.param = Type:Bool() + end + function run(IN,OUT) + error("This will die") + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(scriptSource, WithStdModules({EStandardModule::Base})); + auto targetScript = m_logicEngine.createLuaScript(scriptSource, WithStdModules({EStandardModule::Base})); + + auto output = sourceScript->getOutputs()->getChild("param"); + auto input = targetScript->getInputs()->getChild("param"); + input->set(true); + + m_logicEngine.link(*output, *input); + + EXPECT_FALSE(m_logicEngine.update()); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("This will die")); + EXPECT_THAT(m_logicEngine.getErrors()[0].object, sourceScript); + } + + TEST_F(ALogicEngine_Update, PropagatesValuesOnlyToConnectedLogicNodes) + { + auto scriptSource = R"( + function interface(IN,OUT) + IN.inFloat = Type:Float() + IN.inVec3 = Type:Vec3f() + IN.inInt = Type:Int32() + OUT.outFloat = Type:Float() + OUT.outVec3 = Type:Vec3f() + OUT.outInt = Type:Int32() + end + function run(IN,OUT) + OUT.outFloat = IN.inFloat + OUT.outVec3 = IN.inVec3 + OUT.outInt = IN.inInt + end + )"; + + auto script = m_logicEngine.createLuaScript(scriptSource); + auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*m_camera, "CameraBinding"); + + auto nodeBindingTranslation = nodeBinding->getInputs()->getChild("translation"); + nodeBindingTranslation->set(vec3f{1.f, 2.f, 3.f}); + auto appearanceBindingFloatUniform = appearanceBinding->getInputs()->getChild("floatUniform"); + appearanceBindingFloatUniform->set(42.f); + auto cameraBindingViewportOffsetX = cameraBinding->getInputs()->getChild("viewport")->getChild("offsetX"); + cameraBindingViewportOffsetX->set(43); + + m_logicEngine.update(); + + ramses::UniformInput floatInput; + m_appearance->getEffect().findUniformInput("floatUniform", floatInput); + float floatUniformValue = 0.0f; + m_appearance->getInputValue(floatInput, floatUniformValue); + + EXPECT_FLOAT_EQ(42.f, floatUniformValue); + EXPECT_EQ(43, m_camera->getViewportX()); + { + vec3f values = {0.0f, 0.0f, 0.0f}; + m_node->getTranslation(values); + EXPECT_EQ(values, vec3f(1.f, 2.f, 3.f)); + } + + auto nodeBindingScaling = nodeBinding->getInputs()->getChild("scaling"); + auto cameraBindingVpY = cameraBinding->getInputs()->getChild("viewport")->getChild("offsetY"); + auto scriptOutputVec3 = script->getOutputs()->getChild("outVec3"); + auto scriptOutputFloat = script->getOutputs()->getChild("outFloat"); + auto scriptOutputInt = script->getOutputs()->getChild("outInt"); + auto scriptInputVec3 = script->getInputs()->getChild("inVec3"); + auto scriptInputFloat = script->getInputs()->getChild("inFloat"); + auto scriptInputInt = script->getInputs()->getChild("inInt"); + auto appearanceInput = appearanceBinding->getInputs()->getChild("floatUniform"); + + m_logicEngine.link(*scriptOutputVec3, *nodeBindingScaling); + scriptInputVec3->set(vec3f{3.f, 2.f, 1.f}); + scriptInputFloat->set(42.f); + scriptInputInt->set(43); + + m_logicEngine.update(); + EXPECT_FLOAT_EQ(42.f, floatUniformValue); + EXPECT_EQ(43, m_camera->getViewportX()); + { + vec3f values = {0.0f, 0.0f, 0.0f}; + m_node->getTranslation(values); + EXPECT_EQ(values, vec3f(1.f, 2.f, 3.f)); + } + { + vec3f values = {0.0f, 0.0f, 0.0f}; + m_node->getScaling(values); + EXPECT_EQ(values, vec3f(3.f, 2.f, 1.f)); + } + { + vec3f values = {0.0f, 0.0f, 0.0f}; + m_node->getRotation(values); + EXPECT_EQ(values, vec3f(0.f, 0.f, 0.f)); + } + + ramses::UniformInput floatUniform; + m_appearance->getEffect().findUniformInput("floatUniform", floatUniform); + floatUniformValue = 0.0f; + m_appearance->getInputValue(floatUniform, floatUniformValue); + + EXPECT_FLOAT_EQ(42.f, floatUniformValue); + + m_logicEngine.link(*scriptOutputFloat, *appearanceInput); + m_logicEngine.link(*scriptOutputInt, *cameraBindingVpY); + + m_logicEngine.update(); + + m_appearance->getInputValue(floatUniform, floatUniformValue); + EXPECT_FLOAT_EQ(42.f, floatUniformValue); + + EXPECT_EQ(43, m_camera->getViewportY()); + + m_logicEngine.unlink(*scriptOutputVec3, *nodeBindingScaling); + } + + TEST_F(ALogicEngine_Update, OnlyUpdatesDirtyLogicNodes) + { + m_logicEngine.enableUpdateReport(true); + + auto scriptSource = R"( + function interface(IN,OUT) + IN.inFloat = Type:Float() + OUT.outFloat = Type:Float() + end + function run(IN,OUT) + OUT.outFloat = IN.inFloat + end + )"; + + auto sourceScript = m_logicEngine.createLuaScript(scriptSource, {}); + auto targetScript = m_logicEngine.createLuaScript(scriptSource, {}); + + auto sourceInput = sourceScript->getInputs()->getChild("inFloat"); + auto sourceOutput = sourceScript->getOutputs()->getChild("outFloat"); + auto targetInput = targetScript->getInputs()->getChild("inFloat"); + + EXPECT_TRUE(sourceScript->m_impl.isDirty()); + EXPECT_TRUE(targetScript->m_impl.isDirty()); + + m_logicEngine.link(*sourceOutput, *targetInput); + + EXPECT_TRUE(sourceScript->m_impl.isDirty()); + EXPECT_TRUE(targetScript->m_impl.isDirty()); + + m_logicEngine.update(); + + EXPECT_FALSE(sourceScript->m_impl.isDirty()); + EXPECT_FALSE(targetScript->m_impl.isDirty()); + + // both scripts are updated, because its the first update + auto executedNodes = m_logicEngine.getLastUpdateReport().getNodesExecuted(); + ASSERT_EQ(2u, executedNodes.size()); + EXPECT_EQ(sourceScript, executedNodes[0].first); + EXPECT_EQ(targetScript, executedNodes[1].first); + + m_logicEngine.update(); + + EXPECT_TRUE(m_logicEngine.getLastUpdateReport().getNodesExecuted().empty()); + + targetInput->set(42.f); + + // targetScript is linked input and cannot be set manually so it is not dirty + EXPECT_FALSE(sourceScript->m_impl.isDirty()); + EXPECT_FALSE(targetScript->m_impl.isDirty()); + + m_logicEngine.update(); + + EXPECT_FALSE(sourceScript->m_impl.isDirty()); + EXPECT_FALSE(targetScript->m_impl.isDirty()); + + // Nothing is updated, because targetScript is linked input and cannot be set manually + EXPECT_TRUE(m_logicEngine.getLastUpdateReport().getNodesExecuted().empty()); + + sourceInput->set(24.f); + m_logicEngine.update(); + + // Both scripts are updated, because the input of the first script is changed and changes target through link. + executedNodes = m_logicEngine.getLastUpdateReport().getNodesExecuted(); + ASSERT_EQ(2u, executedNodes.size()); + EXPECT_EQ(sourceScript, executedNodes[0].first); + EXPECT_EQ(targetScript, executedNodes[1].first); + } + + TEST_F(ALogicEngine_Update, OnlyUpdatesDirtyLogicNodesInAComplexLogicGraph) + { + auto scriptSource = R"( + function interface(IN,OUT) + IN.in1 = Type:Int32() + IN.in2 = Type:Int32() + OUT.out = Type:Int32() + end + function run(IN,OUT) + OUT.out = IN.in1 + IN.in2 + end + )"; + + std::array s = {}; + for(auto& si : s) + { + si = m_logicEngine.createLuaScript(scriptSource); + } + + auto in1S0 = s[0]->getInputs()->getChild("in1"); + auto out1S0 = s[0]->getOutputs()->getChild("out"); + auto in1S1 = s[1]->getInputs()->getChild("in1"); + auto in2S1 = s[1]->getInputs()->getChild("in2"); + auto out1S1 = s[1]->getOutputs()->getChild("out"); + auto in1S2 = s[2]->getInputs()->getChild("in1"); + auto in2S2 = s[2]->getInputs()->getChild("in2"); + auto out1S2 = s[2]->getOutputs()->getChild("out"); + auto in1S3 = s[3]->getInputs()->getChild("in1"); + auto in2S3 = s[3]->getInputs()->getChild("in2"); + auto out1S3 = s[3]->getOutputs()->getChild("out"); + auto in1S4 = s[4]->getInputs()->getChild("in1"); + auto in2S4 = s[4]->getInputs()->getChild("in2"); + auto out1S4 = s[4]->getOutputs()->getChild("out"); + auto in1S5 = s[5]->getInputs()->getChild("in1"); + auto in2S5 = s[5]->getInputs()->getChild("in2"); + + /* + s2 ------- + / \ \ + s0 ----- s1 -- s3 - s5 + \ / + s4 + */ + + ASSERT_TRUE(m_logicEngine.link(*out1S0, *in2S2)); + ASSERT_TRUE(m_logicEngine.link(*out1S0, *in2S1)); + ASSERT_TRUE(m_logicEngine.link(*out1S1, *in2S3)); + ASSERT_TRUE(m_logicEngine.link(*out1S2, *in1S1)); + ASSERT_TRUE(m_logicEngine.link(*out1S2, *in1S3)); + ASSERT_TRUE(m_logicEngine.link(*out1S3, *in1S5)); + ASSERT_TRUE(m_logicEngine.link(*out1S3, *in1S4)); + ASSERT_TRUE(m_logicEngine.link(*out1S4, *in2S5)); + + m_logicEngine.enableUpdateReport(true); + + auto expectScriptsExecutedInOrder = [&s, this](std::vector expectedOrder) + { + m_logicEngine.update(); + + auto executedNodes = m_logicEngine.getLastUpdateReport().getNodesExecuted(); + ASSERT_EQ(expectedOrder.size(), executedNodes.size()); + for (size_t i = 0; i < expectedOrder.size(); ++i) + { + EXPECT_EQ(s[expectedOrder[i]], executedNodes[i].first) << "Wrong order for script: " << i << "; expected: " << expectedOrder[i]; + } + }; + + // Based on topology and first script dirty -> executes all scripts + expectScriptsExecutedInOrder({0u, 2u, 1u, 3u, 4u, 5u}); + // Nothing dirty -> executes no scripts + expectScriptsExecutedInOrder({}); + + // Set value of script 4 -> scripts 4 and 5 are executed + in2S4->set(1); + expectScriptsExecutedInOrder({4u, 5u}); + expectScriptsExecutedInOrder({}); + + in1S2->set(2); + expectScriptsExecutedInOrder({ 2u, 1u, 3u, 4u, 5u }); + expectScriptsExecutedInOrder({}); + + in1S0->set(42); + expectScriptsExecutedInOrder({ 0u, 2u, 1u, 3u, 4u, 5u }); + expectScriptsExecutedInOrder({}); + + in1S0->set(24); + in1S2->set(23); + expectScriptsExecutedInOrder({ 0u, 2u, 1u, 3u, 4u, 5u }); + expectScriptsExecutedInOrder({}); + } + + TEST_F(ALogicEngine_Update, AlwaysUpdatesNodeIfDirtyHandlingIsDisabled) + { + auto scriptSource = R"( + function interface(IN,OUT) + IN.inFloat = Type:Float() + OUT.outFloat = Type:Float() + end + function run(IN,OUT) + OUT.outFloat = IN.inFloat + end + )"; + + m_logicEngine.m_impl->disableTrackingDirtyNodes(); + m_logicEngine.enableUpdateReport(true); + + auto sourceScript = m_logicEngine.createLuaScript(scriptSource, {}, "SourceScript"); + auto targetScript = m_logicEngine.createLuaScript(scriptSource, {}, "TargetScript"); + + auto sourceInput = sourceScript->getInputs()->getChild("inFloat"); + auto sourceOutput = sourceScript->getOutputs()->getChild("outFloat"); + auto targetInput = targetScript->getInputs()->getChild("inFloat"); + + m_logicEngine.link(*sourceOutput, *targetInput); + m_logicEngine.update(); + + // both scripts are updated, because its the first update + auto executedNodes = m_logicEngine.getLastUpdateReport().getNodesExecuted(); + ASSERT_EQ(2u, executedNodes.size()); + EXPECT_EQ(sourceScript, executedNodes[0].first); + EXPECT_EQ(targetScript, executedNodes[1].first); + + m_logicEngine.unlink(*sourceOutput, *targetInput); + targetInput->set(42.f); + m_logicEngine.update(); + + // Both scripts are updated, because dirty handling is disabled + executedNodes = m_logicEngine.getLastUpdateReport().getNodesExecuted(); + ASSERT_EQ(2u, executedNodes.size()); + EXPECT_EQ(sourceScript, executedNodes[0].first); + EXPECT_EQ(targetScript, executedNodes[1].first); + + sourceInput->set(24.f); + m_logicEngine.update(); + + // Both scripts are updated, because dirty handling is disabled + executedNodes = m_logicEngine.getLastUpdateReport().getNodesExecuted(); + ASSERT_EQ(2u, executedNodes.size()); + EXPECT_EQ(sourceScript, executedNodes[0].first); + EXPECT_EQ(targetScript, executedNodes[1].first); + } +} diff --git a/client/logic/unittests/api/LogicEngineTest_UpdateReport.cpp b/client/logic/unittests/api/LogicEngineTest_UpdateReport.cpp new file mode 100644 index 000000000..baaa3bfac --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_UpdateReport.cpp @@ -0,0 +1,292 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include +#include "LogicEngineTest_Base.h" +#include "ramses-logic/Property.h" +#include + +namespace ramses +{ + class ALogicEngine_UpdateReport : public ALogicEngine + { + protected: + static void expectReportContainsExecutedNodes(const LogicEngineReport& report, std::vector expectedNodes) + { + const auto& executedNodes = report.getNodesExecuted(); + ASSERT_EQ(executedNodes.size(), expectedNodes.size()); + for (size_t i = 0u; i < executedNodes.size(); ++i) + { + EXPECT_EQ(executedNodes[i].first, expectedNodes[i]); + // measured time cannot be consistently tested, in some test runs the time measured can be reported as 0 + // (fast release builds and/or not fine-grained enough clock) + //EXPECT_TRUE(executedNodes[i].second.count() > 0); + } + } + }; + + TEST_F(ALogicEngine_UpdateReport, UpdateReportIsEmptyIfDisabledAndStatisticsAlsoDisabled) + { + // statistics are implicitly disabled + m_logicEngine.setStatisticsLoggingRate(0u); + + constexpr auto scriptSource = R"( + function interface(IN,OUT) + IN.param = Type:Int32() + end + function run(IN,OUT) + end + )"; + + auto node1 = m_logicEngine.createLuaScript(scriptSource); + auto node2 = m_logicEngine.createLuaScript(scriptSource); + auto node3 = m_logicEngine.createLuaScript(scriptSource); + + // first enable update reporting to fill some data + m_logicEngine.enableUpdateReport(true); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_FALSE(m_logicEngine.getLastUpdateReport().getNodesExecuted().empty()); + + // make nodes dirty + node1->getInputs()->getChild(0u)->set(13); + node2->getInputs()->getChild(0u)->set(13); + node3->getInputs()->getChild(0u)->set(13); + + // disable reporting and expect empty + m_logicEngine.enableUpdateReport(false); + EXPECT_TRUE(m_logicEngine.update()); + const auto report = m_logicEngine.getLastUpdateReport(); + EXPECT_TRUE(report.getNodesExecuted().empty()); + EXPECT_TRUE(report.getNodesSkippedExecution().empty()); + EXPECT_EQ(report.getTopologySortExecutionTime().count(), 0); + EXPECT_EQ(report.getTotalUpdateExecutionTime().count(), 0); + EXPECT_EQ(report.getTotalLinkActivations(), 0u); + } + + TEST_F(ALogicEngine_UpdateReport, UpdateReportContainsUpdatedAndNotUpdatedNodes) + { + constexpr auto scriptSource = R"( + function interface(IN,OUT) + IN.param = Type:Int32() + OUT.param = Type:Int32() + end + function run(IN,OUT) + OUT.param = IN.param + end + )"; + + auto node1 = m_logicEngine.createLuaScript(scriptSource); + auto node2 = m_logicEngine.createLuaScript(scriptSource); + auto node3 = m_logicEngine.createLuaScript(scriptSource); + + // link nodes so order of execution is deterministic + m_logicEngine.link(*node1->getOutputs()->getChild(0u), *node2->getInputs()->getChild(0u)); + m_logicEngine.link(*node2->getOutputs()->getChild(0u), *node3->getInputs()->getChild(0u)); + + m_logicEngine.enableUpdateReport(true); + + // all updated + EXPECT_TRUE(m_logicEngine.update()); + { + const auto report = m_logicEngine.getLastUpdateReport(); + expectReportContainsExecutedNodes(report, { node1, node2, node3 }); + EXPECT_TRUE(report.getNodesSkippedExecution().empty()); + } + + // none updated + EXPECT_TRUE(m_logicEngine.update()); + { + const auto report = m_logicEngine.getLastUpdateReport(); + EXPECT_TRUE(report.getNodesExecuted().empty()); + EXPECT_THAT(report.getNodesSkippedExecution(), ::testing::ElementsAre(node1, node2, node3)); + } + + // re-link to trigger dirty + m_logicEngine.unlink(*node2->getOutputs()->getChild(0u), *node3->getInputs()->getChild(0u)); + m_logicEngine.link(*node2->getOutputs()->getChild(0u), *node3->getInputs()->getChild(0u)); + // node2 and node3 updated + EXPECT_TRUE(m_logicEngine.update()); + { + const auto report = m_logicEngine.getLastUpdateReport(); + expectReportContainsExecutedNodes(report, { node2, node3 }); + EXPECT_THAT(report.getNodesSkippedExecution(), ::testing::ElementsAre(node1)); + } + } + + TEST_F(ALogicEngine_UpdateReport, UpdateReportCanBeStoredInContainer) + { + constexpr auto scriptSource = R"( + function interface(IN,OUT) + IN.param = Type:Int32() + end + function run(IN,OUT) + end + )"; + + auto node1 = m_logicEngine.createLuaScript(scriptSource); + auto node2 = m_logicEngine.createLuaScript(scriptSource); + + // first enable update reporting to fill some data + m_logicEngine.enableUpdateReport(true); + EXPECT_TRUE(m_logicEngine.update()); + auto report1 = m_logicEngine.getLastUpdateReport(); + + EXPECT_TRUE(m_logicEngine.update()); + auto report2 = m_logicEngine.getLastUpdateReport(); + + node1->getInputs()->getChild(0u)->set(13); + node2->getInputs()->getChild(0u)->set(13); + EXPECT_TRUE(m_logicEngine.update()); + auto report3 = m_logicEngine.getLastUpdateReport(); + + std::vector reports; + reports.resize(2u); + reports[0] = std::move(report1); + reports[1] = std::move(report2); + reports.push_back(std::move(report3)); + + EXPECT_EQ(2u, reports[0].getNodesExecuted().size()); + EXPECT_EQ(2u, reports[1].getNodesSkippedExecution().size()); + EXPECT_EQ(2u, reports[2].getNodesExecuted().size()); + + auto reportsOther = std::move(reports); + EXPECT_EQ(2u, reportsOther[0].getNodesExecuted().size()); + EXPECT_EQ(2u, reportsOther[1].getNodesSkippedExecution().size()); + EXPECT_EQ(2u, reportsOther[2].getNodesExecuted().size()); + } + + TEST_F(ALogicEngine_UpdateReport, UpdateReportHasExecutionTimings) + { + constexpr auto slowScriptSource = R"( + function interface(IN,OUT) + end + function run(IN,OUT) + local str="a" + for i=0,1000 do + str = str .. "a" + end + end + )"; + + m_logicEngine.createLuaScript(slowScriptSource); + m_logicEngine.createLuaScript(slowScriptSource); + m_logicEngine.createLuaScript(slowScriptSource); + + m_logicEngine.enableUpdateReport(true); + + EXPECT_TRUE(m_logicEngine.update()); + { + const auto report = m_logicEngine.getLastUpdateReport(); + EXPECT_EQ(3u, report.getNodesExecuted().size()); + EXPECT_TRUE(report.getNodesSkippedExecution().empty()); + + // expect that total update time >= individual times summed up + const auto nodesUpdatesTime = std::accumulate(report.getNodesExecuted().cbegin(), report.getNodesExecuted().cend(), std::chrono::microseconds{ 0u }, + [](auto val, const LogicEngineReport::LogicNodeTimed& n) { return val + n.second; }); + EXPECT_GE(report.getTotalUpdateExecutionTime(), report.getTopologySortExecutionTime() + nodesUpdatesTime); + } + } + + TEST_F(ALogicEngine_UpdateReport, HasLinkActivations) + { + constexpr auto scriptSource = R"( + function interface(IN,OUT) + IN.ints = { + int1 = Type:Int32(), + int2 = Type:Int32() + } + OUT.ints = { + int1 = Type:Int32(), + int2 = Type:Int32() + } + end + function run(IN,OUT) + OUT.ints = IN.ints + end + )"; + + LuaScript* s1 = m_logicEngine.createLuaScript(scriptSource); + LuaScript* s2 = m_logicEngine.createLuaScript(scriptSource); + + m_logicEngine.link(*s1->getOutputs()->getChild("ints")->getChild("int1"), *s2->getInputs()->getChild("ints")->getChild("int1")); + m_logicEngine.link(*s1->getOutputs()->getChild("ints")->getChild("int2"), *s2->getInputs()->getChild("ints")->getChild("int2")); + + m_logicEngine.enableUpdateReport(true); + + // set non-default data + s1->getInputs()->getChild("ints")->getChild("int1")->set(5); + s1->getInputs()->getChild("ints")->getChild("int2")->set(5); + EXPECT_TRUE(m_logicEngine.update()); + { + const auto report = m_logicEngine.getLastUpdateReport(); + EXPECT_EQ(2u, report.getTotalLinkActivations()); + } + EXPECT_TRUE(m_logicEngine.update()); + { + const auto report = m_logicEngine.getLastUpdateReport(); + EXPECT_EQ(0u, report.getTotalLinkActivations()); + } + s1->getInputs()->getChild("ints")->getChild("int1")->set(6); + EXPECT_TRUE(m_logicEngine.update()); + { + const auto report = m_logicEngine.getLastUpdateReport(); + EXPECT_EQ(1u, report.getTotalLinkActivations()); + } + s1->getInputs()->getChild("ints")->getChild("int1")->set(7); + s1->getInputs()->getChild("ints")->getChild("int2")->set(7); + EXPECT_TRUE(m_logicEngine.update()); + { + const auto report = m_logicEngine.getLastUpdateReport(); + EXPECT_EQ(2u, report.getTotalLinkActivations()); + } + } + + TEST_F(ALogicEngine_UpdateReport, UpdateReportCanBeRetrievedNextSuccessUpdateAfterFailedUpdate) + { + constexpr auto scriptSource = R"( + function interface(IN,OUT) + IN.param = Type:Int32() + OUT.param = Type:Int32() + end + function run(IN,OUT) + if IN.param == 33 then error() end + OUT.param = IN.param + end + )"; + + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Base); + auto node1 = m_logicEngine.createLuaScript(scriptSource, config); + auto node2 = m_logicEngine.createLuaScript(scriptSource, config); + auto node3 = m_logicEngine.createLuaScript(scriptSource, config); + + // link nodes so order of execution is deterministic + m_logicEngine.link(*node1->getOutputs()->getChild(0u), *node2->getInputs()->getChild(0u)); + m_logicEngine.link(*node2->getOutputs()->getChild(0u), *node3->getInputs()->getChild(0u)); + + m_logicEngine.enableUpdateReport(true); + + // make it fail + node1->getInputs()->getChild(0u)->set(33); + EXPECT_FALSE(m_logicEngine.update()); + + // all updated + node1->getInputs()->getChild(0u)->set(1); + EXPECT_TRUE(m_logicEngine.update()); + { + const auto report = m_logicEngine.getLastUpdateReport(); + expectReportContainsExecutedNodes(report, { node1, node2, node3 }); + EXPECT_TRUE(report.getNodesSkippedExecution().empty()); + + // expect that total update time >= individual times summed up + const auto nodesUpdatesTime = std::accumulate(report.getNodesExecuted().cbegin(), report.getNodesExecuted().cend(), std::chrono::microseconds{ 0u }, + [](auto val, const LogicEngineReport::LogicNodeTimed& n) { return val + n.second; }); + EXPECT_GE(report.getTotalUpdateExecutionTime(), report.getTopologySortExecutionTime() + nodesUpdatesTime); + } + } +} diff --git a/client/logic/unittests/api/LogicEngineTest_Validation.cpp b/client/logic/unittests/api/LogicEngineTest_Validation.cpp new file mode 100644 index 000000000..28b433768 --- /dev/null +++ b/client/logic/unittests/api/LogicEngineTest_Validation.cpp @@ -0,0 +1,566 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "ramses-logic/Logger.h" +#include "ramses-logic/Property.h" +#include "ramses-framework-api/RamsesFrameworkTypes.h" +#include "ramses-logic/AnimationNodeConfig.h" + +#include "impl/LoggerImpl.h" + +#include "LogTestUtils.h" +#include "WithTempDirectory.h" +#include "RamsesTestUtils.h" + +namespace ramses +{ + class ALogicEngine_Validation : public ALogicEngine + { + }; + + TEST_F(ALogicEngine_Validation, LogsNoWarningsWhenSavingFile_WhenContentValid) + { + ScopedLogContextLevel logCollector{ ELogLevel::Trace, [&](ELogLevel type, std::string_view /*message*/) + { + if (type == ELogLevel::Warn) + { + FAIL() << "Should have no warnings!"; + } + } + }; + + WithTempDirectory tmpDir; + // Empty logic engine has ho warnings + m_logicEngine.saveToFile("noWarnings.rlogic"); + } + + TEST_F(ALogicEngine_Validation, LogsWarningsWhenSavingFile_WhenContentHasValidationIssues) + { + std::vector warnings; + ScopedLogContextLevel logCollector{ ELogLevel::Trace, [&](ELogLevel type, std::string_view message) + { + if (type == ELogLevel::Warn) + { + warnings.emplace_back(message); + } + } + }; + + WithTempDirectory tmpDir; + // Cause some validation issues on purpose + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + nodeBinding.getInputs()->getChild("scaling")->set({1.5f, 1.f, 1.f}); + + m_logicEngine.saveToFile("noWarnings.rlogic"); + + ASSERT_EQ(warnings.size(), 2u); + EXPECT_EQ(warnings[0], "Saving logic engine content with manually updated binding values without calling update() will result in those values being lost!"); + EXPECT_EQ(warnings[1], "[NodeBinding [Id=1]] Node [NodeBinding] has no ingoing links! Node should be deleted or properly linked!"); + + // Fixing the problems -> removes the warning + warnings.clear(); + const auto* intf = m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + m_logicEngine.link(*intf->getOutputs()->getChild("param_vec3f"), *nodeBinding.getInputs()->getChild("scaling")); + m_logicEngine.update(); + + m_logicEngine.saveToFile("noWarnings.rlogic"); + + EXPECT_TRUE(warnings.empty()); + } + + TEST_F(ALogicEngine_Validation, LogsNoContentWarningsWhenSavingFile_WhenContentHasValidationIssues_ButValidationIsDisabled) + { + std::vector infoLogs; + ScopedLogContextLevel logCollector{ ELogLevel::Trace, [&](ELogLevel type, std::string_view message) + { + if (type == ELogLevel::Info) + { + infoLogs.emplace_back(message); + } + else + { + FAIL() << "Unexpected log!"; + } + } + }; + + WithTempDirectory tmpDir; + // Cause some validation issues on purpose + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + nodeBinding.getInputs()->getChild("scaling")->set({ 1.5f, 1.f, 1.f }); + + SaveFileConfig conf; + conf.setValidationEnabled(false); + + // Disabling the validation causes a warning + ASSERT_EQ(infoLogs.size(), 1u); + EXPECT_EQ(infoLogs[0], "Validation before saving was disabled during save*() calls! Possible content issues will not yield further warnings."); + infoLogs.clear(); + + // Content warning doesn't show up because disabled + m_logicEngine.saveToFile("noWarnings.rlogic", conf); + } + + TEST_F(ALogicEngine_Validation, ProducesWarningIfBindingValuesHaveDirtyValue) + { + // Create a binding in a "dirty" state - has a non-default value, but update() wasn't called and didn't pass the value to ramses + RamsesNodeBinding* nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "binding"); + + const auto* intf = m_logicEngine.createLuaInterface(m_interfaceSourceCode, "intf"); + m_logicEngine.link(*intf->getOutputs()->getChild("param_vec3f"), *nodeBinding->getInputs()->getChild("scaling")); + + nodeBinding->getInputs()->getChild("visibility")->set(false); + + // Expects warning + auto warnings = m_logicEngine.validate(); + + ASSERT_EQ(1u, warnings.size()); + EXPECT_EQ(warnings[0].message, "Saving logic engine content with manually updated binding values without calling update() will result in those values being lost!"); + EXPECT_EQ(warnings[0].type, EWarningType::UnsafeDataState); + } + + TEST_F(ALogicEngine_Validation, ProducesWarningIfInterfaceHasUnboundOutputs) + { + LuaInterface* intf = m_logicEngine.createLuaInterface(R"( + function interface(IN,OUT) + + IN.param1 = Type:Int32() + IN.param2 = {a=Type:Float(), b=Type:Int32()} + + end + )", "intf name"); + ASSERT_NE(nullptr, intf); + + // Expects warning + auto warnings = m_logicEngine.validate(); + + ASSERT_EQ(3u, warnings.size()); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::message, ::testing::HasSubstr("Interface [intf name] has unlinked output")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + class ALogicEngine_ValidatingDanglingNodes : public ALogicEngine + { + protected: + ALogicEngine_ValidatingDanglingNodes() + { + createValidTestSetup(); + } + + void createValidTestSetup() + { + //create two identical scripts each containing 1 input and 1 output + m_testScript = m_logicEngine.createLuaScript(m_scriptSrc, {}, "script1"); + auto* script2 = m_logicEngine.createLuaScript(m_scriptSrc, {}, "script2"); + + //cross link inputs and outputs of each script so that none of the scripts is "dangling" + m_logicEngine.link(*m_testScript->getOutputs()->getChild("paramInt32"), *script2->getInputs()->getChild("paramTestReserved")); + m_logicEngine.linkWeak(*script2->getOutputs()->getChild("paramInt32"), *m_testScript->getInputs()->getChild("paramTestReserved")); + } + + void linkNodeInput(LogicNode& node, std::string_view inputName, std::string_view testScriptOutputName = "paramInt32") + { + linkNodeInput(*node.getInputs()->getChild(inputName), testScriptOutputName); + } + + void linkNodeInput(Property& nodeInput, std::string_view testScriptOutputName = "paramInt32") + { + m_logicEngine.link(*m_testScript->getOutputs()->getChild(testScriptOutputName), nodeInput); + } + + void linkNodeOutput(LogicNode& node, std::string_view outputName, std::string_view testScriptInputName = "paramInt32") + { + m_logicEngine.link(*node.getOutputs()->getChild(outputName), *m_testScript->getInputs()->getChild(testScriptInputName)); + } + + const std::string m_scriptSrc = R"SRC( + function interface(IN,OUT) + IN.paramTestReserved = Type:Int32() + + IN.paramInt32 = Type:Int32() + IN.paramInt64 = Type:Int64() + IN.paramFloat = Type:Float() + + OUT.paramInt32 = Type:Int32() + OUT.paramFloat = Type:Float() + OUT.paramVec3f = Type:Vec3f() + end + function run(IN,OUT) + end + )SRC"; + + LuaScript* m_testScript = nullptr; + }; + + // -- scripts -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfScriptHasNoLinks) + { + auto* script = m_logicEngine.createLuaScript(m_scriptSrc, {}, "scr"); + ASSERT_NE(nullptr, script); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(2u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [scr] has no outgoing links! Node should be deleted or properly linked!")), + ::testing::Field(&WarningData::message, ::testing::StrEq("Node [scr] has no ingoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfScriptHasNoIngoingLinks) + { + auto* script = m_logicEngine.createLuaScript(m_scriptSrc, {}, "scr"); + ASSERT_NE(nullptr, script); + + linkNodeOutput(*script, "paramInt32"); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(1u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [scr] has no ingoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfScriptHasNoOutgoingLinks) + { + auto* script = m_logicEngine.createLuaScript(m_scriptSrc, {}, "scr"); + ASSERT_NE(nullptr, script); + + linkNodeInput(*script, "paramInt32"); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(1u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [scr] has no outgoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfScriptHasIngoingAndOutgoingLinks) + { + auto* script = m_logicEngine.createLuaScript(m_scriptSrc, {}, "scr"); + ASSERT_NE(nullptr, script); + + linkNodeOutput(*script, "paramInt32"); + linkNodeInput(*script, "paramInt32"); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(0u, warnings.size()); + } + + // -- interfaces -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfInterfaceHasNoIngoingLinks) + { + LuaInterface* intf = m_logicEngine.createLuaInterface(R"( + function interface(INOUT) + INOUT.param = Type:Int32() + end + )", "intf name"); + ASSERT_NE(nullptr, intf); + + linkNodeOutput(*intf, "param"); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(0u, warnings.size()); + } + + // -- node bindings -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfNodeBindingHasNoIngoingLinks) + { + auto* nodeBind = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "binding"); + ASSERT_NE(nullptr, nodeBind); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(1u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [binding] has no ingoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfNodeBindingHasIngoingLinks) + { + auto* nodeBind = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "binding"); + ASSERT_NE(nullptr, nodeBind); + + linkNodeInput(*nodeBind, "scaling", "paramVec3f"); + m_logicEngine.update(); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(0u, warnings.size()); + } + + // -- appearance bindings -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfAppearanceBindingHasNoIngoingLinks) + { + auto* appearanceBind = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "binding"); + ASSERT_NE(nullptr, appearanceBind); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(1u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [binding] has no ingoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfAppearanceBindingHasIngoingLinks) + { + auto* appearanceBind = m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "binding"); + ASSERT_NE(nullptr, appearanceBind); + + linkNodeInput(*appearanceBind, "floatUniform", "paramFloat"); + m_logicEngine.update(); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(0u, warnings.size()); + } + + // -- camera bindings -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfCameraBindingHasNoIngoingLinks) + { + auto* cameraBind = m_logicEngine.createRamsesCameraBinding(*m_camera, "binding"); + ASSERT_NE(nullptr, cameraBind); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(1u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [binding] has no ingoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfCameraBindingHasIngoingLinks) + { + auto* cameraBind = m_logicEngine.createRamsesCameraBinding(*m_camera, "binding"); + ASSERT_NE(nullptr, cameraBind); + + m_logicEngine.link(*m_testScript->getOutputs()->getChild("paramInt32"), *cameraBind->getInputs()->getChild("viewport")->getChild("offsetX")); + m_logicEngine.update(); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(0u, warnings.size()); + } + + // -- render pass bindings -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfRenderPassBindingHasNoIngoingLinks) + { + auto* passBind = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "binding"); + ASSERT_NE(nullptr, passBind); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(1u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [binding] has no ingoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfRenderPassBindingHasIngoingLinks) + { + auto* passBind = m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "binding"); + ASSERT_NE(nullptr, passBind); + + linkNodeInput(*passBind, "renderOrder", "paramInt32"); + m_logicEngine.update(); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(0u, warnings.size()); + } + + // -- render group bindings -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfRenderGroupBindingHasNoIngoingLinks) + { + RamsesRenderGroupBindingElements elements; + elements.addElement(*m_meshNode, "mesh"); + auto* binding = m_logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements, "binding"); + ASSERT_NE(nullptr, binding); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(1u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [binding] has no ingoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfRenderGroupBindingHasIngoingLinks) + { + RamsesRenderGroupBindingElements elements; + elements.addElement(*m_meshNode, "mesh"); + auto* binding = m_logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements, "binding"); + ASSERT_NE(nullptr, binding); + + linkNodeInput(*binding->getInputs()->getChild("renderOrders")->getChild("mesh"), "paramInt32"); + m_logicEngine.update(); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(0u, warnings.size()); + } + + // -- mesh node bindings -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfMeshNodeBindingHasNoIngoingLinks) + { + m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "binding"); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(1u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [binding] has no ingoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfMeshNodeBindingHasIngoingLinks) + { + const auto binding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNode, "binding"); + ASSERT_NE(nullptr, binding); + + linkNodeInput(*binding->getInputs()->getChild("indexOffset"), "paramInt32"); + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_TRUE(m_logicEngine.validate().empty()); + } + + // -- animations -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfAnimationHasNoLinks) + { + const auto* m_dataFloat = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }); + const auto* m_dataVec2 = m_logicEngine.createDataArray(std::vector{ { 1.f, 2.f }, { 3.f, 4.f }, { 5.f, 6.f } }); + const AnimationChannel channel{ "channel", m_dataFloat, m_dataVec2 }; + AnimationNodeConfig config; + config.addChannel(channel); + auto* anim = m_logicEngine.createAnimationNode(config, "anim"); + ASSERT_NE(nullptr, anim); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(2u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [anim] has no outgoing links! Node should be deleted or properly linked!")), + ::testing::Field(&WarningData::message, ::testing::StrEq("Node [anim] has no ingoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfAnimationHasNoOutgoingLinks) + { + const auto* m_dataFloat = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }); + const auto* m_dataVec2 = m_logicEngine.createDataArray(std::vector{ { 1.f, 2.f }, { 3.f, 4.f }, { 5.f, 6.f } }); + const AnimationChannel channel{ "channel", m_dataFloat, m_dataVec2 }; + AnimationNodeConfig config; + config.addChannel(channel); + auto* anim = m_logicEngine.createAnimationNode(config, "anim"); + ASSERT_NE(nullptr, anim); + + linkNodeInput(*anim, "progress", "paramFloat"); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(1u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [anim] has no outgoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfAnimationHasNoIngoingLinks) + { + const auto* m_dataFloat = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }); + const auto* m_dataVec2 = m_logicEngine.createDataArray(std::vector{ { 1.f, 2.f }, { 3.f, 4.f }, { 5.f, 6.f } }); + const AnimationChannel channel{ "channel", m_dataFloat, m_dataVec2 }; + AnimationNodeConfig config; + config.addChannel(channel); + auto* anim = m_logicEngine.createAnimationNode(config, "anim"); + ASSERT_NE(nullptr, anim); + + linkNodeOutput(*anim, "duration", "paramFloat"); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(1u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [anim] has no ingoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfAnimationHasIngoingAndOutgoingLinks) + { + const auto* m_dataFloat = m_logicEngine.createDataArray(std::vector{ 1.f, 2.f, 3.f }); + const auto* m_dataVec2 = m_logicEngine.createDataArray(std::vector{ { 1.f, 2.f }, { 3.f, 4.f }, { 5.f, 6.f } }); + const AnimationChannel channel{ "channel", m_dataFloat, m_dataVec2 }; + AnimationNodeConfig config; + config.addChannel(channel); + auto* anim = m_logicEngine.createAnimationNode(config, "anim"); + ASSERT_NE(nullptr, anim); + + linkNodeInput(*anim, "progress", "paramFloat"); + linkNodeOutput(*anim, "duration", "paramFloat"); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(0u, warnings.size()); + } + + // -- timers -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, ProducesWarningIfTimerHasNoOutgoingLinks) + { + auto* timer = m_logicEngine.createTimerNode("timer"); + ASSERT_NE(nullptr, timer); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(1u, warnings.size()); + EXPECT_THAT(warnings, ::testing::ElementsAre(::testing::Field(&WarningData::message, ::testing::StrEq("Node [timer] has no outgoing links! Node should be deleted or properly linked!")))); + EXPECT_THAT(warnings, ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfTimerHasOutgoingLinks) + { + auto* timer = m_logicEngine.createTimerNode("timer"); + ASSERT_NE(nullptr, timer); + + linkNodeOutput(*timer, "ticker_us", "paramInt64"); + + auto warnings = m_logicEngine.validate(); + ASSERT_EQ(0u, warnings.size()); + } + + // -- anchor points -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfAnchorPointHasNoOutgoingLinksAndItsBindingsNoIncomingLinks) + { + auto* nodeBind = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodeBinding"); + auto* cameraBind = m_logicEngine.createRamsesCameraBinding(*m_camera, "cameraBinding"); + auto* anchor = m_logicEngine.createAnchorPoint(*nodeBind, *cameraBind, "anchorBinding"); + ASSERT_NE(nullptr, anchor); + + m_logicEngine.update(); + EXPECT_TRUE(m_logicEngine.validate().empty()); + } + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningIfAnchorPointHasOutgoingLink) + { + auto* nodeBind = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodeBinding"); + auto* cameraBind = m_logicEngine.createRamsesCameraBinding(*m_camera, "cameraBinding"); + auto* anchor = m_logicEngine.createAnchorPoint(*nodeBind, *cameraBind, "anchorBinding"); + ASSERT_NE(nullptr, anchor); + + linkNodeOutput(*anchor, "depth", "paramFloat"); + + m_logicEngine.update(); + EXPECT_TRUE(m_logicEngine.validate().empty()); + } + + // -- skin bindings -- // + + TEST_F(ALogicEngine_ValidatingDanglingNodes, DoesNotProduceWarningWhenCheckingSkinBindingAndItsBindingsHaveNoIncomingLinks) + { + const auto skin = createSkinBinding(m_logicEngine); + ASSERT_NE(nullptr, skin); + + m_logicEngine.update(); + EXPECT_TRUE(m_logicEngine.validate().empty()); + } + + TEST(GetVerboseDescriptionFunction, ReturnsCorrectString) + { + EXPECT_STREQ("Performance", GetVerboseDescription(EWarningType::Performance)); + EXPECT_STREQ("Unsafe Data State", GetVerboseDescription(EWarningType::UnsafeDataState)); + EXPECT_STREQ("Uninitialized Data", GetVerboseDescription(EWarningType::UninitializedData)); + EXPECT_STREQ("Precision Loss", GetVerboseDescription(EWarningType::PrecisionLoss)); + EXPECT_STREQ("Unused Content", GetVerboseDescription(EWarningType::UnusedContent)); + EXPECT_STREQ("Duplicate Content", GetVerboseDescription(EWarningType::DuplicateContent)); + EXPECT_STREQ("Other", GetVerboseDescription(EWarningType::Other)); + } +} diff --git a/client/logic/unittests/api/LogicNodeTest.cpp b/client/logic/unittests/api/LogicNodeTest.cpp new file mode 100644 index 000000000..f6c0aea7f --- /dev/null +++ b/client/logic/unittests/api/LogicNodeTest.cpp @@ -0,0 +1,138 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" + +#include "ramses-logic/LogicNode.h" +#include "ramses-logic/Property.h" +#include "impl/LogicNodeImpl.h" +#include "impl/PropertyImpl.h" + +namespace ramses::internal +{ + class LogicNodeDummy : public LogicNode + { + public: + explicit LogicNodeDummy(std::unique_ptr impl) + : LogicNode(std::move(impl)) + { + m_impl.setLogicObject(*this); + } + }; + + class LogicNodeImplMock : public LogicNodeImpl + { + public: + // Forwarding constructor + explicit LogicNodeImplMock(std::string_view name) + : LogicNodeImpl(name, 1u) + { + } + + // Expose protected method for testing + using LogicNodeImpl::setRootProperties; + + MOCK_METHOD(std::optional, update, (), (override, final)); + MOCK_METHOD(void, createRootProperties, (), (override, final)); + }; + + class ALogicNodeImpl : public ::testing::Test + { + }; + + TEST_F(ALogicNodeImpl, RemembersNameGivenInConstructor) + { + const LogicNodeImplMock logicNode("name"); + EXPECT_EQ(logicNode.getName(), "name"); + } + + TEST_F(ALogicNodeImpl, CanReceiveNewName) + { + LogicNodeImplMock logicNode("name"); + logicNode.setName("newName"); + EXPECT_EQ(logicNode.getName(), "newName"); + } + + TEST_F(ALogicNodeImpl, DirtyByDefault) + { + const LogicNodeImplMock logicNode(""); + EXPECT_TRUE(logicNode.isDirty()); + } + + TEST_F(ALogicNodeImpl, DirtyWhenSetDirty) + { + LogicNodeImplMock logicNode(""); + logicNode.setDirty(false); + EXPECT_FALSE(logicNode.isDirty()); + logicNode.setDirty(true); + EXPECT_TRUE(logicNode.isDirty()); + } + + TEST_F(ALogicNodeImpl, TakesOwnershipOfGivenProperties) + { + const HierarchicalTypeData nestedTypeData( + TypeData{ "root", EPropertyType::Struct }, { + HierarchicalTypeData(TypeData{"nested", EPropertyType::Struct}, { + MakeType("float", EPropertyType::Float), + MakeStruct("nested", { TypeData("float", EPropertyType::Float) }) + }) + }); + + auto inputType = nestedTypeData; + auto outputType = nestedTypeData; + // These usually come from subclasses deserialization code + auto inputs = std::make_unique(inputType, EPropertySemantics::ScriptInput); + auto outputs = std::make_unique(outputType, EPropertySemantics::ScriptOutput); + + LogicNodeDummy logicNode{ std::make_unique("name") }; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) known test type + (static_cast(logicNode.m_impl)).setRootProperties(std::move(inputs), std::move(outputs)); + + // const outputs + { + const Property* root = logicNode.getOutputs(); + EXPECT_EQ(&logicNode, &root->getOwningLogicNode()); + ASSERT_EQ(1u, root->getChildCount()); + + const auto nested = root->getChild(0); + EXPECT_EQ(&logicNode, &nested->getOwningLogicNode()); + ASSERT_EQ(2u, nested->getChildCount()); + + const auto nestedFloat = nested->getChild(0); + EXPECT_EQ(&logicNode, &nestedFloat->getOwningLogicNode()); + + const auto nestedNested = nested->getChild(1); + EXPECT_EQ(&logicNode, &nestedNested->getOwningLogicNode()); + ASSERT_EQ(1u, nestedNested->getChildCount()); + + const auto nestedNestedFloat = nestedNested->getChild(0); + EXPECT_EQ(&logicNode, &nestedNestedFloat->getOwningLogicNode()); + } + + // non-const inputs + { + Property* root = logicNode.getInputs(); + EXPECT_EQ(&logicNode, &root->getOwningLogicNode()); + ASSERT_EQ(1u, root->getChildCount()); + + const auto nested = root->getChild(0); + EXPECT_EQ(&logicNode, &nested->getOwningLogicNode()); + ASSERT_EQ(2u, nested->getChildCount()); + + const auto nestedFloat = nested->getChild(0); + EXPECT_EQ(&logicNode, &nestedFloat->getOwningLogicNode()); + + const auto nestedNested = nested->getChild(1); + EXPECT_EQ(&logicNode, &nestedNested->getOwningLogicNode()); + ASSERT_EQ(1u, nestedNested->getChildCount()); + + const auto nestedNestedFloat = nestedNested->getChild(0); + EXPECT_EQ(&logicNode, &nestedNestedFloat->getOwningLogicNode()); + } + } +} diff --git a/client/logic/unittests/api/LuaConfigTest.cpp b/client/logic/unittests/api/LuaConfigTest.cpp new file mode 100644 index 000000000..6a6b3955f --- /dev/null +++ b/client/logic/unittests/api/LuaConfigTest.cpp @@ -0,0 +1,198 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "LogTestUtils.h" + +#include "ramses-logic/LuaConfig.h" +#include "ramses-logic/LogicEngine.h" +#include "impl/LuaConfigImpl.h" + +#include "fmt/format.h" + +namespace ramses::internal +{ + class ALuaConfig : public ::testing::Test + { + protected: + LogicEngine m_logicEngine{ ramses::EFeatureLevel_Latest }; + LuaModule* m_module{ m_logicEngine.createLuaModule(R"( + local mymath = {} + function mymath.add(a,b) + print(a+b) + end + return mymath + )") }; + + std::string m_errorMessage; + ScopedLogContextLevel m_scopedLogs{ELogLevel::Error, [&](ELogLevel msgType, std::string_view message) { + m_errorMessage = message; + if (msgType != ELogLevel::Error) + { + ASSERT_TRUE(false) << "Should be error!"; + } + }}; + }; + + TEST_F(ALuaConfig, IsCreated) + { + LuaConfig config; + EXPECT_TRUE(config.m_impl->getModuleMapping().empty()); + EXPECT_FALSE(config.m_impl->hasDebugLogFunctionsEnabled()); + } + + TEST_F(ALuaConfig, IsCopied) + { + LuaConfig config; + config.addDependency("mod1", *m_module); + config.addDependency("mod2", *m_module); + config.addStandardModuleDependency(EStandardModule::Debug); + config.enableDebugLogFunctions(); + + LuaConfig configCopy(config); + EXPECT_EQ(config.m_impl->getModuleMapping(), configCopy.m_impl->getModuleMapping()); + EXPECT_EQ(config.m_impl->getStandardModules(), configCopy.m_impl->getStandardModules()); + EXPECT_EQ(config.m_impl->hasDebugLogFunctionsEnabled(), configCopy.m_impl->hasDebugLogFunctionsEnabled()); + } + + TEST_F(ALuaConfig, IsCopyAssigned) + { + LuaConfig config; + config.addDependency("mod1", *m_module); + config.addDependency("mod2", *m_module); + config.addStandardModuleDependency(EStandardModule::Debug); + + // Copy assignment + LuaConfig configCopy; + configCopy = config; + EXPECT_EQ(config.m_impl->getModuleMapping(), configCopy.m_impl->getModuleMapping()); + EXPECT_EQ(config.m_impl->getStandardModules(), configCopy.m_impl->getStandardModules()); + EXPECT_EQ(config.m_impl->hasDebugLogFunctionsEnabled(), configCopy.m_impl->hasDebugLogFunctionsEnabled()); + } + + TEST_F(ALuaConfig, IsMoved) + { + LuaConfig config; + config.addDependency("mod1", *m_module); + config.addStandardModuleDependency(EStandardModule::Debug); + config.enableDebugLogFunctions(); + + LuaConfig movedConfig(std::move(config)); + + EXPECT_EQ(movedConfig.m_impl->getModuleMapping().at("mod1"), m_module); + EXPECT_THAT(movedConfig.m_impl->getStandardModules(), ::testing::ElementsAre(EStandardModule::Debug)); + EXPECT_TRUE(movedConfig.m_impl->hasDebugLogFunctionsEnabled()); + } + + TEST_F(ALuaConfig, IsMoveAssigned) + { + LuaConfig config; + config.addDependency("mod1", *m_module); + config.addStandardModuleDependency(EStandardModule::Debug); + + LuaConfig movedAssigned; + movedAssigned = std::move(config); + + EXPECT_EQ(movedAssigned.m_impl->getModuleMapping().at("mod1"), m_module); + EXPECT_THAT(movedAssigned.m_impl->getStandardModules(), ::testing::ElementsAre(EStandardModule::Debug)); + EXPECT_FALSE(movedAssigned.m_impl->hasDebugLogFunctionsEnabled()); + } + + TEST_F(ALuaConfig, ProducesErrorWhenCreatingLuaScriptUsingModuleUnderInvalidName) + { + std::vector invalidLabels = { + "", + "!invalid", + "invalid%", + "3invalid", + "42", + "\n", + }; + + LuaConfig config; + for (const auto& invalidLabel : invalidLabels) + { + EXPECT_FALSE(config.addDependency(invalidLabel, *m_module)); + + EXPECT_EQ(fmt::format("Failed to add dependency '{}'! The alias name should be a valid Lua label.", invalidLabel), m_errorMessage); + } + } + + TEST_F(ALuaConfig, ProducesErrorWhenUsingTheSameLabelTwice) + { + LuaConfig config; + + ASSERT_TRUE(config.addDependency("module", *m_module)); + EXPECT_FALSE(config.addDependency("module", *m_module)); + + EXPECT_EQ("Module dependencies must be uniquely aliased! Alias 'module' is already used!", m_errorMessage); + } + + TEST_F(ALuaConfig, ProducesErrorWhenUsingStandardModuleAsAliasName) + { + LuaConfig config; + + EXPECT_FALSE(config.addDependency("math", *m_module)); + EXPECT_EQ("Failed to add dependency 'math'! The alias collides with a standard library name!", m_errorMessage); + EXPECT_FALSE(config.addDependency("string", *m_module)); + EXPECT_EQ("Failed to add dependency 'string'! The alias collides with a standard library name!", m_errorMessage); + EXPECT_FALSE(config.addDependency("debug", *m_module)); + EXPECT_EQ("Failed to add dependency 'debug'! The alias collides with a standard library name!", m_errorMessage); + EXPECT_FALSE(config.addDependency("table", *m_module)); + EXPECT_EQ("Failed to add dependency 'table'! The alias collides with a standard library name!", m_errorMessage); + } + + TEST_F(ALuaConfig, CanEnableDebugLogFunctions) + { + LuaConfig config; + EXPECT_FALSE(config.m_impl->hasDebugLogFunctionsEnabled()); + config.enableDebugLogFunctions(); + EXPECT_TRUE(config.m_impl->hasDebugLogFunctionsEnabled()); + } + + class ALuaConfig_StdModules : public ::testing::Test + { + }; + + TEST_F(ALuaConfig_StdModules, AddsStandardModuleDependencies) + { + LuaConfig config; + + EXPECT_TRUE(config.m_impl->getStandardModules().empty()); + + ASSERT_TRUE(config.addStandardModuleDependency(EStandardModule::Base)); + EXPECT_THAT(config.m_impl->getStandardModules(), ::testing::ElementsAre(EStandardModule::Base)); + ASSERT_TRUE(config.addStandardModuleDependency(EStandardModule::String)); + EXPECT_THAT(config.m_impl->getStandardModules(), ::testing::ElementsAre(EStandardModule::Base, EStandardModule::String)); + } + + TEST_F(ALuaConfig_StdModules, CantAddTheSameStandardModuleTwice) + { + LuaConfig config; + + EXPECT_TRUE(config.m_impl->getStandardModules().empty()); + + ASSERT_TRUE(config.addStandardModuleDependency(EStandardModule::Base)); + EXPECT_FALSE(config.addStandardModuleDependency(EStandardModule::Base)); + } + + TEST_F(ALuaConfig_StdModules, AddsAllModulesAtOnce) + { + LuaConfig config; + + EXPECT_TRUE(config.addStandardModuleDependency(EStandardModule::All)); + + EXPECT_THAT(config.m_impl->getStandardModules(), ::testing::ElementsAre( + EStandardModule::Base, + EStandardModule::String, + EStandardModule::Table, + EStandardModule::Math, + EStandardModule::Debug)); + } +} diff --git a/client/logic/unittests/api/LuaInterfaceTest.cpp b/client/logic/unittests/api/LuaInterfaceTest.cpp new file mode 100644 index 000000000..d13ee1f96 --- /dev/null +++ b/client/logic/unittests/api/LuaInterfaceTest.cpp @@ -0,0 +1,1158 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" +#include "WithTempDirectory.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaInterface.h" +#include "impl/LuaInterfaceImpl.h" +#include "impl/LogicEngineImpl.h" +#include "impl/PropertyImpl.h" +#include "internals/ErrorReporting.h" + +#include "generated/LuaInterfaceGen.h" + +#include "SerializationTestUtils.h" + +namespace ramses::internal +{ + class ALuaInterface : public ::testing::Test + { + protected: + LuaInterface* createTestInterface(std::string_view source, std::string_view interfaceName = "") + { + return m_logicEngine.createLuaInterface(source, interfaceName); + } + + void createTestInterfaceAndExpectFailure(std::string_view source, std::string_view interfaceName = "") + { + const auto intf = m_logicEngine.createLuaInterface(source, interfaceName); + EXPECT_EQ(intf, nullptr); + } + + const std::string_view m_minimalInterface = R"( + function interface(inputs) + + inputs.param1 = Type:Int32() + inputs.param2 = Type:Float() + + end + )"; + + LogicEngine m_logicEngine{ ramses::EFeatureLevel_Latest }; + }; + + TEST_F(ALuaInterface, CanCompileLuaInterface) + { + const LuaInterface* intf = createTestInterface(m_minimalInterface, "intf name"); + ASSERT_NE(nullptr, intf); + EXPECT_STREQ("intf name", intf->getName().data()); + } + + TEST_F(ALuaInterface, CanExtractInputsFromLuaInterface) + { + const LuaInterface* intf = createTestInterface(m_minimalInterface, "intf name"); + ASSERT_NE(nullptr, intf); + + EXPECT_EQ(2u, intf->getInputs()->getChildCount()); + EXPECT_EQ("", intf->getInputs()->getName()); + + EXPECT_EQ("param1", intf->getInputs()->getChild(0)->getName()); + EXPECT_EQ(ramses::EPropertyType::Int32, intf->getInputs()->getChild(0)->getType()); + + EXPECT_EQ("param2", intf->getInputs()->getChild(1)->getName()); + EXPECT_EQ(ramses::EPropertyType::Float, intf->getInputs()->getChild(1)->getType()); + } + + TEST_F(ALuaInterface, ReturnsSameResultForOutputsAsInputs) + { + const LuaInterface* intf = createTestInterface(m_minimalInterface, "intf name"); + ASSERT_NE(nullptr, intf); + + EXPECT_EQ(2u, intf->getOutputs()->getChildCount()); + EXPECT_EQ("", intf->getOutputs()->getName()); + + EXPECT_EQ("param1", intf->getInputs()->getChild(0)->getName()); + EXPECT_EQ(ramses::EPropertyType::Int32, intf->getOutputs()->getChild(0)->getType()); + + EXPECT_EQ("param2", intf->getOutputs()->getChild(1)->getName()); + EXPECT_EQ(ramses::EPropertyType::Float, intf->getOutputs()->getChild(1)->getType()); + } + + TEST_F(ALuaInterface, FailsIfNameEmpty) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + function interface(inputs) + end + )SCRIPT", ""); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Can't create interface with empty name!")); + } + + TEST_F(ALuaInterface, UpdatingInputsLeadsToUpdatingOutputs) + { + LuaInterface* intf = createTestInterface(m_minimalInterface, "intf name"); + ASSERT_NE(nullptr, intf); + + ASSERT_EQ(intf->getInputs()->getChild(0)->get(), intf->getOutputs()->getChild(0)->get()); + + intf->getInputs()->getChild(0)->set(123); + ASSERT_EQ(intf->getInputs()->getChild(0)->get(), intf->getOutputs()->getChild(0)->get()); + + intf->m_impl.update(); + EXPECT_EQ(123, intf->getOutputs()->getChild(0)->get()); + } + + TEST_F(ALuaInterface, InterfaceFunctionIsExecutedOnlyOnce) + { + LuaInterface* intf = createTestInterface(R"SCRIPT( + local firstExecution = true + + function interface(inputs) + if not firstExecution then + error("a problem happened") + end + + firstExecution = false + inputs.param1 = Type:Int32() + inputs.param2 = Type:Float() + end + + )SCRIPT", "intf name"); + + ASSERT_NE(nullptr, intf); + EXPECT_STREQ("intf name", intf->getName().data()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 0u); + } + + TEST_F(ALuaInterface, ReportsErrorIfInterfaceDidNotCompile) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + function interface(inputs) + not.a.valid.lua.syntax + end + + )SCRIPT", "intf name"); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, + ::testing::HasSubstr("[intf name] Error while loading interface. Lua stack trace:\n[string \"intf name\"]:3: unexpected symbol near 'not'")); + } + + TEST_F(ALuaInterface, ReportsErrorIfNoInterfaceFunctionDefined) + { + createTestInterfaceAndExpectFailure(R"SCRIPT()SCRIPT", "intf name"); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_STREQ("[intf name] No 'interface' function defined!", m_logicEngine.getErrors()[0].message.c_str()); + } + + TEST_F(ALuaInterface, Sandboxing_ReportsErrorIfInitFunctionDefined) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + + function interface(inputs) + inputs.param1 = Type:Int32() + inputs.param2 = Type:Float() + end + + function init() + end + + )SCRIPT", "intf name"); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Unexpected function name 'init'! Only 'interface' function can be declared!")); + } + + TEST_F(ALuaInterface, Sandboxing_ReportsErrorIfRunFunctionDefined) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + + function interface(inputs) + + inputs.param1 = Type:Int32() + inputs.param2 = Type:Float() + end + + function run(IN,OUT) + end + + )SCRIPT", "intf name"); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Unexpected function name 'run'! Only 'interface' function can be declared!")); + } + + TEST_F(ALuaInterface, Sandboxing_ReportsErrorIfGlobalSpecialVariableAccessed) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + + function interface(inputs) + + inputs.param1 = Type:Int32() + inputs.param2 = Type:Float() + + GLOBAL.param3 = Type:Float() + end + + )SCRIPT", "intf name"); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Unexpected global access to key 'GLOBAL' in interface()!")); + } + + TEST_F(ALuaInterface, Sandboxing_ReportsErrorIfLuaGlobalVariablesDefined) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + + someGlobal = 10 + + function interface(inputs) + + inputs.param1 = Type:Int32() + inputs.param2 = Type:Float() + end + + )SCRIPT", "intf name"); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Declaring global variables is forbidden (exception: the 'interface' function)! (found value of type 'number')")); + } + + TEST_F(ALuaInterface, Sandboxing_ReportsErrorWhenTryingToReadUnknownGlobals) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + + function interface(inputs) + local t = IN + end + + )SCRIPT", "intf name"); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Unexpected global access to key 'IN' in interface()!")); + } + + TEST_F(ALuaInterface, Sandboxing_ReportsErrorWhenAccessingGlobalsOutsideInterfaceFunction) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + + table.getn(_G) + function interface(inputs) + end + + )SCRIPT", "intf name"); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Trying to read global variable 'table' in an interface!")); + } + + TEST_F(ALuaInterface, Sandboxing_ReportsErrorWhenSettingGlobals) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + + function interface(inputs) + thisCausesError = 'bad' + end + + )SCRIPT", "intf name"); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Unexpected variable definition 'thisCausesError' in interface()!")); + } + + TEST_F(ALuaInterface, Sandboxing_ReportsErrorWhenTryingToOverrideSpecialGlobalVariable) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + + function interface(inputs) + GLOBAL = {} + end + + )SCRIPT", "intf name"); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Unexpected variable definition 'GLOBAL' in interface()!")); + } + + TEST_F(ALuaInterface, Sandboxing_CanDeclareLocalVariables) + { + LuaInterface* intf = createTestInterface(R"SCRIPT( + + function interface(inputs) + local multiplexersAreAwesomeIfYouLearnThem = 12 + inputs.param = Type:Int32() + end + + )SCRIPT", "intf name"); + + EXPECT_NE(nullptr, intf); + EXPECT_EQ(m_logicEngine.getErrors().size(), 0u); + } + + TEST_F(ALuaInterface, Sandboxing_ReportsErrorIfUnknownFunctionDefined) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + + function interface(inputs) + + inputs.param1 = Type:Int32() + inputs.param2 = Type:Float() + + end + + function HackToCatchDeadlineCozNobodyChecksDeliveries() + end + + )SCRIPT", "intf name"); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Unexpected function name 'HackToCatchDeadlineCozNobodyChecksDeliveries'! Only 'interface' function can be declared!")); + } + + TEST_F(ALuaInterface, Sandboxing_ReportsErrorWhenTryingToDeclareInterfaceFunctionTwice) + { + createTestInterfaceAndExpectFailure(R"SCRIPT( + + function interface(inputs) + end + + function interface(inputs) + end + + )SCRIPT", "intf name"); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Function 'interface' can only be declared once!")); + } + + TEST_F(ALuaInterface, Sandboxing_ForbidsCallingSpecialFunctionsFromInsideInterface) + { + for (const auto& specialFunction : std::vector{ "init", "run", "interface" }) + { + createTestInterfaceAndExpectFailure(fmt::format(R"SCRIPT( + + function interface(inputs) + {}() + end + )SCRIPT", specialFunction), "intf name"); + + EXPECT_THAT(m_logicEngine.getErrors()[0].message, + ::testing::HasSubstr(fmt::format("Unexpected global access to key '{}' in interface()!", specialFunction))); + } + } + + TEST_F(ALuaInterface, CanCreateInterfaceWithComplexTypes) + { + const std::string_view interfaceScript = R"( + function interface(inputs) + + inputs.array_int = Type:Array(2, Type:Int32()) + inputs.array_struct = Type:Array(3, {a=Type:Int32(), b=Type:Float()}) + inputs.struct = {a=Type:Int32(), b={c = Type:Int32(), d=Type:Float()}} + + end + )"; + LuaInterface* intf = createTestInterface(interfaceScript, "intf name"); + ASSERT_NE(nullptr, intf); + + ASSERT_EQ(intf->getInputs()->getChildCount(), 3u); + ASSERT_EQ(intf->getOutputs()->getChildCount(), 3u); + + ASSERT_STREQ(intf->getInputs()->getChild(0)->getName().data(), "array_int"); + EXPECT_EQ(intf->getInputs()->getChild(0)->getType(), EPropertyType::Array); + EXPECT_EQ(intf->getOutputs()->getChild(0)->getType(), EPropertyType::Array); + EXPECT_EQ(intf->getOutputs()->getChild(0)->getChild(0)->getType(), EPropertyType::Int32); + + ASSERT_STREQ(intf->getInputs()->getChild(1)->getName().data(), "array_struct"); + EXPECT_EQ(intf->getInputs()->getChild(1)->getType(), EPropertyType::Array); + EXPECT_EQ(intf->getOutputs()->getChild(1)->getType(), EPropertyType::Array); + EXPECT_EQ(intf->getOutputs()->getChild(1)->getChild(0)->getType(), EPropertyType::Struct); + + ASSERT_STREQ(intf->getInputs()->getChild(2)->getName().data(), "struct"); + EXPECT_EQ(intf->getInputs()->getChild(2)->getType(), EPropertyType::Struct); + EXPECT_EQ(intf->getOutputs()->getChild(2)->getType(), EPropertyType::Struct); + EXPECT_EQ(intf->getOutputs()->getChild(2)->getChild(0)->getType(), EPropertyType::Int32); + EXPECT_EQ(intf->getOutputs()->getChild(2)->getChild(1)->getType(), EPropertyType::Struct); + } + + TEST_F(ALuaInterface, CanUpdateInterfaceValuesWithComplexTypes) + { + const std::string_view interfaceScript = R"( + function interface(inputs) + + inputs.array_int = Type:Array(2, Type:Int32()) + inputs.array_struct = Type:Array(3, {a=Type:Int32(), b=Type:Float()}) + inputs.struct = {a=Type:Int32(), b={c = Type:Int32(), d=Type:Float()}} + + end + )"; + LuaInterface* intf = createTestInterface(interfaceScript, "intf name"); + ASSERT_NE(nullptr, intf); + + intf->getInputs()->getChild(0)->getChild(0)->set(123); + intf->getInputs()->getChild(2)->getChild(0)->set(456); + + ASSERT_EQ(intf->getOutputs()->getChild(0)->getChild(0)->get(), 123); + ASSERT_EQ(intf->getOutputs()->getChild(2)->getChild(0)->get(), 456); + + intf->m_impl.update(); + EXPECT_EQ(123, intf->getOutputs()->getChild(0)->getChild(0)->get()); + EXPECT_EQ(456, intf->getOutputs()->getChild(2)->getChild(0)->get()); + } + + TEST_F(ALuaInterface, CanCheckIfOutputsAreLinked) + { + LuaInterface* intf = createTestInterface(R"( + function interface(IN,OUT) + + IN.param1 = Type:Int32() + IN.param2 = {a=Type:Float(), b=Type:Int32()} + + end + )", "intf name"); + + const auto& intfImpl = intf->m_interface; + + const auto* output1 = intf->getOutputs()->getChild(0); + const auto* output21 = intf->getOutputs()->getChild(1)->getChild(0); + const auto* output22 = intf->getOutputs()->getChild(1)->getChild(1); + + auto unlinkedOutputs = intfImpl.collectUnlinkedProperties(); + EXPECT_THAT(unlinkedOutputs, ::testing::UnorderedElementsAre(output1, output21, output22)); + + LuaScript* inputsScript = m_logicEngine.createLuaScript(R"LUA_SCRIPT( + function interface(IN,OUT) + + IN.param1 = Type:Int32() + IN.param21 = Type:Float() + IN.param22 = Type:Int32() + + end + + function run(IN,OUT) + end + )LUA_SCRIPT", {}); + + //link 1 output + m_logicEngine.link(*output1, *inputsScript->getInputs()->getChild(0)); + unlinkedOutputs = intfImpl.collectUnlinkedProperties(); + EXPECT_THAT(unlinkedOutputs, ::testing::UnorderedElementsAre(output21, output22)); + + //link 2nd output + m_logicEngine.link(*output21, *inputsScript->getInputs()->getChild(1)); + unlinkedOutputs = intfImpl.collectUnlinkedProperties(); + EXPECT_THAT(unlinkedOutputs, ::testing::UnorderedElementsAre(output22)); + + m_logicEngine.link(*output22, *inputsScript->getInputs()->getChild(2)); + unlinkedOutputs = intfImpl.collectUnlinkedProperties(); + EXPECT_TRUE(unlinkedOutputs.empty()); + } + + class ALuaInterfaceWithModule : public ALuaInterface + { + protected: + const std::string_view m_moduleSrc1 = R"( + local mymodule = {} + function mymodule.colorType() + return { + red = Type:Int32(), + blue = Type:Int32(), + green = Type:Int32() + } + end + + return mymodule + )"; + + const std::string_view m_moduleSrc2 = R"( + local mymodule = {} + function mymodule.otherType() + return Type:Float() + end + + return mymodule + )"; + }; + + TEST_F(ALuaInterfaceWithModule, UsesModule) + { + constexpr std::string_view intfSrc = R"( + modules("mymod") + function interface(IN) + IN.color = mymod.colorType() + end + )"; + + LuaModule* mod = m_logicEngine.createLuaModule(m_moduleSrc1); + LuaConfig config; + config.addDependency("mymod", *mod); + + const auto intf = m_logicEngine.createLuaInterface(intfSrc, "intf", config); + ASSERT_NE(intf, nullptr); + + const auto colorProp = intf->getInputs()->getChild(0); + ASSERT_TRUE(colorProp); + EXPECT_EQ("color", colorProp->getName()); + ASSERT_EQ(3u, colorProp->getChildCount()); + ASSERT_TRUE(colorProp->getChild("red")); + ASSERT_TRUE(colorProp->getChild("blue")); + ASSERT_TRUE(colorProp->getChild("green")); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("red")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("blue")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("green")->getType()); + } + + TEST_F(ALuaInterfaceWithModule, UsesTwoModules) + { + constexpr std::string_view intfSrc = R"( + modules("mymod1", "mymod2") + function interface(IN) + IN.color = mymod1.colorType() + IN.val = mymod2.otherType() + end + )"; + + const LuaModule* mod1 = m_logicEngine.createLuaModule(m_moduleSrc1); + const LuaModule* mod2 = m_logicEngine.createLuaModule(m_moduleSrc2); + LuaConfig config; + config.addDependency("mymod1", *mod1); + config.addDependency("mymod2", *mod2); + + const auto intf = m_logicEngine.createLuaInterface(intfSrc, "intf", config); + ASSERT_NE(intf, nullptr); + + const auto colorProp = intf->getInputs()->getChild(0); + ASSERT_TRUE(colorProp); + ASSERT_EQ(3u, colorProp->getChildCount()); + ASSERT_TRUE(colorProp->getChild("red")); + ASSERT_TRUE(colorProp->getChild("blue")); + ASSERT_TRUE(colorProp->getChild("green")); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("red")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("blue")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("green")->getType()); + + const auto valProp = intf->getInputs()->getChild(1); + ASSERT_TRUE(valProp); + EXPECT_EQ(EPropertyType::Float, valProp->getType()); + } + + TEST_F(ALuaInterfaceWithModule, UsesSameModuleUnderMultipleNames) + { + constexpr std::string_view intfSrc = R"( + modules("mymod1", "mymod2") + function interface(IN) + IN.color1 = mymod1.colorType() + IN.color2 = mymod2.colorType() + end + )"; + + const LuaModule* mod = m_logicEngine.createLuaModule(m_moduleSrc1); + LuaConfig config; + config.addDependency("mymod1", *mod); + config.addDependency("mymod2", *mod); + + const auto intf = m_logicEngine.createLuaInterface(intfSrc, "intf", config); + ASSERT_NE(intf, nullptr); + + for (size_t i = 0u; i < 2; ++i) + { + const auto colorProp = intf->getInputs()->getChild(i); + ASSERT_TRUE(colorProp); + ASSERT_EQ(3u, colorProp->getChildCount()); + ASSERT_TRUE(colorProp->getChild("red")); + ASSERT_TRUE(colorProp->getChild("blue")); + ASSERT_TRUE(colorProp->getChild("green")); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("red")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("blue")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("green")->getType()); + } + } + + TEST_F(ALuaInterfaceWithModule, UsesTypeFromModule_StructAsTableField) + { + constexpr std::string_view moduleSrc = R"( + local mymodule = {} + mymodule.colorType = { + red = Type:Int32(), + blue = Type:Int32(), + green = Type:Int32() + } + return mymodule + )"; + + constexpr std::string_view intfSrc = R"( + modules("mymod") + function interface(IN) + IN.color = mymod.colorType + end + )"; + + LuaModule* mod = m_logicEngine.createLuaModule(moduleSrc); + LuaConfig config; + config.addDependency("mymod", *mod); + + const auto intf = m_logicEngine.createLuaInterface(intfSrc, "intf", config); + ASSERT_NE(intf, nullptr); + + const auto colorProp = intf->getInputs()->getChild(0); + ASSERT_TRUE(colorProp); + EXPECT_EQ("color", colorProp->getName()); + ASSERT_EQ(3u, colorProp->getChildCount()); + ASSERT_TRUE(colorProp->getChild("red")); + ASSERT_TRUE(colorProp->getChild("blue")); + ASSERT_TRUE(colorProp->getChild("green")); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("red")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("blue")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("green")->getType()); + } + + TEST_F(ALuaInterfaceWithModule, UsesTypeFromModule_MakesArrayFromIt) + { + constexpr std::string_view intfSrc = R"( + modules("mymod") + function interface(IN) + IN.colors = Type:Array(2, mymod.colorType()) + end + )"; + + LuaModule* mod = m_logicEngine.createLuaModule(m_moduleSrc1); + LuaConfig config; + config.addDependency("mymod", *mod); + + const auto intf = m_logicEngine.createLuaInterface(intfSrc, "intf", config); + ASSERT_NE(intf, nullptr); + + ASSERT_EQ(1u, intf->getInputs()->getChildCount()); + const auto colorsProp = intf->getInputs()->getChild(0); + ASSERT_TRUE(colorsProp); + EXPECT_EQ("colors", colorsProp->getName()); + ASSERT_EQ(2u, colorsProp->getChildCount()); + for (size_t i = 0u; i < 2u; ++i) + { + const auto elementProp = colorsProp->getChild(i); + ASSERT_TRUE(elementProp->getChild("red")); + ASSERT_TRUE(elementProp->getChild("blue")); + ASSERT_TRUE(elementProp->getChild("green")); + EXPECT_EQ(EPropertyType::Int32, elementProp->getChild("red")->getType()); + EXPECT_EQ(EPropertyType::Int32, elementProp->getChild("blue")->getType()); + EXPECT_EQ(EPropertyType::Int32, elementProp->getChild("green")->getType()); + } + } + + TEST_F(ALuaInterfaceWithModule, UsesTypeFromModule_ArrayOfStructs) + { + constexpr std::string_view moduleSrc = R"( + local mymodule = {} + mymodule.colorsType = Type:Array(2, { + red = Type:Int32(), + blue = Type:Int32(), + green = Type:Int32() + }) + return mymodule + )"; + + constexpr std::string_view intfSrc = R"( + modules("mymod") + function interface(IN) + IN.colors = mymod.colorsType + end + )"; + + LuaModule* mod = m_logicEngine.createLuaModule(moduleSrc); + LuaConfig config; + config.addDependency("mymod", *mod); + + const auto intf = m_logicEngine.createLuaInterface(intfSrc, "intf", config); + ASSERT_NE(intf, nullptr); + + ASSERT_EQ(1u, intf->getInputs()->getChildCount()); + const auto colorsProp = intf->getInputs()->getChild(0); + ASSERT_TRUE(colorsProp); + EXPECT_EQ("colors", colorsProp->getName()); + ASSERT_EQ(2u, colorsProp->getChildCount()); + for (size_t i = 0u; i < 2u; ++i) + { + const auto elementProp = colorsProp->getChild(i); + ASSERT_TRUE(elementProp->getChild("red")); + ASSERT_TRUE(elementProp->getChild("blue")); + ASSERT_TRUE(elementProp->getChild("green")); + EXPECT_EQ(EPropertyType::Int32, elementProp->getChild("red")->getType()); + EXPECT_EQ(EPropertyType::Int32, elementProp->getChild("blue")->getType()); + EXPECT_EQ(EPropertyType::Int32, elementProp->getChild("green")->getType()); + } + } + + TEST_F(ALuaInterfaceWithModule, UsesTypeFromModule_StructWithArray) + { + constexpr std::string_view moduleSrc = R"( + local mymodule = {} + mymodule.type = { + arr = Type:Array(2, Type:Float()), + val = Type:Int32() + } + return mymodule + )"; + + constexpr std::string_view intfSrc = R"( + modules("mymod") + function interface(IN) + IN.structWithArray = mymod.type + end + )"; + + LuaModule* mod = m_logicEngine.createLuaModule(moduleSrc); + LuaConfig config; + config.addDependency("mymod", *mod); + + const auto intf = m_logicEngine.createLuaInterface(intfSrc, "intf", config); + ASSERT_NE(intf, nullptr); + + ASSERT_EQ(1u, intf->getInputs()->getChildCount()); + const auto structWithArrayProp = intf->getInputs()->getChild(0); + ASSERT_TRUE(structWithArrayProp); + EXPECT_EQ("structWithArray", structWithArrayProp->getName()); + ASSERT_EQ(2u, structWithArrayProp->getChildCount()); + const auto arrProp = structWithArrayProp->getChild("arr"); + ASSERT_TRUE(arrProp); + ASSERT_EQ(2u, arrProp->getChildCount()); + for (size_t i = 0u; i < 2u; ++i) + { + EXPECT_EQ(EPropertyType::Float, arrProp->getChild(i)->getType()); + } + const auto valProp = structWithArrayProp->getChild("val"); + ASSERT_TRUE(valProp); + EXPECT_EQ(EPropertyType::Int32, valProp->getType()); + } + + TEST_F(ALuaInterfaceWithModule, CanGetTableSizeWithCustomMethod) + { + constexpr std::string_view moduleSrc = R"( + local mod = {} + mod.table1 = { a=1, b=2 } + mod.table2 = { 4, 5, 6, 7 } + mod.table3 = { a=1, b=2, 42 } -- expected size 1, according to Lua semantics + return mod + )"; + + LuaModule* mod = m_logicEngine.createLuaModule(moduleSrc); + LuaConfig config; + config.addDependency("mymod", *mod); + config.addStandardModuleDependency(EStandardModule::Base); + + const auto intf = m_logicEngine.createLuaInterface(R"( + modules("mymod") + function interface(IN) + IN.dummy = Type:Int32() + assert(rl_len(mymod.table1) == 0) + assert(rl_len(mymod.table2) == 4) + assert(rl_len(mymod.table3) == 1) + end + )", "intf", config); + ASSERT_TRUE(intf); + } + + TEST_F(ALuaInterfaceWithModule, UsesModuleWhichDependsOnAnotherModule) + { + constexpr std::string_view wrapModuleSrc = R"( + modules("originalMod") + local mod = {} + mod.wrappedColorType = originalMod.colorType() + return mod + )"; + + constexpr std::string_view intfSrc = R"( + modules("mymod") + function interface(IN) + IN.color = mymod.wrappedColorType + end + )"; + + const LuaModule* originalMod = m_logicEngine.createLuaModule(m_moduleSrc1); + LuaConfig modConfig; + modConfig.addDependency("originalMod", *originalMod); + const LuaModule* wrapMod = m_logicEngine.createLuaModule(wrapModuleSrc, modConfig); + + LuaConfig config; + config.addDependency("mymod", *wrapMod); + const auto intf = m_logicEngine.createLuaInterface(intfSrc, "intf", config); + ASSERT_NE(intf, nullptr); + + const auto colorProp = intf->getInputs()->getChild(0); + ASSERT_TRUE(colorProp); + EXPECT_EQ("color", colorProp->getName()); + ASSERT_EQ(3u, colorProp->getChildCount()); + ASSERT_TRUE(colorProp->getChild("red")); + ASSERT_TRUE(colorProp->getChild("blue")); + ASSERT_TRUE(colorProp->getChild("green")); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("red")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("blue")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("green")->getType()); + } + + TEST_F(ALuaInterfaceWithModule, CanUseStandardModule) + { + constexpr std::string_view intfSrc = R"( + function interface(IN) + IN.vals = Type:Array(math.floor(2.3), Type:Int32()) + end + )"; + + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Math); + + const auto intf = m_logicEngine.createLuaInterface(intfSrc, "intf", config); + ASSERT_NE(intf, nullptr); + + ASSERT_EQ(1u, intf->getInputs()->getChildCount()); + const auto valsProp = intf->getInputs()->getChild(0); + ASSERT_TRUE(valsProp); + EXPECT_EQ("vals", valsProp->getName()); + ASSERT_EQ(2u, valsProp->getChildCount()); + for (size_t i = 0u; i < 2u; ++i) + { + EXPECT_EQ(EPropertyType::Int32, valsProp->getChild(i)->getType()); + } + } + + TEST_F(ALuaInterfaceWithModule, FailsToBeCreatedIfDeclaredDependencyDoesNotMatchProvidedDependency_NotProvidedButDeclared) + { + constexpr std::string_view src = R"( + modules("dep1", "dep2") + function interface(IN) + end + )"; + + LuaModule* mod = m_logicEngine.createLuaModule(m_moduleSrc1); + LuaConfig config; + config.addDependency("dep2", *mod); + + EXPECT_EQ(nullptr, m_logicEngine.createLuaInterface(src, "intf", config)); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Module dependencies declared in source code: dep1, dep2\n Module dependencies provided on create API: dep2")); + } + + TEST_F(ALuaInterfaceWithModule, FailsToBeCreatedIfDeclaredDependencyDoesNotMatchProvidedDependency_ProvidedButNotDeclared) + { + constexpr std::string_view src = R"( + modules("dep1", "dep2") + function interface(IN) + end + )"; + + LuaModule* mod = m_logicEngine.createLuaModule(m_moduleSrc1); + LuaConfig config; + config.addDependency("dep1", *mod); + config.addDependency("dep2", *mod); + config.addDependency("dep3", *mod); + + EXPECT_EQ(nullptr, m_logicEngine.createLuaInterface(src, "intf", config)); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Module dependencies declared in source code: dep1, dep2\n Module dependencies provided on create API: dep1, dep2, dep3")); + } + + TEST_F(ALuaInterfaceWithModule, FailsToBeCreatedIfDeclaredDependencyDoesNotMatchProvidedDependency_ExractionError) + { + constexpr std::string_view src = R"( + modules("dep1", "dep1") -- duplicate dependency + function interface(IN) + end + )"; + + LuaModule* mod = m_logicEngine.createLuaModule(m_moduleSrc1); + LuaConfig config; + config.addDependency("dep1", *mod); + + EXPECT_EQ(nullptr, m_logicEngine.createLuaInterface(src, "intf", config)); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Error while extracting module dependencies: 'dep1' appears more than once in dependency list")); + } + + TEST_F(ALuaInterfaceWithModule, FailsToBeCreatedWhenOverwritingModuleData) + { + constexpr std::string_view moduleSrc = R"( + local mymodule = {} + mymodule.val = 100 + return mymodule + )"; + + constexpr std::string_view intfSrc = R"( + modules("mymod") + function interface(IN) + IN.dummy = Type:Int32() + mymod.val = 42 + end + )"; + + LuaModule* mod = m_logicEngine.createLuaModule(moduleSrc); + LuaConfig config; + config.addDependency("mymod", *mod); + + const auto intf = m_logicEngine.createLuaInterface(intfSrc, "intf", config); + EXPECT_EQ(nullptr, intf); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Modifying module data is not allowed!")); + } + + TEST_F(ALuaInterfaceWithModule, FailsToBeCreatedWhenUsingModuleOverwritingTypeSymbols) + { + constexpr std::string_view modSrc = R"( + local m = {} + function m.killTypes() + Type = {} + end + return m + )"; + const auto luaModule = m_logicEngine.createLuaModule(modSrc); + LuaConfig config; + config.addDependency("mymod", *luaModule); + + const auto intf = m_logicEngine.createLuaInterface(R"( + modules("mymod") + function interface(IN) + IN.dummy = Type:Int32() + mymod.killTypes() + end + )", "intf", config); + EXPECT_EQ(nullptr, intf); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Special global 'Type' symbol should not be overwritten in modules!")); + } + + TEST_F(ALuaInterfaceWithModule, FailsToBeCreatedWhenUsingModuleFromAnotherLogicInstance) + { + LogicEngine otherLogicEngine{ m_logicEngine.getFeatureLevel() }; + const auto luaModule = otherLogicEngine.createLuaModule(m_moduleSrc1); + LuaConfig config; + config.addDependency("mymod", *luaModule); + + const auto intf = m_logicEngine.createLuaInterface(R"( + modules("mymod") + function interface(IN) + IN.dummy = Type:Int32() + end + )", "intf", config); + EXPECT_EQ(nullptr, intf); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_EQ(m_logicEngine.getErrors().front().message, "Failed to map Lua module 'mymod'! It was created on a different instance of LogicEngine."); + } + + TEST_F(ALuaInterfaceWithModule, CanBeSerializedAndLoadedInBaseFeatureLevel_confidenceTest) // just to verify that module dependency does not affect serialized interface + { + WithTempDirectory tempDir; + { + LogicEngine logicEngine{ m_logicEngine.getFeatureLevel() }; + const auto luaModule = logicEngine.createLuaModule(m_moduleSrc1); + LuaConfig config; + config.addDependency("mymod", *luaModule); + + logicEngine.createLuaInterface(R"( + modules("mymod") + function interface(IN) + IN.color = mymod.colorType() + end + )", "intf", config); + + SaveFileConfig configNoValidation; + configNoValidation.setValidationEnabled(false); + EXPECT_TRUE(logicEngine.saveToFile("temp.rlogic", configNoValidation)); + } + + EXPECT_TRUE(m_logicEngine.loadFromFile("temp.rlogic")); + const auto intf = m_logicEngine.findByName("intf"); + ASSERT_NE(intf, nullptr); + + const auto colorProp = intf->getInputs()->getChild(0); + ASSERT_TRUE(colorProp); + ASSERT_EQ(3u, colorProp->getChildCount()); + ASSERT_TRUE(colorProp->getChild("red")); + ASSERT_TRUE(colorProp->getChild("blue")); + ASSERT_TRUE(colorProp->getChild("green")); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("red")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("blue")->getType()); + EXPECT_EQ(EPropertyType::Int32, colorProp->getChild("green")->getType()); + } + + // tests that behavior of deprecated API is unchanged, some old assets have modules keyword in interface with invalid arguments + TEST_F(ALuaInterfaceWithModule, IgnoresModulesDeclaredInScriptIfUsingOldMethodToCreateInterface) + { + constexpr std::string_view intfSrc = R"( + modules("nonExistentMod") + function interface(IN) + IN.dummy = Type:Int32() + end + )"; + + const auto intf = m_logicEngine.createLuaInterface(intfSrc, "intf"); + ASSERT_NE(intf, nullptr); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + ASSERT_EQ(1u, intf->getInputs()->getChildCount()); + const auto prop = intf->getInputs()->getChild(0); + EXPECT_EQ("dummy", prop->getName()); + EXPECT_EQ(EPropertyType::Int32, prop->getType()); + + // if created with new API this will be error + const auto intfInvalid = m_logicEngine.createLuaInterface(intfSrc, "intf", {}); + EXPECT_EQ(intfInvalid, nullptr); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Module dependencies declared in source code do not match those provided by LuaConfig")); + } + + class ALuaInterface_Serialization : public ALuaInterface + { + protected: + enum class ESerializationIssue + { + AllValid, + NameIdMissing, + EmptyName, + RootMissing, + RootNotStruct + }; + + std::unique_ptr deserializeSerializedDataWithIssue(ESerializationIssue issue) + { + { + HierarchicalTypeData inputs = (issue == ESerializationIssue::RootNotStruct ? MakeType("", EPropertyType::Bool) : MakeStruct("", {})); + auto inputsImpl = std::make_unique(std::move(inputs), EPropertySemantics::Interface); + + const std::string_view name = (issue == ESerializationIssue::EmptyName ? "" : "intf"); + auto intf = rlogic_serialization::CreateLuaInterface(m_flatBufferBuilder, + issue == ESerializationIssue::NameIdMissing ? 0 : rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, m_flatBufferBuilder.CreateString(name), 1u, 0u, 0u), + issue == ESerializationIssue::RootMissing ? 0 : PropertyImpl::Serialize(*inputsImpl, m_flatBufferBuilder, m_serializationMap) + ); + m_flatBufferBuilder.Finish(intf); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + return LuaInterfaceImpl::Deserialize(serialized, m_errorReporting, m_deserializationMap); + } + + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + SerializationTestUtils m_testUtils{ m_flatBufferBuilder }; + SerializationMap m_serializationMap; + DeserializationMap m_deserializationMap; + ErrorReporting m_errorReporting; + }; + + TEST_F(ALuaInterface_Serialization, CanSerializeAndDeserializeLuaInterface) + { + WithTempDirectory tempDirectory; + + // Serialize + { + LogicEngine otherEngine{ m_logicEngine.getFeatureLevel() }; + const LuaScript* inputsScript = otherEngine.createLuaScript(R"LUA_SCRIPT( + function interface(IN,OUT) + IN.param1 = Type:Int32() + IN.param2 = { x = Type:Float(), y = Type:Array(2, Type:String()) } + end + + function run(IN,OUT) + end + )LUA_SCRIPT", {}); + + const LuaInterface* intf = otherEngine.createLuaInterface(R"( + function interface(inout) + inout.param1 = Type:Int32() + inout.param2 = { x = Type:Float(), y = Type:Array(2, Type:String()) } + end + )", "intf"); + + otherEngine.link(*intf->getOutputs()->getChild("param1"), *inputsScript->getInputs()->getChild("param1")); + otherEngine.link(*intf->getOutputs()->getChild("param2")->getChild("x"), *inputsScript->getInputs()->getChild("param2")->getChild("x")); + otherEngine.link(*intf->getOutputs()->getChild("param2")->getChild("y")->getChild(0u), *inputsScript->getInputs()->getChild("param2")->getChild("y")->getChild(0u)); + otherEngine.link(*intf->getOutputs()->getChild("param2")->getChild("y")->getChild(1u), *inputsScript->getInputs()->getChild("param2")->getChild("y")->getChild(1u)); + + SaveFileConfig configNoValidation; + configNoValidation.setValidationEnabled(false); + ASSERT_TRUE(otherEngine.saveToFile("interface.rlogic", configNoValidation)); + } + + EXPECT_TRUE(m_logicEngine.loadFromFile("interface.rlogic")); + const auto loadedIntf = m_logicEngine.findByName("intf"); + ASSERT_NE(nullptr, loadedIntf); + + EXPECT_EQ(2u, loadedIntf->getId()); + EXPECT_EQ(loadedIntf->getInputs(), loadedIntf->getOutputs()); + ASSERT_EQ(2u, loadedIntf->getInputs()->getChildCount()); + const auto param1 = loadedIntf->getInputs()->getChild("param1"); + ASSERT_NE(nullptr, param1); + EXPECT_EQ(EPropertyType::Int32, param1->getType()); + + const auto param2 = loadedIntf->getInputs()->getChild("param2"); + ASSERT_NE(nullptr, param2); + EXPECT_EQ(EPropertyType::Struct, param2->getType()); + ASSERT_EQ(2u, param2->getChildCount()); + + const auto param2x = param2->getChild("x"); + ASSERT_NE(nullptr, param2x); + EXPECT_EQ(EPropertyType::Float, param2x->getType()); + + const auto param2y = param2->getChild("y"); + ASSERT_NE(nullptr, param2y); + EXPECT_EQ(EPropertyType::Array, param2y->getType()); + ASSERT_EQ(2u, param2y->getChildCount()); + EXPECT_EQ(EPropertyType::String, param2y->getChild(0u)->getType()); + EXPECT_EQ(EPropertyType::String, param2y->getChild(1u)->getType()); + } + + TEST_F(ALuaInterface_Serialization, FailsToSaveToFileIfInterfaceOutputsNotLinked) + { + createTestInterface(m_minimalInterface, "intf name"); + EXPECT_FALSE(m_logicEngine.saveToFile("interface.rlogic")); + } + + TEST_F(ALuaInterface_Serialization, CanSerializeWithNoIssue) + { + EXPECT_TRUE(deserializeSerializedDataWithIssue(ALuaInterface_Serialization::ESerializationIssue::AllValid)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + + TEST_F(ALuaInterface_Serialization, FailsDeserializationIfEssentialDataMissing) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ALuaInterface_Serialization::ESerializationIssue::NameIdMissing)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of LuaInterface from serialized data: missing name and/or ID!", m_errorReporting.getErrors().back().message); + } + + TEST_F(ALuaInterface_Serialization, FailsDeserializationIfNameEmpty) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ALuaInterface_Serialization::ESerializationIssue::EmptyName)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of LuaInterface from serialized data: empty name!", m_errorReporting.getErrors().back().message); + } + + TEST_F(ALuaInterface_Serialization, FailsDeserializationIfRootPropertyMissing) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ALuaInterface_Serialization::ESerializationIssue::RootMissing)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of LuaInterface from serialized data: missing root property!", m_errorReporting.getErrors().back().message); + } + + TEST_F(ALuaInterface_Serialization, FailsDeserializationIfRootNotStructType) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ALuaInterface_Serialization::ESerializationIssue::RootNotStruct)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of LuaScript from serialized data: root property has unexpected type!", m_errorReporting.getErrors().back().message); + } +} diff --git a/client/logic/unittests/api/LuaModuleTest.cpp b/client/logic/unittests/api/LuaModuleTest.cpp new file mode 100644 index 000000000..43bbe8914 --- /dev/null +++ b/client/logic/unittests/api/LuaModuleTest.cpp @@ -0,0 +1,682 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "WithTempDirectory.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/LuaModule.h" +#include "impl/LuaModuleImpl.h" +#include "internals/ErrorReporting.h" +#include "internals/SolState.h" +#include "internals/DeserializationMap.h" +#include "internals/SerializationMap.h" + +#include "generated/LuaModuleGen.h" + +#include + +namespace ramses::internal +{ + class ALuaModule : public ::testing::Test + { + protected: + const std::string_view m_moduleSourceCode = R"( + local mymath = {} + function mymath.add(a,b) + print(a+b) + end + return mymath + )"; + + LuaConfig createDeps(const std::vector>& dependencies) + { + LuaConfig config; + for (const auto& [alias, moduleSrc] : dependencies) + { + LuaModule* mod = m_logicEngine.createLuaModule(moduleSrc); + config.addDependency(alias, *mod); + } + + return config; + } + + LogicEngine m_logicEngine{ ramses::EFeatureLevel_Latest }; + }; + + TEST_F(ALuaModule, IsCreated) + { + const auto module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymodule"); + ASSERT_NE(nullptr, module); + EXPECT_EQ("mymodule", module->getName()); + EXPECT_EQ(module->getId(), 1u); + } + + TEST_F(ALuaModule, ChangesName) + { + const auto module = m_logicEngine.createLuaModule(m_moduleSourceCode); + ASSERT_NE(nullptr, module); + + module->setName("mm"); + EXPECT_EQ("mm", module->getName()); + EXPECT_EQ(module, this->m_logicEngine.findByName("mm")); + EXPECT_TRUE(this->m_logicEngine.getErrors().empty()); + } + + TEST_F(ALuaModule, FailsCreationIfSourceInvalid) + { + EXPECT_EQ(nullptr, m_logicEngine.createLuaModule("!", {})); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Error while loading module")); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("unexpected symbol near '!'")); + + EXPECT_EQ(nullptr, m_logicEngine.createLuaModule(R"( + local mymath = {} + function mymath.add(a,b) + print(a+b) + return mymath + )")); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Error while loading module")); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("expected (to close 'function'")); + + EXPECT_EQ(nullptr, m_logicEngine.createLuaModule(R"( + local mymath = {} + function mymath.add(a,b) + print(a+b + end + return mymath + )")); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Error while loading module")); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("expected (to close '('")); + } + + TEST_F(ALuaModule, ProducesErrorMessageCorrectlyNotConflictingWithFmtFormatSyntax) + { + auto* module = m_logicEngine.createLuaModule(R"( + local coalaModule = {} + coalaModule.coalaStruct = { + oink1 + oink2 + } + return coalaModule + )", {}, "missingComma"); + + ASSERT_EQ(nullptr, module); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("'}' expected (to close '{'")); + } + + class ALuaModule_SerializationLifecycle : public ALuaModule + { + protected: + SolState m_solState; + ErrorReporting m_errorReporting; + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + DeserializationMap m_deserializationMap; + }; + + TEST_F(ALuaModule_SerializationLifecycle, CanBeSerialized) + { + WithTempDirectory tempDir; + + { + LogicEngine logic{ m_logicEngine.getFeatureLevel() }; + logic.createLuaModule(m_moduleSourceCode, {}, "mymodule"); + EXPECT_TRUE(logic.saveToFile("module.tmp")); + } + + EXPECT_TRUE(m_logicEngine.loadFromFile("module.tmp")); + const auto module = m_logicEngine.findByName("mymodule"); + ASSERT_NE(nullptr, module); + EXPECT_EQ("mymodule", module->getName()); + EXPECT_EQ(module->getId(), 1u); + } + + TEST_F(ALuaModule_SerializationLifecycle, StoresDuplicateByteCodeOnce) + { + SerializationMap serializationMap; + LogicEngine logic{ m_logicEngine.getFeatureLevel() }; + + auto module1 = logic.createLuaModule(m_moduleSourceCode, {}, "mymodule1"); + auto module2 = logic.createLuaModule(m_moduleSourceCode, {}, "mymodule2"); + + auto serOffset1 = LuaModuleImpl::Serialize(module1->m_impl, m_flatBufferBuilder, serializationMap, ELuaSavingMode::ByteCodeOnly); + m_flatBufferBuilder.Finish(serOffset1); + const auto& serialized1 = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + auto serOffset2 = LuaModuleImpl::Serialize(module2->m_impl, m_flatBufferBuilder, serializationMap, ELuaSavingMode::ByteCodeOnly); + m_flatBufferBuilder.Finish(serOffset2); + const auto& serialized2 = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + EXPECT_EQ(serialized1.luaByteCode(), serialized2.luaByteCode()); + } + + TEST_F(ALuaModule_SerializationLifecycle, ProducesErrorWhenNameMissing) + { + { + auto module = rlogic_serialization::CreateLuaModule( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + 0, // no name + 0) + ); + m_flatBufferBuilder.Finish(module); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaModuleImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing name!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of LuaModule from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ALuaModule_SerializationLifecycle, ProducesErrorWhenIdMissing) + { + { + auto module = rlogic_serialization::CreateLuaModule( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 0) // no id + ); + m_flatBufferBuilder.Finish(module); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaModuleImpl::Deserialize(this->m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing or invalid ID!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of LuaModule from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ALuaModule_SerializationLifecycle, ProducesErrorWhenBothsourceAndBytecodeMissing) + { + { + auto module = rlogic_serialization::CreateLuaModule( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0, // no source code + m_flatBufferBuilder.CreateVector(std::vector>()), + m_flatBufferBuilder.CreateVector(std::vector()), + 0 // no bytecode + ); + m_flatBufferBuilder.Finish(module); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaModuleImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaModule from serialized data: has neither Lua source code nor bytecode!"); + } + + TEST_F(ALuaModule_SerializationLifecycle, ProducesErrorWhensourceAndBytecodeEmpty) + { + { + auto module = rlogic_serialization::CreateLuaModule( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(""), // empty source code + m_flatBufferBuilder.CreateVector(std::vector>()), + m_flatBufferBuilder.CreateVector(std::vector()), + m_flatBufferBuilder.CreateVector(std::vector{}) // bytecode empty + ); + m_flatBufferBuilder.Finish(module); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaModuleImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaModule from serialized data: has neither Lua source code nor bytecode!"); + } + + TEST_F(ALuaModule_SerializationLifecycle, ProducesErrorWhenDependenciesMissing) + { + { + auto module = rlogic_serialization::CreateLuaModule( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_moduleSourceCode), + 0, // missing dependencies + m_flatBufferBuilder.CreateVector(std::vector()), + m_flatBufferBuilder.CreateVector(std::vector{1, 0}) + ); + m_flatBufferBuilder.Finish(module); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaModuleImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaModule from serialized data: missing dependencies!"); + } + + TEST_F(ALuaModule_SerializationLifecycle, ProducesErrorWhenByteCodeInvalidAndNoSourceAvailable) + { + { + const std::vector invalidByteCode(10, 0); + + auto module = rlogic_serialization::CreateLuaModule( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0, + m_flatBufferBuilder.CreateVector(std::vector>()), + m_flatBufferBuilder.CreateVector(std::vector()), + m_flatBufferBuilder.CreateVector(invalidByteCode) + ); + m_flatBufferBuilder.Finish(module); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaModuleImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 2u); + EXPECT_THAT(m_errorReporting.getErrors()[0].message, ::testing::HasSubstr("Fatal error during loading of LuaModule 'name': failed loading pre-compiled byte code")); + EXPECT_THAT(m_errorReporting.getErrors()[1].message, ::testing::HasSubstr("Fatal error during loading of LuaModule 'name' from serialized data")); + } + + TEST_F(ALuaModule_SerializationLifecycle, WillTryToRecompileModuleFromSourceWhenByteCodeInvalid) + { + const std::vector invalidByteCode(10, 0); + { + auto module = rlogic_serialization::CreateLuaModule( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_moduleSourceCode), + m_flatBufferBuilder.CreateVector(std::vector>()), + m_flatBufferBuilder.CreateVector(std::vector()), + m_flatBufferBuilder.CreateVector(invalidByteCode) + ); + m_flatBufferBuilder.Finish(module); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaModuleImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + // check that script was recompiled successfully + EXPECT_TRUE(deserialized); + + // serialize again and check that new byte code is produced + { + flatbuffers::FlatBufferBuilder builder; + SerializationMap serializationMap; + const auto fbModule = LuaModuleImpl::Serialize(*deserialized, builder, serializationMap, ELuaSavingMode::SourceAndByteCode); + builder.Finish(fbModule); + const auto serializedWithValidByteCode = flatbuffers::GetRoot(builder.GetBufferPointer()); + ASSERT_TRUE(serializedWithValidByteCode); + ASSERT_TRUE(serializedWithValidByteCode->source()); + EXPECT_EQ(m_moduleSourceCode, serializedWithValidByteCode->source()->c_str()); + + EXPECT_TRUE(serializedWithValidByteCode->luaByteCode()); + EXPECT_TRUE(serializedWithValidByteCode->luaByteCode()->size() > 0); + EXPECT_NE(serializedWithValidByteCode->luaByteCode()->size(), invalidByteCode.size()); + } + } + + TEST_F(ALuaModule_SerializationLifecycle, SerializesSourceCodeOnly_InSourceOnlyMode) + { + auto module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymodule"); + + SerializationMap serializationMap; + const auto fbModule = LuaModuleImpl::Serialize(module->m_impl, m_flatBufferBuilder, serializationMap, ELuaSavingMode::SourceCodeOnly); + m_flatBufferBuilder.Finish(fbModule); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + EXPECT_FALSE(serialized.luaByteCode()); + ASSERT_TRUE(serialized.source()); + EXPECT_EQ(m_moduleSourceCode, serialized.source()->string_view()); + } + + TEST_F(ALuaModule_SerializationLifecycle, SerializesBytecodeOnly_InBytecodeOnlyMode) + { + auto module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymodule"); + + SerializationMap serializationMap; + const auto fbModule = LuaModuleImpl::Serialize(module->m_impl, m_flatBufferBuilder, serializationMap, ELuaSavingMode::ByteCodeOnly); + m_flatBufferBuilder.Finish(fbModule); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serialized.luaByteCode()); + EXPECT_TRUE(serialized.luaByteCode()->size() > 0); + EXPECT_FALSE(serialized.source()); + } + + TEST_F(ALuaModule_SerializationLifecycle, SerializesBytecodeOnly_InSourceCodeOnlyMode_IfNoSourceAvailable) + { + // simulate module with no source code by serializing it with bytecode only and deserializing again + std::unique_ptr moduleWithNoSourceCode; + { + auto module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymodule"); + + SerializationMap serializationMap; + const auto fbModule = LuaModuleImpl::Serialize(module->m_impl, m_flatBufferBuilder, serializationMap, ELuaSavingMode::ByteCodeOnly); + m_flatBufferBuilder.Finish(fbModule); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + moduleWithNoSourceCode = LuaModuleImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + ASSERT_TRUE(moduleWithNoSourceCode); + m_flatBufferBuilder.Clear(); + } + + SerializationMap serializationMap; + const auto fbModule = LuaModuleImpl::Serialize(*moduleWithNoSourceCode, m_flatBufferBuilder, serializationMap, ELuaSavingMode::SourceCodeOnly); + m_flatBufferBuilder.Finish(fbModule); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serialized.luaByteCode()); + EXPECT_TRUE(serialized.luaByteCode()->size() > 0); + EXPECT_FALSE(serialized.source()); + } + + TEST_F(ALuaModule_SerializationLifecycle, SerializesBytecodeOnly_InBothSourceAndBytecodeMode_IfNoSourceAvailable) + { + // simulate module with no source code by serializing it with bytecode only and deserializing again + std::unique_ptr moduleWithNoSourceCode; + { + auto module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymodule"); + + SerializationMap serializationMap; + const auto fbModule = LuaModuleImpl::Serialize(module->m_impl, m_flatBufferBuilder, serializationMap, ELuaSavingMode::ByteCodeOnly); + m_flatBufferBuilder.Finish(fbModule); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + moduleWithNoSourceCode = LuaModuleImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + ASSERT_TRUE(moduleWithNoSourceCode); + m_flatBufferBuilder.Clear(); + } + + SerializationMap serializationMap; + const auto fbModule = LuaModuleImpl::Serialize(*moduleWithNoSourceCode, m_flatBufferBuilder, serializationMap, ELuaSavingMode::SourceAndByteCode); + m_flatBufferBuilder.Finish(fbModule); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serialized.luaByteCode()); + EXPECT_TRUE(serialized.luaByteCode()->size() > 0); + EXPECT_FALSE(serialized.source()); + } + + TEST_F(ALuaModule_SerializationLifecycle, SerializesBothSourceAndBytecode) + { + auto module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymodule"); + + SerializationMap serializationMap; + const auto fbModule = LuaModuleImpl::Serialize(module->m_impl, m_flatBufferBuilder, serializationMap, ELuaSavingMode::SourceAndByteCode); + m_flatBufferBuilder.Finish(fbModule); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serialized.source()); + EXPECT_EQ(m_moduleSourceCode, serialized.source()->string_view()); + + ASSERT_TRUE(serialized.luaByteCode()); + EXPECT_TRUE(serialized.luaByteCode()->size() > 0); + } + + TEST_F(ALuaModule_SerializationLifecycle, DoesNotContainDebugLogFunctionsAfterDeserialization) + { + WithTempDirectory tempDir; + + { + LogicEngine logic{ m_logicEngine.getFeatureLevel() }; + logic.createLuaModule(m_moduleSourceCode, {}, "mymodule"); + EXPECT_TRUE(logic.saveToFile("module.tmp")); + } + + EXPECT_TRUE(m_logicEngine.loadFromFile("module.tmp")); + const auto module = m_logicEngine.findByName("mymodule"); + ASSERT_NE(nullptr, module); + EXPECT_FALSE(module->m_impl.hasDebugLogFunctions()); + } + + class ALuaModuleWithDependency : public ALuaModule + { + protected: + std::string_view m_mathSrc = R"( + local mymath = {} + function mymath.add(a,b) + return a+b + end + return mymath + )"; + + std::string_view m_quadsSrc = R"( + modules("mymath") + local quads = {} + function quads.create(a,b) + return {math.sin(a), math.cos(b), mymath.add(a, b)} + end + return quads + )"; + }; + + TEST_F(ALuaModuleWithDependency, IsCreated) + { + const auto quadsMod = m_logicEngine.createLuaModule(m_quadsSrc, createDeps({{"mymath", m_mathSrc}}), "quadsMod"); + ASSERT_NE(nullptr, quadsMod); + EXPECT_EQ("quadsMod", quadsMod->getName()); + EXPECT_EQ(quadsMod->getId(), 2u); // module dependency has id 1u + } + + TEST_F(ALuaModuleWithDependency, HasTwoDependencies) + { + std::string_view mathSubSrc = R"( + local mymath = {} + function mymath.sub(a,b) + return a-b + end + return mymath + )"; + + std::string_view mathCombinedSrc = R"( + modules("_mathAdd", "_mathSub") + + local mymath = {} + mymath.add=_mathAdd.add + mymath.sub=_mathSub.sub + return mymath + )"; + const auto mathCombined = m_logicEngine.createLuaModule(mathCombinedSrc, createDeps({ {"_mathAdd", m_mathSrc}, {"_mathSub", mathSubSrc} })); + + LuaConfig config; + config.addDependency("_math", *mathCombined); + const auto script = m_logicEngine.createLuaScript(R"( + modules("_math") + function interface(IN,OUT) + OUT.added = Type:Int32() + OUT.subbed = Type:Int32() + end + + function run(IN,OUT) + OUT.added = _math.add(1,2) + OUT.subbed = _math.sub(15,5) + end + )", config); + ASSERT_NE(nullptr, script); + + m_logicEngine.update(); + EXPECT_EQ(3, *script->getOutputs()->getChild("added")->get()); + EXPECT_EQ(10, *script->getOutputs()->getChild("subbed")->get()); + } + + TEST_F(ALuaModuleWithDependency, UsesSameModuleUnderMultipleNames) + { + std::string_view mathCombinedSrc = R"( + modules("math1", "math2") + local math = {} + math.add1=math1.add + math.add2=math2.add + return math + )"; + const auto mathCombined = m_logicEngine.createLuaModule(mathCombinedSrc, createDeps({ {"math1", m_mathSrc}, {"math2", m_mathSrc} })); + + LuaConfig config; + config.addDependency("mathAll", *mathCombined); + const auto script = m_logicEngine.createLuaScript(R"( + modules("mathAll") + + function interface(IN,OUT) + OUT.result = Type:Int32() + end + + function run(IN,OUT) + OUT.result = mathAll.add1(1,2) + mathAll.add2(100,10) + end + )", config); + ASSERT_NE(nullptr, script); + + m_logicEngine.update(); + EXPECT_EQ(113, *script->getOutputs()->getChild("result")->get()); + } + + TEST_F(ALuaModuleWithDependency, TwoModulesDependOnTheSameModule) + { + const auto config = createDeps({ {"mymath", m_mathSrc} }); + + const auto mathUser1 = m_logicEngine.createLuaModule(R"( + modules("mymath") + local mathUser1 = {} + function mathUser1.add(a, b) + return mymath.add(a + 1, b + 1) + end + return mathUser1 + )", config); + + const auto mathUser2 = m_logicEngine.createLuaModule(R"( + modules("mymath") + local mathUser2 = {} + function mathUser2.add(a, b) + return mymath.add(a + 10, b + 10) + end + return mathUser2 + )", config); + + LuaConfig scriptConfig; + scriptConfig.addDependency("math1", *mathUser1); + scriptConfig.addDependency("math2", *mathUser2); + const auto script = m_logicEngine.createLuaScript(R"( + modules("math1", "math2") + function interface(IN,OUT) + OUT.result1 = Type:Int32() + OUT.result2 = Type:Int32() + end + + function run(IN,OUT) + OUT.result1 = math1.add(1,2) + OUT.result2 = math2.add(1,2) + end + )", scriptConfig); + ASSERT_NE(nullptr, script); + + m_logicEngine.update(); + EXPECT_EQ(5, *script->getOutputs()->getChild("result1")->get()); + EXPECT_EQ(23, *script->getOutputs()->getChild("result2")->get()); + } + + TEST_F(ALuaModuleWithDependency, CanBeSerialized) + { + WithTempDirectory tmpDir; + { + LogicEngine logic{ m_logicEngine.getFeatureLevel() }; + LuaConfig config; + config.addDependency("mymath", *logic.createLuaModule(m_mathSrc, {}, "mathMod")); + logic.createLuaModule(m_quadsSrc, config, "quadsMod"); + EXPECT_TRUE(logic.saveToFile("dep_modules.tmp")); + } + + EXPECT_TRUE(m_logicEngine.loadFromFile("dep_modules.tmp")); + + const LuaModule* mathMod = m_logicEngine.findByName("mathMod"); + auto quadsMod = m_logicEngine.findByName("quadsMod"); + ASSERT_NE(mathMod, nullptr); + ASSERT_NE(quadsMod, nullptr); + EXPECT_EQ(mathMod->getId(), 1u); + EXPECT_EQ(quadsMod->getId(), 2u); + + EXPECT_THAT(quadsMod->m_impl.getDependencies(), ::testing::ElementsAre(std::pair({"mymath", mathMod}))); + } + + TEST_F(ALuaModuleWithDependency, UpdatesWithoutIssues_WhenModulesWithRuntimeErrors_AreNeverCalled) + { + std::string_view errors = R"( + local mymath = {} + function mymath.add(a,b) + error("this fails always") + return a+b + end + return mymath + )"; + const auto quadsMod = m_logicEngine.createLuaModule(m_quadsSrc, createDeps({ {"mymath", errors} })); + ASSERT_NE(nullptr, quadsMod); + + // This works fine, because neither the the quads module nor the math modules are ever called + EXPECT_TRUE(m_logicEngine.update()); + } + + class ALuaModuleDependencyMatch : public ALuaModuleWithDependency + { + }; + + TEST_F(ALuaModuleDependencyMatch, FailsToBeCreatedIfDeclaredDependencyDoesNotMatchProvidedDependency_NotProvidedButDeclared) + { + constexpr std::string_view mathExt = R"( + modules("dep1", "dep2") + local mymathExt = {} + mymathExt.pi = 3.14 + return mymathExt + )"; + EXPECT_EQ(nullptr, m_logicEngine.createLuaModule(mathExt, createDeps({ {"dep2", m_moduleSourceCode} }))); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Module dependencies declared in source code: dep1, dep2\n Module dependencies provided on create API: dep2")); + } + + TEST_F(ALuaModuleDependencyMatch, FailsToBeCreatedIfDeclaredDependencyDoesNotMatchProvidedDependency_ProvidedButNotDeclared) + { + constexpr std::string_view mathExt = R"( + modules("dep1", "dep2") + local mymathExt = {} + mymathExt.pi = 3.14 + return mymathExt + )"; + EXPECT_EQ(nullptr, m_logicEngine.createLuaModule(mathExt, createDeps({ {"dep1", m_moduleSourceCode}, {"dep2", m_moduleSourceCode}, {"dep3", m_moduleSourceCode} }))); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Module dependencies declared in source code: dep1, dep2\n Module dependencies provided on create API: dep1, dep2, dep3")); + } + + TEST_F(ALuaModuleDependencyMatch, FailsToBeCreatedIfDeclaredDependencyDoesNotMatchProvidedDependency_ExractionError) + { + constexpr std::string_view mathExt = R"( + modules("dep1", "dep1") -- duplicate dependency + local mymathExt = {} + mymathExt.pi = 3.14 + return mymathExt + )"; + EXPECT_EQ(nullptr, m_logicEngine.createLuaModule(mathExt, createDeps({ {"dep1", m_moduleSourceCode} }))); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Error while extracting module dependencies: 'dep1' appears more than once in dependency list")); + } +} diff --git a/client/logic/unittests/api/LuaScriptTest_Debug.cpp b/client/logic/unittests/api/LuaScriptTest_Debug.cpp new file mode 100644 index 000000000..2c58e60ed --- /dev/null +++ b/client/logic/unittests/api/LuaScriptTest_Debug.cpp @@ -0,0 +1,248 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LuaScriptTest_Base.h" +#include "WithTempDirectory.h" + +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" +#include "impl/LuaScriptImpl.h" +#include "impl/LuaModuleImpl.h" +#include "LogTestUtils.h" + +#include + +namespace ramses +{ + class ALuaScript_Debug : public ALuaScript + { + protected: + std::string_view m_scriptWithInterfaceError = R"( + function interface(IN,OUT) + IN.prop = nil + end + function run(IN,OUT) + end + )"; + + std::string_view m_scriptWithRuntimeError = R"( + function interface(IN,OUT) + end + function run(IN,OUT) + IN.prop = nil + end + )"; + + // Silence logs, unless explicitly enabled, to reduce spam and speed up tests + ScopedLogContextLevel m_silenceLogs{ ELogLevel::Off }; + }; + + TEST_F(ALuaScript_Debug, ProducesErrorWithFullStackTrace_WhenErrorsInInterface) + { + LuaScript* script = m_logicEngine.createLuaScript(m_scriptWithInterfaceError, {}, "errorscript"); + + ASSERT_EQ(nullptr, script); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr( + "[errorscript] Error while loading script. Lua stack trace:\n" + "lua: error: Invalid type of field 'prop'! Expected Type:T() syntax where T=Float,Int32,... Found a value of type 'nil' instead")); + // nullptr because no LogicNode was created + EXPECT_EQ(nullptr, m_logicEngine.getErrors()[0].object); + } + + TEST_F(ALuaScript_Debug, ProducesErrorWithFullStackTrace_WhenRuntimeErrors) + { + LuaScript* script = m_logicEngine.createLuaScript(m_scriptWithRuntimeError, {}, "errorscript"); + + ASSERT_NE(nullptr, script); + m_logicEngine.update(); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr( + "lua: error: Tried to access undefined struct property 'prop'")); + EXPECT_EQ(script, m_logicEngine.getErrors()[0].object); + } + + TEST_F(ALuaScript_Debug, ErrorStackTraceContainsScriptName_WhenScriptWasNotLoadedFromFile) + { + // Script loaded from string, not file + LuaScript* script = m_logicEngine.createLuaScript(m_scriptWithInterfaceError, {}, "errorscript"); + + // Error message contains script name in the stack (file not known) + EXPECT_EQ(nullptr, script); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr( + "[errorscript] Error while loading script. Lua stack trace:\n" + "lua: error: Invalid type of field 'prop'! Expected Type:T() syntax where T=Float,Int32,... Found a value of type 'nil' instead")); + } + + TEST_F(ALuaScript_Debug, LogsDebugMessage) + { + ScopedLogContextLevel enableLogs{ ELogLevel::Info }; + + constexpr std::string_view src = R"( + function init() + rl_logInfo("test1") + end + function interface(IN,OUT) + end + function run(IN,OUT) + rl_logInfo("test2") + rl_logWarn('x'..3) + rl_logError('x'..'error') + end + )"; + + LuaConfig config; + config.enableDebugLogFunctions(); + const LuaScript* script = m_logicEngine.createLuaScript(src, config); + ASSERT_NE(nullptr, script); + EXPECT_TRUE(script->m_script.hasDebugLogFunctions()); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALuaScript_Debug, LogsDebugMessageUsingModule) + { + ScopedLogContextLevel enableLogs{ ELogLevel::Info }; + + constexpr std::string_view moduleSrc = R"( + local mymod = {} + function mymod.concat(a,b) + rl_logWarn(a..b) + end + return mymod + )"; + + constexpr std::string_view src = R"( + modules("mymod") + + function init() + mymod.concat("init1", "init2") + end + function interface(IN,OUT) + mymod.concat("intf1", "intf2") + end + function run(IN,OUT) + mymod.concat("run", "x"..3) + end + )"; + + LuaConfig configModule; + configModule.enableDebugLogFunctions(); + LuaModule* mod = m_logicEngine.createLuaModule(moduleSrc, configModule); + ASSERT_NE(nullptr, mod); + EXPECT_TRUE(mod->m_impl.hasDebugLogFunctions()); + + LuaConfig config; + config.addDependency("mymod", *mod); + const LuaScript* script = m_logicEngine.createLuaScript(src, config); + ASSERT_NE(nullptr, script); + EXPECT_FALSE(script->m_script.hasDebugLogFunctions()); + + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALuaScript_Debug, FailsToCompileScriptWhenUsingLogDebugFunctionWithoutEnabling) + { + constexpr std::string_view src = R"( + function init() + rl_logInfo("test1") + end + function interface(IN,OUT) + end + function run(IN,OUT) + rl_logInfo("test2") + rl_logWarn('x'..3) + rl_logError('x'..'error') + end + )"; + + const LuaScript* script = m_logicEngine.createLuaScript(src); + EXPECT_EQ(nullptr, script); + } + + TEST_F(ALuaScript_Debug, FailsToCompileScriptWhenUsingLogDebugFunctionInModuleWithoutEnabling) + { + constexpr std::string_view moduleSrc = R"( + local mymod = {} + function mymod.concat(a,b) + rl_logWarn(a..b) + end + return mymod + )"; + + constexpr std::string_view src = R"( + modules("mymod") + + function init() + mymod.concat("init1", "init2") + end + function interface(IN,OUT) + mymod.concat("intf1", "intf2") + end + function run(IN,OUT) + mymod.concat("run", "x"..3) + end + )"; + + LuaModule* mod = m_logicEngine.createLuaModule(moduleSrc); + ASSERT_NE(nullptr, mod); + + LuaConfig config; + config.addDependency("mymod", *mod); + const LuaScript* script = m_logicEngine.createLuaScript(src, config); + EXPECT_EQ(nullptr, script); + } + + TEST_F(ALuaScript_Debug, FailsToSaveToFileIfScriptHasDebugLogFunctions) + { + WithTempDirectory tempDir; + + LuaConfig config; + config.enableDebugLogFunctions(); + LuaScript* script = m_logicEngine.createLuaScript(m_minimalScript, config, "script"); + ASSERT_NE(nullptr, script); + EXPECT_TRUE(script->m_script.hasDebugLogFunctions()); + + EXPECT_FALSE(m_logicEngine.saveToFile("willNotSave.tmp")); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_EQ(m_logicEngine.getErrors().front().message, "Cannot save to file, Lua script 'script [Id=1]' has enabled debug log functions, remove this script before saving."); + EXPECT_EQ(m_logicEngine.getErrors().front().object, script); + + // can save after removal + EXPECT_TRUE(m_logicEngine.destroy(*script)); + EXPECT_TRUE(m_logicEngine.saveToFile("willSave.tmp")); + } + + TEST_F(ALuaScript_Debug, FailsToSaveToFileIfModuleHasDebugLogFunctions) + { + WithTempDirectory tempDir; + + constexpr std::string_view moduleSrc = R"( + local mymod = {} + function mymod.concat(a,b) + rl_logWarn(a..b) + end + return mymod + )"; + + LuaConfig config; + config.enableDebugLogFunctions(); + LuaModule* mod = m_logicEngine.createLuaModule(moduleSrc, config, "mod"); + ASSERT_NE(nullptr, mod); + EXPECT_TRUE(mod->m_impl.hasDebugLogFunctions()); + + EXPECT_FALSE(m_logicEngine.saveToFile("willNotSave.tmp")); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_EQ(m_logicEngine.getErrors().front().message, "Cannot save to file, Lua module 'mod [Id=1]' has enabled debug log functions, remove this module before saving."); + EXPECT_EQ(m_logicEngine.getErrors().front().object, mod); + + // can save after removal + EXPECT_TRUE(m_logicEngine.destroy(*mod)); + EXPECT_TRUE(m_logicEngine.saveToFile("willSave.tmp")); + } +} diff --git a/client/logic/unittests/api/LuaScriptTest_Init.cpp b/client/logic/unittests/api/LuaScriptTest_Init.cpp new file mode 100644 index 000000000..e4e3d6f69 --- /dev/null +++ b/client/logic/unittests/api/LuaScriptTest_Init.cpp @@ -0,0 +1,562 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LuaScriptTest_Base.h" + +#include "ramses-logic/Property.h" +#include "impl/LogicEngineImpl.h" +#include "internals/ApiObjects.h" +#include "LogTestUtils.h" +#include "WithTempDirectory.h" + + +namespace ramses +{ + class ALuaScript_Init : public ALuaScript + { + }; + + TEST_F(ALuaScript_Init, CreatesGlobals) + { + auto* script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL.number = 5 + GLOBAL.string = "foo" + GLOBAL.bool = false + end + + function interface(IN,OUT) + OUT.number = Type:Int32() + OUT.string = Type:String() + OUT.bool = Type:Bool() + end + + function run(IN,OUT) + OUT.number = GLOBAL.number + OUT.string = GLOBAL.string + OUT.bool = GLOBAL.bool + end + )"); + + ASSERT_TRUE(m_logicEngine.update()); + EXPECT_EQ(5, *script->getOutputs()->getChild("number")->get()); + EXPECT_EQ("foo", *script->getOutputs()->getChild("string")->get()); + EXPECT_EQ(false, *script->getOutputs()->getChild("bool")->get()); + } + + TEST_F(ALuaScript_Init, CanUseGlobalsInInterface) + { + auto* script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL.inputNames = {"foo", "bar"} + end + + function interface(IN,OUT) + for key,value in pairs(GLOBAL.inputNames) do + OUT[value] = Type:Float() + end + end + + function run(IN,OUT) + for key,value in pairs(GLOBAL.inputNames) do + OUT[value] = 4.2 + end + end + )", WithStdModules({EStandardModule::Base})); + + ASSERT_NE(nullptr, script); + ASSERT_TRUE(m_logicEngine.update()); + EXPECT_FLOAT_EQ(4.2f, *script->getOutputs()->getChild("foo")->get()); + EXPECT_FLOAT_EQ(4.2f, *script->getOutputs()->getChild("bar")->get()); + } + + TEST_F(ALuaScript_Init, CanModifyGlobalsAsIfTheyWereGlobalVariables) + { + auto* script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL.number = 5 + end + + function interface(IN,OUT) + IN.setGlobal = Type:Int32() + OUT.getGlobal = Type:Int32() + end + + function run(IN,OUT) + if IN.setGlobal ~= 0 then + GLOBAL.number = IN.setGlobal + end + OUT.getGlobal = GLOBAL.number + end + )"); + + ASSERT_TRUE(m_logicEngine.update()); + EXPECT_EQ(5, *script->getOutputs()->getChild("getGlobal")->get()); + + EXPECT_TRUE(script->getInputs()->getChild("setGlobal")->set(42)); + ASSERT_TRUE(m_logicEngine.update()); + EXPECT_EQ(42, *script->getOutputs()->getChild("getGlobal")->get()); + } + + TEST_F(ALuaScript_Init, CanDeclareFunctions) + { + auto* script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL.fun = function () return 42 end + end + + function interface(IN,OUT) + OUT.getGlobal = Type:Int32() + end + + function run(IN,OUT) + OUT.getGlobal = GLOBAL.fun() + end + )"); + + ASSERT_TRUE(m_logicEngine.update()); + EXPECT_EQ(42, *script->getOutputs()->getChild("getGlobal")->get()); + } + + TEST_F(ALuaScript_Init, CanUseStandardModules) + { + auto* script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL.number = math.floor(4.2) + end + + function interface(IN,OUT) + OUT.getGlobal = Type:Int32() + end + + function run(IN,OUT) + OUT.getGlobal = GLOBAL.number + end + )", WithStdModules({ EStandardModule::Math })); + + ASSERT_TRUE(m_logicEngine.update()); + EXPECT_EQ(4, *script->getOutputs()->getChild("getGlobal")->get()); + } + + TEST_F(ALuaScript_Init, CanUseCustomModules) + { + const std::string_view moduleSourceCode = R"( + local mymath = {} + function mymath.add(a,b) + return a+b + end + mymath.PI=3.1415 + return mymath + )"; + + LuaConfig config; + config.addDependency("mymath", *m_logicEngine.createLuaModule(moduleSourceCode)); + + auto* script = m_logicEngine.createLuaScript(R"( + modules("mymath") + + function init() + GLOBAL.number = mymath.add(5, mymath.PI) + end + function interface(IN,OUT) + OUT.getGlobal = Type:Float() + end + function run(IN,OUT) + OUT.getGlobal = GLOBAL.number + end + )", config); + + ASSERT_TRUE(m_logicEngine.update()); + EXPECT_FLOAT_EQ(8.1415f, *script->getOutputs()->getChild("getGlobal")->get()); + } + + TEST_F(ALuaScript_Init, IssuesErrorWhenUsingUndeclaredStandardModule) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL.number = math.floor(4.2) + end + function interface(IN,OUT) + end + function run(IN,OUT) + end + )"); + + EXPECT_EQ(script, nullptr); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr( + "Trying to read global variable 'math' in the init() function!")); + } + + TEST_F(ALuaScript_Init, InitializesAfterDeserilization) + { + WithTempDirectory tmpFolder; + + { + LogicEngine tmpLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto* script = tmpLogicEngine.createLuaScript(R"( + function init() + GLOBAL.number = 5 + end + + function interface(IN,OUT) + OUT.globalValueBefore = Type:Int32() + OUT.globalValueAfter = Type:Int32() + end + + function run(IN,OUT) + OUT.globalValueBefore = GLOBAL.number + GLOBAL.number = 42 + OUT.globalValueAfter = GLOBAL.number + end + )", WithStdModules({EStandardModule::Base}), "withGlobals"); + + ASSERT_TRUE(tmpLogicEngine.update()); + ASSERT_EQ(5, *script->getOutputs()->getChild("globalValueBefore")->get()); + ASSERT_EQ(42, *script->getOutputs()->getChild("globalValueAfter")->get()); + ASSERT_TRUE(SaveToFileWithoutValidation(tmpLogicEngine, "withGlobals.bin")); + } + + ASSERT_TRUE(m_logicEngine.loadFromFile("withGlobals.bin")); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(5, *m_logicEngine.findByName("withGlobals")->getOutputs()->getChild("globalValueBefore")->get()); + EXPECT_EQ(42, *m_logicEngine.findByName("withGlobals")->getOutputs()->getChild("globalValueAfter")->get()); + } + + TEST_F(ALuaScript_Init, DoesNotLeaveAnyLuaStackObjectsWhenLuaScriptDestroyed) + { + constexpr std::string_view scriptText = R"( + function interface(IN,OUT) + end + function run(IN,OUT) + end)"; + + for (int i = 0; i < 100; ++i) + { + auto* script = m_logicEngine.createLuaScript(scriptText); + ASSERT_TRUE(script != nullptr); + ASSERT_TRUE(m_logicEngine.destroy(*script)); + EXPECT_EQ(0, m_logicEngine.m_impl->getApiObjects().getNumElementsInLuaStack()); + } + } + + TEST_F(ALuaScript_Init, DoesNotLeaveAnyLuaStackObjectsWhenLuaScriptDestroyed_WithModule) + { + constexpr std::string_view moduleSourceCode = R"( + local mymodule = {} + function mymodule.colorType() + return { + red = Type:Int32(), + blue = Type:Int32(), + green = Type:Int32() + } + end + function mymodule.structWithArray() + return { + value = Type:Int32(), + array = Type:Array(2, Type:Int32()) + } + end + mymodule.color = { + red = 255, + green = 128, + blue = 72 + } + + return mymodule)"; + + constexpr std::string_view scriptSourceCode = R"( + modules("mymodule") + function init() + GLOBAL.number = 5 + end + function interface(IN,OUT) + IN.struct = mymodule.structWithArray() + OUT.struct = mymodule.structWithArray() + OUT.color = mymodule.colorType(); + OUT.value = Type:Int32() + end + function run(IN,OUT) + OUT.struct = IN.struct + OUT.color = mymodule.color + OUT.value = GLOBAL.number + end)"; + + for (int i = 0; i < 100; ++i) + { + auto module = m_logicEngine.createLuaModule(moduleSourceCode); + ASSERT_TRUE(module != nullptr); + LuaConfig config; + config.addDependency("mymodule", *module); + auto script = m_logicEngine.createLuaScript(scriptSourceCode, config); + ASSERT_TRUE(script != nullptr); + ASSERT_TRUE(m_logicEngine.destroy(*script)); + ASSERT_TRUE(m_logicEngine.destroy(*module)); + EXPECT_EQ(0, m_logicEngine.m_impl->getApiObjects().getNumElementsInLuaStack()); + } + } + + TEST_F(ALuaScript_Init, ScriptUsesInterfaceTypeDefinitionFromGlobal) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL.outputType = Type:String() + GLOBAL.outputName = "name" + GLOBAL.outputValue = "MrAnderson" + end + + function interface(IN,OUT) + OUT[GLOBAL.outputName] = GLOBAL.outputType + end + + function run(IN,OUT) + OUT[GLOBAL.outputName] = GLOBAL.outputValue + end)"); + ASSERT_NE(nullptr, script); + + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_STREQ("MrAnderson", script->getOutputs()->getChild("name")->get()->c_str()); + } + + TEST_F(ALuaScript_Init, ScriptUsesInterfaceTypeDefinitionFromGlobal_Array) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL.outputType = Type:Array(2, Type:Int32()) + end + + function interface(IN,OUT) + OUT.array = GLOBAL.outputType + end + + function run(IN,OUT) + OUT.array[2] = 42 + end)"); + ASSERT_NE(nullptr, script); + + EXPECT_TRUE(m_logicEngine.update()); + const auto arrayOutput = script->getOutputs()->getChild("array"); + ASSERT_NE(nullptr, arrayOutput); + ASSERT_EQ(2u, arrayOutput->getChildCount()); + + EXPECT_EQ(42, *arrayOutput->getChild(1u)->get()); + } + + TEST_F(ALuaScript_Init, ScriptUsesInterfaceStructDefinedInGlobal) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL.outputDefinition = { value = Type:Int32() } + end + + function interface(IN,OUT) + OUT.struct = GLOBAL.outputDefinition + end + + function run(IN,OUT) + OUT.struct.value = 666 + end)"); + ASSERT_NE(nullptr, script); + + EXPECT_TRUE(m_logicEngine.update()); + ASSERT_NE(nullptr, script->getOutputs()->getChild("struct")); + ASSERT_NE(nullptr, script->getOutputs()->getChild("struct")->getChild("value")); + EXPECT_EQ(666, *script->getOutputs()->getChild("struct")->getChild("value")->get()); + } + + TEST_F(ALuaScript_Init, ScriptUsesInterfaceStructDefinedInGlobal_WithArray) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL.outputDefinition = { + value = Type:Int32(), + array = Type:Array(2, Type:Int32()) + } + end + + function interface(IN,OUT) + OUT.struct = GLOBAL.outputDefinition + end + + function run(IN,OUT) + OUT.struct.value = 666 + OUT.struct.array[2] = 42 + end)"); + ASSERT_NE(nullptr, script); + + EXPECT_TRUE(m_logicEngine.update()); + ASSERT_NE(nullptr, script->getOutputs()->getChild("struct")); + ASSERT_NE(nullptr, script->getOutputs()->getChild("struct")->getChild("value")); + EXPECT_EQ(666, *script->getOutputs()->getChild("struct")->getChild("value")->get()); + const auto arrayOutput = script->getOutputs()->getChild("struct")->getChild("array"); + ASSERT_NE(nullptr, arrayOutput); + ASSERT_EQ(2u, arrayOutput->getChildCount()); + EXPECT_EQ(42, *arrayOutput->getChild(1u)->get()); + } + + TEST_F(ALuaScript_Init, SaveAndLoadScriptUsingInterfaceStructDefinedInGlobalWithArray) + { + WithTempDirectory tmpFolder; + { + LogicEngine otherLogic{ m_logicEngine.getFeatureLevel() }; + LuaScript* script = otherLogic.createLuaScript(R"( + function init() + GLOBAL.outputDefinition = { + value = Type:Int32(), + array = Type:Array(2, Type:Int32()) + } + end + + function interface(IN,OUT) + OUT.struct = GLOBAL.outputDefinition + end + + function run(IN,OUT) + OUT.struct.value = 666 + OUT.struct.array[2] = 42 + end)", {}, "script"); + ASSERT_NE(nullptr, script); + EXPECT_TRUE(otherLogic.update()); + EXPECT_TRUE(SaveToFileWithoutValidation(otherLogic, "intfInGlobal.bin")); + } + + EXPECT_TRUE(m_logicEngine.loadFromFile("intfInGlobal.bin")); + EXPECT_TRUE(m_logicEngine.update()); + + const auto script = m_logicEngine.findByName("script"); + ASSERT_NE(nullptr, script->getOutputs()->getChild("struct")); + ASSERT_NE(nullptr, script->getOutputs()->getChild("struct")->getChild("value")); + EXPECT_EQ(666, *script->getOutputs()->getChild("struct")->getChild("value")->get()); + const auto arrayOutput = script->getOutputs()->getChild("struct")->getChild("array"); + ASSERT_NE(nullptr, arrayOutput); + ASSERT_EQ(2u, arrayOutput->getChildCount()); + EXPECT_EQ(42, *arrayOutput->getChild(1u)->get()); + } + + class ALuaScript_Init_Sandboxing : public ALuaScript_Init + { + }; + + TEST_F(ALuaScript_Init_Sandboxing, ReportsErrorWhenTryingToReadUnknownGlobals) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + local t = someGlobalVariable + end + + function interface(IN,OUT) + end + + function run(IN,OUT) + end)"); + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr( + "Trying to read global variable 'someGlobalVariable' in the init() function! This" + " can cause undefined behavior and is forbidden! Use the GLOBAL table to read/write global data!")); + } + + TEST_F(ALuaScript_Init_Sandboxing, ReportsErrorWhenTryingToDeclareUnknownGlobals) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + thisCausesError = 'bad' + end + + function interface(IN,OUT) + end + + function run(IN,OUT) + end)"); + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Unexpected global variable definition 'thisCausesError' in init()! Please use the GLOBAL table to declare global data and functions, or use modules!")); + } + + TEST_F(ALuaScript_Init_Sandboxing, ReportsErrorWhenTryingToOverrideGlobals) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL = {} + end + + function interface(IN,OUT) + end + + function run(IN,OUT) + end)"); + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr(" Trying to override the GLOBAL table in init()! You can only add data, but not overwrite the table!")); + } + + TEST_F(ALuaScript_Init_Sandboxing, ReportsErrorWhenTryingToOverrideTypeTable) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + Type = {} + end + + function interface(IN,OUT) + end + + function run(IN,OUT) + end)"); + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr(" Can't override the Type special table in init()")); + } + + TEST_F(ALuaScript_Init_Sandboxing, ReportsErrorWhenTryingToDeclareInitFunctionTwice) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + end + + function init() + end + + function interface(IN,OUT) + end + + function run(IN,OUT) + end)"); + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Function 'init' can only be declared once!")); + } + + TEST_F(ALuaScript_Init_Sandboxing, ForbidsCallingSpecialFunctionsFromInsideInit) + { + for (const auto& specialFunction : std::vector{ "init", "run", "interface" }) + { + LuaScript* script = m_logicEngine.createLuaScript(fmt::format(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + end + + function init() + {}() + end + )", specialFunction)); + + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors()[0].message, + ::testing::HasSubstr(fmt::format("Trying to read global variable '{}' in the init() function! This can cause undefined behavior " + "and is forbidden! Use the GLOBAL table to read/write global data!", specialFunction))); + } + } +} diff --git a/client/logic/unittests/api/LuaScriptTest_Interface.cpp b/client/logic/unittests/api/LuaScriptTest_Interface.cpp new file mode 100644 index 000000000..71a1639b0 --- /dev/null +++ b/client/logic/unittests/api/LuaScriptTest_Interface.cpp @@ -0,0 +1,742 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LuaScriptTest_Base.h" + +#include "ramses-logic/Property.h" +#include "impl/PropertyImpl.h" +#include "LogTestUtils.h" +#include "fmt/format.h" +#include + +namespace ramses +{ + class ALuaScript_Interface : public ALuaScript + { + protected: + // Silence logs, unless explicitly enabled, to reduce spam and speed up tests + ScopedLogContextLevel m_silenceLogs{ ELogLevel::Off }; + }; + + TEST_F(ALuaScript_Interface, DoesNotGenerateErrorWhenOverwritingInputsInInterfaceFunction) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN = {} + end + + function run(IN,OUT) + end + )"); + + ASSERT_NE(nullptr, script); + + EXPECT_EQ(m_logicEngine.getErrors().size(), 0u); + } + + TEST_F(ALuaScript_Interface, DoesNotGenerateErrorWhenOverwritingOutputsInInterfaceFunction) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT = {} + end + + function run(IN,OUT) + end + )"); + + ASSERT_NE(nullptr, script); + + EXPECT_EQ(m_logicEngine.getErrors().size(), 0u); + } + + TEST_F(ALuaScript_Interface, ProducesErrorsIfARuntimeErrorOccursInInterface) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + error("emits error") + end + + function run(IN,OUT) + end + )", WithStdModules({EStandardModule::Base}), "errorInInterface"); + + ASSERT_EQ(nullptr, script); + EXPECT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::AllOf( + ::testing::HasSubstr("[errorInInterface] Error while loading script. Lua stack trace:"), + ::testing::HasSubstr("[C]: in function 'error'"))); + } + + TEST_F(ALuaScript_Interface, DeclaresStruct_WithExplicitTypeSyntax) + { + std::string_view explicitStructSyntax = R"( + function interface(IN,OUT) + IN.struct = Type:Struct({}) + end + + function run(IN,OUT) + end + )"; + + LuaScript* script = m_logicEngine.createLuaScript(explicitStructSyntax); + + auto inputs = script->getInputs(); + + ASSERT_EQ(1u, inputs->getChildCount()); + EXPECT_EQ("struct", inputs->getChild(0)->getName()); + EXPECT_EQ(EPropertyType::Struct, inputs->getChild(0)->getType()); + ASSERT_EQ(0u, inputs->getChild(0)->getChildCount()); + } + + TEST_F(ALuaScript_Interface, DeclaresNestedStruct_WithExplicitTypeSyntax) + { + std::string_view explicitStructSyntax = R"( + function interface(IN,OUT) + IN.struct = Type:Struct({ + nested = Type:Struct({subnested = Type:Float()}) + }) + end + + function run(IN,OUT) + end + )"; + + LuaScript* script = m_logicEngine.createLuaScript(explicitStructSyntax); + + auto inputs = script->getInputs(); + ASSERT_EQ(1u, inputs->getChildCount()); + + auto str = inputs->getChild(0); + EXPECT_EQ(EPropertyType::Struct, str->getType()); + EXPECT_EQ("struct", str->getName()); + ASSERT_EQ(1u, str->getChildCount()); + + auto nested = str->getChild(0); + EXPECT_EQ(EPropertyType::Struct, nested->getType()); + EXPECT_EQ("nested", nested->getName()); + ASSERT_EQ(1u, nested->getChildCount()); + + auto subnested = nested->getChild(0); + EXPECT_EQ(EPropertyType::Float, subnested->getType()); + EXPECT_EQ("subnested", subnested->getName()); + ASSERT_EQ(0u, subnested->getChildCount()); + } + + TEST_F(ALuaScript_Interface, ReportsError_WhenDeclaredStructWithExplicitTypeWithoutArgument) + { + std::string_view explicitStructSyntax = R"( + function interface(IN,OUT) + IN.struct = Type:Struct() + end + + function run(IN,OUT) + end + )"; + + LuaScript* script = m_logicEngine.createLuaScript(explicitStructSyntax); + ASSERT_EQ(nullptr, script); + EXPECT_THAT(m_logicEngine.getErrors().back().message, ::testing::HasSubstr("Type:Struct(T) invoked with invalid type parameter T!")); + } + + TEST_F(ALuaScript_Interface, ReturnsItsTopLevelOutputsByIndex_OrderedLexicographically) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScriptWithOutputs); + + auto outputs = script->getOutputs(); + + ASSERT_EQ(11u, outputs->getChildCount()); + EXPECT_EQ("enabled", outputs->getChild(0)->getName()); + EXPECT_EQ(EPropertyType::Bool, outputs->getChild(0)->getType()); + EXPECT_EQ("name", outputs->getChild(1)->getName()); + EXPECT_EQ(EPropertyType::String, outputs->getChild(1)->getType()); + EXPECT_EQ("speed", outputs->getChild(2)->getName()); + EXPECT_EQ(EPropertyType::Int32, outputs->getChild(2)->getType()); + EXPECT_EQ("speed2", outputs->getChild(3)->getName()); + EXPECT_EQ(EPropertyType::Int64, outputs->getChild(3)->getType()); + EXPECT_EQ("temp", outputs->getChild(4)->getName()); + EXPECT_EQ(EPropertyType::Float, outputs->getChild(4)->getType()); + + // Vec2/3/4 f/i + EXPECT_EQ("vec2f", outputs->getChild(5)->getName()); + EXPECT_EQ(EPropertyType::Vec2f, outputs->getChild(5)->getType()); + EXPECT_EQ("vec2i", outputs->getChild(6)->getName()); + EXPECT_EQ(EPropertyType::Vec2i, outputs->getChild(6)->getType()); + EXPECT_EQ("vec3f", outputs->getChild(7)->getName()); + EXPECT_EQ(EPropertyType::Vec3f, outputs->getChild(7)->getType()); + EXPECT_EQ("vec3i", outputs->getChild(8)->getName()); + EXPECT_EQ(EPropertyType::Vec3i, outputs->getChild(8)->getType()); + EXPECT_EQ("vec4f", outputs->getChild(9)->getName()); + EXPECT_EQ(EPropertyType::Vec4f, outputs->getChild(9)->getType()); + EXPECT_EQ("vec4i", outputs->getChild(10)->getName()); + EXPECT_EQ(EPropertyType::Vec4i, outputs->getChild(10)->getType()); + } + + TEST_F(ALuaScript_Interface, ReturnsNestedOutputsByIndex_OrderedLexicographically_whenDeclaredOneByOne) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.struct = {} + OUT.struct.field3 = {} + OUT.struct.field2 = Type:Float() + OUT.struct.field1 = Type:Int32() + OUT.struct.field3.subfield2 = Type:Float() + OUT.struct.field3.subfield1 = Type:Int32() + end + + function run(IN,OUT) + end + )"); + + ASSERT_NE(nullptr, script); + + auto outputs = script->getOutputs(); + ASSERT_EQ(1u, outputs->getChildCount()); + auto structField = outputs->getChild(0); + EXPECT_EQ("struct", structField->getName()); + EXPECT_EQ(EPropertyType::Struct, structField->getType()); + + ASSERT_EQ(3u, structField->getChildCount()); + auto field1 = structField->getChild(0); + auto field2 = structField->getChild(1); + auto field3 = structField->getChild(2); + + EXPECT_EQ("field1", field1->getName()); + EXPECT_EQ(EPropertyType::Int32, field1->getType()); + EXPECT_EQ("field2", field2->getName()); + EXPECT_EQ(EPropertyType::Float, field2->getType()); + EXPECT_EQ("field3", field3->getName()); + EXPECT_EQ(EPropertyType::Struct, field3->getType()); + + ASSERT_EQ(2u, field3->getChildCount()); + auto subfield1 = field3->getChild(0); + auto subfield2 = field3->getChild(1); + + EXPECT_EQ("subfield1", subfield1->getName()); + EXPECT_EQ(EPropertyType::Int32, subfield1->getType()); + EXPECT_EQ("subfield2", subfield2->getName()); + EXPECT_EQ(EPropertyType::Float, subfield2->getType()); + } + + TEST_F(ALuaScript_Interface, CanDeclarePropertiesProgramatically) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.root = {} + local lastStruct = OUT.root + for i=1,2 do + lastStruct["sub" .. tostring(i)] = {} + lastStruct = lastStruct["sub" .. tostring(i)] + end + end + + function run(IN,OUT) + end + )", WithStdModules({ EStandardModule::Base })); + + ASSERT_NE(nullptr, script); + + auto outputs = script->getOutputs(); + + ASSERT_EQ(1u, outputs->getChildCount()); + auto root = outputs->getChild(0); + EXPECT_EQ("root", root->getName()); + EXPECT_EQ(EPropertyType::Struct, root->getType()); + + ASSERT_EQ(1u, root->getChildCount()); + auto sub1 = root->getChild(0); + + EXPECT_EQ("sub1", sub1->getName()); + EXPECT_EQ(EPropertyType::Struct, sub1->getType()); + + ASSERT_EQ(1u, sub1->getChildCount()); + auto sub2 = sub1->getChild(0); + EXPECT_EQ("sub2", sub2->getName()); + EXPECT_EQ(EPropertyType::Struct, sub2->getType()); + + EXPECT_EQ(0u, sub2->getChildCount()); + } + + TEST_F(ALuaScript_Interface, MarksInputsAsInput) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScriptWithInputs); + auto inputs = script->getInputs(); + const auto inputCount = inputs->getChildCount(); + for (size_t i = 0; i < inputCount; ++i) + { + EXPECT_EQ(internal::EPropertySemantics::ScriptInput, inputs->getChild(i)->m_impl->getPropertySemantics()); + } + } + + TEST_F(ALuaScript_Interface, MarksOutputsAsOutput) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScriptWithOutputs); + auto outputs = script->getOutputs(); + const auto outputCount = outputs->getChildCount(); + for (size_t i = 0; i < outputCount; ++i) + { + EXPECT_EQ(internal::EPropertySemantics::ScriptOutput, outputs->getChild(i)->m_impl->getPropertySemantics()); + } + } + + TEST_F(ALuaScript_Interface, AllowsAccessToWrappedUserdata) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.array_int = Type:Array(2, Type:Int32()) + OUT.struct = {a=Type:Int32(), b={c = Type:Int32(), d=Type:Float()}} + OUT.array_struct = Type:Array(3, {a=Type:Int32(), b=Type:Float()}) + + local expectedUserdata = { + IN, + IN.array_int, + OUT, + OUT.struct, + OUT.struct.a, + OUT.struct.b, + OUT.struct.b.c, + OUT.struct.b.d, + OUT.array_struct, + OUT.array_struct[2], + OUT.array_struct[2].a, + OUT.array_struct[2].b + } + + for k, v in pairs(expectedUserdata) do + if type(v) ~= 'userdata' then + error("Expected userdata!") + end + end + end + + function run(IN,OUT) + end + )", WithStdModules({EStandardModule::Base})); + ASSERT_NE(nullptr, script); + } + + TEST_F(ALuaScript_Interface, ReportsErrorWhenAccessingStructByIndex) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.struct = {a=Type:Int32(), b={c = Type:Int32(), d=Type:Float()}} + local causesError = OUT.struct[1] + end + + function run(IN,OUT) + end + )"); + ASSERT_EQ(nullptr, script); + EXPECT_THAT(m_logicEngine.getErrors().back().message, ::testing::HasSubstr("lua: error: Bad index access to struct 'struct': Expected a string but got object of type number instead!")); + } + + TEST_F(ALuaScript_Interface, ReportsErrorWhenAccessingArrayByString) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.array_int = Type:Array(2, Type:Int32()) + local causesError = IN.array_int["causesError"] + end + + function run(IN,OUT) + end + )"); + ASSERT_EQ(nullptr, script); + EXPECT_THAT(m_logicEngine.getErrors().back().message, ::testing::HasSubstr("lua: error: Invalid index access in array 'array_int'")); + } + + TEST_F(ALuaScript_Interface, ReportsErrorWhenAccessingArrayWithIndexOverflow) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.array_int = Type:Array(2, Type:Int32()) + local causesError = IN.array_int[3] + end + + function run(IN,OUT) + end + )"); + ASSERT_EQ(nullptr, script); + EXPECT_THAT(m_logicEngine.getErrors().back().message, ::testing::HasSubstr("Invalid index access in array 'array_int'. Expected index in the range [0, 2] but got 3 instead!")); + } + + TEST_F(ALuaScript_Interface, AssignsDefaultValuesToItsInputs) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScriptWithInputs); + auto inputs = script->getInputs(); + + auto speedInt32 = inputs->getChild("speed"); + auto tempFloat = inputs->getChild("temp"); + auto nameString = inputs->getChild("name"); + auto enabledBool = inputs->getChild("enabled"); + auto vec_2f = inputs->getChild("vec2f"); + auto vec_3f = inputs->getChild("vec3f"); + auto vec_4f = inputs->getChild("vec4f"); + auto vec_2i = inputs->getChild("vec2i"); + auto vec_3i = inputs->getChild("vec3i"); + auto vec_4i = inputs->getChild("vec4i"); + EXPECT_EQ(0, *speedInt32->get()); + EXPECT_FLOAT_EQ(0.0f, *tempFloat->get()); + EXPECT_EQ("", *nameString->get()); + EXPECT_TRUE(enabledBool->get()); + EXPECT_FALSE(*enabledBool->get()); + EXPECT_EQ(*vec_2f->get(), vec2f(0.0f, 0.0f)); + EXPECT_EQ(*vec_3f->get(), vec3f(0.0f, 0.0f, 0.0f)); + EXPECT_EQ(*vec_4f->get(), vec4f(0.0f, 0.0f, 0.0f, 0.0f)); + EXPECT_EQ(*vec_2i->get(), vec2i(0, 0)); + EXPECT_EQ(*vec_3i->get(), vec3i(0, 0, 0)); + EXPECT_EQ(*vec_4i->get(), vec4i(0, 0, 0, 0)); + } + + TEST_F(ALuaScript_Interface, AssignsDefaultValuesToItsOutputs) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScriptWithOutputs); + auto outputs = script->getOutputs(); + + auto speedInt32 = outputs->getChild("speed"); + auto tempFloat = outputs->getChild("temp"); + auto nameString = outputs->getChild("name"); + auto enabledBool = outputs->getChild("enabled"); + auto vec_2f = outputs->getChild("vec2f"); + auto vec_3f = outputs->getChild("vec3f"); + auto vec_4f = outputs->getChild("vec4f"); + auto vec_2i = outputs->getChild("vec2i"); + auto vec_3i = outputs->getChild("vec3i"); + auto vec_4i = outputs->getChild("vec4i"); + + EXPECT_TRUE(speedInt32->get()); + EXPECT_EQ(0, speedInt32->get().value()); + EXPECT_TRUE(tempFloat->get()); + EXPECT_EQ(0.0f, tempFloat->get().value()); + EXPECT_TRUE(nameString->get()); + EXPECT_EQ("", nameString->get().value()); + EXPECT_TRUE(enabledBool->get()); + EXPECT_EQ(false, *enabledBool->get()); + + EXPECT_TRUE(vec_2f->get()); + EXPECT_TRUE(vec_3f->get()); + EXPECT_TRUE(vec_4f->get()); + EXPECT_TRUE(vec_2i->get()); + EXPECT_TRUE(vec_3i->get()); + EXPECT_TRUE(vec_4i->get()); + vec2f zeroVec2f{ 0.0f, 0.0f }; + vec3f zeroVec3f{ 0.0f, 0.0f, 0.0f }; + vec4f zeroVec4f{ 0.0f, 0.0f, 0.0f, 0.0f }; + vec2i zeroVec2i{ 0, 0 }; + vec3i zeroVec3i{ 0, 0, 0 }; + vec4i zeroVec4i{ 0, 0, 0, 0 }; + EXPECT_EQ(zeroVec2f, *vec_2f->get()); + EXPECT_EQ(zeroVec3f, *vec_3f->get()); + EXPECT_EQ(zeroVec4f, *vec_4f->get()); + EXPECT_EQ(zeroVec2i, *vec_2i->get()); + EXPECT_EQ(zeroVec3i, *vec_3i->get()); + EXPECT_EQ(zeroVec4i, *vec_4i->get()); + } + + TEST_F(ALuaScript_Interface, AssignsDefaultValuesToArrays) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.array_int = Type:Array(3, Type:Int32()) + IN.array_float = Type:Array(3, Type:Float()) + IN.array_vec2f = Type:Array(3, Type:Vec2f()) + OUT.array_int = Type:Array(3, Type:Int32()) + OUT.array_float = Type:Array(3, Type:Float()) + OUT.array_vec2f = Type:Array(3, Type:Vec2f()) + end + + function run(IN,OUT) + end + )"); + + std::initializer_list rootProperties = { script->getInputs(), script->getOutputs() }; + + for (auto rootProp : rootProperties) + { + auto array_int = rootProp->getChild("array_int"); + auto array_float = rootProp->getChild("array_float"); + auto array_vec2f = rootProp->getChild("array_vec2f"); + + for (size_t i = 0; i < 3; ++i) + { + EXPECT_TRUE(array_int->getChild(i)->get()); + EXPECT_EQ(0, *array_int->getChild(i)->get()); + EXPECT_TRUE(array_float->getChild(i)->get()); + EXPECT_FLOAT_EQ(0.0f, *array_float->getChild(i)->get()); + EXPECT_TRUE(array_vec2f->getChild(i)->get()); + EXPECT_EQ(*array_vec2f->getChild(i)->get(), vec2f(0.0f, 0.0f)); + } + } + } + + TEST_F(ALuaScript_Interface, ComputesSizeOfCustomPropertiesUsingCustomLengthFunction) + { + std::string_view scriptSrc = R"( + function init() + -- use global variables to store size info from interface(IN,OUT) - otherwise no way to transfer data + -- outside of interface()! + GLOBAL.sizes = {} + end + function interface(IN,OUT) + IN.unused = Type:Int32() + -- store size of IN with one property + GLOBAL.sizes.inputsSizeSingle = rl_len(IN) + IN.unused2 = Type:Int32() + -- store size of IN with two properties + GLOBAL.sizes.inputsSizeTwo = rl_len(IN) + + -- Create nested output to store (and provide) the sizes of all containers + OUT.sizes = { + inputsSizeSingle = Type:Int32(), + inputsSizeTwo = Type:Int32(), + outputSizeSingleStruct = Type:Int32(), + outputSizeNested = Type:Int32(), + outputSizeArray = Type:Int32(), + outputSizeArrayElem = Type:Int32() + } + -- Store size of OUT + GLOBAL.sizes.outputSizeSingleStruct = rl_len(OUT) + -- Store size of OUT.sizes (nested container) + GLOBAL.sizes.outputSizeNested = rl_len(OUT.sizes) + + OUT.array = Type:Array(5, {a=Type:Int32(), b=Type:Float()}) + GLOBAL.sizes.outputSizeArray = rl_len(OUT.array) + GLOBAL.sizes.outputSizeArrayElem = rl_len(OUT.array[1]) + end + + function run(IN,OUT) + OUT.sizes = GLOBAL.sizes + end + )"; + auto* script = m_logicEngine.createLuaScript(scriptSrc); + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_EQ(1, *script->getOutputs()->getChild("sizes")->getChild("inputsSizeSingle")->get()); + EXPECT_EQ(2, *script->getOutputs()->getChild("sizes")->getChild("inputsSizeTwo")->get()); + EXPECT_EQ(1, *script->getOutputs()->getChild("sizes")->getChild("outputSizeSingleStruct")->get()); + EXPECT_EQ(6, *script->getOutputs()->getChild("sizes")->getChild("outputSizeNested")->get()); + EXPECT_EQ(5, *script->getOutputs()->getChild("sizes")->getChild("outputSizeArray")->get()); + EXPECT_EQ(2, *script->getOutputs()->getChild("sizes")->getChild("outputSizeArrayElem")->get()); + } + + class ALuaScript_Interface_Sandboxing : public ALuaScript_Interface + { + }; + + TEST_F(ALuaScript_Interface_Sandboxing, DeclaringGlobalSymbolsCausesCompilationErrors) + { + auto script = m_logicEngine.createLuaScript(R"( + globalVar = "this will cause error" + )", WithStdModules({ EStandardModule::Base })); + + ASSERT_EQ(nullptr, script); + EXPECT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, + ::testing::HasSubstr("Declaring global variables is forbidden (exceptions: the functions 'init', 'interface' and 'run')! (found value of type 'string')")); + } + + TEST_F(ALuaScript_Interface_Sandboxing, ReportsErrorWhenTryingToReadUnknownGlobals) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + local t = someGlobalVariable + end + + function run(IN,OUT) + end)"); + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr( + "Unexpected global access to key 'someGlobalVariable' in interface()! Only 'GLOBAL' and 'Type' are allowed as a key")); + } + + TEST_F(ALuaScript_Interface_Sandboxing, ReportsErrorWhenSettingGlobals) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + thisCausesError = 'bad' + end + + function run(IN,OUT) + end)"); + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr( + "Unexpected global variable definition 'thisCausesError' in interface()! " + "Use the GLOBAL table inside the init() function to declare global data and functions, or use modules!")); + } + + TEST_F(ALuaScript_Interface_Sandboxing, ReportsErrorWhenTryingToOverrideGlobals) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + end + + function interface(IN,OUT) + GLOBAL = {} + end + + function run(IN,OUT) + end)"); + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Trying to override the GLOBAL table in interface()! You can only read data, but not overwrite the GLOBAL table!")); + } + + TEST_F(ALuaScript_Interface_Sandboxing, ReportsErrorWhenTryingToOverrideTypesTable) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + end + + function interface(IN,OUT) + Type = {} + end + + function run(IN,OUT) + end)"); + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Can't override the 'Type' symbol in interface()!")); + } + + TEST_F(ALuaScript_Interface_Sandboxing, ReportsErrorWhenTryingToDeclareInterfaceFunctionTwice) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + + function interface(IN,OUT) + end + + function run(IN,OUT) + end)"); + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Function 'interface' can only be declared once!")); + } + + TEST_F(ALuaScript_Interface_Sandboxing, ForbidsCallingSpecialFunctionsFromInsideInterface) + { + for (const auto& specialFunction : std::vector{ "init", "run", "interface" }) + { + LuaScript* script = m_logicEngine.createLuaScript(fmt::format(R"( + function init() + end + function run(IN,OUT) + end + + function interface(IN,OUT) + {}() + end + )", specialFunction)); + + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors()[0].message, + ::testing::HasSubstr(fmt::format("Unexpected global access to key '{}' in interface()! Only 'GLOBAL' and 'Type' are allowed as a key", specialFunction))); + } + } + + // Don't modify this test - it captures specific behavior related to a bug with Ramses Logic 1.0 after reworking the type system + // Need to keep as confidence test to safeguard against regressions + TEST_F(ALuaScript_Interface, Bugfix_Confidence_HandlesComplexTypeDeclarationWithNestedStructs) + { + ramses::LogicEngine logicEngine{ m_logicEngine.getFeatureLevel() }; + + std::string scriptText = R"( + function interface(IN,OUT) + local craneGimbal = { + cam_Translation = Type:Vec3f(), + POS_ORIGIN_Translation = Type:Vec3f(), + PITCH_Rotation = Type:Vec3f(), + YAW_Rotation = Type:Vec3f() + } + + local viewport = { + offsetX = Type:Int32(), + offsetY = Type:Int32(), + width = Type:Int32(), + height = Type:Int32() + } + + local frustum_persp = { + nearPlane = Type:Float(), + farPlane = Type:Float(), + fieldOfView = Type:Float(), + aspectRatio = Type:Float() + } + + local frustum_ortho = { + nearPlane = Type:Float(), + farPlane = Type:Float(), + leftPlane = Type:Float(), + rightPlane = Type:Float(), + bottomPlane = Type:Float(), + topPlane = Type:Float() + } + + OUT.CameraSettings = { + CraneGimbal = craneGimbal, + CraneGimbal_R = craneGimbal, + scene_camera = { + Viewport = viewport, + Frustum = frustum_persp, + }, + ui_camera = { + Frustum = frustum_ortho, + Viewport = viewport, + translation = Type:Vec3f(), + } + } + end + function run(IN,OUT) + end + )"; + + auto* script = logicEngine.createLuaScript(scriptText); + ASSERT_TRUE(script != nullptr); + } + + TEST_F(ALuaScript_Interface, OwnsAllProperties_confidenceTest) + { + const auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.array_int = Type:Array(2, Type:Int32()) + OUT.struct = {a=Type:Int32(), b={c = Type:Int32(), d=Type:Float()}} + OUT.array_struct = Type:Array(3, {a=Type:Int32(), b=Type:Float()}) + end + + function run(IN,OUT) + end + )"); + ASSERT_NE(nullptr, script); + + int propsCount = 0; + std::deque props{ script->getInputs(), script->getOutputs() }; + while (!props.empty()) + { + const auto prop = props.back(); + props.pop_back(); + propsCount++; + + EXPECT_EQ(script, &prop->getOwningLogicNode()); + + for (size_t i = 0u; i < prop->getChildCount(); ++i) + props.push_back(prop->getChild(i)); + } + + EXPECT_EQ(20, propsCount); // just check that the unrolled recursion works + } +} diff --git a/client/logic/unittests/api/LuaScriptTest_Lifecycle.cpp b/client/logic/unittests/api/LuaScriptTest_Lifecycle.cpp new file mode 100644 index 000000000..dc7ff42bc --- /dev/null +++ b/client/logic/unittests/api/LuaScriptTest_Lifecycle.cpp @@ -0,0 +1,422 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LuaScriptTest_Base.h" +#include "WithTempDirectory.h" + +#include "impl/LuaScriptImpl.h" +#include "impl/LogicEngineImpl.h" +#include "impl/PropertyImpl.h" + +#include "fmt/format.h" +#include + +namespace ramses::internal +{ + class ALuaScript_Lifecycle : public ALuaScript + { + protected: + WithTempDirectory tempFolder; + }; + + TEST_F(ALuaScript_Lifecycle, ProducesNoErrorsWhenCreatedFromMinimalScript) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScript); + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + } + + TEST_F(ALuaScript_Lifecycle, ProvidesNameAsPassedDuringCreation) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScript, {}, "script name"); + EXPECT_EQ("script name", script->getName()); + } + + //This is actually bad (because it can cause undefined behavior), but we still have a test to show/test how it works + TEST_F(ALuaScript_Lifecycle, KeepsLocalScopeSymbolsDuringRunMethod) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + -- 'Local' symbols in the global space are inherited by functions + local localSymbol = "localSymbol" + + function interface(IN,OUT) + OUT.result = Type:String() + end + + function run(IN,OUT) + -- local symbols from global scope are available here + OUT.result = localSymbol + end + )"); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(script->getOutputs()->getChild("result")->get(), "localSymbol"); + } + + class ALuaScript_LifecycleWithFiles : public ALuaScript_Lifecycle + { + }; + + TEST_F(ALuaScript_LifecycleWithFiles, NoOutputs) + { + { + LogicEngine tempLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto script = tempLogicEngine.createLuaScript( + R"( + function interface(IN,OUT) + IN.param = Type:Int32() + end + function run(IN,OUT) + end + )"); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(SaveToFileWithoutValidation(tempLogicEngine, "script.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("script.bin")); + const LuaScript* loadedScript = *m_logicEngine.getCollection().begin(); + + ASSERT_NE(nullptr, loadedScript); + + auto inputs = loadedScript->getInputs(); + auto outputs = loadedScript->getOutputs(); + + ASSERT_NE(nullptr, inputs); + ASSERT_NE(nullptr, outputs); + + ASSERT_EQ(inputs->getChildCount(), 1u); + ASSERT_EQ(outputs->getChildCount(), 0u); + + EXPECT_EQ("param", inputs->getChild(0u)->getName()); + EXPECT_EQ(EPropertyType::Int32, inputs->getChild(0u)->getType()); + + EXPECT_TRUE(m_logicEngine.update()); + } + } + + TEST_F(ALuaScript_LifecycleWithFiles, Arrays) + { + { + LogicEngine tempLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto script = tempLogicEngine.createLuaScript( + R"( + function interface(IN,OUT) + IN.array = Type:Array(2, Type:Float()) + end + function run(IN,OUT) + end + )", {}, "MyScript"); + + ASSERT_NE(nullptr, script); + + + script->getInputs()->getChild("array")->getChild(0)->set(0.1f); + script->getInputs()->getChild("array")->getChild(1)->set(0.2f); + EXPECT_TRUE(SaveToFileWithoutValidation(tempLogicEngine, "script.bin")); + } + { + m_logicEngine.loadFromFile("script.bin"); + const LuaScript* loadedScript = m_logicEngine.findByName("MyScript"); + + const auto inputs = loadedScript->getInputs(); + + ASSERT_NE(nullptr, inputs); + + ASSERT_EQ(inputs->getChildCount(), 1u); + + // Full type inspection of array type, children and values + EXPECT_EQ("array", inputs->getChild(0u)->getName()); + EXPECT_EQ(EPropertyType::Array, inputs->getChild(0u)->getType()); + EXPECT_EQ(EPropertyType::Float, inputs->getChild(0u)->getChild(0u)->getType()); + EXPECT_EQ(EPropertyType::Float, inputs->getChild(0u)->getChild(1u)->getType()); + EXPECT_EQ("", inputs->getChild(0u)->getChild(0u)->getName()); + EXPECT_EQ("", inputs->getChild(0u)->getChild(1u)->getName()); + EXPECT_EQ(0u, inputs->getChild(0u)->getChild(0u)->getChildCount()); + EXPECT_EQ(0u, inputs->getChild(0u)->getChild(1u)->getChildCount()); + EXPECT_FLOAT_EQ(0.1f, *inputs->getChild(0u)->getChild(0u)->get()); + EXPECT_FLOAT_EQ(0.2f, *inputs->getChild(0u)->getChild(1u)->get()); + + } + } + + TEST_F(ALuaScript_LifecycleWithFiles, NestedArray) + { + { + LogicEngine tempLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto script = tempLogicEngine.createLuaScript( + R"( + function interface(IN,OUT) + IN.nested = + { + array = Type:Array(1, Type:Vec3f()) + } + end + function run(IN,OUT) + end + )", {}, "MyScript"); + + script->getInputs()->getChild("nested")->getChild("array")->getChild(0)->set({1.1f, 1.2f, 1.3f}); + EXPECT_TRUE(SaveToFileWithoutValidation(tempLogicEngine, "arrays.bin")); + } + { + m_logicEngine.loadFromFile("arrays.bin"); + const LuaScript* loadedScript = m_logicEngine.findByName("MyScript"); + + const auto inputs = loadedScript->getInputs(); + + ASSERT_EQ(inputs->getChildCount(), 1u); + + // Type inspection on nested array + const auto nested = inputs->getChild(0u); + EXPECT_EQ("nested", nested->getName()); + auto nested_array = nested->getChild(0u); + EXPECT_EQ("array", nested_array->getName()); + + // Check children of nested array, also values + EXPECT_EQ(1u, nested_array->getChildCount()); + EXPECT_EQ("", nested_array->getChild(0u)->getName()); + EXPECT_EQ(EPropertyType::Vec3f, nested_array->getChild(0u)->getType()); + EXPECT_EQ(0u, nested_array->getChild(0u)->getChildCount()); + EXPECT_EQ(*nested_array->getChild(0u)->get(), vec3f(1.1f, 1.2f, 1.3f)); + + } + } + + TEST_F(ALuaScript_LifecycleWithFiles, NestedProperties) + { + { + LogicEngine tempLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto script = tempLogicEngine.createLuaScript( + R"( + function interface(IN,OUT) + IN.int_param = Type:Int32() + IN.nested_param = { + int_param = Type:Int32() + } + OUT.float_param = Type:Float() + OUT.nested_param = { + float_param = Type:Float() + } + end + function run(IN,OUT) + OUT.float_param = 47.11 + end + )"); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(SaveToFileWithoutValidation(tempLogicEngine, "nested_array.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("nested_array.bin")); + const LuaScript* loadedScript = *m_logicEngine.getCollection().begin(); + + ASSERT_NE(nullptr, loadedScript); + + auto inputs = loadedScript->getInputs(); + auto outputs = loadedScript->getOutputs(); + + ASSERT_NE(nullptr, inputs); + ASSERT_NE(nullptr, outputs); + + ASSERT_EQ(inputs->getChildCount(), 2u); + ASSERT_EQ(outputs->getChildCount(), 2u); + + EXPECT_EQ("int_param", inputs->getChild(0u)->getName()); + EXPECT_EQ(EPropertyType::Int32, inputs->getChild(0u)->getType()); + EXPECT_EQ("float_param", outputs->getChild(0u)->getName()); + EXPECT_EQ(EPropertyType::Float, outputs->getChild(0u)->getType()); + + auto in_child = inputs->getChild(1u); + auto out_child = outputs->getChild(1u); + + EXPECT_EQ("nested_param", in_child->getName()); + EXPECT_EQ(EPropertyType::Struct, in_child->getType()); + EXPECT_EQ("nested_param", out_child->getName()); + EXPECT_EQ(EPropertyType::Struct, out_child->getType()); + + ASSERT_EQ(in_child->getChildCount(), 1u); + ASSERT_EQ(out_child->getChildCount(), 1u); + + auto in_nested_child = in_child->getChild(0u); + auto out_nested_child = out_child->getChild(0u); + + EXPECT_EQ("int_param", in_nested_child->getName()); + EXPECT_EQ(EPropertyType::Int32, in_nested_child->getType()); + EXPECT_EQ("float_param", out_nested_child->getName()); + EXPECT_EQ(EPropertyType::Float, out_nested_child->getType()); + + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_FLOAT_EQ(47.11f, *outputs->getChild(0u)->get()); + } + } + + TEST_F(ALuaScript_LifecycleWithFiles, ArrayOfStructs) + { + { + LogicEngine tempLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto script = tempLogicEngine.createLuaScript( + R"( + function interface(IN,OUT) + local structDecl = { + str = Type:String(), + array = Type:Array(2, Type:Int32()), + nested_struct = { + int = Type:Int32(), + nested_array = Type:Array(1, Type:Float()), + } + } + IN.arrayOfStructs = Type:Array(2, structDecl) + OUT.arrayOfStructs = Type:Array(2, structDecl) + end + function run(IN,OUT) + OUT.arrayOfStructs = IN.arrayOfStructs + end + )"); + + ASSERT_NE(nullptr, script); + script->getInputs()->getChild("arrayOfStructs")->getChild(1)->getChild("nested_struct")->getChild("nested_array")->getChild(0)->set(42.f); + EXPECT_TRUE(SaveToFileWithoutValidation(tempLogicEngine, "array_of_structs.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("array_of_structs.bin")); + LuaScript* loadedScript = *m_logicEngine.getCollection().begin(); + + ASSERT_NE(nullptr, loadedScript); + + auto inputs = loadedScript->getInputs(); + auto outputs = loadedScript->getOutputs(); + + ASSERT_NE(nullptr, inputs); + ASSERT_NE(nullptr, outputs); + + auto loadedInput = loadedScript->getInputs()->getChild("arrayOfStructs")->getChild(1)->getChild("nested_struct")->getChild("nested_array")->getChild(0); + EXPECT_FLOAT_EQ(42.f, *loadedInput->get()); + loadedInput->set(100.0f); + + EXPECT_TRUE(m_logicEngine.update()); + auto loadedOutput = loadedScript->getOutputs()->getChild("arrayOfStructs")->getChild(1)->getChild("nested_struct")->getChild("nested_array")->getChild(0); + EXPECT_FLOAT_EQ(100.0f, *loadedOutput->get()); + } + } + + // This is a confidence test which tests all property types, both as inputs and outputs, and as arrays + // The combination of arrays with different sizes, types, and their values, yields a lot of possible error cases, hence this test + TEST_F(ALuaScript_LifecycleWithFiles, AllPropertyTypes_confidenceTest) + { + const std::vector allPrimitiveTypes = + { + EPropertyType::Float, + EPropertyType::Vec2f, + EPropertyType::Vec3f, + EPropertyType::Vec4f, + EPropertyType::Int32, + EPropertyType::Int64, + EPropertyType::Vec2i, + EPropertyType::Vec3i, + EPropertyType::Vec4i, + EPropertyType::String, + EPropertyType::Bool + }; + + std::string scriptSrc = "function interface(IN,OUT)\n"; + size_t arraySize = 1; + // For each type, create an input, output, and an array version of it with various sizes + for (auto primType : allPrimitiveTypes) + { + const std::string typeName = GetLuaPrimitiveTypeName(primType); + scriptSrc += fmt::format("IN.{} = Type:{}()\n", typeName, typeName); + scriptSrc += fmt::format("IN.array_{} = Type:Array({}, Type:{}())\n", typeName, arraySize, typeName); + scriptSrc += fmt::format("OUT.{} = Type:{}()\n", typeName, typeName); + scriptSrc += fmt::format("OUT.array_{} = Type:Array({}, Type:{}())\n", typeName, arraySize, typeName); + ++arraySize; + } + + scriptSrc += R"( + end + function run(IN,OUT) + end + )"; + + { + LogicEngine tempLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto script = tempLogicEngine.createLuaScript(scriptSrc, {}, "MyScript"); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(SaveToFileWithoutValidation(tempLogicEngine, "arrays.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("arrays.bin")); + const LuaScript* loadedScript = m_logicEngine.findByName("MyScript"); + + auto inputs = loadedScript->getInputs(); + auto outputs = loadedScript->getOutputs(); + + // Test both inputs and outputs + for (auto rootProp : std::initializer_list{ inputs, outputs }) + { + // One primitive for each type, and one array for each type + ASSERT_EQ(rootProp->getChildCount(), allPrimitiveTypes.size() * 2); + + size_t expectedArraySize = 1; + for(const auto primType: allPrimitiveTypes) + { + const auto primitiveChild = rootProp->getChild(GetLuaPrimitiveTypeName(primType)); + const auto arrayChild = inputs->getChild(std::string("array_") + GetLuaPrimitiveTypeName(primType)); + + const std::string typeName = GetLuaPrimitiveTypeName(primType); + + EXPECT_EQ(primType, primitiveChild->getType()); + EXPECT_EQ(typeName, primitiveChild->getName()); + EXPECT_EQ(0u, primitiveChild->getChildCount()); + + EXPECT_EQ("array_" + typeName, arrayChild->getName()); + EXPECT_EQ(EPropertyType::Array, arrayChild->getType()); + EXPECT_EQ(expectedArraySize, arrayChild->getChildCount()); + + for (size_t a = 0; a < expectedArraySize; ++a) + { + const auto arrayElement = arrayChild->getChild(a); + EXPECT_EQ("", arrayElement->getName()); + EXPECT_EQ(primType, arrayElement->getType()); + EXPECT_EQ(0u, arrayElement->getChildCount()); + } + ++expectedArraySize; + } + } + } + } + + TEST_F(ALuaScript_LifecycleWithFiles, OverwritesCurrentData_WhenLoadedASecondTimeFromTheSameFile) + { + { + LogicEngine tempLogicEngine{ m_logicEngine.getFeatureLevel() }; + auto script = tempLogicEngine.createLuaScript( + R"( + function interface(IN,OUT) + IN.data = Type:Int32() + end + function run(IN,OUT) + end + )"); + + ASSERT_NE(nullptr, script); + script->getInputs()->getChild("data")->set(42); + EXPECT_TRUE(SaveToFileWithoutValidation(tempLogicEngine, "script.bin")); + } + + EXPECT_TRUE(m_logicEngine.loadFromFile("script.bin")); + auto loadedScript = *m_logicEngine.getCollection().begin(); + loadedScript->getInputs()->getChild("data")->set(5); + + EXPECT_TRUE(m_logicEngine.loadFromFile("script.bin")); + loadedScript = *m_logicEngine.getCollection().begin(); + EXPECT_EQ(42, *loadedScript->getInputs()->getChild("data")->get()); + } +} diff --git a/client/logic/unittests/api/LuaScriptTest_Modules.cpp b/client/logic/unittests/api/LuaScriptTest_Modules.cpp new file mode 100644 index 000000000..9b64d4ff9 --- /dev/null +++ b/client/logic/unittests/api/LuaScriptTest_Modules.cpp @@ -0,0 +1,1340 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "WithTempDirectory.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaModule.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" +#include "impl/LuaScriptImpl.h" +#include + +using namespace testing; + +namespace ramses::internal +{ + class ALuaScriptWithModule : public ::testing::Test + { + protected: + const std::string_view m_moduleSourceCode = R"( + local mymath = {} + function mymath.add(a,b) + return a+b + end + mymath.PI=3.1415 + return mymath + )"; + const std::string_view m_moduleSourceCode2 = R"( + local myothermath = {} + function myothermath.sub(a,b) + return a-b + end + function myothermath.colorType() + return { + red = Type:Int32(), + blue = Type:Int32(), + green = Type:Int32() + } + end + function myothermath.structWithArray() + return { + value = Type:Int32(), + array = Type:Array(2, Type:Int32()) + } + end + + myothermath.color = { + red = 255, + green = 128, + blue = 72 + } + + return myothermath + )"; + + LuaConfig createDeps(const std::vector>& dependencies) + { + LuaConfig config; + for (const auto& [alias, moduleSrc] : dependencies) + { + LuaModule* mod = m_logicEngine.createLuaModule(moduleSrc); + config.addDependency(alias, *mod); + } + + return config; + } + + static LuaConfig WithStdMath() + { + LuaConfig config; + config.addStandardModuleDependency(EStandardModule::Math); + return config; + } + + LogicEngine m_logicEngine{ ramses::EFeatureLevel_Latest }; + }; + + TEST_F(ALuaScriptWithModule, CanBeCreated) + { + LuaConfig config; + LuaModule* mod = m_logicEngine.createLuaModule(m_moduleSourceCode); + config.addDependency("mymath", *mod); + + const auto script = m_logicEngine.createLuaScript(R"( + modules("mymath") + + function interface(IN,OUT) + OUT.v = Type:Int32() + OUT.pi = Type:Float() + end + + function run(IN,OUT) + OUT.v = mymath.add(1,2) + OUT.pi = mymath.PI + end + )", config); + ASSERT_NE(nullptr, script); + EXPECT_THAT(script->m_script.getModules(), ElementsAre(Pair("mymath", mod))); + + m_logicEngine.update(); + EXPECT_EQ(3, *script->getOutputs()->getChild("v")->get()); + EXPECT_FLOAT_EQ(3.1415f, *script->getOutputs()->getChild("pi")->get()); + } + + TEST_F(ALuaScriptWithModule, UsesModuleUnderDifferentName) + { + const auto script = m_logicEngine.createLuaScript(R"( + modules("mymodule") + + function interface(IN,OUT) + OUT.v = Type:Int32() + OUT.pi = Type:Float() + end + + function run(IN,OUT) + OUT.v = mymodule.add(1,2) + OUT.pi = mymodule.PI + end + )", createDeps({ { "mymodule", m_moduleSourceCode } })); + ASSERT_NE(nullptr, script); + + m_logicEngine.update(); + EXPECT_EQ(3, *script->getOutputs()->getChild("v")->get()); + EXPECT_FLOAT_EQ(3.1415f, *script->getOutputs()->getChild("pi")->get()); + } + + TEST_F(ALuaScriptWithModule, MultipleModules) + { + const auto script = m_logicEngine.createLuaScript(R"( + modules("mymath", "mymath2") + + function interface(IN,OUT) + OUT.v = Type:Int32() + end + + function run(IN,OUT) + OUT.v = mymath.add(1,2) + mymath2.sub(20,10) + end + )", createDeps({ { "mymath", m_moduleSourceCode }, { "mymath2", m_moduleSourceCode2 } })); + + m_logicEngine.update(); + EXPECT_EQ(13, *script->getOutputs()->getChild("v")->get()); + } + + TEST_F(ALuaScriptWithModule, UsesSameModuleUnderMultipleNames) + { + const auto module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymathmodule"); + ASSERT_NE(nullptr, module); + + LuaConfig config; + config.addDependency("mymath", *module); + config.addDependency("mymath2", *module); + + const auto script = m_logicEngine.createLuaScript(R"( + modules("mymath", "mymath2") + + function interface(IN,OUT) + OUT.v = Type:Int32() + end + + function run(IN,OUT) + OUT.v = mymath.add(1,2) + mymath2.add(20,10) + end + )", config); + ASSERT_NE(nullptr, script); + + m_logicEngine.update(); + EXPECT_EQ(33, *script->getOutputs()->getChild("v")->get()); + } + + TEST_F(ALuaScriptWithModule, TwoScriptsUseSameModule) + { + const auto module = m_logicEngine.createLuaModule(m_moduleSourceCode, {}, "mymathmodule"); + ASSERT_NE(nullptr, module); + + LuaConfig config1; + config1.addDependency("mymath", *module); + + const auto script1 = m_logicEngine.createLuaScript(R"( + modules("mymath") + + function interface(IN,OUT) + OUT.v = Type:Int32() + end + + function run(IN,OUT) + OUT.v = mymath.add(1,2) + end + )", config1); + ASSERT_NE(nullptr, script1); + + LuaConfig config2; + config2.addDependency("mymathother", *module); + + const auto script2 = m_logicEngine.createLuaScript(R"( + modules("mymathother") + + function interface(IN,OUT) + OUT.v = Type:Int32() + end + + function run(IN,OUT) + OUT.v = mymathother.add(10,20) + end + )", config2); + ASSERT_NE(nullptr, script1); + + m_logicEngine.update(); + EXPECT_EQ(3, *script1->getOutputs()->getChild("v")->get()); + EXPECT_EQ(30, *script2->getOutputs()->getChild("v")->get()); + } + + TEST_F(ALuaScriptWithModule, ErrorIfModuleReadsGlobalVariablesWhichDontExist) + { + const std::string_view moduleSrc = R"( + local mymath = {} + mymath.PI = peterPan -- does not exist + return mymath + )"; + LuaModule* luaModule = m_logicEngine.createLuaModule(moduleSrc, {}, "mod"); + EXPECT_EQ(nullptr, luaModule); + + EXPECT_FALSE(m_logicEngine.getErrors().empty()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Trying to read global variable 'peterPan' in module! This can cause undefined behavior and is forbidden!")); + } + + TEST_F(ALuaScriptWithModule, ErrorIfModuleWritesGlobalVariables) + { + const std::string_view moduleSrc = R"( + local mymath = {} + someGlobalVar = 15 + return mymath + )"; + LuaModule* luaModule = m_logicEngine.createLuaModule(moduleSrc, {}, "mod"); + EXPECT_EQ(nullptr, luaModule); + + EXPECT_FALSE(m_logicEngine.getErrors().empty()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Declaring global variables is forbidden in modules! (found value of type 'number' assigned to variable 'someGlobalVar')")); + } + + TEST_F(ALuaScriptWithModule, ErrorIfModuleDoesNotReturnTable) + { + std::vector errorCases = { + "return nil", + "return 5", + "return \"TheModule\"", + "return false", + "return true", + }; + + for (const auto& moduleSrc : errorCases) + { + LuaModule* luaModule = m_logicEngine.createLuaModule(moduleSrc, {}, "mod"); + EXPECT_EQ(nullptr, luaModule); + + EXPECT_FALSE(m_logicEngine.getErrors().empty()); + EXPECT_EQ("[mod] Error while loading module. Module script must return a table!", m_logicEngine.getErrors()[0].message); + } + } + + TEST_F(ALuaScriptWithModule, CanUseTableDataAndItsTypeDefinitionFromModule) + { + const auto script = m_logicEngine.createLuaScript(R"( + modules("mymath") + function interface(IN,OUT) + OUT.color = mymath.colorType() + end + function run(IN,OUT) + OUT.color = mymath.color + end + )", createDeps({ { "mymath", m_moduleSourceCode2 } })); + ASSERT_TRUE(script); + + m_logicEngine.update(); + const Property* colorOutput = script->getOutputs()->getChild("color"); + ASSERT_TRUE(colorOutput && colorOutput->getChild("red") && colorOutput->getChild("green") && colorOutput->getChild("blue")); + EXPECT_EQ(255, *colorOutput->getChild("red")->get()); + EXPECT_EQ(128, *colorOutput->getChild("green")->get()); + EXPECT_EQ(72, *colorOutput->getChild("blue")->get()); + } + + TEST_F(ALuaScriptWithModule, CanUseTableDataAndItsTypeDefinitionFromModule_WhenDefinitionIsATableField) + { + const auto modSrc = R"( + local mod = {} + mod.structType = { + a = Type:String(), + b = Type:Int32() + } + return mod + )"; + + const auto script = m_logicEngine.createLuaScript(R"( + modules("mod") + + function interface(IN,OUT) + IN.struct = mod.structType + end + + function run(IN,OUT) + end + )", createDeps({{"mod", modSrc}})); + ASSERT_NE(nullptr, script); + + const Property& structInput = *script->getInputs()->getChild("struct"); + EXPECT_EQ(EPropertyType::String, structInput.getChild("a")->getType()); + EXPECT_EQ(EPropertyType::Int32, structInput.getChild("b")->getType()); + } + + // This is based on our user docs example - it used to be broken, so added as a test here to make sure it works + TEST_F(ALuaScriptWithModule, CanUseTableDataAndItsArrayTypeDefinitionFromModule) + { + const auto modSrc = R"( + local coalaModule = {} + + coalaModule.coalaChief = "Alfred" + + coalaModule.coalaStruct = { + preferredFood = Type:String(), + weight = Type:Int32() + } + + function coalaModule.bark() + print("Coalas don't bark...") + end + + return coalaModule + )"; + + LuaConfig moduleConfig; + moduleConfig.addStandardModuleDependency(EStandardModule::Base); + LuaModule* mod = m_logicEngine.createLuaModule(modSrc, moduleConfig); + + LuaConfig scriptConfig; + scriptConfig.addDependency("coalas", *mod); + scriptConfig.addStandardModuleDependency(EStandardModule::Base); + + const auto script = m_logicEngine.createLuaScript(R"( + modules("coalas") + + function interface(IN,OUT) + OUT.coalas = Type:Array(2, coalas.coalaStruct) + end + + function run(IN,OUT) + OUT.coalas = { + { + preferredFood = "bamboo", + weight = 5 + }, + { + preferredFood = "donuts", + weight = 12 + } + } + + print(coalas.coalaChief .. " says:") + coalas.bark() + end + )", scriptConfig); + ASSERT_NE(nullptr, script); + + m_logicEngine.update(); + const Property& coala1 = *script->getOutputs()->getChild("coalas")->getChild(0); + const Property& coala2 = *script->getOutputs()->getChild("coalas")->getChild(1); + EXPECT_EQ("bamboo", *coala1.getChild("preferredFood")->get()); + EXPECT_EQ(5, *coala1.getChild("weight")->get()); + EXPECT_EQ("donuts", *coala2.getChild("preferredFood")->get()); + EXPECT_EQ(12, *coala2.getChild("weight")->get()); + } + + TEST_F(ALuaScriptWithModule, CanUseTableDataAndItsTypeDefinitionFromModule_WithArray) + { + const auto script = m_logicEngine.createLuaScript(R"( + modules("mymath") + function interface(IN,OUT) + IN.struct = mymath.structWithArray() + OUT.struct = mymath.structWithArray() + end + function run(IN,OUT) + OUT.struct = IN.struct + OUT.struct.array[2] = 42 + end + )", createDeps({ { "mymath", m_moduleSourceCode2 } })); + ASSERT_TRUE(script); + + m_logicEngine.update(); + + const Property* input = script->getInputs()->getChild("struct"); + ASSERT_TRUE(input); + ASSERT_TRUE(input->getChild("value") && input->getChild("value")->getType() == EPropertyType::Int32); + ASSERT_TRUE(input->getChild("array") && input->getChild("array")->getType() == EPropertyType::Array); + + const Property* output = script->getOutputs()->getChild("struct"); + ASSERT_TRUE(output); + ASSERT_TRUE(output->getChild("value") && output->getChild("value")->getType() == EPropertyType::Int32); + ASSERT_TRUE(output->getChild("array") && output->getChild("array")->getType() == EPropertyType::Array); + ASSERT_EQ(2u, output->getChild("array")->getChildCount()); + EXPECT_EQ(42, *output->getChild("array")->getChild(1u)->get()); + } + + TEST_F(ALuaScriptWithModule, CanGetTableSizeWithCustomMethod) + { + const std::string_view modSrc = R"( + local mod = {} + mod.table1 = { a=1, b=2 } + mod.table2 = { 4, 5, 6, 7 } + mod.table3 = { a=1, b=2, 42 } -- expected size 1, according to Lua semantics + return mod + )"; + + const auto script = m_logicEngine.createLuaScript(R"( + modules("mod") + function interface(IN,OUT) + OUT.table1size = Type:Int32() + OUT.table2size = Type:Int32() + OUT.table3size = Type:Int32() + end + function run(IN,OUT) + OUT.table1size = rl_len(mod.table1) + OUT.table2size = rl_len(mod.table2) + OUT.table3size = rl_len(mod.table3) + end + )", createDeps({ { "mod", modSrc } })); + ASSERT_TRUE(script); + + m_logicEngine.update(); + EXPECT_EQ(0, *script->getOutputs()->getChild("table1size")->get()); + EXPECT_EQ(4, *script->getOutputs()->getChild("table2size")->get()); + EXPECT_EQ(1, *script->getOutputs()->getChild("table3size")->get()); + } + + TEST_F(ALuaScriptWithModule, ReadsVecTypeLengthAndValues) + { + const std::string_view modSrc = R"( + local mod = {} + mod.vec4i = { 4, 5, 6, 7 } + mod.vec2f = { 0.1, -0.3 } + return mod + )"; + + auto dependencies = createDeps({ { "mod", modSrc } }); + dependencies.addStandardModuleDependency(EStandardModule::Base); + + const auto script = m_logicEngine.createLuaScript(R"( + modules("mod") + function interface(IN,OUT) + OUT.vec4isize = Type:Int32() + OUT.vec2fsize = Type:Int32() + OUT.vec4i = Type:Vec4i() + OUT.vec2f = Type:Vec2f() + + -- test that vec can be also read during interface extraction + local vec4i = mod.vec4i + assert(rl_len(vec4i) == 4) + assert(vec4i[4] == 7) + end + function run(IN,OUT) + OUT.vec4isize = rl_len(mod.vec4i) + OUT.vec2fsize = rl_len(mod.vec2f) + OUT.vec4i = mod.vec4i + OUT.vec2f = mod.vec2f + end + )", dependencies); + ASSERT_TRUE(script); + + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(4, *script->getOutputs()->getChild("vec4isize")->get()); + EXPECT_EQ(2, *script->getOutputs()->getChild("vec2fsize")->get()); + EXPECT_EQ(*script->getOutputs()->getChild("vec4i")->get(), vec4i(4, 5, 6, 7)); + EXPECT_FLOAT_EQ(0.1f, (*script->getOutputs()->getChild("vec2f")->get())[0]); + EXPECT_FLOAT_EQ(-0.3f, (*script->getOutputs()->getChild("vec2f")->get())[1]); + } + + TEST_F(ALuaScriptWithModule, CanGetTableSizeWithCustomMethod_InsideModuleAswell) + { + const std::string_view modSrc = R"( + local mod = {} + mod.table = { 4, 6 } + mod.tableSize = rl_len(mod.table) + return mod + )"; + + const auto script = m_logicEngine.createLuaScript(R"( + modules("mod") + function interface(IN,OUT) + OUT.size = Type:Int32() + end + function run(IN,OUT) + OUT.size = mod.tableSize + end + )", createDeps({ { "mod", modSrc } })); + ASSERT_TRUE(script); + + m_logicEngine.update(); + EXPECT_EQ(2, *script->getOutputs()->getChild("size")->get()); + } + TEST_F(ALuaScriptWithModule, ReportsErrorWhenCustomLengthFunctionCalledOnInvalidType) + { + const std::string_view modSrc = R"( + local mod = {} + mod.invalidTypeForLength = 42 + return mod + )"; + + const auto script = m_logicEngine.createLuaScript(R"( + modules("mod") + function interface(IN,OUT) + OUT.size = Type:Int32() + end + function run(IN,OUT) + OUT.size = rl_len(mod.invalidTypeForLength) + end + )", createDeps({ { "mod", modSrc } })); + ASSERT_TRUE(script); + + EXPECT_FALSE(m_logicEngine.update()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("lua: error: rl_len() called on an unsupported type 'number'")); + } + + TEST_F(ALuaScriptWithModule, UsesModuleThatDependsOnAnotherModule) + { + const std::string_view wrappedModuleSrc = R"( + modules("mymath") + local wrapped = {} + function wrapped.add(a,b) + return mymath.add(a, b) + 5 + end + return wrapped + )"; + + const auto wrapped = m_logicEngine.createLuaModule(wrappedModuleSrc, createDeps({ { "mymath", m_moduleSourceCode } })); + + LuaConfig config; + config.addDependency("wrapped", *wrapped); + + const auto script = m_logicEngine.createLuaScript(R"( + modules("wrapped") + function interface(IN,OUT) + OUT.result = Type:Int32() + end + function run(IN,OUT) + OUT.result = wrapped.add(10, 20) + end + )", config); + + EXPECT_TRUE(m_logicEngine.update()); + const Property* result = script->getOutputs()->getChild("result"); + ASSERT_TRUE(result); + EXPECT_EQ(35, *result->get()); + } + + TEST_F(ALuaScriptWithModule, SecondLevelDependenciesAreHidden) + { + const std::string_view wrappedModuleSrc = R"( + modules("mymath") + local wrapped = {} + function wrapped.add(a,b) + return a + b + 100 + end + wrapped.PI=42 + return wrapped + )"; + + const auto wrapped = m_logicEngine.createLuaModule(wrappedModuleSrc, createDeps({ {"mymath", m_moduleSourceCode} })); + + LuaConfig config; + config.addDependency("wrapped", *wrapped); + + m_logicEngine.createLuaScript(R"( + modules("wrapped") + function interface(IN,OUT) + OUT.add = Type:Int32() + OUT.PI = Type:Float() + end + function run(IN,OUT) + -- This should generate 'global access error' if indirect dependency is not correctly hidden + mymath.add(10, 20) + end + )", config); + + EXPECT_FALSE(m_logicEngine.update()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Unexpected global access to key 'mymath' in run()! Only 'GLOBAL' is allowed as a key")); + } + + TEST_F(ALuaScriptWithModule, ReloadsModuleUsingTheSameNameCausesItToBeRecompiled) + { + const std::string_view moduleSource = R"( + local mymath = {} + mymath.pi=3.1415 + return mymath + )"; + + const std::string_view moduleSource_Modified = R"( + local mymath = {} + mymath.pi=4 + return mymath + )"; + + const std::string_view scriptSrc = R"( + modules("module") + function interface(IN,OUT) + OUT.pi = Type:Float() + end + function run(IN,OUT) + OUT.pi = module.pi + end + )"; + + auto module = m_logicEngine.createLuaModule(moduleSource, {}, "module"); + + LuaConfig config; + config.addDependency("module", *module); + auto script = m_logicEngine.createLuaScript(scriptSrc, config); + + ASSERT_TRUE(m_logicEngine.update()); + const Property* colorOutput = script->getOutputs()->getChild("pi"); + EXPECT_FLOAT_EQ(3.1415f, *colorOutput->get()); + + ASSERT_TRUE(m_logicEngine.destroy(*script)); + ASSERT_TRUE(m_logicEngine.destroy(*module)); + module = m_logicEngine.createLuaModule(moduleSource_Modified, {}, "module"); + + config = {}; + config.addDependency("module", *module); + script = m_logicEngine.createLuaScript(scriptSrc, config); + + ASSERT_TRUE(m_logicEngine.update()); + colorOutput = script->getOutputs()->getChild("pi"); + EXPECT_FLOAT_EQ(4.0f, *colorOutput->get()); + } + + TEST_F(ALuaScriptWithModule, CanBeSerialized) + { + WithTempDirectory tempDir; + + { + LogicEngine logic{ m_logicEngine.getFeatureLevel() }; + // 2 scripts, one module used by first script, other module used by both scripts + const auto module1 = logic.createLuaModule(m_moduleSourceCode, {}, "mymodule1"); + const auto module2 = logic.createLuaModule(m_moduleSourceCode2, {}, "mymodule2"); + ASSERT_TRUE(module1 && module2); + + LuaConfig config1; + config1.addDependency("mymath", *module1); + config1.addDependency("mymathother", *module2); + + LuaConfig config2; + config2.addDependency("mymath", *module2); + + logic.createLuaScript(R"( + modules("mymath", "mymathother") + function interface(IN,OUT) + OUT.v = Type:Int32() + OUT.color = mymathother.colorType() + end + function run(IN,OUT) + OUT.v = mymath.add(1,2) + mymathother.sub(60,30) + OUT.color = mymathother.color + end + )", config1, "script1"); + logic.createLuaScript(R"( + modules("mymath") + function interface(IN,OUT) + OUT.v = Type:Int32() + end + function run(IN,OUT) + OUT.v = mymath.sub(90,60) + end + )", config2, "script2"); + + SaveFileConfig configNoValidation; + configNoValidation.setValidationEnabled(false); + EXPECT_TRUE(logic.saveToFile("scriptmodules.tmp", configNoValidation)); + } + + EXPECT_TRUE(m_logicEngine.loadFromFile("scriptmodules.tmp")); + m_logicEngine.update(); + + const auto module1 = m_logicEngine.findByName("mymodule1"); + const auto module2 = m_logicEngine.findByName("mymodule2"); + const auto script1 = m_logicEngine.findByName("script1"); + const auto script2 = m_logicEngine.findByName("script2"); + ASSERT_TRUE(module1 && module2 && script1 && script2); + EXPECT_THAT(script1->m_script.getModules(), UnorderedElementsAre(Pair("mymath", module1), Pair("mymathother", module2))); + EXPECT_THAT(script2->m_script.getModules(), UnorderedElementsAre(Pair("mymath", module2))); + + m_logicEngine.update(); + EXPECT_EQ(33, *script1->getOutputs()->getChild("v")->get()); + const Property* colorOutput = script1->getOutputs()->getChild("color"); + ASSERT_TRUE(colorOutput && colorOutput->getChild("red") && colorOutput->getChild("green") && colorOutput->getChild("blue")); + EXPECT_EQ(255, *colorOutput->getChild("red")->get()); + EXPECT_EQ(128, *colorOutput->getChild("green")->get()); + EXPECT_EQ(72, *colorOutput->getChild("blue")->get()); + + EXPECT_EQ(30, *script2->getOutputs()->getChild("v")->get()); + } + + TEST_F(ALuaScriptWithModule, UsesStructPropertyInInterfaceDefinedInModule) + { + const std::string_view moduleDefiningInterfaceType = R"( + local mytypes = {} + function mytypes.mystruct() + return { + name = Type:String(), + address = + { + street = Type:String(), + number = Type:Int32() + } + } + end + return mytypes + )"; + + const auto script = m_logicEngine.createLuaScript(R"( + modules("mytypes") + function interface(IN,OUT) + IN.struct = mytypes.mystruct() + OUT.struct = mytypes.mystruct() + end + + function run(IN,OUT) + OUT.struct = IN.struct + end + )", createDeps({ { "mytypes", moduleDefiningInterfaceType } })); + ASSERT_NE(nullptr, script); + + for (auto rootProp : std::initializer_list{ script->getInputs(), script->getOutputs() }) + { + ASSERT_EQ(1u, rootProp->getChildCount()); + const auto structChild = rootProp->getChild(0); + + EXPECT_EQ("struct", structChild->getName()); + EXPECT_EQ(EPropertyType::Struct, structChild->getType()); + ASSERT_EQ(2u, structChild->getChildCount()); + const auto name = structChild->getChild("name"); + EXPECT_EQ(EPropertyType::String, name->getType()); + const auto address = structChild->getChild("address"); + ASSERT_EQ(2u, address->getChildCount()); + EXPECT_EQ(EPropertyType::Struct, address->getType()); + const auto addressStr = address->getChild("street"); + const auto addressNr = address->getChild("number"); + EXPECT_EQ(EPropertyType::String, addressStr->getType()); + EXPECT_EQ(EPropertyType::Int32, addressNr->getType()); + } + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALuaScriptWithModule, UsesStructPropertyInInterfaceDefinedInModule_UseInArray) + { + const std::string_view moduleDefiningInterfaceType = R"( + local mytypes = {} + function mytypes.mystruct() + return { + name = Type:String(), + address = + { + street = Type:String(), + number = Type:Int32() + } + } + end + return mytypes + )"; + + const auto script = m_logicEngine.createLuaScript(R"( + modules("mytypes") + function interface(IN,OUT) + IN.array_of_structs = Type:Array(2, mytypes.mystruct()) + OUT.array_of_structs = Type:Array(2, mytypes.mystruct()) + end + + function run(IN,OUT) + OUT.array_of_structs = IN.array_of_structs + end + )", createDeps({ { "mytypes", moduleDefiningInterfaceType } })); + ASSERT_NE(nullptr, script); + + for (auto rootProp : std::initializer_list{ script->getInputs(), script->getOutputs() }) + { + ASSERT_EQ(1u, rootProp->getChildCount()); + const auto arrayOfStructs = rootProp->getChild(0); + + EXPECT_EQ("array_of_structs", arrayOfStructs->getName()); + EXPECT_EQ(EPropertyType::Array, arrayOfStructs->getType()); + ASSERT_EQ(2u, arrayOfStructs->getChildCount()); + + for (size_t i = 0; i < 2; ++i) + { + const auto structChild = arrayOfStructs->getChild(i); + EXPECT_EQ(EPropertyType::Struct, structChild->getType()); + EXPECT_EQ("", structChild->getName()); + ASSERT_EQ(2u, structChild->getChildCount()); + const auto name = structChild->getChild("name"); + EXPECT_EQ(EPropertyType::String, name->getType()); + const auto address = structChild->getChild("address"); + ASSERT_EQ(2u, address->getChildCount()); + EXPECT_EQ(EPropertyType::Struct, address->getType()); + const auto addressStr = address->getChild("street"); + const auto addressNr = address->getChild("number"); + EXPECT_EQ(EPropertyType::String, addressStr->getType()); + EXPECT_EQ(EPropertyType::Int32, addressNr->getType()); + } + } + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALuaScriptWithModule, SerializesAndDeserializesScriptWithStructPropertyInInterfaceDefinedInModule) + { + WithTempDirectory tempDirectory; + { + constexpr std::string_view moduleDefiningInterfaceType = R"( + local mytypes = {} + function mytypes.mystruct() + return { + name = Type:String(), + address = + { + street = Type:String(), + number = Type:Int32() + } + } + end + return mytypes)"; + + constexpr std::string_view scriptSrc = R"( + modules("mytypes") + function interface(IN,OUT) + IN.array_of_structs = Type:Array(2, mytypes.mystruct()) + OUT.array_of_structs = Type:Array(2, mytypes.mystruct()) + end + + function run(IN,OUT) + OUT.array_of_structs = IN.array_of_structs + OUT.array_of_structs[2].address.number = 42 + end)"; + + LogicEngine otherLogicEngine{ m_logicEngine.getFeatureLevel() }; + + LuaModule* module = otherLogicEngine.createLuaModule(moduleDefiningInterfaceType); + ASSERT_NE(nullptr, module); + + LuaConfig config; + config.addDependency("mytypes", *module); + LuaScript* script = otherLogicEngine.createLuaScript(scriptSrc, config, "script"); + ASSERT_NE(nullptr, script); + + EXPECT_TRUE(otherLogicEngine.update()); + + SaveFileConfig configNoValidation; + configNoValidation.setValidationEnabled(false); + ASSERT_TRUE(otherLogicEngine.saveToFile("moduleWithInterface.bin", configNoValidation)); + } + + m_logicEngine.loadFromFile("moduleWithInterface.bin"); + const LuaScript* script = m_logicEngine.findByName("script"); + + for (auto rootProp : std::initializer_list{ script->getInputs(), script->getOutputs() }) + { + ASSERT_EQ(1u, rootProp->getChildCount()); + const auto arrayOfStructs = rootProp->getChild(0); + + EXPECT_EQ("array_of_structs", arrayOfStructs->getName()); + EXPECT_EQ(EPropertyType::Array, arrayOfStructs->getType()); + ASSERT_EQ(2u, arrayOfStructs->getChildCount()); + + for (size_t i = 0; i < 2; ++i) + { + const auto structChild = arrayOfStructs->getChild(i); + EXPECT_EQ(EPropertyType::Struct, structChild->getType()); + EXPECT_EQ("", structChild->getName()); + ASSERT_EQ(2u, structChild->getChildCount()); + const auto name = structChild->getChild("name"); + ASSERT_NE(nullptr, name); + EXPECT_EQ(EPropertyType::String, name->getType()); + const auto address = structChild->getChild("address"); + ASSERT_NE(nullptr, address); + ASSERT_EQ(2u, address->getChildCount()); + EXPECT_EQ(EPropertyType::Struct, address->getType()); + const auto addressStr = address->getChild("street"); + const auto addressNr = address->getChild("number"); + ASSERT_NE(nullptr, addressStr); + ASSERT_NE(nullptr, addressNr); + EXPECT_EQ(EPropertyType::String, addressStr->getType()); + EXPECT_EQ(EPropertyType::Int32, addressNr->getType()); + } + } + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(42, *script->getOutputs()->getChild(0u)->getChild(1u)->getChild("address")->getChild("number")->get()); + } + + TEST_F(ALuaScriptWithModule, ScriptOverwritingBaseLibraryWontAffectOtherScriptUsingIt) + { + const auto script1 = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.v = Type:Float() + OUT.v = Type:Int32() + end + function run(IN,OUT) + OUT.v = math.floor(IN.v) + math.floor = nil + end + )", WithStdMath()); + ASSERT_NE(nullptr, script1); + + const auto script2 = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.v = Type:Float() + OUT.v = Type:Int32() + end + function run(IN,OUT) + OUT.v = math.floor(IN.v + 1.0) + end + )", WithStdMath()); + ASSERT_NE(nullptr, script2); + + // first update runs fine + script1->getInputs()->getChild("v")->set(1.2f); + script2->getInputs()->getChild("v")->set(1.3f); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(1, *script1->getOutputs()->getChild("v")->get()); + EXPECT_EQ(2, *script2->getOutputs()->getChild("v")->get()); + + // force update of script2 again, after math.floor was set nil in script1 + // script2 is NOT affected + script2->getInputs()->getChild("v")->set(2.3f); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(3, *script2->getOutputs()->getChild("v")->get()); + + // script1 broke itself by setting its dependency to nil and fails to update + script1->getInputs()->getChild("v")->set(2.2f); + EXPECT_FALSE(m_logicEngine.update()); + } + + TEST_F(ALuaScriptWithModule, ScriptOverwritingBaseLibraryViaModuleWontAffectOtherScriptUsingIt) + { + const std::string_view maliciousModuleSrc = R"( + local mymath = {} + function mymath.breakFloor(v) + local ret = math.floor(v) + math.floor = nil + return ret + end + return mymath + )"; + + const auto maliciousModule = m_logicEngine.createLuaModule(maliciousModuleSrc, WithStdMath()); + + LuaConfig withMaliciousModule; + withMaliciousModule.addDependency("mymath", *maliciousModule); + const auto script1 = m_logicEngine.createLuaScript(R"( + modules("mymath") + function interface(IN,OUT) + IN.v = Type:Float() + OUT.v = Type:Int32() + end + function run(IN,OUT) + OUT.v = mymath.breakFloor(IN.v) + end + )", withMaliciousModule); + ASSERT_NE(nullptr, script1); + + const auto script2 = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.v = Type:Float() + OUT.v = Type:Int32() + end + function run(IN,OUT) + OUT.v = math.floor(IN.v + 1.0) + end + )", WithStdMath()); + ASSERT_NE(nullptr, script2); + + // first update runs fine + script1->getInputs()->getChild("v")->set(1.2f); + script2->getInputs()->getChild("v")->set(1.3f); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(1, *script1->getOutputs()->getChild("v")->get()); + EXPECT_EQ(2, *script2->getOutputs()->getChild("v")->get()); + + // force update of script2 again, after math.floor was set nil in script1 via module + // script2 is NOT affected + script2->getInputs()->getChild("v")->set(2.3f); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(3, *script2->getOutputs()->getChild("v")->get()); + + // module broke itself by setting its math dependency to nil and script1 using it fails to update + script1->getInputs()->getChild("v")->set(2.2f); + EXPECT_FALSE(m_logicEngine.update()); + } + + class ALuaScriptDependencyMatch : public ALuaScriptWithModule + { + }; + + TEST_F(ALuaScriptDependencyMatch, FailsToBeCreatedIfDeclaredDependencyDoesNotMatchProvidedDependency_NotProvidedButDeclared) + { + constexpr std::string_view src = R"( + modules("dep1", "dep2") + function interface(IN,OUT) + end + function run(IN,OUT) + end + )"; + EXPECT_EQ(nullptr, m_logicEngine.createLuaScript(src, createDeps({ {"dep2", m_moduleSourceCode} }))); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Module dependencies declared in source code: dep1, dep2\n Module dependencies provided on create API: dep2")); + } + + TEST_F(ALuaScriptDependencyMatch, FailsToBeCreatedIfDeclaredDependencyDoesNotMatchProvidedDependency_ProvidedButNotDeclared) + { + constexpr std::string_view src = R"( + modules("dep1", "dep2") + function interface(IN,OUT) + end + function run(IN,OUT) + end + )"; + EXPECT_EQ(nullptr, m_logicEngine.createLuaScript(src, createDeps({ {"dep1", m_moduleSourceCode}, {"dep2", m_moduleSourceCode}, {"dep3", m_moduleSourceCode} }))); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Module dependencies declared in source code: dep1, dep2\n Module dependencies provided on create API: dep1, dep2, dep3")); + } + + TEST_F(ALuaScriptDependencyMatch, FailsToBeCreatedIfDeclaredDependencyDoesNotMatchProvidedDependency_ExractionError) + { + constexpr std::string_view src = R"( + modules("dep1", "dep1") -- duplicate dependency + function interface(IN,OUT) + end + function run(IN,OUT) + end + )"; + EXPECT_EQ(nullptr, m_logicEngine.createLuaScript(src, createDeps({ {"dep1", m_moduleSourceCode} }))); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Error while extracting module dependencies: 'dep1' appears more than once in dependency list")); + } + + // Tests specifically modules isolation + class ALuaScriptWithModule_Isolation : public ALuaScriptWithModule + { + protected: + }; + + TEST_F(ALuaScriptWithModule_Isolation, FailsToRunScriptOverwritingModuleFunction_InRunFunction) + { + const std::string_view mymathModuleSrc = R"( + local mymath = {} + function mymath.floor1(v) + return math.floor(v) + end + function mymath.floor2(v) + return math.floor(v) + 100 + end + return mymath + )"; + const auto mymathModule = m_logicEngine.createLuaModule(mymathModuleSrc, WithStdMath(), "mymath"); + ASSERT_NE(nullptr, mymathModule); + + LuaConfig config; + config.addDependency("mymath", *mymathModule); + + const auto script1 = m_logicEngine.createLuaScript(R"( + modules("mymath") + function interface(IN,OUT) + end + function run(IN,OUT) + mymath.floor1 = mymath.floor2 + end + )", config); + ASSERT_NE(nullptr, script1); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Modifying module data is not allowed!")); + } + + TEST_F(ALuaScriptWithModule_Isolation, FailsToCompileScriptOverwritingModuleFunction_InInterfaceFunction) + { + const std::string_view mymathModuleSrc = R"( + local mymath = {} + function mymath.floor1(v) + return math.floor(v) + end + function mymath.floor2(v) + return math.floor(v) + 100 + end + return mymath + )"; + const auto mymathModule = m_logicEngine.createLuaModule(mymathModuleSrc, WithStdMath()); + ASSERT_NE(nullptr, mymathModule); + + LuaConfig config; + config.addDependency("mymath", *mymathModule); + + const auto script1 = m_logicEngine.createLuaScript(R"( + modules("mymath") + function interface(IN,OUT) + mymath.floor1 = mymath.floor2 + end + function run(IN,OUT) + end + )", config); + EXPECT_EQ(nullptr, script1); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Modifying module data is not allowed!")); + } + + TEST_F(ALuaScriptWithModule_Isolation, FailsToRunScriptOverwritingModuleData_InRunFunction) + { + const std::string_view mymathModuleSrc = R"( + local mymath = {} + mymath.data = 1 + return mymath + )"; + const auto mymathModule = m_logicEngine.createLuaModule(mymathModuleSrc); + ASSERT_NE(nullptr, mymathModule); + + LuaConfig config; + config.addDependency("mymath", *mymathModule); + + const auto script1 = m_logicEngine.createLuaScript(R"( + modules("mymath") + function interface(IN,OUT) + end + function run(IN,OUT) + mymath.data = 42 + end + )", config); + ASSERT_NE(nullptr, script1); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Modifying module data is not allowed!")); + } + + TEST_F(ALuaScriptWithModule_Isolation, FailsToCompileScriptOverwritingModuleData_InInterfaceFunction) + { + const std::string_view mymathModuleSrc = R"( + local mymath = {} + mymath.data = 1 + return mymath + )"; + const auto mymathModule = m_logicEngine.createLuaModule(mymathModuleSrc); + ASSERT_NE(nullptr, mymathModule); + + LuaConfig config; + config.addDependency("mymath", *mymathModule); + + const auto script1 = m_logicEngine.createLuaScript(R"( + modules("mymath") + function interface(IN,OUT) + mymath.data = 42 + end + function run(IN,OUT) + end + )", config); + EXPECT_EQ(nullptr, script1); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Modifying module data is not allowed!")); + } + + TEST_F(ALuaScriptWithModule_Isolation, ModuleCannotModifyItsDataWhenPassedFromScript) + { + const std::string_view moduleSrc = R"( + local mod = {} + mod.value = 1 + function mod.modifyModule(theModule) + theModule.value = 42 + end + return mod + )"; + + const std::string_view scriptSrc = R"( + modules("mappedMod") + function interface(IN,OUT) + OUT.result = Type:Int32() + end + + function run(IN,OUT) + -- Will modify the module because it's passed as argument by the + -- script to the module + mappedMod.modifyModule(mappedMod) + OUT.result = mappedMod.value + end + )"; + + m_logicEngine.createLuaScript(scriptSrc, createDeps({ { "mappedMod", moduleSrc } })); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Modifying module data is not allowed!")); + } + + TEST_F(ALuaScriptWithModule_Isolation, FailsToRunScriptOverwritingModuleData_WhenDataNested) + { + const std::string_view moduleSrc = R"( + local mod = {} + mod.people = {joe = {age = 20}} + function mod.getJoeAge() + return mod.people.joe.age + end + return mod + )"; + + const std::string_view scriptSrc = R"( + modules("mappedMod") + function interface(IN,OUT) + OUT.resultBeforeMod = Type:Int32() + OUT.resultAfterMod = Type:Int32() + end + + function run(IN,OUT) + OUT.resultBeforeMod = mappedMod.getJoeAge() + -- This will modify the module's copy of joe + mappedMod.people.joe.age = 42 + OUT.resultAfterMod = mappedMod.getJoeAge() + end + )"; + + m_logicEngine.createLuaScript(scriptSrc, createDeps({ { "mappedMod", moduleSrc } })); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Modifying module data is not allowed!")); + } + + TEST_F(ALuaScriptWithModule_Isolation, FailsToRunScriptUsingModuleOverwritingNestedModuleData_InRunFunction) + { + const std::string_view mymathModuleSrc1 = R"( + local mymath = {} + mymath.data = 1 + return mymath + )"; + const auto mymathModule1 = m_logicEngine.createLuaModule(mymathModuleSrc1); + ASSERT_NE(nullptr, mymathModule1); + + const std::string_view mymathModuleSrc2 = R"( + modules("mymath") + local mymathWrap = {} + function mymathWrap.modify() + mymath.data = 2 + end + return mymathWrap + )"; + LuaConfig configMod; + configMod.addDependency("mymath", *mymathModule1); + const auto mymathModule2 = m_logicEngine.createLuaModule(mymathModuleSrc2, configMod); + ASSERT_NE(nullptr, mymathModule2); + + LuaConfig config; + config.addDependency("mymathWrap", *mymathModule2); + const auto script = m_logicEngine.createLuaScript(R"( + modules("mymathWrap") + function interface(IN,OUT) + end + function run(IN,OUT) + mymathWrap.modify() + end + )", config); + ASSERT_NE(nullptr, script); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Modifying module data is not allowed!")); + } + + TEST_F(ALuaScriptWithModule_Isolation, FailsToRunScriptUsingModuleOverwritingNestedModuleData_InInterfaceFunction) + { + const std::string_view mymathModuleSrc1 = R"( + local mymath = {} + mymath.data = 1 + return mymath + )"; + const auto mymathModule1 = m_logicEngine.createLuaModule(mymathModuleSrc1); + ASSERT_NE(nullptr, mymathModule1); + + const std::string_view mymathModuleSrc2 = R"( + modules("mymath") + local mymathWrap = {} + function mymathWrap.modify() + mymath.data = 2 + end + return mymathWrap + )"; + LuaConfig configMod; + configMod.addDependency("mymath", *mymathModule1); + const auto mymathModule2 = m_logicEngine.createLuaModule(mymathModuleSrc2, configMod); + ASSERT_NE(nullptr, mymathModule2); + + LuaConfig config; + config.addDependency("mymathWrap", *mymathModule2); + const auto script = m_logicEngine.createLuaScript(R"( + modules("mymathWrap") + function interface(IN,OUT) + mymathWrap.modify() + end + function run(IN,OUT) + end + )", config); + EXPECT_EQ(nullptr, script); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Modifying module data is not allowed!")); + } + + + TEST_F(ALuaScriptWithModule_Isolation, FailsToRunScriptUsingModuleOverwritingTypeSymbols_InInterfaceFunction) + { + const std::string_view modSrc = R"( + local m = {} + function m.killTypes() + Type = {} + end + return m + )"; + const auto luaModule = m_logicEngine.createLuaModule(modSrc); + ASSERT_NE(nullptr, luaModule); + + LuaConfig config; + config.addDependency("m", *luaModule); + const auto script = m_logicEngine.createLuaScript(R"( + modules("m") + function interface(IN,OUT) + m.killTypes() + end + function run(IN,OUT) + end + )", config); + EXPECT_EQ(nullptr, script); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors().front().message, ::testing::HasSubstr("Special global 'Type' symbol should not be overwritten in modules!")); + } +} diff --git a/client/logic/unittests/api/LuaScriptTest_Runtime.cpp b/client/logic/unittests/api/LuaScriptTest_Runtime.cpp new file mode 100644 index 000000000..bd2add144 --- /dev/null +++ b/client/logic/unittests/api/LuaScriptTest_Runtime.cpp @@ -0,0 +1,2431 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LuaScriptTest_Base.h" + +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" +#include "fmt/format.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" + +#include "ramses-framework-api/RamsesFramework.h" +#include "ramses-client-api/RamsesClient.h" +#include "ramses-client-api/Effect.h" +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/Scene.h" +#include "ramses-client-api/EffectDescription.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-client-api/Node.h" +#include "ramses-client-api/PerspectiveCamera.h" + +#include + +namespace ramses +{ + class ALuaScript_Runtime : public ALuaScript + { + protected: + }; + + TEST_F(ALuaScript_Runtime, ReportsErrorWhenAssigningVectorComponentsIndividually) + { + m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.vec3f = Type:Vec3f() + end + + function run(IN,OUT) + OUT.vec3f[1] = 1.0 + end + )"); + + m_logicEngine.update(); + + EXPECT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Error while writing to 'vec3f'. Can't assign individual components of vector types, must assign the whole vector")); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorIfUndefinedInputIsUsedInRun) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + local undefined = IN.undefined + end + )"); + + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Tried to access undefined struct property 'undefined'")); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorIfUndefinedOutputIsUsedInRun) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + OUT.undefined = 5 + end + )"); + + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Tried to access undefined struct property 'undefined'")); + } + + TEST_F(ALuaScript_Runtime, ReportsSourceNodeOnRuntimeError) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + error("this causes an error") + end + )", WithStdModules({ EStandardModule::Base })); + + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("this causes an error")); + EXPECT_EQ(script, m_logicEngine.getErrors()[0].object); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenTryingToWriteInputValues) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.value = Type:Float() + end + + function run(IN,OUT) + IN.value = 5 + end + )"); + + EXPECT_FALSE(m_logicEngine.update()); + + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("lua: error: Error while writing to 'value'. Writing input values is not allowed, only outputs!")); + EXPECT_EQ(script, m_logicEngine.getErrors()[0].object); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenTryingToAccessPropertiesWithNonStringIndexAtRunTime) + { + const std::vector wrongIndexTypes = {"[1]", "[true]", "[{x=5}]", "[nil]"}; + + std::vector allErrorCases; + for (const auto& errorType : wrongIndexTypes) + { + allErrorCases.emplace_back(LuaTestError{"inputs" + errorType + " = 5", "lua: error: Bad access to property ''! Expected a string but got object of type"}); + allErrorCases.emplace_back(LuaTestError{"outputs" + errorType + " = 5", "lua: error: Bad access to property ''! Expected a string but got object of type"}); + } + + for (const auto& singleCase : allErrorCases) + { + auto script = m_logicEngine.createLuaScript( + "function interface(inputs,outputs)\n" + "end\n" + "function run(inputs,outputs)\n" + + singleCase.errorCode + + "\n" + "end\n"); + + ASSERT_NE(nullptr, script); + m_logicEngine.update(); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr(singleCase.expectedErrorMessage)); + m_logicEngine.destroy(*script); + } + } + + TEST_F(ALuaScript_Runtime, SetsValueOfTopLevelInputSuccessfully_WhenTemplateMatchesDeclaredInputType) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScriptWithInputs); + auto inputs = script->getInputs(); + + auto speedInt32 = inputs->getChild("speed"); + auto tempFloat = inputs->getChild("temp"); + auto nameString = inputs->getChild("name"); + auto enabledBool = inputs->getChild("enabled"); + auto vec_2f = inputs->getChild("vec2f"); + auto vec_3f = inputs->getChild("vec3f"); + auto vec_4f = inputs->getChild("vec4f"); + auto vec_2i = inputs->getChild("vec2i"); + auto vec_3i = inputs->getChild("vec3i"); + auto vec_4i = inputs->getChild("vec4i"); + + EXPECT_TRUE(speedInt32->set(4711)); + EXPECT_EQ(4711, *speedInt32->get()); + EXPECT_TRUE(tempFloat->set(5.5f)); + EXPECT_FLOAT_EQ(5.5f, *tempFloat->get()); + EXPECT_TRUE(nameString->set("name")); + EXPECT_EQ("name", *nameString->get()); + EXPECT_TRUE(enabledBool->set(true)); + EXPECT_EQ(true, *enabledBool->get()); + + vec2f testvalVec2f{ 1.1f, 1.2f }; + vec3f testvalVec3f{ 2.1f, 2.2f, 2.3f }; + vec4f testvalVec4f{ 3.1f, 3.2f, 3.3f, 3.4f }; + vec2i testvalVec2i{ 1, 2 }; + vec3i testvalVec3i{ 3, 4, 5 }; + vec4i testvalVec4i{ 6, 7, 8, 9 }; + EXPECT_TRUE(vec_2f->set(testvalVec2f)); + EXPECT_TRUE(vec_3f->set(testvalVec3f)); + EXPECT_TRUE(vec_4f->set(testvalVec4f)); + EXPECT_TRUE(vec_2i->set(testvalVec2i)); + EXPECT_TRUE(vec_3i->set(testvalVec3i)); + EXPECT_TRUE(vec_4i->set(testvalVec4i)); + EXPECT_EQ(testvalVec2f, *vec_2f->get()); + EXPECT_EQ(testvalVec3f, *vec_3f->get()); + EXPECT_EQ(testvalVec4f, *vec_4f->get()); + EXPECT_EQ(testvalVec2i, *vec_2i->get()); + EXPECT_EQ(testvalVec3i, *vec_3i->get()); + EXPECT_EQ(testvalVec4i, *vec_4i->get()); + } + + TEST_F(ALuaScript_Runtime, ProvidesCalculatedValueAfterExecution) + { + auto* script = m_logicEngine.createLuaScript(R"( + + function interface(IN,OUT) + IN.a = Type:Int32() + IN.b = Type:Int32() + OUT.result = Type:Int32() + end + + function run(IN,OUT) + OUT.result = IN.a + IN.b + end + )"); + + auto inputs = script->getInputs(); + auto inputA = inputs->getChild("a"); + auto inputB = inputs->getChild("b"); + + auto outputs = script->getOutputs(); + auto result = outputs->getChild("result"); + + inputA->set(3); + inputB->set(4); + + m_logicEngine.update(); + + EXPECT_EQ(7, *result->get()); + } + + TEST_F(ALuaScript_Runtime, ReadsDataFromVec234Inputs) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.vec2f = Type:Vec2f() + IN.vec3f = Type:Vec3f() + IN.vec4f = Type:Vec4f() + IN.vec2i = Type:Vec2i() + IN.vec3i = Type:Vec3i() + IN.vec4i = Type:Vec4i() + OUT.sumOfAllFloats = Type:Float() + OUT.sumOfAllInts = Type:Int32() + end + + function run(IN,OUT) + OUT.sumOfAllFloats = + IN.vec2f[1] + IN.vec2f[2] + + IN.vec3f[1] + IN.vec3f[2] + IN.vec3f[3] + + IN.vec4f[1] + IN.vec4f[2] + IN.vec4f[3] + IN.vec4f[4] + OUT.sumOfAllInts = + IN.vec2i[1] + IN.vec2i[2] + + IN.vec3i[1] + IN.vec3i[2] + IN.vec3i[3] + + IN.vec4i[1] + IN.vec4i[2] + IN.vec4i[3] + IN.vec4i[4] + end + )"); + auto inputs = script->getInputs(); + auto outputs = script->getOutputs(); + + inputs->getChild("vec2f")->set({ 1.1f, 1.2f }); + inputs->getChild("vec3f")->set({ 2.1f, 2.2f, 2.3f }); + inputs->getChild("vec4f")->set({ 3.1f, 3.2f, 3.3f, 3.4f }); + inputs->getChild("vec2i")->set({ 1, 2 }); + inputs->getChild("vec3i")->set({ 3, 4, 5 }); + inputs->getChild("vec4i")->set({ 6, 7, 8, 9 }); + + ASSERT_TRUE(m_logicEngine.update()); + + EXPECT_FLOAT_EQ(21.9f, *outputs->getChild("sumOfAllFloats")->get()); + EXPECT_EQ(45, *outputs->getChild("sumOfAllInts")->get()); + } + + TEST_F(ALuaScript_Runtime, WritesValuesToVectorTypeOutputs) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.vec2f = Type:Vec2f() + OUT.vec3f = Type:Vec3f() + OUT.vec4f = Type:Vec4f() + OUT.vec2i = Type:Vec2i() + OUT.vec3i = Type:Vec3i() + OUT.vec4i = Type:Vec4i() + OUT.nested = { + vec = Type:Vec3i(), + float = Type:Float() + } + end + + function run(IN,OUT) + OUT.vec2f = {0.1, 0.2} + OUT.vec3f = {1.1, 1.2, 1.3} + OUT.vec4f = {2.1, 2.2, 2.3, 2.4} + OUT.vec2i = {1, 2} + OUT.vec3i = {3, 4, 5} + OUT.vec4i = {6, 7, 8, 9} + + OUT.nested = + { + vec = {11, 12, 13}, + float = 15.5 + } + end + )"); + + EXPECT_TRUE(m_logicEngine.update()); + + auto outputs = script->getOutputs(); + + EXPECT_EQ(*outputs->getChild("vec2f")->get(), vec2f(0.1f, 0.2f)); + EXPECT_EQ(*outputs->getChild("vec3f")->get(), vec3f(1.1f, 1.2f, 1.3f)); + EXPECT_EQ(*outputs->getChild("vec4f")->get(), vec4f(2.1f, 2.2f, 2.3f, 2.4f)); + + EXPECT_EQ(*outputs->getChild("vec2i")->get(), vec2i(1, 2)); + EXPECT_EQ(*outputs->getChild("vec3i")->get(), vec3i(3, 4, 5)); + EXPECT_EQ(*outputs->getChild("vec4i")->get(), vec4i(6, 7, 8, 9)); + + EXPECT_EQ(*outputs->getChild("nested")->getChild("vec")->get(), vec3i(11, 12, 13)); + EXPECT_FLOAT_EQ(*outputs->getChild("nested")->getChild("float")->get(), 15.5f); + } + + TEST_F(ALuaScript_Runtime, PermitsAssigningOfVector_FromTable_WithNilsAtTheEnd) + { + // Lua+sol seem to not iterate over nil entries when creating a table + // Still, we test the behavior explicitly + const std::vector allCases = + { + "OUT.vec2f = {1, 2, nil} -- single nil", + "OUT.vec3f = {1, 2, 3, nil}", + "OUT.vec4f = {1, 2, 3, 4, nil}", + "OUT.vec2i = {1, 2, nil}", + "OUT.vec3i = {1, 2, 3, nil}", + "OUT.vec4i = {1, 2, 3, 4, nil}", + "OUT.vec2f = {1, 2, nil, nil} -- two nils", + }; + + for (const auto& aCase : allCases) + { + auto scriptSource = std::string(R"( + function interface(IN,OUT) + OUT.vec2f = Type:Vec2f() + OUT.vec3f = Type:Vec3f() + OUT.vec4f = Type:Vec4f() + OUT.vec2i = Type:Vec2i() + OUT.vec3i = Type:Vec3i() + OUT.vec4i = Type:Vec4i() + end + + function run(IN,OUT) + )"); + scriptSource += aCase; + scriptSource += "\nend\n"; + + auto* script = m_logicEngine.createLuaScript(scriptSource); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + EXPECT_TRUE(m_logicEngine.destroy(*script)); + } + } + + TEST_F(ALuaScript_Runtime, PermitsAssigningOfVector_FromTable_WithKeyValuePairs) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.vec2f = Type:Vec2f() + OUT.vec3i = Type:Vec3i() + end + + function run(IN,OUT) + OUT.vec2f = {[1] = 0.1, [2] = 0.2} + OUT.vec3i = {[3] = 13, [2] = 12, [1] = 11} -- shuffled + end + )"); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.update()); + + auto outputs = script->getOutputs(); + + EXPECT_EQ(*outputs->getChild("vec2f")->get(), vec2f(0.1f, 0.2f)); + + EXPECT_EQ(*outputs->getChild("vec3i")->get(), vec3i(11, 12, 13)); + } + + TEST_F(ALuaScript_Runtime, UsesNestedInputsToProduceResult) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.data = { + a = Type:Int32(), + b = Type:Int32() + } + OUT.result = Type:Int32() + end + function run(IN,OUT) + OUT.result = IN.data.a + IN.data.b + end + )"); + + auto inputs = script->getInputs(); + auto inputA = inputs->getChild("data")->getChild("a"); + auto inputB = inputs->getChild("data")->getChild("b"); + + auto outputs = script->getOutputs(); + auto result = outputs->getChild("result"); + + inputA->set(3); + inputB->set(4); + + m_logicEngine.update(); + m_logicEngine.update(); + + EXPECT_EQ(7, *result->get()); + } + + + TEST_F(ALuaScript_Runtime, StoresDataToNestedOutputs_AsWholeStruct) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.data = Type:Int32() + OUT.struct = { + field1 = Type:Int32(), + field2 = Type:Int32() + } + end + function run(IN,OUT) + OUT.struct = { + field1 = IN.data + IN.data, + field2 = IN.data * IN.data + } + end + )"); + + auto inputs = script->getInputs(); + auto input = inputs->getChild("data"); + + auto outputs = script->getOutputs(); + auto field1 = outputs->getChild("struct")->getChild("field1"); + auto field2 = outputs->getChild("struct")->getChild("field2"); + + input->set(5); + + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_EQ(10, *field1->get()); + EXPECT_EQ(25, *field2->get()); + } + + TEST_F(ALuaScript_Runtime, StoresDataToNestedOutputs_Individually) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.data = Type:Int32() + OUT.data = { + field1 = Type:Int32(), + field2 = Type:Int32() + } + end + function run(IN,OUT) + OUT.data.field1 = IN.data + IN.data + OUT.data.field2 = IN.data * IN.data + end + )"); + + auto inputs = script->getInputs(); + auto input = inputs->getChild("data"); + + auto outputs = script->getOutputs(); + auto field1 = outputs->getChild("data")->getChild("field1"); + auto field2 = outputs->getChild("data")->getChild("field2"); + + input->set(5); + + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_EQ(10, *field1->get()); + EXPECT_EQ(25, *field2->get()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAssigningNestedProperties_Underspecified) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.data = { + field1 = Type:Int32(), + field2 = Type:Int32() + } + end + function run(IN,OUT) + OUT.data = { + field1 = 5 + } + end + )"); + + auto outputs = script->getOutputs(); + auto field1 = outputs->getChild("data")->getChild("field1"); + auto field2 = outputs->getChild("data")->getChild("field2"); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("lua: error: Error while assigning struct 'data', expected a value for property 'field2' but found none!")); + + EXPECT_EQ(5, *field1->get()); + EXPECT_EQ(0, *field2->get()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAssigningNestedProperties_Overspecified) + { + m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.data = { + field1 = Type:Int32(), + field2 = Type:Int32() + } + end + function run(IN,OUT) + OUT.data = { + field1 = 5, + field2 = 5, + not_specified = 5 + } + end + )"); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Unexpected property 'not_specified' while assigning values to struct 'data'!")); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAssigningNestedProperties_WhenFieldHasWrongType) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.data = { + field1 = Type:Int32(), + field2 = Type:Int32() + } + OUT.field2 = Type:Int32() + end + function run(IN,OUT) + OUT.field2 = "this is no integer" + OUT.data = { + field1 = 5, + field2 = "this is no integer" + } + end + )"); + + auto outputs = script->getOutputs(); + auto field1 = outputs->getChild("data")->getChild("field1"); + auto field2 = outputs->getChild("data")->getChild("field2"); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Assigning string to 'Int32' output 'field2'!")); + + EXPECT_EQ(0, *field1->get()); + EXPECT_EQ(0, *field2->get()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAssigningNestedProperties_WhenNestedSubStructDoesNotMatch_AndInterruptsAssignment) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.data = { + a_fieldBefore = Type:Int32(), + b_nested = { + field = Type:Int32() + }, + c_fieldAfter = Type:Int32(), + } + end + function run(IN,OUT) + OUT.data = { + a_fieldBefore = 4, + b_nested = { + wrong_field = 5 + }, + c_fieldAfter = 6, + } + end + )"); + + auto outputs = script->getOutputs(); + auto fieldBefore = outputs->getChild("data")->getChild("a_fieldBefore"); + auto nestedfield = outputs->getChild("data")->getChild("b_nested")->getChild("field"); + auto fieldAfter = outputs->getChild("data")->getChild("c_fieldAfter"); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Unexpected property 'wrong_field' while assigning values to struct 'b_nested'")); + + // Assigned, because it was ordered alphabetically before the erroneous field + EXPECT_EQ(4, *fieldBefore->get()); + // Had error -> keeps old value + EXPECT_EQ(0, *nestedfield->get()); + // Ordered alphabetically after error field -> also keeps old value + EXPECT_EQ(0, *fieldAfter->get()); + } + + TEST_F(ALuaScript_Runtime, AssignsValuesToArrays) + { + std::string_view scriptWithArrays = R"( + function interface(IN,OUT) + IN.array_int = Type:Array(2, Type:Int32()) + IN.array_int64 = Type:Array(2, Type:Int64()) + IN.array_float = Type:Array(3, Type:Float()) + OUT.array_int = Type:Array(2, Type:Int32()) + OUT.array_int64 = Type:Array(2, Type:Int64()) + OUT.array_float = Type:Array(3, Type:Float()) + end + + function run(IN,OUT) + OUT.array_int = IN.array_int + OUT.array_int[2] = 5 + OUT.array_int64 = IN.array_int64 + OUT.array_int64[2] = 5 + OUT.array_float = IN.array_float + OUT.array_float[1] = 1.5 + end + )"; + + auto* script = m_logicEngine.createLuaScript(scriptWithArrays); + + auto inputs = script->getInputs(); + auto in_array_int = inputs->getChild("array_int"); + auto in_array_int64 = inputs->getChild("array_int64"); + auto in_array_float = inputs->getChild("array_float"); + in_array_int->getChild(0)->set(1); + in_array_int->getChild(1)->set(2); + in_array_int64->getChild(0)->set(3); + in_array_int64->getChild(1)->set(4); + in_array_float->getChild(0)->set(0.1f); + in_array_float->getChild(1)->set(0.2f); + in_array_float->getChild(2)->set(0.3f); + + EXPECT_TRUE(m_logicEngine.update()); + + auto outputs = script->getOutputs(); + auto out_array_int = outputs->getChild("array_int"); + auto out_array_int64 = outputs->getChild("array_int64"); + auto out_array_float = outputs->getChild("array_float"); + + EXPECT_EQ(1, *out_array_int->getChild(0)->get()); + EXPECT_EQ(5, *out_array_int->getChild(1)->get()); + + EXPECT_EQ(3, *out_array_int64->getChild(0)->get()); + EXPECT_EQ(5, *out_array_int64->getChild(1)->get()); + + EXPECT_FLOAT_EQ(1.5f, *out_array_float->getChild(0)->get()); + EXPECT_FLOAT_EQ(0.2f, *out_array_float->getChild(1)->get()); + EXPECT_FLOAT_EQ(0.3f, *out_array_float->getChild(2)->get()); + } + + TEST_F(ALuaScript_Runtime, AssignsNestedTableToMultidimensionalArrays) + { + std::string_view scriptWithMultiDimArrays = R"( + function interface(IN,OUT) + OUT.array2d = Type:Array(2, Type:Array(2, Type:Int32())) + end + + function run(IN,OUT) + OUT.array2d = { + {1,2}, {3, 4}, + } + end + )"; + + auto* script = m_logicEngine.createLuaScript(scriptWithMultiDimArrays); + + EXPECT_TRUE(m_logicEngine.update()); + + auto array2DOut = script->getOutputs()->getChild("array2d"); + + int32_t value = 1; + for (int32_t i = 0; i < 2; ++i) + { + for (int32_t j = 0; j < 2; ++j) + { + EXPECT_EQ(value, *array2DOut->getChild(i)->getChild(j)->get()); + ++value; + } + } + } + + TEST_F(ALuaScript_Runtime, AssignsInputValuesToMultidimensionalArrays) + { + std::string_view scriptWithMultiDimArrays = R"( + function interface(IN,OUT) + local arrayType = Type:Array(2, Type:Array(2, Type:Array(2, Type:Int32()))) + IN.array3d = arrayType + OUT.array3d = arrayType + end + + function run(IN,OUT) + OUT.array3d = IN.array3d + end + )"; + + auto* script = m_logicEngine.createLuaScript(scriptWithMultiDimArrays); + + auto array3DIn = script->getInputs()->getChild("array3d"); + + for (int32_t i = 0; i < 2; ++i) + { + for (int32_t j = 0; j < 2; ++j) + { + for (int32_t k = 0; k < 2; ++k) + { + array3DIn->getChild(i)->getChild(j)->getChild(k)->set(i * j * k); + } + } + } + + EXPECT_TRUE(m_logicEngine.update()); + + auto array3DOut = script->getOutputs()->getChild("array3d"); + + for (int32_t i = 0; i < 2; ++i) + { + for (int32_t j = 0; j < 2; ++j) + { + for (int32_t k = 0; k < 2; ++k) + { + EXPECT_EQ(i * j * k, *array3DOut->getChild(i)->getChild(j)->getChild(k)->get()); + } + } + } + } + + TEST_F(ALuaScript_Runtime, AssignsDataToMultidimensionalArraysWithStructs_FromExampleInDocs) + { + std::string_view scriptSrc = R"( + function interface(IN, OUT) + local coalaStruct = Type:Struct({name = Type:String()}) + -- 100 x 100 array (2 dimensions), element type is a struct + OUT.coala_army = Type:Array(100, Type:Array(100, coalaStruct)) + end + + function run(IN, OUT) + for i,row in rl_ipairs(OUT.coala_army) do + for j,coala in rl_ipairs(row) do + coala.name = "soldier " .. tostring(i) .. "-" .. tostring(j) + end + end + end + )"; + + auto* script = m_logicEngine.createLuaScript(scriptSrc, WithStdModules({EStandardModule::Base})); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ("soldier 8-7", *script->getOutputs()->getChild("coala_army")->getChild(7)->getChild(6)->getChild("name")->get()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAccessingArrayWithNonIntegerIndex) + { + const std::string_view scriptTemplate = (R"( + function interface(IN,OUT) + IN.array = Type:Array(2, Type:Int32()) + OUT.array = Type:Array(2, Type:Int32()) + end + function run(IN,OUT) + {} + end + )"); + + const std::vector invalidStatements + { + "IN.array.name = 5", + "OUT.array.name = 5", + "IN.array[true] = 5", + "OUT.array[true] = 5", + "IN.array[{x=5}] = 5", + "OUT.array[{x=5}] = 5", + "IN.array[nil] = 5", + "OUT.array[nil] = 5", + "IN.array[IN] = 5", + "OUT.array[IN] = 5", + }; + + for (const auto& invalidStatement : invalidStatements) + { + auto script = m_logicEngine.createLuaScript(fmt::format(scriptTemplate, invalidStatement)); + + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.update()); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Bad access to property 'array'! Error while extracting integer: expected a number, received")); + m_logicEngine.destroy(*script); + } + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAccessingArrayOutOfRange) + { + const std::string_view scriptTemplate = (R"( + function interface(IN,OUT) + IN.array = Type:Array(2, Type:Int32()) + OUT.array = Type:Array(2, Type:Int32()) + end + function run(IN,OUT) + {} + end + )"); + + std::vector allErrorCases; + for (auto idx : std::initializer_list{ -1, 0, 3}) + { + std::string errorTemplate = + (idx < 0) ? + "Bad access to property 'array'! Error while extracting integer: expected non-negative number, received '-1'" : + "Index out of range! Expected 0 < index <= 2 but received index == {}"; + + for (const auto& prop : std::vector{ "IN", "OUT" }) + { + allErrorCases.emplace_back(LuaTestError{ + fmt::format("{}.array[{}] = 5", prop, idx), + fmt::format(errorTemplate, idx) + }); + } + } + + for (const auto& singleCase : allErrorCases) + { + auto script = m_logicEngine.createLuaScript(fmt::format(scriptTemplate, singleCase.errorCode)); + + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.update()); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr(singleCase.expectedErrorMessage)); + m_logicEngine.destroy(*script); + } + } + + TEST_F(ALuaScript_Runtime, AssignArrayValuesFromLuaTable) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.int_array = Type:Array(2, Type:Int32()) + OUT.int64_array = Type:Array(2, Type:Int64()) + OUT.float_array = Type:Array(2, Type:Float()) + OUT.vec2i_array = Type:Array(2, Type:Vec2i()) + OUT.vec3f_array = Type:Array(2, Type:Vec3f()) + end + function run(IN,OUT) + OUT.int_array = {1, 2} + OUT.int64_array = {3, 4} + OUT.float_array = {0.1, 0.2} + OUT.vec2i_array = {{11, 12}, {21, 22}} + OUT.vec3f_array = {{0.11, 0.12, 0.13}, {0.21, 0.22, 0.23}} + end + )"); + + ASSERT_NE(nullptr, script); + + EXPECT_TRUE(m_logicEngine.update()); + + const auto int_array = script->getOutputs()->getChild("int_array"); + const auto int64_array = script->getOutputs()->getChild("int64_array"); + const auto float_array = script->getOutputs()->getChild("float_array"); + const auto vec2i_array = script->getOutputs()->getChild("vec2i_array"); + const auto vec3f_array = script->getOutputs()->getChild("vec3f_array"); + + EXPECT_EQ(1, *int_array->getChild(0)->get()); + EXPECT_EQ(2, *int_array->getChild(1)->get()); + EXPECT_EQ(3, *int64_array->getChild(0)->get()); + EXPECT_EQ(4, *int64_array->getChild(1)->get()); + EXPECT_FLOAT_EQ(0.1f, *float_array->getChild(0)->get()); + EXPECT_FLOAT_EQ(0.2f, *float_array->getChild(1)->get()); + EXPECT_EQ(*vec2i_array->getChild(0)->get(), vec2i(11, 12)); + EXPECT_EQ(*vec2i_array->getChild(1)->get(), vec2i(21, 22)); + EXPECT_EQ(*vec3f_array->getChild(0)->get(), vec3f(0.11f, 0.12f, 0.13f)); + EXPECT_EQ(*vec3f_array->getChild(1)->get(), vec3f(0.21f, 0.22f, 0.23f)); + } + + TEST_F(ALuaScript_Runtime, AssignArrayValuesFromLuaTable_WithExplicitKeys) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.int_array = Type:Array(3, Type:Int32()) + end + function run(IN,OUT) + OUT.int_array = {[1] = 11, [2] = 12, [3] = 13} + end + )"); + + ASSERT_NE(nullptr, script); + + EXPECT_TRUE(m_logicEngine.update()); + + auto int_array = script->getOutputs()->getChild("int_array"); + + EXPECT_EQ(11, *int_array->getChild(0)->get()); + EXPECT_EQ(12, *int_array->getChild(1)->get()); + EXPECT_EQ(13, *int_array->getChild(2)->get()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAssigningArrayWithFewerElementsThanRequired_UsingExplicitIndices) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.int_array = Type:Array(3, Type:Int32()) + end + function run(IN,OUT) + OUT.int_array = {[1] = 11, [2] = 12} + end + )"); + + ASSERT_NE(nullptr, script); + + EXPECT_FALSE(m_logicEngine.update()); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Error during assignment of array property 'int_array'! Expected a value at index 3")); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAssigningArrayFromLuaTableWithCorrectSizeButWrongIndices) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.int_array = Type:Array(3, Type:Int32()) + end + function run(IN,OUT) + -- 3 values, but use [1, 3, 4] instead of [1, 2, 3] + OUT.int_array = {[1] = 11, [3] = 13, [4] = 14} + end + )"); + + ASSERT_NE(nullptr, script); + + EXPECT_FALSE(m_logicEngine.update()); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Error during assignment of array property 'int_array'! Expected a value at index 2")); + } + + TEST_F(ALuaScript_Runtime, AssignsMultidimensionalArrays) + { + const std::string_view scriptSrc = (R"( + function interface(IN,OUT) + OUT.array2d_int = Type:Array(2, Type:Array(2, Type:Int32())) + OUT.array3d_float = Type:Array(1, Type:Array(1, Type:Array(1, Type:Float()))) + end + function run(IN,OUT) + OUT.array2d_int = {{1, 2}, {3, 4}} + OUT.array3d_float = {{{1.4}}} + end + )"); + + auto script = m_logicEngine.createLuaScript(scriptSrc); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.update()); + + auto outArray2d = script->getOutputs()->getChild("array2d_int"); + EXPECT_EQ(1, *outArray2d->getChild(0)->getChild(0)->get()); + EXPECT_EQ(2, *outArray2d->getChild(0)->getChild(1)->get()); + EXPECT_EQ(3, *outArray2d->getChild(1)->getChild(0)->get()); + EXPECT_EQ(4, *outArray2d->getChild(1)->getChild(1)->get()); + auto outArray3d = script->getOutputs()->getChild("array3d_float"); + EXPECT_FLOAT_EQ(1.4f, *outArray3d->getChild(0)->getChild(0)->getChild(0)->get()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAssigningArraysWrongValues) + { + const std::string_view scriptTemplate = (R"( + function interface(IN,OUT) + OUT.array_int = Type:Array(2, Type:Int32()) + OUT.array_int64 = Type:Array(2, Type:Int64()) + OUT.array_string = Type:Array(2, Type:String()) + OUT.array_vec2f = Type:Array(2, Type:Vec2f()) + end + function run(IN,OUT) + {} + end + )"); + + // This is a subset of all possible permutations, but should cover most types and cases + const std::vector allErrorCases = { + {"OUT.array_int = {}", "Error during assignment of array property 'array_int'! Expected a value at index 1"}, + {"OUT.array_int = {1}", "Error during assignment of array property 'array_int'! Expected a value at index 2"}, + {"OUT.array_int = {1, 2, 3}", "Element size mismatch when assigning array property 'array_int'! Expected array size: 2"}, + {"OUT.array_int = {1, 2.2}", "Error while extracting integer: implicit rounding (fractional part '0.20000000000000018' is not negligible)"}, + {"OUT.array_int = {1, true}", "Assigning bool to 'Int32' output ''"}, + {"OUT.array_int = {nil, 1, 3}", "Error during assignment of array property 'array_int'! Expected a value at index 1"}, + {"OUT.array_int = {1, nil, 3}", "Error during assignment of array property 'array_int'! Expected a value at index 2"}, + {"OUT.array_int64 = {}", "Error during assignment of array property 'array_int64'! Expected a value at index 1"}, + {"OUT.array_int64 = {1}", "Error during assignment of array property 'array_int64'! Expected a value at index 2"}, + {"OUT.array_int64 = {1, 2, 3}", "Element size mismatch when assigning array property 'array_int64'! Expected array size: 2"}, + {"OUT.array_int64 = {1, 2.2}", "Error while extracting integer: implicit rounding (fractional part '0.20000000000000018' is not negligible)"}, + {"OUT.array_int64 = {1, true}", "Assigning bool to 'Int64' output ''"}, + {"OUT.array_int64 = {nil, 1, 3}", "Error during assignment of array property 'array_int64'! Expected a value at index 1"}, + {"OUT.array_int64 = {1, nil, 3}", "Error during assignment of array property 'array_int64'! Expected a value at index 2"}, + // TODO Violin Improve error messages to contain info which array field failed to be assigned + // currently we report empty string which is the name of array elements) - not easy to understand by TA + {"OUT.array_string = {'somestring', 2}", "Assigning number to 'String' output ''"}, + {"OUT.array_string = {'somestring', {}}", "Assigning table to 'String' output ''"}, + {"OUT.array_string = {'somestring', OUT.array_int}", "Can't assign property 'array_int' (type Array) to property '' (type String)"}, + {"OUT.array_vec2f = {1, 2}", "Error while assigning output Vec2 property ''. Expected a Lua table with 2 entries but got object of type number instead!"}, + {"OUT.array_vec2f = {{1, 2}, {5}}", "Error while assigning output Vec2 property ''. Error while extracting array: expected 2 array components in table but got 1 instead!"}, + {"OUT.array_vec2f = {{1, 2}, {}}", "Error while assigning output Vec2 property ''. Error while extracting array: expected 2 array components in table but got 0 instead!"}, + {"OUT.array_int = OUT", "Can't assign property '' (type Struct) to property 'array_int' (type Array)"}, + {"OUT.array_int = IN", "Can't assign property '' (type Struct) to property 'array_int' (type Array)"}, + }; + + for (const auto& singleCase : allErrorCases) + { + auto script = m_logicEngine.createLuaScript(fmt::format(scriptTemplate, singleCase.errorCode)); + + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.update()); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr(singleCase.expectedErrorMessage)); + m_logicEngine.destroy(*script); + } + } + + TEST_F(ALuaScript_Runtime, AssignsValuesArraysInVariousLuaSyntaxStyles) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.array = Type:Array(3, Type:Vec2i()) + OUT.array = Type:Array(3, Type:Vec2i()) + end + function run(IN,OUT) + -- assign from "everything" towards "just one value" to cover as many cases as possible + OUT.array = IN.array + OUT.array[2] = IN.array[2] + OUT.array[3] = {5, 6} + end + )"); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(script->getInputs()->getChild("array")->getChild(0)->set({ 1, 2 })); + EXPECT_TRUE(script->getInputs()->getChild("array")->getChild(1)->set({ 3, 4 })); + EXPECT_TRUE(script->getInputs()->getChild("array")->getChild(2)->set({ 5, 6 })); + ASSERT_TRUE(m_logicEngine.update()); + EXPECT_EQ(*script->getOutputs()->getChild("array")->getChild(0)->get(), vec2i(1, 2)); + EXPECT_EQ(*script->getOutputs()->getChild("array")->getChild(1)->get(), vec2i(3, 4)); + EXPECT_EQ(*script->getOutputs()->getChild("array")->getChild(2)->get(), vec2i(5, 6)); + } + + TEST_F(ALuaScript_Runtime, AssignsValuesArraysInVariousLuaSyntaxStyles_InNestedStruct) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.struct = { + array1 = Type:Array(1, Type:Vec2f()), + array2 = Type:Array(2, Type:Vec3f()) + } + OUT.struct = { + array1 = Type:Array(1, Type:Vec2f()), + array2 = Type:Array(2, Type:Vec3f()) + } + end + function run(IN,OUT) + -- assign from "everything" towards "just one value" to cover as many cases as possible + OUT.struct = IN.struct + OUT.struct.array1 = IN.struct.array1 + OUT.struct.array2[1] = {1.1, 1.2, 1.3} + OUT.struct.array2[2] = IN.struct.array2[2] + end + )"); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(script->getInputs()->getChild("struct")->getChild("array1")->getChild(0)->set({ 0.1f, 0.2f })); + EXPECT_TRUE(script->getInputs()->getChild("struct")->getChild("array2")->getChild(0)->set({ 1.1f, 1.2f, 1.3f })); + EXPECT_TRUE(script->getInputs()->getChild("struct")->getChild("array2")->getChild(1)->set({ 2.1f, 2.2f, 2.3f })); + ASSERT_TRUE(m_logicEngine.update()); + EXPECT_EQ(*script->getOutputs()->getChild("struct")->getChild("array1")->getChild(0)->get(), vec2f(0.1f, 0.2f)); + EXPECT_EQ(*script->getOutputs()->getChild("struct")->getChild("array2")->getChild(0)->get(), vec3f(1.1f, 1.2f, 1.3f)); + EXPECT_EQ(*script->getOutputs()->getChild("struct")->getChild("array2")->getChild(1)->get(), vec3f(2.1f, 2.2f, 2.3f)); + } + + TEST_F(ALuaScript_Runtime, AllowsAssigningArraysFromTableWithNilAtTheEnd) + { + const std::string_view scriptTemplate = (R"( + function interface(IN,OUT) + OUT.array_2ints = Type:Array(2, Type:Int32()) + OUT.array_3ints = Type:Array(3, Type:Int32()) + OUT.array_4ints = Type:Array(4, Type:Int32()) + OUT.array_vec2i = Type:Array(1, Type:Vec2i()) + end + + function run(IN,OUT) + {} + end + )"); + + // Lua+sol seem to not iterate over nil entries when creating a table + // Still, we test the behavior explicitly + const std::vector allCases = + { + "OUT.array_2ints = {1, 2, nil} -- single nil", + "OUT.array_2ints = {1, 2, nil, nil} -- two nils", + "OUT.array_3ints = {1, 2, 3, nil}", + "OUT.array_4ints = {1, 2, 3, 4, nil}", + "OUT.array_vec2i = {{1, 2}, nil}", + }; + + for (const auto& aCase : allCases) + { + auto* script = m_logicEngine.createLuaScript(fmt::format(scriptTemplate, aCase)); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + EXPECT_TRUE(m_logicEngine.destroy(*script)); + } + } + + TEST_F(ALuaScript_Runtime, ReportsErrorWhenAssigningArraysWithMismatchedSizes) + { + const std::string_view scriptTemplate = (R"( + function interface(IN,OUT) + IN.array_float2 = Type:Array(2, Type:Float()) + IN.array_float4 = Type:Array(4, Type:Float()) + IN.array_vec3f = Type:Array(1, Type:Vec3f()) + OUT.array_float3 = Type:Array(3, Type:Float()) + end + + function run(IN,OUT) + {} + end + )"); + + const std::vector allCases = + { + {"OUT.array_float3 = IN.array_float2", "Can't assign property 'array_float2' (#fields=2) to property 'array_float3' (#fields=3)"}, + {"OUT.array_float3 = IN.array_float4", "Can't assign property 'array_float4' (#fields=4) to property 'array_float3' (#fields=3)!"}, + {"OUT.array_float3 = IN.array_vec3f", "Can't assign property 'array_vec3f' (#fields=1) to property 'array_float3' (#fields=3)"}, + {"OUT.array_float3 = {0.1, 0.2}", "Error during assignment of array property 'array_float3'! Expected a value at index 3"}, + {"OUT.array_float3 = {0.1, 0.2, 0.3, 0.4}", "Element size mismatch when assigning array property 'array_float3'! Expected array size: 3"}, + {"OUT.array_float3 = {}", "Error during assignment of array property 'array_float3'! Expected a value at index 1"}, + }; + + for (const auto& aCase : allCases) + { + auto* script = m_logicEngine.createLuaScript(fmt::format(scriptTemplate, aCase.errorCode)); + + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.update()); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr(aCase.expectedErrorMessage)); + EXPECT_TRUE(m_logicEngine.destroy(*script)); + } + } + + TEST_F(ALuaScript_Runtime, ReportsErrorWhenAssigningUserdataArraysWithMismatchedTypes) + { + const std::string_view scriptTemplate = (R"( + function interface(IN,OUT) + IN.array_float = Type:Array(2, Type:Float()) + IN.array_vec2f = Type:Array(2, Type:Vec2f()) + IN.array_vec2i = Type:Array(2, Type:Vec2i()) + OUT.array_int = Type:Array(2, Type:Int32()) + OUT.array_int64 = Type:Array(2, Type:Int64()) + end + + function run(IN,OUT) + {} + end + )"); + + const std::vector allCases = + { + {"OUT.array_int = IN.array_float", "Can't assign property '' (type Float) to property '' (type Int32)!"}, + {"OUT.array_int = IN.array_vec2f", "Can't assign property '' (type Vec2f) to property '' (type Int32)!"}, + {"OUT.array_int = IN.array_vec2i", "Can't assign property '' (type Vec2i) to property '' (type Int32)!"}, + {"OUT.array_int64 = IN.array_float", "Can't assign property '' (type Float) to property '' (type Int64)!"}, + {"OUT.array_int64 = IN.array_vec2f", "Can't assign property '' (type Vec2f) to property '' (type Int64)!"}, + {"OUT.array_int64 = IN.array_vec2i", "Can't assign property '' (type Vec2i) to property '' (type Int64)!"}, + }; + + for (const auto& aCase : allCases) + { + auto* script = m_logicEngine.createLuaScript(fmt::format(scriptTemplate, aCase.errorCode)); + + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.update()); + + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr(aCase.expectedErrorMessage)); + EXPECT_TRUE(m_logicEngine.destroy(*script)); + } + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenImplicitlyRoundingNumbers) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.float1 = Type:Float() + IN.float2 = Type:Float() + OUT.int = Type:Int32() + OUT.int64 = Type:Int64() + end + function run(IN,OUT) + OUT.int = IN.float1 + OUT.int64 = IN.float2 + end + )"); + + auto float1Input = script->getInputs()->getChild("float1"); + auto float2Input = script->getInputs()->getChild("float2"); + auto intOutput = script->getOutputs()->getChild("int"); + auto int64Output = script->getOutputs()->getChild("int64"); + + float1Input->set(1.0f); + float2Input->set(1.0f); + + EXPECT_TRUE(m_logicEngine.update()); + ASSERT_TRUE(m_logicEngine.getErrors().empty()); + EXPECT_EQ(1, *intOutput->get()); + EXPECT_EQ(1, *int64Output->get()); + + float1Input->set(2.5f); + float2Input->set(1.f); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, + ::testing::HasSubstr("Error during assignment of property 'int'! Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_EQ(1, *intOutput->get()); + EXPECT_EQ(1, *int64Output->get()); + + float1Input->set(1.f); + float2Input->set(2.5f); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, + ::testing::HasSubstr("Error during assignment of property 'int64'! Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_EQ(1, *intOutput->get()); + EXPECT_EQ(1, *int64Output->get()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAssigningNilToIntOutputs) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.int = Type:Int32() + end + function run(IN,OUT) + OUT.int = nil + end + )"); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Assigning nil to 'Int32' output 'int'!")); + EXPECT_EQ(0, *script->getOutputs()->getChild("int")->get()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAssigningBoolToIntOutputs) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.int = Type:Int32() + end + function run(IN,OUT) + OUT.int = true + end + )"); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Assigning bool to 'Int32' output 'int'!")); + EXPECT_EQ(0, *script->getOutputs()->getChild("int")->get()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAssigningBoolToStringOutputs) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.str = Type:String() + end + function run(IN,OUT) + OUT.str = "this is quite ok" + OUT.str = true -- this is not ok + end + )"); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Assigning bool to 'String' output 'str'!")); + EXPECT_EQ("this is quite ok", *script->getOutputs()->getChild("str")->get()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenAssigningNumberToStringOutputs) + { + m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.str = Type:String() + end + function run(IN,OUT) + OUT.str = 42 -- this is not ok + end + )"); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Assigning number to 'String' output 'str'!")); + } + + TEST_F(ALuaScript_Runtime, SupportsMultipleLevelsOfNestedInputs_confidenceTest) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.rabbit = { + color = { + r = Type:Float(), + g = Type:Float(), + b = Type:Float() + }, + speed = Type:Int32() + } + OUT.result = Type:Float() + + end + function run(IN,OUT) + OUT.result = (IN.rabbit.color.r + IN.rabbit.color.b + IN.rabbit.color.g) * IN.rabbit.speed + end + )"); + + auto inputs = script->getInputs(); + auto rabbit = inputs->getChild("rabbit"); + auto color = rabbit->getChild("color"); + auto speed = rabbit->getChild("speed"); + + auto outputs = script->getOutputs(); + auto result = outputs->getChild("result"); + + color->getChild("r")->set(0.5f); + color->getChild("g")->set(1.0f); + color->getChild("b")->set(0.75f); + speed->set(20); + + m_logicEngine.update(); + + EXPECT_EQ(45, result->get()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenTryingToAccessFieldsWithNonStringIndexAtRuntime) + { + const std::vector allCases = { + {"local var = IN[0]", + "Bad access to property ''! Expected a string but got object of type number instead!"}, + {"var = IN[true]", + "Bad access to property ''! Expected a string but got object of type bool instead!"}, + {"var = IN[{x = 5}]", + "Bad access to property ''! Expected a string but got object of type table instead!"}, + {"OUT[0] = 5", + "Bad access to property ''! Expected a string but got object of type number instead!"}, + {"OUT[true] = 5", + "Bad access to property ''! Expected a string but got object of type bool instead!"}, + {"OUT[{x = 5}] = 5", + "Bad access to property ''! Expected a string but got object of type table instead!"}, + }; + + for (const auto& singleCase : allCases) + { + auto script = m_logicEngine.createLuaScript( + "function interface(IN,OUT)\n" + "end\n" + "function run(IN,OUT)\n" + + singleCase.errorCode + "\n" + "end\n" + ); + + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr(singleCase.expectedErrorMessage)); + m_logicEngine.destroy(*script); + } + } + + TEST_F(ALuaScript_Runtime, ProducesErrorWhenTryingToCreatePropertiesAtRuntime) + { + const std::vector allCases = + { + {"IN.cannot_create_inputs_here = 5", + "Tried to access undefined struct property 'cannot_create_inputs_here'"}, + {"OUT.cannot_create_outputs_here = 5", + "Tried to access undefined struct property 'cannot_create_outputs_here'"}, + }; + + for (const auto& singleCase : allCases) + { + auto script = m_logicEngine.createLuaScript( + "function interface(IN,OUT)\n" + "end\n" + "function run(IN,OUT)\n" + + singleCase.errorCode + "\n" + "end\n" + ); + + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr(singleCase.expectedErrorMessage)); + m_logicEngine.destroy(*script); + } + } + + TEST_F(ALuaScript_Runtime, AssignsValuesToArraysWithStructs) + { + std::string_view scriptWithArrays = R"( + function interface(IN,OUT) + IN.array_structs = Type:Array(2, {name = Type:String(), age = Type:Int32()}) + OUT.array_structs = Type:Array(2, {name = Type:String(), age = Type:Int32()}) + end + + function run(IN,OUT) + OUT.array_structs = IN.array_structs + OUT.array_structs[2] = {name = "joe", age = 99} + OUT.array_structs[2].age = 78 + end + )"; + + auto* script = m_logicEngine.createLuaScript(scriptWithArrays); + + auto inputs = script->getInputs(); + auto IN_array = inputs->getChild("array_structs"); + IN_array->getChild(0)->getChild("name")->set("donald"); + + EXPECT_TRUE(m_logicEngine.update()); + + auto outputs = script->getOutputs(); + auto OUT_array = outputs->getChild("array_structs"); + + EXPECT_EQ("donald", *OUT_array->getChild(0)->getChild("name")->get()); + EXPECT_EQ("joe", *OUT_array->getChild(1)->getChild("name")->get()); + EXPECT_EQ(78, *OUT_array->getChild(1)->getChild("age")->get()); + } + + // The below test is a truly evil attempt to violate the sandbox/environment protection + // The test makes sure we catch it and report an error accordingly + TEST_F(ALuaScript_Runtime, ForbidsCallingInterfaceFunctionInsideTheRunFunction) + { + m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + + function run(IN,OUT) + interface(IN,OUT) + end + )"); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, ::testing::HasSubstr("Unexpected global access to key 'interface' in run()!")); + } + + + TEST_F(ALuaScript_Runtime, AbortsAfterFirstRuntimeError) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.float = Type:Float() + OUT.float = Type:Float() + end + function run(IN,OUT) + error("next line will not be executed") + OUT.float = IN.float + end + )"); + + ASSERT_NE(nullptr, script); + script->getInputs()->getChild("float")->set(0.1f); + EXPECT_FALSE(m_logicEngine.update()); + EXPECT_FLOAT_EQ(0.0f, *script->getOutputs()->getChild("float")->get()); + } + + TEST_F(ALuaScript_Runtime, AssignOutputsFromInputsInDifferentWays_ConfidenceTest) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.assignmentType = Type:String() + + IN.float = Type:Float() + IN.int = Type:Int32() + IN.struct = { + float = Type:Float(), + int = Type:Int32(), + struct = { + float = Type:Float(), + int = Type:Int32(), + bool = Type:Bool(), + string = Type:String(), + vec2f = Type:Vec2f(), + vec3f = Type:Vec3f(), + vec4f = Type:Vec4f(), + vec2i = Type:Vec2i(), + vec3i = Type:Vec3i(), + vec4i = Type:Vec4i(), + array = Type:Array(2, Type:Vec2i()) + } + } + + OUT.float = Type:Float() + OUT.int = Type:Int32() + OUT.struct = { + float = Type:Float(), + int = Type:Int32(), + struct = { + float = Type:Float(), + int = Type:Int32(), + bool = Type:Bool(), + string = Type:String(), + vec2f = Type:Vec2f(), + vec3f = Type:Vec3f(), + vec4f = Type:Vec4f(), + vec2i = Type:Vec2i(), + vec3i = Type:Vec3i(), + vec4i = Type:Vec4i(), + array = Type:Array(2, Type:Vec2i()) + } + } + end + function run(IN,OUT) + if IN.assignmentType == "nullify" then + OUT.float = 0 + OUT.int = 0 + OUT.struct.float = 0 + OUT.struct.int = 0 + OUT.struct.struct.float = 0 + OUT.struct.struct.int = 0 + OUT.struct.struct.bool = false + OUT.struct.struct.string = "" + OUT.struct.struct.vec2f = {0, 0} + OUT.struct.struct.vec3f = {0, 0, 0} + OUT.struct.struct.vec4f = {0, 0, 0, 0} + OUT.struct.struct.vec2i = {0, 0} + OUT.struct.struct.vec3i = {0, 0, 0} + OUT.struct.struct.vec4i = {0, 0, 0, 0} + OUT.struct.struct.array = {{0, 0}, {0, 0}} + elseif IN.assignmentType == "mirror_individually" then + OUT.float = IN.float + OUT.int = IN.int + OUT.struct.float = IN.struct.float + OUT.struct.int = IN.struct.int + OUT.struct.struct.float = IN.struct.struct.float + OUT.struct.struct.int = IN.struct.struct.int + OUT.struct.struct.bool = IN.struct.struct.bool + OUT.struct.struct.string = IN.struct.struct.string + OUT.struct.struct.vec2f = IN.struct.struct.vec2f + OUT.struct.struct.vec3f = IN.struct.struct.vec3f + OUT.struct.struct.vec4f = IN.struct.struct.vec4f + OUT.struct.struct.vec2i = IN.struct.struct.vec2i + OUT.struct.struct.vec3i = IN.struct.struct.vec3i + OUT.struct.struct.vec4i = IN.struct.struct.vec4i + OUT.struct.struct.array[1] = IN.struct.struct.array[1] + OUT.struct.struct.array[2] = IN.struct.struct.array[2] + elseif IN.assignmentType == "assign_constants" then + OUT.float = 0.1 + OUT.int = 1 + OUT.struct.float = 0.2 + OUT.struct.int = 2 + OUT.struct.struct.float = 0.3 + OUT.struct.struct.int = 3 + OUT.struct.struct.bool = true + OUT.struct.struct.string = "somestring" + OUT.struct.struct.vec2f = { 0.1, 0.2 } + OUT.struct.struct.vec3f = { 1.1, 1.2, 1.3 } + OUT.struct.struct.vec4f = { 2.1, 2.2, 2.3, 2.4 } + OUT.struct.struct.vec2i = { 1, 2 } + OUT.struct.struct.vec3i = { 3, 4, 5 } + OUT.struct.struct.vec4i = { 6, 7, 8, 9 } + OUT.struct.struct.array = { {11, 12}, {13, 14} } + elseif IN.assignmentType == "assign_struct" then + OUT.float = IN.float + OUT.int = IN.int + OUT.struct = IN.struct + else + error("unsupported assignment type!") + end + end + )"); + + ASSERT_NE(nullptr, script); + + script->getInputs()->getChild("float")->set(0.1f); + script->getInputs()->getChild("int")->set(1); + script->getInputs()->getChild("struct")->getChild("float")->set(0.2f); + script->getInputs()->getChild("struct")->getChild("int")->set(2); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("float")->set(0.3f); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("int")->set(3); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("bool")->set(true); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("string")->set("somestring"); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("vec2f")->set({ 0.1f, 0.2f }); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("vec3f")->set({ 1.1f, 1.2f, 1.3f }); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("vec4f")->set({ 2.1f, 2.2f, 2.3f, 2.4f }); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("vec2i")->set({ 1, 2 }); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("vec3i")->set({ 3, 4, 5 }); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("vec4i")->set({ 6, 7, 8, 9 }); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("array")->getChild(0)->set({ 11, 12 }); + script->getInputs()->getChild("struct")->getChild("struct")->getChild("array")->getChild(1)->set({ 13, 14 }); + + std::array assignmentTypes = + { + "mirror_individually", + "assign_constants", + "assign_struct", + }; + + auto outputs = script->getOutputs(); + for (const auto& assignmentType : assignmentTypes) + { + EXPECT_TRUE(script->getInputs()->getChild("assignmentType")->set("nullify")); + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_TRUE(script->getInputs()->getChild("assignmentType")->set(assignmentType)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + + EXPECT_FLOAT_EQ(0.1f, *outputs->getChild("float")->get()); + EXPECT_EQ(1, *outputs->getChild("int")->get()); + + auto struct_lvl1 = outputs->getChild("struct"); + EXPECT_FLOAT_EQ(0.2f, *struct_lvl1->getChild("float")->get()); + EXPECT_EQ(2, *struct_lvl1->getChild("int")->get()); + + auto struct_lvl2 = struct_lvl1->getChild("struct"); + EXPECT_FLOAT_EQ(0.3f, *struct_lvl2->getChild("float")->get()); + EXPECT_EQ(3, *struct_lvl2->getChild("int")->get()); + EXPECT_EQ(true, *struct_lvl2->getChild("bool")->get()); + EXPECT_EQ("somestring", *struct_lvl2->getChild("string")->get()); + + EXPECT_EQ(*struct_lvl2->getChild("vec2f")->get(), vec2f(0.1f, 0.2f)); + EXPECT_EQ(*struct_lvl2->getChild("vec3f")->get(), vec3f(1.1f, 1.2f, 1.3f)); + EXPECT_EQ(*struct_lvl2->getChild("vec4f")->get(), vec4f(2.1f, 2.2f, 2.3f, 2.4f)); + EXPECT_EQ(*struct_lvl2->getChild("vec2i")->get(), vec2i(1, 2)); + EXPECT_EQ(*struct_lvl2->getChild("vec3i")->get(), vec3i(3, 4, 5)); + EXPECT_EQ(*struct_lvl2->getChild("vec4i")->get(), vec4i(6, 7, 8, 9)); + EXPECT_EQ(*struct_lvl2->getChild("array")->getChild(0)->get(), vec2i(11, 12)); + EXPECT_EQ(*struct_lvl2->getChild("array")->getChild(1)->get(), vec2i(13, 14)); + } + } + + TEST_F(ALuaScript_Runtime, ForbidsOverwritingRunFunctionInsideTheRunFunction) + { + m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + run = function() + OUT.str = "... go left! A Kansas city shuffle, lol!" + end + end + )"); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors().back().message, + ::testing::HasSubstr("Unexpected global variable definition 'run' in run()! " + "Use the init() function to declare global data and functions, or use modules!")); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorIfInvalidOutPropertyIsAccessed) + { + auto scriptWithInvalidOutParam = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + OUT.param = 47.11 + end + )"); + + ASSERT_NE(nullptr, scriptWithInvalidOutParam); + m_logicEngine.update(); + EXPECT_FALSE(m_logicEngine.getErrors().empty()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorIfInvalidNestedOutPropertyIsAccessed) + { + auto scriptWithInvalidStructAccess = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + OUT.struct.param = 47.11 + end + )"); + + ASSERT_NE(nullptr, scriptWithInvalidStructAccess); + m_logicEngine.update(); + EXPECT_FALSE(m_logicEngine.getErrors().empty()); + } + + TEST_F(ALuaScript_Runtime, ProducesErrorIfValidestedButInvalidOutPropertyIsAccessed) + { + auto scriptWithValidStructButInvalidField = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + OUT.struct = { + param = Type:Int32() + } + end + function run(IN,OUT) + OUT.struct.invalid = 47.11 + end + )"); + + ASSERT_NE(nullptr, scriptWithValidStructButInvalidField); + m_logicEngine.update(); + EXPECT_FALSE(m_logicEngine.getErrors().empty()); + } + + TEST_F(ALuaScript_Runtime, CanAssignInputDirectlyToOutput) + { + auto script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.param_struct = { + param1 = Type:Float(), + param2_struct = { + a = Type:Int32(), + b = Type:Int32() + } + } + OUT.param_struct = { + param1 = Type:Float(), + param2_struct = { + a = Type:Int32(), + b = Type:Int32() + } + } + end + function run(IN,OUT) + OUT.param_struct = IN.param_struct + end + )"); + + ASSERT_NE(nullptr, script); + + { + auto inputs = script->getInputs(); + auto param_struct = inputs->getChild("param_struct"); + param_struct->getChild("param1")->set(1.0f); + auto param2_struct = param_struct->getChild("param2_struct"); + param2_struct->getChild("a")->set(2); + param2_struct->getChild("b")->set(3); + } + + m_logicEngine.update(); + + { + auto outputs = script->getOutputs(); + auto param_struct = outputs->getChild("param_struct"); + EXPECT_FLOAT_EQ(1.0f, *param_struct->getChild("param1")->get()); + auto param2_struct = param_struct->getChild("param2_struct"); + EXPECT_EQ(2, param2_struct->getChild("a")->get()); + EXPECT_EQ(3, param2_struct->getChild("b")->get()); + } + } + + TEST_F(ALuaScript_Runtime, ProducesNoErrorIfOutputIsSetInFunction) + { + auto script = m_logicEngine.createLuaScript(R"( + function init() + GLOBAL.setPrimitive = function (output) + output.param = 42 + end + GLOBAL.setSubStruct = function (output) + output.struct = { + param = 43 + } + end + end + + function interface(IN,OUT) + OUT.param = Type:Int32() + OUT.struct = { + param = Type:Int32() + } + end + function run(IN,OUT) + GLOBAL.setPrimitive(OUT) + GLOBAL.setSubStruct(OUT) + end + )"); + + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.update()); + const auto outputs = script->getOutputs(); + ASSERT_NE(nullptr, outputs); + + ASSERT_EQ(2u, outputs->getChildCount()); + const auto param = outputs->getChild(0); + const auto struct1 = outputs->getChild(1); + + EXPECT_EQ(42, param->get()); + + ASSERT_EQ(1u, struct1->getChildCount()); + EXPECT_EQ(43, struct1->getChild(0)->get()); + } + + TEST_F(ALuaScript_Runtime, HasNoInfluenceOnBindingsIfTheyAreNotLinked) + { + auto scriptSource = R"( + function interface(IN,OUT) + IN.inFloat = Type:Float() + IN.inVec3 = Type:Vec3f() + OUT.outFloat = Type:Float() + OUT.outVec3 = Type:Vec3f() + end + function run(IN,OUT) + OUT.outFloat = IN.inFloat + OUT.outVec3 = IN.inVec3 + end + )"; + + const std::string_view vertexShaderSource = R"( + #version 300 es + + uniform highp float floatUniform; + + void main() + { + gl_Position = floatUniform * vec4(1.0); + })"; + + const std::string_view fragmentShaderSource = R"( + #version 300 es + + out lowp vec4 color; + void main(void) + { + color = vec4(1.0, 0.0, 0.0, 1.0); + })"; + + auto script1 = m_logicEngine.createLuaScript(scriptSource); + auto script2 = m_logicEngine.createLuaScript(scriptSource); + auto script3 = m_logicEngine.createLuaScript(scriptSource); + + auto script1FloatInput = script1->getInputs()->getChild("inFloat"); + auto script1FloatOutput = script1->getOutputs()->getChild("outFloat"); + auto script1Vec3Input = script1->getInputs()->getChild("inVec3"); + auto script1Vec3Output = script1->getOutputs()->getChild("outVec3"); + auto script2FloatInput = script2->getInputs()->getChild("inFloat"); + auto script2FloatOutput = script2->getOutputs()->getChild("outFloat"); + auto script2Vec3Input = script2->getInputs()->getChild("inVec3"); + auto script2Vec3Output = script2->getOutputs()->getChild("outVec3"); + auto script3FloatInput = script3->getInputs()->getChild("inFloat"); + auto script3FloatOutput = script3->getOutputs()->getChild("outFloat"); + auto script3Vec3Input = script3->getInputs()->getChild("inVec3"); + auto script3Vec3Output = script3->getOutputs()->getChild("outVec3"); + + ramses::RamsesFramework ramsesFramework{ RamsesFrameworkConfig{EFeatureLevel_Latest} }; + auto ramsesClient = ramsesFramework.createClient("client"); + auto ramsesScene = ramsesClient->createScene(ramses::sceneId_t(1)); + + ramses::EffectDescription ramsesEffectDesc; + ramsesEffectDesc.setVertexShader(vertexShaderSource.data()); + ramsesEffectDesc.setFragmentShader(fragmentShaderSource.data()); + auto ramsesEffect = ramsesScene->createEffect(ramsesEffectDesc); + auto ramsesAppearance = ramsesScene->createAppearance(*ramsesEffect); + ramses::PerspectiveCamera* camera = ramsesScene->createPerspectiveCamera(); + + auto nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + auto appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(*ramsesAppearance, "AppearanceBinding"); + auto cameraBinding = m_logicEngine.createRamsesCameraBinding(*camera, "CameraBinding"); + + m_logicEngine.update(); + + EXPECT_TRUE(*nodeBinding->getInputs()->getChild("visibility")->get()); + EXPECT_EQ(*nodeBinding->getInputs()->getChild("translation")->get(), vec3f(0.f, 0.f, 0.f)); + EXPECT_EQ(*nodeBinding->getInputs()->getChild("rotation")->get(), vec3f(0.f, 0.f, 0.f)); + EXPECT_EQ(*nodeBinding->getInputs()->getChild("scaling")->get(), vec3f(1.f, 1.f, 1.f)); + EXPECT_EQ(0.0f, appearanceBinding->getInputs()->getChild("floatUniform")->get()); + EXPECT_EQ(camera->getViewportX(), 0); + EXPECT_EQ(camera->getViewportY(), 0); + EXPECT_EQ(camera->getViewportWidth(), 16u); + EXPECT_EQ(camera->getViewportHeight(), 16u); + EXPECT_NEAR(camera->getVerticalFieldOfView(), 168.579f, 0.001f); + EXPECT_EQ(camera->getAspectRatio(), 1.f); + EXPECT_EQ(camera->getNearPlane(), 0.1f); + EXPECT_EQ(camera->getFarPlane(), 1.f); + + m_logicEngine.link(*script1FloatOutput, *script2FloatInput); + m_logicEngine.link(*script2FloatOutput, *script3FloatInput); + m_logicEngine.link(*script1Vec3Output, *script2Vec3Input); + m_logicEngine.link(*script2Vec3Output, *script3Vec3Input); + + m_logicEngine.update(); + + EXPECT_TRUE(*nodeBinding->getInputs()->getChild("visibility")->get()); + EXPECT_EQ(*nodeBinding->getInputs()->getChild("translation")->get(), vec3f(0.f, 0.f, 0.f)); + EXPECT_EQ(*nodeBinding->getInputs()->getChild("rotation")->get(), vec3f(0.f, 0.f, 0.f)); + EXPECT_EQ(*nodeBinding->getInputs()->getChild("scaling")->get(), vec3f(1.f, 1.f, 1.f)); + EXPECT_EQ(0.0f, appearanceBinding->getInputs()->getChild("floatUniform")->get()); + EXPECT_EQ(camera->getViewportX(), 0); + EXPECT_EQ(camera->getViewportY(), 0); + EXPECT_EQ(camera->getViewportWidth(), 16u); + EXPECT_EQ(camera->getViewportHeight(), 16u); + EXPECT_NEAR(camera->getVerticalFieldOfView(), 168.579f, 0.001f); + EXPECT_EQ(camera->getAspectRatio(), 1.f); + EXPECT_EQ(camera->getNearPlane(), 0.1f); + EXPECT_EQ(camera->getFarPlane(), 1.f); + + m_logicEngine.link(*script3Vec3Output, *nodeBinding->getInputs()->getChild("translation")); + + script1Vec3Input->set(vec3f{1.f, 2.f, 3.f}); + + m_logicEngine.update(); + + EXPECT_EQ(*nodeBinding->getInputs()->getChild("translation")->get(), vec3f(1.f, 2.f, 3.f)); + + m_logicEngine.link(*script3FloatOutput, *appearanceBinding->getInputs()->getChild("floatUniform")); + m_logicEngine.link(*script3FloatOutput, *cameraBinding->getInputs()->getChild("frustum")->getChild("farPlane")); + + script1FloatInput->set(42.f); + + m_logicEngine.update(); + + EXPECT_FLOAT_EQ(42.f, *appearanceBinding->getInputs()->getChild("floatUniform")->get()); + EXPECT_EQ(42.f, *cameraBinding->getInputs()->getChild("frustum")->getChild("farPlane")->get()); + + m_logicEngine.unlink(*script3Vec3Output, *nodeBinding->getInputs()->getChild("translation")); + + script1FloatInput->set(23.f); + script1Vec3Input->set(vec3f{3.f, 2.f, 1.f}); + + m_logicEngine.update(); + + EXPECT_EQ(*nodeBinding->getInputs()->getChild("translation")->get(), vec3f(1.f, 2.f, 3.f)); + EXPECT_FLOAT_EQ(23.f, *appearanceBinding->getInputs()->getChild("floatUniform")->get()); + EXPECT_EQ(23.f, *cameraBinding->getInputs()->getChild("frustum")->getChild("farPlane")->get()); + } + + + TEST_F(ALuaScript_Runtime, IncludesStandardLibraries_WhenConfiguredWithThem) + { + const std::string_view scriptSrc = R"( + function init() + GLOBAL.debug_func = function (arg) + print(arg) + end + end + + function interface(IN,OUT) + OUT.floored_float = Type:Int32() + OUT.string_gsub = Type:String() + OUT.table_maxn = Type:Int32() + OUT.language_of_debug_func = Type:String() + end + function run(IN,OUT) + -- test math lib + OUT.floored_float = math.floor(42.7) + -- test string lib + OUT.string_gsub = string.gsub("This is the text", "the text", "the modified text") + -- test table lib + OUT.table_maxn = table.maxn ({11, 12, 13}) + -- test debug lib + local debuginfo = debug.getinfo (GLOBAL.debug_func) + OUT.language_of_debug_func = debuginfo.what + end + )"; + auto script = m_logicEngine.createLuaScript(scriptSrc, WithStdModules({EStandardModule::Base, EStandardModule::String, EStandardModule::Table, EStandardModule::Debug, EStandardModule::Math})); + ASSERT_NE(nullptr, script); + + m_logicEngine.update(); + + EXPECT_EQ(42, *script->getOutputs()->getChild("floored_float")->get()); + EXPECT_EQ("This is the modified text", *script->getOutputs()->getChild("string_gsub")->get()); + EXPECT_EQ(3, *script->getOutputs()->getChild("table_maxn")->get()); + EXPECT_EQ("Lua", *script->getOutputs()->getChild("language_of_debug_func")->get()); + } + + class ALuaScript_RuntimeIterators : public ALuaScript_Runtime + { + protected: + }; + + TEST_F(ALuaScript_RuntimeIterators, ComputesSizeOfCustomPropertiesUsingCustomLengthFunction) + { + std::string_view scriptSrc = R"( + function interface(IN,OUT) + IN.array_int = Type:Array(2, Type:Int32()) + OUT.struct = {a=Type:Int32(), b={c = Type:Int32()}} + OUT.array_struct = Type:Array(3, {a=Type:Int32(), b=Type:Float()}) + end + + function run(IN,OUT) + if rl_len(IN) ~= 1 then + error("Wrong IN size!") + end + + if rl_len(IN.array_int) ~= 2 then + error("Wrong array size!") + end + + if rl_len(OUT) ~= 2 then + error("Wrong OUT size!") + end + + if rl_len(OUT.struct) ~= 2 then + error("Wrong struct size!") + end + + if rl_len(OUT.struct.b) ~= 1 then + error("Wrong nested struct size!") + end + + if rl_len(OUT.array_struct) ~= 3 then + error("Wrong array struct size!") + end + + if rl_len(OUT.array_struct[1]) ~= 2 then + error("Wrong array struct element size!") + end + end + )"; + auto* script = m_logicEngine.createLuaScript(scriptSrc, WithStdModules({EStandardModule::Base})); + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALuaScript_RuntimeIterators, CallingCustomLengthFunctionOnNormalLuaTables_YieldsSameResultAsBuiltInSizeOperator) + { + std::string_view scriptSrc = R"( + function interface(IN,OUT) + end + + function run(IN,OUT) + local emptyTable = {} + assert(rl_len(emptyTable) == #emptyTable) + local numericTable = {1, 2, 3} + assert(rl_len(numericTable) == #numericTable) + local nonNumericTable = {a=5, b=6} + assert(rl_len(nonNumericTable) == #nonNumericTable) + local nonNumericTable = {a=5, b=6} + assert(rl_len(nonNumericTable) == #nonNumericTable) + end + )"; + auto* script = m_logicEngine.createLuaScript(scriptSrc, WithStdModules({ EStandardModule::Base })); + ASSERT_NE(nullptr, script); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALuaScript_RuntimeIterators, CustomRlNextFunctionWorksLikeItsBuiltInCounterpart_Structs) + { + std::string_view scriptSrc = R"( + function interface(IN,OUT) + IN.struct = {a = Type:Int32(), b = Type:Int32()} + IN.nested = { + struct = {a = Type:Int32(), b = Type:Int32()} + } + OUT.struct = {a = Type:Int32(), b = Type:Int32()} + OUT.nested = { + struct = {a = Type:Int32(), b = Type:Int32()} + } + end + + function run(IN,OUT) + -- propagate data to OUT so that we can test both further down + OUT.struct = IN.struct + OUT.nested = IN.nested + + local objectsToCheck = {IN.struct, IN.nested.struct, OUT.struct, OUT.nested.struct} + + for unused, container in pairs(objectsToCheck) do + ---- no index specified is the same as providing nil (see below) + local k, v = rl_next(container) + assert(k == 'a') + assert(v == 11) + -- index=nil -> yields first element of container and its index + local k, v = rl_next(container, nil) + assert(k == 'a') + assert(v == 11) + -- index==N -> yields element N+1 and its index + local k, v = rl_next(container, 'a') + assert(k == 'b') + assert(v == 12) + local k, v = rl_next(container, 'b') + assert(k == nil) + assert(v == nil) + end + end + )"; + auto* script = m_logicEngine.createLuaScript(scriptSrc, WithStdModules({ EStandardModule::Base, EStandardModule::String })); + ASSERT_NE(nullptr, script); + script->getInputs()->getChild("struct")->getChild("a")->set(11); + script->getInputs()->getChild("struct")->getChild("b")->set(12); + script->getInputs()->getChild("nested")->getChild("struct")->getChild("a")->set(11); + script->getInputs()->getChild("nested")->getChild("struct")->getChild("b")->set(12); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALuaScript_RuntimeIterators, CustomRlNextFunctionWorksLikeItsBuiltInCounterpart_Arrays) + { + std::string_view scriptSrc = R"( + function interface(IN,OUT) + IN.array_int = Type:Array(2, Type:Int32()) + IN.nested = { + array_int = Type:Array(2, Type:Int32()) + } + OUT.array_int = Type:Array(2, Type:Int32()) + OUT.nested = { + array_int = Type:Array(2, Type:Int32()) + } + end + + function run(IN,OUT) + -- propagate data to OUT so that we can test both further down + OUT.array_int = IN.array_int + OUT.nested = IN.nested + + local objectsToCheck = {IN.array_int, IN.nested.array_int, OUT.array_int, OUT.nested.array_int} + + for k, container in pairs(objectsToCheck) do + -- no index specified is the same as providing nil (see below) + local a, b = rl_next(container) + assert(a == 1) + assert(b == 11) + -- index=nil -> yields first element of container and its index + local a, b = rl_next(container, nil) + assert(a == 1) + assert(b == 11) + -- index==N -> yields element N+1 and its index + local a, b = rl_next(container, 1) + assert(a == 2) + assert(b == 12) + local a, b = rl_next(container, 2) + assert(a == nil) + assert(b == nil) + end + end + )"; + auto* script = m_logicEngine.createLuaScript(scriptSrc, WithStdModules({ EStandardModule::Base, EStandardModule::String })); + ASSERT_NE(nullptr, script); + script->getInputs()->getChild("array_int")->getChild(0)->set(11); + script->getInputs()->getChild("array_int")->getChild(1)->set(12); + script->getInputs()->getChild("nested")->getChild("array_int")->getChild(0)->set(11); + script->getInputs()->getChild("nested")->getChild("array_int")->getChild(1)->set(12); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALuaScript_RuntimeIterators, Custom_IPairs_BehavesTheSameAsStandard_IPairs_Function_ForArrays) + { + std::string_view scriptSrc = R"( + function interface(IN,OUT) + IN.array_int = Type:Array(2, Type:Int32()) + IN.nested = { + array_int = Type:Array(2, Type:Int32()) + } + OUT.array_int = Type:Array(2, Type:Int32()) + OUT.nested = { + array_int = Type:Array(2, Type:Int32()) + } + end + + function run(IN,OUT) + -- propagate data to OUT so that we can test both further down + OUT.array_int = IN.array_int + OUT.nested = IN.nested + + -- compare iteration results to a static reference table + local refTable = {[1] = 11, [2] = 12} + + -- test multiple containers (which all have the same contents) + local objectsToCheck = {IN.array_int, IN.nested.array_int, OUT.array_int, OUT.nested.array_int} + for k, container in pairs(objectsToCheck) do + -- iterate manually over reference table... + local refKey = 1 + local refValue = nil + for key, value in rl_ipairs(container) do + if type(key) ~= 'number' then + error('Key should be of type number!') + end + + if key ~= refKey then + error("Expected key==refKey, but found " .. tostring(key) .. " != " .. tostring(refKey)) + end + + refValue = refTable[refKey] + if value ~= refValue then + error("Expected value==refValue, but found " .. tostring(value) .. " != " .. tostring(refValue)) + end + -- progress refTable manually + refKey = refKey + 1 + end + + -- make sure there were exactly as many elements in refTable by checking no element is left to iterate + assert(refKey == 3) + assert(refValue == 12) + end + + end + )"; + auto* script = m_logicEngine.createLuaScript(scriptSrc, WithStdModules({ EStandardModule::Base, EStandardModule::String })); + ASSERT_NE(nullptr, script); + script->getInputs()->getChild("array_int")->getChild(0)->set(11); + script->getInputs()->getChild("array_int")->getChild(1)->set(12); + script->getInputs()->getChild("nested")->getChild("array_int")->getChild(0)->set(11); + script->getInputs()->getChild("nested")->getChild("array_int")->getChild(1)->set(12); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALuaScript_RuntimeIterators, Custom_Pairs_BehavesTheSameAsStandard_Pairs_Function_ForArrays) + { + std::string_view scriptSrc = R"( + function interface(IN,OUT) + IN.array_int = Type:Array(2, Type:Int32()) + IN.nested = { + array_int = Type:Array(2, Type:Int32()) + } + OUT.array_int = Type:Array(2, Type:Int32()) + OUT.nested = { + array_int = Type:Array(2, Type:Int32()) + } + end + + function run(IN,OUT) + -- propagate data to OUT so that we can test both further down + OUT.array_int = IN.array_int + OUT.nested = IN.nested + + -- compare iteration results to a static reference table + local refTable = {[1] = 11, [2] = 12} + + -- test multiple containers (which all have the same contents) + local objectsToCheck = {IN.array_int, IN.nested.array_int, OUT.array_int, OUT.nested.array_int} + for k, container in pairs(objectsToCheck) do + -- iterate manually over reference table... + local refKey,refValue = next(refTable) + -- ...and compare to rl_pairs results + for key, value in rl_pairs(container) do + if type(key) ~= 'number' then + error('Key should be of type number!') + end + + if key ~= refKey then + error("Expected key==refKey, but found " .. tostring(key) .. " != " .. tostring(refKey)) + end + if value ~= refValue then + error("Expected value==refValue, but found " .. tostring(value) .. " != " .. tostring(refValue)) + end + -- progress refTable manually + refKey,refValue = next(refTable, refKey) + end + + -- make sure there were exactly as many elements in refTable by checking no element is left to iterate + assert(refKey == nil) + assert(refValue == nil) + end + + end + )"; + auto* script = m_logicEngine.createLuaScript(scriptSrc, WithStdModules({ EStandardModule::Base, EStandardModule::String })); + ASSERT_NE(nullptr, script); + script->getInputs()->getChild("array_int")->getChild(0)->set(11); + script->getInputs()->getChild("array_int")->getChild(1)->set(12); + script->getInputs()->getChild("nested")->getChild("array_int")->getChild(0)->set(11); + script->getInputs()->getChild("nested")->getChild("array_int")->getChild(1)->set(12); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALuaScript_RuntimeIterators, Custom_Pairs_BehavesTheSameAsStandard_Pairs_Function_ForStructs) + { + std::string_view scriptSrc = R"( + function interface(IN,OUT) + IN.int = Type:Int32() + IN.bool = Type:Bool() + IN.nested = { + int = Type:Int32(), + bool = Type:Bool(), + nested = { + notUsed = Type:Float() + } + } + OUT.int = Type:Int32() + OUT.bool = Type:Bool() + OUT.nested = { + int = Type:Int32(), + bool = Type:Bool(), + nested = { + notUsed = Type:Float() + } + } + end + + function run(IN,OUT) + -- propagate data to OUT so that we can test both further down + OUT.int = IN.int + OUT.bool = IN.bool + OUT.nested = IN.nested + + -- compare iteration results to a static reference table + local refTable = {int = 42, bool = false, nested = {int = 42, bool = false, nested = {}}} + + -- test multiple containers (which all have the same contents) + local objectsToCheck = {IN, IN.nested, OUT, OUT.nested} + for k, container in pairs(objectsToCheck) do + -- iterate manually over reference table... + local refKey,refValue = next(refTable) + -- ...and compare to rl_pairs results + for key, value in rl_pairs(container) do + if type(key) ~= 'string' then + error('Key should be of type string!') + end + + if key ~= refKey then + error("Expected key==refKey, but found " .. tostring(key) .. " != " .. tostring(refKey)) + end + -- compare all values except 'nested', because no value comparison semantics for tables/userdata + if key ~= "nested" and value ~= refValue then + error("Expected value==refValue, but found " .. tostring(value) .. " != " .. tostring(refValue)) + end + -- progress refTable manually + refKey,refValue = next(refTable, refKey) + end + + -- make sure there are no leftover elements in refTable + assert(refKey == nil) + assert(refValue == nil) + end + + end + )"; + auto* script = m_logicEngine.createLuaScript(scriptSrc, WithStdModules({ EStandardModule::Base, EStandardModule::String })); + ASSERT_NE(nullptr, script); + script->getInputs()->getChild("int")->set(42); + script->getInputs()->getChild("bool")->set(false); + script->getInputs()->getChild("nested")->getChild("int")->set(42); + script->getInputs()->getChild("nested")->getChild("bool")->set(false); + EXPECT_TRUE(m_logicEngine.update()); + } + + class ALuaScript_Runtime_Sandboxing : public ALuaScript_Runtime + { + }; + + TEST_F(ALuaScript_Runtime_Sandboxing, ReportsErrorWhenTryingToReadUnknownGlobals) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + + function run(IN,OUT) + local t = someGlobalVariable + end)"); + ASSERT_NE(nullptr, script); + + EXPECT_FALSE(m_logicEngine.update()); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr( + "Unexpected global access to key 'someGlobalVariable' in run()! Only 'GLOBAL' is allowed as a key")); + } + + TEST_F(ALuaScript_Runtime_Sandboxing, ReportsErrorWhenSettingGlobals) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + + function run(IN,OUT) + thisCausesError = 'bad' + end)"); + ASSERT_NE(nullptr, script); + + EXPECT_FALSE(m_logicEngine.update()); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Unexpected global variable definition 'thisCausesError' in run()! Use the init() function to declare global data and functions, or use modules!")); + } + + TEST_F(ALuaScript_Runtime_Sandboxing, ReportsErrorWhenTryingToOverrideGlobals) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + end + + function interface(IN,OUT) + end + + function run(IN,OUT) + GLOBAL = {} + end)"); + ASSERT_NE(nullptr, script); + + EXPECT_FALSE(m_logicEngine.update()); + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr(" Trying to override the GLOBAL table in run()! You can only read data, but not overwrite the table!")); + } + + TEST_F(ALuaScript_Runtime_Sandboxing, ReportsErrorWhenTryingToDeclareRunFunctionTwice) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + + function run(IN,OUT) + end + + function run(IN,OUT) + end)"); + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors().begin()->message, + ::testing::HasSubstr("Function 'run' can only be declared once!")); + } + + TEST_F(ALuaScript_Runtime_Sandboxing, ForbidsCallingSpecialFunctions) + { + for (const auto& specialFunction : std::vector{ "init", "run", "interface" }) + { + LuaScript* script = m_logicEngine.createLuaScript(fmt::format(R"( + function init() + end + function interface(IN,OUT) + end + + function run(IN,OUT) + {}() + end + )", specialFunction)); + + ASSERT_NE(nullptr, script); + + EXPECT_FALSE(m_logicEngine.update()); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, + ::testing::HasSubstr(fmt::format("Unexpected global access to key '{}' in run()! Only 'GLOBAL' is allowed as a key", specialFunction))); + + ASSERT_TRUE(m_logicEngine.destroy(*script)); + } + } +} diff --git a/client/logic/unittests/api/LuaScriptTest_Serialization.cpp b/client/logic/unittests/api/LuaScriptTest_Serialization.cpp new file mode 100644 index 000000000..4c0c03f4b --- /dev/null +++ b/client/logic/unittests/api/LuaScriptTest_Serialization.cpp @@ -0,0 +1,812 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" + +#include "impl/LuaScriptImpl.h" +#include "impl/LogicEngineImpl.h" +#include "impl/PropertyImpl.h" +#include "impl/LuaModuleImpl.h" +#include "internals/ErrorReporting.h" + +#include "generated/LuaScriptGen.h" + +#include "SerializationTestUtils.h" +#include "FeatureLevelTestValues.h" + +namespace ramses::internal +{ + // Serialization unit tests only. For higher-order tests, check ALuaScript_LifecycleWithFiles + class ALuaScript_Serialization : public ::testing::TestWithParam + { + protected: + std::unique_ptr createTestScript(std::string_view source, std::string_view scriptName = "") + { + return std::make_unique( + *LuaCompilationUtils::CompileScriptOrImportPrecompiled(m_solState, {}, {}, std::string{ source }, scriptName, m_errorReporting, {}, {}, {}, false), + scriptName, 1u); + } + + std::vector static GetByteCodeForSource(std::string_view source) + { + sol::state solState; + sol::load_result loadResult = solState.load(source); + sol::protected_function function = loadResult; + sol::bytecode byteCode = function.dump(); + std::vector result; + std::transform(byteCode.cbegin(), byteCode.cend(), std::back_inserter(result), [](auto v) {return uint8_t(v); }); + return result; + } + + std::string_view m_minimalScript = R"( + function interface(IN,OUT) + end + + function run(IN,OUT) + end + )"; + + SolState m_solState; + ErrorReporting m_errorReporting; + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + SerializationTestUtils m_testUtils{ m_flatBufferBuilder }; + SerializationMap m_serializationMap; + DeserializationMap m_deserializationMap; + }; + + INSTANTIATE_TEST_SUITE_P( + ALuaScript_SerializationTests, + ALuaScript_Serialization, + GetFeatureLevelTestValues()); + + // More unit tests with inputs/outputs declared in LogicNode (base class) serialization tests + TEST_P(ALuaScript_Serialization, RemembersBaseClassData) + { + // Serialize + { + std::unique_ptr script = createTestScript(m_minimalScript, "name"); + (void)LuaScriptImpl::Serialize(*script, m_flatBufferBuilder, m_serializationMap, ELuaSavingMode::ByteCodeOnly); + } + + // Inspect flatbuffers data + const auto& serializedScript = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serializedScript.base()); + ASSERT_TRUE(serializedScript.base()->name()); + EXPECT_EQ(serializedScript.base()->name()->string_view(), "name"); + EXPECT_EQ(serializedScript.base()->id(), 1u); + + ASSERT_TRUE(serializedScript.rootInput()); + EXPECT_EQ(serializedScript.rootInput()->rootType(), rlogic_serialization::EPropertyRootType::Struct); + ASSERT_TRUE(serializedScript.rootInput()->children()); + EXPECT_EQ(serializedScript.rootInput()->children()->size(), 0u); + + ASSERT_TRUE(serializedScript.rootOutput()); + EXPECT_EQ(serializedScript.rootOutput()->rootType(), rlogic_serialization::EPropertyRootType::Struct); + ASSERT_TRUE(serializedScript.rootOutput()->children()); + EXPECT_EQ(serializedScript.rootOutput()->children()->size(), 0u); + + // Deserialize + { + std::unique_ptr deserializedScript = LuaScriptImpl::Deserialize(m_solState, serializedScript, m_errorReporting, m_deserializationMap); + + ASSERT_TRUE(deserializedScript); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + + EXPECT_EQ(deserializedScript->getName(), "name"); + EXPECT_EQ(deserializedScript->getId(), 1u); + } + } + + TEST_P(ALuaScript_Serialization, SerializesLuaSourceCode) + { + { + std::unique_ptr script = createTestScript(m_minimalScript, ""); + (void)LuaScriptImpl::Serialize(*script, m_flatBufferBuilder, m_serializationMap, ELuaSavingMode::SourceCodeOnly); + } + + const auto& serializedScript = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + ASSERT_TRUE(serializedScript.luaSourceCode()); + EXPECT_EQ(serializedScript.luaSourceCode()->string_view(), m_minimalScript); + } + + TEST_P(ALuaScript_Serialization, SerializesLuaByteCode) + { + { + std::unique_ptr script = createTestScript(m_minimalScript, ""); + (void)LuaScriptImpl::Serialize(*script, m_flatBufferBuilder, m_serializationMap, ELuaSavingMode::ByteCodeOnly); + } + + const auto& serializedScript = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + ASSERT_TRUE(serializedScript.luaByteCode()); + EXPECT_TRUE(serializedScript.luaByteCode()->size() > 0); + } + + TEST_P(ALuaScript_Serialization, BackwardsCompatiblity_CanDeserializeFromLuaSourceCode) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_minimalScript), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty(""), + 0 // no byte code + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + EXPECT_TRUE(deserialized); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + + TEST_P(ALuaScript_Serialization, StoresDuplicateByteCodeOnce) + { + SerializationMap serializationMap; + + auto script1 = createTestScript(m_minimalScript, "script"); + auto script2 = createTestScript(m_minimalScript, "script2"); + + (void)LuaScriptImpl::Serialize(*script1, m_flatBufferBuilder, serializationMap, ELuaSavingMode::ByteCodeOnly); + const auto& serialized1 = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + const auto byteCode1Offset = serialized1.luaByteCode(); + + (void)LuaScriptImpl::Serialize(*script2, m_flatBufferBuilder, serializationMap, ELuaSavingMode::ByteCodeOnly); + const auto& serialized2 = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + const auto byteCode2Offset = serialized2.luaByteCode(); + + EXPECT_EQ(byteCode1Offset, byteCode2Offset); + } + + TEST_P(ALuaScript_Serialization, DoesNotContainDebugLogFunctionsAfterDeserialization) + { + // Serialize + { + std::unique_ptr script = createTestScript(m_minimalScript, "name"); + (void)LuaScriptImpl::Serialize(*script, m_flatBufferBuilder, m_serializationMap, ELuaSavingMode::ByteCodeOnly); + } + + // Deserialize + const auto& serializedScript = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserializedScript = LuaScriptImpl::Deserialize(m_solState, serializedScript, m_errorReporting, m_deserializationMap); + ASSERT_TRUE(deserializedScript); + EXPECT_FALSE(deserializedScript->hasDebugLogFunctions()); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenNameMissing) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + 0, // no name + 1u) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing name!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of LuaScript from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenIdMissing) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 0) // no id (id gets checked before name) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing or invalid ID!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of LuaScript from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenBothLuaSourceCodeAndBytecodeMissing) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0, // no source code + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty(""), + 0 // no bytecode + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaScript from serialized data: has neither Lua source code nor bytecode!"); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenBothLuaSourceCodeAndBytecodeEmpty) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(""), // source code empty + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty(""), + m_flatBufferBuilder.CreateVector(std::vector{}) // bytecode empty + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaScript from serialized data: has neither Lua source code nor bytecode!"); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenUserModulesMissing) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_minimalScript), + 0, // no user modules + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty(""), + m_flatBufferBuilder.CreateVector(std::vector{1, 0}) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + EXPECT_EQ(nullptr, LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap)); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaScript from serialized data: missing user module dependencies!"); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenStandardModulesMissing) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_minimalScript), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0, // no standard modules + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty(""), + m_flatBufferBuilder.CreateVector(std::vector{1, 0}) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + EXPECT_EQ(nullptr, LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap)); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaScript from serialized data: missing standard module dependencies!"); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenRootInputMissing) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_minimalScript), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + 0, // no root input + m_testUtils.serializeTestProperty(""), + m_flatBufferBuilder.CreateVector(std::vector{1, 0}) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaScript from serialized data: missing root input!"); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenRootOutputMissing) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_minimalScript), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + 0, // no root output + m_flatBufferBuilder.CreateVector(std::vector{1, 0}) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaScript from serialized data: missing root output!"); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenRootInputHasErrors) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_minimalScript), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty("", rlogic_serialization::EPropertyRootType::Struct, true, true), // create root input with errors + m_testUtils.serializeTestProperty(""), + m_flatBufferBuilder.CreateVector(std::vector{1, 0}) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: missing name!"); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenRootOutputHasErrors) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_minimalScript), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty("", rlogic_serialization::EPropertyRootType::Struct, true, true), // create root output with errors + m_flatBufferBuilder.CreateVector(std::vector{1, 0}) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: missing name!"); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenByteCodeInvalidAndNoSourceAvailable) + { + { + std::vector invalidByteCode(10, 0); + + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty(""), + m_flatBufferBuilder.CreateVector(invalidByteCode) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 2u); + EXPECT_THAT(m_errorReporting.getErrors()[0].message, ::testing::HasSubstr("Fatal error during loading of LuaScript 'name': failed loading pre-compiled byte code")); + EXPECT_THAT(m_errorReporting.getErrors()[1].message, ::testing::HasSubstr("Fatal error during loading of LuaScript 'name' from serialized data")); + } + + TEST_P(ALuaScript_Serialization, WillTryToRecompileScriptFromSourceWhenByteCodeInvalid) + { + const std::vector invalidByteCode(1, 0); + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_minimalScript), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty(""), + m_flatBufferBuilder.CreateVector(invalidByteCode) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + // check that script was recompiled successfully + EXPECT_TRUE(deserialized); + + // serialize again and check that new byte code is produced + { + flatbuffers::FlatBufferBuilder builder; + (void)LuaScriptImpl::Serialize(*deserialized, builder, m_serializationMap, ELuaSavingMode::SourceAndByteCode); + const auto serializedWithValidByteCode = flatbuffers::GetRoot(builder.GetBufferPointer()); + ASSERT_TRUE(serializedWithValidByteCode); + ASSERT_TRUE(serializedWithValidByteCode->luaSourceCode()); + EXPECT_EQ(m_minimalScript, serializedWithValidByteCode->luaSourceCode()->c_str()); + + EXPECT_TRUE(serializedWithValidByteCode->luaByteCode()); + EXPECT_TRUE(serializedWithValidByteCode->luaByteCode()->size() > 0); + EXPECT_NE(serializedWithValidByteCode->luaByteCode()->size(), invalidByteCode.size()); + } + } + + TEST_P(ALuaScript_Serialization, SerializesSourceCodeOnly_InSourceOnlyMode) + { + auto script = createTestScript(m_minimalScript, "script"); + + SerializationMap serializationMap; + (void)LuaScriptImpl::Serialize(*script, m_flatBufferBuilder, serializationMap, ELuaSavingMode::SourceCodeOnly); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + EXPECT_FALSE(serialized.luaByteCode()); + ASSERT_TRUE(serialized.luaSourceCode()); + EXPECT_EQ(m_minimalScript, serialized.luaSourceCode()->string_view()); + } + + TEST_P(ALuaScript_Serialization, SerializesBytecodeOnly_InBytecodeOnlyMode) + { + auto script = createTestScript(m_minimalScript, "script"); + + SerializationMap serializationMap; + (void)LuaScriptImpl::Serialize(*script, m_flatBufferBuilder, serializationMap, ELuaSavingMode::ByteCodeOnly); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serialized.luaByteCode()); + EXPECT_TRUE(serialized.luaByteCode()->size() > 0); + EXPECT_FALSE(serialized.luaSourceCode()); + } + + TEST_P(ALuaScript_Serialization, SerializesBytecodeOnly_InSourceCodeOnlyMode_IfNoSourceAvailable) + { + // simulate script with no source code by serializing it with bytecode only and deserializing again + std::unique_ptr scriptWithNoSourceCode; + { + auto script = createTestScript(m_minimalScript, "script"); + + SerializationMap serializationMap; + (void)LuaScriptImpl::Serialize(*script, m_flatBufferBuilder, serializationMap, ELuaSavingMode::ByteCodeOnly); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + scriptWithNoSourceCode = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + ASSERT_TRUE(scriptWithNoSourceCode); + m_flatBufferBuilder.Clear(); + } + + SerializationMap serializationMap; + (void)LuaScriptImpl::Serialize(*scriptWithNoSourceCode, m_flatBufferBuilder, serializationMap, ELuaSavingMode::SourceCodeOnly); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serialized.luaByteCode()); + EXPECT_TRUE(serialized.luaByteCode()->size() > 0); + EXPECT_FALSE(serialized.luaSourceCode()); + } + + TEST_P(ALuaScript_Serialization, SerializesBytecodeOnly_InBothSourceAndBytecodeMode_IfNoSourceAvailable) + { + // simulate script with no source code by serializing it with bytecode only and deserializing again + std::unique_ptr scriptWithNoSourceCode; + { + auto script = createTestScript(m_minimalScript, "script"); + + SerializationMap serializationMap; + (void)LuaScriptImpl::Serialize(*script, m_flatBufferBuilder, serializationMap, ELuaSavingMode::ByteCodeOnly); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + scriptWithNoSourceCode = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + ASSERT_TRUE(scriptWithNoSourceCode); + m_flatBufferBuilder.Clear(); + } + + SerializationMap serializationMap; + (void)LuaScriptImpl::Serialize(*scriptWithNoSourceCode, m_flatBufferBuilder, serializationMap, ELuaSavingMode::SourceAndByteCode); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serialized.luaByteCode()); + EXPECT_TRUE(serialized.luaByteCode()->size() > 0); + EXPECT_FALSE(serialized.luaSourceCode()); + } + + TEST_P(ALuaScript_Serialization, SerializesBothSourceAndBytecode) + { + auto script = createTestScript(m_minimalScript, "script"); + + SerializationMap serializationMap; + (void)LuaScriptImpl::Serialize(*script, m_flatBufferBuilder, serializationMap, ELuaSavingMode::SourceAndByteCode); + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serialized.luaSourceCode()); + EXPECT_EQ(m_minimalScript, serialized.luaSourceCode()->string_view()); + + ASSERT_TRUE(serialized.luaByteCode()); + EXPECT_TRUE(serialized.luaByteCode()->size() > 0); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenScriptDeclaresGlobalVariables) + { + { + const std::string_view src = R"( + global="this will cause error" + function interface(IN,OUT) + end + + function run(IN,OUT) + local a = 10 + end + )"; + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty(""), + m_flatBufferBuilder.CreateVector(GetByteCodeForSource(src)) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 2u); + EXPECT_THAT(m_errorReporting.getErrors()[0].message, ::testing::HasSubstr("Declaring global variables is forbidden (exceptions: the functions 'init', 'interface' and 'run')!")); + EXPECT_THAT(m_errorReporting.getErrors()[1].message, ::testing::HasSubstr("Fatal error during loading of LuaScript 'name' from serialized data!")); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenLuaScriptSourceHasSyntaxErrors) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("script"), + 1u), + m_flatBufferBuilder.CreateString("this.is.bad.code"), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty("") + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 2u); + EXPECT_THAT(m_errorReporting.getErrors()[0].message, ::testing::HasSubstr("Error while loading script. Lua stack trace")); + EXPECT_THAT(m_errorReporting.getErrors()[1].message, ::testing::HasSubstr("Fatal error during loading of LuaScript 'script' from serialized data!")); + } + + // Can't happen in normal usage; still, we test this case because we should not rely on correct export during loading + TEST_P(ALuaScript_Serialization, ProducesErrorWhenLuaScriptSourceBreaksSandbox_WhenLoaded) + { + { + std::string_view brokenScript = R"( + globalVariable = 5 -- breaks sandbox + + function interface(IN,OUT) + end + + function run(IN,OUT) + end + )"; + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("script"), + 1u), + m_flatBufferBuilder.CreateString(brokenScript), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty("") + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 2u); + EXPECT_THAT(m_errorReporting.getErrors()[0].message, + ::testing::HasSubstr("Declaring global variables is forbidden (exceptions: the functions 'init', 'interface' and 'run')! (found value of type 'number')")); + EXPECT_THAT(m_errorReporting.getErrors()[1].message, ::testing::HasSubstr("Fatal error during loading of LuaScript 'script' from serialized data!")); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenLuaScriptSourceBreaksSandbox_WhenExecuted) + { + { + std::string_view brokenScript = R"( + function interface(IN,OUT) + end + + function run(IN,OUT) + globalVariable = 5 -- breaks sandbox + end + )"; + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("script"), + 1u), + m_flatBufferBuilder.CreateString(brokenScript), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty("") + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_TRUE(deserialized); + auto expectedError = deserialized->update(); + EXPECT_TRUE(expectedError); + + EXPECT_THAT(expectedError->message, + ::testing::HasSubstr("Unexpected global variable definition 'globalVariable' in run()! Use the init() function to declare global data and functions, or use modules!")); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenLuaScriptSourceHasRuntimeErrors) + { + { + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("script"), + 1u), + m_flatBufferBuilder.CreateString("error('This is not going to compile')"), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector{ static_cast(EStandardModule::Base) }), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty("") // create root output with errors + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 2u); + EXPECT_THAT(m_errorReporting.getErrors()[0].message, ::testing::HasSubstr("This is not going to compile")); + EXPECT_THAT(m_errorReporting.getErrors()[1].message, ::testing::HasSubstr("Fatal error during loading of LuaScript 'script' from serialized data!")); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenUserModuleHasNoName) + { + { + const auto dummyModuleUsageFB = rlogic_serialization::CreateLuaModuleUsage( + m_flatBufferBuilder, + 0, // no name + 0); // Invalid ID + + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_minimalScript), + m_flatBufferBuilder.CreateVector(std::vector>{ dummyModuleUsageFB }), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty(""), + m_flatBufferBuilder.CreateVector(std::vector{1, 0}) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaScript 'name' module data: missing name!"); + } + + TEST_P(ALuaScript_Serialization, ProducesErrorWhenUserModuleHasNoData) + { + { + const auto dummyModuleUsageFB = rlogic_serialization::CreateLuaModuleUsage( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("moduleName"), + 42); // invalid module id + + auto script = rlogic_serialization::CreateLuaScript( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + m_flatBufferBuilder.CreateString(m_minimalScript), + m_flatBufferBuilder.CreateVector(std::vector>{ dummyModuleUsageFB }), + m_flatBufferBuilder.CreateVector(std::vector{}), + m_testUtils.serializeTestProperty(""), + m_testUtils.serializeTestProperty(""), + m_flatBufferBuilder.CreateVector(std::vector{1, 0}) + ); + m_flatBufferBuilder.Finish(script); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = LuaScriptImpl::Deserialize(m_solState, serialized, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaScript 'name' module data: could not resolve dependent module with id=42!"); + } +} diff --git a/client/logic/unittests/api/LuaScriptTest_Syntax.cpp b/client/logic/unittests/api/LuaScriptTest_Syntax.cpp new file mode 100644 index 000000000..c958f215d --- /dev/null +++ b/client/logic/unittests/api/LuaScriptTest_Syntax.cpp @@ -0,0 +1,581 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LuaScriptTest_Base.h" + +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" + +#include "fmt/format.h" + +namespace ramses +{ + class ALuaScript_Syntax : public ALuaScript + { + protected: + }; + + TEST_F(ALuaScript_Syntax, ProducesErrorIfNoInterfaceIsPresent) + { + LuaScript* scriptNoInterface = m_logicEngine.createLuaScript(R"( + function run(IN,OUT) + end + )", {}, "scriptNoInterface"); + + ASSERT_EQ(nullptr, scriptNoInterface); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("[scriptNoInterface] No 'interface' function defined!")); + } + + TEST_F(ALuaScript_Syntax, ProducesErrorIfNoRunIsPresent) + { + LuaScript* scriptNoRun = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + )", {}, "scriptNoRun"); + + ASSERT_EQ(nullptr, scriptNoRun); + EXPECT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("[scriptNoRun] No 'run' function defined!")); + } + + TEST_F(ALuaScript_Syntax, CannotBeCreatedFromSyntacticallyIncorrectScript) + { + LuaScript* script = m_logicEngine.createLuaScript("this.is.not.valid.lua.code", {}, "badSyntaxScript"); + ASSERT_EQ(nullptr, script); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("'' expected near 'not'")); + } + + TEST_F(ALuaScript_Syntax, ProducesErrorIfScriptReturnsValue) + { + LuaScript* scriptNoRun = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + end + return "a string" + )"); + + ASSERT_EQ(nullptr, scriptNoRun); + EXPECT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Expected no return value in script source, but a value of type 'string' was returned!")); + } + + TEST_F(ALuaScript_Syntax, PropagatesErrorsEmittedInLua_FromGlobalScope) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + error("Expect this error!") + + function interface(IN,OUT) + end + + function run(IN,OUT) + end + )", WithStdModules({EStandardModule::Base}), "scriptWithErrorInGlobalCode"); + EXPECT_EQ(nullptr, script); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr( + "Expect this error!\nstack traceback:\n" + "\t[C]: in function 'error'")); + } + + TEST_F(ALuaScript_Syntax, PropagatesErrorsEmittedInLua_DuringInterfaceDeclaration) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + error("Expect this error!") + end + + function run(IN,OUT) + end + )", WithStdModules({EStandardModule::Base}), "scriptWithErrorInInterface"); + EXPECT_EQ(nullptr, script); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr( + "Expect this error!\nstack traceback:\n" + "\t[C]: in function 'error'")); + } + + TEST_F(ALuaScript_Syntax, PropagatesErrorsEmittedInLua_DuringRun) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + + function run(IN,OUT) + error("Expect this error!") + end + )", WithStdModules({EStandardModule::Base}), "scriptWithErrorInRun"); + + ASSERT_NE(nullptr, script); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr( + "Expect this error!\n" + "stack traceback:\n" + "\t[C]: in function 'error'")); + EXPECT_EQ(script, m_logicEngine.getErrors()[0].object); + EXPECT_EQ(m_logicEngine.getErrors()[0].object->getName(), "scriptWithErrorInRun"); + } + + TEST_F(ALuaScript_Syntax, ProducesErrorWhenIndexingVectorPropertiesOutOfRange) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.vec2f = Type:Vec2f() + IN.vec3f = Type:Vec3f() + IN.vec4f = Type:Vec4f() + IN.vec2i = Type:Vec2i() + IN.vec3i = Type:Vec3i() + IN.vec4i = Type:Vec4i() + + -- Parametrize test in lua, this simplifies test readibility + IN.propertyName = Type:String() + IN.index = Type:Int32() + end + + function run(IN,OUT) + local message = "Value of " .. IN.propertyName .. "[" .. tostring(IN.index) .. "]" .. " is " .. IN[IN.propertyName][IN.index] + end + )", WithStdModules({EStandardModule::Base}), "scriptOOR"); + Property* inputs = script->getInputs(); + + inputs->getChild("vec2f")->set({1.1f, 1.2f}); + inputs->getChild("vec3f")->set({2.1f, 2.2f, 2.3f}); + inputs->getChild("vec4f")->set({3.1f, 3.2f, 3.3f, 3.4f}); + inputs->getChild("vec2i")->set({1, 2}); + inputs->getChild("vec3i")->set({3, 4, 5}); + inputs->getChild("vec4i")->set({6, 7, 8, 9}); + + Property* index = inputs->getChild("index"); + Property* name = inputs->getChild("propertyName"); + + std::map sizeOfEachType = + { + {"vec2f", 2}, + {"vec3f", 3}, + {"vec4f", 4}, + {"vec2i", 2}, + {"vec3i", 3}, + {"vec4i", 4}, + }; + + + for (const auto& typeSizePair : sizeOfEachType) + { + const std::string& typeName = typeSizePair.first; + int32_t componentCount = typeSizePair.second; + name->set(typeName); + + // Include invalid values -1 and N + 1 + for (int32_t i = -1; i <= componentCount + 1; ++i) + { + index->set(i); + + if (i < 1 || i > componentCount) + { + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + + if (i < 0) + { + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Only non-negative integers supported as array index type! Error while extracting integer: expected non-negative number, received '-1'")); + } + else + { + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr(fmt::format("Bad index '{}', expected 1 <= i <= {}", i, componentCount))); + } + + EXPECT_EQ(m_logicEngine.getErrors()[0].object->getName(), "scriptOOR"); + EXPECT_EQ(m_logicEngine.getErrors()[0].object, script); + } + else + { + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + } + } + } + } + + TEST_F(ALuaScript_Syntax, ForbidsCallingInterfaceFunctionDirectly) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + + function run(IN,OUT) + end + + interface(IN,OUT) + )"); + + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors()[0].message, + ::testing::HasSubstr("Trying to read global variable 'interface' outside the scope of init(), interface() and run() functions! " + "This can cause undefined behavior and is forbidden!")); + } + + TEST_F(ALuaScript_Syntax, ForbidsCallingRunFunctionDirectly) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + + function run(IN,OUT) + end + + run(IN,OUT) + )"); + + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors()[0].message, + ::testing::HasSubstr("Trying to read global variable 'run' outside the scope of init(), interface() and run() functions! " + "This can cause undefined behavior and is forbidden!")); + } + + TEST_F(ALuaScript_Syntax, ForbidsCallingInitFunctionDirectly) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function init() + end + function interface(IN,OUT) + end + function run(IN,OUT) + end + + init() + )"); + + ASSERT_EQ(nullptr, script); + + EXPECT_THAT(m_logicEngine.getErrors()[0].message, + ::testing::HasSubstr("Trying to read global variable 'init' outside the scope of init(), interface() and run() functions! " + "This can cause undefined behavior and is forbidden!")); + } + + TEST_F(ALuaScript_Syntax, CanUseLuaSyntaxForComputingArraySize) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.array = Type:Array(3, Type:Int32()) + OUT.array_size = Type:Int32() + end + + function run(IN,OUT) + OUT.array_size = #IN.array + end + )"); + + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(3, *script->getOutputs()->getChild("array_size")->get()); + } + + TEST_F(ALuaScript_Syntax, CanUseLuaSyntaxForComputingComplexArraySize) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.array = Type:Array(3, + { + vec3 = Type:Vec3f(), + vec4i = Type:Vec4i() + } + ) + OUT.array_size = Type:Int32() + end + + function run(IN,OUT) + OUT.array_size = #IN.array + end + )"); + + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(3, *script->getOutputs()->getChild("array_size")->get()); + } + + TEST_F(ALuaScript_Syntax, CanUseLuaSyntaxForComputingStructSize) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.struct = { + data1 = Type:Vec3f(), + data2 = Type:Vec4i(), + data3 = Type:Int32() + } + OUT.struct_size = Type:Int32() + end + + function run(IN,OUT) + OUT.struct_size = #IN.struct + end + )"); + + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(3, *script->getOutputs()->getChild("struct_size")->get()); + } + + TEST_F(ALuaScript_Syntax, CanUseLuaSyntaxForComputingVec234Size) + { + m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.vec2f = Type:Vec2f() + IN.vec3f = Type:Vec3f() + IN.vec4f = Type:Vec4f() + IN.vec2i = Type:Vec2i() + IN.vec3i = Type:Vec3i() + IN.vec4i = Type:Vec4i() + end + + function run(IN,OUT) + if #IN.vec2i ~= 2 then error("Expected vec2i has size 2!") end + if #IN.vec2f ~= 2 then error("Expected vec2f has size 2!") end + if #IN.vec3i ~= 3 then error("Expected vec3i has size 3!") end + if #IN.vec3f ~= 3 then error("Expected vec3f has size 3!") end + if #IN.vec4i ~= 4 then error("Expected vec4i has size 4!") end + if #IN.vec4f ~= 4 then error("Expected vec4f has size 4!") end + end + )"); + + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ALuaScript_Syntax, CanUseLuaSyntaxForComputingSizeOfStrings) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.string = Type:String() + OUT.string_size = Type:Int32() + end + + function run(IN,OUT) + OUT.string_size = #IN.string + end + )"); + + script->getInputs()->getChild("string")->set("abcde"); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(5, *script->getOutputs()->getChild("string_size")->get()); + } + + TEST_F(ALuaScript_Syntax, RaisesErrorWhenTryingToGetSizeOfNonArrayTypes) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.notArray = Type:Int32() + end + + function run(IN,OUT) + local size = #IN.notArray + end + )", {}, "invalidArraySizeAccess"); + + ASSERT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("attempt to get length of field 'notArray' (a number value)")); + EXPECT_EQ(m_logicEngine.getErrors()[0].object->getName(), "invalidArraySizeAccess"); + EXPECT_EQ(m_logicEngine.getErrors()[0].object, script); + } + + TEST_F(ALuaScript_Syntax, ProdocesErrorWhenIndexingVectorWithNonIntegerIndices) + { + LuaScript* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.vec = Type:Vec4i() + + IN.errorType = Type:String() + end + + function run(IN,OUT) + if IN.errorType == "indexWithNil" then + local thisWillFail = IN.vec[nil] + elseif IN.errorType == "indexIsATable" then + local thisWillFail = IN.vec[{1}] + elseif IN.errorType == "indexIsAString" then + local thisWillFail = IN.vec["nope..."] + elseif IN.errorType == "indexIsAFloat" then + local thisWillFail = IN.vec[1.5] + elseif IN.errorType == "indexIsAUserdata" then + local thisWillFail = IN.vec[IN.vec] + else + error("Test problem - check error cases below") + end + end + )", {}, "invalidIndexingScript"); + Property* inputs = script->getInputs(); + + Property* errorType = inputs->getChild("errorType"); + + std::vector errorTypes = + { + "indexWithNil", + "indexIsATable", + "indexIsAString", + "indexIsAFloat", + "indexIsAUserdata", + }; + + for (const auto& error : errorTypes) + { + errorType->set(error); + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("Only non-negative integers supported as array index type!")); + EXPECT_EQ(m_logicEngine.getErrors()[0].object->getName(), "invalidIndexingScript"); + EXPECT_EQ(m_logicEngine.getErrors()[0].object, script); + } + } + + TEST_F(ALuaScript_Syntax, ReportsErrorWhenTryingToAssignVectorTypes_WithMismatchedComponentCount) + { + const std::vector allCases = + { + { + "OUT.vec2f = {} -- none at all", + "Error while assigning output Vec2 property 'vec2f'. Error while extracting array: expected 2 array components in table but got 0 instead!" + }, + { + "OUT.vec3f = {1, 2, 3, 4} -- more than expected", + "Error while assigning output Vec3 property 'vec3f'. Error while extracting array: expected 3 array components in table but got 4 instead!" + }, + { + "OUT.vec4f = {1, 2, 3} -- fewer than required", + "Error while assigning output Vec4 property 'vec4f'. Error while extracting array: expected 4 array components in table but got 3 instead!" + }, + { + "OUT.vec2i = {1, 2, 'wrong'} -- extra component of wrong type", + "Error while assigning output Vec2 property 'vec2i'. Error while extracting array: expected 2 array components in table but got 3 instead!" + }, + { + "OUT.vec3i = {1, 2, {}} -- extra nested table", + "Error while assigning output Vec3 property 'vec3i'. Error while extracting array: unexpected value (type: 'table') at array element # 3! " + "Reason: Error while extracting integer: expected a number, received 'table'" + }, + { + "OUT.vec4i = {1, 2, nil, 4} -- wrong size, nil in-between", + "Error while assigning output Vec4 property 'vec4i'. Error while extracting array: unexpected value (type: 'nil') at array element # 3! " + "Reason: Error while extracting integer: expected a number, received 'nil'" + }, + { + "OUT.vec4i = {1, 2, nil, 3, 4} -- correct size, nil in-between", + "Error while assigning output Vec4 property 'vec4i'. Error while extracting array: expected 4 array components in table but got 5 instead!" + }, + }; + + for(const auto& errorCase : allCases) + { + std::string scriptSource =(R"( + function interface(IN,OUT) + OUT.vec2f = Type:Vec2f() + OUT.vec3f = Type:Vec3f() + OUT.vec4f = Type:Vec4f() + OUT.vec2i = Type:Vec2i() + OUT.vec3i = Type:Vec3i() + OUT.vec4i = Type:Vec4i() + OUT.nested = { + vec = Type:Vec3i(), + float = Type:Float() + } + end + + function run(IN,OUT) + )"); + scriptSource += errorCase.errorCode; + scriptSource += "\nend\n"; + + LuaScript* script = m_logicEngine.createLuaScript(scriptSource, {}, "mismatchedVecSizes"); + + ASSERT_NE(nullptr, script); + EXPECT_FALSE(m_logicEngine.update()); + + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr(errorCase.expectedErrorMessage)); + EXPECT_EQ(m_logicEngine.getErrors()[0].object->getName(), "mismatchedVecSizes"); + EXPECT_EQ(m_logicEngine.getErrors()[0].object, script); + + EXPECT_TRUE(m_logicEngine.destroy(*script)); + } + } + + TEST_F(ALuaScript_Syntax, ProducesErrorIfRunFunctionDoesNotEndCorrectly) + { + LuaScript* scriptWithWrongEndInRun = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + ENDE + )", {}, "missingEndInScript"); + + ASSERT_EQ(nullptr, scriptWithWrongEndInRun); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("'=' expected near ''")); + } + + TEST_F(ALuaScript_Syntax, ProducesErrorIfInterfaceFunctionDoesNotEndCorrectly) + { + LuaScript* scriptWithWrongEndInInterface = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + ENDE + function run(IN,OUT) + end + )", {}, "missingEndInScript"); + + ASSERT_EQ(nullptr, scriptWithWrongEndInInterface); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("'=' expected near 'function'")); + } + + TEST_F(ALuaScript_Syntax, ProducesErrorIfInterfaceFunctionDoesNotEndAtAll) + { + LuaScript* scriptWithNoEndInInterface = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + function run(IN,OUT) + end + )", {}, "endlessInterface"); + + ASSERT_EQ(nullptr, scriptWithNoEndInInterface); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("'end' expected (to close 'function' at line 2) near ''")); + } + + TEST_F(ALuaScript_Syntax, ProducesErrorIfRunFunctionDoesNotEndAtAll) + { + LuaScript* scriptWithNoEndInRun = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + )", {}, "endlessRun"); + + ASSERT_EQ(nullptr, scriptWithNoEndInRun); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("'end' expected (to close 'function' at line 4) near ''")); + } + + TEST_F(ALuaScript_Syntax, ProducesErrorMessageCorrectlyNotConflictingWithFmtFormatSyntax) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + end + function run(IN,OUT) + local coalaModule = {} + coalaModule.coalaStruct = { + oink1 + oink2 + } + end + )", {}, "missingComma"); + + ASSERT_EQ(nullptr, script); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_THAT(m_logicEngine.getErrors()[0].message, ::testing::HasSubstr("'}' expected (to close '{'")); + } +} diff --git a/client/logic/unittests/api/LuaScriptTest_Types.cpp b/client/logic/unittests/api/LuaScriptTest_Types.cpp new file mode 100644 index 000000000..a8b43a7b3 --- /dev/null +++ b/client/logic/unittests/api/LuaScriptTest_Types.cpp @@ -0,0 +1,247 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LuaScriptTest_Base.h" + +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/Property.h" + +namespace ramses +{ + class ALuaScript_Types : public ALuaScript + { + }; + + TEST_F(ALuaScript_Types, FailsToSetValueOfTopLevelInput_WhenTemplateDoesNotMatchDeclaredInputType) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScriptWithInputs); + auto inputs = script->getInputs(); + + auto speedInt32 = inputs->getChild("speed"); + auto tempFloat = inputs->getChild("temp"); + auto nameString = inputs->getChild("name"); + auto enabledBool = inputs->getChild("enabled"); + auto vec_2f = inputs->getChild("vec2f"); + auto vec_3f = inputs->getChild("vec3f"); + auto vec_4f = inputs->getChild("vec4f"); + auto vec_2i = inputs->getChild("vec2i"); + auto vec_3i = inputs->getChild("vec3i"); + auto vec_4i = inputs->getChild("vec4i"); + + EXPECT_FALSE(speedInt32->set(4711.f)); + EXPECT_FALSE(tempFloat->set(4711)); + EXPECT_FALSE(nameString->set(true)); + EXPECT_FALSE(enabledBool->set("some string")); + EXPECT_FALSE(vec_2f->set(4711.f)); + EXPECT_FALSE(vec_3f->set(4711.f)); + EXPECT_FALSE(vec_4f->set(4711.f)); + EXPECT_FALSE(vec_2i->set(4711)); + EXPECT_FALSE(vec_3i->set(4711)); + EXPECT_FALSE(vec_4i->set(4711)); + } + + TEST_F(ALuaScript_Types, FailsToSetArrayDirectly) + { + auto* script = m_logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.array_int = Type:Array(2, Type:Int32()) + end + + function run(IN,OUT) + end + )"); + auto array_int = script->getInputs()->getChild("array_int"); + + EXPECT_FALSE(array_int->set(4711.f)); + EXPECT_FALSE(array_int->set(4711)); + EXPECT_FALSE(array_int->set(true)); + EXPECT_FALSE(array_int->set("some string")); + } + + TEST_F(ALuaScript_Types, ProvidesIndexBasedAndNameBasedAccessToInputProperties) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScriptWithInputs); + ASSERT_NE(nullptr, script); + auto enabled_byIndex = script->getInputs()->getChild(0); + EXPECT_NE(nullptr, enabled_byIndex); + EXPECT_EQ("enabled", enabled_byIndex->getName()); + auto speed_byName = script->getInputs()->getChild("speed"); + EXPECT_NE(nullptr, speed_byName); + EXPECT_EQ("speed", speed_byName->getName()); + } + + TEST_F(ALuaScript_Types, ProvidesIndexBasedAndNameBasedAccessToOutputProperties) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScriptWithOutputs); + ASSERT_NE(nullptr, script); + auto enabled_byIndex = script->getOutputs()->getChild(0); + EXPECT_NE(nullptr, enabled_byIndex); + EXPECT_EQ("enabled", enabled_byIndex->getName()); + auto speed_byName = script->getOutputs()->getChild("speed"); + EXPECT_NE(nullptr, speed_byName); + EXPECT_EQ("speed", speed_byName->getName()); + } + + TEST_F(ALuaScript_Types, AssignsTypeToGlobalInputsStruct) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScript); + + auto inputs = script->getInputs(); + + ASSERT_EQ(0u, inputs->getChildCount()); + EXPECT_EQ("", inputs->getName()); + EXPECT_EQ(EPropertyType::Struct, inputs->getType()); + } + + TEST_F(ALuaScript_Types, AssignsTypeToGlobalOutputsStruct) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScript); + auto outputs = script->getOutputs(); + + ASSERT_EQ(0u, outputs->getChildCount()); + EXPECT_EQ("", outputs->getName()); + EXPECT_EQ(EPropertyType::Struct, outputs->getType()); + } + + TEST_F(ALuaScript_Types, ReturnsItsTopLevelInputsByIndex_IndexEqualsLexicographicOrder) + { + auto* script = m_logicEngine.createLuaScript(m_minimalScriptWithInputs); + + auto inputs = script->getInputs(); + + ASSERT_EQ(11u, inputs->getChildCount()); + EXPECT_EQ("enabled", inputs->getChild(0)->getName()); + EXPECT_EQ(EPropertyType::Bool, inputs->getChild(0)->getType()); + EXPECT_EQ("name", inputs->getChild(1)->getName()); + EXPECT_EQ(EPropertyType::String, inputs->getChild(1)->getType()); + EXPECT_EQ("speed", inputs->getChild(2)->getName()); + EXPECT_EQ(EPropertyType::Int32, inputs->getChild(2)->getType()); + EXPECT_EQ("speed2", inputs->getChild(3)->getName()); + EXPECT_EQ(EPropertyType::Int64, inputs->getChild(3)->getType()); + EXPECT_EQ("temp", inputs->getChild(4)->getName()); + EXPECT_EQ(EPropertyType::Float, inputs->getChild(4)->getType()); + + // Vec2/3/4 f/i + EXPECT_EQ("vec2f", inputs->getChild(5)->getName()); + EXPECT_EQ(EPropertyType::Vec2f, inputs->getChild(5)->getType()); + EXPECT_EQ("vec2i", inputs->getChild(6)->getName()); + EXPECT_EQ(EPropertyType::Vec2i, inputs->getChild(6)->getType()); + EXPECT_EQ("vec3f", inputs->getChild(7)->getName()); + EXPECT_EQ(EPropertyType::Vec3f, inputs->getChild(7)->getType()); + EXPECT_EQ("vec3i", inputs->getChild(8)->getName()); + EXPECT_EQ(EPropertyType::Vec3i, inputs->getChild(8)->getType()); + EXPECT_EQ("vec4f", inputs->getChild(9)->getName()); + EXPECT_EQ(EPropertyType::Vec4f, inputs->getChild(9)->getType()); + EXPECT_EQ("vec4i", inputs->getChild(10)->getName()); + EXPECT_EQ(EPropertyType::Vec4i, inputs->getChild(10)->getType()); + } + + TEST_F(ALuaScript_Types, ProvidesFullTypeInformationOnArrayProperties) + { + std::string_view scriptWithArrays = R"( + function interface(IN,OUT) + IN.array_int = Type:Array(2, Type:Int32()) + IN.array_float = Type:Array(3, Type:Float()) + OUT.array_int = Type:Array(2, Type:Int32()) + OUT.array_float = Type:Array(3, Type:Float()) + end + + function run(IN,OUT) + end + )"; + + auto* script = m_logicEngine.createLuaScript(scriptWithArrays); + + std::initializer_list rootProperties = {script->getInputs(), script->getOutputs()}; + + for (auto rootProp : rootProperties) + { + ASSERT_EQ(2u, rootProp->getChildCount()); + auto array_int = rootProp->getChild("array_int"); + + EXPECT_EQ("array_int", array_int->getName()); + EXPECT_EQ(EPropertyType::Array, array_int->getType()); + EXPECT_EQ(2u, array_int->getChildCount()); + EXPECT_EQ(EPropertyType::Int32, array_int->getChild(0)->getType()); + EXPECT_EQ(EPropertyType::Int32, array_int->getChild(1)->getType()); + EXPECT_EQ("", array_int->getChild(0)->getName()); + EXPECT_EQ("", array_int->getChild(1)->getName()); + EXPECT_EQ(0u, array_int->getChild(0)->getChildCount()); + EXPECT_EQ(0u, array_int->getChild(1)->getChildCount()); + + auto array_float = rootProp->getChild("array_float"); + EXPECT_EQ("array_float", array_float->getName()); + EXPECT_EQ(EPropertyType::Array, array_float->getType()); + EXPECT_EQ(3u, array_float->getChildCount()); + auto array_float_0 = array_float->getChild(0); + auto array_float_1 = array_float->getChild(1); + auto array_float_2 = array_float->getChild(2); + EXPECT_EQ(EPropertyType::Float, array_float_0->getType()); + EXPECT_EQ(EPropertyType::Float, array_float_1->getType()); + EXPECT_EQ(EPropertyType::Float, array_float_2->getType()); + EXPECT_EQ("", array_float_0->getName()); + EXPECT_EQ("", array_float_1->getName()); + EXPECT_EQ("", array_float_2->getName()); + EXPECT_EQ(0u, array_float_0->getChildCount()); + EXPECT_EQ(0u, array_float_1->getChildCount()); + EXPECT_EQ(0u, array_float_2->getChildCount()); + } + } + + TEST_F(ALuaScript_Types, ProvidesFullTypeInformationOnArrayOfStructProperties) + { + std::string_view scriptWithArrays = R"( + function interface(IN,OUT) + local struct_decl = { + name = Type:String(), + address = + { + street = Type:String(), + number = Type:Int32() + } + } + IN.array_of_structs = Type:Array(2, struct_decl) + OUT.array_of_structs = Type:Array(2, struct_decl) + end + + function run(IN,OUT) + end + )"; + + auto* script = m_logicEngine.createLuaScript(scriptWithArrays); + + std::initializer_list rootProperties = { script->getInputs(), script->getOutputs() }; + + for (auto rootProp : rootProperties) + { + ASSERT_EQ(1u, rootProp->getChildCount()); + const auto arrayOfStructs = rootProp->getChild(0); + + EXPECT_EQ("array_of_structs", arrayOfStructs->getName()); + EXPECT_EQ(EPropertyType::Array, arrayOfStructs->getType()); + ASSERT_EQ(2u, arrayOfStructs->getChildCount()); + + for (size_t i = 0; i < 2; ++i) + { + const auto structChild = arrayOfStructs->getChild(i); + EXPECT_EQ(EPropertyType::Struct, structChild->getType()); + EXPECT_EQ("", structChild->getName()); + ASSERT_EQ(2u, structChild->getChildCount()); + const auto name = structChild->getChild("name"); + EXPECT_EQ(EPropertyType::String, name->getType()); + const auto address = structChild->getChild("address"); + ASSERT_EQ(2u, address->getChildCount()); + EXPECT_EQ(EPropertyType::Struct, address->getType()); + const auto addressStr = address->getChild("street"); + const auto addressNr = address->getChild("number"); + EXPECT_EQ(EPropertyType::String, addressStr->getType()); + EXPECT_EQ(EPropertyType::Int32, addressNr->getType()); + } + } + } +} diff --git a/client/logic/unittests/api/PropertyTest.cpp b/client/logic/unittests/api/PropertyTest.cpp new file mode 100644 index 000000000..54e7f615d --- /dev/null +++ b/client/logic/unittests/api/PropertyTest.cpp @@ -0,0 +1,1154 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" + +#include "impl/PropertyImpl.h" +#include "impl/LuaScriptImpl.h" +#include "internals/SolState.h" +#include "internals/ErrorReporting.h" + +#include "ramses-logic/Property.h" +#include "ramses-logic/LogicEngine.h" + +#include "flatbuffers/flatbuffers.h" + +#include "generated/PropertyGen.h" + +#include "LogicNodeDummy.h" +#include "LogTestUtils.h" + +#include +#include + +namespace ramses::internal +{ + class AProperty : public ::testing::Test + { + public: + LogicNodeDummyImpl m_dummyNode{ "DummyNode" }; + + PropertyUniquePtr CreateInputProperty(EPropertyType type, bool assignDummyLogicNode = true) + { + return CreateProperty(MakeType("", type), EPropertySemantics::ScriptInput, assignDummyLogicNode); + } + + PropertyUniquePtr CreateOutputProperty(EPropertyType type, bool assignDummyLogicNode = true) + { + return CreateProperty(MakeType("", type), EPropertySemantics::ScriptOutput, assignDummyLogicNode); + } + + PropertyUniquePtr CreateBindingInput(EPropertyType type) + { + return CreateProperty(MakeType("", type), EPropertySemantics::BindingInput, true); + } + + PropertyUniquePtr CreateProperty(const HierarchicalTypeData& typeData, EPropertySemantics semantics, bool assignDummyLogicNode) + { + auto property = std::make_unique(typeData, semantics); + if (assignDummyLogicNode) + { + property->setLogicNode(m_dummyNode); + } + + return PropertyImpl::CreateProperty(std::move(property)); + } + + // Silence logs, unless explicitly enabled, to reduce spam and speed up tests + ScopedLogContextLevel m_silenceLogs{ELogLevel::Off}; + }; + + TEST_F(AProperty, HasANameAfterCreation) + { + auto property(CreateProperty(MakeType("PropertyName", EPropertyType::Float), EPropertySemantics::ScriptInput, true)); + EXPECT_EQ("PropertyName", property->getName()); + } + + TEST_F(AProperty, HasATypeAfterCreation) + { + auto property(CreateInputProperty(EPropertyType::Float)); + EXPECT_EQ(EPropertyType::Float, property->getType()); + } + + TEST_F(AProperty, HasAReferenceFromImplToHLObject) + { + auto property(CreateInputProperty(EPropertyType::Float)); + auto& impl = *property->m_impl; + EXPECT_EQ(property.get(), &impl.getPropertyInstance()); + const auto& constImpl = impl; + EXPECT_EQ(property.get(), &constImpl.getPropertyInstance()); + } + + TEST_F(AProperty, IsNotLinkedAfterCreation) + { + auto inputProperty(CreateInputProperty(EPropertyType::Float)); + EXPECT_FALSE(inputProperty->isLinked()); + EXPECT_FALSE(inputProperty->hasIncomingLink()); + EXPECT_FALSE(inputProperty->hasOutgoingLink()); + EXPECT_FALSE(inputProperty->getIncomingLink()); + EXPECT_EQ(0u, inputProperty->getOutgoingLinksCount()); + EXPECT_FALSE(inputProperty->getOutgoingLink(0u)); + auto outputProperty(CreateOutputProperty(EPropertyType::Float)); + EXPECT_FALSE(outputProperty->isLinked()); + EXPECT_FALSE(outputProperty->hasIncomingLink()); + EXPECT_FALSE(outputProperty->hasOutgoingLink()); + EXPECT_FALSE(outputProperty->getIncomingLink()); + EXPECT_EQ(0u, outputProperty->getOutgoingLinksCount()); + EXPECT_FALSE(outputProperty->getOutgoingLink(0u)); + } + + TEST_F(AProperty, CanBeLinkedAndUnlinked) + { + auto property1(CreateOutputProperty(EPropertyType::Float)); + auto property2(CreateInputProperty(EPropertyType::Float)); + property2->m_impl->setIncomingLink(*property1->m_impl, false); + EXPECT_FALSE(property1->m_impl->isInput()); + EXPECT_TRUE(property1->m_impl->isOutput()); + EXPECT_TRUE(property1->isLinked()); + EXPECT_TRUE(property1->hasOutgoingLink()); + EXPECT_FALSE(property1->hasIncomingLink()); + EXPECT_FALSE(property1->getIncomingLink()); + EXPECT_TRUE(property2->m_impl->isInput()); + EXPECT_FALSE(property2->m_impl->isOutput()); + EXPECT_TRUE(property2->isLinked()); + EXPECT_TRUE(property2->hasIncomingLink()); + EXPECT_FALSE(property2->hasOutgoingLink()); + ASSERT_TRUE(property2->getIncomingLink()); + EXPECT_EQ(property2->getIncomingLink()->source, property1.get()); + EXPECT_EQ(property2->getIncomingLink()->target, property2.get()); + EXPECT_FALSE(property2->getIncomingLink()->isWeakLink); + ASSERT_EQ(1u, property1->getOutgoingLinksCount()); + ASSERT_TRUE(property1->getOutgoingLink(0u)); + EXPECT_EQ(property1->getOutgoingLink(0u)->source, property1.get()); + EXPECT_EQ(property1->getOutgoingLink(0u)->target, property2.get()); + EXPECT_FALSE(property1->getOutgoingLink(0u)->isWeakLink); + + property2->m_impl->resetIncomingLink(); + EXPECT_FALSE(property1->isLinked()); + EXPECT_FALSE(property2->isLinked()); + EXPECT_FALSE(property1->getIncomingLink()); + EXPECT_EQ(0u, property1->getOutgoingLinksCount()); + EXPECT_FALSE(property2->getIncomingLink()); + EXPECT_EQ(0u, property2->getOutgoingLinksCount()); + } + + TEST_F(AProperty, CanLinkInterfaceProperties) + { + auto property1(CreateProperty(MakeType("name", EPropertyType::Float), EPropertySemantics::Interface, true)); + auto property2(CreateProperty(MakeType("name", EPropertyType::Float), EPropertySemantics::Interface, true)); + property2->m_impl->setIncomingLink(*property1->m_impl, false); + EXPECT_TRUE(property1->m_impl->isInput()); + EXPECT_TRUE(property1->m_impl->isOutput()); + EXPECT_TRUE(property1->isLinked()); + EXPECT_TRUE(property1->hasOutgoingLink()); + EXPECT_FALSE(property1->hasIncomingLink()); + EXPECT_TRUE(property2->m_impl->isInput()); + EXPECT_TRUE(property2->m_impl->isOutput()); + EXPECT_TRUE(property2->isLinked()); + EXPECT_TRUE(property2->hasIncomingLink()); + EXPECT_FALSE(property2->hasOutgoingLink()); + } + + TEST_F(AProperty, CanBeLinkedAndUnlinked_weakLink) + { + auto property1(CreateOutputProperty(EPropertyType::Float)); + auto property2(CreateInputProperty(EPropertyType::Float)); + property2->m_impl->setIncomingLink(*property1->m_impl, true); + EXPECT_FALSE(property1->m_impl->isInput()); + EXPECT_TRUE(property1->m_impl->isOutput()); + EXPECT_TRUE(property1->isLinked()); + EXPECT_TRUE(property1->hasOutgoingLink()); + EXPECT_FALSE(property1->hasIncomingLink()); + EXPECT_FALSE(property1->getIncomingLink()); + EXPECT_TRUE(property2->m_impl->isInput()); + EXPECT_FALSE(property2->m_impl->isOutput()); + EXPECT_TRUE(property2->isLinked()); + EXPECT_TRUE(property2->hasIncomingLink()); + EXPECT_FALSE(property2->hasOutgoingLink()); + ASSERT_TRUE(property2->getIncomingLink()); + EXPECT_EQ(property2->getIncomingLink()->source, property1.get()); + EXPECT_EQ(property2->getIncomingLink()->target, property2.get()); + EXPECT_TRUE(property2->getIncomingLink()->isWeakLink); + ASSERT_EQ(1u, property1->getOutgoingLinksCount()); + ASSERT_TRUE(property1->getOutgoingLink(0u)); + EXPECT_EQ(property1->getOutgoingLink(0u)->source, property1.get()); + EXPECT_EQ(property1->getOutgoingLink(0u)->target, property2.get()); + EXPECT_TRUE(property1->getOutgoingLink(0u)->isWeakLink); + + property2->m_impl->resetIncomingLink(); + EXPECT_FALSE(property1->isLinked()); + EXPECT_FALSE(property2->isLinked()); + EXPECT_FALSE(property1->getIncomingLink()); + EXPECT_EQ(0u, property1->getOutgoingLinksCount()); + EXPECT_FALSE(property2->getIncomingLink()); + EXPECT_EQ(0u, property2->getOutgoingLinksCount()); + } + + TEST_F(AProperty, ReportsNoOutgoingLinkIfOutOfBounds) + { + auto property1(CreateOutputProperty(EPropertyType::Float)); + auto property2(CreateInputProperty(EPropertyType::Float)); + auto property3(CreateInputProperty(EPropertyType::Float)); + property2->m_impl->setIncomingLink(*property1->m_impl, true); + property3->m_impl->setIncomingLink(*property1->m_impl, false); + + ASSERT_EQ(2u, property1->getOutgoingLinksCount()); + // link 1 + ASSERT_TRUE(property1->getOutgoingLink(0u)); + EXPECT_EQ(property1->getOutgoingLink(0u)->source, property1.get()); + EXPECT_EQ(property1->getOutgoingLink(0u)->target, property2.get()); + EXPECT_TRUE(property1->getOutgoingLink(0u)->isWeakLink); + // link 2 + ASSERT_TRUE(property1->getOutgoingLink(1u)); + EXPECT_EQ(property1->getOutgoingLink(1u)->source, property1.get()); + EXPECT_EQ(property1->getOutgoingLink(1u)->target, property3.get()); + EXPECT_FALSE(property1->getOutgoingLink(1u)->isWeakLink); + // link 3 does not exist + EXPECT_FALSE(property1->getOutgoingLink(2u)); + } + + TEST_F(AProperty, CanBeInitializedWithAValue) + { + PropertyImpl property(MakeType("", EPropertyType::Float), EPropertySemantics::ScriptInput, PropertyValue{0.5f}); + EXPECT_FLOAT_EQ(0.5f, property.getValueAs()); + } + + TEST_F(AProperty, CanCheckIfPropertyHasChild) + { + const auto propertyWithChildren(CreateProperty(MakeStruct("", { {"child1", EPropertyType::Int32}, {"child2", EPropertyType::Float} }), EPropertySemantics::ScriptInput, false)); + ASSERT_EQ(2u, propertyWithChildren->getChildCount()); + + EXPECT_FALSE(propertyWithChildren->hasChild("invalidChildName")); + EXPECT_EQ(nullptr, propertyWithChildren->getChild("invalidChildName")); + EXPECT_TRUE(propertyWithChildren->hasChild("child1")); + EXPECT_NE(nullptr, propertyWithChildren->getChild("child1")); + EXPECT_TRUE(propertyWithChildren->hasChild("child2")); + EXPECT_NE(nullptr, propertyWithChildren->getChild("child2")); + } + + TEST_F(AProperty, BindingInputHasNoUserValueBeforeSetExplicitly) + { + auto prop(CreateBindingInput(EPropertyType::Float)); + + EXPECT_FALSE(prop->m_impl->bindingInputHasNewValue()); + EXPECT_FALSE(prop->m_impl->checkForBindingInputNewValueAndReset()); + } + + TEST_F(AProperty, BindingInputHasUserValueAfterSetIsCalledSuccessfully) + { + auto prop(CreateBindingInput(EPropertyType::Float)); + + EXPECT_FALSE(prop->m_impl->bindingInputHasNewValue()); + // Set with wrong type (failed sets) have no effect on user value status + EXPECT_FALSE(prop->set(5)); + EXPECT_FALSE(prop->m_impl->bindingInputHasNewValue()); + EXPECT_TRUE(prop->set(0.5f)); + EXPECT_TRUE(prop->m_impl->checkForBindingInputNewValueAndReset()); + } + + TEST_F(AProperty, BindingInputHasUserValueAfterLinkIsActivated_andValueChanged) + { + auto linkTarget(CreateBindingInput(EPropertyType::Float)); + auto linkSource(CreateProperty(MakeType("", EPropertyType::Float), EPropertySemantics::ScriptOutput, true)); + + // Set to different than default value + linkSource->m_impl->setValue({0.5f}); + + // Simulate link behavior + linkTarget->m_impl->setValue(linkSource->m_impl->getValue()); + EXPECT_TRUE(linkTarget->m_impl->checkForBindingInputNewValueAndReset()); + } + + TEST_F(AProperty, BindingInputHasNewUserValueAfterLinkIsActivated_whenNewValueSameAsOldValue) + { + auto linkTarget(CreateBindingInput(EPropertyType::Float)); + auto linkSource(CreateOutputProperty(EPropertyType::Float)); + + // Set same value to both + linkSource->set(.5f); + linkTarget->set(.5f); + + // Simulate link behavior + linkTarget->m_impl->setValue(linkSource->m_impl->getValue()); + EXPECT_TRUE(linkTarget->m_impl->checkForBindingInputNewValueAndReset()); + } + + TEST_F(AProperty, BindingInputHasNoUserValueAnymore_WhenConsumed) + { + auto prop(CreateBindingInput(EPropertyType::Float)); + + ASSERT_FALSE(prop->m_impl->bindingInputHasNewValue()); + EXPECT_TRUE(prop->set(0.5f)); + // Consume value => has no value any more + EXPECT_TRUE(prop->m_impl->checkForBindingInputNewValueAndReset()); + EXPECT_FALSE(prop->m_impl->bindingInputHasNewValue()); + } + + TEST_F(AProperty, DoesntHaveChildrenAfterCreation) + { + auto desc(CreateInputProperty(EPropertyType::Float)); + ASSERT_EQ(0u, desc->getChildCount()); + } + + TEST_F(AProperty, ReturnsDefaultValue_ForPrimitiveTypes) + { + auto aFloat(CreateInputProperty(EPropertyType::Float)); + ASSERT_TRUE(aFloat->get()); + EXPECT_FLOAT_EQ(0.0f, *aFloat->get()); + + auto aInt(CreateInputProperty(EPropertyType::Int32)); + ASSERT_TRUE(aInt->get()); + EXPECT_EQ(0, *aInt->get()); + + auto aInt64(CreateInputProperty(EPropertyType::Int64)); + ASSERT_TRUE(aInt64->get()); + EXPECT_EQ(0L, *aInt64->get()); + + auto aBool(CreateInputProperty(EPropertyType::Bool)); + ASSERT_TRUE(aBool->get()); + EXPECT_EQ(false, *aBool->get()); + + auto aString(CreateInputProperty(EPropertyType::String)); + ASSERT_TRUE(aString->get()); + EXPECT_EQ("", *aString->get()); + } + + TEST_F(AProperty, ReturnsDefaultValue_VectorTypes) + { + auto aVec2f(CreateInputProperty(EPropertyType::Vec2f)); + auto aVec3f(CreateInputProperty(EPropertyType::Vec3f)); + auto aVec4f(CreateInputProperty(EPropertyType::Vec4f)); + auto aVec2i(CreateInputProperty(EPropertyType::Vec2i)); + auto aVec3i(CreateInputProperty(EPropertyType::Vec3i)); + auto aVec4i(CreateInputProperty(EPropertyType::Vec4i)); + + EXPECT_TRUE(aVec2f->get()); + EXPECT_TRUE(aVec3f->get()); + EXPECT_TRUE(aVec4f->get()); + EXPECT_TRUE(aVec2i->get()); + EXPECT_TRUE(aVec3i->get()); + EXPECT_TRUE(aVec4i->get()); + + vec2f vec2fValue = *aVec2f->get(); + vec3f vec3fValue = *aVec3f->get(); + vec4f vec4fValue = *aVec4f->get(); + + EXPECT_FLOAT_EQ(0.0f, vec2fValue[0]); + EXPECT_FLOAT_EQ(0.0f, vec2fValue[1]); + + EXPECT_FLOAT_EQ(0.0f, vec3fValue[0]); + EXPECT_FLOAT_EQ(0.0f, vec3fValue[1]); + EXPECT_FLOAT_EQ(0.0f, vec3fValue[2]); + + EXPECT_FLOAT_EQ(0.0f, vec4fValue[0]); + EXPECT_FLOAT_EQ(0.0f, vec4fValue[1]); + EXPECT_FLOAT_EQ(0.0f, vec4fValue[2]); + EXPECT_FLOAT_EQ(0.0f, vec4fValue[3]); + + vec2i vec2iValue = *aVec2i->get(); + vec3i vec3iValue = *aVec3i->get(); + vec4i vec4iValue = *aVec4i->get(); + + EXPECT_EQ(0, vec2iValue[0]); + EXPECT_EQ(0, vec2iValue[1]); + + EXPECT_EQ(0, vec3iValue[0]); + EXPECT_EQ(0, vec3iValue[1]); + EXPECT_EQ(0, vec3iValue[2]); + + EXPECT_EQ(0, vec4iValue[0]); + EXPECT_EQ(0, vec4iValue[1]); + EXPECT_EQ(0, vec4iValue[2]); + EXPECT_EQ(0, vec4iValue[3]); + } + + TEST_F(AProperty, ReturnsValueIfItIsSetBeforehand_PrimitiveTypes) + { + auto aFloat(CreateInputProperty(EPropertyType::Float)); + auto aInt32(CreateInputProperty(EPropertyType::Int32)); + auto aInt64(CreateInputProperty(EPropertyType::Int64)); + auto aBool(CreateInputProperty(EPropertyType::Bool)); + auto aString(CreateInputProperty(EPropertyType::String)); + + EXPECT_TRUE(aFloat->set(47.11f)); + EXPECT_TRUE(aInt32->set(5)); + EXPECT_TRUE(aInt64->set(7)); + EXPECT_TRUE(aBool->set(true)); + EXPECT_TRUE(aString->set("hello")); + + const auto valueFloat = aFloat->get(); + const auto valueInt32 = aInt32->get(); + const auto valueInt64 = aInt64->get(); + const auto valueBool = aBool->get(); + const auto valueString = aString->get(); + ASSERT_TRUE(valueFloat); + ASSERT_TRUE(valueInt32); + ASSERT_TRUE(valueInt64); + ASSERT_TRUE(valueBool); + ASSERT_TRUE(valueString); + + EXPECT_FLOAT_EQ(47.11f, *valueFloat); + EXPECT_EQ(5, *valueInt32); + EXPECT_EQ(7, *valueInt64); + EXPECT_EQ(true, *valueBool); + EXPECT_EQ("hello", *valueString); + } + + TEST_F(AProperty, ReturnsValueIfItIsSetBeforehand_VectorTypes_Float) + { + auto avec2f(CreateInputProperty(EPropertyType::Vec2f)); + auto avec3f(CreateInputProperty(EPropertyType::Vec3f)); + auto avec4f(CreateInputProperty(EPropertyType::Vec4f)); + + EXPECT_TRUE(avec2f->set({ 0.1f, 0.2f })); + EXPECT_TRUE(avec3f->set({ 0.1f, 0.2f, 0.3f })); + EXPECT_TRUE(avec4f->set({ 0.1f, 0.2f, 0.3f, 0.4f })); + + auto valueVec2f = avec2f->get(); + auto valueVec3f = avec3f->get(); + auto valueVec4f = avec4f->get(); + ASSERT_TRUE(valueVec2f); + ASSERT_TRUE(valueVec3f); + ASSERT_TRUE(valueVec4f); + + vec2f expectedValueVec2f {0.1f, 0.2f}; + vec3f expectedValueVec3f {0.1f, 0.2f, 0.3f}; + vec4f expectedValueVec4f {0.1f, 0.2f, 0.3f, 0.4f}; + EXPECT_TRUE(expectedValueVec2f == *valueVec2f); + EXPECT_TRUE(expectedValueVec3f == *valueVec3f); + EXPECT_TRUE(expectedValueVec4f == *valueVec4f); + } + + TEST_F(AProperty, ReturnsValueIfItIsSetBeforehand_VectorTypes_Int) + { + auto avec2i(CreateInputProperty(EPropertyType::Vec2i)); + auto avec3i(CreateInputProperty(EPropertyType::Vec3i)); + auto avec4i(CreateInputProperty(EPropertyType::Vec4i)); + + EXPECT_TRUE(avec2i->set({1, 2})); + EXPECT_TRUE(avec3i->set({1, 2, 3})); + EXPECT_TRUE(avec4i->set({1, 2, 3, 4})); + + auto valueVec2i = avec2i->get(); + auto valueVec3i = avec3i->get(); + auto valueVec4i = avec4i->get(); + ASSERT_TRUE(valueVec2i); + ASSERT_TRUE(valueVec3i); + ASSERT_TRUE(valueVec4i); + + vec2i expectedValueVec2i{1, 2}; + vec3i expectedValueVec3i{1, 2, 3}; + vec4i expectedValueVec4i{1, 2, 3, 4}; + EXPECT_TRUE(expectedValueVec2i == *valueVec2i); + EXPECT_TRUE(expectedValueVec3i == *valueVec3i); + EXPECT_TRUE(expectedValueVec4i == *valueVec4i); + } + + TEST_F(AProperty, IsInitializedAsInputOrOutput) + { + auto inputProperty(CreateInputProperty(EPropertyType::Float)); + auto outputProperty(CreateOutputProperty(EPropertyType::Int32)); + + EXPECT_TRUE(inputProperty->m_impl->isInput()); + EXPECT_FALSE(inputProperty->m_impl->isOutput()); + EXPECT_EQ(internal::EPropertySemantics::ScriptInput, inputProperty->m_impl->getPropertySemantics()); + EXPECT_TRUE(outputProperty->m_impl->isOutput()); + EXPECT_FALSE(outputProperty->m_impl->isInput()); + EXPECT_EQ(internal::EPropertySemantics::ScriptOutput, outputProperty->m_impl->getPropertySemantics()); + } + + TEST_F(AProperty, CannotSetOutputManually) + { + auto outputProperty(CreateOutputProperty(EPropertyType::Int32)); + + EXPECT_TRUE(outputProperty->m_impl->isOutput()); + EXPECT_EQ(internal::EPropertySemantics::ScriptOutput, outputProperty->m_impl->getPropertySemantics()); + + EXPECT_FALSE(outputProperty->set(45)); + } + + TEST_F(AProperty, ReturnsNoValueWhenAccessingWithWrongType) + { + auto floatProp(CreateInputProperty(EPropertyType::Float)); + auto vec2fProp(CreateInputProperty(EPropertyType::Vec2f)); + auto vec3fProp(CreateInputProperty(EPropertyType::Vec3f)); + auto vec4fProp(CreateInputProperty(EPropertyType::Vec4f)); + auto int32Prop(CreateInputProperty(EPropertyType::Int32)); + auto int64Prop(CreateInputProperty(EPropertyType::Int64)); + auto vec2iProp(CreateInputProperty(EPropertyType::Vec2i)); + auto vec3iProp(CreateInputProperty(EPropertyType::Vec3i)); + auto vec4iProp(CreateInputProperty(EPropertyType::Vec4i)); + auto boolProp(CreateInputProperty(EPropertyType::Bool)); + auto stringProp(CreateInputProperty(EPropertyType::String)); + auto structProp(CreateInputProperty(EPropertyType::Struct)); + auto arrayProp(CreateInputProperty(EPropertyType::Array)); + + // Floats + EXPECT_TRUE(floatProp->get()); + EXPECT_FALSE(floatProp->get()); + + EXPECT_TRUE(vec2fProp->get()); + EXPECT_FALSE(vec2fProp->get()); + + EXPECT_TRUE(vec3fProp->get()); + EXPECT_FALSE(vec3fProp->get()); + + EXPECT_TRUE(vec4fProp->get()); + EXPECT_FALSE(vec4fProp->get()); + + // Integers + EXPECT_TRUE(int32Prop->get()); + EXPECT_FALSE(int32Prop->get()); + EXPECT_FALSE(int32Prop->get()); + + EXPECT_TRUE(int64Prop->get()); + EXPECT_FALSE(int64Prop->get()); + EXPECT_FALSE(int64Prop->get()); + + EXPECT_TRUE(vec2iProp->get()); + EXPECT_FALSE(vec2iProp->get()); + + EXPECT_TRUE(vec3iProp->get()); + EXPECT_FALSE(vec3iProp->get()); + + EXPECT_TRUE(vec4iProp->get()); + EXPECT_FALSE(vec4iProp->get()); + + // Others + EXPECT_TRUE(boolProp->get()); + EXPECT_FALSE(boolProp->get()); + + EXPECT_TRUE(stringProp->get()); + EXPECT_FALSE(stringProp->get()); + + // Complex types never have value + EXPECT_FALSE(structProp->get()); + EXPECT_FALSE(structProp->get()); + + EXPECT_FALSE(arrayProp->get()); + EXPECT_FALSE(arrayProp->get()); + } + + TEST_F(AProperty, ReturnsNullptrForGetChildByIndexIfPropertyHasNoChildren) + { + auto property_float(CreateInputProperty(EPropertyType::Float)); + + EXPECT_EQ(nullptr, property_float->getChild(0)); + } + + TEST_F(AProperty, ReturnsNullptrForGetChildByNameIfPropertyHasNoChildren) + { + auto property_float(CreateInputProperty(EPropertyType::Float)); + + EXPECT_EQ(nullptr, property_float->getChild("child")); + } + + TEST_F(AProperty, CanBeEmptyAndConst) + { + auto rootImpl = CreateInputProperty(EPropertyType::Struct); + const auto root(std::move(rootImpl)); + + const auto child = root->getChild(0); + EXPECT_EQ(nullptr, child); + } + + TEST_F(AProperty, CanHaveNestedProperties) + { + auto root(CreateProperty(MakeStruct("", {{"child1", EPropertyType::Int32}, {"child2", EPropertyType::Float}}), EPropertySemantics::ScriptInput, false)); + + ASSERT_EQ(2u, root->getChildCount()); + + auto c1 = root->getChild(0); + auto c2 = root->getChild(1); + + EXPECT_EQ("child1", c1->getName()); + EXPECT_EQ("child2", c2->getName()); + + const auto& const_root = root; + auto c3 = const_root->getChild(0); + auto c4 = const_root->getChild(1); + + EXPECT_EQ("child1", c3->getName()); + EXPECT_EQ("child2", c4->getName()); + } + + TEST_F(AProperty, SetsValueIfTheTypeMatches) + { + auto floatProperty(CreateInputProperty(EPropertyType::Float)); + auto int32Property(CreateInputProperty(EPropertyType::Int32)); + auto int64Property(CreateInputProperty(EPropertyType::Int64)); + auto stringProperty(CreateInputProperty(EPropertyType::String)); + auto boolProperty(CreateInputProperty(EPropertyType::Bool)); + + EXPECT_TRUE(floatProperty->set(47.11f)); + EXPECT_TRUE(int32Property->set(4711)); + EXPECT_TRUE(int64Property->set(4711111)); + EXPECT_TRUE(stringProperty->set("4711")); + EXPECT_TRUE(boolProperty->set(true)); + + const auto floatValue = floatProperty->get(); + const auto int32Value = int32Property->get(); + const auto int64Value = int64Property->get(); + const auto stringValue = stringProperty->get(); + const auto boolValue = boolProperty->get(); + + ASSERT_TRUE(floatValue); + ASSERT_TRUE(int32Value); + ASSERT_TRUE(int64Value); + ASSERT_TRUE(stringValue); + ASSERT_TRUE(boolValue); + + ASSERT_EQ(47.11f, *floatValue); + ASSERT_EQ(4711, *int32Value); + ASSERT_EQ(4711111, *int64Value); + ASSERT_EQ("4711", *stringValue); + ASSERT_EQ(true, *boolValue); + } + + TEST_F(AProperty, DoesNotSetValueIfTheTypeDoesNotMatch) + { + auto floatProperty(CreateInputProperty(EPropertyType::Float)); + auto int32Property(CreateInputProperty(EPropertyType::Int32)); + auto int64Property(CreateInputProperty(EPropertyType::Int64)); + auto stringProperty(CreateInputProperty(EPropertyType::String)); + auto boolProperty(CreateInputProperty(EPropertyType::Bool)); + + EXPECT_FALSE(floatProperty->set(4711)); + EXPECT_FALSE(int32Property->set(47.11f)); + EXPECT_FALSE(int64Property->set(47)); + EXPECT_FALSE(stringProperty->set(true)); + EXPECT_FALSE(boolProperty->set("4711")); + EXPECT_FALSE(floatProperty->set({0.1f, 0.2f})); + + const auto floatValue = floatProperty->get(); + const auto int32Value = int32Property->get(); + const auto int64Value = int64Property->get(); + const auto stringValue = stringProperty->get(); + const auto boolValue = boolProperty->get(); + + EXPECT_TRUE(floatValue); + EXPECT_TRUE(int32Value); + EXPECT_TRUE(int64Value); + EXPECT_TRUE(stringValue); + EXPECT_TRUE(boolValue); + EXPECT_EQ(0.0f, floatValue); + EXPECT_EQ(0, int32Value); + EXPECT_EQ(0, int64Value); + EXPECT_EQ("", stringValue); + EXPECT_EQ(false, boolValue); + } + + TEST_F(AProperty, ReturnsChildByName) + { + auto root(CreateProperty(MakeStruct("", { {"child1", EPropertyType::Int32}, {"child2", EPropertyType::Float} }), EPropertySemantics::ScriptInput, true)); + + const auto* c1 = root->getChild("child1"); + EXPECT_EQ("child1", c1->getName()); + + const auto* c2 = root->getChild("child2"); + EXPECT_EQ("child2", c2->getName()); + + const auto* c3 = root->getChild("does_not_exist"); + EXPECT_FALSE(c3); + + const auto& rootConst = root; + c1 = rootConst->getChild("child1"); + c2 = rootConst->getChild("child2"); + EXPECT_EQ("child1", c1->getName()); + EXPECT_EQ("child2", c2->getName()); + } + + TEST_F(AProperty, ReturnsConstChildByName) + { + auto root(CreateProperty(MakeStruct("", { {"child1", EPropertyType::Int32}, {"child2", EPropertyType::Float} }), EPropertySemantics::ScriptInput, true)); + + const auto* c1 = root->getChild("child1"); + EXPECT_EQ("child1", c1->getName()); + + const auto* c2 = root->getChild("child1"); + EXPECT_EQ("child1", c2->getName()); + + const auto* c3 = root->getChild("does_not_exist"); + EXPECT_FALSE(c3); + } + + class AProperty_SerializationLifecycle : public AProperty + { + protected: + ErrorReporting m_errorReporting; + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + SerializationMap m_serializationMap; + DeserializationMap m_deserializationMap; + }; + + TEST_F(AProperty_SerializationLifecycle, StructWithoutChildren) + { + { + PropertyImpl structNoChildren(MakeType("noChildren", EPropertyType::Struct), EPropertySemantics::ScriptInput); + (void)PropertyImpl::Serialize(structNoChildren, m_flatBufferBuilder, m_serializationMap); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + EXPECT_EQ(serialized.name()->string_view(), "noChildren"); + EXPECT_EQ(serialized.children()->size(), 0u); + + { + std::unique_ptr deserialized = PropertyImpl::Deserialize(serialized, EPropertySemantics::ScriptInput, m_errorReporting, m_deserializationMap); + ASSERT_EQ(0u, deserialized->getChildCount()); + EXPECT_EQ(EPropertyType::Struct, deserialized->getType()); + EXPECT_EQ("noChildren", deserialized->getName()); + EXPECT_EQ(EPropertySemantics::ScriptInput, deserialized->getPropertySemantics()); + } + } + + TEST_F(AProperty_SerializationLifecycle, KeepsPropertyOrder) + { + { + auto structType = MakeStruct("parent", + { + TypeData{"child0", EPropertyType::Float}, + TypeData{"child1", EPropertyType::Float}, + TypeData{"child2", EPropertyType::Float} + }); + PropertyImpl structProperty(structType, EPropertySemantics::ScriptInput); + (void)PropertyImpl::Serialize(structProperty, m_flatBufferBuilder, m_serializationMap); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = PropertyImpl::Deserialize(serialized, EPropertySemantics::ScriptInput, m_errorReporting, m_deserializationMap); + + ASSERT_EQ(3u, deserialized->getChildCount()); + EXPECT_EQ(EPropertyType::Struct, deserialized->getType()); + + EXPECT_EQ("child0", deserialized->getChild(0)->getName()); + EXPECT_EQ("child1", deserialized->getChild(1)->getName()); + EXPECT_EQ("child2", deserialized->getChild(2)->getName()); + } + + TEST_F(AProperty_SerializationLifecycle, MultiLevelNesting) + { + { + const HierarchicalTypeData rootType(TypeData{"root", EPropertyType::Struct}, + { + HierarchicalTypeData(TypeData{"nested", EPropertyType::Struct}, + { + MakeType("float", EPropertyType::Float), + MakeStruct("nested", {TypeData("float", EPropertyType::Float)}) + }) + } + ); + + PropertyImpl root(rootType, EPropertySemantics::ScriptInput); + (void)PropertyImpl::Serialize(root, m_flatBufferBuilder, m_serializationMap); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = PropertyImpl::Deserialize(serialized, EPropertySemantics::ScriptInput, m_errorReporting, m_deserializationMap); + + ASSERT_EQ(1u, deserialized->getChildCount()); + EXPECT_EQ(EPropertyType::Struct, deserialized->getType()); + + auto propertyNested1 = deserialized->getChild(0u); + EXPECT_EQ(EPropertyType::Struct, propertyNested1->getType()); + EXPECT_EQ("nested", propertyNested1->getName()); + + ASSERT_EQ(2u, propertyNested1->getChildCount()); + auto propertyFloat1 = propertyNested1->getChild(0u); + auto propertyNested2 = propertyNested1->getChild(1u); + + EXPECT_EQ(EPropertyType::Float, propertyFloat1->getType()); + EXPECT_EQ("float", propertyFloat1->getName()); + EXPECT_EQ(EPropertyType::Struct, propertyNested2->getType()); + EXPECT_EQ("nested", propertyNested2->getName()); + + ASSERT_EQ(1u, propertyNested2->getChildCount()); + auto propertyFloat2 = propertyNested2->getChild(0u); + + EXPECT_EQ(EPropertyType::Float, propertyFloat2->getType()); + EXPECT_EQ("float", propertyFloat2->getName()); + } + + // Making this test templated makes it a lot harder to read, better leave it so - simple, stupid + TEST_F(AProperty_SerializationLifecycle, AllSupportedPropertyTypes) + { + { + auto rootImpl = CreateProperty(MakeStruct("root", + { + {"Int32", EPropertyType::Int32}, + {"Int64", EPropertyType::Int64}, + {"Float", EPropertyType::Float}, + {"Bool", EPropertyType::Bool}, + {"String", EPropertyType::String}, + {"Vec2f", EPropertyType::Vec2f}, + {"Vec3f", EPropertyType::Vec3f}, + {"Vec4f", EPropertyType::Vec4f}, + {"Vec2i", EPropertyType::Vec2i}, + {"Vec3i", EPropertyType::Vec3i}, + {"Vec4i", EPropertyType::Vec4i}, + {"DefaultValue", EPropertyType::Vec4i}, + }), EPropertySemantics::ScriptInput, true); + + auto propInt32 = rootImpl->getChild("Int32"); + auto propInt64 = rootImpl->getChild("Int64"); + auto propFloat = rootImpl->getChild("Float"); + auto propBool = rootImpl->getChild("Bool"); + auto propString = rootImpl->getChild("String"); + auto propVec2f = rootImpl->getChild("Vec2f"); + auto propVec3f = rootImpl->getChild("Vec3f"); + auto propVec4f = rootImpl->getChild("Vec4f"); + auto propVec2i = rootImpl->getChild("Vec2i"); + auto propVec3i = rootImpl->getChild("Vec3i"); + auto propVec4i = rootImpl->getChild("Vec4i"); + + propInt32->set(4711); + propInt64->set(4711111); + propFloat->set(47.11f); + propBool->set(true); + propString->set("4711"); + propVec2f->set({0.1f, 0.2f}); + propVec3f->set({1.1f, 1.2f, 1.3f}); + propVec4f->set({2.1f, 2.2f, 2.3f, 2.4f}); + propVec2i->set({1, 2}); + propVec3i->set({3, 4, 5}); + propVec4i->set({6, 7, 8, 9}); + + (void)PropertyImpl::Serialize(*rootImpl->m_impl, m_flatBufferBuilder, m_serializationMap); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = PropertyImpl::Deserialize(serialized, EPropertySemantics::ScriptInput, m_errorReporting, m_deserializationMap); + + ASSERT_EQ(12u, deserialized->getChildCount()); + EXPECT_EQ(EPropertyType::Struct, deserialized->getType()); + + const auto propInt32 = deserialized->getChild(0); + const auto propInt64 = deserialized->getChild(1); + const auto propFloat = deserialized->getChild(2); + const auto propBool = deserialized->getChild(3); + const auto propString = deserialized->getChild(4); + const auto propVec2f = deserialized->getChild(5); + const auto propVec3f = deserialized->getChild(6); + const auto propVec4f = deserialized->getChild(7); + const auto propVec2i = deserialized->getChild(8); + const auto propVec3i = deserialized->getChild(9); + const auto propVec4i = deserialized->getChild(10); + const auto propDefValue = deserialized->getChild(11); + + EXPECT_EQ("Int32", propInt32->getName()); + EXPECT_EQ("Int64", propInt64->getName()); + EXPECT_EQ("Float", propFloat->getName()); + EXPECT_EQ("Bool", propBool->getName()); + EXPECT_EQ("String", propString->getName()); + EXPECT_EQ("Vec2f", propVec2f->getName()); + EXPECT_EQ("Vec3f", propVec3f->getName()); + EXPECT_EQ("Vec4f", propVec4f->getName()); + EXPECT_EQ("Vec2i", propVec2i->getName()); + EXPECT_EQ("Vec3i", propVec3i->getName()); + EXPECT_EQ("Vec4i", propVec4i->getName()); + EXPECT_EQ("DefaultValue", propDefValue->getName()); + + const vec2f expectedValueVec2f{0.1f, 0.2f}; + const vec3f expectedValueVec3f{1.1f, 1.2f, 1.3f}; + const vec4f expectedValueVec4f{2.1f, 2.2f, 2.3f, 2.4f}; + const vec2i expectedValueVec2i{1, 2}; + const vec3i expectedValueVec3i{3, 4, 5}; + const vec4i expectedValueVec4i{6, 7, 8, 9}; + EXPECT_EQ(4711, *propInt32->get()); + EXPECT_EQ(4711111L, *propInt64->get()); + EXPECT_FLOAT_EQ(47.11f, *propFloat->get()); + EXPECT_TRUE(*propBool->get()); + EXPECT_EQ("4711", *propString->get()); + EXPECT_EQ(expectedValueVec2f, *propVec2f->get()); + EXPECT_EQ(expectedValueVec3f, *propVec3f->get()); + EXPECT_EQ(expectedValueVec4f, *propVec4f->get()); + EXPECT_EQ(expectedValueVec2i, *propVec2i->get()); + EXPECT_EQ(expectedValueVec3i, *propVec3i->get()); + EXPECT_EQ(expectedValueVec4i, *propVec4i->get()); + EXPECT_FALSE(propDefValue->m_impl->checkForBindingInputNewValueAndReset()); + } + + TEST_F(AProperty_SerializationLifecycle, ErrorWhenNameMissing) + { + { + auto propertyOffset = rlogic_serialization::CreateProperty( + m_flatBufferBuilder, + 0 + ); + m_flatBufferBuilder.Finish(propertyOffset); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = PropertyImpl::Deserialize(serialized, EPropertySemantics::ScriptInput, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: missing name!"); + } + + TEST_F(AProperty_SerializationLifecycle, ErrorWhenTypeCorrupted) + { + { + // Simulate bad things with enums, but this can happen with corrupted binary data and we need to handle it safely nevertheless + auto invalidType = static_cast(std::numeric_limits::max()); + auto propertyOffset = rlogic_serialization::CreateProperty( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + invalidType + ); + m_flatBufferBuilder.Finish(propertyOffset); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = PropertyImpl::Deserialize(serialized, EPropertySemantics::ScriptInput, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: invalid type!"); + } + + TEST_F(AProperty_SerializationLifecycle, ErrorWhenChildHasErrors) + { + { + // Child is invalid because it has no name + auto childOffset = rlogic_serialization::CreateProperty( + m_flatBufferBuilder, + 0 + ); + // Parent is fine, but references a corrupt child property + auto propertyOffset = rlogic_serialization::CreateProperty( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + rlogic_serialization::EPropertyRootType::Struct, + m_flatBufferBuilder.CreateVector(std::vector{childOffset}) + ); + m_flatBufferBuilder.Finish(propertyOffset); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = PropertyImpl::Deserialize(serialized, EPropertySemantics::ScriptInput, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: missing name!"); + } + + TEST_F(AProperty_SerializationLifecycle, ErrorWhenComplexTypeHasNoChildInfo) + { + { + // Parent is fine, but references a corrupt child property + auto propertyOffset = rlogic_serialization::CreateProperty( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + rlogic_serialization::EPropertyRootType::Struct, + 0 + ); + m_flatBufferBuilder.Finish(propertyOffset); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = PropertyImpl::Deserialize(serialized, EPropertySemantics::ScriptInput, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: complex type has no child type info!"); + } + + TEST_F(AProperty_SerializationLifecycle, ErrorWhenMissingValueForPrimitiveType_AllUnionTypes) + { + // A bit ugly, but simple way to iterate all enum values + for (rlogic_serialization::PropertyValue valueType = rlogic_serialization::PropertyValue::float_s; valueType <= rlogic_serialization::PropertyValue::MAX; valueType = rlogic_serialization::PropertyValue(uint32_t(valueType) + 1)) + { + { + auto propertyOffset = rlogic_serialization::CreateProperty( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + rlogic_serialization::EPropertyRootType::Primitive, + 0, + valueType, + 0 // no union value type provided -> error when deserialized + ); + m_flatBufferBuilder.Finish(propertyOffset); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + m_errorReporting.clear(); + std::unique_ptr deserialized = PropertyImpl::Deserialize(serialized, EPropertySemantics::ScriptInput, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: invalid union!"); + } + } + + // String requires individual test, other types are tested below in AProperty_SerializationLifecycle_T + TEST_F(AProperty_SerializationLifecycle, ErrorWhenPrimitiveValueIsCorrupt_String) + { + { + auto propertyOffset = rlogic_serialization::CreateProperty( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + rlogic_serialization::EPropertyRootType::Primitive, + 0, + rlogic_serialization::PropertyValue::NONE, // setting NONE here makes the enum tuple invalid and would trigger seg fault if not checked + m_flatBufferBuilder.CreateStruct(m_flatBufferBuilder.CreateString("test string")).Union() + ); + m_flatBufferBuilder.Finish(propertyOffset); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = PropertyImpl::Deserialize(serialized, EPropertySemantics::ScriptInput, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: invalid type!"); + } + + // two element type + template + class AProperty_SerializationLifecycle_T : public AProperty_SerializationLifecycle + { + }; + // The list of types we want to test. + using ValueTypes = ::testing::Types < + rlogic_serialization::float_s, + rlogic_serialization::vec2f_s, + rlogic_serialization::vec3f_s, + rlogic_serialization::vec4f_s, + rlogic_serialization::int32_s, + rlogic_serialization::vec2i_s, + rlogic_serialization::vec3i_s, + rlogic_serialization::vec4i_s, + rlogic_serialization::bool_s + //string_s requires a separate test because it's not a struct but a table, see below + >; + + TYPED_TEST_SUITE(AProperty_SerializationLifecycle_T, ValueTypes); + + TYPED_TEST(AProperty_SerializationLifecycle_T, ErrorWhenPrimitiveValueIsCorrupt) + { + { + const TypeParam unionValue; // data doesn't matter, just use default constructor for simplicity + auto propertyOffset = rlogic_serialization::CreateProperty( + this->m_flatBufferBuilder, + this->m_flatBufferBuilder.CreateString("name"), + rlogic_serialization::EPropertyRootType::Primitive, + 0, + rlogic_serialization::PropertyValue::NONE, // setting NONE here makes the enum tuple invalid and would trigger seg fault if not checked + this->m_flatBufferBuilder.CreateStruct(unionValue).Union() + ); + this->m_flatBufferBuilder.Finish(propertyOffset); + } + + const auto& serialized = *flatbuffers::GetRoot(this->m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = PropertyImpl::Deserialize(serialized, EPropertySemantics::ScriptInput, this->m_errorReporting, this->m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(this->m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(this->m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: invalid type!"); + } + + TEST_F(AProperty, DoesNotSetLogicNodeToDirtyIfValueIsNotChanged) + { + auto int32Property = CreateInputProperty(EPropertyType::Int32); + auto int64Property = CreateInputProperty(EPropertyType::Int64); + auto floatProperty = CreateInputProperty(EPropertyType::Float); + auto vec2fProperty = CreateInputProperty(EPropertyType::Vec2f); + auto vec3iProperty = CreateInputProperty(EPropertyType::Vec3i); + auto stringProperty = CreateInputProperty(EPropertyType::String); + + int32Property->m_impl->setValue(42); + int64Property->m_impl->setValue(int64_t{ 421 }); + floatProperty->m_impl->setValue(42.f); + vec2fProperty->m_impl->setValue(vec2f{4.f, 2.f}); + vec3iProperty->m_impl->setValue(vec3i{4, 2, 3}); + stringProperty->m_impl->setValue(std::string("42")); + + int32Property->m_impl->getLogicNode().setDirty(false); + int64Property->m_impl->getLogicNode().setDirty(false); + floatProperty->m_impl->getLogicNode().setDirty(false); + vec2fProperty->m_impl->getLogicNode().setDirty(false); + vec3iProperty->m_impl->getLogicNode().setDirty(false); + stringProperty->m_impl->getLogicNode().setDirty(false); + + int32Property->m_impl->setValue(42); + int64Property->m_impl->setValue(int64_t{ 421 }); + floatProperty->m_impl->setValue(42.f); + vec2fProperty->m_impl->setValue(vec2f{ 4.f, 2.f }); + vec3iProperty->m_impl->setValue(vec3i{ 4, 2, 3 }); + stringProperty->m_impl->setValue(std::string("42")); + + EXPECT_FALSE(int32Property->m_impl->getLogicNode().isDirty()); + EXPECT_FALSE(int64Property->m_impl->getLogicNode().isDirty()); + EXPECT_FALSE(floatProperty->m_impl->getLogicNode().isDirty()); + EXPECT_FALSE(vec2fProperty->m_impl->getLogicNode().isDirty()); + EXPECT_FALSE(vec3iProperty->m_impl->getLogicNode().isDirty()); + EXPECT_FALSE(stringProperty->m_impl->getLogicNode().isDirty()); + } + + TEST_F(AProperty, SetsLogicNodeToDirtyIfValueIsChanged) + { + auto int32Property = CreateInputProperty(EPropertyType::Int32); + auto int64Property = CreateInputProperty(EPropertyType::Int64); + auto floatProperty = CreateInputProperty(EPropertyType::Float); + auto vec2fProperty = CreateInputProperty(EPropertyType::Vec2f); + auto vec3iProperty = CreateInputProperty(EPropertyType::Vec3i); + auto stringProperty = CreateInputProperty(EPropertyType::String); + + int32Property->m_impl->setValue(42); + int64Property->m_impl->setValue(int64_t{ 421 }); + floatProperty->m_impl->setValue(42.f); + vec2fProperty->m_impl->setValue(vec2f{4.f, 2.f}); + vec3iProperty->m_impl->setValue(vec3i{4, 2, 3}); + stringProperty->m_impl->setValue(std::string("42")); + + EXPECT_TRUE(int32Property->m_impl->getLogicNode().isDirty()); + EXPECT_TRUE(int64Property->m_impl->getLogicNode().isDirty()); + EXPECT_TRUE(floatProperty->m_impl->getLogicNode().isDirty()); + EXPECT_TRUE(vec2fProperty->m_impl->getLogicNode().isDirty()); + EXPECT_TRUE(vec3iProperty->m_impl->getLogicNode().isDirty()); + EXPECT_TRUE(stringProperty->m_impl->getLogicNode().isDirty()); + } + + TEST_F(AProperty, FailsToSetINT64ValueThatCannotBeRepresentedInLua) + { + auto int64Property{ CreateProperty(MakeType("int64input", EPropertyType::Int64), EPropertySemantics::ScriptInput, true) }; + + ELogLevel logType{ ELogLevel::Info }; + std::string logMessage; + ScopedLogContextLevel logCollector{ ELogLevel::Error, [&](ELogLevel type, std::string_view message) + { + logType = type; + logMessage = message; + } + }; + + EXPECT_FALSE(int64Property->set(std::numeric_limits::max())); + EXPECT_EQ(logType, ELogLevel::Error); + EXPECT_EQ(logMessage, fmt::format("Invalid value when setting property 'int64input', Lua cannot handle full range of 64-bit integer, trying to set '{}' which is out of this range!", + std::numeric_limits::max()).c_str()); + + EXPECT_FALSE(int64Property->set(std::numeric_limits::min())); + EXPECT_EQ(logType, ELogLevel::Error); + EXPECT_EQ(logMessage, fmt::format("Invalid value when setting property 'int64input', Lua cannot handle full range of 64-bit integer, trying to set '{}' which is out of this range!", + std::numeric_limits::min()).c_str()); + + static constexpr auto maxIntegerAsDouble = static_cast(1LLU << 53u); + EXPECT_TRUE(int64Property->set(maxIntegerAsDouble)); + EXPECT_FALSE(int64Property->set(maxIntegerAsDouble + 1)); + EXPECT_EQ(logType, ELogLevel::Error); + EXPECT_EQ(logMessage, fmt::format("Invalid value when setting property 'int64input', Lua cannot handle full range of 64-bit integer, trying to set '{}' which is out of this range!", + maxIntegerAsDouble + 1).c_str()); + + EXPECT_TRUE(int64Property->set(-maxIntegerAsDouble)); + EXPECT_FALSE(int64Property->set(-maxIntegerAsDouble - 1)); + EXPECT_EQ(logType, ELogLevel::Error); + EXPECT_EQ(logMessage, fmt::format("Invalid value when setting property 'int64input', Lua cannot handle full range of 64-bit integer, trying to set '{}' which is out of this range!", + -maxIntegerAsDouble - 1).c_str()); + } +} diff --git a/client/logic/unittests/api/PropertyTypeTest.cpp b/client/logic/unittests/api/PropertyTypeTest.cpp new file mode 100644 index 000000000..954fbfd6e --- /dev/null +++ b/client/logic/unittests/api/PropertyTypeTest.cpp @@ -0,0 +1,132 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" + +#include "ramses-logic/EPropertyType.h" + +namespace ramses +{ + TEST(PropertyTypeToEnumTypeTrait, ConvertsSupportedTypesToCorrectEnum) + { + constexpr EPropertyType floatType = PropertyTypeToEnum::TYPE; + constexpr EPropertyType vec2fType = PropertyTypeToEnum::TYPE; + constexpr EPropertyType vec3fType = PropertyTypeToEnum::TYPE; + constexpr EPropertyType vec4fType = PropertyTypeToEnum::TYPE; + constexpr EPropertyType int32Type = PropertyTypeToEnum::TYPE; + constexpr EPropertyType int64Type = PropertyTypeToEnum::TYPE; + constexpr EPropertyType vec2iType = PropertyTypeToEnum::TYPE; + constexpr EPropertyType vec3iType = PropertyTypeToEnum::TYPE; + constexpr EPropertyType vec4iType = PropertyTypeToEnum::TYPE; + constexpr EPropertyType boolType = PropertyTypeToEnum::TYPE; + constexpr EPropertyType stringType = PropertyTypeToEnum::TYPE; + constexpr EPropertyType arrayType = PropertyTypeToEnum>::TYPE; + EXPECT_EQ(floatType, EPropertyType::Float); + EXPECT_EQ(vec2fType, EPropertyType::Vec2f); + EXPECT_EQ(vec3fType, EPropertyType::Vec3f); + EXPECT_EQ(vec4fType, EPropertyType::Vec4f); + EXPECT_EQ(int32Type, EPropertyType::Int32); + EXPECT_EQ(int64Type, EPropertyType::Int64); + EXPECT_EQ(vec2iType, EPropertyType::Vec2i); + EXPECT_EQ(vec3iType, EPropertyType::Vec3i); + EXPECT_EQ(vec4iType, EPropertyType::Vec4i); + EXPECT_EQ(boolType, EPropertyType::Bool); + EXPECT_EQ(stringType, EPropertyType::String); + EXPECT_EQ(arrayType, EPropertyType::Array); + } + + TEST(PropertyTypeToEnumTypeTrait, ConvertsPropertyEnumToType) + { + static_assert(std::is_same_v::TYPE>, "wrong traits"); + static_assert(std::is_same_v::TYPE>, "wrong traits"); + static_assert(std::is_same_v::TYPE>, "wrong traits"); + static_assert(std::is_same_v::TYPE>, "wrong traits"); + static_assert(std::is_same_v::TYPE>, "wrong traits"); + static_assert(std::is_same_v::TYPE>, "wrong traits"); + static_assert(std::is_same_v::TYPE>, "wrong traits"); + static_assert(std::is_same_v::TYPE>, "wrong traits"); + static_assert(std::is_same_v::TYPE>, "wrong traits"); + static_assert(std::is_same_v::TYPE>, "wrong traits"); + static_assert(std::is_same_v::TYPE>, "wrong traits"); + static_assert(std::is_same_v, PropertyEnumToType::TYPE>, "wrong traits"); + } + + TEST(IsPrimitivePropertyTypeTrait, IsTrueOnlyForPrimitiveProperties) + { + static_assert(IsPrimitiveProperty::value, "wrong traits"); + static_assert(IsPrimitiveProperty::value, "wrong traits"); + static_assert(IsPrimitiveProperty::value, "wrong traits"); + static_assert(IsPrimitiveProperty::value, "wrong traits"); + static_assert(IsPrimitiveProperty::value, "wrong traits"); + static_assert(IsPrimitiveProperty::value, "wrong traits"); + static_assert(IsPrimitiveProperty::value, "wrong traits"); + static_assert(IsPrimitiveProperty::value, "wrong traits"); + static_assert(IsPrimitiveProperty::value, "wrong traits"); + static_assert(IsPrimitiveProperty::value, "wrong traits"); + static_assert(IsPrimitiveProperty::value, "wrong traits"); + + static_assert(!IsPrimitiveProperty::value, "wrong traits"); + static_assert(!IsPrimitiveProperty>::value, "wrong traits"); + } + + TEST(GetLuaPrimitiveTypeNameFunction, ProvidesNameForSupportedTypeEnumValues) + { + EXPECT_STREQ("Float", GetLuaPrimitiveTypeName(EPropertyType::Float)); + EXPECT_STREQ("Vec2f", GetLuaPrimitiveTypeName(EPropertyType::Vec2f)); + EXPECT_STREQ("Vec3f", GetLuaPrimitiveTypeName(EPropertyType::Vec3f)); + EXPECT_STREQ("Vec4f", GetLuaPrimitiveTypeName(EPropertyType::Vec4f)); + EXPECT_STREQ("Int32", GetLuaPrimitiveTypeName(EPropertyType::Int32)); + EXPECT_STREQ("Int64", GetLuaPrimitiveTypeName(EPropertyType::Int64)); + EXPECT_STREQ("Vec2i", GetLuaPrimitiveTypeName(EPropertyType::Vec2i)); + EXPECT_STREQ("Vec3i", GetLuaPrimitiveTypeName(EPropertyType::Vec3i)); + EXPECT_STREQ("Vec4i", GetLuaPrimitiveTypeName(EPropertyType::Vec4i)); + EXPECT_STREQ("Bool", GetLuaPrimitiveTypeName(EPropertyType::Bool)); + EXPECT_STREQ("String", GetLuaPrimitiveTypeName(EPropertyType::String)); + EXPECT_STREQ("Struct", GetLuaPrimitiveTypeName(EPropertyType::Struct)); + EXPECT_STREQ("Array", GetLuaPrimitiveTypeName(EPropertyType::Array)); + } + + TEST(PropertyTypeCheck, ChecksPropertyTypeToBeStoredInDataArray) + { + EXPECT_TRUE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Float)); + EXPECT_TRUE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Vec2f)); + EXPECT_TRUE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Vec3f)); + EXPECT_TRUE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Vec4f)); + EXPECT_TRUE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Int32)); + EXPECT_TRUE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Vec2i)); + EXPECT_TRUE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Vec3i)); + EXPECT_TRUE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Vec4i)); + EXPECT_TRUE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Array)); + + EXPECT_FALSE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Bool)); + EXPECT_FALSE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Struct)); + EXPECT_FALSE(CanPropertyTypeBeStoredInDataArray(EPropertyType::String)); + EXPECT_FALSE(CanPropertyTypeBeStoredInDataArray(EPropertyType::Int64)); + + auto invalidType(static_cast(10000)); + EXPECT_FALSE(CanPropertyTypeBeStoredInDataArray(invalidType)); + } + + TEST(PropertyTypeCheck, ChecksPropertyTypeToBeAnimatable) + { + EXPECT_TRUE(CanPropertyTypeBeAnimated(EPropertyType::Float)); + EXPECT_TRUE(CanPropertyTypeBeAnimated(EPropertyType::Vec2f)); + EXPECT_TRUE(CanPropertyTypeBeAnimated(EPropertyType::Vec3f)); + EXPECT_TRUE(CanPropertyTypeBeAnimated(EPropertyType::Vec4f)); + EXPECT_TRUE(CanPropertyTypeBeAnimated(EPropertyType::Int32)); + EXPECT_TRUE(CanPropertyTypeBeAnimated(EPropertyType::Vec2i)); + EXPECT_TRUE(CanPropertyTypeBeAnimated(EPropertyType::Vec3i)); + EXPECT_TRUE(CanPropertyTypeBeAnimated(EPropertyType::Vec4i)); + EXPECT_TRUE(CanPropertyTypeBeAnimated(EPropertyType::Array)); + + EXPECT_FALSE(CanPropertyTypeBeAnimated(EPropertyType::Bool)); + EXPECT_FALSE(CanPropertyTypeBeAnimated(EPropertyType::Struct)); + EXPECT_FALSE(CanPropertyTypeBeAnimated(EPropertyType::String)); + EXPECT_FALSE(CanPropertyTypeBeAnimated(EPropertyType::Int64)); + } +} diff --git a/client/logic/unittests/api/RamsesAppearanceBindingTest.cpp b/client/logic/unittests/api/RamsesAppearanceBindingTest.cpp new file mode 100644 index 000000000..a6a834650 --- /dev/null +++ b/client/logic/unittests/api/RamsesAppearanceBindingTest.cpp @@ -0,0 +1,953 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "RamsesObjectResolverMock.h" +#include "RamsesTestUtils.h" +#include "SerializationTestUtils.h" +#include "WithTempDirectory.h" + +#include "impl/LogicEngineImpl.h" +#include "impl/RamsesAppearanceBindingImpl.h" +#include "impl/PropertyImpl.h" +#include "internals/RamsesHelper.h" +#include "generated/RamsesAppearanceBindingGen.h" + +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/RamsesAppearanceBinding.h" + +#include "ramses-client-api/Effect.h" +#include "ramses-client-api/EffectDescription.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-client-api/Effect.h" +#include "ramses-client-api/Appearance.h" + +#include "ramses-utils.h" + +#include + +namespace ramses::internal +{ + class ARamsesAppearanceBinding : public ALogicEngine + { + protected: + }; + + TEST_F(ARamsesAppearanceBinding, HasANameAfterCreation) + { + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + EXPECT_EQ("AppearanceBinding", appearanceBinding.getName()); + } + + TEST_F(ARamsesAppearanceBinding, HasNoOutputsAfterCreation) + { + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + EXPECT_EQ(nullptr, appearanceBinding.getOutputs()); + } + + TEST_F(ARamsesAppearanceBinding, ProducesNoErrorsDuringUpdate_IfNoRamsesAppearanceIsAssigned) + { + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + EXPECT_EQ(std::nullopt, appearanceBinding.m_impl.update()); + } + + // This fixture only contains serialization unit tests, for higher order tests see `ARamsesAppearanceBinding_WithRamses_AndFiles` + class ARamsesAppearanceBinding_SerializationLifecycle : public ARamsesAppearanceBinding + { + protected: + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + SerializationTestUtils m_testUtils {m_flatBufferBuilder}; + ::testing::StrictMock m_resolverMock; + ErrorReporting m_errorReporting; + SerializationMap m_serializationMap; + DeserializationMap m_deserializationMap; + }; + + // More unit tests with inputs/outputs declared in LogicNode (base class) serialization tests + TEST_F(ARamsesAppearanceBinding_SerializationLifecycle, RemembersBaseClassData) + { + // Serialize + { + RamsesAppearanceBindingImpl binding(*m_appearance, "name", 1u); + binding.createRootProperties(); + (void)RamsesAppearanceBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + // Inspect flatbuffers data + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serializedBinding.base()); + ASSERT_TRUE(serializedBinding.base()->base()); + ASSERT_TRUE(serializedBinding.base()->base()->name()); + EXPECT_EQ(serializedBinding.base()->base()->name()->string_view(), "name"); + EXPECT_EQ(serializedBinding.base()->base()->id(), 1u); + + ASSERT_TRUE(serializedBinding.base()->rootInput()); + EXPECT_EQ(serializedBinding.base()->rootInput()->rootType(), rlogic_serialization::EPropertyRootType::Struct); + ASSERT_TRUE(serializedBinding.base()->rootInput()->children()); + EXPECT_EQ(serializedBinding.base()->rootInput()->children()->size(), 1u); + + // Deserialize + { + EXPECT_CALL(m_resolverMock, findRamsesAppearanceInScene(::testing::Eq("name"), m_appearance->getSceneObjectId())).WillOnce(::testing::Return(m_appearance)); + std::unique_ptr deserializedBinding = RamsesAppearanceBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + + ASSERT_TRUE(deserializedBinding); + EXPECT_EQ(deserializedBinding->getName(), "name"); + EXPECT_EQ(deserializedBinding->getId(), 1u); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + } + + TEST_F(ARamsesAppearanceBinding_SerializationLifecycle, RemembersRamsesAppearanceIdAndType_AndEffectHash) + { + // Serialize + { + RamsesAppearanceBindingImpl binding(*m_appearance, "name", 1u); + binding.createRootProperties(); + (void)RamsesAppearanceBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + // Inspect flatbuffers data + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serializedBinding.base()->boundRamsesObject()); + EXPECT_EQ(serializedBinding.base()->boundRamsesObject()->objectId(), m_appearance->getSceneObjectId().getValue()); + EXPECT_EQ(serializedBinding.base()->boundRamsesObject()->objectType(), static_cast(ramses::ERamsesObjectType::Appearance)); + EXPECT_EQ(serializedBinding.parentEffectId()->resourceIdLow(), m_appearance->getEffect().getResourceId().lowPart); + EXPECT_EQ(serializedBinding.parentEffectId()->resourceIdHigh(), m_appearance->getEffect().getResourceId().highPart); + + // Deserialize + { + EXPECT_CALL(m_resolverMock, findRamsesAppearanceInScene(::testing::Eq("name"), m_appearance->getSceneObjectId())).WillOnce(::testing::Return(m_appearance)); + std::unique_ptr deserializedBinding = RamsesAppearanceBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + + ASSERT_TRUE(deserializedBinding); + EXPECT_EQ(&deserializedBinding->getRamsesAppearance(), m_appearance); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + + // Check that input was deserialized too + EXPECT_EQ(deserializedBinding->getInputs()->getChild("floatUniform")->getType(), EPropertyType::Float); + } + } + + TEST_F(ARamsesAppearanceBinding_SerializationLifecycle, ErrorWhenNoBindingBaseData) + { + { + auto binding = rlogic_serialization::CreateRamsesAppearanceBinding( + m_flatBufferBuilder, + 0 // no base binding info + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesAppearanceBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesAppearanceBinding from serialized data: missing base class info!"); + } + + TEST_F(ARamsesAppearanceBinding_SerializationLifecycle, ErrorWhenNoBindingName) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + 0, // no name! + 1u) + ); + auto binding = rlogic_serialization::CreateRamsesAppearanceBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesAppearanceBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing name!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of RamsesAppearanceBinding from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ARamsesAppearanceBinding_SerializationLifecycle, ErrorWhenNoBindingId) + { + { + auto base = rlogic_serialization::CreateRamsesBinding(m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 0) // no id + ); + auto binding = rlogic_serialization::CreateRamsesAppearanceBinding(m_flatBufferBuilder, base); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesAppearanceBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing or invalid ID!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of RamsesAppearanceBinding from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ARamsesAppearanceBinding_SerializationLifecycle, ErrorWhenNoRootInput) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0 // no root input + ); + auto binding = rlogic_serialization::CreateRamsesAppearanceBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesAppearanceBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesAppearanceBinding from serialized data: missing root input!"); + } + + TEST_F(ARamsesAppearanceBinding_SerializationLifecycle, ErrorWhenBoundAppearanceCannotBeResolved) + { + const ramses::sceneObjectId_t mockObjectId {12}; + { + auto ramsesRef = rlogic_serialization::CreateRamsesReference( + m_flatBufferBuilder, + mockObjectId.getValue() + ); + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + ramsesRef, + m_testUtils.serializeTestProperty("") + ); + auto binding = rlogic_serialization::CreateRamsesAppearanceBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + EXPECT_CALL(m_resolverMock, findRamsesAppearanceInScene(::testing::Eq("name"), mockObjectId)).WillOnce(::testing::Return(nullptr)); + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesAppearanceBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + } + + TEST_F(ARamsesAppearanceBinding_SerializationLifecycle, ErrorWhenRootInputHasErrors) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0, + m_testUtils.serializeTestProperty("", rlogic_serialization::EPropertyRootType::Struct, false, true) // rootInput with errors + ); + auto binding = rlogic_serialization::CreateRamsesAppearanceBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesAppearanceBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: missing name!"); + } + + TEST_F(ARamsesAppearanceBinding_SerializationLifecycle, ReportsErrorWhenDeserializedWithDifferentAppearanceThenDuringSerialization) + { + // Use different shaders than the "trivial ones" below to force different appearance inputs + std::string_view differentVertShader = R"( + #version 100 + + uniform highp float differentFloatUniform; + attribute vec3 a_position; + + void main() + { + gl_Position = differentFloatUniform * vec4(a_position, 1.0); + })"; + + std::string_view fragShader = R"( + #version 100 + + void main(void) + { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + })"; + + ramses::Appearance& differentAppearance = RamsesTestSetup::CreateTestAppearance(*m_scene, differentVertShader, fragShader); + + // Serialize + { + RamsesAppearanceBindingImpl binding(*m_appearance, "name", 1u); + binding.createRootProperties(); + (void)RamsesAppearanceBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + // Inspect flatbuffers data + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + // Deserialize with different appearance -> expect errors + { + EXPECT_CALL(m_resolverMock, findRamsesAppearanceInScene(::testing::Eq("name"), m_appearance->getSceneObjectId())).WillOnce(::testing::Return(&differentAppearance)); + std::unique_ptr deserializedBinding = RamsesAppearanceBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserializedBinding); + EXPECT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesAppearanceBinding from serialized data: effect signature doesn't match after loading!"); + } + } + + class ARamsesAppearanceBinding_WithRamses : public ARamsesAppearanceBinding + { + protected: + const std::string_view m_vertShader_simple = R"( + #version 300 es + + uniform highp float floatUniform; + + void main() + { + gl_Position = floatUniform * vec4(1.0); + })"; + + const std::string_view m_vertShader_twoUniforms = R"( + #version 300 es + + uniform highp float floatUniform1; + uniform highp float floatUniform2; + + void main() + { + gl_Position = floatUniform1 * floatUniform2 * vec4(1.0); + })"; + + const std::string_view m_vertShader_allTypes = R"( + #version 300 es + + uniform highp float floatUniform; + uniform highp int intUniform; + uniform highp ivec2 ivec2Uniform; + uniform highp ivec3 ivec3Uniform; + uniform highp ivec4 ivec4Uniform; + uniform highp vec2 vec2Uniform; + uniform highp vec3 vec3Uniform; + uniform highp vec4 vec4Uniform; + uniform highp ivec2 ivec2Array[2]; + uniform highp vec2 vec2Array[2]; + uniform highp ivec3 ivec3Array[2]; + uniform highp vec3 vec3Array[2]; + uniform highp ivec4 ivec4Array[2]; + uniform highp vec4 vec4Array[2]; + uniform highp vec4 vec4Uniform_shouldHaveDefaultValue; + + void main() + { + gl_Position = floatUniform * vec4(1.0); + })"; + + const std::string_view m_fragShader_trivial = R"( + #version 300 es + + out lowp vec4 color; + void main(void) + { + color = vec4(1.0, 0.0, 0.0, 1.0); + })"; + + ramses::Effect& createTestEffect(std::string_view vertShader, std::string_view fragShader) + { + ramses::EffectDescription effectDesc; + effectDesc.setUniformSemantic("u_DisplayBufferResolution", ramses::EEffectUniformSemantic::DisplayBufferResolution); + effectDesc.setVertexShader(vertShader.data()); + effectDesc.setFragmentShader(fragShader.data()); + return *m_scene->createEffect(effectDesc); + } + + ramses::Appearance& createTestAppearance(ramses::Effect& effect) + { + return *m_scene->createAppearance(effect, "test appearance"); + } + + void expectErrorWhenLoadingFile(std::string_view fileName, std::string_view message) + { + EXPECT_FALSE(m_logicEngine.loadFromFile(fileName, m_scene)); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_EQ(std::string(message), m_logicEngine.getErrors()[0].message); + } + + static float GetUniformValueFloat(ramses::Appearance& appearance, std::string_view uniformName) + { + ramses::UniformInput uniform; + appearance.getEffect().findUniformInput(uniformName, uniform); + assert(uniform.isValid()); + float value = 0.f; + appearance.getInputValue(uniform, value); + return value; + } + + + static void SetUniformValueFloat(ramses::Appearance& appearance, std::string_view uniformName, float value) + { + ramses::UniformInput uniform; + appearance.getEffect().findUniformInput(uniformName, uniform); + assert(uniform.isValid()); + appearance.setInputValue(uniform, value); + } + }; + + TEST_F(ARamsesAppearanceBinding_WithRamses, ReturnsReferenceToRamsesAppearance) + { + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + EXPECT_EQ(m_appearance, &appearanceBinding.getRamsesAppearance()); + } + + TEST_F(ARamsesAppearanceBinding_WithRamses, HasInputsAfterCreation) + { + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + + auto inputs = appearanceBinding.getInputs(); + + ASSERT_EQ(1u, inputs->getChildCount()); + auto floatUniform = inputs->getChild(0); + ASSERT_NE(nullptr, floatUniform); + EXPECT_EQ("floatUniform", floatUniform->getName()); + EXPECT_EQ(EPropertyType::Float, floatUniform->getType()); + } + + TEST_F(ARamsesAppearanceBinding_WithRamses, GivesInputs_BindingInputSemantics) + { + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + + auto inputs = appearanceBinding.getInputs(); + const auto inputCount = inputs->getChildCount(); + for (size_t i = 0; i < inputCount; ++i) + { + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild(i)->m_impl->getPropertySemantics()); + } + } + + TEST_F(ARamsesAppearanceBinding_WithRamses, CreatesOnlyInputsForSupportedUniformTypes) + { + + const std::string_view fragShader_ManyUniformTypes = R"( + #version 300 es + + // This is the same uniform like in the vertex shader - that's intended! + uniform highp float floatUniform; + // Other types, mixed up on purpose with some types which are not supported yet + uniform highp vec2 u_vec2f; + uniform highp sampler2D u_tex2d; + //uniform highp samplerCube cubeTex; // Not supported + uniform highp vec4 u_vec4f; + uniform highp sampler3D u_tex3d; // Not supported + uniform lowp int u_int; + uniform highp samplerCube u_texCube; // Not supported + uniform mediump mat2 u_mat2; // Not supported + uniform mediump mat3 u_mat3; // Not supported + uniform mediump mat4 u_mat4; // Not supported + uniform mediump vec2 u_DisplayBufferResolution; // explicitly prohibited to set by ramses + uniform highp ivec2 u_vec2i; + // Arrays + uniform mediump vec2 u_vec2Array[2]; + uniform mediump ivec2 u_ivec2Array[2]; + + out lowp vec4 color; + void main(void) + { + color = vec4(floatUniform, 0.0, 0.0, 1.0); + color.xy += u_vec2f; + color += texture(u_tex2d, u_vec2f); + color += texture(u_tex3d, vec3(u_vec2f, 1.0)); + color += texture(u_texCube, vec3(u_vec2f, 1.0)); + color.xy += vec2(float(u_vec2i.x), float(u_vec2i.y)); + })"; + + ramses::Appearance& appearance = createTestAppearance(createTestEffect(m_vertShader_simple, fragShader_ManyUniformTypes)); + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(appearance, "AppearanceBinding"); + + const auto inputs = appearanceBinding.getInputs(); + ASSERT_EQ(7u, inputs->getChildCount()); + EXPECT_EQ("floatUniform", inputs->getChild(0)->getName()); + EXPECT_EQ(EPropertyType::Float, inputs->getChild(0)->getType()); + EXPECT_EQ("u_vec2f", inputs->getChild(1)->getName()); + EXPECT_EQ(EPropertyType::Vec2f, inputs->getChild(1)->getType()); + EXPECT_EQ("u_vec4f", inputs->getChild(2)->getName()); + EXPECT_EQ(EPropertyType::Vec4f, inputs->getChild(2)->getType()); + EXPECT_EQ("u_int", inputs->getChild(3)->getName()); + EXPECT_EQ(EPropertyType::Int32, inputs->getChild(3)->getType()); + EXPECT_EQ("u_vec2i", inputs->getChild(4)->getName()); + EXPECT_EQ(EPropertyType::Vec2i, inputs->getChild(4)->getType()); + + // Arrays, also check their children + Property* vec2fArray = inputs->getChild(5); + EXPECT_EQ("u_vec2Array", vec2fArray->getName()); + EXPECT_EQ(EPropertyType::Array, vec2fArray->getType()); + EXPECT_EQ(2u, vec2fArray->getChildCount()); + for (size_t i = 0; i < 2; ++i) + { + EXPECT_EQ("", vec2fArray->getChild(i)->getName()); + EXPECT_EQ(EPropertyType::Vec2f, vec2fArray->getChild(i)->getType()); + } + + Property* vec2iArray = inputs->getChild(6); + EXPECT_EQ("u_ivec2Array", vec2iArray->getName()); + EXPECT_EQ(EPropertyType::Array, vec2iArray->getType()); + EXPECT_EQ(2u, vec2iArray->getChildCount()); + for (size_t i = 0; i < 2; ++i) + { + EXPECT_EQ("", vec2iArray->getChild(i)->getName()); + EXPECT_EQ(EPropertyType::Vec2i, vec2iArray->getChild(i)->getType()); + } + } + + TEST_F(ARamsesAppearanceBinding_WithRamses, UpdatesAppearanceIfInputValuesWereSet) + { + ramses::Appearance& appearance = createTestAppearance(createTestEffect(m_vertShader_allTypes, m_fragShader_trivial)); + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(appearance, "AppearanceBinding"); + auto inputs = appearanceBinding.getInputs(); + ASSERT_EQ(15u, inputs->getChildCount()); + EXPECT_TRUE(inputs->getChild("floatUniform")->set(42.42f)); + EXPECT_TRUE(inputs->getChild("intUniform")->set(42)); + EXPECT_TRUE(inputs->getChild("vec2Uniform")->set({ 0.1f, 0.2f })); + EXPECT_TRUE(inputs->getChild("vec3Uniform")->set({ 1.1f, 1.2f, 1.3f })); + EXPECT_TRUE(inputs->getChild("vec4Uniform")->set({ 2.1f, 2.2f, 2.3f, 2.4f })); + EXPECT_TRUE(inputs->getChild("ivec2Uniform")->set({ 1, 2 })); + EXPECT_TRUE(inputs->getChild("ivec3Uniform")->set({ 3, 4, 5 })); + EXPECT_TRUE(inputs->getChild("ivec4Uniform")->set({ 6, 7, 8, 9 })); + EXPECT_TRUE(inputs->getChild("ivec2Array")->getChild(0)->set({ 11, 12 })); + EXPECT_TRUE(inputs->getChild("ivec2Array")->getChild(1)->set({ 13, 14 })); + EXPECT_TRUE(inputs->getChild("vec2Array")->getChild(0)->set({ .11f, .12f })); + EXPECT_TRUE(inputs->getChild("vec2Array")->getChild(1)->set({ .13f, .14f })); + EXPECT_TRUE(inputs->getChild("ivec3Array")->getChild(0)->set({ 31, 32, 33 })); + EXPECT_TRUE(inputs->getChild("ivec3Array")->getChild(1)->set({ 34, 35, 36 })); + EXPECT_TRUE(inputs->getChild("vec3Array")->getChild(0)->set({ .31f, .32f, .33f })); + EXPECT_TRUE(inputs->getChild("vec3Array")->getChild(1)->set({ .34f, .35f, .36f })); + EXPECT_TRUE(inputs->getChild("ivec4Array")->getChild(0)->set({ 41, 42, 43, 44 })); + EXPECT_TRUE(inputs->getChild("ivec4Array")->getChild(1)->set({ 45, 46, 47, 48 })); + EXPECT_TRUE(inputs->getChild("vec4Array")->getChild(0)->set({ .41f, .42f, .43f, .44f })); + EXPECT_TRUE(inputs->getChild("vec4Array")->getChild(1)->set({ .45f, .46f, .47f, .48f })); + + EXPECT_EQ(std::nullopt, appearanceBinding.m_impl.update()); + + ramses::UniformInput uniform; + { + float resultFloat = 0.f; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("floatUniform", uniform)); + appearance.getInputValue(uniform, resultFloat); + EXPECT_FLOAT_EQ(42.42f, resultFloat); + } + { + int32_t resultInt32 = 0; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("intUniform", uniform)); + appearance.getInputValue(uniform, resultInt32); + EXPECT_EQ(42, resultInt32); + } + { + ramses::vec2f result{ 0.0f, 0.0f }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("vec2Uniform", uniform)); + appearance.getInputValue(uniform, result); + EXPECT_EQ(result, vec2f(0.1f, 0.2f)); + } + { + ramses::vec3f result{ 0.0f, 0.0f, 0.0f }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("vec3Uniform", uniform)); + appearance.getInputValue(uniform, result); + EXPECT_EQ(result, vec3f(1.1f, 1.2f, 1.3f)); + } + { + ramses::vec4f result{ 0.0f, 0.0f, 0.0f, 0.0f }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("vec4Uniform", uniform)); + appearance.getInputValue(uniform, result); + EXPECT_EQ(result, vec4f(2.1f, 2.2f, 2.3f, 2.4f)); + + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("vec4Uniform_shouldHaveDefaultValue", uniform)); + appearance.getInputValue(uniform, result); + EXPECT_EQ(result, vec4f(0.0f, 0.0f, 0.0f, 0.0f)); + } + { + ramses::vec2i result{ 0, 0 }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("ivec2Uniform", uniform)); + appearance.getInputValue(uniform, result); + EXPECT_EQ(result, vec2i(1, 2)); + } + { + ramses::vec3i result{ 0, 0, 0 }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("ivec3Uniform", uniform)); + appearance.getInputValue(uniform, result); + EXPECT_EQ(result, vec3i(3, 4, 5)); + } + { + ramses::vec4i result{ 0, 0, 0, 0 }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("ivec4Uniform", uniform)); + appearance.getInputValue(uniform, result); + EXPECT_EQ(result, vec4i(6, 7, 8, 9)); + } + // Arrays + { + std::array result{ ramses::vec2i{0, 0}, ramses::vec2i{0, 0} }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("ivec2Array", uniform)); + appearance.getInputValue(uniform, 2, result.data()); + EXPECT_THAT(result, ::testing::ElementsAre(ramses::vec2i{ 11, 12 }, ramses::vec2i{ 13, 14 })); + } + { + std::array result{ ramses::vec2f{0.0f, 0.0f}, ramses::vec2f{0.0f, 0.0f} }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("vec2Array", uniform)); + appearance.getInputValue(uniform, 2, result.data()); + EXPECT_THAT(result, ::testing::ElementsAre(ramses::vec2f{ .11f, .12f }, ramses::vec2f{ .13f, .14f })); + } + { + std::array result{ ramses::vec3i{0, 0, 0}, ramses::vec3i{0, 0, 0} }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("ivec3Array", uniform)); + appearance.getInputValue(uniform, 2, result.data()); + EXPECT_THAT(result, ::testing::ElementsAre(ramses::vec3i{ 31, 32, 33 }, ramses::vec3i{ 34, 35, 36 })); + } + { + std::array result = { ramses::vec3f{0.0f, 0.0f, 0.0f}, ramses::vec3f{0.0f, 0.0f, 0.0f} }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("vec3Array", uniform)); + appearance.getInputValue(uniform, 2, result.data()); + EXPECT_THAT(result, ::testing::ElementsAre(ramses::vec3f{ .31f, .32f, .33f }, ramses::vec3f{ .34f, .35f, .36f })); + } + { + std::array result = { ramses::vec4i{0, 0, 0, 0}, ramses::vec4i{0, 0, 0, 0} }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("ivec4Array", uniform)); + appearance.getInputValue(uniform, 2, result.data()); + EXPECT_THAT(result, ::testing::ElementsAre(ramses::vec4i{ 41, 42, 43, 44 }, ramses::vec4i{ 45, 46, 47, 48 })); + } + { + std::array result = { ramses::vec4f{0.0f, 0.0f, 0.0f, 0.0f}, ramses::vec4f{0.0f, 0.0f, 0.0f, 0.0f} }; + ASSERT_EQ(ramses::StatusOK, appearance.getEffect().findUniformInput("vec4Array", uniform)); + appearance.getInputValue(uniform, 2, result.data()); + EXPECT_THAT(result, ::testing::ElementsAre(ramses::vec4f{ .41f, .42f, .43f, .44f }, ramses::vec4f{ .45f, .46f, .47f, .48f })); + } + } + + TEST_F(ARamsesAppearanceBinding_WithRamses, PropagateItsInputsToRamsesAppearanceOnUpdate_OnlyWhenExplicitlySet) + { + ramses::Appearance& appearance = createTestAppearance(createTestEffect(m_vertShader_twoUniforms, m_fragShader_trivial)); + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(appearance, "AppearanceBinding"); + + // Set values directly to ramses appearance + SetUniformValueFloat(appearance, "floatUniform1", 11.f); + SetUniformValueFloat(appearance, "floatUniform2", 22.f); + + // Set only one of the inputs to the binding object, the other one (floatUniform2) not + EXPECT_TRUE(appearanceBinding.getInputs()->getChild("floatUniform1")->set(100.f)); + + EXPECT_TRUE(m_logicEngine.update()); + + // Only propagate the value which was also set in the binding object + EXPECT_FLOAT_EQ(100.f, GetUniformValueFloat(appearance, "floatUniform1")); + EXPECT_FLOAT_EQ(22.f, GetUniformValueFloat(appearance, "floatUniform2")); + } + + TEST_F(ARamsesAppearanceBinding_WithRamses, PropagatesItsInputsToRamsesAppearanceOnUpdate_WithLinksInsteadOfSetCall) + { + ramses::Appearance& appearance = createTestAppearance(createTestEffect(m_vertShader_twoUniforms, m_fragShader_trivial)); + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(appearance, "AppearanceBinding"); + + // Set values directly to ramses appearance + SetUniformValueFloat(appearance, "floatUniform1", 11.f); + SetUniformValueFloat(appearance, "floatUniform2", 22.f); + + // Link binding input to a script (binding is not set directly, but is linked) + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.float = Type:Float() + end + function run(IN,OUT) + OUT.float = 42.42 + end + )"; + LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("float"), *appearanceBinding.getInputs()->getChild("floatUniform1"))); + + EXPECT_TRUE(m_logicEngine.update()); + + // Only propagate the value which was also linked over the binding object's input to a script + EXPECT_FLOAT_EQ(42.42f, GetUniformValueFloat(appearance, "floatUniform1")); + EXPECT_FLOAT_EQ(22.f, GetUniformValueFloat(appearance, "floatUniform2")); + } + + class ARamsesAppearanceBinding_WithRamses_AndFiles : public ARamsesAppearanceBinding_WithRamses + { + protected: + WithTempDirectory tempFolder; + }; + + TEST_F(ARamsesAppearanceBinding_WithRamses_AndFiles, KeepsItsPropertiesAfterDeserialization_WhenNoRamsesLinksAndSceneProvided) + { + { + m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "appearancebinding.bin")); + } + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("appearancebinding.bin", m_scene)); + auto loadedAppearanceBinding = m_logicEngine.findByName("AppearanceBinding"); + EXPECT_EQ(&loadedAppearanceBinding->getRamsesAppearance(), m_appearance); + EXPECT_EQ(loadedAppearanceBinding->getInputs()->getChildCount(), 1u); + EXPECT_EQ(loadedAppearanceBinding->getOutputs(), nullptr); + EXPECT_EQ(loadedAppearanceBinding->getName(), "AppearanceBinding"); + } + } + + TEST_F(ARamsesAppearanceBinding_WithRamses_AndFiles, ContainsItsInputsAfterDeserialization_WithoutReorderingThem) + { + std::vector inputOrderBeforeSaving; + + ramses::Appearance& appearance = createTestAppearance(createTestEffect(m_vertShader_allTypes, m_fragShader_trivial)); + { + auto& rAppearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(appearance, "AppearanceBinding"); + auto inputs = rAppearanceBinding.getInputs(); + inputOrderBeforeSaving.reserve(inputs->getChildCount()); + + for (size_t i = 0; i < inputs->getChildCount(); ++i) + { + inputOrderBeforeSaving.emplace_back(std::string(inputs->getChild(i)->getName())); + } + + inputs->getChild("floatUniform")->set(42.42f); + inputs->getChild("intUniform")->set(42); + inputs->getChild("vec2Uniform")->set({ 0.1f, 0.2f }); + inputs->getChild("vec3Uniform")->set({ 1.1f, 1.2f, 1.3f }); + inputs->getChild("vec4Uniform")->set({ 2.1f, 2.2f, 2.3f, 2.4f }); + inputs->getChild("ivec2Uniform")->set({ 1, 2 }); + inputs->getChild("ivec3Uniform")->set({ 3, 4, 5 }); + inputs->getChild("ivec4Uniform")->set({ 6, 7, 8, 9 }); + inputs->getChild("ivec2Array")->getChild(0)->set({ 11, 12 }); + inputs->getChild("ivec2Array")->getChild(1)->set({ 13, 14 }); + inputs->getChild("vec2Array")->getChild(0)->set({ .11f, .12f }); + inputs->getChild("vec2Array")->getChild(1)->set({ .13f, .14f }); + m_logicEngine.update(); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "logic.bin")); + } + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("logic.bin", m_scene)); + auto loadedAppearanceBinding = m_logicEngine.findByName("AppearanceBinding"); + EXPECT_EQ(loadedAppearanceBinding->getRamsesAppearance().getSceneObjectId(), appearance.getSceneObjectId()); + + const auto& inputs = loadedAppearanceBinding->getInputs(); + ASSERT_EQ(15u, inputs->getChildCount()); + + // check order after deserialization + for (size_t i = 0; i < inputOrderBeforeSaving.size(); ++i) + { + EXPECT_EQ(inputOrderBeforeSaving[i], inputs->getChild(i)->getName()); + } + + auto expectValues = [&inputs](){ + EXPECT_FLOAT_EQ(42.42f, *inputs->getChild("floatUniform")->get()); + EXPECT_EQ(42, *inputs->getChild("intUniform")->get()); + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild("intUniform")->m_impl->getPropertySemantics()); + EXPECT_EQ(*inputs->getChild("vec2Uniform")->get(), vec2f(0.1f, 0.2f)); + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild("vec2Uniform")->m_impl->getPropertySemantics()); + EXPECT_EQ(*inputs->getChild("vec3Uniform")->get(), vec3f(1.1f, 1.2f, 1.3f)); + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild("vec3Uniform")->m_impl->getPropertySemantics()); + EXPECT_EQ(*inputs->getChild("vec4Uniform")->get(), vec4f(2.1f, 2.2f, 2.3f, 2.4f)); + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild("vec4Uniform")->m_impl->getPropertySemantics()); + EXPECT_EQ(*inputs->getChild("vec4Uniform_shouldHaveDefaultValue")->get(), vec4f(.0f, .0f, .0f, .0f)); + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild("vec4Uniform_shouldHaveDefaultValue")->m_impl->getPropertySemantics()); + EXPECT_EQ(*inputs->getChild("ivec2Uniform")->get(), vec2i(1, 2)); + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild("ivec2Uniform")->m_impl->getPropertySemantics()); + EXPECT_EQ(*inputs->getChild("ivec3Uniform")->get(), vec3i(3, 4, 5)); + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild("ivec3Uniform")->m_impl->getPropertySemantics()); + EXPECT_EQ(*inputs->getChild("ivec4Uniform")->get(), vec4i(6, 7, 8, 9)); + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild("ivec4Uniform")->m_impl->getPropertySemantics()); + + // Arrays + EXPECT_EQ(EPropertyType::Array, inputs->getChild("ivec2Array")->getType()); + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild("ivec2Array")->m_impl->getPropertySemantics()); + EXPECT_EQ(*inputs->getChild("ivec2Array")->getChild(0)->get(), vec2i(11, 12)); + EXPECT_EQ(*inputs->getChild("ivec2Array")->getChild(1)->get(), vec2i(13, 14)); + EXPECT_EQ(EPropertyType::Array, inputs->getChild("vec2Array")->getType()); + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild("vec2Array")->m_impl->getPropertySemantics()); + EXPECT_EQ(*inputs->getChild("vec2Array")->getChild(0)->get(), vec2f(.11f, .12f)); + EXPECT_EQ(*inputs->getChild("vec2Array")->getChild(1)->get(), vec2f(.13f, .14f)); + }; + + expectValues(); + + EXPECT_TRUE(m_logicEngine.update()); + + expectValues(); + } + } + + TEST_F(ARamsesAppearanceBinding_WithRamses_AndFiles, ContainsItsInputsAfterDeserialization_WhenRamsesSceneIsRecreatedBetweenSaveAndLoad) + { + const ramses::sceneObjectId_t appearanceIdBeforeReload = m_appearance->getSceneObjectId(); + { + auto& rAppearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + rAppearanceBinding.getInputs()->getChild("floatUniform")->set(42.42f); + m_logicEngine.update(); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "logic.bin")); + } + + recreate(); + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("logic.bin", m_scene)); + auto loadedAppearanceBinding = m_logicEngine.findByName("AppearanceBinding"); + EXPECT_EQ(loadedAppearanceBinding->getRamsesAppearance().getSceneObjectId(), appearanceIdBeforeReload); + + const auto& inputs = loadedAppearanceBinding->getInputs(); + ASSERT_EQ(1u, inputs->getChildCount()); + EXPECT_FLOAT_EQ(42.42f, *inputs->getChild("floatUniform")->get()); + } + } + + TEST_F(ARamsesAppearanceBinding_WithRamses_AndFiles, ProducesError_WhenHavingLinkToAppearance_ButNoSceneWasProvided) + { + { + LogicEngine tempEngineForSaving{ m_logicEngine.getFeatureLevel() }; + tempEngineForSaving.createRamsesAppearanceBinding(*m_appearance, "AppBinding"); + EXPECT_TRUE(SaveToFileWithoutValidation(tempEngineForSaving, "WithRamsesAppearance.bin")); + } + { + EXPECT_FALSE(m_logicEngine.loadFromFile("WithRamsesAppearance.bin")); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(errors.size(), 1u); + EXPECT_EQ(errors[0].message, "Fatal error during loading from file! File contains references to Ramses objects but no Ramses scene was provided!"); + } + } + + TEST_F(ARamsesAppearanceBinding_WithRamses_AndFiles, ProducesErrorIfAppearanceHasDifferentEffectThanSerializedAppearanceBinding) + { + auto& appearance = createTestAppearance(createTestEffect(m_vertShader_simple, m_fragShader_trivial)); + auto* appearanceBinding = m_logicEngine.createRamsesAppearanceBinding(appearance, "AppearanceBinding"); + + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "logic.bin")); + + // Simulate that a difference appearance with the same ID was created, but with different inputs + recreate(); + createTestAppearance(createTestEffect(m_vertShader_allTypes, m_fragShader_trivial)); + + expectErrorWhenLoadingFile("logic.bin", + "Fatal error during loading of RamsesAppearanceBinding from serialized data: effect signature doesn't match after loading!"); + + // Did not overwrite existing objects (because loading from file failed) + EXPECT_EQ(appearanceBinding, m_logicEngine.findByName("AppearanceBinding")); + } + + TEST_F(ARamsesAppearanceBinding_WithRamses_AndFiles, DoesNotReapplyAppearanceUniformValuesToRamses_WhenLoadingFromFileAndCallingUpdate_UntilSetToANewValue) + { + ramses::Appearance& appearance = createTestAppearance(createTestEffect(m_vertShader_simple, m_fragShader_trivial)); + + { + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(appearance, "AppearanceBinding"); + auto inputs = appearanceBinding.getInputs(); + + // Set a different input over the binding object + inputs->getChild("floatUniform")->set(42.42f); + m_logicEngine.update(); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "SomeValuesSet.bin")); + } + + // Set uniform to a different value than the one set on the ramses binding + SetUniformValueFloat(appearance, "floatUniform", 100.0f); + + { + EXPECT_TRUE(m_logicEngine.loadFromFile("SomeValuesSet.bin", m_scene)); + EXPECT_TRUE(m_logicEngine.update()); + + // loadFromFile and update should not set any values whatsoever ... + EXPECT_FLOAT_EQ(100.f, GetUniformValueFloat(appearance, "floatUniform")); + + // ... unless explicitly set again on the binding object + update() called + m_logicEngine.findByName("AppearanceBinding")->getInputs()->getChild("floatUniform")->set(42.42f); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_FLOAT_EQ(42.42f, GetUniformValueFloat(appearance, "floatUniform")); + } + } + + // This is sort of a confidence test, testing a combination of: + // - bindings only propagating their values to ramses appearance if the value was set by an incoming link + // - saving and loading files + // - value only re-applied to ramses if changed. Otherwise not. + // The general expectation is that after loading + update(), the logic scene would overwrite ramses + // properties wrapped by a LogicBinding if they are linked to a script + TEST_F(ARamsesAppearanceBinding_WithRamses_AndFiles, SetsOnlyAppearanceUniformsForWhichTheBindingInputIsLinked_AfterLoadingFromFile_AndCallingUpdate) + { + ramses::Appearance& appearance = createTestAppearance(createTestEffect(m_vertShader_twoUniforms, m_fragShader_trivial)); + + { + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + IN.float = Type:Float() + OUT.float = Type:Float() + end + function run(IN,OUT) + OUT.float = IN.float + end + )"; + + LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + auto& appearanceBinding = *m_logicEngine.createRamsesAppearanceBinding(appearance, "AppearanceBinding"); + + script->getInputs()->getChild("float")->set(42.42f); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("float"), *appearanceBinding.getInputs()->getChild("floatUniform1"))); + ASSERT_TRUE(m_logicEngine.update()); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "SomeValuesLinked.bin")); + } + + // Set uniform1 to a different value than the one set by the link + SetUniformValueFloat(appearance, "floatUniform1", 100.0f); + // Set uniform2 to custom value - it should not be overwritten by logic at all, because there is no link + // or any set() calls to the corresponding RamsesAppearanceBinding input + SetUniformValueFloat(appearance, "floatUniform2", 200.0f); + + { + EXPECT_TRUE(m_logicEngine.loadFromFile("SomeValuesLinked.bin", m_scene)); + + // nothing happens before update() + EXPECT_FLOAT_EQ(100.0f, GetUniformValueFloat(appearance, "floatUniform1")); + EXPECT_FLOAT_EQ(200.0f, GetUniformValueFloat(appearance, "floatUniform2")); + + EXPECT_TRUE(m_logicEngine.update()); + + // Script is executed -> link is activated -> binding is updated, only for the linked uniform + EXPECT_FLOAT_EQ(42.42f, GetUniformValueFloat(appearance, "floatUniform1")); + EXPECT_FLOAT_EQ(200.0f, GetUniformValueFloat(appearance, "floatUniform2")); + + // Reset uniform manually and call update does nothing (must set binding input explicitly to cause overwrite in ramses) + SetUniformValueFloat(appearance, "floatUniform1", 100.0f); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_FLOAT_EQ(100.0f, GetUniformValueFloat(appearance, "floatUniform1")); + EXPECT_FLOAT_EQ(200.0f, GetUniformValueFloat(appearance, "floatUniform2")); + } + } +} diff --git a/client/logic/unittests/api/RamsesCameraBindingTest.cpp b/client/logic/unittests/api/RamsesCameraBindingTest.cpp new file mode 100644 index 000000000..0ff96f40c --- /dev/null +++ b/client/logic/unittests/api/RamsesCameraBindingTest.cpp @@ -0,0 +1,1573 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "RamsesObjectResolverMock.h" +#include "RamsesTestUtils.h" +#include "SerializationTestUtils.h" +#include "WithTempDirectory.h" + +#include "impl/LogicEngineImpl.h" +#include "impl/RamsesCameraBindingImpl.h" +#include "impl/PropertyImpl.h" +#include "internals/RamsesHelper.h" +#include "generated/RamsesCameraBindingGen.h" + +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/RamsesCameraBinding.h" + +#include "ramses-utils.h" +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/OrthographicCamera.h" + +namespace ramses::internal +{ + constexpr int32_t DefaultViewportOffsetX = 0; + constexpr int32_t DefaultViewportOffsetY = 0; + constexpr uint32_t DefaultViewportWidth = 16u; + constexpr uint32_t DefaultViewportHeight = 16u; + + constexpr float NearPlaneDefault = 0.1f; + constexpr float FarPlaneDefault = 1.0f; + + constexpr float PerspectiveFrustumFOVdefault = 168.579f; + constexpr float PerspectiveFrustumARdefault = 1.f; + + constexpr float OrthoFrustumLPdefault = -1.f; + constexpr float OrthoFrustumRPdefault = 1.f; + constexpr float OrthoFrustumBPdefault = -1.f; + constexpr float OrthoFrustumTPdefault = 1.0f; + + class ARamsesCameraBinding : public ALogicEngine + { + protected: + ARamsesCameraBinding() + : m_testScene(*m_ramsesTestSetup.createScene(ramses::sceneId_t(1))) + { + } + + static void ExpectPropertyTypeAndChildCount(const Property* prop, EPropertyType type, uint32_t childCount) + { + ASSERT_NE(nullptr, prop); + EXPECT_EQ(type, prop->getType()); + EXPECT_EQ(childCount, prop->getChildCount()); + } + + static void ExpectDefaultViewportValues(const ramses::Camera& camera) + { + EXPECT_EQ(camera.getViewportX(), DefaultViewportOffsetX); + EXPECT_EQ(camera.getViewportY(), DefaultViewportOffsetY); + EXPECT_EQ(camera.getViewportWidth(), DefaultViewportWidth); + EXPECT_EQ(camera.getViewportHeight(), DefaultViewportHeight); + } + + static void ExpectDefaultPerspectiveCameraFrustumValues(const ramses::PerspectiveCamera& camera) + { + EXPECT_NEAR(camera.getVerticalFieldOfView(), PerspectiveFrustumFOVdefault, 0.001f); + EXPECT_EQ(camera.getAspectRatio(), PerspectiveFrustumARdefault); + EXPECT_EQ(camera.getNearPlane(), NearPlaneDefault); + EXPECT_EQ(camera.getFarPlane(), FarPlaneDefault); + } + + static void ExpectDefaultCameraFrustumPlanes(const ramses::Camera& camera) + { + EXPECT_EQ(camera.getLeftPlane(), OrthoFrustumLPdefault); + EXPECT_EQ(camera.getRightPlane(), OrthoFrustumRPdefault); + EXPECT_EQ(camera.getBottomPlane(), OrthoFrustumBPdefault); + EXPECT_EQ(camera.getTopPlane(), OrthoFrustumTPdefault); + EXPECT_EQ(camera.getNearPlane(), NearPlaneDefault); + EXPECT_EQ(camera.getFarPlane(), FarPlaneDefault); + } + + static void ExpectDefaultValues(const ramses::RamsesCameraBinding& cameraBinding) + { + ExpectDefaultViewportValues(cameraBinding.getRamsesCamera()); + if (cameraBinding.m_cameraBinding.hasFrustumPlanesProperties()) + { + ExpectDefaultCameraFrustumPlanes(cameraBinding.getRamsesCamera()); + } + else + { + ExpectDefaultPerspectiveCameraFrustumValues(*ramses::RamsesUtils::TryConvert(cameraBinding.getRamsesCamera())); + } + } + + static void ExpectInputPropertiesWithFrustumPlanes(const RamsesCameraBindingImpl& cameraBinding) + { + EXPECT_TRUE(cameraBinding.hasFrustumPlanesProperties()); + + const auto inputs = cameraBinding.getInputs(); + ASSERT_EQ(2u, inputs->getChildCount()); + + const auto vpProperties = inputs->getChild("viewport"); + const auto frustum = inputs->getChild("frustum"); + ASSERT_EQ(4u, vpProperties->getChildCount()); + ASSERT_EQ(6u, frustum->getChildCount()); + + const auto vpOffsetX = vpProperties->getChild("offsetX"); + const auto vpOffsety = vpProperties->getChild("offsetY"); + const auto vpWidth = vpProperties->getChild("width"); + const auto vpHeight = vpProperties->getChild("height"); + const auto nP = frustum->getChild("nearPlane"); + const auto fP = frustum->getChild("farPlane"); + const auto lp = frustum->getChild("leftPlane"); + const auto rP = frustum->getChild("rightPlane"); + const auto bP = frustum->getChild("bottomPlane"); + const auto tP = frustum->getChild("topPlane"); + + // Test that internal indices match properties resolved by name + EXPECT_EQ(vpProperties, inputs->getChild(static_cast(ECameraPropertyStructStaticIndex::Viewport))); + EXPECT_EQ(frustum, inputs->getChild(static_cast(ECameraPropertyStructStaticIndex::Frustum))); + + EXPECT_EQ(vpOffsetX, vpProperties->m_impl->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortOffsetX))); + EXPECT_EQ(vpOffsety, vpProperties->m_impl->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortOffsetY))); + EXPECT_EQ(vpWidth, vpProperties->m_impl->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortWidth))); + EXPECT_EQ(vpHeight, vpProperties->m_impl->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortHeight))); + EXPECT_EQ(nP, frustum->m_impl->getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::NearPlane))); + EXPECT_EQ(fP, frustum->m_impl->getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::FarPlane))); + EXPECT_EQ(lp, frustum->m_impl->getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::LeftPlane))); + EXPECT_EQ(rP, frustum->m_impl->getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::RightPlane))); + EXPECT_EQ(bP, frustum->m_impl->getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::BottomPlane))); + EXPECT_EQ(tP, frustum->m_impl->getChild(static_cast(ECameraFrustumPlanesPropertyStaticIndex::TopPlane))); + + ExpectPropertyTypeAndChildCount(inputs->getChild("viewport"), EPropertyType::Struct, 4); + ExpectPropertyTypeAndChildCount(inputs->getChild("frustum"), EPropertyType::Struct, 6); + + ExpectPropertyTypeAndChildCount(vpProperties->getChild("offsetX"), EPropertyType::Int32, 0); + ExpectPropertyTypeAndChildCount(vpProperties->getChild("offsetY"), EPropertyType::Int32, 0); + ExpectPropertyTypeAndChildCount(vpProperties->getChild("width"), EPropertyType::Int32, 0); + ExpectPropertyTypeAndChildCount(vpProperties->getChild("height"), EPropertyType::Int32, 0); + ExpectPropertyTypeAndChildCount(frustum->getChild("nearPlane"), EPropertyType::Float, 0); + ExpectPropertyTypeAndChildCount(frustum->getChild("farPlane"), EPropertyType::Float, 0); + ExpectPropertyTypeAndChildCount(frustum->getChild("leftPlane"), EPropertyType::Float, 0); + ExpectPropertyTypeAndChildCount(frustum->getChild("rightPlane"), EPropertyType::Float, 0); + ExpectPropertyTypeAndChildCount(frustum->getChild("bottomPlane"), EPropertyType::Float, 0); + ExpectPropertyTypeAndChildCount(frustum->getChild("topPlane"), EPropertyType::Float, 0); + } + + static void ExpectInputPropertiesWithoutFrustumPlanes(const RamsesCameraBindingImpl& cameraBinding) + { + EXPECT_FALSE(cameraBinding.hasFrustumPlanesProperties()); + + const auto inputs = cameraBinding.getInputs(); + ASSERT_EQ(2u, inputs->getChildCount()); + + const auto vpProperties = inputs->getChild("viewport"); + const auto frustum = inputs->getChild("frustum"); + ASSERT_EQ(4u, vpProperties->getChildCount()); + ASSERT_EQ(4u, frustum->getChildCount()); + + const auto vpOffsetX = vpProperties->getChild("offsetX"); + const auto vpOffsety = vpProperties->getChild("offsetY"); + const auto vpWidth = vpProperties->getChild("width"); + const auto vpHeight = vpProperties->getChild("height"); + + const auto nP = frustum->getChild("nearPlane"); + const auto fP = frustum->getChild("farPlane"); + const auto fov = frustum->getChild("fieldOfView"); + const auto aR = frustum->getChild("aspectRatio"); + + // Test that internal indices match properties resolved by name + EXPECT_EQ(vpProperties, inputs->getChild(static_cast(ECameraPropertyStructStaticIndex::Viewport))); + EXPECT_EQ(frustum, inputs->getChild(static_cast(ECameraPropertyStructStaticIndex::Frustum))); + + EXPECT_EQ(vpOffsetX, vpProperties->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortOffsetX))); + EXPECT_EQ(vpOffsety, vpProperties->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortOffsetY))); + EXPECT_EQ(vpWidth, vpProperties->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortWidth))); + EXPECT_EQ(vpHeight, vpProperties->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortHeight))); + EXPECT_EQ(nP, frustum->m_impl->getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::NearPlane))); + EXPECT_EQ(fP, frustum->m_impl->getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::FarPlane))); + EXPECT_EQ(fov, frustum->m_impl->getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::FieldOfView))); + EXPECT_EQ(aR, frustum->m_impl->getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::AspectRatio))); + + ExpectPropertyTypeAndChildCount(inputs->getChild("viewport"), EPropertyType::Struct, 4); + ExpectPropertyTypeAndChildCount(inputs->getChild("frustum"), EPropertyType::Struct, 4); + + ExpectPropertyTypeAndChildCount(vpProperties->getChild("offsetX"), EPropertyType::Int32, 0); + ExpectPropertyTypeAndChildCount(vpProperties->getChild("offsetY"), EPropertyType::Int32, 0); + ExpectPropertyTypeAndChildCount(vpProperties->getChild("width"), EPropertyType::Int32, 0); + ExpectPropertyTypeAndChildCount(vpProperties->getChild("height"), EPropertyType::Int32, 0); + ExpectPropertyTypeAndChildCount(frustum->getChild("nearPlane"), EPropertyType::Float, 0); + ExpectPropertyTypeAndChildCount(frustum->getChild("farPlane"), EPropertyType::Float, 0); + ExpectPropertyTypeAndChildCount(frustum->getChild("fieldOfView"), EPropertyType::Float, 0); + ExpectPropertyTypeAndChildCount(frustum->getChild("aspectRatio"), EPropertyType::Float, 0); + } + + RamsesTestSetup m_ramsesTestSetup; + ramses::Scene& m_testScene; + ramses::OrthographicCamera& m_orthoCam = { *m_testScene.createOrthographicCamera() }; + ramses::PerspectiveCamera& m_perspectiveCam = { *m_testScene.createPerspectiveCamera() }; + }; + + TEST_F(ARamsesCameraBinding, HasANameAfterCreation) + { + auto& cameraBinding = *m_logicEngine.createRamsesCameraBinding(*m_camera, "CameraBinding"); + EXPECT_EQ("CameraBinding", cameraBinding.getName()); + } + + TEST_F(ARamsesCameraBinding, HasAIdAfterCreation) + { + auto& cameraBinding = *m_logicEngine.createRamsesCameraBinding(*m_camera, "CameraBinding"); + EXPECT_EQ(cameraBinding.getId(), 1u); + } + + TEST_F(ARamsesCameraBinding, HasNoOutputsAfterCreation) + { + auto& cameraBinding = *m_logicEngine.createRamsesCameraBinding(*m_camera, ""); + EXPECT_EQ(nullptr, cameraBinding.getOutputs()); + } + + TEST_F(ARamsesCameraBinding, ProducesNoErrorsDuringUpdate_IfNoRamsesCameraIsAssigned) + { + auto& cameraBinding = *m_logicEngine.createRamsesCameraBinding(*m_camera, ""); + EXPECT_EQ(std::nullopt, cameraBinding.m_impl.update()); + } + + TEST_F(ARamsesCameraBinding, ReturnsReferenceToRamsesCamera) + { + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + EXPECT_EQ(&m_perspectiveCam, &cameraBinding.getRamsesCamera()); + } + + TEST_F(ARamsesCameraBinding, HasInputsAfterInitializingWithPerspectiveCamera) + { + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + ExpectInputPropertiesWithoutFrustumPlanes(cameraBinding.m_cameraBinding); + } + + TEST_F(ARamsesCameraBinding, HasInputsAfterInitializingFromOrthoCamera) + { + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_orthoCam, ""); + ExpectInputPropertiesWithFrustumPlanes(cameraBinding.m_cameraBinding); + } + + TEST_F(ARamsesCameraBinding, HasInputsAfterInitializingFromOrthoCamera_createdWithFrustumPlanes) + { + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBindingWithFrustumPlanes(m_orthoCam); + ExpectInputPropertiesWithFrustumPlanes(cameraBinding.m_cameraBinding); + } + + TEST_F(ARamsesCameraBinding, HasInputsAfterInitializingFromPerspectiveCamera_createdWithFrustumPlanes) + { + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBindingWithFrustumPlanes(m_perspectiveCam); + ExpectInputPropertiesWithFrustumPlanes(cameraBinding.m_cameraBinding); + } + + TEST_F(ARamsesCameraBinding, DoesNotOverwriteDefaultValues_WhenCreatedFromOrthoCamera) + { + m_logicEngine.createRamsesCameraBinding(m_orthoCam, ""); + m_logicEngine.update(); + + //Expect default values on the camera, because nothing was set so far + ExpectDefaultViewportValues(m_orthoCam); + ExpectDefaultCameraFrustumPlanes(m_orthoCam); + } + + TEST_F(ARamsesCameraBinding, DoesNotOverwriteDefaultValues_WhenCreatedFromPerspectiveCamera) + { + m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + m_logicEngine.update(); + + //Expect default values on the camera, because nothing was set so far + ExpectDefaultViewportValues(m_perspectiveCam); + ExpectDefaultPerspectiveCameraFrustumValues(m_perspectiveCam); + } + + TEST_F(ARamsesCameraBinding, ReportsErrorOnUpdate_WhenSettingZeroToViewportSize) + { + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_orthoCam, ""); + const auto inputs = cameraBinding.getInputs(); + auto vpProperties = inputs->getChild("viewport"); + // Setting illegal viewport values: width and height cannot be 0 so an error will be produced on ramses camera + vpProperties->getChild("width")->set(0); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Camera viewport size must be positive! (width: 0; height: 16)"); + + // Fix width, break height -> still generates error + vpProperties->getChild("width")->set(8); + vpProperties->getChild("height")->set(0); + + // Expect default values on the camera, because setting values failed + ExpectDefaultViewportValues(m_orthoCam); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Camera viewport size must be positive! (width: 8; height: 0)"); + + // Fix height and update recovers the errors + vpProperties->getChild("height")->set(32); + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_EQ(m_orthoCam.getViewportWidth(), 8u); + EXPECT_EQ(m_orthoCam.getViewportHeight(), 32u); + } + + TEST_F(ARamsesCameraBinding, ReportsErrorOnUpdate_WhenSettingNegativeViewportSize) + { + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_orthoCam, ""); + const auto inputs = cameraBinding.getInputs(); + auto vpProperties = inputs->getChild("viewport"); + // Setting illegal viewport values: width and height cannot be 0 so an error will be produced on ramses camera + vpProperties->getChild("width")->set(-1); + vpProperties->getChild("height")->set(-1); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Camera viewport size must be positive! (width: -1; height: -1)"); + + // Setting positive values recovers from the error + vpProperties->getChild("width")->set(10); + vpProperties->getChild("height")->set(12); + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_EQ(m_orthoCam.getViewportWidth(), 10u); + EXPECT_EQ(m_orthoCam.getViewportHeight(), 12u); + } + + TEST_F(ARamsesCameraBinding, ReportsErrorOnUpdate_WhenSettingInvalidFrustumValuesOnOrthoCamera) + { + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_orthoCam, ""); + + auto frustum = cameraBinding.getInputs()->getChild("frustum"); + // Setting illegal frustum values: left plane cannot be smaller than right plane so an error will be produced on ramses camera + frustum->getChild("leftPlane")->set(2.f); + frustum->getChild("rightPlane")->set(1.f); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Camera::setFrustum failed - check validity of given frustum planes"); + + //Still expect default values on the camera, because setting values failed + ExpectDefaultCameraFrustumPlanes(m_orthoCam); + + // Recovers from the error once values are ok + frustum->getChild("rightPlane")->set(3.f); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ARamsesCameraBinding, ReportsErrorOnUpdate_WhenSettingInvalidFrustumValuesOnPerspectiveCamera) + { + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + + auto frustum = cameraBinding.getInputs()->getChild("frustum"); + // Setting illegal frustum values: fov and aspect ratio cannot be 0 so an error will be produced on ramses camera + frustum->getChild("fieldOfView")->set(0.f); + frustum->getChild("aspectRatio")->set(0.f); + + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "PerspectiveCamera::setFrustum failed - check validity of given frustum planes"); + + // Fixing just the FOV does not fix the issue, need to also fix aspect ratio + frustum->getChild("fieldOfView")->set(15.f); + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "PerspectiveCamera::setFrustum failed - check validity of given frustum planes"); + + //Still expect default values on the camera, because setting values failed + ExpectDefaultViewportValues(m_perspectiveCam); + ExpectDefaultPerspectiveCameraFrustumValues(m_perspectiveCam); + + frustum->getChild("aspectRatio")->set(1.f); + EXPECT_TRUE(m_logicEngine.update()); + } + + TEST_F(ARamsesCameraBinding, InitializesInputPropertiesOfPerpespectiveCameraToMatchRamsesDefaultValues) + { + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + + const auto inputs = cameraBinding.getInputs(); + ASSERT_NE(nullptr, inputs); + + const auto vpProperties = inputs->getChild("viewport"); + const auto frustum = inputs->getChild("frustum"); + ASSERT_EQ(4u, vpProperties->getChildCount()); + ASSERT_EQ(4u, frustum->getChildCount()); + + EXPECT_EQ(*vpProperties->getChild("offsetX")->get(), m_perspectiveCam.getViewportX()); + EXPECT_EQ(*vpProperties->getChild("offsetY")->get(), m_perspectiveCam.getViewportY()); + EXPECT_EQ(static_cast(*vpProperties->getChild("width")->get()), m_perspectiveCam.getViewportWidth()); + EXPECT_EQ(static_cast(*vpProperties->getChild("height")->get()), m_perspectiveCam.getViewportHeight()); + + EXPECT_EQ(*frustum->getChild("nearPlane")->get(), m_perspectiveCam.getNearPlane()); + EXPECT_EQ(*frustum->getChild("farPlane")->get(), m_perspectiveCam.getFarPlane()); + EXPECT_NEAR(*frustum->getChild("fieldOfView")->get(), m_perspectiveCam.getVerticalFieldOfView(), 0.001f); + EXPECT_EQ(*frustum->getChild("aspectRatio")->get(), m_perspectiveCam.getAspectRatio()); + } + + TEST_F(ARamsesCameraBinding, InitializesInputPropertiesOfOrthographicCameraToMatchRamsesDefaultValues) + { + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_orthoCam, ""); + + const auto inputs = cameraBinding.getInputs(); + ASSERT_NE(nullptr, inputs); + + const auto vpProperties = inputs->getChild("viewport"); + const auto frustum = inputs->getChild("frustum"); + ASSERT_EQ(4u, vpProperties->getChildCount()); + ASSERT_EQ(6u, frustum->getChildCount()); + + EXPECT_EQ(*vpProperties->getChild("offsetX")->get(), m_orthoCam.getViewportX()); + EXPECT_EQ(*vpProperties->getChild("offsetY")->get(), m_orthoCam.getViewportY()); + EXPECT_EQ(static_cast(*vpProperties->getChild("width")->get()), m_orthoCam.getViewportWidth()); + EXPECT_EQ(static_cast(*vpProperties->getChild("height")->get()), m_orthoCam.getViewportHeight()); + + EXPECT_EQ(*frustum->getChild("nearPlane")->get(), m_orthoCam.getNearPlane()); + EXPECT_EQ(*frustum->getChild("farPlane")->get(), m_orthoCam.getFarPlane()); + EXPECT_EQ(*frustum->getChild("leftPlane")->get(), m_orthoCam.getLeftPlane()); + EXPECT_EQ(*frustum->getChild("rightPlane")->get(), m_orthoCam.getRightPlane()); + EXPECT_EQ(*frustum->getChild("bottomPlane")->get(), m_orthoCam.getBottomPlane()); + EXPECT_EQ(*frustum->getChild("topPlane")->get(), m_orthoCam.getTopPlane()); + } + + TEST_F(ARamsesCameraBinding, MarksInputsAsBindingInputsForPerspectiveCameraBinding) + { + auto* cameraBinding = m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + + const auto inputs = cameraBinding->getInputs(); + for (size_t i = 0; i < inputs->getChildCount(); ++i) + { + const auto inputStruct = inputs->getChild(i); + EXPECT_EQ(EPropertySemantics::BindingInput, inputStruct->m_impl->getPropertySemantics()); + + for (size_t j = 0; j < inputs->getChild(i)->getChildCount(); ++j) + { + const auto inputProperty = inputStruct->m_impl->getChild(j); + EXPECT_EQ(EPropertySemantics::BindingInput, inputProperty->m_impl->getPropertySemantics()); + } + } + } + + TEST_F(ARamsesCameraBinding, MarksInputsAsBindingInputsForOrthoCameraBinding) + { + auto* cameraBinding = m_logicEngine.createRamsesCameraBinding(m_orthoCam, ""); + + const auto inputs = cameraBinding->getInputs(); + for (size_t i = 0; i < inputs->getChildCount(); ++i) + { + const auto inputStruct = inputs->getChild(i); + EXPECT_EQ(EPropertySemantics::BindingInput, inputStruct->m_impl->getPropertySemantics()); + + for (size_t j = 0; j < inputs->getChild(i)->getChildCount(); ++j) + { + const auto inputProperty = inputStruct->m_impl->getChild(j); + EXPECT_EQ(EPropertySemantics::BindingInput, inputProperty->m_impl->getPropertySemantics()); + } + } + } + + TEST_F(ARamsesCameraBinding, ReturnsBoundRamsesCamera) + { + auto* cameraBinding = m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + + EXPECT_EQ(&m_perspectiveCam, &cameraBinding->getRamsesCamera()); + } + + TEST_F(ARamsesCameraBinding, DoesNotModifyRamsesWithoutUpdateBeingCalledWithPerspectiveCamera) + { + auto* cameraBinding = m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + + auto inputs = cameraBinding->getInputs(); + auto vpProperties = inputs->getChild("viewport"); + auto frustum = inputs->getChild("frustum"); + + vpProperties->getChild("offsetX")->set(4); + vpProperties->getChild("offsetY")->set(7); + vpProperties->getChild("width")->set(11); + vpProperties->getChild("height")->set(19); + + frustum->getChild("nearPlane")->set(3.1f); + frustum->getChild("farPlane")->set(.2f); + frustum->getChild("fieldOfView")->set(4.2f); + frustum->getChild("aspectRatio")->set(2.1f); + + ExpectDefaultValues(*cameraBinding); + } + + TEST_F(ARamsesCameraBinding, DoesNotModifyRamsesWithoutUpdateBeingCalledWithOrthoCamera) + { + auto* cameraBinding = m_logicEngine.createRamsesCameraBinding(m_orthoCam, ""); + + auto inputs = cameraBinding->getInputs(); + auto vpProperties = inputs->getChild("viewport"); + auto frustum = inputs->getChild("frustum"); + + vpProperties->getChild("offsetX")->set(4); + vpProperties->getChild("offsetY")->set(7); + vpProperties->getChild("width")->set(11); + vpProperties->getChild("height")->set(19); + + frustum->getChild("nearPlane")->set(3.1f); + frustum->getChild("farPlane")->set(.2f); + frustum->getChild("leftPlane")->set(6.2f); + frustum->getChild("rightPlane")->set(2.8f); + frustum->getChild("bottomPlane")->set(1.9f); + frustum->getChild("topPlane")->set(7.1f); + + ExpectDefaultValues(*cameraBinding); + } + + TEST_F(ARamsesCameraBinding, ModifiesRamsesPerspectiveCamOnUpdate_OnlyAfterExplicitlyAssignedToInputs) + { + auto* cameraBinding = m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + + auto inputs = cameraBinding->getInputs(); + auto vpProperties = inputs->getChild("viewport"); + auto frustum = inputs->getChild("frustum"); + + const int32_t newVpOffsetX = 23; + vpProperties->getChild("offsetX")->set(newVpOffsetX); + + // Update not called yet -> still default values + ExpectDefaultValues(*cameraBinding); + + cameraBinding->m_cameraBinding.update(); + // Only propagated vpOffsetX, the others have default values + EXPECT_EQ(m_perspectiveCam.getViewportX(), newVpOffsetX); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 0); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 16u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 16u); + + ExpectDefaultPerspectiveCameraFrustumValues(m_perspectiveCam); + + // Set and test all properties + const int32_t newVpOffsetY = 13; + const int32_t newVpWidth = 56; + const int32_t newVpHeight = 45; + + const float newFov = 30.f; + const float newAR = 640.f / 480.f; + const float newNearPlane = 4.4f; + const float newFarPlane = 5.1f; + + vpProperties->getChild("offsetY")->set(newVpOffsetY); + vpProperties->getChild("width")->set(newVpWidth); + vpProperties->getChild("height")->set(newVpHeight); + + frustum->getChild("fieldOfView")->set(newFov); + frustum->getChild("aspectRatio")->set(newAR); + frustum->getChild("nearPlane")->set(newNearPlane); + frustum->getChild("farPlane")->set(newFarPlane); + cameraBinding->m_cameraBinding.update(); + + EXPECT_EQ(m_perspectiveCam.getViewportX(), newVpOffsetX); + EXPECT_EQ(m_perspectiveCam.getViewportY(), newVpOffsetY); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), static_cast(newVpWidth)); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), static_cast(newVpHeight)); + + EXPECT_NEAR(m_perspectiveCam.getVerticalFieldOfView(), newFov, 0.001f); + EXPECT_EQ(m_perspectiveCam.getAspectRatio(), newAR); + EXPECT_EQ(m_perspectiveCam.getNearPlane(), newNearPlane); + EXPECT_EQ(m_perspectiveCam.getFarPlane(), newFarPlane); + } + + TEST_F(ARamsesCameraBinding, ModifiesRamsesOrthoCamOnUpdate_OnlyAfterExplicitlyAssignedToInputs) + { + auto* cameraBinding = m_logicEngine.createRamsesCameraBinding(m_orthoCam, ""); + + auto inputs = cameraBinding->getInputs(); + auto vpProperties = inputs->getChild("viewport"); + auto frustum = inputs->getChild("frustum"); + + const int32_t newVpOffsetX = 23; + vpProperties->getChild("offsetX")->set(newVpOffsetX); + + // Update not called yet -> still default values + ExpectDefaultValues(*cameraBinding); + + cameraBinding->m_cameraBinding.update(); + // Only propagated vpOffsetX, the others have default values + EXPECT_EQ(m_orthoCam.getViewportX(), newVpOffsetX); + EXPECT_EQ(m_orthoCam.getViewportY(), 0); + EXPECT_EQ(m_orthoCam.getViewportWidth(), 16u); + EXPECT_EQ(m_orthoCam.getViewportHeight(), 16u); + + ExpectDefaultCameraFrustumPlanes(m_orthoCam); + + // Set and test all properties + const int32_t newVpOffsetY = 13; + const int32_t newVpWidth = 56; + const int32_t newVpHeight = 45; + + const float newLeftPlane = 0.2f; + const float newRightPlane = 0.3f; + const float newBottomPlane = 0.4f; + const float newTopPlane = 0.5f; + const float newNearPlane = 4.f; + const float newFarPlane = 5.1f; + + vpProperties->getChild("offsetY")->set(newVpOffsetY); + vpProperties->getChild("width")->set(newVpWidth); + vpProperties->getChild("height")->set(newVpHeight); + + frustum->getChild("leftPlane")->set(newLeftPlane); + frustum->getChild("rightPlane")->set(newRightPlane); + frustum->getChild("bottomPlane")->set(newBottomPlane); + frustum->getChild("topPlane")->set(newTopPlane); + frustum->getChild("nearPlane")->set(newNearPlane); + frustum->getChild("farPlane")->set(newFarPlane); + cameraBinding->m_cameraBinding.update(); + + EXPECT_EQ(m_orthoCam.getViewportX(), newVpOffsetX); + EXPECT_EQ(m_orthoCam.getViewportY(), newVpOffsetY); + EXPECT_EQ(m_orthoCam.getViewportWidth(), static_cast(newVpWidth)); + EXPECT_EQ(m_orthoCam.getViewportHeight(), static_cast(newVpHeight)); + + EXPECT_EQ(m_orthoCam.getLeftPlane(), newLeftPlane); + EXPECT_EQ(m_orthoCam.getRightPlane(), newRightPlane); + EXPECT_EQ(m_orthoCam.getBottomPlane(), newBottomPlane); + EXPECT_EQ(m_orthoCam.getTopPlane(), newTopPlane); + EXPECT_EQ(m_orthoCam.getNearPlane(), newNearPlane); + EXPECT_EQ(m_orthoCam.getFarPlane(), newFarPlane); + } + + TEST_F(ARamsesCameraBinding, PropagatesItsInputsToRamsesPerspectiveCameraOnUpdate_WithLinksInsteadOfSetCall) + { + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.vpProps = { + vpX = Type:Int32(), + vpY = Type:Int32(), + vpW = Type:Int32(), + vpH = Type:Int32() + } + end + function run(IN,OUT) + OUT.vpProps = { + vpX = 5, + vpY = 10, + vpW = 35, + vpH = 19 + } + end + )"; + + LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + + auto* cameraBinding = m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("vpProps")->getChild("vpX"), *cameraBinding->getInputs()->getChild("viewport")->getChild("offsetX"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("vpProps")->getChild("vpY"), *cameraBinding->getInputs()->getChild("viewport")->getChild("offsetY"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("vpProps")->getChild("vpW"), *cameraBinding->getInputs()->getChild("viewport")->getChild("width"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("vpProps")->getChild("vpH"), *cameraBinding->getInputs()->getChild("viewport")->getChild("height"))); + + // Links have no effect before update() explicitly called + ExpectDefaultValues(*cameraBinding); + + m_logicEngine.update(); + + // Linked values got updates, not-linked values were not modified + EXPECT_EQ(m_perspectiveCam.getViewportX(), 5); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 10); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 35u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 19u); + ExpectDefaultPerspectiveCameraFrustumValues(m_perspectiveCam); + } + + TEST_F(ARamsesCameraBinding, PropagatesItsInputsToRamsesOrthoCameraOnUpdate_WithLinksInsteadOfSetCall) + { + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.frustProps = { + lP = Type:Float(), + rP = Type:Float(), + bP = Type:Float(), + tP = Type:Float(), + nP = Type:Float(), + fP = Type:Float() + } + end + function run(IN,OUT) + OUT.frustProps = { + lP = 0.2, + rP = 0.3, + bP = 0.4, + tP = 0.5, + nP = 0.6, + fP = 0.7 + } + end + )"; + + LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + + auto* cameraBinding = m_logicEngine.createRamsesCameraBinding(m_orthoCam, ""); + + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("frustProps")->getChild("lP"), *cameraBinding->getInputs()->getChild("frustum")->getChild("leftPlane"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("frustProps")->getChild("rP"), *cameraBinding->getInputs()->getChild("frustum")->getChild("rightPlane"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("frustProps")->getChild("bP"), *cameraBinding->getInputs()->getChild("frustum")->getChild("bottomPlane"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("frustProps")->getChild("tP"), *cameraBinding->getInputs()->getChild("frustum")->getChild("topPlane"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("frustProps")->getChild("nP"), *cameraBinding->getInputs()->getChild("frustum")->getChild("nearPlane"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("frustProps")->getChild("fP"), *cameraBinding->getInputs()->getChild("frustum")->getChild("farPlane"))); + + // Links have no effect before update() explicitly called + ExpectDefaultValues(*cameraBinding); + + m_logicEngine.update(); + + // Linked values got updates, not-linked values were not modified + EXPECT_EQ(m_orthoCam.getLeftPlane(), 0.2f); + EXPECT_EQ(m_orthoCam.getRightPlane(), 0.3f); + EXPECT_EQ(m_orthoCam.getBottomPlane(), 0.4f); + EXPECT_EQ(m_orthoCam.getTopPlane(), 0.5f); + EXPECT_EQ(m_orthoCam.getNearPlane(), 0.6f); + EXPECT_EQ(m_orthoCam.getFarPlane(), 0.7f); + ExpectDefaultViewportValues(m_orthoCam); + } + + TEST_F(ARamsesCameraBinding, DoesNotOverrideExistingValuesAfterRamsesCameraIsAssignedToBinding) + { + m_perspectiveCam.setViewport(3, 4, 10u, 11u); + m_perspectiveCam.setFrustum(30.f, 640.f / 480.f, 2.3f, 5.6f); + + m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + + EXPECT_EQ(m_perspectiveCam.getViewportX(), 3); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 4); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 10u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 11u); + + EXPECT_NEAR(m_perspectiveCam.getVerticalFieldOfView(), 30.f, 0.001f); + EXPECT_EQ(m_perspectiveCam.getAspectRatio(), 640.f / 480.f); + EXPECT_EQ(m_perspectiveCam.getNearPlane(), 2.3f); + EXPECT_EQ(m_perspectiveCam.getFarPlane(), 5.6f); + } + + // This fixture only contains serialization unit tests, for higher order tests see `ARamsesCameraBinding_SerializationWithFile` + class ARamsesCameraBinding_SerializationLifecycle : public ARamsesCameraBinding + { + protected: + flatbuffers::Offset serializeRootInput(bool withFrustumPlanes, bool withError = false) + { + std::vector frustumPlanes = { + TypeData{ "nearPlane", EPropertyType::Float }, + TypeData{ "farPlane", EPropertyType::Float }, + }; + + if (withFrustumPlanes) + { + frustumPlanes.emplace_back("leftPlane", EPropertyType::Float); + frustumPlanes.emplace_back("rightPlane", EPropertyType::Float); + frustumPlanes.emplace_back("bottomPlane", EPropertyType::Float); + if (!withError) + frustumPlanes.emplace_back("topPlane", EPropertyType::Float); + } + else + { + frustumPlanes.emplace_back("fieldOfView", EPropertyType::Float); + frustumPlanes.emplace_back("aspectRatio", EPropertyType::Float); + } + + HierarchicalTypeData cameraBindingInputs( + TypeData{ "", EPropertyType::Struct }, + { + MakeStruct("viewport", + { + TypeData{"offsetX", EPropertyType::Int32}, + TypeData{"offsetY", EPropertyType::Int32}, + TypeData{"width", EPropertyType::Int32}, + TypeData{"height", EPropertyType::Int32} + } + ), + MakeStruct("frustum", frustumPlanes), + } + ); + + return PropertyImpl::Serialize(PropertyImpl{ cameraBindingInputs, EPropertySemantics::BindingInput }, m_flatBufferBuilder, m_serializationMap); + } + + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + SerializationTestUtils m_testUtils{ m_flatBufferBuilder }; + ::testing::StrictMock m_resolverMock; + ErrorReporting m_errorReporting; + SerializationMap m_serializationMap; + DeserializationMap m_deserializationMap; + }; + + // More unit tests with inputs/outputs declared in LogicNode (base class) serialization tests + TEST_F(ARamsesCameraBinding_SerializationLifecycle, RemembersBaseClassData) + { + // Serialize + { + RamsesCameraBindingImpl binding(*m_camera, true, "name", 1u); + binding.createRootProperties(); + (void)RamsesCameraBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + // Inspect flatbuffers data + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serializedBinding.base()); + ASSERT_TRUE(serializedBinding.base()->base()); + ASSERT_TRUE(serializedBinding.base()->base()->name()); + EXPECT_EQ(serializedBinding.base()->base()->name()->string_view(), "name"); + EXPECT_EQ(serializedBinding.base()->base()->id(), 1u); + + ASSERT_TRUE(serializedBinding.base()->rootInput()); + EXPECT_EQ(serializedBinding.base()->rootInput()->rootType(), rlogic_serialization::EPropertyRootType::Struct); + ASSERT_TRUE(serializedBinding.base()->rootInput()->children()); + EXPECT_EQ(serializedBinding.base()->rootInput()->children()->size(), 2u); + + // Deserialize + { + EXPECT_CALL(m_resolverMock, findRamsesCameraInScene(::testing::Eq("name"), m_camera->getSceneObjectId())).WillOnce(::testing::Return(m_camera)); + std::unique_ptr deserializedBinding = RamsesCameraBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + + ASSERT_TRUE(deserializedBinding); + EXPECT_EQ(deserializedBinding->getName(), "name"); + EXPECT_EQ(deserializedBinding->getId(), 1u); + EXPECT_EQ(deserializedBinding->getInputs()->getType(), EPropertyType::Struct); + EXPECT_EQ(deserializedBinding->getInputs()->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + EXPECT_EQ(deserializedBinding->getInputs()->getName(), ""); + EXPECT_EQ(deserializedBinding->getInputs()->getChildCount(), 2u); + } + } + + TEST_F(ARamsesCameraBinding_SerializationLifecycle, RemembersRamsesCameraId) + { + // Serialize + { + RamsesCameraBindingImpl binding(*m_camera, true, "name", 1u); + binding.createRootProperties(); + (void)RamsesCameraBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + // Inspect flatbuffers data + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + EXPECT_EQ(serializedBinding.base()->boundRamsesObject()->objectId(), m_camera->getSceneObjectId().getValue()); + EXPECT_EQ(serializedBinding.base()->boundRamsesObject()->objectType(), static_cast(ramses::ERamsesObjectType::OrthographicCamera)); + + // Deserialize + { + EXPECT_CALL(m_resolverMock, findRamsesCameraInScene(::testing::Eq("name"), m_camera->getSceneObjectId())).WillOnce(::testing::Return(m_camera)); + std::unique_ptr deserializedBinding = RamsesCameraBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + + ASSERT_TRUE(deserializedBinding); + EXPECT_EQ(&deserializedBinding->getRamsesCamera(), m_camera); + } + } + + TEST_F(ARamsesCameraBinding_SerializationLifecycle, SerializesInputProperties_withFrustumPlanes) + { + // Serialize + { + RamsesCameraBindingImpl binding(*m_camera, true, "name", 1u); + binding.createRootProperties(); + (void)RamsesCameraBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + // Deserialize + { + EXPECT_CALL(m_resolverMock, findRamsesCameraInScene(::testing::Eq("name"), m_camera->getSceneObjectId())).WillOnce(::testing::Return(m_camera)); + std::unique_ptr deserializedBinding = RamsesCameraBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + ASSERT_TRUE(deserializedBinding); + + ExpectInputPropertiesWithFrustumPlanes(*deserializedBinding); + } + } + + TEST_F(ARamsesCameraBinding_SerializationLifecycle, SerializesInputProperties_withoutFrustumPlanes) + { + auto perspCamera = m_scene->createPerspectiveCamera(); + + // Serialize + { + RamsesCameraBindingImpl binding(*perspCamera, false, "name", 1u); + binding.createRootProperties(); + (void)RamsesCameraBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + // Deserialize + { + EXPECT_CALL(m_resolverMock, findRamsesCameraInScene(::testing::Eq("name"), perspCamera->getSceneObjectId())).WillOnce(::testing::Return(perspCamera)); + std::unique_ptr deserializedBinding = RamsesCameraBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + ASSERT_TRUE(deserializedBinding); + + ExpectInputPropertiesWithoutFrustumPlanes(*deserializedBinding); + } + } + + TEST_F(ARamsesCameraBinding_SerializationLifecycle, ProducesErrorIfInputPropertiesInvalid) + { + { + auto ramsesRef = rlogic_serialization::CreateRamsesReference( + m_flatBufferBuilder, + 12u, + static_cast(ramses::ERamsesObjectType::OrthographicCamera) + ); + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + ramsesRef, + serializeRootInput(true, true) + ); + auto binding = rlogic_serialization::CreateRamsesCameraBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesCameraBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesCameraBinding from serialized data: missing or invalid input properties!"); + } + + TEST_F(ARamsesCameraBinding_SerializationLifecycle, ErrorWhenNoBindingBaseData) + { + { + auto binding = rlogic_serialization::CreateRamsesCameraBinding( + m_flatBufferBuilder, + 0 // no base binding info + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesCameraBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesCameraBinding from serialized data: missing base class info!"); + } + + TEST_F(ARamsesCameraBinding_SerializationLifecycle, ErrorWhenNoBindingName) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + 0, // no name! + 1u) + ); + auto binding = rlogic_serialization::CreateRamsesCameraBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesCameraBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing name!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of RamsesCameraBinding from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ARamsesCameraBinding_SerializationLifecycle, ErrorWhenNoBindingId) + { + { + auto base = rlogic_serialization::CreateRamsesBinding(m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 0)); + auto binding = rlogic_serialization::CreateRamsesCameraBinding(m_flatBufferBuilder, base); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesCameraBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing or invalid ID!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of RamsesCameraBinding from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ARamsesCameraBinding_SerializationLifecycle, ErrorWhenNoRootInput) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0 // no root input + ); + auto binding = rlogic_serialization::CreateRamsesCameraBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesCameraBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesCameraBinding from serialized data: missing root input!"); + } + + TEST_F(ARamsesCameraBinding_SerializationLifecycle, ErrorWhenRootInputHasErrors) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0, + m_testUtils.serializeTestProperty("", rlogic_serialization::EPropertyRootType::Struct, false, true) // rootInput with errors + ); + auto binding = rlogic_serialization::CreateRamsesCameraBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesCameraBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: missing name!"); + } + + TEST_F(ARamsesCameraBinding_SerializationLifecycle, ErrorWhenBoundCameraCannotBeResolved) + { + const ramses::sceneObjectId_t mockObjectId{ 12 }; + { + auto ramsesRef = rlogic_serialization::CreateRamsesReference( + m_flatBufferBuilder, + mockObjectId.getValue() + ); + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + ramsesRef, + serializeRootInput(true) + ); + auto binding = rlogic_serialization::CreateRamsesCameraBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + EXPECT_CALL(m_resolverMock, findRamsesCameraInScene(::testing::Eq("name"), mockObjectId)).WillOnce(::testing::Return(nullptr)); + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesCameraBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + } + + TEST_F(ARamsesCameraBinding_SerializationLifecycle, ErrorWhenSavedCameraTypeDoesNotMatchResolvedCameraType) + { + RamsesTestSetup ramses; + ramses::Scene* scene = ramses.createScene(); + auto* perspCamera = scene->createPerspectiveCamera(); + + const ramses::sceneObjectId_t mockObjectId{ 12 }; + { + auto ramsesRef = rlogic_serialization::CreateRamsesReference( + m_flatBufferBuilder, + mockObjectId.getValue(), + uint32_t(ramses::ERamsesObjectType::OrthographicCamera) // save ortho camera + ); + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + ramsesRef, + serializeRootInput(false) + ); + auto binding = rlogic_serialization::CreateRamsesCameraBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + // resolver returns perspective camera, but orthographic camera is expected -> error + EXPECT_CALL(m_resolverMock, findRamsesCameraInScene(::testing::Eq("name"), mockObjectId)).WillOnce(::testing::Return(perspCamera)); + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesCameraBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesCameraBinding from serialized data: loaded type does not match referenced camera type!"); + } + + class ARamsesCameraBinding_SerializationWithFile : public ARamsesCameraBinding + { + protected: + WithTempDirectory tempFolder; + }; + + TEST_F(ARamsesCameraBinding_SerializationWithFile, ContainsItsDataAfterLoading) + { + const int32_t newVpOffsetX = 10; + const int32_t newVpOffsetY = 13; + const int32_t newVpWidth = 56; + const int32_t newVpHeight = 45; + + const float newFov = 30.f; + const float newAR = 640.f / 480.f; + const float newNearPlane = 4.4f; + const float newFarPlane = 5.1f; + { + auto& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, "CameraBinding"); + + auto inputs = cameraBinding.getInputs(); + auto vpProperties = inputs->getChild("viewport"); + auto frustum = inputs->getChild("frustum"); + + vpProperties->getChild("offsetX")->set(newVpOffsetX); + vpProperties->getChild("offsetY")->set(newVpOffsetY); + vpProperties->getChild("width")->set(newVpWidth); + vpProperties->getChild("height")->set(newVpHeight); + + frustum->getChild("fieldOfView")->set(newFov); + frustum->getChild("aspectRatio")->set(newAR); + frustum->getChild("nearPlane")->set(newNearPlane); + frustum->getChild("farPlane")->set(newFarPlane); + m_logicEngine.update(); + EXPECT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "camerabinding.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("camerabinding.bin", &m_testScene)); + const auto& loadedCameraBinding = *m_logicEngine.findByName("CameraBinding"); + EXPECT_EQ("CameraBinding", loadedCameraBinding.getName()); + EXPECT_EQ(loadedCameraBinding.getId(), 1u); + EXPECT_EQ(loadedCameraBinding.getRamsesCamera().getSceneObjectId(), m_perspectiveCam.getSceneObjectId()); + + const auto& inputs = loadedCameraBinding.getInputs(); + ASSERT_EQ(inputs->getChildCount(), 2u); + auto vpProperties = inputs->getChild("viewport"); + auto frustum = inputs->getChild("frustum"); + ASSERT_EQ(vpProperties->getChildCount(), 4u); + ASSERT_EQ(vpProperties->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + ASSERT_EQ(frustum->getChildCount(), 4u); + ASSERT_EQ(frustum->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + + EXPECT_EQ(*vpProperties->getChild("offsetX")->get(), newVpOffsetX); + EXPECT_EQ(vpProperties->getChild("offsetX")->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + EXPECT_EQ(*vpProperties->getChild("offsetY")->get(), newVpOffsetY); + EXPECT_EQ(vpProperties->getChild("offsetY")->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + EXPECT_EQ(*vpProperties->getChild("width")->get(), newVpWidth); + EXPECT_EQ(vpProperties->getChild("width")->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + EXPECT_EQ(*vpProperties->getChild("height")->get(), newVpHeight); + EXPECT_EQ(vpProperties->getChild("height")->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + + EXPECT_EQ(*frustum->getChild("nearPlane")->get(), newNearPlane); + EXPECT_EQ(frustum->getChild("nearPlane")->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + EXPECT_EQ(*frustum->getChild("farPlane")->get(), newFarPlane); + EXPECT_EQ(frustum->getChild("farPlane")->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + EXPECT_NEAR(*frustum->getChild("fieldOfView")->get(), newFov, 0.001f); + EXPECT_EQ(frustum->getChild("fieldOfView")->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + EXPECT_EQ(*frustum->getChild("aspectRatio")->get(), newAR); + EXPECT_EQ(frustum->getChild("aspectRatio")->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + + // Test that internal indices match properties resolved by name + EXPECT_EQ(vpProperties->getChild("offsetX"), vpProperties->m_impl->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortOffsetX))); + EXPECT_EQ(vpProperties->getChild("offsetY"), vpProperties->m_impl->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortOffsetY))); + EXPECT_EQ(vpProperties->getChild("width"), vpProperties->m_impl->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortWidth))); + EXPECT_EQ(vpProperties->getChild("height"), vpProperties->m_impl->getChild(static_cast(ECameraViewportPropertyStaticIndex::ViewPortHeight))); + + EXPECT_EQ(frustum->getChild("nearPlane"), frustum->m_impl->getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::NearPlane))); + EXPECT_EQ(frustum->getChild("farPlane"), frustum->m_impl->getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::FarPlane))); + EXPECT_EQ(frustum->getChild("fieldOfView"), frustum->m_impl->getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::FieldOfView))); + EXPECT_EQ(frustum->getChild("aspectRatio"), frustum->m_impl->getChild(static_cast(EPerspectiveCameraFrustumPropertyStaticIndex::AspectRatio))); + } + } + + TEST_F(ARamsesCameraBinding_SerializationWithFile, KeepsItsProperties_WhenNoRamsesLinksAndSceneProvided) + { + { + m_logicEngine.createRamsesCameraBinding(*m_camera, "CameraBinding"); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "camerabinding.bin")); + } + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("camerabinding.bin", m_scene)); + auto loadedCameraBinding = m_logicEngine.findByName("CameraBinding"); + EXPECT_EQ(&loadedCameraBinding->getRamsesCamera(), m_camera); + EXPECT_EQ(loadedCameraBinding->getInputs()->getChildCount(), 2u); + EXPECT_EQ(loadedCameraBinding->getOutputs(), nullptr); + EXPECT_EQ(loadedCameraBinding->getName(), "CameraBinding"); + } + } + + TEST_F(ARamsesCameraBinding_SerializationWithFile, RestoresLinkToRamsesCamera) + { + { + m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, "CameraBinding"); + EXPECT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "camerabinding.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("camerabinding.bin", &m_testScene)); + const auto& cameraBinding = *m_logicEngine.findByName("CameraBinding"); + EXPECT_EQ(&cameraBinding.getRamsesCamera(), &m_perspectiveCam); + } + } + + TEST_F(ARamsesCameraBinding_SerializationWithFile, ProducesError_WhenHavingLinkToRamsesCamera_ButNoSceneWasProvided) + { + { + m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, "CameraBinding"); + EXPECT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "camerabinding.bin")); + } + { + EXPECT_FALSE(m_logicEngine.loadFromFile("camerabinding.bin")); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(errors.size(), 1u); + EXPECT_EQ(errors[0].message, "Fatal error during loading from file! File contains references to Ramses objects but no Ramses scene was provided!"); + } + } + + TEST_F(ARamsesCameraBinding_SerializationWithFile, HandlesErrorWhenRamsesSceneWasSerializedWithOneTypeOfCamera_ButLoadedWithADifferentOne) + { + { + m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + EXPECT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "camerabinding.bin")); + } + + // Fake that the ramses scene is exactly the same, just camera type changed (perspective -> ortho) + // This is a bit evil, but quite possible e.g. if camera was switched from perspective -> ortho and the ramses id didn't change because no other change in the scene and + // the camera is exported at the exact same time -> receives the same ID + ramses::Scene& slightlyModifiedScene = *m_ramsesTestSetup.createScene(ramses::sceneId_t(2)); + slightlyModifiedScene.createOrthographicCamera(""); + { + // loadFromFile catches the error -> content is not modified + EXPECT_FALSE(m_logicEngine.loadFromFile("camerabinding.bin", &slightlyModifiedScene)); + // Update still works with the old state of the logic engine + EXPECT_TRUE(m_logicEngine.update()); + } + } + + TEST_F(ARamsesCameraBinding_SerializationWithFile, ProducesError_WhenHavingLinkToRamsesCamera_WhichWasDeleted) + { + { + m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, "CameraBinding"); + EXPECT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "camerabinding.bin")); + } + + m_testScene.destroy(m_perspectiveCam); + + { + EXPECT_FALSE(m_logicEngine.loadFromFile("camerabinding.bin", &m_testScene)); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(errors.size(), 1u); + EXPECT_EQ(errors[0].message, "Fatal error during loading from file! Serialized Ramses Logic object 'CameraBinding' points to a Ramses object (id: 2) which couldn't be found in the provided scene!"); + } + } + + TEST_F(ARamsesCameraBinding_SerializationWithFile, DoesNotModifyRamsesCameraProperties_WhenNoValuesWereExplicitlySetBeforeSaving) + { + { + m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, "CameraBinding"); + EXPECT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "camerabinding.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("camerabinding.bin", &m_testScene)); + EXPECT_TRUE(m_logicEngine.update()); + + ExpectDefaultValues(*m_logicEngine.findByName("CameraBinding")); + } + } + + // Tests that the camera properties don't overwrite ramses' values after loading from file, until + // set() is called again explicitly after loadFromFile() + TEST_F(ARamsesCameraBinding_SerializationWithFile, ReappliesViewportPropertiesToRamsesCamera_OnlyAfterExplicitlySetAgain) + { + { + auto& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, "CameraBinding"); + // Set some values to the binding's inputs. Those should be lost/discarded on save() because we dont' call update() below + auto vpProperties = cameraBinding.getInputs()->getChild("viewport"); + vpProperties->getChild("offsetX")->set(4); + vpProperties->getChild("offsetY")->set(5); + vpProperties->getChild("width")->set(6); + vpProperties->getChild("height")->set(7); + m_logicEngine.update(); + EXPECT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "camerabinding.bin")); + } + + // These values will be used to fill the cache of camera bindings on load() + m_perspectiveCam.setViewport(11, 12, 13u, 14u); + + { + EXPECT_TRUE(m_logicEngine.loadFromFile("camerabinding.bin", &m_testScene)); + + // Artificially set to other values so that we can verify update() didn't change them + m_perspectiveCam.setViewport(9, 8, 1u, 2u); + + EXPECT_TRUE(m_logicEngine.update()); + + // Camera binding does not re-apply its cached values to ramses camera viewport + EXPECT_EQ(m_perspectiveCam.getViewportX(), 9); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 8); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 1u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 2u); + + // Set only one value of viewport struct. Use the same value as the one in cache on purpose! + // Calling set forces set on ramses regardless of the value used + m_logicEngine.findByName("CameraBinding")->getInputs()->getChild("viewport")->getChild("offsetX")->set(11); + m_logicEngine.update(); + EXPECT_TRUE(m_logicEngine.update()); + + // vpOffsetX changed, the rest is taken from the initially saved inputs, not what was set on the camera! + EXPECT_EQ(m_perspectiveCam.getViewportX(), 11); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 12); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 13u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 14u); + } + } + + // This is sort of a confidence test, testing a combination of: + // - bindings only propagating their values to ramses camera if the value was set by an incoming link + // - saving and loading files + // The general expectation is that after loading + update(), the logic scene would overwrite only ramses + // properties wrapped by a LogicBinding which is linked to a script + TEST_F(ARamsesCameraBinding_SerializationWithFile, SetsOnlyRamsesCameraPropertiesForWhichTheBindingInputIsLinked_WhenCallingUpdateAfterLoading) + { + // These values should not be overwritten by logic on update() + m_perspectiveCam.setViewport(9, 8, 1u, 2u); + + { + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.frustProps = { + fov = Type:Float(), + aR = Type:Float(), + nP = Type:Float(), + fP = Type:Float() + } + end + function run(IN,OUT) + OUT.frustProps = { + fov = 30.0, + aR = 640.0 / 480.0, + nP = 2.3, + fP = 5.6 + } + end + )"; + + LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, "CameraBinding"); + + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("frustProps")->getChild("fov"), *cameraBinding.getInputs()->getChild("frustum")->getChild("fieldOfView"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("frustProps")->getChild("aR"), *cameraBinding.getInputs()->getChild("frustum")->getChild("aspectRatio"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("frustProps")->getChild("nP"), *cameraBinding.getInputs()->getChild("frustum")->getChild("nearPlane"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("frustProps")->getChild("fP"), *cameraBinding.getInputs()->getChild("frustum")->getChild("farPlane"))); + + m_logicEngine.update(); + EXPECT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "camerabinding.bin")); + } + + // Modify 'linked' properties before loading to check if logic will overwrite them after load + update + m_perspectiveCam.setFrustum(15.f, 320.f / 240.f, 4.1f, 7.9f); + + { + EXPECT_TRUE(m_logicEngine.loadFromFile("camerabinding.bin", &m_testScene)); + + EXPECT_TRUE(m_logicEngine.update()); + + // Viewport properties were not linked -> their values are not modified + EXPECT_EQ(m_perspectiveCam.getViewportX(), 9); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 8); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 1u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 2u); + // Frustum properties are linked -> values were updated + EXPECT_NEAR(m_perspectiveCam.getVerticalFieldOfView(), 30.f, 0.001f); + EXPECT_EQ(m_perspectiveCam.getAspectRatio(), 640.f / 480.f); + EXPECT_EQ(m_perspectiveCam.getNearPlane(), 2.3f); + EXPECT_EQ(m_perspectiveCam.getFarPlane(), 5.6f); + + // Manually setting values on ramses followed by a logic update has no effect + // Logic is not "dirty" and it doesn't know it needs to update ramses + m_perspectiveCam.setViewport(43, 34, 84u, 62u); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(m_perspectiveCam.getViewportX(), 43); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 34); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 84u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 62u); + } + } + + // Larger confidence tests which verify and document the entire data flow cycle of bindings + // There are smaller tests which test only properties and their data propagation rules (see property unit tests) + // There are also "dirtiness" tests which test when a camera is being re-updated (see logic engine dirtiness tests) + // These tests test everything in combination + + class ARamsesCameraBinding_DataFlow : public ARamsesCameraBinding + { + }; + + TEST_F(ARamsesCameraBinding_DataFlow, WithExplicitSet) + { + // Create camera and preset values + m_perspectiveCam.setViewport(11, 12, 13u, 14u); + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, ""); + // set other values to artificially check that the binding won't override them + m_perspectiveCam.setViewport(9, 8, 1u, 2u); + + // Nothing happens here - binding does not overwrite ramses values because no user value set() was called and no link exists + m_logicEngine.update(); + EXPECT_EQ(m_perspectiveCam.getViewportX(), 9); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 8); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 1u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 2u); + + // Set only two view port properties + auto vpProperties = cameraBinding.getInputs()->getChild("viewport"); + vpProperties->getChild("offsetX")->set(4); + vpProperties->getChild("width")->set(21); + + // Update not called yet -> still has preset values for vpOffsetX and vpWidth in ramses camera, and default frustum values + EXPECT_EQ(m_perspectiveCam.getViewportX(), 9); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 8); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 1u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 2u); + ExpectDefaultPerspectiveCameraFrustumValues(m_perspectiveCam); + + // Update() triggers all viewport to be set on ramses to the two values that were explicitly set + // and the other two previous values of the binding input + m_logicEngine.update(); + EXPECT_EQ(m_perspectiveCam.getViewportX(), 4); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 12); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 21u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 14u); + // Frustum is not modified - only viewport was explicitly set + ExpectDefaultPerspectiveCameraFrustumValues(m_perspectiveCam); + + // Set two properties of each viewPort and frustum property struct + vpProperties->getChild("offsetY")->set(13); + vpProperties->getChild("height")->set(63); + auto frustum = cameraBinding.getInputs()->getChild("frustum"); + frustum->getChild("nearPlane")->set(2.3f); + frustum->getChild("farPlane")->set(5.6f); + + // On update all values of both structs are set + m_logicEngine.update(); + EXPECT_EQ(m_perspectiveCam.getViewportX(), 4); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 13); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 21u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 63u); + + EXPECT_NEAR(m_perspectiveCam.getVerticalFieldOfView(), PerspectiveFrustumFOVdefault, 0.001f); + EXPECT_EQ(m_perspectiveCam.getAspectRatio(), PerspectiveFrustumARdefault); + EXPECT_EQ(m_perspectiveCam.getNearPlane(), 2.3f); + EXPECT_EQ(m_perspectiveCam.getFarPlane(), 5.6f); + + // Calling update again does not "rewrite" the data to ramses. Check this by setting a value manually and call update() again + m_perspectiveCam.setViewport(9, 8, 1u, 2u); + m_logicEngine.update(); + EXPECT_EQ(m_perspectiveCam.getViewportX(), 9); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 8); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 1u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 2u); + + // Set all properties manually this time + vpProperties->getChild("offsetX")->set(4); + vpProperties->getChild("offsetY")->set(5); + vpProperties->getChild("width")->set(6); + vpProperties->getChild("height")->set(7); + + frustum->getChild("fieldOfView")->set(30.f); + frustum->getChild("aspectRatio")->set(640.f / 480.f); + frustum->getChild("nearPlane")->set(1.3f); + frustum->getChild("farPlane")->set(7.6f); + m_logicEngine.update(); + + // All of the property values were passed to ramses + EXPECT_EQ(m_perspectiveCam.getViewportX(), 4); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 5); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 6u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 7u); + + EXPECT_NEAR(m_perspectiveCam.getVerticalFieldOfView(), 30.f, 0.001f); + EXPECT_EQ(m_perspectiveCam.getAspectRatio(), 640.f / 480.f); + EXPECT_EQ(m_perspectiveCam.getNearPlane(), 1.3f); + EXPECT_EQ(m_perspectiveCam.getFarPlane(), 7.6f); + } + + TEST_F(ARamsesCameraBinding_DataFlow, WithLinks) + { + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.vpOffsetX = Type:Int32() + end + function run(IN,OUT) + OUT.vpOffsetX = 15 + end + )"; + + // Create camera and preset values + m_perspectiveCam.setViewport(11, 12, 13u, 14u); + + LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + RamsesCameraBinding& cameraBinding = *m_logicEngine.createRamsesCameraBinding(m_perspectiveCam, "CameraBinding"); + // set other values to artificially check that the binding won't override them + m_perspectiveCam.setViewport(9, 8, 1u, 2u); + + // Adding and removing link does not set anything in ramses + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("vpOffsetX"), *cameraBinding.getInputs()->getChild("viewport")->getChild("offsetX"))); + ASSERT_TRUE(m_logicEngine.unlink(*script->getOutputs()->getChild("vpOffsetX"), *cameraBinding.getInputs()->getChild("viewport")->getChild("offsetX"))); + m_logicEngine.update(); + EXPECT_EQ(m_perspectiveCam.getViewportX(), 9); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 8); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 1u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 2u); + ExpectDefaultPerspectiveCameraFrustumValues(m_perspectiveCam); + + // Create link and calling update -> sets values to ramses set by the link (vpOffsetX) + // and uses cached values in the binding for the other vp properties + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("vpOffsetX"), *cameraBinding.getInputs()->getChild("viewport")->getChild("offsetX"))); + m_logicEngine.update(); + EXPECT_EQ(m_perspectiveCam.getViewportX(), 15); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 12); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 13u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 14u); + // Does not touch the frustum because not linked or set at all + ExpectDefaultPerspectiveCameraFrustumValues(m_perspectiveCam); + + // Link does not overwrite manually set values as long as the actual value didnt change to avoid causing unnecessary sets on ramses + m_perspectiveCam.setViewport(9, 8, 1u, 2u); + m_logicEngine.update(); + EXPECT_EQ(m_perspectiveCam.getViewportX(), 9); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 8); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 1u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 2u); + ExpectDefaultPerspectiveCameraFrustumValues(m_perspectiveCam); + + // Remove link -> value is not overwritten any more + ASSERT_TRUE(m_logicEngine.unlink(*script->getOutputs()->getChild("vpOffsetX"), *cameraBinding.getInputs()->getChild("viewport")->getChild("offsetX"))); + m_perspectiveCam.setViewport(9, 8, 1u, 2u); + m_logicEngine.update(); + EXPECT_EQ(m_perspectiveCam.getViewportX(), 9); + EXPECT_EQ(m_perspectiveCam.getViewportY(), 8); + EXPECT_EQ(m_perspectiveCam.getViewportWidth(), 1u); + EXPECT_EQ(m_perspectiveCam.getViewportHeight(), 2u); + ExpectDefaultPerspectiveCameraFrustumValues(m_perspectiveCam); + } +} diff --git a/client/logic/unittests/api/RamsesMeshNodeBindingTest.cpp b/client/logic/unittests/api/RamsesMeshNodeBindingTest.cpp new file mode 100644 index 000000000..9e781e644 --- /dev/null +++ b/client/logic/unittests/api/RamsesMeshNodeBindingTest.cpp @@ -0,0 +1,408 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" +#include "RamsesTestUtils.h" +#include "SerializationTestUtils.h" +#include "RamsesObjectResolverMock.h" +#include "WithTempDirectory.h" + +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/Property.h" +#include "impl/RamsesMeshNodeBindingImpl.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "impl/PropertyImpl.h" +#include "internals/ErrorReporting.h" +#include "internals/TypeData.h" + +#include "generated/RamsesMeshNodeBindingGen.h" + +namespace ramses::internal +{ + class ARamsesMeshNodeBinding : public ALogicEngine + { + public: + ARamsesMeshNodeBinding() + { + // in order for the tests to be closer to reality, use ramses MeshNode with actual geometry and appearance, + // the geometry affects some of the values exposed in the binding (namely instanceCount) + const std::array vertexPositionsArray = { ramses::vec3f{-1.f, 0.f, -1.f}, ramses::vec3f{1.f, 0.f, -1.f}, ramses::vec3f{0.f, 1.f, -1.f} }; + const ramses::ArrayResource* vertexPositions = m_scene->createArrayResource(3u, vertexPositionsArray.data()); + const std::array indexArray = { 0, 1, 2 }; + const ramses::ArrayResource* indices = m_scene->createArrayResource(3u, indexArray.data()); + + ramses::EffectDescription effectDesc; + effectDesc.setVertexShader(R"( + #version 100 + attribute vec3 a_position; + void main() + { + gl_Position = vec4(a_position, 1.0); + } + )"); + effectDesc.setFragmentShader(R"( + #version 100 + void main(void) + { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + )"); + const ramses::Effect* effect = m_scene->createEffect(effectDesc); + ramses::Appearance* appearance = m_scene->createAppearance(*effect); + + m_geometry = m_scene->createGeometryBinding(*effect); + ramses::AttributeInput positionsInput; + effect->findAttributeInput("a_position", positionsInput); + m_geometry->setInputBuffer(positionsInput, *vertexPositions); + m_geometry->setIndices(*indices); + + m_meshNodeWithGeometry = m_scene->createMeshNode("meshNode"); + m_meshNodeWithGeometry->setAppearance(*appearance); + m_meshNodeWithGeometry->setGeometryBinding(*m_geometry); + EXPECT_EQ(3u, m_meshNodeWithGeometry->getIndexCount()); + + m_meshBinding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNodeWithGeometry, "meshBinding"); + EXPECT_EQ(3u, m_meshNodeWithGeometry->getIndexCount()); + } + + protected: + ramses::GeometryBinding* m_geometry = nullptr; + ramses::MeshNode* m_meshNodeWithGeometry = nullptr; + RamsesMeshNodeBinding* m_meshBinding = nullptr; + }; + + TEST_F(ARamsesMeshNodeBinding, RefersToGivenRamsesObject) + { + EXPECT_EQ(m_meshNodeWithGeometry, &m_meshBinding->getRamsesMeshNode()); + const auto& mbConst = *m_meshBinding; + EXPECT_EQ(m_meshNodeWithGeometry, &mbConst.getRamsesMeshNode()); + const auto& mbImplConst = m_meshBinding->m_meshNodeBinding; + EXPECT_EQ(m_meshNodeWithGeometry, &mbImplConst.getRamsesMeshNode()); + } + + TEST_F(ARamsesMeshNodeBinding, HasInputPropertiesAndNoOutputs) + { + ASSERT_NE(nullptr, m_meshBinding->getInputs()); + ASSERT_EQ(size_t(RamsesMeshNodeBindingImpl::EInputProperty::COUNT), m_meshBinding->getInputs()->getChildCount()); + EXPECT_EQ(m_meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::VertexOffset)), m_meshBinding->getInputs()->getChild("vertexOffset")); + EXPECT_EQ(m_meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexOffset)), m_meshBinding->getInputs()->getChild("indexOffset")); + EXPECT_EQ(m_meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexCount)), m_meshBinding->getInputs()->getChild("indexCount")); + EXPECT_EQ(m_meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::InstanceCount)), m_meshBinding->getInputs()->getChild("instanceCount")); + EXPECT_EQ(nullptr, m_meshBinding->getOutputs()); + } + + TEST_F(ARamsesMeshNodeBinding, InputPropertiesAreInitializedFromBoundMeshNode) + { + m_meshNodeWithGeometry->setStartVertex(42); + m_meshNodeWithGeometry->setStartIndex(43); + m_meshNodeWithGeometry->setIndexCount(44); + m_meshNodeWithGeometry->setInstanceCount(45); + // create new binding + const auto binding = m_logicEngine.createRamsesMeshNodeBinding(*m_meshNodeWithGeometry); + + EXPECT_EQ(42, *binding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::VertexOffset))->get()); + EXPECT_EQ(43, *binding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexOffset))->get()); + EXPECT_EQ(44, *binding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexCount))->get()); + EXPECT_EQ(45, *binding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::InstanceCount))->get()); + } + + TEST_F(ARamsesMeshNodeBinding, SetsModifiedBoundValuesOnUpdate) + { + // initial values + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(0u, m_meshNodeWithGeometry->getStartVertex()); + EXPECT_EQ(0u, m_meshNodeWithGeometry->getStartIndex()); + EXPECT_EQ(3u, m_meshNodeWithGeometry->getIndexCount()); + EXPECT_EQ(1u, m_meshNodeWithGeometry->getInstanceCount()); + + EXPECT_TRUE(m_meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::VertexOffset))->set(42)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(42u, m_meshNodeWithGeometry->getStartVertex()); + EXPECT_EQ(0u, m_meshNodeWithGeometry->getStartIndex()); + EXPECT_EQ(3u, m_meshNodeWithGeometry->getIndexCount()); + EXPECT_EQ(1u, m_meshNodeWithGeometry->getInstanceCount()); + + EXPECT_TRUE(m_meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexOffset))->set(43)); + EXPECT_TRUE(m_meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexCount))->set(44)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(42u, m_meshNodeWithGeometry->getStartVertex()); + EXPECT_EQ(43u, m_meshNodeWithGeometry->getStartIndex()); + EXPECT_EQ(44u, m_meshNodeWithGeometry->getIndexCount()); + EXPECT_EQ(1u, m_meshNodeWithGeometry->getInstanceCount()); + + EXPECT_TRUE(m_meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::InstanceCount))->set(45)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(42u, m_meshNodeWithGeometry->getStartVertex()); + EXPECT_EQ(43u, m_meshNodeWithGeometry->getStartIndex()); + EXPECT_EQ(44u, m_meshNodeWithGeometry->getIndexCount()); + EXPECT_EQ(45u, m_meshNodeWithGeometry->getInstanceCount()); + } + + TEST_F(ARamsesMeshNodeBinding, FailsUpdateIfTryingToSetInvalidValue) + { + // mesh instance count cannot be 0 + EXPECT_TRUE(m_meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::InstanceCount))->set(0)); + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(1u, m_logicEngine.getErrors().size()); + EXPECT_EQ(m_meshBinding, m_logicEngine.getErrors()[0].object); + EXPECT_EQ(EErrorType::RuntimeError, m_logicEngine.getErrors()[0].type); + EXPECT_EQ("MeshNode::setInstanceCount failed: instance count must not be 0!", m_logicEngine.getErrors()[0].message); + } + + class ARamsesMeshNodeBinding_SerializationLifecycle : public ARamsesMeshNodeBinding + { + protected: + enum class ESerializationIssue + { + AllValid, + MissingBase, + MissingName, + MissingRoot, + CorruptedInputProperties, + MissingBoundObject, + UnresolvedMeshNode, + InvalidBoundObjectType + }; + + std::unique_ptr deserializeSerializedDataWithIssue(ESerializationIssue issue) + { + { + auto inputsType = MakeStruct("", { + TypeData{"vertexOffset", EPropertyType::Int32}, + TypeData{"indexOffset", EPropertyType::Int32}, + (issue == ESerializationIssue::CorruptedInputProperties ? TypeData{"wrong", EPropertyType::Int32} : TypeData{"indexCount", EPropertyType::Int32}), + TypeData{"instanceCount", EPropertyType::Int32} + }); + auto inputs = std::make_unique(std::move(inputsType), EPropertySemantics::BindingInput); + + SerializationMap serializationMap; + const auto logicObject = rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + (issue == ESerializationIssue::MissingName ? 0 : m_flatBufferBuilder.CreateString("name")), 1u, 0u, 0u); + auto fbRamsesBinding = rlogic_serialization::CreateRamsesBinding(m_flatBufferBuilder, + logicObject, + (issue == ESerializationIssue::MissingBoundObject ? 0 : rlogic_serialization::CreateRamsesReference(m_flatBufferBuilder, + 1u, (issue == ESerializationIssue::InvalidBoundObjectType ? 0 : static_cast(ramses::ERamsesObjectType::MeshNode)))), + (issue == ESerializationIssue::MissingRoot ? 0 : PropertyImpl::Serialize(*inputs, m_flatBufferBuilder, serializationMap))); + + auto fbMeshNodeBinding = rlogic_serialization::CreateRamsesMeshNodeBinding(m_flatBufferBuilder, (issue == ESerializationIssue::MissingBase ? 0 : fbRamsesBinding)); + m_flatBufferBuilder.Finish(fbMeshNodeBinding); + } + + switch (issue) + { + case ESerializationIssue::AllValid: + case ESerializationIssue::InvalidBoundObjectType: + EXPECT_CALL(m_resolverMock, findRamsesSceneObjectInScene(::testing::Eq("name"), ramses::sceneObjectId_t{ 1u })).WillOnce(::testing::Return(m_meshNodeWithGeometry)); + break; + case ESerializationIssue::UnresolvedMeshNode: + EXPECT_CALL(m_resolverMock, findRamsesSceneObjectInScene(::testing::Eq("name"), ramses::sceneObjectId_t{ 1u })).WillOnce(::testing::Return(nullptr)); + break; + default: + break; + } + + DeserializationMap deserializationMap; + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + return RamsesMeshNodeBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, deserializationMap); + } + + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + ::testing::StrictMock m_resolverMock; + ErrorReporting m_errorReporting; + }; + + TEST_F(ARamsesMeshNodeBinding_SerializationLifecycle, CanSerializeWithNoIssue) + { + EXPECT_TRUE(deserializeSerializedDataWithIssue(ARamsesMeshNodeBinding_SerializationLifecycle::ESerializationIssue::AllValid)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + + TEST_F(ARamsesMeshNodeBinding_SerializationLifecycle, ReportsSerializationError_MissingBase) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesMeshNodeBinding_SerializationLifecycle::ESerializationIssue::MissingBase)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesMeshNodeBinding from serialized data: missing base class info!"); + } + + TEST_F(ARamsesMeshNodeBinding_SerializationLifecycle, ReportsSerializationError_MissingName) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesMeshNodeBinding_SerializationLifecycle::ESerializationIssue::MissingName)); + ASSERT_EQ(2u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LogicObject base from serialized data: missing name!"); + EXPECT_EQ(m_errorReporting.getErrors()[1].message, "Fatal error during loading of RamsesMeshNodeBinding from serialized data: missing name and/or ID!"); + } + + TEST_F(ARamsesMeshNodeBinding_SerializationLifecycle, ReportsSerializationError_MissingRoot) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesMeshNodeBinding_SerializationLifecycle::ESerializationIssue::MissingRoot)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesMeshNodeBinding from serialized data: missing root input!"); + } + + TEST_F(ARamsesMeshNodeBinding_SerializationLifecycle, ReportsSerializationError_CorruptedInputProperties) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesMeshNodeBinding_SerializationLifecycle::ESerializationIssue::CorruptedInputProperties)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesMeshNodeBinding from serialized data: corrupted root input!"); + } + + TEST_F(ARamsesMeshNodeBinding_SerializationLifecycle, ReportsSerializationError_MissingBoundObject) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesMeshNodeBinding_SerializationLifecycle::ESerializationIssue::MissingBoundObject)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesMeshNodeBinding from serialized data: missing ramses object reference!"); + } + + TEST_F(ARamsesMeshNodeBinding_SerializationLifecycle, ReportsSerializationError_UnresolvedMeshNode) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesMeshNodeBinding_SerializationLifecycle::ESerializationIssue::UnresolvedMeshNode)); + // error message is generated in resolver which is mocked here + } + + TEST_F(ARamsesMeshNodeBinding_SerializationLifecycle, ReportsSerializationError_InvalidBoundObjectType) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesMeshNodeBinding_SerializationLifecycle::ESerializationIssue::InvalidBoundObjectType)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesMeshNodeBinding from serialized data: loaded object type does not match referenced object type!"); + } + + TEST_F(ARamsesMeshNodeBinding_SerializationLifecycle, FailsToLoadWhenNoSceneProvided) + { + { + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "binding.bin")); + } + + { + EXPECT_FALSE(m_logicEngine.loadFromFile("binding.bin")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Fatal error during loading from file! File contains references to Ramses objects but no Ramses scene was provided!"); + } + } + + TEST_F(ARamsesMeshNodeBinding_SerializationLifecycle, KeepsItsPropertiesAfterDeserialization) + { + { + EXPECT_TRUE(m_meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexOffset))->set(41)); + EXPECT_TRUE(m_meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::InstanceCount))->set(42)); + EXPECT_TRUE(m_logicEngine.update()); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "binding.bin")); + } + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("binding.bin", m_scene)); + const auto loadedBinding = m_logicEngine.findByName("meshBinding"); + ASSERT_TRUE(loadedBinding); + EXPECT_EQ(m_meshNodeWithGeometry, &loadedBinding->getRamsesMeshNode()); + + ASSERT_NE(nullptr, loadedBinding->getInputs()); + ASSERT_EQ(size_t(RamsesMeshNodeBindingImpl::EInputProperty::COUNT), loadedBinding->getInputs()->getChildCount()); + EXPECT_EQ(loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::VertexOffset)), loadedBinding->getInputs()->getChild("vertexOffset")); + EXPECT_EQ(loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexOffset)), loadedBinding->getInputs()->getChild("indexOffset")); + EXPECT_EQ(loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexCount)), loadedBinding->getInputs()->getChild("indexCount")); + EXPECT_EQ(loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::InstanceCount)), loadedBinding->getInputs()->getChild("instanceCount")); + EXPECT_EQ(nullptr, loadedBinding->getOutputs()); + + EXPECT_EQ(41, *loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexOffset))->get()); + EXPECT_EQ(42, *loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::InstanceCount))->get()); + + // confidence test - can set new values + EXPECT_TRUE(loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexOffset))->set(43)); + EXPECT_TRUE(loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexCount))->set(44)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(0u, m_meshNodeWithGeometry->getStartVertex()); // did not change + EXPECT_EQ(43u, m_meshNodeWithGeometry->getStartIndex()); // changed before saving and again after loading + EXPECT_EQ(44u, m_meshNodeWithGeometry->getIndexCount()); // changed after loading + EXPECT_EQ(42u, m_meshNodeWithGeometry->getInstanceCount()); // changed before saving + } + } + + TEST_F(ARamsesMeshNodeBinding_SerializationLifecycle, DoesNotModifyAnyValueIfNotSetDuringSerializationAndDeserialization) + { + { + EXPECT_TRUE(m_logicEngine.update()); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "binding.bin")); + } + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("binding.bin", m_scene)); + const auto loadedBinding = m_logicEngine.findByName("meshBinding"); + ASSERT_TRUE(loadedBinding); + EXPECT_EQ(m_meshNodeWithGeometry, &loadedBinding->getRamsesMeshNode()); + + ASSERT_NE(nullptr, loadedBinding->getInputs()); + ASSERT_EQ(size_t(RamsesMeshNodeBindingImpl::EInputProperty::COUNT), loadedBinding->getInputs()->getChildCount()); + EXPECT_EQ(loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::VertexOffset)), loadedBinding->getInputs()->getChild("vertexOffset")); + EXPECT_EQ(loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexOffset)), loadedBinding->getInputs()->getChild("indexOffset")); + EXPECT_EQ(loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexCount)), loadedBinding->getInputs()->getChild("indexCount")); + EXPECT_EQ(loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::InstanceCount)), loadedBinding->getInputs()->getChild("instanceCount")); + EXPECT_EQ(nullptr, loadedBinding->getOutputs()); + + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(0u, m_meshNodeWithGeometry->getStartVertex()); + EXPECT_EQ(0u, m_meshNodeWithGeometry->getStartIndex()); + EXPECT_EQ(3u, m_meshNodeWithGeometry->getIndexCount()); + EXPECT_EQ(1u, m_meshNodeWithGeometry->getInstanceCount()); + } + } + + // Note that this is not recommended usage of Ramses binding, modifying the bound object after it is bound + TEST_F(ARamsesMeshNodeBinding, DoesNotModifyIndexCountEvenIfGeometryAssignedAfterBindingIsCreated) + { + auto meshNode = m_scene->createMeshNode(); + auto meshBinding = m_logicEngine.createRamsesMeshNodeBinding(*meshNode); + EXPECT_EQ(0u, meshNode->getIndexCount()); + + // assign geometry which in ramses automatically updates index count based on its indices + meshNode->setGeometryBinding(*m_geometry); + EXPECT_EQ(3u, meshNode->getIndexCount()); + + // mesh binding update must not affect that value even though binding's property holds indexCount=0 because it was initialized with it + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(3u, meshNode->getIndexCount()); + + // the index count value will be overridden only when explicitly set from binding (or link) + EXPECT_TRUE(meshBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexCount))->set(2)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(2u, meshNode->getIndexCount()); + } + + // Note that this is not recommended usage of Ramses binding, modifying the bound object after it is bound + TEST_F(ARamsesMeshNodeBinding, DoesNotModifyIndexCountEvenIfGeometryAssignedAfterBindingIsCreated_SerializedAndDeserialized) + { + auto meshNode = m_scene->createMeshNode(); + + { + m_logicEngine.createRamsesMeshNodeBinding(*meshNode, "bindingWithLateGeometry"); + EXPECT_EQ(0u, meshNode->getIndexCount()); + + // assign geometry which in ramses automatically updates index count based on its indices + meshNode->setGeometryBinding(*m_geometry); + EXPECT_EQ(3u, meshNode->getIndexCount()); + + // mesh binding update must not affect that value even though binding's property holds indexCount=0 because it was initialized with it + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(3u, meshNode->getIndexCount()); + + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "binding.bin")); + } + + ASSERT_TRUE(m_logicEngine.loadFromFile("binding.bin", m_scene)); + const auto loadedBinding = m_logicEngine.findByName("bindingWithLateGeometry"); + ASSERT_TRUE(loadedBinding); + EXPECT_EQ(meshNode, &loadedBinding->getRamsesMeshNode()); + + // value still unchanged + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(3u, meshNode->getIndexCount()); + + // the index count value will be overridden only when explicitly set from binding (or link) + EXPECT_TRUE(loadedBinding->getInputs()->getChild(size_t(RamsesMeshNodeBindingImpl::EInputProperty::IndexCount))->set(2)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(2u, meshNode->getIndexCount()); + } +} diff --git a/client/logic/unittests/api/RamsesNodeBindingTest.cpp b/client/logic/unittests/api/RamsesNodeBindingTest.cpp new file mode 100644 index 000000000..8fb6fe75c --- /dev/null +++ b/client/logic/unittests/api/RamsesNodeBindingTest.cpp @@ -0,0 +1,1356 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "RamsesObjectResolverMock.h" +#include "RamsesTestUtils.h" +#include "SerializationTestUtils.h" +#include "WithTempDirectory.h" +#include "LogTestUtils.h" + +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/LogicEngine.h" + +#include "impl/RamsesNodeBindingImpl.h" +#include "impl/PropertyImpl.h" +#include "impl/LogicEngineImpl.h" + +#include "ramses-client-api/Node.h" +#include "ramses-client-api/MeshNode.h" + +#include "generated/RamsesNodeBindingGen.h" +#include "glm/gtc/type_ptr.hpp" + +namespace ramses::internal +{ + class ARamsesNodeBinding : public ALogicEngine + { + protected: + static void ExpectDefaultValues(ramses::Node& node, ENodePropertyStaticIndex prop) + { + switch (prop) + { + case ENodePropertyStaticIndex::Rotation: + ExpectValues(node, ENodePropertyStaticIndex::Rotation, vec3f{ 0.0f, 0.0f, 0.0f }); + break; + case ENodePropertyStaticIndex::Translation: + ExpectValues(node, ENodePropertyStaticIndex::Translation, vec3f{ 0.0f, 0.0f, 0.0f }); + break; + case ENodePropertyStaticIndex::Scaling: + ExpectValues(node, ENodePropertyStaticIndex::Scaling, vec3f{ 1.0f, 1.0f, 1.0f }); + break; + // Enabled and Visibility bind to a single 3-state node flag + case ENodePropertyStaticIndex::Visibility: + case ENodePropertyStaticIndex::Enabled: + EXPECT_EQ(node.getVisibility(), ramses::EVisibilityMode::Visible); + break; + } + } + + static void ExpectDefaultValues(ramses::Node& node) + { + ExpectDefaultValues(node, ENodePropertyStaticIndex::Translation); + ExpectDefaultValues(node, ENodePropertyStaticIndex::Rotation); + ExpectDefaultValues(node, ENodePropertyStaticIndex::Scaling); + ExpectDefaultValues(node, ENodePropertyStaticIndex::Visibility); + ExpectDefaultValues(node, ENodePropertyStaticIndex::Enabled); + } + + static void ExpectValues(ramses::Node& node, ENodePropertyStaticIndex prop, vec3f expectedValues) + { + vec3f values = {0.0f, 0.0f, 0.0f}; + if (prop == ENodePropertyStaticIndex::Rotation) + { + node.getRotation(values); + } + if (prop == ENodePropertyStaticIndex::Translation) + { + node.getTranslation(values); + } + if (prop == ENodePropertyStaticIndex::Scaling) + { + node.getScaling(values); + } + EXPECT_FLOAT_EQ(values[0], expectedValues[0]); + EXPECT_FLOAT_EQ(values[1], expectedValues[1]); + EXPECT_FLOAT_EQ(values[2], expectedValues[2]); + } + + static void ExpectQuat(ramses::Node& node, const vec4f& expectedValues) + { + quat quat; + node.getRotation(quat); + EXPECT_FLOAT_EQ(quat.x, expectedValues[0]); + EXPECT_FLOAT_EQ(quat.y, expectedValues[1]); + EXPECT_FLOAT_EQ(quat.z, expectedValues[2]); + EXPECT_FLOAT_EQ(quat.w, expectedValues[3]); + } + }; + + TEST_F(ARamsesNodeBinding, KeepsNameProvidedDuringConstruction) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + EXPECT_EQ("NodeBinding", nodeBinding.getName()); + } + + TEST_F(ARamsesNodeBinding, KeepsIdProvidedDuringConstruction) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + EXPECT_EQ(nodeBinding.getId(), 1u); + } + + TEST_F(ARamsesNodeBinding, ReturnsNullptrForOutputs) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + EXPECT_EQ(nullptr, nodeBinding.getOutputs()); + } + + TEST_F(ARamsesNodeBinding, ProvidesAccessToAllNodePropertiesInItsInputs) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + + auto inputs = nodeBinding.getInputs(); + ASSERT_NE(nullptr, inputs); + EXPECT_EQ(5u, inputs->getChildCount()); + + auto rotation = inputs->getChild("rotation"); + auto scaling = inputs->getChild("scaling"); + auto translation = inputs->getChild("translation"); + auto visibility = inputs->getChild("visibility"); + + // Test that internal indices match properties resolved by name + EXPECT_EQ(rotation, inputs->m_impl->getChild(static_cast(ENodePropertyStaticIndex::Rotation))); + EXPECT_EQ(scaling, inputs->m_impl->getChild(static_cast(ENodePropertyStaticIndex::Scaling))); + EXPECT_EQ(translation, inputs->m_impl->getChild(static_cast(ENodePropertyStaticIndex::Translation))); + EXPECT_EQ(visibility, inputs->m_impl->getChild(static_cast(ENodePropertyStaticIndex::Visibility))); + + ASSERT_NE(nullptr, rotation); + EXPECT_EQ(EPropertyType::Vec3f, rotation->getType()); + EXPECT_EQ(0u, rotation->getChildCount()); + + ASSERT_NE(nullptr, scaling); + EXPECT_EQ(EPropertyType::Vec3f, scaling->getType()); + EXPECT_EQ(0u, scaling->getChildCount()); + + ASSERT_NE(nullptr, translation); + EXPECT_EQ(EPropertyType::Vec3f, translation->getType()); + EXPECT_EQ(0u, translation->getChildCount()); + + ASSERT_NE(nullptr, visibility); + EXPECT_EQ(EPropertyType::Bool, visibility->getType()); + EXPECT_EQ(0u, visibility->getChildCount()); + + auto enabled = inputs->getChild("enabled"); + ASSERT_NE(nullptr, enabled); + EXPECT_EQ(enabled, inputs->m_impl->getChild(static_cast(ENodePropertyStaticIndex::Enabled))); + EXPECT_EQ(EPropertyType::Bool, enabled->getType()); + EXPECT_EQ(0u, enabled->getChildCount()); + } + + TEST_F(ARamsesNodeBinding, InitializesInputPropertiesToMatchRamsesDefaultValues) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + + auto inputs = nodeBinding.getInputs(); + ASSERT_NE(nullptr, inputs); + EXPECT_EQ(5u, inputs->getChildCount()); + + vec3f zeroes; + vec3f ones; + + //Check that the default values we assume are indeed the ones in ramses + m_node->getRotation(zeroes); + EXPECT_EQ(zeroes, vec3f(0.f, 0.f, 0.f)); + + EXPECT_EQ(ramses::ERotationType::Euler_XYZ, m_node->getRotationType()); + m_node->getTranslation(zeroes); + EXPECT_EQ(zeroes, vec3f(0.f, 0.f, 0.f)); + m_node->getScaling(ones); + EXPECT_EQ(ones, vec3f(1.f, 1.f, 1.f)); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Visible); + + EXPECT_EQ(zeroes, inputs->getChild("rotation")->get()); + EXPECT_EQ(zeroes, inputs->getChild("translation")->get()); + EXPECT_EQ(ones, inputs->getChild("scaling")->get()); + EXPECT_EQ(true, *inputs->getChild("visibility")->get()); + EXPECT_EQ(true, *inputs->getChild("enabled")->get()); + } + + TEST_F(ARamsesNodeBinding, MarksInputsAsBindingInputs) + { + auto* nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + auto inputs = nodeBinding->getInputs(); + const auto inputCount = inputs->getChildCount(); + for (size_t i = 0; i < inputCount; ++i) + { + EXPECT_EQ(EPropertySemantics::BindingInput, inputs->getChild(i)->m_impl->getPropertySemantics()); + } + } + + TEST_F(ARamsesNodeBinding, ReturnsNodePropertiesForInputsConst) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + const auto inputs = nodeBinding.getInputs(); + ASSERT_NE(nullptr, inputs); + EXPECT_EQ(5u, inputs->getChildCount()); + + auto rotation = inputs->getChild("rotation"); + auto scaling = inputs->getChild("scaling"); + auto translation = inputs->getChild("translation"); + auto visibility = inputs->getChild("visibility"); + + ASSERT_NE(nullptr, rotation); + EXPECT_EQ(EPropertyType::Vec3f, rotation->getType()); + EXPECT_EQ(0u, rotation->getChildCount()); + + ASSERT_NE(nullptr, scaling); + EXPECT_EQ(EPropertyType::Vec3f, scaling->getType()); + EXPECT_EQ(0u, scaling->getChildCount()); + + ASSERT_NE(nullptr, translation); + EXPECT_EQ(EPropertyType::Vec3f, translation->getType()); + EXPECT_EQ(0u, translation->getChildCount()); + + ASSERT_NE(nullptr, visibility); + EXPECT_EQ(EPropertyType::Bool, visibility->getType()); + EXPECT_EQ(0u, visibility->getChildCount()); + + auto enabled = inputs->getChild("enabled"); + ASSERT_NE(nullptr, enabled); + EXPECT_EQ(EPropertyType::Bool, enabled->getType()); + EXPECT_EQ(0u, enabled->getChildCount()); + } + + TEST_F(ARamsesNodeBinding, ReturnsBoundRamsesNode) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + EXPECT_EQ(m_node, &nodeBinding.getRamsesNode()); + } + + TEST_F(ARamsesNodeBinding, DoesNotModifyRamsesWithoutUpdateBeingCalled) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + + auto inputs = nodeBinding.getInputs(); + inputs->getChild("rotation")->set(vec3f{ 0.1f, 0.2f, 0.3f }); + inputs->getChild("scaling")->set(vec3f{ 1.1f, 1.2f, 1.3f }); + inputs->getChild("translation")->set(vec3f{ 2.1f, 2.2f, 2.3f }); + inputs->getChild("visibility")->set(false); + inputs->getChild("enabled")->set(false); + + ExpectDefaultValues(*m_node); + } + + // This test is a bit too big, but splitting it creates a lot of test code duplication... Better keep it like this, it documents behavior quite well + TEST_F(ARamsesNodeBinding, ModifiesRamsesOnUpdate_OnlyAfterExplicitlyAssignedToInputs) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + + nodeBinding.m_nodeBinding.update(); + + ExpectDefaultValues(*m_node); + + auto inputs = nodeBinding.getInputs(); + inputs->getChild("rotation")->set(vec3f{ 0.1f, 0.2f, 0.3f }); + + // Updte not called yet -> still default values + ExpectDefaultValues(*m_node); + + nodeBinding.m_nodeBinding.update(); + // Only propagated rotation, the others still have default values + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 0.1f, 0.2f, 0.3f }); + ExpectDefaultValues(*m_node, ENodePropertyStaticIndex::Translation); + ExpectDefaultValues(*m_node, ENodePropertyStaticIndex::Scaling); + ExpectDefaultValues(*m_node, ENodePropertyStaticIndex::Visibility); + ExpectDefaultValues(*m_node, ENodePropertyStaticIndex::Enabled); + + // Set and test all properties + inputs->getChild("rotation")->set(vec3f{ 42.1f, 42.2f, 42.3f }); + inputs->getChild("scaling")->set(vec3f{ 1.1f, 1.2f, 1.3f }); + inputs->getChild("translation")->set(vec3f{ 2.1f, 2.2f, 2.3f }); + inputs->getChild("visibility")->set(true); + inputs->getChild("enabled")->set(false); + nodeBinding.m_nodeBinding.update(); + + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 42.1f, 42.2f, 42.3f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Scaling, vec3f{ 1.1f, 1.2f, 1.3f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Translation, vec3f{ 2.1f, 2.2f, 2.3f }); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Off); + } + + TEST_F(ARamsesNodeBinding, PropagatesItsInputsToRamsesNodeOnUpdate) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + + auto inputs = nodeBinding.getInputs(); + inputs->getChild("rotation")->set(vec3f{0.1f, 0.2f, 0.3f}); + inputs->getChild("scaling")->set(vec3f{1.1f, 1.2f, 1.3f}); + inputs->getChild("translation")->set(vec3f{2.1f, 2.2f, 2.3f}); + inputs->getChild("visibility")->set(false); + inputs->getChild("enabled")->set(true); + + nodeBinding.m_nodeBinding.update(); + + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 0.1f, 0.2f, 0.3f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Scaling, vec3f{ 1.1f, 1.2f, 1.3f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Translation, vec3f{ 2.1f, 2.2f, 2.3f }); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Invisible); + } + + TEST_F(ARamsesNodeBinding, PropagatesItsInputsToRamsesNodeOnUpdate_WithLinksInsteadOfSetCall) + { + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.rotation = Type:Vec3f() + OUT.visibility = Type:Bool() + end + function run(IN,OUT) + OUT.rotation = {1, 2, 3} + OUT.visibility = false + end + )"; + + LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("rotation"), *nodeBinding.getInputs()->getChild("rotation"))); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("visibility"), *nodeBinding.getInputs()->getChild("visibility"))); + + // Links have no effect before update() explicitly called + ExpectDefaultValues(*m_node); + + m_logicEngine.update(); + + // Linked values got updates, not-linked values were not modified + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 1.f, 2.f, 3.f }); + ExpectDefaultValues(*m_node, ENodePropertyStaticIndex::Scaling); + ExpectDefaultValues(*m_node, ENodePropertyStaticIndex::Translation); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Invisible); + } + + TEST_F(ARamsesNodeBinding, DoesNotOverrideExistingValuesAfterRamsesNodeIsAssignedToBinding) + { + m_node->setVisibility(ramses::EVisibilityMode::Off); + m_node->setRotation({0.1f, 0.2f, 0.3f}, ramses::ERotationType::Euler_ZYX); + m_node->setScaling({1.1f, 1.2f, 1.3f}); + m_node->setTranslation({2.1f, 2.2f, 2.3f}); + + m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 0.1f, 0.2f, 0.3f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Scaling, vec3f{ 1.1f, 1.2f, 1.3f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Translation, vec3f{ 2.1f, 2.2f, 2.3f }); + } + + TEST_F(ARamsesNodeBinding, DoesNotOverrideExistingQuaternionAfterRamsesNodeIsAssignedToBinding) + { + quat quaternion{0.5f, 0.5f, -0.5f, 0.5f}; + m_node->setRotation(quaternion); + + m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Quaternion, "NodeBinding"); + + quat qOut; + m_node->getRotation(qOut); + EXPECT_EQ(quaternion, qOut); + } + + TEST_F(ARamsesNodeBinding, InitializesVisibilityPropertiesFromRamsesNode) + { + // visible + m_node->setVisibility(ramses::EVisibilityMode::Visible); + RamsesNodeBinding* nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + EXPECT_TRUE(*nodeBinding->getInputs()->getChild("visibility")->get()); + EXPECT_TRUE(*nodeBinding->getInputs()->getChild("enabled")->get()); + m_logicEngine.destroy(*nodeBinding); + + // invisible + m_node->setVisibility(ramses::EVisibilityMode::Invisible); + nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + EXPECT_FALSE(*nodeBinding->getInputs()->getChild("visibility")->get()); + EXPECT_TRUE(*nodeBinding->getInputs()->getChild("enabled")->get()); + m_logicEngine.destroy(*nodeBinding); + + // off + m_node->setVisibility(ramses::EVisibilityMode::Off); + nodeBinding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + EXPECT_FALSE(*nodeBinding->getInputs()->getChild("visibility")->get()); + EXPECT_FALSE(*nodeBinding->getInputs()->getChild("enabled")->get()); + m_logicEngine.destroy(*nodeBinding); + } + + TEST_F(ARamsesNodeBinding, ResolvesVisibilityModeFromProperties) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + auto inputs = nodeBinding.getInputs(); + + // visible + inputs->getChild("visibility")->set(true); + inputs->getChild("enabled")->set(true); + nodeBinding.m_nodeBinding.update(); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Visible); + + // invisible + inputs->getChild("visibility")->set(false); + inputs->getChild("enabled")->set(true); + nodeBinding.m_nodeBinding.update(); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Invisible); + + // off with visible (off overrides visibility) + inputs->getChild("visibility")->set(true); + inputs->getChild("enabled")->set(false); + nodeBinding.m_nodeBinding.update(); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Off); + + // off with invisible (off overrides visibility) + inputs->getChild("visibility")->set(false); + inputs->getChild("enabled")->set(false); + nodeBinding.m_nodeBinding.update(); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Off); + } + + class ARamsesNodeBinding_RotationTypes : public ARamsesNodeBinding + { + }; + + TEST_F(ARamsesNodeBinding_RotationTypes, HasRightHandedEulerXYZRotationConventionByDefault) + { + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node); + + EXPECT_EQ(ramses::ERotationType::Euler_XYZ, nodeBinding.getRotationType()); + } + + TEST_F(ARamsesNodeBinding_RotationTypes, AppliesAllEulerConventions) + { + std::vector enumValues = + { + ramses::ERotationType::Euler_XYZ, + ramses::ERotationType::Euler_XZY, + ramses::ERotationType::Euler_YXZ, + ramses::ERotationType::Euler_YZX, + ramses::ERotationType::Euler_ZXY, + ramses::ERotationType::Euler_ZYX, + ramses::ERotationType::Euler_XYX, + ramses::ERotationType::Euler_XZX, + ramses::ERotationType::Euler_YXY, + ramses::ERotationType::Euler_YZY, + ramses::ERotationType::Euler_ZXZ, + ramses::ERotationType::Euler_ZYZ, + }; + + for (const auto& rotationType : enumValues) + { + m_node->setRotation({0.f, 0.f, 0.f}, rotationType); + + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, rotationType); + nodeBinding.getInputs()->getChild("rotation")->set({ 30, 45, 60 }); + + m_logicEngine.update(); + + vec3f rotValues; + m_node->getRotation(rotValues); + + EXPECT_EQ(rotationType, m_node->getRotationType()); + EXPECT_EQ(rotValues, vec3f(30.f, 45.f, 60.f)); + } + } + + TEST_F(ARamsesNodeBinding_RotationTypes, TakesOverRotationConventionFromRamsesNode_WhenEnumMatches) + { + const std::vector enumValues = + { + ramses::ERotationType::Euler_XYZ, + ramses::ERotationType::Euler_XZY, + ramses::ERotationType::Euler_YXZ, + ramses::ERotationType::Euler_YZX, + ramses::ERotationType::Euler_ZXY, + ramses::ERotationType::Euler_ZYX, + ramses::ERotationType::Euler_XYX, + ramses::ERotationType::Euler_XZX, + ramses::ERotationType::Euler_YXY, + ramses::ERotationType::Euler_YZY, + ramses::ERotationType::Euler_ZXZ, + ramses::ERotationType::Euler_ZYZ, + }; + + for (const auto& rotationType : enumValues) + { + m_node->setRotation({1, 2, 3}, rotationType); + ramses::RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, rotationType, "NodeBinding"); + + EXPECT_EQ(*binding->getInputs()->getChild("rotation")->get(), vec3f(1.f, 2.f, 3.f)); + EXPECT_EQ(binding->getRotationType(), rotationType); + + ASSERT_TRUE(m_logicEngine.destroy(*binding)); + } + } + + TEST_F(ARamsesNodeBinding_RotationTypes, TakesOverQuaternionFromRamsesNode_WhenEnumMatches) + { + m_node->setRotation(quat(1.f, 2.f, 3.f, 4.f)); + ramses::RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Quaternion, "NodeBinding"); + + EXPECT_EQ(*binding->getInputs()->getChild("rotation")->get(), vec4f(2.f, 3.f, 4.f, 1.f)); + EXPECT_EQ(binding->getRotationType(), ramses::ERotationType::Quaternion); + + ASSERT_TRUE(m_logicEngine.destroy(*binding)); + } + + TEST_F(ARamsesNodeBinding_RotationTypes, InitializesRotationWithZeroAndPrintsWarning_WhenConventionEnumsDontMatch) + { + const std::vector> mismatchedEnumPairs = + { + {ramses::ERotationType::Euler_ZYX, ramses::ERotationType::Euler_XYZ}, + {ramses::ERotationType::Euler_YZX, ramses::ERotationType::Euler_ZYX}, + {ramses::ERotationType::Euler_ZXY, ramses::ERotationType::Euler_YZX}, + {ramses::ERotationType::Euler_XZY, ramses::ERotationType::Euler_ZXY}, + {ramses::ERotationType::Euler_YXZ, ramses::ERotationType::Euler_XZY}, + {ramses::ERotationType::Euler_XYZ, ramses::ERotationType::Euler_YXZ}, + }; + + std::string warningMessage; + ELogLevel messageType; + ScopedLogContextLevel scopedLogs(ELogLevel::Warn, [&warningMessage, &messageType](ELogLevel msgType, std::string_view message) { + warningMessage = message; + messageType = msgType; + }); + + for (const auto& [logicEnum, ramsesEnum] : mismatchedEnumPairs) + { + m_node->setRotation({1, 2, 3}, ramsesEnum); + ramses::RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, logicEnum, "NodeBinding"); + + EXPECT_EQ(messageType, ELogLevel::Warn); + EXPECT_EQ(warningMessage, fmt::format("Initial rotation values for RamsesNodeBinding '{}' will not be imported from bound Ramses node due to mismatching rotation type.", binding->m_impl.getIdentificationString())); + + EXPECT_EQ(*binding->getInputs()->getChild("rotation")->get(), vec3f(0.f, 0.f, 0.f)); + EXPECT_EQ(binding->getRotationType(), logicEnum); + + ASSERT_TRUE(m_logicEngine.destroy(*binding)); + + messageType = ELogLevel::Off; + warningMessage = ""; + } + } + + TEST_F(ARamsesNodeBinding_RotationTypes, InitializesQuaternionWithIdentityAndPrintsWarning_WhenConventionEnumsDontMatch) + { + std::string warningMessage; + ELogLevel messageType; + ScopedLogContextLevel scopedLogs(ELogLevel::Warn, [&warningMessage, &messageType](ELogLevel msgType, std::string_view message) { + warningMessage = message; + messageType = msgType; + }); + + m_node->setRotation({1, 2, 3}); + ramses::RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Quaternion, "NodeBinding"); + + EXPECT_EQ(messageType, ELogLevel::Warn); + EXPECT_EQ(warningMessage, fmt::format("Initial rotation values for RamsesNodeBinding '{}' will not be imported from bound Ramses node due to mismatching rotation type. Expected Quaternion, got Euler.", binding->m_impl.getIdentificationString())); + + EXPECT_EQ(*binding->getInputs()->getChild("rotation")->get(), vec4f(0.f, 0.f, 0.f, 1.f)); + EXPECT_EQ(binding->getRotationType(), ramses::ERotationType::Quaternion); + + ASSERT_TRUE(m_logicEngine.destroy(*binding)); + + messageType = ELogLevel::Off; + warningMessage = ""; + } + + TEST_F(ARamsesNodeBinding_RotationTypes, PrintsNoWarning_WhenConventionEnumsDontMatch_InSpecialCaseWhereRamsesRotationValuesAreZero) + { + ScopedLogContextLevel scopedLogs(ELogLevel::Warn, [](ELogLevel /*unused*/, std::string_view /*unused*/) { + FAIL() << "Should not cause any warnings!"; + }); + + m_node->setRotation({0.f, 0.f, 0.f}, ramses::ERotationType::Euler_XYX); + ramses::RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + + EXPECT_EQ(*binding->getInputs()->getChild("rotation")->get(), vec3f(0.f, 0.f, 0.f)); + EXPECT_EQ(binding->getRotationType(), ramses::ERotationType::Euler_XYZ); + + ASSERT_TRUE(m_logicEngine.destroy(*binding)); + } + + TEST_F(ARamsesNodeBinding_RotationTypes, PrintsNoWarning_WhenNoQuaternion_InSpecialCaseWhereRamsesRotationValuesAreZero) + { + ScopedLogContextLevel scopedLogs(ELogLevel::Warn, [](ELogLevel /*unused*/, std::string_view /*unused*/) { + FAIL() << "Should not cause any warnings!"; + }); + + m_node->setRotation({0.f, 0.f, 0.f}, ramses::ERotationType::Euler_XYX); + ramses::RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Quaternion, "NodeBinding"); + + EXPECT_EQ(*binding->getInputs()->getChild("rotation")->get(), vec4f(0.f, 0.f, 0.f, 1.f)); + EXPECT_EQ(binding->getRotationType(), ramses::ERotationType::Quaternion); + + ASSERT_TRUE(m_logicEngine.destroy(*binding)); + } + + TEST_F(ARamsesNodeBinding_RotationTypes, InitializesQuaternionAsVec4f) + { + ramses::RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Quaternion); + + EXPECT_EQ(5u, binding->getInputs()->getChildCount()); + EXPECT_EQ(EPropertyType::Vec4f, binding->getInputs()->getChild("rotation")->getType()); + + // Test other inputs to check they are not affected + EXPECT_EQ(EPropertyType::Vec3f, binding->getInputs()->getChild("translation")->getType()); + EXPECT_EQ(EPropertyType::Vec3f, binding->getInputs()->getChild("scaling")->getType()); + EXPECT_EQ(EPropertyType::Bool, binding->getInputs()->getChild("visibility")->getType()); + EXPECT_EQ(EPropertyType::Bool, binding->getInputs()->getChild("enabled")->getType()); + } + + TEST_F(ARamsesNodeBinding_RotationTypes, InitializesQuaternionValues_WhichResultsToNoRotationInEuler) + { + ramses::RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Quaternion); + ramses::Property* quatInput = binding->getInputs()->getChild("rotation"); + + ASSERT_EQ(quatInput->getType(), EPropertyType::Vec4f); + EXPECT_EQ(*quatInput->get(), vec4f(0.f, 0.f, 0.f, 1.f)); + + EXPECT_TRUE(m_logicEngine.update()); + + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 0.0f, 0.0f, 0.0f }); + } + + TEST_F(ARamsesNodeBinding_RotationTypes, QuaternionValuesAreReturned) + { + ramses::RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Quaternion); + ramses::Property* quatInput = binding->getInputs()->getChild("rotation"); + + quatInput->set({0.5f, 0, 0, 0.8660254f}); + EXPECT_TRUE(m_logicEngine.update()); + ExpectQuat(*m_node, vec4f{0.5f, 0.0f, 0.0f, 0.8660254f}); + + quatInput->set({ 0, 0.258819f, 0, 0.9659258f }); + EXPECT_TRUE(m_logicEngine.update()); + ExpectQuat(*m_node, vec4f{0, 0.258819f, 0, 0.9659258f}); + + quatInput->set({ 0, 0, 0.7071068f, 0.7071068f }); + EXPECT_TRUE(m_logicEngine.update()); + ExpectQuat(*m_node, vec4f{0, 0, 0.7071068f, 0.7071068f}); + } + + // This is a confidence test that checks that no matter which rotation type is used, the final result (matrix) + // is always the same as if each rotation/axis was put in its own node with the order given by the node hierarchy + TEST_F(ARamsesNodeBinding_RotationTypes, Confidence_ComplexEulerRotations_ProduceTheSameRotationResult_AsWithSeparateRamsesNodesWithSingleAxisRotations) + { + std::array threeNodes = { + m_scene->createNode(), + m_scene->createNode(), + m_scene->createNode(), + }; + + threeNodes[2]->setParent(*threeNodes[1]); + threeNodes[1]->setParent(*threeNodes[0]); + + const std::vector> combinationsToTest = + { + {ramses::ERotationType::Euler_ZYX, {2, 1, 0}}, + {ramses::ERotationType::Euler_YZX, {1, 2, 0}}, + {ramses::ERotationType::Euler_ZXY, {2, 0, 1}}, + {ramses::ERotationType::Euler_XZY, {0, 2, 1}}, + {ramses::ERotationType::Euler_YXZ, {1, 0, 2}}, + {ramses::ERotationType::Euler_XYZ, {0, 1, 2}}, + }; + + for (const auto& [rotationType, axesOrdering] : combinationsToTest) + { + // Set the rotations for all 3 nodes according to the rotationType, use the same rotation values always + for (glm::length_t i = 0; i < 3; ++i) + { + const auto axis = axesOrdering[i]; + const auto nodeIndex = 2-i; + if (axis == 0) + { + threeNodes[nodeIndex]->setRotation({10, 0, 0}, ramses::ERotationType::Euler_XYZ); + } + else if (axis == 1) + { + threeNodes[nodeIndex]->setRotation({0, 20, 0}, ramses::ERotationType::Euler_XYZ); + } + else + { + threeNodes[nodeIndex]->setRotation({0, 0, 30}, ramses::ERotationType::Euler_XYZ); + } + } + + m_node->setRotation({0, 0, 0}, rotationType); + RamsesNodeBinding* binding = m_logicEngine.createRamsesNodeBinding(*m_node, rotationType); + binding->getInputs()->getChild("rotation")->set({10, 20, 30}); + EXPECT_TRUE(m_logicEngine.update()); + + matrix44f matFromLogic; + matrix44f matFromRamses; + + m_node->getModelMatrix(matFromLogic); + threeNodes[2]->getModelMatrix(matFromRamses); + + for (glm::length_t i = 0u; i < 4; i++) + { + for (glm::length_t k = 0u; k < 4; k++) + { + EXPECT_NEAR(matFromLogic[i][k], matFromRamses[i][k], 1.0e-6f); + } + } + + m_logicEngine.destroy(*binding); + } + } + + // This fixture only contains serialization unit tests, for higher order tests see `ARamsesNodeBinding_SerializationWithFile` + class ARamsesNodeBinding_SerializationLifecycle : public ARamsesNodeBinding + { + protected: + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + SerializationTestUtils m_testUtils{ m_flatBufferBuilder }; + ::testing::StrictMock m_resolverMock; + ErrorReporting m_errorReporting; + SerializationMap m_serializationMap; + DeserializationMap m_deserializationMap; + }; + + // More unit tests with inputs/outputs declared in LogicNode (base class) serialization tests + TEST_F(ARamsesNodeBinding_SerializationLifecycle, RemembersBaseClassData) + { + // Serialize + { + RamsesNodeBindingImpl binding(*m_node, ramses::ERotationType::Euler_XYZ, "name", 1u); + binding.createRootProperties(); + (void)RamsesNodeBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + // Inspect flatbuffers data + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serializedBinding.base()); + ASSERT_TRUE(serializedBinding.base()->base()); + ASSERT_TRUE(serializedBinding.base()->base()->name()); + EXPECT_EQ(serializedBinding.base()->base()->name()->string_view(), "name"); + EXPECT_EQ(serializedBinding.base()->base()->id(), 1u); + + ASSERT_TRUE(serializedBinding.base()->rootInput()); + EXPECT_EQ(serializedBinding.base()->rootInput()->rootType(), rlogic_serialization::EPropertyRootType::Struct); + ASSERT_TRUE(serializedBinding.base()->rootInput()->children()); + EXPECT_EQ(serializedBinding.base()->rootInput()->children()->size(), 5u); + + // Deserialize + { + EXPECT_CALL(m_resolverMock, findRamsesNodeInScene(::testing::Eq("name"), m_node->getSceneObjectId())).WillOnce(::testing::Return(m_node)); + std::unique_ptr deserializedBinding = RamsesNodeBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + + ASSERT_TRUE(deserializedBinding); + EXPECT_EQ(deserializedBinding->getName(), "name"); + EXPECT_EQ(deserializedBinding->getId(), 1u); + EXPECT_EQ(deserializedBinding->getInputs()->getType(), EPropertyType::Struct); + EXPECT_EQ(deserializedBinding->getInputs()->m_impl->getPropertySemantics(), EPropertySemantics::BindingInput); + EXPECT_EQ(deserializedBinding->getInputs()->getName(), ""); + EXPECT_EQ(deserializedBinding->getInputs()->getChildCount(), 5u); + } + } + + TEST_F(ARamsesNodeBinding_SerializationLifecycle, RemembersRamsesNodeId) + { + // Serialize + { + RamsesNodeBindingImpl binding(*m_node, ramses::ERotationType::Euler_XYZ, "node", 1u); + binding.createRootProperties(); + (void)RamsesNodeBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + // Inspect flatbuffers data + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + EXPECT_EQ(serializedBinding.base()->boundRamsesObject()->objectId(), m_node->getSceneObjectId().getValue()); + EXPECT_EQ(serializedBinding.base()->boundRamsesObject()->objectType(), static_cast(ramses::ERamsesObjectType::Node)); + + // Deserialize + { + EXPECT_CALL(m_resolverMock, findRamsesNodeInScene(::testing::Eq("node"), m_node->getSceneObjectId())).WillOnce(::testing::Return(m_node)); + std::unique_ptr deserializedBinding = RamsesNodeBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + + ASSERT_TRUE(deserializedBinding); + EXPECT_EQ(&deserializedBinding->getRamsesNode(), m_node); + } + } + + TEST_F(ARamsesNodeBinding_SerializationLifecycle, DoesNotOverwriteRamsesValuesAfterLoad) + { + // Serialize + { + RamsesNodeBindingImpl binding(*m_node, ramses::ERotationType::Euler_XYZ, "node", 1u); + binding.createRootProperties(); + // Set non-standard values. These will not be used after deserialization, instead the binding + // will re-load the values from ramses + binding.getInputs()->getChild("rotation")->set({100, 200, 300}); + binding.update(); + (void)RamsesNodeBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + // Deserialize + { + // Set values different than the ones during serialization so that we can check after + // deserialization they were not touched + m_node->setRotation({11, 12, 13}); + + EXPECT_CALL(m_resolverMock, findRamsesNodeInScene(::testing::Eq("node"), m_node->getSceneObjectId())).WillOnce(::testing::Return(m_node)); + std::unique_ptr deserializedBinding = RamsesNodeBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_EQ(&deserializedBinding->getRamsesNode(), m_node); + + deserializedBinding->update(); + vec3f rotation; + m_node->getRotation(rotation); + EXPECT_FLOAT_EQ(rotation[0], 11); + EXPECT_FLOAT_EQ(rotation[1], 12); + EXPECT_FLOAT_EQ(rotation[2], 13); + } + } + + TEST_F(ARamsesNodeBinding_SerializationLifecycle, ErrorWhenNoBindingBaseData) + { + { + auto binding = rlogic_serialization::CreateRamsesNodeBinding( + m_flatBufferBuilder, + 0 // no base binding info + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesNodeBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesNodeBinding from serialized data: missing base class info!"); + } + + TEST_F(ARamsesNodeBinding_SerializationLifecycle, ErrorWhenNoBindingName) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + 0, // no name! + 1u) + ); + auto binding = rlogic_serialization::CreateRamsesNodeBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesNodeBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing name!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of RamsesNodeBinding from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ARamsesNodeBinding_SerializationLifecycle, ErrorWhenNoBindingId) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 0)); + auto binding = rlogic_serialization::CreateRamsesNodeBinding(m_flatBufferBuilder, base); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesNodeBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing or invalid ID!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of RamsesNodeBinding from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ARamsesNodeBinding_SerializationLifecycle, ErrorWhenNoRootInput) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0 // no root input + ); + auto binding = rlogic_serialization::CreateRamsesNodeBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesNodeBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesNodeBinding from serialized data: missing root input!"); + } + + TEST_F(ARamsesNodeBinding_SerializationLifecycle, ErrorWhenRootInputHasErrors) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0, + m_testUtils.serializeTestProperty("", rlogic_serialization::EPropertyRootType::Struct, false, true) // rootInput with errors + ); + auto binding = rlogic_serialization::CreateRamsesNodeBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesNodeBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: missing name!"); + } + + TEST_F(ARamsesNodeBinding_SerializationLifecycle, ErrorWhenBoundNodeCannotBeResolved) + { + const ramses::sceneObjectId_t mockObjectId{ 12 }; + { + auto ramsesRef = rlogic_serialization::CreateRamsesReference( + m_flatBufferBuilder, + mockObjectId.getValue() + ); + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + ramsesRef, + m_testUtils.serializeTestProperty("") + ); + auto binding = rlogic_serialization::CreateRamsesNodeBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + EXPECT_CALL(m_resolverMock, findRamsesNodeInScene(::testing::Eq("name"), mockObjectId)).WillOnce(::testing::Return(nullptr)); + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesNodeBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + } + + TEST_F(ARamsesNodeBinding_SerializationLifecycle, ErrorWhenSavedNodeTypeDoesNotMatchResolvedNodeType) + { + RamsesTestSetup ramses; + ramses::Scene* scene = ramses.createScene(); + auto* meshNode = scene->createMeshNode(); + + const ramses::sceneObjectId_t mockObjectId{ 12 }; + { + auto ramsesRef = rlogic_serialization::CreateRamsesReference( + m_flatBufferBuilder, + mockObjectId.getValue(), + uint32_t(ramses::ERamsesObjectType::Node) // save normal node + ); + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + ramsesRef, + m_testUtils.serializeTestProperty("") + ); + auto binding = rlogic_serialization::CreateRamsesNodeBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + // resolver returns mesh node, but normal node is expected -> error + EXPECT_CALL(m_resolverMock, findRamsesNodeInScene(::testing::Eq("name"), mockObjectId)).WillOnce(::testing::Return(meshNode)); + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesNodeBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesNodeBinding from serialized data: loaded node type does not match referenced node type!"); + } + + // TODO Violin needs more tests here: + // - deserialized with wrong object type which is not compatible to node + // - deserialized with properties but without node, or the other way around + // - rotation type different than the one in ramses node + + class ARamsesNodeBinding_SerializationWithFile : public ARamsesNodeBinding + { + protected: + WithTempDirectory tempFolder; + }; + + TEST_F(ARamsesNodeBinding_SerializationWithFile, ContainsItsDataAfterDeserialization) + { + { + LogicEngine tempEngineForSaving{ m_logicEngine.getFeatureLevel() }; + RamsesNodeBinding& nodeBinding = *tempEngineForSaving.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_YXZ, "NodeBinding"); + nodeBinding.getInputs()->getChild("rotation")->set(vec3f{ 0.1f, 0.2f, 0.3f }); + nodeBinding.getInputs()->getChild("translation")->set(vec3f{ 1.1f, 1.2f, 1.3f }); + nodeBinding.getInputs()->getChild("scaling")->set(vec3f{ 2.1f, 2.2f, 2.3f }); + nodeBinding.getInputs()->getChild("visibility")->set(true); + nodeBinding.getInputs()->getChild("enabled")->set(true); + tempEngineForSaving.update(); + EXPECT_TRUE(SaveToFileWithoutValidation(tempEngineForSaving, "OneBinding.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("OneBinding.bin", m_scene)); + const auto& nodeBinding = *m_logicEngine.findByName("NodeBinding"); + EXPECT_EQ("NodeBinding", nodeBinding.getName()); + EXPECT_EQ(nodeBinding.getId(), 1u); + + const auto& inputs = nodeBinding.getInputs(); + ASSERT_EQ(inputs->getChildCount(), 5u); + + auto rotation = inputs->getChild("rotation"); + auto translation = inputs->getChild("translation"); + auto scaling = inputs->getChild("scaling"); + auto visibility = inputs->getChild("visibility"); + EXPECT_EQ(ramses::ERotationType::Euler_YXZ, nodeBinding.getRotationType()); + ASSERT_NE(nullptr, rotation); + EXPECT_EQ("rotation", rotation->getName()); + EXPECT_EQ(EPropertyType::Vec3f, rotation->getType()); + EXPECT_EQ(EPropertySemantics::BindingInput, rotation->m_impl->getPropertySemantics()); + EXPECT_EQ(*rotation->get(), vec3f(0.1f, 0.2f, 0.3f)); + ASSERT_NE(nullptr, translation); + EXPECT_EQ("translation", translation->getName()); + EXPECT_EQ(EPropertyType::Vec3f, translation->getType()); + EXPECT_EQ(EPropertySemantics::BindingInput, translation->m_impl->getPropertySemantics()); + EXPECT_EQ(*translation->get(), vec3f(1.1f, 1.2f, 1.3f)); + ASSERT_NE(nullptr, scaling); + EXPECT_EQ("scaling", scaling->getName()); + EXPECT_EQ(EPropertyType::Vec3f, scaling->getType()); + EXPECT_EQ(EPropertySemantics::BindingInput, scaling->m_impl->getPropertySemantics()); + EXPECT_EQ(*scaling->get(), vec3f(2.1f, 2.2f, 2.3f)); + ASSERT_NE(nullptr, visibility); + EXPECT_EQ("visibility", visibility->getName()); + EXPECT_EQ(EPropertyType::Bool, visibility->getType()); + EXPECT_EQ(EPropertySemantics::BindingInput, visibility->m_impl->getPropertySemantics()); + EXPECT_TRUE(*visibility->get()); + + // Test that internal indices match properties resolved by name + EXPECT_EQ(rotation, inputs->m_impl->getChild(static_cast(ENodePropertyStaticIndex::Rotation))); + EXPECT_EQ(scaling, inputs->m_impl->getChild(static_cast(ENodePropertyStaticIndex::Scaling))); + EXPECT_EQ(translation, inputs->m_impl->getChild(static_cast(ENodePropertyStaticIndex::Translation))); + EXPECT_EQ(visibility, inputs->m_impl->getChild(static_cast(ENodePropertyStaticIndex::Visibility))); + + auto enabled = inputs->getChild("enabled"); + EXPECT_EQ("enabled", enabled->getName()); + EXPECT_EQ(EPropertyType::Bool, enabled->getType()); + EXPECT_EQ(EPropertySemantics::BindingInput, enabled->m_impl->getPropertySemantics()); + EXPECT_TRUE(*enabled->get()); + EXPECT_EQ(enabled, inputs->m_impl->getChild(static_cast(ENodePropertyStaticIndex::Enabled))); + } + } + + TEST_F(ARamsesNodeBinding_SerializationWithFile, RestoresLinkToRamsesNodeAfterLoadingFromFile) + { + { + LogicEngine tempEngineForSaving{ m_logicEngine.getFeatureLevel() }; + tempEngineForSaving.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + EXPECT_TRUE(SaveToFileWithoutValidation(tempEngineForSaving, "OneBinding.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("OneBinding.bin", m_scene)); + const auto& nodeBinding = *m_logicEngine.findByName("NodeBinding"); + EXPECT_EQ(&nodeBinding.getRamsesNode(), m_node); + } + } + + TEST_F(ARamsesNodeBinding_SerializationWithFile, ProducesErrorWhenDeserializingFromFile_WhenHavingLinkToRamsesNode_ButNoSceneWasProvided) + { + { + LogicEngine tempEngineForSaving{ m_logicEngine.getFeatureLevel() }; + tempEngineForSaving.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + EXPECT_TRUE(SaveToFileWithoutValidation(tempEngineForSaving, "WithRamsesNode.bin")); + } + { + EXPECT_FALSE(m_logicEngine.loadFromFile("WithRamsesNode.bin")); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(errors.size(), 1u); + EXPECT_EQ(errors[0].message, "Fatal error during loading from file! File contains references to Ramses objects but no Ramses scene was provided!"); + } + } + + TEST_F(ARamsesNodeBinding_SerializationWithFile, ProducesErrorWhenDeserializingFromFile_WhenHavingLinkToRamsesNode_WhichWasDeleted) + { + { + LogicEngine tempEngineForSaving{ m_logicEngine.getFeatureLevel() }; + tempEngineForSaving.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + EXPECT_TRUE(SaveToFileWithoutValidation(tempEngineForSaving, "RamsesNodeDeleted.bin")); + } + + m_scene->destroy(*m_node); + + { + EXPECT_FALSE(m_logicEngine.loadFromFile("RamsesNodeDeleted.bin", m_scene)); + auto errors = m_logicEngine.getErrors(); + ASSERT_EQ(errors.size(), 1u); + EXPECT_EQ(errors[0].message, "Fatal error during loading from file! Serialized Ramses Logic object 'NodeBinding' points to a Ramses object (id: 1) which couldn't be found in the provided scene!"); + } + } + + TEST_F(ARamsesNodeBinding_SerializationWithFile, DoesNotModifyRamsesNodePropertiesAfterLoadingFromFile_WhenNoValuesWereExplicitlySetBeforeSaving) + { + { + LogicEngine tempEngineForSaving{ m_logicEngine.getFeatureLevel() }; + tempEngineForSaving.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + EXPECT_TRUE(SaveToFileWithoutValidation(tempEngineForSaving, "NoValuesSet.bin")); + } + { + EXPECT_TRUE(m_logicEngine.loadFromFile("NoValuesSet.bin", m_scene)); + EXPECT_TRUE(m_logicEngine.update()); + + ExpectDefaultValues(*m_node); + } + } + + // Tests that the node properties don't overwrite ramses' values after loading from file, until + // set() is called again explicitly after loadFromFile() + TEST_F(ARamsesNodeBinding_SerializationWithFile, DoesNotReapplyPropertiesToRamsesAfterLoading_UntilExplicitlySetAgain) + { + { + LogicEngine tempEngineForSaving{ m_logicEngine.getFeatureLevel() }; + RamsesNodeBinding& nodeBinding = *tempEngineForSaving.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + // Set some values to the binding's inputs + nodeBinding.getInputs()->getChild("translation")->set(vec3f{ 1.1f, 1.2f, 1.3f }); + nodeBinding.getInputs()->getChild("rotation")->set(vec3f{ 2.1f, 2.2f, 2.3f }); + nodeBinding.getInputs()->getChild("scaling")->set(vec3f{ 3.1f, 3.2f, 3.3f }); + nodeBinding.getInputs()->getChild("visibility")->set(true); + nodeBinding.getInputs()->getChild("enabled")->set(false); + tempEngineForSaving.update(); + EXPECT_TRUE(SaveToFileWithoutValidation(tempEngineForSaving, "AllValuesSet.bin")); + } + + // Set properties to other values to check if they are overwritten after load + m_node->setTranslation({100.f, 100.f, 100.f}); + m_node->setRotation({100.f, 100.f, 100.f}, ramses::ERotationType::Euler_ZYX); + m_node->setScaling({100.f, 100.f, 100.f}); + m_node->setVisibility(ramses::EVisibilityMode::Invisible); + + { + EXPECT_TRUE(m_logicEngine.loadFromFile("AllValuesSet.bin", m_scene)); + + EXPECT_TRUE(m_logicEngine.update()); + + // Node binding does not re-apply its values to ramses node + ExpectValues(*m_node, ENodePropertyStaticIndex::Translation, vec3f{ 100.f, 100.f, 100.f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 100.f, 100.f, 100.f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Scaling, vec3f{ 100.f, 100.f, 100.f }); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Invisible); + + // Set only scaling. Use the same value as before save on purpose! Calling set forces set on ramses + m_logicEngine.findByName("NodeBinding")->getInputs()->getChild("scaling")->set(vec3f{ 3.1f, 3.2f, 3.3f }); + EXPECT_TRUE(m_logicEngine.update()); + + // Only scaling changed, the rest is unchanged + ExpectValues(*m_node, ENodePropertyStaticIndex::Translation, vec3f{ 100.f, 100.f, 100.f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 100.f, 100.f, 100.f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Scaling, vec3f{ 3.1f, 3.2f, 3.3f }); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Invisible); + } + } + + // This is sort of a confidence test, testing a combination of: + // - bindings only propagating their values to ramses node if the value was set by an incoming link + // - saving and loading files + // The general expectation is that after loading + update(), the logic scene would overwrite only ramses + // properties wrapped by a LogicBinding which is linked to a script + TEST_F(ARamsesNodeBinding_SerializationWithFile, SetsOnlyRamsesNodePropertiesForWhichTheBindingInputIsLinked_AfterLoadingFromFile_AndCallingUpdate) + { + // These values should not be overwritten by logic on update() + m_node->setScaling({22, 33, 44}); + m_node->setTranslation({100, 200, 300}); + + { + LogicEngine tempEngineForSaving{ m_logicEngine.getFeatureLevel() }; + + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.rotation = Type:Vec3f() + OUT.visibility = Type:Bool() + end + function run(IN,OUT) + OUT.rotation = {1, 2, 3} + OUT.visibility = false + end + )"; + + LuaScript* script = tempEngineForSaving.createLuaScript(scriptSrc); + + RamsesNodeBinding& nodeBinding = *tempEngineForSaving.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + + ASSERT_TRUE(tempEngineForSaving.link(*script->getOutputs()->getChild("rotation"), *nodeBinding.getInputs()->getChild("rotation"))); + ASSERT_TRUE(tempEngineForSaving.link(*script->getOutputs()->getChild("visibility"), *nodeBinding.getInputs()->getChild("visibility"))); + + tempEngineForSaving.update(); + EXPECT_TRUE(SaveToFileWithoutValidation(tempEngineForSaving, "SomeInputsLinked.bin")); + } + + // Modify 'linked' properties before loading to check if logic will overwrite them after load + update + m_node->setRotation({100, 100, 100}, ramses::ERotationType::Euler_ZYX); + m_node->setVisibility(ramses::EVisibilityMode::Visible); + + { + EXPECT_TRUE(m_logicEngine.loadFromFile("SomeInputsLinked.bin", m_scene)); + + EXPECT_TRUE(m_logicEngine.update()); + + // Translation and Scaling were not linked -> their values are not modified + ExpectValues(*m_node, ENodePropertyStaticIndex::Translation, { 100.f, 200.f, 300.f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Scaling, { 22.f, 33.f, 44.f }); + // Rotation and visibility are linked -> values were updated + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 1.f, 2.f, 3.f }); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Invisible); + + // Manually setting values on ramses followed by a logic update has no effect + // Logic is not "dirty" and it doesn't know it needs to update ramses + m_node->setRotation({1, 2, 3}, ramses::ERotationType::Euler_ZYX); + EXPECT_TRUE(m_logicEngine.update()); + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 1.f, 2.f, 3.f }); + } + } + + // Larger confidence tests which verify and document the entire data flow cycle of bindings + // There are smaller tests which test only properties and their data propagation rules (see property unit tests) + // There are also "dirtiness" tests which test when a node is being re-updated (see logic engine dirtiness tests) + // These tests test everything in combination + class ARamsesNodeBinding_DataFlow : public ARamsesNodeBinding + { + }; + + TEST_F(ARamsesNodeBinding_DataFlow, WithExplicitSet) + { + // Create node and preset values + m_node->setRotation({1.f, 1.f, 1.f}, ramses::ERotationType::Euler_ZYX); + m_node->setScaling({2.f, 2.f, 2.f}); + m_node->setTranslation({3.f, 3.f, 3.f}); + m_node->setVisibility(ramses::EVisibilityMode::Invisible); + + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + + m_logicEngine.update(); + + // Nothing happened - binding did not overwrite preset values because no user value set() + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 1.f, 1.f, 1.f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Scaling, vec3f{ 2.f, 2.f, 2.f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Translation, vec3f{ 3.f, 3.f, 3.f }); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Invisible); + + // Set rotation only + Property* inputs = nodeBinding.getInputs(); + inputs->getChild("rotation")->set(vec3f{ 42.f, 42.f, 42.f }); + + // Update not called yet -> still has preset values for rotation in ramses node + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 1.f, 1.f, 1.f }); + + // Update() only propagates rotation and does not touch other data + m_logicEngine.update(); + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 42.f, 42.f, 42.f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Scaling, vec3f{ 2.f, 2.f, 2.f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Translation, vec3f{ 3.f, 3.f, 3.f }); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Invisible); + + // Calling update again does not "rewrite" the data to ramses. Check this by setting a value manually and call update() again + m_node->setRotation({1.f, 1.f, 1.f}, ramses::ERotationType::Euler_ZYX); + m_logicEngine.update(); + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 1.f, 1.f, 1.f }); + + // Set all properties manually this time + inputs->getChild("rotation")->set(vec3f{ 100.f, 100.f, 100.f }); + inputs->getChild("scaling")->set(vec3f{ 200.f, 200.f, 200.f }); + inputs->getChild("translation")->set(vec3f{ 300.f, 300.f, 300.f }); + inputs->getChild("visibility")->set(true); + inputs->getChild("enabled")->set(true); + m_logicEngine.update(); + + // All of the property values were passed to ramses + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 100.f, 100.f, 100.f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Scaling, vec3f{ 200.f, 200.f, 200.f }); + ExpectValues(*m_node, ENodePropertyStaticIndex::Translation, vec3f{ 300.f, 300.f, 300.f }); + EXPECT_EQ(m_node->getVisibility(), ramses::EVisibilityMode::Visible); + } + + TEST_F(ARamsesNodeBinding_DataFlow, WithLinks) + { + // Create node and preset values + m_node->setRotation({1.f, 1.f, 1.f}, ramses::ERotationType::Euler_ZYX); + m_node->setScaling({2.f, 2.f, 2.f}); + m_node->setTranslation({3.f, 3.f, 3.f}); + m_node->setVisibility(ramses::EVisibilityMode::Off); + + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + IN.rotation = Type:Vec3f() + OUT.rotation = Type:Vec3f() + end + function run(IN,OUT) + OUT.rotation = IN.rotation + end + )"; + + LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + script->getInputs()->getChild("rotation")->set({ 1.f, 2.f, 3.f }); + RamsesNodeBinding& nodeBinding = *m_logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + + // Adding and removing link does not set anything in ramses + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("rotation"), *nodeBinding.getInputs()->getChild("rotation"))); + ASSERT_TRUE(m_logicEngine.unlink(*script->getOutputs()->getChild("rotation"), *nodeBinding.getInputs()->getChild("rotation"))); + m_logicEngine.update(); + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 1.f, 1.f, 1.f }); + + // Create link and calling update -> sets values to ramses + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("rotation"), *nodeBinding.getInputs()->getChild("rotation"))); + m_logicEngine.update(); + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 1.f, 2.f, 3.f }); + + // Link does not overwrite manually set value ... + m_node->setRotation({100.f, 100.f, 100.f}, ramses::ERotationType::Euler_ZYX); + m_logicEngine.update(); + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 100.f, 100.f, 100.f }); + + // ... until the linked script is re-executed and provides a new value + script->getInputs()->getChild("rotation")->set({11.f, 12.f, 13.f}); + m_logicEngine.update(); + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 11.f, 12.f, 13.f }); + + // Remove link -> value is not overwritten any more + ASSERT_TRUE(m_logicEngine.unlink(*script->getOutputs()->getChild("rotation"), *nodeBinding.getInputs()->getChild("rotation"))); + m_node->setRotation({100.f, 100.f, 100.f}, ramses::ERotationType::Euler_ZYX); + m_logicEngine.update(); + ExpectValues(*m_node, ENodePropertyStaticIndex::Rotation, vec3f{ 100.f, 100.f, 100.f }); + } +} diff --git a/client/logic/unittests/api/RamsesRenderGroupBindingElementsTest.cpp b/client/logic/unittests/api/RamsesRenderGroupBindingElementsTest.cpp new file mode 100644 index 000000000..33f2bc467 --- /dev/null +++ b/client/logic/unittests/api/RamsesRenderGroupBindingElementsTest.cpp @@ -0,0 +1,81 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" +#include "ramses-client-api/RenderGroup.h" +#include "impl/RamsesRenderGroupBindingElementsImpl.h" + +namespace ramses::internal +{ + class ARamsesRenderGroupBindingElements : public ALogicEngine + { + protected: + void expectElements(const RamsesRenderGroupBindingElementsImpl::Elements& elements) const + { + EXPECT_EQ(m_elements.m_impl->getElements(), elements); + } + + RamsesRenderGroupBindingElements m_elements; + }; + + TEST_F(ARamsesRenderGroupBindingElements, CanAddMeshAndRenderGroupElement) + { + EXPECT_TRUE(m_elements.addElement(*m_meshNode, "mesh")); + EXPECT_TRUE(m_elements.addElement(*m_renderGroup, "rg")); + expectElements({ { "mesh", m_meshNode }, { "rg", m_renderGroup } }); + } + + TEST_F(ARamsesRenderGroupBindingElements, CanBeCopyAndMoveConstructed) + { + EXPECT_TRUE(m_elements.addElement(*m_meshNode, "mesh")); + const RamsesRenderGroupBindingElementsImpl::Elements expectedElements{ { "mesh", m_meshNode } }; + + RamsesRenderGroupBindingElements elementsCopy{ m_elements }; + EXPECT_EQ(elementsCopy.m_impl->getElements(), expectedElements); + + RamsesRenderGroupBindingElements elementsMove{ std::move(elementsCopy) }; + EXPECT_EQ(elementsMove.m_impl->getElements(), expectedElements); + } + + TEST_F(ARamsesRenderGroupBindingElements, CanBeCopyAndMoveAssigned) + { + EXPECT_TRUE(m_elements.addElement(*m_meshNode, "mesh")); + const RamsesRenderGroupBindingElementsImpl::Elements expectedElements{ { "mesh", m_meshNode } }; + + RamsesRenderGroupBindingElements elementsCopy; + elementsCopy = m_elements; + EXPECT_EQ(elementsCopy.m_impl->getElements(), expectedElements); + + RamsesRenderGroupBindingElements elementsMove; + elementsMove = std::move(elementsCopy); + EXPECT_EQ(elementsMove.m_impl->getElements(), expectedElements); + } + + TEST_F(ARamsesRenderGroupBindingElements, AddsElementUnderItsObjectNameIfNoElementNameProvided) + { + EXPECT_TRUE(m_elements.addElement(*m_meshNode)); + expectElements({ { "meshNode", m_meshNode } }); + } + + TEST_F(ARamsesRenderGroupBindingElements, FailsToAddElementIfNoElementNameProvidedAndObjectNameEmpty) + { + EXPECT_STREQ("", m_renderGroup->getName()); + EXPECT_FALSE(m_elements.addElement(*m_renderGroup)); + expectElements({}); + } + + TEST_F(ARamsesRenderGroupBindingElements, FailsToAddElementMoreThanOnce) + { + EXPECT_TRUE(m_elements.addElement(*m_meshNode, "mesh")); + EXPECT_TRUE(m_elements.addElement(*m_renderGroup, "rg")); + expectElements({ { "mesh", m_meshNode }, { "rg", m_renderGroup } }); + EXPECT_FALSE(m_elements.addElement(*m_meshNode, "mesh")); + EXPECT_FALSE(m_elements.addElement(*m_renderGroup, "rg")); + expectElements({ { "mesh", m_meshNode }, { "rg", m_renderGroup } }); + } +} diff --git a/client/logic/unittests/api/RamsesRenderGroupBindingTest.cpp b/client/logic/unittests/api/RamsesRenderGroupBindingTest.cpp new file mode 100644 index 000000000..37cd435ae --- /dev/null +++ b/client/logic/unittests/api/RamsesRenderGroupBindingTest.cpp @@ -0,0 +1,495 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "RamsesObjectResolverMock.h" +#include "RamsesTestUtils.h" +#include "SerializationTestUtils.h" +#include "WithTempDirectory.h" + +#include "impl/LogicEngineImpl.h" +#include "impl/RamsesRenderGroupBindingImpl.h" +#include "impl/PropertyImpl.h" +#include "internals/RamsesHelper.h" +#include "generated/RamsesRenderGroupBindingGen.h" + +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/LogicEngine.h" + +#include "ramses-client-api/RenderGroup.h" + +#include "ramses-utils.h" + +namespace ramses::internal +{ + class ARamsesRenderGroupBinding : public ALogicEngine + { + }; + + TEST_F(ARamsesRenderGroupBinding, HasANameAfterCreation) + { + auto& renderGroupBinding = *createRenderGroupBinding(); + EXPECT_EQ("renderGroupBinding", renderGroupBinding.getName()); + } + + TEST_F(ARamsesRenderGroupBinding, RefersToGivenRenderGroup) + { + auto& renderGroupBinding = *createRenderGroupBinding(); + EXPECT_EQ(m_renderGroup, &renderGroupBinding.getRamsesRenderGroup()); + const auto& rpConst = renderGroupBinding; + EXPECT_EQ(m_renderGroup, &rpConst.getRamsesRenderGroup()); + const auto& rpImplConst = renderGroupBinding.m_renderGroupBinding; + EXPECT_EQ(m_renderGroup, &rpImplConst.getRamsesRenderGroup()); + } + + TEST_F(ARamsesRenderGroupBinding, HasInputsAfterCreationWithCorrectNamesAndValues) + { + const auto mn1 = m_scene->createMeshNode("meshNodeName1"); + const auto mn2 = m_scene->createMeshNode("meshNodeName2"); + const auto rg1 = m_scene->createRenderGroup("renderGroupName1"); + const auto rg2 = m_scene->createRenderGroup("renderGroupName2"); + // in order to control render order of these objects they have to be contained in the rendergroup to bind + m_renderGroup->addMeshNode(*mn1, -100); + m_renderGroup->addMeshNode(*mn2, 33); + m_renderGroup->addRenderGroup(*rg1, -60); + m_renderGroup->addRenderGroup(*rg2, 42); + + // add objects as elements to control through rendergroup binding + // (intentionally in specific order and using both object or element name) + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*mn2, "elementMesh2Name")); + EXPECT_TRUE(elements.addElement(*rg2, "elementRG2Name")); + EXPECT_TRUE(elements.addElement(*mn1)); + EXPECT_TRUE(elements.addElement(*rg1)); + + const auto& renderGroupBinding = *m_logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements); + ASSERT_NE(nullptr, renderGroupBinding.getInputs()); + ASSERT_EQ(1u, renderGroupBinding.getInputs()->getChildCount()); + const auto renderOrdersProp = renderGroupBinding.getInputs()->getChild("renderOrders"); + ASSERT_NE(nullptr, renderOrdersProp); + ASSERT_EQ(4u, renderOrdersProp->getChildCount()); + + for (size_t i = 0u; i < renderOrdersProp->getChildCount(); ++i) + { + ASSERT_EQ(EPropertyType::Int32, renderOrdersProp->getChild(i)->getType()); + EXPECT_EQ(EPropertySemantics::BindingInput, renderOrdersProp->getChild(i)->m_impl->getPropertySemantics()); + } + + const auto prop1 = renderOrdersProp->getChild(0u); + EXPECT_EQ("elementMesh2Name", prop1->getName()); + EXPECT_EQ(33, *prop1->get()); + + const auto prop2 = renderOrdersProp->getChild(1u); + EXPECT_EQ("elementRG2Name", prop2->getName()); + EXPECT_EQ(42, *prop2->get()); + + const auto prop3 = renderOrdersProp->getChild(2u); + EXPECT_EQ("meshNodeName1", prop3->getName()); + EXPECT_EQ(-100, *prop3->get()); + + const auto prop4 = renderOrdersProp->getChild(3u); + EXPECT_EQ("renderGroupName1", prop4->getName()); + EXPECT_EQ(-60, *prop4->get()); + } + + TEST_F(ARamsesRenderGroupBinding, HasNoOutputsAfterCreation) + { + auto& renderGroupBinding = *createRenderGroupBinding(); + EXPECT_EQ(nullptr, renderGroupBinding.getOutputs()); + } + + TEST_F(ARamsesRenderGroupBinding, FailsToBeCreatedIfNoProvidedElement) + { + RamsesRenderGroupBindingElements emptyElements; + EXPECT_EQ(nullptr, m_logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, emptyElements)); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot create RamsesRenderGroupBinding, there were no elements provided."); + } + + TEST_F(ARamsesRenderGroupBinding, FailsToBeCreatedIfProvidedElementNotContainedInRenderGroupInRamses) + { + const auto mn1 = m_scene->createMeshNode("meshNodeName1"); + RamsesRenderGroupBindingElements elements1; + EXPECT_TRUE(elements1.addElement(*m_meshNode)); // contained + EXPECT_TRUE(elements1.addElement(*mn1)); // not contained + EXPECT_EQ(nullptr, m_logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements1)); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot create RamsesRenderGroupBinding, one or more of the provided elements is not contained in the RenderGroup to bind."); + + const auto rg1 = m_scene->createRenderGroup("renderGroupName1"); + RamsesRenderGroupBindingElements elements2; + EXPECT_TRUE(elements2.addElement(*m_meshNode)); // contained + EXPECT_TRUE(elements2.addElement(*rg1)); // not contained + EXPECT_EQ(nullptr, m_logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements2)); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot create RamsesRenderGroupBinding, one or more of the provided elements is not contained in the RenderGroup to bind."); + } + + TEST_F(ARamsesRenderGroupBinding, RemovesLinkWhenDestroyed) + { + auto& renderGroupBinding = *createRenderGroupBinding(); + + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.val = Type:Int32() + end + function run(IN,OUT) + OUT.val = 42 + end + )"; + const LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("val"), *renderGroupBinding.getInputs()->getChild("renderOrders")->getChild(0u))); + EXPECT_TRUE(script->getOutputs()->getChild("val")->isLinked()); + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_TRUE(m_logicEngine.destroy(renderGroupBinding)); + EXPECT_FALSE(script->getOutputs()->getChild("val")->isLinked()); + } + + TEST_F(ARamsesRenderGroupBinding, TakesInitialValuesFromRamsesRenderGroup) + { + const auto nestedRG = m_scene->createRenderGroup(); + m_renderGroup->addRenderGroup(*nestedRG, 41); + m_renderGroup->addMeshNode(*m_meshNode, 42); + + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*m_meshNode, "mesh")); + EXPECT_TRUE(elements.addElement(*nestedRG, "rg")); + const auto& renderGroupBinding = m_logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements); + + EXPECT_EQ(41, *renderGroupBinding->getInputs()->getChild("renderOrders")->getChild("rg")->get()); + EXPECT_EQ(42, *renderGroupBinding->getInputs()->getChild("renderOrders")->getChild("mesh")->get()); + } + + TEST_F(ARamsesRenderGroupBinding, AppliesChangesToBoundObject) + { + auto& renderGroupBinding = *createRenderGroupBinding(); + EXPECT_TRUE(renderGroupBinding.getInputs()->getChild(0u)->getChild(0u)->set(42)); + m_logicEngine.update(); + int32_t renderOrder = -1; + m_renderGroup->getMeshNodeOrder(*m_meshNode, renderOrder); + EXPECT_EQ(42, renderOrder); + + EXPECT_TRUE(renderGroupBinding.getInputs()->getChild(0u)->getChild(0u)->set(-100)); + m_logicEngine.update(); + m_renderGroup->getMeshNodeOrder(*m_meshNode, renderOrder); + EXPECT_EQ(-100, renderOrder); + } + + TEST_F(ARamsesRenderGroupBinding, PropagateItsInputsToRamsesRenderGroupOnUpdate_OnlyWhenExplicitlySet) + { + const auto nestedRG = m_scene->createRenderGroup(); + m_renderGroup->addRenderGroup(*nestedRG, 41); + m_renderGroup->addMeshNode(*m_meshNode, 42); + + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*m_meshNode, "mesh")); + EXPECT_TRUE(elements.addElement(*nestedRG, "rg")); + auto& renderGroupBinding = *m_logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements); + + m_logicEngine.update(); + int32_t renderOrder = -1; + m_renderGroup->getRenderGroupOrder(*nestedRG, renderOrder); + EXPECT_EQ(41, renderOrder); + m_renderGroup->getMeshNodeOrder(*m_meshNode, renderOrder); + EXPECT_EQ(42, renderOrder); + + // update only mesh + EXPECT_TRUE(renderGroupBinding.getInputs()->getChild("renderOrders")->getChild("mesh")->set(-100)); + m_logicEngine.update(); + m_renderGroup->getRenderGroupOrder(*nestedRG, renderOrder); + EXPECT_EQ(41, renderOrder); // stays unchanged + m_renderGroup->getMeshNodeOrder(*m_meshNode, renderOrder); + EXPECT_EQ(-100, renderOrder); + } + + TEST_F(ARamsesRenderGroupBinding, PropagatesItsInputsToRamsesRenderGroupOnUpdate_WithLinksInsteadOfSetCall) + { + auto& renderGroupBinding = *createRenderGroupBinding(); + + // Link binding input to a script (binding is not set directly, but is linked) + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.val = Type:Int32() + end + function run(IN,OUT) + OUT.val = 42 + end + )"; + const LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("val"), *renderGroupBinding.getInputs()->getChild("renderOrders")->getChild(0u))); + EXPECT_TRUE(m_logicEngine.update()); + + int32_t renderOrder = -1; + m_renderGroup->getMeshNodeOrder(*m_meshNode, renderOrder); + EXPECT_EQ(42, renderOrder); + } + + TEST_F(ARamsesRenderGroupBinding, GracefullyFailsUpdateIfChangingRenderOrderOfMeshNodeWhichWasRemovedFromBoundRenderGroup) + { + auto& renderGroupBinding = *createRenderGroupBinding(); + EXPECT_TRUE(renderGroupBinding.getInputs()->getChild(0u)->getChild(0u)->set(42)); + EXPECT_TRUE(m_logicEngine.update()); + + // remove mesh from rendergroup using ramses API + // this is illegal but can happen + m_renderGroup->removeMeshNode(*m_meshNode); + + EXPECT_TRUE(renderGroupBinding.getInputs()->getChild(0u)->getChild(0u)->set(-42)); + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot set render order of MeshNode which is not contained in bound RenderGroup."); + } + + TEST_F(ARamsesRenderGroupBinding, GracefullyFailsUpdateIfChangingRenderOrderOfRenderGroupWhichWasRemovedFromBoundRenderGroup) + { + const auto nestedRG = m_scene->createRenderGroup(); + m_renderGroup->addRenderGroup(*nestedRG); + + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*m_meshNode, "mesh")); + EXPECT_TRUE(elements.addElement(*nestedRG, "rg")); + auto& renderGroupBinding = *m_logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements); + + EXPECT_TRUE(renderGroupBinding.getInputs()->getChild(0u)->getChild("rg")->set(42)); + EXPECT_TRUE(m_logicEngine.update()); + + // remove nested rendergroup from bound rendergroup using ramses API + // this is illegal but can happen + m_renderGroup->removeRenderGroup(*nestedRG); + + EXPECT_TRUE(renderGroupBinding.getInputs()->getChild(0u)->getChild("rg")->set(-42)); + EXPECT_FALSE(m_logicEngine.update()); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Cannot set render order of RenderGroup which is not contained in bound RenderGroup."); + } + + class ARamsesRenderGroupBinding_SerializationLifecycle : public ARamsesRenderGroupBinding + { + protected: + enum class ESerializationIssue + { + AllValid, + MissingName, + MissingRootInput, + RootInputNotStruct, + MissingRamsesRefObject, + RamsesRefObjectNotInScene, + MismatchedRamsesRefObjectType, + PropertiesDoNotMatchElements, + MissingElementRamsesRefObject, + ElementRamsesRefObjectNotInScene, + MismatchedElementRamsesRefObjectType + }; + + std::unique_ptr deserializeSerializedDataWithIssue(ESerializationIssue issue) + { + { + auto inputsType = MakeStruct("", { + TypeData{"renderOrders", EPropertyType::Struct}, + }); + + if (issue != ESerializationIssue::PropertiesDoNotMatchElements) + inputsType.children.front().children.push_back(MakeType("elementName", EPropertyType::Int32)); + + HierarchicalTypeData inputs = (issue == ESerializationIssue::RootInputNotStruct ? MakeType("", EPropertyType::Bool) : inputsType); + auto inputsImpl = std::make_unique(std::move(inputs), EPropertySemantics::BindingInput); + + auto fbRamsesBinding = rlogic_serialization::CreateRamsesBinding(m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, (issue == ESerializationIssue::MissingName ? 0 : m_flatBufferBuilder.CreateString("name")), 1u, 0u, 0u), + (issue == ESerializationIssue::MissingRamsesRefObject ? 0 : rlogic_serialization::CreateRamsesReference(m_flatBufferBuilder, + 1u, (issue == ESerializationIssue::MismatchedRamsesRefObjectType ? 0 : static_cast(ramses::ERamsesObjectType::RenderGroup)))), + (issue == ESerializationIssue::MissingRootInput ? 0 : PropertyImpl::Serialize(*inputsImpl, m_flatBufferBuilder, m_serializationMap))); + + std::vector> elementsFB; + elementsFB.push_back(rlogic_serialization::CreateElement( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("elementName"), + (issue == ESerializationIssue::MissingElementRamsesRefObject ? 0 : rlogic_serialization::CreateRamsesReference(m_flatBufferBuilder, + 2u, (issue == ESerializationIssue::MismatchedElementRamsesRefObjectType ? 0 : static_cast(m_meshNode->getType())))))); + + auto fbRenderGroupBinding = rlogic_serialization::CreateRamsesRenderGroupBinding(m_flatBufferBuilder, fbRamsesBinding, m_flatBufferBuilder.CreateVector(elementsFB)); + m_flatBufferBuilder.Finish(fbRenderGroupBinding); + } + + switch (issue) + { + case ESerializationIssue::AllValid: + case ESerializationIssue::MismatchedRamsesRefObjectType: + case ESerializationIssue::PropertiesDoNotMatchElements: + case ESerializationIssue::MissingElementRamsesRefObject: + case ESerializationIssue::ElementRamsesRefObjectNotInScene: + case ESerializationIssue::MismatchedElementRamsesRefObjectType: + EXPECT_CALL(m_resolverMock, findRamsesRenderGroupInScene(::testing::Eq("name"), ramses::sceneObjectId_t{ 1u })).WillOnce(::testing::Return(m_renderGroup)); + break; + case ESerializationIssue::RamsesRefObjectNotInScene: + EXPECT_CALL(m_resolverMock, findRamsesRenderGroupInScene(::testing::Eq("name"), ramses::sceneObjectId_t{ 1u })).WillOnce(::testing::Return(nullptr)); + break; + default: + break; + } + + switch (issue) + { + case ESerializationIssue::AllValid: + case ESerializationIssue::MismatchedElementRamsesRefObjectType: + EXPECT_CALL(m_resolverMock, findRamsesSceneObjectInScene(::testing::Eq("name"), ramses::sceneObjectId_t{ 2u })).WillOnce(::testing::Return(m_meshNode)); + break; + case ESerializationIssue::ElementRamsesRefObjectNotInScene: + EXPECT_CALL(m_resolverMock, findRamsesSceneObjectInScene(::testing::Eq("name"), ramses::sceneObjectId_t{ 2u })).WillOnce(::testing::Return(nullptr)); + break; + default: + break; + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + return RamsesRenderGroupBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + } + + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + SerializationTestUtils m_testUtils {m_flatBufferBuilder}; + ::testing::StrictMock m_resolverMock; + ErrorReporting m_errorReporting; + SerializationMap m_serializationMap; + DeserializationMap m_deserializationMap; + }; + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, CanSerializeWithNoIssue) + { + EXPECT_TRUE(deserializeSerializedDataWithIssue(ARamsesRenderGroupBinding_SerializationLifecycle::ESerializationIssue::AllValid)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, ReportsSerializationError_MissingName) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderGroupBinding_SerializationLifecycle::ESerializationIssue::MissingName)); + ASSERT_EQ(2u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LogicObject base from serialized data: missing name!"); + EXPECT_EQ(m_errorReporting.getErrors()[1].message, "Fatal error during loading of RamsesRenderGroupBinding from serialized data: missing name and/or ID!"); + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, ReportsSerializationError_MissingRootInput) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderGroupBinding_SerializationLifecycle::ESerializationIssue::MissingRootInput)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderGroupBinding from serialized data: missing root input!"); + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, ReportsSerializationError_RootInputNotStruct) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderGroupBinding_SerializationLifecycle::ESerializationIssue::RootInputNotStruct)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderGroupBinding from serialized data: root input has unexpected type!"); + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, ReportsSerializationError_MissingRamsesRefObject) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderGroupBinding_SerializationLifecycle::ESerializationIssue::MissingRamsesRefObject)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderGroupBinding from serialized data: missing ramses object reference!"); + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, ReportsSerializationError_RamsesRefObjectNotInScene) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderGroupBinding_SerializationLifecycle::ESerializationIssue::RamsesRefObjectNotInScene)); + // error log is in ramses object resolver which is mocked + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, ReportsSerializationError_MismatchedRamsesRefObjectType) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderGroupBinding_SerializationLifecycle::ESerializationIssue::MismatchedRamsesRefObjectType)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderGroupBinding from serialized data: loaded object type does not match referenced object type!"); + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, ReportsSerializationError_PropertiesDoNotMatchElements) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderGroupBinding_SerializationLifecycle::ESerializationIssue::PropertiesDoNotMatchElements)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderGroupBinding from serialized data: input properties do not match RenderGroup's elements!"); + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, ReportsSerializationError_MissingElementRamsesRefObject) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderGroupBinding_SerializationLifecycle::ESerializationIssue::MissingElementRamsesRefObject)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderGroupBinding 'name' elements data: missing name or Ramses reference!"); + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, ReportsSerializationError_ElementRamsesRefObjectNotInScene) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderGroupBinding_SerializationLifecycle::ESerializationIssue::ElementRamsesRefObjectNotInScene)); + // error log is in ramses object resolver which is mocked + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, ReportsSerializationError_MismatchedElementRamsesRefObjectType) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderGroupBinding_SerializationLifecycle::ESerializationIssue::MismatchedElementRamsesRefObjectType)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderGroupBinding 'name' elements data: loaded element object type does not match referenced object type!"); + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, KeepsItsPropertiesAfterDeserialization) + { + { + const auto nestedRG = m_scene->createRenderGroup(); + m_renderGroup->addRenderGroup(*nestedRG); + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*m_meshNode, "mesh")); + EXPECT_TRUE(elements.addElement(*nestedRG, "rg")); + auto& renderGroupBinding = *m_logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements, "renderGroupBinding"); + + EXPECT_TRUE(renderGroupBinding.getInputs()->getChild(0u)->getChild("mesh")->set(41)); + EXPECT_TRUE(renderGroupBinding.getInputs()->getChild(0u)->getChild("rg")->set(42)); + EXPECT_TRUE(m_logicEngine.update()); + + // binding has no inputs linked, validatation would fail + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "binding.bin")); + } + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("binding.bin", m_scene)); + const auto loadedBinding = m_logicEngine.findByName("renderGroupBinding"); + EXPECT_EQ(&loadedBinding->getRamsesRenderGroup(), m_renderGroup); + + ASSERT_NE(nullptr, loadedBinding->getInputs()); + ASSERT_EQ(1u, loadedBinding->getInputs()->getChildCount()); + const auto renderOrdersProp = loadedBinding->getInputs()->getChild("renderOrders"); + ASSERT_NE(nullptr, renderOrdersProp); + ASSERT_EQ(2u, renderOrdersProp->getChildCount()); + + const auto prop1 = renderOrdersProp->getChild(0u); + EXPECT_EQ("mesh", prop1->getName()); + EXPECT_EQ(EPropertyType::Int32, prop1->getType()); + EXPECT_EQ(EPropertySemantics::BindingInput, prop1->m_impl->getPropertySemantics()); + EXPECT_EQ(41, *prop1->get()); + + const auto prop2 = renderOrdersProp->getChild(1u); + EXPECT_EQ("rg", prop2->getName()); + EXPECT_EQ(EPropertyType::Int32, prop2->getType()); + EXPECT_EQ(EPropertySemantics::BindingInput, prop2->m_impl->getPropertySemantics()); + EXPECT_EQ(42, *prop2->get()); + } + } + + TEST_F(ARamsesRenderGroupBinding_SerializationLifecycle, FailsToLoadWhenNoSceneProvided) + { + { + createRenderGroupBinding(); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "binding.bin")); + } + + { + EXPECT_FALSE(m_logicEngine.loadFromFile("binding.bin")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Fatal error during loading from file! File contains references to Ramses objects but no Ramses scene was provided!"); + } + } +} diff --git a/client/logic/unittests/api/RamsesRenderPassBindingTest.cpp b/client/logic/unittests/api/RamsesRenderPassBindingTest.cpp new file mode 100644 index 000000000..5838e5759 --- /dev/null +++ b/client/logic/unittests/api/RamsesRenderPassBindingTest.cpp @@ -0,0 +1,590 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" + +#include "RamsesObjectResolverMock.h" +#include "RamsesTestUtils.h" +#include "SerializationTestUtils.h" +#include "WithTempDirectory.h" + +#include "impl/LogicEngineImpl.h" +#include "impl/RamsesRenderPassBindingImpl.h" +#include "impl/PropertyImpl.h" +#include "internals/RamsesHelper.h" +#include "generated/RamsesRenderPassBindingGen.h" + +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/LogicEngine.h" + +#include "ramses-client-api/RenderPass.h" + +#include "ramses-utils.h" + +namespace ramses::internal +{ + class ARamsesRenderPassBinding : public ALogicEngine + { + }; + + TEST_F(ARamsesRenderPassBinding, HasANameAfterCreation) + { + auto& renderPassBinding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + EXPECT_EQ("renderPass", renderPassBinding.getName()); + } + + TEST_F(ARamsesRenderPassBinding, RefersToGivenRenderPass) + { + auto& renderPassBinding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + EXPECT_EQ(m_renderPass, &renderPassBinding.getRamsesRenderPass()); + const auto& rpConst = renderPassBinding; + EXPECT_EQ(m_renderPass, &rpConst.getRamsesRenderPass()); + const auto& rpImplConst = renderPassBinding.m_renderPassBinding; + EXPECT_EQ(m_renderPass, &rpImplConst.getRamsesRenderPass()); + } + + TEST_F(ARamsesRenderPassBinding, HasInputsAfterCreation) + { + auto& renderPassBinding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + ASSERT_NE(nullptr, renderPassBinding.getInputs()); + ASSERT_EQ(std::size_t(RamsesRenderPassBindingImpl::EPropertyIndex_COUNT), renderPassBinding.getInputs()->getChildCount()); + EXPECT_EQ(renderPassBinding.getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_Enabled), renderPassBinding.getInputs()->getChild("enabled")); + EXPECT_EQ(EPropertyType::Bool, renderPassBinding.getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_Enabled)->getType()); + EXPECT_EQ(renderPassBinding.getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_RenderOrder), renderPassBinding.getInputs()->getChild("renderOrder")); + EXPECT_EQ(EPropertyType::Int32, renderPassBinding.getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_RenderOrder)->getType()); + EXPECT_EQ(renderPassBinding.getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_ClearColor), renderPassBinding.getInputs()->getChild("clearColor")); + EXPECT_EQ(EPropertyType::Vec4f, renderPassBinding.getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_ClearColor)->getType()); + EXPECT_EQ(renderPassBinding.getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_RenderOnce), renderPassBinding.getInputs()->getChild("renderOnce")); + EXPECT_EQ(EPropertyType::Bool, renderPassBinding.getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_RenderOnce)->getType()); + } + + TEST_F(ARamsesRenderPassBinding, HasNoOutputsAfterCreation) + { + auto& renderPassBinding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + EXPECT_EQ(nullptr, renderPassBinding.getOutputs()); + } + + // This fixture only contains serialization unit tests, for higher order tests see `ARamsesRenderPassBinding_WithRamses_AndFiles` + class ARamsesRenderPassBinding_SerializationLifecycle : public ARamsesRenderPassBinding + { + protected: + enum class ESerializationIssue + { + AllValid, + RootNotStruct, + BoundObjectReferenceMissing, + BoundObjectTypeMismatch + }; + + std::unique_ptr deserializeSerializedDataWithIssue(ESerializationIssue issue) + { + { + auto inputsType = MakeStruct("", { + TypeData{"enabled", EPropertyType::Bool}, + TypeData{"renderOrder", EPropertyType::Int32}, + TypeData{"clearColor", EPropertyType::Vec4f}, + TypeData{"renderOnce", EPropertyType::Bool} + }); + + HierarchicalTypeData inputs = (issue == ESerializationIssue::RootNotStruct ? MakeType("", EPropertyType::Bool) : inputsType); + auto inputsImpl = std::make_unique(std::move(inputs), EPropertySemantics::BindingInput); + + auto fbRamsesBinding = rlogic_serialization::CreateRamsesBinding(m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, m_flatBufferBuilder.CreateString("name"), 1u, 0u, 0u), + (issue == ESerializationIssue::BoundObjectReferenceMissing ? 0 : rlogic_serialization::CreateRamsesReference(m_flatBufferBuilder, + 1u, (issue == ESerializationIssue::BoundObjectTypeMismatch ? 0 : static_cast(ramses::ERamsesObjectType::RenderPass)))), + PropertyImpl::Serialize(*inputsImpl, m_flatBufferBuilder, m_serializationMap)); + + auto fbRenderPassBinding = rlogic_serialization::CreateRamsesRenderPassBinding(m_flatBufferBuilder, fbRamsesBinding); + m_flatBufferBuilder.Finish(fbRenderPassBinding); + } + + switch (issue) + { + case ESerializationIssue::AllValid: + case ESerializationIssue::BoundObjectTypeMismatch: + EXPECT_CALL(m_resolverMock, findRamsesRenderPassInScene(::testing::Eq("name"), ramses::sceneObjectId_t{ 1u })).WillOnce(::testing::Return(m_renderPass)); + break; + case ESerializationIssue::RootNotStruct: + case ESerializationIssue::BoundObjectReferenceMissing: + break; + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + return RamsesRenderPassBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + } + + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + SerializationTestUtils m_testUtils {m_flatBufferBuilder}; + ::testing::StrictMock m_resolverMock; + ErrorReporting m_errorReporting; + SerializationMap m_serializationMap; + DeserializationMap m_deserializationMap; + }; + + // More unit tests with inputs/outputs declared in LogicNode (base class) serialization tests + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, StoresBaseClassData) + { + // Serialize + { + RamsesRenderPassBindingImpl binding(*m_renderPass, "name", 1u); + binding.createRootProperties(); + (void)RamsesRenderPassBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + // Inspect flatbuffers data + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serializedBinding.base()); + ASSERT_TRUE(serializedBinding.base()->base()); + ASSERT_TRUE(serializedBinding.base()->base()->name()); + EXPECT_EQ(serializedBinding.base()->base()->name()->string_view(), "name"); + EXPECT_EQ(serializedBinding.base()->base()->id(), 1u); + + ASSERT_TRUE(serializedBinding.base()->rootInput()); + EXPECT_EQ(serializedBinding.base()->rootInput()->rootType(), rlogic_serialization::EPropertyRootType::Struct); + ASSERT_TRUE(serializedBinding.base()->rootInput()->children()); + EXPECT_EQ(serializedBinding.base()->rootInput()->children()->size(), size_t(RamsesRenderPassBindingImpl::EPropertyIndex_COUNT)); + + // Deserialize + { + EXPECT_CALL(m_resolverMock, findRamsesRenderPassInScene(::testing::Eq("name"), m_renderPass->getSceneObjectId())).WillOnce(::testing::Return(m_renderPass)); + std::unique_ptr deserializedBinding = RamsesRenderPassBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + + ASSERT_TRUE(deserializedBinding); + EXPECT_EQ(deserializedBinding->getName(), "name"); + EXPECT_EQ(deserializedBinding->getId(), 1u); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + } + + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, StoresIdAndTypeAndRenderPassRef) + { + // Serialize + { + RamsesRenderPassBindingImpl binding(*m_renderPass, "name", 1u); + binding.createRootProperties(); + (void)RamsesRenderPassBindingImpl::Serialize(binding, m_flatBufferBuilder, m_serializationMap); + } + + // Inspect flatbuffers data + const auto& serializedBinding = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + + ASSERT_TRUE(serializedBinding.base()->boundRamsesObject()); + EXPECT_EQ(serializedBinding.base()->boundRamsesObject()->objectId(), m_renderPass->getSceneObjectId().getValue()); + EXPECT_EQ(serializedBinding.base()->boundRamsesObject()->objectType(), static_cast(ramses::ERamsesObjectType::RenderPass)); + + // Deserialize + { + EXPECT_CALL(m_resolverMock, findRamsesRenderPassInScene(::testing::Eq("name"), m_renderPass->getSceneObjectId())).WillOnce(::testing::Return(m_renderPass)); + std::unique_ptr deserializedBinding = RamsesRenderPassBindingImpl::Deserialize(serializedBinding, m_resolverMock, m_errorReporting, m_deserializationMap); + + ASSERT_TRUE(deserializedBinding); + EXPECT_EQ(&deserializedBinding->getRamsesRenderPass(), m_renderPass); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + + // Check that input was deserialized too + EXPECT_EQ(deserializedBinding->getInputs()->getChild("enabled")->getType(), EPropertyType::Bool); + } + } + + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, ErrorWhenNoBindingBaseData) + { + { + auto binding = rlogic_serialization::CreateRamsesRenderPassBinding( + m_flatBufferBuilder, + 0 // no base binding info + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesRenderPassBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderPassBinding from serialized data: missing base class info!"); + } + + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, ErrorWhenNoBindingName) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + 0, // no name! + 1u) + ); + auto binding = rlogic_serialization::CreateRamsesRenderPassBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesRenderPassBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing name!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of RamsesRenderPassBinding from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, ErrorWhenNoBindingId) + { + { + auto base = rlogic_serialization::CreateRamsesBinding(m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 0) // no id + ); + auto binding = rlogic_serialization::CreateRamsesRenderPassBinding(m_flatBufferBuilder, base); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesRenderPassBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(2u, this->m_errorReporting.getErrors().size()); + EXPECT_EQ("Fatal error during loading of LogicObject base from serialized data: missing or invalid ID!", this->m_errorReporting.getErrors()[0].message); + EXPECT_EQ("Fatal error during loading of RamsesRenderPassBinding from serialized data: missing name and/or ID!", this->m_errorReporting.getErrors()[1].message); + } + + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, ErrorWhenNoRootInput) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0 // no root input + ); + auto binding = rlogic_serialization::CreateRamsesRenderPassBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesRenderPassBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderPassBinding from serialized data: missing root input!"); + } + + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, ErrorWhenBoundRenderPassCannotBeResolved) + { + const ramses::sceneObjectId_t mockObjectId {12}; + { + auto ramsesRef = rlogic_serialization::CreateRamsesReference( + m_flatBufferBuilder, + mockObjectId.getValue() + ); + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + ramsesRef, + m_testUtils.serializeTestProperty("") + ); + auto binding = rlogic_serialization::CreateRamsesRenderPassBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + EXPECT_CALL(m_resolverMock, findRamsesRenderPassInScene(::testing::Eq("name"), mockObjectId)).WillOnce(::testing::Return(nullptr)); + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesRenderPassBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + } + + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, ErrorWhenRootInputHasErrors) + { + { + auto base = rlogic_serialization::CreateRamsesBinding( + m_flatBufferBuilder, + rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + m_flatBufferBuilder.CreateString("name"), + 1u), + 0, + m_testUtils.serializeTestProperty("", rlogic_serialization::EPropertyRootType::Struct, false, true) // rootInput with errors + ); + auto binding = rlogic_serialization::CreateRamsesRenderPassBinding( + m_flatBufferBuilder, + base + ); + m_flatBufferBuilder.Finish(binding); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = RamsesRenderPassBindingImpl::Deserialize(serialized, m_resolverMock, m_errorReporting, m_deserializationMap); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of Property from serialized data: missing name!"); + } + + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, CanSerializeWithNoIssue) + { + EXPECT_TRUE(deserializeSerializedDataWithIssue(ARamsesRenderPassBinding_SerializationLifecycle::ESerializationIssue::AllValid)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, ReportsSerializationError_RootNotStructType) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderPassBinding_SerializationLifecycle::ESerializationIssue::RootNotStruct)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderPassBinding from serialized data: root input has unexpected type!"); + } + + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, ReportsSerializationError_BoundObjectReferenceMissing) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderPassBinding_SerializationLifecycle::ESerializationIssue::BoundObjectReferenceMissing)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderPassBinding from serialized data: missing ramses object reference!"); + } + + TEST_F(ARamsesRenderPassBinding_SerializationLifecycle, ReportsSerializationError_BoundObjectTypeMismatch) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ARamsesRenderPassBinding_SerializationLifecycle::ESerializationIssue::BoundObjectTypeMismatch)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderPassBinding from serialized data: loaded object type does not match referenced object type!"); + } + + class ARamsesRenderPassBinding_WithRamses : public ARamsesRenderPassBinding + { + }; + + TEST_F(ARamsesRenderPassBinding_WithRamses, ReturnsReferenceToRamsesRenderPass) + { + auto& renderPassBinding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + EXPECT_EQ(m_renderPass, &renderPassBinding.getRamsesRenderPass()); + } + + TEST_F(ARamsesRenderPassBinding_WithRamses, GivesInputs_BindingInputSemantics) + { + auto& renderPassBinding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + for (size_t i = 0; i < renderPassBinding.getInputs()->getChildCount(); ++i) + { + EXPECT_EQ(EPropertySemantics::BindingInput, renderPassBinding.getInputs()->getChild(i)->m_impl->getPropertySemantics()); + } + } + + TEST_F(ARamsesRenderPassBinding_WithRamses, TakesInitialValuesFromRamsesRenderPass) + { + m_renderPass->setEnabled(false); + m_renderPass->setRenderOrder(42); + m_renderPass->setClearColor({0.1f, 0.2f, 0.3f, 0.4f}); + m_renderPass->setRenderOnce(true); + + auto& renderPassBinding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + auto inputs = renderPassBinding.getInputs(); + EXPECT_FALSE(*inputs->getChild("enabled")->get()); + EXPECT_EQ(42, *inputs->getChild("renderOrder")->get()); + EXPECT_EQ(*inputs->getChild("clearColor")->get(), vec4f(0.1f, 0.2f, 0.3f, 0.4f)); + EXPECT_TRUE(inputs->getChild("renderOnce")->set(true)); + } + + TEST_F(ARamsesRenderPassBinding_WithRamses, UpdatesRenderPassIfInputValuesWereSet) + { + auto& renderPassBinding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + auto inputs = renderPassBinding.getInputs(); + ASSERT_EQ(std::size_t(RamsesRenderPassBindingImpl::EPropertyIndex_COUNT), inputs->getChildCount()); + EXPECT_TRUE(inputs->getChild("enabled")->set(false)); + EXPECT_TRUE(inputs->getChild("renderOrder")->set(42)); + EXPECT_TRUE(inputs->getChild("clearColor")->set({ 0.1f, 0.2f, 0.3f, 0.4f })); + EXPECT_TRUE(inputs->getChild("renderOnce")->set(true)); + + EXPECT_TRUE(m_logicEngine.update()); + + EXPECT_FALSE(m_renderPass->isEnabled()); + EXPECT_EQ(42, m_renderPass->getRenderOrder()); + vec4f clearColor = m_renderPass->getClearColor(); + EXPECT_EQ(clearColor, vec4f(0.1f, 0.2f, 0.3f, 0.4f)); + EXPECT_TRUE(m_renderPass->isRenderOnce()); + } + + TEST_F(ARamsesRenderPassBinding_WithRamses, PropagateItsInputsToRamsesRenderPassOnUpdate_OnlyWhenExplicitlySet) + { + auto& renderPassBinding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + + // Set values directly + m_renderPass->setEnabled(false); + m_renderPass->setRenderOrder(3); + + // Set only one of the inputs to the binding object, the other one (enabled) not + EXPECT_TRUE(renderPassBinding.getInputs()->getChild("renderOrder")->set(99)); + + EXPECT_TRUE(m_logicEngine.update()); + + // Only propagate the value which was also set in the binding object + EXPECT_FALSE(m_renderPass->isEnabled()); + EXPECT_EQ(99, m_renderPass->getRenderOrder()); + } + + TEST_F(ARamsesRenderPassBinding_WithRamses, PropagatesItsInputsToRamsesRenderPassOnUpdate_WithLinksInsteadOfSetCall) + { + auto& renderPassBinding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + + // Set values directly + m_renderPass->setRenderOrder(13); + m_renderPass->setEnabled(false); + + // Link binding input to a script (binding is not set directly, but is linked) + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.val = Type:Int32() + end + function run(IN,OUT) + OUT.val = 42 + end + )"; + const LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("val"), *renderPassBinding.getInputs()->getChild("renderOrder"))); + + EXPECT_TRUE(m_logicEngine.update()); + + // Only propagate the value which was also linked over the binding object's input to a script + EXPECT_EQ(42, m_renderPass->getRenderOrder()); + EXPECT_FALSE(m_renderPass->isEnabled()); + } + + class ARamsesRenderPassBinding_WithRamses_AndFiles : public ARamsesRenderPassBinding_WithRamses + { + protected: + WithTempDirectory tempFolder; + }; + + TEST_F(ARamsesRenderPassBinding_WithRamses_AndFiles, KeepsItsPropertiesAfterDeserialization_WhenNoRamsesLinksAndSceneProvided) + { + { + m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "binding.bin")); + } + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("binding.bin", m_scene)); + auto loadedBinding = m_logicEngine.findByName("renderPass"); + EXPECT_EQ(&loadedBinding->getRamsesRenderPass(), m_renderPass); + EXPECT_EQ(loadedBinding->getName(), "renderPass"); + + ASSERT_NE(nullptr, loadedBinding->getInputs()); + ASSERT_EQ(std::size_t(RamsesRenderPassBindingImpl::EPropertyIndex_COUNT), loadedBinding->getInputs()->getChildCount()); + EXPECT_EQ(loadedBinding->getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_Enabled), loadedBinding->getInputs()->getChild("enabled")); + EXPECT_EQ(EPropertyType::Bool, loadedBinding->getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_Enabled)->getType()); + EXPECT_EQ(loadedBinding->getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_RenderOrder), loadedBinding->getInputs()->getChild("renderOrder")); + EXPECT_EQ(EPropertyType::Int32, loadedBinding->getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_RenderOrder)->getType()); + EXPECT_EQ(loadedBinding->getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_ClearColor), loadedBinding->getInputs()->getChild("clearColor")); + EXPECT_EQ(EPropertyType::Vec4f, loadedBinding->getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_ClearColor)->getType()); + EXPECT_EQ(loadedBinding->getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_RenderOnce), loadedBinding->getInputs()->getChild("renderOnce")); + EXPECT_EQ(EPropertyType::Bool, loadedBinding->getInputs()->getChild(RamsesRenderPassBindingImpl::EPropertyIndex_RenderOnce)->getType()); + } + } + + TEST_F(ARamsesRenderPassBinding_WithRamses_AndFiles, KeepsPropertyValueAfterDeserializationWithScene) + { + const ramses::sceneObjectId_t bindingIdBeforeReload = m_renderPass->getSceneObjectId(); + { + auto& binding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + binding.getInputs()->getChild("renderOrder")->set(42); + m_logicEngine.update(); + EXPECT_EQ(42, m_renderPass->getRenderOrder()); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "logic.bin")); + } + + { + ASSERT_TRUE(m_logicEngine.loadFromFile("logic.bin", m_scene)); + auto loadedBinding = m_logicEngine.findByName("renderPass"); + EXPECT_EQ(loadedBinding->getRamsesRenderPass().getSceneObjectId(), bindingIdBeforeReload); + + ASSERT_NE(nullptr, loadedBinding->getInputs()); + ASSERT_EQ(std::size_t(RamsesRenderPassBindingImpl::EPropertyIndex_COUNT), loadedBinding->getInputs()->getChildCount()); + EXPECT_EQ(42, *loadedBinding->getInputs()->getChild("renderOrder")->get()); + EXPECT_EQ(42, m_renderPass->getRenderOrder()); + } + } + + TEST_F(ARamsesRenderPassBinding_WithRamses_AndFiles, ProducesError_WhenHavingLinkToRenderPass_ButNoSceneWasProvided) + { + { + LogicEngine tempEngineForSaving{ m_logicEngine.getFeatureLevel() }; + tempEngineForSaving.createRamsesRenderPassBinding(*m_renderPass, "AppBinding"); + EXPECT_TRUE(SaveToFileWithoutValidation(tempEngineForSaving, "WithRamsesRenderPass.bin")); + } + { + EXPECT_FALSE(m_logicEngine.loadFromFile("WithRamsesRenderPass.bin")); + const auto& errors = m_logicEngine.getErrors(); + ASSERT_EQ(errors.size(), 1u); + EXPECT_EQ(errors[0].message, "Fatal error during loading from file! File contains references to Ramses objects but no Ramses scene was provided!"); + } + } + + // This is sort of a confidence test, testing a combination of: + // - bindings only propagating their values to ramses if the value was set by an incoming link + // - saving and loading files + // - value only re-applied to ramses if changed. Otherwise not. + // The general expectation is that after loading + update(), the logic scene would overwrite ramses + // properties wrapped by a LogicBinding if they are linked to a script + TEST_F(ARamsesRenderPassBinding_WithRamses_AndFiles, SetsOnlyRenderPassValuesForWhichTheBindingInputIsLinked_AfterLoadingFromFile_AndCallingUpdate) + { + { + const std::string_view scriptSrc = R"( + function interface(IN,OUT) + OUT.val = Type:Int32() + end + function run(IN,OUT) + OUT.val = 42 + end + )"; + + LuaScript* script = m_logicEngine.createLuaScript(scriptSrc); + auto& renderPassBinding = *m_logicEngine.createRamsesRenderPassBinding(*m_renderPass, "renderPass"); + + ASSERT_TRUE(m_logicEngine.link(*script->getOutputs()->getChild("val"), *renderPassBinding.getInputs()->getChild("renderOrder"))); + ASSERT_TRUE(m_logicEngine.update()); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "SomeValuesLinked.bin")); + } + + // Set renderOrder to a different value than the one set by the link + m_renderPass->setRenderOrder(13); + // Set renderOnce to custom value - it should not be overwritten by logic at all, because there is no link + // or any set() calls to the corresponding RamsesRenderPassBinding input + m_renderPass->setRenderOnce(true); + + { + EXPECT_TRUE(m_logicEngine.loadFromFile("SomeValuesLinked.bin", m_scene)); + + // nothing happens before update() + EXPECT_EQ(13, m_renderPass->getRenderOrder()); + EXPECT_TRUE(m_renderPass->isRenderOnce()); + + EXPECT_TRUE(m_logicEngine.update()); + + // Script is executed -> link is activated -> binding is updated, only for the linked property + EXPECT_EQ(42, m_renderPass->getRenderOrder()); + EXPECT_TRUE(m_renderPass->isRenderOnce()); + + // Reset uniform manually and call update does nothing (must set binding input explicitly to cause overwrite in ramses) + m_renderPass->setRenderOrder(13); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(13, m_renderPass->getRenderOrder()); + EXPECT_TRUE(m_renderPass->isRenderOnce()); + } + } +} diff --git a/client/logic/unittests/api/SaveFileConfigTest.cpp b/client/logic/unittests/api/SaveFileConfigTest.cpp new file mode 100644 index 000000000..b26c1f097 --- /dev/null +++ b/client/logic/unittests/api/SaveFileConfigTest.cpp @@ -0,0 +1,87 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "LogTestUtils.h" + +#include "ramses-logic/SaveFileConfig.h" +#include "impl/SaveFileConfigImpl.h" + +#include "fmt/format.h" + +namespace ramses::internal +{ + class ASaveFileConfig : public ::testing::Test + { + protected: + static void setValues(SaveFileConfig& config) + { + config.setMetadataString("metadata"); + config.setExporterVersion(1u, 2u, 3u, 4u); + config.setValidationEnabled(false); + config.setLuaSavingMode(ELuaSavingMode::SourceAndByteCode); + } + + static void checkValues(const SaveFileConfig& config) + { + EXPECT_EQ("metadata", config.m_impl->getMetadataString()); + EXPECT_EQ(1u, config.m_impl->getExporterMajorVersion()); + EXPECT_EQ(2u, config.m_impl->getExporterMinorVersion()); + EXPECT_EQ(3u, config.m_impl->getExporterPatchVersion()); + EXPECT_EQ(4u, config.m_impl->getExporterFileFormatVersion()); + EXPECT_FALSE(config.m_impl->getValidationEnabled()); + EXPECT_EQ(ELuaSavingMode::SourceAndByteCode, config.m_impl->getLuaSavingMode()); + } + }; + + TEST_F(ASaveFileConfig, HasDefaultValues) + { + SaveFileConfig config; + EXPECT_TRUE(config.m_impl->getMetadataString().empty()); + EXPECT_EQ(0u, config.m_impl->getExporterMajorVersion()); + EXPECT_EQ(0u, config.m_impl->getExporterMinorVersion()); + EXPECT_EQ(0u, config.m_impl->getExporterPatchVersion()); + EXPECT_EQ(0u, config.m_impl->getExporterFileFormatVersion()); + EXPECT_TRUE(config.m_impl->getValidationEnabled()); + EXPECT_EQ(ELuaSavingMode::SourceAndByteCode, config.m_impl->getLuaSavingMode()); + } + + TEST_F(ASaveFileConfig, IsCopied) + { + SaveFileConfig config; + setValues(config); + SaveFileConfig configCopy(config); + checkValues(configCopy); + } + + TEST_F(ASaveFileConfig, IsCopyAssigned) + { + SaveFileConfig config; + setValues(config); + SaveFileConfig configCopy; + configCopy = config; + checkValues(configCopy); + } + + TEST_F(ASaveFileConfig, IsMoved) + { + SaveFileConfig config; + setValues(config); + SaveFileConfig movedConfig(std::move(config)); + checkValues(movedConfig); + } + + TEST_F(ASaveFileConfig, IsMoveAssigned) + { + SaveFileConfig config; + setValues(config); + SaveFileConfig movedAssigned; + movedAssigned = std::move(config); + checkValues(movedAssigned); + } +} diff --git a/client/logic/unittests/api/SkinBindingTest.cpp b/client/logic/unittests/api/SkinBindingTest.cpp new file mode 100644 index 000000000..2dc3f116d --- /dev/null +++ b/client/logic/unittests/api/SkinBindingTest.cpp @@ -0,0 +1,276 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LogicEngineTest_Base.h" +#include "RamsesTestUtils.h" +#include "SerializationTestUtils.h" +#include "WithTempDirectory.h" + +#include "ramses-logic/SkinBinding.h" +#include "impl/LogicEngineImpl.h" +#include "impl/SkinBindingImpl.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "impl/RamsesAppearanceBindingImpl.h" +#include "internals/DeserializationMap.h" + +#include "generated/SkinBindingGen.h" +#include "glm/gtc/type_ptr.hpp" + +namespace ramses::internal +{ + class ASkinBinding : public ALogicEngine + { + public: + ASkinBinding() + : m_appearance{ m_scene->createAppearance(createTestEffect()) } + { + m_appearance->getEffect().findUniformInput("jointMat", m_uniform); + + // add some transformations to the joints before calculating inverse mats and creating skin + m_jointNodes[0]->setTranslation({1.f, 2.f, 3.f}); + m_jointNodes[1]->setRotation({10.f, 20.f, 30.f}); + + m_skin = createSkinBinding(); + } + + protected: + const ramses::Effect& createTestEffect() + { + ramses::EffectDescription effectDesc; + effectDesc.setVertexShader(m_vertShader.data()); + effectDesc.setFragmentShader(m_fragShader.data()); + return *m_scene->createEffect(effectDesc); + } + + SkinBinding* createSkinBinding() + { + std::vector inverseMats; + inverseMats.resize(2u); + + m_jointNodes[0]->getInverseModelMatrix(inverseMats[0]); + m_jointNodes[1]->getInverseModelMatrix(inverseMats[1]); + + return m_logicEngine.createSkinBinding(m_joints, inverseMats, *m_appearanceBinding, m_uniform, "skin"); + } + + // this VS is not capable of vertex skinning but that is not needed for the tests here + const std::string_view m_vertShader = R"( + #version 100 + + uniform highp float floatUniform; + uniform highp mat4 jointMat[2]; + attribute vec3 a_position; + + void main() + { + gl_Position = floatUniform * vec4(a_position, 1.0) * jointMat[1]; + })"; + + const std::string_view m_fragShader = R"( + #version 100 + void main(void) + { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + })"; + + std::vector m_jointNodes{ m_scene->createNode(), m_scene->createNode() }; + std::vector m_joints{ m_logicEngine.createRamsesNodeBinding(*m_jointNodes[0]), m_logicEngine.createRamsesNodeBinding(*m_jointNodes[1]) }; + + ramses::Appearance* m_appearance{ m_scene->createAppearance(createTestEffect()) }; + RamsesAppearanceBinding* m_appearanceBinding{ m_logicEngine.createRamsesAppearanceBinding(*m_appearance) }; + ramses::UniformInput m_uniform; + + SkinBinding* m_skin = nullptr; + }; + + TEST_F(ASkinBinding, RefersToGivenRamsesObjects) + { + EXPECT_EQ(m_appearanceBinding, &m_skin->getAppearanceBinding()); + const auto& skinConst = *m_skin; + EXPECT_EQ(m_appearanceBinding, &skinConst.getAppearanceBinding()); + const auto& skinImplConst = m_skin->m_skinBinding; + EXPECT_EQ(&m_appearanceBinding->m_appearanceBinding, &skinImplConst.getAppearanceBinding()); + + EXPECT_EQ(ramses::EDataType::Matrix44F, *m_skin->getAppearanceUniformInput().getDataType()); + EXPECT_EQ(2u, m_skin->getAppearanceUniformInput().getElementCount()); + } + + TEST_F(ASkinBinding, HasNoInputNorOutputs) + { + EXPECT_EQ(nullptr, m_skin->getInputs()); + EXPECT_EQ(nullptr, m_skin->getOutputs()); + } + + TEST_F(ASkinBinding, SetsBoundUniformFromInitialJoints) + { + EXPECT_TRUE(m_logicEngine.update()); + + // initially inverse binding mats are equal to the inverse transformation mats of joints + // so result should be identity matrices + const matrix44f expectedMat = { + 1.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, + 0.f, 0.f, 1.f, 0.f, + 0.f, 0.f, 0.f, 1.f + }; + + std::array uniformData{}; + m_appearance->getInputValue(m_uniform, 2u, uniformData.data()); + const matrix44f mat1 = uniformData[0]; + const matrix44f mat2 = uniformData[1]; + + for (glm::length_t i = 0u; i < 16; ++i) + EXPECT_NEAR(expectedMat[i/4][i%4], mat1[i/4][i%4], 1e-7f) << i; + + for (glm::length_t i = 0u; i < 16; ++i) + EXPECT_NEAR(expectedMat[i/4][i%4], mat2[i/4][i%4], 1e-7f) << i; + } + + TEST_F(ASkinBinding, UpdatesBoundUniformOnJointChange) + { + m_jointNodes[0]->setRotation({-1.f, -2.f, -3.f}); + m_jointNodes[1]->setTranslation({-1.f, -2.f, -3.f}); + EXPECT_TRUE(m_logicEngine.update()); + + const matrix44f expectedMat1 = { + 0.998f, -0.0523f, 0.0349f, 0.f, + 0.0529f, 0.9984f, -0.0174f, 0.f, + -0.0339f, 0.01925f, 0.9992f, 0.f, + -0.00209f, -0.00235f, 0.00227f, 1.f + }; + const matrix44f expectedMat2 = { + 1.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, + 0.f, 0.f, 1.f, 0.f, + -1.f, -2.f, -3.f, 1.f + }; + + std::array uniformData{}; + m_appearance->getInputValue(m_uniform, 2u, uniformData.data()); + const matrix44f mat1 = uniformData[0]; + const matrix44f mat2 = uniformData[1]; + + for (glm::length_t i = 0u; i < 16; ++i) + EXPECT_NEAR(expectedMat1[i/4][i%4], mat1[i/4][i%4], 1e-4f) << i; + + for (glm::length_t i = 0u; i < 16; ++i) + EXPECT_NEAR(expectedMat2[i/4][i%4], mat2[i/4][i%4], 1e-4f) << i; + } + + class ASkinBinding_SerializationLifecycle : public ASkinBinding + { + protected: + enum class ESerializationIssue + { + AllValid, + MissingName, + MissingJoints, + NumJointsMismatchMatrices, + UnresolvedJointNodeBinding, + UnresolvedAppearanceBinding, + InvalidUniform + }; + + std::unique_ptr deserializeSerializedDataWithIssue(ESerializationIssue issue) + { + { + const auto fbLogicObject = rlogic_serialization::CreateLogicObject(m_flatBufferBuilder, + (issue == ESerializationIssue::MissingName ? 0 : m_flatBufferBuilder.CreateString("name")), 1u, 0u, 0u); + + const std::vector jointIds{ 0u, 1u }; + + std::vector inverseBindMatData; + inverseBindMatData.resize((issue == ESerializationIssue::NumJointsMismatchMatrices ? 16u : 32u), 0.f); + + auto fbSkinBinding = rlogic_serialization::CreateSkinBinding(m_flatBufferBuilder, + fbLogicObject, + (issue == ESerializationIssue::MissingJoints ? 0u : m_flatBufferBuilder.CreateVector(jointIds)), + m_flatBufferBuilder.CreateVector(inverseBindMatData), + 2u, + (issue == ESerializationIssue::InvalidUniform ? m_flatBufferBuilder.CreateString("wrongUniform") : m_flatBufferBuilder.CreateString("jointMat"))); + m_flatBufferBuilder.Finish(fbSkinBinding); + } + + DeserializationMap deserializationMap; + deserializationMap.storeLogicObject(0u, m_joints[0]->m_nodeBinding); + if (issue != ESerializationIssue::UnresolvedJointNodeBinding) + deserializationMap.storeLogicObject(1u, m_joints[1]->m_nodeBinding); + if (issue != ESerializationIssue::UnresolvedAppearanceBinding) + deserializationMap.storeLogicObject(2u, m_appearanceBinding->m_appearanceBinding); + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + return SkinBindingImpl::Deserialize(serialized, m_errorReporting, deserializationMap); + } + + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + ErrorReporting m_errorReporting; + }; + + TEST_F(ASkinBinding_SerializationLifecycle, CanSerializeWithNoIssue) + { + EXPECT_TRUE(deserializeSerializedDataWithIssue(ASkinBinding_SerializationLifecycle::ESerializationIssue::AllValid)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + + TEST_F(ASkinBinding_SerializationLifecycle, ReportsSerializationError_MissingName) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ASkinBinding_SerializationLifecycle::ESerializationIssue::MissingName)); + ASSERT_EQ(2u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LogicObject base from serialized data: missing name!"); + EXPECT_EQ(m_errorReporting.getErrors()[1].message, "Fatal error during loading of SkinBinding from serialized data: missing name and/or ID!"); + } + + TEST_F(ASkinBinding_SerializationLifecycle, ReportsSerializationError_MissingJoints) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ASkinBinding_SerializationLifecycle::ESerializationIssue::MissingJoints)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of SkinBinding from serialized data: missing or corrupted joints and/or inverse matrices data!"); + } + + TEST_F(ASkinBinding_SerializationLifecycle, ReportsSerializationError_NumJointsMismatchMatrices) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ASkinBinding_SerializationLifecycle::ESerializationIssue::NumJointsMismatchMatrices)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of SkinBinding from serialized data: missing or corrupted joints and/or inverse matrices data!"); + } + + TEST_F(ASkinBinding_SerializationLifecycle, ReportsSerializationError_UnresolvedJointNodeBinding) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ASkinBinding_SerializationLifecycle::ESerializationIssue::UnresolvedJointNodeBinding)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of SkinBinding from serialized data: could not resolve referenced node binding!"); + } + + TEST_F(ASkinBinding_SerializationLifecycle, ReportsSerializationError_UnresolvedAppearanceBinding) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ASkinBinding_SerializationLifecycle::ESerializationIssue::UnresolvedAppearanceBinding)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of SkinBinding from serialized data: could not resolve referenced appearance binding!"); + } + + TEST_F(ASkinBinding_SerializationLifecycle, ReportsSerializationError_InvalidUniform) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(ASkinBinding_SerializationLifecycle::ESerializationIssue::InvalidUniform)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of SkinBinding from serialized data: invalid or mismatching uniform input!"); + } + + TEST_F(ASkinBinding_SerializationLifecycle, FailsToLoadWhenNoSceneProvided) + { + { + createSkinBinding(); + ASSERT_TRUE(SaveToFileWithoutValidation(m_logicEngine, "binding.bin")); + } + + { + EXPECT_FALSE(m_logicEngine.loadFromFile("binding.bin")); + ASSERT_EQ(m_logicEngine.getErrors().size(), 1u); + EXPECT_EQ(m_logicEngine.getErrors()[0].message, "Fatal error during loading from file! File contains references to Ramses objects but no Ramses scene was provided!"); + } + } +} diff --git a/client/logic/unittests/api/TimerNodeTest.cpp b/client/logic/unittests/api/TimerNodeTest.cpp new file mode 100644 index 000000000..a6787ce08 --- /dev/null +++ b/client/logic/unittests/api/TimerNodeTest.cpp @@ -0,0 +1,260 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "WithTempDirectory.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/LuaScript.h" +#include "impl/TimerNodeImpl.h" +#include "impl/PropertyImpl.h" +#include "internals/ErrorReporting.h" +#include "internals/SerializationMap.h" +#include "internals/DeserializationMap.h" +#include "internals/TypeData.h" +#include "internals/EPropertySemantics.h" +#include "generated/TimerNodeGen.h" +#include "flatbuffers/flatbuffers.h" +#include +#include + +namespace ramses::internal +{ + class ATimerNode : public ::testing::Test + { + protected: + LogicEngine m_logicEngine{ ramses::EFeatureLevel_Latest }; + }; + + TEST_F(ATimerNode, IsCreated) + { + const auto timerNode = m_logicEngine.createTimerNode("timerNode"); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + ASSERT_NE(nullptr, timerNode); + EXPECT_EQ(timerNode, m_logicEngine.findByName("timerNode")); + + EXPECT_EQ("timerNode", timerNode->getName()); + } + + TEST_F(ATimerNode, IsDestroyed) + { + const auto timerNode = m_logicEngine.createTimerNode("timerNode"); + EXPECT_TRUE(m_logicEngine.destroy(*timerNode)); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + EXPECT_EQ(nullptr, m_logicEngine.findByName("timerNode")); + } + + TEST_F(ATimerNode, FailsToBeDestroyedIfFromOtherLogicInstance) + { + auto timerNode = m_logicEngine.createTimerNode("timerNode"); + + LogicEngine otherEngine{ m_logicEngine.getFeatureLevel() }; + EXPECT_FALSE(otherEngine.destroy(*timerNode)); + ASSERT_FALSE(otherEngine.getErrors().empty()); + EXPECT_EQ("Failed to destroy object 'timerNode [Id=1]', cannot find it in this LogicEngine instance.", otherEngine.getErrors().front().message); + } + + TEST_F(ATimerNode, ChangesName) + { + const auto timerNode = m_logicEngine.createTimerNode("timerNode"); + + timerNode->setName("an"); + EXPECT_EQ("an", timerNode->getName()); + EXPECT_EQ(timerNode, m_logicEngine.findByName("an")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + } + + TEST_F(ATimerNode, HasPropertiesAfterCreation) + { + const auto timerNode = m_logicEngine.createTimerNode("timerNode"); + + const auto rootIn = timerNode->getInputs(); + EXPECT_EQ("", rootIn->getName()); + ASSERT_EQ(1u, rootIn->getChildCount()); + EXPECT_EQ("ticker_us", rootIn->getChild(0u)->getName()); + EXPECT_EQ(EPropertyType::Int64, rootIn->getChild(0u)->getType()); + + const auto rootOut = timerNode->getOutputs(); + EXPECT_EQ("", rootOut->getName()); + ASSERT_EQ(1u, rootOut->getChildCount()); + EXPECT_EQ("ticker_us", rootOut->getChild(0u)->getName()); + EXPECT_EQ(EPropertyType::Int64, rootOut->getChild(0u)->getType()); + } + + TEST_F(ATimerNode, OutputsTicker_userTicker) + { + const auto timerNode = m_logicEngine.createTimerNode("timerNode"); + + EXPECT_TRUE(timerNode->getInputs()->getChild(0u)->set(1000000)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(1000000, *timerNode->getOutputs()->getChild(0u)->get()); + + // +0.5 second + EXPECT_TRUE(timerNode->getInputs()->getChild(0u)->set(1500000)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(1500000, *timerNode->getOutputs()->getChild(0u)->get()); + + // no change + EXPECT_TRUE(timerNode->getInputs()->getChild(0u)->set(1500000)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(1500000, *timerNode->getOutputs()->getChild(0u)->get()); + + // +10 second + EXPECT_TRUE(timerNode->getInputs()->getChild(0u)->set(11500000)); + EXPECT_TRUE(m_logicEngine.update()); + EXPECT_EQ(11500000, *timerNode->getOutputs()->getChild(0u)->get()); + } + + TEST_F(ATimerNode, OutputsTicker_autoTicker) + { + const auto timerNode = m_logicEngine.createTimerNode("timerNode"); + + // auto ticker is enabled if ticker input == 0 + EXPECT_TRUE(timerNode->getInputs()->getChild(0u)->set(0)); + + EXPECT_TRUE(m_logicEngine.update()); + const auto initialTicker = *timerNode->getOutputs()->getChild(0u)->get(); + EXPECT_TRUE(initialTicker > 0); + + auto ticker = initialTicker; + for (int i = 0; i < 10; ++i) + { + EXPECT_TRUE(m_logicEngine.update()); + const auto nextTicker = *timerNode->getOutputs()->getChild(0u)->get(); + EXPECT_TRUE(nextTicker >= ticker); + ticker = nextTicker; + std::this_thread::sleep_for(std::chrono::milliseconds{ 1 }); + } + + // check that auto ticker is actually progressing + EXPECT_GT(ticker, initialTicker); + } + + TEST_F(ATimerNode, CanBeSerializedAndDeserialized) + { + WithTempDirectory tempDir; + { + LogicEngine otherEngine{ m_logicEngine.getFeatureLevel() }; + otherEngine.createTimerNode("timerNode"); + SaveFileConfig configNoValidation; + configNoValidation.setValidationEnabled(false); + ASSERT_TRUE(otherEngine.saveToFile("logic_timerNode.bin", configNoValidation)); + } + + ASSERT_TRUE(m_logicEngine.loadFromFile("logic_timerNode.bin")); + EXPECT_TRUE(m_logicEngine.getErrors().empty()); + + EXPECT_EQ(1u, m_logicEngine.getCollection().size()); + const auto timerNode = m_logicEngine.findByName("timerNode"); + ASSERT_TRUE(timerNode); + EXPECT_EQ("timerNode", timerNode->getName()); + + const auto rootIn = timerNode->getInputs(); + EXPECT_EQ("", rootIn->getName()); + ASSERT_EQ(1u, rootIn->getChildCount()); + EXPECT_EQ("ticker_us", rootIn->getChild(0u)->getName()); + EXPECT_EQ(EPropertyType::Int64, rootIn->getChild(0u)->getType()); + + const auto rootOut = timerNode->getOutputs(); + EXPECT_EQ("", rootOut->getName()); + ASSERT_EQ(1u, rootOut->getChildCount()); + EXPECT_EQ("ticker_us", rootOut->getChild(0u)->getName()); + EXPECT_EQ(EPropertyType::Int64, rootOut->getChild(0u)->getType()); + } + + class ATimerNode_SerializationLifecycle : public ATimerNode + { + protected: + enum class ESerializationIssue + { + AllValid, + NameMissing, + IdMissing, + RootInMissing, + RootOutMissing, + PropertyInMissing, + PropertyOutMissing, + PropertyInWrongName, + PropertyOutWrongName + }; + + std::unique_ptr deserializeSerializedDataWithIssue(ESerializationIssue issue) + { + flatbuffers::FlatBufferBuilder flatBufferBuilder; + SerializationMap serializationMap; + DeserializationMap deserializationMap; + + { + HierarchicalTypeData inputs = MakeStruct("", {}); + if (issue == ESerializationIssue::PropertyInWrongName) + { + inputs.children.push_back(MakeType("wrongInput", EPropertyType::Int64)); + } + else if (issue != ESerializationIssue::PropertyInMissing) + { + inputs.children.push_back(MakeType("ticker_us", EPropertyType::Int64)); + } + auto inputsImpl = std::make_unique(std::move(inputs), EPropertySemantics::ScriptInput); + + HierarchicalTypeData outputs = MakeStruct("", {}); + if (issue == ESerializationIssue::PropertyOutWrongName) + { + outputs.children.push_back(MakeType("wrongOutput", EPropertyType::Int64)); + } + else if (issue != ESerializationIssue::PropertyOutMissing) + { + outputs.children.push_back(MakeType("ticker_us", EPropertyType::Int64)); + } + auto outputsImpl = std::make_unique(std::move(outputs), EPropertySemantics::ScriptOutput); + + const auto timerNodeFB = rlogic_serialization::CreateTimerNode( + flatBufferBuilder, + rlogic_serialization::CreateLogicObject(flatBufferBuilder, + issue == ESerializationIssue::NameMissing ? 0 : flatBufferBuilder.CreateString("timerNode"), + issue == ESerializationIssue::IdMissing ? 0 : 1u), + issue == ESerializationIssue::RootInMissing ? 0 : PropertyImpl::Serialize(*inputsImpl, flatBufferBuilder, serializationMap), + issue == ESerializationIssue::RootOutMissing ? 0 : PropertyImpl::Serialize(*outputsImpl, flatBufferBuilder, serializationMap) + ); + + flatBufferBuilder.Finish(timerNodeFB); + } + + const auto& serialized = *flatbuffers::GetRoot(flatBufferBuilder.GetBufferPointer()); + return TimerNodeImpl::Deserialize(serialized, m_errorReporting, deserializationMap); + } + + ErrorReporting m_errorReporting; + }; + + TEST_F(ATimerNode_SerializationLifecycle, FailsDeserializationIfEssentialDataMissing) + { + EXPECT_TRUE(deserializeSerializedDataWithIssue(ATimerNode_SerializationLifecycle::ESerializationIssue::AllValid)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + + for (const auto issue : { ESerializationIssue::NameMissing, ESerializationIssue::IdMissing, ESerializationIssue::RootInMissing, ESerializationIssue::RootOutMissing }) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(issue)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of TimerNode from serialized data: missing name, id or in/out property data!", m_errorReporting.getErrors().back().message); + m_errorReporting.clear(); + } + } + + TEST_F(ATimerNode_SerializationLifecycle, FailsDeserializationIfInvalidOrMissingProperties) + { + for (const auto issue : { ESerializationIssue::PropertyInMissing, ESerializationIssue::PropertyOutMissing, ESerializationIssue::PropertyInWrongName, ESerializationIssue::PropertyOutWrongName }) + { + EXPECT_FALSE(deserializeSerializedDataWithIssue(issue)); + ASSERT_FALSE(m_errorReporting.getErrors().empty()); + EXPECT_EQ("Fatal error during loading of TimerNode 'timerNode': missing or invalid properties!", m_errorReporting.getErrors().front().message); + m_errorReporting.clear(); + } + } +} diff --git a/client/logic/unittests/internal/ApiObjectsTest.cpp b/client/logic/unittests/internal/ApiObjectsTest.cpp new file mode 100644 index 000000000..df760c276 --- /dev/null +++ b/client/logic/unittests/internal/ApiObjectsTest.cpp @@ -0,0 +1,2494 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" + +#include "generated/LogicEngineGen.h" + +#include "internals/ApiObjects.h" +#include "internals/SolState.h" +#include "internals/ErrorReporting.h" + +#include "impl/LogicEngineImpl.h" +#include "impl/PropertyImpl.h" +#include "impl/LuaModuleImpl.h" +#include "impl/LuaScriptImpl.h" +#include "impl/LuaInterfaceImpl.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "impl/RamsesAppearanceBindingImpl.h" +#include "impl/RamsesCameraBindingImpl.h" +#include "impl/RamsesRenderPassBindingImpl.h" +#include "impl/RamsesRenderGroupBindingImpl.h" +#include "impl/RamsesMeshNodeBindingImpl.h" +#include "impl/DataArrayImpl.h" +#include "impl/AnchorPointImpl.h" +#include "impl/SkinBindingImpl.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/LuaModule.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesRenderGroupBindingElements.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/AnchorPoint.h" +#include "ramses-logic/SkinBinding.h" +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/RenderPass.h" +#include "RamsesTestUtils.h" +#include "LogTestUtils.h" +#include "SerializationTestUtils.h" +#include "RamsesObjectResolverMock.h" +#include "FeatureLevelTestValues.h" + +namespace ramses::internal +{ + class AnApiObjects : public ::testing::TestWithParam + { + protected: + AnApiObjects() + { + m_renderGroup->addMeshNode(*m_meshNode); + } + + ErrorReporting m_errorReporting; + ApiObjects m_apiObjects{ GetParam() }; + flatbuffers::FlatBufferBuilder m_flatBufferBuilder; + SerializationTestUtils m_testUtils{ m_flatBufferBuilder }; + ::testing::StrictMock m_resolverMock; + + RamsesTestSetup m_ramses; + ramses::Scene* m_scene = { m_ramses.createScene() }; + ramses::Node* m_node = { m_scene->createNode() }; + ramses::PerspectiveCamera* m_camera = { m_scene->createPerspectiveCamera() }; + ramses::Appearance* m_appearance = { &RamsesTestSetup::CreateTrivialTestAppearance(*m_scene) }; + ramses::RenderPass* m_renderPass = { m_scene->createRenderPass() }; + ramses::RenderGroup* m_renderGroup = { m_scene->createRenderGroup() }; + ramses::MeshNode* m_meshNode = { m_scene->createMeshNode("meshNode") }; + + const std::string_view m_moduleSrc = R"( + local mymath = {} + return mymath + )"; + + const std::string_view m_valid_empty_script = R"( + function interface(IN,OUT) + end + function run(IN,OUT) + end + )"; + + const std::string_view m_valid_empty_interface = R"( + function interface(IN,OUT) + end + )"; + + LuaScript* createScript() + { + return createScript(m_apiObjects, m_valid_empty_script); + } + + LuaScript* createScript(ApiObjects& apiObjects, std::string_view source) + { + auto script = apiObjects.createLuaScript(source, {}, "script", m_errorReporting); + EXPECT_NE(nullptr, script); + return script; + } + + LuaInterface* createInterface() + { + return createInterface(m_apiObjects); + } + + LuaInterface* createInterface(ApiObjects& apiObjects) + { + auto intf = apiObjects.createLuaInterface(m_valid_empty_interface, {}, "intf", m_errorReporting, true); + EXPECT_NE(nullptr, intf); + return intf; + } + + AnchorPoint* createAnchorPoint() + { + const auto node = m_apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "node"); + const auto camera = m_apiObjects.createRamsesCameraBinding(*m_camera, true, "camera"); + EXPECT_TRUE(node && camera); + return m_apiObjects.createAnchorPoint(node->m_nodeBinding, camera->m_cameraBinding, "anchor"); + } + + RamsesRenderGroupBinding* createRenderGroupBinding() + { + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*m_meshNode, "mesh")); + return m_apiObjects.createRamsesRenderGroupBinding(*m_renderGroup, elements, "renderGroupBinding"); + } + + static SkinBinding* createSkinBinding(const RamsesNodeBinding& joint, const RamsesAppearanceBinding& appearance, ApiObjects& apiObjects) + { + ramses::UniformInput uniform; + appearance.getRamsesAppearance().getEffect().findUniformInput("jointMat", uniform); + EXPECT_TRUE(uniform.isValid()); + return apiObjects.createSkinBinding({ &joint.m_nodeBinding }, { matrix44f{ 0.f } }, appearance.m_appearanceBinding, uniform, "skin"); + } + + SkinBinding* createSkinBinding(ApiObjects& apiObjects) + { + const auto node = apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodeForSkin"); + const auto appearance = apiObjects.createRamsesAppearanceBinding(*m_appearance, "appearanceForSkin"); + return createSkinBinding(*node, *appearance, apiObjects); + } + + // Silence logs, unless explicitly enabled, to reduce spam and speed up tests + ScopedLogContextLevel m_silenceLogs{ ELogLevel::Off }; + + size_t m_emptySerializedSizeTotal{164u}; + }; + + + INSTANTIATE_TEST_SUITE_P( + AnApiObjectsTests, + AnApiObjects, + ramses::internal::GetFeatureLevelTestValues()); + + TEST_P(AnApiObjects, CreatesScriptFromValidLuaWithoutErrors) + { + const LuaScript* script = createScript(); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_EQ(script, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(script, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, DestroysScriptWithoutErrors) + { + LuaScript* script = createScript(); + ASSERT_TRUE(m_apiObjects.destroy(*script, m_errorReporting)); + EXPECT_TRUE(m_apiObjects.getApiObjectOwningContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + } + + TEST_P(AnApiObjects, ProducesErrorsWhenDestroyingScriptFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + LuaScript* script = createScript(otherInstance, m_valid_empty_script); + EXPECT_EQ(script, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(script, otherInstance.getApiObjectContainer().back()); + ASSERT_FALSE(m_apiObjects.destroy(*script, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'script [Id=1]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, script); + + // Did not affect existence in otherInstance! + EXPECT_EQ(script, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(script, otherInstance.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, CreatesInterfaceFromValidLuaWithoutErrors) + { + const LuaInterface* intf = createInterface(); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_EQ(intf, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(intf, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, DestroysInterfaceWithoutErrors) + { + LuaInterface* intf = createInterface(); + ASSERT_TRUE(m_apiObjects.destroy(*intf, m_errorReporting)); + EXPECT_TRUE(m_apiObjects.getApiObjectOwningContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + } + + TEST_P(AnApiObjects, ProducesErrorsWhenDestroyingInterfaceFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + LuaInterface* intf = createInterface(otherInstance); + EXPECT_EQ(intf, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(intf, otherInstance.getApiObjectContainer().back()); + ASSERT_FALSE(m_apiObjects.destroy(*intf, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'intf [Id=1]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, intf); + + // Did not affect existence in otherInstance! + EXPECT_EQ(intf, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(intf, otherInstance.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, CreatesLuaModule) + { + auto module = m_apiObjects.createLuaModule(m_moduleSrc, {}, "module", m_errorReporting); + EXPECT_NE(nullptr, module); + + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + ASSERT_EQ(1u, m_apiObjects.getApiObjectContainer().size()); + EXPECT_EQ(1u, m_apiObjects.getApiObjectContainer().size()); + EXPECT_EQ(1u, m_apiObjects.getApiObjectOwningContainer().size()); + EXPECT_EQ(module, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(module, m_apiObjects.getApiObjectContainer().back()); + EXPECT_EQ(module, m_apiObjects.getApiObjectContainer().front()); + } + + TEST_P(AnApiObjects, CreatesRamsesNodeBindingWithoutErrors) + { + RamsesNodeBinding* ramsesNodeBinding = m_apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + EXPECT_NE(nullptr, ramsesNodeBinding); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_EQ(ramsesNodeBinding, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(ramsesNodeBinding, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, DestroysRamsesNodeBindingWithoutErrors) + { + RamsesNodeBinding* ramsesNodeBinding = m_apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + ASSERT_NE(nullptr, ramsesNodeBinding); + m_apiObjects.destroy(*ramsesNodeBinding, m_errorReporting); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectOwningContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + } + + TEST_P(AnApiObjects, ProducesErrorsWhenDestroyingRamsesNodeBindingFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + RamsesNodeBinding* ramsesNodeBinding = otherInstance.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "NodeBinding"); + ASSERT_TRUE(ramsesNodeBinding); + EXPECT_EQ(ramsesNodeBinding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(ramsesNodeBinding, otherInstance.getApiObjectContainer().back()); + ASSERT_FALSE(m_apiObjects.destroy(*ramsesNodeBinding, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'NodeBinding [Id=1]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, ramsesNodeBinding); + + // Did not affect existence in otherInstance! + EXPECT_EQ(ramsesNodeBinding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(ramsesNodeBinding, otherInstance.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, CreatesRamsesCameraBindingWithoutErrors) + { + RamsesCameraBinding* ramsesCameraBinding = m_apiObjects.createRamsesCameraBinding(*m_camera, false, "CameraBinding"); + EXPECT_NE(nullptr, ramsesCameraBinding); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_EQ(ramsesCameraBinding, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(ramsesCameraBinding, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, DestroysRamsesCameraBindingWithoutErrors) + { + RamsesCameraBinding* ramsesCameraBinding = m_apiObjects.createRamsesCameraBinding(*m_camera, true, "CameraBinding"); + ASSERT_NE(nullptr, ramsesCameraBinding); + m_apiObjects.destroy(*ramsesCameraBinding, m_errorReporting); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectOwningContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + } + + TEST_P(AnApiObjects, ProducesErrorsWhenDestroyingRamsesCameraBindingFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + RamsesCameraBinding* ramsesCameraBinding = otherInstance.createRamsesCameraBinding(*m_camera, true, "CameraBinding"); + ASSERT_TRUE(ramsesCameraBinding); + EXPECT_EQ(ramsesCameraBinding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(ramsesCameraBinding, otherInstance.getApiObjectContainer().back()); + ASSERT_FALSE(m_apiObjects.destroy(*ramsesCameraBinding, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'CameraBinding [Id=1]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, ramsesCameraBinding); + + // Did not affect existence in otherInstance! + EXPECT_EQ(ramsesCameraBinding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(ramsesCameraBinding, otherInstance.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, CreatesRamsesRenderPassBindingWithoutErrors) + { + RamsesRenderPassBinding* binding = m_apiObjects.createRamsesRenderPassBinding(*m_renderPass, "RenderPassBinding"); + EXPECT_NE(nullptr, binding); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_EQ(binding, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, DestroysRamsesRenderPassBindingWithoutErrors) + { + RamsesRenderPassBinding* binding = m_apiObjects.createRamsesRenderPassBinding(*m_renderPass, "RenderPassBinding"); + ASSERT_NE(nullptr, binding); + m_apiObjects.destroy(*binding, m_errorReporting); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectOwningContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + } + + TEST_P(AnApiObjects, ProducesErrorsWhenDestroyingRamsesRenderPassBindingFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + RamsesRenderPassBinding* binding = otherInstance.createRamsesRenderPassBinding(*m_renderPass, "RenderPassBinding"); + ASSERT_TRUE(binding); + EXPECT_EQ(binding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, otherInstance.getApiObjectContainer().back()); + ASSERT_FALSE(m_apiObjects.destroy(*binding, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'RenderPassBinding [Id=1]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, binding); + + // Did not affect existence in otherInstance! + EXPECT_EQ(binding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, otherInstance.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, CreatesRamsesRenderGroupBindingWithoutErrors) + { + const auto binding = createRenderGroupBinding(); + EXPECT_NE(nullptr, binding); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_EQ(binding, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, DestroysRamsesRenderGroupBindingWithoutErrors) + { + auto binding = createRenderGroupBinding(); + ASSERT_NE(nullptr, binding); + m_apiObjects.destroy(*binding, m_errorReporting); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectOwningContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + } + + TEST_P(AnApiObjects, ProducesErrorsWhenDestroyingRamsesRenderGroupBindingFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*m_meshNode)); + auto binding = otherInstance.createRamsesRenderGroupBinding(*m_renderGroup, elements, "RenderGroupBinding"); + ASSERT_TRUE(binding); + EXPECT_EQ(binding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, otherInstance.getApiObjectContainer().back()); + ASSERT_FALSE(m_apiObjects.destroy(*binding, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'RenderGroupBinding [Id=1]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, binding); + + // Did not affect existence in otherInstance! + EXPECT_EQ(binding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, otherInstance.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, CreatesRamsesMeshNodeBindingWithoutErrors) + { + const auto binding = m_apiObjects.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + EXPECT_NE(nullptr, binding); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_EQ(binding, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, DestroysRamsesMeshNodeBindingWithoutErrors) + { + auto binding = m_apiObjects.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + ASSERT_NE(nullptr, binding); + m_apiObjects.destroy(*binding, m_errorReporting); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectOwningContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + } + + TEST_P(AnApiObjects, ProducesErrorsWhenDestroyingRamsesMeshNodeBindingFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + auto binding = otherInstance.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + ASSERT_TRUE(binding); + EXPECT_EQ(binding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, otherInstance.getApiObjectContainer().back()); + EXPECT_FALSE(m_apiObjects.destroy(*binding, m_errorReporting)); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'mb [Id=1]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, binding); + + // Did not affect existence in otherInstance! + EXPECT_EQ(binding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, otherInstance.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, CreatesRamsesAppearanceBindingWithoutErrors) + { + RamsesAppearanceBinding* binding = m_apiObjects.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + EXPECT_NE(nullptr, binding); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_EQ(binding, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, DestroysRamsesAppearanceBindingWithoutErrors) + { + RamsesAppearanceBinding* binding = m_apiObjects.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + ASSERT_TRUE(binding); + ASSERT_TRUE(m_apiObjects.destroy(*binding, m_errorReporting)); + EXPECT_TRUE(m_apiObjects.getApiObjectOwningContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + } + + TEST_P(AnApiObjects, ProducesErrorsWhenDestroyingRamsesAppearanceBindingFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + RamsesAppearanceBinding* binding = otherInstance.createRamsesAppearanceBinding(*m_appearance, "AppearanceBinding"); + ASSERT_TRUE(binding); + EXPECT_EQ(binding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, otherInstance.getApiObjectContainer().back()); + ASSERT_FALSE(m_apiObjects.destroy(*binding, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'AppearanceBinding [Id=1]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, binding); + + // Did not affect existence in otherInstance! + EXPECT_EQ(binding, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(binding, otherInstance.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, CreatesDataArray) + { + const std::vector data{ 1.f, 2.f, 3.f }; + auto dataArray = m_apiObjects.createDataArray(data, "data"); + EXPECT_NE(nullptr, dataArray); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + ASSERT_EQ(1u, m_apiObjects.getApiObjectContainer().size()); + EXPECT_EQ(dataArray, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(dataArray, m_apiObjects.getApiObjectContainer().back()); + EXPECT_EQ(EPropertyType::Float, m_apiObjects.getApiObjectContainer().front()->getDataType()); + ASSERT_NE(nullptr, m_apiObjects.getApiObjectContainer().front()->getData()); + EXPECT_EQ(data, *m_apiObjects.getApiObjectContainer().front()->getData()); + } + + TEST_P(AnApiObjects, DestroysDataArray) + { + auto dataArray = m_apiObjects.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data"); + EXPECT_TRUE(m_apiObjects.destroy(*dataArray, m_errorReporting)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectOwningContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + } + + TEST_P(AnApiObjects, FailsToDestroysDataArrayIfUsedInAnimationNode) + { + auto dataArray1 = m_apiObjects.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data1"); + auto dataArray2 = m_apiObjects.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data2"); + auto dataArray3 = m_apiObjects.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data3"); + auto dataArray4 = m_apiObjects.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data4"); + + AnimationNodeConfig config; + EXPECT_TRUE(config.addChannel({ "channel1", dataArray1, dataArray2 })); + EXPECT_TRUE(config.addChannel({ "channel2", dataArray1, dataArray2, EInterpolationType::Cubic, dataArray3, dataArray4 })); + auto animNode = m_apiObjects.createAnimationNode(*config.m_impl, "animNode"); + + EXPECT_FALSE(m_apiObjects.destroy(*dataArray1, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy data array 'data1', it is used in animation node 'animNode' channel 'channel1'"); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, dataArray1); + m_errorReporting.clear(); + + EXPECT_FALSE(m_apiObjects.destroy(*dataArray2, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy data array 'data2', it is used in animation node 'animNode' channel 'channel1'"); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, dataArray2); + m_errorReporting.clear(); + + EXPECT_FALSE(m_apiObjects.destroy(*dataArray3, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy data array 'data3', it is used in animation node 'animNode' channel 'channel2'"); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, dataArray3); + m_errorReporting.clear(); + + EXPECT_FALSE(m_apiObjects.destroy(*dataArray4, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy data array 'data4', it is used in animation node 'animNode' channel 'channel2'"); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, dataArray4); + m_errorReporting.clear(); + + // succeeds after destroying animation node + EXPECT_TRUE(m_apiObjects.destroy(*animNode, m_errorReporting)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.destroy(*dataArray1, m_errorReporting)); + EXPECT_TRUE(m_apiObjects.destroy(*dataArray2, m_errorReporting)); + EXPECT_TRUE(m_apiObjects.destroy(*dataArray3, m_errorReporting)); + EXPECT_TRUE(m_apiObjects.destroy(*dataArray4, m_errorReporting)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + + TEST_P(AnApiObjects, FailsToDestroyDataArrayFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + auto dataArray = otherInstance.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data"); + ASSERT_NE(nullptr, dataArray); + EXPECT_EQ(dataArray, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(dataArray, otherInstance.getApiObjectContainer().back()); + EXPECT_FALSE(m_apiObjects.destroy(*dataArray, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'data [Id=1]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, dataArray); + + // Did not affect existence in otherInstance! + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_EQ(dataArray, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(dataArray, otherInstance.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, CreatesAnimationNode) + { + auto dataArray = m_apiObjects.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data"); + ASSERT_NE(nullptr, dataArray); + AnimationNodeConfig config; + EXPECT_TRUE(config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear })); + auto animNode = m_apiObjects.createAnimationNode(*config.m_impl, "animNode"); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_EQ(animNode, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(animNode, m_apiObjects.getApiObjectContainer().back()); + EXPECT_EQ(2u, m_apiObjects.getApiObjectContainer().size()); + EXPECT_EQ(2u, m_apiObjects.getApiObjectOwningContainer().size()); + ASSERT_EQ(1u, m_apiObjects.getApiObjectContainer().size()); + EXPECT_EQ(animNode, m_apiObjects.getApiObjectContainer().front()); + } + + TEST_P(AnApiObjects, DestroysAnimationNode) + { + auto dataArray = m_apiObjects.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data"); + ASSERT_NE(nullptr, dataArray); + AnimationNodeConfig config; + EXPECT_TRUE(config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear })); + auto animNode = m_apiObjects.createAnimationNode(*config.m_impl, "animNode"); + EXPECT_TRUE(m_apiObjects.destroy(*animNode, m_errorReporting)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + // did not affect data array + EXPECT_TRUE(!m_apiObjects.getApiObjectContainer().empty()); + EXPECT_EQ(1u, m_apiObjects.getApiObjectOwningContainer().size()); + EXPECT_EQ(1u, m_apiObjects.getApiObjectContainer().size()); + } + + TEST_P(AnApiObjects, FailsToDestroyAnimationNodeFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + auto dataArray = otherInstance.createDataArray(std::vector{ 1.f, 2.f, 3.f }, "data"); + ASSERT_NE(nullptr, dataArray); + AnimationNodeConfig config; + EXPECT_TRUE(config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear })); + auto animNode = otherInstance.createAnimationNode(*config.m_impl, "animNode"); + EXPECT_EQ(animNode, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(animNode, otherInstance.getApiObjectContainer().back()); + EXPECT_FALSE(m_apiObjects.destroy(*animNode, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'animNode [Id=2]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, animNode); + + // Did not affect existence in otherInstance! + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_EQ(animNode, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(animNode, otherInstance.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, CreatesTimerNode) + { + auto timerNode = m_apiObjects.createTimerNode("timerNode"); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + ASSERT_EQ(1u, m_apiObjects.getApiObjectOwningContainer().size()); + EXPECT_EQ(timerNode, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_THAT(m_apiObjects.getApiObjectContainer(), ::testing::ElementsAre(timerNode)); + EXPECT_THAT(m_apiObjects.getApiObjectContainer(), ::testing::ElementsAre(timerNode)); + } + + TEST_P(AnApiObjects, DestroysTimerNode) + { + auto timerNode = m_apiObjects.createTimerNode("timerNode"); + EXPECT_TRUE(m_apiObjects.destroy(*timerNode, m_errorReporting)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectOwningContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + } + + TEST_P(AnApiObjects, FailsToDestroyTimerNodeFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + auto timerNode = otherInstance.createTimerNode("timerNode"); + EXPECT_FALSE(m_apiObjects.destroy(*timerNode, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'timerNode [Id=1]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, timerNode); + + // Did not affect existence in otherInstance! + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_EQ(timerNode, otherInstance.getApiObjectContainer().front()); + EXPECT_EQ(timerNode, otherInstance.getApiObjectOwningContainer().front().get()); + EXPECT_EQ(timerNode, otherInstance.getApiObjectContainer().front()); + } + + TEST_P(AnApiObjects, CreatesAnchorPointWithoutErrors) + { + AnchorPoint* anchor = createAnchorPoint(); + EXPECT_NE(nullptr, anchor); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_EQ(anchor, m_apiObjects.getApiObjectContainer().front()); + EXPECT_EQ(anchor, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(anchor, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, DestroysAnchorPointWithoutErrors) + { + AnchorPoint* anchor = createAnchorPoint(); + ASSERT_NE(nullptr, anchor); + m_apiObjects.destroy(*anchor, m_errorReporting); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_NE(anchor, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_NE(anchor, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, ProducesErrorsWhenDestroyingAnchorPointFromAnotherClassInstance) + { + ApiObjects otherInstance{ GetParam() }; + const auto node = otherInstance.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "node"); + const auto camera = otherInstance.createRamsesCameraBinding(*m_camera, true, "camera"); + AnchorPoint* anchor = otherInstance.createAnchorPoint(node->m_nodeBinding, camera->m_cameraBinding, "anchor"); + ASSERT_TRUE(anchor); + EXPECT_EQ(anchor, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(anchor, otherInstance.getApiObjectContainer().back()); + ASSERT_FALSE(m_apiObjects.destroy(*anchor, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy object 'anchor [Id=3]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, anchor); + + // Did not affect existence in otherInstance! + EXPECT_EQ(anchor, otherInstance.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(anchor, otherInstance.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, FailsToDestroyNodeOrCameraBindingIfUsedInAnchorPoint) + { + const auto node = m_apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "node"); + const auto camera = m_apiObjects.createRamsesCameraBinding(*m_camera, true, "camera"); + AnchorPoint* anchor = m_apiObjects.createAnchorPoint(node->m_nodeBinding, camera->m_cameraBinding, "anchor"); + + EXPECT_FALSE(m_apiObjects.destroy(*node, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy Ramses node binding 'node', it is used in anchor point 'anchor'"); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, node); + m_errorReporting.clear(); + + EXPECT_FALSE(m_apiObjects.destroy(*camera, m_errorReporting)); + EXPECT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy Ramses camera binding 'camera', it is used in anchor point 'anchor'"); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, camera); + m_errorReporting.clear(); + + // succeeds after destroying anchor point + EXPECT_TRUE(m_apiObjects.destroy(*anchor, m_errorReporting)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.destroy(*node, m_errorReporting)); + EXPECT_TRUE(m_apiObjects.destroy(*camera, m_errorReporting)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + + TEST_P(AnApiObjects, CreatesSkinBindingWithoutErrors) + { + auto skin = createSkinBinding(m_apiObjects); + ASSERT_NE(nullptr, skin); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_EQ(skin, m_apiObjects.getApiObjectContainer().front()); + EXPECT_EQ(skin, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_EQ(skin, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, DestroysSkinBindingWithoutErrors) + { + auto skin = createSkinBinding(m_apiObjects); + ASSERT_NE(nullptr, skin); + m_apiObjects.destroy(*skin, m_errorReporting); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_NE(skin, m_apiObjects.getApiObjectOwningContainer().back().get()); + EXPECT_NE(skin, m_apiObjects.getApiObjectContainer().back()); + } + + TEST_P(AnApiObjects, ProducesErrorsWhenDestroyingSkinBindingFromAnotherClassInstance) + { + auto skin = createSkinBinding(m_apiObjects); + ASSERT_NE(nullptr, skin); + + ApiObjects otherInstance{ GetParam() }; + + ErrorReporting errorReporting; + EXPECT_FALSE(otherInstance.destroy(*skin, errorReporting)); + ASSERT_EQ(errorReporting.getErrors().size(), 1u); + EXPECT_EQ(errorReporting.getErrors()[0].message, "Failed to destroy object 'skin [Id=3]', cannot find it in this LogicEngine instance."); + EXPECT_EQ(errorReporting.getErrors()[0].object, skin); + } + + TEST_P(AnApiObjects, FailsToDestroyNodeOrAppearanceBindingIfUsedInSkinBinding) + { + auto skin = createSkinBinding(m_apiObjects); + ASSERT_NE(nullptr, skin); + + const auto& nodes = m_apiObjects.getApiObjectContainer(); + const auto it = std::find_if(nodes.cbegin(), nodes.cend(), [](const auto& n) { return n->getName() == "nodeForSkin"; }); + ASSERT_TRUE(it != nodes.cend()); + const auto nodeUsedInSkin = *it; + + const auto& appearances = m_apiObjects.getApiObjectContainer(); + const auto it2 = std::find_if(appearances.cbegin(), appearances.cend(), [](const auto& a) { return a->getName() == "appearanceForSkin"; }); + ASSERT_TRUE(it2 != appearances.cend()); + const auto appearanceUsedInSkin = *it2; + + EXPECT_FALSE(m_apiObjects.destroy(*nodeUsedInSkin, m_errorReporting)); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy Ramses node binding 'nodeForSkin', it is used in skin binding 'skin'"); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, nodeUsedInSkin); + m_errorReporting.clear(); + + EXPECT_FALSE(m_apiObjects.destroy(*appearanceUsedInSkin, m_errorReporting)); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Failed to destroy Ramses appearance binding 'appearanceForSkin', it is used in skin binding 'skin'"); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, appearanceUsedInSkin); + m_errorReporting.clear(); + + // succeeds after destroying skin binding + EXPECT_TRUE(m_apiObjects.destroy(*skin, m_errorReporting)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + EXPECT_TRUE(m_apiObjects.destroy(*nodeUsedInSkin, m_errorReporting)); + EXPECT_TRUE(m_apiObjects.destroy(*appearanceUsedInSkin, m_errorReporting)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + } + + TEST_P(AnApiObjects, ProvidesEmptyCollections_WhenNothingWasCreated) + { + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectContainer().empty()); + EXPECT_TRUE(m_apiObjects.getApiObjectOwningContainer().empty()); + + const ApiObjects& apiObjectsConst = m_apiObjects; + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectContainer().empty()); + EXPECT_TRUE(apiObjectsConst.getApiObjectOwningContainer().empty()); + } + + TEST_P(AnApiObjects, ProvidesNonEmptyScriptCollection_WhenScriptsWereCreated) + { + const LuaScript* script = createScript(); + ApiObjectContainer& scripts = m_apiObjects.getApiObjectContainer(); + + EXPECT_EQ(*scripts.begin(), script); + EXPECT_EQ(*scripts.cbegin(), script); + + EXPECT_NE(scripts.begin(), scripts.end()); + EXPECT_NE(scripts.cbegin(), scripts.cend()); + + EXPECT_EQ(script, *scripts.begin()); + } + + TEST_P(AnApiObjects, ProvidesNonEmptyInterfaceCollection_WhenInterfacesWereCreated) + { + const LuaInterface* intf = createInterface(); + EXPECT_THAT(m_apiObjects.getApiObjectContainer(), ::testing::ElementsAre(intf)); + const auto& constApiObjects = m_apiObjects; + EXPECT_THAT(constApiObjects.getApiObjectContainer(), ::testing::ElementsAre(intf)); + } + + TEST_P(AnApiObjects, ProvidesNonEmptyNodeBindingsCollection_WhenNodeBindingsWereCreated) + { + RamsesNodeBinding* binding = m_apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + ApiObjectContainer& nodes = m_apiObjects.getApiObjectContainer(); + EXPECT_THAT(nodes, ::testing::ElementsAre(binding)); + } + + TEST_P(AnApiObjects, ProvidesNonEmptyAppearanceBindingsCollection_WhenAppearanceBindingsWereCreated) + { + RamsesAppearanceBinding* binding = m_apiObjects.createRamsesAppearanceBinding(*m_appearance, ""); + ApiObjectContainer& appearances = m_apiObjects.getApiObjectContainer(); + EXPECT_THAT(appearances, ::testing::ElementsAre(binding)); + } + + TEST_P(AnApiObjects, ProvidesNonEmptyCameraBindingsCollection_WhenCameraBindingsWereCreated) + { + RamsesCameraBinding* binding = m_apiObjects.createRamsesCameraBinding(*m_camera, true, ""); + ApiObjectContainer& cameras = m_apiObjects.getApiObjectContainer(); + EXPECT_THAT(cameras, ::testing::ElementsAre(binding)); + } + + TEST_P(AnApiObjects, ProvidesNonEmptyRenderPassBindingsCollection_WhenRenderPassBindingsWereCreated) + { + RamsesRenderPassBinding* binding = m_apiObjects.createRamsesRenderPassBinding(*m_renderPass, ""); + ApiObjectContainer& renderPasses = m_apiObjects.getApiObjectContainer(); + EXPECT_THAT(renderPasses, ::testing::ElementsAre(binding)); + } + + TEST_P(AnApiObjects, ProvidesNonEmptyRenderGroupBindingsCollection_WhenRenderGroupBindingsWereCreated) + { + const auto binding = createRenderGroupBinding(); + ApiObjectContainer& renderGroups = m_apiObjects.getApiObjectContainer(); + EXPECT_THAT(renderGroups, ::testing::ElementsAre(binding)); + } + + TEST_P(AnApiObjects, ProvidesNonEmptyAnchorPointsCollection_WhenAnchorPointsWereCreated) + { + AnchorPoint* anchor = createAnchorPoint(); + const auto& anchors = m_apiObjects.getApiObjectContainer(); + EXPECT_THAT(anchors, ::testing::ElementsAre(anchor)); + } + + TEST_P(AnApiObjects, ProvidesNonEmptySkinBindingsCollection_WhenSkinBindingsWereCreated) + { + const auto skin = createSkinBinding(m_apiObjects); + const auto& skins = m_apiObjects.getApiObjectContainer(); + EXPECT_THAT(skins, ::testing::ElementsAre(skin)); + } + + TEST_P(AnApiObjects, ProvidesNonEmptyOwningAndLogicObjectsCollection_WhenLogicObjectsWereCreated) + { + const ApiObjectContainer& logicObjects = m_apiObjects.getApiObjectContainer(); + const ApiObjectOwningContainer& ownedObjects = m_apiObjects.getApiObjectOwningContainer(); + + auto* luaModule = m_apiObjects.createLuaModule(m_moduleSrc, {}, "module", m_errorReporting); + auto* luaScript = createScript(m_apiObjects, m_valid_empty_script); + auto* luaInterface = createInterface(); + auto* nodeBinding = m_apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + auto* appearanceBinding = m_apiObjects.createRamsesAppearanceBinding(*m_appearance, ""); + auto* cameraBinding = m_apiObjects.createRamsesCameraBinding(*m_camera, true, ""); + auto* dataArray = m_apiObjects.createDataArray(std::vector{1.f, 2.f, 3.f}, "data"); + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear }); + auto* animationNode = m_apiObjects.createAnimationNode(*config.m_impl, "animNode"); + auto* timerNode = m_apiObjects.createTimerNode("timerNode"); + auto* renderPassBinding = m_apiObjects.createRamsesRenderPassBinding(*m_renderPass, ""); + auto* anchor = m_apiObjects.createAnchorPoint(nodeBinding->m_nodeBinding, cameraBinding->m_cameraBinding, "anchor"); + auto* renderGroupBinding = createRenderGroupBinding(); + auto* skin = createSkinBinding(*nodeBinding, *appearanceBinding, m_apiObjects); + auto* meshBinding = m_apiObjects.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + + // feature level 01 always present + std::vector expectedObjects{ luaModule, luaScript, luaInterface, nodeBinding, appearanceBinding, cameraBinding, dataArray, animationNode, + timerNode, renderPassBinding, anchor, renderGroupBinding, skin, meshBinding }; + + std::vector ownedLogicObjectsRawPointers; + std::transform(ownedObjects.cbegin(), ownedObjects.cend(), std::back_inserter(ownedLogicObjectsRawPointers), [](auto& obj) { return obj.get(); }); + EXPECT_EQ(logicObjects, expectedObjects); + EXPECT_EQ(ownedLogicObjectsRawPointers, expectedObjects); + + const ApiObjects& apiObjectsConst = m_apiObjects; + EXPECT_EQ(logicObjects, apiObjectsConst.getApiObjectContainer()); + } + + TEST_P(AnApiObjects, logicObjectsGetUniqueIds) + { + const auto* luaModule = m_apiObjects.createLuaModule(m_moduleSrc, {}, "module", m_errorReporting); + const auto* luaScript = createScript(m_apiObjects, m_valid_empty_script); + const auto* luaInterface = createInterface(); + const auto* nodeBinding = m_apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + const auto* appearanceBinding = m_apiObjects.createRamsesAppearanceBinding(*m_appearance, ""); + const auto* cameraBinding = m_apiObjects.createRamsesCameraBinding(*m_camera, true, ""); + const auto* dataArray = m_apiObjects.createDataArray(std::vector{1.f, 2.f, 3.f}, "data"); + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear }); + const auto* animationNode = m_apiObjects.createAnimationNode(*config.m_impl, "animNode"); + const auto* timerNode = m_apiObjects.createTimerNode("timerNode"); + const auto* renderPassBinding = m_apiObjects.createRamsesRenderPassBinding(*m_renderPass, ""); + const auto* anchor = createAnchorPoint(); + const auto* renderGroupBinding = createRenderGroupBinding(); + const auto* skin = createSkinBinding(*nodeBinding, *appearanceBinding, m_apiObjects); + const auto* meshBinding = m_apiObjects.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + + std::unordered_set logicObjectIds = + { + luaModule->getId(), + luaScript->getId(), + luaInterface->getId(), + nodeBinding->getId(), + appearanceBinding->getId(), + cameraBinding->getId(), + dataArray->getId(), + animationNode->getId(), + timerNode->getId(), + renderPassBinding->getId(), + anchor->getId(), + renderGroupBinding->getId(), + skin->getId(), + meshBinding->getId() + }; + EXPECT_EQ(14u, logicObjectIds.size()); + } + + TEST_P(AnApiObjects, canGetLogicObjectById) + { + const auto* luaModule = m_apiObjects.createLuaModule(m_moduleSrc, {}, "module", m_errorReporting); + const auto* luaScript = createScript(m_apiObjects, m_valid_empty_script); + const auto* nodeBinding = m_apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + const auto* appearanceBinding = m_apiObjects.createRamsesAppearanceBinding(*m_appearance, ""); + const auto* cameraBinding = m_apiObjects.createRamsesCameraBinding(*m_camera, true, ""); + const auto* dataArray = m_apiObjects.createDataArray(std::vector{1.f, 2.f, 3.f}, "data"); + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear }); + const auto* animationNode = m_apiObjects.createAnimationNode(*config.m_impl, "animNode"); + const auto* timerNode = m_apiObjects.createTimerNode("timerNode"); + const auto* luaInterface = createInterface(); + const auto* renderPassBinding = m_apiObjects.createRamsesRenderPassBinding(*m_renderPass, ""); + const auto* anchor = createAnchorPoint(); + const auto* renderGroupBinding = createRenderGroupBinding(); + const auto* skin = createSkinBinding(*nodeBinding, *appearanceBinding, m_apiObjects); + const auto* meshBinding = m_apiObjects.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + + EXPECT_EQ(luaModule->getId(), 1u); + EXPECT_EQ(luaScript->getId(), 2u); + EXPECT_EQ(nodeBinding->getId(), 3u); + EXPECT_EQ(appearanceBinding->getId(), 4u); + EXPECT_EQ(cameraBinding->getId(), 5u); + EXPECT_EQ(dataArray->getId(), 6u); + EXPECT_EQ(animationNode->getId(), 7u); + EXPECT_EQ(timerNode->getId(), 8u); + EXPECT_EQ(luaInterface->getId(), 9u); + EXPECT_EQ(renderPassBinding->getId(), 10u); + EXPECT_EQ(anchor->getId(), 13u); + EXPECT_EQ(renderGroupBinding->getId(), 14u); + EXPECT_EQ(skin->getId(), 15u); + EXPECT_EQ(meshBinding->getId(), 16u); + + EXPECT_EQ(m_apiObjects.getApiObjectById(1u), luaModule); + EXPECT_EQ(m_apiObjects.getApiObjectById(2u), luaScript); + EXPECT_EQ(m_apiObjects.getApiObjectById(3u), nodeBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(4u), appearanceBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(5u), cameraBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(6u), dataArray); + EXPECT_EQ(m_apiObjects.getApiObjectById(7u), animationNode); + EXPECT_EQ(m_apiObjects.getApiObjectById(8u), timerNode); + EXPECT_EQ(m_apiObjects.getApiObjectById(9u), luaInterface); + EXPECT_EQ(m_apiObjects.getApiObjectById(10u), renderPassBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(13u), anchor); + EXPECT_EQ(m_apiObjects.getApiObjectById(14u), renderGroupBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(15u), skin); + EXPECT_EQ(m_apiObjects.getApiObjectById(16u), meshBinding); + } + + TEST_P(AnApiObjects, logicObjectIdsAreRemovedFromIdMappingWhenObjectIsDestroyed) + { + const auto* luaModule = m_apiObjects.createLuaModule(m_moduleSrc, {}, "module", m_errorReporting); + auto* luaScript = createScript(m_apiObjects, m_valid_empty_script); + const auto* nodeBinding = m_apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + auto* appearanceBinding = m_apiObjects.createRamsesAppearanceBinding(*m_appearance, ""); + const auto* cameraBinding = m_apiObjects.createRamsesCameraBinding(*m_camera, true, ""); + const auto* dataArray = m_apiObjects.createDataArray(std::vector{1.f, 2.f, 3.f}, "data"); + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear }); + auto* animationNode = m_apiObjects.createAnimationNode(*config.m_impl, "animNode"); + const auto* timerNode = m_apiObjects.createTimerNode("timerNode"); + const auto* luaInterface = createInterface(); + const RamsesRenderPassBinding* renderPassBinding = m_apiObjects.createRamsesRenderPassBinding(*m_renderPass, ""); + const AnchorPoint* anchor = createAnchorPoint(); + const RamsesRenderGroupBinding* renderGroupBinding = createRenderGroupBinding(); + const SkinBinding* skinBinding = createSkinBinding(m_apiObjects); + const RamsesMeshNodeBinding* meshBinding = m_apiObjects.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + + EXPECT_EQ(m_apiObjects.getApiObjectById(1u), luaModule); + EXPECT_EQ(m_apiObjects.getApiObjectById(2u), luaScript); + EXPECT_EQ(m_apiObjects.getApiObjectById(3u), nodeBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(4u), appearanceBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(5u), cameraBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(6u), dataArray); + EXPECT_EQ(m_apiObjects.getApiObjectById(7u), animationNode); + EXPECT_EQ(m_apiObjects.getApiObjectById(8u), timerNode); + EXPECT_EQ(m_apiObjects.getApiObjectById(9u), luaInterface); + EXPECT_EQ(m_apiObjects.getApiObjectById(10u), renderPassBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(13u), anchor); + EXPECT_EQ(m_apiObjects.getApiObjectById(14u), renderGroupBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(17u), skinBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(18u), meshBinding); + + EXPECT_TRUE(m_apiObjects.destroy(*luaScript, m_errorReporting)); + EXPECT_TRUE(m_apiObjects.destroy(*appearanceBinding, m_errorReporting)); + EXPECT_TRUE(m_apiObjects.destroy(*animationNode, m_errorReporting)); + + EXPECT_EQ(m_apiObjects.getApiObjectById(1u), luaModule); + EXPECT_EQ(m_apiObjects.getApiObjectById(2u), nullptr); + EXPECT_EQ(m_apiObjects.getApiObjectById(3u), nodeBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(4u), nullptr); + EXPECT_EQ(m_apiObjects.getApiObjectById(5u), cameraBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(6u), dataArray); + EXPECT_EQ(m_apiObjects.getApiObjectById(7u), nullptr); + EXPECT_EQ(m_apiObjects.getApiObjectById(8u), timerNode); + EXPECT_EQ(m_apiObjects.getApiObjectById(9u), luaInterface); + EXPECT_EQ(m_apiObjects.getApiObjectById(10u), renderPassBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(13u), anchor); + EXPECT_EQ(m_apiObjects.getApiObjectById(14u), renderGroupBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(17u), skinBinding); + EXPECT_EQ(m_apiObjects.getApiObjectById(18u), meshBinding); + } + + TEST_P(AnApiObjects, logicObjectsGenerateIdentificationString) + { + const auto* luaModule = m_apiObjects.createLuaModule(m_moduleSrc, {}, "module", m_errorReporting); + auto* luaScript = createScript(m_apiObjects, m_valid_empty_script); + const auto* nodeBinding = m_apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodeBinding"); + auto* appearanceBinding = m_apiObjects.createRamsesAppearanceBinding(*m_appearance, "appearanceBinding"); + const auto* cameraBinding = m_apiObjects.createRamsesCameraBinding(*m_camera, true, "cameraBinding"); + const auto* dataArray = m_apiObjects.createDataArray(std::vector{1.f, 2.f, 3.f}, "dataArray"); + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear }); + auto* animationNode = m_apiObjects.createAnimationNode(*config.m_impl, "animNode"); + const auto* timerNode = m_apiObjects.createTimerNode("timerNode"); + const auto* luaInterface = createInterface(); + const auto* renderPassBinding = m_apiObjects.createRamsesRenderPassBinding(*m_renderPass, "renderPassBinding"); + const auto* anchor = createAnchorPoint(); + const auto renderGroupBinding = createRenderGroupBinding(); + const auto skin = createSkinBinding(m_apiObjects); + const auto* meshBinding = m_apiObjects.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + + EXPECT_EQ(luaModule->m_impl.getIdentificationString(), "module [Id=1]"); + EXPECT_EQ(luaScript->m_impl.getIdentificationString(), "script [Id=2]"); + EXPECT_EQ(nodeBinding->m_impl.getIdentificationString(), "nodeBinding [Id=3]"); + EXPECT_EQ(appearanceBinding->m_impl.getIdentificationString(), "appearanceBinding [Id=4]"); + EXPECT_EQ(cameraBinding->m_impl.getIdentificationString(), "cameraBinding [Id=5]"); + EXPECT_EQ(dataArray->m_impl.getIdentificationString(), "dataArray [Id=6]"); + EXPECT_EQ(animationNode->m_impl.getIdentificationString(), "animNode [Id=7]"); + EXPECT_EQ(timerNode->m_impl.getIdentificationString(), "timerNode [Id=8]"); + EXPECT_EQ(luaInterface->m_impl.getIdentificationString(), "intf [Id=9]"); + EXPECT_EQ(renderPassBinding->m_impl.getIdentificationString(), "renderPassBinding [Id=10]"); + EXPECT_EQ(anchor->m_impl.getIdentificationString(), "anchor [Id=13]"); + EXPECT_EQ(renderGroupBinding->m_impl.getIdentificationString(), "renderGroupBinding [Id=14]"); + EXPECT_EQ(skin->m_impl.getIdentificationString(), "skin [Id=17]"); + EXPECT_EQ(meshBinding->m_impl.getIdentificationString(), "mb [Id=18]"); + } + + TEST_P(AnApiObjects, logicObjectsGenerateIdentificationStringWithUserId) + { + auto* luaModule = m_apiObjects.createLuaModule(m_moduleSrc, {}, "module", m_errorReporting); + auto* luaScript = createScript(m_apiObjects, m_valid_empty_script); + auto* nodeBinding = m_apiObjects.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodeBinding"); + auto* appearanceBinding = m_apiObjects.createRamsesAppearanceBinding(*m_appearance, "appearanceBinding"); + auto* cameraBinding = m_apiObjects.createRamsesCameraBinding(*m_camera, true, "cameraBinding"); + RamsesRenderPassBinding* renderPassBinding = m_apiObjects.createRamsesRenderPassBinding(*m_renderPass, "renderPassBinding"); + auto* dataArray = m_apiObjects.createDataArray(std::vector{1.f, 2.f, 3.f}, "dataArray"); + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear }); + auto* animationNode = m_apiObjects.createAnimationNode(*config.m_impl, "animNode"); + auto* timerNode = m_apiObjects.createTimerNode("timerNode"); + auto* luaInterface = createInterface(); + AnchorPoint* anchor = createAnchorPoint(); + RamsesRenderGroupBinding* renderGroupBinding = createRenderGroupBinding(); + SkinBinding* skin = createSkinBinding(m_apiObjects); + RamsesMeshNodeBinding* meshBinding = m_apiObjects.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + + EXPECT_TRUE(luaModule->setUserId(1u, 2u)); + EXPECT_TRUE(luaScript->setUserId(3u, 4u)); + EXPECT_TRUE(nodeBinding->setUserId(5u, 6u)); + EXPECT_TRUE(appearanceBinding->setUserId(7u, 8u)); + EXPECT_TRUE(cameraBinding->setUserId(9u, 10u)); + EXPECT_TRUE(renderPassBinding->setUserId(11u, 12u)); + EXPECT_TRUE(dataArray->setUserId(13u, 14u)); + EXPECT_TRUE(animationNode->setUserId(15u, 16u)); + EXPECT_TRUE(timerNode->setUserId(17u, 18u)); + EXPECT_TRUE(luaInterface->setUserId(19u, 20u)); + EXPECT_TRUE(anchor->setUserId(21u, 22u)); + EXPECT_TRUE(renderGroupBinding->setUserId(23u, 24u)); + EXPECT_TRUE(skin->setUserId(25u, 26u)); + EXPECT_TRUE(meshBinding->setUserId(27u, 28u)); + + EXPECT_EQ(luaModule->m_impl.getIdentificationString(), "module [Id=1 UserId=00000000000000010000000000000002]"); + EXPECT_EQ(luaScript->m_impl.getIdentificationString(), "script [Id=2 UserId=00000000000000030000000000000004]"); + EXPECT_EQ(nodeBinding->m_impl.getIdentificationString(), "nodeBinding [Id=3 UserId=00000000000000050000000000000006]"); + EXPECT_EQ(appearanceBinding->m_impl.getIdentificationString(), "appearanceBinding [Id=4 UserId=00000000000000070000000000000008]"); + EXPECT_EQ(cameraBinding->m_impl.getIdentificationString(), "cameraBinding [Id=5 UserId=0000000000000009000000000000000A]"); + EXPECT_EQ(renderPassBinding->m_impl.getIdentificationString(), "renderPassBinding [Id=6 UserId=000000000000000B000000000000000C]"); + EXPECT_EQ(dataArray->m_impl.getIdentificationString(), "dataArray [Id=7 UserId=000000000000000D000000000000000E]"); + EXPECT_EQ(animationNode->m_impl.getIdentificationString(), "animNode [Id=8 UserId=000000000000000F0000000000000010]"); + EXPECT_EQ(timerNode->m_impl.getIdentificationString(), "timerNode [Id=9 UserId=00000000000000110000000000000012]"); + EXPECT_EQ(luaInterface->m_impl.getIdentificationString(), "intf [Id=10 UserId=00000000000000130000000000000014]"); + EXPECT_EQ(anchor->m_impl.getIdentificationString(), "anchor [Id=13 UserId=00000000000000150000000000000016]"); + EXPECT_EQ(renderGroupBinding->m_impl.getIdentificationString(), "renderGroupBinding [Id=14 UserId=00000000000000170000000000000018]"); + EXPECT_EQ(skin->m_impl.getIdentificationString(), "skin [Id=17 UserId=0000000000000019000000000000001A]"); + EXPECT_EQ(meshBinding->m_impl.getIdentificationString(), "mb [Id=18 UserId=000000000000001B000000000000001C]"); + } + + TEST_P(AnApiObjects, ValidatesThatAllLuaInterfaceOutputsAreLinked_GeneratesWarningsIfOutputsNotLinked) + { + LuaInterface* intf = m_apiObjects.createLuaInterface(R"( + function interface(IN,OUT) + + IN.param1 = Type:Int32() + IN.param2 = {a=Type:Float(), b=Type:Int32()} + + end + )", {}, "intf name", m_errorReporting, true); + ASSERT_NE(nullptr, intf); + + ValidationResults validationResults; + m_apiObjects.validateInterfaces(validationResults); + EXPECT_EQ(3u, validationResults.getWarnings().size()); + EXPECT_THAT(validationResults.getWarnings(), + ::testing::Each(::testing::Field(&WarningData::message, ::testing::HasSubstr("Interface [intf name] has unlinked output")))); + EXPECT_THAT(validationResults.getWarnings(), ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_P(AnApiObjects, ValidatesThatAllLuaInterfaceOutputsAreLinked_DoesNotGenerateWarningsIfAllOutputsLinked) + { + LuaInterface* intf = m_apiObjects.createLuaInterface(R"( + function interface(IN,OUT) + + IN.param1 = Type:Int32() + IN.param2 = {a=Type:Float(), b=Type:Int32()} + + end + )", {}, "intf name", m_errorReporting, true); + + LuaScript* inputsScript = m_apiObjects.createLuaScript(R"LUA_SCRIPT( + function interface(IN,OUT) + + IN.param1 = Type:Int32() + IN.param21 = Type:Float() + IN.param22 = Type:Int32() + + end + + function run(IN,OUT) + end + )LUA_SCRIPT", {}, "inputs script", m_errorReporting); + + const auto* output1 = intf->getOutputs()->getChild(0); + const auto* output21 = intf->getOutputs()->getChild(1)->getChild(0); + const auto* output22 = intf->getOutputs()->getChild(1)->getChild(1); + + m_apiObjects.getLogicNodeDependencies().link(*output1->m_impl, *inputsScript->getInputs()->getChild(0)->m_impl, false, m_errorReporting); + m_apiObjects.getLogicNodeDependencies().link(*output21->m_impl, *inputsScript->getInputs()->getChild(1)->m_impl, false, m_errorReporting); + m_apiObjects.getLogicNodeDependencies().link(*output22->m_impl, *inputsScript->getInputs()->getChild(2)->m_impl, false, m_errorReporting); + + ValidationResults validationResults; + m_apiObjects.validateInterfaces(validationResults); + EXPECT_TRUE(validationResults.getWarnings().empty()); + } + + TEST_P(AnApiObjects, ValidatesThatLuaInterfacesNamesAreUnique) + { + // single interface -> no warning + const auto intf1 = createInterface(); + { + ValidationResults validationResults; + m_apiObjects.validateInterfaces(validationResults); + EXPECT_TRUE(validationResults.getWarnings().empty()); + } + + // two interfaces with same name -> warning + auto intf2 = createInterface(); + { + ValidationResults validationResults; + m_apiObjects.validateInterfaces(validationResults); + ASSERT_EQ(1u, validationResults.getWarnings().size()); + EXPECT_EQ(validationResults.getWarnings().front().message, "Interface [intf] does not have a unique name"); + EXPECT_EQ(validationResults.getWarnings().front().object, intf1); + EXPECT_EQ(validationResults.getWarnings().front().type, EWarningType::Other); + } + + // rename conflicting intf -> no warning + intf2->setName("otherIntf"); + { + ValidationResults validationResults; + m_apiObjects.validateInterfaces(validationResults); + EXPECT_TRUE(validationResults.getWarnings().empty()); + } + + // another interface with same name -> warning + auto intf3 = createInterface(); + intf3->setName("otherIntf"); + { + ValidationResults validationResults; + m_apiObjects.validateInterfaces(validationResults); + ASSERT_EQ(1u, validationResults.getWarnings().size()); + EXPECT_EQ(validationResults.getWarnings().front().message, "Interface [otherIntf] does not have a unique name"); + EXPECT_EQ(validationResults.getWarnings().front().object, intf2); + EXPECT_EQ(validationResults.getWarnings().front().type, EWarningType::Other); + } + } + + TEST_P(AnApiObjects, ValidatesDanglingNodes_ProducesWarningIfNodeHasNoIngoingOrOutgoingLinks) + { + auto* script = m_apiObjects.createLuaScript(R"( + function interface(IN, OUT) + IN.param1 = Type:Int32() + OUT.param1 = Type:Int32() + end + function run(IN,OUT) + end + )", {}, "script name", m_errorReporting); + ASSERT_NE(nullptr, script); + + ValidationResults validationResults; + m_apiObjects.validateDanglingNodes(validationResults); + EXPECT_EQ(2u, validationResults.getWarnings().size()); + EXPECT_THAT(validationResults.getWarnings()[0].message, ::testing::HasSubstr("Node [script name] has no outgoing links")); + EXPECT_THAT(validationResults.getWarnings()[1].message, ::testing::HasSubstr("Node [script name] has no ingoing links")); + EXPECT_THAT(validationResults.getWarnings(), ::testing::Each(::testing::Field(&WarningData::type, ::testing::Eq(EWarningType::UnusedContent)))); + } + + TEST_P(AnApiObjects, ValidatesDanglingNodes_DoesNotProduceWarningIfNodeHasNoInputs) + { + const auto script = m_apiObjects.createLuaScript(R"( + function interface(IN,OUT) + OUT.param1 = Type:Int32() + end + function run(IN,OUT) + end + )", {}, "script name", m_errorReporting); + ASSERT_NE(nullptr, script); + + const auto dummyInputScript = m_apiObjects.createLuaScript(R"LUA_SCRIPT( + function interface(IN) + IN.param1 = Type:Int32() + end + + function run(IN,OUT) + end + )LUA_SCRIPT", {}, "dummy script", m_errorReporting); + ASSERT_NE(nullptr, dummyInputScript); + + // link script's output in order to pass outputs validation + m_apiObjects.getLogicNodeDependencies().link(*script->getOutputs()->getChild(0u)->m_impl, *dummyInputScript->getInputs()->getChild(0u)->m_impl, false, m_errorReporting); + + ValidationResults validationResults; + m_apiObjects.validateDanglingNodes(validationResults); + EXPECT_TRUE(validationResults.getWarnings().empty()); + } + + TEST_P(AnApiObjects, ValidatesDanglingNodes_DoesNotProduceWarningIfNodeHasNoOutputs) + { + const auto script = m_apiObjects.createLuaScript(R"( + function interface(IN,OUT) + IN.param1 = Type:Int32() + end + function run(IN,OUT) + end + )", {}, "script name", m_errorReporting); + ASSERT_NE(nullptr, script); + + const auto dummyOutputScript = m_apiObjects.createLuaScript(R"LUA_SCRIPT( + function interface(IN,OUT) + OUT.param1 = Type:Int32() + end + + function run(IN,OUT) + end + )LUA_SCRIPT", {}, "dummy script", m_errorReporting); + ASSERT_NE(nullptr, dummyOutputScript); + + // link script's input in order to pass inputs validation + m_apiObjects.getLogicNodeDependencies().link(*dummyOutputScript->getOutputs()->getChild(0u)->m_impl, *script->getInputs()->getChild(0u)->m_impl, false, m_errorReporting); + + ValidationResults validationResults; + m_apiObjects.validateDanglingNodes(validationResults); + EXPECT_TRUE(validationResults.getWarnings().empty()); + } + + class AnApiObjects_SceneMismatch : public AnApiObjects + { + protected: + RamsesTestSetup m_testSetup; + ramses::Scene* scene1 {m_testSetup.createScene(ramses::sceneId_t(1))}; + ramses::Scene* scene2 {m_testSetup.createScene(ramses::sceneId_t(2))}; + }; + + INSTANTIATE_TEST_SUITE_P( + AnApiObjects_SceneMismatchTests, + AnApiObjects_SceneMismatch, + ramses::internal::GetFeatureLevelTestValues()); + + TEST_P(AnApiObjects_SceneMismatch, recognizesNodeBindingsCarryNodesFromDifferentScenes) + { + m_apiObjects.createRamsesNodeBinding(*scene1->createNode("node1"), ramses::ERotationType::Euler_XYZ, "binding1"); + RamsesNodeBinding* binding2 = m_apiObjects.createRamsesNodeBinding(*scene2->createNode("node2"), ramses::ERotationType::Euler_XYZ, "binding2"); + + EXPECT_FALSE(m_apiObjects.checkBindingsReferToSameRamsesScene(m_errorReporting)); + EXPECT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ("Ramses node 'node2' is from scene with id:2 but other objects are from scene with id:1!", m_errorReporting.getErrors()[0].message); + EXPECT_EQ(binding2, m_errorReporting.getErrors()[0].object); + } + + TEST_P(AnApiObjects_SceneMismatch, recognizesNodeBindingAndAppearanceBindingAreFromDifferentScenes) + { + m_apiObjects.createRamsesNodeBinding(*scene1->createNode("node"), ramses::ERotationType::Euler_XYZ, "node binding"); + RamsesAppearanceBinding* appBinding = m_apiObjects.createRamsesAppearanceBinding(RamsesTestSetup::CreateTrivialTestAppearance(*scene2), "app binding"); + + EXPECT_FALSE(m_apiObjects.checkBindingsReferToSameRamsesScene(m_errorReporting)); + EXPECT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ("Ramses appearance 'test appearance' is from scene with id:2 but other objects are from scene with id:1!", m_errorReporting.getErrors()[0].message); + EXPECT_EQ(appBinding, m_errorReporting.getErrors()[0].object); + } + + TEST_P(AnApiObjects_SceneMismatch, detectsNodeBindingIsFromDifferentScene) + { + m_apiObjects.createRamsesNodeBinding(*scene1->createNode("node"), ramses::ERotationType::Euler_XYZ, "node binding"); + auto* otherSceneBinding = m_apiObjects.createRamsesNodeBinding(*scene2->createPerspectiveCamera("test camera"), ramses::ERotationType::Euler_XYZ, "other binding"); + + EXPECT_FALSE(m_apiObjects.checkBindingsReferToSameRamsesScene(m_errorReporting)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ("Ramses node 'test camera' is from scene with id:2 but other objects are from scene with id:1!", m_errorReporting.getErrors()[0].message); + EXPECT_EQ(otherSceneBinding, m_errorReporting.getErrors()[0].object); + } + + TEST_P(AnApiObjects_SceneMismatch, detectsAppearanceBindingIsFromDifferentScene) + { + m_apiObjects.createRamsesNodeBinding(*scene1->createNode("node"), ramses::ERotationType::Euler_XYZ, "node binding"); + auto* otherSceneBinding = m_apiObjects.createRamsesAppearanceBinding(RamsesTestSetup::CreateTrivialTestAppearance(*scene2), "other binding"); + + EXPECT_FALSE(m_apiObjects.checkBindingsReferToSameRamsesScene(m_errorReporting)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ("Ramses appearance 'test appearance' is from scene with id:2 but other objects are from scene with id:1!", m_errorReporting.getErrors()[0].message); + EXPECT_EQ(otherSceneBinding, m_errorReporting.getErrors()[0].object); + } + + TEST_P(AnApiObjects_SceneMismatch, detectsCameraBindingIsFromDifferentScene) + { + m_apiObjects.createRamsesNodeBinding(*scene1->createNode("node"), ramses::ERotationType::Euler_XYZ, "node binding"); + auto* otherSceneBinding = m_apiObjects.createRamsesCameraBinding(*scene2->createPerspectiveCamera("camera"), true, "other binding"); + + EXPECT_FALSE(m_apiObjects.checkBindingsReferToSameRamsesScene(m_errorReporting)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ("Ramses camera 'camera' is from scene with id:2 but other objects are from scene with id:1!", m_errorReporting.getErrors()[0].message); + EXPECT_EQ(otherSceneBinding, m_errorReporting.getErrors()[0].object); + } + + TEST_P(AnApiObjects_SceneMismatch, detectsRenderPassBindingIsFromDifferentScene) + { + m_apiObjects.createRamsesNodeBinding(*scene1->createNode("node"), ramses::ERotationType::Euler_XYZ, "node binding"); + auto* otherSceneBinding = m_apiObjects.createRamsesRenderPassBinding(*scene2->createRenderPass("render pass"), "other binding"); + + EXPECT_FALSE(m_apiObjects.checkBindingsReferToSameRamsesScene(m_errorReporting)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ("Ramses render pass 'render pass' is from scene with id:2 but other objects are from scene with id:1!", m_errorReporting.getErrors()[0].message); + EXPECT_EQ(otherSceneBinding, m_errorReporting.getErrors()[0].object); + } + + TEST_P(AnApiObjects_SceneMismatch, detectsRenderGroupBindingIsFromDifferentScene) + { + m_apiObjects.createRamsesNodeBinding(*scene1->createNode("node"), ramses::ERotationType::Euler_XYZ, "node binding"); + + auto rg = scene2->createRenderGroup("render group"); + const auto mesh = scene2->createMeshNode("mesh"); + EXPECT_EQ(ramses::StatusOK, rg->addMeshNode(*mesh)); + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*mesh)); + const auto* otherSceneBinding = m_apiObjects.createRamsesRenderGroupBinding(*rg, elements, "other binding"); + + EXPECT_FALSE(m_apiObjects.checkBindingsReferToSameRamsesScene(m_errorReporting)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ("Ramses render group 'render group' is from scene with id:2 but other objects are from scene with id:1!", m_errorReporting.getErrors()[0].message); + EXPECT_EQ(otherSceneBinding, m_errorReporting.getErrors()[0].object); + } + + TEST_P(AnApiObjects_SceneMismatch, detectsMeshNodeBindingIsFromDifferentScene) + { + m_apiObjects.createRamsesNodeBinding(*scene1->createNode("node"), ramses::ERotationType::Euler_XYZ, "node binding"); + + const auto otherSceneBinding = m_apiObjects.createRamsesMeshNodeBinding(*scene2->createMeshNode("mesh"), "mb"); + + EXPECT_FALSE(m_apiObjects.checkBindingsReferToSameRamsesScene(m_errorReporting)); + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + EXPECT_EQ("Ramses mesh node 'mesh' is from scene with id:2 but other objects are from scene with id:1!", m_errorReporting.getErrors()[0].message); + EXPECT_EQ(otherSceneBinding, m_errorReporting.getErrors()[0].object); + } + + class AnApiObjects_Serialization : public AnApiObjects + { + }; + + INSTANTIATE_TEST_SUITE_P( + AnApiObjects_SerializationTests, + AnApiObjects_Serialization, + ramses::internal::GetFeatureLevelTestValues()); + + TEST_P(AnApiObjects_Serialization, AlwaysCreatesEmptyFlatbuffersContainers_WhenNoObjectsPresent) + { + // Create without API objects -> serialize + flatbuffers::FlatBufferBuilder builder; + { + ApiObjects toSerialize(GetParam()); + ApiObjects::Serialize(toSerialize, builder, ELuaSavingMode::ByteCodeOnly); + } + + const auto& serialized = *flatbuffers::GetRoot(builder.GetBufferPointer()); + + // Has all containers, size = 0 because no content + ASSERT_NE(nullptr, serialized.luaScripts()); + ASSERT_EQ(0u, serialized.luaScripts()->size()); + + ASSERT_NE(nullptr, serialized.luaInterfaces()); + ASSERT_EQ(0u, serialized.luaInterfaces()->size()); + + ASSERT_NE(nullptr, serialized.nodeBindings()); + ASSERT_EQ(0u, serialized.nodeBindings()->size()); + + ASSERT_NE(nullptr, serialized.appearanceBindings()); + ASSERT_EQ(0u, serialized.appearanceBindings()->size()); + + ASSERT_NE(nullptr, serialized.cameraBindings()); + ASSERT_EQ(0u, serialized.cameraBindings()->size()); + + ASSERT_NE(nullptr, serialized.renderPassBindings()); + ASSERT_EQ(0u, serialized.renderPassBindings()->size()); + + ASSERT_NE(nullptr, serialized.links()); + ASSERT_EQ(0u, serialized.links()->size()); + + EXPECT_EQ(0u, serialized.lastObjectId()); + } + + TEST_P(AnApiObjects_Serialization, CreatesFlatbufferContainer_ForScripts) + { + // Create test flatbuffer with only a script + flatbuffers::FlatBufferBuilder builder; + { + ApiObjects toSerialize(GetParam()); + createScript(toSerialize, m_valid_empty_script); + ApiObjects::Serialize(toSerialize, builder, ELuaSavingMode::SourceAndByteCode); + } + + const auto& serialized = *flatbuffers::GetRoot(builder.GetBufferPointer()); + + ASSERT_NE(nullptr, serialized.luaScripts()); + ASSERT_EQ(1u, serialized.luaScripts()->size()); + const rlogic_serialization::LuaScript& serializedScript = *serialized.luaScripts()->Get(0); + EXPECT_EQ("script", serializedScript.base()->name()->str()); + EXPECT_EQ(1u, serializedScript.base()->id()); + EXPECT_EQ(m_valid_empty_script, serializedScript.luaSourceCode()->str()); + EXPECT_TRUE(serializedScript.luaByteCode()->size() > 0); + + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "test", m_errorReporting, GetParam()); + EXPECT_TRUE(deserialized); + } + + TEST_P(AnApiObjects_Serialization, CreatesFlatbufferContainer_ForInterfaces) + { + // Create test flatbuffer with only a script + flatbuffers::FlatBufferBuilder builder; + { + ApiObjects toSerialize(GetParam()); + createInterface(toSerialize); + ApiObjects::Serialize(toSerialize, builder, ELuaSavingMode::ByteCodeOnly); + } + + const auto& serialized = *flatbuffers::GetRoot(builder.GetBufferPointer()); + + ASSERT_NE(nullptr, serialized.luaInterfaces()); + ASSERT_EQ(1u, serialized.luaInterfaces()->size()); + const rlogic_serialization::LuaInterface& serializedInterface = *serialized.luaInterfaces()->Get(0); + EXPECT_EQ("intf", serializedInterface.base()->name()->str()); + EXPECT_EQ(1u, serializedInterface.base()->id()); + + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "test", m_errorReporting, GetParam()); + EXPECT_TRUE(deserialized); + } + + TEST_P(AnApiObjects_Serialization, CreatesFlatbufferContainers_ForBindings) + { + // Create test flatbuffer with only a node binding + flatbuffers::FlatBufferBuilder builder; + { + ApiObjects toSerialize(GetParam()); + toSerialize.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "node"); + toSerialize.createRamsesAppearanceBinding(*m_appearance, "appearance"); + toSerialize.createRamsesCameraBinding(*m_camera, true, "camera"); + toSerialize.createRamsesRenderPassBinding(*m_renderPass, "rp"); + ApiObjects::Serialize(toSerialize, builder, ELuaSavingMode::ByteCodeOnly); + } + + const auto& serialized = *flatbuffers::GetRoot(builder.GetBufferPointer()); + + ASSERT_NE(nullptr, serialized.nodeBindings()); + ASSERT_EQ(1u, serialized.nodeBindings()->size()); + const rlogic_serialization::RamsesNodeBinding& serializedNodeBinding = *serialized.nodeBindings()->Get(0); + EXPECT_EQ("node", serializedNodeBinding.base()->base()->name()->str()); + EXPECT_EQ(1u, serializedNodeBinding.base()->base()->id()); + + ASSERT_NE(nullptr, serialized.appearanceBindings()); + ASSERT_EQ(1u, serialized.appearanceBindings()->size()); + const rlogic_serialization::RamsesAppearanceBinding& serializedAppBinding = *serialized.appearanceBindings()->Get(0); + EXPECT_EQ("appearance", serializedAppBinding.base()->base()->name()->str()); + EXPECT_EQ(2u, serializedAppBinding.base()->base()->id()); + + ASSERT_NE(nullptr, serialized.cameraBindings()); + ASSERT_EQ(1u, serialized.cameraBindings()->size()); + const rlogic_serialization::RamsesCameraBinding& serializedCameraBinding = *serialized.cameraBindings()->Get(0); + EXPECT_EQ("camera", serializedCameraBinding.base()->base()->name()->str()); + EXPECT_EQ(3u, serializedCameraBinding.base()->base()->id()); + + ASSERT_NE(nullptr, serialized.renderPassBindings()); + ASSERT_EQ(1u, serialized.renderPassBindings()->size()); + const rlogic_serialization::RamsesRenderPassBinding& serializedRenderPassBinding = *serialized.renderPassBindings()->Get(0); + EXPECT_EQ("rp", serializedRenderPassBinding.base()->base()->name()->str()); + EXPECT_EQ(4u, serializedRenderPassBinding.base()->base()->id()); + } + + TEST_P(AnApiObjects_Serialization, CreatesFlatbufferContainers_ForLinks) + { + // Create test flatbuffer with a link between script and binding + flatbuffers::FlatBufferBuilder builder; + { + ApiObjects toSerialize(GetParam()); + + const std::string_view scriptWithOutput = R"( + function interface(IN,OUT) + OUT.nested = { + anUnusedValue = Type:Float(), + rotation = Type:Vec3f() + } + end + function run(IN,OUT) + end + )"; + + const LuaScript* script = createScript(toSerialize, scriptWithOutput); + RamsesNodeBinding* nodeBinding = toSerialize.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, ""); + ASSERT_TRUE(toSerialize.getLogicNodeDependencies().link( + *script->getOutputs()->getChild("nested")->getChild("rotation")->m_impl, + *nodeBinding->getInputs()->getChild("rotation")->m_impl, + false, + m_errorReporting)); + ApiObjects::Serialize(toSerialize, builder, ELuaSavingMode::ByteCodeOnly); + } + + const auto& serialized = *flatbuffers::GetRoot(builder.GetBufferPointer()); + + // Asserts both script and binding objects existence + ASSERT_EQ(1u, serialized.luaScripts()->size()); + ASSERT_EQ(1u, serialized.nodeBindings()->size()); + const rlogic_serialization::LuaScript& script = *serialized.luaScripts()->Get(0); + const rlogic_serialization::RamsesNodeBinding& binding = *serialized.nodeBindings()->Get(0); + + ASSERT_NE(nullptr, serialized.links()); + ASSERT_EQ(1u, serialized.links()->size()); + const rlogic_serialization::Link& link = *serialized.links()->Get(0); + + EXPECT_EQ(script.rootOutput()->children()->Get(0)->children()->Get(1), link.sourceProperty()); + EXPECT_EQ(binding.base()->rootInput()->children()->Get(size_t(ENodePropertyStaticIndex::Rotation)), link.targetProperty()); + } + + TEST_P(AnApiObjects_Serialization, ReConstructsImplMappingsWhenCreatedFromDeserializedData) + { + // Create dummy data and serialize + flatbuffers::FlatBufferBuilder builder; + { + ApiObjects toSerialize(GetParam()); + createScript(toSerialize, m_valid_empty_script); + createInterface(toSerialize); + const auto nodeBinding = toSerialize.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "node"); + const auto appearanceBinding = toSerialize.createRamsesAppearanceBinding(*m_appearance, "appearance"); + toSerialize.createRamsesCameraBinding(*m_camera, true, "camera"); + toSerialize.createRamsesRenderPassBinding(*m_renderPass, "rp"); + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*m_meshNode, "mesh")); + toSerialize.createRamsesRenderGroupBinding(*m_renderGroup, elements, "rg"); + createSkinBinding(*nodeBinding, *appearanceBinding, toSerialize); + toSerialize.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + + ApiObjects::Serialize(toSerialize, builder, ELuaSavingMode::ByteCodeOnly); + } + + auto& serialized = *flatbuffers::GetRoot(builder.GetBufferPointer()); + + EXPECT_CALL(m_resolverMock, findRamsesNodeInScene(::testing::Eq("node"), m_node->getSceneObjectId())).WillOnce(::testing::Return(m_node)); + EXPECT_CALL(m_resolverMock, findRamsesAppearanceInScene(::testing::Eq("appearance"), m_appearance->getSceneObjectId())).WillOnce(::testing::Return(m_appearance)); + EXPECT_CALL(m_resolverMock, findRamsesCameraInScene(::testing::Eq("camera"), m_camera->getSceneObjectId())).WillOnce(::testing::Return(m_camera)); + EXPECT_CALL(m_resolverMock, findRamsesRenderPassInScene(::testing::Eq("rp"), m_renderPass->getSceneObjectId())).WillOnce(::testing::Return(m_renderPass)); + EXPECT_CALL(m_resolverMock, findRamsesRenderGroupInScene(::testing::Eq("rg"), m_renderGroup->getSceneObjectId())).WillOnce(::testing::Return(m_renderGroup)); + EXPECT_CALL(m_resolverMock, findRamsesSceneObjectInScene(::testing::Eq("rg"), m_meshNode->getSceneObjectId())).WillOnce(::testing::Return(m_meshNode)); + EXPECT_CALL(m_resolverMock, findRamsesSceneObjectInScene(::testing::Eq("mb"), m_meshNode->getSceneObjectId())).WillOnce(::testing::Return(m_meshNode)); + std::unique_ptr apiObjectsOptional = ApiObjects::Deserialize(serialized, &m_resolverMock, "", m_errorReporting, GetParam()); + + ASSERT_TRUE(apiObjectsOptional); + + ApiObjects& apiObjects = *apiObjectsOptional; + + ASSERT_EQ(9u, apiObjects.getApiObjectOwningContainer().size()); + + LuaScript* script = apiObjects.getApiObjectContainer()[0]; + EXPECT_EQ(script->getName(), "script"); + EXPECT_EQ(script, &script->m_script.getLogicObject()); + + LuaInterface* intf = apiObjects.getApiObjectContainer()[0]; + EXPECT_EQ(intf->getName(), "intf"); + EXPECT_EQ(intf, &intf->m_interface.getLogicObject()); + + RamsesNodeBinding* nodeBinding = apiObjects.getApiObjectContainer()[0]; + EXPECT_EQ(nodeBinding->getName(), "node"); + EXPECT_EQ(nodeBinding, &nodeBinding->m_nodeBinding.getLogicObject()); + + RamsesAppearanceBinding* appBinding = apiObjects.getApiObjectContainer()[0]; + EXPECT_EQ(appBinding->getName(), "appearance"); + EXPECT_EQ(appBinding, &appBinding->m_appearanceBinding.getLogicObject()); + + RamsesCameraBinding* camBinding = apiObjects.getApiObjectContainer()[0]; + EXPECT_EQ(camBinding->getName(), "camera"); + EXPECT_EQ(camBinding, &camBinding->m_cameraBinding.getLogicObject()); + + RamsesRenderPassBinding* rpBinding = apiObjects.getApiObjectContainer()[0]; + EXPECT_EQ(rpBinding->getName(), "rp"); + EXPECT_EQ(rpBinding, &rpBinding->m_renderPassBinding.getLogicObject()); + + const auto rgBinding = apiObjects.getApiObjectContainer()[0]; + EXPECT_EQ(rgBinding->getName(), "rg"); + EXPECT_EQ(rgBinding, &rgBinding->m_renderGroupBinding.getLogicObject()); + + const auto skin = apiObjects.getApiObjectContainer()[0]; + EXPECT_EQ(skin->getName(), "skin"); + EXPECT_EQ(skin, &skin->m_skinBinding.getLogicObject()); + + const auto meshBinding = apiObjects.getApiObjectContainer()[0]; + EXPECT_EQ(meshBinding->getName(), "mb"); + EXPECT_EQ(meshBinding, &meshBinding->m_meshNodeBinding.getLogicObject()); + } + + TEST_P(AnApiObjects_Serialization, ObjectsCreatedAfterLoadingReceiveUniqueId) + { + ApiObjects beforeSaving(GetParam()); + + // Create dummy data and serialize + flatbuffers::FlatBufferBuilder builder; + { + createScript(beforeSaving, m_valid_empty_script); + createScript(beforeSaving, m_valid_empty_script); + createScript(beforeSaving, m_valid_empty_script); + + ApiObjects::Serialize(beforeSaving, builder, ELuaSavingMode::ByteCodeOnly); + } + + auto& serialized = *flatbuffers::GetRoot(builder.GetBufferPointer()); + + EXPECT_EQ(3u, serialized.lastObjectId()); + + std::unique_ptr afterLoadingObjects = ApiObjects::Deserialize(serialized, &m_resolverMock, "", m_errorReporting, GetParam()); + + auto* newScript = createScript(*afterLoadingObjects, m_valid_empty_script); + // new script's ID does not overlap with one of the IDs of the objects before saving + EXPECT_EQ(nullptr, beforeSaving.getApiObjectById(newScript->getId())); + } + + TEST_P(AnApiObjects_Serialization, ReConstructsLinksWhenCreatedFromDeserializedData) + { + // Create dummy data and serialize + flatbuffers::FlatBufferBuilder builder; + { + ApiObjects toSerialize(GetParam()); + + const std::string_view scriptForLinks = R"( + function interface(IN,OUT) + IN.integer = Type:Int32() + OUT.nested = { + unused = Type:Float(), + integer = Type:Int32() + } + end + function run(IN,OUT) + end + )"; + + auto script1 = createScript(toSerialize, scriptForLinks); + auto script2 = createScript(toSerialize, scriptForLinks); + ASSERT_TRUE(toSerialize.getLogicNodeDependencies().link( + *script1->getOutputs()->getChild("nested")->getChild("integer")->m_impl, + *script2->getInputs()->getChild("integer")->m_impl, + false, + m_errorReporting)); + + ApiObjects::Serialize(toSerialize, builder, ELuaSavingMode::ByteCodeOnly); + } + + auto& serialized = *flatbuffers::GetRoot(builder.GetBufferPointer()); + + std::unique_ptr apiObjectsOptional = ApiObjects::Deserialize(serialized, &m_resolverMock, "", m_errorReporting, GetParam()); + + ASSERT_TRUE(apiObjectsOptional); + + ApiObjects& apiObjects = *apiObjectsOptional; + + LuaScript* script1 = apiObjects.getApiObjectContainer()[0]; + ASSERT_TRUE(script1); + + LuaScript* script2 = apiObjects.getApiObjectContainer()[1]; + ASSERT_TRUE(script2); + + EXPECT_TRUE(apiObjects.getLogicNodeDependencies().isLinked(script1->m_impl)); + EXPECT_TRUE(apiObjects.getLogicNodeDependencies().isLinked(script2->m_impl)); + + const PropertyImpl* script1Output = script1->getOutputs()->getChild("nested")->getChild("integer")->m_impl.get(); + const PropertyImpl* script2Input = script2->getInputs()->getChild("integer")->m_impl.get(); + EXPECT_EQ(script1Output, script2Input->getIncomingLink().property); + EXPECT_FALSE(script2Input->getIncomingLink().isWeakLink); + + ASSERT_EQ(1u, script1Output->getOutgoingLinks().size()); + EXPECT_EQ(script1Output->getOutgoingLinks()[0].property, script2Input); + EXPECT_FALSE(script1Output->getOutgoingLinks()[0].isWeakLink); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenLuaModulesContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + 0, // no modules container + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing Lua modules container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenScriptsContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0, // no scripts container + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing Lua scripts container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenInterfacesContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0, // no interfaces container + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing Lua interfaces container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenNodeBindingsContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0, // no node bindings container + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing node bindings container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenAppearanceBindingsContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0, // no appearance bindings container + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing appearance bindings container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenCameraBindingsContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0, // no camera bindings container + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing camera bindings container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenRenderPassBindingsContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + 0u, // no render pass bindings container + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing renderpass bindings container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenLinksContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0, // no links container + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing links container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenDataArrayContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0, // no data array container + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing data arrays container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenAnimationNodeContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0, // no animation nodes container + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing animation nodes container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenTimerNodeContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0, // no timer nodes container + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing timer nodes container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenAnchorPointContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, // no anchor points container + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing anchor points container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenRenderGroupBindingContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, // no render group bindings container + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing rendergroup bindings container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenSkinBindingContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, // no skin bindings container + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing skin bindings container!"); + } + + TEST_P(AnApiObjects_Serialization, ErrorWhenMeshNodeBindingContainerMissing) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u // no mesh node bindings container + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading from serialized data: missing meshnode bindings container!"); + } + + TEST_P(AnApiObjects_Serialization, ReportsErrorWhenScriptCouldNotBeDeserialized) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{ m_testUtils.serializeTestScriptWithError() }), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 2u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LogicObject base from serialized data: missing base table!"); + EXPECT_EQ(m_errorReporting.getErrors()[1].message, "Fatal error during loading of LuaScript from serialized data: missing name and/or ID!"); + } + + TEST_P(AnApiObjects_Serialization, ReportsErrorWhenInterfaceCouldNotBeDeserialized) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{m_testUtils.serializeTestInterfaceWithError()}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LuaInterface from serialized data: empty name!"); + } + + TEST_P(AnApiObjects_Serialization, ReportsErrorWhenModuleCouldNotBeDeserialized) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{ m_testUtils.serializeTestModule(true) }), // module has errors + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 2u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of LogicObject base from serialized data: missing name!"); + EXPECT_EQ(m_errorReporting.getErrors()[1].message, "Fatal error during loading of LuaModule from serialized data: missing name and/or ID!"); + } + + TEST_P(AnApiObjects_Serialization, ReportsErrorWhenRenderPassBindingCouldNotBeDeserialized) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{ m_testUtils.serializeTestRenderPassBindingWithError() }), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderPassBinding from serialized data: missing base class info!"); + } + + TEST_P(AnApiObjects_Serialization, ReportsErrorWhenRenderGroupBindingCouldNotBeDeserialized) + { + { + auto apiObjects = rlogic_serialization::CreateApiObjects( + m_flatBufferBuilder, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + 0u, + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{ m_testUtils.serializeTestRenderGroupBindingWithError() }), + m_flatBufferBuilder.CreateVector(std::vector>{}), + m_flatBufferBuilder.CreateVector(std::vector>{}) + ); + m_flatBufferBuilder.Finish(apiObjects); + } + + const auto& serialized = *flatbuffers::GetRoot(m_flatBufferBuilder.GetBufferPointer()); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "unit test", m_errorReporting, GetParam()); + + EXPECT_FALSE(deserialized); + ASSERT_EQ(m_errorReporting.getErrors().size(), 1u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "Fatal error during loading of RamsesRenderGroupBinding from serialized data: missing base class info!"); + } + + TEST_P(AnApiObjects_Serialization, FillsLogicObjectAndOwnedContainerOnDeserialization) + { + // Create dummy data and serialize + flatbuffers::FlatBufferBuilder builder; + { + ApiObjects toSerialize{ GetParam() }; + toSerialize.createLuaModule(m_moduleSrc, {}, "module", m_errorReporting); + createScript(toSerialize, m_valid_empty_script); + createInterface(toSerialize); + const auto node = toSerialize.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "node"); + const auto appearance = toSerialize.createRamsesAppearanceBinding(*m_appearance, "appearance"); + const auto camera = toSerialize.createRamsesCameraBinding(*m_camera, true, "camera"); + auto dataArray = toSerialize.createDataArray(std::vector{1.f, 2.f, 3.f}, "data"); + AnimationNodeConfig config; + config.addChannel({ "channel", dataArray, dataArray, EInterpolationType::Linear }); + toSerialize.createAnimationNode(*config.m_impl, "animNode"); + toSerialize.createTimerNode("timerNode"); + toSerialize.createRamsesRenderPassBinding(*m_renderPass, "rp"); + toSerialize.createAnchorPoint(node->m_nodeBinding, camera->m_cameraBinding, "anchor"); + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*m_meshNode, "mesh")); + toSerialize.createRamsesRenderGroupBinding(*m_renderGroup, elements, "rg"); + createSkinBinding(*node, *appearance, toSerialize); + toSerialize.createRamsesMeshNodeBinding(*m_meshNode, "mb"); + + ApiObjects::Serialize(toSerialize, builder, ELuaSavingMode::ByteCodeOnly); + } + + auto& serialized = *flatbuffers::GetRoot(builder.GetBufferPointer()); + + EXPECT_CALL(m_resolverMock, findRamsesNodeInScene(::testing::Eq("node"), m_node->getSceneObjectId())).WillOnce(::testing::Return(m_node)); + EXPECT_CALL(m_resolverMock, findRamsesAppearanceInScene(::testing::Eq("appearance"), m_appearance->getSceneObjectId())).WillOnce(::testing::Return(m_appearance)); + EXPECT_CALL(m_resolverMock, findRamsesCameraInScene(::testing::Eq("camera"), m_camera->getSceneObjectId())).WillOnce(::testing::Return(m_camera)); + EXPECT_CALL(m_resolverMock, findRamsesRenderPassInScene(::testing::Eq("rp"), m_renderPass->getSceneObjectId())).WillOnce(::testing::Return(m_renderPass)); + EXPECT_CALL(m_resolverMock, findRamsesRenderGroupInScene(::testing::Eq("rg"), m_renderGroup->getSceneObjectId())).WillOnce(::testing::Return(m_renderGroup)); + EXPECT_CALL(m_resolverMock, findRamsesSceneObjectInScene(::testing::Eq("rg"), m_meshNode->getSceneObjectId())).WillOnce(::testing::Return(m_meshNode)); + EXPECT_CALL(m_resolverMock, findRamsesSceneObjectInScene(::testing::Eq("mb"), m_meshNode->getSceneObjectId())).WillOnce(::testing::Return(m_meshNode)); + std::unique_ptr deserialized = ApiObjects::Deserialize(serialized, &m_resolverMock, "", m_errorReporting, GetParam()); + + ASSERT_TRUE(deserialized); + + ApiObjects& apiObjects = *deserialized; + + const ApiObjectContainer& logicObjects = apiObjects.getApiObjectContainer(); + const ApiObjectOwningContainer& ownedObjects = apiObjects.getApiObjectOwningContainer(); + + std::vector expected; + expected = std::vector{ + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0], + apiObjects.getApiObjectContainer()[0] + }; + + ASSERT_EQ(expected.size(), logicObjects.size()); + ASSERT_EQ(logicObjects.size(), ownedObjects.size()); + for (size_t i = 0; i < expected.size(); ++i) + { + EXPECT_EQ(logicObjects[i], expected[i]); + EXPECT_EQ(ownedObjects[i].get(), expected[i]); + EXPECT_EQ(logicObjects[i], &logicObjects[i]->m_impl->getLogicObject()); + } + } +} diff --git a/client/logic/unittests/internal/DirectedAcyclicGraphTest.cpp b/client/logic/unittests/internal/DirectedAcyclicGraphTest.cpp new file mode 100644 index 000000000..1b4e52e87 --- /dev/null +++ b/client/logic/unittests/internal/DirectedAcyclicGraphTest.cpp @@ -0,0 +1,578 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" + +#include "internals/DirectedAcyclicGraph.h" + +#include "LogicNodeDummy.h" + +namespace ramses::internal +{ + + class ADirectedAcyclicGraph : public ::testing::Test + { + protected: + DirectedAcyclicGraph m_graph; + + std::array m_testNodes + { + LogicNodeDummyImpl { "1", false }, + LogicNodeDummyImpl { "2", false }, + LogicNodeDummyImpl { "3", false }, + LogicNodeDummyImpl { "4", false }, + LogicNodeDummyImpl { "5", false }, + LogicNodeDummyImpl { "6", false }, + }; + + // For easier access in the tests + LogicNodeDummyImpl& N1 { m_testNodes[0] }; + LogicNodeDummyImpl& N2 { m_testNodes[1] }; + LogicNodeDummyImpl& N3 { m_testNodes[2] }; + LogicNodeDummyImpl& N4 { m_testNodes[3] }; + LogicNodeDummyImpl& N5 { m_testNodes[4] }; + LogicNodeDummyImpl& N6 { m_testNodes[5] }; + + std::unordered_map m_ordering; + + void addTestNodesToGraph(size_t howMany) + { + for (size_t i = 0; i < howMany; ++i) + { + m_graph.addNode(m_testNodes[i]); + } + } + + void updateOrdering() + { + m_ordering.clear(); + + const NodeVector orderedNodes = *m_graph.getTopologicallySortedNodes(); + for (size_t i = 0; i < orderedNodes.size(); ++i) + { + m_ordering.insert({orderedNodes[i], i}); + } + } + + NodeVector getSortedTestNodes() + { + return *m_graph.getTopologicallySortedNodes(); + } + + size_t getRank(const LogicNodeImpl& node) const + { + return m_ordering.at(&node); + } + }; + + TEST_F(ADirectedAcyclicGraph, ContainsOnlyNodesWhichAreExplicitlyAdded) + { + addTestNodesToGraph(1); + + EXPECT_TRUE(m_graph.containsNode(N1)); + EXPECT_FALSE(m_graph.containsNode(N2)); + } + + TEST_F(ADirectedAcyclicGraph, SingleNodeWithNoEdgesHasZeroDegree) + { + addTestNodesToGraph(1); + + EXPECT_EQ(0u, m_graph.getInDegree(N1)); + EXPECT_EQ(0u, m_graph.getOutDegree(N1)); + } + + TEST_F(ADirectedAcyclicGraph, EachLinkIncreasesOutAndInDegreeOfRespectiveNodesByExactlyOne) + { + addTestNodesToGraph(2); + m_graph.addEdge(N1, N2); + + EXPECT_EQ(1u, m_graph.getOutDegree(N1)); + EXPECT_EQ(1u, m_graph.getInDegree(N2)); + + m_graph.addEdge(N1, N2); + + EXPECT_EQ(2u, m_graph.getOutDegree(N1)); + EXPECT_EQ(2u, m_graph.getInDegree(N2)); + + m_graph.removeEdge(N1, N2); + EXPECT_EQ(1u, m_graph.getOutDegree(N1)); + EXPECT_EQ(1u, m_graph.getInDegree(N2)); + + m_graph.removeEdge(N1, N2); + EXPECT_EQ(0u, m_graph.getOutDegree(N1)); + EXPECT_EQ(0u, m_graph.getInDegree(N2)); + } + + TEST_F(ADirectedAcyclicGraph, Given_TriangleGraph_When_RemovingOneEdge_Then_InOutDegreesUpdatedCorrectly) + { + /* + * N2 + * / \ + * / \ + * N1 ------- N3 + */ + + addTestNodesToGraph(3); + m_graph.addEdge(N1, N2); + m_graph.addEdge(N1, N3); + m_graph.addEdge(N2, N3); + + EXPECT_EQ(2u, m_graph.getOutDegree(N1)); + EXPECT_EQ(0u, m_graph.getInDegree(N1)); + EXPECT_EQ(1u, m_graph.getOutDegree(N2)); + EXPECT_EQ(1u, m_graph.getInDegree(N2)); + EXPECT_EQ(0u, m_graph.getOutDegree(N3)); + EXPECT_EQ(2u, m_graph.getInDegree(N3)); + + /* + * N2 + * \ + * \ + * N1 ------- N3 + */ + m_graph.removeEdge(N1, N2); + + EXPECT_EQ(1u, m_graph.getOutDegree(N1)); + EXPECT_EQ(0u, m_graph.getInDegree(N1)); + EXPECT_EQ(1u, m_graph.getOutDegree(N2)); + EXPECT_EQ(0u, m_graph.getInDegree(N2)); + EXPECT_EQ(0u, m_graph.getOutDegree(N3)); + EXPECT_EQ(2u, m_graph.getInDegree(N3)); + } + + TEST_F(ADirectedAcyclicGraph, RemovingSourceNode_ResultsInCorrectInAndOutDegreesOfTargetNode) + { + addTestNodesToGraph(2); + m_graph.addEdge(N1, N2); + m_graph.addEdge(N1, N2); + + m_graph.removeNode(N1); + + EXPECT_EQ(0u, m_graph.getOutDegree(N2)); + EXPECT_EQ(0u, m_graph.getInDegree(N2)); + + // Confidence - check that removed node is really gone + updateOrdering(); + EXPECT_THAT(getSortedTestNodes(), ::testing::ElementsAre(&N2)); + } + + TEST_F(ADirectedAcyclicGraph, ReportsNewEdgeOnlyTheFirstTimeTwoNodesAreConnected) + { + addTestNodesToGraph(2); + EXPECT_TRUE(m_graph.addEdge(N1, N2)); + EXPECT_FALSE(m_graph.addEdge(N1, N2)); + EXPECT_FALSE(m_graph.addEdge(N1, N2)); + + // Remove all three edges, and add again -> new edge reported again + m_graph.removeEdge(N1, N2); + m_graph.removeEdge(N1, N2); + m_graph.removeEdge(N1, N2); + EXPECT_TRUE(m_graph.addEdge(N1, N2)); + } + + TEST_F(ADirectedAcyclicGraph, RemovingTargetNode_ResultsInCorrectInAndOutDegreesOfSourceNode) + { + addTestNodesToGraph(2); + m_graph.addEdge(N1, N2); + m_graph.addEdge(N1, N2); + + m_graph.removeNode(N2); + + EXPECT_EQ(0u, m_graph.getOutDegree(N1)); + EXPECT_EQ(0u, m_graph.getInDegree(N1)); + + // Confidence - check that removed node is really gone + updateOrdering(); + EXPECT_THAT(getSortedTestNodes(), ::testing::ElementsAre(&N1)); + } + + TEST_F(ADirectedAcyclicGraph, RemovingMiddleNode_ResultsInCorrectInAndOutDegreeOfOtherNodes) + { + /* + * N1 -- x2-- N2 -- x2-- N3 + */ + addTestNodesToGraph(3); + m_graph.addEdge(N1, N2); + m_graph.addEdge(N1, N2); + m_graph.addEdge(N2, N3); + m_graph.addEdge(N2, N3); + + /* + * N1 (removed) N3 + */ + m_graph.removeNode(N2); + + EXPECT_EQ(0u, m_graph.getOutDegree(N1)); + EXPECT_EQ(0u, m_graph.getInDegree(N1)); + EXPECT_EQ(0u, m_graph.getOutDegree(N3)); + EXPECT_EQ(0u, m_graph.getInDegree(N3)); + + // Confidence - check that removed node is really gone + updateOrdering(); + EXPECT_THAT(getSortedTestNodes(), ::testing::UnorderedElementsAre(&N1, &N3)); + } + + TEST_F(ADirectedAcyclicGraph, RemovingOneSourceNode_DoesNotAffectEdgesOfOtherSourceNodes) + { + /* + * N1 <- remove this node + * \ + * N3 + * / <- multiplicity = 2 + * N2 + */ + addTestNodesToGraph(3); + m_graph.addEdge(N1, N3); + m_graph.addEdge(N2, N3); + m_graph.addEdge(N2, N3); + + m_graph.removeNode(N1); + + EXPECT_EQ(2u, m_graph.getOutDegree(N2)); + EXPECT_EQ(0u, m_graph.getInDegree(N2)); + EXPECT_EQ(0u, m_graph.getOutDegree(N3)); + EXPECT_EQ(2u, m_graph.getInDegree(N3)); + + // Confidence - check that removed node is really gone + updateOrdering(); + EXPECT_THAT(getSortedTestNodes(), ::testing::UnorderedElementsAre(&N2, &N3)); + } + + TEST_F(ADirectedAcyclicGraph, RemovingOneTargetNode_DoesNotAffectEdgesOfOtherTargetNodes) + { + /* + * N2 <- remove this node + * / + * N1 + * \ <- multiplicity = 2 + * N3 + */ + addTestNodesToGraph(3); + m_graph.addEdge(N1, N2); + m_graph.addEdge(N1, N3); + m_graph.addEdge(N1, N3); + + m_graph.removeNode(N2); + + EXPECT_EQ(2u, m_graph.getOutDegree(N1)); + EXPECT_EQ(0u, m_graph.getInDegree(N1)); + EXPECT_EQ(0u, m_graph.getOutDegree(N3)); + EXPECT_EQ(2u, m_graph.getInDegree(N3)); + + // Confidence - check that removed node is really gone + updateOrdering(); + EXPECT_THAT(getSortedTestNodes(), ::testing::UnorderedElementsAre(&N1, &N3)); + } + + TEST_F(ADirectedAcyclicGraph, ReturnsTwoNodesInRightOrder) + { + addTestNodesToGraph(2); + m_graph.addEdge(N1, N2); + + updateOrdering(); + + EXPECT_THAT(getSortedTestNodes(), ::testing::ElementsAre(&N1, &N2)); + } + + TEST_F(ADirectedAcyclicGraph, ReversesOrderIfLinkIsReversed) + { + addTestNodesToGraph(2); + + m_graph.addEdge(N1, N2); + EXPECT_THAT(getSortedTestNodes(), ::testing::ElementsAre(&N1, &N2)); + + m_graph.removeEdge(N1, N2); + m_graph.addEdge(N2, N1); + EXPECT_THAT(getSortedTestNodes(), ::testing::ElementsAre(&N2, &N1)); + } + + TEST_F(ADirectedAcyclicGraph, ComputesRightOrderForComplexGraph) + { + addTestNodesToGraph(6); + + /* ----- + * / \ + * N2 -- N3 -- N6 + * / \ + * / \ + * N1 -- N4 -- N5 + */ + + m_graph.addEdge(N1, N3); + m_graph.addEdge(N1, N4); + m_graph.addEdge(N2, N3); + m_graph.addEdge(N2, N6); + m_graph.addEdge(N3, N5); + m_graph.addEdge(N3, N6); + m_graph.addEdge(N4, N5); + + updateOrdering(); + + EXPECT_LT(getRank(N1), getRank(N3)); + EXPECT_LT(getRank(N1), getRank(N4)); + EXPECT_LT(getRank(N2), getRank(N6)); + EXPECT_LT(getRank(N2), getRank(N3)); + EXPECT_LT(getRank(N3), getRank(N6)); + EXPECT_LT(getRank(N3), getRank(N5)); + EXPECT_LT(getRank(N4), getRank(N5)); + } + + TEST_F(ADirectedAcyclicGraph, ComputesRightOrderForComplexGraphAfterLinksAreChanged) + { + addTestNodesToGraph(6); + + /* ----- + * / \ + * N2 -- N3 -- N6 + * / \ + * / \ + * N1 -- N4 -- N5 + */ + + m_graph.addEdge(N1, N3); + m_graph.addEdge(N1, N4); + m_graph.addEdge(N2, N3); + m_graph.addEdge(N2, N6); + m_graph.addEdge(N3, N5); + m_graph.addEdge(N3, N6); + m_graph.addEdge(N4, N5); + + /* ----- + * / \ + * N2 -- N3 -- N6 -- N1 -- N4 -- N5 + * \ / + * \ --------------- / + * + */ + + m_graph.removeEdge(N1, N3); + m_graph.addEdge(N6, N1); + + updateOrdering(); + + EXPECT_LT(getRank(N2), getRank(N3)); + EXPECT_LT(getRank(N3), getRank(N6)); + EXPECT_LT(getRank(N6), getRank(N1)); + EXPECT_LT(getRank(N1), getRank(N4)); + EXPECT_LT(getRank(N2), getRank(N6)); + EXPECT_LT(getRank(N4), getRank(N5)); + EXPECT_LT(getRank(N3), getRank(N5)); + } + + TEST_F(ADirectedAcyclicGraph, ComputesRightOrderIfANodeIsRemovedFromBeginning) + { + addTestNodesToGraph(6); + + /* ----- + * / \ + * N2 -- N3 -- N6 + * / \ + * / \ + * N1 -- N4 -- N5 + */ + + m_graph.addEdge(N1, N3); + m_graph.addEdge(N1, N4); + m_graph.addEdge(N2, N3); + m_graph.addEdge(N2, N6); + m_graph.addEdge(N3, N5); + m_graph.addEdge(N3, N6); + m_graph.addEdge(N4, N5); + + /* + * (removed) N3 -- N6 + * / \ + * / \ + * N1 -- N4 -- N5 + */ + + m_graph.removeNode(N2); + + updateOrdering(); + + EXPECT_LT(getRank(N1), getRank(N3)); + EXPECT_LT(getRank(N1), getRank(N4)); + EXPECT_LT(getRank(N3), getRank(N6)); + EXPECT_LT(getRank(N3), getRank(N5)); + EXPECT_LT(getRank(N4), getRank(N5)); + + // Confidence - check that removed node is really gone + EXPECT_THAT(getSortedTestNodes(), ::testing::UnorderedElementsAre(&N1, &N3, &N4, &N5, &N6)); + } + + TEST_F(ADirectedAcyclicGraph, ComputesRightOrderIfANodeIsRemovedFromEnd) + { + addTestNodesToGraph(6); + + /* ----- + * / \ + * N2 -- N3 -- N6 + * / \ + * / \ + * N1 -- N4 -- N5 + */ + + m_graph.addEdge(N1, N3); + m_graph.addEdge(N1, N4); + m_graph.addEdge(N2, N3); + m_graph.addEdge(N2, N6); + m_graph.addEdge(N3, N5); + m_graph.addEdge(N3, N6); + m_graph.addEdge(N4, N5); + + /* ----- + * / \ + * N2 -- N3 -- N6 + * / + * / + * N1 -- N4 (removed) + */ + + m_graph.removeNode(N5); + + updateOrdering(); + + EXPECT_LT(getRank(N1), getRank(N3)); + EXPECT_LT(getRank(N1), getRank(N4)); + EXPECT_LT(getRank(N2), getRank(N3)); + EXPECT_LT(getRank(N2), getRank(N6)); + EXPECT_LT(getRank(N3), getRank(N6)); + + // Confidence - check that removed node is really gone + EXPECT_THAT(getSortedTestNodes(), ::testing::UnorderedElementsAre(&N1, &N2, &N3, &N4, &N6)); + } + + TEST_F(ADirectedAcyclicGraph, ComputesRightOrderIfANodeIsRemovedFromTheMiddle) + { + addTestNodesToGraph(6); + + /* ----- + * / \ + * N2 -- N3 -- N6 + * / \ + * / \ + * N1 -- N4 -- N5 + */ + + m_graph.addEdge(N1, N3); + m_graph.addEdge(N1, N4); + m_graph.addEdge(N2, N3); + m_graph.addEdge(N2, N6); + m_graph.addEdge(N3, N5); + m_graph.addEdge(N3, N6); + m_graph.addEdge(N4, N5); + + + /* ----- + * / \ + * N2 N3 N6 + * + * + * N1 -- N4 -- N5 + */ + + m_graph.removeNode(N3); + + updateOrdering(); + + EXPECT_LT(getRank(N1), getRank(N4)); + EXPECT_LT(getRank(N2), getRank(N6)); + EXPECT_LT(getRank(N4), getRank(N5)); + + // Confidence - check that removed node is really gone + EXPECT_THAT(getSortedTestNodes(), ::testing::UnorderedElementsAre(&N1, &N2, &N4, &N5, &N6)); + } + + TEST_F(ADirectedAcyclicGraph, CycleDoesNotCauseStackOverflow_NoIdentifyableRootNode) + { + addTestNodesToGraph(3); + + // N1 -> N2 -> N3 -> N1 -> .... (infinity) + m_graph.addEdge(N1, N2); + m_graph.addEdge(N2, N3); + m_graph.addEdge(N3, N1); + + EXPECT_FALSE(m_graph.getTopologicallySortedNodes().has_value()); + // Can call again, still reports false and does not crash + EXPECT_FALSE(m_graph.getTopologicallySortedNodes().has_value()); + } + + TEST_F(ADirectedAcyclicGraph, CycleDoesNotCauseStackOverflow_WithRootNode) + { + addTestNodesToGraph(4); + + // N1 -> N2 -> N3 -> N4 -> N2 .... (infinity) + m_graph.addEdge(N1, N2); + m_graph.addEdge(N2, N3); + m_graph.addEdge(N3, N4); + m_graph.addEdge(N4, N2); + + EXPECT_FALSE(m_graph.getTopologicallySortedNodes().has_value()); + // Can call again, still reports false and does not crash + EXPECT_FALSE(m_graph.getTopologicallySortedNodes().has_value()); + } + + TEST_F(ADirectedAcyclicGraph, RemovesMultiLinksBetweenTwoNodes_OneByOne) + { + addTestNodesToGraph(2); + + /* + * N1 -x2-> N2 + */ + m_graph.addEdge(N1, N2); + m_graph.addEdge(N1, N2); + + EXPECT_THAT(getSortedTestNodes(), ::testing::ElementsAre(&N1, &N2)); + + m_graph.removeEdge(N1, N2); + EXPECT_THAT(getSortedTestNodes(), ::testing::ElementsAre(&N1, &N2)); + /* + * N1 -x1-> N2 + */ + + m_graph.removeEdge(N1, N2); + + // Both nodes still here, but no ordering guarantees any more + EXPECT_THAT(getSortedTestNodes(), ::testing::UnorderedElementsAre(&N1, &N2)); + } + + TEST_F(ADirectedAcyclicGraph, Confidence_ReAddingNode_WithMultiLinksToNeighbours_AndReversingOrderOfLinks_ReversesOrderOfNodes) + { + addTestNodesToGraph(3); + + /* + * N1 -x2-> N2 -x3-> N3 + */ + m_graph.addEdge(N1, N2); + m_graph.addEdge(N1, N2); + m_graph.addEdge(N2, N3); + m_graph.addEdge(N2, N3); + m_graph.addEdge(N2, N3); + + EXPECT_THAT(getSortedTestNodes(), ::testing::ElementsAre(&N1, &N2, &N3)); + + /* + * N1 (removed) N3 + */ + m_graph.removeNode(N2); + + // No links -> No ordering. Check that the removed node is not in the list now + EXPECT_THAT(getSortedTestNodes(), ::testing::UnorderedElementsAre(&N1, &N3)); + + // Re-add the node, and create links in the other direction + m_graph.addNode(N2); + + /* + * N3 -> N2 -> N1 + */ + m_graph.addEdge(N3, N2); + m_graph.addEdge(N2, N1); + + EXPECT_THAT(getSortedTestNodes(), ::testing::ElementsAre(&N3, &N2, &N1)); + } +} diff --git a/client/logic/unittests/internal/EnvironmentProtectionTest.cpp b/client/logic/unittests/internal/EnvironmentProtectionTest.cpp new file mode 100644 index 000000000..6c1778f88 --- /dev/null +++ b/client/logic/unittests/internal/EnvironmentProtectionTest.cpp @@ -0,0 +1,865 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" + +#include "internals/SolState.h" +#include "internals/EnvironmentProtection.h" + +namespace ramses::internal +{ + class AEnvironmentProtection : public ::testing::Test + { + protected: + + sol::table getInternalEnvironment() + { + return EnvironmentProtection::GetProtectedEnvironmentTable(m_protEnv); + } + + SolState m_solState; + sol::environment m_protEnv{ m_solState.createEnvironment({}, {}, false) }; + }; + + TEST_F(AEnvironmentProtection, ForbidsNotStringKeyGlobalAccessOfAnySort) + { + const std::vector invalidStatements = { + "_G[0] = 42", + "local l=_G[0]", + "_G[{}] = 42", + "local l=_G[{}]", + "_G[_G] = 42", + "local l=_G[_G]", + "_G[true] = 42", + "local l=_G[true]", + }; + + const std::vector protectionFlags = { + EEnvProtectionFlag::LoadScript, + EEnvProtectionFlag::InitFunction, + EEnvProtectionFlag::InterfaceFunctionInScript, + EEnvProtectionFlag::RunFunction, + }; + + for (const auto pFlag : protectionFlags) + { + EnvironmentProtection::SetEnvironmentProtectionLevel(m_protEnv, pFlag); + for (const auto& statement : invalidStatements) + { + sol::protected_function loadedScript = m_solState.loadScript(statement, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), ::testing::HasSubstr("Assigning global variables with a non-string index is prohibited! (key type used")); + } + } + } + + class AEnvironmentProtection_LoadScript : public AEnvironmentProtection + { + protected: + AEnvironmentProtection_LoadScript() + { + EnvironmentProtection::SetEnvironmentProtectionLevel(m_protEnv, EEnvProtectionFlag::LoadScript); + } + }; + + TEST_F(AEnvironmentProtection_LoadScript, AllowsDeclaringWhitelistedFunctions) + { + const std::string_view script = R"( + function init() + return 5 + end + function interface(IN,OUT) + return 6 + end + function run(IN,OUT) + return 7 + end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_TRUE(result.valid()); + + sol::protected_function init = getInternalEnvironment()["init"]; + sol::protected_function iface = getInternalEnvironment()["interface"]; + sol::protected_function run = getInternalEnvironment()["run"]; + + const int initResult = init(); + const int ifaceResult = iface(); + const int runResult = run(); + + EXPECT_EQ(5, initResult); + EXPECT_EQ(6, ifaceResult); + EXPECT_EQ(7, runResult); + } + + TEST_F(AEnvironmentProtection_LoadScript, AllowsDeclaringWhitelistedFunctions_ExoticSyntax) + { + const std::string_view script = R"( + -- This is a somewhat exotic way to create a global function, must should have the same level of protection + _G["run"] = function () return 5 end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + sol::protected_function run = getInternalEnvironment()["run"]; + const int runResult = run(); + EXPECT_EQ(5, runResult); + } + + TEST_F(AEnvironmentProtection_LoadScript, ForbidsDeclaringUnknownFunctions) + { + const std::string_view script = R"( + function thisIsNotAllowed() + return 5 + end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), ::testing::HasSubstr("Unexpected function name 'thisIsNotAllowed'! Allowed names: 'init', 'interface', 'run'")); + } + + TEST_F(AEnvironmentProtection_LoadScript, ForbidsDeclaringUnknownFunctions_ExoticSyntax) + { + const std::string_view script = R"( + -- This is a somewhat exotic way to create a global function, must be handled as error just as the normal case above + _G["thisIsNotAllowed"] = function () return 5 end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), ::testing::HasSubstr("Unexpected function name 'thisIsNotAllowed'! Allowed names: 'init', 'interface', 'run'")); + } + + TEST_F(AEnvironmentProtection_LoadScript, CatchesWritingToGlobalsAsError) + { + const std::string_view script = R"( + global="this generates error" + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), ::testing::HasSubstr("Declaring global variables is forbidden (exceptions: the functions 'init', 'interface' and 'run')!")); + } + + TEST_F(AEnvironmentProtection_LoadScript, CatchesReadingGlobalsAsError) + { + const std::string_view script = R"( + local t=_G["this generates error"] + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Trying to read global variable 'this generates error' outside the scope of init(), interface() and run() functions! This can cause undefined behavior and is forbidden!")); + } + + TEST_F(AEnvironmentProtection_LoadScript, ForbidsOverwritingSpecialFunctions) + { + const std::string_view script = R"( + function init() + return 5 + end + function init() + return 6 + end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Function 'init' can only be declared once!")); + + sol::protected_function init = getInternalEnvironment()["init"]; + int val = init(); + EXPECT_EQ(val, 5); + } + + TEST_F(AEnvironmentProtection_LoadScript, ForbidsOverwritingTheGlobalTable) + { + const std::string_view script = R"( + GLOBAL = {} + )"; + + getInternalEnvironment()["GLOBAL"] = m_solState.createTable(); + getInternalEnvironment()["GLOBAL"]["data"] = 5; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Declaring global variables is forbidden (exceptions: the functions 'init', 'interface' and 'run')! (found value of type 'table')")); + + const int val = getInternalEnvironment()["GLOBAL"]["data"]; + EXPECT_EQ(val, 5); + } + + class AEnvironmentProtection_LoadInterface : public AEnvironmentProtection + { + protected: + AEnvironmentProtection_LoadInterface() + { + EnvironmentProtection::SetEnvironmentProtectionLevel(m_protEnv, EEnvProtectionFlag::LoadInterface); + } + }; + + TEST_F(AEnvironmentProtection_LoadInterface, AllowsDeclaringInterfaceFunction) + { + const std::string_view script = R"( + function interface(IN,OUT) + return 6 + end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_TRUE(result.valid()); + + sol::protected_function iface = getInternalEnvironment()["interface"]; + + const int ifaceResult = iface(); + EXPECT_EQ(6, ifaceResult); + } + + TEST_F(AEnvironmentProtection_LoadInterface, ForbidsDeclaringUnknownFunctions) + { + const std::string_view script = R"( + function thisIsNotAllowed() + return 5 + end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), ::testing::HasSubstr("Unexpected function name 'thisIsNotAllowed'! Only 'interface' function can be declared!")); + } + + TEST_F(AEnvironmentProtection_LoadInterface, CatchesWritingToGlobalsAsError) + { + const std::string_view script = R"( + global="this generates error" + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), ::testing::HasSubstr("Declaring global variables is forbidden (exception: the 'interface' function)!")); + } + + TEST_F(AEnvironmentProtection_LoadInterface, CatchesReadingGlobalsAsError) + { + const std::string_view script = R"( + local t=_G["this generates error"] + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Trying to read global variable 'this generates error' in an interface!")); + } + + TEST_F(AEnvironmentProtection_LoadInterface, ForbidsOverwritingInterfaceFunction) + { + const std::string_view script = R"( + function interface(IN,OUT) + return 5 + end + function interface(IN,OUT) + return 6 + end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Function 'interface' can only be declared once!")); + } + + TEST_F(AEnvironmentProtection_LoadInterface, ForbidsOverwritingTheGlobalTable) + { + const std::string_view script = R"( + GLOBAL = {} + )"; + + getInternalEnvironment()["GLOBAL"] = m_solState.createTable(); + getInternalEnvironment()["GLOBAL"]["data"] = 5; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Declaring global variables is forbidden (exception: the 'interface' function)! (found value of type 'table')")); + + const int val = getInternalEnvironment()["GLOBAL"]["data"]; + EXPECT_EQ(val, 5); + } + + class AEnvironmentProtection_InitFunction: public AEnvironmentProtection + { + protected: + AEnvironmentProtection_InitFunction() + { + EnvironmentProtection::SetEnvironmentProtectionLevel(m_protEnv, EEnvProtectionFlag::InitFunction); + } + }; + + TEST_F(AEnvironmentProtection_InitFunction, AllowsDeclaringLocalFunctions) + { + const std::string_view script = R"( + local fun = function () + return 5 + end + return fun + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_TRUE(result.valid()); + + sol::protected_function fun = result; + + const int funcResult = fun(); + + EXPECT_EQ(5, funcResult); + } + + TEST_F(AEnvironmentProtection_InitFunction, ForbidsDeclaringGlobalFunctions) + { + const std::string_view script = R"( + function thisIsNotAllowed() + return 5 + end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), ::testing::HasSubstr("Unexpected global variable definition 'thisIsNotAllowed' in init()! Please use the GLOBAL table to declare global data and functions, or use modules!")); + } + + TEST_F(AEnvironmentProtection_InitFunction, CatchesWritingToGlobalsAsError) + { + const std::string_view script = R"( + global="this generates error" + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), ::testing::HasSubstr("Unexpected global variable definition 'global' in init()! Please use the GLOBAL table to declare global data and functions, or use modules!")); + } + + TEST_F(AEnvironmentProtection_InitFunction, CatchesReadingGlobalsAsError) + { + const std::string_view script = R"( + local t=_G["this generates error"] + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr( + "Trying to read global variable 'this generates error' in the init() function! " + "This can cause undefined behavior and is forbidden! Use the GLOBAL table to read/write global data!")); + } + + TEST_F(AEnvironmentProtection_InitFunction, AllowsReadingPredefinedGlobalsTable) + { + const std::string_view script = R"( + return GLOBAL.data + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + getInternalEnvironment()["GLOBAL"] = m_solState.createTable(); + getInternalEnvironment()["GLOBAL"]["data"] = 5; + + sol::protected_function_result result = loadedScript(); + ASSERT_TRUE(result.valid()); + const int resultData = result; + EXPECT_EQ(resultData, 5); + } + + TEST_F(AEnvironmentProtection_InitFunction, ForbidsOverwritingGlobalTable) + { + const std::string_view script = R"( + GLOBAL = {data = 42} + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + getInternalEnvironment()["GLOBAL"] = m_solState.createTable(); + getInternalEnvironment()["GLOBAL"]["data"] = 5; + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Trying to override the GLOBAL table in init()! You can only add data, but not overwrite the table!")); + } + + TEST_F(AEnvironmentProtection_InitFunction, ForbidsDeletingGlobalTable) + { + getInternalEnvironment()["GLOBAL"] = m_solState.createTable(); + getInternalEnvironment()["GLOBAL"]["data"] = 5; + + const std::string_view script = R"( + GLOBAL = nil + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Trying to override the GLOBAL table in init()! You can only add data, but not overwrite the table!")); + } + + TEST_F(AEnvironmentProtection_InitFunction, AllowsAddingDataToGlobalTable_DoesNotOverwriteExistingData) + { + const std::string_view script = R"( + GLOBAL.moreData = 15 + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + m_protEnv.raw_set("GLOBAL", m_solState.createTable()); + m_protEnv["GLOBAL"]["data"] = 5; + + loadedScript(); + const int existingData = m_protEnv["GLOBAL"]["data"]; + const int moreData = m_protEnv["GLOBAL"]["moreData"]; + EXPECT_EQ(existingData, 5); + EXPECT_EQ(moreData, 15); + } + + class AEnvironmentProtection_InterfaceFunctionInScript : public AEnvironmentProtection + { + protected: + AEnvironmentProtection_InterfaceFunctionInScript() + { + EnvironmentProtection::SetEnvironmentProtectionLevel(m_protEnv, EEnvProtectionFlag::InterfaceFunctionInScript); + } + }; + + TEST_F(AEnvironmentProtection_InterfaceFunctionInScript, ForbidsDeclaringGlobalFunctions) + { + const std::string_view script = R"( + function thisIsNotAllowed() + return 5 + end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr( + "Unexpected global variable definition 'thisIsNotAllowed' in interface()! " + "Use the GLOBAL table inside the init() function to declare global data and functions, or use modules!")); + } + + TEST_F(AEnvironmentProtection_InterfaceFunctionInScript, CatchesWritingToGlobalsAsError) + { + const std::string_view script = R"( + global="this generates error" + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr( + "Unexpected global variable definition 'global' in interface()! " + "Use the GLOBAL table inside the init() function to declare global data and functions, or use modules!")); + } + + TEST_F(AEnvironmentProtection_InterfaceFunctionInScript, CatchesReadingGlobalsAsError) + { + const std::string_view script = R"( + local t=_G["this generates error"] + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Unexpected global access to key 'this generates error' in interface()! Only 'GLOBAL' and 'Type' are allowed as a key")); + } + + TEST_F(AEnvironmentProtection_InterfaceFunctionInScript, AllowsReadingPredefinedGlobalsTable) + { + const std::string_view script = R"( + return GLOBAL.data + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + getInternalEnvironment()["GLOBAL"] = m_solState.createTable(); + getInternalEnvironment()["GLOBAL"]["data"] = 5; + + sol::protected_function_result result = loadedScript(); + ASSERT_TRUE(result.valid()); + const int resultData = result; + EXPECT_EQ(resultData, 5); + } + + class AEnvironmentProtection_InterfaceFunctionInInterface : public AEnvironmentProtection + { + protected: + AEnvironmentProtection_InterfaceFunctionInInterface() + { + EnvironmentProtection::SetEnvironmentProtectionLevel(m_protEnv, EEnvProtectionFlag::InterfaceFunctionInInterface); + } + }; + + TEST_F(AEnvironmentProtection_InterfaceFunctionInInterface, ForbidsDeclaringGlobalFunctions) + { + const std::string_view script = R"( + function thisIsNotAllowed() + return 5 + end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Unexpected variable definition 'thisIsNotAllowed' in interface()!")); + } + + TEST_F(AEnvironmentProtection_InterfaceFunctionInInterface, CatchesWritingToGlobalsAsError) + { + const std::string_view script = R"( + global="this generates error" + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Unexpected variable definition 'global' in interface()!")); + } + + TEST_F(AEnvironmentProtection_InterfaceFunctionInInterface, CatchesReadingGlobalsAsError) + { + const std::string_view script = R"( + local t=_G["this generates error"] + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Unexpected global access to key 'this generates error' in interface()!")); + } + + TEST_F(AEnvironmentProtection_InterfaceFunctionInInterface, ForbidsReadingPredefinedGlobalsTable) + { + const std::string_view script = R"( + return GLOBAL.data + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Unexpected global access to key 'GLOBAL' in interface()!")); + } + + class AEnvironmentProtection_RunFunction : public AEnvironmentProtection + { + protected: + AEnvironmentProtection_RunFunction() + { + EnvironmentProtection::SetEnvironmentProtectionLevel(m_protEnv, EEnvProtectionFlag::RunFunction); + } + }; + + TEST_F(AEnvironmentProtection_RunFunction, ForbidsDeclaringGlobalFunctions) + { + const std::string_view script = R"( + function thisIsNotAllowed() + return 5 + end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Unexpected global variable definition 'thisIsNotAllowed' in run()! Use the init() function to declare global data and functions, or use modules!")); + } + + TEST_F(AEnvironmentProtection_RunFunction, CatchesWritingToGlobalsAsError) + { + const std::string_view script = R"( + global="this generates error" + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Unexpected global variable definition 'global' in run()! Use the init() function to declare global data and functions, or use modules!")); + } + + TEST_F(AEnvironmentProtection_RunFunction, CatchesReadingGlobalsAsError) + { + const std::string_view script = R"( + local t=_G["this generates error"] + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Unexpected global access to key 'this generates error' in run()! Only 'GLOBAL' is allowed as a key")); + } + + TEST_F(AEnvironmentProtection_RunFunction, AllowsReadingPredefinedGlobalsTable) + { + const std::string_view script = R"( + return GLOBAL.data + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + getInternalEnvironment()["GLOBAL"] = m_solState.createTable(); + getInternalEnvironment()["GLOBAL"]["data"] = 5; + + sol::protected_function_result result = loadedScript(); + ASSERT_TRUE(result.valid()); + const int resultData = result; + EXPECT_EQ(resultData, 5); + } + + TEST_F(AEnvironmentProtection_RunFunction, ForbidsOverwritingGlobalTable) + { + const std::string_view script = R"( + GLOBAL = {data = 42} + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + getInternalEnvironment()["GLOBAL"] = m_solState.createTable(); + getInternalEnvironment()["GLOBAL"]["data"] = 5; + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Trying to override the GLOBAL table in run()! You can only read data, but not overwrite the table!")); + } + + class AEnvironmentProtection_Module: public AEnvironmentProtection + { + protected: + AEnvironmentProtection_Module() + { + EnvironmentProtection::SetEnvironmentProtectionLevel(m_protEnv, EEnvProtectionFlag::Module); + } + }; + + TEST_F(AEnvironmentProtection_Module, ForbidsDeclaringGlobalFunctions) + { + const std::string_view script = R"( + function thisIsNotAllowed() + return 5 + end + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Declaring global variables is forbidden in modules! (found value of type 'function' assigned to variable 'thisIsNotAllowed')")); + } + + TEST_F(AEnvironmentProtection_Module, CatchesWritingToGlobalsAsError) + { + const std::string_view script = R"( + global="this generates error" + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Declaring global variables is forbidden in modules! (found value of type 'string' assigned to variable 'global')")); + } + + TEST_F(AEnvironmentProtection_Module, CatchesReadingGlobalsAsError) + { + const std::string_view script = R"( + local t=_G["this generates error"] + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Trying to read global variable 'this generates error' in module! This can cause undefined behavior and is forbidden!")); + } + + // This never happens in practice, because modules live in different environment than the + // script which has the GLOBAL table. Still, this test makes sure that even if the symbol leaked + // somehow, it's still not accessible in the module + TEST_F(AEnvironmentProtection_Module, DoesNotAllowReadingPredefinedGlobalsTable) + { + const std::string_view script = R"( + return GLOBAL.data + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + getInternalEnvironment()["GLOBAL"] = m_solState.createTable(); + getInternalEnvironment()["GLOBAL"]["data"] = 5; + + sol::protected_function_result result = loadedScript(); + ASSERT_TRUE(result.valid()); + const int resultData = result; + EXPECT_EQ(resultData, 5); + } + + TEST_F(AEnvironmentProtection_Module, ForbidsOverwritingTypeTable) + { + const std::string_view script = R"( + Type = {} + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + m_protEnv.set_on(loadedScript); + + getInternalEnvironment()["Type"] = m_solState.createTable(); + getInternalEnvironment()["Type"]["Int"] = [](){return 5;}; + + sol::protected_function_result result = loadedScript(); + ASSERT_FALSE(result.valid()); + sol::error error = result; + EXPECT_THAT(error.what(), + ::testing::HasSubstr("Special global 'Type' symbol should not be overwritten in modules!")); + } +} diff --git a/client/logic/unittests/internal/ErrorReportingTest.cpp b/client/logic/unittests/internal/ErrorReportingTest.cpp new file mode 100644 index 000000000..7eff953ff --- /dev/null +++ b/client/logic/unittests/internal/ErrorReportingTest.cpp @@ -0,0 +1,107 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" + +#include "internals/ErrorReporting.h" +#include "ramses-logic/Logger.h" +#include "LogicNodeDummy.h" + +namespace ramses::internal +{ + class AErrorReporting : public ::testing::Test + { + protected: + AErrorReporting() + { + // Explicitly check that default logging does not affect custom error logs + Logger::SetDefaultLogging(false); + + Logger::SetLogHandler([this](ELogLevel type, std::string_view message) { + EXPECT_EQ(ELogLevel::Error, type); + m_loggedErrors.emplace_back(std::string(message)); + }); + } + + void TearDown() override + { + // Unset custom logger to avoid interference with other tests which use logs + Logger::SetLogHandler({}); + } + + ErrorReporting m_errorReporting; + + std::vector m_loggedErrors; + }; + + TEST_F(AErrorReporting, ProducesNoErrorsDuringConstruction) + { + EXPECT_EQ(0u, m_errorReporting.getErrors().size()); + } + + TEST_F(AErrorReporting, ProducesNoLogsDuringConstruction) + { + EXPECT_EQ(0u, m_loggedErrors.size()); + } + + TEST_F(AErrorReporting, StoresSourceLogicObjectWhenProvided) + { + class TestObject : public LogicObject + { + public: + TestObject() : LogicObject(std::make_unique("", 0)) + { + } + }; + TestObject object1; + TestObject object2; + + m_errorReporting.add("error 1", &object1, EErrorType::ContentStateError); + m_errorReporting.add("error 2", &object2, EErrorType::BinaryVersionMismatch); + + ASSERT_EQ(m_errorReporting.getErrors().size(), 2u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "error 1"); + EXPECT_EQ(m_errorReporting.getErrors()[0].object, &object1); + EXPECT_EQ(m_errorReporting.getErrors()[0].type, EErrorType::ContentStateError); + EXPECT_EQ(m_errorReporting.getErrors()[1].message, "error 2"); + EXPECT_EQ(m_errorReporting.getErrors()[1].object, &object2); + EXPECT_EQ(m_errorReporting.getErrors()[1].type, EErrorType::BinaryVersionMismatch); + } + + TEST_F(AErrorReporting, StoresErrorsInTheOrderAdded) + { + m_errorReporting.add("error 1", nullptr, EErrorType::IllegalArgument); + m_errorReporting.add("error 2", nullptr, EErrorType::IllegalArgument); + + ASSERT_EQ(m_errorReporting.getErrors().size(), 2u); + EXPECT_EQ(m_errorReporting.getErrors()[0].message, "error 1"); + EXPECT_EQ(m_errorReporting.getErrors()[1].message, "error 2"); + } + + TEST_F(AErrorReporting, LogsErrorsInTheOrderAdded) + { + m_errorReporting.add("error 1", nullptr, EErrorType::IllegalArgument); + m_errorReporting.add("error 2", nullptr, EErrorType::IllegalArgument); + + ASSERT_EQ(m_loggedErrors.size(), 2u); + EXPECT_EQ(m_loggedErrors[0], "error 1"); + EXPECT_EQ(m_loggedErrors[1], "error 2"); + + } + + TEST_F(AErrorReporting, ClearsErrors) + { + m_errorReporting.add("error 1", nullptr, EErrorType::IllegalArgument); + + ASSERT_EQ(1u, m_errorReporting.getErrors().size()); + + m_errorReporting.clear(); + + ASSERT_TRUE(m_errorReporting.getErrors().empty()); + } +} diff --git a/client/logic/unittests/internal/LogicNodeDependenciesTest.cpp b/client/logic/unittests/internal/LogicNodeDependenciesTest.cpp new file mode 100644 index 000000000..ed57475c1 --- /dev/null +++ b/client/logic/unittests/internal/LogicNodeDependenciesTest.cpp @@ -0,0 +1,502 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "internals/LogicNodeDependencies.h" +#include "internals/ErrorReporting.h" +#include "LogicNodeDummy.h" + +namespace ramses::internal +{ + class ALogicNodeDependencies : public ::testing::Test + { + protected: + void expectSortedNodeOrder(std::initializer_list nodes) + { + EXPECT_THAT(*m_dependencies.getTopologicallySortedNodes(), ::testing::ElementsAreArray(nodes)); + } + + void expectUnsortedNodeOrder(std::initializer_list nodes) + { + EXPECT_THAT(*m_dependencies.getTopologicallySortedNodes(), ::testing::UnorderedElementsAreArray(nodes)); + } + + static void expectLink(const PropertyImpl& output, std::initializer_list inputs) + { + // check that linked destinations all point to source + for (auto& input : inputs) + { + EXPECT_EQ(&output, input.property->getIncomingLink().property); + EXPECT_EQ(input.isWeakLink, input.property->getIncomingLink().isWeakLink); + } + // check that source points to all destinations + ASSERT_EQ(output.getOutgoingLinks().size(), inputs.size()); + auto srcOutputIt = output.getOutgoingLinks().cbegin(); + for (auto& input : inputs) + { + EXPECT_EQ(srcOutputIt->property, input.property); + EXPECT_EQ(srcOutputIt->isWeakLink, input.isWeakLink); + ++srcOutputIt; + } + } + + static void expectNoLinks(const PropertyImpl& property) + { + if (property.isInput()) + { + EXPECT_EQ(nullptr, property.getIncomingLink().property); + } + if (property.isOutput()) + { + EXPECT_TRUE(property.getOutgoingLinks().empty()); + } + } + + LogicNodeDummyImpl m_nodeA{ "A", false }; + LogicNodeDummyImpl m_nodeB{ "B", false }; + + LogicNodeDependencies m_dependencies; + ErrorReporting m_errorReporting; + }; + + TEST_F(ALogicNodeDependencies, IsEmptyAfterConstruction) + { + EXPECT_TRUE((*m_dependencies.getTopologicallySortedNodes()).empty()); + } + + TEST_F(ALogicNodeDependencies, RemovingNode_RemovesItFromTopologyList) + { + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(m_nodeB); + m_dependencies.removeNode(m_nodeA); + + expectSortedNodeOrder({&m_nodeB}); + } + + TEST_F(ALogicNodeDependencies, SingleDisconnectedNode) + { + m_dependencies.addNode(m_nodeA); + expectSortedNodeOrder({&m_nodeA}); + } + + TEST_F(ALogicNodeDependencies, ConnectingTwoNodes_CreatesALink) + { + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(m_nodeB); + + PropertyImpl& output = *m_nodeA.getOutputs()->getChild("output1")->m_impl; + PropertyImpl& input = *m_nodeB.getInputs()->getChild("input1")->m_impl; + + EXPECT_TRUE(m_dependencies.link(output, input, false, m_errorReporting)); + + // Sorted topologically + expectSortedNodeOrder({ &m_nodeA, &m_nodeB }); + + // Has exactly one link + expectLink(output, { {&input, false} }); + } + + TEST_F(ALogicNodeDependencies, ConnectingTwoNodes_CreatesABackLink) + { + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(m_nodeB); + + PropertyImpl& output = *m_nodeA.getOutputs()->getChild("output1")->m_impl; + PropertyImpl& input = *m_nodeB.getInputs()->getChild("input1")->m_impl; + + EXPECT_TRUE(m_dependencies.link(output, input, true, m_errorReporting)); + + // Back links do not affect topological order + expectUnsortedNodeOrder({ &m_nodeA, &m_nodeB }); + + // Has exactly one link + expectLink(output, { {&input, true} }); + } + + TEST_F(ALogicNodeDependencies, DisconnectingTwoNodes_RemovesLinks) + { + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(m_nodeB); + + PropertyImpl& output = *m_nodeA.getOutputs()->getChild("output1")->m_impl; + PropertyImpl& input = *m_nodeB.getInputs()->getChild("input1")->m_impl; + + EXPECT_TRUE(m_dependencies.link(output, input, false, m_errorReporting)); + EXPECT_TRUE(m_dependencies.unlink(output, input, m_errorReporting)); + + // both nodes still there, but no ordering guarantees without the link + expectUnsortedNodeOrder({ &m_nodeA, &m_nodeB }); + + expectNoLinks(input); + expectNoLinks(output); + } + + TEST_F(ALogicNodeDependencies, RemovingSourceNode_RemovesLinks) + { + auto nodeToDelete = std::make_unique("node"); + + m_dependencies.addNode(*nodeToDelete); + m_dependencies.addNode(m_nodeB); + + PropertyImpl& input = *m_nodeB.getInputs()->getChild("input1")->m_impl; + EXPECT_TRUE(m_dependencies.link(*nodeToDelete->getOutputs()->getChild("output1")->m_impl, input, false, m_errorReporting)); + + m_dependencies.removeNode(*nodeToDelete); + nodeToDelete = nullptr; + + // only target node left + expectSortedNodeOrder({ &m_nodeB }); + expectNoLinks(input); + } + + TEST_F(ALogicNodeDependencies, RemovingTargetNode_RemovesLinks) + { + auto nodeToDelete = std::make_unique("node"); + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(*nodeToDelete); + + PropertyImpl& output = *m_nodeA.getOutputs()->getChild("output1")->m_impl; + EXPECT_TRUE(m_dependencies.link(output, *nodeToDelete->getInputs()->getChild("input1")->m_impl, false, m_errorReporting)); + + m_dependencies.removeNode(*nodeToDelete); + nodeToDelete = nullptr; + + // only source node left + expectSortedNodeOrder({ &m_nodeA }); + expectNoLinks(output); + } + + TEST_F(ALogicNodeDependencies, RemovingMiddleNode_DoesNotAffectRelativeOrderOfOtherNodes) + { + auto nodeToDelete = std::make_unique("M", false); + + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(*nodeToDelete); + m_dependencies.addNode(m_nodeB); + + PropertyImpl& output1A = *m_nodeA.getOutputs()->getChild("output1")->m_impl; + PropertyImpl& output2A = *m_nodeA.getOutputs()->getChild("output2")->m_impl; + PropertyImpl& output1M = *nodeToDelete->getOutputs()->getChild("output1")->m_impl; + PropertyImpl& input1M = *nodeToDelete->getInputs()->getChild("input1")->m_impl; + PropertyImpl& input1B = *m_nodeB.getInputs()->getChild("input1")->m_impl; + PropertyImpl& input2B = *m_nodeB.getInputs()->getChild("input2")->m_impl; + + // A -> M -> B + // \ / + // ---->------- + EXPECT_TRUE(m_dependencies.link(output1A, input1M, false, m_errorReporting)); + EXPECT_TRUE(m_dependencies.link(output1M, input1B, false, m_errorReporting)); + EXPECT_TRUE(m_dependencies.link(output2A, input2B, false, m_errorReporting)); + + expectSortedNodeOrder({ &m_nodeA, nodeToDelete.get(), &m_nodeB }); + + m_dependencies.removeNode(*nodeToDelete); + nodeToDelete = nullptr; + + // only other two nodes left (A and B). Their relative order is not changed + expectSortedNodeOrder({ &m_nodeA, &m_nodeB }); + + // Only link A->B remains + expectLink(output2A, { { &input2B, false } }); + + // Other links are gone + expectNoLinks(output1A); + expectNoLinks(input1B); + } + + TEST_F(ALogicNodeDependencies, ReversingDependencyOfTwoNodes_InvertsTopologicalOrder) + { + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(m_nodeB); + + // Node A -> Node B (output of node A linked to input of node B) + PropertyImpl& inputB = *m_nodeB.getInputs()->getChild("input1")->m_impl; + PropertyImpl& outputA = *m_nodeA.getOutputs()->getChild("output1")->m_impl; + + EXPECT_TRUE(m_dependencies.link(outputA, inputB, false, m_errorReporting)); + expectSortedNodeOrder({ &m_nodeA, &m_nodeB }); + + // Reverse dependency + // Node B -> Node A (output of node B linked to input of node A) + EXPECT_TRUE(m_dependencies.unlink(outputA, inputB, m_errorReporting)); + PropertyImpl& inputA = *m_nodeA.getInputs()->getChild("input1")->m_impl; + PropertyImpl& outputB = *m_nodeB.getOutputs()->getChild("output1")->m_impl; + + EXPECT_TRUE(m_dependencies.link(outputB, inputA, false, m_errorReporting)); + + // Still no disconnected nodes, but now topological order is B -> A + expectSortedNodeOrder({ &m_nodeB, &m_nodeA }); + + // Has exactly one link + expectLink(outputB, { { &inputA, false } }); + // Other links are gone + expectNoLinks(inputB); + expectNoLinks(outputA); + } + + TEST_F(ALogicNodeDependencies, ReversingDependencyOfTwoNodes_UsingBackLink_DoesNotAffectTopologicalOrder) + { + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(m_nodeB); + + // Node A -> Node B (output of node A linked to input of node B) + PropertyImpl& inputB = *m_nodeB.getInputs()->getChild("input1")->m_impl; + PropertyImpl& outputA = *m_nodeA.getOutputs()->getChild("output1")->m_impl; + + EXPECT_TRUE(m_dependencies.link(outputA, inputB, false, m_errorReporting)); + expectSortedNodeOrder({ &m_nodeA, &m_nodeB }); + + // Reverse dependency but only using back link + // Node B -> Node A (output of node B linked to input of node A) + EXPECT_TRUE(m_dependencies.unlink(outputA, inputB, m_errorReporting)); + PropertyImpl& inputA = *m_nodeA.getInputs()->getChild("input1")->m_impl; + PropertyImpl& outputB = *m_nodeB.getOutputs()->getChild("output1")->m_impl; + + EXPECT_TRUE(m_dependencies.link(outputB, inputA, true, m_errorReporting)); + + // Nodes are linked only using back link, this has no effect on topological order + expectUnsortedNodeOrder({ &m_nodeB, &m_nodeA }); + + // Has exactly one link + expectLink(outputB, { { &inputA, true } }); + // Other links are gone + expectNoLinks(inputB); + expectNoLinks(outputA); + } + + TEST_F(ALogicNodeDependencies, NodesConnectedInBothDirectionsFormingCycleWithBackLink) + { + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(m_nodeB); + + PropertyImpl& inputA = *m_nodeA.getInputs()->getChild("input1")->m_impl; + PropertyImpl& outputA = *m_nodeA.getOutputs()->getChild("output1")->m_impl; + PropertyImpl& inputB = *m_nodeB.getInputs()->getChild("input1")->m_impl; + PropertyImpl& outputB = *m_nodeB.getOutputs()->getChild("output1")->m_impl; + + // Node A -> Node B (output of node A linked to input of node B) + EXPECT_TRUE(m_dependencies.link(outputA, inputB, false, m_errorReporting)); + expectSortedNodeOrder({ &m_nodeA, &m_nodeB }); + + // connect in reverse direction using back link + EXPECT_TRUE(m_dependencies.link(outputB, inputA, true, m_errorReporting)); + expectSortedNodeOrder({ &m_nodeA, &m_nodeB }); + + expectLink(outputA, { { &inputB, false } }); + expectLink(outputB, { { &inputA, true } }); + } + + class ALogicNodeDependencies_NestedLinks : public ALogicNodeDependencies + { + protected: + + ALogicNodeDependencies_NestedLinks() + { + m_dependencies.addNode(*m_nodeANested); + m_dependencies.addNode(*m_nodeBNested); + + m_nestedOutputA = m_nodeANested->getOutputs()->getChild("outputStruct")->getChild("nested")->m_impl.get(); + m_nestedInputB = m_nodeBNested->getInputs()->getChild("inputStruct")->getChild("nested")->m_impl.get(); + m_arrayOutputA = m_nodeANested->getOutputs()->getChild("outputArray")->getChild(0)->m_impl.get(); + m_arrayInputB = m_nodeBNested->getInputs()->getChild("inputArray")->getChild(0)->m_impl.get(); + } + + std::unique_ptr m_nodeANested = std::make_unique( "A", true ); + std::unique_ptr m_nodeBNested = std::make_unique( "B", true ); + + PropertyImpl* m_nestedOutputA; + PropertyImpl* m_nestedInputB; + PropertyImpl* m_arrayOutputA; + PropertyImpl* m_arrayInputB; + }; + + TEST_F(ALogicNodeDependencies_NestedLinks, ReportsErrorWhenUnlinkingStructInputs_BasedOnTheirType) + { + PropertyImpl* structProperty = m_nodeBNested->getInputs()->getChild("inputStruct")->m_impl.get(); + EXPECT_FALSE(m_dependencies.unlink(*m_nestedOutputA, *structProperty, m_errorReporting)); + EXPECT_EQ("Can't unlink properties of complex types directly!", m_errorReporting.getErrors()[0].message); + } + + TEST_F(ALogicNodeDependencies_NestedLinks, ReportsErrorWhenUnlinkingArrayInputs_BasedOnTheirType) + { + PropertyImpl* arrayProperty = m_nodeBNested->getInputs()->getChild("inputArray")->m_impl.get(); + EXPECT_FALSE(m_dependencies.unlink(*m_nestedOutputA, *arrayProperty, m_errorReporting)); + EXPECT_EQ("Can't unlink properties of complex types directly!", m_errorReporting.getErrors()[0].message); + } + + TEST_F(ALogicNodeDependencies_NestedLinks, ReportsErrorWhenUnlinkingStructs_WithLinkedChildren) + { + EXPECT_TRUE(m_dependencies.link(*m_nestedOutputA, *m_nestedInputB, false, m_errorReporting)); + EXPECT_TRUE(m_errorReporting.getErrors().empty()); + + // Still can't unlink complex type + PropertyImpl* outputParentStruct = m_nodeANested->getOutputs()->getChild("outputStruct")->m_impl.get(); + PropertyImpl* inputParentStruct = m_nodeBNested->getInputs()->getChild("inputStruct")->m_impl.get(); + EXPECT_FALSE(m_dependencies.unlink(*outputParentStruct, *inputParentStruct, m_errorReporting)); + EXPECT_EQ("Can't unlink properties of complex types directly!", m_errorReporting.getErrors()[0].message); + } + + TEST_F(ALogicNodeDependencies_NestedLinks, ConnectingTwoNodes_CreatesALink) + { + EXPECT_TRUE(m_dependencies.link(*m_nestedOutputA, *m_nestedInputB, false, m_errorReporting)); + + // Sorted topologically + expectSortedNodeOrder({m_nodeANested.get(), m_nodeBNested.get()}); + + // Has exactly one link + expectLink(*m_nestedOutputA, { { m_nestedInputB, false } }); + } + + TEST_F(ALogicNodeDependencies_NestedLinks, DisconnectingTwoNodes_RemovesLinks) + { + EXPECT_TRUE(m_dependencies.link(*m_nestedOutputA, *m_nestedInputB, false, m_errorReporting)); + EXPECT_TRUE(m_dependencies.unlink(*m_nestedOutputA, *m_nestedInputB, m_errorReporting)); + + // both nodes still there, but no ordering guarantees without the link + expectUnsortedNodeOrder({ m_nodeANested.get(), m_nodeBNested.get() }); + + expectNoLinks(*m_nestedOutputA); + expectNoLinks(*m_nestedInputB); + } + + TEST_F(ALogicNodeDependencies_NestedLinks, RemovingSourceNode_RemovesLinks) + { + EXPECT_TRUE(m_dependencies.link(*m_nestedOutputA, *m_nestedInputB, false, m_errorReporting)); + + m_dependencies.removeNode(*m_nodeANested); + m_nodeANested = nullptr; + + // only target node left + expectSortedNodeOrder({ m_nodeBNested.get() }); + expectNoLinks(*m_nestedInputB); + } + + TEST_F(ALogicNodeDependencies_NestedLinks, RemovingTargetNode_RemovesLinks) + { + EXPECT_TRUE(m_dependencies.link(*m_nestedOutputA, *m_nestedInputB, false, m_errorReporting)); + + m_dependencies.removeNode(*m_nodeBNested); + m_nodeBNested = nullptr; + + // only source node left + expectSortedNodeOrder({ m_nodeANested.get() }); + + // No links + expectNoLinks(*m_nestedOutputA); + } + + TEST_F(ALogicNodeDependencies_NestedLinks, ReversingDependencyOfTwoNodes_InvertsTopologicalOrder) + { + EXPECT_TRUE(m_dependencies.link(*m_nestedOutputA, *m_nestedInputB, false, m_errorReporting)); + expectSortedNodeOrder({ m_nodeANested.get(), m_nodeBNested.get() }); + + // Reverse dependency + // Node B -> Node A (output of node B linked to input of node A) + EXPECT_TRUE(m_dependencies.unlink(*m_nestedOutputA, *m_nestedInputB, m_errorReporting)); + PropertyImpl& nestedInputA = *m_nodeANested->getInputs()->getChild("inputStruct")->getChild("nested")->m_impl; + PropertyImpl& nestedOutputB = *m_nodeBNested->getOutputs()->getChild("outputStruct")->getChild("nested")->m_impl; + + EXPECT_TRUE(m_dependencies.link(nestedOutputB, nestedInputA, false, m_errorReporting)); + + // Still no disconnected nodes, but now topological order is B -> A + expectSortedNodeOrder({ m_nodeBNested.get(), m_nodeANested.get() }); + + // Has exactly one link + expectLink(nestedOutputB, { { &nestedInputA, false } }); + expectNoLinks(*m_nestedOutputA); + expectNoLinks(*m_nestedInputB); + } + + TEST_F(ALogicNodeDependencies, AddsDependencyToBinding) + { + RamsesBindingDummyImpl binding; + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(binding); + + m_dependencies.addBindingDependency(binding, m_nodeA); + + expectSortedNodeOrder({ &binding, &m_nodeA }); + } + + TEST_F(ALogicNodeDependencies, AddsDependencyToBinding_WithLinkedNodes) + { + RamsesBindingDummyImpl binding; + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(m_nodeB); + m_dependencies.addNode(binding); + + PropertyImpl& output = *m_nodeA.getOutputs()->getChild("output1")->m_impl; + PropertyImpl& input = *m_nodeB.getInputs()->getChild("input1")->m_impl; + EXPECT_TRUE(m_dependencies.link(output, input, false, m_errorReporting)); + + m_dependencies.addBindingDependency(binding, m_nodeA); + + // Sorted topologically + expectSortedNodeOrder({ &binding, &m_nodeA, &m_nodeB }); + + // Has exactly one link + expectLink(output, { {&input, false} }); + } + + TEST_F(ALogicNodeDependencies, RemovesDependencyToBinding) + { + RamsesBindingDummyImpl binding; + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(binding); + + m_dependencies.addBindingDependency(binding, m_nodeA); + expectSortedNodeOrder({ &binding, &m_nodeA }); + + m_dependencies.removeBindingDependency(binding, m_nodeA); + expectUnsortedNodeOrder({ &binding, &m_nodeA }); + } + + TEST_F(ALogicNodeDependencies, RemovesDependencyToBindingAfterNodeRemoved) + { + RamsesBindingDummyImpl binding; + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(binding); + + m_dependencies.addBindingDependency(binding, m_nodeA); + expectSortedNodeOrder({ &binding, &m_nodeA }); + + m_dependencies.removeNode(m_nodeA); + expectUnsortedNodeOrder({ &binding }); + } + + TEST_F(ALogicNodeDependencies, RemovesDependencyToBindingAfterBindingRemoved) + { + RamsesBindingDummyImpl binding; + m_dependencies.addNode(m_nodeA); + m_dependencies.addNode(binding); + + m_dependencies.addBindingDependency(binding, m_nodeA); + expectSortedNodeOrder({ &binding, &m_nodeA }); + + m_dependencies.removeNode(binding); + expectUnsortedNodeOrder({ &m_nodeA }); + } + + TEST_F(ALogicNodeDependencies, RespectsSwappedDependencyBetweenBindings) + { + RamsesBindingDummyImpl binding1; + RamsesBindingDummyImpl binding2; + m_dependencies.addNode(binding1); + m_dependencies.addNode(binding2); + + m_dependencies.addBindingDependency(binding1, binding2); + expectSortedNodeOrder({ &binding1, &binding2 }); + + m_dependencies.removeBindingDependency(binding1, binding2); + + m_dependencies.addBindingDependency(binding2, binding1); + expectSortedNodeOrder({ &binding2, &binding1 }); + } +} diff --git a/client/logic/unittests/internal/LuaCustomizationsTest.cpp b/client/logic/unittests/internal/LuaCustomizationsTest.cpp new file mode 100644 index 000000000..308dec612 --- /dev/null +++ b/client/logic/unittests/internal/LuaCustomizationsTest.cpp @@ -0,0 +1,1040 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" + +#include "ramses-logic/Property.h" +#include "impl/PropertyImpl.h" +#include "internals/LuaCustomizations.h" +#include "internals/WrappedLuaProperty.h" +#include "internals/LuaCompilationUtils.h" +#include "internals/ErrorReporting.h" +#include "internals/PropertyTypeExtractor.h" +#include "glm/gtx/range.hpp" +#include + +namespace ramses::internal +{ + enum class EWrappedType + { + RuntimeProperty, + Extractor + }; + + class TheLuaCustomizations: public ::testing::Test + { + protected: + TheLuaCustomizations() + { + m_sol.open_libraries(); + LuaCustomizations::RegisterTypes(m_sol); + + m_interfaceEnvironment = sol::environment(m_sol, sol::create, m_sol.globals()); + PropertyTypeExtractor::RegisterTypes(m_interfaceEnvironment); + + m_structProp.getChild("field1")->m_impl->setValue(5); + m_structProp.getChild("field2")->m_impl->setValue(6); + + m_arrayProp.getChild(0)->m_impl->setValue(11); + m_arrayProp.getChild(1)->m_impl->setValue(12); + m_arrayProp.getChild(2)->m_impl->setValue(13); + } + + sol::protected_function_result run_WithResult(std::string_view source) + { + sol::protected_function loaded = m_sol.load(source); + assert(loaded.valid()); + return loaded(); + } + + sol::protected_function_result run_WithResult_InEnv(std::string_view source) + { + sol::protected_function loaded = m_sol.load(source); + m_interfaceEnvironment.set_on(loaded); + assert(loaded.valid()); + return loaded(); + } + + void expectNoErrors(std::string_view source) + { + sol::protected_function_result loaded = run_WithResult(source); + ASSERT_TRUE (loaded.valid()); + } + + void expectError(std::string_view source, std::string_view errorSubstring) + { + sol::protected_function_result loaded = run_WithResult(source); + ASSERT_FALSE(loaded.valid()); + sol::error e = loaded; + EXPECT_THAT(e.what(), ::testing::HasSubstr(errorSubstring)); + } + + void expectNoErrors_WithEnv(std::string_view source) + { + sol::protected_function_result loaded = run_WithResult_InEnv(source); + ASSERT_TRUE (loaded.valid()); + } + + void expectError_WithEnv(std::string_view source, std::string_view errorSubstring) + { + sol::protected_function_result loaded = run_WithResult_InEnv(source); + ASSERT_FALSE(loaded.valid()); + sol::error e = loaded; + EXPECT_THAT(e.what(), ::testing::HasSubstr(errorSubstring)); + } + + void createTestStruct(const std::string& name, EWrappedType wrappedType) + { + switch (wrappedType) + { + case EWrappedType::RuntimeProperty: + m_sol[name] = std::ref(m_wrappedStruct); + break; + case EWrappedType::Extractor: + m_interfaceEnvironment[name] = std::ref(m_structExtractor); + break; + } + } + + void createTestArray(const std::string& name) + { + m_sol[name] = std::ref(m_wrappedArray); + } + + template + void createTestProperty(const std::string& name, const VALUE& value) + { + HierarchicalTypeData type = { MakeType("V", TYPE) }; + auto prop = std::make_unique(type, EPropertySemantics::ScriptInput); + auto wrappedProp = std::make_unique(*prop); + m_sol[name] = std::ref(*wrappedProp); + + prop->setValue(value); + + m_propertyImpls.push_back(std::move(prop)); + m_wrappedProperties.push_back(std::move(wrappedProp)); + } + + template + void createTestVec(const std::string& name) + { + using VecClassType = typename PropertyEnumToType::TYPE; + VecClassType vecData; + std::iota(begin(vecData), end(vecData), static_cast(100)); + + createTestProperty(name, vecData); + } + + sol::state m_sol; + sol::environment m_interfaceEnvironment; + // Initialize test content with dummy data + HierarchicalTypeData m_structType = { TypeData("S", EPropertyType::Struct), {MakeType("field1", EPropertyType::Int32), MakeType("field2", EPropertyType::Int32)} }; + HierarchicalTypeData m_arrayType = { MakeArray("A", 3, EPropertyType::Int32) }; + PropertyImpl m_structProp = {m_structType, EPropertySemantics::ScriptInput}; + PropertyImpl m_arrayProp = {m_arrayType, EPropertySemantics::ScriptInput}; + WrappedLuaProperty m_wrappedStruct = WrappedLuaProperty{ m_structProp }; + WrappedLuaProperty m_wrappedArray = WrappedLuaProperty{ m_arrayProp }; + PropertyTypeExtractor m_structExtractor = { "S", EPropertyType::Struct }; + // to manage ownership + std::vector> m_propertyImpls; + std::vector> m_wrappedProperties; + }; + + TEST_F(TheLuaCustomizations, RegistersFunctions) + { + EXPECT_TRUE(m_sol["rl_len"].valid()); + EXPECT_TRUE(m_sol["rl_next"].valid()); + EXPECT_TRUE(m_sol["rl_pairs"].valid()); + EXPECT_TRUE(m_sol["rl_ipairs"].valid()); + } + + class TheLuaCustomizations_Len : public TheLuaCustomizations + { + protected: + }; + + TEST_F(TheLuaCustomizations_Len, ComputesLengthOfStruct_DuringRuntime) + { + createTestStruct("S", EWrappedType::RuntimeProperty); + expectNoErrors(R"( + assert(rl_len(S) == 2) + )"); + } + + TEST_F(TheLuaCustomizations_Len, ComputesLengthOfArray_DuringRuntime) + { + createTestArray("A"); + expectNoErrors(R"( + assert(rl_len(A) == 3) + )"); + } + + TEST_F(TheLuaCustomizations_Len, ComputesLengthOfVec_DuringRuntime) + { + createTestVec("V"); + expectNoErrors(R"( + assert(rl_len(V) == 4) + )"); + } + + TEST_F(TheLuaCustomizations_Len, ComputesLengthOfStruct_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv(R"( + assert(rl_len(S) == 0) + S.newField = Type:Int32() + assert(rl_len(S) == 1) + )"); + } + + TEST_F(TheLuaCustomizations_Len, ComputesLengthOfArray_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv(R"( + S.array1 = Type:Array(3, Type:Float()) + assert(rl_len(S.array1) == 3) + S.array2 = Type:Array(2, {a = Type:Int32(), b = Type:Float()}) + assert(rl_len(S.array2) == 2) + )"); + } + + TEST_F(TheLuaCustomizations_Len, ComputesGetsLengthOfStandardTables) + { + expectNoErrors(R"( + assert(rl_len({1, 2, 3}) == 3) + )"); + } + + TEST_F(TheLuaCustomizations_Len, ProducesErrorWhenCallingCustomLengthFunctionOnBadTypes) + { + expectError("rl_len(5)", "lua: error: rl_len() called on an unsupported type 'number'"); + expectError("rl_len(\"a string\")", "lua: error: rl_len() called on an unsupported type 'string'"); + expectError("rl_len(true)", "lua: error: rl_len() called on an unsupported type 'bool'"); + } + + TEST_F(TheLuaCustomizations_Len, ProducesErrorWhenCallingCustomLengthFunctionOnUnsupportedInterfaceTypes) + { + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.v = Type:Int32() + print(rl_len(S.v)) + )", "rl_len() called on an unsupported type 'Int32'"); + + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.vec = Type:Vec2i() + print(rl_len(S.vec)) + )", "rl_len() called on an unsupported type 'Vec2i'"); + + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.s = Type:String() + print(rl_len(S.s)) + )", "rl_len() called on an unsupported type 'String'"); + } + + class TheLuaCustomizations_Next : public TheLuaCustomizations + { + }; + + TEST_F(TheLuaCustomizations_Next, IteratesOverStruct_DuringRuntime) + { + createTestStruct("S", EWrappedType::RuntimeProperty); + expectNoErrors(R"( + k,v = rl_next(S) + assert(k == 'field1') + assert(v == 5) + k,v = rl_next(S, 'field1') + assert(k == 'field2') + assert(v == 6) + k,v = rl_next(S, 'field2') + assert(k == nil) + assert(v == nil) + )"); + } + + TEST_F(TheLuaCustomizations_Next, CanBeUsedOnStructs_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv(R"( + S.field1 = Type:Int32() + S.field2 = Type:Float() + + k,v = rl_next(S) + assert(k == 'field1') + k,v = rl_next(S, 'field1') + assert(k == 'field2') + k,v = rl_next(S, 'field2') + assert(k == nil) + assert(v == nil) + )"); + } + + TEST_F(TheLuaCustomizations_Next, CanBeUsedOnArrays_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv(R"( + S.array1 = Type:Array(2, Type:Float()) + + k,v = rl_next(S.array1) + assert(k == 1) + k,v = rl_next(S.array1, 1) + assert(k == 2) + k,v = rl_next(S.array1, 2) + assert(k == nil) + assert(v == nil) + )"); + } + + TEST_F(TheLuaCustomizations_Next, IteratesOverArray_DuringRuntime) + { + createTestArray("A"); + expectNoErrors(R"( + k,v = rl_next(A) + assert(k == 1) + assert(v == 11) + k,v = rl_next(A, 1) + assert(k == 2) + assert(v == 12) + k,v = rl_next(A, 2) + assert(k == 3) + assert(v == 13) + k,v = rl_next(A, 3) + assert(k == nil) + assert(v == nil) + )"); + } + + TEST_F(TheLuaCustomizations_Next, IteratesOverVec_DuringRuntime) + { + createTestVec("V"); + expectNoErrors(R"( + k,v = rl_next(V) + assert(k == 1) + assert(v == 100) + k,v = rl_next(V, 1) + assert(k == 2) + assert(v == 101) + k,v = rl_next(V, 2) + assert(k == nil) + assert(v == nil) + )"); + + createTestVec("V"); + expectNoErrors(R"( + k,v = rl_next(V) + assert(k == 1) + assert(v == 100) + k,v = rl_next(V, 1) + assert(k == 2) + assert(v == 101) + k,v = rl_next(V, 2) + assert(k == 3) + assert(v == 102) + k,v = rl_next(V, 3) + assert(k == nil) + assert(v == nil) + )"); + + createTestVec("V"); + expectNoErrors(R"( + k,v = rl_next(V) + assert(k == 1) + assert(v == 100) + k,v = rl_next(V, 1) + assert(k == 2) + assert(v == 101) + k,v = rl_next(V, 2) + assert(k == 3) + assert(v == 102) + k,v = rl_next(V, 3) + assert(k == 4) + assert(v == 103) + k,v = rl_next(V, 4) + assert(k == nil) + assert(v == nil) + )"); + + createTestVec("V"); + expectNoErrors(R"( + k,v = rl_next(V) + assert(k == 1) + assert(v == 100) + k,v = rl_next(V, 1) + assert(k == 2) + assert(v == 101) + k,v = rl_next(V, 2) + assert(k == nil) + assert(v == nil) + )"); + + createTestVec("V"); + expectNoErrors(R"( + k,v = rl_next(V) + assert(k == 1) + assert(v == 100) + k,v = rl_next(V, 1) + assert(k == 2) + assert(v == 101) + k,v = rl_next(V, 2) + assert(k == 3) + assert(v == 102) + k,v = rl_next(V, 3) + assert(k == nil) + assert(v == nil) + )"); + + createTestVec("V"); + expectNoErrors(R"( + k,v = rl_next(V) + assert(k == 1) + assert(v == 100) + k,v = rl_next(V, 1) + assert(k == 2) + assert(v == 101) + k,v = rl_next(V, 2) + assert(k == 3) + assert(v == 102) + k,v = rl_next(V, 3) + assert(k == 4) + assert(v == 103) + k,v = rl_next(V, 4) + assert(k == nil) + assert(v == nil) + )"); + } + + TEST_F(TheLuaCustomizations_Next, ReportsErrorsWhenCalledOnWrongType) + { + expectError("rl_next('string')", "lua: error: rl_next() called on an unsupported type 'string'"); + expectError("rl_next(true)", "lua: error: rl_next() called on an unsupported type 'bool'"); + expectError("rl_next(next)", "lua: error: rl_next() called on an unsupported type 'function'"); + expectError("rl_next(rl_next)", "lua: error: rl_next() called on an unsupported type 'function'"); + } + + TEST_F(TheLuaCustomizations_Next, IteratesOverEmptyContainers_DuringInterfaceExtraction) + { + PropertyTypeExtractor structExtractorEmpty = { "S", EPropertyType::Struct }; + PropertyTypeExtractor arrayExtractorEmpty = { "A", EPropertyType::Array }; + + m_interfaceEnvironment["S"] = std::ref(structExtractorEmpty); + m_interfaceEnvironment["A"] = std::ref(arrayExtractorEmpty); + + expectNoErrors_WithEnv(R"( + k,v = rl_next(A) + assert(k == nil) + assert(v == nil) + + k,v = rl_next(S) + assert(k == nil) + assert(v == nil) + )"); + } + + TEST_F(TheLuaCustomizations_Next, ReportsErrorsWhenBadArrayIndexGiven_DuringRuntime) + { + createTestArray("A"); + + expectError("rl_next(A, 0)", "Invalid key value '0' for rl_next(). Expected a number in the range [1, 3]!"); + expectError("rl_next(A, 4)", "Invalid key value '4' for rl_next(). Expected a number in the range [1, 3]!"); + expectError("rl_next(A, 'string')", "Invalid key to rl_next() of type: Error while extracting integer: expected a number, received 'string'"); + expectError("rl_next(A, {})", "Invalid key to rl_next() of type: Error while extracting integer: expected a number, received 'table'"); + expectError("rl_next(A, true)", "Invalid key to rl_next() of type: Error while extracting integer: expected a number, received 'bool'"); + expectError("rl_next(A, 1.5)", "Invalid key to rl_next() of type: Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)"); + expectError("rl_next(A, 1.001)", "Invalid key to rl_next() of type: Error while extracting integer: implicit rounding (fractional part '0.0009999999999998899' is not negligible)"); + } + + TEST_F(TheLuaCustomizations_Next, ReportsErrorsWhenBadVecIndexGiven_DuringRuntime) + { + createTestVec("V"); + + expectError("rl_next(V, 0)", "Index out of range! Expected 0 < index <= 2 but received index == 0"); + expectError("rl_next(V, 3)", "Index out of range! Expected 0 < index <= 2 but received index == 3"); + expectError("rl_next(V, 'string')", "Bad access to property 'V'! Error while extracting integer: expected a number, received 'string'"); + expectError("rl_next(V, {})", "Bad access to property 'V'! Error while extracting integer: expected a number, received 'table'"); + expectError("rl_next(V, true)", "Bad access to property 'V'! Error while extracting integer: expected a number, received 'bool'"); + expectError("rl_next(V, 1.5)", "Bad access to property 'V'! Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)"); + expectError("rl_next(V, 1.001)", "Bad access to property 'V'! Error while extracting integer: implicit rounding (fractional part '0.0009999999999998899' is not negligible)"); + } + + TEST_F(TheLuaCustomizations_Next, ReportsErrorsWhenBadArrayIndexGiven_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv("S.array = Type:Array(2, Type:Float())"); + + expectError_WithEnv("rl_next(S.array, 0)", "lua: error: Invalid key value '0' for rl_next(). Expected a number in the range [1, 2]!"); + expectError_WithEnv("rl_next(S.array, 3)", "lua: error: Invalid key value '3' for rl_next(). Expected a number in the range [1, 2]!"); + expectError_WithEnv("rl_next(S.array, 'not a number')", "lua: error: Invalid key to rl_next() of type: Error while extracting integer: expected a number, received 'string'"); + } + + TEST_F(TheLuaCustomizations_Next, ReportsErrorsWhenBadStructsIndexGivenToCustomRlNextFunction_DuringRuntime) + { + createTestStruct("S", EWrappedType::RuntimeProperty); + + expectError("rl_next(S, 0)", "Bad access to property 'S'! Expected a string but got object of type number instead!"); + expectError("rl_next(S, 1)", "Bad access to property 'S'! Expected a string but got object of type number instead!"); + expectError("rl_next(S, {})", "Bad access to property 'S'! Expected a string but got object of type table instead!"); + expectError("rl_next(S, true)", "Bad access to property 'S'! Expected a string but got object of type bool instead!"); + expectError("rl_next(S, 1.5)", "Bad access to property 'S'! Expected a string but got object of type number instead!"); + expectError("rl_next(S, 1.001)", "Bad access to property 'S'! Expected a string but got object of type number instead!"); + } + + TEST_F(TheLuaCustomizations_Next, ReportsErrorsWhenBadStructsIndexGivenToCustomRlNextFunction_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv("S.field = Type:Int32()"); + expectError_WithEnv("rl_next(S, 0)", "lua: error: Invalid key to rl_next(): Expected a string but got object of type number instead!"); + expectError_WithEnv("rl_next(S, 1)", "lua: error: Invalid key to rl_next(): Expected a string but got object of type number instead!"); + expectError_WithEnv("rl_next(S, {})", "lua: error: Invalid key to rl_next(): Expected a string but got object of type table instead!"); + expectError_WithEnv("rl_next(S, true)", "lua: error: Invalid key to rl_next(): Expected a string but got object of type bool instead!"); + expectError_WithEnv("rl_next(S, 1.5)", "lua: error: Invalid key to rl_next(): Expected a string but got object of type number instead!"); + expectError_WithEnv("rl_next(S, 1.001)", "lua: error: Invalid key to rl_next(): Expected a string but got object of type number instead!"); + } + + TEST_F(TheLuaCustomizations_Next, ReportsErrorsForUnexistingPropertyInStruct_DuringRuntime) + { + createTestStruct("S", EWrappedType::RuntimeProperty); + expectError("rl_next(S, 'no such field')", "lua: error: Tried to access undefined struct property 'no such field'"); + } + + TEST_F(TheLuaCustomizations_Next, ReportsErrorsForUnexistingPropertyInStruct_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv("S.field = Type:Int32()"); + expectError_WithEnv("rl_next(S, 'no such field')", "lua: error: Could not find field named 'no such field' in struct object 'S'"); + } + + TEST_F(TheLuaCustomizations_Next, ProducesErrorWhenUsedOnUnsupportedInterfaceTypes) + { + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.v = Type:Int32() + rl_next(S.v) + )", "rl_next() called on an unsupported type 'Int32'"); + + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.vec = Type:Vec2i() + rl_next(S.vec) + )", "rl_next() called on an unsupported type 'Vec2i'"); + + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.s = Type:String() + rl_next(S.s) + )", "rl_next() called on an unsupported type 'String'"); + } + + TEST_F(TheLuaCustomizations_Next, WorksForWriteProtectedModules) + { + SolState solState; + ErrorReporting errors; + + // Compile test module + std::optional mod = LuaCompilationUtils::CompileModuleOrImportPrecompiled(solState, {}, {}, R"( + local mod = {} + mod.mytable = {nested = {a = 42}} + return mod + )", "", errors, {}, false); + ASSERT_TRUE(errors.getErrors().empty()); + + sol::load_result loadResult = solState.loadScript(R"( + -- module has one key/value pair - a table named 'mytable' + k, mytable = rl_next(mod) + assert(k == 'mytable') + assert(type(mytable) == 'table') + + -- next after 'mytable' is nil + k,v = rl_next(mod, 'mytable') + assert(k == nil) + assert(v == nil) + + k,nested = rl_next(mytable) + assert(k == 'nested') + assert(type(nested) == 'table') + + k,v = rl_next(nested) + assert(k == 'a') + assert(v == 42) + + k,v = rl_next(nested, 'a') + assert(k == nil) + assert(v == nil) + )", ""); + + ASSERT_TRUE (loadResult.valid()); + + // Apply environment, same as for real modules + sol::protected_function mainFunction = loadResult; + sol::environment env = solState.createEnvironment({EStandardModule::Base}, {}, false); + env.set_on(mainFunction); + env["mod"] = mod->moduleTable; + + sol::protected_function_result main_result = mainFunction(); + ASSERT_TRUE (main_result.valid()); + } + + class TheLuaCustomizations_Pairs : public TheLuaCustomizations + { + }; + + TEST_F(TheLuaCustomizations_Pairs, IteratesOverStructFields_DuringRuntime) + { + createTestStruct("S", EWrappedType::RuntimeProperty); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_pairs(S) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == 'field1,field2,') + assert(values == '5,6,') + )"); + } + + TEST_F(TheLuaCustomizations_Pairs, IteratesOverArrayFields_DuringRuntime) + { + createTestArray("A"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_pairs(A) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,3,') + assert(values == '11,12,13,') + )"); + } + + TEST_F(TheLuaCustomizations_Pairs, IteratesOverVecElements_DuringRuntime) + { + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_pairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,') + assert(values == '100,101,') + )"); + + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_pairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,3,') + assert(values == '100,101,102,') + )"); + + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_pairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,3,4,') + assert(values == '100,101,102,103,') + )"); + + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_pairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,') + assert(values == '100,101,') + )"); + + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_pairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,3,') + assert(values == '100,101,102,') + )"); + + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_pairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,3,4,') + assert(values == '100,101,102,103,') + )"); + } + + TEST_F(TheLuaCustomizations_Pairs, IteratesOverStructFields_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv(R"( + -- Define some test fields + S.field1 = Type:Int32() + S.field2 = Type:String() + + local keys = "" + local values = "" + for k,v in rl_pairs(S) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == 'field1,field2,') + assert(values == '4,10,') -- The IDs of the type labels Type:Int32()/Type:String() + )"); + } + + TEST_F(TheLuaCustomizations_Pairs, IteratesOverArrayFields_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv(R"( + -- Define some test fields + S.array = Type:Array(2, Type:Bool()) + + local keys = "" + local values = "" + for k,v in rl_pairs(S.array) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,') + assert(values == '11,11,') -- 11 is the enum value behind Type:Bool() + )"); + } + + TEST_F(TheLuaCustomizations_Pairs, IteratesOverComplexArrays_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv(R"( + -- Define some test fields + S.array = Type:Array(2, {a=Type:Int32(), b=Type:Float()}) + + local keys = "" + local values = "" + for k,v in rl_pairs(S.array) do + for nk,nv in rl_pairs(v) do + keys = keys .. tostring(nk) .. "," + values = values .. tostring(nv) .. "," + end + end + assert(keys == 'a,b,a,b,') + assert(values == '4,0,4,0,') + )"); + } + + TEST_F(TheLuaCustomizations_Pairs, WorksForWriteprotectedModules) + { + SolState solState; + ErrorReporting errors; + + // Compile test module + std::optional mod = LuaCompilationUtils::CompileModuleOrImportPrecompiled(solState, {}, {}, R"( + local mod = {} + mod.mytable = { + nested = {a = 11, b = 12}} + return mod + )", "", errors, {}, false); + ASSERT_TRUE(errors.getErrors().empty()); + + sol::load_result loadResult = solState.loadScript(R"( + for k,v in rl_pairs(mod.mytable.nested) do + if k == 'a' then + valueOfA = v + elseif k == 'b' then + valueOfB = v + else + assert(false) + end + end + assert(valueOfA == 11) + assert(valueOfB == 12) + )", ""); + + ASSERT_TRUE(loadResult.valid()); + + // Apply environment, same as for real modules + sol::protected_function mainFunction = loadResult; + sol::environment env = solState.createEnvironment({ EStandardModule::Base }, {}, false); + env.set_on(mainFunction); + env["mod"] = mod->moduleTable; + + sol::protected_function_result main_result = mainFunction(); + EXPECT_TRUE (main_result.valid()); + } + + TEST_F(TheLuaCustomizations_Pairs, ReportsErrorWhenUsedOnNotUserdataTypes_DuringRuntime) + { + createTestStruct("S", EWrappedType::RuntimeProperty); + expectError("rl_pairs('string')", "lua: error: rl_pairs() called on an unsupported type 'string'. Use only with user types like IN/OUT, modules etc.!"); + expectError("rl_pairs(true)", "lua: error: rl_pairs() called on an unsupported type 'bool'. Use only with user types like IN/OUT, modules etc.!"); + expectError("rl_pairs(1.5)", "lua: error: rl_pairs() called on an unsupported type 'number'. Use only with user types like IN/OUT, modules etc.!"); + } + + TEST_F(TheLuaCustomizations_Pairs, ProducesErrorWhenUsedOnUnsupportedInterfaceTypes) + { + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.v = Type:Int32() + for k,v in rl_pairs(S.v) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + )", "rl_next() called on an unsupported type 'Int32'"); + + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.vec = Type:Vec2i() + for k,v in rl_pairs(S.vec) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + )", "rl_next() called on an unsupported type 'Vec2i'"); + + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.s = Type:String() + for k,v in rl_pairs(S.s) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + )", "rl_next() called on an unsupported type 'String'"); + } + + + class TheLuaCustomizations_IPairs : public TheLuaCustomizations + { + }; + + TEST_F(TheLuaCustomizations_IPairs, IteratesOverArrayFields_DuringRuntime) + { + createTestArray("A"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_ipairs(A) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,3,') + assert(values == '11,12,13,') + )"); + } + + TEST_F(TheLuaCustomizations_IPairs, IteratesOverVecElements_DuringRuntime) + { + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_ipairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,') + assert(values == '100,101,') + )"); + + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_ipairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,3,') + assert(values == '100,101,102,') + )"); + + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_ipairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,3,4,') + assert(values == '100,101,102,103,') + )"); + + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_ipairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,') + assert(values == '100,101,') + )"); + + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_ipairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,3,') + assert(values == '100,101,102,') + )"); + + createTestVec("V"); + expectNoErrors(R"( + local keys = "" + local values = "" + for k,v in rl_ipairs(V) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + assert(keys == '1,2,3,4,') + assert(values == '100,101,102,103,') + )"); + } + + TEST_F(TheLuaCustomizations_IPairs, IteratesOverArrayFields_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv(R"( + -- Define some test fields + S.array = Type:Array(3, Type:Int64()) + + local keys = "" + local values = "" + for k,v in rl_ipairs(S.array) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + print(keys) + print(values) + assert(keys == '1,2,3,') + assert(values == '5,5,5,') -- 5 is the enum value behind Type:Int64() + )"); + } + + TEST_F(TheLuaCustomizations_IPairs, IteratesOverComplexArrays_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectNoErrors_WithEnv(R"( + -- Define some test fields + S.array = Type:Array(2, {a=Type:Int32(), b=Type:Float()}) + + local keys = "" + for k,v in rl_ipairs(S.array) do + keys = keys .. tostring(k) .. "," + assert(type(v) == 'userdata') + end + assert(keys == '1,2,') + )"); + } + + TEST_F(TheLuaCustomizations_IPairs, WorksForWriteprotectedModules) + { + SolState solState; + ErrorReporting errors; + + // Compile test module + std::optional mod = LuaCompilationUtils::CompileModuleOrImportPrecompiled(solState, {}, {}, R"( + local mod = {} + mod.mytable = { + nested = {[1] = 11, [2] = 12}} + return mod + )", "", errors, {}, false); + ASSERT_TRUE(errors.getErrors().empty()); + + // Check that iterating over custom indexed table works and the order is the same (ascending by numeric index) + sol::load_result loadResult = solState.loadScript(R"( + local expected = {[1] = 11, [2] = 12} + for k,v in rl_ipairs(mod.mytable.nested) do + assert(expected[k] == v) + end + )", ""); + + ASSERT_TRUE(loadResult.valid()); + + // Apply environment, same as for real modules + sol::protected_function mainFunction = loadResult; + sol::environment env = solState.createEnvironment({ EStandardModule::Base }, {}, false); + env.set_on(mainFunction); + env["mod"] = mod->moduleTable; + + sol::protected_function_result main_result = mainFunction(); + ASSERT_TRUE(main_result.valid()); + } + + TEST_F(TheLuaCustomizations_IPairs, ReportsErrorWhenUsedOnStruct_DuringRuntime) + { + createTestStruct("S", EWrappedType::RuntimeProperty); + expectError("rl_ipairs(S)", "rl_ipairs() called on an unsupported type 'Struct'. Use only with array-like built-in types or modules!"); + } + + TEST_F(TheLuaCustomizations_IPairs, ReportsErrorWhenUsedOnStruct_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv("rl_ipairs(S)", "rl_ipairs() called on an unsupported type 'Struct'. Use only with array-like built-in types or modules!"); + } + + TEST_F(TheLuaCustomizations_IPairs, ReportsErrorWhenUsedOnNotUserdataTypes_DuringRuntime) + { + createTestStruct("S", EWrappedType::RuntimeProperty); + expectError("rl_ipairs('string')", "lua: error: rl_ipairs() called on an unsupported type 'string'. Use only with user types like IN/OUT, modules etc.!"); + expectError("rl_ipairs(true)", "lua: error: rl_ipairs() called on an unsupported type 'bool'. Use only with user types like IN/OUT, modules etc.!"); + expectError("rl_ipairs(1.5)", "lua: error: rl_ipairs() called on an unsupported type 'number'. Use only with user types like IN/OUT, modules etc.!"); + } + + TEST_F(TheLuaCustomizations_Pairs, ProducesErrorWhenUsedUnsupportedInterfaceTypes_DuringInterfaceExtraction) + { + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.v = Type:Int32() + for k,v in rl_ipairs(S.v) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + )", "rl_ipairs() called on an unsupported type 'Int32'. Use only with array-like built-in types or modules!"); + + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.vec = Type:Vec2i() + for k,v in rl_ipairs(S.vec) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + )", "rl_ipairs() called on an unsupported type 'Vec2i'. Use only with array-like built-in types or modules!"); + + createTestStruct("S", EWrappedType::Extractor); + expectError_WithEnv(R"( + S.s = Type:String() + for k,v in rl_ipairs(S.s) do + keys = keys .. tostring(k) .. "," + values = values .. tostring(v) .. "," + end + )", "rl_ipairs() called on an unsupported type 'String'. Use only with array-like built-in types or modules!"); + } +} diff --git a/client/logic/unittests/internal/LuaTypeConversionsTest.cpp b/client/logic/unittests/internal/LuaTypeConversionsTest.cpp new file mode 100644 index 000000000..19a69c04c --- /dev/null +++ b/client/logic/unittests/internal/LuaTypeConversionsTest.cpp @@ -0,0 +1,452 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" + +#include "internals/LuaTypeConversions.h" + +namespace ramses::internal +{ + class TheLuaTypeConversions : public ::testing::Test + { + protected: + sol::state m_sol; + }; + + TEST_F(TheLuaTypeConversions, ProvidesCorrectIndexUpperBoundsForVecTypes) + { + EXPECT_EQ(LuaTypeConversions::GetMaxIndexForVectorType(EPropertyType::Vec2f), 2u); + EXPECT_EQ(LuaTypeConversions::GetMaxIndexForVectorType(EPropertyType::Vec3f), 3u); + EXPECT_EQ(LuaTypeConversions::GetMaxIndexForVectorType(EPropertyType::Vec4f), 4u); + EXPECT_EQ(LuaTypeConversions::GetMaxIndexForVectorType(EPropertyType::Vec2i), 2u); + EXPECT_EQ(LuaTypeConversions::GetMaxIndexForVectorType(EPropertyType::Vec3i), 3u); + EXPECT_EQ(LuaTypeConversions::GetMaxIndexForVectorType(EPropertyType::Vec4i), 4u); + } + + TEST_F(TheLuaTypeConversions, ExtractsStringViewFromSolObject) + { + m_sol["a_string"] = "string content"; + const sol::object solObject = m_sol["a_string"]; + + const DataOrError asStringView = LuaTypeConversions::ExtractSpecificType(solObject); + EXPECT_EQ("string content", asStringView.getData()); + } + + TEST_F(TheLuaTypeConversions, RecognizesNonStringDataWhenExtractingStringView) + { + m_sol["aNumber"] = 5; + m_sol["aBool"] = true; + m_sol["aTable"] = m_sol.create_table_with("a", 5); + m_sol["aFunction"] = [](){}; + + std::string errorMsg; + + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["aNumber"]).getError(), ::testing::HasSubstr("Expected a string but got object of type number instead!")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["aBool"]).getError(), ::testing::HasSubstr("Expected a string but got object of type bool instead!")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["aTable"]).getError(), ::testing::HasSubstr("Expected a string but got object of type table instead!")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["aFunction"]).getError(), ::testing::HasSubstr("Expected a string but got object of type function instead!")); + } + + TEST_F(TheLuaTypeConversions, ExtractsSignedIntegers) + { + m_sol["positiveInt"] = 5; + m_sol["negativeInt"] = -6; + const sol::object positiveInt = m_sol["positiveInt"]; + const sol::object negativeInt = m_sol["negativeInt"]; + + const DataOrError positiveIntOpt = LuaTypeConversions::ExtractSpecificType(positiveInt); + const DataOrError negativeIntOpt = LuaTypeConversions::ExtractSpecificType(negativeInt); + const DataOrError positiveInt64Opt = LuaTypeConversions::ExtractSpecificType(positiveInt); + const DataOrError negativeInt64Opt = LuaTypeConversions::ExtractSpecificType(negativeInt); + + EXPECT_EQ(5, positiveIntOpt.getData()); + EXPECT_EQ(-6, negativeIntOpt.getData()); + EXPECT_EQ(5, positiveInt64Opt.getData()); + EXPECT_EQ(-6, negativeInt64Opt.getData()); + } + + TEST_F(TheLuaTypeConversions, ExtractsSignedIntegers_AllowsEpsilonRounding) + { + m_sol["positiveIntPlusEps"] = 5.0 + std::numeric_limits::epsilon(); + m_sol["positiveIntMinusEps"] = 5.0 - std::numeric_limits::epsilon(); + m_sol["negativeIntPlusEps"] = -6 + std::numeric_limits::epsilon(); + m_sol["negativeIntMinusEps"] = -6 - std::numeric_limits::epsilon(); + m_sol["zeroMinusEps"] = 0.0 - std::numeric_limits::epsilon(); + const sol::object positiveIntPlusEps = m_sol["positiveIntPlusEps"]; + const sol::object positiveIntMinusEps = m_sol["positiveIntMinusEps"]; + const sol::object negativeIntPlusEps = m_sol["negativeIntPlusEps"]; + const sol::object negativeIntMinusEps = m_sol["negativeIntMinusEps"]; + const sol::object zeroMinusEps = m_sol["zeroMinusEps"]; + + EXPECT_EQ(5, LuaTypeConversions::ExtractSpecificType(positiveIntPlusEps).getData()); + EXPECT_EQ(5, LuaTypeConversions::ExtractSpecificType(positiveIntMinusEps).getData()); + EXPECT_EQ(-6, LuaTypeConversions::ExtractSpecificType(negativeIntPlusEps).getData()); + EXPECT_EQ(-6, LuaTypeConversions::ExtractSpecificType(negativeIntMinusEps).getData()); + EXPECT_EQ(0, LuaTypeConversions::ExtractSpecificType(zeroMinusEps).getData()); + + EXPECT_EQ(5, LuaTypeConversions::ExtractSpecificType(positiveIntPlusEps).getData()); + EXPECT_EQ(5, LuaTypeConversions::ExtractSpecificType(positiveIntMinusEps).getData()); + EXPECT_EQ(-6, LuaTypeConversions::ExtractSpecificType(negativeIntPlusEps).getData()); + EXPECT_EQ(-6, LuaTypeConversions::ExtractSpecificType(negativeIntMinusEps).getData()); + EXPECT_EQ(0, LuaTypeConversions::ExtractSpecificType(zeroMinusEps).getData()); + } + + TEST_F(TheLuaTypeConversions, ExtractsSignedIntegers_ForbidsLargerThanEpsilonRounding) + { + m_sol["positiveIntPlusEps"] = 5.0 + 5*std::numeric_limits::epsilon(); + m_sol["positiveIntMinusEps"] = 5.0 - 5*std::numeric_limits::epsilon(); + m_sol["negativeIntPlusEps"] = -6 + 5*std::numeric_limits::epsilon(); + m_sol["negativeIntMinusEps"] = -6 - 5 * std::numeric_limits::epsilon(); + m_sol["zeroMinusEps"] = 0.0 - 5 * std::numeric_limits::epsilon(); + const sol::object positiveIntPlusEps = m_sol["positiveIntPlusEps"]; + const sol::object positiveIntMinusEps = m_sol["positiveIntMinusEps"]; + const sol::object negativeIntPlusEps = m_sol["negativeIntPlusEps"]; + const sol::object negativeIntMinusEps = m_sol["negativeIntMinusEps"]; + const sol::object zeroMinusEps = m_sol["zeroMinusEps"]; + + const std::string errorSubstr = "Error while extracting integer: implicit rounding (fractional part"; + + // 32 bit + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(positiveIntPlusEps).getError(), ::testing::HasSubstr(errorSubstr)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(positiveIntMinusEps).getError(), ::testing::HasSubstr(errorSubstr)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(negativeIntPlusEps).getError(), ::testing::HasSubstr(errorSubstr)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(negativeIntMinusEps).getError(), ::testing::HasSubstr(errorSubstr)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(zeroMinusEps).getError(), ::testing::HasSubstr(errorSubstr)); + + // 64 bit + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(positiveIntPlusEps).getError(), ::testing::HasSubstr(errorSubstr)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(positiveIntMinusEps).getError(), ::testing::HasSubstr(errorSubstr)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(negativeIntPlusEps).getError(), ::testing::HasSubstr(errorSubstr)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(negativeIntMinusEps).getError(), ::testing::HasSubstr(errorSubstr)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(zeroMinusEps).getError(), ::testing::HasSubstr(errorSubstr)); + } + + TEST_F(TheLuaTypeConversions, ExtractsUnsignedIntegers_AcceptsUpToEpsilonRounding) + { + m_sol["okRoundingPos"] = 5.0 + std::numeric_limits::epsilon(); + m_sol["okRoundingNeg"] = 5.0 - std::numeric_limits::epsilon(); + m_sol["zeroMinusEps"] = 0.0 - std::numeric_limits::epsilon(); + m_sol["tooMuchRoundingPos"] = 5.0 + 5 * std::numeric_limits::epsilon(); + m_sol["tooMuchRoundingNeg"] = 5.0 - 5 * std::numeric_limits::epsilon(); + m_sol["zeroRoundingError"] = 0.0 - 5 * std::numeric_limits::epsilon(); + const sol::object okRoundingPos = m_sol["okRoundingPos"]; + const sol::object okRoundingNeg = m_sol["okRoundingNeg"]; + const sol::object zeroMinusEps = m_sol["zeroMinusEps"]; + + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["tooMuchRoundingPos"]).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["tooMuchRoundingNeg"]).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["zeroRoundingError"]).getError(), ::testing::HasSubstr("Error while extracting integer: expected non-negative number, received")); + + EXPECT_EQ(5u, LuaTypeConversions::ExtractSpecificType(okRoundingPos).getData()); + EXPECT_EQ(5u, LuaTypeConversions::ExtractSpecificType(okRoundingNeg).getData()); + EXPECT_EQ(0u, LuaTypeConversions::ExtractSpecificType(zeroMinusEps).getData()); + } + + TEST_F(TheLuaTypeConversions, ExtractsUnsignedIntegers) + { + m_sol["uint"] = 5; + const sol::object uint = m_sol["uint"]; + + EXPECT_EQ(5u, LuaTypeConversions::ExtractSpecificType(uint).getData()); + } + + TEST_F(TheLuaTypeConversions, CatchesErrorWhenCastingNegativeNumberToUnsignedInteger) + { + m_sol["negative"] = -5; + + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["negative"]).getError(), ::testing::HasSubstr("Error while extracting integer: expected non-negative number, received")); + } + + TEST_F(TheLuaTypeConversions, ExtractsUnsignedIntegers_AllowsEpsilonRounding) + { + m_sol["uint"] = 5.0 + std::numeric_limits::epsilon(); + const sol::object uint = m_sol["uint"]; + + EXPECT_EQ(5u, LuaTypeConversions::ExtractSpecificType(uint).getData()); + } + + TEST_F(TheLuaTypeConversions, ExtractsFloats) + { + m_sol["float"] = 0.5f; + m_sol["negFloat"] = -0.5f; + m_sol["floatWithIntegralPart"] = 1.5f; + const sol::object float_ = m_sol["float"]; + const sol::object negFloat = m_sol["negFloat"]; + const sol::object floatWithIntegralPart = m_sol["floatWithIntegralPart"]; + + EXPECT_FLOAT_EQ(0.5f, LuaTypeConversions::ExtractSpecificType(float_).getData()); + EXPECT_FLOAT_EQ(-0.5f, LuaTypeConversions::ExtractSpecificType(negFloat).getData()); + EXPECT_FLOAT_EQ(1.5f, LuaTypeConversions::ExtractSpecificType(floatWithIntegralPart).getData()); + } + + TEST_F(TheLuaTypeConversions, ExtractsExtremeSignedIntegers) + { + constexpr int32_t maxIntValue = std::numeric_limits::max(); + constexpr int32_t lowestIntValue = std::numeric_limits::lowest(); + m_sol["maxInt"] = maxIntValue; + m_sol["lowInt"] = lowestIntValue; + const sol::object maxInt = m_sol["maxInt"]; + const sol::object lowInt = m_sol["lowInt"]; + + EXPECT_EQ(maxIntValue, LuaTypeConversions::ExtractSpecificType(maxInt).getData()); + EXPECT_EQ(lowestIntValue, LuaTypeConversions::ExtractSpecificType(lowInt).getData()); + } + + TEST_F(TheLuaTypeConversions, ExtractsExtremeUnsignedIntegers) + { + // Maximum size_t is too large for Lua (throws exception as expected), use uint32_t instead + constexpr size_t maxUIntValue = std::numeric_limits::max(); + constexpr size_t lowestUIntValue = std::numeric_limits::lowest(); + m_sol["maxUInt"] = maxUIntValue; + m_sol["lowUInt"] = lowestUIntValue; + const sol::object maxUInt = m_sol["maxUInt"]; + const sol::object lowUInt = m_sol["lowUInt"]; + + EXPECT_EQ(maxUIntValue, LuaTypeConversions::ExtractSpecificType(maxUInt).getData()); + EXPECT_EQ(lowestUIntValue, LuaTypeConversions::ExtractSpecificType(lowUInt).getData()); + } + + TEST_F(TheLuaTypeConversions, ExtractsExtremeFloats) + { + // Test numbers around the boundaries a) of the integral part and b) of the fractional part + constexpr float maxFloatValue = std::numeric_limits::max(); + constexpr float lowestFloatValue = std::numeric_limits::lowest(); + constexpr float epsilonValue = std::numeric_limits::epsilon(); + constexpr float negEpsilonValue = -std::numeric_limits::epsilon(); + + m_sol["maxFloat"] = maxFloatValue; + m_sol["lowestFloat"] = lowestFloatValue; + m_sol["epsilon"] = epsilonValue; + m_sol["negEpsilon"] = negEpsilonValue; + + const sol::object maxFloat = m_sol["maxFloat"]; + const sol::object lowestFloat = m_sol["lowestFloat"]; + const sol::object epsilon = m_sol["epsilon"]; + const sol::object negEpsilon = m_sol["negEpsilon"]; + + EXPECT_FLOAT_EQ(maxFloatValue, LuaTypeConversions::ExtractSpecificType(maxFloat).getData()); + EXPECT_FLOAT_EQ(lowestFloatValue,LuaTypeConversions::ExtractSpecificType(lowestFloat).getData()); + EXPECT_FLOAT_EQ(epsilonValue, LuaTypeConversions::ExtractSpecificType(epsilon).getData()); + EXPECT_FLOAT_EQ(negEpsilonValue, LuaTypeConversions::ExtractSpecificType(negEpsilon).getData()); + } + + TEST_F(TheLuaTypeConversions, RoundsDoublesToFloats) + { + // Test numbers around the boundaries a) of the integral part and b) of the fractional part + constexpr double dblEpsilon = std::numeric_limits::epsilon() * 10; + + m_sol["onePlusEpsilon"] = 1.0 + dblEpsilon; + m_sol["oneMinusEpsilon"] = 1.0 - dblEpsilon; + + const sol::object onePlusEpsilon = m_sol["onePlusEpsilon"]; + const sol::object oneMinusEpsilon = m_sol["oneMinusEpsilon"]; + + EXPECT_FLOAT_EQ(1.0f, LuaTypeConversions::ExtractSpecificType(onePlusEpsilon).getData()); + EXPECT_FLOAT_EQ(1.0f, LuaTypeConversions::ExtractSpecificType(oneMinusEpsilon).getData()); + } + + TEST_F(TheLuaTypeConversions, ExtractsTableOfFloatsToFloatArray) + { + m_sol.script(R"( + floats = {0.1, 10000.42} + )"); + + const DataOrError> floatArray = LuaTypeConversions::ExtractArray(m_sol["floats"]); + + EXPECT_FLOAT_EQ(0.1f, floatArray.getData()[0]); + EXPECT_FLOAT_EQ(10000.42f, floatArray.getData()[1]); + } + + TEST_F(TheLuaTypeConversions, ExtractsTableOfIntegersToSignedIntegerArray) + { + m_sol.script(R"( + ints = {11, -12, (1.5 - 2.5)} + )"); + + const DataOrError> intsArray = LuaTypeConversions::ExtractArray(m_sol["ints"]); + + EXPECT_EQ(11, intsArray.getData()[0]); + EXPECT_EQ(-12, intsArray.getData()[1]); + EXPECT_EQ(-1, intsArray.getData()[2]); + } + + TEST_F(TheLuaTypeConversions, FailsValueExtractionWhenSymbolDoesNotExist) + { + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["noSuchSymbol"]).getError(), ::testing::HasSubstr("Error while extracting integer: expected a number, received 'nil'")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["noSuchSymbol"]).getError(), ::testing::HasSubstr("Error while extracting integer: expected a number, received 'nil'")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["noSuchSymbol"]).getError(), ::testing::HasSubstr("Error while extracting floating point number: expected a number, received 'nil'")); + } + + TEST_F(TheLuaTypeConversions, FailsValueExtractionWhenTypesDontMatch) + { + m_sol.script(R"( + integer = 5 + aString = "string" + aNil = nil + )"); + + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["aString"]).getError(), ::testing::HasSubstr("Error while extracting integer: expected a number, received 'string'")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["aString"]).getError(), ::testing::HasSubstr("Error while extracting integer: expected a number, received 'string'")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["aString"]).getError(), ::testing::HasSubstr("Error while extracting floating point number: expected a number, received 'string'")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["aNil"]).getError(), ::testing::HasSubstr("Error while extracting integer: expected a number, received 'nil'")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["aNil"]).getError(), ::testing::HasSubstr("Error while extracting integer: expected a number, received 'nil'")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["aNil"]).getError(), ::testing::HasSubstr("Error while extracting floating point number: expected a number, received 'nil'")); + } + + TEST_F(TheLuaTypeConversions, ReportsErrorWhenTableAndArraySizeDontMatch) + { + m_sol.script(R"( + ints = {11, 12, 13, 14, 15} + )"); + + auto error = LuaTypeConversions::ExtractArray(m_sol["ints"]).getError(); + EXPECT_THAT(error, ::testing::HasSubstr("Error while extracting array: expected 3 array components in table but got 5 instead!")); + } + + class TheLuaTypeConversions_CatchNumericErrors : public TheLuaTypeConversions + { + }; + + TEST_F(TheLuaTypeConversions_CatchNumericErrors, WhenNarrowingToSignedIntegers) + { + constexpr double largerThanMaxInt32Value = std::numeric_limits::max() + double(1.0); + constexpr double smallerThanLowestInt32Value = std::numeric_limits::lowest() - double(1.0); + m_sol["largerThanMaxInt32"] = largerThanMaxInt32Value; + m_sol["smallerThanLowestInt32"] = smallerThanLowestInt32Value; + + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["largerThanMaxInt32"]).getError(), + ::testing::HasSubstr("Error while extracting integer: integral part too large to fit in a signed 32-bit integer ('2147483648')")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["smallerThanLowestInt32"]).getError(), + ::testing::HasSubstr("Error while extracting integer: integral part too large to fit in a signed 32-bit integer")); + } + + TEST_F(TheLuaTypeConversions_CatchNumericErrors, WhenNarrowingFloats) + { + // Adding is not enough, have to multiply to get out of float range + constexpr double largerThanMaxFloatValue = std::numeric_limits::max() * double(2.0); + constexpr double smallerThanLowestFloatValue = std::numeric_limits::lowest() * double(2.0); + m_sol["largerThanMaxFloat"] = largerThanMaxFloatValue; + m_sol["smallerThanLowestFloat"] = smallerThanLowestFloatValue; + + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["largerThanMaxFloat"]).getError(), + ::testing::HasSubstr("Error while extracting floating point number: value would cause overflow in float")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["smallerThanLowestFloat"]).getError(), + ::testing::HasSubstr("Error while extracting floating point number: value would cause overflow in float")); + } + + TEST_F(TheLuaTypeConversions_CatchNumericErrors, WhenNarrowingUnsignedIntegers) + { + // Adding is not enough, have to multiply to get out of range + constexpr double largerThanMaxUIntValue = double(std::numeric_limits::max()) * 2.0; + m_sol["largerThanMaxUInt"] = largerThanMaxUIntValue; + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["largerThanMaxUInt"]).getError(), ::testing::HasSubstr("Error while extracting integer: integral part too large to fit in 64-bit unsigned integer")); + } + + TEST_F(TheLuaTypeConversions_CatchNumericErrors, WhenImplicitlyRoundingFloats) + { + // Combinations: positive and negative (X) with and without integral parts + m_sol["float"] = 0.5f; + m_sol["negFloat"] = -0.5f; + m_sol["largerThanOneFloat"] = 1.5f; + m_sol["smallerThanMinusOne"] = -1.5f; + const sol::object float_ = m_sol["float"]; + const sol::object negFloat = m_sol["negFloat"]; + const sol::object largerThanOneFloat = m_sol["largerThanOneFloat"]; + const sol::object smallerThanMinusOne = m_sol["smallerThanMinusOne"]; + + // Check signed and unsigned types alike, both should fail + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(float_).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(negFloat).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(largerThanOneFloat).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(smallerThanMinusOne).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(float_).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(negFloat).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(largerThanOneFloat).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(smallerThanMinusOne).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(float_).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(negFloat).getError(), ::testing::HasSubstr("Error while extracting integer: expected non-negative number, received '-0.5'")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(largerThanOneFloat).getError(), ::testing::HasSubstr("Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(smallerThanMinusOne).getError(), ::testing::HasSubstr("Error while extracting integer: expected non-negative number, received '-1.5'")); + } + + TEST_F(TheLuaTypeConversions_CatchNumericErrors, WhenImplicitlyRoundingFloats_RoundingErrorLargerThanEpsilon) + { + // Test numbers around the boundaries a) of the integral part and b) of the fractional part + constexpr double dblEpsilon = std::numeric_limits::epsilon() * 2; + + m_sol["onePlusEpsilon"] = 1.0 + dblEpsilon; + m_sol["oneMinusEpsilon"] = 1.0 - dblEpsilon; + + const sol::object onePlusEpsilon = m_sol["onePlusEpsilon"]; + const sol::object oneMinusEpsilon = m_sol["oneMinusEpsilon"]; + + const std::string errorMsg = "Error while extracting integer: implicit rounding (fractional part"; + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(onePlusEpsilon).getError(), ::testing::HasSubstr(errorMsg)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(oneMinusEpsilon).getError(), ::testing::HasSubstr(errorMsg)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(onePlusEpsilon).getError(), ::testing::HasSubstr(errorMsg)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(oneMinusEpsilon).getError(), ::testing::HasSubstr(errorMsg)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(onePlusEpsilon).getError(), ::testing::HasSubstr(errorMsg)); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(oneMinusEpsilon).getError(), ::testing::HasSubstr(errorMsg)); + } + + TEST_F(TheLuaTypeConversions_CatchNumericErrors, WhenImplicitlyRoundingLargeNumbersToInt64) + { + m_sol["dblMax"] = std::numeric_limits::max(); + EXPECT_THAT(LuaTypeConversions::ExtractSpecificType(m_sol["dblMax"]).getError(), + ::testing::HasSubstr("Error while extracting integer: integral part too large to fit in a signed 64-bit integer ('1.7976931348623157e+308')")); + } + + TEST_F(TheLuaTypeConversions_CatchNumericErrors, CatchesError_WhenNarrowing_WhileExtractingIntegerArray) + { + m_sol["oneAboveLargestSignedInt"] = double(std::numeric_limits::max()) + 1; + m_sol.script(R"( + notOnlyInts = {11, 12, oneAboveLargestSignedInt} + )"); + + const std::string errorMsg = LuaTypeConversions::ExtractArray(m_sol["notOnlyInts"]).getError(); + EXPECT_THAT(errorMsg, + ::testing::HasSubstr("Error while extracting array: unexpected value (type: 'number') at array element # 3! Reason: Error while extracting integer: integral part too large to fit in a signed 32-bit integer")); + } + + TEST_F(TheLuaTypeConversions_CatchNumericErrors, CatchesError_WhenImplicitlyRoundingFloats_WhileExtractingIntegerArray) + { + m_sol.script(R"( + notOnlyInts = {11, 12, 0.5} + )"); + + const std::string errorMsg = LuaTypeConversions::ExtractArray(m_sol["notOnlyInts"]).getError(); + EXPECT_THAT(errorMsg, + ::testing::HasSubstr("Error while extracting array: unexpected value (type: 'number') at array element # 3! Reason: Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + } + + TEST_F(TheLuaTypeConversions_CatchNumericErrors, CatchesError_WhenNegativeFloatFound_WhileExtractingIntegerArray) + { + m_sol.script(R"( + notOnlyInts = {11, 12, -1.5} + )"); + + const std::string errorMsg = LuaTypeConversions::ExtractArray(m_sol["notOnlyInts"]).getError(); + EXPECT_THAT(errorMsg, + ::testing::HasSubstr( + "Error while extracting array: unexpected value (type: 'number') at array element # 3! " + "Reason: Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + } + + TEST_F(TheLuaTypeConversions_CatchNumericErrors, CatchesError_WhenNarrowing_WhileExtractingFloatArray) + { + constexpr double largerThanMaxFloatValue = std::numeric_limits::max() * double(2.0); + m_sol["largerThanMaxFloat"] = largerThanMaxFloatValue; + m_sol.script(R"( + tooLarge = {11, 12, largerThanMaxFloat} + )"); + + const std::string errorMsg = LuaTypeConversions::ExtractArray(m_sol["tooLarge"]).getError(); + EXPECT_THAT(errorMsg, + ::testing::HasSubstr("Error while extracting array: unexpected value (type: 'number') at array element # 3! Reason: Error while extracting floating point number: value would cause overflow in float")); + } +} diff --git a/client/logic/unittests/internal/PropertyTypeExtractorTest.cpp b/client/logic/unittests/internal/PropertyTypeExtractorTest.cpp new file mode 100644 index 000000000..ff4c306be --- /dev/null +++ b/client/logic/unittests/internal/PropertyTypeExtractorTest.cpp @@ -0,0 +1,416 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" + +#include "internals/PropertyTypeExtractor.h" + +namespace ramses::internal +{ + TEST(ThePropertyTypeExtractorGlobalSymbols, AreVisibleOnlyToSpecifiedEnvironment) + { + sol::state sol; + sol::environment env(sol, sol::create, sol.globals()); + + PropertyTypeExtractor::RegisterTypes(env); + + // Environment now has the Type symbols (used to declare data types) + EXPECT_TRUE(env["Type"].valid()); + + // Global lua state doesn't know this symbol + EXPECT_FALSE(sol["Type"].valid()); + } + + TEST(ThePropertyTypeExtractorGlobalSymbols, UnregisteringSymbolsWillNotKeepAnythingInLuaStack) + { + sol::state sol; + sol::environment env(sol, sol::create, sol.globals()); + + PropertyTypeExtractor::RegisterTypes(env); + EXPECT_TRUE(lua_gettop(sol.lua_state()) >= 0); + + PropertyTypeExtractor::UnregisterTypes(env); + EXPECT_TRUE(lua_gettop(sol.lua_state()) == 0); + } + + class APropertyTypeExtractor : public ::testing::Test + { + protected: + APropertyTypeExtractor() + : m_env(m_sol, sol::create, m_sol.globals()) + { + PropertyTypeExtractor::RegisterTypes(m_env); + } + + HierarchicalTypeData extractTypeInfo(std::string_view source) + { + auto resultAndType = extractTypeInfo_WithResult(source); + EXPECT_TRUE(resultAndType.second.valid()); + return resultAndType.first; + } + + std::pair extractTypeInfo_WithResult(std::string_view source) + { + return extractTypeInfo_ThroughEnvironment(source, m_env); + } + + std::pair extractTypeInfo_ThroughEnvironment(std::string_view source, sol::environment& env) + { + // Reference temporary extractor + PropertyTypeExtractor extractor("IN", EPropertyType::Struct); + env["IN"] = std::ref(extractor); + + // Load script and apply environment + sol::protected_function loaded = m_sol.load(source); + assert(loaded.valid()); + env.set_on(loaded); + + // Execute script, nullify extractor reference, return results + sol::protected_function_result result = loaded(); + env["IN"] = sol::lua_nil; + return std::make_pair(extractor.getExtractedTypeData(), std::move(result)); + } + + sol::state m_sol; + sol::environment m_env; + }; + + TEST_F(APropertyTypeExtractor, ExtractsSinglePrimitiveProperty) + { + const HierarchicalTypeData typeInfo = extractTypeInfo(R"( + IN.newInt = Type:Int32() + )"); + + const HierarchicalTypeData expected = MakeStruct("IN", {{"newInt", EPropertyType::Int32}}); + + EXPECT_EQ(typeInfo, expected); + } + + TEST_F(APropertyTypeExtractor, ExtractsAllPrimitiveTypes_OrdersByPropertyNameLexicographically) + { + const HierarchicalTypeData typeInfo = extractTypeInfo(R"( + IN.bool = Type:Bool() + IN.string = Type:String() + IN.int32 = Type:Int32() + IN.int64 = Type:Int64() + IN.vec2i = Type:Vec2i() + IN.vec3i = Type:Vec3i() + IN.vec4i = Type:Vec4i() + IN.float = Type:Float() + IN.vec2f = Type:Vec2f() + IN.vec3f = Type:Vec3f() + IN.vec4f = Type:Vec4f() + )"); + + const HierarchicalTypeData expected = MakeStruct("IN", + { + {"bool", EPropertyType::Bool}, + {"float", EPropertyType::Float}, + {"int32", EPropertyType::Int32}, + {"int64", EPropertyType::Int64}, + {"string", EPropertyType::String}, + {"vec2f", EPropertyType::Vec2f}, + {"vec2i", EPropertyType::Vec2i}, + {"vec3f", EPropertyType::Vec3f}, + {"vec3i", EPropertyType::Vec3i}, + {"vec4f", EPropertyType::Vec4f}, + {"vec4i", EPropertyType::Vec4i}, + } + ); + + EXPECT_EQ(typeInfo, expected); + } + + TEST_F(APropertyTypeExtractor, ExtractsNestedTypes_OrdersByPropertyNameLexicographically_WhenUsingLuaTable) + { + const HierarchicalTypeData typeInfo = extractTypeInfo(R"( + IN.nested = { + int = Type:Int32(), + vec4f = Type:Vec4f(), + vec2i = Type:Vec2i(), + bool = Type:Bool() + } + )"); + + const HierarchicalTypeData expected{ + TypeData{"IN", EPropertyType::Struct}, + { + HierarchicalTypeData{ + TypeData{"nested", EPropertyType::Struct}, { + MakeType("bool", EPropertyType::Bool), + MakeType("int", EPropertyType::Int32), + MakeType("vec2i", EPropertyType::Vec2i), + MakeType("vec4f", EPropertyType::Vec4f) + } + } + } + }; + + EXPECT_EQ(typeInfo, expected); + } + + TEST_F(APropertyTypeExtractor, ExtractsNestedTypes_OrdersLexicographically_WhenDeclaredOneByOne) + { + const HierarchicalTypeData typeInfo = extractTypeInfo(R"( + IN.nested = {} + IN.nested.s2 = {} + IN.nested.s2.i2 = Type:Int32() + IN.nested.s2.i1 = Type:Int32() + IN.nested.b1 = Type:Bool() + )"); + + const HierarchicalTypeData expected { + TypeData{"IN", EPropertyType::Struct}, // Root property + { + // Child properties + HierarchicalTypeData{ + TypeData{"nested", EPropertyType::Struct}, { + MakeType("b1", EPropertyType::Bool), + MakeStruct("s2", { + {"i1", EPropertyType::Int32}, + {"i2", EPropertyType::Int32} + }), + } + } + } + }; + + EXPECT_EQ(typeInfo, expected); + } + + class APropertyTypeExtractor_Errors : public APropertyTypeExtractor + { + protected: + sol::error expectErrorDuringTypeExtraction(std::string_view luaCode) + { + const sol::protected_function_result result = extractTypeInfo_WithResult(luaCode).second; + assert(!result.valid()); + sol::error error = result; + return error; + } + }; + + TEST_F(APropertyTypeExtractor_Errors, ProducesErrorWhenIndexingUndeclaredProperty) + { + const sol::error error = expectErrorDuringTypeExtraction("prop = IN.doesNotExist"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Field 'doesNotExist' does not exist in struct 'IN'")); + } + + TEST_F(APropertyTypeExtractor_Errors, ProducesErrorWhenDeclaringPropertyTwice) + { + const sol::error error = expectErrorDuringTypeExtraction( + R"( + IN.property = Type:Int32() + IN.property = Type:Float() + )"); + + EXPECT_THAT(error.what(), ::testing::HasSubstr("lua: error: Field 'property' already exists! Can't declare the same field twice!")); + } + + TEST_F(APropertyTypeExtractor_Errors, ProducesErrorWhenTryingToAccessInterfaceProperties_WithNonStringIndex) + { + sol::error error = expectErrorDuringTypeExtraction("prop = IN[1]"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Bad index access to struct 'IN': Expected a string but got object of type number instead!")); + error = expectErrorDuringTypeExtraction("prop = IN[true]"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Bad index access to struct 'IN': Expected a string but got object of type bool instead!")); + error = expectErrorDuringTypeExtraction("prop = IN[{x=5}]"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Bad index access to struct 'IN': Expected a string but got object of type table instead!")); + error = expectErrorDuringTypeExtraction("prop = IN[nil]"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Bad index access to struct 'IN': Expected a string but got object of type nil instead!")); + } + + TEST_F(APropertyTypeExtractor_Errors, ProducesErrorWhenTryingToCreateInterfaceProperties_WithNonStringIndex) + { + sol::error error = expectErrorDuringTypeExtraction("IN[1] = Type:Int32()"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Invalid index for new field on struct 'IN': Expected a string but got object of type number instead!")); + error = expectErrorDuringTypeExtraction("IN[true] = Type:Int32()"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Invalid index for new field on struct 'IN': Expected a string but got object of type bool instead!")); + error = expectErrorDuringTypeExtraction("IN[{x=5}] = Type:Int32()"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Invalid index for new field on struct 'IN': Expected a string but got object of type table instead!")); + error = expectErrorDuringTypeExtraction("IN[nil] = Type:Int32()"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Invalid index for new field on struct 'IN': Expected a string but got object of type nil instead!")); + } + + TEST_F(APropertyTypeExtractor_Errors, InvalidTypeSpecifiers) + { + const std::vector wrongStatements = { + "IN.bad_type = nil", + "IN.bad_type = 'not a type'", + "IN.bad_type = true", + "IN.bad_type = 150000", + }; + + for (const auto& statement : wrongStatements) + { + const sol::error error = expectErrorDuringTypeExtraction(statement); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Invalid type of field 'bad_type'! Expected Type:T() syntax where T=Float,Int32,... Found a value of type")); + } + } + + TEST_F(APropertyTypeExtractor_Errors, InvalidTypeSpecifiers_Nested) + { + const std::vector wrongStatements = { + "IN.parent = {bad_type = 'not a type'}", + "IN.parent = {bad_type = true}", + "IN.parent = {bad_type = 150000}", + }; + + for (const auto& statement : wrongStatements) + { + const sol::error error = expectErrorDuringTypeExtraction(statement); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Invalid type of field 'bad_type'! Expected Type:T() syntax where T=Float,Int32,... Found a value of type")); + } + } + + TEST_F(APropertyTypeExtractor_Errors, NoNameProvided_ForNestedProperty) + { + const sol::error error1 = expectErrorDuringTypeExtraction("IN.parent = {Type:Int32()}"); + const sol::error error2 = expectErrorDuringTypeExtraction("IN.parent = {5}"); + EXPECT_THAT(error1.what(), ::testing::HasSubstr("Invalid index for new field on struct 'parent': Expected a string but got object of type number instead!")); + EXPECT_THAT(error2.what(), ::testing::HasSubstr("Invalid index for new field on struct 'parent': Expected a string but got object of type number instead!")); + } + + TEST_F(APropertyTypeExtractor_Errors, CorrectNameButWrongTypeProvided_ForNestedProperty) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.no_nested_type = { correct_key = 'but wrong type' }"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Invalid type of field 'correct_key'! Expected Type:T() syntax where T=Float,Int32,... Found a value of type 'string' instead")); + } + + TEST_F(APropertyTypeExtractor_Errors, UserdataAssignedToPropertyCausesError) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.very_wrong = IN"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Invalid type of field 'very_wrong'! Expected Type:T() syntax where T=Float,Int32,... Found a value of type 'userdata' instead")); + } + + class APropertyTypeExtractor_Arrays : public APropertyTypeExtractor + { + protected: + }; + + TEST_F(APropertyTypeExtractor_Arrays, DeclaresArrayOfPrimitives) + { + const HierarchicalTypeData typeInfo = extractTypeInfo("IN.primArray = Type:Array(3, Type:Int32())"); + + const HierarchicalTypeData arrayType = MakeArray("primArray", 3, EPropertyType::Int32); + const HierarchicalTypeData expected({"IN", EPropertyType::Struct}, {arrayType}); + + EXPECT_EQ(typeInfo, expected); + } + + TEST_F(APropertyTypeExtractor_Arrays, DeclaresArrayOfStructs) + { + const HierarchicalTypeData typeInfo = extractTypeInfo("IN.structArray = Type:Array(3, {a = Type:Int32(), b = Type:Vec3f()})"); + + ASSERT_EQ(1u, typeInfo.children.size()); + const HierarchicalTypeData& arrayType = typeInfo.children[0]; + + EXPECT_EQ(arrayType.typeData, TypeData("structArray", EPropertyType::Array)); + + for (const auto& arrayField : arrayType.children) + { + EXPECT_EQ(arrayField.typeData, TypeData("", EPropertyType::Struct)); + EXPECT_THAT(arrayField.children, + ::testing::ElementsAre( + MakeType("a", EPropertyType::Int32), + MakeType("b", EPropertyType::Vec3f) + ) + ); + } + + // Order within a struct is arbitrary, BUT each two structs in the array have the exact same order of child properties! + ASSERT_EQ(3u, arrayType.children.size()); + EXPECT_EQ(arrayType.children[0], arrayType.children[1]); + EXPECT_EQ(arrayType.children[1], arrayType.children[2]); + } + + TEST_F(APropertyTypeExtractor_Arrays, DeclaresArrayOfArrays) + { + const HierarchicalTypeData typeInfo = extractTypeInfo("IN.array2d = Type:Array(3, Type:Array(2, Type:Int32()))"); + + ASSERT_EQ(1u, typeInfo.children.size()); + const HierarchicalTypeData& arrayType = typeInfo.children[0]; + + EXPECT_EQ(arrayType.typeData, TypeData("array2d", EPropertyType::Array)); + + for (const auto& arrayField : arrayType.children) + { + EXPECT_EQ(arrayField.typeData, TypeData("", EPropertyType::Array)); + EXPECT_THAT(arrayField.children, + ::testing::ElementsAre( + MakeType("", EPropertyType::Int32), + MakeType("", EPropertyType::Int32) + ) + ); + } + } + + class APropertyTypeExtractor_ArrayErrors : public APropertyTypeExtractor_Errors + { + protected: + }; + + TEST_F(APropertyTypeExtractor_ArrayErrors, ArrayDefinedWithoutArguments) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.array = Type:Array()"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Type:Array(N, T) invoked with bad size argument! Error while extracting integer: expected a number, received 'nil'")); + } + + TEST_F(APropertyTypeExtractor_ArrayErrors, ArrayWithFirstArgumentNotANumber) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.array = Type:Array('not a number')"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Type:Array(N, T) invoked with bad size argument! Error while extracting integer: expected a number, received 'string'")); + } + + TEST_F(APropertyTypeExtractor_ArrayErrors, ArrayWithoutTypeArgument) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.array = Type:Array(5)"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Type:Array(N, T) invoked with invalid type parameter T!")); + } + + TEST_F(APropertyTypeExtractor_ArrayErrors, ArrayWithInvalidTypeArgument) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.array = Type:Array(5, 9000)"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Invalid element type T of array field 'array'! Found a value of type T='number' instead of T=Type:() in call array = Type:Array(N, T)!")); + } + + TEST_F(APropertyTypeExtractor_ArrayErrors, ArrayWithZeroSize) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.array = Type:Array(0, Type:Int32())"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Type:Array(N, T) invoked with invalid size parameter N=0 (must be in the range [1, 255])!")); + } + + TEST_F(APropertyTypeExtractor_ArrayErrors, ArrayWithOutOfBoundsSize) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.array = Type:Array(256, Type:Int32())"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Type:Array(N, T) invoked with invalid size parameter N=256 (must be in the range [1, 255])!")); + } + + TEST_F(APropertyTypeExtractor_ArrayErrors, ArrayWithNegativeSize) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.array = Type:Array(-1, Type:Int32())"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Type:Array(N, T) invoked with bad size argument! Error while extracting integer: expected non-negative number, received '-1'")); + } + + TEST_F(APropertyTypeExtractor_ArrayErrors, ArrayWithFloatSize) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.array = Type:Array(1.5, Type:Int32())"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Type:Array(N, T) invoked with bad size argument! Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + } + + TEST_F(APropertyTypeExtractor_ArrayErrors, ArrayWithUserDataInsteadOfSize) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.array = Type:Array(IN, Type:Int32())"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Type:Array(N, T) invoked with bad size argument! Error while extracting integer: expected a number, received 'userdata'")); + } + + TEST_F(APropertyTypeExtractor_ArrayErrors, ArrayWithUserDataInsteadOfTypeInfo) + { + const sol::error error = expectErrorDuringTypeExtraction("IN.array = Type:Array(5, IN)"); + EXPECT_THAT(error.what(), ::testing::HasSubstr("Invalid element type T of array field 'array'! Found a value of type T='userdata' instead of T=Type:() in call array = Type:Array(N, T)!")); + } +} diff --git a/client/logic/unittests/internal/RamsesHelperTest.cpp b/client/logic/unittests/internal/RamsesHelperTest.cpp new file mode 100644 index 000000000..a09adb807 --- /dev/null +++ b/client/logic/unittests/internal/RamsesHelperTest.cpp @@ -0,0 +1,35 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" + +#include "internals/RamsesHelper.h" + +namespace ramses::internal +{ + TEST(ARamsesHelper, ConvertsRamsesUniformTypeToEpropertyType) + { + EXPECT_EQ(EPropertyType::Int32, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::Int32)); + EXPECT_EQ(std::nullopt, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::UInt16)); + EXPECT_EQ(std::nullopt, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::UInt32)); + EXPECT_EQ(EPropertyType::Float, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::Float)); + EXPECT_EQ(EPropertyType::Vec2f, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::Vector2F)); + EXPECT_EQ(EPropertyType::Vec3f, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::Vector3F)); + EXPECT_EQ(EPropertyType::Vec4f, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::Vector4F)); + EXPECT_EQ(EPropertyType::Vec2i, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::Vector2I)); + EXPECT_EQ(EPropertyType::Vec3i, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::Vector3I)); + EXPECT_EQ(EPropertyType::Vec4i, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::Vector4I)); + EXPECT_EQ(std::nullopt, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::Matrix22F)); + EXPECT_EQ(std::nullopt, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::Matrix33F)); + EXPECT_EQ(std::nullopt, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::Matrix44F)); + EXPECT_EQ(std::nullopt, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::TextureSampler2D)); + EXPECT_EQ(std::nullopt, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::TextureSampler3D)); + EXPECT_EQ(std::nullopt, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::TextureSamplerCube)); + EXPECT_EQ(std::nullopt, ConvertRamsesUniformTypeToPropertyType(ramses::EDataType::TextureSamplerExternal)); + } +} diff --git a/client/logic/unittests/internal/RamsesObjectResolverTest.cpp b/client/logic/unittests/internal/RamsesObjectResolverTest.cpp new file mode 100644 index 000000000..920e30442 --- /dev/null +++ b/client/logic/unittests/internal/RamsesObjectResolverTest.cpp @@ -0,0 +1,140 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" + +#include "internals/RamsesObjectResolver.h" +#include "internals/ErrorReporting.h" +#include "impl/RamsesNodeBindingImpl.h" +#include "RamsesTestUtils.h" + +#include "ramses-client-api/Node.h" +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/RenderPass.h" + +namespace ramses::internal +{ + class ARamsesObjectResolver : public ::testing::Test + { + protected: + RamsesTestSetup m_ramsesTestSetup; + ramses::Scene* m_scene { m_ramsesTestSetup.createScene() }; + ErrorReporting m_errors; + RamsesObjectResolver m_resolver {m_errors, *m_scene}; + }; + + TEST_F(ARamsesObjectResolver, FindsSceneNodeByItsId) + { + ramses::Node* node = m_scene->createNode(); + EXPECT_EQ(node, m_resolver.findRamsesNodeInScene("some logic node", node->getSceneObjectId())); + EXPECT_TRUE(m_errors.getErrors().empty()); + } + + TEST_F(ARamsesObjectResolver, FindsAppearanceByItsId) + { + ramses::Appearance& appearance = RamsesTestSetup::CreateTrivialTestAppearance(*m_scene); + EXPECT_EQ(&appearance, m_resolver.findRamsesAppearanceInScene("some logic node", appearance.getSceneObjectId())); + EXPECT_TRUE(m_errors.getErrors().empty()); + } + + TEST_F(ARamsesObjectResolver, FindsCameraByItsId) + { + ramses::Camera* camera = m_scene->createPerspectiveCamera(); + EXPECT_EQ(camera, m_resolver.findRamsesCameraInScene("some logic node", camera->getSceneObjectId())); + EXPECT_TRUE(m_errors.getErrors().empty()); + } + + TEST_F(ARamsesObjectResolver, FindsRenderPassByItsId) + { + const ramses::RenderPass* rp = m_scene->createRenderPass(); + EXPECT_EQ(rp, m_resolver.findRamsesRenderPassInScene("some logic node", rp->getSceneObjectId())); + EXPECT_TRUE(m_errors.getErrors().empty()); + } + + TEST_F(ARamsesObjectResolver, ReportsErrorWhenNodeWithGivenIdDoesNotExist) + { + ramses::sceneObjectId_t fakeNodeId{ 42 }; + + EXPECT_FALSE(m_resolver.findRamsesNodeInScene("some logic node", fakeNodeId)); + EXPECT_EQ(1u, m_errors.getErrors().size()); + EXPECT_EQ("Fatal error during loading from file! Serialized Ramses Logic object 'some logic node' points to a Ramses object (id: 42) which couldn't be found in the provided scene!", m_errors.getErrors()[0].message); + } + + TEST_F(ARamsesObjectResolver, ReportsErrorWhenAppearanceWithGivenIdDoesNotExist) + { + ramses::sceneObjectId_t fakeAppearanceId{ 42 }; + + EXPECT_FALSE(m_resolver.findRamsesAppearanceInScene("some logic node", fakeAppearanceId)); + EXPECT_EQ(1u, m_errors.getErrors().size()); + EXPECT_EQ("Fatal error during loading from file! Serialized Ramses Logic object 'some logic node' points to a Ramses object (id: 42) which couldn't be found in the provided scene!", m_errors.getErrors()[0].message); + } + + TEST_F(ARamsesObjectResolver, ReportsErrorWhenCameraWithGivenIdDoesNotExist) + { + ramses::sceneObjectId_t fakeCameraId{ 42 }; + + EXPECT_FALSE(m_resolver.findRamsesCameraInScene("some logic node", fakeCameraId)); + EXPECT_EQ(1u, m_errors.getErrors().size()); + EXPECT_EQ("Fatal error during loading from file! Serialized Ramses Logic object 'some logic node' points to a Ramses object (id: 42) which couldn't be found in the provided scene!", m_errors.getErrors()[0].message); + } + + TEST_F(ARamsesObjectResolver, ReportsErrorWhenRenderPassWithGivenIdDoesNotExist) + { + ramses::sceneObjectId_t fakeRenderPassId{ 42 }; + + EXPECT_FALSE(m_resolver.findRamsesRenderPassInScene("some logic node", fakeRenderPassId)); + EXPECT_EQ(1u, m_errors.getErrors().size()); + EXPECT_EQ("Fatal error during loading from file! Serialized Ramses Logic object 'some logic node' points to a Ramses object (id: 42) which couldn't be found in the provided scene!", m_errors.getErrors()[0].message); + } + + TEST_F(ARamsesObjectResolver, ReportsErrorWhenObjectWithGivenIdExists_ButIsNotANode) + { + ramses::Appearance& appearance = RamsesTestSetup::CreateTrivialTestAppearance(*m_scene); + + EXPECT_FALSE(m_resolver.findRamsesNodeInScene("some logic node", appearance.getSceneObjectId())); + ASSERT_EQ(1u, m_errors.getErrors().size()); + EXPECT_EQ("Fatal error during loading from file! Ramses binding 'some logic node' points to a Ramses scene object (id: 2) which is not of the same type!", m_errors.getErrors()[0].message); + } + + TEST_F(ARamsesObjectResolver, ReportsErrorWhenResolvedObjectExists_ButIsNotCamera) + { + ramses::Node* node = m_scene->createNode(); + + EXPECT_FALSE(m_resolver.findRamsesCameraInScene("some logic node", node->getSceneObjectId())); + ASSERT_EQ(1u, m_errors.getErrors().size()); + EXPECT_EQ("Fatal error during loading from file! Ramses binding 'some logic node' points to a Ramses scene object (id: 1) which is not of the same type!", m_errors.getErrors()[0].message); + } + + TEST_F(ARamsesObjectResolver, ReportsErrorWhenResolvedObjectExists_ButIsNotAppearance) + { + ramses::Node* node = m_scene->createNode(); + + EXPECT_FALSE(m_resolver.findRamsesAppearanceInScene("some logic node", node->getSceneObjectId())); + ASSERT_EQ(1u, m_errors.getErrors().size()); + EXPECT_EQ("Fatal error during loading from file! Ramses binding 'some logic node' points to a Ramses scene object (id: 1) which is not of the same type!", m_errors.getErrors()[0].message); + } + + TEST_F(ARamsesObjectResolver, ReportsErrorWhenResolvedObjectExists_ButIsNotRenderPass) + { + ramses::Node* node = m_scene->createNode(); + + EXPECT_FALSE(m_resolver.findRamsesRenderPassInScene("some logic node", node->getSceneObjectId())); + ASSERT_EQ(1u, m_errors.getErrors().size()); + EXPECT_EQ("Fatal error during loading from file! Ramses binding 'some logic node' points to a Ramses scene object (id: 1) which is not of the same type!", m_errors.getErrors()[0].message); + } + + // Special case (ramses Camera is also a Node) - test that it works as expected when resolved by Id + TEST_F(ARamsesObjectResolver, ResolvesRamsesCamera_WhenUsedAsIfItWasOnlyNode) + { + ramses::Camera* camera = m_scene->createPerspectiveCamera(); + + EXPECT_EQ(camera, m_resolver.findRamsesNodeInScene("some logic node", camera->getSceneObjectId())); + EXPECT_TRUE(m_errors.getErrors().empty()); + } +} diff --git a/client/logic/unittests/internal/SolHelperTest.cpp b/client/logic/unittests/internal/SolHelperTest.cpp new file mode 100644 index 000000000..d67a44b24 --- /dev/null +++ b/client/logic/unittests/internal/SolHelperTest.cpp @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" + +#include "internals/SolHelper.h" + +namespace sol_helper +{ + TEST(ASolHelper, ConvertsSolTypeToString) + { + EXPECT_EQ("none", sol_helper::GetSolTypeName(sol::type::none)); + EXPECT_EQ("nil", sol_helper::GetSolTypeName(sol::type::lua_nil)); + EXPECT_EQ("string", sol_helper::GetSolTypeName(sol::type::string)); + EXPECT_EQ("number", sol_helper::GetSolTypeName(sol::type::number)); + EXPECT_EQ("thread", sol_helper::GetSolTypeName(sol::type::thread)); + EXPECT_EQ("bool", sol_helper::GetSolTypeName(sol::type::boolean)); + EXPECT_EQ("function", sol_helper::GetSolTypeName(sol::type::function)); + EXPECT_EQ("userdata", sol_helper::GetSolTypeName(sol::type::userdata)); + EXPECT_EQ("lightuserdata", sol_helper::GetSolTypeName(sol::type::lightuserdata)); + EXPECT_EQ("table", sol_helper::GetSolTypeName(sol::type::table)); + EXPECT_EQ("poly", sol_helper::GetSolTypeName(sol::type::poly)); + } +} diff --git a/client/logic/unittests/internal/SolStateTest.cpp b/client/logic/unittests/internal/SolStateTest.cpp new file mode 100644 index 000000000..708a6026e --- /dev/null +++ b/client/logic/unittests/internal/SolStateTest.cpp @@ -0,0 +1,244 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" + +#include "internals/SolState.h" +#include "internals/SolWrapper.h" +#include "internals/LuaCompilationUtils.h" +#include "internals/ErrorReporting.h" + +namespace ramses::internal +{ + class ASolState : public ::testing::Test + { + protected: + SolState m_solState; + + const std::string_view m_valid_empty_script = R"( + function interface(IN,OUT) + end + function run(IN,OUT) + end + )"; + }; + + TEST_F(ASolState, DoesNotHaveErrorsAfterLoadingEmptyScript) + { + + auto load_result = m_solState.loadScript("", "emptyScript"); + EXPECT_TRUE(load_result.valid()); + } + + TEST_F(ASolState, HasNoErrorsAfterLoadingValidScript) + { + auto load_result = m_solState.loadScript(m_valid_empty_script, "validEmptryScript"); + EXPECT_TRUE(load_result.valid()); + } + + TEST_F(ASolState, DoesNotLoadAScriptWithErrors) + { + auto load_result = m_solState.loadScript("this.does.not.compile", "cantCompileScript"); + EXPECT_FALSE(load_result.valid()); + sol::error error = load_result; + EXPECT_THAT(error.what(), ::testing::HasSubstr("'' expected near 'not'")); + } + + TEST_F(ASolState, CreatesNewEnvironment) + { + sol::environment env = m_solState.createEnvironment({}, {}, false); + EXPECT_TRUE(env.valid()); + } + + TEST_F(ASolState, NewEnvironment_DoesNotExposeTypeSymbols) + { + sol::environment env = m_solState.createEnvironment({}, {}, false); + ASSERT_TRUE(env.valid()); + + EXPECT_FALSE(env["Type"].valid()); + } + + TEST_F(ASolState, CreatesCustomMethods) + { + sol::environment env = m_solState.createEnvironment({}, {}, false); + ASSERT_TRUE(env.valid()); + + EXPECT_TRUE(env["modules"].valid()); + EXPECT_TRUE(env["rl_len"].valid()); + } + + TEST_F(ASolState, NewEnvironment_DoesNotExposeDebugLogFunctions) + { + sol::environment env = m_solState.createEnvironment({}, {}, false); + ASSERT_TRUE(env.valid()); + + EXPECT_FALSE(env["rl_logInfo"].valid()); + EXPECT_FALSE(env["rl_logWarn"].valid()); + EXPECT_FALSE(env["rl_logError"].valid()); + } + + TEST_F(ASolState, NewEnvironment_ExposesDebugLogFunctions) + { + sol::environment env = m_solState.createEnvironment({}, {}, true); + ASSERT_TRUE(env.valid()); + + EXPECT_TRUE(env["rl_logInfo"].valid()); + EXPECT_TRUE(env["rl_logWarn"].valid()); + EXPECT_TRUE(env["rl_logError"].valid()); + } + + class ASolState_Environment : public ASolState + { + protected: + sol::environment m_env {m_solState.createEnvironment({}, {}, false)}; + }; + + TEST_F(ASolState_Environment, HidesGlobalStandardModulesByDefault) + { + EXPECT_FALSE(m_env["print"].valid()); + EXPECT_FALSE(m_env["debug"].valid()); + EXPECT_FALSE(m_env["string"].valid()); + EXPECT_FALSE(m_env["table"].valid()); + EXPECT_FALSE(m_env["error"].valid()); + EXPECT_FALSE(m_env["math"].valid()); + } + + TEST_F(ASolState_Environment, ExposesOnlyRequestedGlobalStandardModules) + { + sol::environment env = m_solState.createEnvironment({EStandardModule::Math}, {}, false); + ASSERT_TRUE(env.valid()); + + EXPECT_TRUE(env["math"].valid()); + + EXPECT_FALSE(env["print"].valid()); + EXPECT_FALSE(env["debug"].valid()); + EXPECT_FALSE(env["string"].valid()); + EXPECT_FALSE(env["table"].valid()); + EXPECT_FALSE(env["error"].valid()); + } + + TEST_F(ASolState_Environment, ExposesRequestedGlobalStandardModules_TwoModules) + { + sol::environment env = m_solState.createEnvironment({ EStandardModule::String, EStandardModule::Table }, {}, false); + ASSERT_TRUE(env.valid()); + + EXPECT_TRUE(env["string"].valid()); + EXPECT_TRUE(env["table"].valid()); + + EXPECT_FALSE(env["math"].valid()); + EXPECT_FALSE(env["print"].valid()); + EXPECT_FALSE(env["debug"].valid()); + EXPECT_FALSE(env["error"].valid()); + } + + TEST_F(ASolState_Environment, ExposesRequestedGlobalStandardModules_BaseLib) + { + sol::environment env = m_solState.createEnvironment({ EStandardModule::Base }, {}, false); + ASSERT_TRUE(env.valid()); + + EXPECT_TRUE(env["error"].valid()); + EXPECT_TRUE(env["tostring"].valid()); + EXPECT_TRUE(env["print"].valid()); + + EXPECT_FALSE(env["table"].valid()); + EXPECT_FALSE(env["math"].valid()); + EXPECT_FALSE(env["debug"].valid()); + EXPECT_FALSE(env["string"].valid()); + } + + TEST_F(ASolState_Environment, HasNoFunctionsExpectedByUserScript) + { + EXPECT_FALSE(m_env["interface"].valid()); + EXPECT_FALSE(m_env["run"].valid()); + } + + TEST_F(ASolState_Environment, NewEnvironment_TwoEnvironmentsShareNoData) + { + sol::environment env2 = m_solState.createEnvironment({}, {}, false); + ASSERT_TRUE(env2.valid()); + + m_env["thisBelongsTo"] = "m_env"; + env2["thisBelongsTo"] = "env2"; + + const std::string data1 = m_env["thisBelongsTo"]; + const std::string data2 = env2["thisBelongsTo"]; + + EXPECT_EQ(data1, "m_env"); + EXPECT_EQ(data2, "env2"); + } + + TEST_F(ASolState_Environment, HasNoAccessToPreviouslyDeclaredGlobalSymbols) + { + const std::string_view script = R"( + global= "this is global" + function func() + return global + end + return func + )"; + + // Execute the script and obtain the func pointer 'func' + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + sol::function func = loadedScript(); + + // Apply fresh environment to func + sol::environment newEnv = m_solState.createEnvironment({}, {}, false); + ASSERT_TRUE(newEnv.valid()); + newEnv.set_on(func); + + // Func has no access to 'global' because it was defined _before_ applying the new environment + sol::object result = func(); + EXPECT_EQ(result, sol::lua_nil); + } + + // Similar to NewlyCreatedEnvironment_HasNoAccessToPreviouslyDeclaredGlobalSymbols + // But here the environment is applied before global symbols are declared -> access to those is available + TEST_F(ASolState_Environment, HasAccessToGlobalSymbols_DeclaredAfterApplyingTheEnvironment) + { + const std::string_view script = R"( + global = "this is global" + function func() + return global + end + return func + )"; + + sol::protected_function loadedScript = m_solState.loadScript(script, "test script"); + + // Apply a fresh environments to loaded script _before_ executing it + m_env.set_on(loadedScript); + sol::function func = loadedScript(); + + // Can access global symbol, because it lives in the new environment + const std::string result = func(); + EXPECT_EQ(result, "this is global"); + } + + TEST_F(ASolState_Environment, OverridesEnvironmentOfScript_AfterAppliedOnIt) + { + const std::string_view reportData = R"( + if data ~= nil then + return "data: " .. data + else + return "no data" + end + )"; + + sol::protected_function script = m_solState.loadScript(reportData, "test script"); + + std::string dataStatus = script(); + EXPECT_EQ(dataStatus, "no data"); + + m_env["data"] = "a lot of data!"; + + m_env.set_on(script); + + dataStatus = script(); + EXPECT_EQ(dataStatus, "data: a lot of data!"); + } +} diff --git a/client/logic/unittests/internal/TypeDataTest.cpp b/client/logic/unittests/internal/TypeDataTest.cpp new file mode 100644 index 000000000..5e680a65e --- /dev/null +++ b/client/logic/unittests/internal/TypeDataTest.cpp @@ -0,0 +1,122 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gmock/gmock.h" + +#include "internals/TypeData.h" + +namespace ramses::internal +{ + class ATypeData : public ::testing::Test + { + }; + + TEST_F(ATypeData, IsEqualToItself) + { + const TypeData data("name", EPropertyType::Bool); + EXPECT_EQ(data, data); + } + + TEST_F(ATypeData, IsNotEqualToAnotherTypeWithDifferentName) + { + const TypeData data1("name", EPropertyType::Int32); + const TypeData data2("name2", EPropertyType::Int32); + EXPECT_NE(data1, data2); + } + + TEST_F(ATypeData, IsNotEqualToAnotherTypeWithDifferentType) + { + const TypeData data1("name", EPropertyType::Int32); + const TypeData data2("name", EPropertyType::Bool); + EXPECT_NE(data1, data2); + } + + class AHierarchicalTypeData : public ::testing::Test + { + protected: + + HierarchicalTypeData m_data{ + TypeData("root", EPropertyType::Struct), + { + HierarchicalTypeData(TypeData("bool", EPropertyType::Bool),{}), + HierarchicalTypeData(TypeData("array", EPropertyType::Array),{}), + }}; + }; + + TEST_F(AHierarchicalTypeData, IsEqualToItself) + { + EXPECT_EQ(m_data, m_data); + } + + TEST_F(AHierarchicalTypeData, IsNotEqualToAnotherTypeWithDifferentName) + { + HierarchicalTypeData data1{ TypeData("name", EPropertyType::Struct), {} }; + HierarchicalTypeData data2{ TypeData("otherName", EPropertyType::Struct), {} }; + EXPECT_NE(data1, data2); + } + + TEST_F(AHierarchicalTypeData, IsNotEqualToAnotherTypeWithDifferentType) + { + HierarchicalTypeData data1{ TypeData("name", EPropertyType::Struct), {} }; + HierarchicalTypeData data2{ TypeData("name", EPropertyType::Array), {} }; + EXPECT_NE(data1, data2); + } + + TEST_F(AHierarchicalTypeData, IsNotEqualToAnotherTypeWithDifferentChildren) + { + HierarchicalTypeData noChildren{ TypeData("root", EPropertyType::Struct), std::vector() }; + HierarchicalTypeData moreChildren{ TypeData("root", EPropertyType::Struct), std::vector(3, HierarchicalTypeData(TypeData("bool", EPropertyType::Bool),{})) }; + HierarchicalTypeData fewerChildren{ TypeData("root", EPropertyType::Struct), std::vector(1, HierarchicalTypeData(TypeData("bool", EPropertyType::Bool),{})) }; + EXPECT_NE(m_data, noChildren); + EXPECT_NE(m_data, moreChildren); + EXPECT_NE(m_data, fewerChildren); + } + + TEST_F(AHierarchicalTypeData, IsNotEqualToAnotherTypeWithAdditionalSubchildren) + { + HierarchicalTypeData moreSubchildren{ + TypeData("root", EPropertyType::Struct), + { + HierarchicalTypeData(TypeData("bool", EPropertyType::Bool),{}), + HierarchicalTypeData(TypeData("array", EPropertyType::Array), + { + HierarchicalTypeData(TypeData("subchild", EPropertyType::Bool), {}), + }), + } }; + EXPECT_NE(m_data, moreSubchildren); + } + + TEST(StaticDataTypeHelper, CreatesPrimitiveType) + { + const HierarchicalTypeData expected{ + TypeData("name", EPropertyType::Vec4i), {}}; + EXPECT_EQ(expected, MakeType("name", EPropertyType::Vec4i)); + } + + TEST(StaticDataTypeHelper, CreatesArray) + { + const HierarchicalTypeData expected{ + TypeData("array", EPropertyType::Array), + { + HierarchicalTypeData(TypeData("", EPropertyType::Vec2f),{}), + HierarchicalTypeData(TypeData("", EPropertyType::Vec2f),{}) + } }; + EXPECT_EQ(expected, MakeArray("array", 2, EPropertyType::Vec2f)); + } + + TEST(StaticDataTypeHelper, CreatesStruct) + { + const HierarchicalTypeData expected{ + TypeData("struct", EPropertyType::Struct), + { + HierarchicalTypeData(TypeData("a", EPropertyType::Float),{}), + HierarchicalTypeData(TypeData("b", EPropertyType::String),{}) + } }; + EXPECT_EQ(expected, MakeStruct("struct", {{"a", EPropertyType::Float}, {"b", EPropertyType::String}})); + } +} diff --git a/client/logic/unittests/internal/TypeUtilsTest.cpp b/client/logic/unittests/internal/TypeUtilsTest.cpp new file mode 100644 index 000000000..48211caad --- /dev/null +++ b/client/logic/unittests/internal/TypeUtilsTest.cpp @@ -0,0 +1,88 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "internals/TypeUtils.h" + +#include "gmock/gmock.h" + +#include "LogicNodeDummy.h" + +namespace ramses::internal +{ + class ATypeUtils : public ::testing::Test + { + protected: + std::unique_ptr createArrayProperty(size_t size, EPropertyType type) + { + auto property = std::make_unique(MakeArray("", size, type), EPropertySemantics::BindingInput); + property->setLogicNode(dummyNode); + + return property; + } + + LogicNodeDummyImpl dummyNode{ "DummyNode" }; + }; + + TEST_F(ATypeUtils, DistinguishesValidTypeEnumsFromInvalidOnes) + { + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Bool)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Int32)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Int64)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Float)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Vec2i)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Vec3i)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Vec4i)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Vec2f)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Vec3f)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Vec4f)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::String)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Struct)); + EXPECT_TRUE(TypeUtils::IsValidType(EPropertyType::Array)); + + auto invalidType(static_cast(10000)); + EXPECT_FALSE(TypeUtils::IsValidType(invalidType)); + } + + TEST_F(ATypeUtils, ReportsPropertyTypeTraits) + { + EXPECT_TRUE(TypeUtils::IsPrimitiveType(EPropertyType::Bool)); + EXPECT_TRUE(TypeUtils::IsPrimitiveType(EPropertyType::Int32)); + EXPECT_TRUE(TypeUtils::IsPrimitiveType(EPropertyType::Int64)); + EXPECT_TRUE(TypeUtils::IsPrimitiveType(EPropertyType::Float)); + EXPECT_TRUE(TypeUtils::IsPrimitiveType(EPropertyType::Vec2i)); + EXPECT_TRUE(TypeUtils::IsPrimitiveType(EPropertyType::Vec4f)); + EXPECT_TRUE(TypeUtils::IsPrimitiveType(EPropertyType::String)); + + EXPECT_FALSE(TypeUtils::IsPrimitiveType(EPropertyType::Struct)); + EXPECT_FALSE(TypeUtils::IsPrimitiveType(EPropertyType::Array)); + + EXPECT_TRUE(TypeUtils::CanHaveChildren(EPropertyType::Struct)); + EXPECT_TRUE(TypeUtils::CanHaveChildren(EPropertyType::Array)); + + EXPECT_FALSE(TypeUtils::CanHaveChildren(EPropertyType::Bool)); + EXPECT_FALSE(TypeUtils::CanHaveChildren(EPropertyType::Vec2i)); + } + + TEST_F(ATypeUtils, DsitinguishesBetweenVectorAndNonVectorTypes) + { + EXPECT_TRUE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Vec2i)); + EXPECT_TRUE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Vec3i)); + EXPECT_TRUE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Vec4i)); + EXPECT_TRUE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Vec2f)); + EXPECT_TRUE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Vec3f)); + EXPECT_TRUE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Vec4f)); + + EXPECT_FALSE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Bool)); + EXPECT_FALSE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Int32)); + EXPECT_FALSE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Int64)); + EXPECT_FALSE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Float)); + EXPECT_FALSE(TypeUtils::IsPrimitiveVectorType(EPropertyType::String)); + EXPECT_FALSE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Struct)); + EXPECT_FALSE(TypeUtils::IsPrimitiveVectorType(EPropertyType::Array)); + } +} diff --git a/client/logic/unittests/internal/WrappedLuaPropertyTest.cpp b/client/logic/unittests/internal/WrappedLuaPropertyTest.cpp new file mode 100644 index 000000000..4d4bc4804 --- /dev/null +++ b/client/logic/unittests/internal/WrappedLuaPropertyTest.cpp @@ -0,0 +1,533 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "LuaScriptTest_Base.h" + +#include "impl/PropertyImpl.h" +#include "ramses-logic/Property.h" +#include "internals/WrappedLuaProperty.h" +#include "LogTestUtils.h" + +#include "fmt/format.h" + +namespace ramses::internal +{ + class AWrappedLuaProperty : public ::testing::Test + { + protected: + AWrappedLuaProperty() + { + WrappedLuaProperty::RegisterTypes(m_sol); + } + + PropertyImpl makeTestProperty(HierarchicalTypeData typeInfo, EPropertySemantics semantics = EPropertySemantics::ScriptOutput) + { + PropertyImpl prop(std::move(typeInfo), semantics); + setDummyDataRecursively(prop); + return prop; + } + + void setDummyDataRecursively(PropertyImpl& prop) + { + switch (prop.getType()) + { + case EPropertyType::Float: + prop.setValue(0.5f); + break; + case EPropertyType::Int32: + prop.setValue(42); + break; + case EPropertyType::Int64: + prop.setValue(int64_t{ 421 }); + break; + case EPropertyType::String: + prop.setValue(std::string("hello")); + break; + case EPropertyType::Bool: + prop.setValue(false); + break; + case EPropertyType::Vec2f: + prop.setValue(vec2f{ 0.1f, 0.2f }); + break; + case EPropertyType::Vec3f: + prop.setValue(vec3f{ 1.1f, 1.2f, 1.3f }); + break; + case EPropertyType::Vec4f: + prop.setValue(vec4f{ 2.1f, 2.2f, 2.3f, 1500000.4f }); + break; + case EPropertyType::Vec2i: + prop.setValue(vec2i{ 11, -12 }); + break; + case EPropertyType::Vec3i: + prop.setValue(vec3i{ 11, 12, 13 }); + break; + case EPropertyType::Vec4i: + prop.setValue(vec4i{ 11, 12, 13, 14000000 }); + break; + case EPropertyType::Array: + case EPropertyType::Struct: + for (size_t i = 0; i < prop.getChildCount(); ++i) + { + setDummyDataRecursively(*prop.getChild(i)->m_impl); + } + break; + } + } + + sol::protected_function_result run_WithResult(std::string_view source) + { + // Reference temporary extractor + sol::protected_function loaded = m_sol.load(source); + assert(loaded.valid()); + return loaded(); + } + + HierarchicalTypeData m_structWithAllPrimitiveTypes = MakeStruct("ROOT", { + TypeData{"Float", EPropertyType::Float}, + TypeData{"Vec2f", EPropertyType::Vec2f}, + TypeData{"Vec3f", EPropertyType::Vec3f}, + TypeData{"Vec4f", EPropertyType::Vec4f}, + TypeData{"Int32", EPropertyType::Int32}, + TypeData{"Int64", EPropertyType::Int64}, + TypeData{"Vec2i", EPropertyType::Vec2i}, + TypeData{"Vec3i", EPropertyType::Vec3i}, + TypeData{"Vec4i", EPropertyType::Vec4i}, + TypeData{"String", EPropertyType::String}, + TypeData{"Bool", EPropertyType::Bool}}); + + sol::state m_sol; + ScopedLogContextLevel m_silenceLogs = ScopedLogContextLevel{ELogLevel::Off}; + }; + + class AWrappedLuaProperty_Access : public AWrappedLuaProperty + { + protected: + template + T extractValue(std::string_view statement) + { + const std::string assignment = fmt::format("value = {}", statement); + run_WithResult(assignment); + T resultValue = m_sol["value"]; + return resultValue; + } + }; + + TEST_F(AWrappedLuaProperty_Access, WrapsProperty_ByReference) + { + PropertyImpl inputInt = makeTestProperty(MakeStruct("ROOT", {TypeData{"int", EPropertyType::Int32}})); + WrappedLuaProperty wrapped(inputInt); + m_sol["ROOT"] = std::ref(wrapped); + + WrappedLuaProperty& wrappedRef = m_sol["ROOT"]; + EXPECT_EQ(&wrappedRef, &wrapped); + const WrappedLuaProperty& wrappedRefConst = m_sol["ROOT"]; + EXPECT_EQ(&wrappedRefConst, &wrapped); + } + + TEST_F(AWrappedLuaProperty_Access, ExposesChildPropertiesAsWrappedObjects) + { + PropertyImpl nestedStruct(HierarchicalTypeData{ TypeData("Nested", EPropertyType::Struct), {m_structWithAllPrimitiveTypes} }, EPropertySemantics::ScriptInput); + WrappedLuaProperty wrapped(nestedStruct); + m_sol["Nested"] = std::ref(wrapped); + + // Use sol access overloads to check that type resolving works + WrappedLuaProperty& innerStructRef = m_sol["Nested"]["ROOT"]; + // size() is the only externally exposed property of the wrapper, use it to verify the type resolving worked + EXPECT_EQ(11u, innerStructRef.size()); + } + + TEST_F(AWrappedLuaProperty_Access, ResolvesPrimitiveTypes) + { + PropertyImpl inputAllPrimitives = makeTestProperty(m_structWithAllPrimitiveTypes, EPropertySemantics::ScriptInput); + WrappedLuaProperty wrapped(inputAllPrimitives); + m_sol["ROOT"] = std::ref(wrapped); + + EXPECT_FLOAT_EQ(0.5f, extractValue("ROOT.Float")); + EXPECT_FALSE(extractValue("ROOT.Bool")); + EXPECT_EQ("hello", extractValue("ROOT.String")); + EXPECT_EQ(42, extractValue("ROOT.Int32")); + } + + TEST_F(AWrappedLuaProperty_Access, ResolvesPrimitiveTypes_FloatVecTypes) + { + for (auto semantics: std::vector{EPropertySemantics::ScriptInput, EPropertySemantics::ScriptOutput}) + { + PropertyImpl inputAllPrimitives = makeTestProperty(m_structWithAllPrimitiveTypes, semantics); + WrappedLuaProperty wrapped(inputAllPrimitives); + m_sol["ROOT"] = std::ref(wrapped); + + EXPECT_FLOAT_EQ(0.1f, extractValue("ROOT.Vec2f[1]")); + EXPECT_FLOAT_EQ(0.2f, extractValue("ROOT.Vec2f[2]")); + EXPECT_FLOAT_EQ(1.1f, extractValue("ROOT.Vec3f[1]")); + EXPECT_FLOAT_EQ(1.2f, extractValue("ROOT.Vec3f[2]")); + EXPECT_FLOAT_EQ(1.3f, extractValue("ROOT.Vec3f[3]")); + EXPECT_FLOAT_EQ(2.1f, extractValue("ROOT.Vec4f[1]")); + EXPECT_FLOAT_EQ(2.2f, extractValue("ROOT.Vec4f[2]")); + EXPECT_FLOAT_EQ(2.3f, extractValue("ROOT.Vec4f[3]")); + EXPECT_FLOAT_EQ(1500000.4f, extractValue("ROOT.Vec4f[4]")); + } + } + + TEST_F(AWrappedLuaProperty_Access, ResolvesPrimitiveTypes_Int32VecTypes) + { + for (auto semantics : std::vector{ EPropertySemantics::ScriptInput, EPropertySemantics::ScriptOutput }) + { + PropertyImpl inputAllPrimitives = makeTestProperty(m_structWithAllPrimitiveTypes, semantics); + WrappedLuaProperty wrapped(inputAllPrimitives); + m_sol["ROOT"] = std::ref(wrapped); + + EXPECT_EQ(11, extractValue("ROOT.Vec2i[1]")); + EXPECT_EQ(-12, extractValue("ROOT.Vec2i[2]")); + EXPECT_EQ(11, extractValue("ROOT.Vec3i[1]")); + EXPECT_EQ(12, extractValue("ROOT.Vec3i[2]")); + EXPECT_EQ(13, extractValue("ROOT.Vec3i[3]")); + EXPECT_EQ(11, extractValue("ROOT.Vec4i[1]")); + EXPECT_EQ(12, extractValue("ROOT.Vec4i[2]")); + EXPECT_EQ(13, extractValue("ROOT.Vec4i[3]")); + EXPECT_EQ(14000000, extractValue("ROOT.Vec4i[4]")); + } + } + + TEST_F(AWrappedLuaProperty_Access, ResolvesNestedStructFields) + { + for (auto semantics : std::vector{ EPropertySemantics::ScriptInput, EPropertySemantics::ScriptOutput }) + { + PropertyImpl nestedStruct(HierarchicalTypeData{ TypeData("Nested", EPropertyType::Struct), {m_structWithAllPrimitiveTypes} }, semantics); + setDummyDataRecursively(nestedStruct); + WrappedLuaProperty wrapped(nestedStruct); + m_sol["Nested"] = std::ref(wrapped); + + EXPECT_EQ(11, extractValue("Nested.ROOT.Vec2i[1]")); + EXPECT_FLOAT_EQ(2.2f, extractValue("Nested.ROOT.Vec4f[2]")); + } + } + + TEST_F(AWrappedLuaProperty_Access, ResolvesArrayElements) + { + for (auto semantics : std::vector{ EPropertySemantics::ScriptInput, EPropertySemantics::ScriptOutput }) + { + PropertyImpl nestedArray(HierarchicalTypeData{ TypeData("Nested", EPropertyType::Struct), {MakeArray("array", 2, EPropertyType::Vec3f)} }, semantics); + setDummyDataRecursively(nestedArray); + WrappedLuaProperty wrapped(nestedArray); + m_sol["Nested"] = std::ref(wrapped); + + //Set second element to a different value + nestedArray.getChild("array")->getChild(1)->m_impl->setValue(vec3f{15, 16, 17}); + + EXPECT_FLOAT_EQ(1.1f, extractValue("Nested.array[1][1]")); + EXPECT_FLOAT_EQ(1.2f, extractValue("Nested.array[1][2]")); + EXPECT_FLOAT_EQ(1.3f, extractValue("Nested.array[1][3]")); + EXPECT_FLOAT_EQ(15.f, extractValue("Nested.array[2][1]")); + EXPECT_FLOAT_EQ(16.f, extractValue("Nested.array[2][2]")); + EXPECT_FLOAT_EQ(17.f, extractValue("Nested.array[2][3]")); + } + } + + + class AWrappedLuaProperty_Assignment : public AWrappedLuaProperty_Access + { + protected: + template + T assignExpressionToValue(std::string_view expression, std::string_view result) + { + run_WithResult(fmt::format("tempValue = {}", expression)); + run_WithResult(fmt::format("{} = tempValue", result)); + const T resultValue = m_sol["tempValue"]; + return resultValue; + } + }; + + TEST_F(AWrappedLuaProperty_Assignment, AssignsPrimitiveFieldToWrappedOutputStruct) + { + PropertyImpl outputAllPrimitives = makeTestProperty(m_structWithAllPrimitiveTypes, EPropertySemantics::ScriptOutput); + WrappedLuaProperty wrapped(outputAllPrimitives); + m_sol["ROOT"] = std::ref(wrapped); + + const sol::protected_function_result result = run_WithResult(R"( + ROOT.Int = 12 + )"); + + EXPECT_EQ(12, assignExpressionToValue("12", "ROOT.Int")); + } + + TEST_F(AWrappedLuaProperty_Assignment, CatchesErrorWhenTryingToAssignPrimitiveInputFields) + { + PropertyImpl input = makeTestProperty(MakeStruct("ROOT", {TypeData{"Int32", EPropertyType::Int32}}), EPropertySemantics::ScriptInput); + WrappedLuaProperty wrapped(input); + m_sol["ROOT"] = std::ref(wrapped); + + const sol::protected_function_result result = run_WithResult(R"( + ROOT.Int32 = 12 + )"); + + ASSERT_FALSE(result.valid()); + const sol::error err = result; + EXPECT_THAT(err.what(), ::testing::HasSubstr("Error while writing to 'Int32'. Writing input values is not allowed, only outputs!")); + } + + TEST_F(AWrappedLuaProperty_Assignment, OfStructProperties) + { + PropertyImpl struct1(HierarchicalTypeData{ TypeData("S1", EPropertyType::Struct), {m_structWithAllPrimitiveTypes} }, EPropertySemantics::ScriptOutput); + PropertyImpl struct2(HierarchicalTypeData{ TypeData("S2", EPropertyType::Struct), {m_structWithAllPrimitiveTypes} }, EPropertySemantics::ScriptOutput); + WrappedLuaProperty wrappedS1(struct1); + WrappedLuaProperty wrappedS2(struct1); + m_sol["S1"] = std::ref(wrappedS1); + m_sol["S2"] = std::ref(wrappedS2); + + const sol::protected_function_result result = run_WithResult(R"( + -- Assign from Lua table properties + S1.ROOT = { + Float = 0.5, + Vec2f = {11.1, 11.2}, + Vec3f = {11.1, 11.2, 11.3}, + Vec4f = {11.1, 11.2, 11.3, 11.4}, + Int32 = 42, + Int64 = 421, + Vec2i = {21, 22}, + Vec3i = {31, 32, 33}, + Vec4i = {41, 42, 43, 44}, + String = "abc", + Bool = true + } + -- Assign whole struct S1 -> to S2 + S2.ROOT = S1.ROOT + )"); + + ASSERT_TRUE(result.valid()); + + EXPECT_FLOAT_EQ(0.5f, extractValue("S2.ROOT.Float")); + EXPECT_TRUE(extractValue("S2.ROOT.Bool")); + EXPECT_EQ("abc", extractValue("S2.ROOT.String")); + EXPECT_EQ(42, extractValue("S2.ROOT.Int32")); + EXPECT_EQ(421, extractValue("S2.ROOT.Int64")); + + EXPECT_EQ(21, extractValue("S2.ROOT.Vec2i[1]")); + EXPECT_EQ(22, extractValue("S2.ROOT.Vec2i[2]")); + EXPECT_EQ(31, extractValue("S2.ROOT.Vec3i[1]")); + EXPECT_EQ(32, extractValue("S2.ROOT.Vec3i[2]")); + EXPECT_EQ(33, extractValue("S2.ROOT.Vec3i[3]")); + EXPECT_EQ(41, extractValue("S2.ROOT.Vec4i[1]")); + EXPECT_EQ(42, extractValue("S2.ROOT.Vec4i[2]")); + EXPECT_EQ(43, extractValue("S2.ROOT.Vec4i[3]")); + EXPECT_EQ(44, extractValue("S2.ROOT.Vec4i[4]")); + + EXPECT_FLOAT_EQ(11.1f, extractValue("S2.ROOT.Vec2f[1]")); + EXPECT_FLOAT_EQ(11.2f, extractValue("S2.ROOT.Vec2f[2]")); + EXPECT_FLOAT_EQ(11.1f, extractValue("S2.ROOT.Vec3f[1]")); + EXPECT_FLOAT_EQ(11.2f, extractValue("S2.ROOT.Vec3f[2]")); + EXPECT_FLOAT_EQ(11.3f, extractValue("S2.ROOT.Vec3f[3]")); + EXPECT_FLOAT_EQ(11.1f, extractValue("S2.ROOT.Vec4f[1]")); + EXPECT_FLOAT_EQ(11.2f, extractValue("S2.ROOT.Vec4f[2]")); + EXPECT_FLOAT_EQ(11.3f, extractValue("S2.ROOT.Vec4f[3]")); + EXPECT_FLOAT_EQ(11.4f, extractValue("S2.ROOT.Vec4f[4]")); + } + + TEST_F(AWrappedLuaProperty_Assignment, OfInvalidTypeToStructCausesError) + { + PropertyImpl aStruct(HierarchicalTypeData{ TypeData("S", EPropertyType::Struct), {m_structWithAllPrimitiveTypes} }, EPropertySemantics::ScriptOutput); + WrappedLuaProperty wrappedStruct(aStruct); + m_sol["S"] = std::ref(wrappedStruct); + + const sol::protected_function_result result = run_WithResult(R"( + S.ROOT = "this is not a table" + )"); + + ASSERT_FALSE(result.valid()); + const sol::error err = result; + EXPECT_THAT(err.what(), ::testing::HasSubstr("Unexpected type (string) while assigning value of struct field 'ROOT' (expected a table or another struct)")); + } + + TEST_F(AWrappedLuaProperty_Assignment, OfArrays) + { + PropertyImpl array1(HierarchicalTypeData{ TypeData("A1", EPropertyType::Struct), {MakeArray("data", 3, EPropertyType::Float)} }, EPropertySemantics::ScriptOutput); + PropertyImpl array2(HierarchicalTypeData{ TypeData("A2", EPropertyType::Struct), {MakeArray("data", 3, EPropertyType::Float)} }, EPropertySemantics::ScriptOutput); + WrappedLuaProperty wrappedA1(array1); + WrappedLuaProperty wrappedA2(array2); + m_sol["A1"] = std::ref(wrappedA1); + m_sol["A2"] = std::ref(wrappedA2); + + const sol::protected_function_result result = run_WithResult(R"( + -- Assignment from Lua table converts to array data + A1.data = { 1.1, 1.2, 1.3 } + -- Assign whole array A1 -> to A2 + A2.data = A1.data + )"); + + ASSERT_TRUE(result.valid()); + + EXPECT_FLOAT_EQ(1.1f, extractValue("A2.data[1]")); + EXPECT_FLOAT_EQ(1.2f, extractValue("A2.data[2]")); + EXPECT_FLOAT_EQ(1.3f, extractValue("A2.data[3]")); + } + + TEST_F(AWrappedLuaProperty_Assignment, OfInvalidTypeToArrayCausesError) + { + PropertyImpl array(HierarchicalTypeData{ TypeData("A", EPropertyType::Struct), {MakeArray("data", 3, EPropertyType::Float)} }, EPropertySemantics::ScriptOutput); + WrappedLuaProperty wrappedA(array); + m_sol["A"] = std::ref(wrappedA); + + const sol::protected_function_result result = run_WithResult(R"( + A.data = 5 + )"); + + ASSERT_FALSE(result.valid()); + const sol::error err = result; + EXPECT_THAT(err.what(), ::testing::HasSubstr("Unexpected type (number) while assigning value of array field 'data' (expected a table or another array)")); + } + + TEST_F(AWrappedLuaProperty_Assignment, OfInvalidTypeToPrimitiveCausesError) + { + for (auto fieldType : std::vector{ EPropertyType::Float, EPropertyType::Int32, EPropertyType::Int64, EPropertyType::String, EPropertyType::Bool }) + { + PropertyImpl root(HierarchicalTypeData{ TypeData("A", EPropertyType::Struct), {MakeType("field", fieldType)} }, EPropertySemantics::ScriptOutput); + WrappedLuaProperty wrapped(root); + m_sol["A"] = std::ref(wrapped); + + const sol::protected_function_result result = run_WithResult(R"( + A.field = {this = "is not a primitive type"} + )"); + + ASSERT_FALSE(result.valid()); + const sol::error err = result; + EXPECT_THAT(err.what(), ::testing::HasSubstr(fmt::format("Assigning table to '{}' output 'field'", GetLuaPrimitiveTypeName(fieldType)))); + } + } + + TEST_F(AWrappedLuaProperty_Assignment, OfVectorComponentsCausesError) + { + for (auto fieldType : std::vector{ EPropertyType::Vec2f, EPropertyType::Vec3f, EPropertyType::Vec4f, EPropertyType::Vec2i, EPropertyType::Vec3i, EPropertyType::Vec4i }) + { + PropertyImpl root(HierarchicalTypeData{ TypeData("IN", EPropertyType::Struct), {MakeType("vec", fieldType)} }, EPropertySemantics::ScriptOutput); + WrappedLuaProperty wrapped(root); + m_sol["IN"] = std::ref(wrapped); + + const sol::protected_function_result result = run_WithResult(R"( + IN.vec[1] = 1 + )"); + + ASSERT_FALSE(result.valid()); + const sol::error err = result; + EXPECT_THAT(err.what(), ::testing::HasSubstr(fmt::format("Error while writing to 'vec'. Can't assign individual components of vector types, must assign the whole vector"))); + } + } + + // Can't be triggered from user code, this is purely internal unit test + TEST_F(AWrappedLuaProperty_Assignment, OfUnexpectedUsertypeCausesError) + { + class AnotherUserType + { + } anotherUserObject; + + m_sol.new_usertype("AnotherUserType"); + m_sol["anotherUserObject"] = std::ref(anotherUserObject); + + PropertyImpl root(HierarchicalTypeData{ TypeData("A", EPropertyType::Struct), {MakeType("field", EPropertyType::Float)} }, EPropertySemantics::ScriptOutput); + WrappedLuaProperty wrapped(root); + m_sol["A"] = std::ref(wrapped); + + const sol::protected_function_result result = run_WithResult(R"( + A.field = anotherUserObject + )"); + + ASSERT_FALSE(result.valid()); + const sol::error err = result; + EXPECT_THAT(err.what(), ::testing::HasSubstr("Implementation error: Unexpected userdata")); + } + + TEST_F(AWrappedLuaProperty_Assignment, CatchesErrorWhenTryingToAssignStructInputFields) + { + PropertyImpl nestedStruct(HierarchicalTypeData{ TypeData("ROOT", EPropertyType::Struct), {m_structWithAllPrimitiveTypes} }, EPropertySemantics::ScriptInput); + WrappedLuaProperty wrapped(nestedStruct); + m_sol["ROOT"] = std::ref(wrapped); + + const sol::protected_function_result result = run_WithResult(R"( + ROOT.ROOT = {Int32 = 5, Float = 4.4} + )"); + + ASSERT_FALSE(result.valid()); + const sol::error err = result; + EXPECT_THAT(err.what(), ::testing::HasSubstr("Error while writing to 'ROOT'. Writing input values is not allowed, only outputs!")); + } + + TEST_F(AWrappedLuaProperty_Assignment, CatchesErrorWhenTryingToAssignArrayInputFields) + { + PropertyImpl nestedStruct(HierarchicalTypeData{ TypeData("ROOT", EPropertyType::Struct), {MakeArray("array", 2, EPropertyType::Float)}}, EPropertySemantics::ScriptInput); + WrappedLuaProperty wrapped(nestedStruct); + m_sol["ROOT"] = std::ref(wrapped); + + const sol::protected_function_result result = run_WithResult(R"( + ROOT.array[1] = 5 + )"); + + ASSERT_FALSE(result.valid()); + const sol::error err = result; + EXPECT_THAT(err.what(), ::testing::HasSubstr("Error while writing to 'idx: 1'. Writing input values is not allowed, only outputs!")); + } + + TEST_F(AWrappedLuaProperty_Assignment, AppliesNumericChecks_StructFields) + { + PropertyImpl root(m_structWithAllPrimitiveTypes, EPropertySemantics::ScriptOutput); + WrappedLuaProperty wrapped(root); + m_sol["ROOT"] = std::ref(wrapped); + + sol::error err = run_WithResult(R"( + ROOT.Int32 = 1000000000000000000000000000000000000000 + )"); + + EXPECT_THAT(err.what(), + ::testing::HasSubstr("Error during assignment of property 'Int32'! Error while extracting integer: integral part too large to fit in a signed 32-bit integer")); + + err = run_WithResult(R"( + ROOT.Int64 = 1000000000000000000000000000000000000000 + )"); + + EXPECT_THAT(err.what(), + ::testing::HasSubstr("Error during assignment of property 'Int64'! Error while extracting integer: integral part too large to fit in a signed 64-bit integer")); + + err = run_WithResult(R"( + ROOT.Float = 1000000000000000000000000000000000000000.0 + )"); + + EXPECT_THAT(err.what(), + ::testing::HasSubstr("Error during assignment of property 'Float'! Error while extracting floating point number: value would cause overflow in float")); + } + + TEST_F(AWrappedLuaProperty_Assignment, AppliesNumericChecks_StructFields_Vec) + { + PropertyImpl root(m_structWithAllPrimitiveTypes, EPropertySemantics::ScriptOutput); + WrappedLuaProperty wrapped(root); + m_sol["ROOT"] = std::ref(wrapped); + + sol::error err = run_WithResult(R"( + ROOT.Vec2f = {1000000000000000000000000000000000000000.0, 0} + )"); + + EXPECT_THAT(err.what(), + ::testing::HasSubstr( + "lua: error: Error while assigning output Vec2 property 'Vec2f'. " + "Error while extracting array: unexpected value (type: 'number') at array element # 1! " + "Reason: Error while extracting floating point number: value would cause overflow in float")); + } + + TEST_F(AWrappedLuaProperty_Assignment, AppliesNumericChecks_Array) + { + PropertyImpl nestedStruct(HierarchicalTypeData{ TypeData("ROOT", EPropertyType::Struct), {MakeArray("array", 2, EPropertyType::Int32)} }, EPropertySemantics::ScriptOutput); + WrappedLuaProperty wrapped(nestedStruct); + m_sol["ROOT"] = std::ref(wrapped); + + sol::error err = run_WithResult(R"( + ROOT.array = {1.5, 0} + )"); + + EXPECT_THAT(err.what(), + ::testing::HasSubstr( + "lua: error: Error during assignment of property ''! Error while extracting integer: implicit rounding (fractional part '0.5' is not negligible)")); + } + + +} diff --git a/client/logic/unittests/res/testLogic_01.rlogic b/client/logic/unittests/res/testLogic_01.rlogic new file mode 100644 index 0000000000000000000000000000000000000000..f8d7cee51bbe4e103b66894f88537fb41df08b4d GIT binary patch literal 19416 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 Yb!ath9Le-&onc^f>+s0%$ja&d7vmqBfdBvi literal 0 HcmV?d00001 diff --git a/client/logic/unittests/res/testScene_01.ramses b/client/logic/unittests/res/testScene_01.ramses new file mode 100644 index 0000000000000000000000000000000000000000..aa618b70a8ba14fa234e7f2ae9bb2058bb0a74f2 GIT binary patch literal 5852 zcmd6reP~s67{~86D_t+@W|{pVClXT8vDF$=w>ICVSOe2E!-gHV<92Pk_j2!bC^{Te zh$TTpp<$q9e^fM4Op3}8Az7HxpJIjd@`o>#&_89wdOqho&)K;z(?t?K@VnpN^ZcH- z-}8HZ=bUZr3b!ZYWmbFXOgx#G`#^nFbyam+`PwD%>{2(=HFraGZGCM`O?!L02wOZi zyD9BG>h*fvb8FhlTVnz3pBUg)OI& zuq4VbC)*ri3pq3wv0v1S=Tl|X1nsMM5TAb1N)KJcN^cwz34 z5v9_y67vxeixe+IGR8-judR&}Oo?NbqnTruPdAbu_QDven4c^B_0plU@O6u#KmlDZ z9cvQ0k(kBkdg*YU_!><=w2z%I^@qs^@7YtgW9yW%z0X*F_!ueA(dh7u!2b5D__4a1 z4mQgMue|=*mu(kjoIn27oK+PreZU!1s-JSf$vPJ(&_`C3Y%?GZA&^2rpl#Z8{RLyp z(3H%HuVf5L9WqYHhu z6{^ad@KldE>meBvq_M9V1&l&6M)PdK08cr@iz6gsI34C#-l}u*xJ~Em(oP%Qkjx+y zU=6V_L5F!l?h|eyO|+n(gbK+Rl!JoyH?h;4CpFvT@f{Kj7RN3h9ymDrplmj!uXDiF&y^Z-()h-(!dkE@5vI~p;pv&sSNN*#f^%5cOl=U z_=X_m8&Etd2ptO`c1m!kjPDi8B8q>+DBzEp|wp zyx>B?J%Wn`_X{o&d`GZZa6l0KK|%Dt5Jdm1;3B~v1+lv*h~4jk%LM-rlork*_Q#t+ zO6ISX3R07-m&rPbWQUh;05ALWZ>#jHPZ&V(IIw2{GD!N*FKjj)cN3l6p3~!Hx_s~U zGC;#Di6EWhr-WzZL?l%|t(5$gmU7zN9xv^ZH-u8u0Wyqn#{RfZtn&&Z&EV}M1@TWb zOe}E9Awy(=5oBoZ7qX#Qkn!cXnFmz=sXX zC%@Nl8gG=Xr#B^W((%m}%Gz_}P;8Krft;UTbvKpr+_al$_t-CFnZR?r)W%MQJ(2-L z!x<4AU5+{IlwsGlNf2{Pt*@i``~7P-pwmnCc-iz8(uFX#ZiFWm&VoK2#{BQ1mzGwp zx$xWJpD%tsbYkI|Q-`cuI8)}9NOV8=WeucM*6zfIkQ>qN5XnQ-igl|e&s$`+PMwMo zSB0Fy`lY=@hnIFzZYE>|_4l5r)C1P(&u5M*V|bh`+s(?UtlyONoL;Z} zL1*|X&nxuYn~Zme!+2u)i~>Jf3nZk~*}ZPLJ{p|H;WV6?M5~=X9Br0UQ&l~qFulIm z)i*nw60?7iR!iFLT$t=mrX?>w7l}X1k$?Q@&q(QL1ISS*(LB~1`43u7&o0YI589{> z7r-90$n62ZeB{1(R+cNs;_9W!?F;{X;6&dFg~cysQS4oDEla-#ZC`6^+6wjm&n}Ga XSsJm}hDPd7k%!x;F0zdquUr2CecOqV literal 0 HcmV?d00001 diff --git a/framework/Animation/Animation/test/AnimationInstanceTest.h b/client/logic/unittests/shared/FeatureLevelTestValues.h similarity index 58% rename from framework/Animation/Animation/test/AnimationInstanceTest.h rename to client/logic/unittests/shared/FeatureLevelTestValues.h index 1f1870b40..72f7ffc1a 100644 --- a/framework/Animation/Animation/test/AnimationInstanceTest.h +++ b/client/logic/unittests/shared/FeatureLevelTestValues.h @@ -1,28 +1,22 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2012 BMW Car IT GmbH +// Copyright (C) 2022 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#ifndef RAMSES_ANIMATIONINSTANCETEST_H -#define RAMSES_ANIMATIONINSTANCETEST_H +#pragma once -#include "framework_common_gmock_header.h" #include "gtest/gtest.h" +#include "ramses-framework-api/EFeatureLevel.h" -namespace ramses_internal +namespace ramses::internal { - class AnimationInstanceTest : public testing::Test + static + ::testing::internal::ValueArray + GetFeatureLevelTestValues() { - public: - AnimationInstanceTest() - { - } - - protected: - }; + return ::testing::Values(ramses::EFeatureLevel_01); + } } - -#endif diff --git a/client/logic/unittests/shared/LogTestUtils.h b/client/logic/unittests/shared/LogTestUtils.h new file mode 100644 index 000000000..59dca7630 --- /dev/null +++ b/client/logic/unittests/shared/LogTestUtils.h @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-framework-api/RamsesFrameworkTypes.h" +#include "ramses-logic/Logger.h" + +namespace ramses +{ + class ScopedLogContextLevel + { + public: + explicit ScopedLogContextLevel(ELogLevel verbosityLimit) + : m_savedLogVerbosityLimit(Logger::GetLogVerbosityLimit()) + { + Logger::SetLogVerbosityLimit(verbosityLimit); + } + + ScopedLogContextLevel(ELogLevel logPriority, const ramses::Logger::LogHandlerFunc& handler) + : ScopedLogContextLevel(logPriority) + { + Logger::SetLogHandler(handler); + m_unsetCustomHandler = true; + } + + ~ScopedLogContextLevel() + { + Logger::SetLogVerbosityLimit(m_savedLogVerbosityLimit); + + if (m_unsetCustomHandler) + { + // Set an empty lambda to avoid side effects + Logger::SetLogHandler([](ELogLevel type, std::string_view message){ + (void)type; + (void)message; + }); + } + } + + ScopedLogContextLevel(const ScopedLogContextLevel&) = default; + ScopedLogContextLevel& operator=(const ScopedLogContextLevel&) = default; + + private: + ELogLevel m_savedLogVerbosityLimit; + bool m_unsetCustomHandler = false; + }; + + struct TestLog + { + ELogLevel type; + std::string message; + }; + + class TestLogCollector + { + public: + explicit TestLogCollector(ELogLevel verbosityLimit) + : m_logCollector(verbosityLimit, [this](ELogLevel type, std::string_view message) + { + logs.emplace_back(TestLog{type, std::string{message}}); + }) + { + } + + std::vector logs; + + private: + ScopedLogContextLevel m_logCollector; + }; +} diff --git a/client/logic/unittests/shared/LogicEngineTest_Base.h b/client/logic/unittests/shared/LogicEngineTest_Base.h new file mode 100644 index 000000000..1770a2b31 --- /dev/null +++ b/client/logic/unittests/shared/LogicEngineTest_Base.h @@ -0,0 +1,167 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "RamsesTestUtils.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LuaModule.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesRenderPassBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesRenderGroupBindingElements.h" +#include "ramses-logic/RamsesMeshNodeBinding.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/TimerNode.h" +#include "ramses-logic/AnchorPoint.h" +#include "ramses-logic/SkinBinding.h" +#include "ramses-client-api/OrthographicCamera.h" +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-utils.h" + +namespace ramses +{ + class ALogicEngineBase + { + public: + explicit ALogicEngineBase(ramses::EFeatureLevel featureLevel = ramses::EFeatureLevel_Latest) + : m_logicEngine{ featureLevel } + { + // make ramses camera valid, needed for anchor points + m_camera->setFrustum(-1.f, 1.f, -1.f, 1.f, 0.1f, 1.f); + m_renderGroup->addMeshNode(*m_meshNode); + m_saveFileConfigNoValidation.setValidationEnabled(false); + } + + static LuaConfig CreateDeps(const std::vector>& dependencies) + { + LuaConfig config; + for (const auto& [alias, module] : dependencies) + { + config.addDependency(alias, *module); + } + + return config; + } + + static LuaConfig WithStdModules(std::initializer_list modules) + { + LuaConfig config; + for (auto m : modules) + { + config.addStandardModuleDependency(m); + } + return config; + } + + static bool SaveToFileWithoutValidation(LogicEngine& logicEngine, std::string_view filename) + { + SaveFileConfig configNoValidation; + configNoValidation.setValidationEnabled(false); + return logicEngine.saveToFile(filename, configNoValidation); + } + + protected: + LogicEngine m_logicEngine; + SaveFileConfig m_saveFileConfigNoValidation; + RamsesTestSetup m_ramses; + ramses::Scene* m_scene = { m_ramses.createScene() }; + ramses::Node* m_node = { m_scene->createNode() }; + ramses::OrthographicCamera* m_camera = { m_scene->createOrthographicCamera() }; + ramses::Appearance* m_appearance = { &RamsesTestSetup::CreateTrivialTestAppearance(*m_scene) }; + ramses::RenderPass* m_renderPass = { m_scene->createRenderPass() }; + ramses::RenderGroup* m_renderGroup = { m_scene->createRenderGroup() }; + ramses::MeshNode* m_meshNode = { m_scene->createMeshNode("meshNode") }; + + const std::string_view m_valid_empty_script = R"( + function interface(IN,OUT) + end + function run(IN,OUT) + end + )"; + + const std::string_view m_invalid_empty_script = R"( + )"; + + const std::string_view m_moduleSourceCode = R"( + local mymath = {} + function mymath.add(a,b) + print(a+b) + end + return mymath + )"; + + const std::string_view m_interfaceSourceCode = R"( + function interface(inout_params) + inout_params.param_vec3f = Type:Vec3f() + end + )"; + + void recreate() + { + const ramses::sceneId_t sceneId = m_scene->getSceneId(); + + m_ramses.destroyScene(*m_scene); + m_scene = m_ramses.createScene(sceneId); + m_node = m_scene->createNode(); + m_camera = m_scene->createOrthographicCamera(); + m_appearance = &RamsesTestSetup::CreateTrivialTestAppearance(*m_scene); + m_renderPass = m_scene->createRenderPass(); + m_renderGroup = m_scene->createRenderGroup(); + m_meshNode = m_scene->createMeshNode(); + } + + RamsesRenderGroupBinding* createRenderGroupBinding(LogicEngine& logicEngine) + { + RamsesRenderGroupBindingElements elements; + EXPECT_TRUE(elements.addElement(*m_meshNode, "mesh")); + return logicEngine.createRamsesRenderGroupBinding(*m_renderGroup, elements, "renderGroupBinding"); + } + + RamsesRenderGroupBinding* createRenderGroupBinding() + { + return createRenderGroupBinding(m_logicEngine); + } + + static SkinBinding* createSkinBinding(const RamsesNodeBinding& nodeBinding, RamsesAppearanceBinding& appearanceBinding, LogicEngine& logicEngine) + { + ramses::UniformInput uniform; + appearanceBinding.getRamsesAppearance().getEffect().findUniformInput("jointMat", uniform); + EXPECT_TRUE(uniform.isValid()); + return logicEngine.createSkinBinding({ &nodeBinding }, { matrix44f{ 0.f } }, appearanceBinding, uniform, "skin"); + } + + SkinBinding* createSkinBinding(LogicEngine& logicEngine) + { + const auto nodeBinding = logicEngine.createRamsesNodeBinding(*m_node, ramses::ERotationType::Euler_XYZ, "nodeForSkin"); + auto appearanceBinding = logicEngine.createRamsesAppearanceBinding(*m_appearance, "appearanceForSkin"); + return createSkinBinding(*nodeBinding, *appearanceBinding, logicEngine); + } + + size_t m_emptySerializedSizeTotal{164u}; + }; + + class ALogicEngine : public ALogicEngineBase, public ::testing::Test + { + public: + explicit ALogicEngine(ramses::EFeatureLevel featureLevel = ramses::EFeatureLevel_Latest) + : ALogicEngineBase{ featureLevel } + { + } + }; +} diff --git a/framework/Communication/TransportCommon/src/ConnectionSystemBase.cpp b/client/logic/unittests/shared/LogicNodeDummy.cpp similarity index 83% rename from framework/Communication/TransportCommon/src/ConnectionSystemBase.cpp rename to client/logic/unittests/shared/LogicNodeDummy.cpp index 1dddf2fa7..999b98f58 100644 --- a/framework/Communication/TransportCommon/src/ConnectionSystemBase.cpp +++ b/client/logic/unittests/shared/LogicNodeDummy.cpp @@ -1,9 +1,9 @@ // ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW Car IT GmbH +// Copyright (C) 2020 BMW AG // ------------------------------------------------------------------------- // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#include "TransportCommon/ConnectionSystemBase.h" +#include "LogicNodeDummy.h" diff --git a/client/logic/unittests/shared/LogicNodeDummy.h b/client/logic/unittests/shared/LogicNodeDummy.h new file mode 100644 index 000000000..b36ee3d91 --- /dev/null +++ b/client/logic/unittests/shared/LogicNodeDummy.h @@ -0,0 +1,86 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "impl/LogicNodeImpl.h" +#include "impl/PropertyImpl.h" +#include "impl/RamsesBindingImpl.h" + +#include "ramses-logic/LogicNode.h" +#include "ramses-logic/Property.h" + +namespace ramses::internal +{ + class LogicNodeDummyImpl : public LogicNodeImpl + { + public: + explicit LogicNodeDummyImpl(std::string_view name, bool createNestedProperties = false) + : LogicNodeImpl(name, 1u) + { + setRootProperties( + std::make_unique(CreateTestInputsType(createNestedProperties), EPropertySemantics::ScriptInput), + std::make_unique(CreateTestOutputsType(createNestedProperties), EPropertySemantics::ScriptOutput)); + } + + std::optional update() override + { + return std::nullopt; + } + + void createRootProperties() final {} + + private: + static HierarchicalTypeData CreateTestInputsType(bool createNestedProperties) + { + HierarchicalTypeData inputsStruct = MakeStruct("", { + {"input1", EPropertyType::Int32}, + {"input2", EPropertyType::Int32}, + }); + + if (createNestedProperties) + { + inputsStruct.children.emplace_back(MakeStruct("inputStruct", { {"nested", EPropertyType::Int32} })); + inputsStruct.children.emplace_back(MakeArray("inputArray", 1, EPropertyType::Int32)); + } + + return inputsStruct; + } + + static HierarchicalTypeData CreateTestOutputsType(bool createNestedProperties) + { + HierarchicalTypeData outputsStruct = MakeStruct("", { + {"output1", EPropertyType::Int32}, + {"output2", EPropertyType::Int32}, + }); + + if (createNestedProperties) + { + outputsStruct.children.emplace_back(MakeStruct("outputStruct", { {"nested", EPropertyType::Int32} })); + outputsStruct.children.emplace_back(MakeArray("outputArray", 1, EPropertyType::Int32)); + } + + return outputsStruct; + } + }; + + class RamsesBindingDummyImpl : public RamsesBindingImpl + { + public: + RamsesBindingDummyImpl() : RamsesBindingImpl("dummybinding", 1u) + { + } + + std::optional update() override + { + return std::nullopt; + } + + void createRootProperties() final {} + }; +} diff --git a/client/logic/unittests/shared/LuaScriptTest_Base.h b/client/logic/unittests/shared/LuaScriptTest_Base.h new file mode 100644 index 000000000..870d9c27c --- /dev/null +++ b/client/logic/unittests/shared/LuaScriptTest_Base.h @@ -0,0 +1,71 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "LogicEngineTest_Base.h" + +namespace ramses +{ + class ALuaScript : public ALogicEngine + { + protected: + std::string_view m_minimalScript = R"( + function interface(IN,OUT) + end + + function run(IN,OUT) + end + )"; + + std::string_view m_minimalScriptWithInputs = R"( + function interface(IN,OUT) + IN.speed = Type:Int32() + IN.speed2 = Type:Int64() + IN.temp = Type:Float() + IN.name = Type:String() + IN.enabled = Type:Bool() + IN.vec2f = Type:Vec2f() + IN.vec3f = Type:Vec3f() + IN.vec4f = Type:Vec4f() + IN.vec2i = Type:Vec2i() + IN.vec3i = Type:Vec3i() + IN.vec4i = Type:Vec4i() + end + + function run(IN,OUT) + end + )"; + + std::string_view m_minimalScriptWithOutputs = R"( + function interface(IN,OUT) + OUT.speed = Type:Int32() + OUT.speed2 = Type:Int64() + OUT.temp = Type:Float() + OUT.name = Type:String() + OUT.enabled = Type:Bool() + OUT.vec2f = Type:Vec2f() + OUT.vec3f = Type:Vec3f() + OUT.vec4f = Type:Vec4f() + OUT.vec2i = Type:Vec2i() + OUT.vec3i = Type:Vec3i() + OUT.vec4i = Type:Vec4i() + end + + function run(IN,OUT) + end + )"; + + // Convenience type for testing + struct LuaTestError + { + std::string errorCode; + std::string expectedErrorMessage; + }; + }; +} diff --git a/client/logic/unittests/shared/PropertyLinkTestUtils.h b/client/logic/unittests/shared/PropertyLinkTestUtils.h new file mode 100644 index 000000000..e2eb10704 --- /dev/null +++ b/client/logic/unittests/shared/PropertyLinkTestUtils.h @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2022 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "gtest/gtest.h" + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/LogicNode.h" +#include "ramses-logic/Property.h" +#include +#include +#include +#include + +namespace ramses +{ + class PropertyLinkTestUtils + { + public: + // This util will collect all existing links in logic engine and match them with given expected links, + // i.e. the expected links must not only exist but also be the only links in the engine. + // This also tests all the relevant public API for querying links. + static void ExpectLinks(const LogicEngine& logicEngine, std::vector expectedLinks) + { + // collect all exisintg links from logic engine + const auto allObjects = logicEngine.getCollection(); + + std::vector allProperties; + for (const auto obj : allObjects) + { + const auto logicNode = obj->as(); + if (!logicNode) + continue; + + std::deque props{ logicNode->getInputs(), logicNode->getOutputs() }; + while (!props.empty()) + { + auto prop = props.back(); + props.pop_back(); + if (prop == nullptr) + continue; + + allProperties.push_back(prop); + + for (size_t i = 0u; i < prop->getChildCount(); ++i) + props.push_back(prop->getChild(i)); + } + } + + std::vector allLinks; + for (auto prop : allProperties) + { + if (prop->hasIncomingLink()) + allLinks.push_back(*prop->getIncomingLink()); + } + + EXPECT_EQ(expectedLinks.size(), allLinks.size()) << "Count of expected links does not match actual count of existing links"; + if (expectedLinks.size() != allLinks.size()) + return; + + auto propertLinkComp = [](const PropertyLink& a, const PropertyLink& b) { + assert(a.source != b.source || a.target != b.target); + if (a.source != b.source) + return a.source < b.source; + return a.target < b.target; + }; + std::sort(expectedLinks.begin(), expectedLinks.end(), propertLinkComp); + std::sort(allLinks.begin(), allLinks.end(), propertLinkComp); + + for (size_t i = 0u; i < allLinks.size(); ++i) + { + EXPECT_EQ(expectedLinks[i].source, allLinks[i].source) << fmt::format("expected and actual source of link #{} does not match", i); + EXPECT_EQ(expectedLinks[i].target, allLinks[i].target) << fmt::format("expected and actual target of link #{} does not match", i); + EXPECT_EQ(expectedLinks[i].isWeakLink, allLinks[i].isWeakLink) << fmt::format("expected and actual weak link flag of link #{} does not match", i); + + // checks for the other existing (mostly redundant) API + EXPECT_TRUE(expectedLinks[i].source->isLinked()); + EXPECT_TRUE(expectedLinks[i].source->hasOutgoingLink()); + EXPECT_TRUE(expectedLinks[i].source->getOutgoingLinksCount() > 0u); + EXPECT_TRUE(expectedLinks[i].target->isLinked()); + EXPECT_TRUE(expectedLinks[i].target->hasIncomingLink()); + } + + // test also logic engine getter + auto engineLinks = logicEngine.getPropertyLinks(); + std::sort(engineLinks.begin(), engineLinks.end(), propertLinkComp); + const bool equals = std::equal(engineLinks.cbegin(), engineLinks.cend(), expectedLinks.cbegin(), expectedLinks.cend(), [](const auto& l1, const auto& l2) { + return l1.source == l2.source + && l1.target == l2.target + && l1.isWeakLink == l2.isWeakLink; + }); + EXPECT_TRUE(equals) << "Links retrieved from LogicEngine API do not match!"; + } + }; +} diff --git a/client/logic/unittests/shared/RamsesObjectResolverMock.h b/client/logic/unittests/shared/RamsesObjectResolverMock.h new file mode 100644 index 000000000..6fb1366eb --- /dev/null +++ b/client/logic/unittests/shared/RamsesObjectResolverMock.h @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "gmock/gmock.h" + +#include "internals/RamsesObjectResolver.h" + +namespace ramses::internal +{ + class RamsesObjectResolverMock : public IRamsesObjectResolver + { + public: + MOCK_METHOD(ramses::Node*, findRamsesNodeInScene, (std::string_view logicNodeName, ramses::sceneObjectId_t objectId), (const, override)); + MOCK_METHOD(ramses::Appearance*, findRamsesAppearanceInScene, (std::string_view logicNodeName, ramses::sceneObjectId_t objectId), (const, override)); + MOCK_METHOD(ramses::Camera*, findRamsesCameraInScene, (std::string_view logicNodeName, ramses::sceneObjectId_t objectId), (const, override)); + MOCK_METHOD(ramses::RenderPass*, findRamsesRenderPassInScene, (std::string_view logicNodeName, ramses::sceneObjectId_t objectId), (const, override)); + MOCK_METHOD(ramses::RenderGroup*, findRamsesRenderGroupInScene, (std::string_view logicNodeName, ramses::sceneObjectId_t objectId), (const, override)); + MOCK_METHOD(ramses::SceneObject*, findRamsesSceneObjectInScene, (std::string_view logicNodeName, ramses::sceneObjectId_t objectId), (const, override)); + }; +} diff --git a/client/logic/unittests/shared/RamsesTestUtils.h b/client/logic/unittests/shared/RamsesTestUtils.h new file mode 100644 index 000000000..0b30e530d --- /dev/null +++ b/client/logic/unittests/shared/RamsesTestUtils.h @@ -0,0 +1,177 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include +#include +#include + +#include "ramses-client-api/RamsesClient.h" +#include "ramses-client-api/Scene.h" +#include "ramses-client-api/EffectDescription.h" +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/RenderPass.h" +#include "ramses-client-api/RenderGroup.h" +#include "ramses-client-api/AttributeInput.h" +#include "ramses-client-api/GeometryBinding.h" +#include "ramses-client-api/Effect.h" +#include "ramses-client-api/MeshNode.h" +#include "ramses-framework-api/RamsesFramework.h" +#include "ramses-framework-api/DataTypes.h" + +namespace ramses +{ + struct TriangleTestScene + { + TriangleTestScene(ramses::RamsesClient& client, ramses::sceneId_t sceneId) + { + scene = client.createScene(sceneId, ramses::SceneConfig(), "simple triangle scene"); + camera = scene->createPerspectiveCamera(); + camera->setFrustum(20.0f, 1.0f, 0.1f, 100.0f); + camera->setViewport(0, 0, 800, 800); + camera->setTranslation({0.0f, 0.0f, 5.0f}); + renderPass = scene->createRenderPass(); + renderPass->setClearFlags(ramses::EClearFlags_None); + renderPass->setCamera(*camera); + renderGroup = scene->createRenderGroup(); + renderPass->addRenderGroup(*renderGroup); + + std::array vertexPositionsArray{ ramses::vec3f{-1.f, 0.f, -1.f}, ramses::vec3f{1.f, 0.f, -1.f}, ramses::vec3f{0.f, 1.f, -1.f} }; + ramses::ArrayResource* vertexPositions = scene->createArrayResource(3u, vertexPositionsArray.data()); + + ramses::EffectDescription effectDesc; + effectDesc.setVertexShader(R"( + #version 100 + + uniform highp mat4 mvpMatrix; + + attribute vec3 a_position; + + void main() + { + gl_Position = mvpMatrix * vec4(a_position, 1.0); + } + )"); + effectDesc.setFragmentShader(R"( + #version 100 + + uniform highp float green; + uniform highp float blue; + + void main(void) + { + gl_FragColor = vec4(1.0, green, blue, 1.0); + } + )"); + + effectDesc.setUniformSemantic("mvpMatrix", ramses::EEffectUniformSemantic::ModelViewProjectionMatrix); + + const ramses::Effect* effect = scene->createEffect(effectDesc, ramses::ResourceCacheFlag_DoNotCache); + appearance = scene->createAppearance(*effect); + + ramses::GeometryBinding* geometry = scene->createGeometryBinding(*effect); + ramses::AttributeInput positionsInput; + effect->findAttributeInput("a_position", positionsInput); + geometry->setInputBuffer(positionsInput, *vertexPositions); + + meshNode = scene->createMeshNode("triangle mesh node"); + meshNode->setAppearance(*appearance); + meshNode->setIndexCount(3); + meshNode->setGeometryBinding(*geometry); + + renderGroup->addMeshNode(*meshNode); + + scene->flush(); + scene->publish(); + } + + ramses::Scene* scene; + ramses::PerspectiveCamera* camera; + ramses::RenderPass* renderPass; + ramses::RenderGroup* renderGroup; + ramses::Appearance* appearance; + ramses::MeshNode* meshNode; + }; + + + class RamsesTestSetup + { + public: + RamsesTestSetup() + { + ramses::RamsesFrameworkConfig frameworkConfig{ramses::EFeatureLevel_Latest}; + frameworkConfig.setLogLevel(ramses::ELogLevel::Off); + m_ramsesFramework = std::make_unique(frameworkConfig); + m_ramsesClient = m_ramsesFramework->createClient("test client"); + } + + ramses::Scene* createScene(ramses::sceneId_t sceneId = ramses::sceneId_t(1)) + { + return m_ramsesClient->createScene(sceneId); + } + + void destroyScene(ramses::Scene& scene) + { + m_ramsesClient->destroy(scene); + } + + ramses::Scene& loadSceneFromFile(std::string_view fileName) + { + auto scene = m_ramsesClient->loadSceneFromFile(fileName); + assert(scene); + return *scene; + } + + static ramses::Appearance& CreateTestAppearance(ramses::Scene& scene, std::string_view vertShader, std::string_view fragShader) + { + ramses::EffectDescription effectDesc; + effectDesc.setUniformSemantic("u_DisplayBufferResolution", ramses::EEffectUniformSemantic::DisplayBufferResolution); + effectDesc.setVertexShader(vertShader.data()); + effectDesc.setFragmentShader(fragShader.data()); + return *scene.createAppearance(*scene.createEffect(effectDesc), "test appearance"); + } + + static ramses::Appearance& CreateTrivialTestAppearance(ramses::Scene& scene) + { + std::string_view vertShader = R"( + #version 100 + + uniform highp float floatUniform; + uniform highp mat4 jointMat[1]; + attribute vec3 a_position; + + void main() + { + gl_Position = floatUniform * vec4(a_position, 1.0) * jointMat[0]; + })"; + + std::string_view fragShader = R"( + #version 100 + + void main(void) + { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + })"; + + return CreateTestAppearance(scene, vertShader, fragShader); + } + + TriangleTestScene createTriangleTestScene(ramses::sceneId_t sceneId = ramses::sceneId_t(1)) + { + return TriangleTestScene(*m_ramsesClient, sceneId); + } + + private: + std::unique_ptr m_ramsesFramework; + ramses::RamsesClient* m_ramsesClient; + }; +} diff --git a/client/logic/unittests/shared/SerializationTestUtils.h b/client/logic/unittests/shared/SerializationTestUtils.h new file mode 100644 index 000000000..2673dc804 --- /dev/null +++ b/client/logic/unittests/shared/SerializationTestUtils.h @@ -0,0 +1,112 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "generated/LuaScriptGen.h" +#include "generated/LuaInterfaceGen.h" +#include "generated/PropertyGen.h" +#include "generated/RamsesRenderPassBindingGen.h" +#include "generated/RamsesRenderGroupBindingGen.h" + +namespace ramses::internal +{ + class SerializationTestUtils + { + public: + explicit SerializationTestUtils(flatbuffers::FlatBufferBuilder& builder) + : m_builder(builder) + { + } + + flatbuffers::Offset serializeTestProperty( + std::string_view name, + rlogic_serialization::EPropertyRootType type = rlogic_serialization::EPropertyRootType::Struct, + bool withChildren = true, + bool withErrors = false) + { + if (withErrors) + { + // Unnamed property -> causes errors down the hierarchy + return rlogic_serialization::CreateProperty( + m_builder, + 0); + } + + if (withChildren) + { + auto child = rlogic_serialization::CreateProperty( + m_builder, + m_builder.CreateString("child"), + rlogic_serialization::EPropertyRootType::Primitive, + 0, + rlogic_serialization::PropertyValue::float_s, + m_builder.CreateStruct(rlogic_serialization::float_s{0.42f}).Union() + ); + return rlogic_serialization::CreateProperty( + m_builder, + m_builder.CreateString(name), + type, + m_builder.CreateVector(std::vector{child})); + } + + return rlogic_serialization::CreateProperty( + m_builder, + m_builder.CreateString(name), + type); + } + + flatbuffers::Offset serializeTestScriptWithError() + { + return rlogic_serialization::CreateLuaScript( + m_builder, + 0 // no base -> causes errors + ); + } + + flatbuffers::Offset serializeTestInterfaceWithError() + { + return rlogic_serialization::CreateLuaInterface( + m_builder, + rlogic_serialization::CreateLogicObject(m_builder, + m_builder.CreateString(""), // empty name -> causes errors + 1u) + ); + } + + flatbuffers::Offset serializeTestModule(bool withError = false) + { + return rlogic_serialization::CreateLuaModule(m_builder, + rlogic_serialization::CreateLogicObject(m_builder, + withError ? 0 : m_builder.CreateString("moduleName"), + 1u), + m_builder.CreateString("{}"), + m_builder.CreateVector(std::vector>{}), + m_builder.CreateVector(std::vector{}) + ); + } + + flatbuffers::Offset serializeTestRenderPassBindingWithError() + { + return rlogic_serialization::CreateRamsesRenderPassBinding( + m_builder, + 0 // no base -> causes errors + ); + } + + flatbuffers::Offset serializeTestRenderGroupBindingWithError() + { + return rlogic_serialization::CreateRamsesRenderGroupBinding( + m_builder, + 0 // no base -> causes errors + ); + } + + flatbuffers::FlatBufferBuilder& m_builder; + }; +} diff --git a/client/logic/unittests/shared/WithTempDirectory.h b/client/logic/unittests/shared/WithTempDirectory.h new file mode 100644 index 000000000..c8f4d9915 --- /dev/null +++ b/client/logic/unittests/shared/WithTempDirectory.h @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "internals/StdFilesystemWrapper.h" + +namespace ramses +{ + // Works like pushd/popd with temp directory + class WithTempDirectory + { + public: + WithTempDirectory() + : m_previousPath(fs::current_path()) + { + fs::create_directory("sandbox"); + fs::current_path(m_previousPath / "sandbox"); + } + + ~WithTempDirectory() + { + fs::current_path(m_previousPath); + fs::remove_all(m_previousPath / "sandbox"); + } + + WithTempDirectory(const WithTempDirectory&) = delete; + WithTempDirectory& operator=(const WithTempDirectory&) = delete; + + private: + fs::path m_previousPath; + }; + +} diff --git a/demo/ramses-dcsm-scene-references-demo/CMakeLists.txt b/client/logic/unittests/testAssetProducer/CMakeLists.txt similarity index 69% rename from demo/ramses-dcsm-scene-references-demo/CMakeLists.txt rename to client/logic/unittests/testAssetProducer/CMakeLists.txt index 19d514d3d..b2c638e37 100644 --- a/demo/ramses-dcsm-scene-references-demo/CMakeLists.txt +++ b/client/logic/unittests/testAssetProducer/CMakeLists.txt @@ -1,15 +1,16 @@ # ------------------------------------------------------------------------- -# Copyright (C) 2020 BMW AG +# Copyright (C) 2021 BMW AG # ------------------------------------------------------------------------- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # ------------------------------------------------------------------------- -RENDERER_MODULE_PER_CONFIG_DYNAMIC(ramses-dcsm-scene-references-demo +createModule( + NAME testAssetProducer TYPE BINARY - ENABLE_INSTALL ON + ENABLE_INSTALL OFF + SRC_FILES main.cpp + DEPENDENCIES ramses-shared-lib +) - FILES_SOURCE src/* - RESOURCE_FOLDER res - ) diff --git a/client/logic/unittests/testAssetProducer/main.cpp b/client/logic/unittests/testAssetProducer/main.cpp new file mode 100644 index 000000000..d80d5ffb3 --- /dev/null +++ b/client/logic/unittests/testAssetProducer/main.cpp @@ -0,0 +1,355 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2021 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-logic/LogicEngine.h" +#include "ramses-logic/Property.h" +#include "ramses-logic/LuaModule.h" +#include "ramses-logic/LuaScript.h" +#include "ramses-logic/LuaInterface.h" +#include "ramses-logic/RamsesNodeBinding.h" +#include "ramses-logic/RamsesCameraBinding.h" +#include "ramses-logic/RamsesAppearanceBinding.h" +#include "ramses-logic/RamsesRenderGroupBinding.h" +#include "ramses-logic/RamsesRenderGroupBindingElements.h" +#include "ramses-logic/DataArray.h" +#include "ramses-logic/AnimationNode.h" +#include "ramses-logic/AnimationNodeConfig.h" +#include "ramses-logic/EStandardModule.h" + +#include "ramses-client.h" +#include "ramses-utils.h" + +#include + +ramses::Appearance* createTestAppearance(ramses::Scene& scene) +{ + const std::string_view vertShader = R"( + #version 100 + + uniform highp float floatUniform; + uniform highp float animatedFloatUniform; + uniform highp mat4 jointMat[1]; + attribute vec3 a_position; + + void main() + { + gl_Position = floatUniform * animatedFloatUniform * vec4(a_position, 1.0) * jointMat[0]; + })"; + + const std::string_view fragShader = R"( + #version 100 + + void main(void) + { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + })"; + + ramses::EffectDescription effectDesc; + effectDesc.setUniformSemantic("u_DisplayBufferResolution", ramses::EEffectUniformSemantic::DisplayBufferResolution); + effectDesc.setVertexShader(vertShader.data()); + effectDesc.setFragmentShader(fragShader.data()); + + return scene.createAppearance(*scene.createEffect(effectDesc), "test appearance"); +} + +void createTriangle(ramses::Scene& scene) +{ + const std::string_view vertShader = R"( + #version 100 + + uniform highp mat4 mvpMatrix; + attribute vec3 a_position; + + void main() + { + gl_Position = mvpMatrix * vec4(a_position, 1.0); + })"; + + const std::string_view fragShader = R"( + #version 100 + + void main(void) + { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + })"; + + ramses::EffectDescription effectDesc; + effectDesc.setUniformSemantic("mvpMatrix", ramses::EEffectUniformSemantic::ModelViewProjectionMatrix); + effectDesc.setVertexShader(vertShader.data()); + effectDesc.setFragmentShader(fragShader.data()); + auto effect = scene.createEffect(effectDesc); + auto appearance = scene.createAppearance(*effect, "triangle appearance"); + + ramses::GeometryBinding* geometry = scene.createGeometryBinding(*effect, "triangle geometry"); + + const std::array vertexPositionsData{ ramses::vec3f{-1.f, 0.f, -1.f}, ramses::vec3f{1.f, 0.f, -1.f}, ramses::vec3f{0.f, 1.f, -1.f} }; + ramses::ArrayResource* vertexPositions = scene.createArrayResource(3u, vertexPositionsData.data()); + ramses::AttributeInput positionsInput; + effect->findAttributeInput("a_position", positionsInput); + geometry->setInputBuffer(positionsInput, *vertexPositions); + + ramses::MeshNode* meshNode = scene.createMeshNode("triangle mesh node"); + meshNode->setAppearance(*appearance); + meshNode->setIndexCount(3); + meshNode->setGeometryBinding(*geometry); + + auto* camera = scene.createPerspectiveCamera("triangle camera"); + camera->setViewport(0, 0, 1280u, 480u); + camera->setFrustum(19.f, 1280.f / 480.f, 0.1f, 1500.f); + camera->setTranslation({0.0f, 0.0f, 5.0f}); + ramses::RenderPass* renderPass = scene.createRenderPass("triangle render pass"); + renderPass->setClearFlags(ramses::EClearFlags_None); + renderPass->setCamera(*camera); + ramses::RenderGroup* renderGroup = scene.createRenderGroup(); + renderGroup->addMeshNode(*meshNode); + renderPass->addRenderGroup(*renderGroup); +} + +void createTriangleLogic(ramses::LogicEngine& logic, ramses::Scene& scene) +{ + auto camera = ramses::RamsesUtils::TryConvert(*scene.findObjectByName("triangle camera")); + auto camNodeBinding = logic.createRamsesNodeBinding(*camera, ramses::ERotationType::Euler_XYZ, "triangleCamNodeBinding"); + auto camBinding = logic.createRamsesCameraBinding(*camera, "triangleCamBinding"); + + auto intf = logic.createLuaInterface(R"( + function interface(inout) + inout.CraneGimbal = { Yaw = Type:Float(), Pitch = Type:Float() } + inout.Viewport = { Width = Type:Int32(), Height = Type:Int32() } + end + )", "Interface_CameraCrane"); + + auto script = logic.createLuaScript(R"( + function interface(IN,OUT) + IN.yaw = Type:Float() + IN.pitch = Type:Float() + IN.width = Type:Int32() + IN.height = Type:Int32() + OUT.translation = Type:Vec3f() + end + function run(IN,OUT) + OUT.translation = { IN.yaw/20, IN.pitch/50, 5 } + end + )", {}, "CameraCrane"); + + logic.link(*intf->getOutputs()->getChild("CraneGimbal")->getChild("Yaw"), *script->getInputs()->getChild("yaw")); + logic.link(*intf->getOutputs()->getChild("CraneGimbal")->getChild("Pitch"), *script->getInputs()->getChild("pitch")); + logic.link(*intf->getOutputs()->getChild("Viewport")->getChild("Width"), *camBinding->getInputs()->getChild("viewport")->getChild("width")); + logic.link(*intf->getOutputs()->getChild("Viewport")->getChild("Height"), *camBinding->getInputs()->getChild("viewport")->getChild("height")); + + logic.link(*script->getOutputs()->getChild("translation"), *camNodeBinding->getInputs()->getChild("translation")); + + intf->getInputs()->getChild("CraneGimbal")->getChild("Yaw")->set(0.f); + intf->getInputs()->getChild("CraneGimbal")->getChild("Pitch")->set(0.f); + intf->getInputs()->getChild("Viewport")->getChild("Width")->set(1280); + intf->getInputs()->getChild("Viewport")->getChild("Height")->set(480); +} + +int main(int argc, char* argv[]) +{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) bounds are checked + const std::vector args(argv, argv + argc); + + constexpr ramses::EFeatureLevel featureLevel = ramses::EFeatureLevel_Latest; + + std::string basePath {"."}; + std::string ramsesFilename = std::string("testScene_0") + std::to_string(featureLevel) + ".ramses"; + std::string logicFilename = std::string("testLogic_0") + std::to_string(featureLevel) + ".rlogic"; + + if (args.size() == 2u) + { + basePath = args[1]; + } + else if (args.size() == 4u) + { + basePath = args[1]; + ramsesFilename = args[2]; + logicFilename = args[3]; + } + + if (args.size() == 3 || args.size() > 4u) + { + std::cerr + << "Generator of ramses and ramses logic test content.\n\n" + << "Synopsis:\n" + << " testAssetProducer\n" + << " testAssetProducer \n" + << " testAssetProducer \n\n"; + return 1; + } + + ramses::RamsesFrameworkConfig frameworkConfig{ramses::EFeatureLevel_Latest}; + ramses::RamsesFramework ramsesFramework{frameworkConfig}; + ramses::RamsesClient* ramsesClient = ramsesFramework.createClient(""); + + ramses::Scene* scene = ramsesClient->createScene(ramses::sceneId_t(123u), ramses::SceneConfig(), ""); + scene->flush(); + ramses::LogicEngine logicEngine{ featureLevel }; + + ramses::LuaScript* script1 = logicEngine.createLuaScript(R"( + function interface(IN,OUT) + IN.intInput = Type:Int32() + IN.int64Input = Type:Int64() + IN.vec2iInput = Type:Vec2i() + IN.vec3iInput = Type:Vec3i() + IN.vec4iInput = Type:Vec4i() + IN.floatInput = Type:Float() + IN.vec2fInput = Type:Vec2f() + IN.vec3fInput = Type:Vec3f() + IN.vec4fInput = Type:Vec4f() + IN.boolInput = Type:Bool() + IN.stringInput = Type:String() + IN.structInput = { + nested = { + data1 = Type:String(), + data2 = Type:Int32() + } + } + IN.arrayInput = Type:Array(9, Type:Float()) + OUT.floatOutput = Type:Float() + OUT.nodeTranslation = Type:Vec3f() + OUT.boolOutput = Type:Bool() + end + function run(IN,OUT) + OUT.floatOutput = IN.floatInput + OUT.nodeTranslation = {IN.floatInput, 2, 3} + OUT.boolOutput = false + end + )", {}, "script1"); + + const auto luaNestedModuleMath = logicEngine.createLuaModule(R"( + local mymath = {} + function mymath.sub(a,b) + return a - b + end + return mymath + )", {}, "nestedModuleMath"); + + ramses::LuaConfig config; + config.addDependency("nestedMath", *luaNestedModuleMath); + + const auto luaModuleMath = logicEngine.createLuaModule(R"( + modules('nestedMath') + local mymath = {} + mymath.sub=nestedMath.sub + function mymath.add(a,b) + return a + b + end + return mymath + )", config, "moduleMath"); + + const auto luaModuleTypes = logicEngine.createLuaModule(R"( + local mytypes = {} + function mytypes.camViewport() + return { + offsetX = Type:Int32(), + offsetY = Type:Int32(), + width = Type:Int32(), + height = Type:Int32() + } + end + return mytypes + )", {}, "moduleTypes"); + + config = {}; + config.addDependency("modulemath", *luaModuleMath); + config.addDependency("moduletypes", *luaModuleTypes); + config.addStandardModuleDependency(ramses::EStandardModule::Math); + + ramses::LuaScript* script2 = logicEngine.createLuaScript(R"( + modules("modulemath", "moduletypes") + function interface(IN,OUT) + IN.floatInput = Type:Float() + OUT.cameraViewport = moduletypes.camViewport() + OUT.floatUniform = Type:Float() + OUT.nestedModulesResult = Type:Int32() + end + function run(IN,OUT) + OUT.floatUniform = IN.floatInput + 5.0 + local roundedFloat = math.ceil(IN.floatInput) + OUT.cameraViewport = { + offsetX = modulemath.add(2, roundedFloat), + offsetY = modulemath.add(4, roundedFloat), + width = modulemath.add(100, roundedFloat), + height = modulemath.add(200, roundedFloat) + } + OUT.nestedModulesResult = modulemath.sub(1000, roundedFloat) + end + )", config, "script2"); + + const auto intf = logicEngine.createLuaInterface(R"( + function interface(inout) + inout.struct = { floatInput = Type:Float() } + end)", "intf"); + + ramses::Node* node = { scene->createNode("test node") }; + ramses::OrthographicCamera* cameraOrtho = { scene->createOrthographicCamera("test camera") }; + cameraOrtho->setFrustum(-1.f, 1.f, -1.f, 1.f, 0.1f, 10.f); + ramses::Appearance* appearance = { createTestAppearance(*scene) }; + ramses::RenderPass* renderPass = scene->createRenderPass(); + ramses::PerspectiveCamera* cameraPersp = { scene->createPerspectiveCamera("test persp camera") }; + auto renderGroup = scene->createRenderGroup(); + const auto nestedRenderGroup = scene->createRenderGroup(); + const auto meshNode = scene->createMeshNode(); + renderGroup->addMeshNode(*meshNode); + renderGroup->addRenderGroup(*nestedRenderGroup); + + // create triangle that can actually be visible when rendered + createTriangle(*scene); + createTriangleLogic(logicEngine, *scene); + + ramses::RamsesNodeBinding* nodeBinding = logicEngine.createRamsesNodeBinding(*node, ramses::ERotationType::Euler_XYZ, "nodebinding"); + ramses::RamsesCameraBinding* camBindingOrtho = logicEngine.createRamsesCameraBinding(*cameraOrtho, "camerabinding"); + ramses::RamsesAppearanceBinding* appBinding = logicEngine.createRamsesAppearanceBinding(*appearance, "appearancebinding"); + logicEngine.createRamsesCameraBinding(*cameraPersp, "camerabindingPersp"); + logicEngine.createRamsesRenderPassBinding(*renderPass, "renderpassbinding"); + logicEngine.createAnchorPoint(*nodeBinding, *camBindingOrtho, "anchorpoint"); + logicEngine.createRamsesCameraBindingWithFrustumPlanes(*cameraPersp, "camerabindingPerspWithFrustumPlanes"); + + ramses::RamsesRenderGroupBindingElements elements; + elements.addElement(*meshNode, "mesh"); + elements.addElement(*nestedRenderGroup, "nestedRenderGroup"); + logicEngine.createRamsesRenderGroupBinding(*renderGroup, elements, "rendergroupbinding"); + + ramses::UniformInput uniform; + appearance->getEffect().findUniformInput("jointMat", uniform); + logicEngine.createSkinBinding({ nodeBinding }, { ramses::matrix44f{ 0.f } }, * appBinding, uniform, "skin"); + logicEngine.createDataArray(std::vector>{ { 1.f, 2.f, 3.f, 4.f, 5.f }, { 6.f, 7.f, 8.f, 9.f, 10.f } }, "dataarrayOfArrays"); + + logicEngine.createRamsesMeshNodeBinding(*meshNode, "meshnodebinding"); + + const auto dataArray = logicEngine.createDataArray(std::vector{ 1.f, 2.f }, "dataarray"); + ramses::AnimationNodeConfig animConfig; + animConfig.addChannel({ "channel", dataArray, dataArray, ramses::EInterpolationType::Linear }); + const auto animNode = logicEngine.createAnimationNode(animConfig, "animNode"); + animConfig.setExposingOfChannelDataAsProperties(true); + logicEngine.createAnimationNode(animConfig, "animNodeWithDataProperties"); + logicEngine.createTimerNode("timerNode"); + + logicEngine.link(*intf->getOutputs()->getChild("struct")->getChild("floatInput"), *script1->getInputs()->getChild("floatInput")); + logicEngine.link(*script1->getOutputs()->getChild("floatOutput"), *script2->getInputs()->getChild("floatInput")); + logicEngine.link(*script1->getOutputs()->getChild("nodeTranslation"), *nodeBinding->getInputs()->getChild("translation")); + logicEngine.link(*script2->getOutputs()->getChild("cameraViewport")->getChild("offsetX"), *camBindingOrtho->getInputs()->getChild("viewport")->getChild("offsetX")); + logicEngine.link(*script2->getOutputs()->getChild("cameraViewport")->getChild("offsetY"), *camBindingOrtho->getInputs()->getChild("viewport")->getChild("offsetY")); + logicEngine.link(*script2->getOutputs()->getChild("cameraViewport")->getChild("width"), *camBindingOrtho->getInputs()->getChild("viewport")->getChild("width")); + logicEngine.link(*script2->getOutputs()->getChild("cameraViewport")->getChild("height"), *camBindingOrtho->getInputs()->getChild("viewport")->getChild("height")); + logicEngine.link(*script2->getOutputs()->getChild("floatUniform"), *appBinding->getInputs()->getChild("floatUniform")); + logicEngine.link(*animNode->getOutputs()->getChild("channel"), *appBinding->getInputs()->getChild("animatedFloatUniform")); + + logicEngine.link(*script1->getOutputs()->getChild("boolOutput"), *nodeBinding->getInputs()->getChild("enabled")); + + if (!logicEngine.update()) + return 1; + + ramses::SaveFileConfig noValidationConfig; + noValidationConfig.setValidationEnabled(false); + logicEngine.saveToFile(basePath + "/" + logicFilename, noValidationConfig); + + [[maybe_unused]] auto status = scene->saveToFile(basePath + "/" + ramsesFilename, false); + + return 0; +} diff --git a/client/ramses-client-api/Appearance.cpp b/client/ramses-client-api/Appearance.cpp new file mode 100644 index 000000000..dfee20513 --- /dev/null +++ b/client/ramses-client-api/Appearance.cpp @@ -0,0 +1,325 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2014 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +// API +#include "ramses-client-api/Appearance.h" +#include "ramses-client-api/TextureSampler.h" +#include "ramses-client-api/TextureSamplerMS.h" +#include "ramses-client-api/TextureSamplerExternal.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-client-api/DataObject.h" + +// internal +#include "AppearanceImpl.h" + +namespace ramses +{ + Appearance::Appearance(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } + { + } + + status_t Appearance::setBlendingFactors(EBlendFactor srcColor, EBlendFactor destColor, EBlendFactor srcAlpha, EBlendFactor destAlpha) + { + const status_t status = m_impl.setBlendingFactors(srcColor, destColor, srcAlpha, destAlpha); + LOG_HL_CLIENT_API4(status, srcColor, destColor, srcAlpha, destAlpha); + return status; + } + + status_t Appearance::getBlendingFactors(EBlendFactor& srcColor, EBlendFactor& destColor, EBlendFactor& srcAlpha, EBlendFactor& destAlpha) const + { + return m_impl.getBlendingFactors(srcColor, destColor, srcAlpha, destAlpha); + } + + status_t Appearance::setBlendingOperations(EBlendOperation operationColor, EBlendOperation operationAlpha) + { + const status_t status = m_impl.setBlendingOperations(operationColor, operationAlpha); + LOG_HL_CLIENT_API2(status, operationColor, operationAlpha); + return status; + } + + status_t Appearance::getBlendingOperations(EBlendOperation& operationColor, EBlendOperation& operationAlpha) const + { + return m_impl.getBlendingOperations(operationColor, operationAlpha); + } + + ramses::status_t Appearance::setBlendingColor(const vec4f& color) + { + const status_t status = m_impl.setBlendingColor(color); + LOG_HL_CLIENT_API4(status, color[0], color[1], color[2], color[3]); + return status; + } + + ramses::status_t Appearance::getBlendingColor(vec4f& color) const + { + return m_impl.getBlendingColor(color); + } + + status_t Appearance::setDepthWrite(EDepthWrite mode) + { + const status_t status = m_impl.setDepthWrite(mode); + LOG_HL_CLIENT_API1(status, mode); + return status; + } + + status_t Appearance::getDepthWriteMode(EDepthWrite& mode) const + { + return m_impl.getDepthWriteMode(mode); + } + + status_t Appearance::setDepthFunction(EDepthFunc func) + { + const status_t status = m_impl.setDepthFunction(func); + LOG_HL_CLIENT_API1(status, func); + return status; + } + + status_t Appearance::getDepthFunction(EDepthFunc& func) const + { + return m_impl.getDepthFunction(func); + } + + status_t Appearance::setScissorTest(EScissorTest state, int16_t x, int16_t y, uint16_t width, uint16_t height) + { + const status_t status = m_impl.setScissorTest(state, x, y, width, height); + LOG_HL_CLIENT_API5(status, state, x, y, width, height); + return status; + } + + status_t Appearance::getScissorTestState(EScissorTest& state) const + { + return m_impl.getScissorTestState(state); + } + + status_t Appearance::getScissorRegion(int16_t& x, int16_t& y, uint16_t& width, uint16_t& height) const + { + return m_impl.getScissorRegion(x, y, width, height); + } + + status_t Appearance::setStencilFunction(EStencilFunc func, uint8_t ref, uint8_t mask) + { + const status_t status = m_impl.setStencilFunc(func, ref, mask); + LOG_HL_CLIENT_API3(status, func, ref, mask); + return status; + } + + status_t Appearance::getStencilFunction(EStencilFunc& func, uint8_t& ref, uint8_t& mask) const + { + return m_impl.getStencilFunc(func, ref, mask); + } + + status_t Appearance::setStencilOperation(EStencilOperation sfail, EStencilOperation dpfail, EStencilOperation dppass) + { + const status_t status = m_impl.setStencilOperation(sfail, dpfail, dppass); + LOG_HL_CLIENT_API3(status, sfail, dpfail, dppass); + return status; + } + + status_t Appearance::getStencilOperation(EStencilOperation& sfail, EStencilOperation& dpfail, EStencilOperation& dppass) const + { + return m_impl.getStencilOperation(sfail, dpfail, dppass); + } + + status_t Appearance::setCullingMode(ECullMode mode) + { + const status_t status = m_impl.setCullingMode(mode); + LOG_HL_CLIENT_API1(status, mode); + return status; + } + + status_t Appearance::getCullingMode(ECullMode& mode) const + { + return m_impl.getCullingMode(mode); + } + + status_t Appearance::setDrawMode(EDrawMode mode) + { + const status_t status = m_impl.setDrawMode(mode); + LOG_HL_CLIENT_API1(status, mode); + return status; + } + + status_t Appearance::getDrawMode(EDrawMode& mode) const + { + return m_impl.getDrawMode(mode); + } + + status_t Appearance::setColorWriteMask(bool writeRed, bool writeGreen, bool writeBlue, bool writeAlpha) + { + const status_t status = m_impl.setColorWriteMask(writeRed, writeGreen, writeBlue, writeAlpha); + LOG_HL_CLIENT_API4(status, writeRed, writeGreen, writeBlue, writeAlpha); + return status; + } + + status_t Appearance::getColorWriteMask(bool& writeRed, bool& writeGreen, bool& writeBlue, bool& writeAlpha) const + { + return m_impl.getColorWriteMask(writeRed, writeGreen, writeBlue, writeAlpha); + } + + status_t Appearance::setInputTexture(const UniformInput& input, const TextureSampler& textureSampler) + { + const status_t status = m_impl.setInputTexture(input.m_impl, textureSampler.m_impl); + LOG_HL_CLIENT_API2(status, LOG_API_GENERIC_OBJECT_STRING(input), LOG_API_RAMSESOBJECT_STRING(textureSampler)); + return status; + } + + status_t Appearance::getInputTexture(const UniformInput& input, const TextureSampler*& textureSampler) const + { + return m_impl.getInputTexture(input.m_impl, textureSampler); + } + + status_t Appearance::setInputTexture(const UniformInput& input, const TextureSamplerMS& textureSampler) + { + const status_t status = m_impl.setInputTexture(input.m_impl, textureSampler.m_impl); + LOG_HL_CLIENT_API2(status, LOG_API_GENERIC_OBJECT_STRING(input), LOG_API_RAMSESOBJECT_STRING(textureSampler)); + return status; + } + + status_t Appearance::getInputTextureMS(const UniformInput& input, const TextureSamplerMS*& textureSampler) const + { + return m_impl.getInputTextureMS(input.m_impl, textureSampler); + } + + status_t Appearance::setInputTexture(const UniformInput& input, const TextureSamplerExternal& textureSampler) + { + const status_t status = m_impl.setInputTexture(input.m_impl, textureSampler.m_impl); + LOG_HL_CLIENT_API2(status, LOG_API_GENERIC_OBJECT_STRING(input), LOG_API_RAMSESOBJECT_STRING(textureSampler)); + return status; + } + + status_t Appearance::getInputTextureExternal(const UniformInput& input, const TextureSamplerExternal*& textureSampler) const + { + return m_impl.getInputTextureExternal(input.m_impl, textureSampler); + } + + status_t Appearance::bindInput(const UniformInput& input, const DataObject& dataObject) + { + const status_t status = m_impl.bindInput(input.m_impl, dataObject.m_impl); + LOG_HL_CLIENT_API2(status, LOG_API_GENERIC_OBJECT_STRING(input), LOG_API_RAMSESOBJECT_STRING(dataObject)); + return status; + } + + status_t Appearance::unbindInput(const UniformInput& input) + { + const status_t status = m_impl.unbindInput(input.m_impl); + LOG_HL_CLIENT_API1(status, LOG_API_GENERIC_OBJECT_STRING(input)); + return status; + } + + bool Appearance::isInputBound(const UniformInput& input) const + { + return m_impl.isInputBound(input.m_impl); + } + + const DataObject* Appearance::getDataObjectBoundToInput(const UniformInput& input) const + { + return m_impl.getBoundDataObject(input.m_impl); + } + + const Effect& Appearance::getEffect() const + { + return m_impl.getEffect(); + } + + template status_t Appearance::setInputValueInternal(const UniformInput& input, T&& value) + { + // API uses ref/move forwarding for possibility to move but current implementation does not make use of it + return setInputValueInternal(input, 1u, &value); + } + + template status_t Appearance::setInputValueInternal(const UniformInput& input, size_t elementCount, const T* values) + { + return m_impl.setInputValue(input.m_impl, elementCount, values); + } + + template status_t Appearance::getInputValueInternal(const UniformInput& input, T& value) const + { + return getInputValueInternal(input, 1u, &value); + } + + template status_t Appearance::getInputValueInternal(const UniformInput& input, size_t elementCount, T* valuesOut) const + { + return m_impl.getInputValue(input.m_impl, elementCount, valuesOut); + } + + // const l-value instances + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, const int32_t&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, const float&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, const vec2i&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, const vec3i&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, const vec4i&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, const vec2f&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, const vec3f&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, const vec4f&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, const matrix22f&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, const matrix33f&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, const matrix44f&); + + // l-value instances + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, int32_t&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, float&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec2i&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec3i&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec4i&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec2f&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec3f&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec4f&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, matrix22f&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, matrix33f&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, matrix44f&); + + // r-value instances + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, int32_t&&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, float&&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec2i&&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec3i&&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec4i&&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec2f&&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec3f&&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, vec4f&&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, matrix22f&&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, matrix33f&&); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, matrix44f&&); + + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, size_t, const int32_t*); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, size_t, const float*); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, size_t, const vec2i*); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, size_t, const vec3i*); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, size_t, const vec4i*); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, size_t, const vec2f*); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, size_t, const vec3f*); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, size_t, const vec4f*); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, size_t, const matrix22f*); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, size_t, const matrix33f*); + template RAMSES_API status_t Appearance::setInputValueInternal(const UniformInput&, size_t, const matrix44f*); + + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, int32_t&) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, float&) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, vec2i&) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, vec3i&) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, vec4i&) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, vec2f&) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, vec3f&) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, vec4f&) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, matrix22f&) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, matrix33f&) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, matrix44f&) const; + + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, size_t, int32_t*) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, size_t, float*) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, size_t, vec2i*) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, size_t, vec3i*) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, size_t, vec4i*) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, size_t, vec2f*) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, size_t, vec3f*) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, size_t, vec4f*) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, size_t, matrix22f*) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, size_t, matrix33f*) const; + template RAMSES_API status_t Appearance::getInputValueInternal(const UniformInput&, size_t, matrix44f*) const; +} diff --git a/client/ramses-client-api/ArrayBuffer.cpp b/client/ramses-client-api/ArrayBuffer.cpp new file mode 100644 index 000000000..3e1c62b4b --- /dev/null +++ b/client/ramses-client-api/ArrayBuffer.cpp @@ -0,0 +1,73 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2017 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +// API +#include "ramses-client-api/ArrayBuffer.h" + +// Internal +#include "ArrayBufferImpl.h" + +namespace ramses +{ + ArrayBuffer::ArrayBuffer(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } + { + } + + template status_t ArrayBuffer::updateDataInternal(uint32_t firstElement, uint32_t numElements, const T* bufferData) + { + if (GetEDataType() != m_impl.getDataType()) + return m_impl.addErrorEntry("ArrayBuffer::updateData: Wrong data type used to update buffer!"); + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) we store all data types as bytes internally + const status_t status = m_impl.updateData(firstElement, numElements, reinterpret_cast(bufferData)); + LOG_HL_CLIENT_API3(status, firstElement, numElements, LOG_API_GENERIC_PTR_STRING(bufferData)); + return status; + } + + uint32_t ArrayBuffer::getMaximumNumberOfElements() const + { + return m_impl.getMaximumNumberOfElements(); + } + + uint32_t ArrayBuffer::getUsedNumberOfElements() const + { + return m_impl.getUsedNumberOfElements(); + } + + EDataType ArrayBuffer::getDataType() const + { + return m_impl.getDataType(); + } + + template status_t ArrayBuffer::getDataInternal(T* buffer, uint32_t numElements) const + { + if (GetEDataType() != m_impl.getDataType()) + return m_impl.addErrorEntry("ArrayBuffer::getData: Wrong data type used to get data!"); + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) we store all data types as bytes internally + return m_impl.getData(reinterpret_cast(buffer), numElements); + } + + template RAMSES_API status_t ArrayBuffer::updateDataInternal(uint32_t, uint32_t, const uint16_t*); + template RAMSES_API status_t ArrayBuffer::updateDataInternal(uint32_t, uint32_t, const uint32_t*); + template RAMSES_API status_t ArrayBuffer::updateDataInternal(uint32_t, uint32_t, const float*); + template RAMSES_API status_t ArrayBuffer::updateDataInternal(uint32_t, uint32_t, const vec2f*); + template RAMSES_API status_t ArrayBuffer::updateDataInternal(uint32_t, uint32_t, const vec3f*); + template RAMSES_API status_t ArrayBuffer::updateDataInternal(uint32_t, uint32_t, const vec4f*); + template RAMSES_API status_t ArrayBuffer::updateDataInternal(uint32_t, uint32_t, const Byte*); + + template RAMSES_API status_t ArrayBuffer::getDataInternal(uint16_t*, uint32_t) const; + template RAMSES_API status_t ArrayBuffer::getDataInternal(uint32_t*, uint32_t) const; + template RAMSES_API status_t ArrayBuffer::getDataInternal(float*, uint32_t) const; + template RAMSES_API status_t ArrayBuffer::getDataInternal(vec2f*, uint32_t) const; + template RAMSES_API status_t ArrayBuffer::getDataInternal(vec3f*, uint32_t) const; + template RAMSES_API status_t ArrayBuffer::getDataInternal(vec4f*, uint32_t) const; + template RAMSES_API status_t ArrayBuffer::getDataInternal(Byte*, uint32_t) const; +} diff --git a/client/ramses-client/ramses-client-api/ArrayResource.cpp b/client/ramses-client-api/ArrayResource.cpp similarity index 73% rename from client/ramses-client/ramses-client-api/ArrayResource.cpp rename to client/ramses-client-api/ArrayResource.cpp index 6b2f58979..ca6259cab 100644 --- a/client/ramses-client/ramses-client-api/ArrayResource.cpp +++ b/client/ramses-client-api/ArrayResource.cpp @@ -12,24 +12,20 @@ namespace ramses { - ArrayResource::ArrayResource(ArrayResourceImpl& pimpl) - : Resource(pimpl) - , impl(pimpl) - { - } - - ArrayResource::~ArrayResource() + ArrayResource::ArrayResource(std::unique_ptr impl) + : Resource{ std::move(impl) } + , m_impl{ static_cast(Resource::m_impl) } { } uint32_t ArrayResource::getNumberOfElements() const { - return impl.getElementCount(); + return m_impl.getElementCount(); } EDataType ArrayResource::getDataType() const { - return impl.getElementType(); + return m_impl.getElementType(); } } diff --git a/client/ramses-client-api/AttributeInput.cpp b/client/ramses-client-api/AttributeInput.cpp new file mode 100644 index 000000000..dc55e2f21 --- /dev/null +++ b/client/ramses-client-api/AttributeInput.cpp @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2014 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-client-api/AttributeInput.h" +#include "EffectInputImpl.h" + +namespace ramses +{ + AttributeInput::AttributeInput() + : EffectInput{ std::make_unique() } + { + } + + AttributeInput::~AttributeInput() = default; + + AttributeInput::AttributeInput(const AttributeInput& other) + : EffectInput{ std::make_unique(other.m_impl) } + { + } + + AttributeInput::AttributeInput(AttributeInput&& other) noexcept + : EffectInput{ std::unique_ptr(static_cast(other.StatusObject::m_impl.release())) } + { + } + + AttributeInput& AttributeInput::operator=(const AttributeInput& other) + { + StatusObject::m_impl = std::make_unique(other.m_impl); + m_impl = static_cast(*StatusObject::m_impl); + return *this; + } + + AttributeInput& AttributeInput::operator=(AttributeInput&& other) noexcept + { + StatusObject::m_impl = std::move(other.StatusObject::m_impl); + m_impl = static_cast(*StatusObject::m_impl); + return *this; + } + + EEffectAttributeSemantic AttributeInput::getSemantics() const + { + return m_impl.get().getAttributeSemantics(); + } +} diff --git a/client/ramses-client/ramses-client-api/BlitPass.cpp b/client/ramses-client-api/BlitPass.cpp similarity index 70% rename from client/ramses-client/ramses-client-api/BlitPass.cpp rename to client/ramses-client-api/BlitPass.cpp index 90cb8e08e..df9be3c28 100644 --- a/client/ramses-client/ramses-client-api/BlitPass.cpp +++ b/client/ramses-client-api/BlitPass.cpp @@ -14,59 +14,55 @@ namespace ramses { - BlitPass::BlitPass(BlitPassImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - BlitPass::~BlitPass() + BlitPass::BlitPass(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } { } status_t BlitPass::setBlittingRegion(uint32_t sourceX, uint32_t sourceY, uint32_t destinationX, uint32_t destinationY, uint32_t width, uint32_t height) { - const status_t status = impl.setBlittingRegion(sourceX, sourceY, destinationX, destinationY, width, height); + const status_t status = m_impl.setBlittingRegion(sourceX, sourceY, destinationX, destinationY, width, height); LOG_HL_CLIENT_API6(status, sourceX, sourceY, destinationX, destinationY, width, height); return status; } void BlitPass::getBlittingRegion(uint32_t& sourceX, uint32_t& sourceY, uint32_t& destinationX, uint32_t& destinationY, uint32_t& width, uint32_t& height) const { - impl.getBlittingRegion(sourceX, sourceY, destinationX, destinationY, width, height); + m_impl.getBlittingRegion(sourceX, sourceY, destinationX, destinationY, width, height); } const RenderBuffer& BlitPass::getSourceRenderBuffer() const { - return impl.getSourceRenderBuffer(); + return m_impl.getSourceRenderBuffer(); } const RenderBuffer& BlitPass::getDestinationRenderBuffer() const { - return impl.getDestinationRenderBuffer(); + return m_impl.getDestinationRenderBuffer(); } status_t BlitPass::setRenderOrder(int32_t renderOrder) { - const status_t status = impl.setRenderOrder(renderOrder); + const status_t status = m_impl.setRenderOrder(renderOrder); LOG_HL_CLIENT_API1(status, renderOrder); return status; } int32_t BlitPass::getRenderOrder() const { - return impl.getRenderOrder(); + return m_impl.getRenderOrder(); } status_t BlitPass::setEnabled(bool enable) { - const status_t status = impl.setEnabled(enable); + const status_t status = m_impl.setEnabled(enable); LOG_HL_CLIENT_API1(status, enable); return status; } bool BlitPass::isEnabled() const { - return impl.isEnabled(); + return m_impl.isEnabled(); } } diff --git a/client/ramses-client/ramses-client-api/Camera.cpp b/client/ramses-client-api/Camera.cpp similarity index 59% rename from client/ramses-client/ramses-client-api/Camera.cpp rename to client/ramses-client-api/Camera.cpp index 05507bebf..53a423594 100644 --- a/client/ramses-client/ramses-client-api/Camera.cpp +++ b/client/ramses-client-api/Camera.cpp @@ -8,146 +8,142 @@ // API #include "ramses-client-api/Camera.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector4f.h" +#include "ramses-client-api/DataObject.h" // internal #include "CameraNodeImpl.h" namespace ramses { - Camera::Camera(CameraNodeImpl& pimpl) - : Node(pimpl) - , impl(pimpl) + Camera::Camera(std::unique_ptr impl) + : Node{ std::move(impl) } + , m_impl{ static_cast(Node::m_impl) } { } - Camera::~Camera() = default; - status_t Camera::setFrustum(float leftPlane, float rightPlane, float bottomPlane, float topPlane, float nearPlane, float farPlane) { - const status_t status = impl.setFrustum(leftPlane, rightPlane, bottomPlane, topPlane, nearPlane, farPlane); + const status_t status = m_impl.setFrustum(leftPlane, rightPlane, bottomPlane, topPlane, nearPlane, farPlane); LOG_HL_CLIENT_API6(status, leftPlane, rightPlane, bottomPlane, topPlane, nearPlane, farPlane); return status; } status_t Camera::setViewport(int32_t x, int32_t y, uint32_t width, uint32_t height) { - const status_t status = impl.setViewport(x, y, width, height); + const status_t status = m_impl.setViewport(x, y, width, height); LOG_HL_CLIENT_API4(status, x, y, width, height); return status; } int32_t Camera::getViewportX() const { - return impl.getViewportX(); + return m_impl.getViewportX(); } int32_t Camera::getViewportY() const { - return impl.getViewportY(); + return m_impl.getViewportY(); } uint32_t Camera::getViewportWidth() const { - return impl.getViewportWidth(); + return m_impl.getViewportWidth(); } uint32_t Camera::getViewportHeight() const { - return impl.getViewportHeight(); + return m_impl.getViewportHeight(); } float Camera::getLeftPlane() const { - return impl.getLeftPlane(); + return m_impl.getLeftPlane(); } float Camera::getRightPlane() const { - return impl.getRightPlane(); + return m_impl.getRightPlane(); } float Camera::getBottomPlane() const { - return impl.getBottomPlane(); + return m_impl.getBottomPlane(); } float Camera::getTopPlane() const { - return impl.getTopPlane(); + return m_impl.getTopPlane(); } float Camera::getNearPlane() const { - return impl.getNearPlane(); + return m_impl.getNearPlane(); } float Camera::getFarPlane() const { - return impl.getFarPlane(); + return m_impl.getFarPlane(); } - status_t Camera::getProjectionMatrix(float(&projectionMatrix)[16]) const + status_t Camera::getProjectionMatrix(matrix44f& projectionMatrix) const { - return impl.getProjectionMatrix(projectionMatrix); + return m_impl.getProjectionMatrix(projectionMatrix); } - status_t Camera::bindViewportOffset(const DataVector2i& offsetData) + status_t Camera::bindViewportOffset(const DataObject& offsetData) { - const status_t status = impl.bindViewportOffset(offsetData); + const status_t status = m_impl.bindViewportOffset(offsetData); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(offsetData)); return status; } - status_t Camera::bindViewportSize(const DataVector2i& sizeData) + status_t Camera::bindViewportSize(const DataObject& sizeData) { - const status_t status = impl.bindViewportSize(sizeData); + const status_t status = m_impl.bindViewportSize(sizeData); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(sizeData)); return status; } - status_t Camera::bindFrustumPlanes(const DataVector4f& frustumPlanesData, const DataVector2f& nearFarPlanesData) + status_t Camera::bindFrustumPlanes(const DataObject& frustumPlanesData, const DataObject& nearFarPlanesData) { - const status_t status = impl.bindFrustumPlanes(frustumPlanesData, nearFarPlanesData); + const status_t status = m_impl.bindFrustumPlanes(frustumPlanesData, nearFarPlanesData); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(frustumPlanesData), LOG_API_RAMSESOBJECT_STRING(nearFarPlanesData)); return status; } status_t Camera::unbindViewportOffset() { - const status_t status = impl.unbindViewportOffset(); + const status_t status = m_impl.unbindViewportOffset(); LOG_HL_CLIENT_API_NOARG(status); return status; } status_t Camera::unbindViewportSize() { - const status_t status = impl.unbindViewportSize(); + const status_t status = m_impl.unbindViewportSize(); LOG_HL_CLIENT_API_NOARG(status); return status; } status_t Camera::unbindFrustumPlanes() { - const status_t status = impl.unbindFrustumPlanes(); + const status_t status = m_impl.unbindFrustumPlanes(); LOG_HL_CLIENT_API_NOARG(status); return status; } bool Camera::isViewportOffsetBound() const { - return impl.isViewportOffsetBound(); + return m_impl.isViewportOffsetBound(); } bool Camera::isViewportSizeBound() const { - return impl.isViewportSizeBound(); + return m_impl.isViewportSizeBound(); } bool Camera::isFrustumPlanesBound() const { - return impl.isFrustumPlanesBound(); + return m_impl.isFrustumPlanesBound(); } } diff --git a/client/ramses-client/ramses-client-api/ClientObject.cpp b/client/ramses-client-api/ClientObject.cpp similarity index 76% rename from client/ramses-client/ramses-client-api/ClientObject.cpp rename to client/ramses-client-api/ClientObject.cpp index bd37a9a73..698ec1d33 100644 --- a/client/ramses-client/ramses-client-api/ClientObject.cpp +++ b/client/ramses-client-api/ClientObject.cpp @@ -14,13 +14,9 @@ namespace ramses { - ClientObject::ClientObject(ClientObjectImpl& pimpl) - : RamsesObject(pimpl) - , impl(pimpl) - { - } - - ClientObject::~ClientObject() + ClientObject::ClientObject(std::unique_ptr impl) + : RamsesObject{ std::move(impl) } + , m_impl{ static_cast(RamsesObject::m_impl) } { } } diff --git a/client/ramses-client-api/DataObject.cpp b/client/ramses-client-api/DataObject.cpp new file mode 100644 index 000000000..202351d6b --- /dev/null +++ b/client/ramses-client-api/DataObject.cpp @@ -0,0 +1,91 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2016 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +// API +#include "ramses-client-api/DataObject.h" + +//internal +#include "DataObjectImpl.h" + +namespace ramses +{ + DataObject::DataObject(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } + { + } + + EDataType DataObject::getDataType() const + { + return m_impl.getDataType(); + } + + template + status_t DataObject::setValueInternal(T&& value) + { + return m_impl.setValue(value); + + } + + template + status_t DataObject::getValueInternal(T& value) const + { + return m_impl.getValue(value); + } + + // const l-value instances + template RAMSES_API status_t DataObject::setValueInternal(const int32_t&); + template RAMSES_API status_t DataObject::setValueInternal(const float&); + template RAMSES_API status_t DataObject::setValueInternal(const vec2i&); + template RAMSES_API status_t DataObject::setValueInternal(const vec3i&); + template RAMSES_API status_t DataObject::setValueInternal(const vec4i&); + template RAMSES_API status_t DataObject::setValueInternal(const vec2f&); + template RAMSES_API status_t DataObject::setValueInternal(const vec3f&); + template RAMSES_API status_t DataObject::setValueInternal(const vec4f&); + template RAMSES_API status_t DataObject::setValueInternal(const matrix22f&); + template RAMSES_API status_t DataObject::setValueInternal(const matrix33f&); + template RAMSES_API status_t DataObject::setValueInternal(const matrix44f&); + + // l-value instances + template RAMSES_API status_t DataObject::setValueInternal(int32_t&); + template RAMSES_API status_t DataObject::setValueInternal(float&); + template RAMSES_API status_t DataObject::setValueInternal(vec2i&); + template RAMSES_API status_t DataObject::setValueInternal(vec3i&); + template RAMSES_API status_t DataObject::setValueInternal(vec4i&); + template RAMSES_API status_t DataObject::setValueInternal(vec2f&); + template RAMSES_API status_t DataObject::setValueInternal(vec3f&); + template RAMSES_API status_t DataObject::setValueInternal(vec4f&); + template RAMSES_API status_t DataObject::setValueInternal(matrix22f&); + template RAMSES_API status_t DataObject::setValueInternal(matrix33f&); + template RAMSES_API status_t DataObject::setValueInternal(matrix44f&); + + // r-value instances + template RAMSES_API status_t DataObject::setValueInternal(int32_t&&); + template RAMSES_API status_t DataObject::setValueInternal(float&&); + template RAMSES_API status_t DataObject::setValueInternal(vec2i&&); + template RAMSES_API status_t DataObject::setValueInternal(vec3i&&); + template RAMSES_API status_t DataObject::setValueInternal(vec4i&&); + template RAMSES_API status_t DataObject::setValueInternal(vec2f&&); + template RAMSES_API status_t DataObject::setValueInternal(vec3f&&); + template RAMSES_API status_t DataObject::setValueInternal(vec4f&&); + template RAMSES_API status_t DataObject::setValueInternal(matrix22f&&); + template RAMSES_API status_t DataObject::setValueInternal(matrix33f&&); + template RAMSES_API status_t DataObject::setValueInternal(matrix44f&&); + + template RAMSES_API status_t DataObject::getValueInternal(int32_t&) const; + template RAMSES_API status_t DataObject::getValueInternal(float&) const; + template RAMSES_API status_t DataObject::getValueInternal(vec2i&) const; + template RAMSES_API status_t DataObject::getValueInternal(vec3i&) const; + template RAMSES_API status_t DataObject::getValueInternal(vec4i&) const; + template RAMSES_API status_t DataObject::getValueInternal(vec2f&) const; + template RAMSES_API status_t DataObject::getValueInternal(vec3f&) const; + template RAMSES_API status_t DataObject::getValueInternal(vec4f&) const; + template RAMSES_API status_t DataObject::getValueInternal(matrix22f&) const; + template RAMSES_API status_t DataObject::getValueInternal(matrix33f&) const; + template RAMSES_API status_t DataObject::getValueInternal(matrix44f&) const; +} diff --git a/client/ramses-client-api/Effect.cpp b/client/ramses-client-api/Effect.cpp new file mode 100644 index 000000000..5e332d21e --- /dev/null +++ b/client/ramses-client-api/Effect.cpp @@ -0,0 +1,74 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2014 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +// API +#include "ramses-client-api/Effect.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-client-api/AttributeInput.h" + +// internal +#include "EffectImpl.h" + +namespace ramses +{ + Effect::Effect(std::unique_ptr impl) + : Resource{ std::move(impl) } + , m_impl{ static_cast(Resource::m_impl) } + { + } + + size_t Effect::getUniformInputCount() const + { + return m_impl.getUniformInputCount(); + } + + size_t Effect::getAttributeInputCount() const + { + return m_impl.getAttributeInputCount(); + } + + status_t Effect::getUniformInput(size_t index, UniformInput& uniformInput) const + { + return m_impl.getUniformInput(index, uniformInput.m_impl); + } + + status_t Effect::findUniformInput(EEffectUniformSemantic uniformSemantic, UniformInput& uniformInput) const + { + return m_impl.findUniformInput(uniformSemantic, uniformInput.m_impl); + } + + status_t Effect::getAttributeInput(size_t index, AttributeInput& attributeInput) const + { + return m_impl.getAttributeInput(index, attributeInput.m_impl); + } + + status_t Effect::findAttributeInput(EEffectAttributeSemantic attributeSemantic, AttributeInput& attributeInput) const + { + return m_impl.findAttributeInput(attributeSemantic, attributeInput.m_impl); + } + + bool Effect::hasGeometryShader() const + { + return m_impl.hasGeometryShader(); + } + + status_t Effect::getGeometryShaderInputType(EDrawMode& expectedGeometryInputType) const + { + return m_impl.getGeometryShaderInputType(expectedGeometryInputType); + } + + status_t Effect::findUniformInput(std::string_view inputName, UniformInput& uniformInput) const + { + return m_impl.findUniformInput(inputName, uniformInput.m_impl); + } + + status_t Effect::findAttributeInput(std::string_view inputName, AttributeInput& attributeInput) const + { + return m_impl.findAttributeInput(inputName, attributeInput.m_impl); + } +} diff --git a/client/ramses-client-api/EffectDescription.cpp b/client/ramses-client-api/EffectDescription.cpp new file mode 100644 index 000000000..5af7574a6 --- /dev/null +++ b/client/ramses-client-api/EffectDescription.cpp @@ -0,0 +1,138 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2014 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +// API +#include "ramses-client-api/EffectDescription.h" + +// internal +#include "EffectDescriptionImpl.h" + +namespace ramses +{ + EffectDescription::EffectDescription() + : StatusObject{ std::make_unique() } + , m_impl{ static_cast(*StatusObject::m_impl) } + { + } + + EffectDescription::~EffectDescription() = default; + + EffectDescription::EffectDescription(const EffectDescription& other) + : StatusObject{ std::make_unique(other.m_impl) } + , m_impl{ static_cast(*StatusObject::m_impl) } + { + } + + EffectDescription::EffectDescription(EffectDescription&& other) noexcept + : StatusObject{ std::move(other.StatusObject::m_impl) } + , m_impl{ static_cast(*StatusObject::m_impl) } + { + } + + EffectDescription& EffectDescription::operator=(const EffectDescription& other) + { + StatusObject::m_impl = std::make_unique(other.m_impl); + m_impl = static_cast(*StatusObject::m_impl); + return *this; + } + + EffectDescription& EffectDescription::operator=(EffectDescription&& other) noexcept + { + StatusObject::m_impl = std::move(other.StatusObject::m_impl); + m_impl = static_cast(*StatusObject::m_impl); + return *this; + } + + status_t EffectDescription::setVertexShader(std::string_view shaderSource) + { + const status_t status = m_impl.get().setVertexShader(shaderSource); + LOG_HL_CLIENT_API1(status, shaderSource); + return status; + } + + status_t EffectDescription::setFragmentShader(std::string_view shaderSource) + { + const status_t status = m_impl.get().setFragmentShader(shaderSource); + LOG_HL_CLIENT_API1(status, shaderSource); + return status; + } + + status_t EffectDescription::setGeometryShader(std::string_view shaderSource) + { + const status_t status = m_impl.get().setGeometryShader(shaderSource); + LOG_HL_CLIENT_API1(status, shaderSource); + return status; + } + + status_t EffectDescription::setVertexShaderFromFile(std::string_view shaderSourceFileName) + { + const status_t status = m_impl.get().setVertexShaderFromFile(shaderSourceFileName); + LOG_HL_CLIENT_API1(status, shaderSourceFileName); + return status; + } + + status_t EffectDescription::setFragmentShaderFromFile(std::string_view shaderSourceFileName) + { + const status_t status = m_impl.get().setFragmentShaderFromFile(shaderSourceFileName); + LOG_HL_CLIENT_API1(status, shaderSourceFileName); + return status; + } + + status_t EffectDescription::setGeometryShaderFromFile(std::string_view shaderSourceFileName) + { + const status_t status = m_impl.get().setGeometryShaderFromFile(shaderSourceFileName); + LOG_HL_CLIENT_API1(status, shaderSourceFileName); + return status; + } + + status_t EffectDescription::addCompilerDefine(std::string_view define) + { + const status_t status = m_impl.get().addCompilerDefine(define); + LOG_HL_CLIENT_API1(status, define); + return status; + } + + status_t EffectDescription::setUniformSemantic(std::string_view inputName, EEffectUniformSemantic semanticType) + { + const status_t status = m_impl.get().setUniformSemantic(inputName, semanticType); + LOG_HL_CLIENT_API2(status, inputName, semanticType); + return status; + } + + status_t EffectDescription::setAttributeSemantic(std::string_view inputName, EEffectAttributeSemantic semanticType) + { + const status_t status = m_impl.get().setAttributeSemantic(inputName, semanticType); + LOG_HL_CLIENT_API2(status, inputName, semanticType); + return status; + } + + const char* EffectDescription::getVertexShader() const + { + return m_impl.get().getVertexShader(); + } + + const char* EffectDescription::getFragmentShader() const + { + return m_impl.get().getFragmentShader(); + } + + const char* EffectDescription::getGeometryShader() const + { + return m_impl.get().getGeometryShader(); + } + + size_t EffectDescription::getNumberOfCompilerDefines() const + { + return m_impl.get().getNumberOfCompilerDefines(); + } + + const char* EffectDescription::getCompilerDefine(size_t index) const + { + return m_impl.get().getCompilerDefine(index); + } +} diff --git a/client/ramses-client/ramses-client-api/EffectInput.cpp b/client/ramses-client-api/EffectInput.cpp similarity index 64% rename from client/ramses-client/ramses-client-api/EffectInput.cpp rename to client/ramses-client-api/EffectInput.cpp index 9ff0b6818..90b534eb4 100644 --- a/client/ramses-client/ramses-client-api/EffectInput.cpp +++ b/client/ramses-client-api/EffectInput.cpp @@ -11,23 +11,24 @@ namespace ramses { - EffectInput::EffectInput(EffectInputImpl& effectInputImpl) - : StatusObject(effectInputImpl) - , impl(effectInputImpl) + EffectInput::EffectInput(std::unique_ptr effectInputImpl) + : StatusObject{ std::move(effectInputImpl) } + , m_impl{ static_cast(*StatusObject::m_impl) } { } - EffectInput::~EffectInput() + const char* EffectInput::getName() const { + return m_impl.get().getName().c_str(); } - const char* EffectInput::getName() const + bool EffectInput::isValid() const { - return impl.getName().c_str(); + return m_impl.get().isValid(); } - bool EffectInput::isValid() const + std::optional EffectInput::getDataType() const { - return impl.isValid(); + return m_impl.get().getDataType(); } } diff --git a/client/ramses-client/ramses-client-api/GeometryBinding.cpp b/client/ramses-client-api/GeometryBinding.cpp similarity index 74% rename from client/ramses-client/ramses-client-api/GeometryBinding.cpp rename to client/ramses-client-api/GeometryBinding.cpp index cfe88625a..bf40a2324 100644 --- a/client/ramses-client/ramses-client-api/GeometryBinding.cpp +++ b/client/ramses-client-api/GeometryBinding.cpp @@ -17,60 +17,56 @@ namespace ramses { - GeometryBinding::GeometryBinding(GeometryBindingImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - GeometryBinding::~GeometryBinding() + GeometryBinding::GeometryBinding(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } { } status_t GeometryBinding::setIndices(const ArrayResource& indicesResource) { - const status_t status = impl.setIndices(indicesResource.impl); + const status_t status = m_impl.setIndices(indicesResource.m_impl); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(indicesResource)); return status; } status_t GeometryBinding::setIndices(const ArrayBuffer& arrayBuffer) { - const status_t status = impl.setIndices(arrayBuffer.impl); + const status_t status = m_impl.setIndices(arrayBuffer.m_impl); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(arrayBuffer)); return status; } status_t GeometryBinding::setInputBuffer(const AttributeInput& attributeInput, const ArrayResource& arrayResource, uint32_t instancingDivisor) { - const status_t status = impl.setInputBuffer(attributeInput.impl, arrayResource.impl, instancingDivisor, 0u, 0u); + const status_t status = m_impl.setInputBuffer(attributeInput.m_impl, arrayResource.m_impl, instancingDivisor, 0u, 0u); LOG_HL_CLIENT_API3(status, LOG_API_GENERIC_OBJECT_STRING(attributeInput), LOG_API_RAMSESOBJECT_STRING(arrayResource), instancingDivisor); return status; } status_t GeometryBinding::setInputBuffer(const AttributeInput& attributeInput, const ArrayResource& arrayResource, uint16_t offset, uint16_t stride) { - const status_t status = impl.setInputBuffer(attributeInput.impl, arrayResource.impl, 0u, offset, stride); + const status_t status = m_impl.setInputBuffer(attributeInput.m_impl, arrayResource.m_impl, 0u, offset, stride); LOG_HL_CLIENT_API4(status, LOG_API_GENERIC_OBJECT_STRING(attributeInput), LOG_API_RAMSESOBJECT_STRING(arrayResource), offset, stride); return status; } status_t GeometryBinding::setInputBuffer(const AttributeInput& attributeInput, const ArrayBuffer& arrayBuffer, uint32_t instancingDivisor /*= 0*/) { - const status_t status = impl.setInputBuffer(attributeInput.impl, arrayBuffer.impl, instancingDivisor, 0u, 0u); + const status_t status = m_impl.setInputBuffer(attributeInput.m_impl, arrayBuffer.m_impl, instancingDivisor, 0u, 0u); LOG_HL_CLIENT_API3(status, LOG_API_GENERIC_OBJECT_STRING(attributeInput), LOG_API_RAMSESOBJECT_STRING(arrayBuffer), instancingDivisor); return status; } status_t GeometryBinding::setInputBuffer(const AttributeInput& attributeInput, const ArrayBuffer& arrayBuffer, uint16_t offset, uint16_t stride) { - const status_t status = impl.setInputBuffer(attributeInput.impl, arrayBuffer.impl, 0u, offset, stride); + const status_t status = m_impl.setInputBuffer(attributeInput.m_impl, arrayBuffer.m_impl, 0u, offset, stride); LOG_HL_CLIENT_API4(status, LOG_API_GENERIC_OBJECT_STRING(attributeInput), LOG_API_RAMSESOBJECT_STRING(arrayBuffer), offset, stride); return status; } const Effect& GeometryBinding::getEffect() const { - return impl.getEffect(); + return m_impl.getEffect(); } } diff --git a/client/ramses-client/ramses-client-api/MeshNode.cpp b/client/ramses-client-api/MeshNode.cpp similarity index 69% rename from client/ramses-client/ramses-client-api/MeshNode.cpp rename to client/ramses-client-api/MeshNode.cpp index e0c9fd92b..6b881856d 100644 --- a/client/ramses-client/ramses-client-api/MeshNode.cpp +++ b/client/ramses-client-api/MeshNode.cpp @@ -17,100 +17,96 @@ namespace ramses { - MeshNode::MeshNode(MeshNodeImpl& pimpl) - : Node(pimpl) - , impl(pimpl) - { - } - - MeshNode::~MeshNode() + MeshNode::MeshNode(std::unique_ptr impl) + : Node{ std::move(impl) } + , m_impl{ static_cast(Node::m_impl) } { } status_t MeshNode::setAppearance(Appearance& appearance) { - const status_t status = impl.setAppearance(appearance.impl); + const status_t status = m_impl.setAppearance(appearance.m_impl); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(appearance)); return status; } status_t MeshNode::setGeometryBinding(GeometryBinding& geometry) { - const status_t status = impl.setGeometryBinding(geometry.impl); + const status_t status = m_impl.setGeometryBinding(geometry.m_impl); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(geometry)); return status; } status_t MeshNode::removeAppearanceAndGeometry() { - const status_t status = impl.removeAppearanceAndGeometry(); + const status_t status = m_impl.removeAppearanceAndGeometry(); LOG_HL_CLIENT_API_NOARG(status); return status; } status_t MeshNode::setStartIndex(uint32_t startIndex) { - const status_t status = impl.setStartIndex(startIndex); + const status_t status = m_impl.setStartIndex(startIndex); LOG_HL_CLIENT_API1(status, startIndex); return status; } status_t MeshNode::setStartVertex(uint32_t startVertex) { - const status_t status = impl.setStartVertex(startVertex); + const status_t status = m_impl.setStartVertex(startVertex); LOG_HL_CLIENT_API1(status, startVertex); return status; } status_t MeshNode::setIndexCount(uint32_t indexCount) { - const status_t status = impl.setIndexCount(indexCount); + const status_t status = m_impl.setIndexCount(indexCount); LOG_HL_CLIENT_API1(status, indexCount); return status; } const Appearance* MeshNode::getAppearance() const { - return impl.getAppearance(); + return m_impl.getAppearance(); } Appearance* MeshNode::getAppearance() { - return impl.getAppearance(); + return m_impl.getAppearance(); } const GeometryBinding* MeshNode::getGeometryBinding() const { - return impl.getGeometryBinding(); + return m_impl.getGeometryBinding(); } GeometryBinding* MeshNode::getGeometryBinding() { - return impl.getGeometryBinding(); + return m_impl.getGeometryBinding(); } uint32_t MeshNode::getStartIndex() const { - return impl.getStartIndex(); + return m_impl.getStartIndex(); } uint32_t MeshNode::getStartVertex() const { - return impl.getStartVertex(); + return m_impl.getStartVertex(); } uint32_t MeshNode::getIndexCount() const { - return impl.getIndexCount(); + return m_impl.getIndexCount(); } status_t MeshNode::setInstanceCount(uint32_t instanceCount) { - return impl.setInstanceCount(instanceCount); + return m_impl.setInstanceCount(instanceCount); } uint32_t MeshNode::getInstanceCount() const { - return impl.getInstanceCount(); + return m_impl.getInstanceCount(); } } diff --git a/client/ramses-client-api/Node.cpp b/client/ramses-client-api/Node.cpp new file mode 100644 index 000000000..7e38664a6 --- /dev/null +++ b/client/ramses-client-api/Node.cpp @@ -0,0 +1,180 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2014 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +// API +#include "ramses-client-api/Node.h" + +// internal +#include "NodeImpl.h" +#include "VisibilityModeUtils.h" + +namespace ramses +{ + Node::Node(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } + { + } + + bool Node::hasChild() const + { + return m_impl.hasChild(); + } + + size_t Node::getChildCount() const + { + return m_impl.getChildCount(); + } + + Node* Node::getChild(size_t index) + { + return m_impl.getChild(index); + } + + const Node* Node::getChild(size_t index) const + { + return m_impl.getChild(index); + } + + status_t Node::addChild(Node& node) + { + const status_t status = m_impl.addChild(node.m_impl); + LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(node)); + return status; + } + + status_t Node::removeChild(Node& node) + { + const status_t status = m_impl.removeChild(node.m_impl); + LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(node)); + return status; + } + + status_t Node::removeAllChildren() + { + const status_t status = m_impl.removeAllChildren(); + LOG_HL_CLIENT_API_NOARG(status); + return status; + } + + bool Node::hasParent() const + { + return m_impl.hasParent(); + } + + Node* Node::getParent() + { + return m_impl.getParent(); + } + + const Node* Node::getParent() const + { + return m_impl.getParent(); + } + + status_t Node::setParent(Node& node) + { + const status_t status = m_impl.setParent(node.m_impl); + LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(node)); + return status; + } + + status_t Node::removeParent() + { + return m_impl.removeParent(); + } + + status_t Node::getModelMatrix(matrix44f& modelMatrix) const + { + return m_impl.getModelMatrix(modelMatrix); + } + + status_t Node::getInverseModelMatrix(matrix44f& inverseModelMatrix) const + { + return m_impl.getInverseModelMatrix(inverseModelMatrix); + } + + status_t Node::setRotation(const vec3f& rotation, ERotationType rotationType) + { + const status_t status = m_impl.setRotation(rotation, rotationType); + LOG_HL_CLIENT_API4(status, rotation.x, rotation.y, rotation.z, rotationType); + return status; + } + + status_t Node::setRotation(const quat& rotation) + { + const status_t status = m_impl.setRotation(rotation); + LOG_HL_CLIENT_API4(status, rotation.w, rotation.x, rotation.y, rotation.z); + return status; + } + + ramses::ERotationType Node::getRotationType() const + { + return m_impl.getRotationType(); + } + + ramses::status_t Node::getRotation(vec3f& rotation) const + { + return m_impl.getRotation(rotation); + } + + ramses::status_t Node::getRotation(quat& rotation) const + { + return m_impl.getRotation(rotation); + } + + ramses::status_t Node::translate(const vec3f& translation) + { + const status_t status = m_impl.translate(translation); + LOG_HL_CLIENT_API3(status, translation.x, translation.y, translation.z); + return status; + } + + ramses::status_t Node::setTranslation(const vec3f& translation) + { + const status_t status = m_impl.setTranslation(translation); + LOG_HL_CLIENT_API3(status, translation.x, translation.y, translation.z); + return status; + } + + ramses::status_t Node::getTranslation(vec3f& translation) const + { + return m_impl.getTranslation(translation); + } + + ramses::status_t Node::scale(const vec3f& scaling) + { + const status_t status = m_impl.scale(scaling); + LOG_HL_CLIENT_API3(status, scaling.x, scaling.y, scaling.z); + return status; + } + + ramses::status_t Node::setScaling(const vec3f& scaling) + { + const status_t status = m_impl.setScaling(scaling); + LOG_HL_CLIENT_API3(status, scaling.x, scaling.y, scaling.z); + return status; + } + + ramses::status_t Node::getScaling(vec3f& scaling) const + { + return m_impl.getScaling(scaling); + } + + ramses::status_t Node::setVisibility(EVisibilityMode mode) + { + const status_t status = m_impl.setVisibility(mode); + LOG_HL_CLIENT_API1(status, VisibilityModeUtils::ToString(mode)); + return status; + } + + EVisibilityMode Node::getVisibility() const + { + return m_impl.getVisibility(); + } +} diff --git a/client/ramses-client/ramses-client-api/OrthographicCamera.cpp b/client/ramses-client-api/OrthographicCamera.cpp similarity index 78% rename from client/ramses-client/ramses-client-api/OrthographicCamera.cpp rename to client/ramses-client-api/OrthographicCamera.cpp index 05e5f6eb2..6ff163b1d 100644 --- a/client/ramses-client/ramses-client-api/OrthographicCamera.cpp +++ b/client/ramses-client-api/OrthographicCamera.cpp @@ -9,12 +9,13 @@ // API #include "ramses-client-api/OrthographicCamera.h" +//internal +#include "CameraNodeImpl.h" + namespace ramses { - OrthographicCamera::OrthographicCamera(CameraNodeImpl& pimpl) - : Camera(pimpl) + OrthographicCamera::OrthographicCamera(std::unique_ptr impl) + : Camera{ std::move(impl) } { } - - OrthographicCamera::~OrthographicCamera() = default; } diff --git a/client/ramses-client/ramses-client-api/PerspectiveCamera.cpp b/client/ramses-client-api/PerspectiveCamera.cpp similarity index 74% rename from client/ramses-client/ramses-client-api/PerspectiveCamera.cpp rename to client/ramses-client-api/PerspectiveCamera.cpp index 2de83aac5..357f0be22 100644 --- a/client/ramses-client/ramses-client-api/PerspectiveCamera.cpp +++ b/client/ramses-client-api/PerspectiveCamera.cpp @@ -14,29 +14,25 @@ namespace ramses { - PerspectiveCamera::PerspectiveCamera(CameraNodeImpl& pimpl) - : Camera(pimpl) - { - } - - PerspectiveCamera::~PerspectiveCamera() + PerspectiveCamera::PerspectiveCamera(std::unique_ptr impl) + : Camera{ std::move(impl) } { } status_t PerspectiveCamera::setFrustum(float fov, float aspectRatio, float nearPlane, float farPlane) { - const status_t status = impl.setPerspectiveFrustum(fov, aspectRatio, nearPlane, farPlane); + const status_t status = m_impl.setPerspectiveFrustum(fov, aspectRatio, nearPlane, farPlane); LOG_HL_CLIENT_API4(status, fov, aspectRatio, nearPlane, farPlane); return status; } float PerspectiveCamera::getVerticalFieldOfView() const { - return impl.getVerticalFieldOfView(); + return m_impl.getVerticalFieldOfView(); } float PerspectiveCamera::getAspectRatio() const { - return impl.getAspectRatio(); + return m_impl.getAspectRatio(); } } diff --git a/client/ramses-client/ramses-client-api/PickableObject.cpp b/client/ramses-client-api/PickableObject.cpp similarity index 73% rename from client/ramses-client/ramses-client-api/PickableObject.cpp rename to client/ramses-client-api/PickableObject.cpp index 188204b64..cda2f012b 100644 --- a/client/ramses-client/ramses-client-api/PickableObject.cpp +++ b/client/ramses-client-api/PickableObject.cpp @@ -16,52 +16,50 @@ namespace ramses { - PickableObject::PickableObject(PickableObjectImpl& pimpl) - : Node(pimpl) - , impl(pimpl) + PickableObject::PickableObject(std::unique_ptr impl) + : Node{ std::move(impl) } + , m_impl{ static_cast(Node::m_impl) } { } - PickableObject::~PickableObject() = default; - const ArrayBuffer& PickableObject::getGeometryBuffer() const { - return impl.getGeometryBuffer(); + return m_impl.getGeometryBuffer(); } const Camera* PickableObject::getCamera() const { - return impl.getCamera(); + return m_impl.getCamera(); } status_t PickableObject::setCamera(const Camera& camera) { - const status_t status = impl.setCamera(camera.impl); + const status_t status = m_impl.setCamera(camera.m_impl); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(camera)); return status; } pickableObjectId_t PickableObject::getPickableObjectId() const { - return impl.getPickableObjectId(); + return m_impl.getPickableObjectId(); } status_t PickableObject::setPickableObjectId(pickableObjectId_t id) { - const status_t status = impl.setPickableObjectId(id); + const status_t status = m_impl.setPickableObjectId(id); LOG_HL_CLIENT_API1(status, id); return status; } status_t PickableObject::setEnabled(bool enable) { - const status_t status = impl.setEnabled(enable); + const status_t status = m_impl.setEnabled(enable); LOG_HL_CLIENT_API1(status, enable); return status; } bool PickableObject::isEnabled() const { - return impl.isEnabled(); + return m_impl.isEnabled(); } } diff --git a/client/ramses-client/ramses-client-api/RamsesClient.cpp b/client/ramses-client-api/RamsesClient.cpp similarity index 54% rename from client/ramses-client/ramses-client-api/RamsesClient.cpp rename to client/ramses-client-api/RamsesClient.cpp index cb5f78b9f..302f2938e 100644 --- a/client/ramses-client/ramses-client-api/RamsesClient.cpp +++ b/client/ramses-client-api/RamsesClient.cpp @@ -18,90 +18,100 @@ namespace ramses { - RamsesClient::RamsesClient(RamsesClientImpl& impl_) - : RamsesObject(impl_) - , impl(impl_) + RamsesClient::RamsesClient(std::unique_ptr impl) + : RamsesObject{ std::move(impl) } + , m_impl{ static_cast(RamsesObject::m_impl) } { - impl.setHLObject(this); + m_impl.setHLObject(this); } - RamsesClient::~RamsesClient() + Scene* RamsesClient::createScene(sceneId_t sceneId, const SceneConfig& sceneConfig /*= SceneConfig()*/, std::string_view name) { - LOG_HL_CLIENT_API_NOARG(LOG_API_VOID); - } - - Scene* RamsesClient::createScene(sceneId_t sceneId, const SceneConfig& sceneConfig /*= SceneConfig()*/, const char* name) - { - Scene* scene = impl.createScene(sceneId, sceneConfig.impl, name); + Scene* scene = m_impl.createScene(sceneId, sceneConfig.m_impl, name); LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(scene), sceneId, name); return scene; } status_t RamsesClient::destroy(Scene& scene) { - const status_t status = impl.destroy(scene); + const status_t status = m_impl.destroy(scene); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(scene)); return status; } - ramses::Scene* RamsesClient::loadSceneFromFile(const char* fileName, bool localOnly) + ramses::Scene* RamsesClient::loadSceneFromFile(std::string_view fileName, bool localOnly) { - auto scene = impl.loadSceneFromFile(fileName, localOnly); + auto scene = m_impl.loadSceneFromFile(fileName, localOnly); LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(scene), fileName, localOnly); return scene; } + // NOLINTNEXTLINE(modernize-avoid-c-arrays) ramses::Scene* RamsesClient::loadSceneFromMemory(std::unique_ptr data, size_t size, bool localOnly) { - auto scene = impl.loadSceneFromMemory(std::move(data), size, localOnly); + auto scene = m_impl.loadSceneFromMemory(std::move(data), size, localOnly); LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(scene), size, localOnly); return scene; } ramses::Scene* RamsesClient::loadSceneFromFileDescriptor(int fd, size_t offset, size_t length, bool localOnly) { - auto scene = impl.loadSceneFromFileDescriptor(fd, offset, length, localOnly); + auto scene = m_impl.loadSceneFromFileDescriptor(fd, offset, length, localOnly); LOG_HL_CLIENT_API4(LOG_API_RAMSESOBJECT_PTR_STRING(scene), fd, offset, length, localOnly); return scene; } ramses::Scene* RamsesClient::loadSceneFromFileDescriptor(sceneId_t sceneId, int fd, size_t offset, size_t length, bool localOnly) { - auto scene = impl.loadSceneFromFileDescriptor(sceneId, fd, offset, length, localOnly); + auto scene = m_impl.loadSceneFromFileDescriptor(sceneId, fd, offset, length, localOnly); LOG_HL_CLIENT_API5(LOG_API_RAMSESOBJECT_PTR_STRING(scene), sceneId, fd, offset, length, localOnly); return scene; } - status_t RamsesClient::loadSceneFromFileAsync(const char* fileName, bool localOnly) + status_t RamsesClient::loadSceneFromFileAsync(std::string_view fileName, bool localOnly) { - auto status = impl.loadSceneFromFileAsync(fileName, localOnly); + auto status = m_impl.loadSceneFromFileAsync(fileName, localOnly); LOG_HL_CLIENT_API2(status, fileName, localOnly); return status; } - const Scene* RamsesClient::findSceneByName(const char* name) const + bool RamsesClient::GetFeatureLevelFromFile(std::string_view fileName, EFeatureLevel& detectedFeatureLevel) + { + const bool ret = RamsesClientImpl::GetFeatureLevelFromFile(fileName, detectedFeatureLevel); + LOG_HL_CLIENT_STATIC_API1(ret, fileName); + return ret; + } + + bool RamsesClient::GetFeatureLevelFromFile(int fd, size_t offset, size_t length, EFeatureLevel& detectedFeatureLevel) + { + const bool ret = RamsesClientImpl::GetFeatureLevelFromFile(fd, offset, length, detectedFeatureLevel); + LOG_HL_CLIENT_STATIC_API3(ret, fd, offset, length); + return ret; + } + + const Scene* RamsesClient::findSceneByName(std::string_view name) const { - return impl.findSceneByName(name); + return m_impl.findSceneByName(name); } - Scene* RamsesClient::findSceneByName(const char* name) + Scene* RamsesClient::findSceneByName(std::string_view name) { - return impl.findSceneByName(name); + return m_impl.findSceneByName(name); } const Scene* RamsesClient::getScene(sceneId_t sceneId) const { - return impl.getScene(sceneId); + return m_impl.getScene(sceneId); } Scene* RamsesClient::getScene(sceneId_t sceneId) { - return impl.getScene(sceneId); + return m_impl.getScene(sceneId); } status_t RamsesClient::dispatchEvents(IClientEventHandler& clientEventHandler) { - auto status = impl.dispatchEvents(clientEventHandler); + auto status = m_impl.dispatchEvents(clientEventHandler); LOG_HL_RENDERER_API1(status, LOG_API_GENERIC_OBJECT_STRING(clientEventHandler)); return status; } diff --git a/client/ramses-client/ramses-client-api/RamsesObject.cpp b/client/ramses-client-api/RamsesObject.cpp similarity index 66% rename from client/ramses-client/ramses-client-api/RamsesObject.cpp rename to client/ramses-client-api/RamsesObject.cpp index c93858fba..19b6e052b 100644 --- a/client/ramses-client/ramses-client-api/RamsesObject.cpp +++ b/client/ramses-client-api/RamsesObject.cpp @@ -14,36 +14,32 @@ namespace ramses { - RamsesObject::RamsesObject(RamsesObjectImpl& pimpl) - : StatusObject(pimpl) - , impl(pimpl) - { - impl.setRamsesObject(*this); - } - - RamsesObject::~RamsesObject() + RamsesObject::RamsesObject(std::unique_ptr impl) + : StatusObject{ std::move(impl) } + , m_impl{ static_cast(*StatusObject::m_impl) } { + m_impl.setRamsesObject(*this); } const char* RamsesObject::getName() const { - return impl.getName().c_str(); + return m_impl.getName().c_str(); } - status_t RamsesObject::setName(const char* name) + status_t RamsesObject::setName(std::string_view name) { - const status_t status =impl.setName(*this, name); + const status_t status =m_impl.setName(*this, name); LOG_HL_CLIENT_API1(status, name); return status; } ramses::ERamsesObjectType RamsesObject::getType() const { - return impl.getType(); + return m_impl.getType(); } bool RamsesObject::isOfType(ERamsesObjectType type) const { - return impl.isOfType(type); + return m_impl.isOfType(type); } } diff --git a/client/ramses-client/ramses-client-api/RenderBuffer.cpp b/client/ramses-client-api/RenderBuffer.cpp similarity index 71% rename from client/ramses-client/ramses-client-api/RenderBuffer.cpp rename to client/ramses-client-api/RenderBuffer.cpp index f67a48ddb..308bf2e22 100644 --- a/client/ramses-client/ramses-client-api/RenderBuffer.cpp +++ b/client/ramses-client-api/RenderBuffer.cpp @@ -14,43 +14,39 @@ namespace ramses { - RenderBuffer::RenderBuffer(RenderBufferImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - RenderBuffer::~RenderBuffer() + RenderBuffer::RenderBuffer(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } { } uint32_t RenderBuffer::getWidth() const { - return impl.getWidth(); + return m_impl.getWidth(); } uint32_t RenderBuffer::getHeight() const { - return impl.getHeight(); + return m_impl.getHeight(); } ERenderBufferType RenderBuffer::getBufferType() const { - return impl.getBufferType(); + return m_impl.getBufferType(); } ERenderBufferFormat RenderBuffer::getBufferFormat() const { - return impl.getBufferFormat(); + return m_impl.getBufferFormat(); } ERenderBufferAccessMode RenderBuffer::getAccessMode() const { - return impl.getAccessMode(); + return m_impl.getAccessMode(); } uint32_t RenderBuffer::getSampleCount() const { - return impl.getSampleCount(); + return m_impl.getSampleCount(); } } diff --git a/client/ramses-client/ramses-client-api/RenderGroup.cpp b/client/ramses-client-api/RenderGroup.cpp similarity index 71% rename from client/ramses-client/ramses-client-api/RenderGroup.cpp rename to client/ramses-client-api/RenderGroup.cpp index 82dfce348..14f41b314 100644 --- a/client/ramses-client/ramses-client-api/RenderGroup.cpp +++ b/client/ramses-client-api/RenderGroup.cpp @@ -15,74 +15,70 @@ namespace ramses { - RenderGroup::RenderGroup(RenderGroupImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - RenderGroup::~RenderGroup() + RenderGroup::RenderGroup(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } { } status_t RenderGroup::addMeshNode(const MeshNode& mesh, int32_t orderWithinGroup) { - const status_t status = impl.addMeshNode(mesh.impl, orderWithinGroup); + const status_t status = m_impl.addMeshNode(mesh.m_impl, orderWithinGroup); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(mesh), orderWithinGroup); return status; } status_t RenderGroup::removeMeshNode(const MeshNode& mesh) { - const status_t status = impl.remove(mesh.impl); + const status_t status = m_impl.remove(mesh.m_impl); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(mesh)); return status; } bool RenderGroup::containsMeshNode(const MeshNode& mesh) const { - return impl.contains(mesh.impl); + return m_impl.contains(mesh.m_impl); } status_t RenderGroup::getMeshNodeOrder(const MeshNode& mesh, int32_t& orderWithinGroup) const { - return impl.getMeshNodeOrder(mesh.impl, orderWithinGroup); + return m_impl.getMeshNodeOrder(mesh.m_impl, orderWithinGroup); } status_t RenderGroup::addRenderGroup(const RenderGroup& renderGroup, int32_t orderWithinGroup) { - const status_t status = impl.addRenderGroup(renderGroup.impl, orderWithinGroup); + const status_t status = m_impl.addRenderGroup(renderGroup.m_impl, orderWithinGroup); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(renderGroup), orderWithinGroup); return status; } status_t RenderGroup::removeRenderGroup(const RenderGroup& renderGroup) { - const status_t status = impl.remove(renderGroup.impl); + const status_t status = m_impl.remove(renderGroup.m_impl); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(renderGroup)); return status; } bool RenderGroup::containsRenderGroup(const RenderGroup& renderGroup) const { - return impl.contains(renderGroup.impl); + return m_impl.contains(renderGroup.m_impl); } status_t RenderGroup::getRenderGroupOrder(const RenderGroup& renderGroup, int32_t& orderWithinGroup) const { - return impl.getRenderGroupOrder(renderGroup.impl, orderWithinGroup); + return m_impl.getRenderGroupOrder(renderGroup.m_impl, orderWithinGroup); } status_t RenderGroup::removeAllRenderables() { - const status_t status = impl.removeAllRenderables(); + const status_t status = m_impl.removeAllRenderables(); LOG_HL_CLIENT_API_NOARG(status); return status; } ramses::status_t RenderGroup::removeAllRenderGroups() { - const status_t status = impl.removeAllRenderGroups(); + const status_t status = m_impl.removeAllRenderGroups(); LOG_HL_CLIENT_API_NOARG(status); return status; } diff --git a/client/ramses-client/ramses-client-api/RenderPass.cpp b/client/ramses-client-api/RenderPass.cpp similarity index 64% rename from client/ramses-client/ramses-client-api/RenderPass.cpp rename to client/ramses-client-api/RenderPass.cpp index c0fa4d59c..4bd81a71e 100644 --- a/client/ramses-client/ramses-client-api/RenderPass.cpp +++ b/client/ramses-client-api/RenderPass.cpp @@ -14,147 +14,138 @@ // internal #include "RenderPassImpl.h" -#include "Math3d/Vector4.h" namespace ramses { - RenderPass::RenderPass(RenderPassImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - RenderPass::~RenderPass() + RenderPass::RenderPass(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } { } status_t RenderPass::setCamera(const Camera& camera) { - const status_t status = impl.setCamera(camera.impl); + const status_t status = m_impl.setCamera(camera.m_impl); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(camera)); return status; } const Camera* RenderPass::getCamera() const { - return impl.getCamera(); + return m_impl.getCamera(); } Camera* RenderPass::getCamera() { - return impl.getCamera(); + return m_impl.getCamera(); } status_t RenderPass::addRenderGroup(const RenderGroup& renderGroup, int32_t orderWithinPass) { - const status_t status = impl.addRenderGroup(renderGroup.impl, orderWithinPass); + const status_t status = m_impl.addRenderGroup(renderGroup.m_impl, orderWithinPass); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(renderGroup), orderWithinPass); return status; } status_t RenderPass::removeRenderGroup(const RenderGroup& renderGroup) { - const status_t status = impl.remove(renderGroup.impl); + const status_t status = m_impl.remove(renderGroup.m_impl); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(renderGroup)); return status; } bool RenderPass::containsRenderGroup(const RenderGroup& renderGroup) const { - return impl.contains(renderGroup.impl); + return m_impl.contains(renderGroup.m_impl); } status_t RenderPass::getRenderGroupOrder(const RenderGroup& renderGroup, int32_t& orderWithinPass) const { - return impl.getRenderGroupOrder(renderGroup.impl, orderWithinPass); + return m_impl.getRenderGroupOrder(renderGroup.m_impl, orderWithinPass); } status_t RenderPass::removeAllRenderGroups() { - const status_t status = impl.removeAllRenderGroups(); + const status_t status = m_impl.removeAllRenderGroups(); LOG_HL_CLIENT_API_NOARG(status); return status; } status_t RenderPass::setRenderTarget(RenderTarget* renderTarget) { - const status_t status = impl.setRenderTarget(renderTarget ? &renderTarget->impl : nullptr); + const status_t status = m_impl.setRenderTarget(renderTarget ? &renderTarget->m_impl : nullptr); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_PTR_STRING(renderTarget)); return status; } const RenderTarget* RenderPass::getRenderTarget() const { - return impl.getRenderTarget(); + return m_impl.getRenderTarget(); } - status_t RenderPass::setClearColor(float r, float g, float b, float a) + status_t RenderPass::setClearColor(const vec4f& color) { - const status_t status = impl.setClearColor(ramses_internal::Vector4(r, g, b, a)); - LOG_HL_CLIENT_API4(status, r, g, b, a); + const status_t status = m_impl.setClearColor(color); + LOG_HL_CLIENT_API4(status, color.r, color.g, color.b, color.a); return status; } - void RenderPass::getClearColor(float &r, float &g, float &b, float &a) const + vec4f RenderPass::getClearColor() const { - const ramses_internal::Vector4& color = impl.getClearColor(); - r = color.r; - g = color.g; - b = color.b; - a = color.a; + return m_impl.getClearColor(); } status_t RenderPass::setClearFlags(uint32_t clearFlags) { - const status_t status = impl.setClearFlags(clearFlags); + const status_t status = m_impl.setClearFlags(clearFlags); LOG_HL_CLIENT_API1(status, clearFlags); return status; } uint32_t RenderPass::getClearFlags() const { - return impl.getClearFlags(); + return m_impl.getClearFlags(); } status_t RenderPass::setRenderOrder(int32_t renderOrder) { - const status_t status = impl.setRenderOrder(renderOrder); + const status_t status = m_impl.setRenderOrder(renderOrder); LOG_HL_CLIENT_API1(status, renderOrder); return status; } int32_t RenderPass::getRenderOrder() const { - return impl.getRenderOrder(); + return m_impl.getRenderOrder(); } status_t RenderPass::setEnabled(bool enable) { - const status_t status = impl.setEnabled(enable); + const status_t status = m_impl.setEnabled(enable); LOG_HL_CLIENT_API1(status, enable); return status; } bool RenderPass::isEnabled() const { - return impl.isEnabled(); + return m_impl.isEnabled(); } status_t RenderPass::setRenderOnce(bool enable) { - const status_t status = impl.setRenderOnce(enable); + const status_t status = m_impl.setRenderOnce(enable); LOG_HL_CLIENT_API1(status, enable); return status; } bool RenderPass::isRenderOnce() const { - return impl.isRenderOnce(); + return m_impl.isRenderOnce(); } status_t RenderPass::retriggerRenderOnce() { - const status_t status = impl.retriggerRenderOnce(); + const status_t status = m_impl.retriggerRenderOnce(); LOG_HL_CLIENT_API_NOARG(status); return status; } diff --git a/client/ramses-client/ramses-client-api/RenderTarget.cpp b/client/ramses-client-api/RenderTarget.cpp similarity index 74% rename from client/ramses-client/ramses-client-api/RenderTarget.cpp rename to client/ramses-client-api/RenderTarget.cpp index ea37c62ea..ade9e292e 100644 --- a/client/ramses-client/ramses-client-api/RenderTarget.cpp +++ b/client/ramses-client-api/RenderTarget.cpp @@ -14,23 +14,19 @@ namespace ramses { - RenderTarget::RenderTarget(RenderTargetImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - RenderTarget::~RenderTarget() + RenderTarget::RenderTarget(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } { } uint32_t RenderTarget::getWidth() const { - return impl.getWidth(); + return m_impl.getWidth(); } uint32_t RenderTarget::getHeight() const { - return impl.getHeight(); + return m_impl.getHeight(); } } diff --git a/client/ramses-client-api/RenderTargetDescription.cpp b/client/ramses-client-api/RenderTargetDescription.cpp new file mode 100644 index 000000000..a5dc0dbda --- /dev/null +++ b/client/ramses-client-api/RenderTargetDescription.cpp @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2017 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +// API +#include "ramses-client-api/RenderTargetDescription.h" +#include "ramses-client-api/RenderBuffer.h" + +// internal +#include "RenderTargetDescriptionImpl.h" + +namespace ramses +{ + RenderTargetDescription::RenderTargetDescription() + : StatusObject{ std::make_unique() } + , m_impl{ static_cast(*StatusObject::m_impl) } + { + } + + RenderTargetDescription::~RenderTargetDescription() = default; + + RenderTargetDescription::RenderTargetDescription(const RenderTargetDescription& other) + : StatusObject{ std::make_unique(other.m_impl) } + , m_impl{ static_cast(*StatusObject::m_impl) } + { + } + + RenderTargetDescription::RenderTargetDescription(RenderTargetDescription&& other) noexcept + : StatusObject{ std::move(other.StatusObject::m_impl) } + , m_impl{ static_cast(*StatusObject::m_impl) } + { + } + + RenderTargetDescription& RenderTargetDescription::operator=(const RenderTargetDescription& other) + { + StatusObject::m_impl = std::make_unique(other.m_impl); + m_impl = static_cast(*StatusObject::m_impl); + return *this; + } + + RenderTargetDescription& RenderTargetDescription::operator=(RenderTargetDescription&& other) noexcept + { + StatusObject::m_impl = std::move(other.StatusObject::m_impl); + m_impl = static_cast(*StatusObject::m_impl); + return *this; + } + + status_t RenderTargetDescription::addRenderBuffer(const RenderBuffer& renderBuffer) + { + const status_t status = m_impl.get().addRenderBuffer(renderBuffer.m_impl); + LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(renderBuffer)); + return status; + } +} diff --git a/client/ramses-client/ramses-client-api/Resource.cpp b/client/ramses-client-api/Resource.cpp similarity index 76% rename from client/ramses-client/ramses-client-api/Resource.cpp rename to client/ramses-client-api/Resource.cpp index 14beac761..d38af1c31 100644 --- a/client/ramses-client/ramses-client-api/Resource.cpp +++ b/client/ramses-client-api/Resource.cpp @@ -14,18 +14,14 @@ namespace ramses { - Resource::Resource(ResourceImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - Resource::~Resource() + Resource::Resource(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } { } resourceId_t Resource::getResourceId() const { - return impl.getResourceId(); + return m_impl.getResourceId(); } } diff --git a/client/ramses-client/ramses-client-api/Scene.cpp b/client/ramses-client-api/Scene.cpp similarity index 50% rename from client/ramses-client/ramses-client-api/Scene.cpp rename to client/ramses-client-api/Scene.cpp index 0ee684b64..dc51e18a5 100644 --- a/client/ramses-client/ramses-client-api/Scene.cpp +++ b/client/ramses-client-api/Scene.cpp @@ -20,30 +20,16 @@ #include "ramses-client-api/Texture2D.h" #include "ramses-client-api/Texture3D.h" #include "ramses-client-api/TextureCube.h" -#include "ramses-client-api/StreamTexture.h" #include "ramses-client-api/Effect.h" #include "ramses-client-api/RenderBuffer.h" #include "ramses-client-api/DataObject.h" #include "ramses-client-api/PerspectiveCamera.h" #include "ramses-client-api/OrthographicCamera.h" #include "ramses-client-api/GeometryBinding.h" -#include "ramses-client-api/AnimationSystem.h" -#include "ramses-client-api/AnimationSystemRealTime.h" #include "ramses-client-api/RenderGroup.h" #include "ramses-client-api/BlitPass.h" #include "ramses-client-api/PickableObject.h" #include "ramses-client-api/RenderTarget.h" -#include "ramses-client-api/DataFloat.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-client-api/DataVector4f.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector3i.h" -#include "ramses-client-api/DataVector4i.h" -#include "ramses-client-api/DataMatrix22f.h" -#include "ramses-client-api/DataMatrix33f.h" -#include "ramses-client-api/DataMatrix44f.h" -#include "ramses-client-api/DataInt32.h" #include "ramses-client-api/ArrayBuffer.h" #include "ramses-client-api/Texture2DBuffer.h" #include "ramses-client-api/ArrayResource.h" @@ -52,242 +38,216 @@ #include "SceneImpl.h" #include "EffectImpl.h" #include "TextureSamplerImpl.h" -#include "Utils/StringUtils.h" #include "RamsesFrameworkTypesImpl.h" #include "RamsesClientTypesImpl.h" namespace ramses { - Scene::Scene(SceneImpl& pimpl) - : ClientObject(pimpl) - , impl(pimpl) + Scene::Scene(std::unique_ptr impl) + : ClientObject{ std::move(impl) } + , m_impl{ static_cast(ClientObject::m_impl) } { } - Scene::~Scene() + PerspectiveCamera* Scene::createPerspectiveCamera(std::string_view name) { - } - - PerspectiveCamera* Scene::createPerspectiveCamera(const char* name) - { - PerspectiveCamera* perspectiveCamera = impl.createPerspectiveCamera(name); + PerspectiveCamera* perspectiveCamera = m_impl.createPerspectiveCamera(name); LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(perspectiveCamera), name); return perspectiveCamera; } - OrthographicCamera* Scene::createOrthographicCamera(const char* name) + OrthographicCamera* Scene::createOrthographicCamera(std::string_view name) { - OrthographicCamera* orthographicCamera = impl.createOrthographicCamera(name); + OrthographicCamera* orthographicCamera = m_impl.createOrthographicCamera(name); LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(orthographicCamera), name); return orthographicCamera; } - Appearance* Scene::createAppearance(const Effect& effect, const char* name) + Appearance* Scene::createAppearance(const Effect& effect, std::string_view name) { - Appearance* appearance = impl.createAppearance(effect, name); + Appearance* appearance = m_impl.createAppearance(effect, name); LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(appearance), LOG_API_RAMSESOBJECT_STRING(effect), name); return appearance; } - GeometryBinding* Scene::createGeometryBinding(const Effect& effect, const char* name) + GeometryBinding* Scene::createGeometryBinding(const Effect& effect, std::string_view name) { - GeometryBinding* geomBinding = impl.createGeometryBinding(effect, name); + GeometryBinding* geomBinding = m_impl.createGeometryBinding(effect, name); LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(geomBinding), LOG_API_RAMSESOBJECT_STRING(effect), name); return geomBinding; } - StreamTexture* Scene::createStreamTexture(const Texture2D& fallbackTexture, waylandIviSurfaceId_t source, const char* name) - { - StreamTexture* tex = impl.createStreamTexture(fallbackTexture, source, name); - LOG_HL_CLIENT_API3(LOG_API_RAMSESOBJECT_PTR_STRING(tex), LOG_API_RAMSESOBJECT_STRING(fallbackTexture), source, name); - return tex; - } - - Node* Scene::createNode(const char* name) + Node* Scene::createNode(std::string_view name) { - Node* node = impl.createNode(name); + Node* node = m_impl.createNode(name); LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(node), name); return node; } - MeshNode* Scene::createMeshNode(const char* name) + MeshNode* Scene::createMeshNode(std::string_view name) { - MeshNode* meshNode = impl.createMeshNode(name); + MeshNode* meshNode = m_impl.createMeshNode(name); LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(meshNode), name); return meshNode; } status_t Scene::publish(EScenePublicationMode publicationMode) { - const status_t status = impl.publish(publicationMode); + const status_t status = m_impl.publish(publicationMode); LOG_HL_CLIENT_API1(status, publicationMode); return status; } status_t Scene::unpublish() { - const status_t status = impl.unpublish(); + const status_t status = m_impl.unpublish(); LOG_HL_CLIENT_API_NOARG(status); return status; } bool Scene::isPublished() const { - return impl.isPublished(); + return m_impl.isPublished(); } sceneId_t Scene::getSceneId() const { - return impl.getSceneId(); + return m_impl.getSceneId(); } - status_t Scene::saveToFile(const char* fileName, bool compress) const + status_t Scene::saveToFile(std::string_view fileName, bool compress) const { - const auto status = impl.saveToFile(fileName, compress); + const auto status = m_impl.saveToFile(fileName, compress); LOG_HL_CLIENT_API2(status, fileName, compress); return status; } status_t Scene::destroy(SceneObject& object) { - const status_t status = impl.destroy(object); + const status_t status = m_impl.destroy(object); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(object)); return status; } status_t Scene::setExpirationTimestamp(uint64_t ptpExpirationTimestampInMilliseconds) { - const status_t status = impl.setExpirationTimestamp(ptpExpirationTimestampInMilliseconds); + const status_t status = m_impl.setExpirationTimestamp(ptpExpirationTimestampInMilliseconds); LOG_HL_CLIENT_API1(status, ptpExpirationTimestampInMilliseconds); return status; } status_t Scene::flush(sceneVersionTag_t sceneVersionTag) { - const status_t status = impl.flush(sceneVersionTag); - LOG_HL_CLIENT_API2(status, sceneVersionTag, impl.getSceneId()); + const status_t status = m_impl.flush(sceneVersionTag); + LOG_HL_CLIENT_API2(status, sceneVersionTag, m_impl.getSceneId()); return status; } status_t Scene::resetUniformTimeMs() { - const auto status = impl.resetUniformTimeMs(); + const auto status = m_impl.resetUniformTimeMs(); LOG_HL_CLIENT_API_NOARG(status); return status; } int32_t Scene::getUniformTimeMs() const { - return impl.getUniformTimeMs(); - } - - AnimationSystem* Scene::createAnimationSystem(uint32_t flags, const char* name) - { - AnimationSystem* animationSystem = impl.createAnimationSystem(flags, name); - LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(animationSystem), flags, name); - return animationSystem; - } - - AnimationSystemRealTime* Scene::createRealTimeAnimationSystem(uint32_t flags, const char* name) - { - AnimationSystemRealTime* animationSystemRealtime = impl.createRealTimeAnimationSystem(flags, name); - LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(animationSystemRealtime), flags, name); - return animationSystemRealtime; + return m_impl.getUniformTimeMs(); } - ArrayBuffer* Scene::createArrayBuffer(EDataType dataType, uint32_t maxNumElements, const char* name /*= nullptr*/) + ArrayBuffer* Scene::createArrayBuffer(EDataType dataType, uint32_t maxNumElements, std::string_view name /*= {}*/) { - auto dataBufferObject = impl.createArrayBuffer(dataType, maxNumElements, name); + auto dataBufferObject = m_impl.createArrayBuffer(dataType, maxNumElements, name); LOG_HL_CLIENT_API3(LOG_API_RAMSESOBJECT_PTR_STRING(dataBufferObject), maxNumElements, dataType, name); return dataBufferObject; } - Texture2DBuffer* Scene::createTexture2DBuffer(ETextureFormat textureFormat, uint32_t width, uint32_t height, uint32_t mipLevelCount, const char* name /*= 0*/) + Texture2DBuffer* Scene::createTexture2DBuffer(ETextureFormat textureFormat, uint32_t width, uint32_t height, size_t mipLevelCount, std::string_view name /*= 0*/) { - Texture2DBuffer* texture2DBuffer = impl.createTexture2DBuffer(mipLevelCount, width, height, textureFormat, name); - LOG_HL_CLIENT_API5(LOG_API_RAMSESOBJECT_PTR_STRING(texture2DBuffer), mipLevelCount, width, height, getTextureFormatString(textureFormat), name); + Texture2DBuffer* texture2DBuffer = m_impl.createTexture2DBuffer(mipLevelCount, width, height, textureFormat, name); + LOG_HL_CLIENT_API5(LOG_API_RAMSESOBJECT_PTR_STRING(texture2DBuffer), mipLevelCount, width, height, toString(textureFormat), name); return texture2DBuffer; } - SceneReference* Scene::createSceneReference(sceneId_t referencedScene, const char* name /*= nullptr*/) + SceneReference* Scene::createSceneReference(sceneId_t referencedScene, std::string_view name /*= {}*/) { - SceneReference* sceneReference = impl.createSceneReference(referencedScene, name); + SceneReference* sceneReference = m_impl.createSceneReference(referencedScene, name); LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(sceneReference), referencedScene, name); return sceneReference; } status_t Scene::linkData(SceneReference* providerReference, dataProviderId_t providerId, SceneReference* consumerReference, dataConsumerId_t consumerId) { - auto status = impl.linkData(providerReference, providerId, consumerReference, consumerId); + auto status = m_impl.linkData(providerReference, providerId, consumerReference, consumerId); LOG_HL_CLIENT_API4(status, LOG_API_RAMSESOBJECT_PTR_STRING(providerReference), providerId, LOG_API_RAMSESOBJECT_PTR_STRING(consumerReference), consumerId); return status; } ramses::status_t Scene::unlinkData(SceneReference* consumerReference, dataConsumerId_t consumerId) { - auto status = impl.unlinkData(consumerReference, consumerId); + auto status = m_impl.unlinkData(consumerReference, consumerId); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_PTR_STRING(consumerReference), consumerId); return status; } - const RamsesObject* Scene::findObjectByName(const char* name) const + const RamsesObject* Scene::findObjectByName(std::string_view name) const { - return impl.findObjectByName(name); + return m_impl.findObjectByName(name); } - RamsesObject* Scene::findObjectByName(const char* name) + RamsesObject* Scene::findObjectByName(std::string_view name) { - return impl.findObjectByName(name); + return m_impl.findObjectByName(name); } const SceneObject* Scene::findObjectById(sceneObjectId_t id) const { - return impl.findObjectById(id); + return m_impl.findObjectById(id); } SceneObject* Scene::findObjectById(sceneObjectId_t id) { - return impl.findObjectById(id); + return m_impl.findObjectById(id); } - RenderGroup* Scene::createRenderGroup(const char* name) + RenderGroup* Scene::createRenderGroup(std::string_view name) { - RenderGroup* renderGroup = impl.createRenderGroup(name); + RenderGroup* renderGroup = m_impl.createRenderGroup(name); LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(renderGroup), name); return renderGroup; } - RenderPass* Scene::createRenderPass(const char* name) + RenderPass* Scene::createRenderPass(std::string_view name) { - RenderPass* renderPass = impl.createRenderPass(name); + RenderPass* renderPass = m_impl.createRenderPass(name); LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(renderPass), name); return renderPass; } - BlitPass* Scene::createBlitPass(const RenderBuffer& sourceRenderBuffer, const RenderBuffer& destinationRenderBuffer, const char* name /*= 0*/) + BlitPass* Scene::createBlitPass(const RenderBuffer& sourceRenderBuffer, const RenderBuffer& destinationRenderBuffer, std::string_view name /*= {}*/) { - BlitPass* blitPass = impl.createBlitPass(sourceRenderBuffer, destinationRenderBuffer, name); + BlitPass* blitPass = m_impl.createBlitPass(sourceRenderBuffer, destinationRenderBuffer, name); LOG_HL_CLIENT_API3(LOG_API_RAMSESOBJECT_PTR_STRING(blitPass), LOG_API_RAMSESOBJECT_STRING(sourceRenderBuffer), LOG_API_RAMSESOBJECT_STRING(destinationRenderBuffer), name); return blitPass; } - PickableObject* Scene::createPickableObject(const ArrayBuffer& geometryBuffer, const pickableObjectId_t id, const char* name /* = nullptr */) + PickableObject* Scene::createPickableObject(const ArrayBuffer& geometryBuffer, const pickableObjectId_t id, std::string_view name /* = {} */) { - PickableObject* pickableObject = impl.createPickableObject(geometryBuffer, id, name); + PickableObject* pickableObject = m_impl.createPickableObject(geometryBuffer, id, name); LOG_HL_CLIENT_API3(LOG_API_RAMSESOBJECT_PTR_STRING(pickableObject), LOG_API_RAMSESOBJECT_STRING(geometryBuffer), id, name); return pickableObject; } - RenderBuffer* Scene::createRenderBuffer(uint32_t width, uint32_t height, ERenderBufferType bufferType, ERenderBufferFormat bufferFormat, ERenderBufferAccessMode accessMode, uint32_t sampleCount, const char* name) + RenderBuffer* Scene::createRenderBuffer(uint32_t width, uint32_t height, ERenderBufferType bufferType, ERenderBufferFormat bufferFormat, ERenderBufferAccessMode accessMode, uint32_t sampleCount, std::string_view name) { - RenderBuffer* renderBuffer = impl.createRenderBuffer(width, height, bufferType, bufferFormat, accessMode, sampleCount, name); + RenderBuffer* renderBuffer = m_impl.createRenderBuffer(width, height, bufferType, bufferFormat, accessMode, sampleCount, name); LOG_HL_CLIENT_API7(LOG_API_RAMSESOBJECT_PTR_STRING(renderBuffer), width, height, bufferType, bufferFormat, accessMode, sampleCount, name); return renderBuffer; } - RenderTarget* Scene::createRenderTarget(const RenderTargetDescription& rtDesc, const char* name) + RenderTarget* Scene::createRenderTarget(const RenderTargetDescription& rtDesc, std::string_view name) { - RenderTarget* renderTarget = impl.createRenderTarget(rtDesc.impl, name); + RenderTarget* renderTarget = m_impl.createRenderTarget(rtDesc.m_impl, name); LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(renderTarget), LOG_API_GENERIC_OBJECT_STRING(rtDesc), name); return renderTarget; } @@ -299,9 +259,9 @@ namespace ramses ETextureSamplingMethod magSamplingMethod, const Texture2D& texture, uint32_t anisotropyLevel, - const char* name) + std::string_view name) { - TextureSampler* texSampler = impl.createTextureSampler(wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel, texture, name); + TextureSampler* texSampler = m_impl.createTextureSampler(wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel, texture, name); LOG_HL_CLIENT_API7(LOG_API_RAMSESOBJECT_PTR_STRING(texSampler), wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel, LOG_API_RAMSESOBJECT_STRING(texture), name); return texSampler; } @@ -313,9 +273,9 @@ namespace ramses ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, const Texture3D& texture, - const char* name) + std::string_view name) { - TextureSampler* texSampler = impl.createTextureSampler(wrapUMode, wrapVMode, wrapRMode, minSamplingMethod, magSamplingMethod, texture, name); + TextureSampler* texSampler = m_impl.createTextureSampler(wrapUMode, wrapVMode, wrapRMode, minSamplingMethod, magSamplingMethod, texture, name); LOG_HL_CLIENT_API7(LOG_API_RAMSESOBJECT_PTR_STRING(texSampler), wrapUMode, wrapVMode, wrapRMode, minSamplingMethod, magSamplingMethod, LOG_API_RAMSESOBJECT_STRING(texture), name); return texSampler; } @@ -327,9 +287,9 @@ namespace ramses ETextureSamplingMethod magSamplingMethod, const TextureCube& texture, uint32_t anisotropyLevel, - const char* name) + std::string_view name) { - TextureSampler* texSampler = impl.createTextureSampler(wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel,texture, name); + TextureSampler* texSampler = m_impl.createTextureSampler(wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel,texture, name); LOG_HL_CLIENT_API7(LOG_API_RAMSESOBJECT_PTR_STRING(texSampler), wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel, LOG_API_RAMSESOBJECT_STRING(texture), name); return texSampler; } @@ -341,9 +301,9 @@ namespace ramses ETextureSamplingMethod magSamplingMethod, const RenderBuffer& renderBuffer, uint32_t anisotropyLevel, - const char* name) + std::string_view name) { - TextureSampler* texSampler = impl.createTextureSampler(wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel, renderBuffer, name); + TextureSampler* texSampler = m_impl.createTextureSampler(wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel, renderBuffer, name); LOG_HL_CLIENT_API7(LOG_API_RAMSESOBJECT_PTR_STRING(texSampler), wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel, LOG_API_RAMSESOBJECT_STRING(renderBuffer), name); return texSampler; } @@ -355,232 +315,161 @@ namespace ramses ETextureSamplingMethod magSamplingMethod, const Texture2DBuffer& texture2DBuffer, uint32_t anisotropyLevel, - const char* name) + std::string_view name) { - TextureSampler* texSampler = impl.createTextureSampler(wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel, texture2DBuffer, name); + TextureSampler* texSampler = m_impl.createTextureSampler(wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel, texture2DBuffer, name); LOG_HL_CLIENT_API7(LOG_API_RAMSESOBJECT_PTR_STRING(texSampler), wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, anisotropyLevel, LOG_API_RAMSESOBJECT_STRING(texture2DBuffer), name); return texSampler; } - TextureSampler* Scene::createTextureSampler( - ETextureAddressMode wrapUMode, - ETextureAddressMode wrapVMode, - ETextureSamplingMethod minSamplingMethod, - ETextureSamplingMethod magSamplingMethod, - const StreamTexture& streamTexture, - const char* name) - { - TextureSampler* texSampler = impl.createTextureSampler(wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, streamTexture, name); - LOG_HL_CLIENT_API6(LOG_API_RAMSESOBJECT_PTR_STRING(texSampler), wrapUMode, wrapVMode, minSamplingMethod, magSamplingMethod, LOG_API_RAMSESOBJECT_STRING(streamTexture), name); - return texSampler; - } - - TextureSamplerMS* Scene::createTextureSamplerMS(const RenderBuffer& renderBuffer, const char* name) + TextureSamplerMS* Scene::createTextureSamplerMS(const RenderBuffer& renderBuffer, std::string_view name) { - TextureSamplerMS* texSampler = impl.createTextureSamplerMS(renderBuffer, name); + TextureSamplerMS* texSampler = m_impl.createTextureSamplerMS(renderBuffer, name); LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(texSampler), LOG_API_RAMSESOBJECT_STRING(renderBuffer), name); return texSampler; } - TextureSamplerExternal* Scene::createTextureSamplerExternal(ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, const char *name) + TextureSamplerExternal* Scene::createTextureSamplerExternal(ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, std::string_view name) { - TextureSamplerExternal* texSampler = impl.createTextureSamplerExternal(minSamplingMethod, magSamplingMethod, name); + TextureSamplerExternal* texSampler = m_impl.createTextureSamplerExternal(minSamplingMethod, magSamplingMethod, name); LOG_HL_CLIENT_API3(LOG_API_RAMSESOBJECT_PTR_STRING(texSampler), minSamplingMethod, magSamplingMethod, name); return texSampler; } status_t Scene::createTransformationDataProvider(const Node& node, dataProviderId_t dataId) { - status_t status = impl.createTransformationDataProvider(node, dataId); + status_t status = m_impl.createTransformationDataProvider(node, dataId); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(node), dataId); return status; } status_t Scene::createTransformationDataConsumer(const Node& node, dataConsumerId_t dataId) { - status_t status = impl.createTransformationDataConsumer(node, dataId); + status_t status = m_impl.createTransformationDataConsumer(node, dataId); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(node), dataId); return status; } status_t Scene::createDataProvider(const DataObject& dataObject, dataProviderId_t dataId) { - status_t status = impl.createDataProvider(dataObject, dataId); + status_t status = m_impl.createDataProvider(dataObject, dataId); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(dataObject), dataId); return status; } status_t Scene::createDataConsumer(const DataObject& dataObject, dataConsumerId_t dataId) { - status_t status = impl.createDataConsumer(dataObject, dataId); + status_t status = m_impl.createDataConsumer(dataObject, dataId); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(dataObject), dataId); return status; } status_t Scene::createTextureProvider(const Texture2D& texture, dataProviderId_t dataId) { - status_t status = impl.createTextureProvider(texture, dataId); + status_t status = m_impl.createTextureProvider(texture, dataId); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(texture), dataId); return status; } status_t Scene::updateTextureProvider(const Texture2D& texture, dataProviderId_t dataId) { - status_t status = impl.updateTextureProvider(texture, dataId); + status_t status = m_impl.updateTextureProvider(texture, dataId); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(texture), dataId); return status; } status_t Scene::createTextureConsumer(const TextureSampler& sampler, dataConsumerId_t dataId) { - status_t status = impl.createTextureConsumer(sampler, dataId); + status_t status = m_impl.createTextureConsumer(sampler, dataId); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(sampler), dataId); return status; } status_t Scene::createTextureConsumer(const TextureSamplerMS& sampler, dataConsumerId_t dataId) { - status_t status = impl.createTextureConsumer(sampler, dataId); + status_t status = m_impl.createTextureConsumer(sampler, dataId); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(sampler), dataId); return status; } status_t Scene::createTextureConsumer(const TextureSamplerExternal& sampler, dataConsumerId_t dataId) { - status_t status = impl.createTextureConsumer(sampler, dataId); + status_t status = m_impl.createTextureConsumer(sampler, dataId); LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(sampler), dataId); return status; } - DataFloat* Scene::createDataFloat(const char* name) - { - DataFloat* dataObject = impl.createDataFloat(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), name); - return dataObject; - } - - DataVector2f* Scene::createDataVector2f(const char* name) - { - DataVector2f* dataObject = impl.createDataVector2f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), name); - return dataObject; - } - - DataVector3f* Scene::createDataVector3f(const char* name) + DataObject* Scene::createDataObject(EDataType dataType, std::string_view name) { - DataVector3f* dataObject = impl.createDataVector3f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), name); - return dataObject; - } - - DataVector4f* Scene::createDataVector4f(const char* name) - { - DataVector4f* dataObject = impl.createDataVector4f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), name); - return dataObject; - } - - DataMatrix22f* Scene::createDataMatrix22f(const char* name) - { - DataMatrix22f* dataObject = impl.createDataMatrix22f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), name); - return dataObject; - } - - DataMatrix33f* Scene::createDataMatrix33f(const char* name) - { - DataMatrix33f* dataObject = impl.createDataMatrix33f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), name); - return dataObject; - } - - DataMatrix44f* Scene::createDataMatrix44f(const char* name) - { - DataMatrix44f* dataObject = impl.createDataMatrix44f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), name); - return dataObject; - } - - DataInt32* Scene::createDataInt32(const char* name) - { - DataInt32* dataObject = impl.createDataInt32(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), name); - return dataObject; - } - - DataVector2i* Scene::createDataVector2i(const char* name) - { - DataVector2i* dataObject = impl.createDataVector2i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), name); - return dataObject; - } - - DataVector3i* Scene::createDataVector3i(const char* name) - { - DataVector3i* dataObject = impl.createDataVector3i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), name); - return dataObject; - } - - DataVector4i* Scene::createDataVector4i(const char* name) - { - DataVector4i* dataObject = impl.createDataVector4i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), name); + DataObject* dataObject = m_impl.createDataObject(dataType, name); + LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(dataObject), dataType, name); return dataObject; } RamsesClient& Scene::getRamsesClient() { - return impl.getHlRamsesClient(); + return m_impl.getHlRamsesClient(); } - ArrayResource* Scene::createArrayResource(EDataType type, uint32_t numElements, const void* arrayData, resourceCacheFlag_t cacheFlag, const char* name) + template + ArrayResource* Scene::createArrayResourceInternal(uint32_t numElements, const T* arrayData, resourceCacheFlag_t cacheFlag, std::string_view name) { - auto arr = impl.createArrayResource(type, numElements, arrayData, cacheFlag, name); - LOG_HL_CLIENT_API5(LOG_API_RESOURCE_PTR_STRING(arr), numElements, type, LOG_API_GENERIC_PTR_STRING(arrayData), cacheFlag, name); + auto arr = m_impl.createArrayResource(numElements, arrayData, cacheFlag, name); + LOG_HL_CLIENT_API5(LOG_API_RESOURCE_PTR_STRING(arr), numElements, GetEDataType(), LOG_API_GENERIC_PTR_STRING(arrayData), cacheFlag, name); return arr; } - Texture2D* Scene::createTexture2D(ETextureFormat format, uint32_t width, uint32_t height, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name /* = 0 */) + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + Texture2D* Scene::createTexture2D(ETextureFormat format, uint32_t width, uint32_t height, size_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, std::string_view name /* = {} */) { - Texture2D* tex = impl.createTexture2D(width, height, format, mipMapCount, mipLevelData, generateMipChain, swizzle, cacheFlag, name); - LOG_HL_CLIENT_API9(LOG_API_RESOURCE_PTR_STRING(tex), width, height, getTextureFormatString(format), mipMapCount, LOG_API_GENERIC_PTR_STRING(mipLevelData), generateMipChain, swizzle, cacheFlag, name); + Texture2D* tex = m_impl.createTexture2D(width, height, format, mipMapCount, mipLevelData, generateMipChain, swizzle, cacheFlag, name); + LOG_HL_CLIENT_API9(LOG_API_RESOURCE_PTR_STRING(tex), width, height, toString(format), mipMapCount, LOG_API_GENERIC_PTR_STRING(mipLevelData), generateMipChain, swizzle, cacheFlag, name); return tex; } - Texture3D* Scene::createTexture3D(ETextureFormat format, uint32_t width, uint32_t height, uint32_t depth, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, resourceCacheFlag_t cacheFlag, const char* name /* = 0 */) + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + Texture3D* Scene::createTexture3D(ETextureFormat format, uint32_t width, uint32_t height, uint32_t depth, size_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, resourceCacheFlag_t cacheFlag, std::string_view name /* = {} */) { - Texture3D* tex = impl.createTexture3D(width, height, depth, format, mipMapCount, mipLevelData, generateMipChain, cacheFlag, name); - LOG_HL_CLIENT_API9(LOG_API_RESOURCE_PTR_STRING(tex), width, height, depth, getTextureFormatString(format), mipMapCount, LOG_API_GENERIC_PTR_STRING(mipLevelData), generateMipChain, cacheFlag, name); + Texture3D* tex = m_impl.createTexture3D(width, height, depth, format, mipMapCount, mipLevelData, generateMipChain, cacheFlag, name); + LOG_HL_CLIENT_API9(LOG_API_RESOURCE_PTR_STRING(tex), width, height, depth, toString(format), mipMapCount, LOG_API_GENERIC_PTR_STRING(mipLevelData), generateMipChain, cacheFlag, name); return tex; } - TextureCube* Scene::createTextureCube(ETextureFormat format, uint32_t size, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name /* = 0 */) + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + TextureCube* Scene::createTextureCube(ETextureFormat format, uint32_t size, size_t mipMapCount, const CubeMipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, std::string_view name /* = {} */) { - TextureCube* tex = impl.createTextureCube(size, format, mipMapCount, mipLevelData, generateMipChain, swizzle, cacheFlag, name); - LOG_HL_CLIENT_API8(LOG_API_RESOURCE_PTR_STRING(tex), size, getTextureFormatString(format), mipMapCount, LOG_API_GENERIC_PTR_STRING(mipLevelData), generateMipChain, swizzle, cacheFlag, name); + TextureCube* tex = m_impl.createTextureCube(size, format, mipMapCount, mipLevelData, generateMipChain, swizzle, cacheFlag, name); + LOG_HL_CLIENT_API8(LOG_API_RESOURCE_PTR_STRING(tex), size, toString(format), mipMapCount, LOG_API_GENERIC_PTR_STRING(mipLevelData), generateMipChain, swizzle, cacheFlag, name); return tex; } - Effect* Scene::createEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, const char* name) + Effect* Scene::createEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, std::string_view name) { - Effect* effect = impl.createEffect(effectDesc, cacheFlag, name); + Effect* effect = m_impl.createEffect(effectDesc, cacheFlag, name); LOG_HL_CLIENT_API3(LOG_API_RESOURCE_PTR_STRING(effect), LOG_API_GENERIC_OBJECT_STRING(effectDesc), cacheFlag, name); return effect; } std::string Scene::getLastEffectErrorMessages() const { - return impl.getLastEffectErrorMessages(); + return m_impl.getLastEffectErrorMessages(); } const Resource* Scene::getResource(resourceId_t id) const { - return impl.getResource(id); + return m_impl.getResource(id); } Resource* Scene::getResource(resourceId_t id) { - return impl.getResource(id); + return m_impl.getResource(id); } + + template RAMSES_API ArrayResource* Scene::createArrayResourceInternal(uint32_t, const uint16_t*, resourceCacheFlag_t, std::string_view); + template RAMSES_API ArrayResource* Scene::createArrayResourceInternal(uint32_t, const uint32_t*, resourceCacheFlag_t, std::string_view); + template RAMSES_API ArrayResource* Scene::createArrayResourceInternal(uint32_t, const float*, resourceCacheFlag_t, std::string_view); + template RAMSES_API ArrayResource* Scene::createArrayResourceInternal(uint32_t, const vec2f*, resourceCacheFlag_t, std::string_view); + template RAMSES_API ArrayResource* Scene::createArrayResourceInternal(uint32_t, const vec3f*, resourceCacheFlag_t, std::string_view); + template RAMSES_API ArrayResource* Scene::createArrayResourceInternal(uint32_t, const vec4f*, resourceCacheFlag_t, std::string_view); + template RAMSES_API ArrayResource* Scene::createArrayResourceInternal(uint32_t, const Byte*, resourceCacheFlag_t, std::string_view); } diff --git a/client/ramses-client-api/SceneConfig.cpp b/client/ramses-client-api/SceneConfig.cpp new file mode 100644 index 000000000..299a55076 --- /dev/null +++ b/client/ramses-client-api/SceneConfig.cpp @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2015 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +// API +#include "ramses-client-api/SceneConfig.h" + +// internal +#include "SceneConfigImpl.h" +#include "APILoggingMacros.h" + +namespace ramses +{ + SceneConfig::SceneConfig() + : StatusObject{ std::make_unique() } + , m_impl{ static_cast(*StatusObject::m_impl) } + { + } + + SceneConfig::~SceneConfig() = default; + + SceneConfig::SceneConfig(const SceneConfig& other) + : StatusObject{ std::make_unique(other.m_impl) } + , m_impl{ static_cast(*StatusObject::m_impl) } + { + } + + SceneConfig::SceneConfig(SceneConfig&& other) noexcept + : StatusObject{ std::move(other.StatusObject::m_impl) } + , m_impl{ static_cast(*StatusObject::m_impl) } + { + } + + SceneConfig& SceneConfig::operator=(const SceneConfig& other) + { + StatusObject::m_impl = std::make_unique(other.m_impl); + m_impl = static_cast(*StatusObject::m_impl); + return *this; + } + + SceneConfig& SceneConfig::operator=(SceneConfig&& other) noexcept + { + StatusObject::m_impl = std::move(other.StatusObject::m_impl); + m_impl = static_cast(*StatusObject::m_impl); + return *this; + } + + status_t SceneConfig::setPublicationMode(EScenePublicationMode publicationMode) + { + const status_t status = m_impl.get().setPublicationMode(publicationMode); + LOG_HL_CLIENT_API1(status, publicationMode); + return status; + } +} diff --git a/client/ramses-client/ramses-client-api/SceneGraphIterator.cpp b/client/ramses-client-api/SceneGraphIterator.cpp similarity index 80% rename from client/ramses-client/ramses-client-api/SceneGraphIterator.cpp rename to client/ramses-client-api/SceneGraphIterator.cpp index 3f3cbe571..974b0dea9 100644 --- a/client/ramses-client/ramses-client-api/SceneGraphIterator.cpp +++ b/client/ramses-client-api/SceneGraphIterator.cpp @@ -15,19 +15,15 @@ namespace ramses { SceneGraphIterator::SceneGraphIterator(Node& startNode, ETreeTraversalStyle traversalStyle, ERamsesObjectType objectType) - : impl(new SceneGraphIteratorImpl(startNode, traversalStyle, objectType)) + : m_impl{ std::make_unique(startNode, traversalStyle, objectType) } { } - Node* SceneGraphIterator::getNext() - { - return impl->getNext(); - } + SceneGraphIterator::~SceneGraphIterator() = default; - SceneGraphIterator::~SceneGraphIterator() + Node* SceneGraphIterator::getNext() { - delete impl; + return m_impl->getNext(); } - } diff --git a/client/ramses-client/ramses-client-api/SceneObject.cpp b/client/ramses-client-api/SceneObject.cpp similarity index 73% rename from client/ramses-client/ramses-client-api/SceneObject.cpp rename to client/ramses-client-api/SceneObject.cpp index b4f459f6a..f3d49f2cd 100644 --- a/client/ramses-client/ramses-client-api/SceneObject.cpp +++ b/client/ramses-client-api/SceneObject.cpp @@ -14,23 +14,19 @@ namespace ramses { - SceneObject::SceneObject(SceneObjectImpl& pimpl) - : ClientObject(pimpl) - , impl(pimpl) - { - } - - SceneObject::~SceneObject() + SceneObject::SceneObject(std::unique_ptr impl) + : ClientObject{ std::move(impl) } + , m_impl{ static_cast(ClientObject::m_impl) } { } sceneObjectId_t SceneObject::getSceneObjectId() const { - return impl.getSceneObjectId(); + return m_impl.getSceneObjectId(); } sceneId_t SceneObject::getSceneId() const { - return impl.getSceneId(); + return m_impl.getSceneId(); } } diff --git a/client/ramses-client/ramses-client-api/SceneReference.cpp b/client/ramses-client-api/SceneReference.cpp similarity index 71% rename from client/ramses-client/ramses-client-api/SceneReference.cpp rename to client/ramses-client-api/SceneReference.cpp index d0dc63318..9cb7b50a0 100644 --- a/client/ramses-client/ramses-client-api/SceneReference.cpp +++ b/client/ramses-client-api/SceneReference.cpp @@ -11,42 +11,40 @@ namespace ramses { - SceneReference::SceneReference(SceneReferenceImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) + SceneReference::SceneReference(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } { } - SceneReference::~SceneReference() = default; - status_t SceneReference::requestState(RendererSceneState requestedState) { - const status_t status = impl.requestState(requestedState); + const status_t status = m_impl.requestState(requestedState); LOG_HL_CLIENT_API1(status, static_cast(requestedState)); return status; } sceneId_t SceneReference::getReferencedSceneId() const { - return impl.getReferencedSceneId(); + return m_impl.getReferencedSceneId(); } status_t SceneReference::requestNotificationsForSceneVersionTags(bool flag) { - const auto status = impl.requestNotificationsForSceneVersionTags(flag); + const auto status = m_impl.requestNotificationsForSceneVersionTags(flag); LOG_HL_CLIENT_API1(status, flag); return status; } status_t SceneReference::setRenderOrder(int32_t renderOrder) { - const auto status = impl.setRenderOrder(renderOrder); + const auto status = m_impl.setRenderOrder(renderOrder); LOG_HL_CLIENT_API1(status, renderOrder); return status; } ramses::RendererSceneState SceneReference::getRequestedState() const { - return impl.getRequestedState(); + return m_impl.getRequestedState(); } } diff --git a/client/ramses-client/ramses-client-api/Texture2D.cpp b/client/ramses-client-api/Texture2D.cpp similarity index 72% rename from client/ramses-client/ramses-client-api/Texture2D.cpp rename to client/ramses-client-api/Texture2D.cpp index 52bae3031..b52f30dfd 100644 --- a/client/ramses-client/ramses-client-api/Texture2D.cpp +++ b/client/ramses-client-api/Texture2D.cpp @@ -12,34 +12,30 @@ namespace ramses { - Texture2D::Texture2D(Texture2DImpl& pimpl) - : Resource(pimpl) - , impl(pimpl) - { - } - - Texture2D::~Texture2D() + Texture2D::Texture2D(std::unique_ptr impl) + : Resource{ std::move(impl) } + , m_impl{ static_cast(Resource::m_impl) } { } uint32_t Texture2D::getWidth() const { - return impl.getWidth(); + return m_impl.getWidth(); } uint32_t Texture2D::getHeight() const { - return impl.getHeight(); + return m_impl.getHeight(); } ETextureFormat Texture2D::getTextureFormat() const { - return impl.getTextureFormat(); + return m_impl.getTextureFormat(); } const TextureSwizzle& Texture2D::getTextureSwizzle() const { - return impl.getTextureSwizzle(); + return m_impl.getTextureSwizzle(); } } diff --git a/client/ramses-client-api/Texture2DBuffer.cpp b/client/ramses-client-api/Texture2DBuffer.cpp new file mode 100644 index 000000000..2c84af178 --- /dev/null +++ b/client/ramses-client-api/Texture2DBuffer.cpp @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2017 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +// API +#include "ramses-client-api/Texture2DBuffer.h" + +// Internal +#include "Texture2DBufferImpl.h" + +namespace ramses +{ + Texture2DBuffer::Texture2DBuffer(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } + { + } + + status_t Texture2DBuffer::updateData(size_t mipLevel, uint32_t offsetX, uint32_t offsetY, uint32_t width, uint32_t height, const void* data) + { + const status_t status = m_impl.setData(static_cast(data), mipLevel, offsetX, offsetY, width, height); + LOG_HL_CLIENT_API6(status, LOG_API_GENERIC_PTR_STRING(data), mipLevel, offsetX, offsetY, width, height); + return status; + } + + size_t Texture2DBuffer::getMipLevelCount() const + { + return m_impl.getMipLevelCount(); + } + + status_t Texture2DBuffer::getMipLevelSize(size_t mipLevel, uint32_t& widthOut, uint32_t& heightOut) const + { + return m_impl.getMipLevelSize(mipLevel, widthOut, heightOut); + } + + size_t Texture2DBuffer::getMipLevelDataSizeInBytes(size_t mipLevel) const + { + return m_impl.getMipLevelDataSizeInBytes(mipLevel); + } + + ETextureFormat Texture2DBuffer::getTexelFormat() const + { + return m_impl.getTexelFormat(); + } + + status_t Texture2DBuffer::getMipLevelData(size_t mipLevel, void* buffer, size_t bufferSize) const + { + return m_impl.getMipLevelData(mipLevel, static_cast(buffer), bufferSize); + } + +} diff --git a/client/ramses-client/ramses-client-api/Texture3D.cpp b/client/ramses-client-api/Texture3D.cpp similarity index 72% rename from client/ramses-client/ramses-client-api/Texture3D.cpp rename to client/ramses-client-api/Texture3D.cpp index 6ca714de8..60be1cdfb 100644 --- a/client/ramses-client/ramses-client-api/Texture3D.cpp +++ b/client/ramses-client-api/Texture3D.cpp @@ -11,34 +11,30 @@ namespace ramses { - Texture3D::Texture3D(Texture3DImpl& pimpl) - : Resource(pimpl) - , impl(pimpl) - { - } - - Texture3D::~Texture3D() + Texture3D::Texture3D(std::unique_ptr impl) + : Resource{ std::move(impl) } + , m_impl{ static_cast(Resource::m_impl) } { } uint32_t Texture3D::getWidth() const { - return impl.getWidth(); + return m_impl.getWidth(); } uint32_t Texture3D::getHeight() const { - return impl.getHeight(); + return m_impl.getHeight(); } uint32_t Texture3D::getDepth() const { - return impl.getDepth(); + return m_impl.getDepth(); } ETextureFormat Texture3D::getTextureFormat() const { - return impl.getTextureFormat(); + return m_impl.getTextureFormat(); } } diff --git a/client/ramses-client/ramses-client-api/TextureCube.cpp b/client/ramses-client-api/TextureCube.cpp similarity index 72% rename from client/ramses-client/ramses-client-api/TextureCube.cpp rename to client/ramses-client-api/TextureCube.cpp index 2d005f8cf..68b5edb55 100644 --- a/client/ramses-client/ramses-client-api/TextureCube.cpp +++ b/client/ramses-client-api/TextureCube.cpp @@ -11,28 +11,24 @@ namespace ramses { - TextureCube::TextureCube(TextureCubeImpl& pimpl) - : Resource(pimpl) - , impl(pimpl) - { - } - - TextureCube::~TextureCube() + TextureCube::TextureCube(std::unique_ptr impl) + : Resource{ std::move(impl) } + , m_impl{ static_cast(Resource::m_impl) } { } ETextureFormat TextureCube::getTextureFormat() const { - return impl.getTextureFormat(); + return m_impl.getTextureFormat(); } uint32_t TextureCube::getSize() const { - return impl.getSize(); + return m_impl.getSize(); } const TextureSwizzle& TextureCube::getTextureSwizzle() const { - return impl.getTextureSwizzle(); + return m_impl.getTextureSwizzle(); } } diff --git a/client/ramses-client/ramses-client-api/TextureEnums.cpp b/client/ramses-client-api/TextureEnums.cpp similarity index 77% rename from client/ramses-client/ramses-client-api/TextureEnums.cpp rename to client/ramses-client-api/TextureEnums.cpp index 2c917e942..92aec73d0 100644 --- a/client/ramses-client/ramses-client-api/TextureEnums.cpp +++ b/client/ramses-client-api/TextureEnums.cpp @@ -11,7 +11,7 @@ namespace ramses { - static const char* const TextureSamplingMethodNames[] = + const std::array TextureSamplingMethodNames = { "ETextureSamplingMethod_Nearest", "ETextureSamplingMethod_Linear", @@ -21,14 +21,13 @@ namespace ramses "ETextureSamplingMethod_Linear_MipMapLinear", }; - - static const char* const TextureAddressModeNames[] = { + const std::array TextureAddressModeNames = { "ETextureAddressMode_Clamp", "ETextureAddressMode_Repeat", "ETextureAddressMode_Mirror" }; - static const char* const TextureFormatNames[] = { + const std::array TextureFormatNames = { "ETextureFormat_Invalid", "ETextureFormat_R8", "ETextureFormat_RG8", @@ -79,7 +78,7 @@ namespace ramses "ETextureFormat_ASTC_SRGBA_12x12" }; - static const char* const TextureCubeFaceNames[] = { + const std::array TextureCubeFaceNames = { "ETextureCubeFace_PositiveX", "ETextureCubeFace_NegativeX", "ETextureCubeFace_PositiveY", @@ -88,29 +87,28 @@ namespace ramses "ETextureCubeFace_NegativeZ" }; - ENUM_TO_STRING(ETextureSamplingMethod, TextureSamplingMethodNames, ETextureSamplingMethod_NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(ETextureAddressMode, TextureAddressModeNames, ETextureAddressMode_NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(ETextureFormat, TextureFormatNames, ETextureFormat::NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(ETextureCubeFace, TextureCubeFaceNames, ETextureCubeFace_NUMBER_OF_ELEMENTS); + ENUM_TO_STRING_NO_EXTRA_LAST(ETextureSamplingMethod, TextureSamplingMethodNames, ETextureSamplingMethod::Linear_MipMapLinear); + ENUM_TO_STRING_NO_EXTRA_LAST(ETextureAddressMode, TextureAddressModeNames, ETextureAddressMode::Mirror); + ENUM_TO_STRING_NO_EXTRA_LAST(ETextureFormat, TextureFormatNames, ETextureFormat::ASTC_SRGBA_12x12); + ENUM_TO_STRING_NO_EXTRA_LAST(ETextureCubeFace, TextureCubeFaceNames, ETextureCubeFace::NegativeZ); - const char* getTextureSamplingMethodString(ETextureSamplingMethod samplingMethod) + const char* toString(ETextureSamplingMethod samplingMethod) { return EnumToString(samplingMethod); } - const char* getTextureAddressModeString(ETextureAddressMode addressMode) + const char* toString(ETextureAddressMode addressMode) { return EnumToString(addressMode); } - const char* getTextureFormatString(ETextureFormat format) + const char* toString(ETextureFormat format) { return EnumToString(format); } - const char* getTextureCubeFaceString(ETextureCubeFace face) + const char* toString(ETextureCubeFace face) { return EnumToString(face); } - } diff --git a/client/ramses-client/ramses-client-api/TextureSampler.cpp b/client/ramses-client-api/TextureSampler.cpp similarity index 68% rename from client/ramses-client/ramses-client-api/TextureSampler.cpp rename to client/ramses-client-api/TextureSampler.cpp index ed2a82295..35c3722f2 100644 --- a/client/ramses-client/ramses-client-api/TextureSampler.cpp +++ b/client/ramses-client-api/TextureSampler.cpp @@ -13,7 +13,6 @@ #include "ramses-client-api/TextureCube.h" #include "ramses-client-api/Texture2DBuffer.h" #include "ramses-client-api/RenderBuffer.h" -#include "ramses-client-api/StreamTexture.h" #include "APILoggingMacros.h" // internal @@ -21,89 +20,78 @@ namespace ramses { - TextureSampler::TextureSampler(TextureSamplerImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - TextureSampler::~TextureSampler() + TextureSampler::TextureSampler(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } { } ETextureAddressMode TextureSampler::getWrapUMode() const { - return impl.getWrapUMode(); + return m_impl.getWrapUMode(); } ETextureAddressMode TextureSampler::getWrapVMode() const { - return impl.getWrapVMode(); + return m_impl.getWrapVMode(); } ETextureAddressMode TextureSampler::getWrapRMode() const { - return impl.getWrapRMode(); + return m_impl.getWrapRMode(); } ETextureSamplingMethod TextureSampler::getMinSamplingMethod() const { - return impl.getMinSamplingMethod(); + return m_impl.getMinSamplingMethod(); } ETextureSamplingMethod TextureSampler::getMagSamplingMethod() const { - return impl.getMagSamplingMethod(); + return m_impl.getMagSamplingMethod(); } uint32_t TextureSampler::getAnisotropyLevel() const { - return impl.getAnisotropyLevel(); + return m_impl.getAnisotropyLevel(); } ERamsesObjectType TextureSampler::getTextureType() const { - return impl.getTextureType(); + return m_impl.getTextureType(); } status_t TextureSampler::setTextureData(const Texture2D& dataSource) { - const status_t status = impl.setTextureData(dataSource); + const status_t status = m_impl.setTextureData(dataSource); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(dataSource)); return status; } status_t TextureSampler::setTextureData(const Texture3D& dataSource) { - const status_t status = impl.setTextureData(dataSource); + const status_t status = m_impl.setTextureData(dataSource); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(dataSource)); return status; } status_t TextureSampler::setTextureData(const TextureCube& dataSource) { - const status_t status = impl.setTextureData(dataSource); + const status_t status = m_impl.setTextureData(dataSource); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(dataSource)); return status; } status_t TextureSampler::setTextureData(const Texture2DBuffer& dataSource) { - const status_t status = impl.setTextureData(dataSource); + const status_t status = m_impl.setTextureData(dataSource); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(dataSource)); return status; } status_t TextureSampler::setTextureData(const RenderBuffer& dataSource) { - const status_t status = impl.setTextureData(dataSource); - LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(dataSource)); - return status; - } - - status_t TextureSampler::setTextureData(const StreamTexture& dataSource) - { - const status_t status = impl.setTextureData(dataSource); + const status_t status = m_impl.setTextureData(dataSource); LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(dataSource)); return status; } diff --git a/client/ramses-client/ramses-client-api/TextureSamplerExternal.cpp b/client/ramses-client-api/TextureSamplerExternal.cpp similarity index 74% rename from client/ramses-client/ramses-client-api/TextureSamplerExternal.cpp rename to client/ramses-client-api/TextureSamplerExternal.cpp index be8733320..71afaa7c7 100644 --- a/client/ramses-client/ramses-client-api/TextureSamplerExternal.cpp +++ b/client/ramses-client-api/TextureSamplerExternal.cpp @@ -14,13 +14,9 @@ namespace ramses { - TextureSamplerExternal::TextureSamplerExternal(TextureSamplerImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - TextureSamplerExternal::~TextureSamplerExternal() + TextureSamplerExternal::TextureSamplerExternal(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } { } } diff --git a/client/ramses-client/ramses-client-api/TextureSamplerMS.cpp b/client/ramses-client-api/TextureSamplerMS.cpp similarity index 75% rename from client/ramses-client/ramses-client-api/TextureSamplerMS.cpp rename to client/ramses-client-api/TextureSamplerMS.cpp index b9408dfe0..689d7e6c3 100644 --- a/client/ramses-client/ramses-client-api/TextureSamplerMS.cpp +++ b/client/ramses-client-api/TextureSamplerMS.cpp @@ -14,13 +14,9 @@ namespace ramses { - TextureSamplerMS::TextureSamplerMS(TextureSamplerImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - TextureSamplerMS::~TextureSamplerMS() + TextureSamplerMS::TextureSamplerMS(std::unique_ptr impl) + : SceneObject{ std::move(impl) } + , m_impl{ static_cast(SceneObject::m_impl) } { } } diff --git a/client/ramses-client-api/UniformInput.cpp b/client/ramses-client-api/UniformInput.cpp new file mode 100644 index 000000000..139a64f79 --- /dev/null +++ b/client/ramses-client-api/UniformInput.cpp @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2014 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-client-api/UniformInput.h" +#include "EffectInputImpl.h" + +namespace ramses +{ + UniformInput::UniformInput() + : EffectInput{ std::make_unique() } + { + } + + UniformInput::~UniformInput() = default; + + UniformInput::UniformInput(const UniformInput& other) + : EffectInput{ std::make_unique(other.m_impl) } + { + } + + UniformInput::UniformInput(UniformInput&& other) noexcept + : EffectInput{ std::unique_ptr(static_cast(other.StatusObject::m_impl.release())) } + { + } + + UniformInput& UniformInput::operator=(const UniformInput& other) + { + StatusObject::m_impl = std::make_unique(other.m_impl); + m_impl = static_cast(*StatusObject::m_impl); + return *this; + } + + UniformInput& UniformInput::operator=(UniformInput&& other) noexcept + { + StatusObject::m_impl = std::move(other.StatusObject::m_impl); + m_impl = static_cast(*StatusObject::m_impl); + return *this; + } + + EEffectUniformSemantic UniformInput::getSemantics() const + { + return m_impl.get().getUniformSemantics(); + } + + size_t UniformInput::getElementCount() const + { + return m_impl.get().getElementCount(); + } +} diff --git a/client/ramses-client-api/include/ramses-client-api/Appearance.h b/client/ramses-client-api/include/ramses-client-api/Appearance.h new file mode 100644 index 000000000..c262562c3 --- /dev/null +++ b/client/ramses-client-api/include/ramses-client-api/Appearance.h @@ -0,0 +1,511 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2014 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#ifndef RAMSES_APPEARANCE_H +#define RAMSES_APPEARANCE_H + +#include "ramses-client-api/SceneObject.h" +#include "ramses-framework-api/AppearanceEnums.h" +#include "ramses-framework-api/DataTypes.h" + +namespace ramses +{ + class UniformInput; + class DataObject; + class TextureSampler; + class TextureSamplerMS; + class TextureSamplerExternal; + class Effect; + + /** + * @brief The Appearance describes how an object should look like. This includes GLSL uniform values, + * and GPU states such as blending, buffer configurations, masks etc. The API to set uniform values + * is aligned to the glUniformX API of OpenGL. Beware that boolean values are reported and handled + * as int (0 is false, anything else is true) - similar to OpenGL conventions. + * @ingroup CoreAPI + */ + class Appearance : public SceneObject + { + public: + /** + * @brief Sets blending factors for source/destination color/alpha. + * Blending operations need to be set as well in order to enable blending. + * + * @param[in] srcColor Source color blending factor + * @param[in] destColor Destination color blending factor + * @param[in] srcAlpha Source alpha blending factor + * @param[in] destAlpha Destination alpha blending factor + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setBlendingFactors(EBlendFactor srcColor, EBlendFactor destColor, EBlendFactor srcAlpha, EBlendFactor destAlpha); + + /** + * @brief Gets blending factors for source/destination color/alpha. + * + * @param[out] srcColor Source color blending factor + * @param[out] destColor Destination color blending factor + * @param[out] srcAlpha Source alpha blending factor + * @param[out] destAlpha Destination alpha blending factor + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getBlendingFactors(EBlendFactor& srcColor, EBlendFactor& destColor, EBlendFactor& srcAlpha, EBlendFactor& destAlpha) const; + + /** + * @brief Sets blending operation for color and alpha. + * Blending factors need to be set as well in order to enable blending. + * + * @param[in] operationColor Blending operation for color + * @param[in] operationAlpha Blending operation for alpha + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setBlendingOperations(EBlendOperation operationColor, EBlendOperation operationAlpha); + + /** + * @brief Gets blending operation for color and alpha. + * + * @param[out] operationColor Blending operation for color + * @param[out] operationAlpha Blending operation for alpha + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getBlendingOperations(EBlendOperation& operationColor, EBlendOperation& operationAlpha) const; + + /** + * @brief Sets blending color that can be used as blending color constant for some blending factors. + * The default value is (0,0,0,0) + * + * @param[in] color RGBA channels of blending color to set ([0;1] range) + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setBlendingColor(const vec4f& color); + + /** + * @brief Gets blending color set via #setBlendingColor + * + * @param[out] color RGBA channels of blending color ([0;1] range) + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getBlendingColor(vec4f& color) const; + + /** + * @brief Enables or disables writing to depth buffer. + * + * @param[in] mode Flag denoting enabling or disabling depth writes. + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setDepthWrite(EDepthWrite mode); + + /** + * @brief Gets the current state of writing to depth buffer. + * @param[out] mode Depth write mode + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getDepthWriteMode(EDepthWrite& mode) const; + + /** + * @brief Sets depth comparison function. + * Depth writing has to be enabled in order for this to have any effect. + * Default depth comparison function is less or equal. + * + * @param[in] func Depth comparison function to be used + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setDepthFunction(EDepthFunc func); + + /** + * @brief Gets depth comparison function. + * + * @param[out] func Depth comparison function to be used + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getDepthFunction(EDepthFunc& func) const; + + /** + * @brief Enables or disables scissor test and sets region for scissor test + * + * @param[in] state Flag denoting enabling or disabling scissor test. + * @param[in] x Offset of scissor region on x-axis. + * @param[in] y Offset of scissor region on y-axis. + * @param[in] width Width of scissor region. + * @param[in] height Height of scissor region. + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setScissorTest(EScissorTest state, int16_t x, int16_t y, uint16_t width, uint16_t height); + + /** + * @brief Gets the current state of scissor test. + * @param[out] state State of scissor test + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getScissorTestState(EScissorTest& state) const; + + /** + * @brief Gets region for scissor test + * + * @param[out] x Offset of scissor region on x-axis. + * @param[out] y Offset of scissor region on y-axis. + * @param[out] width Width of scissor region. + * @param[out] height Height of scissor region. + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getScissorRegion(int16_t& x, int16_t& y, uint16_t& width, uint16_t& height) const; + + /** + * @brief Sets stencil function, reference and mask value for stencil testing. + * Stencil is disabled by default. + * + * @param[in] func Stencil function to be used + * @param[in] ref Stencil reference value to be used + * @param[in] mask Stencil mask value to be used + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setStencilFunction(EStencilFunc func, uint8_t ref, uint8_t mask); + + /** + * @brief Gets stencil function, reference and mask value + * + * @param[out] func Stencil function currently set + * @param[out] ref Stencil reference value currently set + * @param[out] mask Stencil mask value currently set + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getStencilFunction(EStencilFunc& func, uint8_t& ref, uint8_t& mask) const; + + /** + * @brief Sets stencil operations for stencil testing. + * Default stencil operation values are keep. + * + * @param[in] sfail Stencil operation when stencil test fails + * @param[in] dpfail Stencil operation when the stencil test passes, but the depth test fails + * @param[in] dppass Stencil operation when both the stencil test and the depth test pass + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setStencilOperation(EStencilOperation sfail, EStencilOperation dpfail, EStencilOperation dppass); + + /** + * @brief Gets stencil operations + * + * @param[out] sfail Stencil operation when stencil test fails + * @param[out] dpfail Stencil operation when the stencil test passes, but the depth test fails + * @param[out] dppass Stencil operation when both the stencil test and the depth test pass + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getStencilOperation(EStencilOperation& sfail, EStencilOperation& dpfail, EStencilOperation& dppass) const; + + /** + * @brief Sets the culling mode indicating which side of mesh will be removed before rasterization. + * Default culling mode is BackFaceCulling. + * + * @param[in] mode Culling mode to be used. + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setCullingMode(ECullMode mode); + + /** + * @brief Sets the draw mode indicating by which primitive the mesh will be rendered + * Default draw mode is #ramses::EDrawMode::Triangles, however if the effect used + * has a geometry shader, then the input type declared in the geometry shader is set + * as draw mode automatically when #Appearance is created. + * + * @param[in] mode Draw mode to be used. + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setDrawMode(EDrawMode mode); + + /** + * @brief Gets the culling mode indicating which side of mesh will be removed before rasterization. + * + * @param[out] mode Culling mode to be used. + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getCullingMode(ECullMode& mode) const; + + /** + * @brief Gets the draw mode indicating by which primitive the mesh will be rendered + * + * @param[out] mode draw mode to be used. + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getDrawMode(EDrawMode& mode) const; + + /** + * @brief Sets color write mask. + * If needed certain color channels can stay untouched using the color write mask. + * By default writing to all color channels is enabled. + * + * @param[in] writeRed Enable/disable flag for red channel + * @param[in] writeGreen Enable/disable flag for green channel + * @param[in] writeBlue Enable/disable flag for blue channel + * @param[in] writeAlpha Enable/disable flag for alpha channel + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setColorWriteMask(bool writeRed, bool writeGreen, bool writeBlue, bool writeAlpha); + + /** + * @brief Gets color write mask. + * + * @param[out] writeRed Enable/disable flag for red channel + * @param[out] writeGreen Enable/disable flag for green channel + * @param[out] writeBlue Enable/disable flag for blue channel + * @param[out] writeAlpha Enable/disable flag for alpha channel + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getColorWriteMask(bool& writeRed, bool& writeGreen, bool& writeBlue, bool& writeAlpha) const; + + /** + * @brief Sets value to uniform input. + * Value type must pass #ramses::IsUniformInputDataType. + * This method will fail if value type not compatible with the uniform data type + * (see #ramses::UniformInput::getDataType). + * + * @param[in] input The effect uniform input to set the value to + * @param[in] value The value to set + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + template + status_t setInputValue(const UniformInput& input, T&& value); + + /** + * @brief Sets value(s) to uniform input. + * Value type must pass #ramses::IsUniformInputDataType. + * This method will fail if value type not compatible with the uniform data type + * (see #ramses::UniformInput::getDataType). + * + * @param[in] input The effect uniform input to set the value to + * @param[in] elementCount The number of elements to use from \c values. + * Must match #ramses::UniformInput::getElementCount. + * @param[in] values Pointer the the values, must contain at least \c elementCount elements. + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + template + status_t setInputValue(const UniformInput& input, size_t elementCount, const T* values); + + /** + * @brief Gets value of uniform input. + * Value type must pass #ramses::IsUniformInputDataType. + * This method will fail if value type not compatible with the uniform data type + * (see #ramses::UniformInput::getDataType). + * + * @param[in] input The effect uniform input + * @param[out] value The value + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + template + status_t getInputValue(const UniformInput& input, T& value) const; + + /** + * @brief Gets value(s) of uniform input. + * Value type must pass #ramses::IsUniformInputDataType. + * This method will fail if value type not compatible with the uniform data type + * (see #ramses::UniformInput::getDataType). + * + * @param[in] input The effect uniform input + * @param[in] elementCount The number of elements to copy to \c valuesOut. + * Must match #ramses::UniformInput::getElementCount. + * @param[out] valuesOut location where the values are copied to + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + template + status_t getInputValue(const UniformInput& input, size_t elementCount, T* valuesOut) const; + + /** + * @brief Sets texture sampler to the input + * + * @param[in] input The effect uniform input to set the value to + * @param[in] textureSampler The texture sampler + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setInputTexture(const UniformInput& input, const TextureSampler& textureSampler); + + /** + * @brief Sets multisampled texture sampler to the input + * + * @param[in] input The multisampled texture sampler uniform input to set the value to + * @param[in] textureSampler The multisampled texture sampler + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setInputTexture(const UniformInput& input, const TextureSamplerMS& textureSampler); + + /** + * @brief Sets external texture sampler to the input + * + * @param[in] input The external texture sampler uniform input to set the value to + * @param[in] textureSampler The external texture sampler + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setInputTexture(const UniformInput& input, const TextureSamplerExternal& textureSampler); + + /** + * @brief Gets texture sampler currently set to the input + * + * @param[in] input The effect uniform input + * @param[out] textureSampler Will set texture sampler pointer to the TextureSampler object set to the uniform input, + * nullptr if none set or there was an error + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getInputTexture(const UniformInput& input, const TextureSampler*& textureSampler) const; + + /** + * @brief Gets texture sampler currently set to the input + * + * @param[in] input The effect uniform input + * @param[out] textureSampler Will set texture sampler pointer to the TextureSamplerMS object set to the uniform input, + * nullptr if none set or there was an error + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getInputTextureMS(const UniformInput& input, const TextureSamplerMS*& textureSampler) const; + + /** + * @brief Gets texture sampler currently set to the input + * + * @param[in] input The effect uniform input + * @param[out] textureSampler Will set texture sampler pointer to the TextureSamplerExternal object set to the uniform input, + * nullptr if none set or there was an error + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getInputTextureExternal(const UniformInput& input, const TextureSamplerExternal*& textureSampler) const; + + /** + * @brief Bind a DataObject to the Appearance's uniform input. + * The value from the DataObject will be used and any change made on the DataObject + * will be reflected in the Appearance. One DataObject can be bound to multiple Appearances. + * The data type of the DataObject must match the uniform input data type otherwise + * the call will fail and report error. + * DataObject cannot be bound to an input with semantics, texture input or array input. + * Once a DataObject is bound to an input the value cannot be set or get using \c set/getInputValue*() anymore. + * Binding a DataObject to an already bound input will unbind the old one and bind the new one. + * + * @param[in] input The effect uniform input to bind the DataObject to + * @param[in] dataObject The DataObject to be bound + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t bindInput(const UniformInput& input, const DataObject& dataObject); + + /** + * @brief Unbind a previously bound DataObject from the Appearance's uniform input. + * Any previously set value that was set before binding will now be used. + * Appropriate \c set/getInputValue*() method must be used to set or get the value + * or another DataObject can be bound. + * + * @param[in] input The effect uniform input to unbind the DataObject from + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t unbindInput(const UniformInput& input); + + /** + * @brief Check whether a uniform input has any DataObject bound to it. + * + * @param[in] input The effect uniform input to check + * @return \c true if there is any DataObject bound to the input, false otherwise + */ + [[nodiscard]] RAMSES_API bool isInputBound(const UniformInput& input) const; + + /** + * @brief Gets the data object bound to a uniform input. + * + * @param[in] input The effect uniform input to get the bound data object for + * @return \c The data object bound the uniform input if existing, otherwise returns nullptr + */ + [[nodiscard]] RAMSES_API const DataObject* getDataObjectBoundToInput(const UniformInput& input) const; + + /** + * @brief Gets the effect used to create this appearance + * + * @return The effect used to create the appearance. + */ + [[nodiscard]] RAMSES_API const Effect& getEffect() const; + + /** + * Stores internal data for implementation specifics of Appearance. + */ + class AppearanceImpl& m_impl; + + protected: + /** + * @brief Scene is the factory for creating Appearance instances. + */ + friend class RamsesObjectRegistry; + + /** + * @brief Constructor of Appearance + * + * @param[in] impl Internal data for implementation specifics of Appearance (sink - instance becomes owner) + */ + explicit Appearance(std::unique_ptr impl); + + private: + /// Internal implementation of #setInputValue + template RAMSES_API status_t setInputValueInternal(const UniformInput& input, T&& value); + /// Internal implementation of #setInputValue + template RAMSES_API status_t setInputValueInternal(const UniformInput& input, size_t elementCount, const T* values); + /// Internal implementation of #getInputValue + template RAMSES_API status_t getInputValueInternal(const UniformInput& input, T& value) const; + /// Internal implementation of #getInputValue + template RAMSES_API status_t getInputValueInternal(const UniformInput& input, size_t elementCount, T* valuesOut) const; + }; + + template status_t Appearance::setInputValue(const UniformInput& input, T&& value) + { + static_assert(IsUniformInputDataType(), "Unsupported uniform data type!"); + return setInputValueInternal(input, std::forward(value)); + } + + template status_t Appearance::setInputValue(const UniformInput& input, size_t elementCount, const T* values) + { + static_assert(IsUniformInputDataType(), "Unsupported uniform data type!"); + return setInputValueInternal(input, elementCount, values); + } + + template status_t Appearance::getInputValue(const UniformInput& input, T& value) const + { + static_assert(IsUniformInputDataType(), "Unsupported uniform data type!"); + return getInputValueInternal(input, value); + } + + template status_t Appearance::getInputValue(const UniformInput& input, size_t elementCount, T* valuesOut) const + { + static_assert(IsUniformInputDataType(), "Unsupported uniform data type!"); + return getInputValueInternal(input, elementCount, valuesOut); + } +} + +#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/ArrayBuffer.h b/client/ramses-client-api/include/ramses-client-api/ArrayBuffer.h similarity index 61% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/ArrayBuffer.h rename to client/ramses-client-api/include/ramses-client-api/ArrayBuffer.h index c6f29c70b..30e2a1d3c 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/ArrayBuffer.h +++ b/client/ramses-client-api/include/ramses-client-api/ArrayBuffer.h @@ -10,11 +10,13 @@ #define RAMSES_ARRAYBUFFER_H #include "ramses-client-api/SceneObject.h" -#include "ramses-client-api/EDataType.h" +#include "ramses-framework-api/EDataType.h" +#include "ramses-framework-api/DataTypes.h" namespace ramses { /** + * @ingroup CoreAPI * @brief The ArrayBuffer is a data object used to provide vertex or index data to #ramses::GeometryBinding::setInputBuffer * and #ramses::GeometryBinding::setIndices. The buffer data of an ArrayBuffer is not filled initially and can be fully * or partially updated in between scene flushes. @@ -23,13 +25,16 @@ namespace ramses * is defined as one byte, rather than a logical vertex element. Hence, all functions of #ArrayBuffer * referring to element refer to a single byte within byte array. */ - class RAMSES_API ArrayBuffer : public SceneObject + class ArrayBuffer : public SceneObject { public: /** * @brief Update data of the ArrayBuffer object. * - * @details For #ArrayBuffer objects of type #ramses::EDataType::ByteBlob an element is defined as 1 byte. + * @details Data type of provided data must pass #ramses::IsArrayResourceDataType. + * This method will fail if the provided data type does not match this #ArrayBuffer data type + * (see #ramses::ArrayBuffer::getDataType and #ramses::GetEDataType). + * For #ArrayBuffer objects of type #ramses::EDataType::ByteBlob an element is defined as 1 byte. * * @param firstElement The element index at which data update should begin. * @param numElements The number of elements to be updated. @@ -43,7 +48,8 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t updateData(uint32_t firstElement, uint32_t numElements, const void* bufferData); + template + status_t updateData(uint32_t firstElement, uint32_t numElements, const T* bufferData); /** * @brief Returns the maximum number of data elements that can be stored in the data buffer. @@ -53,7 +59,7 @@ namespace ramses * * @return Maximum number of elements */ - uint32_t getMaximumNumberOfElements() const; + [[nodiscard]] RAMSES_API uint32_t getMaximumNumberOfElements() const; /** * @brief Returns the used number of data elements. @@ -64,50 +70,67 @@ namespace ramses * * @return Used size in number of elements */ - uint32_t getUsedNumberOfElements() const; + [[nodiscard]] RAMSES_API uint32_t getUsedNumberOfElements() const; /** * @brief Returns the data type associated with the buffer data * * @return data type of buffer data */ - EDataType getDataType() const; + [[nodiscard]] RAMSES_API EDataType getDataType() const; /** - * @brief Copies the data of the data buffer into a user-provided buffer. The buffer must - * be sufficiently large to hold the data for the whole data buffer + * @brief Copies the data of the data buffer into a user-provided buffer. + * + * @details The buffer must be sufficiently large to hold the data for the \c numElements to be copied out. + * This method will fail if the provided data type does not match this #ArrayBuffer data type + * (see #ramses::ArrayBuffer::getDataType and #ramses::GetEDataType). + * For #ArrayBuffer objects of type #ramses::EDataType::ByteBlob an element is defined as 1 byte. * * @param[out] buffer The buffer where the buffer data will be copied into - * @param[in] numElements The number of elements to copy. For #ArrayBuffer objects of type #ramses::EDataType::ByteBlob this - * is the size in bytes of the data to copy. + * @param[in] numElements The number of elements to copy * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t getData(void* buffer, uint32_t numElements) const; + template + status_t getData(T* buffer, uint32_t numElements) const; /** * Stores internal data for implementation specifics of ArrayBuffer. */ - class ArrayBufferImpl& impl; + class ArrayBufferImpl& m_impl; protected: /** * @brief Scene is the factory for creating ArrayBuffer instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for ArrayBuffer. * - * @param[in] pimpl Internal data for implementation specifics of ArrayBuffer (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of ArrayBuffer (sink - instance becomes owner) */ - explicit ArrayBuffer(ArrayBufferImpl& pimpl); + explicit ArrayBuffer(std::unique_ptr impl); - /** - * @brief Destructor of the ArrayBuffer - */ - virtual ~ArrayBuffer(); + private: + /// Internal implementation of #updateData + template RAMSES_API status_t updateDataInternal(uint32_t firstElement, uint32_t numElements, const T* bufferData); + /// Internal implementation of #getData + template RAMSES_API status_t getDataInternal(T* buffer, uint32_t numElements) const; }; + + template status_t ArrayBuffer::updateData(uint32_t firstElement, uint32_t numElements, const T* bufferData) + { + static_assert(IsArrayResourceDataType(), "Unsupported data type!"); + return updateDataInternal(firstElement, numElements, bufferData); + } + + template status_t ArrayBuffer::getData(T* buffer, uint32_t numElements) const + { + static_assert(IsArrayResourceDataType(), "Unsupported data type!"); + return getDataInternal(buffer, numElements); + } } #endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/ArrayResource.h b/client/ramses-client-api/include/ramses-client-api/ArrayResource.h similarity index 60% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/ArrayResource.h rename to client/ramses-client-api/include/ramses-client-api/ArrayResource.h index c19e5877f..60e9d6939 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/ArrayResource.h +++ b/client/ramses-client-api/include/ramses-client-api/ArrayResource.h @@ -10,11 +10,12 @@ #define RAMSES_ARRAYRESOURCE_H #include "ramses-client-api/Resource.h" -#include "ramses-client-api/EDataType.h" +#include "ramses-framework-api/EDataType.h" namespace ramses { /** + * @ingroup CoreAPI * @brief The #ArrayResource stores a data array of a given type. The data is immutable. * The resource can be used as input for a #ramses::GeometryBinding. * @@ -22,57 +23,36 @@ namespace ramses * is defined as one byte, rather than a logical vertex element. Hence, number of elements is * the same as size in bytes. */ - class RAMSES_API ArrayResource : public Resource + class ArrayResource : public Resource { public: /** * Stores internal data for implementation specifics of ArrayResource. */ - class ArrayResourceImpl& impl; + class ArrayResourceImpl& m_impl; /** * @brief Returns number of elements of the array. */ - uint32_t getNumberOfElements() const; + [[nodiscard]] RAMSES_API uint32_t getNumberOfElements() const; /** * @brief Returns the data type of the data array. */ - EDataType getDataType() const; + [[nodiscard]] RAMSES_API EDataType getDataType() const; protected: - /** * @brief Scene is the factory for creating ArrayResource instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor of ArrayResource * - * @param[in] pimpl Internal data for implementation specifics of ArrayResource (sink - instance becomes owner) - */ - explicit ArrayResource(ArrayResourceImpl& pimpl); - - /** - * @brief Copy constructor of ArrayResource - * - * @param[in] other Other instance of ArrayResource class - */ - ArrayResource(const ArrayResource& other); - - /** - * @brief Assignment operator of ArrayResource. - * - * @param[in] other Other instance of ArrayResource class - * @return This instance after assignment - */ - ArrayResource& operator=(const ArrayResource& other); - - /** - * @brief Destructor of the ArrayResource + * @param[in] impl Internal data for implementation specifics of ArrayResource (sink - instance becomes owner) */ - virtual ~ArrayResource(); + explicit ArrayResource(std::unique_ptr impl); }; } diff --git a/client/ramses-client-api/include/ramses-client-api/AttributeInput.h b/client/ramses-client-api/include/ramses-client-api/AttributeInput.h new file mode 100644 index 000000000..6b31ab78d --- /dev/null +++ b/client/ramses-client-api/include/ramses-client-api/AttributeInput.h @@ -0,0 +1,69 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2014 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#ifndef RAMSES_ATTRIBUTEINPUT_H +#define RAMSES_ATTRIBUTEINPUT_H + +#include "ramses-client-api/EffectInput.h" +#include "ramses-client-api/EffectInputSemantic.h" + +namespace ramses +{ + /** + * @ingroup CoreAPI + * @brief The AttributeInput is a description of an attribute effect input + */ + class AttributeInput : public EffectInput + { + public: + /** + * @brief Constructor of AttributeInput. + */ + RAMSES_API AttributeInput(); + + /** + * @brief Destructor of AttributeInput. + */ + RAMSES_API ~AttributeInput() override; + + /** + * @brief Returns the effect input semantics. + * + * @return Effect input semantics + */ + [[nodiscard]] RAMSES_API EEffectAttributeSemantic getSemantics() const; + + /** + * @brief Copy constructor + * @param other source to copy from + */ + RAMSES_API AttributeInput(const AttributeInput& other); + + /** + * @brief Move constructor + * @param other source to move from + */ + RAMSES_API AttributeInput(AttributeInput&& other) noexcept; + + /** + * @brief Copy assignment + * @param other source to copy from + * @return this instance + */ + RAMSES_API AttributeInput& operator=(const AttributeInput& other); + + /** + * @brief Move assignment + * @param other source to move from + * @return this instance + */ + RAMSES_API AttributeInput& operator=(AttributeInput&& other) noexcept; + }; +} + +#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/BlitPass.h b/client/ramses-client-api/include/ramses-client-api/BlitPass.h similarity index 81% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/BlitPass.h rename to client/ramses-client-api/include/ramses-client-api/BlitPass.h index 81f0422a2..a5e477afd 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/BlitPass.h +++ b/client/ramses-client-api/include/ramses-client-api/BlitPass.h @@ -16,28 +16,28 @@ namespace ramses class RenderBuffer; /** + * @ingroup CoreAPI * @brief The BlitPass blits contents of one RendeBuffer to another. The source and destination * RenderBuffer objects must have same type, format and dimensions. BlitPass objects are ordered together * using a render order, which is also shared with RenderPass objects, i.e, BlitPass and RenderPass objects * can all be ordered relative to each other. */ - class RAMSES_API BlitPass : public SceneObject + class BlitPass : public SceneObject { public: - /** * @brief Get the source render buffer used for blitting. * * @return The source render buffer. */ - const RenderBuffer& getSourceRenderBuffer() const; + [[nodiscard]] RAMSES_API const RenderBuffer& getSourceRenderBuffer() const; /** * @brief Get the destination render buffer used for blitting. * * @return The destination render buffer. */ - const RenderBuffer& getDestinationRenderBuffer() const; + [[nodiscard]] RAMSES_API const RenderBuffer& getDestinationRenderBuffer() const; /** * @brief Set the region for blitting from source and destination render buffers. @@ -55,8 +55,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setBlittingRegion(uint32_t sourceX, uint32_t sourceY, uint32_t destinationX, uint32_t destinationY, uint32_t width, uint32_t height); - + RAMSES_API status_t setBlittingRegion(uint32_t sourceX, uint32_t sourceY, uint32_t destinationX, uint32_t destinationY, uint32_t width, uint32_t height); /** * @brief Get the blitting region in source and destination render buffers. @@ -69,7 +68,7 @@ namespace ramses * @param[out] height Height of blitting region, used for source and destination blitting regions * */ - void getBlittingRegion(uint32_t& sourceX, uint32_t& sourceY, uint32_t& destinationX, uint32_t& destinationY, uint32_t& width, uint32_t& height) const; + RAMSES_API void getBlittingRegion(uint32_t& sourceX, uint32_t& sourceY, uint32_t& destinationX, uint32_t& destinationY, uint32_t& width, uint32_t& height) const; /** * @brief Set the render order for the blit pass. @@ -83,14 +82,14 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setRenderOrder(int32_t renderOrder); + RAMSES_API status_t setRenderOrder(int32_t renderOrder); /** * @brief Get the render order of this blit pass. * * @return The render order of this blit pass. */ - int32_t getRenderOrder() const; + [[nodiscard]] RAMSES_API int32_t getRenderOrder() const; /** * @brief Enable/Disable blit pass @@ -99,37 +98,32 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setEnabled(bool enable); + RAMSES_API status_t setEnabled(bool enable); /** * @brief Get the enable state of the blit pass * * @return Indicates if the blit pass is enabled */ - bool isEnabled() const; + [[nodiscard]] RAMSES_API bool isEnabled() const; /** * Stores internal data for implementation specifics of BlitPass. */ - class BlitPassImpl& impl; + class BlitPassImpl& m_impl; protected: /** * @brief Scene is the factory for creating BlitPass instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for BlitPass. * - * @param pimpl Internal data for implementation specifics of BlitPass (sink - instance becomes owner) - */ - explicit BlitPass(BlitPassImpl& pimpl); - - /** - * @brief Destructor of the BlitPass + * @param impl Internal data for implementation specifics of BlitPass (sink - instance becomes owner) */ - virtual ~BlitPass(); + explicit BlitPass(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/Camera.h b/client/ramses-client-api/include/ramses-client-api/Camera.h similarity index 81% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/Camera.h rename to client/ramses-client-api/include/ramses-client-api/Camera.h index adc0f20d7..d53c16d64 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/Camera.h +++ b/client/ramses-client-api/include/ramses-client-api/Camera.h @@ -13,16 +13,15 @@ namespace ramses { - class DataVector2i; - class DataVector2f; - class DataVector4f; + class DataObject; /** + * @ingroup CoreAPI * @brief The #Camera base class is part of a scene and defines a view into the scene * defined by the client application. It is also a #Node with transformation. * @details A valid camera for rendering must have viewport and frustum set. */ - class RAMSES_API Camera : public Node + class Camera : public Node { public: /** @@ -50,7 +49,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setFrustum(float leftPlane, float rightPlane, float bottomPlane, float topPlane, float nearPlane, float farPlane); + RAMSES_API status_t setFrustum(float leftPlane, float rightPlane, float bottomPlane, float topPlane, float nearPlane, float farPlane); /** * @brief Sets the viewport to be used when rendering with this camera. @@ -72,7 +71,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setViewport(int32_t x, int32_t y, uint32_t width, uint32_t height); + RAMSES_API status_t setViewport(int32_t x, int32_t y, uint32_t width, uint32_t height); /** * @brief Returns the horizontal offset of the viewport in pixels relative to lower left corner of destination render buffer. @@ -81,7 +80,7 @@ namespace ramses * * @return horizontal offset of the viewport in pixels */ - int32_t getViewportX() const; + [[nodiscard]] RAMSES_API int32_t getViewportX() const; /** * @brief Returns the vertical offset of the viewport in pixels relative to lower left corner of destination render buffer. @@ -90,7 +89,7 @@ namespace ramses * * @return vertical offset of the viewport in pixels */ - int32_t getViewportY() const; + [[nodiscard]] RAMSES_API int32_t getViewportY() const; /** * @brief Returns the viewport width in pixels @@ -99,7 +98,7 @@ namespace ramses * * @return viewport width in pixels */ - uint32_t getViewportWidth() const; + [[nodiscard]] RAMSES_API uint32_t getViewportWidth() const; /** * @brief Returns the viewport height in pixels @@ -108,7 +107,7 @@ namespace ramses * * @return viewport height in pixels */ - uint32_t getViewportHeight() const; + [[nodiscard]] RAMSES_API uint32_t getViewportHeight() const; /** * @brief Returns the left plane of the camera frustum @@ -117,7 +116,7 @@ namespace ramses * * @return the left plane of the Camera */ - float getLeftPlane() const; + [[nodiscard]] RAMSES_API float getLeftPlane() const; /** * @brief Returns the right plane of the camera frustum @@ -126,7 +125,7 @@ namespace ramses * * @return the right plane of the #Camera */ - float getRightPlane() const; + [[nodiscard]] RAMSES_API float getRightPlane() const; /** * @brief Returns the bottom plane of the camera frustum @@ -135,7 +134,7 @@ namespace ramses * * @return the bottom plane of the #Camera */ - float getBottomPlane() const; + [[nodiscard]] RAMSES_API float getBottomPlane() const; /** * @brief Returns the top plane of the camera frustum @@ -144,7 +143,7 @@ namespace ramses * * @return the top plane of the #Camera */ - float getTopPlane() const; + [[nodiscard]] RAMSES_API float getTopPlane() const; /** * @brief Returns the near plane of the camera frustum @@ -153,7 +152,7 @@ namespace ramses * * @return the near plane of the #Camera */ - float getNearPlane() const; + [[nodiscard]] RAMSES_API float getNearPlane() const; /** * @brief Returns the far plane of the camera frustum @@ -162,7 +161,7 @@ namespace ramses * * @return the far plane of the #Camera */ - float getFarPlane() const; + [[nodiscard]] RAMSES_API float getFarPlane() const; /** * @brief Gets projection matrix based on camera parameters. @@ -173,55 +172,54 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t getProjectionMatrix(float (&projectionMatrix)[16]) const; + [[nodiscard]] RAMSES_API status_t getProjectionMatrix(matrix44f& projectionMatrix) const; /** * @brief Binds a #ramses::DataObject to be used as source for viewport offset values. - * * @details In addition to #setViewport, which sets viewport parameters directly, - * a #ramses::DataVector2i can be bound to viewport offset and size. + * a #ramses::DataObject can be bound to viewport offset and size. * When a #ramses::DataObject is bound the values from it override those set using #setViewport, * see #ramses::DataObject for possible use cases. * - * @param[in] offsetData Data object with 2 integers that will be used as source for viewport offset values + * @param[in] offsetData Data object of type #ramses::EDataType::Vector2I that will be used as source for viewport offset values * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t bindViewportOffset(const DataVector2i& offsetData); + RAMSES_API status_t bindViewportOffset(const DataObject& offsetData); /** * @brief Binds a #ramses::DataObject to be used as source for viewport size values. * * @details In addition to #setViewport, which sets viewport parameters directly, - * a #ramses::DataVector2i can be bound to viewport offset and size. + * a #ramses::DataObject can be bound to viewport offset and size. * When a #ramses::DataObject is bound the values from it override those set using #setViewport, * see #ramses::DataObject for possible use cases. * - * @param[in] sizeData Data object with 2 integers that will be used as source for viewport size values + * @param[in] sizeData Data object of type #ramses::EDataType::Vector2I that will be used as source for viewport size values * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t bindViewportSize(const DataVector2i& sizeData); + RAMSES_API status_t bindViewportSize(const DataObject& sizeData); /** * @brief Binds #ramses::DataObject to be used as source for frustum planes values. * * @details In addition to #setFrustum (alternatively #ramses::PerspectiveCamera::setFrustum), which sets - * frustum planes directly, a #ramses::DataVector4f can be bound to provide values for left, right, - * bottom, top planes and a #ramses::DataVector2f for near and far planes. + * frustum planes directly, a #ramses::DataObject can be bound to provide values for left, right, + * bottom, top planes and another #ramses::DataObject for near and far planes. * When a #ramses::DataObject is bound the values from it override those set using #setFrustum, * see #ramses::DataObject for possible use cases. * See #ramses::RamsesUtils::SetPerspectiveCameraFrustumToDataObjects providing way to conveniently * set perspective frustum on data objects also with basic validity checking. * - * @param[in] frustumPlanesData Data object with 4 floats that will be used as source for frustum planes values. + * @param[in] frustumPlanesData Data object of type #ramses::EDataType::Vector4F that will be used as source for frustum planes values. * The (x, y, z, w) floats represent (left, right, bottom, top) frustum planes. - * @param[in] nearFarPlanesData Data object with 2 floats that will be used as source for frustum planes values. + * @param[in] nearFarPlanesData Data object of type #ramses::EDataType::Vector2F that will be used as source for frustum planes values. * The (x, y) floats represent (near, far) frustum planes. * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t bindFrustumPlanes(const DataVector4f& frustumPlanesData, const DataVector2f& nearFarPlanesData); + RAMSES_API status_t bindFrustumPlanes(const DataObject& frustumPlanesData, const DataObject& nearFarPlanesData); /** * @brief Unbinds any bound #ramses::DataObject from viewport offset (see #bindViewportOffset). @@ -229,7 +227,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t unbindViewportOffset(); + RAMSES_API status_t unbindViewportOffset(); /** * @brief Unbinds any bound #ramses::DataObject from viewport size (see #bindViewportSize). @@ -237,7 +235,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t unbindViewportSize(); + RAMSES_API status_t unbindViewportSize(); /** * @brief Unbinds any bound #ramses::DataObject from frustum planes (see #bindFrustumPlanes). @@ -245,46 +243,43 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t unbindFrustumPlanes(); + RAMSES_API status_t unbindFrustumPlanes(); /** * @brief Checks if there is a #ramses::DataObject bound to viewport offset (see #bindViewportOffset). * @return True if there is any #ramses::DataObject bound, false otherwise. */ - bool isViewportOffsetBound() const; + [[nodiscard]] RAMSES_API bool isViewportOffsetBound() const; /** * @brief Checks if there is a #ramses::DataObject bound to viewport size (see #bindViewportSize). * @return True if there is any #ramses::DataObject bound, false otherwise. */ - bool isViewportSizeBound() const; + [[nodiscard]] RAMSES_API bool isViewportSizeBound() const; /** * @brief Checks if there is a #ramses::DataObject bound to viewport size (see #bindFrustumPlanes). * @return True if there is any #ramses::DataObject bound, false otherwise. */ - bool isFrustumPlanesBound() const; + [[nodiscard]] RAMSES_API bool isFrustumPlanesBound() const; /** * Stores internal data for implementation specifics of Camera. */ - class CameraNodeImpl& impl; + class CameraNodeImpl& m_impl; protected: /** * @brief Scene is the factory for creating #Camera instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for #Camera. * - * @param[in] pimpl Internal data for implementation specifics of #Camera (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of #Camera (sink - instance becomes owner) */ - explicit Camera(CameraNodeImpl& pimpl); - - /** Protected trivial destructor to avoid deleting by user*/ - virtual ~Camera(); + explicit Camera(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/ClientObject.h b/client/ramses-client-api/include/ramses-client-api/ClientObject.h similarity index 51% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/ClientObject.h rename to client/ramses-client-api/include/ramses-client-api/ClientObject.h index d8758ee0f..307d979ba 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/ClientObject.h +++ b/client/ramses-client-api/include/ramses-client-api/ClientObject.h @@ -14,47 +14,24 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief The ClientObject is a base class for all client API objects owned by a RamsesClient. */ - class RAMSES_API ClientObject : public RamsesObject + class ClientObject : public RamsesObject { public: /** * Stores internal data for implementation specifics of ClientObject. */ - class ClientObjectImpl& impl; + class ClientObjectImpl& m_impl; protected: /** * @brief Constructor for ClientObject. * - * @param[in] pimpl Internal data for implementation specifics of ClientObject (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of ClientObject (sink - instance becomes owner) */ - explicit ClientObject(ClientObjectImpl& pimpl); - - /** - * @brief Destructor of the ClientObject - */ - virtual ~ClientObject(); - - /** - * @brief RamsesClientImpl is the factory for creating client objects. - */ - friend class RamsesClientImpl; - - private: - /** - * @brief Copy constructor of ClientObject - */ - ClientObject(const ClientObject& other); - - /** - * @brief Assignment operator of ClientObject. - * - * @param[in] other Instance to assign from - * @return This instance after assignment - */ - ClientObject& operator=(const ClientObject& other); + explicit ClientObject(std::unique_ptr impl); }; } diff --git a/client/ramses-client-api/include/ramses-client-api/DataObject.h b/client/ramses-client-api/include/ramses-client-api/DataObject.h new file mode 100644 index 000000000..3c3ed44c0 --- /dev/null +++ b/client/ramses-client-api/include/ramses-client-api/DataObject.h @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2016 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#ifndef RAMSES_DATAOBJECT_H +#define RAMSES_DATAOBJECT_H + +#include "ramses-client-api/SceneObject.h" +#include "ramses-framework-api/EDataType.h" +#include "ramses-framework-api/DataTypes.h" + +namespace ramses +{ + /** + * @ingroup CoreAPI + * @brief The DataObject is a data container for storing data within a scene. + * @details The DataObject can be bound to some inputs of some object types + * (e.g. #ramses::Appearance::bindInput or #ramses::Camera::bindViewportOffset). + * When a data object is bound to an input the data object value overrides whatever was previously + * set to that input using its direct setter. + * A single #DataObject can be bound to multiple inputs (also to other #ramses::RamsesObject types + * where applicable) providing a way to distribute value across instances/inputs. + * Using #DataObject also allows use of data linking across scenes between data object provider and consumer + * (#ramses::Scene::createDataProvider and #ramses::Scene::createDataConsumer) see SDK examples for typical use cases. + * A value set to a #DataObject is then propagated everywhere it is bound to and it is linked to. + */ + class DataObject : public SceneObject + { + public: + /** + * @brief Returns the data type this #DataObject holds. + * + * @return data type held in this #DataObject + */ + [[nodiscard]] RAMSES_API EDataType getDataType() const; + + /** + * @brief Sets/updates the stored value. + * @details Type of \c value must match #getDataType (see #ramses::GetEDataType). + * + * @param[in] value new value. + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + template + status_t setValue(T&& value); + + /** + * @brief Gets the stored value. + * @details Type of \c value must match #getDataType (see #ramses::GetEDataType). + * + * @param[out] value stored value. + * @return status == 0 for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + template + status_t getValue(T& value) const; + + /** + * Stores internal data for implementation specifics + */ + class DataObjectImpl& m_impl; + + protected: + /** + * @brief Scene is the factory for creating DataObject instances. + */ + friend class RamsesObjectRegistry; + + /** + * @brief Constructor of DataObject + * + * @param[in] impl Internal data for implementation specifics of DataObject + */ + explicit DataObject(std::unique_ptr impl); + + private: + /// Internal implementation of #setValue + template RAMSES_API status_t setValueInternal(T&& value); + /// Internal implementation of #getValue + template RAMSES_API status_t getValueInternal(T& value) const; + }; + + template status_t DataObject::setValue(T&& value) + { + static_assert(IsUniformInputDataType(), "Unsupported data type!"); + return setValueInternal(std::forward(value)); + } + + template status_t DataObject::getValue(T& value) const + { + static_assert(IsUniformInputDataType(), "Unsupported data type!"); + return getValueInternal(value); + } +} + +#endif diff --git a/client/ramses-client-api/include/ramses-client-api/ERotationType.h b/client/ramses-client-api/include/ramses-client-api/ERotationType.h new file mode 100644 index 000000000..003ab3aab --- /dev/null +++ b/client/ramses-client-api/include/ramses-client-api/ERotationType.h @@ -0,0 +1,48 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +namespace ramses +{ + /** + * @ingroup CoreAPI + * Specifies the rotation convention used in calculation of transforms in a right-handed coordinate system. The + * order of the letters in each enum value represents the order in extrinsic notation. Extrinsic convention means + * a rotation around any ordered set of axes ABC is done in the specified order around the original world axes which + * are fixed. So the object gets rotated around world original axis A, then the result is rotated around world original + * axis B, then the result of both is rotated around world original axis C. + * This is opposite to intrinsic notation, where each axis of rotation gets affected by the rotations applied to preceding + * axes in the rotation order, i.e., the rotation axes get rotated with objects' rotation around every axis, e.g., rotation + * around axes ABC would result in rotation around world original axis A, then rotation around axis B from the object's + * perspective according to its orientation from the applied rotation around axis A, then rotation around axis C from the object's perspective + * according to its orientation as result of applied rotations axes around A and B. + * + * Extrinsic rotation conventions also mean that the order of axes specified is the inverse order of applying + * rotation matrix multiplication. For example Euler_ZYX means the rotation matrix will be of the shape Rx * Ry * Rz * v + * where v is a vector or another matrix and R are right-handed rotation matrices which rotate around an + * axis specified by the rotation order. Check the specific enum documentation for the exact rotation order + * in terms of 'which rotation is applied first'. + */ + enum class ERotationType + { + Euler_XYZ, ///< rotates around X then Y then Z axis in world coordinate system + Euler_XZY, ///< rotates around X then Z then Y axis in world coordinate system + Euler_YXZ, ///< rotates around Y then X then Z axis in world coordinate system + Euler_YZX, ///< rotates around Y then Z then X axis in world coordinate system + Euler_ZXY, ///< rotates around Z then X then Y axis in world coordinate system + Euler_ZYX, ///< rotates around Z then Y then X axis in world coordinate system + Euler_XYX, ///< rotates around X then Y then X axis in world coordinate system + Euler_XZX, ///< rotates around X then Z then X axis in world coordinate system + Euler_YXY, ///< rotates around Y then X then Y axis in world coordinate system + Euler_YZY, ///< rotates around Y then Z then Y axis in world coordinate system + Euler_ZXZ, ///< rotates around Z then X then Z axis in world coordinate system + Euler_ZYZ, ///< rotates around Z then Y then Z axis in world coordinate system + Quaternion, ///< rotation is defined by a normalized quaternion + }; +} diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/EScenePublicationMode.h b/client/ramses-client-api/include/ramses-client-api/EScenePublicationMode.h similarity index 87% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/EScenePublicationMode.h rename to client/ramses-client-api/include/ramses-client-api/EScenePublicationMode.h index dffe3cfbc..4905ef137 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/EScenePublicationMode.h +++ b/client/ramses-client-api/include/ramses-client-api/EScenePublicationMode.h @@ -12,15 +12,16 @@ namespace ramses { /** + * @ingroup CoreAPI * Specifies the mode of scene publication. * * When using localOnly publication it is possible to render the scene locally without calling * #ramses::RamsesFramework::connect. */ - enum EScenePublicationMode + enum class EScenePublicationMode { - EScenePublicationMode_LocalAndRemote = 0, - EScenePublicationMode_LocalOnly + LocalAndRemote, + LocalOnly }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/EVisibilityMode.h b/client/ramses-client-api/include/ramses-client-api/EVisibilityMode.h similarity index 95% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/EVisibilityMode.h rename to client/ramses-client-api/include/ramses-client-api/EVisibilityMode.h index 6b6b25f00..3026848eb 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/EVisibilityMode.h +++ b/client/ramses-client-api/include/ramses-client-api/EVisibilityMode.h @@ -9,11 +9,12 @@ #ifndef RAMSES_EVISIBILITYMODE_H #define RAMSES_EVISIBILITYMODE_H -#include +#include namespace ramses { /** + * @ingroup CoreAPI * Specifies the mode of node visibility. */ diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/Effect.h b/client/ramses-client-api/include/ramses-client-api/Effect.h similarity index 69% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/Effect.h rename to client/ramses-client-api/include/ramses-client-api/Effect.h index 551a6b9a1..0c51fb88c 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/Effect.h +++ b/client/ramses-client-api/include/ramses-client-api/Effect.h @@ -12,7 +12,9 @@ #include "ramses-framework-api/RamsesFrameworkTypes.h" #include "ramses-client-api/Resource.h" #include "ramses-client-api/EffectInputSemantic.h" -#include "ramses-client-api/AppearanceEnums.h" +#include "ramses-framework-api/AppearanceEnums.h" + +#include namespace ramses { @@ -20,25 +22,25 @@ namespace ramses class AttributeInput; /** + * @ingroup CoreAPI * @brief An effect describes how an object will be rendered to the screen. */ - class RAMSES_API Effect : public Resource + class Effect : public Resource { public: - /** * @brief Gets number of uniform inputs. * * @return Number of uniform inputs */ - uint32_t getUniformInputCount() const; + [[nodiscard]] RAMSES_API size_t getUniformInputCount() const; /** * @brief Gets number of attribute inputs. * * @return Number of attribute inputs */ - uint32_t getAttributeInputCount() const; + [[nodiscard]] RAMSES_API size_t getAttributeInputCount() const; /** * @brief Gets uniform input at given index. @@ -48,7 +50,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t getUniformInput(uint32_t index, UniformInput& uniformInput) const; + RAMSES_API status_t getUniformInput(size_t index, UniformInput& uniformInput) const; /** * @brief Gets attribute input at given index. @@ -58,7 +60,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t getAttributeInput(uint32_t index, AttributeInput& attributeInput) const; + RAMSES_API status_t getAttributeInput(size_t index, AttributeInput& attributeInput) const; /** * @brief Finds uniform input by input name. @@ -68,7 +70,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t findUniformInput(const char* inputName, UniformInput& uniformInput) const; + RAMSES_API status_t findUniformInput(std::string_view inputName, UniformInput& uniformInput) const; /** * @brief Finds attribute input by input name. @@ -78,7 +80,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t findAttributeInput(const char* inputName, AttributeInput& attributeInput) const; + RAMSES_API status_t findAttributeInput(std::string_view inputName, AttributeInput& attributeInput) const; /** * @brief Finds uniform input that represents a semantic input (if existing). @@ -88,7 +90,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t findUniformInput(EEffectUniformSemantic uniformSemantic, UniformInput& uniformInput) const; + RAMSES_API status_t findUniformInput(EEffectUniformSemantic uniformSemantic, UniformInput& uniformInput) const; /** * @brief Finds attribute input that represents a semantic input (if existing). @@ -98,15 +100,14 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t findAttributeInput(EEffectAttributeSemantic attributeSemantic, AttributeInput& attributeInput) const; + RAMSES_API status_t findAttributeInput(EEffectAttributeSemantic attributeSemantic, AttributeInput& attributeInput) const; /** - * @brief Returns whether the \p effect has a geometry shader attached to it. + * @brief Checks if the \p effect has a geometry shader attached to it. * - * @param[in] effect effect to check for existance of geometry shader * @return true if the effect has a geometry shader attached to it, false otherwise */ - static bool hasGeometryShader(const Effect& effect); + [[nodiscard]] RAMSES_API bool hasGeometryShader() const; /** * @brief If the \p effect has a geometry shader attached to it (see #hasGeometryShader) this method @@ -115,50 +116,29 @@ namespace ramses * * See also #ramses::Appearance::setDrawMode(). * - * @param[in] effect effect to check for geometry input type * @param[out] expectedGeometryInputType geometry type expected by the geometry shader of /p effect * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - static status_t getGeometryShaderInputType(const Effect& effect, EDrawMode& expectedGeometryInputType); + RAMSES_API status_t getGeometryShaderInputType(EDrawMode& expectedGeometryInputType) const; /** * @brief Stores internal data for implementation specifics of Effect. */ - class EffectImpl& impl; + class EffectImpl& m_impl; protected: /** * @brief Scene is the convenience library for application developers */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor of Effect * - * @param[in] pimpl Internal data for implementation specifics of Effect (sink - instance becomes owner) - */ - explicit Effect(EffectImpl& pimpl); - - /** - * @brief Copy constructor of Effect - * - * @param[in] other Other instance of Effect class - */ - Effect(const Effect& other); - - /** - * @brief Assignment operator of Effect. - * - * @param[in] other Other instance of Effect class - * @return This instance after assignment - */ - Effect& operator=(const Effect& other); - - /** - * @brief Destructor of the Effect + * @param[in] impl Internal data for implementation specifics of Effect (sink - instance becomes owner) */ - virtual ~Effect(); + explicit Effect(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/EffectDescription.h b/client/ramses-client-api/include/ramses-client-api/EffectDescription.h similarity index 69% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/EffectDescription.h rename to client/ramses-client-api/include/ramses-client-api/EffectDescription.h index deff73e11..ee85ffe37 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/EffectDescription.h +++ b/client/ramses-client-api/include/ramses-client-api/EffectDescription.h @@ -12,18 +12,27 @@ #include "ramses-framework-api/StatusObject.h" #include "ramses-client-api/EffectInputSemantic.h" +#include + namespace ramses { + class EffectDescriptionImpl; + /** + * @ingroup CoreAPI * @brief An effect description holds all necessary information for an effect to be created. */ - class RAMSES_API EffectDescription : public StatusObject + class EffectDescription : public StatusObject { public: /** * @brief Constructor of EffectDescription */ - EffectDescription(); + RAMSES_API EffectDescription(); + /** + * @brief Destructor of EffectDescription + */ + RAMSES_API ~EffectDescription() override; /** * @brief Sets vertex shader source from string. @@ -31,42 +40,42 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setVertexShader(const char* shaderSource); + RAMSES_API status_t setVertexShader(std::string_view shaderSource); /** * @brief Sets fragment shader source from string. * @param[in] shaderSource Fragment shader source code. * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setFragmentShader(const char* shaderSource); + RAMSES_API status_t setFragmentShader(std::string_view shaderSource); /** * @brief Sets geometry shader source from string. * @param[in] shaderSource Geometry shader source code. * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setGeometryShader(const char* shaderSource); + RAMSES_API status_t setGeometryShader(std::string_view shaderSource); /** * @brief Reads and sets vertex shader source from file. * @param[in] shaderSourceFileName File with vertex shader source code. * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setVertexShaderFromFile(const char* shaderSourceFileName); + RAMSES_API status_t setVertexShaderFromFile(std::string_view shaderSourceFileName); /** * @brief Reads and sets fragment shader source from file. * @param[in] shaderSourceFileName File with fragment shader source code. * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setFragmentShaderFromFile(const char* shaderSourceFileName); + RAMSES_API status_t setFragmentShaderFromFile(std::string_view shaderSourceFileName); /** * @brief Reads and sets geometry shader source from file. * @param[in] shaderSourceFileName File with geometry shader source code. * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setGeometryShaderFromFile(const char* shaderSourceFileName); + RAMSES_API status_t setGeometryShaderFromFile(std::string_view shaderSourceFileName); /** * @brief Adds a compiler define. * The define string will be injected as defined into the final shader code. @@ -74,7 +83,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t addCompilerDefine(const char* define); + RAMSES_API status_t addCompilerDefine(std::string_view define); /** * @brief Sets an uniform semantic. @@ -87,7 +96,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setUniformSemantic(const char* inputName, EEffectUniformSemantic semanticType); + RAMSES_API status_t setUniformSemantic(std::string_view inputName, EEffectUniformSemantic semanticType); /** * @brief Sets an attribute semantic. @@ -98,46 +107,66 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setAttributeSemantic(const char* inputName, EEffectAttributeSemantic semanticType); + RAMSES_API status_t setAttributeSemantic(std::string_view inputName, EEffectAttributeSemantic semanticType); /** * @brief Gets vertex shader code. * @return Vertex shader source code. Empty string if not previously set. */ - const char* getVertexShader() const; + [[nodiscard]] RAMSES_API const char* getVertexShader() const; /** * @brief Gets fragment shader code that is currently set. * @return Fragment shader source code. Empty string if not previously set. */ - const char* getFragmentShader() const; + [[nodiscard]] RAMSES_API const char* getFragmentShader() const; /** * @brief Gets geometry shader code that is currently set. * @return Geometry shader source code. Empty string if not previously set. */ - const char* getGeometryShader() const; + [[nodiscard]] RAMSES_API const char* getGeometryShader() const; /** * @brief Gets number of compiler defines. * @return Number of compiler defines that were previously added. */ - uint32_t getNumberOfCompilerDefines() const; + [[nodiscard]] RAMSES_API size_t getNumberOfCompilerDefines() const; /** * @brief Gets compiler define. * @param[in] index Index of define to retrieve. * @return Compiler define for given index. nullptr if not previously set. */ - const char* getCompilerDefine(uint32_t index) const; + [[nodiscard]] RAMSES_API const char* getCompilerDefine(size_t index) const; /** - * @brief Stores internal data for implementation specifics of EffectDescription. - */ - class EffectDescriptionImpl& impl; + * @brief Copy constructor + * @param other source to copy from + */ + RAMSES_API EffectDescription(const EffectDescription& other); - protected: /** - * @brief RamsesClientImpl needs access to internals of EffectDescription. + * @brief Move constructor + * @param other source to move from + */ + RAMSES_API EffectDescription(EffectDescription&& other) noexcept; + + /** + * @brief Copy assignment + * @param other source to copy from + * @return this instance + */ + RAMSES_API EffectDescription& operator=(const EffectDescription& other); + + /** + * @brief Move assignment + * @param other source to move from + * @return this instance + */ + RAMSES_API EffectDescription& operator=(EffectDescription&& other) noexcept; + + /** + * @brief Stores internal data for implementation specifics of EffectDescription. */ - friend class RamsesClientImpl; + std::reference_wrapper m_impl; }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/EffectInput.h b/client/ramses-client-api/include/ramses-client-api/EffectInput.h similarity index 69% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/EffectInput.h rename to client/ramses-client-api/include/ramses-client-api/EffectInput.h index 3b91f75a1..576ef7bec 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/EffectInput.h +++ b/client/ramses-client-api/include/ramses-client-api/EffectInput.h @@ -10,38 +10,45 @@ #define RAMSES_EFFECTINPUT_H #include "ramses-framework-api/StatusObject.h" +#include "ramses-framework-api/EDataType.h" +#include namespace ramses { + class EffectInputImpl; + /** + * @ingroup CoreAPI * @brief The EffectInput is a description of an effect input */ - class RAMSES_API EffectInput : public StatusObject + class EffectInput : public StatusObject { public: /** - * Stores internal data for implementation specifics of EffectInput. + * @brief Returns the name of the effect input. + * + * @return Name of the effect input */ - class EffectInputImpl& impl; + [[nodiscard]] RAMSES_API const char* getName() const; /** - * @brief Destructor of EffectInput + * @brief Returns the state of the EffectInput object. + * + * @return Returns true if this EffectInput object is initialized and refers to an existing effect input */ - virtual ~EffectInput(); + [[nodiscard]] RAMSES_API bool isValid() const; /** - * @brief Returns the name of the effect input. + * @brief Returns the effect input data type. * - * @return Name of the effect input + * @return Effect input data type if #isValid, std::nullopt otherwise */ - const char* getName() const; + [[nodiscard]] RAMSES_API std::optional getDataType() const; /** - * @brief Returns the state of the EffectInput object. - * - * @return Returns true if this EffectInput object is initialized and refers to an existing effect input + * Stores internal data for implementation specifics of EffectInput. */ - bool isValid() const; + std::reference_wrapper m_impl; protected: /** @@ -49,7 +56,7 @@ namespace ramses * * @param[in] effectInputImpl Internal data for implementation specifics of EffectInput (sink - instance becomes owner) */ - explicit EffectInput(EffectInputImpl& effectInputImpl); + explicit EffectInput(std::unique_ptr effectInputImpl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/EffectInputSemantic.h b/client/ramses-client-api/include/ramses-client-api/EffectInputSemantic.h similarity index 98% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/EffectInputSemantic.h rename to client/ramses-client-api/include/ramses-client-api/EffectInputSemantic.h index 41d93ba4d..3fac5ab34 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/EffectInputSemantic.h +++ b/client/ramses-client-api/include/ramses-client-api/EffectInputSemantic.h @@ -12,6 +12,7 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief Effect uniform semantic type */ enum class EEffectUniformSemantic @@ -36,6 +37,7 @@ namespace ramses }; /** + * @ingroup CoreAPI * @brief Effect attribute semantic type */ enum class EEffectAttributeSemantic diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/GeometryBinding.h b/client/ramses-client-api/include/ramses-client-api/GeometryBinding.h similarity index 74% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/GeometryBinding.h rename to client/ramses-client-api/include/ramses-client-api/GeometryBinding.h index 8f56f994f..d22a99544 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/GeometryBinding.h +++ b/client/ramses-client-api/include/ramses-client-api/GeometryBinding.h @@ -19,12 +19,12 @@ namespace ramses class Effect; /** + * @ingroup CoreAPI * @brief A geometry binding together with an appearance describe how an object will be rendered to the screen. */ - class RAMSES_API GeometryBinding : public SceneObject + class GeometryBinding : public SceneObject { public: - /** * @brief Assign a data array with data type UInt16 or UInt32 to be used when accessing vertex data. * @@ -34,7 +34,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setIndices(const ArrayResource& indicesResource); + RAMSES_API status_t setIndices(const ArrayResource& indicesResource); /** * @brief Assign indices (using index data buffer) to be used when accessing vertex data. @@ -45,7 +45,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setIndices(const ArrayBuffer& arrayBuffer); + RAMSES_API status_t setIndices(const ArrayBuffer& arrayBuffer); /** * @brief Assign a data array resource to a given effect attribute input. @@ -56,7 +56,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setInputBuffer(const AttributeInput& attributeInput, const ArrayResource& arrayResource, uint32_t instancingDivisor = 0); + RAMSES_API status_t setInputBuffer(const AttributeInput& attributeInput, const ArrayResource& arrayResource, uint32_t instancingDivisor = 0); /** * @brief Assign a data array resource to a given effect attribute input with offset and stride. @@ -69,7 +69,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setInputBuffer(const AttributeInput& attributeInput, const ArrayResource& arrayResource, uint16_t offset, uint16_t stride); + RAMSES_API status_t setInputBuffer(const AttributeInput& attributeInput, const ArrayResource& arrayResource, uint16_t offset, uint16_t stride); /** * @brief Assign a vertex attribute buffer to a given effect attribute input. @@ -80,7 +80,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setInputBuffer(const AttributeInput& attributeInput, const ArrayBuffer& arrayBuffer, uint32_t instancingDivisor = 0); + RAMSES_API status_t setInputBuffer(const AttributeInput& attributeInput, const ArrayBuffer& arrayBuffer, uint32_t instancingDivisor = 0); /** * @brief Assign vertex attribute buffer with offset and stride. @@ -93,52 +93,32 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setInputBuffer(const AttributeInput& attributeInput, const ArrayBuffer& arrayBuffer, uint16_t offset, uint16_t stride); + RAMSES_API status_t setInputBuffer(const AttributeInput& attributeInput, const ArrayBuffer& arrayBuffer, uint16_t offset, uint16_t stride); /** * @brief Gets the effect used to create this geometry binding * * @return The effect used to create the geometry binding. */ - const Effect& getEffect() const; + [[nodiscard]] RAMSES_API const Effect& getEffect() const; /** * @brief Stores internal data for implementation specifics of GeometryBinding. */ - class GeometryBindingImpl& impl; + class GeometryBindingImpl& m_impl; protected: /** * @brief Scene is the factory for creating GeometryBinding instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor of GeometryBinding * - * @param[in] pimpl Internal data for implementation specifics of GeometryBinding (sink - instance becomes owner) - */ - explicit GeometryBinding(GeometryBindingImpl& pimpl); - - /** - * @brief Copy constructor of GeometryBinding - * - * @param[in] other Other instance of GeometryBinding class - */ - GeometryBinding(const GeometryBinding& other); - - /** - * @brief Assignment operator of GeometryBinding. - * - * @param[in] other Other instance of GeometryBinding class - * @return This instance after assignment - */ - GeometryBinding& operator=(const GeometryBinding& other); - - /** - * @brief Destructor of the GeometryBinding + * @param[in] impl Internal data for implementation specifics of GeometryBinding (sink - instance becomes owner) */ - virtual ~GeometryBinding(); + explicit GeometryBinding(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/IClientEventHandler.h b/client/ramses-client-api/include/ramses-client-api/IClientEventHandler.h similarity index 96% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/IClientEventHandler.h rename to client/ramses-client-api/include/ramses-client-api/IClientEventHandler.h index 77bc8cd46..015418330 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/IClientEventHandler.h +++ b/client/ramses-client-api/include/ramses-client-api/IClientEventHandler.h @@ -13,12 +13,15 @@ #include "ramses-framework-api/RamsesFrameworkTypes.h" #include "ramses-framework-api/RendererSceneState.h" +#include + namespace ramses { class Scene; class SceneReference; /** + * @ingroup CoreAPI * @brief Provides an interface for handling the result of client events. * Implementation of this interface must be passed to RamsesClient::dispatchEvents * which will in return invoke methods of the interface according to events that occurred since last dispatching. @@ -34,7 +37,7 @@ namespace ramses * * @param filename The filename of the scene file that failed to load. */ - virtual void sceneFileLoadFailed(const char* filename) = 0; + virtual void sceneFileLoadFailed(std::string_view filename) = 0; /** * @brief This method will be called when asynchronous loading of a scene file @@ -45,7 +48,7 @@ namespace ramses * @param filename The filename of the scene file that finished loading. * @param loadedScene Pointer to the newly loaded scene. */ - virtual void sceneFileLoadSucceeded(const char* filename, Scene* loadedScene) = 0; + virtual void sceneFileLoadSucceeded(std::string_view filename, Scene* loadedScene) = 0; /** * @brief This method will be called when state on renderer side of a scene referenced diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/MeshNode.h b/client/ramses-client-api/include/ramses-client-api/MeshNode.h similarity index 76% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/MeshNode.h rename to client/ramses-client-api/include/ramses-client-api/MeshNode.h index 6cead5c91..fbd0fae02 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/MeshNode.h +++ b/client/ramses-client-api/include/ramses-client-api/MeshNode.h @@ -18,13 +18,13 @@ namespace ramses class Effect; /** + * @ingroup CoreAPI * @brief The MeshNode holds all information which is * needed to render an object to the screen. */ - class RAMSES_API MeshNode : public Node + class MeshNode : public Node { public: - /** * @brief Sets the Appearance of the MeshNode. * @@ -32,7 +32,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setAppearance(Appearance& appearance); + RAMSES_API status_t setAppearance(Appearance& appearance); /** * @brief Sets the GeometryBinding of the MeshNode. @@ -45,7 +45,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setGeometryBinding(GeometryBinding& geometry); + RAMSES_API status_t setGeometryBinding(GeometryBinding& geometry); /** * @brief Sets the first index of indices array that will be used for rendering. @@ -54,13 +54,13 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setStartIndex(uint32_t startIndex); + RAMSES_API status_t setStartIndex(uint32_t startIndex); /** * @brief Gets the first index of indices array that will be used for rendering. * @return the first index of indices array that will be used for rendering. */ - uint32_t getStartIndex() const; + [[nodiscard]] RAMSES_API uint32_t getStartIndex() const; /** * @brief Sets the offset of the first vertex to use from each vertex array @@ -70,13 +70,13 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setStartVertex(uint32_t startVertex); + RAMSES_API status_t setStartVertex(uint32_t startVertex); /** * @brief Gets the first vertex of vertex arrays that will be used for rendering. * @return the first index of indices array that will be used for rendering. */ - uint32_t getStartVertex() const; + [[nodiscard]] RAMSES_API uint32_t getStartVertex() const; /** * @brief Sets the number of indices that will be used for rendering. @@ -85,19 +85,19 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setIndexCount(uint32_t indexCount); + RAMSES_API status_t setIndexCount(uint32_t indexCount); /** * @brief Gets the number of indices that will be used for rendering. * @return the number of indices that will be used for rendering. */ - uint32_t getIndexCount() const; + [[nodiscard]] RAMSES_API uint32_t getIndexCount() const; /** * @brief Returns the appearance. * @return The appearance, null on failure or if none is set. */ - const Appearance* getAppearance() const; + [[nodiscard]] RAMSES_API const Appearance* getAppearance() const; /** * @brief Removes the Appearance and GeometryBinding previously set to the MeshNode. @@ -105,25 +105,25 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t removeAppearanceAndGeometry(); + RAMSES_API status_t removeAppearanceAndGeometry(); /** * @brief Returns the appearance. * @return The appearance, null on failure or if none is set. */ - Appearance* getAppearance(); + RAMSES_API Appearance* getAppearance(); /** * @brief Returns the geometry binding. * @return The geometry binding, null on failure or if none is set. */ - const GeometryBinding* getGeometryBinding() const; + [[nodiscard]] RAMSES_API const GeometryBinding* getGeometryBinding() const; /** * @brief Returns the geometry binding. * @return The geometry binding, null on failure or if none is set. */ - GeometryBinding* getGeometryBinding(); + RAMSES_API GeometryBinding* getGeometryBinding(); /** * @brief Sets the number of instances that will be drawn for this @@ -134,52 +134,32 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setInstanceCount(uint32_t instanceCount); + RAMSES_API status_t setInstanceCount(uint32_t instanceCount); /** * @brief Gets the number of instance that will be drawn for this mesh * by the renderer. * @return the number of instances of this mesh drawn on rendering. */ - uint32_t getInstanceCount() const; + [[nodiscard]] RAMSES_API uint32_t getInstanceCount() const; /** * Stores internal data for implementation specifics of MeshNode. */ - class MeshNodeImpl& impl; + class MeshNodeImpl& m_impl; protected: /** * @brief Scene is the factory for creating MeshNode instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for MeshNode. * - * @param[in] pimpl Internal data for implementation specifics of MeshNode (sink - instance becomes owner) - */ - explicit MeshNode(MeshNodeImpl& pimpl); - - /** - * @brief Copy constructor of MeshNode - * - * @param[in] other Other instance of MeshNode class - */ - MeshNode(const MeshNode& other); - - /** - * @brief Assignment operator of MeshNode. - * - * @param[in] other Other instance of MeshNode class - * @return This instance after assignment - */ - MeshNode& operator=(const MeshNode& other); - - /** - * @brief Destructor of the MeshNode + * @param[in] impl Internal data for implementation specifics of MeshNode (sink - instance becomes owner) */ - virtual ~MeshNode(); + explicit MeshNode(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/MipLevelData.h b/client/ramses-client-api/include/ramses-client-api/MipLevelData.h similarity index 85% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/MipLevelData.h rename to client/ramses-client-api/include/ramses-client-api/MipLevelData.h index c7106903d..9cf224c62 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/MipLevelData.h +++ b/client/ramses-client-api/include/ramses-client-api/MipLevelData.h @@ -16,6 +16,7 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief Struct containing information about one mip-map level of a texture. * * NOTE: the texel data is stored according to OpenGL convention (first byte is from the bottom texel row). See docs of glTexImage2D for more info. @@ -25,11 +26,7 @@ namespace ramses /** * @brief Default constructor */ - MipLevelData() - : m_size(0) - , m_data(nullptr) - { - } + MipLevelData() = default; /** * @brief Constructs a MipLevelData. The texel data must be stored according to OpenGL conventions. @@ -46,13 +43,14 @@ namespace ramses } /// The size of mipmap data in bytes - uint32_t m_size; + uint32_t m_size{0}; /// Pointer to raw bytes data of mipmap level - const uint8_t* m_data; + const uint8_t* m_data{nullptr}; }; /** + * @ingroup CoreAPI * @brief Struct containing information about one mip-map level of a cube texture. All faces of the cube * texture must have the same size! */ @@ -61,16 +59,7 @@ namespace ramses /** * @brief Default constructor */ - CubeMipLevelData() - : m_faceDataSize(0) - , m_dataPX(nullptr) - , m_dataNX(nullptr) - , m_dataPY(nullptr) - , m_dataNY(nullptr) - , m_dataPZ(nullptr) - , m_dataNZ(nullptr) - { - } + CubeMipLevelData() = default; /** * @brief Constructs a MipLevelData. The texel data must be stored according to OpenGL conventions. @@ -101,23 +90,21 @@ namespace ramses /// size of mip-level data of all faces in Bytes, /// all faces must have the same data size - uint32_t m_faceDataSize; + uint32_t m_faceDataSize{0}; /// Data for face in positive X direction - const uint8_t* m_dataPX; + const uint8_t* m_dataPX{nullptr}; /// Data for face in negative X direction - const uint8_t* m_dataNX; + const uint8_t* m_dataNX{nullptr}; /// Data for face in positive Y direction - const uint8_t* m_dataPY; + const uint8_t* m_dataPY{nullptr}; /// Data for face in negative Y direction - const uint8_t* m_dataNY; + const uint8_t* m_dataNY{nullptr}; /// Data for face in positive Z direction - const uint8_t* m_dataPZ; + const uint8_t* m_dataPZ{nullptr}; /// Data for face in negative Z direction - const uint8_t* m_dataNZ; + const uint8_t* m_dataNZ{nullptr}; }; - - } #endif diff --git a/client/ramses-client-api/include/ramses-client-api/Node.h b/client/ramses-client-api/include/ramses-client-api/Node.h new file mode 100644 index 000000000..913cd235e --- /dev/null +++ b/client/ramses-client-api/include/ramses-client-api/Node.h @@ -0,0 +1,293 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2014 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#ifndef RAMSES_NODE_H +#define RAMSES_NODE_H + +#include "ramses-client-api/SceneObject.h" +#include "ramses-client-api/EVisibilityMode.h" +#include "ramses-client-api/ERotationType.h" +#include "ramses-framework-api/DataTypes.h" + +namespace ramses +{ + /** + * @ingroup CoreAPI + * @brief The Node is the base class of all nodes and provides + * scene graph functionality which propagates to its children. + */ + class Node : public SceneObject + { + public: + /** + * @brief Returns if node has at least one child Node. + * + * @return true if this Node has at least one child Node, false otherwise. + */ + [[nodiscard]] RAMSES_API bool hasChild() const; + + /** + * @brief Gets the number of child Nodes of this node. + * + * @return Number of child Nodes of this Node. + */ + [[nodiscard]] RAMSES_API size_t getChildCount() const; + + /** + * @brief Gets child node at provided index. + * + * @param[in] index Index of the child Node to be returned. Index range is [0..getChildCount()-1]. + * + * @return Pointer to Node, if child at index exists. + * @return nullptr if child at index does not exist. + */ + [[nodiscard]] RAMSES_API Node* getChild(size_t index); + /** @copydoc getChild(size_t) */ + [[nodiscard]] RAMSES_API const Node* getChild(size_t index) const; + + /** + * @brief Adds child Node to this node. + * + * @param[in] node Node to be set as child for this Node. + * + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t addChild(Node& node); + + /** + * @brief Removes a child Node from this node. + * + * @param[in] node Node to be removed as child from this Node. + * + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t removeChild(Node& node); + + /** + * @brief Removes all child Nodes from this node. + * + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t removeAllChildren(); + + /** + * @brief Returns if Node has a parent Node. + * + * @return true if Node has a parent Node, false otherwise. + */ + [[nodiscard]] RAMSES_API bool hasParent() const; + + /** + * @brief Gets parent Node of this Node. + * + * @return Pointer to parent Node, if parent exists, nullptr otherwise. + */ + [[nodiscard]] RAMSES_API Node* getParent(); + /** @copydoc getParent() */ + [[nodiscard]] RAMSES_API const Node* getParent() const; + + /** + * @brief Sets parent Node for this node. + * + * @param[in] node Node to be set as parent for this Node. + * + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setParent(Node& node); + + /** + * @brief Removes the parent Node from this Node. + * + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t removeParent(); + + /** + * @brief Gets model (world in scene space) matrix computed from the scene graph. + * Performance note: this call will cause computation of a transformation chain, + * however the result is cached therefore subsequent calls will have little to none + * performance overhead as long as topology or transformation in the chain does not change. + * + * @param[out] modelMatrix Will be filled with the model matrix 4x4 column-major + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getModelMatrix(matrix44f& modelMatrix) const; + + /** + * @brief Gets inverse model (world in scene space) matrix computed from the scene graph. + * Performance note: this call will cause computation of a transformation chain, + * however the result is cached therefore subsequent calls will have little to none + * performance overhead as long as topology or transformation in the chain does not change. + * + * @param[out] inverseModelMatrix Will be filled with the inverse model matrix 4x4 column-major + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getInverseModelMatrix(matrix44f& inverseModelMatrix) const; + + /** + * @brief Sets the absolute rotation in all three directions for right-handed rotation using the chosen Euler + * angles rotation convention. If this function is used to set, then only + * #ramses::Node::getRotation(vec3f&)const can be used to get node rotation. + * Will return an error if the given rotationType is not an Euler convention. + * + * @param[in] rotation Euler angles in degrees + * @param[in] rotationType The rotation convention to use for calculation of rotation matrix. Default value set to Euler_XYZ rotation convention. + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setRotation(const vec3f& rotation, ERotationType rotationType = ERotationType::Euler_XYZ); + + /** + * @brief Sets the absolute rotation defined by a quaternion. + * If this function is used to set, then only #ramses::Node::getRotation(quat&)const can be used to get the node rotation. + * + * @param[in] rotation a normalized quaternion + * + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setRotation(const quat& rotation); + + /** + * @brief Returns the current rotation type applied to the node + * The value is modified by #ramses::Node::setRotation(). + * Default rotation type is #ramses::ERotationType::Euler_XYZ + * + * @return rotation type + */ + [[nodiscard]] RAMSES_API ERotationType getRotationType() const; + + /** + * @brief Retrieves the absolute rotation for right-handed rotation in all three directions for the used Euler + * angles rotation convention. This function will return an error if no Euler rotation convention is set + * (check #ramses::Node::getRotationType() before) + * + * Default value is 0 for all components. + * + * @param[out] rotation Euler angles in degrees + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getRotation(vec3f& rotation) const; + + /** + * @brief Retrieves the absolute rotation defined by a quaternion. + * This function will return an error if #ramses::Node::getRotationType() != #ramses::ERotationType::Quaternion + * Default value is an identity quaternion: glm::identity() + * + * @param[out] rotation quaternion + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getRotation(quat& rotation) const; + + /** + * @brief Translates in all three directions with the given values. + * + * @param[in] translation relative translation from origin. + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t translate(const vec3f& translation); + + /** + * @brief Sets the absolute translation the absolute values. + * + * @param[in] translation absolute translation value + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setTranslation(const vec3f& translation); + + /** + * @brief Retrieves the current absolute translation. + * + * @param[out] translation Current translation + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getTranslation(vec3f& translation) const; + + /** + * @brief Scales in all three directions with the given values. + * + * @param[in] scaling The relative scaling factor. + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t scale(const vec3f& scaling); + + /** + * @brief Sets the absolute scale in all three dimensions. + * + * @param[in] scaling scaling factor. + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setScaling(const vec3f& scaling); + + /** + * @brief Retrieves the current absolute scale in all three dimensions. + * + * @param[out] scaling The current scaling factor. + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t getScaling(vec3f& scaling) const; + + /** + * @brief Sets the visibility of the Node. + * Visibility of a node determines if a renderable is rendered or not and if + * its resources are loaded. See \ref EVisibilityMode for more details. + * Those attributes are propagated down to the node's children recursively. A node + * can only be rendered, if none of its parents are Invisible or Off, the node's + * resources are only loaded if none of its parents are Off. + * + * @param[in] mode Visibility mode for this node (default is Visible) + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + RAMSES_API status_t setVisibility(EVisibilityMode mode); + + /** + * @brief Gets the visibility property of the Node. This is just the visibility + * state of this node object, NOT the hierarchically accumulated visibility of its parents. + * + * @return StatusOK for success, otherwise the returned status can be used + * to resolve error message using getStatusMessage(). + */ + [[nodiscard]] RAMSES_API EVisibilityMode getVisibility() const; + + /** + * Stores internal data for implementation specifics of Node. + */ + class NodeImpl& m_impl; + + protected: + /** + * @brief Scene is the factory for creating Node instances. + */ + friend class RamsesObjectRegistry; + + /** + * @brief Constructor for Node. + * + * @param[in] impl Internal data for implementation specifics of Node (sink - instance becomes owner) + */ + explicit Node(std::unique_ptr impl); + }; +} + +#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/OrthographicCamera.h b/client/ramses-client-api/include/ramses-client-api/OrthographicCamera.h similarity index 74% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/OrthographicCamera.h rename to client/ramses-client-api/include/ramses-client-api/OrthographicCamera.h index 36d2ab7a0..9705f7d3f 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/OrthographicCamera.h +++ b/client/ramses-client-api/include/ramses-client-api/OrthographicCamera.h @@ -14,27 +14,25 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief The OrthographicCamera is a local camera which defines an orthographic view into the scene. * @details A valid camera for rendering must have viewport and frustum set, see #ramses::Camera * for ways to set these parameters. */ - class RAMSES_API OrthographicCamera : public Camera + class OrthographicCamera : public Camera { protected: /** * @brief Scene is the factory for creating OrthographicCamera instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for OrthographicCamera. * - * @param[in] pimpl Internal data for implementation specifics of OrthographicCamera (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of OrthographicCamera (sink - instance becomes owner) */ - explicit OrthographicCamera(CameraNodeImpl& pimpl); - - /** Protected trivial destructor to avoid deleting by user*/ - virtual ~OrthographicCamera(); + explicit OrthographicCamera(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/PerspectiveCamera.h b/client/ramses-client-api/include/ramses-client-api/PerspectiveCamera.h similarity index 87% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/PerspectiveCamera.h rename to client/ramses-client-api/include/ramses-client-api/PerspectiveCamera.h index ee3ad0c48..2ee0f7783 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/PerspectiveCamera.h +++ b/client/ramses-client-api/include/ramses-client-api/PerspectiveCamera.h @@ -14,12 +14,13 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief The #PerspectiveCamera is a local camera which defines a perspective view into the scene. * @details A valid camera for rendering must have viewport and frustum set. * Frustum planes can be set using #ramses::Camera::setFrustum or #ramses::PerspectiveCamera::setFrustum, * depending if input is concrete frustum planes or field of view and aspect ratio. */ - class RAMSES_API PerspectiveCamera : public Camera + class PerspectiveCamera : public Camera { public: /** @@ -49,7 +50,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setFrustum(float fov, float aspectRatio, float nearPlane, float farPlane); + RAMSES_API status_t setFrustum(float fov, float aspectRatio, float nearPlane, float farPlane); /** * @brief Gets the vertical field of view opening angle in degrees. @@ -58,7 +59,7 @@ namespace ramses * * @return Vertical field of view of this camera. */ - float getVerticalFieldOfView() const; + [[nodiscard]] RAMSES_API float getVerticalFieldOfView() const; /** * @brief Gets the aspect ratio between camera frustum width and height (set via #setFrustum, not viewport). @@ -67,23 +68,20 @@ namespace ramses * * @return Aspect ratio of this camera. */ - float getAspectRatio() const; + [[nodiscard]] RAMSES_API float getAspectRatio() const; protected: /** * @brief Scene is the factory for creating PerspectiveCamera instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for PerspectiveCamera. * - * @param[in] pimpl Internal data for implementation specifics of PerspectiveCamera (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of PerspectiveCamera (sink - instance becomes owner) */ - explicit PerspectiveCamera(CameraNodeImpl& pimpl); - - /** Protected trivial destructor to avoid deleting by user*/ - virtual ~PerspectiveCamera(); + explicit PerspectiveCamera(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/PickableObject.h b/client/ramses-client-api/include/ramses-client-api/PickableObject.h similarity index 87% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/PickableObject.h rename to client/ramses-client-api/include/ramses-client-api/PickableObject.h index 228135648..a28f1e6e7 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/PickableObject.h +++ b/client/ramses-client-api/include/ramses-client-api/PickableObject.h @@ -17,6 +17,7 @@ namespace ramses class Camera; /** + * @ingroup CoreAPI * @brief PickableObject provides a way to specify a 'pickable' area. * @details The purpose of the PickableObject is to enable user to add 'pickable' components to a scene. * When this area is picked (see ramses::RamsesRenderer API) a message is sent to RamsesClient with list of picked objects, @@ -40,7 +41,7 @@ namespace ramses * 4) Assign the camera used to render the car to our PickableObject - the pickable geometry was computed from the car's geometry * and therefore should use same camera for both rendering and picking. */ - class RAMSES_API PickableObject : public Node + class PickableObject : public Node { public: /** @@ -49,7 +50,7 @@ namespace ramses * @return The geometry buffer. * **/ - const ArrayBuffer& getGeometryBuffer() const; + [[nodiscard]] RAMSES_API const ArrayBuffer& getGeometryBuffer() const; /** * @@ -58,7 +59,7 @@ namespace ramses * @return The PickableObject's camera, nullptr if no camera assigned. * **/ - const Camera* getCamera() const; + [[nodiscard]] RAMSES_API const Camera* getCamera() const; /** * @@ -72,7 +73,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). **/ - status_t setCamera(const Camera& camera); + RAMSES_API status_t setCamera(const Camera& camera); /** * @@ -81,7 +82,7 @@ namespace ramses * @return The PickableObject's user ID * **/ - pickableObjectId_t getPickableObjectId() const; + [[nodiscard]] RAMSES_API pickableObjectId_t getPickableObjectId() const; /** * @@ -93,7 +94,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). **/ - status_t setPickableObjectId(pickableObjectId_t id); + RAMSES_API status_t setPickableObjectId(pickableObjectId_t id); /** * @brief Enable/Disable PickableObject @@ -105,37 +106,32 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setEnabled(bool enabled); + RAMSES_API status_t setEnabled(bool enabled); /** * @brief Get the enabled state of the PickableObject * * @return Indicates if the PickableObject is enabled */ - bool isEnabled() const; + [[nodiscard]] RAMSES_API bool isEnabled() const; /** * Stores internal data for implementation specifics of PickableObject. */ - class PickableObjectImpl& impl; + class PickableObjectImpl& m_impl; protected: /** * @brief Scene is the factory for creating PickableObject instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for PickableObject. * - * @param pimpl Internal data for implementation specifics of PickableObject (sink - instance becomes owner) + * @param impl Internal data for implementation specifics of PickableObject (sink - instance becomes owner) */ - explicit PickableObject(PickableObjectImpl& pimpl); - - /** - * @brief Destructor of the PickableObject - */ - virtual ~PickableObject() override; + explicit PickableObject(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/RamsesClient.h b/client/ramses-client-api/include/ramses-client-api/RamsesClient.h similarity index 79% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/RamsesClient.h rename to client/ramses-client-api/include/ramses-client-api/RamsesClient.h index d92f0f755..b569d15d9 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/RamsesClient.h +++ b/client/ramses-client-api/include/ramses-client-api/RamsesClient.h @@ -13,6 +13,8 @@ #include "ramses-client-api/SceneConfig.h" #include "ramses-framework-api/RamsesFramework.h" +#include + /** * ramses namespace * @@ -27,10 +29,11 @@ namespace ramses /** * @brief Entry point of RAMSES client API. + * @ingroup CoreAPI * * The RAMSES client class handles application state and and is a factory for scenes and client resources. */ - class RAMSES_API RamsesClient : public RamsesObject + class RamsesClient : public RamsesObject { public: /** @@ -41,7 +44,7 @@ namespace ramses * @param[in] name The optional name of the created Scene. * @return A pointer to the created Scene, null on failure */ - Scene* createScene(sceneId_t sceneId, const SceneConfig& sceneConfig = SceneConfig(), const char* name = nullptr); + RAMSES_API Scene* createScene(sceneId_t sceneId, const SceneConfig& sceneConfig = SceneConfig(), std::string_view name = {}); /** * @brief Loads scene contents and resources from a file. @@ -56,7 +59,7 @@ namespace ramses * SceneConfig::setPublicationMode(EScenePublicationMode_LocalOnly) before saving. * @return New instance of scene with contents loaded from a file. */ - Scene* loadSceneFromFile(const char* fileName, bool localOnly = false); + RAMSES_API Scene* loadSceneFromFile(std::string_view fileName, bool localOnly = false); /** * @brief Loads scene contents and resources from a memory buffer. @@ -79,7 +82,8 @@ namespace ramses * SceneConfig::setPublicationMode(EScenePublicationMode_LocalOnly) before saving. * @return New instance of scene with contents loaded from a file. */ - Scene* loadSceneFromMemory(std::unique_ptr data, size_t size, bool localOnly = false); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + RAMSES_API Scene* loadSceneFromMemory(std::unique_ptr data, size_t size, bool localOnly = false); /** * @brief Loads scene contents and resources from an open file descriptor. @@ -97,14 +101,14 @@ namespace ramses * at offset. * * @param[in] fd Open and readable filedescriptor. - * @param[in] offset Absolute starting position of ramses scenen within fd. + * @param[in] offset Absolute starting position of ramses scene within fd. * @param[in] length Size of the scene data within fd. * @param[in] localOnly Marks the scene to be loaded as valid for local only * optimization. This has the same effect as calling * SceneConfig::setPublicationMode(EScenePublicationMode_LocalOnly) before saving. * @return New instance of scene with contents loaded from a file. */ - Scene* loadSceneFromFileDescriptor(int fd, size_t offset, size_t length, bool localOnly = false); + RAMSES_API Scene* loadSceneFromFileDescriptor(int fd, size_t offset, size_t length, bool localOnly = false); /** * @brief Loads scene contents and resources from an open file descriptor. @@ -124,14 +128,14 @@ namespace ramses * * @param[in] sceneId The sceneId to use for the returned scene. * @param[in] fd Open and readable filedescriptor. - * @param[in] offset Absolute starting position of ramses scenen within fd. + * @param[in] offset Absolute starting position of ramses scene within fd. * @param[in] length Size of the scene data within fd. * @param[in] localOnly Marks the scene to be loaded as valid for local only * optimization. This has the same effect as calling * SceneConfig::setPublicationMode(EScenePublicationMode_LocalOnly) before saving. * @return New instance of scene with contents loaded from a file. */ - Scene* loadSceneFromFileDescriptor(sceneId_t sceneId, int fd, size_t offset, size_t length, bool localOnly = false); + RAMSES_API Scene* loadSceneFromFileDescriptor(sceneId_t sceneId, int fd, size_t offset, size_t length, bool localOnly = false); /** * @brief Loads scene contents and resources asynchronously from a file. @@ -151,7 +155,27 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t loadSceneFromFileAsync(const char* fileName, bool localOnly = false); + RAMSES_API status_t loadSceneFromFileAsync(std::string_view fileName, bool localOnly = false); + + /** + * Attempts to parse feature level from a Ramses scene file. + * + * @param[in] fileName file path to Ramses scene file + * @param[out] detectedFeatureLevel feature level detected in given file (valid only if parsing successful!) + * @return true if parsing was successful, false otherwise. + */ + RAMSES_API static bool GetFeatureLevelFromFile(std::string_view fileName, EFeatureLevel& detectedFeatureLevel); + + /** + * Attempts to parse feature level from a Ramses scene file. + * + * @param[in] fd open and readable file descriptor + * @param[in] offset absolute starting position of ramses scene within \c fd + * @param[in] length size of the scene data within \c fd + * @param[out] detectedFeatureLevel feature level detected in given file (valid only if parsing successful!) + * @return true if parsing was successful, false otherwise. + */ + RAMSES_API static bool GetFeatureLevelFromFile(int fd, size_t offset, size_t length, EFeatureLevel& detectedFeatureLevel); /** * @brief Destroys the given Scene. The reference of Scene is invalid after this call @@ -160,7 +184,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t destroy(Scene& scene); + RAMSES_API status_t destroy(Scene& scene); /** * @brief Find a scene from the client by name. @@ -168,12 +192,12 @@ namespace ramses * @param[in] name The name of the scene to find. * @return Pointer to the scene if found, nullptr otherwise. */ - const Scene* findSceneByName(const char* name) const; + [[nodiscard]] RAMSES_API const Scene* findSceneByName(std::string_view name) const; /** - * @copydoc findSceneByName(const char*) const + * @copydoc findSceneByName(std::string_view) const **/ - Scene* findSceneByName(const char* name); + RAMSES_API Scene* findSceneByName(std::string_view name); /** * @brief Get a scene from the client by scene id. @@ -181,12 +205,12 @@ namespace ramses * @param[in] sceneId The id of the scene to get. * @return Pointer to the scene if found, nullptr otherwise. */ - const Scene* getScene(sceneId_t sceneId) const; + [[nodiscard]] RAMSES_API const Scene* getScene(sceneId_t sceneId) const; /** * @copydoc getScene(sceneId_t) const **/ - Scene* getScene(sceneId_t sceneId); + RAMSES_API Scene* getScene(sceneId_t sceneId); /** * @brief Some methods on the client provide asynchronous results. These can be synchronously @@ -201,46 +225,23 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t dispatchEvents(IClientEventHandler& clientEventHandler); + RAMSES_API status_t dispatchEvents(IClientEventHandler& clientEventHandler); /** * Stores internal data for implementation specifics of the API. */ - class RamsesClientImpl& impl; + class RamsesClientImpl& m_impl; + private: /** * @brief Constructor of RamsesClient */ - explicit RamsesClient(RamsesClientImpl&); + explicit RamsesClient(std::unique_ptr); - /** - * @brief Deleted default constructor - */ - RamsesClient() = delete; - - /** - * @brief Deleted copy constructor - * @param other unused - */ - RamsesClient(const RamsesClient& other) = delete; - - /** - * @brief Deleted copy assignment - * @param other unused - * @return unused - */ - RamsesClient& operator=(const RamsesClient& other) = delete; - - private: /** * @brief ClientFactory is the factory for RamsesClient */ friend class ClientFactory; - - /** - * @brief Destructor of RamsesClient - */ - virtual ~RamsesClient(); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/RamsesObject.h b/client/ramses-client-api/include/ramses-client-api/RamsesObject.h similarity index 65% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/RamsesObject.h rename to client/ramses-client-api/include/ramses-client-api/RamsesObject.h index 3c5d117b4..c6ba5e5fc 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/RamsesObject.h +++ b/client/ramses-client-api/include/ramses-client-api/RamsesObject.h @@ -12,12 +12,15 @@ #include "ramses-framework-api/StatusObject.h" #include "ramses-client-api/RamsesObjectTypes.h" +#include + namespace ramses { /** + * @ingroup CoreAPI * @brief The RamsesObject is a base class for all client API objects owned by the framework. */ - class RAMSES_API RamsesObject : public StatusObject + class RamsesObject : public StatusObject { public: /** @@ -25,7 +28,7 @@ namespace ramses * * @return Name of the object */ - const char* getName() const; + [[nodiscard]] RAMSES_API const char* getName() const; /** * @brief Changes the name of the object. @@ -34,14 +37,14 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setName(const char* name); + RAMSES_API status_t setName(std::string_view name); /** * @brief Gets type of the object. * * @return Type of the object, see ERamsesObjectType enum for possible values. */ - ERamsesObjectType getType() const; + [[nodiscard]] RAMSES_API ERamsesObjectType getType() const; /** * @brief Checks if the object is of given type. @@ -49,39 +52,22 @@ namespace ramses * @param[in] type Type to check against. * @return True if object is of given type, ie. it can be converted to given type. */ - bool isOfType(ERamsesObjectType type) const; + [[nodiscard]] RAMSES_API bool isOfType(ERamsesObjectType type) const; /** * Stores internal data for implementation specifics of RamsesObject. */ - class RamsesObjectImpl& impl; + class RamsesObjectImpl& m_impl; protected: /** * @brief Constructor for RamsesObject. * - * @param[in] pimpl Internal data for implementation specifics of RamsesObject (sink - instance becomes owner) - */ - explicit RamsesObject(RamsesObjectImpl& pimpl); - - /** - * @brief Destructor of the RamsesObject - */ - virtual ~RamsesObject(); - - private: - /** - * @brief Copy constructor of RamsesObject + * @param[in] impl Internal data for implementation specifics of RamsesObject (sink - instance becomes owner) */ - RamsesObject(const RamsesObject& other); + explicit RamsesObject(std::unique_ptr impl); - /** - * @brief Assignment operator of RamsesObject. - * - * @param[in] other Instance to assign from - * @return This instance after assignment - */ - RamsesObject& operator=(const RamsesObject& other); + friend class RamsesObjectRegistry; }; } diff --git a/client/ramses-client-api/include/ramses-client-api/RamsesObjectTypes.h b/client/ramses-client-api/include/ramses-client-api/RamsesObjectTypes.h new file mode 100644 index 000000000..409b5eea9 --- /dev/null +++ b/client/ramses-client-api/include/ramses-client-api/RamsesObjectTypes.h @@ -0,0 +1,63 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2014 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#ifndef RAMSES_RAMSESOBJECTTYPES_H +#define RAMSES_RAMSESOBJECTTYPES_H + +#include "ramses-framework-api/RamsesFrameworkTypes.h" + +namespace ramses +{ + /** + * @ingroup CoreAPI + * @brief RamsesObject type ID + */ + enum class ERamsesObjectType + { + Invalid, + ClientObject, // base type + RamsesObject, // base type + SceneObject, // base type + Client, + Scene, + Node, // base type + MeshNode, + Camera, // base type + PerspectiveCamera, + OrthographicCamera, + Effect, + Appearance, + GeometryBinding, + PickableObject, + Resource, // base type + Texture2D, + Texture3D, + TextureCube, + ArrayResource, + RenderGroup, + RenderPass, + BlitPass, + TextureSampler, + TextureSamplerMS, + RenderBuffer, + RenderTarget, + ArrayBufferObject, + Texture2DBuffer, + DataObject, + SceneReference, + + TextureSamplerExternal, + // Whenever new type of object is added + // its traits must be registered in RamsesObjectTypeTraits.h using helper macros + // and added to appropriate test type list(s) in RamsesObjectTestTypes.h + // and added a conversion template instantiation in RamsesObjectTypeUtils.cpp + NUMBER_OF_TYPES + }; +} + +#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderBuffer.h b/client/ramses-client-api/include/ramses-client-api/RenderBuffer.h similarity index 75% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/RenderBuffer.h rename to client/ramses-client-api/include/ramses-client-api/RenderBuffer.h index 3852eba12..48a5bfa0b 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderBuffer.h +++ b/client/ramses-client-api/include/ramses-client-api/RenderBuffer.h @@ -15,80 +15,75 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief RenderBuffer can be used with RenderTarget as buffer for writing or with TextureSampler as buffer for reading. * * @details A RenderBuffer can be used by one or more RenderTargets for rendering content into it. * There are two basic types of RenderBuffers - color and depth, depth buffer can have additionally stencil data. * A RenderBuffer can be used by one or more TextureSamplers for sampling data from it in a shader. */ - class RAMSES_API RenderBuffer : public SceneObject + class RenderBuffer : public SceneObject { public: - /** * @brief Returns the width of the RenderBuffer in pixels * * @returns width in pixels */ - uint32_t getWidth() const; + [[nodiscard]] RAMSES_API uint32_t getWidth() const; /** * @brief Returns the height of the RenderBuffer in pixels * * @returns height in pixels */ - uint32_t getHeight() const; + [[nodiscard]] RAMSES_API uint32_t getHeight() const; /** * @brief Returns the type of the RenderBuffer * * @returns RenderBuffer type (color, depth, depth/stencil) */ - ERenderBufferType getBufferType() const; + [[nodiscard]] RAMSES_API ERenderBufferType getBufferType() const; /** * @brief Returns the data format of the RenderBuffer * * @returns RenderBuffer data format */ - ERenderBufferFormat getBufferFormat() const; + [[nodiscard]] RAMSES_API ERenderBufferFormat getBufferFormat() const; /** * @brief Returns the read/write access mode of the RenderBuffer * * @returns RenderBuffer access mode */ - ERenderBufferAccessMode getAccessMode() const; + [[nodiscard]] RAMSES_API ERenderBufferAccessMode getAccessMode() const; /** * @brief Returns the sample count used for MSAA * * @returns RenderBuffer sample count */ - uint32_t getSampleCount() const; + [[nodiscard]] RAMSES_API uint32_t getSampleCount() const; /** * Stores internal data for implementation specifics of RenderBuffer. */ - class RenderBufferImpl& impl; + class RenderBufferImpl& m_impl; protected: /** * @brief Scene is the factory for creating RenderBuffer instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for RenderBuffer. * - * @param[in] pimpl Internal data for implementation specifics of RenderBuffer (sink - instance becomes owner) - */ - explicit RenderBuffer(RenderBufferImpl& pimpl); - - /** - * @brief Destructor of the RenderBuffer + * @param[in] impl Internal data for implementation specifics of RenderBuffer (sink - instance becomes owner) */ - virtual ~RenderBuffer(); + explicit RenderBuffer(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderGroup.h b/client/ramses-client-api/include/ramses-client-api/RenderGroup.h similarity index 83% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/RenderGroup.h rename to client/ramses-client-api/include/ramses-client-api/RenderGroup.h index a798b9e05..fd7fdaf40 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderGroup.h +++ b/client/ramses-client-api/include/ramses-client-api/RenderGroup.h @@ -16,6 +16,7 @@ namespace ramses class MeshNode; /** + * @ingroup CoreAPI * @brief The RenderGroup is a container used to collect renderables which are supposed * to be rendered together. Renderables added to it can be ordered within the RenderGroup * so that they will be rendered in given order. RenderGroup can then be added to a RenderPass @@ -27,7 +28,7 @@ namespace ramses * The order inside a nested RenderGroup is local, i.e. all its renderables/RenderGroups are * rendered before the next renderable/RenderGroup of its parent RenderGroup. */ - class RAMSES_API RenderGroup : public SceneObject + class RenderGroup : public SceneObject { public: /** @@ -41,7 +42,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t addMeshNode(const MeshNode& mesh, int32_t orderWithinGroup = 0); + RAMSES_API status_t addMeshNode(const MeshNode& mesh, int32_t orderWithinGroup = 0); /** * @brief Remove a mesh from this RenderGroup. @@ -50,7 +51,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t removeMeshNode(const MeshNode& mesh); + RAMSES_API status_t removeMeshNode(const MeshNode& mesh); /** * @brief Checks whether a mesh was added to this RenderGroup. @@ -58,7 +59,7 @@ namespace ramses * @param[in] mesh The mesh to query * @return \c true if the mesh is contained in this RenderGroup \c false otherwise */ - bool containsMeshNode(const MeshNode& mesh) const; + [[nodiscard]] RAMSES_API bool containsMeshNode(const MeshNode& mesh) const; /** * @brief Gets a render order of given MeshNode within this RenderGroup. @@ -68,7 +69,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t getMeshNodeOrder(const MeshNode& mesh, int32_t& orderWithinGroup) const; + RAMSES_API status_t getMeshNodeOrder(const MeshNode& mesh, int32_t& orderWithinGroup) const; /** * @brief Add a RenderGroup to this RenderGroup. @@ -81,7 +82,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t addRenderGroup(const RenderGroup& renderGroup, int32_t orderWithinGroup = 0); + RAMSES_API status_t addRenderGroup(const RenderGroup& renderGroup, int32_t orderWithinGroup = 0); /** * @brief Remove a RenderGroup from this RenderGroup. @@ -90,7 +91,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t removeRenderGroup(const RenderGroup& renderGroup); + RAMSES_API status_t removeRenderGroup(const RenderGroup& renderGroup); /** * @brief Checks whether a RenderGroup was added to this RenderGroup. @@ -98,7 +99,7 @@ namespace ramses * @param[in] renderGroup The RenderGroup to query * @return \c true if the RenderGroup is contained in this RenderGroup \c false otherwise */ - bool containsRenderGroup(const RenderGroup& renderGroup) const; + [[nodiscard]] RAMSES_API bool containsRenderGroup(const RenderGroup& renderGroup) const; /** * @brief Gets a render order of given RenderGroup within this RenderGroup. @@ -108,7 +109,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t getRenderGroupOrder(const RenderGroup& renderGroup, int32_t& orderWithinGroup) const; + RAMSES_API status_t getRenderGroupOrder(const RenderGroup& renderGroup, int32_t& orderWithinGroup) const; /** * @brief Will remove all renderables from this RenderGroup. @@ -117,7 +118,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t removeAllRenderables(); + RAMSES_API status_t removeAllRenderables(); /** * @brief Will remove all RenderGroups from this RenderGroup. @@ -126,30 +127,25 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t removeAllRenderGroups(); + RAMSES_API status_t removeAllRenderGroups(); /** * Stores internal data for implementation specifics of RenderGroup. */ - class RenderGroupImpl& impl; + class RenderGroupImpl& m_impl; protected: /** * @brief Scene is the factory for creating RenderGroup instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for RenderGroup. * - * @param[in] pimpl Internal data for implementation specifics of RenderGroup (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of RenderGroup (sink - instance becomes owner) */ - explicit RenderGroup(RenderGroupImpl& pimpl); - - /** - * @brief Destructor of the RenderGroup - */ - virtual ~RenderGroup(); + explicit RenderGroup(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderGroupMeshIterator.h b/client/ramses-client-api/include/ramses-client-api/RenderGroupMeshIterator.h similarity index 79% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/RenderGroupMeshIterator.h rename to client/ramses-client-api/include/ramses-client-api/RenderGroupMeshIterator.h index a81e9df6b..94312c44c 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderGroupMeshIterator.h +++ b/client/ramses-client-api/include/ramses-client-api/RenderGroupMeshIterator.h @@ -10,6 +10,7 @@ #define RAMSES_RENDERGROUPMESHITERATOR_H #include "ramses-framework-api/APIExport.h" +#include namespace ramses { @@ -20,9 +21,10 @@ namespace ramses class IteratorImpl; /** + * @ingroup CoreAPI * @brief The RenderGroupMeshIterator traverses MeshNodes in a RenderGroup. */ - class RAMSES_API RenderGroupMeshIterator + class RenderGroupMeshIterator { public: /** @@ -30,12 +32,12 @@ namespace ramses * * @param[in] renderGroup RenderGroup whose MeshNodes to iterate through **/ - explicit RenderGroupMeshIterator(const RenderGroup& renderGroup); + RAMSES_API explicit RenderGroupMeshIterator(const RenderGroup& renderGroup); /** * @brief Destructor **/ - ~RenderGroupMeshIterator(); + RAMSES_API ~RenderGroupMeshIterator(); /** * @brief Iterate through all MeshNodes. @@ -45,12 +47,10 @@ namespace ramses * * Iterator is invalid and may no longer be used if any objects are added or removed. **/ - const MeshNode* getNext(); + RAMSES_API const MeshNode* getNext(); private: - RenderGroupMeshIterator(const RenderGroupMeshIterator& iterator); - RenderGroupMeshIterator& operator=(const RenderGroupMeshIterator& iterator); - IteratorImpl* impl; + std::unique_ptr> m_impl; }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderPass.h b/client/ramses-client-api/include/ramses-client-api/RenderPass.h similarity index 82% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/RenderPass.h rename to client/ramses-client-api/include/ramses-client-api/RenderPass.h index 0c44df917..7d0486a8e 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderPass.h +++ b/client/ramses-client-api/include/ramses-client-api/RenderPass.h @@ -10,6 +10,7 @@ #define RAMSES_RENDERPASS_H #include "ramses-client-api/SceneObject.h" +#include "ramses-framework-api/DataTypes.h" namespace ramses { @@ -18,6 +19,7 @@ namespace ramses class RenderTarget; /** + * @ingroup CoreAPI * @brief The RenderPass is a container used to collect meshes which are supposed * to be rendered together. * @details A RenderPass has a Camera which is used for all MeshNodes @@ -27,7 +29,7 @@ namespace ramses * which is also shared with BlitPass objects, i.e, RenderPass and BlitPass objects * can all be ordered relative to each other. */ - class RAMSES_API RenderPass : public SceneObject + class RenderPass : public SceneObject { public: /** @@ -36,18 +38,18 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setCamera(const Camera& camera); + RAMSES_API status_t setCamera(const Camera& camera); /** * Get the camera of this RenderPass. * @return The camera or null if Camera has not been set. */ - const Camera* getCamera() const; + [[nodiscard]] RAMSES_API const Camera* getCamera() const; /** * @copydoc getCamera() const */ - Camera* getCamera(); + [[nodiscard]] RAMSES_API Camera* getCamera(); /** * @brief Add a RenderGroup to this RenderPass for rendering. @@ -60,7 +62,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t addRenderGroup(const RenderGroup& renderGroup, int32_t orderWithinPass = 0); + RAMSES_API status_t addRenderGroup(const RenderGroup& renderGroup, int32_t orderWithinPass = 0); /** * @brief Remove a RenderGroup from this RenderPass. @@ -69,7 +71,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t removeRenderGroup(const RenderGroup& renderGroup); + RAMSES_API status_t removeRenderGroup(const RenderGroup& renderGroup); /** * @brief Checks whether a RenderGroup is part of the RenderPass @@ -77,7 +79,7 @@ namespace ramses * @param[in] renderGroup The RenderGroup to look for * @return \c true if the mesh is used in this RenderPass \c false otherwise */ - bool containsRenderGroup(const RenderGroup& renderGroup) const; + [[nodiscard]] RAMSES_API bool containsRenderGroup(const RenderGroup& renderGroup) const; /** * @brief Gets a render order of given RenderGroup within this RenderPass. @@ -87,7 +89,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t getRenderGroupOrder(const RenderGroup& renderGroup, int32_t& orderWithinPass) const; + RAMSES_API status_t getRenderGroupOrder(const RenderGroup& renderGroup, int32_t& orderWithinPass) const; /** * @brief Will make the RenderPass empty. @@ -95,7 +97,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t removeAllRenderGroups(); + RAMSES_API status_t removeAllRenderGroups(); /** * @brief Set the render target for the render pass to render into. @@ -104,14 +106,14 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setRenderTarget(RenderTarget* renderTarget); + RAMSES_API status_t setRenderTarget(RenderTarget* renderTarget); /** * @brief Get the render target of this render pass. * * @return The render target or null if render target has not been set. */ - const RenderTarget* getRenderTarget() const; + [[nodiscard]] RAMSES_API const RenderTarget* getRenderTarget() const; /** * @brief Set the render order for the render pass. @@ -125,32 +127,32 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setRenderOrder(int32_t renderOrder); + RAMSES_API status_t setRenderOrder(int32_t renderOrder); /** * @brief Get the render order of this render pass. * * @return The render order of this render pass. */ - int32_t getRenderOrder() const; + [[nodiscard]] RAMSES_API int32_t getRenderOrder() const; /** - * @brief Set the clear color for the RenderPass (default: [0,0,0,0]) + * @brief Set the clear color for the RenderPass (default: [0,0,0,1]) * @details The clear color will be used to clear a render target assigned to this RenderPass * if clear flag is enabled, see setClearFlag. * - * @param r,g,b,a color channels of clear color + * @param color color channels of clear color (RGBA in 0-1 range) * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setClearColor(float r, float g, float b, float a); + RAMSES_API status_t setClearColor(const vec4f& color); /** * @brief Returns the clear color of the RenderPass * - * @param[out] r,g,b,a color channels of clear color + * @return color channels of clear color */ - void getClearColor(float &r, float &g, float &b, float &a) const; + [[nodiscard]] RAMSES_API vec4f getClearColor() const; /** * @brief Set the clear flags which enable/disable the clearing of the render target assigned to this RenderPass(default: #ramses::EClearFlags_All) @@ -161,14 +163,14 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setClearFlags(uint32_t clearFlags); + RAMSES_API status_t setClearFlags(uint32_t clearFlags); /** * @brief Returns the clear flags of the RenderPass * * @returns clear flags, which is a bitmask of the #ramses::EClearFlags enum */ - uint32_t getClearFlags() const; + [[nodiscard]] RAMSES_API uint32_t getClearFlags() const; /** * @brief Enable/Disable render pass @@ -177,14 +179,14 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setEnabled(bool enable); + RAMSES_API status_t setEnabled(bool enable); /** * @brief Get the enable state of the render pass * * @return Indicates if the render pass is enabled */ - bool isEnabled() const; + [[nodiscard]] RAMSES_API bool isEnabled() const; /** * @brief Set/unset render once flag - rendering of the render pass only once. @@ -204,14 +206,14 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setRenderOnce(bool enable); + RAMSES_API status_t setRenderOnce(bool enable); /** * @brief Get the render once state of the render pass * * @return Indicates if the render pass is to be rendered only once */ - bool isRenderOnce() const; + [[nodiscard]] RAMSES_API bool isRenderOnce() const; /** * @brief Will re-render a render once pass. @@ -225,30 +227,25 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t retriggerRenderOnce(); + RAMSES_API status_t retriggerRenderOnce(); /** * Stores internal data for implementation specifics of RenderPass. */ - class RenderPassImpl& impl; + class RenderPassImpl& m_impl; protected: /** * @brief Scene is the factory for creating RenderPass instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for RenderPass. * - * @param[in] pimpl Internal data for implementation specifics of RenderPass (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of RenderPass (sink - instance becomes owner) */ - explicit RenderPass(RenderPassImpl& pimpl); - - /** - * @brief Destructor of the RenderPass - */ - virtual ~RenderPass(); + explicit RenderPass(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderPassGroupIterator.h b/client/ramses-client-api/include/ramses-client-api/RenderPassGroupIterator.h similarity index 79% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/RenderPassGroupIterator.h rename to client/ramses-client-api/include/ramses-client-api/RenderPassGroupIterator.h index fcc3d08e8..3db8f4c28 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderPassGroupIterator.h +++ b/client/ramses-client-api/include/ramses-client-api/RenderPassGroupIterator.h @@ -10,6 +10,7 @@ #define RAMSES_RENDERPASSGROUPITERATOR_H #include "ramses-framework-api/APIExport.h" +#include namespace ramses { @@ -20,9 +21,10 @@ namespace ramses class IteratorImpl; /** + * @ingroup CoreAPI * @brief The RenderPassGroupIterator traverses RenderGroups in a RenderPass. */ - class RAMSES_API RenderPassGroupIterator + class RenderPassGroupIterator { public: /** @@ -30,12 +32,12 @@ namespace ramses * * @param[in] renderPass RenderPass whose RenderGroups to iterate through **/ - explicit RenderPassGroupIterator(const RenderPass& renderPass); + RAMSES_API explicit RenderPassGroupIterator(const RenderPass& renderPass); /** * @brief Destructor **/ - ~RenderPassGroupIterator(); + RAMSES_API ~RenderPassGroupIterator(); /** * @brief Iterate through all RenderGroups. @@ -45,12 +47,10 @@ namespace ramses * * Iterator is invalid and may no longer be used if any objects are added or removed. **/ - const RenderGroup* getNext(); + RAMSES_API const RenderGroup* getNext(); private: - RenderPassGroupIterator(const RenderPassGroupIterator& iterator); - RenderPassGroupIterator& operator=(const RenderPassGroupIterator& iterator); - IteratorImpl* impl; + std::unique_ptr> m_impl; }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderTarget.h b/client/ramses-client-api/include/ramses-client-api/RenderTarget.h similarity index 73% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/RenderTarget.h rename to client/ramses-client-api/include/ramses-client-api/RenderTarget.h index dbe8badcf..5988ddefa 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderTarget.h +++ b/client/ramses-client-api/include/ramses-client-api/RenderTarget.h @@ -14,9 +14,10 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief The RenderTarget can be used as an output for a RenderPass */ - class RAMSES_API RenderTarget : public SceneObject + class RenderTarget : public SceneObject { public: @@ -25,37 +26,32 @@ namespace ramses * * @returns width in pixels */ - uint32_t getWidth() const; + [[nodiscard]] RAMSES_API uint32_t getWidth() const; /** * @brief Returns the height of the RenderTarget in pixels * * @returns height in pixels */ - uint32_t getHeight() const; + [[nodiscard]] RAMSES_API uint32_t getHeight() const; /** * Stores internal data for implementation specifics of RenderTarget. */ - class RenderTargetImpl& impl; + class RenderTargetImpl& m_impl; protected: /** * @brief Scene is the factory for creating RenderTarget instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for RenderTarget. * - * @param[in] pimpl Internal data for implementation specifics of RenderTarget (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of RenderTarget (sink - instance becomes owner) */ - explicit RenderTarget(RenderTargetImpl& pimpl); - - /** - * @brief Destructor of the RenderTarget - */ - virtual ~RenderTarget(); + explicit RenderTarget(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderTargetDescription.h b/client/ramses-client-api/include/ramses-client-api/RenderTargetDescription.h similarity index 60% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/RenderTargetDescription.h rename to client/ramses-client-api/include/ramses-client-api/RenderTargetDescription.h index e361b81a7..27ee55805 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/RenderTargetDescription.h +++ b/client/ramses-client-api/include/ramses-client-api/RenderTargetDescription.h @@ -14,17 +14,24 @@ namespace ramses { class RenderBuffer; + class RenderTargetDescriptionImpl; /** + * @ingroup CoreAPI * @brief RenderTargetDescription holds all necessary information for a RenderTarget to be created. */ - class RAMSES_API RenderTargetDescription : public StatusObject + class RenderTargetDescription : public StatusObject { public: /** * @brief Constructor of RenderTargetDescription */ - RenderTargetDescription(); + RAMSES_API RenderTargetDescription(); + + /** + * @brief Destructor of RenderTargetDescription + */ + RAMSES_API ~RenderTargetDescription() override; /** * @brief Adds a RenderBuffer to the RenderTargetDescription. @@ -38,12 +45,38 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t addRenderBuffer(const RenderBuffer& renderBuffer); + RAMSES_API status_t addRenderBuffer(const RenderBuffer& renderBuffer); + + /** + * @brief Copy constructor + * @param other source to copy from + */ + RAMSES_API RenderTargetDescription(const RenderTargetDescription& other); + + /** + * @brief Move constructor + * @param other source to move from + */ + RAMSES_API RenderTargetDescription(RenderTargetDescription&& other) noexcept; + + /** + * @brief Copy assignment + * @param other source to copy from + * @return this instance + */ + RAMSES_API RenderTargetDescription& operator=(const RenderTargetDescription& other); + + /** + * @brief Move assignment + * @param other source to move from + * @return this instance + */ + RAMSES_API RenderTargetDescription& operator=(RenderTargetDescription&& other) noexcept; /** * @brief Stores internal data for implementation specifics of RenderTargetDescription. */ - class RenderTargetDescriptionImpl& impl; + std::reference_wrapper m_impl; }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/Resource.h b/client/ramses-client-api/include/ramses-client-api/Resource.h similarity index 66% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/Resource.h rename to client/ramses-client-api/include/ramses-client-api/Resource.h index 40014b6b1..cbbf92372 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/Resource.h +++ b/client/ramses-client-api/include/ramses-client-api/Resource.h @@ -14,39 +14,30 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief The Resource is the base class of all resources, such as arrays and textures. */ - class RAMSES_API Resource : public SceneObject + class Resource : public SceneObject { public: /** * @brief Get resource Id. * @return the Id of the resource. */ - resourceId_t getResourceId() const; + [[nodiscard]] RAMSES_API resourceId_t getResourceId() const; /** * Stores internal data for implementation specifics of Resource. */ - class ResourceImpl& impl; + class ResourceImpl& m_impl; protected: - /** - * @brief RamsesClient is the factory for destroying Resource instances. - */ - friend class RamsesClientImpl; - /** * @brief Constructor for Resource. * - * @param[in] pimpl Internal data for implementation specifics of Resource (sink - instance becomes owner) - */ - explicit Resource(ResourceImpl& pimpl); - - /** - * @brief Destructor for Resource. + * @param[in] impl Internal data for implementation specifics of Resource (sink - instance becomes owner) */ - virtual ~Resource(); + explicit Resource(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/Scene.h b/client/ramses-client-api/include/ramses-client-api/Scene.h similarity index 76% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/Scene.h rename to client/ramses-client-api/include/ramses-client-api/Scene.h index f03797ba4..38cff0a73 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/Scene.h +++ b/client/ramses-client-api/include/ramses-client-api/Scene.h @@ -11,15 +11,15 @@ #include "ramses-client-api/ClientObject.h" #include "ramses-client-api/TextureEnums.h" -#include "ramses-client-api/AnimationSystemEnums.h" #include "ramses-client-api/EScenePublicationMode.h" -#include "ramses-client-api/EDataType.h" #include "ramses-client-api/MipLevelData.h" #include "ramses-client-api/TextureSwizzle.h" - +#include "ramses-framework-api/EDataType.h" +#include "ramses-framework-api/DataTypes.h" #include "ramses-framework-api/RamsesFrameworkTypes.h" #include +#include namespace ramses { @@ -32,8 +32,6 @@ namespace ramses class GeometryBinding; class SceneImpl; class RamsesClientImpl; - class AnimationSystem; - class AnimationSystemRealTime; class RenderGroup; class RenderPass; class RenderBuffer; @@ -46,18 +44,6 @@ namespace ramses class TextureSamplerExternal; class AttributeInput; class DataObject; - class DataFloat; - class DataVector2f; - class DataVector3f; - class DataVector4f; - class DataMatrix22f; - class DataMatrix33f; - class DataMatrix44f; - class DataInt32; - class DataVector2i; - class DataVector3i; - class DataVector4i; - class StreamTexture; class Texture2D; class Texture3D; class TextureCube; @@ -75,11 +61,12 @@ namespace ramses class Resource; /** + * @ingroup CoreAPI * @brief The Scene holds a scene graph. * It is the essential class for distributing * content to the ramses system. */ - class RAMSES_API Scene : public ClientObject + class Scene : public ClientObject { public: /** @@ -97,7 +84,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t publish(EScenePublicationMode publicationMode = EScenePublicationMode_LocalAndRemote); + RAMSES_API status_t publish(EScenePublicationMode publicationMode = EScenePublicationMode::LocalAndRemote); /** * @brief Unpublish the scene from the ramses system @@ -108,7 +95,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t unpublish(); + RAMSES_API status_t unpublish(); /** * @brief Returns whether scene is currently published to the ramses system @@ -116,14 +103,14 @@ namespace ramses * @return true, if scene is currently published. * @return false, if scene is currently not published. */ - bool isPublished() const; + [[nodiscard]] RAMSES_API bool isPublished() const; /** * @brief Returns scene id defined at scene creation time * * @return Scene id. */ - sceneId_t getSceneId() const; + [[nodiscard]] RAMSES_API sceneId_t getSceneId() const; /** * @brief Saves all scene contents to a file. @@ -134,7 +121,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t saveToFile(const char* fileName, bool compress) const; + [[nodiscard]] RAMSES_API status_t saveToFile(std::string_view fileName, bool compress) const; /** * @brief Creates a Perspective Camera in this Scene @@ -142,7 +129,7 @@ namespace ramses * @param[in] name The optional name of the Camera * @return Pointer to the created Camera, null on failure */ - PerspectiveCamera* createPerspectiveCamera(const char* name = nullptr); + RAMSES_API PerspectiveCamera* createPerspectiveCamera(std::string_view name = {}); /** * @brief Creates a Orthographic Camera in this Scene @@ -150,7 +137,7 @@ namespace ramses * @param[in] name The optional name of the Camera * @return Pointer to the created Camera, null on failure */ - OrthographicCamera* createOrthographicCamera(const char* name = nullptr); + RAMSES_API OrthographicCamera* createOrthographicCamera(std::string_view name = {}); /** * @brief Creates a new Appearance. @@ -159,7 +146,7 @@ namespace ramses * @param[in] name The optional name of the created Appearance. * @return A pointer to the created Appearance, null on failure */ - Appearance* createAppearance(const Effect& effect, const char* name = nullptr); + RAMSES_API Appearance* createAppearance(const Effect& effect, std::string_view name = {}); /** * @brief Creates a new GeometryBinding. @@ -168,17 +155,7 @@ namespace ramses * @param[in] name The optional name of the created GeometryBinding. * @return A pointer to the created GeometryBinding, null on failure */ - GeometryBinding* createGeometryBinding(const Effect& effect, const char* name = nullptr); - - /** - * @brief Create a Stream Texture - * - * @param[in] fallbackTexture Texture2D used as a fallback texture. - * @param[in] source Stream source identifier - * @param[in] name The name of the Stream Texture. - * @return A pointer to the created Stream Texture, null on failure. - */ - StreamTexture* createStreamTexture(const Texture2D& fallbackTexture, waylandIviSurfaceId_t source, const char* name = nullptr); + RAMSES_API GeometryBinding* createGeometryBinding(const Effect& effect, std::string_view name = {}); /** * @brief Creates a scene graph node. @@ -193,7 +170,7 @@ namespace ramses * @param[in] name Optional name of the object. * @return Pointer to the created Node, nullptr on failure. **/ - Node* createNode(const char* name = nullptr); + RAMSES_API Node* createNode(std::string_view name = {}); /** * @brief Creates a scene graph MeshNode. @@ -203,7 +180,7 @@ namespace ramses * @param[in] name The optional name of the MeshNode. * @return Pointer to the created MeshNode, null on failure. */ - MeshNode* createMeshNode(const char* name = nullptr); + RAMSES_API MeshNode* createMeshNode(std::string_view name = {}); /** * @brief Destroys a previously created object using this scene @@ -215,7 +192,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t destroy(SceneObject& object); + RAMSES_API status_t destroy(SceneObject& object); /** * @brief Expiration timestamp is a point in time till which the scene is considered to be up-to-date. @@ -239,7 +216,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setExpirationTimestamp(uint64_t ptpExpirationTimestampInMilliseconds); + RAMSES_API status_t setExpirationTimestamp(uint64_t ptpExpirationTimestampInMilliseconds); /** * @brief Commits all changes done to the scene since the last flush or since scene creation. This makes a new @@ -253,7 +230,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t flush(sceneVersionTag_t sceneVersionTag = InvalidSceneVersionTag); + RAMSES_API status_t flush(sceneVersionTag_t sceneVersionTag = InvalidSceneVersionTag); /** * @brief resets the semantic uniform #ramses::EEffectUniformSemantic::TimeMs @@ -264,7 +241,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t resetUniformTimeMs(); + RAMSES_API status_t resetUniformTimeMs(); /** * @brief Gets the current value used for the semantic uniform #ramses::EEffectUniformSemantic::TimeMs @@ -275,7 +252,7 @@ namespace ramses * * @return time in milliseconds, value range is 0 .. std::numeric_limits::max() (~24 days) */ - int32_t getUniformTimeMs() const; + [[nodiscard]] RAMSES_API int32_t getUniformTimeMs() const; /** * @brief Get an object from the scene by name @@ -283,12 +260,12 @@ namespace ramses * @param[in] name The name of the object to get. * @return Pointer to the object if found, nullptr otherwise. */ - const RamsesObject* findObjectByName(const char* name) const; + [[nodiscard]] RAMSES_API const RamsesObject* findObjectByName(std::string_view name) const; /** - * @copydoc findObjectByName(const char*) const + * @copydoc findObjectByName(std::string_view) const **/ - RamsesObject* findObjectByName(const char* name); + RAMSES_API RamsesObject* findObjectByName(std::string_view name); /** * @brief Get an object from the scene by id @@ -296,17 +273,12 @@ namespace ramses * @param[in] id The id of the object to get. * @return Pointer to the object if found, nullptr otherwise. */ - const SceneObject* findObjectById(sceneObjectId_t id) const; + [[nodiscard]] RAMSES_API const SceneObject* findObjectById(sceneObjectId_t id) const; /** * @copydoc findObjectById(sceneObjectId_t id) const **/ - SceneObject* findObjectById(sceneObjectId_t id); - - /** - * Stores internal data for implementation specifics of Scene. - */ - SceneImpl& impl; + RAMSES_API SceneObject* findObjectById(sceneObjectId_t id); /** * @brief Create a RenderGroup instance in the scene. @@ -314,7 +286,7 @@ namespace ramses * @param[in] name The optional name of the created RenderGroup instance. * @return A pointer to the created RenderGroup, null on failure **/ - RenderGroup* createRenderGroup(const char* name = nullptr); + RAMSES_API RenderGroup* createRenderGroup(std::string_view name = {}); /** * @brief Create a render pass in the scene. @@ -322,7 +294,7 @@ namespace ramses * @param[in] name The optional name of the created render pass. * @return A render pass. **/ - RenderPass* createRenderPass(const char* name = nullptr); + RAMSES_API RenderPass* createRenderPass(std::string_view name = {}); /** * @brief Create a blit pass in the scene. @@ -334,7 +306,7 @@ namespace ramses * @param[in] name The optional name of the created blit pass. * @return A pointer to a BlitPass if successful or nullptr on failure. **/ - BlitPass* createBlitPass(const RenderBuffer& sourceRenderBuffer, const RenderBuffer& destinationRenderBuffer, const char* name = nullptr); + RAMSES_API BlitPass* createBlitPass(const RenderBuffer& sourceRenderBuffer, const RenderBuffer& destinationRenderBuffer, std::string_view name = {}); /** * @brief Create a RenderBuffer to be used with RenderTarget for rendering into and TextureSampler for sampling from. @@ -352,7 +324,7 @@ namespace ramses * @param[in] name Optional name of the object. * @return Pointer to the created RenderBuffer, null on failure. **/ - RenderBuffer* createRenderBuffer(uint32_t width, uint32_t height, ERenderBufferType bufferType, ERenderBufferFormat bufferFormat, ERenderBufferAccessMode accessMode, uint32_t sampleCount = 0u, const char* name = nullptr); + RAMSES_API RenderBuffer* createRenderBuffer(uint32_t width, uint32_t height, ERenderBufferType bufferType, ERenderBufferFormat bufferFormat, ERenderBufferAccessMode accessMode, uint32_t sampleCount = 0u, std::string_view name = {}); /** * @brief Create a PickableObject. @@ -371,7 +343,7 @@ namespace ramses * @param[in] name Name of the PickableObject. * @return Pointer to the created PickableObject, nullptr on failure. **/ - PickableObject* createPickableObject(const ArrayBuffer& geometryBuffer, const pickableObjectId_t id, const char* name = nullptr); + RAMSES_API PickableObject* createPickableObject(const ArrayBuffer& geometryBuffer, const pickableObjectId_t id, std::string_view name = {}); /** * @brief Create a render target providing a set of RenderBuffers. @@ -380,7 +352,7 @@ namespace ramses * @param[in] name Optional name of the object. * @return Pointer to the created RenderTarget, null on failure. **/ - RenderTarget* createRenderTarget(const RenderTargetDescription& rtDesc, const char* name = nullptr); + RAMSES_API RenderTarget* createRenderTarget(const RenderTargetDescription& rtDesc, std::string_view name = {}); /** * @brief Creates a texture sampler object. @@ -395,14 +367,14 @@ namespace ramses * @param[in] name Optional name of the object. * @return Pointer to the created TextureSampler, null on failure. */ - TextureSampler* createTextureSampler( + RAMSES_API TextureSampler* createTextureSampler( ETextureAddressMode wrapUMode, ETextureAddressMode wrapVMode, ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, const Texture2D& texture, uint32_t anisotropyLevel = 1, - const char* name = nullptr); + std::string_view name = {}); /** * @brief Creates a texture sampler object. @@ -415,14 +387,14 @@ namespace ramses * @param[in] name Optional name of the object. * @return Pointer to the created TextureSampler, null on failure. */ - TextureSampler* createTextureSampler( + RAMSES_API TextureSampler* createTextureSampler( ETextureAddressMode wrapUMode, ETextureAddressMode wrapVMode, ETextureAddressMode wrapRMode, ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, const Texture3D& texture, - const char* name = nullptr); + std::string_view name = {}); /** * @brief Creates a texture sampler object. @@ -437,14 +409,14 @@ namespace ramses * @param[in] name Optional name of the object. * @return Pointer to the created TextureSampler, null on failure. */ - TextureSampler* createTextureSampler( + RAMSES_API TextureSampler* createTextureSampler( ETextureAddressMode wrapUMode, ETextureAddressMode wrapVMode, ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, const TextureCube& texture, uint32_t anisotropyLevel = 1, - const char* name = nullptr); + std::string_view name = {}); /** * @brief Creates a texture sampler object. @@ -459,14 +431,14 @@ namespace ramses * @param[in] name Optional name of the object. * @return Pointer to the created TextureSampler, null on failure. */ - TextureSampler* createTextureSampler( + RAMSES_API TextureSampler* createTextureSampler( ETextureAddressMode wrapUMode, ETextureAddressMode wrapVMode, ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, const RenderBuffer& renderBuffer, uint32_t anisotropyLevel = 1, - const char* name = nullptr); + std::string_view name = {}); /** * @brief Creates a texture sampler object for mutable texture. @@ -481,32 +453,14 @@ namespace ramses * @param[in] name Optional name of the object. * @return Pointer to the created TextureSampler, null on failure. */ - TextureSampler* createTextureSampler( + RAMSES_API TextureSampler* createTextureSampler( ETextureAddressMode wrapUMode, ETextureAddressMode wrapVMode, ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, const Texture2DBuffer& texture2DBuffer, uint32_t anisotropyLevel = 1, - const char* name = nullptr); - - /** - * @brief Creates a texture sampler object. - * @param[in] wrapUMode texture wrap mode for u axis. - * @param[in] wrapVMode texture wrap mode for v axis. - * @param[in] minSamplingMethod texture min sampling method. - * @param[in] magSamplingMethod texture mag sampling method. Must be set to either Nearest or Linear. - * @param[in] streamTexture StreamTexture to be used with this sampler object. - * @param[in] name Optional name of the object. - * @return Pointer to the created TextureSampler, null on failure. - */ - TextureSampler* createTextureSampler( - ETextureAddressMode wrapUMode, - ETextureAddressMode wrapVMode, - ETextureSamplingMethod minSamplingMethod, - ETextureSamplingMethod magSamplingMethod, - const StreamTexture& streamTexture, - const char* name = nullptr); + std::string_view name = {}); /** * @brief Creates a multisampled texture sampler object. @@ -514,7 +468,7 @@ namespace ramses * @param[in] name Optional name of the object. * @return Pointer to the created #ramses::TextureSamplerMS, null on failure. */ - TextureSamplerMS* createTextureSamplerMS(const RenderBuffer& renderBuffer, const char* name); + RAMSES_API TextureSamplerMS* createTextureSamplerMS(const RenderBuffer& renderBuffer, std::string_view name); /** * @brief Creates a texture sampler object that can sample from external textures. @@ -537,33 +491,33 @@ namespace ramses * @param[in] name Optional name of the object. * @return Pointer to the created TextureSampler, null on failure. */ - TextureSamplerExternal* createTextureSamplerExternal( + RAMSES_API TextureSamplerExternal* createTextureSamplerExternal( ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, - const char* name = nullptr); + std::string_view name = {}); /** - * @brief Create a new ArrayResource. It makes a copy of the given data of a certain type as a resource, an immutable data object. + * @brief Create a new #ramses::ArrayResource. It makes a copy of the given data of a certain type as a resource, an immutable data object. + * Data type must be a type that passes #ramses::IsArrayResourceDataType, i.e. one of #ramses::EDataType. * See #ramses::ArrayResource for more details. - - * @details If an #ramses::ArrayResource object is created with type #ramses::EDataType::ByteBlob then an element + * + * @details If an #ramses::ArrayResource object is created with type #ramses::Byte (#ramses::EDataType::ByteBlob) then an element * is defined as one byte, rather than a logical vertex element. Hence, functions of the class * #ramses::ArrayResource referring to element refer to a single byte within byte array, element size is 1 byte * and number of elements is the same as max size in bytes. * - * @param[in] type The data type of the array elements. * @param[in] numElements The number of elements of the given data type to use for the resource. * @param[in] arrayData Pointer to the data to be used to create the array from. * @param[in] cacheFlag The optional flag sent to the renderer. The value describes how the cache implementation should handle the resource. * @param[in] name The optional name of the ArrayResource. * @return A pointer to the created ArrayResource, null on failure */ + template ArrayResource* createArrayResource( - EDataType type, uint32_t numElements, - const void* arrayData, + const T* arrayData, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache, - const char* name = nullptr); + std::string_view name = {}); /** * @brief Create a new Texture2D. It makes a copy of the given data of a certain type as a resource, an immutable data object. @@ -584,16 +538,16 @@ namespace ramses * @param[in] name The name of the Texture2D. * @return A pointer to the created Texture2D, null on failure. Will fail with data == nullptr and/or width/height == 0. */ - Texture2D* createTexture2D( + RAMSES_API Texture2D* createTexture2D( ETextureFormat format, uint32_t width, uint32_t height, - uint32_t mipMapCount, - const MipLevelData mipLevelData[], + size_t mipMapCount, + const MipLevelData mipLevelData[], // NOLINT(modernize-avoid-c-arrays) bool generateMipChain = false, const TextureSwizzle& swizzle = {}, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache, - const char* name = nullptr); + std::string_view name = {}); /** * @brief Create a new Texture3D. It makes a copy of the given data of a certain type as a resource, an immutable data object. @@ -613,16 +567,16 @@ namespace ramses * @param[in] name The name of the Texture3D. * @return A pointer to the created Texture3D, null on failure. Will fail with data == nullptr and/or width/height/depth == 0. */ - Texture3D* createTexture3D( + RAMSES_API Texture3D* createTexture3D( ETextureFormat format, uint32_t width, uint32_t height, uint32_t depth, - uint32_t mipMapCount, - const MipLevelData mipLevelData[], + size_t mipMapCount, + const MipLevelData mipLevelData[], // NOLINT(modernize-avoid-c-arrays) bool generateMipChain = false, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache, - const char* name = nullptr); + std::string_view name = {}); /** * @brief Create a new Cube Texture. It makes a copy of the given data of a certain type as a resource, an immutable data object. @@ -641,15 +595,15 @@ namespace ramses * @param[in] name The name of the Cube Texture. * @return A pointer to the created Cube Texture, null on failure. Will fail with any face-data == nullptr and/or size == 0. */ - TextureCube* createTextureCube( + RAMSES_API TextureCube* createTextureCube( ETextureFormat format, uint32_t size, - uint32_t mipMapCount, - const CubeMipLevelData mipLevelData[], + size_t mipMapCount, + const CubeMipLevelData mipLevelData[], // NOLINT(modernize-avoid-c-arrays) bool generateMipChain = false, const TextureSwizzle& swizzle = {}, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache, - const char* name = nullptr); + std::string_view name = {}); /** * @brief Create a new Effect by parsing a GLSL shader described by an EffectDescription instance. @@ -661,14 +615,14 @@ namespace ramses * @param[in] name The name of the created Effect. * @return A pointer to the created Effect, null on failure */ - Effect* createEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache, const char* name = nullptr); + RAMSES_API Effect* createEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache, std::string_view name = {}); /** * @brief Get the GLSL error messages that were produced at the creation of the last Effect * * @return A string containing the GLSL error messages of the last effect */ - std::string getLastEffectErrorMessages() const; + [[nodiscard]] RAMSES_API std::string getLastEffectErrorMessages() const; /** * @brief Create a new #ramses::ArrayBuffer. The created object is a mutable buffer object that can be used as index or @@ -683,12 +637,12 @@ namespace ramses * #ramses::ArrayBuffer referring to element refer to a single byte within byte array, element size is 1 byte * and max number of elements is the same as max size in bytes. * - * @param[in] dataType Data type of the array data. + * @param[in] dataType Data type of the array data. See #ramses::GetEDataType to get #ramses::EDataType from static type. * @param[in] maxNumElements The maximum number of data elements this buffer can hold. * @param[in] name The optional name of the created array buffer. * @return A pointer to the created array buffer. */ - ArrayBuffer* createArrayBuffer(EDataType dataType, uint32_t maxNumElements, const char* name = nullptr); + RAMSES_API ArrayBuffer* createArrayBuffer(EDataType dataType, uint32_t maxNumElements, std::string_view name = {}); /** * @brief Create a new Texture2DBuffer. The created object is a mutable buffer object that can be used @@ -706,84 +660,16 @@ namespace ramses * @param[in] name The optional name of the created Texture2DBuffer. * @return A pointer to the created Texture2DBuffer. */ - Texture2DBuffer* createTexture2DBuffer(ETextureFormat textureFormat, uint32_t width, uint32_t height, uint32_t mipLevelCount, const char* name = nullptr); - - /** - * @brief Creates a data object within the scene, which holds a data value of type float. - * @param[in] name optional name of the object. - * @return Pointer to the created DataFloat, null on failure. - */ - DataFloat* createDataFloat(const char* name = nullptr); - - /** - * @brief Creates a data object within the scene, which holds a data value of type Vector2f. - * @param[in] name optional name of the object. - * @return Pointer to the created DataVector2f, null on failure. - */ - DataVector2f* createDataVector2f(const char* name = nullptr); - - /** - * @brief Creates a data object within the scene, which holds a data value of type Vector3f. - * @param[in] name optional name of the object. - * @return Pointer to the created DataVector3f, null on failure. - */ - DataVector3f* createDataVector3f(const char* name = nullptr); - - /** - * @brief Creates a data object within the scene, which holds a data value of type Vector4f. - * @param[in] name optional name of the object. - * @return Pointer to the created DataVector4f, null on failure. - */ - DataVector4f* createDataVector4f(const char* name = nullptr); - - /** - * @brief Creates a data object within the scene, which holds a data value of type Matrix22f. - * @param[in] name optional name of the object. - * @return Pointer to the created Matrix22f, null on failure. - */ - DataMatrix22f* createDataMatrix22f(const char* name = nullptr); + RAMSES_API Texture2DBuffer* createTexture2DBuffer(ETextureFormat textureFormat, uint32_t width, uint32_t height, size_t mipLevelCount, std::string_view name = {}); /** - * @brief Creates a data object within the scene, which holds a data value of type Matrix33f. + * @brief Creates a #ramses::DataObject within the scene, which holds a data value of given type. + * @details For supported data types see #ramses::IsDataObjectDataType. + * @param[in] dataType data type that this #ramses::DataObject will hold. * @param[in] name optional name of the object. - * @return Pointer to the created Matrix33f, null on failure. + * @return Pointer to the created #ramses::DataObject, null on failure. */ - DataMatrix33f* createDataMatrix33f(const char* name = nullptr); - - /** - * @brief Creates a data object within the scene, which holds a data value of type Matrix44f. - * @param[in] name optional name of the object. - * @return Pointer to the created Matrix44f, null on failure. - */ - DataMatrix44f* createDataMatrix44f(const char* name = nullptr); - - /** - * @brief Creates a data object within the scene, which holds a data value of type int32. - * @param[in] name optional name of the object. - * @return Pointer to the created DataInt32, null on failure. - */ - DataInt32* createDataInt32(const char* name = nullptr); - - /** - * @brief Creates a data object within the scene, which holds a data value of type Vector2i. - * @param[in] name optional name of the object. - * @return Pointer to the created DataVector2i, null on failure. - */ - DataVector2i* createDataVector2i(const char* name = nullptr); - - /** - * @brief Creates a data object within the scene, which holds a data value of type Vector3i. - * @param[in] name optional name of the object. - * @return Pointer to the created DataVector3i, null on failure. - */ - DataVector3i* createDataVector3i(const char* name = nullptr); - - /** - * @brief Creates a data object within the scene, which holds a data value of type Vector4i. - * @param[in] name optional name of the object. - * @return Pointer to the created DataVector4i, null on failure. - */ - DataVector4i* createDataVector4i(const char* name = nullptr); + RAMSES_API DataObject* createDataObject(EDataType dataType, std::string_view name = {}); /** * @brief Annotates a Node as a transformation data provider. @@ -794,7 +680,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t createTransformationDataProvider(const Node& node, dataProviderId_t dataId); + RAMSES_API status_t createTransformationDataProvider(const Node& node, dataProviderId_t dataId); /** * @brief Annotates a Node as a transformation data consumer. @@ -805,7 +691,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t createTransformationDataConsumer(const Node& node, dataConsumerId_t dataId); + RAMSES_API status_t createTransformationDataConsumer(const Node& node, dataConsumerId_t dataId); /** * @brief Annotates a DataObject as a data provider. @@ -816,7 +702,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t createDataProvider(const DataObject& dataObject, dataProviderId_t dataId); + RAMSES_API status_t createDataProvider(const DataObject& dataObject, dataProviderId_t dataId); /** * @brief Annotates a DataObject as a data consumer. @@ -827,7 +713,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t createDataConsumer(const DataObject& dataObject, dataConsumerId_t dataId); + RAMSES_API status_t createDataConsumer(const DataObject& dataObject, dataConsumerId_t dataId); /** * @brief Annotates a Texture2D as a content provider. @@ -838,7 +724,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t createTextureProvider(const Texture2D& texture, dataProviderId_t dataId); + RAMSES_API status_t createTextureProvider(const Texture2D& texture, dataProviderId_t dataId); /** * @brief Sets a new texture to an existing provider. @@ -850,7 +736,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t updateTextureProvider(const Texture2D& texture, dataProviderId_t dataId); + RAMSES_API status_t updateTextureProvider(const Texture2D& texture, dataProviderId_t dataId); /** * @brief Annotates a #ramses::TextureSampler as a content consumer. @@ -861,7 +747,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t createTextureConsumer(const TextureSampler& sampler, dataConsumerId_t dataId); + RAMSES_API status_t createTextureConsumer(const TextureSampler& sampler, dataConsumerId_t dataId); /** * @brief Annotates a #ramses::TextureSamplerMS as a content consumer. @@ -872,7 +758,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t createTextureConsumer(const TextureSamplerMS& sampler, dataConsumerId_t dataId); + RAMSES_API status_t createTextureConsumer(const TextureSamplerMS& sampler, dataConsumerId_t dataId); /** * @brief Annotates a #ramses::TextureSamplerExternal as a content consumer. @@ -887,39 +773,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t createTextureConsumer(const TextureSamplerExternal& sampler, dataConsumerId_t dataId); - - /** - * @brief Create a new animation system. The animation system will be - * updated on renderer side after calls to AnimationSystem::setTime(). - * The animation system is not automatically updated on client side. - * If live updates of animated values are needed on client side, provide - * the creation flag EAnimationSystemFlags_ClientSideProcessing. Calls to - * AnimationSystem::setTime() then also update the animation systems - * client side state. - * - * @param[in] flags Optional creation flags for the animation system. - * @param[in] name The optional name of the created animation system. - * @return A reference to the created animation system. - */ - AnimationSystem* createAnimationSystem(uint32_t flags = EAnimationSystemFlags_Default, const char* name = nullptr); - - /** - * @brief Create a new animation system that is designed to work with system - * time. The animation system will be updated automatically every frame on - * renderer side using its system time. The animation system is not - * automatically updated on client side. If live updates of animated values - * are needed on client side, provide the creation flag - * EAnimationSystemFlags_ClientSideProcessing, and make sure to call - * AnimationSystem::updateLocalTime() before accessing any values. Calls - * to AnimationSystem::updateLocalTime() are also mandatory before any - * client side changes to the state of the animation system. - * - * @param[in] flags Optional creation flags for the animation system. - * @param[in] name The optional name of the created animation system. - * @return A reference to the created animation system. - */ - AnimationSystemRealTime* createRealTimeAnimationSystem(uint32_t flags = EAnimationSystemFlags_Default, const char* name = nullptr); + RAMSES_API status_t createTextureConsumer(const TextureSamplerExternal& sampler, dataConsumerId_t dataId); /** * @brief Creates a new SceneReference object. @@ -939,7 +793,7 @@ namespace ramses * @param[in] name The optional name of the created SceneReference. * @return A pointer to the created SceneReference. */ - SceneReference* createSceneReference(sceneId_t referencedScene, const char* name = nullptr); + RAMSES_API SceneReference* createSceneReference(sceneId_t referencedScene, std::string_view name = {}); /** * @brief Tell the RamsesRenderer to link a data provider to a data consumer across two scenes. @@ -972,7 +826,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t linkData(SceneReference* providerReference, dataProviderId_t providerId, SceneReference* consumerReference, dataConsumerId_t consumerId); + RAMSES_API status_t linkData(SceneReference* providerReference, dataProviderId_t providerId, SceneReference* consumerReference, dataConsumerId_t consumerId); /** * @brief Removes an existing link between two scenes (see #ramses::Scene::linkData). @@ -985,14 +839,14 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t unlinkData(SceneReference* consumerReference, dataConsumerId_t consumerId); + RAMSES_API status_t unlinkData(SceneReference* consumerReference, dataConsumerId_t consumerId); /** * @brief Getter for #ramses::RamsesClient this Scene was created from * * @return the parent RamsesCLient */ - RamsesClient& getRamsesClient(); + RAMSES_API RamsesClient& getRamsesClient(); /** * @brief Get a resource which is owned by the scene by id @@ -1000,12 +854,17 @@ namespace ramses * @param[in] id The resource id of the resource to get. * @return Pointer to the resource if found, nullptr otherwise. */ - const Resource* getResource(resourceId_t id) const; + [[nodiscard]] RAMSES_API const Resource* getResource(resourceId_t id) const; /** * @copydoc getResource(resourceId_t id) const **/ - Resource* getResource(resourceId_t id); + RAMSES_API Resource* getResource(resourceId_t id); + + /** + * Stores internal data for implementation specifics of Scene. + */ + SceneImpl& m_impl; protected: /** @@ -1016,30 +875,20 @@ namespace ramses /** * @brief Constructor of the Scene * - * @param[in] pimpl Internal data for implementation specifics of Scene (sink - instance becomes owner) - */ - explicit Scene(SceneImpl& pimpl); - - /** - * @brief Copy constructor of Scene - * - * @param[in] other Other instance of Scene class + * @param[in] impl Internal data for implementation specifics of Scene (sink - instance becomes owner) */ - Scene(const Scene& other); + explicit Scene(std::unique_ptr impl); - /** - * @brief Assignment operator of Scene. - * - * @param[in] other Other instance of Scene class - * @return This instance after assignment - */ - Scene& operator=(const Scene& other); - - /** - * @brief Destructor of the Scene - */ - virtual ~Scene(); + private: + /// Internal implementation of #createArrayResource + template RAMSES_API ArrayResource* createArrayResourceInternal(uint32_t numElements, const T* arrayData, resourceCacheFlag_t cacheFlag, std::string_view name); }; + + template ArrayResource* Scene::createArrayResource(uint32_t numElements, const T* arrayData, resourceCacheFlag_t cacheFlag, std::string_view name) + { + static_assert(IsArrayResourceDataType(), "Unsupported data type!"); + return createArrayResourceInternal(numElements, arrayData, cacheFlag, name); + } } #endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneConfig.h b/client/ramses-client-api/include/ramses-client-api/SceneConfig.h similarity index 61% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/SceneConfig.h rename to client/ramses-client-api/include/ramses-client-api/SceneConfig.h index 93372d915..6105c2d1b 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneConfig.h +++ b/client/ramses-client-api/include/ramses-client-api/SceneConfig.h @@ -15,27 +15,24 @@ namespace ramses { + class SceneConfigImpl; + /** + * @ingroup CoreAPI * @brief The SceneConfig holds a set of parameters to be used when creating a scene. */ - class RAMSES_API SceneConfig : public StatusObject + class SceneConfig : public StatusObject { public: /** * @brief Empty constructor of SceneConfig - has default values */ - SceneConfig(); - - /** - * @brief Copy constructor of SceneConfig - * @param[in] other Other instance of SceneConfig - */ - SceneConfig(const SceneConfig& other); + RAMSES_API SceneConfig(); /** * @brief Destructor of SceneConfig */ - virtual ~SceneConfig(); + RAMSES_API ~SceneConfig() override; /** * @brief Set the publication mode that will be used for this scene. @@ -48,12 +45,38 @@ namespace ramses * @return StatusOK on success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setPublicationMode(EScenePublicationMode publicationMode); + RAMSES_API status_t setPublicationMode(EScenePublicationMode publicationMode); + + /** + * @brief Copy constructor + * @param other source to copy from + */ + RAMSES_API SceneConfig(const SceneConfig& other); + + /** + * @brief Move constructor + * @param other source to move from + */ + RAMSES_API SceneConfig(SceneConfig&& other) noexcept; + + /** + * @brief Copy assignment + * @param other source to copy from + * @return this instance + */ + RAMSES_API SceneConfig& operator=(const SceneConfig& other); + + /** + * @brief Move assignment + * @param other source to move from + * @return this instance + */ + RAMSES_API SceneConfig& operator=(SceneConfig&& other) noexcept; /** * Stores internal data for implementation specifics of SceneConfig. */ - class SceneConfigImpl& impl; + std::reference_wrapper m_impl; }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneGraphIterator.h b/client/ramses-client-api/include/ramses-client-api/SceneGraphIterator.h similarity index 64% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/SceneGraphIterator.h rename to client/ramses-client-api/include/ramses-client-api/SceneGraphIterator.h index 4c48794f0..2f0b5a706 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneGraphIterator.h +++ b/client/ramses-client-api/include/ramses-client-api/SceneGraphIterator.h @@ -11,22 +11,25 @@ #include "ramses-client-api/RamsesObjectTypes.h" #include "ramses-framework-api/APIExport.h" +#include namespace ramses { /// Tree traversal style - enum ETreeTraversalStyle + enum class ETreeTraversalStyle { - ETreeTraversalStyle_DepthFirst = 0, //(with pre-order) - ETreeTraversalStyle_BreadthFirst + DepthFirst, //(with pre-order) + BreadthFirst }; + class SceneGraphIteratorImpl; class Node; /** + * @ingroup CoreAPI * @brief A SceneObjectIterator can iterate through the nodes in the scene graph with the order specified as traversal style */ - class RAMSES_API SceneGraphIterator + class SceneGraphIterator { public: /** @@ -37,12 +40,12 @@ namespace ramses * @param[in] traversalStyle traversal style that should be used * @param[in] objectType Type of objects to iterate through */ - SceneGraphIterator(Node& startNode, ETreeTraversalStyle traversalStyle, ERamsesObjectType objectType = ERamsesObjectType_Node); + RAMSES_API SceneGraphIterator(Node& startNode, ETreeTraversalStyle traversalStyle, ERamsesObjectType objectType = ERamsesObjectType::Node); /** - * @brief Destructor + * @brief Destructor. */ - ~SceneGraphIterator(); + RAMSES_API ~SceneGraphIterator(); /** * @brief Returns the next node while iterating. @@ -51,28 +54,13 @@ namespace ramses * * Iterator is invalid and may no longer be used if any nodes are added or removed. */ - Node* getNext(); + RAMSES_API Node* getNext(); protected: - /** - * @brief Copy constructor of SceneGraphIterator - * - * @param[in] other Other instance of SceneGraphIterator class - */ - SceneGraphIterator(const SceneGraphIterator& other); - - /** - * @brief Assignment operator of SceneGraphIterator. - * - * @param[in] other Other instance of SceneGraphIterator class - * @return This instance after assignment - */ - SceneGraphIterator& operator=(const SceneGraphIterator& other); - /** * @brief Stores internal data for implementation specifics of SceneGraphIterator. */ - class SceneGraphIteratorImpl* impl; + std::unique_ptr m_impl; }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneIterator.h b/client/ramses-client-api/include/ramses-client-api/SceneIterator.h similarity index 82% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/SceneIterator.h rename to client/ramses-client-api/include/ramses-client-api/SceneIterator.h index 7e2e48184..d150e20da 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneIterator.h +++ b/client/ramses-client-api/include/ramses-client-api/SceneIterator.h @@ -18,11 +18,12 @@ namespace ramses class Scene; /** + * @ingroup CoreAPI * @brief The SceneIterator traverses scenes in a RamsesClient. * * It provides a way to traverse all scenes created with a given client. */ - class RAMSES_API SceneIterator + class SceneIterator { public: /** @@ -30,12 +31,12 @@ namespace ramses * * @param[in] client RamsesClient whose scenes to iterate through **/ - explicit SceneIterator(const RamsesClient& client); + RAMSES_API explicit SceneIterator(const RamsesClient& client); /** * Destructor **/ - ~SceneIterator(); + RAMSES_API ~SceneIterator(); /** * @brief Returns the next scene while iterating. @@ -43,12 +44,10 @@ namespace ramses * @return The next scene, null when no more scenes are available. * The iterator is invalid and may not be used after any scenes are added or removed. **/ - Scene* getNext(); + RAMSES_API Scene* getNext(); private: - SceneIterator(const SceneIterator& iterator); - SceneIterator& operator=(const SceneIterator& iterator); - SceneIteratorImpl* impl; + std::unique_ptr m_impl; }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneObject.h b/client/ramses-client-api/include/ramses-client-api/SceneObject.h similarity index 56% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/SceneObject.h rename to client/ramses-client-api/include/ramses-client-api/SceneObject.h index 33c8375c1..e22b43e51 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneObject.h +++ b/client/ramses-client-api/include/ramses-client-api/SceneObject.h @@ -14,61 +14,38 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief The SceneObject is a base class for all client API objects owned by a Scene. */ - class RAMSES_API SceneObject : public ClientObject + class SceneObject : public ClientObject { public: /** * Stores internal data for implementation specifics of SceneObject. */ - class SceneObjectImpl& impl; + class SceneObjectImpl& m_impl; /** * @brief Returns scene object id which is automatically assigned at creation time of object and is unique within scope of one scene. * * @return Scene object id. */ - sceneObjectId_t getSceneObjectId() const; + [[nodiscard]] RAMSES_API sceneObjectId_t getSceneObjectId() const; /** * @brief Returns sceneid to which this object belongs to * * @return Scene id this object belongs to */ - sceneId_t getSceneId() const; + [[nodiscard]] RAMSES_API sceneId_t getSceneId() const; protected: /** * @brief Constructor for SceneObject. * - * @param[in] pimpl Internal data for implementation specifics of SceneObject (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of SceneObject (sink - instance becomes owner) */ - explicit SceneObject(SceneObjectImpl& pimpl); - - /** - * @brief Destructor of the SceneObject - */ - virtual ~SceneObject(); - - /** - * SceneImpl is the factory for creating object instances which derive from SceneObject. - */ - friend class SceneImpl; - - private: - /** - * @brief Copy constructor of SceneObject - */ - SceneObject(const SceneObject& other); - - /** - * @brief Assignment operator of SceneObject. - * - * @param[in] other Instance to assign from - * @return This instance after assignment - */ - SceneObject& operator=(const SceneObject& other); + explicit SceneObject(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneObjectIterator.h b/client/ramses-client-api/include/ramses-client-api/SceneObjectIterator.h similarity index 78% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/SceneObjectIterator.h rename to client/ramses-client-api/include/ramses-client-api/SceneObjectIterator.h index 3ac620314..217d67c09 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneObjectIterator.h +++ b/client/ramses-client-api/include/ramses-client-api/SceneObjectIterator.h @@ -17,11 +17,12 @@ namespace ramses class Scene; /** + * @ingroup CoreAPI * @brief The SceneObjectIterator traverses objects in a Scene. * * It provides a way to traverse all objects owned by a given scene. */ - class RAMSES_API SceneObjectIterator + class SceneObjectIterator { public: /** @@ -30,12 +31,12 @@ namespace ramses * @param[in] scene Scene whose objects to iterate through * @param[in] objectType Optional type of objects to iterate through. **/ - explicit SceneObjectIterator(const Scene& scene, ERamsesObjectType objectType = ERamsesObjectType_RamsesObject); + RAMSES_API explicit SceneObjectIterator(const Scene& scene, ERamsesObjectType objectType = ERamsesObjectType::RamsesObject); /** * @brief Destructor **/ - ~SceneObjectIterator(); + RAMSES_API ~SceneObjectIterator(); /** * @brief Iterate through all objects of given type @@ -43,12 +44,10 @@ namespace ramses * * Iterator is invalid and may no longer be used if any objects are added or removed. **/ - RamsesObject* getNext(); + RAMSES_API RamsesObject* getNext(); private: - SceneObjectIterator(const SceneObjectIterator& iterator); - SceneObjectIterator& operator=(const SceneObjectIterator& iterator); - ObjectIteratorImpl* impl; + std::unique_ptr m_impl; }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneReference.h b/client/ramses-client-api/include/ramses-client-api/SceneReference.h similarity index 90% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/SceneReference.h rename to client/ramses-client-api/include/ramses-client-api/SceneReference.h index 28e3fdbb6..f8ee4978b 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SceneReference.h +++ b/client/ramses-client-api/include/ramses-client-api/SceneReference.h @@ -15,6 +15,7 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief The SceneReference object refers to another ramses scene using its sceneId. * @details The SceneReference object references a scene, which might be otherwise unknown * to this RamsesClient, but is or expected to be known to a RamsesRenderer subscribed to its master scene @@ -33,7 +34,7 @@ namespace ramses * so it is possible to change its master scene regardless of its actual state on renderer side (even if actively rendered) * but this should be done only with extra caution and understanding of the consequences mentioned above. */ - class RAMSES_API SceneReference : public SceneObject + class SceneReference : public SceneObject { public: /** @@ -63,19 +64,19 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t requestState(RendererSceneState requestedState); + RAMSES_API status_t requestState(RendererSceneState requestedState); /** * @brief Get the currently requested state for this scene reference. * @return The state of the reference. */ - RendererSceneState getRequestedState() const; + [[nodiscard]] RAMSES_API RendererSceneState getRequestedState() const; /** * @brief Get the sceneId of the referenced scene. * @return The scene id of the referenced scene */ - sceneId_t getReferencedSceneId() const; + [[nodiscard]] RAMSES_API sceneId_t getReferencedSceneId() const; /** * @brief Request callbacks (#ramses::IClientEventHandler::sceneReferenceFlushed) to be triggered whenever a flush @@ -93,7 +94,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t requestNotificationsForSceneVersionTags(bool flag); + RAMSES_API status_t requestNotificationsForSceneVersionTags(bool flag); /** * @brief Set scene render order @@ -106,30 +107,25 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setRenderOrder(int32_t renderOrder); + RAMSES_API status_t setRenderOrder(int32_t renderOrder); /** * Stores internal data for implementation specifics of SceneReference. */ - class SceneReferenceImpl& impl; + class SceneReferenceImpl& m_impl; protected: /** * @brief Scene is the factory for creating SceneReference instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for SceneReference. * - * @param[in] pimpl Internal data for implementation specifics of SceneReference (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of SceneReference (sink - instance becomes owner) */ - explicit SceneReference(SceneReferenceImpl& pimpl); - - /** - * @brief Destructor of the SceneReference - */ - virtual ~SceneReference() override; + explicit SceneReference(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/Texture2D.h b/client/ramses-client-api/include/ramses-client-api/Texture2D.h similarity index 60% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/Texture2D.h rename to client/ramses-client-api/include/ramses-client-api/Texture2D.h index 805c4ab23..e281ff18e 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/Texture2D.h +++ b/client/ramses-client-api/include/ramses-client-api/Texture2D.h @@ -18,77 +18,57 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief Texture represents a 2-D texture resource. */ - class RAMSES_API Texture2D : public Resource + class Texture2D : public Resource { public: - /** - * Stores internal data for implementation specifics of Texture. - */ - class Texture2DImpl& impl; - /** * @brief Gets texture width * * @return Texture width */ - uint32_t getWidth() const; + [[nodiscard]] RAMSES_API uint32_t getWidth() const; /** * @brief Gets texture height * * @return Texture height */ - uint32_t getHeight() const; + [[nodiscard]] RAMSES_API uint32_t getHeight() const; /** * @brief Gets texture format * * @return Texture format */ - ETextureFormat getTextureFormat() const; + [[nodiscard]] RAMSES_API ETextureFormat getTextureFormat() const; /** * @brief Gets swizzle description * * @return Swizzle Description */ - const TextureSwizzle& getTextureSwizzle() const; - - protected: - /** - * @brief Scene is the factory for creating Texture instances. - */ - friend class SceneImpl; + [[nodiscard]] RAMSES_API const TextureSwizzle& getTextureSwizzle() const; /** - * @brief Constructor of Texture - * - * @param[in] pimpl Internal data for implementation specifics of Texture (sink - instance becomes owner) - */ - explicit Texture2D(Texture2DImpl& pimpl); - - /** - * @brief Destructor of the Texture + * Stores internal data for implementation specifics of Texture. */ - virtual ~Texture2D(); + class Texture2DImpl& m_impl; - private: + protected: /** - * @brief Copy constructor of Texture2D - * - * @param[in] other Other instance of Texture2D class + * @brief Scene is the factory for creating Texture instances. */ - Texture2D(const Texture2D& other); + friend class RamsesObjectRegistry; /** - * @brief Assignment operator of Texture. + * @brief Constructor of Texture * - * @param[in] other Other instance of Texture class - * @return This instance after assignment + * @param[in] impl Internal data for implementation specifics of Texture (sink - instance becomes owner) */ - Texture2D& operator=(const Texture2D& other); + explicit Texture2D(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/Texture2DBuffer.h b/client/ramses-client-api/include/ramses-client-api/Texture2DBuffer.h similarity index 83% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/Texture2DBuffer.h rename to client/ramses-client-api/include/ramses-client-api/Texture2DBuffer.h index e962c0cfa..347c1d9c2 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/Texture2DBuffer.h +++ b/client/ramses-client-api/include/ramses-client-api/Texture2DBuffer.h @@ -15,6 +15,7 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief The Texture2DBuffer is a mutable texture buffer used to hold texture data with the possibility * to perform partial updates. This object _must_ be initialized with data, otherwise the contents of it * are not specified (garbage data or black, depending on driver behavior). @@ -22,10 +23,9 @@ namespace ramses * according to OpenGL specification (each further mipMap level has half the size of the previous * mipMap level). Refer to documentation of glTexStorage2D for more details. */ - class RAMSES_API Texture2DBuffer : public SceneObject + class Texture2DBuffer : public SceneObject { public: - /** * @brief Update a subregion of the data of Texture2DBuffer. The caller is responsible to check that * the data has the correct size, i.e. the size of a texel times the number of texels specified in the @@ -43,7 +43,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t updateData(uint32_t mipLevel, uint32_t offsetX, uint32_t offsetY, uint32_t width, uint32_t height, const void* data); + RAMSES_API status_t updateData(size_t mipLevel, uint32_t offsetX, uint32_t offsetY, uint32_t width, uint32_t height, const void* data); /** * @brief Returns the number of mipmap levels created for the Texture2DBuffer (same as provided in @@ -51,7 +51,7 @@ namespace ramses * * @return number of mipmap levels */ - uint32_t getMipLevelCount() const; + [[nodiscard]] RAMSES_API size_t getMipLevelCount() const; /** * @brief Returns the size of a specific mipmap level in texels @@ -62,7 +62,7 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t getMipLevelSize(uint32_t mipLevel, uint32_t& widthOut, uint32_t& heightOut) const; + RAMSES_API status_t getMipLevelSize(size_t mipLevel, uint32_t& widthOut, uint32_t& heightOut) const; /** * @brief Returns the size of a specific mipmap level in bytes @@ -70,14 +70,14 @@ namespace ramses * @param[in] mipLevel The mipMap level of which the size will be returned * @return Size of data in bytes for given mip level, 0 if mipLevel invalid */ - uint32_t getMipLevelDataSizeInBytes(uint32_t mipLevel) const; + [[nodiscard]] RAMSES_API size_t getMipLevelDataSizeInBytes(size_t mipLevel) const; /** * @brief Returns the texel format provided at creation * * @return The texel format provided at creation */ - ETextureFormat getTexelFormat() const; + [[nodiscard]] RAMSES_API ETextureFormat getTexelFormat() const; /** * @brief Copies the data of a single mip-level into a user-provided buffer. @@ -90,30 +90,25 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t getMipLevelData(uint32_t mipLevel, void* buffer, uint32_t bufferSize) const; + RAMSES_API status_t getMipLevelData(size_t mipLevel, void* buffer, size_t bufferSize) const; /** * Stores internal data for implementation specifics of Texture2DBuffer. */ - class Texture2DBufferImpl& impl; + class Texture2DBufferImpl& m_impl; protected: /** * @brief Scene is the factory for creating Texture2DBuffer instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for Texture2DBuffer. * - * @param[in] pimpl Internal data for implementation specifics of Texture2DBuffer (sink - instance becomes owner) - */ - explicit Texture2DBuffer(Texture2DBufferImpl& pimpl); - - /** - * @brief Destructor of the Texture2DBuffer + * @param[in] impl Internal data for implementation specifics of Texture2DBuffer (sink - instance becomes owner) */ - virtual ~Texture2DBuffer(); + explicit Texture2DBuffer(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/Texture3D.h b/client/ramses-client-api/include/ramses-client-api/Texture3D.h similarity index 60% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/Texture3D.h rename to client/ramses-client-api/include/ramses-client-api/Texture3D.h index 054536605..8d37f562f 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/Texture3D.h +++ b/client/ramses-client-api/include/ramses-client-api/Texture3D.h @@ -16,77 +16,57 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief Texture represents a texture resource */ - class RAMSES_API Texture3D : public Resource + class Texture3D : public Resource { public: - /** - * Stores internal data for implementation specifics of Texture. - */ - class Texture3DImpl& impl; - /** * @brief Gets texture width * * @return Texture width */ - uint32_t getWidth() const; + [[nodiscard]] RAMSES_API uint32_t getWidth() const; /** * @brief Gets texture height * * @return Texture height */ - uint32_t getHeight() const; + [[nodiscard]] RAMSES_API uint32_t getHeight() const; /** * @brief Gets texture depth * * @return Texture depth */ - uint32_t getDepth() const; + [[nodiscard]] RAMSES_API uint32_t getDepth() const; /** * @brief Gets texture format * * @return Texture format */ - ETextureFormat getTextureFormat() const; - - protected: - /** - * @brief Scene is the factory for creating Texture instances. - */ - friend class SceneImpl; + [[nodiscard]] RAMSES_API ETextureFormat getTextureFormat() const; /** - * @brief Constructor of Texture - * - * @param[in] pimpl Internal data for implementation specifics of Texture (sink - instance becomes owner) - */ - explicit Texture3D(Texture3DImpl& pimpl); - - /** - * @brief Destructor of the Texture + * Stores internal data for implementation specifics of Texture. */ - virtual ~Texture3D(); + class Texture3DImpl& m_impl; - private: + protected: /** - * @brief Copy constructor of Texture - * - * @param[in] other Other instance of Texture + * @brief Scene is the factory for creating Texture instances. */ - Texture3D(const Texture3D& other); + friend class RamsesObjectRegistry; /** - * @brief Assignment operator of Texture. + * @brief Constructor of Texture * - * @param[in] other Other instance of Texture class - * @return This instance after assignment + * @param[in] impl Internal data for implementation specifics of Texture (sink - instance becomes owner) */ - Texture3D& operator=(const Texture3D& other); + explicit Texture3D(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureCube.h b/client/ramses-client-api/include/ramses-client-api/TextureCube.h similarity index 73% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/TextureCube.h rename to client/ramses-client-api/include/ramses-client-api/TextureCube.h index b0f30b481..79dc2109f 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureCube.h +++ b/client/ramses-client-api/include/ramses-client-api/TextureCube.h @@ -19,56 +19,50 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief TextureCube stores pixel data with 6 equally sized quadratic faces. */ - - class RAMSES_API TextureCube : public Resource + class TextureCube : public Resource { public: - - /** - * Stores internal data for implementation specifics of TextureCube. - */ - class TextureCubeImpl& impl; - /** * @brief Gets cube texture edge length * * @return Texture cube edge length */ - uint32_t getSize() const; + [[nodiscard]] RAMSES_API uint32_t getSize() const; /** * @brief Gets texture format * * @return Texture format */ - ETextureFormat getTextureFormat() const; + [[nodiscard]] RAMSES_API ETextureFormat getTextureFormat() const; /** * @brief Gets swizzle description * * @return Swizzle Description */ - const TextureSwizzle& getTextureSwizzle() const; + [[nodiscard]] RAMSES_API const TextureSwizzle& getTextureSwizzle() const; + + /** + * Stores internal data for implementation specifics of TextureCube. + */ + class TextureCubeImpl& m_impl; protected: /** * @brief Scene is the factory for creating TextureCube instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor of TextureCube * - * @param[in] pimpl Internal data for implementation specifics of TextureCube (sink - instance becomes owner) - */ - explicit TextureCube(TextureCubeImpl& pimpl); - - /** - * @brief Destructor of the TextureCube + * @param[in] impl Internal data for implementation specifics of TextureCube (sink - instance becomes owner) */ - virtual ~TextureCube(); + explicit TextureCube(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureEnums.h b/client/ramses-client-api/include/ramses-client-api/TextureEnums.h similarity index 70% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/TextureEnums.h rename to client/ramses-client-api/include/ramses-client-api/TextureEnums.h index 3ca6b7d9c..e9d68a566 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureEnums.h +++ b/client/ramses-client-api/include/ramses-client-api/TextureEnums.h @@ -14,31 +14,34 @@ namespace ramses { + /** + * @addtogroup CoreAPI + * @{ + */ + /// Texture sampling method - enum ETextureSamplingMethod + enum class ETextureSamplingMethod { - ETextureSamplingMethod_Nearest = 0, - ETextureSamplingMethod_Linear, - ETextureSamplingMethod_Nearest_MipMapNearest, - ETextureSamplingMethod_Nearest_MipMapLinear, - ETextureSamplingMethod_Linear_MipMapNearest, - ETextureSamplingMethod_Linear_MipMapLinear, - ETextureSamplingMethod_NUMBER_OF_ELEMENTS + Nearest, + Linear, + Nearest_MipMapNearest, + Nearest_MipMapLinear, + Linear_MipMapNearest, + Linear_MipMapLinear, }; /// Texture address mode - enum ETextureAddressMode + enum class ETextureAddressMode { - ETextureAddressMode_Clamp = 0, - ETextureAddressMode_Repeat, - ETextureAddressMode_Mirror, - ETextureAddressMode_NUMBER_OF_ELEMENTS + Clamp, + Repeat, + Mirror, }; /// Texture data format enum class ETextureFormat { - Invalid = 0, + Invalid, R8, @@ -99,57 +102,54 @@ namespace ramses ASTC_SRGBA_10x10, ASTC_SRGBA_12x10, ASTC_SRGBA_12x12, - - NUMBER_OF_ELEMENTS }; /// Cube texture face identifier - enum ETextureCubeFace + enum class ETextureCubeFace { - ETextureCubeFace_PositiveX = 0, - ETextureCubeFace_NegativeX, - ETextureCubeFace_PositiveY, - ETextureCubeFace_NegativeY, - ETextureCubeFace_PositiveZ, - ETextureCubeFace_NegativeZ, - ETextureCubeFace_NUMBER_OF_ELEMENTS + PositiveX, + NegativeX, + PositiveY, + NegativeY, + PositiveZ, + NegativeZ, }; /// Enum for type of depth buffer created within a RenderTarget - enum ERenderTargetDepthBufferType + enum class ERenderTargetDepthBufferType { - ERenderTargetDepthBufferType_None = 0, - ERenderTargetDepthBufferType_Depth, - ERenderTargetDepthBufferType_DepthStencil + None, + Depth, + DepthStencil }; /// Enum for type of a RenderBuffer - enum ERenderBufferType + enum class ERenderBufferType { - ERenderBufferType_Color = 0, - ERenderBufferType_Depth, - ERenderBufferType_DepthStencil + Color, + Depth, + DepthStencil }; /// Enum for format of a RenderBuffer - enum ERenderBufferFormat + enum class ERenderBufferFormat { - ERenderBufferFormat_RGBA4 = 0, - ERenderBufferFormat_R8, - ERenderBufferFormat_RG8, - ERenderBufferFormat_RGB8, - ERenderBufferFormat_RGBA8, - ERenderBufferFormat_R16F, - ERenderBufferFormat_R32F, - ERenderBufferFormat_RG16F, - ERenderBufferFormat_RG32F, - ERenderBufferFormat_RGB16F, - ERenderBufferFormat_RGB32F, - ERenderBufferFormat_RGBA16F, - ERenderBufferFormat_RGBA32F, + RGBA4, + R8, + RG8, + RGB8, + RGBA8, + R16F, + R32F, + RG16F, + RG32F, + RGB16F, + RGB32F, + RGBA16F, + RGBA32F, - ERenderBufferFormat_Depth24, - ERenderBufferFormat_Depth24_Stencil8 + Depth24, + Depth24_Stencil8 }; ///Enum for color of texture channel @@ -165,10 +165,10 @@ namespace ramses /// Enum for access mode of a RenderBuffer - enum ERenderBufferAccessMode + enum class ERenderBufferAccessMode { - ERenderBufferAccessMode_WriteOnly = 0, ///< RenderBuffer with this access can only be used in RenderTarget - ERenderBufferAccessMode_ReadWrite ///< RenderBuffer with this access can be used both in RenderTarget and TextureSampler + WriteOnly, ///< RenderBuffer with this access can only be used in RenderTarget + ReadWrite ///< RenderBuffer with this access can be used both in RenderTarget and TextureSampler }; /** @@ -178,7 +178,7 @@ namespace ramses * @param samplingMethod The enum parameter for which you will get the string * @return String representation of the sampling method */ - RAMSES_API const char* getTextureSamplingMethodString(ETextureSamplingMethod samplingMethod); + RAMSES_API const char* toString(ETextureSamplingMethod samplingMethod); /** * @brief Returns string representation for address mode @@ -187,7 +187,7 @@ namespace ramses * @param addressMode The enum parameter for which you will get the string * @return String representation of the address mode */ - RAMSES_API const char* getTextureAddressModeString(ETextureAddressMode addressMode); + RAMSES_API const char* toString(ETextureAddressMode addressMode); /** * @brief Returns string representation for texture format @@ -196,7 +196,7 @@ namespace ramses * @param format The enum parameter for which you will get the string * @return String representation of the texture format */ - RAMSES_API const char* getTextureFormatString(ETextureFormat format); + RAMSES_API const char* toString(ETextureFormat format); /** * @brief Returns string representation for texture's cube face @@ -205,7 +205,7 @@ namespace ramses * @param face The enum parameter for which you will get the string * @return String representation of the cube face */ - RAMSES_API const char* getTextureCubeFaceString(ETextureCubeFace face); + RAMSES_API const char* toString(ETextureCubeFace face); /** * Returns if given texture format supports mipmap chain generation @@ -252,6 +252,10 @@ namespace ramses return true; } } + + /** + * @} + */ } #endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSampler.h b/client/ramses-client-api/include/ramses-client-api/TextureSampler.h similarity index 71% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSampler.h rename to client/ramses-client-api/include/ramses-client-api/TextureSampler.h index a3b075461..90bbcdef5 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSampler.h +++ b/client/ramses-client-api/include/ramses-client-api/TextureSampler.h @@ -20,12 +20,12 @@ namespace ramses class TextureCube; class Texture2DBuffer; class RenderBuffer; - class StreamTexture; /** + * @ingroup CoreAPI * @brief The TextureSampler holds a texture and its sampling parameters */ - class RAMSES_API TextureSampler : public SceneObject + class TextureSampler : public SceneObject { public: /** @@ -33,49 +33,49 @@ namespace ramses * * @return ETextureAddressMode wrap mode for u axis */ - ETextureAddressMode getWrapUMode() const; + [[nodiscard]] RAMSES_API ETextureAddressMode getWrapUMode() const; /** * @brief Gets the texture wrap mode for the v axis * * @return ETextureAddressMode wrap mode for v axis */ - ETextureAddressMode getWrapVMode() const; + [[nodiscard]] RAMSES_API ETextureAddressMode getWrapVMode() const; /** * @brief Gets the texture wrap mode for the r axis * * @return ETextureAddressMode wrap mode for r axis */ - ETextureAddressMode getWrapRMode() const; + [[nodiscard]] RAMSES_API ETextureAddressMode getWrapRMode() const; /** * @brief Gets the texture min sampling method * * @return ETextureSamplingMethod min sampling method */ - ETextureSamplingMethod getMinSamplingMethod() const; + [[nodiscard]] RAMSES_API ETextureSamplingMethod getMinSamplingMethod() const; /** * @brief Gets the texture mag sampling method * * @return ETextureSamplingMethod mag sampling method */ - ETextureSamplingMethod getMagSamplingMethod() const; + [[nodiscard]] RAMSES_API ETextureSamplingMethod getMagSamplingMethod() const; /** * @brief Gets the texture sampling anisotropy level * * @return The texture sampling anisotropy level. */ - uint32_t getAnisotropyLevel() const; + [[nodiscard]] RAMSES_API uint32_t getAnisotropyLevel() const; /** * @brief Gets the type of the texture * * @return Type of the texture, see ERamsesObjectType enum for possible values. */ - ERamsesObjectType getTextureType() const; + [[nodiscard]] RAMSES_API ERamsesObjectType getTextureType() const; /** * @brief Replaces current texture content source with a new one. @@ -88,40 +88,33 @@ namespace ramses * @return StatusOK for success, otherwise the returned status can be used * to resolve error message using getStatusMessage(). */ - status_t setTextureData(const Texture2D& dataSource); + RAMSES_API status_t setTextureData(const Texture2D& dataSource); /// @copybrief setTextureData(const Texture2D&) - status_t setTextureData(const Texture3D& dataSource); + RAMSES_API status_t setTextureData(const Texture3D& dataSource); /// @copybrief setTextureData(const Texture2D&) - status_t setTextureData(const TextureCube& dataSource); + RAMSES_API status_t setTextureData(const TextureCube& dataSource); /// @copybrief setTextureData(const Texture2D&) - status_t setTextureData(const Texture2DBuffer& dataSource); + RAMSES_API status_t setTextureData(const Texture2DBuffer& dataSource); /// @copybrief setTextureData(const Texture2D&) - status_t setTextureData(const RenderBuffer& dataSource); - /// @copybrief setTextureData(const Texture2D&) - status_t setTextureData(const StreamTexture& dataSource); + RAMSES_API status_t setTextureData(const RenderBuffer& dataSource); /** * Stores internal data for implementation specifics of TextureSampler. */ - class TextureSamplerImpl& impl; + class TextureSamplerImpl& m_impl; protected: /** * @brief Scene is the factory for creating TextureSampler instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for TextureSampler. * - * @param[in] pimpl Internal data for implementation specifics of TextureSampler (sink - instance becomes owner) - */ - explicit TextureSampler(TextureSamplerImpl& pimpl); - - /** - * @brief Destructor of the TextureSampler + * @param[in] impl Internal data for implementation specifics of TextureSampler (sink - instance becomes owner) */ - virtual ~TextureSampler(); + explicit TextureSampler(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSamplerExternal.h b/client/ramses-client-api/include/ramses-client-api/TextureSamplerExternal.h similarity index 72% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSamplerExternal.h rename to client/ramses-client-api/include/ramses-client-api/TextureSamplerExternal.h index 2a890f8ce..d10127738 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSamplerExternal.h +++ b/client/ramses-client-api/include/ramses-client-api/TextureSamplerExternal.h @@ -14,34 +14,30 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief The #ramses::TextureSamplerExternal is used to sample from external textures data when bound * to a #ramses::Appearance uniform input (#ramses::Appearance::setInputTexture called with #ramses::TextureSamplerExternal) */ - class RAMSES_API TextureSamplerExternal : public SceneObject + class TextureSamplerExternal : public SceneObject { public: /** * Stores internal data for implementation specifics of TextureSamplerExternal. */ - class TextureSamplerImpl& impl; + class TextureSamplerImpl& m_impl; protected: /** * @brief Scene is the factory for creating TextureSamplerExternal instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for TextureSamplerExternal. * - * @param[in] pimpl Internal data for implementation specifics of TextureSamplerExternal (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of TextureSamplerExternal (sink - instance becomes owner) */ - explicit TextureSamplerExternal(TextureSamplerImpl& pimpl); - - /** - * @brief Destructor of the TextureSamplerExternal - */ - virtual ~TextureSamplerExternal(); + explicit TextureSamplerExternal(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSamplerMS.h b/client/ramses-client-api/include/ramses-client-api/TextureSamplerMS.h similarity index 73% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSamplerMS.h rename to client/ramses-client-api/include/ramses-client-api/TextureSamplerMS.h index c692bd05e..93cc694d7 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSamplerMS.h +++ b/client/ramses-client-api/include/ramses-client-api/TextureSamplerMS.h @@ -14,34 +14,30 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief The #ramses::TextureSamplerMS is used to sample multisampled data when bound * to a #ramses::Appearance uniform input (#ramses::Appearance::setInputTexture called with #ramses::TextureSamplerMS) */ - class RAMSES_API TextureSamplerMS : public SceneObject + class TextureSamplerMS : public SceneObject { public: /** * Stores internal data for implementation specifics of TextureSamplerMS. */ - class TextureSamplerImpl& impl; + class TextureSamplerImpl& m_impl; protected: /** * @brief Scene is the factory for creating TextureSamplerMS instances. */ - friend class SceneImpl; + friend class RamsesObjectRegistry; /** * @brief Constructor for TextureSamplerMS. * - * @param[in] pimpl Internal data for implementation specifics of TextureSamplerMS (sink - instance becomes owner) + * @param[in] impl Internal data for implementation specifics of TextureSamplerMS (sink - instance becomes owner) */ - explicit TextureSamplerMS(TextureSamplerImpl& pimpl); - - /** - * @brief Destructor of the TextureSamplerMS - */ - virtual ~TextureSamplerMS(); + explicit TextureSamplerMS(std::unique_ptr impl); }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSwizzle.h b/client/ramses-client-api/include/ramses-client-api/TextureSwizzle.h similarity index 98% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSwizzle.h rename to client/ramses-client-api/include/ramses-client-api/TextureSwizzle.h index 6e1564fa4..f13717c51 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/TextureSwizzle.h +++ b/client/ramses-client-api/include/ramses-client-api/TextureSwizzle.h @@ -14,6 +14,7 @@ namespace ramses { /** + * @ingroup CoreAPI * @brief Information of how color channels of a texture are reordered or set to fixed value (one, zero). * For example swizzling a texture with a red triangle (R:1, G:0, B:0, A:1) to TextureSwizzle(Blue, Green, Red, Alpha) would turn the triangle blue. * The resulting color channels would be: (R:0, G:0, B:1, A:1). The red input color channel was basically rerouted to the blue output color channel. diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/UniformInput.h b/client/ramses-client-api/include/ramses-client-api/UniformInput.h similarity index 50% rename from client/ramses-client/ramses-client-api/include/ramses-client-api/UniformInput.h rename to client/ramses-client-api/include/ramses-client-api/UniformInput.h index 5df23905c..1dc67533d 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/UniformInput.h +++ b/client/ramses-client-api/include/ramses-client-api/UniformInput.h @@ -11,40 +11,65 @@ #include "ramses-client-api/EffectInput.h" #include "ramses-client-api/EffectInputSemantic.h" -#include "ramses-client-api/EffectInputDataType.h" +#include "ramses-framework-api/EDataType.h" namespace ramses { /** + * @ingroup CoreAPI * @brief The UniformInput is a description of an uniform effect input */ - class RAMSES_API UniformInput : public EffectInput + class UniformInput : public EffectInput { public: /** * @brief Constructor of UniformInput. */ - UniformInput(); + RAMSES_API UniformInput(); /** - * @brief Returns the effect input data type. - * - * @return Effect input data type + * @brief Destructor of UniformInput */ - EEffectInputDataType getDataType() const; + RAMSES_API ~UniformInput() override; /** * @brief Returns the effect input semantics. * * @return Effect input semantics */ - EEffectUniformSemantic getSemantics() const; + [[nodiscard]] RAMSES_API EEffectUniformSemantic getSemantics() const; /** * @brief Returns the number of elements that are assigned to this effect input * @return the element count or 0 if not initialized */ - uint32_t getElementCount() const; + [[nodiscard]] RAMSES_API size_t getElementCount() const; + + /** + * @brief Copy constructor + * @param other source to copy from + */ + RAMSES_API UniformInput(const UniformInput& other); + + /** + * @brief Move constructor + * @param other source to move from + */ + RAMSES_API UniformInput(UniformInput&& other) noexcept; + + /** + * @brief Copy assignment + * @param other source to copy from + * @return this instance + */ + RAMSES_API UniformInput& operator=(const UniformInput& other); + + /** + * @brief Move assignment + * @param other source to move from + * @return this instance + */ + RAMSES_API UniformInput& operator=(UniformInput&& other) noexcept; }; } diff --git a/client/ramses-client/ramses-client-api/include/ramses-client.h b/client/ramses-client-api/include/ramses-client.h similarity index 50% rename from client/ramses-client/ramses-client-api/include/ramses-client.h rename to client/ramses-client-api/include/ramses-client.h index f1c54b482..326621f3c 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-client.h +++ b/client/ramses-client-api/include/ramses-client.h @@ -9,6 +9,11 @@ #ifndef RAMSES_RAMSES_CLIENT_H #define RAMSES_RAMSES_CLIENT_H +/** + * @defgroup CoreAPI The Ramses Core API + * This group contains all of the Ramses Core API types. + */ + #include "ramses-client-api/RamsesClient.h" // Scene @@ -25,24 +30,11 @@ #include "ramses-client-api/BlitPass.h" #include "ramses-client-api/RenderGroup.h" #include "ramses-client-api/GeometryBinding.h" -#include "ramses-client-api/StreamTexture.h" #include "ramses-client-api/RenderTarget.h" #include "ramses-client-api/RenderTargetDescription.h" #include "ramses-client-api/RenderBuffer.h" #include "ramses-client-api/IClientEventHandler.h" - -// Data bindings -#include "ramses-client-api/DataFloat.h" -#include "ramses-client-api/DataInt32.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-client-api/DataVector3i.h" -#include "ramses-client-api/DataVector4f.h" -#include "ramses-client-api/DataVector4i.h" -#include "ramses-client-api/DataMatrix22f.h" -#include "ramses-client-api/DataMatrix33f.h" -#include "ramses-client-api/DataMatrix44f.h" +#include "ramses-client-api/DataObject.h" // Effect #include "ramses-client-api/EffectDescription.h" @@ -60,41 +52,8 @@ #include "ramses-client-api/ArrayBuffer.h" #include "ramses-client-api/Texture2DBuffer.h" -// Animation -#include "ramses-client-api/AnimatedProperty.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/AnimationSystem.h" -#include "ramses-client-api/AnimationSystemRealTime.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/SplineLinearInt32.h" -#include "ramses-client-api/SplineLinearVector2f.h" -#include "ramses-client-api/SplineLinearVector2i.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/SplineLinearVector3i.h" -#include "ramses-client-api/SplineLinearVector4f.h" -#include "ramses-client-api/SplineLinearVector4i.h" -#include "ramses-client-api/SplineBezierFloat.h" -#include "ramses-client-api/SplineBezierInt32.h" -#include "ramses-client-api/SplineBezierVector2f.h" -#include "ramses-client-api/SplineBezierVector2i.h" -#include "ramses-client-api/SplineBezierVector3f.h" -#include "ramses-client-api/SplineBezierVector3i.h" -#include "ramses-client-api/SplineBezierVector4f.h" -#include "ramses-client-api/SplineBezierVector4i.h" -#include "ramses-client-api/SplineStepBool.h" -#include "ramses-client-api/SplineStepFloat.h" -#include "ramses-client-api/SplineStepInt32.h" -#include "ramses-client-api/SplineStepVector2f.h" -#include "ramses-client-api/SplineStepVector2i.h" -#include "ramses-client-api/SplineStepVector3f.h" -#include "ramses-client-api/SplineStepVector3i.h" -#include "ramses-client-api/SplineStepVector4f.h" -#include "ramses-client-api/SplineStepVector4i.h" - // Iterators #include "ramses-client-api/SceneIterator.h" -#include "ramses-client-api/AnimationSystemObjectIterator.h" #include "ramses-client-api/RenderPassGroupIterator.h" #include "ramses-client-api/RenderGroupMeshIterator.h" #include "ramses-client-api/SceneGraphIterator.h" diff --git a/client/ramses-client/ramses-client-api/include/ramses-utils.h b/client/ramses-client-api/include/ramses-utils.h similarity index 87% rename from client/ramses-client/ramses-client-api/include/ramses-utils.h rename to client/ramses-client-api/include/ramses-utils.h index b7cc54db9..ec040d42e 100644 --- a/client/ramses-client/ramses-client-api/include/ramses-utils.h +++ b/client/ramses-client-api/include/ramses-utils.h @@ -17,6 +17,12 @@ #include #include +#include + +/** + * @defgroup UtilsAPI The Ramses Utils API + * This group contains all of the Ramses Utility API types. + */ namespace ramses { @@ -26,12 +32,12 @@ namespace ramses class Scene; class Texture2D; class UniformInput; - class DataVector2f; - class DataVector4f; + class DataObject; struct MipLevelData; struct CubeMipLevelData; /** + * @ingroup UtilsAPI * @brief Temporary functions for convenience. All of these can be implemented on top * of the RAMSES Client API, but are offered here as convenience. */ @@ -65,7 +71,7 @@ namespace ramses * @param[in] name Name for the created texture * @return Created texture object or nullptr on error */ - static Texture2D* CreateTextureResourceFromPng(const char* pngFilePath, Scene& scene, const TextureSwizzle& swizzle = {}, const char* name = nullptr); + static Texture2D* CreateTextureResourceFromPng(const char* pngFilePath, Scene& scene, const TextureSwizzle& swizzle = {}, std::string_view name = {}); /** * @brief Creates a Texture from the given png memory buffer. @@ -76,7 +82,7 @@ namespace ramses * @param[in] name Name for the created texture * @return Created texture object or nullptr on error */ - static Texture2D* CreateTextureResourceFromPngBuffer(const std::vector& pngData, Scene& scene, const TextureSwizzle& swizzle = {}, const char* name = nullptr); + static Texture2D* CreateTextureResourceFromPngBuffer(const std::vector& pngData, Scene& scene, const TextureSwizzle& swizzle = {}, std::string_view name = {}); /** * @brief Generate mip maps from original texture 2D data. You obtain ownership of all the @@ -92,7 +98,7 @@ namespace ramses * only the original mip map level is part of the result. * You are responsible to destroy the generated data, e.g. by using RamsesUtils::DeleteGeneratedMipMaps */ - static MipLevelData* GenerateMipMapsTexture2D(uint32_t width, uint32_t height, uint8_t bytesPerPixel, uint8_t* data, uint32_t& mipMapCount); + static MipLevelData* GenerateMipMapsTexture2D(uint32_t width, uint32_t height, uint8_t bytesPerPixel, uint8_t* data, size_t& mipMapCount); /** * @brief Creates a png from image data, e.g. data generated by RamsesClientService::readPixels. @@ -141,21 +147,21 @@ namespace ramses * only the original mip map level is part of the result. * You are responsible to destroy the generated data, e.g. using RamsesUtils::DeleteGeneratedMipMaps */ - static CubeMipLevelData* GenerateMipMapsTextureCube(uint32_t faceWidth, uint32_t faceHeight, uint8_t bytesPerPixel, uint8_t* data, uint32_t& mipMapCount); + static CubeMipLevelData* GenerateMipMapsTextureCube(uint32_t faceWidth, uint32_t faceHeight, uint8_t bytesPerPixel, uint8_t* data, size_t& mipMapCount); /** * @brief Deletes mip map data created with RamsesUtils::GenerateMipMapsTexture2D. * @param[in, out] data Generated mip map data. * @param[in] mipMapCount Number of mip map levels in the generated data. */ - static void DeleteGeneratedMipMaps(MipLevelData*& data, uint32_t mipMapCount); + static void DeleteGeneratedMipMaps(MipLevelData*& data, size_t mipMapCount); /** * @brief Deletes mip map data created with RamsesUtils::GenerateMipMapsTextureCube. * @param[in, out] data Generated mip map data. * @param[in] mipMapCount Number of mip map levels in the generated data. */ - static void DeleteGeneratedMipMaps(CubeMipLevelData*& data, uint32_t mipMapCount); + static void DeleteGeneratedMipMaps(CubeMipLevelData*& data, size_t mipMapCount); /** * @brief Returns the identifier of a node, which is printed in the renderer logs. The identifier is guaranteed to be @@ -182,11 +188,11 @@ namespace ramses * but typically matches the viewport aspect ratio. * @param[in] nearPlane Near plane of the camera frustum, must be > 0. * @param[in] farPlane Far plane of the camera frustum, must be > nearPlane. - * @param[in] frustumPlanesData Data object where resulting first 4 frustum planes will be set to. - * @param[in] nearFarPlanesData Data object where resulting near/far frustum planes will be set to. + * @param[in] frustumPlanesData Data object where resulting first 4 frustum planes will be set to, must be created with #ramses::EDataType::Vector4F. + * @param[in] nearFarPlanesData Data object where resulting near/far frustum planes will be set to, must be created with #ramses::EDataType::Vector2F. * @return True for success, false otherwise. */ - static bool SetPerspectiveCameraFrustumToDataObjects(float fov, float aspectRatio, float nearPlane, float farPlane, DataVector4f& frustumPlanesData, DataVector2f& nearFarPlanesData); + static bool SetPerspectiveCameraFrustumToDataObjects(float fov, float aspectRatio, float nearPlane, float farPlane, DataObject& frustumPlanesData, DataObject& nearFarPlanesData); /** * @brief Convenience wrapper for RamsesClient::loadSceneFromMemory with automatic deleter @@ -207,11 +213,31 @@ namespace ramses * @return The loaded ramses Scene or nullptr on error * */ - static Scene* LoadSceneFromMemory(RamsesClient& client, std::unique_ptr data, size_t size, bool localOnly); + static Scene* LoadSceneFromMemory(RamsesClient& client, std::unique_ptr data, size_t size, bool localOnly); // NOLINT(modernize-avoid-c-arrays) + + /** + * @brief Dumps all objects of a scene which do not contribute to the visual appearance of the scene on screen. + * This includes disabled RenderPass-es, invisible MeshNode-s, client resources which are not used by the scene, and so on. + * The output is in text form, starts with a list of all unrequired objects and their names and concludes with a + * statistic (number of unrequired objects out of all objects of that type) + * + * @param[in] scene the source scene + */ + static void DumpUnrequiredSceneObjects(const Scene& scene); + + /** + * @brief As #DumpUnrequiredSceneObjects but write to given stream. + * + * @param[in] scene the source scene + * @param[out] out stream to write to + */ + static void DumpUnrequiredSceneObjectsToFile(const Scene& scene, std::ostream& out); }; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) inline Scene* RamsesUtils::LoadSceneFromMemory(RamsesClient& client, std::unique_ptr data, size_t size, bool localOnly) { + // NOLINTNEXTLINE(modernize-avoid-c-arrays) std::unique_ptr dataWithDeleter(data.release(), [](const unsigned char* ptr) { delete[] ptr; }); return client.loadSceneFromMemory(std::move(dataWithDeleter), size, localOnly); } diff --git a/client/ramses-client/CMakeLists.txt b/client/ramses-client/CMakeLists.txt deleted file mode 100644 index 26a58eb59..000000000 --- a/client/ramses-client/CMakeLists.txt +++ /dev/null @@ -1,96 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (C) 2018 BMW Car IT GmbH -# ------------------------------------------------------------------------- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -# ------------------------------------------------------------------------- - -file(GLOB - RAMSES_CLIENT_FILES_SOURCE - impl/*.cpp - impl/glslEffectBlock/*.cpp - impl/ClientCommands/*.cpp - ramses-client-api/*.cpp - impl/ramses-text/*.cpp - ramses-text-api/*.cpp) -file(GLOB - RAMSES_CLIENT_API_INCLUDE_BASE - ramses-client-api/include - ramses-text-api/include) - - -add_library(ramses-client-api INTERFACE) -target_include_directories(ramses-client-api INTERFACE ${RAMSES_CLIENT_API_INCLUDE_BASE}) - -ACME_MODULE( - - #========================================================================== - # general module information - #========================================================================== - NAME ramses-client - TYPE STATIC_LIBRARY - ENABLE_INSTALL OFF - - #========================================================================== - # files of this module - #========================================================================== - INCLUDE_BASE impl - FILES_PRIVATE_HEADER impl/*.h - impl/glslEffectBlock/*.h - impl/ClientCommands/*.h - ramses-client-api/include/*.h - ramses-client-api/include/ramses-client-api/*.h - FILES_SOURCE ${RAMSES_CLIENT_FILES_SOURCE} - impl/ramses-text/*.h - ramses-text-api/include/*.h - ramses-text-api/include/ramses-text-api/*.h - #========================================================================== - # dependencies - #========================================================================== - DEPENDENCIES ramses-client-api - ramses-framework - ramses-glslang - freetype - harfbuzz -) - -INSTALL(DIRECTORY ramses-client-api/include/ DESTINATION "${PROJECT_INSTALL_HEADER}" COMPONENT ramses-sdk-devel) -INSTALL(DIRECTORY ramses-text-api/include/ DESTINATION "${PROJECT_INSTALL_HEADER}" COMPONENT ramses-sdk-devel) - -if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - target_compile_options(ramses-client PRIVATE "-Wsuggest-override") -endif() - -ACME_MODULE( - - #========================================================================== - # general module information - #========================================================================== - NAME ramses-client-test - TYPE TEST - - #========================================================================== - # files of this module - #========================================================================== - INCLUDE_BASE test - FILES_PRIVATE_HEADER test/*.h - test/ClientCommands/*.h - FILES_SOURCE test/*.cpp - test/ClientCommands/*.cpp - RESOURCE_FOLDER test/res - - #========================================================================== - # dependencies - #========================================================================== - DEPENDENCIES ramses-client - FrameworkTestUtils - ramses-gmock-main -) - -set(ramses-shared-lib-MIXIN - ${ramses-shared-lib-MIXIN} - INCLUDE_BASE ${RAMSES_CLIENT_API_INCLUDE_BASE} - FILES_SOURCE ${RAMSES_CLIENT_FILES_SOURCE} - DEPENDENCIES ramses-client - CACHE INTERNAL "") diff --git a/client/ramses-client/impl/AnimatedPropertyFactory.cpp b/client/ramses-client/impl/AnimatedPropertyFactory.cpp deleted file mode 100644 index 9280c9d60..000000000 --- a/client/ramses-client/impl/AnimatedPropertyFactory.cpp +++ /dev/null @@ -1,160 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "AnimatedPropertyFactory.h" -#include "AnimatedPropertyUtils.h" -#include "AnimationSystemImpl.h" -#include "NodeImpl.h" -#include "AppearanceImpl.h" -#include "EffectInputImpl.h" -#include "DataObjectImpl.h" -#include "SceneImpl.h" -#include "Scene/ClientScene.h" -#include "Scene/SceneDataBinding.h" -#include "Utils/LogMacros.h" - -namespace ramses -{ - AnimatedPropertyFactory::AnimatedPropertyFactory(AnimationSystemImpl& animationSystem) - : m_animationSystem(animationSystem) - { - } - - AnimatedProperty* AnimatedPropertyFactory::createAnimatedProperty(const NodeImpl& propertyOwner, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name) - { - AnimatedPropertyImpl* pimpl = createAnimatedPropertyImpl(propertyOwner, ePropertyComponent, bindID, name); - if (pimpl != nullptr) - { - AnimatedProperty* animProperty = new AnimatedProperty(*pimpl); - return animProperty; - } - - return nullptr; - } - - AnimatedProperty* AnimatedPropertyFactory::createAnimatedProperty(const EffectInputImpl& propertyOwner, const AppearanceImpl& appearance, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name) - { - AnimatedPropertyImpl* pimpl = createAnimatedPropertyImpl(propertyOwner, appearance, ePropertyComponent, bindID, name); - if (pimpl != nullptr) - { - AnimatedProperty* animProperty = new AnimatedProperty(*pimpl); - return animProperty; - } - - return nullptr; - } - - AnimatedProperty* AnimatedPropertyFactory::createAnimatedProperty(const DataObjectImpl& propertyOwner, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name) - { - AnimatedPropertyImpl* pimpl = createAnimatedPropertyImpl(propertyOwner, ePropertyComponent, bindID, name); - if (pimpl != nullptr) - { - AnimatedProperty* animProperty = new AnimatedProperty(*pimpl); - return animProperty; - } - - return nullptr; - } - - AnimatedPropertyImpl* AnimatedPropertyFactory::createAnimatedPropertyImpl(const NodeImpl& propertyOwner, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name) - { - if (!AnimatedPropertyUtils::isComponentMatchingTransformNode(ePropertyComponent)) - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "AnimatedPropertyFactory::createAnimatedProperty: cannot initialize animated property, requested property and/or component is not compatible"); - return nullptr; - } - - if (!m_animationSystem.getSceneImpl().containsSceneObject(propertyOwner)) - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "AnimatedPropertyFactory::createAnimatedProperty: cannot initialize animated property, requested property and animation system do not belong to the same scene"); - return nullptr; - } - - using ContainerTraitsClass = ramses_internal::DataBindContainerToTraitsSelector::ContainerTraitsClassType; - if (bindID == ContainerTraitsClass::TransformNode_Rotation - && propertyOwner.getIScene().getRotationConvention(propertyOwner.getTransformHandle()) != ramses_internal::ERotationConvention::Legacy_ZYX) - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "AnimatedPropertyFactory::createAnimatedProperty: cannot initialize animated property, can not animate rotation for node that does not use legacy rotation convention"); - return nullptr; - } - - const ramses_internal::MemoryHandle handle1 = propertyOwner.getTransformHandle().asMemoryHandle(); - const ramses_internal::MemoryHandle handle2 = ramses_internal::InvalidMemoryHandle; - - return createAnimatedPropertyImpl(handle1, handle2, ePropertyComponent, bindID, name); - } - - AnimatedPropertyImpl* AnimatedPropertyFactory::createAnimatedPropertyImpl(const EffectInputImpl& propertyOwner, const AppearanceImpl& appearance, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name) - { - if (!AnimatedPropertyUtils::isComponentMatchingEffectInput(ePropertyComponent, propertyOwner.getDataType())) - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "AnimatedPropertyFactory::createAnimatedProperty: cannot initialize animated property, requested property and/or component is not compatible"); - return nullptr; - } - - if (!m_animationSystem.getSceneImpl().containsSceneObject(appearance)) - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "AnimatedPropertyFactory::createAnimatedProperty: cannot initialize animated property, requested property and animation system do not belong to the same scene"); - return nullptr; - } - - ramses_internal::MemoryHandle handle2 = ramses_internal::InvalidMemoryHandle; - ramses_internal::MemoryHandle handle1 = ramses_internal::InvalidMemoryHandle; - - const ramses_internal::DataLayoutHandle dataLayout = m_animationSystem.getIScene().getLayoutOfDataInstance(appearance.getUniformDataInstance()); - const ramses_internal::DataFieldHandle dataField(propertyOwner.getInputIndex()); - const ramses_internal::EDataType dataType = m_animationSystem.getIScene().getDataLayout(dataLayout).getField(dataField).dataType; - if (dataType == ramses_internal::EDataType::DataReference) - { - const ramses_internal::DataInstanceHandle dataRef = m_animationSystem.getIScene().getDataReference(appearance.getUniformDataInstance(), dataField); - handle1 = dataRef.asMemoryHandle(); - handle2 = 0u; - } - else - { - handle1 = appearance.getUniformDataInstance().asMemoryHandle(); - handle2 = propertyOwner.getInputIndex(); - } - - return createAnimatedPropertyImpl(handle1, handle2, ePropertyComponent, bindID, name); - } - - AnimatedPropertyImpl* AnimatedPropertyFactory::createAnimatedPropertyImpl(const DataObjectImpl& propertyOwner, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name) - { - if (!AnimatedPropertyUtils::isComponentMatchingEffectInput(ePropertyComponent, propertyOwner.getDataType())) - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "AnimatedPropertyFactory::createAnimatedProperty: cannot initialize animated property, requested property and/or component is not compatible"); - return nullptr; - } - - if (!m_animationSystem.getSceneImpl().containsSceneObject(propertyOwner)) - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "AnimatedPropertyFactory::createAnimatedProperty: cannot initialize animated property, requested property and animation system do not belong to the same scene"); - return nullptr; - } - - const ramses_internal::MemoryHandle handle1 = propertyOwner.getDataReference().asMemoryHandle(); - const ramses_internal::MemoryHandle handle2 = 0u; - - return createAnimatedPropertyImpl(handle1, handle2, ePropertyComponent, bindID, name); - } - - AnimatedPropertyImpl* AnimatedPropertyFactory::createAnimatedPropertyImpl(ramses_internal::MemoryHandle handle1, ramses_internal::MemoryHandle handle2, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name) - { - using ContainerTraitsClass = ramses_internal::DataBindContainerToTraitsSelector::ContainerTraitsClassType; - const ramses_internal::DataBindContainerTraits& containerTraits = ContainerTraitsClass::m_dataBindTraits[bindID]; - const ramses_internal::TDataBindID dataBindID = bindID; - const ramses_internal::EVectorComponent vectorComponent = AnimatedPropertyUtils::getVectorComponentFromProperty(ePropertyComponent); - const ramses_internal::EDataTypeID dataTypeID = containerTraits.m_eDataTypeID; - - AnimatedPropertyImpl* propertyData = new AnimatedPropertyImpl(m_animationSystem, name); - propertyData->initializeFrameworkData(m_animationSystem.getIScene(), handle1, handle2, dataBindID, vectorComponent, dataTypeID); - - return propertyData; - } -} diff --git a/client/ramses-client/impl/AnimatedPropertyFactory.h b/client/ramses-client/impl/AnimatedPropertyFactory.h deleted file mode 100644 index a2aab9b68..000000000 --- a/client/ramses-client/impl/AnimatedPropertyFactory.h +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATEDPROPERTYFACTORY_H -#define RAMSES_ANIMATEDPROPERTYFACTORY_H - -#include "ramses-client-api/AnimatedProperty.h" -#include "AnimatedPropertyImpl.h" -#include "Utils/DataBindCommon.h" - -namespace ramses -{ - class NodeImpl; - class EffectInputImpl; - class DataObjectImpl; - class AppearanceImpl; - class AnimationSystemImpl; - - class AnimatedPropertyFactory - { - public: - explicit AnimatedPropertyFactory(AnimationSystemImpl& animationSystem); - - AnimatedProperty* createAnimatedProperty(const NodeImpl& propertyOwner, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name); - AnimatedProperty* createAnimatedProperty(const EffectInputImpl& propertyOwner, const AppearanceImpl& appearance, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name); - AnimatedProperty* createAnimatedProperty(const DataObjectImpl& propertyOwner, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name); - - private: - AnimatedPropertyImpl* createAnimatedPropertyImpl(const NodeImpl& propertyOwner, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name); - AnimatedPropertyImpl* createAnimatedPropertyImpl(const EffectInputImpl& propertyOwner, const AppearanceImpl& appearance, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name); - AnimatedPropertyImpl* createAnimatedPropertyImpl(const DataObjectImpl& propertyOwner, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name); - AnimatedPropertyImpl* createAnimatedPropertyImpl(ramses_internal::MemoryHandle handle1, ramses_internal::MemoryHandle handle2, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name); - - AnimationSystemImpl& m_animationSystem; - }; -} - -#endif diff --git a/client/ramses-client/impl/AnimatedPropertyImpl.cpp b/client/ramses-client/impl/AnimatedPropertyImpl.cpp deleted file mode 100644 index a9b57dcd6..000000000 --- a/client/ramses-client/impl/AnimatedPropertyImpl.cpp +++ /dev/null @@ -1,129 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "AnimatedPropertyImpl.h" -#include "AnimationSystemImpl.h" -#include "SerializationContext.h" -#include "AnimationAPI/IAnimationSystem.h" -#include "Animation/AnimationDataBind.h" -#include "Animation/AnimationInstance.h" -#include "Collections/IOutputStream.h" -#include "Collections/IInputStream.h" -#include "Scene/SceneDataBinding.h" -#include "SceneObjectImpl.h" -#include "Scene/Scene.h" -#include "Scene/ClientScene.h" - -namespace ramses -{ - AnimatedPropertyImpl::AnimatedPropertyImpl(AnimationSystemImpl& animationSystem, const char* name) - : AnimationObjectImpl(animationSystem, ERamsesObjectType_AnimatedProperty, name) - , m_vectorComponent(ramses_internal::EVectorComponent_All) - , m_dataTypeID(ramses_internal::EDataTypeID_Invalid) - { - } - - void AnimatedPropertyImpl::initializeFrameworkData( - ramses_internal::IScene& scene, - ramses_internal::MemoryHandle handle1, - ramses_internal::MemoryHandle handle2, - ramses_internal::TDataBindID dataBindID, - ramses_internal::EVectorComponent vectorComponent, - ramses_internal::EDataTypeID dataTypeID) - { - assert(!m_dataBindHandle.isValid()); - m_dataBindHandle = getIAnimationSystem().allocateDataBinding(scene, dataBindID, handle1, handle2); - - m_vectorComponent = vectorComponent; - m_dataTypeID = dataTypeID; - } - - void AnimatedPropertyImpl::deinitializeFrameworkData() - { - getIAnimationSystem().removeDataBinding(m_dataBindHandle); - m_dataBindHandle = ramses_internal::DataBindHandle::Invalid(); - } - - status_t AnimatedPropertyImpl::serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const - { - CHECK_RETURN_ERR(AnimationObjectImpl::serialize(outStream, serializationContext)); - - outStream << m_dataBindHandle; - outStream << static_cast(m_vectorComponent); - outStream << static_cast(m_dataTypeID); - - return StatusOK; - } - - status_t AnimatedPropertyImpl::deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) - { - CHECK_RETURN_ERR(AnimationObjectImpl::deserialize(inStream, serializationContext)); - - inStream >> m_dataBindHandle; - uint32_t enumInt = 0u; - inStream >> enumInt; - m_vectorComponent = static_cast(enumInt); - inStream >> enumInt; - m_dataTypeID = static_cast(enumInt); - - return StatusOK; - } - - status_t AnimatedPropertyImpl::validate() const - { - status_t status = AnimationObjectImpl::validate(); - - const auto& dataBind = *getIAnimationSystem().getDataBinding(m_dataBindHandle); - if (!dataBind.isPropertyValid()) - status = addValidationMessage(EValidationSeverity_Error, "property to animate does not exist in scene"); - else - { - using ContainerTraitsClass = ramses_internal::DataBindContainerToTraitsSelector::ContainerTraitsClassType; - if (dataBind.getBindID() == ContainerTraitsClass::TransformNode_Rotation) - { - const ramses_internal::TransformHandle transformHandle = static_cast(dataBind.getHandle()); - - if (getIScene().getRotationConvention(transformHandle) != ramses_internal::ERotationConvention::Legacy_ZYX) - status = addValidationMessage(EValidationSeverity_Error, "trying to animate rotation for node that does not use legacy rotation convention"); - } - } - - // find all animations using this property - uint32_t propertyUsageCount = 0u; - const uint32_t animInstanceCount = getIAnimationSystem().getTotalAnimationInstanceCount(); - for (ramses_internal::AnimationInstanceHandle animInstHandle(0u); animInstHandle < animInstanceCount; ++animInstHandle) - { - if (getIAnimationSystem().containsAnimationInstance(animInstHandle)) - { - const ramses_internal::AnimationInstance& animInst = getIAnimationSystem().getAnimationInstance(animInstHandle); - if (contains_c(animInst.getDataBindings(), m_dataBindHandle)) - ++propertyUsageCount; - } - } - - if (propertyUsageCount > 1u) - status = std::max(status, addValidationMessage(EValidationSeverity_Warning, "this AnimatedProperty seems to be used by multiple animations, this is fine only if those animations do not overlap")); - - return status; - } - - ramses_internal::DataBindHandle AnimatedPropertyImpl::getDataBindHandle() const - { - return m_dataBindHandle; - } - - ramses_internal::EVectorComponent AnimatedPropertyImpl::getVectorComponent() const - { - return m_vectorComponent; - } - - ramses_internal::EDataTypeID AnimatedPropertyImpl::getDataTypeID() const - { - return m_dataTypeID; - } -} diff --git a/client/ramses-client/impl/AnimatedPropertyImpl.h b/client/ramses-client/impl/AnimatedPropertyImpl.h deleted file mode 100644 index 7bfe2e465..000000000 --- a/client/ramses-client/impl/AnimatedPropertyImpl.h +++ /dev/null @@ -1,52 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATEDPROPERTYIMPL_H -#define RAMSES_ANIMATEDPROPERTYIMPL_H - -#include "AnimationObjectImpl.h" -#include "Utils/DataBindCommon.h" -#include "Animation/AnimationCommon.h" - -namespace ramses_internal -{ - class IScene; - class IAnimationSystem; -} - -namespace ramses -{ - class AnimatedPropertyImpl final : public AnimationObjectImpl - { - public: - AnimatedPropertyImpl(AnimationSystemImpl& animationSystem, const char* name); - - void initializeFrameworkData( - ramses_internal::IScene& scene, - ramses_internal::MemoryHandle handle1, - ramses_internal::MemoryHandle handle2, - ramses_internal::TDataBindID dataBindID, - ramses_internal::EVectorComponent vectorComponent, - ramses_internal::EDataTypeID dataTypeID); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t validate() const override; - - ramses_internal::DataBindHandle getDataBindHandle() const; - ramses_internal::EVectorComponent getVectorComponent() const; - ramses_internal::EDataTypeID getDataTypeID() const; - - private: - ramses_internal::DataBindHandle m_dataBindHandle; - ramses_internal::EVectorComponent m_vectorComponent; - ramses_internal::EDataTypeID m_dataTypeID; - }; -} - -#endif diff --git a/client/ramses-client/impl/AnimatedPropertyUtils.cpp b/client/ramses-client/impl/AnimatedPropertyUtils.cpp deleted file mode 100644 index ab553f2f7..000000000 --- a/client/ramses-client/impl/AnimatedPropertyUtils.cpp +++ /dev/null @@ -1,80 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "AnimatedPropertyUtils.h" - -namespace ramses -{ - bool AnimatedPropertyUtils::isComponentMatchingTransformNode(EAnimatedPropertyComponent ePropertyComponent) - { - return isComponentValidVec3(ePropertyComponent); - } - - bool AnimatedPropertyUtils::isComponentMatchingInputFloat(EAnimatedPropertyComponent ePropertyComponent) - { - return isComponentValidScalar(ePropertyComponent); - } - - bool AnimatedPropertyUtils::isComponentMatchingInputVec4(EAnimatedPropertyComponent ePropertyComponent) - { - return isComponentValidVec4(ePropertyComponent); - } - - bool AnimatedPropertyUtils::isComponentMatchingEffectInput(EAnimatedPropertyComponent ePropertyComponent, ramses_internal::EDataType dataType) - { - const ramses_internal::UInt32 numberOfComponents = EnumToNumComponents(dataType); - switch (ePropertyComponent) - { - case EAnimatedPropertyComponent_All: - case EAnimatedPropertyComponent_X: - return true; - case EAnimatedPropertyComponent_Y: - return (numberOfComponents >= 2); - case EAnimatedPropertyComponent_Z: - return (numberOfComponents >= 3); - case EAnimatedPropertyComponent_W: - return (numberOfComponents >= 4); - default: - return false; - } - } - - bool AnimatedPropertyUtils::isComponentValidScalar(EAnimatedPropertyComponent ePropertyComponent) - { - return (ePropertyComponent == EAnimatedPropertyComponent_All) || (ePropertyComponent == EAnimatedPropertyComponent_X); - } - - bool AnimatedPropertyUtils::isComponentValidVec3(EAnimatedPropertyComponent ePropertyComponent) - { - return (ePropertyComponent == EAnimatedPropertyComponent_All) || (ePropertyComponent <= EAnimatedPropertyComponent_Z); - } - - bool AnimatedPropertyUtils::isComponentValidVec4(EAnimatedPropertyComponent ePropertyComponent) - { - UNUSED(ePropertyComponent); - return true; - } - - ramses_internal::EVectorComponent AnimatedPropertyUtils::getVectorComponentFromProperty(EAnimatedPropertyComponent ePropertyComponent) - { - switch (ePropertyComponent) - { - case EAnimatedPropertyComponent_X: - return ramses_internal::EVectorComponent_X; - case EAnimatedPropertyComponent_Y: - return ramses_internal::EVectorComponent_Y; - case EAnimatedPropertyComponent_Z: - return ramses_internal::EVectorComponent_Z; - case EAnimatedPropertyComponent_W: - return ramses_internal::EVectorComponent_W; - case EAnimatedPropertyComponent_All: - default: - return ramses_internal::EVectorComponent_All; - } - } -} diff --git a/client/ramses-client/impl/AnimatedPropertyUtils.h b/client/ramses-client/impl/AnimatedPropertyUtils.h deleted file mode 100644 index d6ce7077f..000000000 --- a/client/ramses-client/impl/AnimatedPropertyUtils.h +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATEDPROPERTYUTILS_H -#define RAMSES_ANIMATEDPROPERTYUTILS_H - -#include "ramses-client-api/AnimatedProperty.h" -#include "Animation/AnimationCommon.h" -#include "SceneAPI/EDataType.h" - -namespace ramses -{ - class AnimatedPropertyUtils - { - public: - static bool isComponentMatchingTransformNode(EAnimatedPropertyComponent ePropertyComponent); - static bool isComponentMatchingMeshNode(EAnimatedPropertyComponent ePropertyComponent); - static bool isComponentMatchingInputFloat(EAnimatedPropertyComponent ePropertyComponent); - static bool isComponentMatchingInputVec4(EAnimatedPropertyComponent ePropertyComponent); - static bool isComponentMatchingEffectInput(EAnimatedPropertyComponent ePropertyComponent, ramses_internal::EDataType dataType); - static ramses_internal::EVectorComponent getVectorComponentFromProperty(EAnimatedPropertyComponent ePropertyComponent); - - AnimatedPropertyUtils() = delete; - ~AnimatedPropertyUtils() = delete; - - private: - static bool isComponentValidScalar(EAnimatedPropertyComponent ePropertyComponent); - static bool isComponentValidVec3(EAnimatedPropertyComponent ePropertyComponent); - static bool isComponentValidVec4(EAnimatedPropertyComponent ePropertyComponent); - - }; -} - -#endif diff --git a/client/ramses-client/impl/AnimationImpl.cpp b/client/ramses-client/impl/AnimationImpl.cpp deleted file mode 100644 index 0e2cafe82..000000000 --- a/client/ramses-client/impl/AnimationImpl.cpp +++ /dev/null @@ -1,168 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/AnimatedProperty.h" -#include "ramses-client-api/Spline.h" - -// internal -#include "AnimationSystemImpl.h" -#include "AnimationImpl.h" -#include "SplineImpl.h" -#include "AnimatedPropertyImpl.h" -#include "RamsesObjectRegistryIterator.h" -#include "RamsesObjectTypeUtils.h" - -// framework -#include "SerializationContext.h" -#include "Animation/Animation.h" -#include "Animation/AnimationDataBind.h" -#include "Animation/AnimationInstance.h" - - -namespace ramses -{ - AnimationImpl::AnimationImpl(AnimationSystemImpl& animationSystem, const char* name) - : AnimationObjectImpl(animationSystem, ERamsesObjectType_Animation, name) - { - } - - AnimationImpl::~AnimationImpl() - { - } - - status_t AnimationImpl::stop(timeMilliseconds_t delay) - { - const timeMilliseconds_t stopTime = getIAnimationSystem().getTime().getTimeStamp() + delay; - return stopAt(stopTime); - } - - status_t AnimationImpl::stopAt(globalTimeStamp_t timeStamp) - { - const ramses_internal::AnimationTime stopTime(timeStamp); - getIAnimationSystem().setAnimationStopTime(m_animationHandle, stopTime); - return StatusOK; - } - - status_t AnimationImpl::serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const - { - CHECK_RETURN_ERR(AnimationObjectImpl::serialize(outStream, serializationContext)); - - outStream << m_animationInstanceHandle; - outStream << m_animationHandle; - - return StatusOK; - } - - status_t AnimationImpl::deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) - { - CHECK_RETURN_ERR(AnimationObjectImpl::deserialize(inStream, serializationContext)); - - inStream >> m_animationInstanceHandle; - inStream >> m_animationHandle; - - return StatusOK; - } - - status_t AnimationImpl::validate() const - { - status_t status = AnimationObjectImpl::validate(); - - const ramses_internal::AnimationInstance& animInstance = getIAnimationSystem().getAnimationInstance(m_animationInstanceHandle); - - // validate assigned databinding / AnimatedProperty - const ramses_internal::DataBindHandleVector& dataBinds = animInstance.getDataBindings(); - for(const auto& dataBind : dataBinds) - { - const AnimatedPropertyImpl* animProperty = findAnimatedProperty(dataBind); - if (animProperty == nullptr) - status = addValidationMessage(EValidationSeverity_Error, "assigned AnimatedProperty does not exist anymore, was probably destroyed but still used by Animation"); - else - status = addValidationOfDependentObject(*animProperty); - } - - // validate assigned spline - const SplineImpl* spline = findSpline(animInstance.getSplineHandle()); - if (spline == nullptr) - status = addValidationMessage(EValidationSeverity_Error, "assigned Spline does not exist anymore, was probably destroyed but still used by Animation"); - else - status = std::max(status, addValidationOfDependentObject(*spline)); - - return status; - } - - void AnimationImpl::initializeFrameworkData(const AnimatedPropertyImpl& animatedProperty, ramses_internal::SplineHandle splineHandle, ramses_internal::EInterpolationType interpolationType) - { - m_animationInstanceHandle = getIAnimationSystem().allocateAnimationInstance(splineHandle, interpolationType, animatedProperty.getVectorComponent()); - getIAnimationSystem().addDataBindingToAnimationInstance(m_animationInstanceHandle, animatedProperty.getDataBindHandle()); - - m_animationHandle = getIAnimationSystem().allocateAnimation(m_animationInstanceHandle); - } - - void AnimationImpl::deinitializeFrameworkData() - { - getIAnimationSystem().removeAnimation(m_animationHandle); - getIAnimationSystem().removeAnimationInstance(m_animationInstanceHandle); - - m_animationHandle = ramses_internal::AnimationHandle::Invalid(); - m_animationInstanceHandle = ramses_internal::AnimationInstanceHandle::Invalid(); - } - - globalTimeStamp_t AnimationImpl::getStartTime() const - { - const ramses_internal::Animation& animation = getIAnimationSystem().getAnimation(m_animationHandle); - return animation.m_startTime.getTimeStamp(); - } - - globalTimeStamp_t AnimationImpl::getStopTime() const - { - const ramses_internal::Animation& animation = getIAnimationSystem().getAnimation(m_animationHandle); - return animation.m_stopTime.getTimeStamp(); - } - - ramses_internal::AnimationInstanceHandle AnimationImpl::getAnimationInstanceHandle() const - { - return m_animationInstanceHandle; - } - - ramses_internal::AnimationHandle AnimationImpl::getAnimationHandle() const - { - return m_animationHandle; - } - - const AnimatedPropertyImpl* AnimationImpl::findAnimatedProperty(ramses_internal::DataBindHandle handle) const - { - RamsesObjectRegistryIterator iter(getAnimationSystemImpl().getObjectRegistry(), ERamsesObjectType_AnimatedProperty); - while (const AnimatedProperty* animProperty = iter.getNext()) - { - if (animProperty->impl.getDataBindHandle() == handle) - { - return &animProperty->impl; - } - } - - return nullptr; - } - - const SplineImpl* AnimationImpl::findSpline(ramses_internal::SplineHandle handle) const - { - RamsesObjectVector splines; - getAnimationSystemImpl().getObjectRegistry().getObjectsOfType(splines, ERamsesObjectType_Spline); - - for (const auto it : splines) - { - const Spline& spline = RamsesObjectTypeUtils::ConvertTo(*it); - if (spline.impl.getSplineHandle() == handle) - { - return &spline.impl; - } - } - - return nullptr; - } -} diff --git a/client/ramses-client/impl/AnimationImpl.h b/client/ramses-client/impl/AnimationImpl.h deleted file mode 100644 index bec9a294c..000000000 --- a/client/ramses-client/impl/AnimationImpl.h +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONIMPL_H -#define RAMSES_ANIMATIONIMPL_H - -// API -#include "ramses-client-api/AnimationTypes.h" - -// internal -#include "AnimationObjectImpl.h" -#include "Animation/AnimationCommon.h" - -namespace ramses -{ - class AnimatedProperty; - class SplineImpl; - class AnimatedPropertyImpl; - - class AnimationImpl final : public AnimationObjectImpl - { - public: - explicit AnimationImpl(AnimationSystemImpl& animationSystem, const char* name); - virtual ~AnimationImpl() override; - - void initializeFrameworkData(const AnimatedPropertyImpl& animatedProperty, ramses_internal::SplineHandle splineHandle, ramses_internal::EInterpolationType interpolationType); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t validate() const override; - - status_t stop(timeMilliseconds_t delay); - status_t stopAt(globalTimeStamp_t timeStamp); - - globalTimeStamp_t getStartTime() const; - globalTimeStamp_t getStopTime() const; - - ramses_internal::AnimationInstanceHandle getAnimationInstanceHandle() const; - ramses_internal::AnimationHandle getAnimationHandle() const; - - const AnimatedPropertyImpl* findAnimatedProperty(ramses_internal::DataBindHandle handle) const; - const SplineImpl* findSpline(ramses_internal::SplineHandle handle) const; - - private: - ramses_internal::AnimationInstanceHandle m_animationInstanceHandle; - ramses_internal::AnimationHandle m_animationHandle; - }; -} - -#endif diff --git a/client/ramses-client/impl/AnimationObjectImpl.cpp b/client/ramses-client/impl/AnimationObjectImpl.cpp deleted file mode 100644 index 407f4e9f6..000000000 --- a/client/ramses-client/impl/AnimationObjectImpl.cpp +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "AnimationObjectImpl.h" -#include "AnimationSystemImpl.h" - -namespace ramses -{ - AnimationObjectImpl::AnimationObjectImpl(AnimationSystemImpl& animationSystem, ERamsesObjectType type, const char* name) - : SceneObjectImpl(animationSystem.getSceneImpl(), type, name) - , m_animationSystem(animationSystem) - { - } - - AnimationObjectImpl::~AnimationObjectImpl() - { - } - - const AnimationSystemImpl& AnimationObjectImpl::getAnimationSystemImpl() const - { - return m_animationSystem; - } - - AnimationSystemImpl& AnimationObjectImpl::getAnimationSystemImpl() - { - return m_animationSystem; - } - - const ramses_internal::IAnimationSystem& AnimationObjectImpl::getIAnimationSystem() const - { - return m_animationSystem.getIAnimationSystem(); - } - - ramses_internal::IAnimationSystem& AnimationObjectImpl::getIAnimationSystem() - { - return m_animationSystem.getIAnimationSystem(); - } -} diff --git a/client/ramses-client/impl/AnimationObjectImpl.h b/client/ramses-client/impl/AnimationObjectImpl.h deleted file mode 100644 index 877718238..000000000 --- a/client/ramses-client/impl/AnimationObjectImpl.h +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONOBJECTIMPL_H -#define RAMSES_ANIMATIONOBJECTIMPL_H - -#include "SceneObjectImpl.h" - -namespace ramses_internal -{ - class IAnimationSystem; -} - -namespace ramses -{ - class AnimationSystemImpl; - - class AnimationObjectImpl : public SceneObjectImpl - { - public: - explicit AnimationObjectImpl(AnimationSystemImpl& animationSystem, ERamsesObjectType type, const char* name); - virtual ~AnimationObjectImpl(); - - const AnimationSystemImpl& getAnimationSystemImpl() const; - AnimationSystemImpl& getAnimationSystemImpl(); - - const ramses_internal::IAnimationSystem& getIAnimationSystem() const; - ramses_internal::IAnimationSystem& getIAnimationSystem(); - - private: - AnimationSystemImpl& m_animationSystem; - }; -} - -#endif diff --git a/client/ramses-client/impl/AnimationSequenceImpl.cpp b/client/ramses-client/impl/AnimationSequenceImpl.cpp deleted file mode 100644 index 3c2617646..000000000 --- a/client/ramses-client/impl/AnimationSequenceImpl.cpp +++ /dev/null @@ -1,455 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSequence.h" -#include "AnimationSequenceImpl.h" -#include "AnimationSystemImpl.h" -#include "AnimationImpl.h" -#include "SceneImpl.h" -#include "RamsesObjectTypeUtils.h" -#include "RamsesObjectRegistryIterator.h" -#include "Animation/Animation.h" -#include "SerializationContext.h" - -namespace ramses -{ - AnimationSequenceImpl::AnimationSequenceImpl(AnimationSystemImpl& animationSystem, const char* name) - : AnimationObjectImpl(animationSystem, ERamsesObjectType_AnimationSequence, name) - , m_playbackSpeed(1.f) - { - } - - AnimationSequenceImpl::~AnimationSequenceImpl() - { - } - - void AnimationSequenceImpl::deinitializeFrameworkData() - { - } - - status_t AnimationSequenceImpl::serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const - { - CHECK_RETURN_ERR(AnimationObjectImpl::serialize(outStream, serializationContext)); - - outStream << static_cast(m_animations.size()); - for (const auto& item : m_animations) - { - outStream << item.key; - outStream << item.value.startTime; - outStream << item.value.stopTime; - outStream << item.value.flags; - outStream << item.value.loopDuration; - } - - outStream << m_playbackSpeed; - outStream << m_startTime.getTimeStamp(); - - return StatusOK; - } - - status_t AnimationSequenceImpl::deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) - { - CHECK_RETURN_ERR(AnimationObjectImpl::deserialize(inStream, serializationContext)); - - uint32_t count = 0u; - inStream >> count; - for (uint32_t i = 0u; i < count; ++i) - { - ramses_internal::AnimationHandle handle; - inStream >> handle; - - SequenceItem item; - inStream >> item.startTime; - inStream >> item.stopTime; - inStream >> item.flags; - inStream >> item.loopDuration; - - m_animations.put(handle, item); - } - - inStream >> m_playbackSpeed; - - ramses_internal::AnimationTime::TimeStamp time; - inStream >> time; - m_startTime = time; - - return StatusOK; - } - - status_t AnimationSequenceImpl::validate() const - { - status_t status = AnimationObjectImpl::validate(); - - if (m_animations.size() == 0u) - status = addValidationMessage(EValidationSeverity_Warning, "sequence does not contain any animations"); - - for(const auto& animation : m_animations) - { - const AnimationImpl* anim = findAnimation(animation.key); - if (anim == nullptr) - status = addValidationMessage(EValidationSeverity_Error, "animation contained in sequence does not exist anymore, was probably destroyed but still used by it"); - else - status = std::max(status, addValidationOfDependentObject(*anim)); - } - - RamsesObjectRegistryIterator iter(getAnimationSystemImpl().getObjectRegistry(), ERamsesObjectType_AnimationSequence); - while (const AnimationSequence* sequence = iter.getNext()) - { - const AnimationSequenceImpl& sequenceImpl = sequence->impl; - if (&sequenceImpl != this) - { - for(const auto& animation : m_animations) - { - if (sequenceImpl.m_animations.contains(animation.key)) - status = std::max(status, addValidationMessage(EValidationSeverity_Warning, "animation seems to be contained in more than one sequence, this is fine only if usage of those sequences does not overlap")); - } - } - } - - return status; - } - - status_t AnimationSequenceImpl::addAnimation(const Animation& animation, sequenceTimeStamp_t startTimeInSequence, sequenceTimeStamp_t stopTimeInSequence) - { - const ramses_internal::AnimationHandle handle = animation.impl.getAnimationHandle(); - - if (stopTimeInSequence == 0u) - { - stopTimeInSequence = getAnimationStopTime(handle, startTimeInSequence, m_playbackSpeed); - } - - SequenceItem item; - item.startTime = startTimeInSequence; - item.stopTime = stopTimeInSequence; - item.flags = 0; - item.loopDuration = 0; - m_animations.put(handle, item); - - return StatusOK; - } - - status_t AnimationSequenceImpl::removeAnimation(const Animation& animation) - { - m_animations.remove(animation.impl.getAnimationHandle()); - return StatusOK; - } - - status_t AnimationSequenceImpl::start(timeMilliseconds_t offset) - { - const globalTimeStamp_t startTime = getIAnimationSystem().getTime().getTimeStamp() + offset; - return startAt(startTime, false); - } - - status_t AnimationSequenceImpl::startAt(globalTimeStamp_t timeStamp) - { - return startAt(timeStamp, false); - } - - status_t AnimationSequenceImpl::startReverse(timeMilliseconds_t offset) - { - const globalTimeStamp_t startTime = getIAnimationSystem().getTime().getTimeStamp() + offset; - return startAt(startTime, true); - } - - status_t AnimationSequenceImpl::startReverseAt(globalTimeStamp_t timeStamp) - { - return startAt(timeStamp, true); - } - - status_t AnimationSequenceImpl::startAt(globalTimeStamp_t timeStamp, bool reverse) - { - const ramses_internal::AnimationTime startTime(timeStamp); - - m_startTime = startTime; - - for (auto& item : m_animations) - { - if (reverse) - { - item.value.flags |= ramses_internal::Animation::EAnimationFlags_Reverse; - } - else - { - item.value.flags &= ~ramses_internal::Animation::EAnimationFlags_Reverse; - } - } - - return flushSequenceData(true); - } - - status_t AnimationSequenceImpl::flushSequenceData(bool forceFlush) - { - if (forceFlush || isSequenceActive()) - { - for (const auto& item : m_animations) - { - setAnimationData(item.key, item.value); - } - } - - return StatusOK; - } - - void AnimationSequenceImpl::setAnimationData(ramses_internal::AnimationHandle handle, const SequenceItem& item) - { - getIAnimationSystem().setAnimationStartTime(handle, m_startTime + item.startTime); - getIAnimationSystem().setAnimationStopTime(handle, m_startTime + item.stopTime); - getIAnimationSystem().setAnimationProperties( - handle, - m_playbackSpeed, - item.flags, - item.loopDuration, - getIAnimationSystem().getTime()); - } - - status_t AnimationSequenceImpl::stop(timeMilliseconds_t delay) - { - const timeMilliseconds_t stopTime = getIAnimationSystem().getTime().getTimeStamp() + delay; - return stopAt(stopTime); - } - - status_t AnimationSequenceImpl::stopAt(globalTimeStamp_t timeStamp) - { - if (isSequenceActive()) - { - for (const auto& item : m_animations) - { - getIAnimationSystem().setAnimationStopTime(item.key, timeStamp); - } - } - - return StatusOK; - } - - status_t AnimationSequenceImpl::setPlaybackSpeed(float playbackSpeed) - { - if (playbackSpeed <= 0.f) - { - return addErrorEntry("AnimationSequence::setPlaybackSpeed failed, argument must be greater than zero."); - } - - if (m_playbackSpeed != playbackSpeed) - { - const float invPlaybackSpeed = m_playbackSpeed / playbackSpeed; - m_playbackSpeed = playbackSpeed; - - // adjust start and stop times within sequence - for (auto& item : m_animations) - { - item.value.startTime = static_cast(item.value.startTime * invPlaybackSpeed); - item.value.stopTime = static_cast(item.value.stopTime * invPlaybackSpeed); - } - - if (isSequenceActive()) - { - // adjust sequence start time if it is being played right now - const ramses_internal::AnimationTime& currentGlobalTime = getIAnimationSystem().getTime(); - const ramses_internal::AnimationTime::Duration sinceSequenceStart = currentGlobalTime.getDurationSince(m_startTime); - if (sinceSequenceStart != ramses_internal::AnimationTime::InvalidTimeStamp) - { - const ramses_internal::AnimationTime::Duration newSinceSequenceStart = static_cast(sinceSequenceStart * invPlaybackSpeed); - m_startTime = currentGlobalTime.getTimeStamp() - newSinceSequenceStart; - } - - return flushSequenceData(true); - } - } - - return StatusOK; - } - - float AnimationSequenceImpl::getPlaybackSpeed() const - { - return m_playbackSpeed; - } - - sequenceTimeStamp_t AnimationSequenceImpl::getAnimationStopTime( - ramses_internal::AnimationHandle handle, - sequenceTimeStamp_t startTime, - float playbackSpeed) - { - const ramses_internal::Animation& animation = getIAnimationSystem().getAnimation(handle); - ramses_internal::AnimationTime::Duration animDuration = getIAnimationSystem().getAnimationDurationFromSpline(handle); - animDuration = static_cast(animDuration / playbackSpeed); - const bool looping = (animation.m_flags & ramses_internal::Animation::EAnimationFlags_Looping) != 0; - if (looping) - { - animDuration <<= 20u; - } - const sequenceTimeStamp_t stopTimeInSequence = startTime + static_cast(animDuration); - return stopTimeInSequence; - } - - status_t AnimationSequenceImpl::setAnimationLooping(const Animation& animation, timeMilliseconds_t loopDuration) - { - SequenceItemMap::Iterator it = m_animations.find(animation.impl.getAnimationHandle()); - if (it != m_animations.end()) - { - loopDuration = (loopDuration > 0 ? loopDuration : 0u); - const bool loopEnabled = (it->value.flags & ramses_internal::Animation::EAnimationFlags_Looping) != 0; - if (!loopEnabled || it->value.loopDuration != loopDuration) - { - it->value.loopDuration = loopDuration; - it->value.flags |= ramses_internal::Animation::EAnimationFlags_Looping; - } - - return flushSequenceData(); - } - - return addErrorEntry("AnimationSequence::setAnimationLooping failed, animation is not included in sequence"); - } - - bool AnimationSequenceImpl::isAnimationLooping(const Animation& animation) const - { - SequenceItemMap::ConstIterator it = m_animations.find(animation.impl.getAnimationHandle()); - if (it != m_animations.end()) - { - return (it->value.flags & ramses_internal::Animation::EAnimationFlags_Looping) != 0; - } - - return false; - } - - timeMilliseconds_t AnimationSequenceImpl::getAnimationLoopDuration(const Animation& animation) const - { - SequenceItemMap::ConstIterator it = m_animations.find(animation.impl.getAnimationHandle()); - if (it != m_animations.end()) - { - return it->value.loopDuration; - } - - return 0; - } - - status_t AnimationSequenceImpl::setAnimationRelative(const Animation& animation) - { - SequenceItemMap::Iterator it = m_animations.find(animation.impl.getAnimationHandle()); - if (it != m_animations.end()) - { - if ((it->value.flags & ramses_internal::Animation::EAnimationFlags_Relative) == 0u) - { - it->value.flags |= ramses_internal::Animation::EAnimationFlags_Relative; - } - - return flushSequenceData(); - } - - return addErrorEntry("AnimationSequence::setAnimationRelative failed, animation is not included in sequence"); - } - - status_t AnimationSequenceImpl::setAnimationAbsolute(const Animation& animation) - { - SequenceItemMap::Iterator it = m_animations.find(animation.impl.getAnimationHandle()); - if (it != m_animations.end()) - { - if ((it->value.flags & ramses_internal::Animation::EAnimationFlags_Relative) != 0) - { - it->value.flags &= ~ramses_internal::Animation::EAnimationFlags_Relative; - } - - return flushSequenceData(); - } - - return addErrorEntry("AnimationSequence::setAnimationAbsolute failed, animation is not included in sequence"); - } - - bool AnimationSequenceImpl::isAnimationRelative(const Animation& animation) const - { - SequenceItemMap::ConstIterator it = m_animations.find(animation.impl.getAnimationHandle()); - if (it != m_animations.end()) - { - return (it->value.flags & ramses_internal::Animation::EAnimationFlags_Relative) != 0; - } - - return false; - } - - uint32_t AnimationSequenceImpl::getNumAnimations() const - { - return static_cast(m_animations.size()); - } - - bool AnimationSequenceImpl::containsAnimation(const Animation& animation) const - { - return m_animations.contains(animation.impl.getAnimationHandle()); - } - - sequenceTimeStamp_t AnimationSequenceImpl::getAnimationStartTimeInSequence(const Animation& animation) const - { - SequenceItemMap::ConstIterator it = m_animations.find(animation.impl.getAnimationHandle()); - if (it != m_animations.end()) - { - return it->value.startTime; - } - - return InvalidSequenceTimeStamp; - } - - sequenceTimeStamp_t AnimationSequenceImpl::getAnimationStopTimeInSequence(const Animation& animation) const - { - SequenceItemMap::ConstIterator it = m_animations.find(animation.impl.getAnimationHandle()); - if (it != m_animations.end()) - { - return it->value.stopTime; - } - - return InvalidSequenceTimeStamp; - } - - bool AnimationSequenceImpl::isSequenceActive() const - { - if (!m_startTime.isValid()) - { - return false; - } - - const ramses_internal::AnimationTime& currentTime = getIAnimationSystem().getTime(); - for (const auto& anim : m_animations) - { - // if any animation is active, sequence is active - const ramses_internal::Animation& animation = getIAnimationSystem().getAnimation(anim.key); - if (currentTime.getTimeStamp() >= animation.m_startTime.getTimeStamp() && - currentTime.getTimeStamp() < animation.m_stopTime.getTimeStamp()) - { - return true; - } - } - - return false; - } - - const AnimationImpl* AnimationSequenceImpl::findAnimation(ramses_internal::AnimationHandle handle) const - { - RamsesObjectRegistryIterator iter(getAnimationSystemImpl().getObjectRegistry(), ERamsesObjectType_Animation); - while (const Animation* anim = iter.getNext()) - { - if (anim->impl.getAnimationHandle() == handle) - { - return &anim->impl; - } - } - - return nullptr; - } - - sequenceTimeStamp_t AnimationSequenceImpl::getAnimationSequenceStopTime() const - { - sequenceTimeStamp_t sequenceTime = 0; - for (const auto& item : m_animations) - { - const sequenceTimeStamp_t animationTime = item.value.stopTime; - if (animationTime > sequenceTime) - { - sequenceTime = animationTime; - } - } - - return sequenceTime; - } -} diff --git a/client/ramses-client/impl/AnimationSequenceImpl.h b/client/ramses-client/impl/AnimationSequenceImpl.h deleted file mode 100644 index 654646b0f..000000000 --- a/client/ramses-client/impl/AnimationSequenceImpl.h +++ /dev/null @@ -1,90 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONSEQUENCEIMPL_H -#define RAMSES_ANIMATIONSEQUENCEIMPL_H - -// API -#include "ramses-client-api/AnimationTypes.h" - -// internal -#include "AnimationObjectImpl.h" - -// RAMSES framework -#include "AnimationAPI/IAnimationSystem.h" -#include "Collections/HashMap.h" - -namespace ramses -{ - class Animation; - class SceneImpl; - - struct SequenceItem - { - sequenceTimeStamp_t startTime; - sequenceTimeStamp_t stopTime; - timeMilliseconds_t loopDuration; - ramses_internal::UInt32 flags; - }; - - class AnimationSequenceImpl final : public AnimationObjectImpl - { - public: - explicit AnimationSequenceImpl(AnimationSystemImpl& animationSystem, const char* name); - virtual ~AnimationSequenceImpl() override; - - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t validate() const override; - - status_t addAnimation(const Animation& animation, sequenceTimeStamp_t startTimeInSequence, sequenceTimeStamp_t stopTimeInSequence); - status_t removeAnimation(const Animation& animation); - status_t start(timeMilliseconds_t offset); - status_t startAt(globalTimeStamp_t timeStamp); - status_t startReverse(timeMilliseconds_t offset); - status_t startReverseAt(globalTimeStamp_t timeStamp); - status_t stop(timeMilliseconds_t delay); - status_t stopAt(globalTimeStamp_t timeStamp); - status_t setPlaybackSpeed(float playbackSpeed); - float getPlaybackSpeed() const; - - status_t setAnimationRelative(const Animation& animation); - status_t setAnimationAbsolute(const Animation& animation); - bool isAnimationRelative(const Animation& animation) const; - - status_t setAnimationLooping(const Animation& animation, timeMilliseconds_t loopDuration); - bool isAnimationLooping(const Animation& animation) const; - timeMilliseconds_t getAnimationLoopDuration(const Animation& animation) const; - - uint32_t getNumAnimations() const; - bool containsAnimation(const Animation& animation) const; - sequenceTimeStamp_t getAnimationStartTimeInSequence(const Animation& animation) const; - sequenceTimeStamp_t getAnimationStopTimeInSequence(const Animation& animation) const; - sequenceTimeStamp_t getAnimationSequenceStopTime() const; - - private: - status_t startAt(globalTimeStamp_t timeStamp, bool reverse); - status_t flushSequenceData(bool forceFlush = false); - void setAnimationData(ramses_internal::AnimationHandle handle, const SequenceItem& item); - sequenceTimeStamp_t getAnimationStopTime( - ramses_internal::AnimationHandle handle, - sequenceTimeStamp_t startTime, - float playbackSpeed); - bool isSequenceActive() const; - const AnimationImpl* findAnimation(ramses_internal::AnimationHandle handle) const; - - using SequenceItemMap = ramses_internal::HashMap; - - SequenceItemMap m_animations; - float m_playbackSpeed; - ramses_internal::AnimationTime m_startTime; - }; -} - -#endif diff --git a/client/ramses-client/impl/AnimationSystemData.cpp b/client/ramses-client/impl/AnimationSystemData.cpp deleted file mode 100644 index 602affaba..000000000 --- a/client/ramses-client/impl/AnimationSystemData.cpp +++ /dev/null @@ -1,295 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/SplineStepBool.h" -#include "ramses-client-api/SplineStepInt32.h" -#include "ramses-client-api/SplineStepFloat.h" -#include "ramses-client-api/SplineStepVector2f.h" -#include "ramses-client-api/SplineStepVector3f.h" -#include "ramses-client-api/SplineStepVector4f.h" -#include "ramses-client-api/SplineStepVector2i.h" -#include "ramses-client-api/SplineStepVector3i.h" -#include "ramses-client-api/SplineStepVector4i.h" -#include "ramses-client-api/SplineLinearInt32.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/SplineLinearVector2f.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/SplineLinearVector4f.h" -#include "ramses-client-api/SplineLinearVector2i.h" -#include "ramses-client-api/SplineLinearVector3i.h" -#include "ramses-client-api/SplineLinearVector4i.h" -#include "ramses-client-api/SplineBezierInt32.h" -#include "ramses-client-api/SplineBezierFloat.h" -#include "ramses-client-api/SplineBezierVector2f.h" -#include "ramses-client-api/SplineBezierVector3f.h" -#include "ramses-client-api/SplineBezierVector4f.h" -#include "ramses-client-api/SplineBezierVector2i.h" -#include "ramses-client-api/SplineBezierVector3i.h" -#include "ramses-client-api/SplineBezierVector4i.h" - -#include "AnimationSystemData.h" -#include "AnimationImpl.h" -#include "AnimationSystemImpl.h" -#include "AnimationSequenceImpl.h" -#include "SplineImpl.h" -#include "SerializationContext.h" -#include "RamsesObjectTypeUtils.h" -#include "SerializationHelper.h" -#include "RamsesObjectRegistryIterator.h" - -#include "Scene/ClientScene.h" -#include "Scene/SceneDataBinding.h" -#include "AnimationAPI/IAnimationSystem.h" -#include "Animation/AnimationData.h" - -namespace ramses -{ - AnimationSystemData::AnimationSystemData() - : m_objectRegistry() - , m_animationSystem(nullptr) - { - } - - AnimationSystemData::~AnimationSystemData() - { - RamsesObjectVector objects; - m_objectRegistry.getObjectsOfType(objects, ERamsesObjectType_AnimationObject); - for (const auto it : objects) - { - delete &RamsesObjectTypeUtils::ConvertTo(*it); - } - } - - void AnimationSystemData::initializeFrameworkData(AnimationSystemImpl& animationSystem) - { - m_animationSystem = &animationSystem; - m_animatedPropertyFactory = std::make_unique(animationSystem); - } - - void AnimationSystemData::deinitializeFrameworkData() - { - } - - status_t AnimationSystemData::serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const - { - return SerializationHelper::SerializeObjectsInRegistry(outStream, serializationContext, m_objectRegistry); - } - - template - T& AnimationSystemData::createImplHelper(ERamsesObjectType) - { - return *new T(*m_animationSystem, ""); - } - template <> - SplineImpl& AnimationSystemData::createImplHelper(ERamsesObjectType type) - { - return *new SplineImpl(*m_animationSystem, type, ""); - } - - template - status_t AnimationSystemData::createAndDeserializeObjectImpls(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext, uint32_t count) - { - for (uint32_t i = 0u; i < count; ++i) - { - ObjectImplType& impl = createImplHelper(TYPE_ID_OF_RAMSES_OBJECT::ID); - ObjectIDType objectID = DeserializationContext::GetObjectIDNull(); - const status_t status = SerializationHelper::DeserializeObjectImpl(inStream, serializationContext, impl, objectID); - if (status != StatusOK) - { - delete &impl; - return status; - } - m_objectRegistry.addObject(*new ObjectType(impl)); - } - - return StatusOK; - } - - status_t AnimationSystemData::deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) - { - uint32_t totalCount = 0u; - uint32_t typesCount = 0u; - SerializationHelper::DeserializeNumberOfObjectTypes(inStream, totalCount, typesCount); - - m_objectRegistry.reserveAdditionalGeneralCapacity(totalCount); - for (uint32_t i = 0u; i < typesCount; ++i) - { - uint32_t count = 0u; - const ERamsesObjectType type = SerializationHelper::DeserializeObjectTypeAndCount(inStream, count); - assert(m_objectRegistry.getNumberOfObjects(type) == 0u); - m_objectRegistry.reserveAdditionalObjectCapacity(type, count); - - status_t status = StatusOK; - switch (type) - { - case ERamsesObjectType_SplineStepBool: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineStepFloat: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineStepInt32: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineStepVector2f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineStepVector3f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineStepVector4f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineStepVector2i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineStepVector3i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineStepVector4i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineLinearFloat: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineLinearInt32: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineLinearVector2f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineLinearVector3f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineLinearVector4f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineLinearVector2i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineLinearVector3i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineLinearVector4i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineBezierFloat: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineBezierInt32: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineBezierVector2f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineBezierVector3f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineBezierVector4f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineBezierVector2i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineBezierVector3i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_SplineBezierVector4i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_AnimatedProperty: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_Animation: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_AnimationSequence: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - default: - return m_animationSystem->addErrorEntry("AnimationSystem::deserialize failed, unexpected object type in file stream."); - } - - CHECK_RETURN_ERR(status); - } - - return StatusOK; - } - - Animation* AnimationSystemData::createAnimation(const AnimatedPropertyImpl& animatedProperty, const Spline& spline, const char* name) - { - assert(m_animationSystem != nullptr); - const ramses_internal::EDataTypeID dataBindDataTypeID = animatedProperty.getDataTypeID(); - const ramses_internal::EDataTypeID splineDataTypeID = SplineImpl::GetDataTypeForSplineType(spline.getType()); - if (ramses_internal::AnimationData::CheckDataTypeCompatibility(splineDataTypeID, dataBindDataTypeID, animatedProperty.getVectorComponent())) - { - AnimationImpl& pimpl = *new AnimationImpl(*m_animationSystem, name); - - const ramses_internal::EInterpolationType interpolationType = SplineImpl::GetInterpolationTypeForSplineType(spline.impl.getType()); - pimpl.initializeFrameworkData(animatedProperty, spline.impl.getSplineHandle(), interpolationType); - Animation* animation = new Animation(pimpl); - m_objectRegistry.addObject(*animation); - - return animation; - } - - return nullptr; - } - - AnimationSequence* AnimationSystemData::createAnimationSequence(const char* name) - { - assert(m_animationSystem != nullptr); - AnimationSequenceImpl& pimpl = *new AnimationSequenceImpl(*m_animationSystem, name); - AnimationSequence* sequence = new AnimationSequence(pimpl); - m_objectRegistry.addObject(*sequence); - return sequence; - } - - status_t AnimationSystemData::destroy(AnimationObject& object) - { - if (&object.impl.getAnimationSystemImpl() != m_animationSystem) - { - return m_animationSystem->addErrorEntry("AnimationSystem::destroy failed, object is not in this animation system."); - } - - object.impl.deinitializeFrameworkData(); - m_objectRegistry.removeObject(object); - delete &object; - - return StatusOK; - } - - const RamsesObject* AnimationSystemData::findObjectByName(const char* name) const - { - return m_objectRegistry.findObjectByName(name); - } - - RamsesObject* AnimationSystemData::findObjectByName(const char* name) - { - return m_objectRegistry.findObjectByName(name); - } - - const RamsesObjectRegistry& AnimationSystemData::getObjectRegistry() const - { - return m_objectRegistry; - } - - AnimatedProperty* AnimationSystemData::createAnimatedProperty(const EffectInputImpl& propertyOwner, const AppearanceImpl& appearance, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name) - { - AnimatedProperty* animProperty = m_animatedPropertyFactory->createAnimatedProperty(propertyOwner, appearance, ePropertyComponent, bindID, name); - if (animProperty != nullptr) - { - m_objectRegistry.addObject(*animProperty); - } - - return animProperty; - } -} diff --git a/client/ramses-client/impl/AnimationSystemData.h b/client/ramses-client/impl/AnimationSystemData.h deleted file mode 100644 index 7a44d6bb4..000000000 --- a/client/ramses-client/impl/AnimationSystemData.h +++ /dev/null @@ -1,92 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONSYSTEMDATA_H -#define RAMSES_ANIMATIONSYSTEMDATA_H - -// internal -#include "AnimatedPropertyFactory.h" -#include "SplineImpl.h" -#include "SceneImpl.h" - -// framework -#include "RamsesObjectRegistry.h" -#include - -namespace ramses -{ - class Spline; - class AnimatedProperty; - class Animation; - class AnimationSequence; - class EffectInputImpl; - class AppearanceImpl; - - class AnimationSystemData - { - public: - AnimationSystemData(); - ~AnimationSystemData(); - - void initializeFrameworkData(AnimationSystemImpl& animationSystem); - void deinitializeFrameworkData(); - - status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const; - status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext); - - template - SplineType* createSpline(ramses_internal::EInterpolationType interpolationType, ramses_internal::EDataTypeID dataType, ERamsesObjectType objectType, const char* name); - Animation* createAnimation(const AnimatedPropertyImpl& animatedProperty, const Spline& spline, const char* name); - AnimationSequence* createAnimationSequence(const char* name); - template - AnimatedProperty* createAnimatedProperty(const PropertyOwnerType& propertyOwner, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name); - AnimatedProperty* createAnimatedProperty(const EffectInputImpl& propertyOwner, const AppearanceImpl& appearance, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name); - - status_t destroy(AnimationObject& object); - - const RamsesObject* findObjectByName(const char* name) const; - RamsesObject* findObjectByName(const char* name); - const RamsesObjectRegistry& getObjectRegistry() const; - - private: - template - T& createImplHelper(ERamsesObjectType type); - template - status_t createAndDeserializeObjectImpls(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext, uint32_t count); - - RamsesObjectRegistry m_objectRegistry; - AnimationSystemImpl* m_animationSystem; - - std::unique_ptr m_animatedPropertyFactory; - }; - - template - SplineType* AnimationSystemData::createSpline(ramses_internal::EInterpolationType interpolationType, ramses_internal::EDataTypeID dataType, ERamsesObjectType objectType, const char* name) - { - assert(m_animationSystem != nullptr); - SplineImpl& splineImpl = *new SplineImpl(*m_animationSystem, objectType, name); - splineImpl.initializeFrameworkData(interpolationType, dataType); - SplineType* spline = new SplineType(splineImpl); - m_objectRegistry.addObject(*spline); - return spline; - } - - template - AnimatedProperty* AnimationSystemData::createAnimatedProperty(const PropertyOwnerType& propertyOwner, EAnimatedPropertyComponent ePropertyComponent, ramses_internal::TDataBindID bindID, const char* name) - { - AnimatedProperty* animProperty = m_animatedPropertyFactory->createAnimatedProperty(propertyOwner, ePropertyComponent, bindID, name); - if (animProperty != nullptr) - { - m_objectRegistry.addObject(*animProperty); - } - - return animProperty; - } -} - -#endif diff --git a/client/ramses-client/impl/AnimationSystemImpl.cpp b/client/ramses-client/impl/AnimationSystemImpl.cpp deleted file mode 100644 index 2438be909..000000000 --- a/client/ramses-client/impl/AnimationSystemImpl.cpp +++ /dev/null @@ -1,498 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/SplineStepBool.h" -#include "ramses-client-api/SplineStepInt32.h" -#include "ramses-client-api/SplineStepFloat.h" -#include "ramses-client-api/SplineStepVector2f.h" -#include "ramses-client-api/SplineStepVector3f.h" -#include "ramses-client-api/SplineStepVector4f.h" -#include "ramses-client-api/SplineStepVector2i.h" -#include "ramses-client-api/SplineStepVector3i.h" -#include "ramses-client-api/SplineStepVector4i.h" -#include "ramses-client-api/SplineLinearInt32.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/SplineLinearVector2f.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/SplineLinearVector4f.h" -#include "ramses-client-api/SplineLinearVector2i.h" -#include "ramses-client-api/SplineLinearVector3i.h" -#include "ramses-client-api/SplineLinearVector4i.h" -#include "ramses-client-api/SplineBezierInt32.h" -#include "ramses-client-api/SplineBezierFloat.h" -#include "ramses-client-api/SplineBezierVector2f.h" -#include "ramses-client-api/SplineBezierVector3f.h" -#include "ramses-client-api/SplineBezierVector4f.h" -#include "ramses-client-api/SplineBezierVector2i.h" -#include "ramses-client-api/SplineBezierVector3i.h" -#include "ramses-client-api/SplineBezierVector4i.h" -#include "ramses-client-api/AnimatedProperty.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/UniformInput.h" -#include "ramses-client-api/DataObject.h" -#include "ramses-client-api/Appearance.h" - -#include "AnimationSystemImpl.h" -#include "SceneImpl.h" -#include "SplineImpl.h" -#include "AnimationImpl.h" -#include "DataObjectImpl.h" -#include "AppearanceImpl.h" -#include "EffectImpl.h" -#include "EffectInputImpl.h" -#include "NodeImpl.h" -#include "RamsesObjectTypeUtils.h" -#include "RamsesObjectRegistryIterator.h" - -#include "Scene/ClientScene.h" -#include "Scene/SceneDataBinding.h" -#include "Animation/AnimationSystem.h" -#include "AnimationAPI/IAnimationSystem.h" - -#include "PlatformAbstraction/PlatformTime.h" -#include "Utils/LogMacros.h" - -namespace ramses -{ - AnimationSystemImpl::AnimationSystemImpl(SceneImpl& sceneImpl, ERamsesObjectType type, const char* name) - : SceneObjectImpl(sceneImpl, type, name) - , m_animationSystem(nullptr) - , m_animationSystemHandle(ramses_internal::AnimationSystemHandle::Invalid()) - { - } - - AnimationSystemImpl::~AnimationSystemImpl() - { - } - - void AnimationSystemImpl::initializeFrameworkData(ramses_internal::IAnimationSystem& animationSystem) - { - m_data.initializeFrameworkData(*this); - m_animationSystemHandle = getIScene().addAnimationSystem(&animationSystem); - m_animationSystem = &animationSystem; - m_animationSystem->registerAnimationLogicListener(&m_animationStateCollector); - } - - void AnimationSystemImpl::deinitializeFrameworkData() - { - assert(m_animationSystem != nullptr); - m_animationSystem->unregisterAnimationLogicListener(&m_animationStateCollector); - m_data.deinitializeFrameworkData(); - getIScene().removeAnimationSystem(m_animationSystemHandle); - } - - status_t AnimationSystemImpl::serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const - { - CHECK_RETURN_ERR(SceneObjectImpl::serialize(outStream, serializationContext)); - - assert(m_animationSystem != nullptr); - outStream << m_animationSystemHandle; - - return m_data.serialize(outStream, serializationContext); - } - - status_t AnimationSystemImpl::deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) - { - CHECK_RETURN_ERR(SceneObjectImpl::deserialize(inStream, serializationContext)); - - inStream >> m_animationSystemHandle; - - m_animationSystem = getIScene().getAnimationSystem(m_animationSystemHandle); - if (m_animationSystem == nullptr) - { - return addErrorEntry("AnimationSystem deserialize failed: could not retrieve animation system data!"); - } - m_animationSystem->registerAnimationLogicListener(&m_animationStateCollector); - - m_data.initializeFrameworkData(*this); - CHECK_RETURN_ERR(m_data.deserialize(inStream, serializationContext)); - - return StatusOK; - } - - status_t AnimationSystemImpl::validate() const - { - status_t status = ClientObjectImpl::validate(); - - uint32_t objectCount[ERamsesObjectType_NUMBER_OF_TYPES]; - for (uint32_t i = 0u; i < ERamsesObjectType_NUMBER_OF_TYPES; ++i) - { - const ERamsesObjectType type = static_cast(i); - if (RamsesObjectTypeUtils::IsTypeMatchingBaseType(type, ERamsesObjectType_AnimationObject) && - RamsesObjectTypeUtils::IsConcreteType(type)) - { - objectCount[i] = 0u; - RamsesObjectRegistryIterator iter(getObjectRegistry(), ERamsesObjectType(i)); - while (const RamsesObject* obj = iter.getNext()) - { - status = std::max(status, addValidationOfDependentObject(obj->impl)); - ++objectCount[i]; - } - } - } - - for (uint32_t i = 0u; i < ERamsesObjectType_NUMBER_OF_TYPES; ++i) - { - const ERamsesObjectType type = static_cast(i); - if (RamsesObjectTypeUtils::IsTypeMatchingBaseType(type, ERamsesObjectType_AnimationObject) && - RamsesObjectTypeUtils::IsConcreteType(type)) - { - ramses_internal::StringOutputStream msg; - msg << "Number of " << RamsesObjectTypeUtils::GetRamsesObjectTypeName(type) << " instances: " << objectCount[i]; - addValidationMessage(EValidationSeverity_Info, ramses_internal::String(msg.release())); - } - } - - return status; - } - - status_t AnimationSystemImpl::setTime(globalTimeStamp_t timeStamp) - { - m_animationStateCollector.resetCollections(); - assert(m_animationSystem != nullptr); - m_animationSystem->setTime(ramses_internal::AnimationTime(timeStamp)); - return StatusOK; - } - - globalTimeStamp_t AnimationSystemImpl::getTime() const - { - assert(m_animationSystem != nullptr); - return m_animationSystem->getTime().getTimeStamp(); - } - - status_t AnimationSystemImpl::updateLocalTime(globalTimeStamp_t systemTime) - { - ramses_internal::AnimationTime timeStamp = ramses_internal::AnimationTime(systemTime); - if (systemTime == 0u) - { - if (m_animationSystem->useSynchronizedClock()) - { - timeStamp = ramses_internal::PlatformTime::GetMillisecondsSynchronized(); - } - else - { - timeStamp = ramses_internal::PlatformTime::GetMillisecondsAbsolute(); - } - } - - m_animationStateCollector.resetCollections(); - assert(m_animationSystem != nullptr); - ramses_internal::AnimationSystem& nonDistributedAnimationSystem = static_cast(*m_animationSystem); - nonDistributedAnimationSystem.ramses_internal::AnimationSystem::setTime(timeStamp); - - return StatusOK; - } - - SplineStepBool* AnimationSystemImpl::createSplineStepBool(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Step, ramses_internal::EDataTypeID_Boolean, ERamsesObjectType_SplineStepBool, name); - } - - SplineStepInt32* AnimationSystemImpl::createSplineStepInt32(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Step, ramses_internal::EDataTypeID_Int32, ERamsesObjectType_SplineStepInt32, name); - } - - SplineStepFloat* AnimationSystemImpl::createSplineStepFloat(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Step, ramses_internal::EDataTypeID_Float, ERamsesObjectType_SplineStepFloat, name); - } - - SplineStepVector2f* AnimationSystemImpl::createSplineStepVector2f(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Step, ramses_internal::EDataTypeID_Vector2f, ERamsesObjectType_SplineStepVector2f, name); - } - - SplineStepVector3f* AnimationSystemImpl::createSplineStepVector3f(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Step, ramses_internal::EDataTypeID_Vector3f, ERamsesObjectType_SplineStepVector3f, name); - } - - SplineStepVector4f* AnimationSystemImpl::createSplineStepVector4f(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Step, ramses_internal::EDataTypeID_Vector4f, ERamsesObjectType_SplineStepVector4f, name); - } - - SplineStepVector2i* AnimationSystemImpl::createSplineStepVector2i(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Step, ramses_internal::EDataTypeID_Vector2i, ERamsesObjectType_SplineStepVector2i, name); - } - - SplineStepVector3i* AnimationSystemImpl::createSplineStepVector3i(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Step, ramses_internal::EDataTypeID_Vector3i, ERamsesObjectType_SplineStepVector3i, name); - } - - SplineStepVector4i* AnimationSystemImpl::createSplineStepVector4i(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Step, ramses_internal::EDataTypeID_Vector4i, ERamsesObjectType_SplineStepVector4i, name); - } - - SplineLinearInt32* AnimationSystemImpl::createSplineLinearInt32(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Linear, ramses_internal::EDataTypeID_Int32, ERamsesObjectType_SplineLinearInt32, name); - } - - SplineLinearFloat* AnimationSystemImpl::createSplineLinearFloat(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Linear, ramses_internal::EDataTypeID_Float, ERamsesObjectType_SplineLinearFloat, name); - } - - SplineLinearVector2f* AnimationSystemImpl::createSplineLinearVector2f(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Linear, ramses_internal::EDataTypeID_Vector2f, ERamsesObjectType_SplineLinearVector2f, name); - } - - SplineLinearVector3f* AnimationSystemImpl::createSplineLinearVector3f(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Linear, ramses_internal::EDataTypeID_Vector3f, ERamsesObjectType_SplineLinearVector3f, name); - } - - SplineLinearVector4f* AnimationSystemImpl::createSplineLinearVector4f(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Linear, ramses_internal::EDataTypeID_Vector4f, ERamsesObjectType_SplineLinearVector4f, name); - } - - SplineLinearVector2i* AnimationSystemImpl::createSplineLinearVector2i(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Linear, ramses_internal::EDataTypeID_Vector2i, ERamsesObjectType_SplineLinearVector2i, name); - } - - SplineLinearVector3i* AnimationSystemImpl::createSplineLinearVector3i(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Linear, ramses_internal::EDataTypeID_Vector3i, ERamsesObjectType_SplineLinearVector3i, name); - } - - SplineLinearVector4i* AnimationSystemImpl::createSplineLinearVector4i(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Linear, ramses_internal::EDataTypeID_Vector4i, ERamsesObjectType_SplineLinearVector4i, name); - } - - SplineBezierInt32* AnimationSystemImpl::createSplineBezierInt32(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Bezier, ramses_internal::EDataTypeID_Int32, ERamsesObjectType_SplineBezierInt32, name); - } - - SplineBezierFloat* AnimationSystemImpl::createSplineBezierFloat(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Bezier, ramses_internal::EDataTypeID_Float, ERamsesObjectType_SplineBezierFloat, name); - } - - SplineBezierVector2f* AnimationSystemImpl::createSplineBezierVector2f(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Bezier, ramses_internal::EDataTypeID_Vector2f, ERamsesObjectType_SplineBezierVector2f, name); - } - - SplineBezierVector3f* AnimationSystemImpl::createSplineBezierVector3f(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Bezier, ramses_internal::EDataTypeID_Vector3f, ERamsesObjectType_SplineBezierVector3f, name); - } - - SplineBezierVector4f* AnimationSystemImpl::createSplineBezierVector4f(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Bezier, ramses_internal::EDataTypeID_Vector4f, ERamsesObjectType_SplineBezierVector4f, name); - } - - SplineBezierVector2i* AnimationSystemImpl::createSplineBezierVector2i(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Bezier, ramses_internal::EDataTypeID_Vector2i, ERamsesObjectType_SplineBezierVector2i, name); - } - - SplineBezierVector3i* AnimationSystemImpl::createSplineBezierVector3i(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Bezier, ramses_internal::EDataTypeID_Vector3i, ERamsesObjectType_SplineBezierVector3i, name); - } - - SplineBezierVector4i* AnimationSystemImpl::createSplineBezierVector4i(const char* name) - { - return m_data.createSpline(ramses_internal::EInterpolationType_Bezier, ramses_internal::EDataTypeID_Vector4i, ERamsesObjectType_SplineBezierVector4i, name); - } - - Animation* AnimationSystemImpl::createAnimation(const AnimatedProperty& animatedProperty, const Spline& spline, const char* name) - { - if (!containsAnimationObject(animatedProperty.impl) || - !containsAnimationObject(spline.impl)) - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "AnimationSystem::createAnimation: failed to create Animation, provided property and/or spline belong to another AnimationSystem!"); - return nullptr; - } - - return m_data.createAnimation(animatedProperty.impl, spline, name); - } - - AnimationSequence* AnimationSystemImpl::createAnimationSequence(const char* name) - { - return m_data.createAnimationSequence(name); - } - - AnimatedProperty* AnimationSystemImpl::createAnimatedProperty(const NodeImpl& propertyOwner, EAnimatedProperty property, EAnimatedPropertyComponent propertyComponent, const char* name) - { - using ContainerTraitsClass = ramses_internal::DataBindContainerToTraitsSelector::ContainerTraitsClassType; - ramses_internal::TDataBindID dataBindID(std::numeric_limits::max()); - switch (property) - { - case EAnimatedProperty_Translation: - dataBindID = ContainerTraitsClass::TransformNode_Translation; - break; - case EAnimatedProperty_Rotation: - dataBindID = ContainerTraitsClass::TransformNode_Rotation; - break; - case EAnimatedProperty_Scaling: - dataBindID = ContainerTraitsClass::TransformNode_Scaling; - break; - } - - // (Violin) this cast is necessary because the animation needs to force creation of the internal - // transform so that it tries to change it by direct access to LL handle - // The actual problem is that the propertyOwner is const - it shouldn't (it is being changed in the animation) - const_cast(propertyOwner).initializeTransform(); - - return m_data.createAnimatedProperty(propertyOwner, propertyComponent, dataBindID, name); - } - - AnimatedProperty* AnimationSystemImpl::createAnimatedProperty(const UniformInput& propertyOwner, const Appearance& appearance, EAnimatedPropertyComponent propertyComponent, const char* name) - { - if (!propertyOwner.isValid() || propertyOwner.impl.getEffectHash() != appearance.impl.getEffectImpl()->getLowlevelResourceHash()) - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "AnimationSystem::createAnimatedProperty: failed to create AnimatedProperty, uniform input invalid or does not match provided Appearance!"); - return nullptr; - } - - ramses_internal::TDataBindID bindId = 0u; - if (!GetDataBindIDForDataType(propertyOwner.impl.getDataType(), bindId)) - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "AnimationSystem::createAnimatedProperty: failed to create AnimatedProperty, unsupported data type to animate!"); - return nullptr; - } - - return m_data.createAnimatedProperty(propertyOwner.impl, appearance.impl, propertyComponent, bindId, name); - } - - ramses::AnimatedProperty* AnimationSystemImpl::createAnimatedProperty(const DataObject& propertyOwner, EAnimatedPropertyComponent propertyComponent, const char* name) - { - ramses_internal::TDataBindID bindId = 0u; - if (!GetDataBindIDForDataType(propertyOwner.impl.getDataType(), bindId)) - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "AnimationSystem::createAnimatedProperty: failed to create AnimatedProperty, unsupported data type to animate!"); - return nullptr; - } - - return m_data.createAnimatedProperty(propertyOwner.impl, propertyComponent, bindId, name); - } - - status_t AnimationSystemImpl::destroy(AnimationObject& animationObject) - { - return m_data.destroy(animationObject); - } - - ramses_internal::IAnimationSystem& AnimationSystemImpl::getIAnimationSystem() - { - assert(m_animationSystem != nullptr); - return *m_animationSystem; - } - - ramses_internal::AnimationSystemHandle AnimationSystemImpl::getAnimationSystemHandle() const - { - assert(m_animationSystemHandle.isValid()); - return m_animationSystemHandle; - } - - bool AnimationSystemImpl::containsAnimationObject(const AnimationObjectImpl& object) const - { - return &object.getAnimationSystemImpl() == this; - } - - const RamsesObject* AnimationSystemImpl::findObjectByName(const char* name) const - { - return m_data.findObjectByName(name); - } - - RamsesObject* AnimationSystemImpl::findObjectByName(const char* name) - { - return m_data.findObjectByName(name); - } - - const RamsesObjectRegistry& AnimationSystemImpl::getObjectRegistry() const - { - return m_data.getObjectRegistry(); - } - - uint32_t AnimationSystemImpl::getNumberOfFinishedAnimationsSincePreviousUpdate() const - { - return static_cast(m_animationStateCollector.getCollectedFinishedAnimations().size()); - } - - const Animation* AnimationSystemImpl::getFinishedAnimationSincePreviousUpdate(uint32_t index) const - { - const ramses_internal::AnimationHandleVector& animHandles = m_animationStateCollector.getCollectedFinishedAnimations(); - if (index < animHandles.size()) - { - return findAnimationByHandle(animHandles[index]); - } - - return nullptr; - } - - Animation* AnimationSystemImpl::getFinishedAnimationSincePreviousUpdate(uint32_t index) - { - // non-const version of getFinishedAnimationSincePreviousUpdate cast to its const version to avoid duplicating code - return const_cast((const_cast(*this)).getFinishedAnimationSincePreviousUpdate(index)); - } - - const Animation* AnimationSystemImpl::findAnimationByHandle(ramses_internal::AnimationHandle handle) const - { - RamsesObjectRegistryIterator iter(getObjectRegistry(), ERamsesObjectType_Animation); - while (const Animation* animation = iter.getNext()) - { - if (animation->impl.getAnimationHandle() == handle) - { - return animation; - } - } - - return nullptr; - } - - bool AnimationSystemImpl::GetDataBindIDForDataType(ramses_internal::EDataType dataType, ramses_internal::TDataBindID& bindId) - { - using ContainerTraitsClass = ramses_internal::DataBindContainerToTraitsSelector::ContainerTraitsClassType; - - switch (dataType) - { - case ramses_internal::EDataType::Float: - bindId = ContainerTraitsClass::DataField_Float; - break; - case ramses_internal::EDataType::Vector2F: - bindId = ContainerTraitsClass::DataField_Vector2f; - break; - case ramses_internal::EDataType::Vector3F: - bindId = ContainerTraitsClass::DataField_Vector3f; - break; - case ramses_internal::EDataType::Vector4F: - bindId = ContainerTraitsClass::DataField_Vector4f; - break; - case ramses_internal::EDataType::Int32: - bindId = ContainerTraitsClass::DataField_Integer; - break; - case ramses_internal::EDataType::Vector2I: - bindId = ContainerTraitsClass::DataField_Vector2i; - break; - case ramses_internal::EDataType::Vector3I: - bindId = ContainerTraitsClass::DataField_Vector3i; - break; - case ramses_internal::EDataType::Vector4I: - bindId = ContainerTraitsClass::DataField_Vector4i; - break; - default: - return false; - } - - return true; - } -} diff --git a/client/ramses-client/impl/AnimationSystemImpl.h b/client/ramses-client/impl/AnimationSystemImpl.h deleted file mode 100644 index 2c05abcf2..000000000 --- a/client/ramses-client/impl/AnimationSystemImpl.h +++ /dev/null @@ -1,139 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONSYSTEMIMPL_H -#define RAMSES_ANIMATIONSYSTEMIMPL_H - -// internal -#include "SceneObjectImpl.h" -#include "AnimationSystemData.h" - -// framework -#include "Animation/AnimationStateChangeCollector.h" - -namespace ramses_internal -{ - class IAnimationSystem; -} - -namespace ramses -{ - class Spline; - class SplineStepBool; - class SplineStepInt32; - class SplineStepFloat; - class SplineStepVector2f; - class SplineStepVector3f; - class SplineStepVector4f; - class SplineStepVector2i; - class SplineStepVector3i; - class SplineStepVector4i; - class SplineLinearInt32; - class SplineLinearFloat; - class SplineLinearVector2f; - class SplineLinearVector3f; - class SplineLinearVector4f; - class SplineLinearVector2i; - class SplineLinearVector3i; - class SplineLinearVector4i; - class SplineBezierInt32; - class SplineBezierFloat; - class SplineBezierVector2f; - class SplineBezierVector3f; - class SplineBezierVector4f; - class SplineBezierVector2i; - class SplineBezierVector3i; - class SplineBezierVector4i; - class AnimatedProperty; - class Animation; - class AnimationSequence; - class SplineImpl; - class AnimationImpl; - class SceneImpl; - class UniformInput; - class DataObject; - class Appearance; - class NodeImpl; - - class AnimationSystemImpl final : public SceneObjectImpl - { - public: - AnimationSystemImpl(SceneImpl& sceneImpl, ERamsesObjectType type, const char* name); - virtual ~AnimationSystemImpl() override; - - void initializeFrameworkData(ramses_internal::IAnimationSystem& animationSystem); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t validate() const override; - - status_t setTime(globalTimeStamp_t timeStamp); - globalTimeStamp_t getTime() const; - status_t updateLocalTime(globalTimeStamp_t systemTime); - - SplineStepBool* createSplineStepBool(const char* name); - SplineStepInt32* createSplineStepInt32(const char* name); - SplineStepFloat* createSplineStepFloat(const char* name); - SplineStepVector2f* createSplineStepVector2f(const char* name); - SplineStepVector3f* createSplineStepVector3f(const char* name); - SplineStepVector4f* createSplineStepVector4f(const char* name); - SplineStepVector2i* createSplineStepVector2i(const char* name); - SplineStepVector3i* createSplineStepVector3i(const char* name); - SplineStepVector4i* createSplineStepVector4i(const char* name); - SplineLinearInt32* createSplineLinearInt32(const char* name); - SplineLinearFloat* createSplineLinearFloat(const char* name); - SplineLinearVector2f* createSplineLinearVector2f(const char* name); - SplineLinearVector3f* createSplineLinearVector3f(const char* name); - SplineLinearVector4f* createSplineLinearVector4f(const char* name); - SplineLinearVector2i* createSplineLinearVector2i(const char* name); - SplineLinearVector3i* createSplineLinearVector3i(const char* name); - SplineLinearVector4i* createSplineLinearVector4i(const char* name); - SplineBezierInt32* createSplineBezierInt32(const char* name); - SplineBezierFloat* createSplineBezierFloat(const char* name); - SplineBezierVector2f* createSplineBezierVector2f(const char* name); - SplineBezierVector3f* createSplineBezierVector3f(const char* name); - SplineBezierVector4f* createSplineBezierVector4f(const char* name); - SplineBezierVector2i* createSplineBezierVector2i(const char* name); - SplineBezierVector3i* createSplineBezierVector3i(const char* name); - SplineBezierVector4i* createSplineBezierVector4i(const char* name); - - Animation* createAnimation(const AnimatedProperty& animatedProperty, const Spline& spline, const char* name); - AnimationSequence* createAnimationSequence(const char* name); - - AnimatedProperty* createAnimatedProperty(const NodeImpl& propertyOwner, EAnimatedProperty property, EAnimatedPropertyComponent propertyComponent, const char* name); - AnimatedProperty* createAnimatedProperty(const UniformInput& propertyOwner, const Appearance& appearance, EAnimatedPropertyComponent propertyComponent, const char* name); - AnimatedProperty* createAnimatedProperty(const DataObject& propertyOwner, EAnimatedPropertyComponent propertyComponent, const char* name); - - status_t destroy(AnimationObject& animationObject); - - ramses_internal::IAnimationSystem& getIAnimationSystem(); - ramses_internal::AnimationSystemHandle getAnimationSystemHandle() const; - - bool containsAnimationObject(const AnimationObjectImpl& object) const; - const RamsesObject* findObjectByName(const char* name) const; - RamsesObject* findObjectByName(const char* name); - const RamsesObjectRegistry& getObjectRegistry() const; - - uint32_t getNumberOfFinishedAnimationsSincePreviousUpdate() const; - const Animation* getFinishedAnimationSincePreviousUpdate(uint32_t index) const; - Animation* getFinishedAnimationSincePreviousUpdate(uint32_t index); - - private: - const Animation* findAnimationByHandle(ramses_internal::AnimationHandle handle) const; - - static bool GetDataBindIDForDataType(ramses_internal::EDataType dataType, ramses_internal::TDataBindID& bindId); - - ramses_internal::IAnimationSystem* m_animationSystem; - ramses_internal::AnimationSystemHandle m_animationSystemHandle; - ramses_internal::AnimationStateChangeCollector m_animationStateCollector; - - AnimationSystemData m_data; - }; -} - -#endif diff --git a/client/ramses-client/impl/AnimationSystemIteratorImpl.h b/client/ramses-client/impl/AnimationSystemIteratorImpl.h deleted file mode 100644 index 97c99c424..000000000 --- a/client/ramses-client/impl/AnimationSystemIteratorImpl.h +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONSYSTEMITERATORIMPL_H -#define RAMSES_ANIMATIONSYSTEMITERATORIMPL_H - -#include "ramses-client-api/RamsesObjectTypes.h" -#include "ObjectIteratorImpl.h" -#include "AnimationSystemImpl.h" - -namespace ramses -{ - class RamsesObject; - - class AnimationSystemIteratorImpl : public ObjectIteratorImpl - { - public: - AnimationSystemIteratorImpl(const AnimationSystemImpl& animSystem, ERamsesObjectType filterType) - : ObjectIteratorImpl(animSystem.getObjectRegistry(), filterType) - { - } - }; -} - -#endif diff --git a/client/ramses-client/impl/AnimationSystemObjectIterator.cpp b/client/ramses-client/impl/AnimationSystemObjectIterator.cpp deleted file mode 100644 index ff8acd27b..000000000 --- a/client/ramses-client/impl/AnimationSystemObjectIterator.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/AnimationSystemObjectIterator.h" -#include "AnimationSystemIteratorImpl.h" -#include "SceneImpl.h" -#include "ramses-client-api/AnimationSystem.h" -#include "AnimationSystemImpl.h" - -namespace ramses -{ - - AnimationSystemObjectIterator::AnimationSystemObjectIterator(const AnimationSystem& animationSystem, ERamsesObjectType objectType) - : impl(new AnimationSystemIteratorImpl(animationSystem.impl, objectType)) - { - } - - AnimationSystemObjectIterator::~AnimationSystemObjectIterator() - { - delete impl; - } - - RamsesObject* AnimationSystemObjectIterator::getNext() - { - return impl->getNext(); - } -} diff --git a/client/ramses-client/impl/AppearanceImpl.cpp b/client/ramses-client/impl/AppearanceImpl.cpp index 322fe4b58..e04439f4b 100644 --- a/client/ramses-client/impl/AppearanceImpl.cpp +++ b/client/ramses-client/impl/AppearanceImpl.cpp @@ -28,15 +28,15 @@ #include "SceneUtils/DataLayoutCreationHelper.h" #include "SceneUtils/ISceneDataArrayAccessor.h" #include "SceneUtils/DataInstanceHelper.h" -#include "Math3d/Matrix22f.h" #include "SceneAPI/EDataType.h" #include "ObjectIteratorImpl.h" +#include "DataTypeUtils.h" #include namespace ramses { - AppearanceImpl::AppearanceImpl(SceneImpl& scene, const char* appearancename) - : SceneObjectImpl(scene, ERamsesObjectType_Appearance, appearancename) + AppearanceImpl::AppearanceImpl(SceneImpl& scene, std::string_view appearancename) + : SceneObjectImpl(scene, ERamsesObjectType::Appearance, appearancename) , m_effectImpl(nullptr) { } @@ -47,34 +47,28 @@ namespace ramses status_t AppearanceImpl::setBlendingFactors(EBlendFactor srcColor, EBlendFactor destColor, EBlendFactor srcAlpha, EBlendFactor destAlpha) { - getIScene().setRenderStateBlendFactors(m_renderStateHandle, - AppearanceUtils::GetBlendFactorInternal(srcColor), - AppearanceUtils::GetBlendFactorInternal(destColor), - AppearanceUtils::GetBlendFactorInternal(srcAlpha), - AppearanceUtils::GetBlendFactorInternal(destAlpha)); + getIScene().setRenderStateBlendFactors(m_renderStateHandle, srcColor, destColor, srcAlpha, destAlpha); return StatusOK; } ramses::status_t AppearanceImpl::getBlendingFactors(EBlendFactor& srcColor, EBlendFactor& destColor, EBlendFactor& srcAlpha, EBlendFactor& destAlpha) const { const ramses_internal::RenderState& rs = getIScene().getRenderState(m_renderStateHandle); - srcColor = AppearanceUtils::GetBlendFactorFromInternal(rs.blendFactorSrcColor); - destColor = AppearanceUtils::GetBlendFactorFromInternal(rs.blendFactorDstColor); - srcAlpha = AppearanceUtils::GetBlendFactorFromInternal(rs.blendFactorSrcAlpha); - destAlpha = AppearanceUtils::GetBlendFactorFromInternal(rs.blendFactorDstAlpha); + srcColor = rs.blendFactorSrcColor; + destColor = rs.blendFactorDstColor; + srcAlpha = rs.blendFactorSrcAlpha; + destAlpha = rs.blendFactorDstAlpha; return StatusOK; } status_t AppearanceImpl::setBlendingOperations(EBlendOperation operationColor, EBlendOperation operationAlpha) { - if ((operationColor == EBlendOperation_Disabled) != (operationAlpha == EBlendOperation_Disabled)) + if ((operationColor == EBlendOperation::Disabled) != (operationAlpha == EBlendOperation::Disabled)) { return addErrorEntry("Appearance::setBlendingOperations: invalid combination - one of operationColor or operationAlpha disabled and the other not!"); } - getIScene().setRenderStateBlendOperations(m_renderStateHandle, - AppearanceUtils::GetBlendOperationInternal(operationColor), - AppearanceUtils::GetBlendOperationInternal(operationAlpha)); + getIScene().setRenderStateBlendOperations(m_renderStateHandle, operationColor, operationAlpha); return StatusOK; } @@ -82,61 +76,56 @@ namespace ramses status_t AppearanceImpl::getBlendingOperations(EBlendOperation& operationColor, EBlendOperation& operationAlpha) const { const ramses_internal::RenderState& rs = getIScene().getRenderState(m_renderStateHandle); - operationColor = AppearanceUtils::GetBlendOperationFromInternal(rs.blendOperationColor); - operationAlpha = AppearanceUtils::GetBlendOperationFromInternal(rs.blendOperationAlpha); + operationColor = rs.blendOperationColor; + operationAlpha = rs.blendOperationAlpha; return StatusOK; } - ramses::status_t AppearanceImpl::setBlendingColor(float red, float green, float blue, float alpha) + ramses::status_t AppearanceImpl::setBlendingColor(const vec4f& color) { - getIScene().setRenderStateBlendColor(m_renderStateHandle, { red, green, blue, alpha }); + getIScene().setRenderStateBlendColor(m_renderStateHandle, color); return StatusOK; } - ramses::status_t AppearanceImpl::getBlendingColor(float& red, float& green, float& blue, float& alpha) const + ramses::status_t AppearanceImpl::getBlendingColor(vec4f& color) const { - const ramses_internal::RenderState& rs = getIScene().getRenderState(m_renderStateHandle); - red = rs.blendColor.r; - green = rs.blendColor.g; - blue = rs.blendColor.b; - alpha = rs.blendColor.a; - + color = getIScene().getRenderState(m_renderStateHandle).blendColor; return StatusOK; } status_t AppearanceImpl::setDepthFunction(EDepthFunc func) { - getIScene().setRenderStateDepthFunc(m_renderStateHandle, AppearanceUtils::GetDepthFuncInternal(func)); + getIScene().setRenderStateDepthFunc(m_renderStateHandle, func); return StatusOK; } status_t AppearanceImpl::getDepthFunction(EDepthFunc& func) const { - func = AppearanceUtils::GetDepthFuncFromInternal(getIScene().getRenderState(m_renderStateHandle).depthFunc); + func = getIScene().getRenderState(m_renderStateHandle).depthFunc; return StatusOK; } status_t AppearanceImpl::setDepthWrite(EDepthWrite flag) { - getIScene().setRenderStateDepthWrite(m_renderStateHandle, AppearanceUtils::GetDepthWriteInternal(flag)); + getIScene().setRenderStateDepthWrite(m_renderStateHandle, flag); return StatusOK; } ramses::status_t AppearanceImpl::getDepthWriteMode(EDepthWrite& mode) const { - mode = AppearanceUtils::GetDepthWriteFromInternal(getIScene().getRenderState(m_renderStateHandle).depthWrite); + mode = getIScene().getRenderState(m_renderStateHandle).depthWrite; return StatusOK; } status_t AppearanceImpl::setScissorTest(EScissorTest flag, int16_t x, int16_t y, uint16_t width, uint16_t height) { - getIScene().setRenderStateScissorTest(m_renderStateHandle, AppearanceUtils::GetScissorTestInternal(flag), { x, y, width, height }); + getIScene().setRenderStateScissorTest(m_renderStateHandle, flag, { x, y, width, height }); return StatusOK; } status_t AppearanceImpl::getScissorTestState(EScissorTest& mode) const { - mode = AppearanceUtils::GetScissorTestFromInternal(getIScene().getRenderState(m_renderStateHandle).scissorTest); + mode = getIScene().getRenderState(m_renderStateHandle).scissorTest; return StatusOK; } @@ -153,14 +142,14 @@ namespace ramses status_t AppearanceImpl::setStencilFunc(EStencilFunc func, uint8_t ref, uint8_t mask) { - getIScene().setRenderStateStencilFunc(m_renderStateHandle, AppearanceUtils::GetStencilFuncInternal(func), ref, mask); + getIScene().setRenderStateStencilFunc(m_renderStateHandle, func, ref, mask); return StatusOK; } status_t AppearanceImpl::getStencilFunc(EStencilFunc& func, uint8_t& ref, uint8_t& mask) const { const ramses_internal::RenderState& rs = getIScene().getRenderState(m_renderStateHandle); - func = AppearanceUtils::GetStencilFuncFromInternal(rs.stencilFunc); + func = rs.stencilFunc; ref = rs.stencilRefValue; mask = rs.stencilMask; return StatusOK; @@ -168,32 +157,28 @@ namespace ramses status_t AppearanceImpl::setStencilOperation(EStencilOperation sfail, EStencilOperation dpfail, EStencilOperation dppass) { - getIScene().setRenderStateStencilOps( - m_renderStateHandle, - AppearanceUtils::GetStencilOperationInternal(sfail), - AppearanceUtils::GetStencilOperationInternal(dpfail), - AppearanceUtils::GetStencilOperationInternal(dppass)); + getIScene().setRenderStateStencilOps( m_renderStateHandle, sfail, dpfail, dppass); return StatusOK; } status_t AppearanceImpl::getStencilOperation(EStencilOperation& sfail, EStencilOperation& dpfail, EStencilOperation& dppass) const { const ramses_internal::RenderState& rs = getIScene().getRenderState(m_renderStateHandle); - sfail = AppearanceUtils::GetStencilOperationFromInternal(rs.stencilOpFail); - dpfail = AppearanceUtils::GetStencilOperationFromInternal(rs.stencilOpDepthFail); - dppass = AppearanceUtils::GetStencilOperationFromInternal(rs.stencilOpDepthPass); + sfail = rs.stencilOpFail; + dpfail = rs.stencilOpDepthFail; + dppass = rs.stencilOpDepthPass; return StatusOK; } status_t AppearanceImpl::setCullingMode(ECullMode mode) { - getIScene().setRenderStateCullMode(m_renderStateHandle, AppearanceUtils::GetCullModeInternal(mode)); + getIScene().setRenderStateCullMode(m_renderStateHandle, mode); return StatusOK; } status_t AppearanceImpl::getCullingMode(ECullMode& mode) const { - mode = AppearanceUtils::GetCullModeFromInternal(getIScene().getRenderState(m_renderStateHandle).cullMode); + mode = getIScene().getRenderState(m_renderStateHandle).cullMode; return StatusOK; } @@ -209,13 +194,13 @@ namespace ramses } } - getIScene().setRenderStateDrawMode(m_renderStateHandle, AppearanceUtils::GetDrawModeInternal(mode)); + getIScene().setRenderStateDrawMode(m_renderStateHandle, mode); return StatusOK; } status_t AppearanceImpl::getDrawMode(EDrawMode& mode) const { - mode = AppearanceUtils::GetDrawModeFromInternal(getIScene().getRenderState(m_renderStateHandle).drawMode); + mode = getIScene().getRenderState(m_renderStateHandle).drawMode; return StatusOK; } @@ -313,18 +298,18 @@ namespace ramses status_t AppearanceImpl::validateEffect() const { - ObjectIteratorImpl iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType_Effect); + ObjectIteratorImpl iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType::Effect); RamsesObject* ramsesObject = iter.getNext(); while (nullptr != ramsesObject) { const Effect& effect = RamsesObjectTypeUtils::ConvertTo(*ramsesObject); - if (&effect.impl == m_effectImpl) + if (&effect.m_impl == m_effectImpl) return addValidationOfDependentObject(*m_effectImpl); ramsesObject = iter.getNext(); } - return addValidationMessage(EValidationSeverity_Error, "Appearance is referring to an invalid Effect"); + return addValidationMessage(EValidationSeverity::Error, "Appearance is referring to an invalid Effect"); } status_t AppearanceImpl::validateUniforms() const @@ -340,14 +325,14 @@ namespace ramses const ramses_internal::TextureSamplerHandle samplerHandle = getIScene().getDataTextureSamplerHandle(m_uniformInstance, fieldHandle); if (!getIScene().isTextureSamplerAllocated(samplerHandle)) - return addValidationMessage(EValidationSeverity_Error, "Appearance is using a Texture Sampler that does not exist"); + return addValidationMessage(EValidationSeverity::Error, "Appearance is using a Texture Sampler that does not exist"); - RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType_TextureSampler); + RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType::TextureSampler); while (const TextureSampler* sampler = iter.getNext()) { - if (samplerHandle == sampler->impl.getTextureSamplerHandle()) + if (samplerHandle == sampler->m_impl.getTextureSamplerHandle()) { - status = std::max(status, addValidationOfDependentObject(sampler->impl)); + status = std::max(status, addValidationOfDependentObject(sampler->m_impl)); break; } } @@ -359,16 +344,16 @@ namespace ramses { const ramses_internal::DataInstanceHandle boundInstance = getIScene().getDataReference(m_uniformInstance, ramses_internal::DataFieldHandle(bindableInput.key)); if (!getIScene().isDataInstanceAllocated(boundInstance)) - return addValidationMessage(EValidationSeverity_Error, "Appearance's input is bound to a DataObject that does not exist"); + return addValidationMessage(EValidationSeverity::Error, "Appearance's input is bound to a DataObject that does not exist"); - ObjectIteratorImpl iterator(getSceneImpl().getObjectRegistry(), ERamsesObjectType_DataObject); + ObjectIteratorImpl iterator(getSceneImpl().getObjectRegistry(), ERamsesObjectType::DataObject); RamsesObject* ramsesObject = nullptr; while (nullptr != (ramsesObject = iterator.getNext())) { const DataObject& dataObject = RamsesObjectTypeUtils::ConvertTo(*ramsesObject); - if (boundInstance == dataObject.impl.getDataReference()) + if (boundInstance == dataObject.m_impl.getDataReference()) { - status = std::max(status, addValidationOfDependentObject(dataObject.impl)); + status = std::max(status, addValidationOfDependentObject(dataObject.m_impl)); break; } } @@ -455,17 +440,17 @@ namespace ramses } } - status_t AppearanceImpl::checkEffectInputValidityAndValueCompatibility(const EffectInputImpl& input, uint32_t valueElementCount, std::initializer_list valueDataType) const + status_t AppearanceImpl::checkEffectInputValidityAndValueCompatibility(const EffectInputImpl& input, size_t valueElementCount, std::initializer_list valueDataType) const { if (input.getEffectHash() != m_effectImpl->getLowlevelResourceHash()) { return addErrorEntry("Appearance::set failed, input is not properly initialized or cannot be used with this appearance."); } - const auto result = std::find(valueDataType.begin(), valueDataType.end(),input.getDataType()); + const auto result = std::find(valueDataType.begin(), valueDataType.end(), input.getInternalDataType()); if (result == valueDataType.end()) { - return addErrorEntry(::fmt::format("Appearance::set failed, value type does not match input data type {}", EnumToString(input.getDataType()))); + return addErrorEntry(::fmt::format("Appearance::set failed, value type does not match input data type {}", EnumToString(input.getInternalDataType()))); } if (input.getElementCount() != valueElementCount) @@ -486,32 +471,18 @@ namespace ramses } template - status_t AppearanceImpl::setInputValue(const EffectInputImpl& input, uint32_t elementCount, const T* valuesIn) + status_t AppearanceImpl::setInputValue(const EffectInputImpl& input, size_t elementCount, const T* valuesIn) { return setDataArrayChecked(elementCount, valuesIn, input); } template - status_t AppearanceImpl::getInputValue(const EffectInputImpl& input, uint32_t elementCount, T* valuesOut) const + status_t AppearanceImpl::getInputValue(const EffectInputImpl& input, size_t elementCount, T* valuesOut) const { return getDataArrayChecked(elementCount, valuesOut, input); } - template - status_t AppearanceImpl::setInputValueWithElementTypeCast(const EffectInputImpl& input, uint32_t elementCount, const ElementT* valuesIn) - { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) TODO(tobias) questionable if correct because ContainerT not POD - return setDataArrayChecked(elementCount, reinterpret_cast(valuesIn), input); - } - - template - status_t AppearanceImpl::getInputValueWithElementTypeCast(const EffectInputImpl& input, uint32_t elementCount, ElementT* valuesOut) const - { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) TODO(tobias) questionable if correct because ContainerT not POD - return getDataArrayChecked(elementCount, reinterpret_cast(valuesOut), input); - } - template - status_t AppearanceImpl::setDataArrayChecked(uint32_t elementCount, const T* values, const EffectInputImpl& input) + status_t AppearanceImpl::setDataArrayChecked(size_t elementCount, const T* values, const EffectInputImpl& input) { if (input.getSemantics() != ramses_internal::EFixedSemantics::Invalid) { @@ -519,17 +490,18 @@ namespace ramses } CHECK_RETURN_ERR(checkEffectInputValidityAndValueCompatibility(input, elementCount, {ramses_internal::TypeToEDataTypeTraits::DataType})); - const BindableInput* bindableInput = m_bindableInputs.get(input.getInputIndex()); + const uint32_t inputIndex = static_cast(input.getInputIndex()); + const BindableInput* bindableInput = m_bindableInputs.get(inputIndex); const bool isBindable = (bindableInput != nullptr); if (isBindable && bindableInput->externallyBoundDataObject) { return addErrorEntry("Appearance::set failed, given uniform input is currently bound to a DataObject. Either unbind it from input first or set value on the DataObject itself."); } - const ramses_internal::DataFieldHandle dataField(input.getInputIndex()); + const ramses_internal::DataFieldHandle dataField(inputIndex); if (isBindable) { - const ramses_internal::DataInstanceHandle dataReference = getDataReference(dataField, input.getDataType()); + const ramses_internal::DataInstanceHandle dataReference = getDataReference(dataField, input.getInternalDataType()); const T* currentValues = ramses_internal::ISceneDataArrayAccessor::GetDataArray(&getIScene(), dataReference, ramses_internal::DataFieldHandle(0u)); if (ramses_internal::PlatformMemory::Compare(currentValues, values, 1u * sizeof(T)) != 0) { @@ -538,11 +510,12 @@ namespace ramses } else { - assert(getIScene().getDataLayout(m_uniformLayout).getField(dataField).elementCount == elementCount); + static_assert( std::is_same_v == true ); + assert(getIScene().getDataLayout(m_uniformLayout).getField(dataField).elementCount == static_cast(elementCount)); const T* currentValues = ramses_internal::ISceneDataArrayAccessor::GetDataArray(&getIScene(), m_uniformInstance, dataField); if (ramses_internal::PlatformMemory::Compare(currentValues, values, elementCount * sizeof(T)) != 0) { - ramses_internal::ISceneDataArrayAccessor::SetDataArray(&getIScene(), m_uniformInstance, dataField, elementCount, values); + ramses_internal::ISceneDataArrayAccessor::SetDataArray(&getIScene(), m_uniformInstance, dataField, static_cast(elementCount), values); } } @@ -550,7 +523,7 @@ namespace ramses } template - status_t AppearanceImpl::getDataArrayChecked(uint32_t elementCount, T* values, const EffectInputImpl& input) const + status_t AppearanceImpl::getDataArrayChecked(size_t elementCount, T* values, const EffectInputImpl& input) const { if (input.getSemantics() != ramses_internal::EFixedSemantics::Invalid) { @@ -558,22 +531,22 @@ namespace ramses } CHECK_RETURN_ERR(checkEffectInputValidityAndValueCompatibility(input, elementCount, {ramses_internal::TypeToEDataTypeTraits::DataType})); - const BindableInput* bindableInput = m_bindableInputs.get(input.getInputIndex()); + const BindableInput* bindableInput = m_bindableInputs.get(static_cast(input.getInputIndex())); const bool isBindable = (bindableInput != nullptr); if (isBindable && bindableInput->externallyBoundDataObject) { return addErrorEntry("Appearance::get failed, given uniform input is currently bound to a DataObject. Either unbind it from input first or get value from the DataObject itself."); } - const ramses_internal::DataFieldHandle dataField(input.getInputIndex()); + const ramses_internal::DataFieldHandle dataField(static_cast(input.getInputIndex())); if (isBindable) { - const ramses_internal::DataInstanceHandle dataReference = getDataReference(dataField, input.getDataType()); - ramses_internal::PlatformMemory::Copy(values, ramses_internal::ISceneDataArrayAccessor::GetDataArray(&getIScene(), dataReference, ramses_internal::DataFieldHandle(0u)), EnumToSize(input.getDataType())); + const ramses_internal::DataInstanceHandle dataReference = getDataReference(dataField, input.getInternalDataType()); + ramses_internal::PlatformMemory::Copy(values, ramses_internal::ISceneDataArrayAccessor::GetDataArray(&getIScene(), dataReference, ramses_internal::DataFieldHandle(0u)), EnumToSize(input.getInternalDataType())); } else { - ramses_internal::PlatformMemory::Copy(values, ramses_internal::ISceneDataArrayAccessor::GetDataArray(&getIScene(), m_uniformInstance, dataField), elementCount * EnumToSize(input.getDataType())); + ramses_internal::PlatformMemory::Copy(values, ramses_internal::ISceneDataArrayAccessor::GetDataArray(&getIScene(), m_uniformInstance, dataField), elementCount * EnumToSize(input.getInternalDataType())); } return StatusOK; @@ -595,14 +568,14 @@ namespace ramses CHECK_RETURN_ERR(checkEffectInputValidityAndValueCompatibility(input, 1u, {ramses_internal::EDataType::TextureSampler2D, ramses_internal::EDataType::TextureSampler3D, ramses_internal::EDataType::TextureSamplerCube})); - const ramses_internal::DataFieldHandle dataField(input.getInputIndex()); + const ramses_internal::DataFieldHandle dataField(static_cast(input.getInputIndex())); const auto samplerHandle = getIScene().getDataTextureSamplerHandle(m_uniformInstance, dataField); if (samplerHandle.isValid()) { - RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType_TextureSampler); + RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType::TextureSampler); while (const TextureSampler* sampler = iter.getNext()) { - if (samplerHandle == sampler->impl.getTextureSamplerHandle()) + if (samplerHandle == sampler->m_impl.getTextureSamplerHandle()) { textureSampler = sampler; break; @@ -618,14 +591,14 @@ namespace ramses CHECK_RETURN_ERR(checkEffectInputValidityAndValueCompatibility(input, 1u, {ramses_internal::EDataType::TextureSampler2DMS})); - const ramses_internal::DataFieldHandle dataField(input.getInputIndex()); + const ramses_internal::DataFieldHandle dataField(static_cast(input.getInputIndex())); const auto samplerHandle = getIScene().getDataTextureSamplerHandle(m_uniformInstance, dataField); if (samplerHandle.isValid()) { - RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType_TextureSamplerMS); + RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType::TextureSamplerMS); while (const TextureSamplerMS* sampler = iter.getNext()) { - if (samplerHandle == sampler->impl.getTextureSamplerHandle()) + if (samplerHandle == sampler->m_impl.getTextureSamplerHandle()) { textureSampler = sampler; break; @@ -641,14 +614,14 @@ namespace ramses CHECK_RETURN_ERR(checkEffectInputValidityAndValueCompatibility(input, 1u, {ramses_internal::EDataType::TextureSamplerExternal})); - const ramses_internal::DataFieldHandle dataField(input.getInputIndex()); + const ramses_internal::DataFieldHandle dataField(static_cast(input.getInputIndex())); const auto samplerHandle = getIScene().getDataTextureSamplerHandle(m_uniformInstance, dataField); if (samplerHandle.isValid()) { - RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType_TextureSamplerExternal); + RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType::TextureSamplerExternal); while (const TextureSamplerExternal* sampler = iter.getNext()) { - if (samplerHandle == sampler->impl.getTextureSamplerHandle()) + if (samplerHandle == sampler->m_impl.getTextureSamplerHandle()) { textureSampler = sampler; break; @@ -661,25 +634,21 @@ namespace ramses status_t AppearanceImpl::bindInput(const EffectInputImpl& input, const DataObjectImpl& dataObject) { if (!isFromTheSameSceneAs(dataObject)) - { return addErrorEntry("Appearance::bindInput failed, dataObject is not from the same scene as this appearance"); - } - CHECK_RETURN_ERR(checkEffectInputValidityAndValueCompatibility(input, 1u, {dataObject.getDataType()})); + CHECK_RETURN_ERR(checkEffectInputValidityAndValueCompatibility(input, 1u, {DataTypeUtils::ConvertDataTypeToInternal(dataObject.getDataType())})); - const uint32_t inputIndex = input.getInputIndex(); + const uint32_t inputIndex = static_cast(input.getInputIndex()); BindableInput* bindableInput = m_bindableInputs.get(inputIndex); if (bindableInput == nullptr) - { return addErrorEntry("Appearance::bindInput failed, given uniform input cannot be bound to a DataObject."); - } return bindInputInternal(input, dataObject); } status_t AppearanceImpl::unbindInput(const EffectInputImpl& input) { - const uint32_t inputIndex = input.getInputIndex(); + const uint32_t inputIndex = static_cast(input.getInputIndex()); BindableInput* bindableInput = m_bindableInputs.get(inputIndex); if (bindableInput == nullptr || !bindableInput->externallyBoundDataObject) { @@ -691,7 +660,7 @@ namespace ramses bool AppearanceImpl::isInputBound(const EffectInputImpl& input) const { - const uint32_t inputIndex = input.getInputIndex(); + const uint32_t inputIndex = static_cast(input.getInputIndex()); const BindableInput* bindableInput = m_bindableInputs.get(inputIndex); return (bindableInput != nullptr) && bindableInput->externallyBoundDataObject != nullptr; } @@ -700,7 +669,7 @@ namespace ramses { CHECK_RETURN_ERR(checkEffectInputValidityAndValueCompatibility(input, 1u, {textureSampler.getTextureDataType()})); - const ramses_internal::DataFieldHandle dataField(input.getInputIndex()); + const ramses_internal::DataFieldHandle dataField(static_cast(input.getInputIndex())); const ramses_internal::TextureSamplerHandle samplerHandle = textureSampler.getTextureSamplerHandle(); getIScene().setDataTextureSamplerHandle(m_uniformInstance, dataField, samplerHandle); return StatusOK; @@ -708,7 +677,7 @@ namespace ramses status_t AppearanceImpl::bindInputInternal(const EffectInputImpl& input, const DataObjectImpl& dataObject) { - const uint32_t inputIndex = input.getInputIndex(); + const uint32_t inputIndex = static_cast(input.getInputIndex()); const ramses_internal::DataFieldHandle dataField(inputIndex); getIScene().setDataReference(m_uniformInstance, dataField, dataObject.getDataReference()); @@ -721,7 +690,7 @@ namespace ramses status_t AppearanceImpl::unbindInputInternal(const EffectInputImpl& input) { - const uint32_t inputIndex = input.getInputIndex(); + const uint32_t inputIndex = static_cast(input.getInputIndex()); BindableInput* bindableInput = m_bindableInputs.get(inputIndex); const ramses_internal::DataFieldHandle dataField(inputIndex); getIScene().setDataReference(m_uniformInstance, dataField, bindableInput->dataReference); @@ -737,7 +706,7 @@ namespace ramses const ramses::DataObject* AppearanceImpl::getBoundDataObject(const EffectInputImpl& input) const { - const uint32_t inputIndex = input.getInputIndex(); + const uint32_t inputIndex = static_cast(input.getInputIndex()); const BindableInput* bindableInput = m_bindableInputs.get(inputIndex); if (bindableInput && bindableInput->externallyBoundDataObject) return &RamsesObjectTypeUtils::ConvertTo(bindableInput->externallyBoundDataObject->getRamsesObject()); @@ -745,39 +714,27 @@ namespace ramses return nullptr; } - template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, uint32_t, const int32_t*); - template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, uint32_t, int32_t*) const; - template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, uint32_t, const float*); - template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, uint32_t, float*) const; - template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, uint32_t, const ramses_internal::Vector2i*); - template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, uint32_t, ramses_internal::Vector2i*) const; - template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, uint32_t, const ramses_internal::Vector3i*); - template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, uint32_t, ramses_internal::Vector3i*) const; - template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, uint32_t, const ramses_internal::Vector4i*); - template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, uint32_t, ramses_internal::Vector4i*) const; - template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, uint32_t, const ramses_internal::Vector2*); - template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, uint32_t, ramses_internal::Vector2*) const; - template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, uint32_t, const ramses_internal::Vector3*); - template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, uint32_t, ramses_internal::Vector3*) const; - template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, uint32_t, const ramses_internal::Vector4*); - template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, uint32_t, ramses_internal::Vector4*) const; - - template status_t AppearanceImpl::setInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, const int32_t*); - template status_t AppearanceImpl::getInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, int32_t*) const; - template status_t AppearanceImpl::setInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, const int32_t*); - template status_t AppearanceImpl::getInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, int32_t*) const; - template status_t AppearanceImpl::setInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, const int32_t*); - template status_t AppearanceImpl::getInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, int32_t*) const; - template status_t AppearanceImpl::setInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, const float*); - template status_t AppearanceImpl::getInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, float*) const; - template status_t AppearanceImpl::setInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, const float*); - template status_t AppearanceImpl::getInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, float*) const; - template status_t AppearanceImpl::setInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, const float*); - template status_t AppearanceImpl::getInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, float*) const; - template status_t AppearanceImpl::setInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, const float*); - template status_t AppearanceImpl::getInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, float*) const; - template status_t AppearanceImpl::setInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, const float*); - template status_t AppearanceImpl::getInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, float*) const; - template status_t AppearanceImpl::setInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, const float*); - template status_t AppearanceImpl::getInputValueWithElementTypeCast(const EffectInputImpl&, uint32_t, float*) const; + template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, size_t, const int32_t*); + template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, size_t, int32_t*) const; + template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, size_t, const float*); + template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, size_t, float*) const; + + template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, size_t, const vec2i*); + template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, size_t, vec2i*) const; + template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, size_t, const vec3i*); + template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, size_t, vec3i*) const; + template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, size_t, const vec4i*); + template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, size_t, vec4i*) const; + template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, size_t, const vec2f*); + template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, size_t, vec2f*) const; + template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, size_t, const vec3f*); + template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, size_t, vec3f*) const; + template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, size_t, const vec4f*); + template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, size_t, vec4f*) const; + template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, size_t, const matrix22f*); + template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, size_t, matrix22f*) const; + template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, size_t, const matrix33f*); + template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, size_t, matrix33f*) const; + template status_t AppearanceImpl::setInputValue(const EffectInputImpl&, size_t, const matrix44f*); + template status_t AppearanceImpl::getInputValue(const EffectInputImpl&, size_t, matrix44f*) const; } diff --git a/client/ramses-client/impl/AppearanceImpl.h b/client/ramses-client/impl/AppearanceImpl.h index c29467747..ed2c0194a 100644 --- a/client/ramses-client/impl/AppearanceImpl.h +++ b/client/ramses-client/impl/AppearanceImpl.h @@ -11,7 +11,7 @@ // client api #include "ramses-client-api/Appearance.h" -#include "ramses-client-api/AppearanceEnums.h" +#include "ramses-framework-api/AppearanceEnums.h" // internal #include "SceneObjectImpl.h" @@ -20,7 +20,9 @@ #include "SceneAPI/Handles.h" #include "SceneAPI/EDataType.h" #include "Collections/HashMap.h" + #include +#include namespace ramses_internal { @@ -38,15 +40,15 @@ namespace ramses class AppearanceImpl final : public SceneObjectImpl { public: - AppearanceImpl(SceneImpl& scene, const char* appearancename); - virtual ~AppearanceImpl() override; + AppearanceImpl(SceneImpl& scene, std::string_view appearancename); + ~AppearanceImpl() override; void initializeFrameworkData(const EffectImpl& effect); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; + status_t validate() const override; const EffectImpl* getEffectImpl() const; const Effect& getEffect() const; @@ -55,8 +57,8 @@ namespace ramses status_t getBlendingFactors(EBlendFactor& srcColor, EBlendFactor& destColor, EBlendFactor& srcAlpha, EBlendFactor& destAlpha) const; status_t setBlendingOperations(EBlendOperation operationColor, EBlendOperation operationAlpha); status_t getBlendingOperations(EBlendOperation& operationColor, EBlendOperation& operationAlpha) const; - status_t setBlendingColor(float red, float green, float blue, float alpha); - status_t getBlendingColor(float& red, float& green, float& blue, float& alpha) const; + status_t setBlendingColor(const vec4f& color); + status_t getBlendingColor(vec4f& color) const; status_t setDepthFunction(EDepthFunc func); status_t getDepthFunction(EDepthFunc& func) const; status_t setDepthWrite(EDepthWrite flag); @@ -76,13 +78,9 @@ namespace ramses status_t getColorWriteMask(bool& writeRed, bool& writeGreen, bool& writeBlue, bool& writeAlpha) const; template - status_t setInputValue(const EffectInputImpl& input, uint32_t elementCount, const T* valuesIn); + status_t setInputValue(const EffectInputImpl& input, size_t elementCount, const T* valuesIn); template - status_t getInputValue(const EffectInputImpl& input, uint32_t elementCount, T* valuesOut) const; - template - status_t setInputValueWithElementTypeCast(const EffectInputImpl& input, uint32_t elementCount, const ElementT* valuesIn); - template - status_t getInputValueWithElementTypeCast(const EffectInputImpl& input, uint32_t elementCount, ElementT* valuesOut) const; + status_t getInputValue(const EffectInputImpl& input, size_t elementCount, T* valuesOut) const; status_t setInputTexture(const EffectInputImpl& input, const TextureSamplerImpl& textureSampler); status_t getInputTexture(const EffectInputImpl& input, const TextureSampler*& textureSampler); @@ -101,13 +99,13 @@ namespace ramses private: void createUniformDataInstance(const EffectImpl& effect); - status_t checkEffectInputValidityAndValueCompatibility(const EffectInputImpl& input, uint32_t valueElementCount, std::initializer_list valueDataType) const; + status_t checkEffectInputValidityAndValueCompatibility(const EffectInputImpl& input, size_t valueElementCount, std::initializer_list valueDataType) const; ramses_internal::DataInstanceHandle getDataReference(ramses_internal::DataFieldHandle dataField, ramses_internal::EDataType expectedDataType) const; template - status_t setDataArrayChecked(uint32_t elementCount, const T* values, const EffectInputImpl& input); + status_t setDataArrayChecked(size_t elementCount, const T* values, const EffectInputImpl& input); template - status_t getDataArrayChecked(uint32_t elementCount, T* values, const EffectInputImpl& input) const; + status_t getDataArrayChecked(size_t elementCount, T* values, const EffectInputImpl& input) const; status_t setInputTextureInternal(const EffectInputImpl& input, const TextureSamplerImpl& textureSampler); status_t bindInputInternal(const EffectInputImpl& input, const DataObjectImpl& dataObject); diff --git a/client/ramses-client/impl/AppearanceUtils.h b/client/ramses-client/impl/AppearanceUtils.h index cb2a6b33f..d9ddc47d8 100644 --- a/client/ramses-client/impl/AppearanceUtils.h +++ b/client/ramses-client/impl/AppearanceUtils.h @@ -9,7 +9,7 @@ #ifndef RAMSES_APPEARANCEUTILS_H #define RAMSES_APPEARANCEUTILS_H -#include "ramses-client-api/AppearanceEnums.h" +#include "ramses-framework-api/AppearanceEnums.h" #include "SceneAPI/RenderState.h" namespace ramses @@ -17,452 +17,27 @@ namespace ramses class AppearanceUtils { public: - static ramses_internal::EBlendOperation GetBlendOperationInternal(EBlendOperation blendOp) - { - switch (blendOp) - { - case EBlendOperation_Disabled: - return ramses_internal::EBlendOperation::Disabled; - case EBlendOperation_Add: - return ramses_internal::EBlendOperation::Add; - case EBlendOperation_Subtract: - return ramses_internal::EBlendOperation::Subtract; - case EBlendOperation_ReverseSubtract: - return ramses_internal::EBlendOperation::ReverseSubtract; - case EBlendOperation_Min: - return ramses_internal::EBlendOperation::Min; - case EBlendOperation_Max: - return ramses_internal::EBlendOperation::Max; - default: - assert(false); - return ramses_internal::EBlendOperation::Disabled; - } - } - - static EBlendOperation GetBlendOperationFromInternal(ramses_internal::EBlendOperation blendOp) - { - switch (blendOp) - { - case ramses_internal::EBlendOperation::Disabled: - return EBlendOperation_Disabled; - case ramses_internal::EBlendOperation::Add: - return EBlendOperation_Add; - case ramses_internal::EBlendOperation::Subtract: - return EBlendOperation_Subtract; - case ramses_internal::EBlendOperation::ReverseSubtract: - return EBlendOperation_ReverseSubtract; - case ramses_internal::EBlendOperation::Min: - return EBlendOperation_Min; - case ramses_internal::EBlendOperation::Max: - return EBlendOperation_Max; - default: - assert(false); - return EBlendOperation_Disabled; - } - } - - static ramses_internal::EBlendFactor GetBlendFactorInternal(EBlendFactor blendFactor) - { - switch (blendFactor) - { - case EBlendFactor_Zero: - return ramses_internal::EBlendFactor::Zero; - case EBlendFactor_One: - return ramses_internal::EBlendFactor::One; - case EBlendFactor_SrcAlpha: - return ramses_internal::EBlendFactor::SrcAlpha; - case EBlendFactor_OneMinusSrcAlpha: - return ramses_internal::EBlendFactor::OneMinusSrcAlpha; - case EBlendFactor_DstAlpha: - return ramses_internal::EBlendFactor::DstAlpha; - case EBlendFactor_OneMinusDstAlpha: - return ramses_internal::EBlendFactor::OneMinusDstAlpha; - case EBlendFactor_SrcColor: - return ramses_internal::EBlendFactor::SrcColor; - case EBlendFactor_OneMinusSrcColor: - return ramses_internal::EBlendFactor::OneMinusSrcColor; - case EBlendFactor_DstColor: - return ramses_internal::EBlendFactor::DstColor; - case EBlendFactor_OneMinusDstColor: - return ramses_internal::EBlendFactor::OneMinusDstColor; - case EBlendFactor_ConstColor: - return ramses_internal::EBlendFactor::ConstColor; - case EBlendFactor_OneMinusConstColor: - return ramses_internal::EBlendFactor::OneMinusConstColor; - case EBlendFactor_ConstAlpha: - return ramses_internal::EBlendFactor::ConstAlpha; - case EBlendFactor_OneMinusConstAlpha: - return ramses_internal::EBlendFactor::OneMinusConstAlpha; - case EBlendFactor_AlphaSaturate: - return ramses_internal::EBlendFactor::AlphaSaturate; - default: - assert(false); - return ramses_internal::EBlendFactor::One; - } - } - - static EBlendFactor GetBlendFactorFromInternal(ramses_internal::EBlendFactor blendFactor) - { - switch (blendFactor) - { - case ramses_internal::EBlendFactor::Zero: - return EBlendFactor_Zero; - case ramses_internal::EBlendFactor::One: - return EBlendFactor_One; - case ramses_internal::EBlendFactor::SrcAlpha: - return EBlendFactor_SrcAlpha; - case ramses_internal::EBlendFactor::OneMinusSrcAlpha: - return EBlendFactor_OneMinusSrcAlpha; - case ramses_internal::EBlendFactor::DstAlpha: - return EBlendFactor_DstAlpha; - case ramses_internal::EBlendFactor::OneMinusDstAlpha: - return EBlendFactor_OneMinusDstAlpha; - - case ramses_internal::EBlendFactor::SrcColor: - return EBlendFactor_SrcColor; - case ramses_internal::EBlendFactor::OneMinusSrcColor: - return EBlendFactor_OneMinusSrcColor; - case ramses_internal::EBlendFactor::DstColor: - return EBlendFactor_DstColor; - case ramses_internal::EBlendFactor::OneMinusDstColor: - return EBlendFactor_OneMinusDstColor; - case ramses_internal::EBlendFactor::ConstColor: - return EBlendFactor_ConstColor; - case ramses_internal::EBlendFactor::OneMinusConstColor: - return EBlendFactor_OneMinusConstColor; - case ramses_internal::EBlendFactor::ConstAlpha: - return EBlendFactor_ConstAlpha; - case ramses_internal::EBlendFactor::OneMinusConstAlpha: - return EBlendFactor_OneMinusConstAlpha; - case ramses_internal::EBlendFactor::AlphaSaturate: - return EBlendFactor_AlphaSaturate; - default: - assert(false); - return EBlendFactor_One; - } - } - - static ramses_internal::ECullMode GetCullModeInternal(ECullMode cullMode) - { - switch (cullMode) - { - case ECullMode_Disabled: - return ramses_internal::ECullMode::Disabled; - case ECullMode_FrontFacing: - return ramses_internal::ECullMode::FrontFacing; - case ECullMode_BackFacing: - return ramses_internal::ECullMode::BackFacing; - case ECullMode_FrontAndBackFacing: - return ramses_internal::ECullMode::FrontAndBackFacing; - default: - assert(false); - return ramses_internal::ECullMode::Invalid; - } - } - - static ECullMode GetCullModeFromInternal(ramses_internal::ECullMode cullMode) - { - switch (cullMode) - { - case ramses_internal::ECullMode::Disabled: - return ECullMode_Disabled; - case ramses_internal::ECullMode::FrontFacing: - return ECullMode_FrontFacing; - case ramses_internal::ECullMode::BackFacing: - return ECullMode_BackFacing; - case ramses_internal::ECullMode::FrontAndBackFacing: - return ECullMode_FrontAndBackFacing; - default: - assert(false); - return ECullMode_Disabled; - } - } - - static ramses_internal::EDepthWrite GetDepthWriteInternal(EDepthWrite depthWrite) - { - switch (depthWrite) - { - case EDepthWrite_Disabled: - return ramses_internal::EDepthWrite::Disabled; - case EDepthWrite_Enabled: - return ramses_internal::EDepthWrite::Enabled; - default: - assert(false); - return ramses_internal::EDepthWrite::Enabled; - } - } - - static EDepthWrite GetDepthWriteFromInternal(ramses_internal::EDepthWrite depthWrite) - { - switch (depthWrite) - { - case ramses_internal::EDepthWrite::Disabled: - return EDepthWrite_Disabled; - case ramses_internal::EDepthWrite::Enabled: - return EDepthWrite_Enabled; - default: - assert(false); - return EDepthWrite_Enabled; - } - } - - static ramses_internal::EScissorTest GetScissorTestInternal(EScissorTest scissorTest) - { - switch (scissorTest) - { - case EScissorTest_Disabled: - return ramses_internal::EScissorTest::Disabled; - case EScissorTest_Enabled: - return ramses_internal::EScissorTest::Enabled; - default: - assert(false); - return ramses_internal::EScissorTest::Disabled; - } - } - - static EScissorTest GetScissorTestFromInternal(ramses_internal::EScissorTest scissorTest) - { - switch (scissorTest) - { - case ramses_internal::EScissorTest::Disabled: - return EScissorTest_Disabled; - case ramses_internal::EScissorTest::Enabled: - return EScissorTest_Enabled; - default: - assert(false); - return EScissorTest_Disabled; - } - } - - static ramses_internal::EDepthFunc GetDepthFuncInternal(EDepthFunc depthFunc) - { - switch (depthFunc) - { - case EDepthFunc_Disabled: - return ramses_internal::EDepthFunc::Disabled; - case EDepthFunc_Greater: - return ramses_internal::EDepthFunc::Greater; - case EDepthFunc_GreaterEqual: - return ramses_internal::EDepthFunc::GreaterEqual; - case EDepthFunc_Less: - return ramses_internal::EDepthFunc::Smaller; - case EDepthFunc_LessEqual: - return ramses_internal::EDepthFunc::SmallerEqual; - case EDepthFunc_Always: - return ramses_internal::EDepthFunc::AlwaysPass; - case EDepthFunc_Never: - return ramses_internal::EDepthFunc::NeverPass; - case EDepthFunc_Equal: - return ramses_internal::EDepthFunc::Equal; - case EDepthFunc_NotEqual: - return ramses_internal::EDepthFunc::NotEqual; - default: - assert(false); - return ramses_internal::EDepthFunc::Invalid; - } - } - - static EDepthFunc GetDepthFuncFromInternal(ramses_internal::EDepthFunc depthFunc) - { - switch (depthFunc) - { - case ramses_internal::EDepthFunc::Disabled: - return EDepthFunc_Disabled; - case ramses_internal::EDepthFunc::Greater: - return EDepthFunc_Greater; - case ramses_internal::EDepthFunc::GreaterEqual: - return EDepthFunc_GreaterEqual; - case ramses_internal::EDepthFunc::Smaller: - return EDepthFunc_Less; - case ramses_internal::EDepthFunc::SmallerEqual: - return EDepthFunc_LessEqual; - case ramses_internal::EDepthFunc::AlwaysPass: - return EDepthFunc_Always; - case ramses_internal::EDepthFunc::NeverPass: - return EDepthFunc_Never; - case ramses_internal::EDepthFunc::Equal: - return EDepthFunc_Equal; - case ramses_internal::EDepthFunc::NotEqual: - return EDepthFunc_NotEqual; - default: - assert(false); - return EDepthFunc_LessEqual; - } - } - - static ramses_internal::EStencilFunc GetStencilFuncInternal(EStencilFunc stencilFunc) - { - switch (stencilFunc) - { - case EStencilFunc_Disabled: - return ramses_internal::EStencilFunc::Disabled; - case EStencilFunc_Never: - return ramses_internal::EStencilFunc::NeverPass; - case EStencilFunc_Always: - return ramses_internal::EStencilFunc::AlwaysPass; - case EStencilFunc_Equal: - return ramses_internal::EStencilFunc::Equal; - case EStencilFunc_NotEqual: - return ramses_internal::EStencilFunc::NotEqual; - case EStencilFunc_Less: - return ramses_internal::EStencilFunc::Less; - case EStencilFunc_LessEqual: - return ramses_internal::EStencilFunc::LessEqual; - case EStencilFunc_Greater: - return ramses_internal::EStencilFunc::Greater; - case EStencilFunc_GreaterEqual: - return ramses_internal::EStencilFunc::GreaterEqual; - default: - assert(false); - return ramses_internal::EStencilFunc::Disabled; - } - } - - static EStencilFunc GetStencilFuncFromInternal(ramses_internal::EStencilFunc stencilFunc) - { - switch (stencilFunc) - { - case ramses_internal::EStencilFunc::Disabled: - return EStencilFunc_Disabled; - case ramses_internal::EStencilFunc::NeverPass: - return EStencilFunc_Never; - case ramses_internal::EStencilFunc::AlwaysPass: - return EStencilFunc_Always; - case ramses_internal::EStencilFunc::Equal: - return EStencilFunc_Equal; - case ramses_internal::EStencilFunc::NotEqual: - return EStencilFunc_NotEqual; - case ramses_internal::EStencilFunc::Less: - return EStencilFunc_Less; - case ramses_internal::EStencilFunc::LessEqual: - return EStencilFunc_LessEqual; - case ramses_internal::EStencilFunc::Greater: - return EStencilFunc_Greater; - case ramses_internal::EStencilFunc::GreaterEqual: - return EStencilFunc_GreaterEqual; - default: - assert(false); - return EStencilFunc_Disabled; - } - } - - static ramses_internal::EStencilOp GetStencilOperationInternal(EStencilOperation stencilOp) - { - switch (stencilOp) - { - case EStencilOperation_Keep: - return ramses_internal::EStencilOp::Keep; - case EStencilOperation_Zero: - return ramses_internal::EStencilOp::Zero; - case EStencilOperation_Replace: - return ramses_internal::EStencilOp::Replace; - case EStencilOperation_Increment: - return ramses_internal::EStencilOp::Increment; - case EStencilOperation_IncrementWrap: - return ramses_internal::EStencilOp::IncrementWrap; - case EStencilOperation_Decrement: - return ramses_internal::EStencilOp::Decrement; - case EStencilOperation_DecrementWrap: - return ramses_internal::EStencilOp::DecrementWrap; - case EStencilOperation_Invert: - return ramses_internal::EStencilOp::Invert; - default: - assert(false); - return ramses_internal::EStencilOp::Keep; - } - } - - static EStencilOperation GetStencilOperationFromInternal(ramses_internal::EStencilOp stencilOp) - { - switch (stencilOp) - { - case ramses_internal::EStencilOp::Keep: - return EStencilOperation_Keep; - case ramses_internal::EStencilOp::Zero: - return EStencilOperation_Zero; - case ramses_internal::EStencilOp::Replace: - return EStencilOperation_Replace; - case ramses_internal::EStencilOp::Increment: - return EStencilOperation_Increment; - case ramses_internal::EStencilOp::IncrementWrap: - return EStencilOperation_IncrementWrap; - case ramses_internal::EStencilOp::Decrement: - return EStencilOperation_Decrement; - case ramses_internal::EStencilOp::DecrementWrap: - return EStencilOperation_DecrementWrap; - case ramses_internal::EStencilOp::Invert: - return EStencilOperation_Invert; - default: - assert(false); - return EStencilOperation_Keep; - } - } - - static ramses_internal::EDrawMode GetDrawModeInternal(EDrawMode mode) - { - switch (mode) - { - case EDrawMode_Points: - return ramses_internal::EDrawMode::Points; - case EDrawMode_Lines: - return ramses_internal::EDrawMode::Lines; - case EDrawMode_LineLoop: - return ramses_internal::EDrawMode::LineLoop; - case EDrawMode_Triangles: - return ramses_internal::EDrawMode::Triangles; - case EDrawMode_TriangleStrip: - return ramses_internal::EDrawMode::TriangleStrip; - case EDrawMode_TriangleFan: - return ramses_internal::EDrawMode::TriangleFan; - case EDrawMode_LineStrip: - return ramses_internal::EDrawMode::LineStrip; - default: - assert(false); - return ramses_internal::EDrawMode::Triangles; - } - } - static bool GeometryShaderCompatibleWithDrawMode(EDrawMode geometryShaderInputType, EDrawMode drawMode) { - switch (drawMode) - { - case EDrawMode_Points: - return geometryShaderInputType == EDrawMode_Points; - case EDrawMode_Lines: - case EDrawMode_LineStrip: - case EDrawMode_LineLoop: - return geometryShaderInputType == EDrawMode_Lines; - case EDrawMode_Triangles: - case EDrawMode_TriangleStrip: - case EDrawMode_TriangleFan: - return geometryShaderInputType == EDrawMode_Triangles; - default: - return false; - } - } + // only basic 'variant' (i.e. no strip/fan) of a primitive is allowed as GS input declaration + assert(geometryShaderInputType == EDrawMode::Points || geometryShaderInputType == EDrawMode::Lines || geometryShaderInputType == EDrawMode::Triangles); - static EDrawMode GetDrawModeFromInternal(ramses_internal::EDrawMode drawMode) - { switch (drawMode) { - case ramses_internal::EDrawMode::Points: - return EDrawMode_Points; - case ramses_internal::EDrawMode::Lines: - return EDrawMode_Lines; - case ramses_internal::EDrawMode::LineLoop: - return EDrawMode_LineLoop; - case ramses_internal::EDrawMode::Triangles: - return EDrawMode_Triangles; - case ramses_internal::EDrawMode::TriangleStrip: - return EDrawMode_TriangleStrip; - case ramses_internal::EDrawMode::TriangleFan: - return EDrawMode_TriangleFan; - case ramses_internal::EDrawMode::LineStrip: - return EDrawMode_LineStrip; - default: - assert(false); - return EDrawMode_Triangles; + case EDrawMode::Points: + return geometryShaderInputType == EDrawMode::Points; + case EDrawMode::Lines: + case EDrawMode::LineStrip: + case EDrawMode::LineLoop: + return geometryShaderInputType == EDrawMode::Lines; + case EDrawMode::Triangles: + case EDrawMode::TriangleStrip: + case EDrawMode::TriangleFan: + return geometryShaderInputType == EDrawMode::Triangles; } + + assert(false); + return false; } }; } diff --git a/client/ramses-client/impl/ArrayBufferImpl.cpp b/client/ramses-client/impl/ArrayBufferImpl.cpp index 626d113a9..e30fa3346 100644 --- a/client/ramses-client/impl/ArrayBufferImpl.cpp +++ b/client/ramses-client/impl/ArrayBufferImpl.cpp @@ -14,8 +14,8 @@ namespace ramses { - ArrayBufferImpl::ArrayBufferImpl(SceneImpl& scene, const char* databufferName) - : SceneObjectImpl(scene, ERamsesObjectType_DataBufferObject, databufferName) + ArrayBufferImpl::ArrayBufferImpl(SceneImpl& scene, std::string_view databufferName) + : SceneObjectImpl(scene, ERamsesObjectType::ArrayBufferObject, databufferName) { } @@ -60,7 +60,7 @@ namespace ramses EDataType ArrayBufferImpl::getDataType() const { const ramses_internal::GeometryDataBuffer& dataBuffer = getIScene().getDataBuffer(m_dataBufferHandle); - return DataTypeUtils::ConvertDataTypeFromInternal(dataBuffer.dataType); + return DataTypeUtils::ConvertDataTypeFromInternal(dataBuffer.dataType); } status_t ArrayBufferImpl::getData(ramses_internal::Byte* buffer, uint32_t numElements) const @@ -135,15 +135,15 @@ namespace ramses } if (usedAsInput && !isInitialized) - return addValidationMessage(EValidationSeverity_Warning, "DataBuffer is used as geometry input but there is no data set, this could lead to graphical glitches if actually rendered."); + return addValidationMessage(EValidationSeverity::Warning, "DataBuffer is used as geometry input but there is no data set, this could lead to graphical glitches if actually rendered."); if (!usedAsInput) - return addValidationMessage(EValidationSeverity_Warning, "DataBuffer is not used anywhere, destroy it if not needed."); + return addValidationMessage(EValidationSeverity::Warning, "DataBuffer is not used anywhere, destroy it if not needed."); return status; } - status_t ArrayBufferImpl::updateData(uint32_t firstElement, uint32_t numElements, const void* bufferData) + status_t ArrayBufferImpl::updateData(uint32_t firstElement, uint32_t numElements, const ramses_internal::Byte* bufferData) { const ramses_internal::GeometryDataBuffer& dataBuffer = getIScene().getDataBuffer(m_dataBufferHandle); const size_t maximumSizeInBytes = dataBuffer.data.size(); @@ -154,7 +154,7 @@ namespace ramses return addErrorEntry("DataBuffer::update failed - trying to write data beyond maximum size"); } - getIScene().updateDataBuffer(m_dataBufferHandle, offsetInBytes, dataSizeInBytes, static_cast(bufferData)); + getIScene().updateDataBuffer(m_dataBufferHandle, offsetInBytes, dataSizeInBytes, bufferData); return StatusOK; } diff --git a/client/ramses-client/impl/ArrayBufferImpl.h b/client/ramses-client/impl/ArrayBufferImpl.h index d86ab556d..4229e2445 100644 --- a/client/ramses-client/impl/ArrayBufferImpl.h +++ b/client/ramses-client/impl/ArrayBufferImpl.h @@ -11,24 +11,26 @@ #include "SceneObjectImpl.h" #include "SceneAPI/Handles.h" -#include "ramses-client-api/EDataType.h" +#include "ramses-framework-api/EDataType.h" + +#include namespace ramses { class ArrayBufferImpl : public SceneObjectImpl { public: - ArrayBufferImpl(SceneImpl& scene, const char* databufferName); - virtual ~ArrayBufferImpl() override; + ArrayBufferImpl(SceneImpl& scene, std::string_view databufferName); + ~ArrayBufferImpl() override; void initializeFrameworkData(EDataType dataType, uint32_t numElements); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + status_t validate() const override; - status_t updateData(uint32_t firstElement, uint32_t numElements, const void* bufferData); + status_t updateData(uint32_t firstElement, uint32_t numElements, const ramses_internal::Byte* bufferData); ramses_internal::DataBufferHandle getDataBufferHandle() const; uint32_t getMaximumNumberOfElements() const; diff --git a/client/ramses-client/impl/ArrayResourceImpl.cpp b/client/ramses-client/impl/ArrayResourceImpl.cpp index 7d27b8b1e..60d5c5bce 100644 --- a/client/ramses-client/impl/ArrayResourceImpl.cpp +++ b/client/ramses-client/impl/ArrayResourceImpl.cpp @@ -10,8 +10,8 @@ namespace ramses { - ArrayResourceImpl::ArrayResourceImpl(ramses_internal::ResourceHashUsage arrayHash, SceneImpl& scene, const char* name) - : ResourceImpl(ERamsesObjectType_ArrayResource, std::move(arrayHash), scene, name) + ArrayResourceImpl::ArrayResourceImpl(ramses_internal::ResourceHashUsage arrayHash, SceneImpl& scene, std::string_view name) + : ResourceImpl(ERamsesObjectType::ArrayResource, std::move(arrayHash), scene, name) , m_elementCount(0) , m_elementType(EDataType::UInt16) { diff --git a/client/ramses-client/impl/ArrayResourceImpl.h b/client/ramses-client/impl/ArrayResourceImpl.h index c2d6a9c46..fb2709169 100644 --- a/client/ramses-client/impl/ArrayResourceImpl.h +++ b/client/ramses-client/impl/ArrayResourceImpl.h @@ -11,19 +11,21 @@ // internal #include "ResourceImpl.h" -#include "ramses-client-api/EDataType.h" +#include "ramses-framework-api/EDataType.h" + +#include namespace ramses { class ArrayResourceImpl final : public ResourceImpl { public: - ArrayResourceImpl(ramses_internal::ResourceHashUsage arrayHash, SceneImpl& scene, const char* name); - virtual ~ArrayResourceImpl() override; + ArrayResourceImpl(ramses_internal::ResourceHashUsage arrayHash, SceneImpl& scene, std::string_view name); + ~ArrayResourceImpl() override; void initializeFromFrameworkData(uint32_t elementCount, EDataType elementType); - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; uint32_t getElementCount() const; EDataType getElementType() const; diff --git a/client/ramses-client/impl/BlitPassImpl.cpp b/client/ramses-client/impl/BlitPassImpl.cpp index ba16a79ce..ca03d956b 100644 --- a/client/ramses-client/impl/BlitPassImpl.cpp +++ b/client/ramses-client/impl/BlitPassImpl.cpp @@ -16,8 +16,8 @@ namespace ramses { - BlitPassImpl::BlitPassImpl(SceneImpl& scene, const char* blitpassName) - : SceneObjectImpl(scene, ERamsesObjectType_BlitPass, blitpassName) + BlitPassImpl::BlitPassImpl(SceneImpl& scene, std::string_view blitpassName) + : SceneObjectImpl(scene, ERamsesObjectType::BlitPass, blitpassName) { } @@ -145,10 +145,10 @@ namespace ramses const ramses_internal::BlitPass& blitPass = getIScene().getBlitPass(m_blitPassHandle); if (!getIScene().isRenderBufferAllocated(blitPass.sourceRenderBuffer)) - status = addValidationMessage(EValidationSeverity_Error, "blitpass references a deleted source render buffer"); + status = addValidationMessage(EValidationSeverity::Error, "blitpass references a deleted source render buffer"); if (!getIScene().isRenderBufferAllocated(blitPass.destinationRenderBuffer)) - status = addValidationMessage(EValidationSeverity_Error, "blitpass references a deleted destination render buffer"); + status = addValidationMessage(EValidationSeverity::Error, "blitpass references a deleted destination render buffer"); return status; } diff --git a/client/ramses-client/impl/BlitPassImpl.h b/client/ramses-client/impl/BlitPassImpl.h index e7c2fcc69..ed8c83ac4 100644 --- a/client/ramses-client/impl/BlitPassImpl.h +++ b/client/ramses-client/impl/BlitPassImpl.h @@ -12,6 +12,8 @@ #include "SceneObjectImpl.h" #include "SceneAPI/Handles.h" +#include + namespace ramses { class RenderBufferImpl; @@ -20,15 +22,15 @@ namespace ramses class BlitPassImpl final : public SceneObjectImpl { public: - BlitPassImpl(SceneImpl& scene, const char* blitpassName); - virtual ~BlitPassImpl() override; + BlitPassImpl(SceneImpl& scene, std::string_view blitpassName); + ~BlitPassImpl() override; void initializeFrameworkData(const RenderBufferImpl& sourceRenderBuffer, const RenderBufferImpl& destinationRenderBuffer); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; + status_t validate() const override; const RenderBuffer& getSourceRenderBuffer() const; const RenderBuffer& getDestinationRenderBuffer() const; diff --git a/client/ramses-client/impl/CameraNodeImpl.cpp b/client/ramses-client/impl/CameraNodeImpl.cpp index ebca88a2b..33868bf18 100644 --- a/client/ramses-client/impl/CameraNodeImpl.cpp +++ b/client/ramses-client/impl/CameraNodeImpl.cpp @@ -6,18 +6,17 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. // ------------------------------------------------------------------------- -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector4f.h" +#include "ramses-client-api/DataObject.h" #include "CameraNodeImpl.h" #include "DataObjectImpl.h" #include "SerializationContext.h" #include "Scene/ClientScene.h" #include "Math3d/CameraMatrixHelper.h" +#include namespace ramses { - CameraNodeImpl::CameraNodeImpl(SceneImpl& scene, ERamsesObjectType cameraType, const char* cameraName) + CameraNodeImpl::CameraNodeImpl(SceneImpl& scene, ERamsesObjectType cameraType, std::string_view cameraName) : NodeImpl(scene, cameraType, cameraName) { } @@ -100,7 +99,7 @@ namespace ramses getIScene().setDataSingleVector4f(m_frustumPlanesDataReference, ramses_internal::DataFieldHandle{ 0 }, { -1.f, 1.f, -1.f, 1.f }); getIScene().setDataSingleVector2f(m_frustumNearFarDataReference, ramses_internal::DataFieldHandle{ 0 }, { 0.1f, 1.f }); - const auto projType = (getType() == ERamsesObjectType_PerspectiveCamera ? ramses_internal::ECameraProjectionType::Perspective : ramses_internal::ECameraProjectionType::Orthographic); + const auto projType = (getType() == ERamsesObjectType::PerspectiveCamera ? ramses_internal::ECameraProjectionType::Perspective : ramses_internal::ECameraProjectionType::Orthographic); m_cameraHandle = getIScene().allocateCamera(projType, getNodeHandle(), m_dataInstance, ramses_internal::CameraHandle::Invalid()); } @@ -136,16 +135,16 @@ namespace ramses status_t status = NodeImpl::validate(); if (!m_frustumInitialized && !isFrustumPlanesBound()) - status = addValidationMessage(EValidationSeverity_Error, "Camera frustum is not initialized!"); + status = addValidationMessage(EValidationSeverity::Error, "Camera frustum is not initialized!"); if (!getProjectionParams().isValid()) - status = addValidationMessage(EValidationSeverity_Error, "Camera frustum invalid!"); + status = addValidationMessage(EValidationSeverity::Error, "Camera frustum invalid!"); if (!m_viewportInitialized && !(isViewportOffsetBound() && isViewportSizeBound())) - status = addValidationMessage(EValidationSeverity_Error, "Camera viewport is not initialized!"); + status = addValidationMessage(EValidationSeverity::Error, "Camera viewport is not initialized!"); if (getViewportWidth() == 0 || getViewportHeight() == 0) - status = addValidationMessage(EValidationSeverity_Error, "Camera viewport invalid!"); + status = addValidationMessage(EValidationSeverity::Error, "Camera viewport invalid!"); return status; } @@ -157,7 +156,7 @@ namespace ramses status_t CameraNodeImpl::setPerspectiveFrustum(float fovY, float aspectRatio, float nearPlane, float farPlane) { - assert(isOfType(ERamsesObjectType_PerspectiveCamera)); + assert(isOfType(ERamsesObjectType::PerspectiveCamera)); const auto params = ramses_internal::ProjectionParams::Perspective(fovY, aspectRatio, nearPlane, farPlane); if (!params.isValid()) @@ -171,13 +170,13 @@ namespace ramses float CameraNodeImpl::getVerticalFieldOfView() const { - assert(isOfType(ERamsesObjectType_PerspectiveCamera)); + assert(isOfType(ERamsesObjectType::PerspectiveCamera)); return ramses_internal::ProjectionParams::GetPerspectiveFovY(getProjectionParams()); } float CameraNodeImpl::getAspectRatio() const { - assert(isOfType(ERamsesObjectType_PerspectiveCamera)); + assert(isOfType(ERamsesObjectType::PerspectiveCamera)); return ramses_internal::ProjectionParams::GetAspectRatio(getProjectionParams()); } @@ -277,15 +276,14 @@ namespace ramses return m_cameraHandle; } - status_t CameraNodeImpl::getProjectionMatrix(float(&projectionMatrix)[16]) const + status_t CameraNodeImpl::getProjectionMatrix(matrix44f& projectionMatrix) const { if (!m_frustumInitialized) { return addErrorEntry("CameraImpl::getProjectionMatrix failed - Camera frustum is not initialized!"); } - const ramses_internal::Matrix44f projMatrix = ramses_internal::CameraMatrixHelper::ProjectionMatrix(getProjectionParams()); - ramses_internal::PlatformMemory::Copy(projectionMatrix, projMatrix.data, sizeof(projectionMatrix)); + projectionMatrix = ramses_internal::CameraMatrixHelper::ProjectionMatrix(getProjectionParams()); return StatusOK; } @@ -313,37 +311,37 @@ namespace ramses getIScene().setDataSingleVector2f(m_frustumNearFarDataReference, ramses_internal::DataFieldHandle{ 0 }, { params.nearPlane, params.farPlane }); } - status_t CameraNodeImpl::bindViewportOffset(const DataVector2i& offsetData) + status_t CameraNodeImpl::bindViewportOffset(const DataObject& offsetData) { - if (!isFromTheSameSceneAs(offsetData.impl)) - { + if (offsetData.getDataType() != EDataType::Vector2I) + return addErrorEntry("Camera::bindViewportOffset failed, data object must be of type EDataType::Vector2I"); + if (!isFromTheSameSceneAs(offsetData.m_impl)) return addErrorEntry("Camera::bindViewportOffset failed, viewport offset data object is not from the same scene as this camera"); - } - getIScene().setDataReference(m_dataInstance, ramses_internal::Camera::ViewportOffsetField, offsetData.impl.getDataReference()); + getIScene().setDataReference(m_dataInstance, ramses_internal::Camera::ViewportOffsetField, offsetData.m_impl.getDataReference()); return StatusOK; } - status_t CameraNodeImpl::bindViewportSize(const DataVector2i& sizeData) + status_t CameraNodeImpl::bindViewportSize(const DataObject& sizeData) { - if (!isFromTheSameSceneAs(sizeData.impl)) - { + if (sizeData.getDataType() != EDataType::Vector2I) + return addErrorEntry("Camera::bindViewportSize failed, data object must be of type EDataType::Vector2I"); + if (!isFromTheSameSceneAs(sizeData.m_impl)) return addErrorEntry("Camera::bindViewportSize failed, viewport size data object is not from the same scene as this camera"); - } - getIScene().setDataReference(m_dataInstance, ramses_internal::Camera::ViewportSizeField, sizeData.impl.getDataReference()); + getIScene().setDataReference(m_dataInstance, ramses_internal::Camera::ViewportSizeField, sizeData.m_impl.getDataReference()); return StatusOK; } - status_t CameraNodeImpl::bindFrustumPlanes(const DataVector4f& frustumPlanesData, const DataVector2f& nearFarData) + status_t CameraNodeImpl::bindFrustumPlanes(const DataObject& frustumPlanesData, const DataObject& nearFarData) { - if (!isFromTheSameSceneAs(frustumPlanesData.impl) || !isFromTheSameSceneAs(nearFarData.impl)) - { + if (frustumPlanesData.getDataType() != EDataType::Vector4F || nearFarData.getDataType() != EDataType::Vector2F) + return addErrorEntry("Camera::bindFrustumPlanes failed, data objects must be of type EDataType::Vector4F and EDataType::Vector2F"); + if (!isFromTheSameSceneAs(frustumPlanesData.m_impl) || !isFromTheSameSceneAs(nearFarData.m_impl)) return addErrorEntry("Camera::bindFrustumPlanes failed, one of the frustum planes data object is not from the same scene as this camera"); - } - getIScene().setDataReference(m_dataInstance, ramses_internal::Camera::FrustumPlanesField, frustumPlanesData.impl.getDataReference()); - getIScene().setDataReference(m_dataInstance, ramses_internal::Camera::FrustumNearFarPlanesField, nearFarData.impl.getDataReference()); + getIScene().setDataReference(m_dataInstance, ramses_internal::Camera::FrustumPlanesField, frustumPlanesData.m_impl.getDataReference()); + getIScene().setDataReference(m_dataInstance, ramses_internal::Camera::FrustumNearFarPlanesField, nearFarData.m_impl.getDataReference()); return StatusOK; } diff --git a/client/ramses-client/impl/CameraNodeImpl.h b/client/ramses-client/impl/CameraNodeImpl.h index a5c598a11..e90d6ac82 100644 --- a/client/ramses-client/impl/CameraNodeImpl.h +++ b/client/ramses-client/impl/CameraNodeImpl.h @@ -17,17 +17,17 @@ #include "SceneAPI/ECameraProjectionType.h" #include "Math3d/ProjectionParams.h" +#include + namespace ramses { - class DataVector2i; - class DataVector2f; - class DataVector4f; + class DataObject; class CameraNodeImpl final : public NodeImpl { public: - CameraNodeImpl(SceneImpl& scene, ERamsesObjectType cameraType, const char* cameraName); - virtual ~CameraNodeImpl() override; + CameraNodeImpl(SceneImpl& scene, ERamsesObjectType cameraType, std::string_view cameraName); + ~CameraNodeImpl() override; // Common for all camera types ramses_internal::ECameraProjectionType getProjectionType() const; @@ -39,11 +39,11 @@ namespace ramses uint32_t getViewportHeight() const; void initializeFrameworkData(); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + status_t validate() const override; ramses_internal::CameraHandle getCameraHandle() const; status_t setFrustum(float leftPlane, float rightPlane, float bottomPlane, float topPlane, float nearPlane, float farPlane); @@ -58,11 +58,11 @@ namespace ramses float getVerticalFieldOfView() const; float getAspectRatio() const; - status_t getProjectionMatrix(float(&projectionMatrix)[16]) const; + status_t getProjectionMatrix(matrix44f& projectionMatrix) const; - status_t bindViewportOffset(const DataVector2i& offsetData); - status_t bindViewportSize(const DataVector2i& sizeData); - status_t bindFrustumPlanes(const DataVector4f& frustumPlanesData, const DataVector2f& nearFarData); + status_t bindViewportOffset(const DataObject& offsetData); + status_t bindViewportSize(const DataObject& sizeData); + status_t bindFrustumPlanes(const DataObject& frustumPlanesData, const DataObject& nearFarData); status_t unbindViewportOffset(); status_t unbindViewportSize(); status_t unbindFrustumPlanes(); diff --git a/client/ramses-client/impl/ClientApplicationLogic.cpp b/client/ramses-client/impl/ClientApplicationLogic.cpp index eaa51dfe9..ce454b042 100644 --- a/client/ramses-client/impl/ClientApplicationLogic.cpp +++ b/client/ramses-client/impl/ClientApplicationLogic.cpp @@ -63,7 +63,7 @@ namespace ramses_internal m_scenegraphProviderComponent->handleUnpublishScene(sceneId); } - Bool ClientApplicationLogic::isScenePublished(SceneId sceneId) const + bool ClientApplicationLogic::isScenePublished(SceneId sceneId) const { return m_publishedScenes.contains(sceneId); } diff --git a/client/ramses-client/impl/ClientApplicationLogic.h b/client/ramses-client/impl/ClientApplicationLogic.h index 39faa5d3d..2c07ebacd 100644 --- a/client/ramses-client/impl/ClientApplicationLogic.h +++ b/client/ramses-client/impl/ClientApplicationLogic.h @@ -40,7 +40,7 @@ namespace ramses_internal { public: explicit ClientApplicationLogic(const Guid& myId, PlatformLock& frameworkLock); - virtual ~ClientApplicationLogic() override; + ~ClientApplicationLogic() override; void init(IResourceProviderComponent& resources, ISceneGraphProviderComponent& scenegraph); void deinit(); @@ -49,24 +49,24 @@ namespace ramses_internal void createScene(ClientScene& scene, bool enableLocalOnlyOptimization); void publishScene(SceneId sceneId, EScenePublicationMode publicationMode); void unpublishScene(SceneId sceneId); - Bool isScenePublished(SceneId sceneId) const; + [[nodiscard]] bool isScenePublished(SceneId sceneId) const; bool flush(SceneId sceneId, const FlushTimeInformation& timeInfo, SceneVersionTag versionTag); void removeScene(SceneId sceneId); - virtual void handleSceneReferenceEvent(SceneReferenceEvent const& event, const Guid& rendererId) override; - virtual void handleResourceAvailabilityEvent(ResourceAvailabilityEvent const& event, const Guid& rendererId) override; + void handleSceneReferenceEvent(SceneReferenceEvent const& event, const Guid& rendererId) override; + void handleResourceAvailabilityEvent(ResourceAvailabilityEvent const& event, const Guid& rendererId) override; // Resource handling ManagedResource addResource(const IResource* resource); - ManagedResource getResource(ResourceContentHash hash) const; - ManagedResource loadResource(const ResourceContentHash& hash) const; - ResourceHashUsage getHashUsage(const ResourceContentHash& hash) const; + [[nodiscard]] ManagedResource getResource(ResourceContentHash hash) const; + [[nodiscard]] ManagedResource loadResource(const ResourceContentHash& hash) const; + [[nodiscard]] ResourceHashUsage getHashUsage(const ResourceContentHash& hash) const; SceneFileHandle addResourceFile(InputStreamContainerSPtr resourceFileInputStream, const ResourceTableOfContents& toc); void removeResourceFile(SceneFileHandle handle); void loadResourceFromFile(SceneFileHandle handle); void reserveResourceCount(uint32_t totalCount); - bool hasResourceFile(SceneFileHandle handle) const; + [[nodiscard]] bool hasResourceFile(SceneFileHandle handle) const; std::vector popSceneReferenceEvents(); diff --git a/client/ramses-client/impl/ClientCommands/DumpSceneToFile.cpp b/client/ramses-client/impl/ClientCommands/DumpSceneToFile.cpp index 27086dcb9..34fec6bcb 100644 --- a/client/ramses-client/impl/ClientCommands/DumpSceneToFile.cpp +++ b/client/ramses-client/impl/ClientCommands/DumpSceneToFile.cpp @@ -23,11 +23,11 @@ namespace ramses_internal getArgument<2>().setDefaultValue(""); } - Bool DumpSceneToFile::execute(uint64_t& sceneId, String& fileName, String& sendViaDLT) const + bool DumpSceneToFile::execute(uint64_t& sceneId, std::string& fileName, std::string& sendViaDLT) const { SceneCommandDumpSceneToFile command; command.fileName = fileName; - command.sendViaDLT = sendViaDLT == String("-sendViaDLT"); + command.sendViaDLT = sendViaDLT == "-sendViaDLT"; m_client.enqueueSceneCommand(ramses::sceneId_t(sceneId), std::move(command)); return true; diff --git a/client/ramses-client/impl/ClientCommands/DumpSceneToFile.h b/client/ramses-client/impl/ClientCommands/DumpSceneToFile.h index 899f825a3..6f234dcaf 100644 --- a/client/ramses-client/impl/ClientCommands/DumpSceneToFile.h +++ b/client/ramses-client/impl/ClientCommands/DumpSceneToFile.h @@ -10,9 +10,10 @@ #define RAMSES_DUMPSCENETOFILE_H #include "Ramsh/RamshCommandArguments.h" -#include "Collections/String.h" #include "ramses-framework-api/RamsesFrameworkTypes.h" +#include + namespace ramses { class RamsesClientImpl; @@ -20,11 +21,11 @@ namespace ramses namespace ramses_internal { - class DumpSceneToFile : public RamshCommandArgs + class DumpSceneToFile : public RamshCommandArgs { public: explicit DumpSceneToFile(ramses::RamsesClientImpl& client); - virtual Bool execute(uint64_t& sceneId, String& fileName, String& sendViaDLT) const override; + bool execute(uint64_t& sceneId, std::string& fileName, std::string& sendViaDLT) const override; private: ramses::RamsesClientImpl& m_client; diff --git a/client/ramses-client/impl/ClientCommands/FlushSceneVersion.cpp b/client/ramses-client/impl/ClientCommands/FlushSceneVersion.cpp index 2fd32687e..34b815c16 100644 --- a/client/ramses-client/impl/ClientCommands/FlushSceneVersion.cpp +++ b/client/ramses-client/impl/ClientCommands/FlushSceneVersion.cpp @@ -21,7 +21,7 @@ namespace ramses_internal getArgument<1>().setDescription("scene id"); } - Bool FlushSceneVersion::execute(ramses::sceneVersionTag_t& sceneVersion, uint64_t& sceneId) const + bool FlushSceneVersion::execute(ramses::sceneVersionTag_t& sceneVersion, uint64_t& sceneId) const { SceneCommandFlushSceneVersion command; command.sceneVersion = sceneVersion; diff --git a/client/ramses-client/impl/ClientCommands/FlushSceneVersion.h b/client/ramses-client/impl/ClientCommands/FlushSceneVersion.h index c2cca3bd0..f2178001f 100644 --- a/client/ramses-client/impl/ClientCommands/FlushSceneVersion.h +++ b/client/ramses-client/impl/ClientCommands/FlushSceneVersion.h @@ -23,7 +23,7 @@ namespace ramses_internal { public: explicit FlushSceneVersion(ramses::RamsesClientImpl& client); - virtual Bool execute(ramses::sceneVersionTag_t& sceneVersion, uint64_t& sceneId) const override; + bool execute(ramses::sceneVersionTag_t& sceneVersion, uint64_t& sceneId) const override; private: ramses::RamsesClientImpl& m_client; diff --git a/client/ramses-client/impl/ClientCommands/ForceFallbackImage.cpp b/client/ramses-client/impl/ClientCommands/ForceFallbackImage.cpp deleted file mode 100644 index 2d295f8c2..000000000 --- a/client/ramses-client/impl/ClientCommands/ForceFallbackImage.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2017 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ForceFallbackImage.h" -#include "RamsesClientImpl.h" -#include "SceneCommandBuffer.h" - -namespace ramses_internal -{ - ForceFallbackImage::ForceFallbackImage(ramses::RamsesClientImpl& client) - : m_client(client) - { - description = "force fallback image"; - registerKeyword("forceFallbackImage"); - getArgument<0>().setDescription("fallback flag"); - getArgument<1>().setDescription("scene id"); - getArgument<2>().setDescription("stream texture name"); - } - - Bool ForceFallbackImage::execute(UInt32& forceFallback, uint64_t& sceneId, String& streamTextureName) const - { - SceneCommandForceFallback command; - command.streamTextureName = streamTextureName; - command.forceFallback = forceFallback != 0u; - - m_client.enqueueSceneCommand(ramses::sceneId_t(sceneId), std::move(command)); - return true; - } -} diff --git a/client/ramses-client/impl/ClientCommands/ForceFallbackImage.h b/client/ramses-client/impl/ClientCommands/ForceFallbackImage.h deleted file mode 100644 index ca92f34c2..000000000 --- a/client/ramses-client/impl/ClientCommands/ForceFallbackImage.h +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2017 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_FORCEFALLBACKIMAGE_H -#define RAMSES_FORCEFALLBACKIMAGE_H - -#include "Ramsh/RamshCommandArguments.h" -#include "Collections/String.h" -#include "ramses-framework-api/RamsesFrameworkTypes.h" - -namespace ramses -{ - class RamsesClientImpl; -} - -namespace ramses_internal -{ - class ForceFallbackImage : public RamshCommandArgs - { - public: - explicit ForceFallbackImage(ramses::RamsesClientImpl& client); - virtual Bool execute(UInt32& forceFallback, uint64_t& sceneId, String& streamTextureName) const override; - - private: - ramses::RamsesClientImpl& m_client; - }; -} - -#endif //RAMSES_FORCEFALLBACKIMAGE_H diff --git a/client/ramses-client/impl/ClientCommands/LogMemoryUtils.cpp b/client/ramses-client/impl/ClientCommands/LogMemoryUtils.cpp index c15624257..c0b9fe452 100644 --- a/client/ramses-client/impl/ClientCommands/LogMemoryUtils.cpp +++ b/client/ramses-client/impl/ClientCommands/LogMemoryUtils.cpp @@ -19,7 +19,6 @@ #include "SceneAPI/DataSlot.h" #include "SceneAPI/TextureBuffer.h" #include "SceneAPI/GeometryDataBuffer.h" -#include "SceneAPI/StreamTexture.h" #include "SceneAPI/RenderBuffer.h" #include "SceneAPI/RenderTarget.h" #include "SceneAPI/BlitPass.h" @@ -35,12 +34,12 @@ namespace ramses_internal { MemoryInfoVector memoryInfos; ramses::RamsesObjectVector resources; - scene.getObjectRegistry().getObjectsOfType(resources, ramses::ERamsesObjectType_Resource); + scene.getObjectRegistry().getObjectsOfType(resources, ramses::ERamsesObjectType::Resource); for (const auto it : resources) { const ramses::Resource& resource = ramses::RamsesObjectTypeUtils::ConvertTo(*it); - const IResource* resourceObject = scene.getClientImpl().getResource(resource.impl.getLowlevelResourceHash()).get(); + const IResource* resourceObject = scene.getClientImpl().getResource(resource.m_impl.getLowlevelResourceHash()).get(); if (nullptr != resourceObject) { @@ -57,7 +56,7 @@ namespace ramses_internal } } - auto createMemInfo = [](const String& logMessage, uint32_t numElements, const std::function< size_t(uint32_t) >& sizeOfIndividualElement){ + auto createMemInfo = [](const auto& logMessage, uint32_t numElements, const std::function< size_t(uint32_t) >& sizeOfIndividualElement){ StringOutputStream stream; stream << numElements << " " << logMessage << " allocated"; @@ -80,7 +79,6 @@ namespace ramses_internal memoryInfos.push_back(createMemInfo("BlitPasses", iscene.getBlitPassCount(), [](uint32_t){return sizeof(BlitPass);})); memoryInfos.push_back(createMemInfo("RenderBuffers", iscene.getRenderBufferCount(), [](uint32_t){return sizeof(RenderBuffer);})); memoryInfos.push_back(createMemInfo("TextureSamplers", iscene.getTextureSamplerCount(), [](uint32_t){return sizeof(TextureSampler);})); - memoryInfos.push_back(createMemInfo("StreamTextures", iscene.getStreamTextureCount(), [](uint32_t){return sizeof(StreamTexture);})); memoryInfos.push_back(createMemInfo("DataSlots", iscene.getDataSlotCount(), [](uint32_t){return sizeof(DataSlot);})); memoryInfos.push_back(createMemInfo("Nodes", iscene.getNodeCount(), [&iscene](uint32_t h){ diff --git a/client/ramses-client/impl/ClientCommands/LogMemoryUtils.h b/client/ramses-client/impl/ClientCommands/LogMemoryUtils.h index 3345be40e..75487ef50 100644 --- a/client/ramses-client/impl/ClientCommands/LogMemoryUtils.h +++ b/client/ramses-client/impl/ClientCommands/LogMemoryUtils.h @@ -10,15 +10,16 @@ #define RAMSES_LOGMEMORYUTILS_H #include "SceneImpl.h" -#include "Collections/String.h" #include "Collections/Vector.h" +#include + namespace ramses_internal { struct MemoryInfo { - uint32_t memoryUsage = 0; - String logInfoMesage = ""; + uint32_t memoryUsage{0}; + std::string logInfoMesage{}; }; using MemoryInfoVector = std::vector; diff --git a/client/ramses-client/impl/ClientCommands/LogResourceMemoryUsage.cpp b/client/ramses-client/impl/ClientCommands/LogResourceMemoryUsage.cpp index 164af0dc7..6f35fd16e 100644 --- a/client/ramses-client/impl/ClientCommands/LogResourceMemoryUsage.cpp +++ b/client/ramses-client/impl/ClientCommands/LogResourceMemoryUsage.cpp @@ -7,6 +7,7 @@ // ------------------------------------------------------------------------- #include "LogResourceMemoryUsage.h" +#include "SceneCommandBuffer.h" #include "RamsesClientImpl.h" #include "Utils/LogMacros.h" @@ -20,7 +21,7 @@ namespace ramses_internal registerKeyword("lm"); } - Bool LogResourceMemoryUsage::execute(uint64_t& sceneId) const + bool LogResourceMemoryUsage::execute(uint64_t& sceneId) const { LOG_INFO(CONTEXT_CLIENT,"LogResourceMemoryUsage"); SceneCommandLogResourceMemoryUsage command; diff --git a/client/ramses-client/impl/ClientCommands/LogResourceMemoryUsage.h b/client/ramses-client/impl/ClientCommands/LogResourceMemoryUsage.h index 1a5af8486..97fb7492d 100644 --- a/client/ramses-client/impl/ClientCommands/LogResourceMemoryUsage.h +++ b/client/ramses-client/impl/ClientCommands/LogResourceMemoryUsage.h @@ -23,7 +23,7 @@ namespace ramses_internal { public: explicit LogResourceMemoryUsage(ramses::RamsesClientImpl& client); - virtual Bool execute(uint64_t& sceneId) const override; + bool execute(uint64_t& sceneId) const override; private: ramses::RamsesClientImpl& m_client; }; diff --git a/client/ramses-client/impl/ClientCommands/PrintSceneList.cpp b/client/ramses-client/impl/ClientCommands/PrintSceneList.cpp index 077ffbb54..4f9769eaa 100644 --- a/client/ramses-client/impl/ClientCommands/PrintSceneList.cpp +++ b/client/ramses-client/impl/ClientCommands/PrintSceneList.cpp @@ -8,7 +8,6 @@ #include "PrintSceneList.h" #include "ramses-client-api/Scene.h" -#include "SceneImpl.h" #include "RamsesClientImpl.h" #include "Utils/LogMacros.h" @@ -21,18 +20,16 @@ namespace ramses_internal registerKeyword("printSceneList"); } - Bool PrintSceneList::executeInput(const std::vector& input) + bool PrintSceneList::executeInput(const std::vector& input) { UNUSED(input); LOG_INFO_F(CONTEXT_CLIENT, ([&](StringOutputStream& sos) { sos << "PrintSceneList::executeInput: "; sos << "Scenes: \n\r"; - - const ramses::SceneVector sceneList = m_client.getListOfScenes(); - for (auto scene : sceneList) + for (const auto& scene : m_client.getListOfScenes()) { - sos << " Id: " << scene->impl.getSceneId() << " Name: \"" << scene->getName() << "\"\n\r"; + sos << " Id: " << scene->getSceneId() << " Name: \"" << scene->getName() << "\"\n\r"; } })); diff --git a/client/ramses-client/impl/ClientCommands/PrintSceneList.h b/client/ramses-client/impl/ClientCommands/PrintSceneList.h index b4d8a1062..bb0ec6da1 100644 --- a/client/ramses-client/impl/ClientCommands/PrintSceneList.h +++ b/client/ramses-client/impl/ClientCommands/PrintSceneList.h @@ -22,7 +22,7 @@ namespace ramses_internal { public: explicit PrintSceneList(const ramses::RamsesClientImpl& client); - virtual bool executeInput(const std::vector& input) override; + bool executeInput(const std::vector& input) override; private: const ramses::RamsesClientImpl& m_client; }; diff --git a/client/ramses-client/impl/ClientCommands/SceneCommandBuffer.h b/client/ramses-client/impl/ClientCommands/SceneCommandBuffer.h index 3dd39bee4..1d593d75a 100644 --- a/client/ramses-client/impl/ClientCommands/SceneCommandBuffer.h +++ b/client/ramses-client/impl/ClientCommands/SceneCommandBuffer.h @@ -11,20 +11,15 @@ #include "ramses-framework-api/EValidationSeverity.h" #include "ramses-framework-api/RamsesFrameworkTypes.h" -#include "Collections/String.h" #include "PlatformAbstraction/VariantWrapper.h" + #include #include +#include namespace ramses_internal { // Commands - struct SceneCommandForceFallback - { - String streamTextureName; - bool forceFallback; - }; - struct SceneCommandFlushSceneVersion { ramses::sceneVersionTag_t sceneVersion; @@ -32,13 +27,13 @@ namespace ramses_internal struct SceneCommandValidationRequest { - ramses::EValidationSeverity severity = ramses::EValidationSeverity_Info; - String optionalObjectName; + ramses::EValidationSeverity severity = ramses::EValidationSeverity::Info; + std::string optionalObjectName; }; struct SceneCommandDumpSceneToFile { - String fileName; + std::string fileName; bool sendViaDLT; }; @@ -60,8 +55,7 @@ namespace ramses_internal void execute(V&& visitor); private: - using CommandVariant = absl::variant; @@ -87,7 +81,7 @@ namespace ramses_internal } for (const auto& v : localBuffer) - absl::visit(visitor, v); + std::visit(visitor, v); } } #endif diff --git a/client/ramses-client/impl/ClientCommands/SceneCommandVisitor.cpp b/client/ramses-client/impl/ClientCommands/SceneCommandVisitor.cpp index ce7404e33..9f6346d91 100644 --- a/client/ramses-client/impl/ClientCommands/SceneCommandVisitor.cpp +++ b/client/ramses-client/impl/ClientCommands/SceneCommandVisitor.cpp @@ -12,29 +12,11 @@ #include "SceneImpl.h" #include "RamsesClientImpl.h" #include "LogMemoryUtils.h" -#include "ramses-client-api/StreamTexture.h" #include "Utils/LogMacros.h" #include namespace ramses_internal { - void SceneCommandVisitor::operator()(const SceneCommandForceFallback& cmd) - { - ramses::RamsesObject* object = m_scene.findObjectByName(cmd.streamTextureName.c_str()); - if (object) - { - if (object->getType() == ramses::ERamsesObjectType_StreamTexture) - { - ramses::StreamTexture& streamtexture = ramses::RamsesObjectTypeUtils::ConvertTo(*object); - LOG_INFO(CONTEXT_CLIENT, "SceneCommandVisitor::execute: " << (cmd.forceFallback?"Enable":"Disable") << " force fallback image for \"" << cmd.streamTextureName << "\""); - streamtexture.forceFallbackImage(cmd.forceFallback); - } - else - LOG_ERROR(CONTEXT_CLIENT, "SceneCommandVisitor::execute: Set force fallback setting but the object with name \"" << cmd.streamTextureName << "\" is not a StreamTexture"); - } - else - LOG_ERROR(CONTEXT_CLIENT, "SceneCommandVisitor::execute: Set force fallback setting but couldn't find any object with name \"" << cmd.streamTextureName << "\""); - } void SceneCommandVisitor::operator()(const SceneCommandFlushSceneVersion& cmd) { @@ -54,7 +36,7 @@ namespace ramses_internal { if (const ramses::RamsesObject* ro = m_scene.findObjectByName(cmd.optionalObjectName.c_str())) { - ro->validate(); + std::ignore = ro->validate(); LOG_INFO(ramses_internal::CONTEXT_CLIENT, "Validation: " << ro->getValidationReport(cmd.severity)); } else @@ -64,7 +46,7 @@ namespace ramses_internal void SceneCommandVisitor::operator()(const SceneCommandDumpSceneToFile& cmd) const { - const String sceneDumpFileWithExtension = cmd.fileName + ".ramses"; + const std::string sceneDumpFileWithExtension = cmd.fileName + ".ramses"; const ramses::status_t status = m_scene.saveToFile(sceneDumpFileWithExtension.c_str(), false); if (status == ramses::StatusOK) { @@ -94,7 +76,7 @@ namespace ramses_internal })); } - void SceneCommandVisitor::SendSceneViaDLT(const String& sceneDumpFileName) + void SceneCommandVisitor::SendSceneViaDLT(const std::string& sceneDumpFileName) { if (GetRamsesLogger().transmitFile(sceneDumpFileName, false)) { diff --git a/client/ramses-client/impl/ClientCommands/SceneCommandVisitor.h b/client/ramses-client/impl/ClientCommands/SceneCommandVisitor.h index ac70752c2..3f5a4af82 100644 --- a/client/ramses-client/impl/ClientCommands/SceneCommandVisitor.h +++ b/client/ramses-client/impl/ClientCommands/SceneCommandVisitor.h @@ -10,6 +10,8 @@ #ifndef RAMSES_SCENECOMMANDVISITOR_H #define RAMSES_SCENECOMMANDVISITOR_H +#include + namespace ramses { class SceneImpl; @@ -18,12 +20,10 @@ namespace ramses namespace ramses_internal { - struct SceneCommandForceFallback; struct SceneCommandFlushSceneVersion; struct SceneCommandValidationRequest; struct SceneCommandDumpSceneToFile; struct SceneCommandLogResourceMemoryUsage; - class String; class SceneCommandVisitor { @@ -32,14 +32,13 @@ namespace ramses_internal : m_scene(scene) {} - void operator()(const SceneCommandForceFallback& cmd); void operator()(const SceneCommandFlushSceneVersion& cmd); void operator()(const SceneCommandValidationRequest& cmd); void operator()(const SceneCommandDumpSceneToFile& cmd) const; void operator()(const SceneCommandLogResourceMemoryUsage& cmd) const; private: - static void SendSceneViaDLT(const String& sceneDumpFileName); + static void SendSceneViaDLT(const std::string& sceneDumpFileName); ramses::SceneImpl& m_scene; }; diff --git a/client/ramses-client/impl/ClientCommands/ValidateCommand.cpp b/client/ramses-client/impl/ClientCommands/ValidateCommand.cpp index f73c0679c..b1490d2fe 100644 --- a/client/ramses-client/impl/ClientCommands/ValidateCommand.cpp +++ b/client/ramses-client/impl/ClientCommands/ValidateCommand.cpp @@ -27,21 +27,21 @@ namespace ramses_internal getArgument<2>().setDefaultValue(""); } - Bool ValidateCommand::execute(uint64_t& sceneId, String& severity, String& objectName) const + bool ValidateCommand::execute(uint64_t& sceneId, std::string& severity, std::string& objectName) const { SceneCommandValidationRequest command; - if (severity == String("info")) + if (severity == "info") { - command.severity = ramses::EValidationSeverity_Info; + command.severity = ramses::EValidationSeverity::Info; } - else if (severity == String("warning")) + else if (severity == "warning") { - command.severity = ramses::EValidationSeverity_Warning; + command.severity = ramses::EValidationSeverity::Warning; } - else if (severity == String("error")) + else if (severity == "error") { - command.severity = ramses::EValidationSeverity_Error; + command.severity = ramses::EValidationSeverity::Error; } else { diff --git a/client/ramses-client/impl/ClientCommands/ValidateCommand.h b/client/ramses-client/impl/ClientCommands/ValidateCommand.h index d10b617de..1fba1e6d0 100644 --- a/client/ramses-client/impl/ClientCommands/ValidateCommand.h +++ b/client/ramses-client/impl/ClientCommands/ValidateCommand.h @@ -10,9 +10,10 @@ #define RAMSES_VALIDATECOMMAND_H #include "Ramsh/RamshCommandArguments.h" -#include "Collections/String.h" #include "ramses-framework-api/RamsesFrameworkTypes.h" +#include + namespace ramses { class RamsesClientImpl; @@ -20,11 +21,11 @@ namespace ramses namespace ramses_internal { - class ValidateCommand : public RamshCommandArgs + class ValidateCommand : public RamshCommandArgs { public: explicit ValidateCommand(ramses::RamsesClientImpl& client); - virtual Bool execute(uint64_t& sceneId, String& severity, String& objectName) const override; + bool execute(uint64_t& sceneId, std::string& severity, std::string& objectName) const override; private: ramses::RamsesClientImpl& m_client; }; diff --git a/client/ramses-client/impl/ClientFactory.cpp b/client/ramses-client/impl/ClientFactory.cpp index daa6ac195..d45877d87 100644 --- a/client/ramses-client/impl/ClientFactory.cpp +++ b/client/ramses-client/impl/ClientFactory.cpp @@ -12,11 +12,11 @@ namespace ramses { - ClientUniquePtr ClientFactory::createClient(RamsesFrameworkImpl* impl, const char* applicationName) const + ClientUniquePtr ClientFactory::createClient(RamsesFrameworkImpl& framework, std::string_view applicationName) const { - ClientUniquePtr client(new RamsesClient(*new RamsesClientImpl(*impl, applicationName)), - [](RamsesClient* client_) { delete client_; }); - return client; + auto impl = std::make_unique(framework, applicationName); + return ClientUniquePtr{ new RamsesClient{ std::move(impl) }, + [](RamsesClient* client_) { delete client_; } }; } bool ClientFactory::RegisterClientFactory() diff --git a/client/ramses-client/impl/ClientFactory.h b/client/ramses-client/impl/ClientFactory.h index 0ff75e64e..04d173a3b 100644 --- a/client/ramses-client/impl/ClientFactory.h +++ b/client/ramses-client/impl/ClientFactory.h @@ -12,6 +12,8 @@ #include "ramses-client-api/RamsesClient.h" #include "RamsesObjectFactoryInterfaces.h" +#include + namespace ramses { class ClientFactory : public IClientFactory @@ -19,7 +21,7 @@ namespace ramses public: static bool RegisterClientFactory(); - virtual ClientUniquePtr createClient(RamsesFrameworkImpl* impl, const char* applicationName) const override; + ClientUniquePtr createClient(RamsesFrameworkImpl& framework, std::string_view applicationName) const override; }; } #endif diff --git a/client/ramses-client/impl/ClientObjectImpl.cpp b/client/ramses-client/impl/ClientObjectImpl.cpp index 28437effd..211f5df37 100644 --- a/client/ramses-client/impl/ClientObjectImpl.cpp +++ b/client/ramses-client/impl/ClientObjectImpl.cpp @@ -10,7 +10,7 @@ namespace ramses { - ClientObjectImpl::ClientObjectImpl(RamsesClientImpl& client, ERamsesObjectType type, const char* name) + ClientObjectImpl::ClientObjectImpl(RamsesClientImpl& client, ERamsesObjectType type, std::string_view name) : RamsesObjectImpl(type, name) , m_client(client) { diff --git a/client/ramses-client/impl/ClientObjectImpl.h b/client/ramses-client/impl/ClientObjectImpl.h index 424a81eee..266a0adb3 100644 --- a/client/ramses-client/impl/ClientObjectImpl.h +++ b/client/ramses-client/impl/ClientObjectImpl.h @@ -11,6 +11,8 @@ #include "RamsesObjectImpl.h" +#include + namespace ramses { class RamsesClientImpl; @@ -18,8 +20,8 @@ namespace ramses class ClientObjectImpl : public RamsesObjectImpl { public: - explicit ClientObjectImpl(RamsesClientImpl& client, ERamsesObjectType type, const char* name); - virtual ~ClientObjectImpl(); + explicit ClientObjectImpl(RamsesClientImpl& client, ERamsesObjectType type, std::string_view name); + ~ClientObjectImpl() override; // impl methods const RamsesClientImpl& getClientImpl() const; diff --git a/client/ramses-client/impl/DataObjectImpl.cpp b/client/ramses-client/impl/DataObjectImpl.cpp index 5ab307a0f..d4cf32393 100644 --- a/client/ramses-client/impl/DataObjectImpl.cpp +++ b/client/ramses-client/impl/DataObjectImpl.cpp @@ -10,36 +10,25 @@ #include "SceneObjectImpl.h" #include "Scene/ClientScene.h" #include "SceneUtils/ISceneDataArrayAccessor.h" -#include "Math3d/Vector4.h" -#include "Math3d/Vector3.h" -#include "Math3d/Vector2.h" -#include "Math3d/Vector4i.h" -#include "Math3d/Vector3i.h" -#include "Math3d/Vector2i.h" -#include "Math3d/Matrix22f.h" -#include "Math3d/Matrix33f.h" -#include "Math3d/Matrix44f.h" #include "SceneAPI/ResourceContentHash.h" +#include "DataTypeUtils.h" namespace ramses { - DataObjectImpl::DataObjectImpl(SceneImpl& scene, ERamsesObjectType type, const char* name) - : SceneObjectImpl(scene, type, name) - , m_dataType(GetDataTypeForDataObjectType(type)) + DataObjectImpl::DataObjectImpl(SceneImpl& scene, ERamsesObjectType ramsesType, EDataType dataType, std::string_view name) + : SceneObjectImpl{ scene, ramsesType, name } + , m_dataType{ dataType } { } - DataObjectImpl::~DataObjectImpl() - { - //done on deinitialization - } + DataObjectImpl::~DataObjectImpl() = default; void DataObjectImpl::initializeFrameworkData() { ramses_internal::ClientScene& scene = getIScene(); // create data layout on scene - m_layoutHandle = scene.allocateDataLayout({ ramses_internal::DataFieldInfo(m_dataType) }, ramses_internal::ResourceContentHash::Invalid()); + m_layoutHandle = scene.allocateDataLayout({ ramses_internal::DataFieldInfo(DataTypeUtils::ConvertDataTypeToInternal(m_dataType)) }, ramses_internal::ResourceContentHash::Invalid()); // allocate data instance based on created layout m_dataReference = scene.allocateDataInstance(m_layoutHandle); @@ -73,49 +62,19 @@ namespace ramses uint32_t enumType = 0u; inStream >> enumType; - m_dataType = static_cast(enumType); + m_dataType = static_cast(enumType); inStream >> m_layoutHandle; inStream >> m_dataReference; return StatusOK; } - ramses_internal::EDataType DataObjectImpl::GetDataTypeForDataObjectType(ERamsesObjectType type) - { - switch (type) - { - case ERamsesObjectType_DataFloat: - return ramses_internal::EDataType::Float; - case ERamsesObjectType_DataVector2f: - return ramses_internal::EDataType::Vector2F; - case ERamsesObjectType_DataVector3f: - return ramses_internal::EDataType::Vector3F; - case ERamsesObjectType_DataVector4f: - return ramses_internal::EDataType::Vector4F; - case ERamsesObjectType_DataMatrix22f: - return ramses_internal::EDataType::Matrix22F; - case ERamsesObjectType_DataMatrix33f: - return ramses_internal::EDataType::Matrix33F; - case ERamsesObjectType_DataMatrix44f: - return ramses_internal::EDataType::Matrix44F; - case ERamsesObjectType_DataInt32: - return ramses_internal::EDataType::Int32; - case ERamsesObjectType_DataVector2i: - return ramses_internal::EDataType::Vector2I; - case ERamsesObjectType_DataVector3i: - return ramses_internal::EDataType::Vector3I; - case ERamsesObjectType_DataVector4i: - return ramses_internal::EDataType::Vector4I; - default: - assert(false); - return ramses_internal::EDataType::Invalid; - } - } - template status_t DataObjectImpl::setValue(const T& value) { - assert(ramses_internal::TypeToEDataTypeTraits::DataType == m_dataType); + if (ramses_internal::TypeToEDataTypeTraits::DataType != DataTypeUtils::ConvertDataTypeToInternal(m_dataType)) + return addErrorEntry("DataObject::setValue failed, value type does not match DataObject data type"); + ramses_internal::ISceneDataArrayAccessor::SetDataArray(&getIScene(), m_dataReference, ramses_internal::DataFieldHandle(0u), 1u, &value); return StatusOK; } @@ -123,7 +82,9 @@ namespace ramses template status_t DataObjectImpl::getValue(T& value) const { - assert(ramses_internal::TypeToEDataTypeTraits::DataType == m_dataType); + if (ramses_internal::TypeToEDataTypeTraits::DataType != DataTypeUtils::ConvertDataTypeToInternal(m_dataType)) + return addErrorEntry("DataObject::getValue failed, value type does not match DataObject data type"); + const T* data = ramses_internal::ISceneDataArrayAccessor::GetDataArray(&getIScene(), m_dataReference, ramses_internal::DataFieldHandle(0u)); assert(data != nullptr); value = data[0]; @@ -131,7 +92,7 @@ namespace ramses return StatusOK; } - ramses_internal::EDataType DataObjectImpl::getDataType() const + EDataType DataObjectImpl::getDataType() const { return m_dataType; } @@ -143,25 +104,25 @@ namespace ramses template status_t DataObjectImpl::setValue(const int32_t&); template status_t DataObjectImpl::setValue(const float&); - template status_t DataObjectImpl::setValue(const ramses_internal::Vector2&); - template status_t DataObjectImpl::setValue(const ramses_internal::Vector3&); - template status_t DataObjectImpl::setValue(const ramses_internal::Vector4&); - template status_t DataObjectImpl::setValue(const ramses_internal::Vector2i&); - template status_t DataObjectImpl::setValue(const ramses_internal::Vector3i&); - template status_t DataObjectImpl::setValue(const ramses_internal::Vector4i&); - template status_t DataObjectImpl::setValue(const ramses_internal::Matrix22f&); - template status_t DataObjectImpl::setValue(const ramses_internal::Matrix33f&); - template status_t DataObjectImpl::setValue(const ramses_internal::Matrix44f&); + template status_t DataObjectImpl::setValue(const glm::vec2&); + template status_t DataObjectImpl::setValue(const glm::vec3&); + template status_t DataObjectImpl::setValue(const glm::vec4&); + template status_t DataObjectImpl::setValue(const glm::ivec2&); + template status_t DataObjectImpl::setValue(const glm::ivec3&); + template status_t DataObjectImpl::setValue(const glm::ivec4&); + template status_t DataObjectImpl::setValue(const glm::mat2&); + template status_t DataObjectImpl::setValue(const glm::mat3&); + template status_t DataObjectImpl::setValue(const glm::mat4&); template status_t DataObjectImpl::getValue(int32_t&) const; template status_t DataObjectImpl::getValue(float&) const; - template status_t DataObjectImpl::getValue(ramses_internal::Vector2&) const; - template status_t DataObjectImpl::getValue(ramses_internal::Vector3&) const; - template status_t DataObjectImpl::getValue(ramses_internal::Vector4&) const; - template status_t DataObjectImpl::getValue(ramses_internal::Vector2i&) const; - template status_t DataObjectImpl::getValue(ramses_internal::Vector3i&) const; - template status_t DataObjectImpl::getValue(ramses_internal::Vector4i&) const; - template status_t DataObjectImpl::getValue(ramses_internal::Matrix22f&) const; - template status_t DataObjectImpl::getValue(ramses_internal::Matrix33f&) const; - template status_t DataObjectImpl::getValue(ramses_internal::Matrix44f&) const; + template status_t DataObjectImpl::getValue(glm::vec2&) const; + template status_t DataObjectImpl::getValue(glm::vec3&) const; + template status_t DataObjectImpl::getValue(glm::vec4&) const; + template status_t DataObjectImpl::getValue(glm::ivec2&) const; + template status_t DataObjectImpl::getValue(glm::ivec3&) const; + template status_t DataObjectImpl::getValue(glm::ivec4&) const; + template status_t DataObjectImpl::getValue(glm::mat2&) const; + template status_t DataObjectImpl::getValue(glm::mat3&) const; + template status_t DataObjectImpl::getValue(glm::mat4&) const; } diff --git a/client/ramses-client/impl/DataObjectImpl.h b/client/ramses-client/impl/DataObjectImpl.h index 6e428619a..316de943f 100644 --- a/client/ramses-client/impl/DataObjectImpl.h +++ b/client/ramses-client/impl/DataObjectImpl.h @@ -11,14 +11,11 @@ //internal #include "SceneObjectImpl.h" -#include "SceneAPI/EDataType.h" -#include "Math3d/Vector2.h" -#include "Math3d/Vector3.h" -#include "Math3d/Vector4.h" -#include "Math3d/Matrix44f.h" -#include "Math3d/Vector2i.h" -#include "Math3d/Vector3i.h" -#include "Math3d/Vector4i.h" +#include "ramses-framework-api/EDataType.h" +#include "DataTypesImpl.h" +#include "SceneAPI/Handles.h" + +#include namespace ramses_internal { @@ -30,30 +27,26 @@ namespace ramses class DataObjectImpl final : public SceneObjectImpl { public: - DataObjectImpl(SceneImpl& scene, ERamsesObjectType type, const char* name); - virtual ~DataObjectImpl() override; + DataObjectImpl(SceneImpl& scene, ERamsesObjectType ramsesType, EDataType dataType, std::string_view name); + ~DataObjectImpl() override; + + void initializeFrameworkData(); + void deinitializeFrameworkData() override; - void initializeFrameworkData(); - virtual void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + EDataType getDataType() const; - // Setters for the stored value template status_t setValue(const T& value); - - // Getters for the stored value template status_t getValue(T& value) const; - ramses_internal::EDataType getDataType() const; ramses_internal::DataInstanceHandle getDataReference() const; private: - static ramses_internal::EDataType GetDataTypeForDataObjectType(ERamsesObjectType type); - - ramses_internal::EDataType m_dataType; + EDataType m_dataType; ramses_internal::DataLayoutHandle m_layoutHandle; ramses_internal::DataInstanceHandle m_dataReference; diff --git a/client/ramses-client/impl/DataSlotUtils.cpp b/client/ramses-client/impl/DataSlotUtils.cpp index db6ad3818..efd7a50e2 100644 --- a/client/ramses-client/impl/DataSlotUtils.cpp +++ b/client/ramses-client/impl/DataSlotUtils.cpp @@ -15,7 +15,7 @@ namespace ramses_internal { bool HasDataSlotId(const ClientScene& scene, DataSlotId id) { - const UInt32 slotHandleCount = scene.getDataSlotCount(); + const uint32_t slotHandleCount = scene.getDataSlotCount(); for (DataSlotHandle slotHandle(0u); slotHandle < slotHandleCount; slotHandle++) { if (scene.isDataSlotAllocated(slotHandle) && scene.getDataSlot(slotHandle).id == id) @@ -29,7 +29,7 @@ namespace ramses_internal bool HasDataSlotIdForNode(const ClientScene& scene, NodeHandle nodeHandle) { - const UInt32 slotHandleCount = scene.getDataSlotCount(); + const uint32_t slotHandleCount = scene.getDataSlotCount(); for (DataSlotHandle slotHandle(0u); slotHandle < slotHandleCount; slotHandle++) { if (scene.isDataSlotAllocated(slotHandle) && scene.getDataSlot(slotHandle).attachedNode == nodeHandle) @@ -43,7 +43,7 @@ namespace ramses_internal bool HasDataSlotIdForDataObject(const ClientScene& scene, DataInstanceHandle dataRef) { - const UInt32 slotHandleCount = scene.getDataSlotCount(); + const uint32_t slotHandleCount = scene.getDataSlotCount(); for (DataSlotHandle slotHandle(0u); slotHandle < slotHandleCount; slotHandle++) { if (scene.isDataSlotAllocated(slotHandle) && @@ -58,7 +58,7 @@ namespace ramses_internal bool HasDataSlotIdForTextureSampler(const ClientScene& scene, TextureSamplerHandle sampler) { - const UInt32 slotHandleCount = scene.getDataSlotCount(); + const uint32_t slotHandleCount = scene.getDataSlotCount(); for (DataSlotHandle slotHandle(0u); slotHandle < slotHandleCount; slotHandle++) { if (scene.isDataSlotAllocated(slotHandle) && @@ -73,7 +73,7 @@ namespace ramses_internal bool HasDataSlotIdForTexture(const ClientScene& scene, const ResourceContentHash& texture) { - const UInt32 slotHandleCount = scene.getDataSlotCount(); + const uint32_t slotHandleCount = scene.getDataSlotCount(); for (DataSlotHandle slotHandle(0u); slotHandle < slotHandleCount; slotHandle++) { if (scene.isDataSlotAllocated(slotHandle) && diff --git a/client/ramses-client/impl/DataTypeUtils.h b/client/ramses-client/impl/DataTypeUtils.h deleted file mode 100644 index 34c0e51bc..000000000 --- a/client/ramses-client/impl/DataTypeUtils.h +++ /dev/null @@ -1,122 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2017 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_CLIENT_DATATYPEUTILS_H -#define RAMSES_CLIENT_DATATYPEUTILS_H - -#include "ramses-client-api/EDataType.h" -#include "SceneAPI/EDataType.h" -#include "SceneAPI/EDataBufferType.h" -#include - -namespace ramses -{ - class DataTypeUtils - { - public: - static EDataType ConvertDataTypeFromInternal(ramses_internal::EDataType type) - { - switch (type) - { - case ramses_internal::EDataType::UInt16: - return EDataType::UInt16; - case ramses_internal::EDataType::UInt32: - return EDataType::UInt32; - case ramses_internal::EDataType::Float: - return EDataType::Float; - case ramses_internal::EDataType::Vector2F: - return EDataType::Vector2F; - case ramses_internal::EDataType::Vector3F: - return EDataType::Vector3F; - case ramses_internal::EDataType::Vector4F: - return EDataType::Vector4F; - case ramses_internal::EDataType::ByteBlob: - return EDataType::ByteBlob; - default: - assert(false); - return EDataType::UInt16; - } - } - - static ramses_internal::EDataType ConvertDataTypeToInternal(EDataType type) - { - switch (type) - { - case EDataType::UInt16: - return ramses_internal::EDataType::UInt16; - case EDataType::UInt32: - return ramses_internal::EDataType::UInt32; - case EDataType::Float: - return ramses_internal::EDataType::Float; - case EDataType::Vector2F: - return ramses_internal::EDataType::Vector2F; - case EDataType::Vector3F: - return ramses_internal::EDataType::Vector3F; - case EDataType::Vector4F: - return ramses_internal::EDataType::Vector4F; - case EDataType::ByteBlob: - return ramses_internal::EDataType::ByteBlob; - } - assert(false); - return ramses_internal::EDataType::UInt16; - } - - static ramses_internal::EResourceType DeductResourceTypeFromDataType(EDataType type) - { - switch (type) - { - case ramses::EDataType::UInt16: - case ramses::EDataType::UInt32: - return ramses_internal::EResourceType_IndexArray; - case ramses::EDataType::Float: - case ramses::EDataType::Vector2F: - case ramses::EDataType::Vector3F: - case ramses::EDataType::Vector4F: - case ramses::EDataType::ByteBlob: - return ramses_internal::EResourceType_VertexArray; - } - assert(false); - return ramses_internal::EResourceType_Invalid; - } - - static ramses_internal::EDataBufferType DeductBufferTypeFromDataType(EDataType type) - { - switch (type) - { - case ramses::EDataType::UInt16: - case ramses::EDataType::UInt32: - return ramses_internal::EDataBufferType::IndexBuffer; - case ramses::EDataType::Float: - case ramses::EDataType::Vector2F: - case ramses::EDataType::Vector3F: - case ramses::EDataType::Vector4F: - case ramses::EDataType::ByteBlob: - return ramses_internal::EDataBufferType::VertexBuffer; - } - assert(false); - return ramses_internal::EDataBufferType::Invalid; - } - - static bool IsValidIndicesType(EDataType type) - { - return type == EDataType::UInt16 || - type == EDataType::UInt32; - } - - static bool IsValidVerticesType(EDataType type) - { - return type == EDataType::Float || - type == EDataType::Vector2F || - type == EDataType::Vector3F || - type == EDataType::Vector4F || - type == EDataType::ByteBlob; - } - }; -} - -#endif diff --git a/client/ramses-client/impl/EffectDescriptionImpl.cpp b/client/ramses-client/impl/EffectDescriptionImpl.cpp index 497ae381e..d835e4f5e 100644 --- a/client/ramses-client/impl/EffectDescriptionImpl.cpp +++ b/client/ramses-client/impl/EffectDescriptionImpl.cpp @@ -25,25 +25,25 @@ namespace ramses { } - status_t EffectDescriptionImpl::setVertexShader(const char* shaderSource) + status_t EffectDescriptionImpl::setVertexShader(std::string_view shaderSource) { m_vertexShaderSource = shaderSource; return StatusOK; } - status_t EffectDescriptionImpl::setFragmentShader(const char* shaderSource) + status_t EffectDescriptionImpl::setFragmentShader(std::string_view shaderSource) { m_fragmentShaderSource = shaderSource; return StatusOK; } - status_t EffectDescriptionImpl::setGeometryShader(const char* shaderSource) + status_t EffectDescriptionImpl::setGeometryShader(std::string_view shaderSource) { m_geometryShaderSource = shaderSource; return StatusOK; } - status_t EffectDescriptionImpl::setVertexShaderFromFile(const char* shaderSourceFileName) + status_t EffectDescriptionImpl::setVertexShaderFromFile(std::string_view shaderSourceFileName) { if (!ReadFileContentsToString(shaderSourceFileName, m_vertexShaderSource)) { @@ -53,7 +53,7 @@ namespace ramses return StatusOK; } - status_t EffectDescriptionImpl::setFragmentShaderFromFile(const char* shaderSourceFileName) + status_t EffectDescriptionImpl::setFragmentShaderFromFile(std::string_view shaderSourceFileName) { if (!ReadFileContentsToString(shaderSourceFileName, m_fragmentShaderSource)) { @@ -63,7 +63,7 @@ namespace ramses return StatusOK; } - ramses::status_t EffectDescriptionImpl::setGeometryShaderFromFile(const char* shaderSourceFileName) + ramses::status_t EffectDescriptionImpl::setGeometryShaderFromFile(std::string_view shaderSourceFileName) { if (!ReadFileContentsToString(shaderSourceFileName, m_geometryShaderSource)) { @@ -73,37 +73,35 @@ namespace ramses return StatusOK; } - status_t EffectDescriptionImpl::addCompilerDefine(const char* define) + status_t EffectDescriptionImpl::addCompilerDefine(std::string_view define) { - const ramses_internal::String defineStr(define); - if (defineStr.size() == 0u) + if (define.empty()) { return addErrorEntry("EffectDescription::addCompilerDefine cannot add empty define!"); } - m_compilerDefines.push_back(defineStr); + m_compilerDefines.emplace_back(define); return StatusOK; } - status_t EffectDescriptionImpl::setSemantic(const char* semanticName, ramses_internal::EFixedSemantics semanticType) + status_t EffectDescriptionImpl::setSemantic(std::string_view semanticName, ramses_internal::EFixedSemantics semanticType) { - const ramses_internal::String semanticNameStr(semanticName); - if (semanticNameStr.size() == 0u) + if (semanticName.empty()) { return addErrorEntry("EffectDescription::setSemantic cannot set empty semantic name!"); } - m_inputSemantics.put(semanticNameStr, semanticType); + m_inputSemantics.put(std::string{semanticName}, semanticType); return StatusOK; } - status_t EffectDescriptionImpl::setUniformSemantic(const char* semanticName, EEffectUniformSemantic semanticType) + status_t EffectDescriptionImpl::setUniformSemantic(std::string_view semanticName, EEffectUniformSemantic semanticType) { const ramses_internal::EFixedSemantics semanticTypeInternal = EffectInputSemanticUtils::GetEffectInputSemanticInternal(semanticType); return setSemantic(semanticName, semanticTypeInternal); } - status_t EffectDescriptionImpl::setAttributeSemantic(const char* semanticName, EEffectAttributeSemantic semanticType) + status_t EffectDescriptionImpl::setAttributeSemantic(std::string_view semanticName, EEffectAttributeSemantic semanticType) { const ramses_internal::EFixedSemantics semanticTypeInternal = EffectInputSemanticUtils::GetEffectInputSemanticInternal(semanticType); return setSemantic(semanticName, semanticTypeInternal); @@ -124,17 +122,17 @@ namespace ramses return m_geometryShaderSource.c_str(); } - uint32_t EffectDescriptionImpl::getNumberOfCompilerDefines() const + size_t EffectDescriptionImpl::getNumberOfCompilerDefines() const { - return static_cast(m_compilerDefines.size()); + return m_compilerDefines.size(); } - const ramses_internal::StringVector& EffectDescriptionImpl::getCompilerDefines() const + const std::vector& EffectDescriptionImpl::getCompilerDefines() const { return m_compilerDefines; } - const char* EffectDescriptionImpl::getCompilerDefine(uint32_t index) const + const char* EffectDescriptionImpl::getCompilerDefine(size_t index) const { if (index < getNumberOfCompilerDefines()) { @@ -149,7 +147,7 @@ namespace ramses return m_inputSemantics; } - bool EffectDescriptionImpl::ReadFileContentsToString(const char* fileName, ramses_internal::String& fileContents) + bool EffectDescriptionImpl::ReadFileContentsToString(std::string_view fileName, std::string& fileContents) { ramses_internal::File inFile(fileName); if (!inFile.exists()) @@ -164,8 +162,8 @@ namespace ramses return false; } - ramses_internal::UInt fileSize = 0; - ramses_internal::UInt readBytes = 0; + size_t fileSize = 0; + size_t readBytes = 0; if (!inFile.getSizeInBytes(fileSize)) { LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "EffectDescriptionImpl::ReadFileContentsToString: error reading file info: " << fileName); diff --git a/client/ramses-client/impl/EffectDescriptionImpl.h b/client/ramses-client/impl/EffectDescriptionImpl.h index 19653e251..4479ab667 100644 --- a/client/ramses-client/impl/EffectDescriptionImpl.h +++ b/client/ramses-client/impl/EffectDescriptionImpl.h @@ -19,47 +19,50 @@ // ramses framework #include "Collections/HashMap.h" #include "SceneAPI/EFixedSemantics.h" -#include "Utils/StringUtils.h" + +#include +#include +#include namespace ramses { class EffectDescriptionImpl : public StatusObjectImpl { public: - using SemanticsMap = ramses_internal::HashMap; + using SemanticsMap = ramses_internal::HashMap; EffectDescriptionImpl(); - virtual ~EffectDescriptionImpl(); + ~EffectDescriptionImpl() override; - status_t setVertexShader(const char* shaderSource); - status_t setFragmentShader(const char* shaderSource); - status_t setGeometryShader(const char* shaderSource); - status_t setVertexShaderFromFile(const char* shaderSourceFileName); - status_t setFragmentShaderFromFile(const char* shaderSourceFileName); - status_t setGeometryShaderFromFile(const char* shaderSourceFileName); - status_t addCompilerDefine(const char* define); - status_t setUniformSemantic(const char* semanticName, EEffectUniformSemantic semanticType); - status_t setAttributeSemantic(const char* semanticName, EEffectAttributeSemantic semanticType); + status_t setVertexShader(std::string_view shaderSource); + status_t setFragmentShader(std::string_view shaderSource); + status_t setGeometryShader(std::string_view shaderSource); + status_t setVertexShaderFromFile(std::string_view shaderSourceFileName); + status_t setFragmentShaderFromFile(std::string_view shaderSourceFileName); + status_t setGeometryShaderFromFile(std::string_view shaderSourceFileName); + status_t addCompilerDefine(std::string_view define); + status_t setUniformSemantic(std::string_view semanticName, EEffectUniformSemantic semanticType); + status_t setAttributeSemantic(std::string_view semanticName, EEffectAttributeSemantic semanticType); const char* getVertexShader() const; const char* getFragmentShader() const; const char* getGeometryShader() const; - uint32_t getNumberOfCompilerDefines() const; - const ramses_internal::StringVector& getCompilerDefines() const; - const char* getCompilerDefine(uint32_t index) const; + size_t getNumberOfCompilerDefines() const; + const std::vector& getCompilerDefines() const; + const char* getCompilerDefine(size_t index) const; const SemanticsMap& getSemanticsMap() const; - static bool ReadFileContentsToString(const char* fileName, ramses_internal::String& fileContents); + static bool ReadFileContentsToString(std::string_view fileName, std::string& fileContents); private: - status_t setSemantic(const char* semanticName, ramses_internal::EFixedSemantics semanticType); + status_t setSemantic(std::string_view semanticName, ramses_internal::EFixedSemantics semanticType); - ramses_internal::String m_vertexShaderSource; - ramses_internal::String m_fragmentShaderSource; - ramses_internal::String m_geometryShaderSource; + std::string m_vertexShaderSource; + std::string m_fragmentShaderSource; + std::string m_geometryShaderSource; - ramses_internal::StringVector m_compilerDefines; - SemanticsMap m_inputSemantics; + std::vector m_compilerDefines; + SemanticsMap m_inputSemantics; }; } diff --git a/client/ramses-client/impl/EffectImpl.cpp b/client/ramses-client/impl/EffectImpl.cpp index 19af30ac7..8331c4711 100644 --- a/client/ramses-client/impl/EffectImpl.cpp +++ b/client/ramses-client/impl/EffectImpl.cpp @@ -20,8 +20,8 @@ namespace ramses { - EffectImpl::EffectImpl(ramses_internal::ResourceHashUsage hashUsage, SceneImpl& scene, const char* effectname) - : ResourceImpl(ERamsesObjectType_Effect, std::move(hashUsage), scene, effectname) + EffectImpl::EffectImpl(ramses_internal::ResourceHashUsage hashUsage, SceneImpl& scene, std::string_view effectname) + : ResourceImpl(ERamsesObjectType::Effect, std::move(hashUsage), scene, effectname) { } @@ -29,7 +29,7 @@ namespace ramses { } - void EffectImpl::initializeFromFrameworkData(const ramses_internal::EffectInputInformationVector& uniformInputs, const ramses_internal::EffectInputInformationVector& attributeInputs, absl::optional geometryShaderInputType) + void EffectImpl::initializeFromFrameworkData(const ramses_internal::EffectInputInformationVector& uniformInputs, const ramses_internal::EffectInputInformationVector& attributeInputs, std::optional geometryShaderInputType) { m_effectUniformInputs = uniformInputs; m_effectAttributeInputs = attributeInputs; @@ -45,7 +45,7 @@ namespace ramses { outStream << input.inputName; outStream << static_cast(input.dataType); - outStream << input.elementCount; + outStream << static_cast(input.elementCount); outStream << static_cast(input.semantics); } @@ -57,8 +57,8 @@ namespace ramses outStream << static_cast(input.semantics); } - // TODO (backported) enable this once 27 merged to master - //outStream << static_cast(m_geometryShaderInputType); + const int32_t gsInputType = (m_geometryShaderInputType ? static_cast(*m_geometryShaderInputType) : -1); + outStream << gsInputType; return StatusOK; } @@ -102,25 +102,25 @@ namespace ramses m_effectAttributeInputs[i].semantics = static_cast(semanticAsUInt); } - // TODO (backported) enable this once 27 merged to master - //ramses_internal::EDrawMode geometryShaderInputType; - //inStream >> geometryShaderInputType; - //m_geometryShaderInputType = geometryShaderInputType; + int32_t gsInputType = -1; + inStream >> gsInputType; + if (gsInputType >= 0) + m_geometryShaderInputType = static_cast(gsInputType); return StatusOK; } - uint32_t EffectImpl::getUniformInputCount() const + size_t EffectImpl::getUniformInputCount() const { - return static_cast(m_effectUniformInputs.size()); + return m_effectUniformInputs.size(); } - uint32_t EffectImpl::getAttributeInputCount() const + size_t EffectImpl::getAttributeInputCount() const { - return static_cast(m_effectAttributeInputs.size()); + return m_effectAttributeInputs.size(); } - status_t EffectImpl::getUniformInput(uint32_t index, EffectInputImpl& inputImpl) const + status_t EffectImpl::getUniformInput(size_t index, EffectInputImpl& inputImpl) const { if (index >= getUniformInputCount()) { @@ -134,7 +134,7 @@ namespace ramses status_t EffectImpl::findUniformInput(EEffectUniformSemantic uniformSemantic, EffectInputImpl& inputImpl) const { - const uint32_t index = findEffectInputIndex(m_effectUniformInputs, EffectInputSemanticUtils::GetEffectInputSemanticInternal(uniformSemantic)); + const size_t index = findEffectInputIndex(m_effectUniformInputs, EffectInputSemanticUtils::GetEffectInputSemanticInternal(uniformSemantic)); if (index == InvalidInputIndex) { return addErrorEntry("Effect: getUniformInput failed, semantic is not defined in effect!"); @@ -146,7 +146,7 @@ namespace ramses return StatusOK; } - status_t EffectImpl::getAttributeInput(uint32_t index, EffectInputImpl& inputImpl) const + status_t EffectImpl::getAttributeInput(size_t index, EffectInputImpl& inputImpl) const { if (index >= getAttributeInputCount()) { @@ -160,7 +160,7 @@ namespace ramses status_t EffectImpl::findAttributeInput(EEffectAttributeSemantic attributeSemantic, EffectInputImpl& inputImpl) const { - const uint32_t index = findEffectInputIndex(m_effectAttributeInputs, EffectInputSemanticUtils::GetEffectInputSemanticInternal(attributeSemantic)); + const size_t index = findEffectInputIndex(m_effectAttributeInputs, EffectInputSemanticUtils::GetEffectInputSemanticInternal(attributeSemantic)); if (index == InvalidInputIndex) { return addErrorEntry("Effect: getAttributeInput failed, semantic not defined in effect!"); @@ -172,9 +172,9 @@ namespace ramses return StatusOK; } - status_t EffectImpl::findUniformInput(const char* inputName, EffectInputImpl& inputImpl) const + status_t EffectImpl::findUniformInput(std::string_view inputName, EffectInputImpl& inputImpl) const { - const uint32_t index = getEffectInputIndex(m_effectUniformInputs, inputName); + const size_t index = getEffectInputIndex(m_effectUniformInputs, inputName); if (index == InvalidInputIndex) return addErrorEntry((ramses_internal::StringOutputStream() << "Effect::findUniformInput: failed, uniform input '" << inputName << "' could not be found in effect '" << getName()).c_str()); @@ -184,9 +184,9 @@ namespace ramses return StatusOK; } - status_t EffectImpl::findAttributeInput(const char* inputName, EffectInputImpl& inputImpl) const + status_t EffectImpl::findAttributeInput(std::string_view inputName, EffectInputImpl& inputImpl) const { - const uint32_t index = getEffectInputIndex(m_effectAttributeInputs, inputName); + const size_t index = getEffectInputIndex(m_effectAttributeInputs, inputName); if (index == InvalidInputIndex) return addErrorEntry((ramses_internal::StringOutputStream() << "Effect::findAttributeInput: failed, attribute input '" << inputName << "' could not be found in effect '" << getName()).c_str()); @@ -206,10 +206,10 @@ namespace ramses return m_effectAttributeInputs; } - uint32_t EffectImpl::getEffectInputIndex(const ramses_internal::EffectInputInformationVector& effectInputVector, const char* inputName) const + size_t EffectImpl::getEffectInputIndex(const ramses_internal::EffectInputInformationVector& effectInputVector, std::string_view inputName) const { - const uint32_t numInputs = static_cast(effectInputVector.size()); - for (uint32_t i = 0u; i < numInputs; ++i) + const size_t numInputs = effectInputVector.size(); + for (size_t i = 0u; i < numInputs; ++i) { const ramses_internal::EffectInputInformation& effectInputInfo = effectInputVector[i]; if (effectInputInfo.inputName == inputName) @@ -221,13 +221,13 @@ namespace ramses return InvalidInputIndex; } - uint32_t EffectImpl::findEffectInputIndex(const ramses_internal::EffectInputInformationVector& effectInputVector, ramses_internal::EFixedSemantics inputSemantics) const + size_t EffectImpl::findEffectInputIndex(const ramses_internal::EffectInputInformationVector& effectInputVector, ramses_internal::EFixedSemantics inputSemantics) const { if (ramses_internal::EFixedSemantics::Invalid == inputSemantics) return InvalidInputIndex; - const uint32_t numInputs = static_cast(effectInputVector.size()); - for (uint32_t i = 0u; i < numInputs; ++i) + const size_t numInputs = effectInputVector.size(); + for (size_t i = 0u; i < numInputs; ++i) { const ramses_internal::EffectInputInformation& effectInputInfo = effectInputVector[i]; if (effectInputInfo.semantics == inputSemantics) @@ -239,7 +239,7 @@ namespace ramses return InvalidInputIndex; } - void EffectImpl::initializeEffectInputData(EffectInputImpl& effectInputImpl, const ramses_internal::EffectInputInformation& effectInputInfo, uint32_t index) const + void EffectImpl::initializeEffectInputData(EffectInputImpl& effectInputImpl, const ramses_internal::EffectInputInformation& effectInputInfo, size_t index) const { effectInputImpl.initialize( getLowlevelResourceHash(), @@ -263,7 +263,7 @@ namespace ramses return addErrorEntry((ramses_internal::StringOutputStream() << "Effect::getGeometryShaderInputType: failed, effect '" << getName() << "' has no geometry shader attached to it!").c_str()); } - inputType = AppearanceUtils::GetDrawModeFromInternal(*m_geometryShaderInputType); + inputType = *m_geometryShaderInputType; return StatusOK; } } diff --git a/client/ramses-client/impl/EffectImpl.h b/client/ramses-client/impl/EffectImpl.h index 1f2d01956..9da9cac14 100644 --- a/client/ramses-client/impl/EffectImpl.h +++ b/client/ramses-client/impl/EffectImpl.h @@ -20,7 +20,8 @@ #include "SceneUtils/DataLayoutCreationHelper.h" #include "SceneAPI/RenderState.h" -#include "absl/types/optional.h" +#include +#include namespace ramses { @@ -29,40 +30,40 @@ namespace ramses class EffectImpl final : public ResourceImpl { public: - EffectImpl(ramses_internal::ResourceHashUsage hashUsage, SceneImpl& scene, const char* effectname); - virtual ~EffectImpl() override; + EffectImpl(ramses_internal::ResourceHashUsage hashUsage, SceneImpl& scene, std::string_view effectname); + ~EffectImpl() override; - void initializeFromFrameworkData(const ramses_internal::EffectInputInformationVector& uniformInputs, const ramses_internal::EffectInputInformationVector& attributeInputs, absl::optional geometryShaderInputType); + void initializeFromFrameworkData(const ramses_internal::EffectInputInformationVector& uniformInputs, const ramses_internal::EffectInputInformationVector& attributeInputs, std::optional geometryShaderInputType); - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - uint32_t getUniformInputCount() const; - uint32_t getAttributeInputCount() const; + size_t getUniformInputCount() const; + size_t getAttributeInputCount() const; bool hasGeometryShader() const; status_t getGeometryShaderInputType(EDrawMode& inputType) const; - status_t getUniformInput(uint32_t index, EffectInputImpl& inputImpl) const; + status_t getUniformInput(size_t index, EffectInputImpl& inputImpl) const; status_t findUniformInput(EEffectUniformSemantic uniformSemantic, EffectInputImpl& inputImpl) const; - status_t getAttributeInput(uint32_t index, EffectInputImpl& inputImpl) const; + status_t getAttributeInput(size_t index, EffectInputImpl& inputImpl) const; status_t findAttributeInput(EEffectAttributeSemantic attributeSemantic, EffectInputImpl& inputImpl) const; - status_t findUniformInput(const char* inputName, EffectInputImpl& inputImpl) const; - status_t findAttributeInput(const char* inputName, EffectInputImpl& inputImpl) const; + status_t findUniformInput(std::string_view inputName, EffectInputImpl& inputImpl) const; + status_t findAttributeInput(std::string_view inputName, EffectInputImpl& inputImpl) const; const ramses_internal::EffectInputInformationVector& getUniformInputInformation() const; const ramses_internal::EffectInputInformationVector& getAttributeInputInformation() const; private: - static const uint32_t InvalidInputIndex = 0xffff; + static const size_t InvalidInputIndex = 0xffff; - uint32_t getEffectInputIndex(const ramses_internal::EffectInputInformationVector& effectInputVector, const char* inputName) const; - uint32_t findEffectInputIndex(const ramses_internal::EffectInputInformationVector& effectInputVector, ramses_internal::EFixedSemantics inputSemantics) const; - void initializeEffectInputData(EffectInputImpl& effectInputImpl, const ramses_internal::EffectInputInformation& effectInputInfo, uint32_t index) const; + size_t getEffectInputIndex(const ramses_internal::EffectInputInformationVector& effectInputVector, std::string_view inputName) const; + size_t findEffectInputIndex(const ramses_internal::EffectInputInformationVector& effectInputVector, ramses_internal::EFixedSemantics inputSemantics) const; + void initializeEffectInputData(EffectInputImpl& effectInputImpl, const ramses_internal::EffectInputInformation& effectInputInfo, size_t index) const; ramses_internal::EffectInputInformationVector m_effectUniformInputs; ramses_internal::EffectInputInformationVector m_effectAttributeInputs; - absl::optional m_geometryShaderInputType; + std::optional m_geometryShaderInputType; }; } diff --git a/client/ramses-client/impl/EffectInputImpl.cpp b/client/ramses-client/impl/EffectInputImpl.cpp index c2b0f204f..499452c50 100644 --- a/client/ramses-client/impl/EffectInputImpl.cpp +++ b/client/ramses-client/impl/EffectInputImpl.cpp @@ -7,7 +7,7 @@ // ------------------------------------------------------------------------- #include "EffectInputImpl.h" -#include "EffectInputUtils.h" +#include "DataTypeUtils.h" #include "EffectInputSemanticUtils.h" #include "SceneAPI/IScene.h" @@ -29,11 +29,11 @@ namespace ramses void EffectInputImpl::initialize( const ramses_internal::ResourceContentHash& effectHash, - const ramses_internal::String& name, + std::string_view name, ramses_internal::EDataType dataType, ramses_internal::EFixedSemantics semantics, - uint32_t elementCount, - uint32_t index) + size_t elementCount, + size_t index) { m_effectHash = effectHash; m_name = name; @@ -53,22 +53,20 @@ namespace ramses return m_effectHash; } - const ramses_internal::String& EffectInputImpl::getName() const + const std::string& EffectInputImpl::getName() const { return m_name; } - EEffectInputDataType EffectInputImpl::getUniformInputDataType() const + std::optional EffectInputImpl::getDataType() const { - return EffectInputUtils::GetEffectInputDataType(m_dataType); - } + if (!isValid()) + return std::nullopt; - EEffectInputDataType EffectInputImpl::getAttributeInputDataType() const - { - return EffectInputUtils::GetEffectInputDataTypeFromBuffer(m_dataType); + return DataTypeUtils::ConvertDataTypeFromInternal(m_dataType); } - ramses_internal::EDataType EffectInputImpl::getDataType() const + ramses_internal::EDataType EffectInputImpl::getInternalDataType() const { return m_dataType; } @@ -78,12 +76,12 @@ namespace ramses return m_semantics; } - uint32_t EffectInputImpl::getElementCount() const + size_t EffectInputImpl::getElementCount() const { return m_elementCount; } - uint32_t EffectInputImpl::getInputIndex() const + size_t EffectInputImpl::getInputIndex() const { return m_inputIndex; } @@ -104,8 +102,8 @@ namespace ramses outStream << m_name; outStream << static_cast(m_dataType); outStream << static_cast(m_semantics); - outStream << m_elementCount; - outStream << m_inputIndex; + outStream << static_cast(m_elementCount); + outStream << static_cast(m_inputIndex); return StatusOK; } @@ -117,16 +115,19 @@ namespace ramses uint32_t dataType; inStream >> dataType; - m_dataType = ramses_internal::EDataType(dataType); uint32_t semantics; inStream >> semantics; - m_semantics = ramses_internal::EFixedSemantics(semantics); - inStream >> m_elementCount; - inStream >> m_inputIndex; + uint32_t elementCount; + inStream >> elementCount; + m_elementCount = elementCount; + + uint32_t inputIndex; + inStream >> inputIndex; + m_inputIndex = inputIndex; return StatusOK; } diff --git a/client/ramses-client/impl/EffectInputImpl.h b/client/ramses-client/impl/EffectInputImpl.h index 5fe76018d..ff484137f 100644 --- a/client/ramses-client/impl/EffectInputImpl.h +++ b/client/ramses-client/impl/EffectInputImpl.h @@ -10,7 +10,7 @@ #define RAMSES_EFFECTINPUTIMPL_H // client api -#include "ramses-client-api/EffectInputDataType.h" +#include "ramses-framework-api/EDataType.h" #include "ramses-client-api/EffectInputSemantic.h" // internal @@ -20,7 +20,9 @@ #include "SceneAPI/ResourceContentHash.h" #include "SceneAPI/EDataType.h" #include "SceneAPI/EFixedSemantics.h" -#include "Collections/String.h" + +#include +#include namespace ramses { @@ -28,27 +30,26 @@ namespace ramses { public: EffectInputImpl(); - virtual ~EffectInputImpl(); + ~EffectInputImpl() override; void initialize( const ramses_internal::ResourceContentHash& effectHash, - const ramses_internal::String& name, + std::string_view name, ramses_internal::EDataType dataType, ramses_internal::EFixedSemantics semantics, - uint32_t elementCount, - uint32_t index); + size_t elementCount, + size_t index); bool isValid() const; ramses_internal::ResourceContentHash getEffectHash() const; - const ramses_internal::String& getName() const; - ramses_internal::EDataType getDataType() const; + const std::string& getName() const; + ramses_internal::EDataType getInternalDataType() const; ramses_internal::EFixedSemantics getSemantics() const; - uint32_t getElementCount() const; - uint32_t getInputIndex() const; + size_t getElementCount() const; + size_t getInputIndex() const; - EEffectInputDataType getUniformInputDataType() const; - EEffectInputDataType getAttributeInputDataType() const; + std::optional getDataType() const; EEffectUniformSemantic getUniformSemantics() const; EEffectAttributeSemantic getAttributeSemantics() const; @@ -60,11 +61,11 @@ namespace ramses private: ramses_internal::ResourceContentHash m_effectHash; - ramses_internal::String m_name; + std::string m_name; ramses_internal::EDataType m_dataType; ramses_internal::EFixedSemantics m_semantics; - uint32_t m_elementCount; - uint32_t m_inputIndex; + size_t m_elementCount; + size_t m_inputIndex; }; } diff --git a/client/ramses-client/impl/EffectInputUtils.h b/client/ramses-client/impl/EffectInputUtils.h deleted file mode 100644 index e96c67a8d..000000000 --- a/client/ramses-client/impl/EffectInputUtils.h +++ /dev/null @@ -1,69 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_EFFECTINPUTUTILS_H -#define RAMSES_EFFECTINPUTUTILS_H - -#include "ramses-client-api/EffectInputDataType.h" -#include "SceneAPI/EDataType.h" -#include - -namespace ramses -{ - class EffectInputUtils - { - public: - - static EEffectInputDataType GetEffectInputDataType(ramses_internal::EDataType dataType) - { - switch (dataType) - { - case ramses_internal::EDataType::Int32 : return EEffectInputDataType_Int32; - case ramses_internal::EDataType::UInt16 : return EEffectInputDataType_UInt16; - case ramses_internal::EDataType::UInt32 : return EEffectInputDataType_UInt32; - case ramses_internal::EDataType::Float : return EEffectInputDataType_Float; - case ramses_internal::EDataType::Vector2F : return EEffectInputDataType_Vector2F; - case ramses_internal::EDataType::Vector3F : return EEffectInputDataType_Vector3F; - case ramses_internal::EDataType::Vector4F : return EEffectInputDataType_Vector4F; - case ramses_internal::EDataType::Vector2I : return EEffectInputDataType_Vector2I; - case ramses_internal::EDataType::Vector3I : return EEffectInputDataType_Vector3I; - case ramses_internal::EDataType::Vector4I : return EEffectInputDataType_Vector4I; - case ramses_internal::EDataType::Matrix22F : return EEffectInputDataType_Matrix22F; - case ramses_internal::EDataType::Matrix33F : return EEffectInputDataType_Matrix33F; - case ramses_internal::EDataType::Matrix44F : return EEffectInputDataType_Matrix44F; - case ramses_internal::EDataType::TextureSampler2D : return EEffectInputDataType_TextureSampler2D; - case ramses_internal::EDataType::TextureSampler2DMS : return EEffectInputDataType_TextureSampler2DMS; - case ramses_internal::EDataType::TextureSamplerExternal : return EEffectInputDataType_TextureSamplerExternal; - case ramses_internal::EDataType::TextureSampler3D : return EEffectInputDataType_TextureSampler3D; - case ramses_internal::EDataType::TextureSamplerCube : return EEffectInputDataType_TextureSamplerCube; - case ramses_internal::EDataType::Invalid : return EEffectInputDataType_Invalid; - default: - assert(false); - return EEffectInputDataType_Invalid; - } - } - - static EEffectInputDataType GetEffectInputDataTypeFromBuffer(ramses_internal::EDataType dataType) - { - switch (dataType) - { - case ramses_internal::EDataType::UInt16Buffer : return EEffectInputDataType_UInt16; - case ramses_internal::EDataType::FloatBuffer : return EEffectInputDataType_Float; - case ramses_internal::EDataType::Vector2Buffer : return EEffectInputDataType_Vector2F; - case ramses_internal::EDataType::Vector3Buffer : return EEffectInputDataType_Vector3F; - case ramses_internal::EDataType::Vector4Buffer : return EEffectInputDataType_Vector4F; - case ramses_internal::EDataType::Invalid : return EEffectInputDataType_Invalid; - default: - assert(false); - return EEffectInputDataType_Invalid; - } - } - }; -} - -#endif diff --git a/client/ramses-client/impl/GeometryBindingImpl.cpp b/client/ramses-client/impl/GeometryBindingImpl.cpp index a953e41b6..87c151d8d 100644 --- a/client/ramses-client/impl/GeometryBindingImpl.cpp +++ b/client/ramses-client/impl/GeometryBindingImpl.cpp @@ -28,8 +28,8 @@ namespace ramses { - GeometryBindingImpl::GeometryBindingImpl(SceneImpl& scene, const char* name) - : SceneObjectImpl(scene, ERamsesObjectType_GeometryBinding, name) + GeometryBindingImpl::GeometryBindingImpl(SceneImpl& scene, std::string_view name) + : SceneObjectImpl(scene, ERamsesObjectType::GeometryBinding, name) , m_effectImpl(nullptr) , m_indicesCount(0u) { @@ -114,18 +114,18 @@ namespace ramses status_t GeometryBindingImpl::validateEffect() const { - ObjectIteratorImpl iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType_Effect); + ObjectIteratorImpl iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType::Effect); RamsesObject* ramsesObject = iter.getNext(); while (nullptr != ramsesObject) { const Effect& effect = RamsesObjectTypeUtils::ConvertTo(*ramsesObject); - if (&effect.impl == m_effectImpl) + if (&effect.m_impl == m_effectImpl) return addValidationOfDependentObject(*m_effectImpl); ramsesObject = iter.getNext(); } - return addValidationMessage(EValidationSeverity_Error, "GeometryBinding is referring to an invalid Effect"); + return addValidationMessage(EValidationSeverity::Error, "GeometryBinding is referring to an invalid Effect"); } status_t GeometryBindingImpl::validateAttribute() const @@ -164,19 +164,19 @@ namespace ramses { const Resource* resource = getSceneImpl().scanForResourceWithHash(resourceHash); if (nullptr == resource) - return addValidationMessage(EValidationSeverity_Error, "GeometryBinding is referring to resource that does not exist"); + return addValidationMessage(EValidationSeverity::Error, "GeometryBinding is referring to resource that does not exist"); - return addValidationOfDependentObject(resource->impl); + return addValidationOfDependentObject(resource->m_impl); } status_t GeometryBindingImpl::validateDataBuffer(ramses_internal::DataBufferHandle dataBuffer, ramses_internal::EDataType fieldDataType) const { if (!getIScene().isDataBufferAllocated(dataBuffer)) - return addValidationMessage(EValidationSeverity_Error, "GeometryBinding is referring to data buffer that does not exist"); + return addValidationMessage(EValidationSeverity::Error, "GeometryBinding is referring to data buffer that does not exist"); const auto dataBufferType = getIScene().getDataBuffer(dataBuffer).dataType; if (!dataTypeMatchesInputType(dataBufferType, fieldDataType)) - return addValidationMessage(EValidationSeverity_Error, "GeometryBinding is referring to data buffer with type that does not match data layout field type"); + return addValidationMessage(EValidationSeverity::Error, "GeometryBinding is referring to data buffer with type that does not match data layout field type"); const ArrayBufferImpl* dataBufferImpl = findDataBuffer(dataBuffer); assert(nullptr != dataBufferImpl); @@ -186,12 +186,12 @@ namespace ramses ramses::ArrayBufferImpl* GeometryBindingImpl::findDataBuffer(ramses_internal::DataBufferHandle dataBufferHandle) const { - RamsesObjectRegistryIterator arrayBufferIter(getSceneImpl().getObjectRegistry(), ERamsesObjectType_DataBufferObject); + RamsesObjectRegistryIterator arrayBufferIter(getSceneImpl().getObjectRegistry(), ERamsesObjectType::ArrayBufferObject); while (const ArrayBuffer* dataBuffer = arrayBufferIter.getNext()) { - if (dataBuffer->impl.getDataBufferHandle() == dataBufferHandle) + if (dataBuffer->m_impl.getDataBufferHandle() == dataBufferHandle) { - return &dataBuffer->impl; + return &dataBuffer->m_impl; } } @@ -326,11 +326,11 @@ namespace ramses if ((offset > 0 || stride > 0) && bufferResource.getElementType() != EDataType::ByteBlob) return addErrorEntry("GeometryBinding::setInputBuffer failed, custom stride/offset can be used only with array resources of type byte blob"); - if (!dataTypeMatchesInputType(DataTypeUtils::ConvertDataTypeToInternal(bufferResource.getElementType()), input.getDataType())) + if (!dataTypeMatchesInputType(DataTypeUtils::ConvertDataTypeToInternal(bufferResource.getElementType()), input.getInternalDataType())) return addErrorEntry("GeometryBinding::setInputBuffer failed, array resource type does not match input data type"); // data field index on low level scene is indexed starting after reserved slot for indices - const ramses_internal::DataFieldHandle dataField(input.getInputIndex() + IndicesDataFieldIndex + 1u); + const ramses_internal::DataFieldHandle dataField(static_cast(input.getInputIndex()) + IndicesDataFieldIndex + 1u); getIScene().setDataResource(m_attributeInstance, dataField, bufferResource.getLowlevelResourceHash(), ramses_internal::DataBufferHandle::Invalid(), instancingDivisor, offset, stride); return StatusOK; @@ -359,11 +359,11 @@ namespace ramses if ((offset > 0 || stride > 0) && dataBuffer.getDataType() != EDataType::ByteBlob) return addErrorEntry("GeometryBinding::setInputBuffer failed, custom stride/offset can be used only with data buffers of type byte blob"); - if (!dataTypeMatchesInputType(dataBufferDataType, input.getDataType())) + if (!dataTypeMatchesInputType(dataBufferDataType, input.getInternalDataType())) return addErrorEntry("GeometryBinding::setInputBuffer failed, vertex data buffer type does not match input data type"); // data field index on low level scene is indexed starting after reserved slot for indices - const ramses_internal::DataFieldHandle dataField(input.getInputIndex() + IndicesDataFieldIndex + 1u); + const ramses_internal::DataFieldHandle dataField(static_cast(input.getInputIndex()) + IndicesDataFieldIndex + 1u); getIScene().setDataResource(m_attributeInstance, dataField, ramses_internal::ResourceContentHash::Invalid(), dataBufferHandle, instancingDivisor, offset, stride); return StatusOK; diff --git a/client/ramses-client/impl/GeometryBindingImpl.h b/client/ramses-client/impl/GeometryBindingImpl.h index 8cf3f1b07..400fadf4f 100644 --- a/client/ramses-client/impl/GeometryBindingImpl.h +++ b/client/ramses-client/impl/GeometryBindingImpl.h @@ -16,6 +16,8 @@ // ramses framework #include "SceneAPI/EDataType.h" +#include + namespace ramses_internal { class IScene; @@ -32,15 +34,15 @@ namespace ramses class GeometryBindingImpl final : public SceneObjectImpl { public: - GeometryBindingImpl(SceneImpl& scene, const char* name); - virtual ~GeometryBindingImpl() override; + GeometryBindingImpl(SceneImpl& scene, std::string_view name); + ~GeometryBindingImpl() override; void initializeFrameworkData(const EffectImpl& effect); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; + status_t validate() const override; status_t setInputBuffer(const EffectInputImpl& input, const ArrayResourceImpl& bufferResource, uint32_t instancingDivisor, uint16_t offset, uint16_t stride); status_t setInputBuffer(const EffectInputImpl& input, const ArrayBufferImpl& dataBuffer, uint32_t instancingDivisor, uint16_t offset, uint16_t stride); diff --git a/client/ramses-client/impl/IRamsesObjectRegistry.h b/client/ramses-client/impl/IRamsesObjectRegistry.h index 8345c6ff1..c27b95a87 100644 --- a/client/ramses-client/impl/IRamsesObjectRegistry.h +++ b/client/ramses-client/impl/IRamsesObjectRegistry.h @@ -10,7 +10,8 @@ #define RAMSES_IRAMSESOBJECTREGISTRY_H #include "ramses-client-api/RamsesObjectTypes.h" -#include "Collections/String.h" + +#include namespace ramses { @@ -22,7 +23,7 @@ namespace ramses public: virtual ~IRamsesObjectRegistry() {}; - virtual void updateName(RamsesObject&, const ramses_internal::String&) = 0; + virtual void updateName(RamsesObject&, const std::string&) = 0; }; } diff --git a/client/ramses-client/impl/IteratorImpl.h b/client/ramses-client/impl/IteratorImpl.h index cbe5c1c45..dda22af1b 100644 --- a/client/ramses-client/impl/IteratorImpl.h +++ b/client/ramses-client/impl/IteratorImpl.h @@ -19,13 +19,17 @@ namespace ramses public: using ObjectVector = std::vector; - IteratorImpl() + IteratorImpl() = default; + + explicit IteratorImpl(ObjectVector&& objects) + : m_objects{ std::move(objects) } + , m_objectIterator{ m_objects.begin() } { } explicit IteratorImpl(const ObjectVector& objects) - : m_objects(objects) - , m_objectIterator(m_objects.begin()) + : m_objects{ objects } + , m_objectIterator{ m_objects.begin() } { } diff --git a/client/ramses-client/impl/MeshNodeImpl.cpp b/client/ramses-client/impl/MeshNodeImpl.cpp index ac5618e76..c250cf398 100644 --- a/client/ramses-client/impl/MeshNodeImpl.cpp +++ b/client/ramses-client/impl/MeshNodeImpl.cpp @@ -28,8 +28,8 @@ namespace ramses { - MeshNodeImpl::MeshNodeImpl(SceneImpl& scene, const char* nodeName) - : NodeImpl(scene, ERamsesObjectType_MeshNode, nodeName) + MeshNodeImpl::MeshNodeImpl(SceneImpl& scene, std::string_view nodeName) + : NodeImpl(scene, ERamsesObjectType::MeshNode, nodeName) , m_appearanceImpl(nullptr) , m_geometryImpl(nullptr) { @@ -91,22 +91,22 @@ namespace ramses { status_t status = NodeImpl::validate(); if (nullptr == m_appearanceImpl) - status = addValidationMessage(EValidationSeverity_Error, "meshnode does not have an appearance set"); + status = addValidationMessage(EValidationSeverity::Error, "meshnode does not have an appearance set"); else status = std::max(status, addValidationOfDependentObject(*m_appearanceImpl)); if (nullptr == m_geometryImpl) - status = addValidationMessage(EValidationSeverity_Error, "meshnode does not have a geometryBinding set"); + status = addValidationMessage(EValidationSeverity::Error, "meshnode does not have a geometryBinding set"); else { status = std::max(status, addValidationOfDependentObject(*m_geometryImpl)); const bool hasIndexArray = m_geometryImpl->getIndicesCount() > 0; if (hasIndexArray && (m_geometryImpl->getIndicesCount() < getStartIndex() + getIndexCount())) - status = addValidationMessage(EValidationSeverity_Error, "startIndex + indexCount exceeds indices of indexarray"); + status = addValidationMessage(EValidationSeverity::Error, "startIndex + indexCount exceeds indices of indexarray"); if (getIndexCount() == 0) - status = addValidationMessage(EValidationSeverity_Error, "indexCount must be greater 0"); + status = addValidationMessage(EValidationSeverity::Error, "indexCount must be greater 0"); } return status; } diff --git a/client/ramses-client/impl/MeshNodeImpl.h b/client/ramses-client/impl/MeshNodeImpl.h index 04ec8a321..4138a63d9 100644 --- a/client/ramses-client/impl/MeshNodeImpl.h +++ b/client/ramses-client/impl/MeshNodeImpl.h @@ -12,6 +12,8 @@ // internal #include "NodeImpl.h" +#include + namespace ramses_internal { class ClientApplicationLogic; @@ -27,15 +29,15 @@ namespace ramses class MeshNodeImpl final : public NodeImpl { public: - MeshNodeImpl(SceneImpl& scene, const char* nodeName); - virtual ~MeshNodeImpl() override; + MeshNodeImpl(SceneImpl& scene, std::string_view nodeName); + ~MeshNodeImpl() override; void initializeFrameworkData(); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; + status_t validate() const override; status_t setAppearance(AppearanceImpl& appearanceImpl); status_t setGeometryBinding(GeometryBindingImpl& geometryImpl); diff --git a/client/ramses-client/impl/NodeImpl.cpp b/client/ramses-client/impl/NodeImpl.cpp index 8ff253fb2..61c5e8827 100644 --- a/client/ramses-client/impl/NodeImpl.cpp +++ b/client/ramses-client/impl/NodeImpl.cpp @@ -12,16 +12,12 @@ #include "SerializationContext.h" #include "RamsesObjectTypeUtils.h" #include "Scene/ClientScene.h" -#include "Math3d/Vector3.h" -#include "RotationConventionUtils.h" +#include "RotationTypeUtils.h" +#include "glm/gtc/type_ptr.hpp" namespace ramses { - static const ramses_internal::Vector3 IdentityTranslation(0.0f, 0.0f, 0.0f); - static const ramses_internal::Vector3 IdentityRotation(0.0f, 0.0f, 0.0f); - static const ramses_internal::Vector3 IdentityScaling(1.0f, 1.0f, 1.0f); - - NodeImpl::NodeImpl(SceneImpl& scene, ERamsesObjectType type, const char* nodeName) + NodeImpl::NodeImpl(SceneImpl& scene, ERamsesObjectType type, std::string_view nodeName) : SceneObjectImpl(scene, type, nodeName) , m_parent(nullptr) , m_visibilityMode(EVisibilityMode::Visible) @@ -71,9 +67,9 @@ namespace ramses } } - const ramses_internal::UInt32 childCount = getIScene().getChildCount(m_nodeHandle); + const uint32_t childCount = getIScene().getChildCount(m_nodeHandle); m_children.reserve(childCount); - for (ramses_internal::UInt32 i = 0; i < childCount; i++) + for (uint32_t i = 0; i < childCount; i++) { const ramses_internal::NodeHandle childNodeHandle = getIScene().getChild(m_nodeHandle, i); NodeImpl* childNode = serializationContext.getNodeImplForHandle(childNodeHandle); @@ -111,20 +107,20 @@ namespace ramses return !m_children.empty(); } - uint32_t NodeImpl::getChildCount() const + size_t NodeImpl::getChildCount() const { - return static_cast(m_children.size()); + return m_children.size(); } - Node* NodeImpl::getChild(uint32_t index) + Node* NodeImpl::getChild(size_t index) { // Use const version of getChild to avoid code duplication return const_cast(const_cast(*this).getChild(index)); } - const Node* NodeImpl::getChild(uint32_t index) const + const Node* NodeImpl::getChild(size_t index) const { - if (index >= static_cast(m_children.size())) + if (index >= m_children.size()) { return nullptr; } @@ -133,12 +129,12 @@ namespace ramses return &RamsesObjectTypeUtils::ConvertTo(child.getRamsesObject()); } - NodeImpl& NodeImpl::getChildImpl(uint32_t index) + NodeImpl& NodeImpl::getChildImpl(size_t index) { return const_cast(const_cast(*this).getChildImpl(index)); } - const NodeImpl& NodeImpl::getChildImpl(uint32_t index) const + const NodeImpl& NodeImpl::getChildImpl(size_t index) const { assert(index < m_children.size()); return *m_children[index]; @@ -236,17 +232,15 @@ namespace ramses return m_parent->removeChild(*this); } - status_t NodeImpl::getModelMatrix(float(&modelMatrix)[16]) const + status_t NodeImpl::getModelMatrix(matrix44f& modelMatrix) const { - const ramses_internal::Matrix44f mat44 = getIScene().updateMatrixCache(ramses_internal::ETransformationMatrixType_World, m_nodeHandle); - ramses_internal::PlatformMemory::Copy(modelMatrix, mat44.data, sizeof(modelMatrix)); + modelMatrix = getIScene().updateMatrixCache(ramses_internal::ETransformationMatrixType_World, m_nodeHandle); return StatusOK; } - status_t NodeImpl::getInverseModelMatrix(float(&inverseModelMatrix)[16]) const + status_t NodeImpl::getInverseModelMatrix(matrix44f& inverseModelMatrix) const { - const ramses_internal::Matrix44f mat44 = getIScene().updateMatrixCache(ramses_internal::ETransformationMatrixType_Object, m_nodeHandle); - ramses_internal::PlatformMemory::Copy(inverseModelMatrix, mat44.data, sizeof(inverseModelMatrix)); + inverseModelMatrix = getIScene().updateMatrixCache(ramses_internal::ETransformationMatrixType_Object, m_nodeHandle); return StatusOK; } @@ -281,211 +275,174 @@ namespace ramses getIScene().removeChildFromNode(m_nodeHandle, child.m_nodeHandle); } - status_t NodeImpl::setRotationInternal(const ramses_internal::Vector3& rotation, ramses_internal::ERotationConvention rotationConventionInternal) + status_t NodeImpl::translate(const vec3f& translation) { if (!m_transformHandle.isValid()) { - if (rotation == IdentityRotation && - rotationConventionInternal == ramses_internal::ERotationConvention::Legacy_ZYX) + if (translation == ramses_internal::IScene::IdentityTranslation) { return StatusOK; } initializeTransform(); } - - if (rotation != getIScene().getRotation(m_transformHandle) - || rotationConventionInternal != getIScene().getRotationConvention(m_transformHandle)) - { - getIScene().setRotation(m_transformHandle, rotation, rotationConventionInternal); - } - + const auto& previous = getIScene().getTranslation(m_transformHandle); + getIScene().setTranslation(m_transformHandle, previous + glm::vec3(translation)); return StatusOK; } - status_t NodeImpl::translate(float x, float y, float z) + status_t NodeImpl::setTranslation(const vec3f& translation) { if (!m_transformHandle.isValid()) { - if (ramses_internal::Vector3(x, y, z) == IdentityTranslation) + if (translation == ramses_internal::IScene::IdentityTranslation) { return StatusOK; } initializeTransform(); } - const ramses_internal::Vector3& previous = getIScene().getTranslation(m_transformHandle); - getIScene().setTranslation(m_transformHandle, previous + ramses_internal::Vector3(x, y, z)); + if (translation != getIScene().getTranslation(m_transformHandle)) + { + getIScene().setTranslation(m_transformHandle, translation); + } return StatusOK; } - status_t NodeImpl::setTranslation(float x, float y, float z) + status_t NodeImpl::getTranslation(vec3f& translation) const { if (!m_transformHandle.isValid()) { - if (ramses_internal::Vector3(x, y, z) == IdentityTranslation) - { - return StatusOK; - } - initializeTransform(); - } - const ramses_internal::Vector3 newValue(x, y, z); - if (newValue != getIScene().getTranslation(m_transformHandle)) - { - getIScene().setTranslation(m_transformHandle, newValue); + translation = ramses_internal::IScene::IdentityTranslation; + return StatusOK; } + + translation = getIScene().getTranslation(m_transformHandle); return StatusOK; } - status_t NodeImpl::getTranslation(float& x, float& y, float& z) const + ramses::status_t NodeImpl::setRotation(const vec3f& rotation, ERotationType rotationType) { - if (!m_transformHandle.isValid()) + if (rotationType == ERotationType::Quaternion) { - x = IdentityTranslation.x; - y = IdentityTranslation.y; - z = IdentityTranslation.z; - return StatusOK; + return addErrorEntry("Invalid rotation rotationType: Quaternion"); } - - const ramses_internal::Vector3& value = getIScene().getTranslation(m_transformHandle); - x = value[0]; - y = value[1]; - z = value[2]; - return StatusOK; + const auto rotationConventionInternal = RotationTypeUtils::ConvertRotationTypeToInternal(rotationType); + return setRotationInternal({rotation.x, rotation.y, rotation.z, 1.f}, rotationConventionInternal); } - status_t NodeImpl::rotate(float x, float y, float z) + ramses::status_t NodeImpl::setRotationInternal(glm::vec4&& rotation, ramses_internal::ERotationType rotationType) { if (!m_transformHandle.isValid()) { - if (ramses_internal::Vector3(x, y, z) == IdentityRotation) + if (rotation == ramses_internal::IScene::IdentityRotation) { return StatusOK; } initializeTransform(); } - else if (ramses_internal::ERotationConvention::Legacy_ZYX != getIScene().getRotationConvention(m_transformHandle)) - return addErrorEntry("Node::rotate() can only be used with legacy left-handed rotation: setRotation(float,float,float)"); - const ramses_internal::Vector3& previous = getIScene().getRotation(m_transformHandle); - const ramses_internal::Vector3 increment(x, y, z); - getIScene().setRotation(m_transformHandle, previous + increment, ramses_internal::ERotationConvention::Legacy_ZYX); + if (rotation != getIScene().getRotation(m_transformHandle) + || rotationType != getIScene().getRotationType(m_transformHandle)) + { + getIScene().setRotation(m_transformHandle, rotation, rotationType); + } return StatusOK; } - status_t NodeImpl::setRotation(float x, float y, float z) - { - return setRotationInternal({ x, y, z }, ramses_internal::ERotationConvention::Legacy_ZYX); - } - - ramses::status_t NodeImpl::setRotation(float x, float y, float z, ERotationConvention rotationConvention) + ERotationType NodeImpl::getRotationType() const { - const auto rotationConventionInternal = RotationConventionUtils::ConvertRotationConventionToInternal(rotationConvention); - return setRotationInternal({ x, y, z }, rotationConventionInternal); + if (!m_transformHandle.isValid()) + { + return ERotationType::Euler_XYZ; + } + const auto rotationConventionInternal = getIScene().getRotationType(m_transformHandle); + return RotationTypeUtils::ConvertRotationTypeFromInternal(rotationConventionInternal); } - status_t NodeImpl::getRotation(float& x, float& y, float& z) const + status_t NodeImpl::getRotation(vec3f& rotation) const { if (!m_transformHandle.isValid()) { - x = IdentityRotation.x; - y = IdentityRotation.y; - z = IdentityRotation.z; + rotation = ramses_internal::IScene::IdentityRotation; return StatusOK; } - else if (ramses_internal::ERotationConvention::Legacy_ZYX != getIScene().getRotationConvention(m_transformHandle)) - return addErrorEntry("Node::getRotation(float,float,float) can only be used with legacy left-handed rotation: setRotation(float,float,float)"); - const ramses_internal::Vector3& value = getIScene().getRotation(m_transformHandle); - x = value[0]; - y = value[1]; - z = value[2]; + if (ramses_internal::ERotationType::Quaternion == getIScene().getRotationType(m_transformHandle)) + { + return addErrorEntry("Node::getRotation(vec3f&) failed: rotation was set by quaternion before. Check Node::getRotationType()."); + } + + rotation = getIScene().getRotation(m_transformHandle); return StatusOK; } - status_t NodeImpl::getRotation(float& x, float& y, float& z, ERotationConvention& rotationConvention) const + status_t NodeImpl::setRotation(const quat& rotation) + { + glm::vec4 vec{rotation.x, rotation.y, rotation.z, rotation.w}; + return setRotationInternal(std::move(vec), ramses_internal::ERotationType::Quaternion); + } + + ramses::status_t NodeImpl::getRotation(quat& rotation) const { if (!m_transformHandle.isValid()) { - x = IdentityRotation.x; - y = IdentityRotation.y; - z = IdentityRotation.z; - rotationConvention = ERotationConvention::XYZ; + rotation = glm::identity(); return StatusOK; } - else if (ramses_internal::ERotationConvention::Legacy_ZYX == getIScene().getRotationConvention(m_transformHandle)) - { - if (getIScene().getRotation(m_transformHandle) == IdentityRotation) - { - x = IdentityRotation.x; - y = IdentityRotation.y; - z = IdentityRotation.z; - rotationConvention = ramses::ERotationConvention::XYZ; - return StatusOK; - } - - return addErrorEntry("Node::getRotation(float,float,float,ERotationConvention) can only be used with right-handed rotation conventions: setRotation(float,float,float,ERotationConvention)"); + if (ramses_internal::ERotationType::Quaternion != getIScene().getRotationType(m_transformHandle)) + { + return addErrorEntry("Node::getRotation(quat&) failed: rotation was set by euler angles before. Check Node::getRotationType()."); } - const ramses_internal::Vector3& value = getIScene().getRotation(m_transformHandle); - x = value[0]; - y = value[1]; - z = value[2]; - - rotationConvention = RotationConventionUtils::ConvertDataTypeFromInternal(getIScene().getRotationConvention(m_transformHandle)); + const auto& value = getIScene().getRotation(m_transformHandle); + rotation = quat(value.w, value.x, value.y, value.z); return StatusOK; } - status_t NodeImpl::scale(float x, float y, float z) + status_t NodeImpl::scale(const vec3f& scaling) { if (!m_transformHandle.isValid()) { - if (ramses_internal::Vector3(x, y, z) == IdentityScaling) + if (scaling == ramses_internal::IScene::IdentityScaling) { return StatusOK; } initializeTransform(); } - const ramses_internal::Vector3& previous = getIScene().getScaling(m_transformHandle); - const ramses_internal::Vector3 factor(x, y, z); - getIScene().setScaling(m_transformHandle, previous * factor); + const auto& previous = getIScene().getScaling(m_transformHandle); + getIScene().setScaling(m_transformHandle, previous * scaling); return StatusOK; } - status_t NodeImpl::setScaling(float x, float y, float z) + status_t NodeImpl::setScaling(const vec3f& scaling) { if (!m_transformHandle.isValid()) { - if (ramses_internal::Vector3(x, y, z) == IdentityScaling) + if (scaling == ramses_internal::IScene::IdentityScaling) { return StatusOK; } initializeTransform(); } - const ramses_internal::Vector3 newValue(x, y, z); - if (newValue != getIScene().getScaling(m_transformHandle)) + if (scaling != getIScene().getScaling(m_transformHandle)) { - getIScene().setScaling(m_transformHandle, newValue); + getIScene().setScaling(m_transformHandle, scaling); } return StatusOK; } - status_t NodeImpl::getScaling(float& x, float& y, float& z) const + status_t NodeImpl::getScaling(vec3f& scaling) const { if (!m_transformHandle.isValid()) { - x = IdentityScaling.x; - y = IdentityScaling.y; - z = IdentityScaling.z; + scaling = ramses_internal::IScene::IdentityScaling; return StatusOK; } - const ramses_internal::Vector3& value = getIScene().getScaling(m_transformHandle); - x = value[0]; - y = value[1]; - z = value[2]; + scaling = getIScene().getScaling(m_transformHandle); return StatusOK; } diff --git a/client/ramses-client/impl/NodeImpl.h b/client/ramses-client/impl/NodeImpl.h index 68cdad2d0..7af4f3672 100644 --- a/client/ramses-client/impl/NodeImpl.h +++ b/client/ramses-client/impl/NodeImpl.h @@ -12,18 +12,16 @@ // internal #include "SceneObjectImpl.h" #include "ramses-client-api/EVisibilityMode.h" -#include "ramses-client-api/ERotationConvention.h" +#include "ramses-client-api/ERotationType.h" +#include "DataTypesImpl.h" // ramses framework #include "SceneAPI/Handles.h" #include "SceneAPI/SceneId.h" -#include "SceneAPI/ERotationConvention.h" +#include "SceneAPI/ERotationType.h" #include "Collections/Vector.h" -namespace ramses_internal -{ - class Vector3; -} +#include namespace ramses { @@ -33,14 +31,14 @@ namespace ramses class NodeImpl : public SceneObjectImpl { public: - NodeImpl(SceneImpl& scene, ERamsesObjectType type, const char* nodeName); - virtual ~NodeImpl() override; + NodeImpl(SceneImpl& scene, ERamsesObjectType type, std::string_view nodeName); + ~NodeImpl() override; void initializeFrameworkData(); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; status_t addChild(NodeImpl& childNode); status_t removeChild(NodeImpl& node); @@ -49,12 +47,12 @@ namespace ramses status_t removeAllChildren(); bool hasChild() const; - uint32_t getChildCount() const; - Node* getChild(uint32_t index); - const Node* getChild(uint32_t index) const; + size_t getChildCount() const; + Node* getChild(size_t index); + const Node* getChild(size_t index) const; - NodeImpl& getChildImpl(uint32_t index); - const NodeImpl& getChildImpl(uint32_t index) const; + NodeImpl& getChildImpl(size_t index); + const NodeImpl& getChildImpl(size_t index) const; bool hasParent() const; Node* getParent(); @@ -62,20 +60,20 @@ namespace ramses NodeImpl* getParentImpl(); const NodeImpl* getParentImpl() const; - status_t getModelMatrix(float(&modelMatrix)[16]) const; - status_t getInverseModelMatrix(float(&inverseModelMatrix)[16]) const; - - status_t translate(float x, float y, float z); - status_t setTranslation(float x, float y, float z); - status_t getTranslation(float& x, float& y, float& z) const; - status_t rotate(float x, float y, float z); - status_t setRotation(float x, float y, float z); - status_t setRotation(float x, float y, float z, ERotationConvention rotationConvention); - status_t getRotation(float& x, float& y, float& z) const; - status_t getRotation(float& x, float& y, float& z, ERotationConvention& rotationConvention) const; - status_t scale(float x, float y, float z); - status_t setScaling(float x, float y, float z); - status_t getScaling(float& x, float& y, float& z) const; + status_t getModelMatrix(glm::mat4x4& modelMatrix) const; + status_t getInverseModelMatrix(glm::mat4x4& inverseModelMatrix) const; + + status_t translate(const vec3f& translation); + status_t setTranslation(const vec3f& translation); + status_t getTranslation(vec3f& translation) const; + status_t setRotation(const vec3f& rotation, ERotationType rotationType); + ERotationType getRotationType() const; + status_t getRotation(vec3f& rotation) const; + status_t setRotation(const quat& rotation); + status_t getRotation(quat& rotation) const; + status_t scale(const vec3f& scaling); + status_t setScaling(const vec3f& scaling); + status_t getScaling(vec3f& scaling) const; status_t setVisibility(EVisibilityMode mode); EVisibilityMode getVisibility() const; @@ -93,7 +91,7 @@ namespace ramses using NodeVector = std::vector; void removeChildInternally(NodeVector::iterator childIt); - status_t setRotationInternal(const ramses_internal::Vector3& rotation, ramses_internal::ERotationConvention rotationConventionInternal); + status_t setRotationInternal(glm::vec4&& rotation, ramses_internal::ERotationType rotationType); ramses_internal::NodeHandle m_nodeHandle; diff --git a/client/ramses-client/impl/PickableObjectImpl.cpp b/client/ramses-client/impl/PickableObjectImpl.cpp index 834daf6e6..ec317e2ed 100644 --- a/client/ramses-client/impl/PickableObjectImpl.cpp +++ b/client/ramses-client/impl/PickableObjectImpl.cpp @@ -20,8 +20,8 @@ namespace ramses { - PickableObjectImpl::PickableObjectImpl(SceneImpl& scene, const char* pickableObjectName) - : NodeImpl(scene, ERamsesObjectType_PickableObject, pickableObjectName) + PickableObjectImpl::PickableObjectImpl(SceneImpl& scene, std::string_view pickableObjectName) + : NodeImpl(scene, ERamsesObjectType::PickableObject, pickableObjectName) { } @@ -89,14 +89,14 @@ namespace ramses const ramses_internal::PickableObject& pickableObject = getIScene().getPickableObject(m_pickableObjectHandle); if (!getIScene().isDataBufferAllocated(pickableObject.geometryHandle)) - status = addValidationMessage(EValidationSeverity_Error, "pickable object references a deleted geometry buffer"); + status = addValidationMessage(EValidationSeverity::Error, "pickable object references a deleted geometry buffer"); else status = std::max(status, addValidationOfDependentObject(*m_geometryBufferImpl)); if (!pickableObject.cameraHandle.isValid()) - status = std::max(status, addValidationMessage(EValidationSeverity_Warning, "pickable object references no camera, a valid camera must be set")); + status = std::max(status, addValidationMessage(EValidationSeverity::Warning, "pickable object references no camera, a valid camera must be set")); else if (!getIScene().isCameraAllocated(pickableObject.cameraHandle)) - status = addValidationMessage(EValidationSeverity_Error, "pickable object references a deleted camera"); + status = addValidationMessage(EValidationSeverity::Error, "pickable object references a deleted camera"); return status; } @@ -122,10 +122,10 @@ namespace ramses } else { - ramses_internal::String str = + std::string str = "PickableObject::setCamera failed - camera is not valid, maybe camera was not initialized:\n"; - str += cameraImpl.getValidationReport(EValidationSeverity_Warning); - return addErrorEntry(str.c_str()); + str += cameraImpl.getValidationReport(EValidationSeverity::Warning); + return addErrorEntry(str); } return cameraValidity; diff --git a/client/ramses-client/impl/PickableObjectImpl.h b/client/ramses-client/impl/PickableObjectImpl.h index 5996fff6d..9d6736d14 100644 --- a/client/ramses-client/impl/PickableObjectImpl.h +++ b/client/ramses-client/impl/PickableObjectImpl.h @@ -13,6 +13,8 @@ #include "SceneAPI/Handles.h" #include "NodeImpl.h" +#include + namespace ramses { class ArrayBufferImpl; @@ -23,15 +25,15 @@ namespace ramses class PickableObjectImpl final : public NodeImpl { public: - PickableObjectImpl(SceneImpl& scene, const char* pickableObjectName); - virtual ~PickableObjectImpl() override = default; + PickableObjectImpl(SceneImpl& scene, std::string_view pickableObjectName); + ~PickableObjectImpl() override = default; void initializeFrameworkData(const ArrayBufferImpl& geometryBuffer, pickableObjectId_t id); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; + status_t validate() const override; const ArrayBuffer& getGeometryBuffer() const; diff --git a/client/ramses-client/impl/RamsesClientImpl.cpp b/client/ramses-client/impl/RamsesClientImpl.cpp index 7f70686e0..443ebabb0 100644 --- a/client/ramses-client/impl/RamsesClientImpl.cpp +++ b/client/ramses-client/impl/RamsesClientImpl.cpp @@ -15,8 +15,6 @@ #include "SceneImpl.h" #include "SceneConfigImpl.h" #include "ResourceImpl.h" -#include "AnimationSystemImpl.h" -#include "AnimatedPropertyImpl.h" #include "RamsesFrameworkImpl.h" #include "RamsesClientImpl.h" #include "RamsesObjectRegistryIterator.h" @@ -34,10 +32,8 @@ #include "Components/FileInputStreamContainer.h" #include "Components/MemoryInputStreamContainer.h" #include "Components/OffsetFileInputStreamContainer.h" -#include "Animation/AnimationSystemFactory.h" #include "Resource/IResource.h" #include "ClientCommands/PrintSceneList.h" -#include "ClientCommands/ForceFallbackImage.h" #include "ClientCommands/FlushSceneVersion.h" #include "SerializationContext.h" #include "Utils/BinaryFileOutputStream.h" @@ -46,7 +42,6 @@ #include "Utils/LogContext.h" #include "Utils/File.h" #include "Collections/IInputStream.h" -#include "Collections/String.h" #include "Collections/HashMap.h" #include "PlatformAbstraction/PlatformTime.h" #include "Utils/LogMacros.h" @@ -55,7 +50,6 @@ #include "FrameworkFactoryRegistry.h" #include "Ramsh/Ramsh.h" #include "DataTypeUtils.h" -#include "ResourceDataPoolImpl.h" #include "Resource/ArrayResource.h" #include "Resource/EffectResource.h" #include "Resource/TextureResource.h" @@ -70,27 +64,24 @@ namespace ramses { static const bool clientRegisterSuccess = ClientFactory::RegisterClientFactory(); - RamsesClientImpl::RamsesClientImpl(RamsesFrameworkImpl& framework, const char* applicationName) - : RamsesObjectImpl(ERamsesObjectType_Client, applicationName) + RamsesClientImpl::RamsesClientImpl(RamsesFrameworkImpl& framework, std::string_view applicationName) + : RamsesObjectImpl(ERamsesObjectType::Client, applicationName) , m_appLogic(framework.getParticipantAddress().getParticipantId(), framework.getFrameworkLock()) , m_sceneFactory() , m_framework(framework) , m_loadFromFileTaskQueue(framework.getTaskQueue()) , m_deleteSceneQueue(framework.getTaskQueue()) - , m_resourceDataPool(*new ResourceDataPoolImpl(*this)) { assert(!framework.isConnected()); m_appLogic.init(framework.getResourceComponent(), framework.getScenegraphComponent()); m_cmdPrintSceneList = std::make_shared(*this); m_cmdPrintValidation = std::make_shared(*this); - m_cmdForceFallbackImage = std::make_shared(*this); m_cmdFlushSceneVersion = std::make_shared(*this); m_cmdDumpSceneToFile = std::make_shared(*this); m_cmdLogResourceMemoryUsage = std::make_shared(*this); framework.getRamsh().add(m_cmdPrintSceneList); framework.getRamsh().add(m_cmdPrintValidation); - framework.getRamsh().add(m_cmdForceFallbackImage); framework.getRamsh().add(m_cmdFlushSceneVersion); framework.getRamsh().add(m_cmdDumpSceneToFile); framework.getRamsh().add(m_cmdLogResourceMemoryUsage); @@ -105,15 +96,10 @@ namespace ramses // delete async loaded scenes that were never collected via calling dispatchEvents ramses_internal::PlatformGuard g(m_clientLock); - for (auto& loadStatus : m_asyncSceneLoadStatusVec) - { - delete loadStatus.scene; - } + m_asyncSceneLoadStatusVec.clear(); + LOG_INFO(CONTEXT_CLIENT, "RamsesClientImpl::~RamsesClientImpl deleting scenes"); - for (auto& scene : m_scenes) - { - delete scene; - } + m_scenes.clear(); m_framework.getPeriodicLogger().removePeriodicLogSupplier(&m_framework.getScenegraphComponent()); } @@ -139,7 +125,7 @@ namespace ramses return m_appLogic; } - Scene* RamsesClientImpl::createScene(sceneId_t sceneId, const SceneConfigImpl& sceneConfig, const char* name) + Scene* RamsesClientImpl::createScene(sceneId_t sceneId, const SceneConfigImpl& sceneConfig, std::string_view name) { if (!sceneId.isValid()) { @@ -158,41 +144,41 @@ namespace ramses return nullptr; } - SceneImpl& pimpl = *new SceneImpl(*internalScene, sceneConfig, *m_hlClient); - pimpl.initializeFrameworkData(); - Scene* scene = new Scene(pimpl); - m_scenes.push_back(scene); + auto impl = std::make_unique(*internalScene, sceneConfig, *m_hlClient); + impl->initializeFrameworkData(); + m_scenes.push_back(SceneOwningPtr{ new Scene{ std::move(impl) }, [](Scene* s) { delete s; } }); - return scene; + return m_scenes.back().get(); } - RamsesClientImpl::DeleteSceneRunnable::DeleteSceneRunnable(Scene* scene, ramses_internal::ClientScene* llscene) - : m_scene(scene) - , m_lowLevelScene(llscene) + RamsesClientImpl::DeleteSceneRunnable::DeleteSceneRunnable(SceneOwningPtr scene, InternalSceneOwningPtr llscene) + : m_scene{ std::move(scene) } + , m_lowLevelScene{ std::move(llscene) } { } void RamsesClientImpl::DeleteSceneRunnable::execute() { - delete m_scene; - delete m_lowLevelScene; + m_scene.reset(); + m_lowLevelScene.reset(); } status_t RamsesClientImpl::destroy(Scene& scene) { ramses_internal::PlatformGuard g(m_clientLock); - SceneVector::iterator iter = ramses_internal::find_c(m_scenes, &scene); + auto iter = std::find_if(m_scenes.begin(), m_scenes.end(), [&scene](auto& s) { return s.get() == &scene; }); if (iter != m_scenes.end()) { + auto sceneOwnPtr = std::move(*iter); m_scenes.erase(iter); - const ramses_internal::SceneId sceneID(scene.impl.getSceneId().getValue()); + const ramses_internal::SceneId sceneID(scene.getSceneId().getValue()); auto llscene = m_sceneFactory.releaseScene(sceneID); getClientApplication().removeScene(sceneID); - scene.impl.closeSceneFile(); - auto task = new DeleteSceneRunnable(&scene, llscene); + scene.m_impl.closeSceneFile(); + auto task = new DeleteSceneRunnable(std::move(sceneOwnPtr), std::move(llscene)); m_deleteSceneQueue.enqueue(*task); task->release(); @@ -210,7 +196,7 @@ namespace ramses for (const auto res : resources) { assert(res != nullptr); - const ramses_internal::ResourceContentHash& hash = res->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash& hash = res->m_impl.getLowlevelResourceHash(); const ramses_internal::ManagedResource managedRes = getClientApplication().getResource(hash); if (managedRes) { @@ -237,7 +223,7 @@ namespace ramses return m_appLogic.getResource(hash); } - Scene* RamsesClientImpl::loadSceneObjectFromStream(const std::string& caller, + SceneOwningPtr RamsesClientImpl::loadSceneObjectFromStream(const std::string& caller, std::string const& filename, ramses_internal::IInputStream& inputStream, bool localOnly, @@ -269,40 +255,37 @@ namespace ramses } internalScene->preallocateSceneSize(sizeInformation); - ramses_internal::AnimationSystemFactory animSystemFactory(ramses_internal::EAnimationSystemOwner_Client, &internalScene->getSceneActionCollection()); - // need first to create the pimpl, so that internal framework components know the new scene SceneConfigImpl sceneConfig; if (localOnly) { LOG_INFO(ramses_internal::CONTEXT_CLIENT, "RamsesClient::" << caller << ": Mark file loaded from " << filename << " with sceneId " << createInfo.m_id << " as local only"); - sceneConfig.setPublicationMode(EScenePublicationMode_LocalOnly); + sceneConfig.setPublicationMode(EScenePublicationMode::LocalOnly); } - SceneImpl& pimpl = *new SceneImpl(*internalScene, sceneConfig, *m_hlClient); + auto impl = std::make_unique(*internalScene, sceneConfig, *m_hlClient); // now the scene is registered, so it's possible to load the low level content into the scene LOG_TRACE(ramses_internal::CONTEXT_CLIENT, " Reading low level scene from stream"); - ramses_internal::ScenePersistation::ReadSceneFromStream(inputStream, *internalScene, &animSystemFactory); + ramses_internal::ScenePersistation::ReadSceneFromStream(inputStream, *internalScene); LOG_TRACE(ramses_internal::CONTEXT_CLIENT, " Deserializing high level scene objects from stream"); DeserializationContext deserializationContext; SerializationHelper::DeserializeObjectID(inputStream); - const auto stat = pimpl.deserialize(inputStream, deserializationContext); + const auto stat = impl->deserialize(inputStream, deserializationContext); if (stat != StatusOK) { LOG_ERROR(ramses_internal::CONTEXT_CLIENT, " Failed to deserialize high level scene:"); LOG_ERROR(ramses_internal::CONTEXT_CLIENT, getStatusMessage(stat)); - delete &pimpl; return nullptr; } LOG_TRACE(ramses_internal::CONTEXT_CLIENT, " Done with preparing scene from input stream."); - return new Scene(pimpl); + return SceneOwningPtr{ new Scene{ std::move(impl) }, [](Scene* s) { delete s; } }; } - Scene* RamsesClientImpl::loadSceneFromCreationConfig(const SceneCreationConfig& cconfig) + SceneOwningPtr RamsesClientImpl::loadSceneFromCreationConfig(const SceneCreationConfig& cconfig) { // this stream contains scene data AND resource data and will be handed over to and held open by resource component as resource stream ramses_internal::IInputStream& inputStream = cconfig.streamContainer->getStream(); @@ -312,7 +295,7 @@ namespace ramses return nullptr; } - if (!ReadRamsesVersionAndPrintWarningOnMismatch(inputStream, "scene file")) + if (!ReadRamsesVersionAndPrintWarningOnMismatch(inputStream, "scene file", getFramework().getFeatureLevel())) { LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "RamsesClient::" << cconfig.caller << ": failed to read from scene source " << cconfig.dataSource); return nullptr; @@ -323,7 +306,7 @@ namespace ramses inputStream >> sceneObjectStart; inputStream >> llResourceStart; - Scene* scene = nullptr; + SceneOwningPtr scene; if (cconfig.prefetchData) { std::vector sceneData(static_cast(llResourceStart - sceneObjectStart)); @@ -354,17 +337,16 @@ namespace ramses ramses_internal::ResourceTableOfContents loadedTOC; loadedTOC.readTOCPosAndTOCFromStream(inputStream); const ramses_internal::SceneFileHandle fileHandle = m_appLogic.addResourceFile(cconfig.streamContainer, loadedTOC); - scene->impl.setSceneFileHandle(fileHandle); + scene->m_impl.setSceneFileHandle(fileHandle); LOG_INFO_P(CONTEXT_CLIENT, "RamsesClient::{}: Source '{}' has handle {}", cconfig.caller, cconfig.dataSource, fileHandle); return scene; } - Scene* RamsesClientImpl::loadSceneFromFile(const char* fileName, bool localOnly) + Scene* RamsesClientImpl::loadSceneFromFile(std::string_view fileName, bool localOnly) { - const std::string stdFilename(fileName ? fileName : ""); - if (stdFilename.empty()) + if (fileName.empty()) { LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "RamsesClient::loadSceneFromFile: filename may not be empty"); return nullptr; @@ -372,14 +354,15 @@ namespace ramses return loadSceneSynchonousCommon({ "loadSceneFromFile", - stdFilename, - std::make_shared(ramses_internal::String(std::move(stdFilename))), + std::string{fileName}, + std::make_shared(fileName), true, localOnly, sceneId_t(), }); } + // NOLINTNEXTLINE(modernize-avoid-c-arrays) Scene* RamsesClientImpl::loadSceneFromMemory(std::unique_ptr data, size_t size, bool localOnly) { if (!data) @@ -456,55 +439,99 @@ namespace ramses Scene* RamsesClientImpl::loadSceneSynchonousCommon(const SceneCreationConfig& cconfig) { - const ramses_internal::UInt64 start = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); - Scene* scene = loadSceneFromCreationConfig(cconfig); + const uint64_t start = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); + auto scene = loadSceneFromCreationConfig(cconfig); if (!scene) - { return nullptr; - } - finalizeLoadedScene(scene); - const ramses_internal::UInt64 end = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); + auto* scenePtr = scene.get(); + finalizeLoadedScene(std::move(scene)); + + const uint64_t end = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); LOG_INFO(ramses_internal::CONTEXT_CLIENT, "RamsesClient::" << cconfig.caller << " Scene loaded from '" << cconfig.dataSource << "' in " << (end - start) << " ms"); - return scene; + return scenePtr; } - void RamsesClientImpl::finalizeLoadedScene(Scene* scene) + void RamsesClientImpl::finalizeLoadedScene(SceneOwningPtr scene) { // add to the known list of scenes ramses_internal::PlatformGuard g(m_clientLock); - m_scenes.push_back(scene); + m_scenes.push_back(std::move(scene)); + } + + void RamsesClientImpl::WriteCurrentBuildVersionToStream(ramses_internal::IOutputStream& stream, EFeatureLevel featureLevel) + { + ramses_internal::RamsesVersion::WriteToStream(stream, ::ramses_sdk::RAMSES_SDK_RAMSES_VERSION, ::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH, featureLevel); + } + + bool RamsesClientImpl::GetFeatureLevelFromStream(ramses_internal::IInputStreamContainer& streamContainer, const std::string& desc, EFeatureLevel& detectedFeatureLevel) + { + ramses_internal::RamsesVersion::VersionInfo readVersion; + if (!ramses_internal::RamsesVersion::ReadFromStream(streamContainer.getStream(), readVersion, detectedFeatureLevel)) + { + LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "RamsesClient::GetFeatureLevelFromSceneFile: failed to read RAMSES version and feature level from '" << desc << "'."); + return false; + } + + return true; + } + + bool RamsesClientImpl::GetFeatureLevelFromFile(std::string_view fileName, EFeatureLevel& detectedFeatureLevel) + { + ramses_internal::FileInputStreamContainer streamContainer{ fileName }; + return GetFeatureLevelFromStream(streamContainer, std::string{fileName}, detectedFeatureLevel); } - void RamsesClientImpl::WriteCurrentBuildVersionToStream(ramses_internal::IOutputStream& stream) + bool RamsesClientImpl::GetFeatureLevelFromFile(int fd, size_t offset, size_t length, EFeatureLevel& detectedFeatureLevel) { - ramses_internal::RamsesVersion::WriteToStream(stream, ::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_STRING, ::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH); + if (fd <= 0) + { + LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "RamsesClient::GetFeatureLevelFromFile: filedescriptor must be valid " << fd); + return false; + } + if (length == 0u) + { + LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "RamsesClient::GetFeatureLevelFromFile: length may not be 0"); + return false; + } + + ramses_internal::OffsetFileInputStreamContainer streamContainer{ fd, offset, length }; + return GetFeatureLevelFromStream(streamContainer, fmt::format("fileDescriptor fd:{} offset:{} length:{}", fd, offset, length), detectedFeatureLevel); } - bool RamsesClientImpl::ReadRamsesVersionAndPrintWarningOnMismatch(ramses_internal::IInputStream& inputStream, const ramses_internal::String& verboseFileName) + bool RamsesClientImpl::ReadRamsesVersionAndPrintWarningOnMismatch(ramses_internal::IInputStream& inputStream, std::string_view verboseFileName, EFeatureLevel featureLevel) { // return false on read error only, not version mismatch ramses_internal::RamsesVersion::VersionInfo readVersion; - if (!ramses_internal::RamsesVersion::ReadFromStream(inputStream, readVersion)) + EFeatureLevel featureLevelFromFile = EFeatureLevel_01; + if (!ramses_internal::RamsesVersion::ReadFromStream(inputStream, readVersion, featureLevelFromFile)) { LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "RamsesClient::ReadRamsesVersionAndPrintWarningOnMismatch: failed to read RAMSES version for " << verboseFileName << ", file probably corrupt. Loading aborted."); return false; } - LOG_INFO(ramses_internal::CONTEXT_CLIENT, "RAMSES version in file '" << verboseFileName << "': [" << readVersion.versionString << "]; GitHash: [" << readVersion.gitHash << "]"); + LOG_INFO(ramses_internal::CONTEXT_CLIENT, "RAMSES version in file '" << verboseFileName << "': [" << readVersion.versionString << "]; GitHash: [" << readVersion.gitHash << "]; FeatureLevel: [" << featureLevelFromFile << "];"); if (!ramses_internal::RamsesVersion::MatchesMajorMinor(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MAJOR_INT, ::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MINOR_INT, readVersion)) { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "RamsesClient::ReadRamsesVersionAndPrintWarningOnMismatch: Version of file " << verboseFileName << "does not match MAJOR.MINOR of this build. Cannot load the file."); - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "SDK version of loader: [" << ::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_STRING << "]; GitHash: [" << ::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH << "]"); + LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "RamsesClient::ReadRamsesVersionAndPrintWarningOnMismatch: Version of file " << verboseFileName << " does not match MAJOR.MINOR of this build. Cannot load the file."); + LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "SDK version of loader: [" << ::ramses_sdk::RAMSES_SDK_RAMSES_VERSION << "]; GitHash: [" << ::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH << "]"); return false; } + + if (featureLevelFromFile != featureLevel) + { + LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "RamsesClient::ReadRamsesVersionAndPrintWarningOnMismatch: Feature level of file '" << verboseFileName + << "' is " << featureLevelFromFile << " which does not match feature level of this Ramses instance (" << featureLevel << "). Cannot load the file."); + return false; + } + return true; } - status_t RamsesClientImpl::loadSceneFromFileAsync(const char* fileName, bool localOnly) + status_t RamsesClientImpl::loadSceneFromFileAsync(std::string_view fileName, bool localOnly) { - const std::string stdFilename(fileName ? fileName : ""); + const std::string stdFilename(fileName); if (stdFilename.empty()) return addErrorEntry("RamsesClient::loadSceneFromFileAsync: filename may not be empty"); @@ -512,7 +539,7 @@ namespace ramses new LoadSceneRunnable(*this, SceneCreationConfig{ "loadSceneFromFileAsync", stdFilename, - std::make_shared(ramses_internal::String(std::move(stdFilename))), + std::make_shared(stdFilename), true, localOnly, sceneId_t() @@ -527,7 +554,7 @@ namespace ramses for (auto const& scene : getListOfScenes()) { if (masterSceneId == scene->getSceneId()) - return scene->impl.getSceneReference(referencedSceneId); + return scene->m_impl.getSceneReference(referencedSceneId); } return nullptr; @@ -541,15 +568,15 @@ namespace ramses localAsyncSceneLoadStatus.swap(m_asyncSceneLoadStatusVec); } - for (const auto& sceneStatus : localAsyncSceneLoadStatus) + for (auto& sceneStatus : localAsyncSceneLoadStatus) { if (sceneStatus.scene) { // finalize scene - Scene* scene = sceneStatus.scene; - const ramses_internal::UInt64 start = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); - finalizeLoadedScene(scene); - const ramses_internal::UInt64 end = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); + Scene* scene = sceneStatus.scene.get(); + const uint64_t start = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); + finalizeLoadedScene(std::move(sceneStatus.scene)); + const uint64_t end = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); LOG_INFO(ramses_internal::CONTEXT_CLIENT, "RamsesClient::dispatchEvents(sceneFileLoadSucceeded): Synchronous postprocessing of scene loaded from '" << sceneStatus.sceneFilename << "' (sceneName: " << scene->getName() << ", sceneId " << scene->getSceneId() << ") in " << (end - start) << " ms"); @@ -575,7 +602,7 @@ namespace ramses LOG_INFO(CONTEXT_CLIENT, "RamsesClient::dispatchEvents master:reference scene state changed: " << rendererEvent.masterSceneId << ":" << rendererEvent.referencedScene << " " << EnumToString(rendererEvent.sceneState)); - sr->impl.setReportedState(SceneReferenceImpl::GetSceneReferenceState(rendererEvent.sceneState)); + sr->m_impl.setReportedState(SceneReferenceImpl::GetSceneReferenceState(rendererEvent.sceneState)); clientEventHandler.sceneReferenceStateChanged(*sr, SceneReferenceImpl::GetSceneReferenceState(rendererEvent.sceneState)); } else @@ -614,9 +641,9 @@ namespace ramses void RamsesClientImpl::LoadSceneRunnable::execute() { - const ramses_internal::UInt64 start = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); - Scene* scene = m_client.loadSceneFromCreationConfig(m_cconfig); - const ramses_internal::UInt64 end = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); + const uint64_t start = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); + auto scene = m_client.loadSceneFromCreationConfig(m_cconfig); + const uint64_t end = ramses_internal::PlatformTime::GetMillisecondsMonotonic(); if (scene) { @@ -627,10 +654,10 @@ namespace ramses ramses_internal::PlatformGuard g(m_client.m_clientLock); // NOTE: only used for real files by name for now, not sure how to report other cases to user. // therefore can assume dataSource is the filename - m_client.m_asyncSceneLoadStatusVec.push_back({scene, m_cconfig.dataSource}); + m_client.m_asyncSceneLoadStatusVec.push_back({std::move(scene), m_cconfig.dataSource}); } - SceneVector RamsesClientImpl::getListOfScenes() const + const SceneVector& RamsesClientImpl::getListOfScenes() const { ramses_internal::PlatformGuard g(m_clientLock); return m_scenes; @@ -651,17 +678,17 @@ namespace ramses return m_appLogic.loadResource(hash); } - const Scene* RamsesClientImpl::findSceneByName(const char* name) const + const Scene* RamsesClientImpl::findSceneByName(std::string_view name) const { ramses_internal::PlatformGuard g(m_clientLock); for (const auto& scene : m_scenes) - if (scene->impl.getName() == name) - return scene; + if (scene->getName() == name) + return scene.get(); return nullptr; } - Scene* RamsesClientImpl::findSceneByName(const char* name) + Scene* RamsesClientImpl::findSceneByName(std::string_view name) { // Non-const version of findObjectByName cast to its const version to avoid duplicating code return const_cast((const_cast(*this)).findSceneByName(name)); @@ -672,7 +699,7 @@ namespace ramses ramses_internal::PlatformGuard g(m_clientLock); for (const auto& scene : m_scenes) if (scene->getSceneId() == sceneId) - return scene; + return scene.get(); return nullptr; } @@ -692,7 +719,7 @@ namespace ramses return managedRes; } - RamsesClientImpl& RamsesClientImpl::createImpl(const char* name, RamsesFrameworkImpl& components) + RamsesClientImpl& RamsesClientImpl::createImpl(std::string_view name, RamsesFrameworkImpl& components) { return *new RamsesClientImpl(components, name); } @@ -720,29 +747,19 @@ namespace ramses status_t status = StatusOK; for(const auto& scene : m_scenes) { - const status_t sceneStatus = addValidationOfDependentObject(scene->impl); + const status_t sceneStatus = addValidationOfDependentObject(scene->m_impl); if (StatusOK != sceneStatus) status = sceneStatus; } ramses_internal::StringOutputStream msg; msg << "Contains " << m_scenes.size() << " scenes"; - addValidationMessage(EValidationSeverity_Info, ramses_internal::String{ msg.release() }); + addValidationMessage(EValidationSeverity::Info, msg.release()); return status; } - ResourceDataPool& RamsesClientImpl::getResourceDataPool() - { - return m_resourceDataPool; - } - - ResourceDataPool const& RamsesClientImpl::getResourceDataPool() const - { - return m_resourceDataPool; - } - - ramses_internal::ManagedResource RamsesClientImpl::createManagedArrayResource(uint32_t numElements, EDataType type, const void* arrayData, resourceCacheFlag_t cacheFlag, const char* name) + ramses_internal::ManagedResource RamsesClientImpl::createManagedArrayResource(uint32_t numElements, EDataType type, const void* arrayData, resourceCacheFlag_t cacheFlag, std::string_view name) { if (0u == numElements || nullptr == arrayData) { @@ -761,8 +778,8 @@ namespace ramses ramses_internal::ManagedResource RamsesClientImpl::createManagedTexture(ramses_internal::EResourceType textureType, uint32_t width, uint32_t height, uint32_t depth, ETextureFormat format, - uint32_t mipMapCount, const MipDataStorageType mipLevelData[], bool generateMipChain, - const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name) + uint32_t mipMapCount, const MipDataStorageType mipLevelData[], bool generateMipChain, // NOLINT(modernize-avoid-c-arrays) + const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, std::string_view name) { if (!TextureUtils::TextureParametersValid(width, height, depth, mipMapCount) || !TextureUtils::MipDataValid(width, height, depth, mipMapCount, mipLevelData, format)) { @@ -794,25 +811,24 @@ namespace ramses uint32_t width, uint32_t height, uint32_t depth, ETextureFormat format, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, - const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name); + const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, std::string_view name); template ramses_internal::ManagedResource RamsesClientImpl::createManagedTexture(ramses_internal::EResourceType textureType, uint32_t width, uint32_t height, uint32_t depth, ETextureFormat format, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[], bool generateMipChain, - const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name); + const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, std::string_view name); - ramses_internal::ManagedResource RamsesClientImpl::createManagedEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, const char* name, std::string& errorMessages) + ramses_internal::ManagedResource RamsesClientImpl::createManagedEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, std::string_view name, std::string& errorMessages) { //create effect using vertex and fragment shaders - ramses_internal::String effectName(name); - ramses_internal::GlslEffect effectBlock(effectDesc.getVertexShader(), effectDesc.getFragmentShader(), effectDesc.getGeometryShader(), effectDesc.impl.getCompilerDefines(), - effectDesc.impl.getSemanticsMap(), effectName); + ramses_internal::GlslEffect effectBlock(effectDesc.getVertexShader(), effectDesc.getFragmentShader(), effectDesc.getGeometryShader(), effectDesc.m_impl.get().getCompilerDefines(), + effectDesc.m_impl.get().getSemanticsMap(), name); errorMessages.clear(); ramses_internal::EffectResource* effectResource = effectBlock.createEffectResource(ramses_internal::ResourceCacheFlag(cacheFlag.getValue())); if (!effectResource) { - errorMessages = effectBlock.getEffectErrorMessages().stdRef(); - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "RamsesClient::createEffect Failed to create effect resource (name: '" << effectName << "') :\n " << effectBlock.getEffectErrorMessages()); + errorMessages = effectBlock.getEffectErrorMessages(); + LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "RamsesClient::createEffect Failed to create effect resource (name: '" << name << "') :\n " << effectBlock.getEffectErrorMessages()); return {}; } return manageResource(effectResource); diff --git a/client/ramses-client/impl/RamsesClientImpl.h b/client/ramses-client/impl/RamsesClientImpl.h index b0d9e6e5c..dfb67e1a8 100644 --- a/client/ramses-client/impl/RamsesClientImpl.h +++ b/client/ramses-client/impl/RamsesClientImpl.h @@ -17,9 +17,9 @@ #include "ramses-client-api/IClientEventHandler.h" #include "ramses-client-api/TextureSwizzle.h" #include "ramses-client-api/Scene.h" -#include "ramses-client-api/ResourceDataPool.h" // RAMSES framework +#include "ramses-framework-api/EFeatureLevel.h" #include "Utils/LogContext.h" #include "SceneFactory.h" #include "ClientApplicationLogic.h" @@ -29,7 +29,6 @@ #include "Collections/Vector.h" #include "RamsesObjectVector.h" #include "ClientCommands/ValidateCommand.h" -#include "ClientCommands/ForceFallbackImage.h" #include "ClientCommands/DumpSceneToFile.h" #include "ClientCommands/LogResourceMemoryUsage.h" #include "PlatformAbstraction/PlatformLock.h" @@ -41,12 +40,12 @@ #include "SceneImpl.h" #include +#include namespace ramses_internal { class IInputStream; class PrintSceneList; - class ForceFallbackImage; class FlushSceneVersion; class BinaryFileOutputStream; class BinaryFileInputStream; @@ -55,7 +54,6 @@ namespace ramses_internal namespace ramses { - class Scene; class Effect; class Texture3D; class Texture2D; @@ -74,17 +72,20 @@ namespace ramses class ResourceImpl; class RamsesClient; - using SceneVector = std::vector; - using ResourceVector = std::vector; + using SceneOwningPtr = std::unique_ptr>; + using SceneVector = std::vector; + using InternalSceneOwningPtr = std::unique_ptr; + using ResourceVector = std::vector; // resources are owned by Scene's object registry class RamsesClientImpl final : public RamsesObjectImpl { public: - virtual ~RamsesClientImpl() override; + RamsesClientImpl(RamsesFrameworkImpl& ramsesFramework, std::string_view applicationName); + ~RamsesClientImpl() override; void setHLObject(RamsesClient* hlClient); - virtual void deinitializeFrameworkData() override final; + void deinitializeFrameworkData() override; virtual ramses_internal::ManagedResource getResource(ramses_internal::ResourceContentHash hash) const; template @@ -97,28 +98,29 @@ namespace ramses const ramses_internal::ClientApplicationLogic& getClientApplication() const; ramses_internal::ClientApplicationLogic& getClientApplication(); - Scene* createScene(sceneId_t sceneId, const SceneConfigImpl& sceneConfig, const char* name); + Scene* createScene(sceneId_t sceneId, const SceneConfigImpl& sceneConfig, std::string_view name); status_t destroy(Scene& scene); - Scene* loadSceneFromFile(const char* fileName, bool localOnly); + Scene* loadSceneFromFile(std::string_view fileName, bool localOnly); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) Scene* loadSceneFromMemory(std::unique_ptr data, size_t size, bool localOnly); Scene* loadSceneFromFileDescriptor(int fd, size_t offset, size_t length, bool localOnly); Scene* loadSceneFromFileDescriptor(sceneId_t sceneId, int fd, size_t offset, size_t length, bool localOnly); - status_t loadSceneFromFileAsync(const char* fileName, bool localOnly); + status_t loadSceneFromFileAsync(std::string_view fileName, bool localOnly); status_t dispatchEvents(IClientEventHandler& clientEventHandler); - SceneVector getListOfScenes() const; - const Scene* findSceneByName(const char* name) const; - Scene* findSceneByName(const char* name); + const SceneVector& getListOfScenes() const; + const Scene* findSceneByName(std::string_view name) const; + Scene* findSceneByName(std::string_view name); const Scene* getScene(sceneId_t sceneId) const; Scene* getScene(sceneId_t sceneId); RamsesFrameworkImpl& getFramework(); - static RamsesClientImpl& createImpl(const char* name, RamsesFrameworkImpl& components); + static RamsesClientImpl& createImpl(std::string_view name, RamsesFrameworkImpl& components); - virtual status_t validate() const override; + status_t validate() const override; template void enqueueSceneCommand(sceneId_t sceneId, T cmd); @@ -130,22 +132,19 @@ namespace ramses SceneReference* findSceneReference(sceneId_t masterSceneId, sceneId_t referencedSceneId); - ramses_internal::ManagedResource createManagedArrayResource(uint32_t numElements, EDataType type, const void* arrayData, resourceCacheFlag_t cacheFlag, const char* name); - template - ramses_internal::ManagedResource createManagedTexture(ramses_internal::EResourceType textureType, uint32_t width, uint32_t height, uint32_t depth, ETextureFormat format, uint32_t mipMapCount, const MipDataStorageType mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name); - ramses_internal::ManagedResource createManagedEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, const char* name, std::string& errorMessages); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + ramses_internal::ManagedResource createManagedArrayResource(uint32_t numElements, EDataType type, const void* arrayData, resourceCacheFlag_t cacheFlag, std::string_view name); + template // NOLINTNEXTLINE(modernize-avoid-c-arrays) + ramses_internal::ManagedResource createManagedTexture(ramses_internal::EResourceType textureType, uint32_t width, uint32_t height, uint32_t depth, ETextureFormat format, uint32_t mipMapCount, const MipDataStorageType mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, std::string_view name); + ramses_internal::ManagedResource createManagedEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, std::string_view name, std::string& errorMessages); void writeLowLevelResourcesToStream(const ResourceObjects& resources, ramses_internal::BinaryFileOutputStream& resourceOutputStream, bool compress) const; - static bool ReadRamsesVersionAndPrintWarningOnMismatch(ramses_internal::IInputStream& inputStream, const ramses_internal::String& verboseFileName); - static void WriteCurrentBuildVersionToStream(ramses_internal::IOutputStream& stream); - - ResourceDataPool& getResourceDataPool(); - ResourceDataPool const& getResourceDataPool() const; + static bool ReadRamsesVersionAndPrintWarningOnMismatch(ramses_internal::IInputStream& inputStream, std::string_view verboseFileName, EFeatureLevel featureLevel); + static void WriteCurrentBuildVersionToStream(ramses_internal::IOutputStream& stream, EFeatureLevel featureLevel); + static bool GetFeatureLevelFromFile(std::string_view fileName, EFeatureLevel& detectedFeatureLevel); + static bool GetFeatureLevelFromFile(int fd, size_t offset, size_t length, EFeatureLevel& detectedFeatureLevel); private: - friend class ClientFactory; - RamsesClientImpl(RamsesFrameworkImpl& ramsesFramework, const char* applicationName); - struct SceneCreationConfig { std::string caller; @@ -160,7 +159,7 @@ namespace ramses { public: LoadSceneRunnable(RamsesClientImpl& client, SceneCreationConfig&& cconfig); - virtual void execute() override; + void execute() override; private: RamsesClientImpl& m_client; @@ -170,17 +169,17 @@ namespace ramses class DeleteSceneRunnable : public ramses_internal::ITask { public: - DeleteSceneRunnable(Scene* scene, ramses_internal::ClientScene* llscene); - virtual void execute() override; + DeleteSceneRunnable(SceneOwningPtr scene, InternalSceneOwningPtr llscene); + void execute() override; private: - Scene* m_scene; - ramses_internal::ClientScene* m_lowLevelScene; + SceneOwningPtr m_scene; + InternalSceneOwningPtr m_lowLevelScene; }; struct SceneLoadStatus { - Scene* scene; + SceneOwningPtr scene; std::string sceneFilename; }; @@ -189,25 +188,26 @@ namespace ramses ramses_internal::ManagedResource manageResource(const ramses_internal::IResource* res); Scene* loadSceneSynchonousCommon(const SceneCreationConfig& cconf); - Scene* loadSceneFromCreationConfig(const SceneCreationConfig& cconf); - Scene* loadSceneObjectFromStream(const std::string& caller, + SceneOwningPtr loadSceneFromCreationConfig(const SceneCreationConfig& cconf); + SceneOwningPtr loadSceneObjectFromStream(const std::string& caller, std::string const& filename, ramses_internal::IInputStream& inputStream, bool localOnly, sceneId_t sceneId); - void finalizeLoadedScene(Scene* scene); + void finalizeLoadedScene(SceneOwningPtr scene); status_t validateScenes() const; + static bool GetFeatureLevelFromStream(ramses_internal::IInputStreamContainer& streamContainer, const std::string& desc, EFeatureLevel& detectedFeatureLevel); + RamsesClient* m_hlClient = nullptr; ramses_internal::ClientApplicationLogic m_appLogic; - ramses_internal::SceneFactory m_sceneFactory; + ramses_internal::SceneFactory m_sceneFactory; - SceneVector m_scenes; + SceneVector m_scenes; std::shared_ptr m_cmdPrintSceneList; std::shared_ptr m_cmdPrintValidation; - std::shared_ptr m_cmdForceFallbackImage; std::shared_ptr m_cmdFlushSceneVersion; std::shared_ptr m_cmdDumpSceneToFile; std::shared_ptr m_cmdLogResourceMemoryUsage; @@ -219,17 +219,15 @@ namespace ramses ramses_internal::EnqueueOnlyOneAtATimeQueue m_deleteSceneQueue; std::vector m_asyncSceneLoadStatusVec; - - ResourceDataPool m_resourceDataPool; }; template void RamsesClientImpl::enqueueSceneCommand(sceneId_t sceneId, T cmd) { ramses_internal::PlatformGuard guard(m_clientLock); - auto it = std::find_if(m_scenes.begin(), m_scenes.end(), [&](Scene* scene) { return scene->impl.getSceneId() == sceneId; }); + auto it = std::find_if(m_scenes.begin(), m_scenes.end(), [&](const auto& scene) { return scene->getSceneId() == sceneId; }); if (it != m_scenes.end()) - (*it)->impl.enqueueSceneCommand(std::move(cmd)); + (*it)->m_impl.enqueueSceneCommand(std::move(cmd)); } } diff --git a/client/ramses-client/impl/RamsesClientTypesImpl.h b/client/ramses-client/impl/RamsesClientTypesImpl.h index fa9319d17..86cbce6da 100644 --- a/client/ramses-client/impl/RamsesClientTypesImpl.h +++ b/client/ramses-client/impl/RamsesClientTypesImpl.h @@ -13,7 +13,7 @@ #include "ramses-client-api/TextureSwizzle.h" #include "SceneAPI/TextureEnums.h" #include "TextureUtils.h" -#include "ramses-client-api/EDataType.h" +#include "ramses-framework-api/EDataType.h" template <> struct fmt::formatter : public ramses_internal::SimpleFormatterBase @@ -31,7 +31,7 @@ struct fmt::formatter : public ramses_internal::SimpleFo namespace ramses { - static const char* DataTypeNames[] = + const std::array DataTypeNames = { "UINT16", "UINT32", diff --git a/client/ramses-client/impl/RamsesObjectImpl.cpp b/client/ramses-client/impl/RamsesObjectImpl.cpp index 72011beec..9293a09a7 100644 --- a/client/ramses-client/impl/RamsesObjectImpl.cpp +++ b/client/ramses-client/impl/RamsesObjectImpl.cpp @@ -17,7 +17,7 @@ namespace ramses { - RamsesObjectImpl::RamsesObjectImpl(ERamsesObjectType type, const char* name) + RamsesObjectImpl::RamsesObjectImpl(ERamsesObjectType type, std::string_view name) : m_type(type) , m_name(name) { @@ -52,18 +52,20 @@ namespace ramses return RamsesObjectTypeUtils::IsTypeMatchingBaseType(m_type, type); } - const ramses_internal::String& RamsesObjectImpl::getName() const + const std::string& RamsesObjectImpl::getName() const { return m_name; } - status_t RamsesObjectImpl::setName(RamsesObject& object, const char* name) + status_t RamsesObjectImpl::setName(RamsesObject& object, std::string_view name) { + std::string newName{name}; if (m_objectRegistry) { - m_objectRegistry->updateName(object, name); + // updateName must be called before m_name is changed + m_objectRegistry->updateName(object, newName); } - m_name = name; + std::swap(m_name, newName); return StatusOK; } @@ -111,15 +113,15 @@ namespace ramses status_t RamsesObjectImpl::validate() const { const status_t status = StatusObjectImpl::validate(); - ramses_internal::String message{ fmt::format("{} '{}'", RamsesObjectTypeUtils::GetRamsesObjectTypeName(getType()), getName()) }; - StatusObjectImpl::addValidationMessage(EValidationSeverity_Info, std::move(message)); + std::string message{ fmt::format("{} '{}'", RamsesObjectTypeUtils::GetRamsesObjectTypeName(getType()), getName()) }; + StatusObjectImpl::addValidationMessage(EValidationSeverity::Info, std::move(message)); return status; } - status_t RamsesObjectImpl::addValidationMessage(EValidationSeverity severity, ramses_internal::String message) const + status_t RamsesObjectImpl::addValidationMessage(EValidationSeverity severity, std::string message) const { - message = fmt::format("{} '{}': {}", RamsesObjectTypeUtils::GetRamsesObjectTypeName(getType()), getName(), message.stdRef()); + message = fmt::format("{} '{}': {}", RamsesObjectTypeUtils::GetRamsesObjectTypeName(getType()), getName(), message); return StatusObjectImpl::addValidationMessage(severity, std::move(message)); } } diff --git a/client/ramses-client/impl/RamsesObjectImpl.h b/client/ramses-client/impl/RamsesObjectImpl.h index 499ece01b..e4325cef8 100644 --- a/client/ramses-client/impl/RamsesObjectImpl.h +++ b/client/ramses-client/impl/RamsesObjectImpl.h @@ -16,9 +16,8 @@ #include "StatusObjectImpl.h" #include "RamsesObjectHandle.h" -// framework -#include "Collections/String.h" - +#include +#include namespace ramses_internal { @@ -36,8 +35,8 @@ namespace ramses class RamsesObjectImpl : public StatusObjectImpl { public: - explicit RamsesObjectImpl(ERamsesObjectType type, const char* name); - virtual ~RamsesObjectImpl() override; + explicit RamsesObjectImpl(ERamsesObjectType type, std::string_view name); + ~RamsesObjectImpl() override; void setObjectRegistry(IRamsesObjectRegistry& objectRegistry); void setObjectRegistryHandle(RamsesObjectHandle handle); @@ -45,8 +44,8 @@ namespace ramses ERamsesObjectType getType() const; bool isOfType(ERamsesObjectType type) const; - const ramses_internal::String& getName() const; - virtual status_t setName(RamsesObject& object, const char* name); + const std::string& getName() const; + virtual status_t setName(RamsesObject& object, std::string_view name); const RamsesObject& getRamsesObject() const; RamsesObject& getRamsesObject(); void setRamsesObject(RamsesObject& ramsesObject); @@ -57,14 +56,14 @@ namespace ramses virtual void deinitializeFrameworkData() = 0; - virtual status_t validate() const override; + status_t validate() const override; protected: - virtual status_t addValidationMessage(EValidationSeverity severity, ramses_internal::String message) const override; + status_t addValidationMessage(EValidationSeverity severity, std::string message) const override; private: ERamsesObjectType m_type; - ramses_internal::String m_name; + std::string m_name; RamsesObject* m_ramsesObject = nullptr; IRamsesObjectRegistry* m_objectRegistry = nullptr; diff --git a/client/ramses-client/impl/RamsesObjectRegistry.cpp b/client/ramses-client/impl/RamsesObjectRegistry.cpp index 924d44355..f5205b53d 100644 --- a/client/ramses-client/impl/RamsesObjectRegistry.cpp +++ b/client/ramses-client/impl/RamsesObjectRegistry.cpp @@ -17,43 +17,45 @@ namespace ramses { - RamsesObjectRegistry::~RamsesObjectRegistry() - { - } + RamsesObjectRegistry::~RamsesObjectRegistry() = default; - void RamsesObjectRegistry::addObject(RamsesObject& object) + void RamsesObjectRegistry::registerObjectInternal(RamsesObject& object) { assert(!containsObject(object)); - const ERamsesObjectType type = object.impl.getType(); - const RamsesObjectHandle handle = m_objects[type].allocate(); - *m_objects[type].getMemory(handle) = &object; - object.impl.setObjectRegistry(*this); - object.impl.setObjectRegistryHandle(handle); + const ERamsesObjectType type = object.m_impl.getType(); + const RamsesObjectHandle handle = m_objects[static_cast(type)].allocate(); + *m_objects[static_cast(type)].getMemory(handle) = &object; + object.m_impl.setObjectRegistry(*this); + object.m_impl.setObjectRegistryHandle(handle); - updateName(object, object.impl.getName()); + updateName(object, object.m_impl.getName()); trackSceneObjectById(object); } - void RamsesObjectRegistry::removeObject(RamsesObject& object) + void RamsesObjectRegistry::destroyAndUnregisterObject(RamsesObject& object) { assert(containsObject(object)); - if (object.isOfType(ERamsesObjectType_Node)) - setNodeDirty(RamsesObjectTypeUtils::ConvertTo(object).impl, false); + if (object.isOfType(ERamsesObjectType::Node)) + setNodeDirty(RamsesObjectTypeUtils::ConvertTo(object).m_impl, false); - if (object.isOfType(ERamsesObjectType_SceneObject)) + if (object.isOfType(ERamsesObjectType::SceneObject)) { const sceneObjectId_t sceneObjectId = RamsesObjectTypeUtils::ConvertTo(object).getSceneObjectId(); assert(m_objectsById.contains(sceneObjectId)); m_objectsById.remove(sceneObjectId); } - m_objectsByName.remove(object.impl.getName()); + m_objectsByName.remove(object.m_impl.getName()); - const RamsesObjectHandle handle = object.impl.getObjectRegistryHandle(); - const ERamsesObjectType type = object.impl.getType(); + const RamsesObjectHandle handle = object.m_impl.getObjectRegistryHandle(); + const auto type = static_cast(object.m_impl.getType()); m_objects[type].release(handle); + + auto it = std::find_if(m_objectsOwningContainer.begin(), m_objectsOwningContainer.end(), [&object](auto& ro) { return ro.get() == &object; }); + assert(it != m_objectsOwningContainer.end()); + m_objectsOwningContainer.erase(it); } void RamsesObjectRegistry::reserveAdditionalGeneralCapacity(uint32_t additionalCount) @@ -61,32 +63,34 @@ namespace ramses // not every object has a name. this might reserve more than needed but never more than // num of current objects with name + additionalCapacity m_objectsByName.reserve(m_objectsByName.size() + additionalCount); + m_objectsOwningContainer.reserve(m_objectsOwningContainer.size() + additionalCount); } void RamsesObjectRegistry::reserveAdditionalObjectCapacity(ERamsesObjectType type, uint32_t additionalCount) { assert(RamsesObjectTypeUtils::IsConcreteType(type)); - m_objects[type].preallocateSize(m_objects[type].getActualCount() + additionalCount); + const auto index = static_cast(type); + m_objects[index].preallocateSize(m_objects[index].getActualCount() + additionalCount); } uint32_t RamsesObjectRegistry::getNumberOfObjects(ERamsesObjectType type) const { assert(RamsesObjectTypeUtils::IsConcreteType(type)); - return m_objects[type].getActualCount(); + return m_objects[static_cast(type)].getActualCount(); } bool RamsesObjectRegistry::containsObject(const RamsesObject& object) const { - const RamsesObjectHandle handle = object.impl.getObjectRegistryHandle(); - const ERamsesObjectType type = object.impl.getType(); - const RamsesObjectsPool& objectsPool = m_objects[type]; + const RamsesObjectHandle handle = object.m_impl.getObjectRegistryHandle(); + const ERamsesObjectType type = object.m_impl.getType(); + const RamsesObjectsPool& objectsPool = m_objects[static_cast(type)]; return objectsPool.isAllocated(handle) && (*objectsPool.getMemory(handle) == &object); } - void RamsesObjectRegistry::updateName(RamsesObject& object, const ramses_internal::String& name) + void RamsesObjectRegistry::updateName(RamsesObject& object, const std::string& name) { assert(containsObject(object)); - const ramses_internal::String& oldName = object.impl.getName(); + const std::string& oldName = object.m_impl.getName(); if (!oldName.empty()) { m_objectsByName.remove(oldName); @@ -99,7 +103,7 @@ namespace ramses void RamsesObjectRegistry::trackSceneObjectById(RamsesObject& object) { - if (object.isOfType(ERamsesObjectType_SceneObject)) + if (object.isOfType(ERamsesObjectType::SceneObject)) { SceneObject& sceneObject = RamsesObjectTypeUtils::ConvertTo(object); const sceneObjectId_t sceneObjectId = RamsesObjectTypeUtils::ConvertTo(object).getSceneObjectId(); @@ -108,14 +112,14 @@ namespace ramses } } - RamsesObject* RamsesObjectRegistry::findObjectByName(const char* name) + RamsesObject* RamsesObjectRegistry::findObjectByName(std::string_view name) { RamsesObject* object(nullptr); - m_objectsByName.get(name, object); + m_objectsByName.get(std::string{name}, object); return object; } - const RamsesObject* RamsesObjectRegistry::findObjectByName(const char* name) const + const RamsesObject* RamsesObjectRegistry::findObjectByName(std::string_view name) const { // const version of findObjectByName cast to its non-const version to avoid duplicating code return const_cast((const_cast(*this)).findObjectByName(name)); @@ -141,22 +145,22 @@ namespace ramses // preallocate memory in container uint32_t objectCount = 0u; - for (uint32_t i = 0u; i < ERamsesObjectType_NUMBER_OF_TYPES; ++i) + for (uint32_t i = 0u; i < static_cast(ERamsesObjectType::NUMBER_OF_TYPES); ++i) { const ERamsesObjectType type = ERamsesObjectType(i); if (RamsesObjectTypeUtils::IsConcreteType(type) && RamsesObjectTypeUtils::IsTypeMatchingBaseType(type, ofType)) { - objectCount += m_objects[type].getActualCount(); + objectCount += m_objects[i].getActualCount(); } } objects.reserve(objectCount); - for (uint32_t i = 0u; i < ERamsesObjectType_NUMBER_OF_TYPES; ++i) + for (uint32_t i = 0u; i < static_cast(ERamsesObjectType::NUMBER_OF_TYPES); ++i) { const ERamsesObjectType type = ERamsesObjectType(i); if (RamsesObjectTypeUtils::IsConcreteType(type) && RamsesObjectTypeUtils::IsTypeMatchingBaseType(type, ofType)) { - const RamsesObjectsPool& objectsPool = m_objects[type]; + const RamsesObjectsPool& objectsPool = m_objects[i]; for (RamsesObjectHandle handle(0u); handle < objectsPool.getTotalCount(); ++handle) { if (objectsPool.isAllocated(handle)) diff --git a/client/ramses-client/impl/RamsesObjectRegistry.h b/client/ramses-client/impl/RamsesObjectRegistry.h index 92cdca915..e4dd47c60 100644 --- a/client/ramses-client/impl/RamsesObjectRegistry.h +++ b/client/ramses-client/impl/RamsesObjectRegistry.h @@ -10,15 +10,19 @@ #define RAMSES_RAMSESOBJECTREGISTRY_H #include "ramses-framework-api/RamsesFrameworkTypes.h" +#include "ramses-client-api/RamsesObject.h" #include "RamsesObjectHandle.h" #include "RamsesObjectVector.h" #include "IRamsesObjectRegistry.h" #include "Collections/HashMap.h" -#include "Collections/String.h" #include "Utils/MemoryPool.h" +#include +#include +#include + namespace ramses { class RamsesObject; @@ -28,49 +32,69 @@ namespace ramses class RamsesObjectRegistry final : public IRamsesObjectRegistry { public: - virtual ~RamsesObjectRegistry() override; + ~RamsesObjectRegistry() override; + + template + T& createAndRegisterObject(std::unique_ptr impl); + void destroyAndUnregisterObject(RamsesObject& object); - void addObject(RamsesObject& object); - void removeObject(RamsesObject& object); - void reserveAdditionalGeneralCapacity(uint32_t additionalCount); - void reserveAdditionalObjectCapacity(ERamsesObjectType type, uint32_t additionalCount); - uint32_t getNumberOfObjects(ERamsesObjectType type) const; + void reserveAdditionalGeneralCapacity(uint32_t additionalCount); + void reserveAdditionalObjectCapacity(ERamsesObjectType type, uint32_t additionalCount); + [[nodiscard]] uint32_t getNumberOfObjects(ERamsesObjectType type) const; - void getObjectsOfType(RamsesObjectVector& objects, ERamsesObjectType ofType) const; + void getObjectsOfType(RamsesObjectVector& objects, ERamsesObjectType ofType) const; // IRamsesObjectRegistry - virtual void updateName(RamsesObject& object, const ramses_internal::String& name) override final; + void updateName(RamsesObject& object, const std::string& name) override; - const RamsesObject* findObjectByName(const char* name) const; - RamsesObject* findObjectByName(const char* name); + [[nodiscard]] const RamsesObject* findObjectByName(std::string_view name) const; + RamsesObject* findObjectByName(std::string_view name); - const SceneObject* findObjectById(sceneObjectId_t id) const; + [[nodiscard]] const SceneObject* findObjectById(sceneObjectId_t id) const; SceneObject* findObjectById(sceneObjectId_t id); void setNodeDirty(NodeImpl& node, bool dirty); - bool isNodeDirty(const NodeImpl& node) const; + [[nodiscard]] bool isNodeDirty(const NodeImpl& node) const; - const NodeImplSet& getDirtyNodes() const; - void clearDirtyNodes(); + [[nodiscard]] const NodeImplSet& getDirtyNodes() const; + void clearDirtyNodes(); private: - bool containsObject(const RamsesObject& object) const; + void registerObjectInternal(RamsesObject& object); + [[nodiscard]] bool containsObject(const RamsesObject& object) const; void trackSceneObjectById(RamsesObject& object); - using ObjectNameMap = ramses_internal::HashMap; + using ObjectNameMap = ramses_internal::HashMap; ObjectNameMap m_objectsByName; using ObjectIdMap = ramses_internal::HashMap; ObjectIdMap m_objectsById; using RamsesObjectsPool = ramses_internal::MemoryPool; - RamsesObjectsPool m_objects[ERamsesObjectType_NUMBER_OF_TYPES]; + std::array(ERamsesObjectType::NUMBER_OF_TYPES)> m_objects; + + using RamsesObjectUniquePtr = std::unique_ptr>; + std::vector m_objectsOwningContainer; NodeImplSet m_dirtyNodes; friend class RamsesObjectRegistryIterator; }; + + template + T& RamsesObjectRegistry::createAndRegisterObject(std::unique_ptr impl) + { + static_assert(std::is_base_of_v, "Meant for RamsesObject instances only"); + + std::unique_ptr> object{ new T{ std::move(impl) }, [](RamsesObject* o) { delete o; } }; + T* objectRawPtr = object.get(); + this->m_objectsOwningContainer.push_back(std::move(object)); + + this->registerObjectInternal(*objectRawPtr); + + return *objectRawPtr; + } } #endif diff --git a/client/ramses-client/impl/RamsesObjectRegistryIterator.h b/client/ramses-client/impl/RamsesObjectRegistryIterator.h index 1bb7cbed4..fc0839b9f 100644 --- a/client/ramses-client/impl/RamsesObjectRegistryIterator.h +++ b/client/ramses-client/impl/RamsesObjectRegistryIterator.h @@ -19,7 +19,7 @@ namespace ramses { public: RamsesObjectRegistryIterator(const RamsesObjectRegistry& registry, ERamsesObjectType type) - : m_objects(registry.m_objects[type]) + : m_objects(registry.m_objects[static_cast(type)]) , m_objectsTotalCount(m_objects.getTotalCount()) , m_current(0u) { diff --git a/client/ramses-client/impl/RamsesObjectTypeTraits.h b/client/ramses-client/impl/RamsesObjectTypeTraits.h index f34281cb8..a9728bd6e 100644 --- a/client/ramses-client/impl/RamsesObjectTypeTraits.h +++ b/client/ramses-client/impl/RamsesObjectTypeTraits.h @@ -10,6 +10,7 @@ #define RAMSES_RAMSESOBJECTTYPETRAITS_H #include "ramses-client-api/RamsesObjectTypes.h" +#include namespace ramses { @@ -70,81 +71,37 @@ namespace ramses // - conversion from class type to type ID and vice versa // - provide info about closest base class type ID // - provide info whether type is concrete (can be instantiated) or a pure base type - DEFINE_RAMSES_OBJECT_TRAITS(RamsesClient, ERamsesObjectType_Client, ERamsesObjectType_RamsesObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(Scene, ERamsesObjectType_Scene, ERamsesObjectType_ClientObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(AnimationSystem, ERamsesObjectType_AnimationSystem, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(AnimationSystemRealTime, ERamsesObjectType_AnimationSystemRealTime, ERamsesObjectType_AnimationSystem, true); - DEFINE_RAMSES_OBJECT_TRAITS(MeshNode, ERamsesObjectType_MeshNode, ERamsesObjectType_Node, true); - DEFINE_RAMSES_OBJECT_TRAITS(PerspectiveCamera, ERamsesObjectType_PerspectiveCamera, ERamsesObjectType_Camera, true); - DEFINE_RAMSES_OBJECT_TRAITS(OrthographicCamera, ERamsesObjectType_OrthographicCamera, ERamsesObjectType_Camera, true); - DEFINE_RAMSES_OBJECT_TRAITS(Effect, ERamsesObjectType_Effect, ERamsesObjectType_Resource, true); - DEFINE_RAMSES_OBJECT_TRAITS(AnimatedProperty, ERamsesObjectType_AnimatedProperty, ERamsesObjectType_AnimationObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(Animation, ERamsesObjectType_Animation, ERamsesObjectType_AnimationObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(AnimationSequence, ERamsesObjectType_AnimationSequence, ERamsesObjectType_AnimationObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(Appearance, ERamsesObjectType_Appearance, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(GeometryBinding, ERamsesObjectType_GeometryBinding, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(PickableObject, ERamsesObjectType_PickableObject, ERamsesObjectType_Node, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineStepBool, ERamsesObjectType_SplineStepBool, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineStepFloat, ERamsesObjectType_SplineStepFloat, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineStepInt32, ERamsesObjectType_SplineStepInt32, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineStepVector2f, ERamsesObjectType_SplineStepVector2f, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineStepVector3f, ERamsesObjectType_SplineStepVector3f, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineStepVector4f, ERamsesObjectType_SplineStepVector4f, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineStepVector2i, ERamsesObjectType_SplineStepVector2i, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineStepVector3i, ERamsesObjectType_SplineStepVector3i, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineStepVector4i, ERamsesObjectType_SplineStepVector4i, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineLinearFloat, ERamsesObjectType_SplineLinearFloat, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineLinearInt32, ERamsesObjectType_SplineLinearInt32, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineLinearVector2f, ERamsesObjectType_SplineLinearVector2f, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineLinearVector3f, ERamsesObjectType_SplineLinearVector3f, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineLinearVector4f, ERamsesObjectType_SplineLinearVector4f, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineLinearVector2i, ERamsesObjectType_SplineLinearVector2i, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineLinearVector3i, ERamsesObjectType_SplineLinearVector3i, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineLinearVector4i, ERamsesObjectType_SplineLinearVector4i, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineBezierFloat, ERamsesObjectType_SplineBezierFloat, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineBezierInt32, ERamsesObjectType_SplineBezierInt32, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineBezierVector2f, ERamsesObjectType_SplineBezierVector2f, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineBezierVector3f, ERamsesObjectType_SplineBezierVector3f, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineBezierVector4f, ERamsesObjectType_SplineBezierVector4f, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineBezierVector2i, ERamsesObjectType_SplineBezierVector2i, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineBezierVector3i, ERamsesObjectType_SplineBezierVector3i, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(SplineBezierVector4i, ERamsesObjectType_SplineBezierVector4i, ERamsesObjectType_Spline, true); - DEFINE_RAMSES_OBJECT_TRAITS(Texture2D, ERamsesObjectType_Texture2D, ERamsesObjectType_Resource, true); - DEFINE_RAMSES_OBJECT_TRAITS(Texture3D, ERamsesObjectType_Texture3D, ERamsesObjectType_Resource, true); - DEFINE_RAMSES_OBJECT_TRAITS(TextureCube, ERamsesObjectType_TextureCube, ERamsesObjectType_Resource, true); - DEFINE_RAMSES_OBJECT_TRAITS(ArrayResource, ERamsesObjectType_ArrayResource, ERamsesObjectType_Resource, true); - DEFINE_RAMSES_OBJECT_TRAITS(RenderGroup, ERamsesObjectType_RenderGroup, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(RenderPass, ERamsesObjectType_RenderPass, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(BlitPass, ERamsesObjectType_BlitPass, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(TextureSampler, ERamsesObjectType_TextureSampler, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(TextureSamplerMS, ERamsesObjectType_TextureSamplerMS, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(RenderBuffer, ERamsesObjectType_RenderBuffer, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(RenderTarget, ERamsesObjectType_RenderTarget, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(DataFloat, ERamsesObjectType_DataFloat, ERamsesObjectType_DataObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(DataVector2f, ERamsesObjectType_DataVector2f, ERamsesObjectType_DataObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(DataVector3f, ERamsesObjectType_DataVector3f, ERamsesObjectType_DataObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(DataVector4f, ERamsesObjectType_DataVector4f, ERamsesObjectType_DataObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(DataMatrix22f, ERamsesObjectType_DataMatrix22f, ERamsesObjectType_DataObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(DataMatrix33f, ERamsesObjectType_DataMatrix33f, ERamsesObjectType_DataObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(DataMatrix44f, ERamsesObjectType_DataMatrix44f, ERamsesObjectType_DataObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(DataInt32, ERamsesObjectType_DataInt32, ERamsesObjectType_DataObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(DataVector2i, ERamsesObjectType_DataVector2i, ERamsesObjectType_DataObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(DataVector3i, ERamsesObjectType_DataVector3i, ERamsesObjectType_DataObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(DataVector4i, ERamsesObjectType_DataVector4i, ERamsesObjectType_DataObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(StreamTexture, ERamsesObjectType_StreamTexture, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(RamsesObject, ERamsesObjectType_RamsesObject, ERamsesObjectType_Invalid, false); - DEFINE_RAMSES_OBJECT_TRAITS(ClientObject, ERamsesObjectType_ClientObject, ERamsesObjectType_RamsesObject, false); - DEFINE_RAMSES_OBJECT_TRAITS(SceneObject, ERamsesObjectType_SceneObject, ERamsesObjectType_ClientObject, false); - DEFINE_RAMSES_OBJECT_TRAITS(AnimationObject, ERamsesObjectType_AnimationObject, ERamsesObjectType_SceneObject, false); - DEFINE_RAMSES_OBJECT_TRAITS(Node, ERamsesObjectType_Node, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(Camera, ERamsesObjectType_Camera, ERamsesObjectType_Node, false); - DEFINE_RAMSES_OBJECT_TRAITS(Spline, ERamsesObjectType_Spline, ERamsesObjectType_AnimationObject, false); - DEFINE_RAMSES_OBJECT_TRAITS(Resource, ERamsesObjectType_Resource, ERamsesObjectType_SceneObject, false); - DEFINE_RAMSES_OBJECT_TRAITS(DataObject, ERamsesObjectType_DataObject, ERamsesObjectType_SceneObject, false); - DEFINE_RAMSES_OBJECT_TRAITS(ArrayBuffer, ERamsesObjectType_DataBufferObject, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(Texture2DBuffer, ERamsesObjectType_Texture2DBuffer, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(SceneReference, ERamsesObjectType_SceneReference, ERamsesObjectType_SceneObject, true); - DEFINE_RAMSES_OBJECT_TRAITS(TextureSamplerExternal, ERamsesObjectType_TextureSamplerExternal, ERamsesObjectType_SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(RamsesClient, ERamsesObjectType::Client, ERamsesObjectType::RamsesObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(Scene, ERamsesObjectType::Scene, ERamsesObjectType::ClientObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(MeshNode, ERamsesObjectType::MeshNode, ERamsesObjectType::Node, true); + DEFINE_RAMSES_OBJECT_TRAITS(PerspectiveCamera, ERamsesObjectType::PerspectiveCamera, ERamsesObjectType::Camera, true); + DEFINE_RAMSES_OBJECT_TRAITS(OrthographicCamera, ERamsesObjectType::OrthographicCamera, ERamsesObjectType::Camera, true); + DEFINE_RAMSES_OBJECT_TRAITS(Effect, ERamsesObjectType::Effect, ERamsesObjectType::Resource, true); + DEFINE_RAMSES_OBJECT_TRAITS(Appearance, ERamsesObjectType::Appearance, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(GeometryBinding, ERamsesObjectType::GeometryBinding, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(PickableObject, ERamsesObjectType::PickableObject, ERamsesObjectType::Node, true); + DEFINE_RAMSES_OBJECT_TRAITS(Texture2D, ERamsesObjectType::Texture2D, ERamsesObjectType::Resource, true); + DEFINE_RAMSES_OBJECT_TRAITS(Texture3D, ERamsesObjectType::Texture3D, ERamsesObjectType::Resource, true); + DEFINE_RAMSES_OBJECT_TRAITS(TextureCube, ERamsesObjectType::TextureCube, ERamsesObjectType::Resource, true); + DEFINE_RAMSES_OBJECT_TRAITS(ArrayResource, ERamsesObjectType::ArrayResource, ERamsesObjectType::Resource, true); + DEFINE_RAMSES_OBJECT_TRAITS(RenderGroup, ERamsesObjectType::RenderGroup, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(RenderPass, ERamsesObjectType::RenderPass, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(BlitPass, ERamsesObjectType::BlitPass, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(TextureSampler, ERamsesObjectType::TextureSampler, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(TextureSamplerMS, ERamsesObjectType::TextureSamplerMS, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(RenderBuffer, ERamsesObjectType::RenderBuffer, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(RenderTarget, ERamsesObjectType::RenderTarget, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(RamsesObject, ERamsesObjectType::RamsesObject, ERamsesObjectType::Invalid, false); + DEFINE_RAMSES_OBJECT_TRAITS(ClientObject, ERamsesObjectType::ClientObject, ERamsesObjectType::RamsesObject, false); + DEFINE_RAMSES_OBJECT_TRAITS(SceneObject, ERamsesObjectType::SceneObject, ERamsesObjectType::ClientObject, false); + DEFINE_RAMSES_OBJECT_TRAITS(Node, ERamsesObjectType::Node, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(Camera, ERamsesObjectType::Camera, ERamsesObjectType::Node, false); + DEFINE_RAMSES_OBJECT_TRAITS(Resource, ERamsesObjectType::Resource, ERamsesObjectType::SceneObject, false); + DEFINE_RAMSES_OBJECT_TRAITS(DataObject, ERamsesObjectType::DataObject, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(ArrayBuffer, ERamsesObjectType::ArrayBufferObject, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(Texture2DBuffer, ERamsesObjectType::Texture2DBuffer, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(SceneReference, ERamsesObjectType::SceneReference, ERamsesObjectType::SceneObject, true); + DEFINE_RAMSES_OBJECT_TRAITS(TextureSamplerExternal, ERamsesObjectType::TextureSamplerExternal, ERamsesObjectType::SceneObject, true); struct RamsesObjectTraitsEntry { @@ -154,12 +111,12 @@ namespace ramses }; #define DEFINE_RAMSES_OBJECT_TRAITS_LIST_BEGIN() \ - const RamsesObjectTraitsEntry RamsesObjectTraits[] = \ + const std::array RamsesObjectTraits = \ { \ - { ERamsesObjectType_Invalid, ERamsesObjectType_Invalid, false }, + RamsesObjectTraitsEntry{ ERamsesObjectType::Invalid, ERamsesObjectType::Invalid, false }, #define DEFINE_RAMSES_OBJECT_TRAITS_LIST(_typeId) \ - { _typeId, CLASS_OF_RAMSES_OBJECT_TYPE<_typeId>::BaseTypeID, CLASS_OF_RAMSES_OBJECT_TYPE<_typeId>::IsConcreteType }, -#define DATA_BIND_DEFINE_END() \ + RamsesObjectTraitsEntry{ _typeId, CLASS_OF_RAMSES_OBJECT_TYPE<_typeId>::BaseTypeID, CLASS_OF_RAMSES_OBJECT_TYPE<_typeId>::IsConcreteType }, +#define DEFINE_RAMSES_OBJECT_TRAITS_LIST_END() \ }; // Define dynamic traits for all RamsesObject types that allow: @@ -167,82 +124,40 @@ namespace ramses // - provide info whether type is concrete (can be instantiated) or a pure base type // NOTE THAT ALL TYPES BELOW MUST BE PROVIDED IN ORDER OF ERamsesObjectType ENUMERATION DEFINE_RAMSES_OBJECT_TRAITS_LIST_BEGIN() - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_ClientObject) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_RamsesObject) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SceneObject) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_AnimationObject) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Client) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Scene) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_AnimationSystem) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_AnimationSystemRealTime) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Node) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_MeshNode) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Camera) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_PerspectiveCamera) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_OrthographicCamera) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Effect) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_AnimatedProperty) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Animation) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_AnimationSequence) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Appearance) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_GeometryBinding) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_PickableObject) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Spline) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineStepBool) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineStepFloat) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineStepInt32) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineStepVector2f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineStepVector3f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineStepVector4f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineStepVector2i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineStepVector3i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineStepVector4i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineLinearFloat) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineLinearInt32) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineLinearVector2f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineLinearVector3f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineLinearVector4f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineLinearVector2i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineLinearVector3i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineLinearVector4i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineBezierFloat) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineBezierInt32) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineBezierVector2f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineBezierVector3f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineBezierVector4f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineBezierVector2i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineBezierVector3i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SplineBezierVector4i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Resource) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Texture2D) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Texture3D) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_TextureCube) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_ArrayResource) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_RenderGroup) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_RenderPass) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_BlitPass) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_TextureSampler) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_TextureSamplerMS) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_RenderBuffer) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_RenderTarget) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataBufferObject) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_Texture2DBuffer) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataObject) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataFloat) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataVector2f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataVector3f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataVector4f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataMatrix22f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataMatrix33f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataMatrix44f) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataInt32) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataVector2i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataVector3i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_DataVector4i) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_StreamTexture) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_SceneReference) - DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType_TextureSamplerExternal) - DATA_BIND_DEFINE_END() + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::ClientObject) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::RamsesObject) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::SceneObject) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::Client) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::Scene) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::Node) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::MeshNode) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::Camera) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::PerspectiveCamera) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::OrthographicCamera) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::Effect) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::Appearance) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::GeometryBinding) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::PickableObject) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::Resource) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::Texture2D) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::Texture3D) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::TextureCube) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::ArrayResource) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::RenderGroup) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::RenderPass) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::BlitPass) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::TextureSampler) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::TextureSamplerMS) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::RenderBuffer) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::RenderTarget) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::ArrayBufferObject) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::Texture2DBuffer) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::DataObject) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::SceneReference) + DEFINE_RAMSES_OBJECT_TRAITS_LIST(ERamsesObjectType::TextureSamplerExternal) + DEFINE_RAMSES_OBJECT_TRAITS_LIST_END() + + static_assert(static_cast(ERamsesObjectType::NUMBER_OF_TYPES) == RamsesObjectTraits.size(), "Every RamsesObject type must register its traits!"); } #endif diff --git a/client/ramses-client/impl/RamsesObjectTypeUtils.cpp b/client/ramses-client/impl/RamsesObjectTypeUtils.cpp index 1d47f02d6..182a35583 100644 --- a/client/ramses-client/impl/RamsesObjectTypeUtils.cpp +++ b/client/ramses-client/impl/RamsesObjectTypeUtils.cpp @@ -11,55 +11,23 @@ namespace ramses { - static const char* const RamsesObjectTypeNames[] = + const std::array RamsesObjectTypeNames = { "ERamsesObjectType_Invalid", "ERamsesObjectType_RamsesObject", "ERamsesObjectType_ClientObject", "ERamsesObjectType_SceneObject", - "ERamsesObjectType_AnimationObject", "ERamsesObjectType_Client", "ERamsesObjectType_Scene", - "ERamsesObjectType_AnimationSystem", - "ERamsesObjectType_AnimationSystemRealTime", "ERamsesObjectType_Node", "ERamsesObjectType_MeshNode", "ERamsesObjectType_Camera", "ERamsesObjectType_PerspectiveCamera", "ERamsesObjectType_OrthographicCamera", "ERamsesObjectType_Effect", - "ERamsesObjectType_AnimatedProperty", - "ERamsesObjectType_Animation", - "ERamsesObjectType_AnimationSequence", "ERamsesObjectType_Appearance", "ERamsesObjectType_Geometry", "ERamsesObjectType_PickableObject", - "ERamsesObjectType_Spline", - "ERamsesObjectType_SplineStepBool", - "ERamsesObjectType_SplineStepFloat", - "ERamsesObjectType_SplineStepInt32", - "ERamsesObjectType_SplineStepVector2f", - "ERamsesObjectType_SplineStepVector3f", - "ERamsesObjectType_SplineStepVector4f", - "ERamsesObjectType_SplineStepVector2i", - "ERamsesObjectType_SplineStepVector3i", - "ERamsesObjectType_SplineStepVector4i", - "ERamsesObjectType_SplineLinearFloat", - "ERamsesObjectType_SplineLinearInt32", - "ERamsesObjectType_SplineLinearVector2f", - "ERamsesObjectType_SplineLinearVector3f", - "ERamsesObjectType_SplineLinearVector4f", - "ERamsesObjectType_SplineLinearVector2i", - "ERamsesObjectType_SplineLinearVector3i", - "ERamsesObjectType_SplineLinearVector4i", - "ERamsesObjectType_SplineBezierFloat", - "ERamsesObjectType_SplineBezierInt32", - "ERamsesObjectType_SplineBezierVector2f", - "ERamsesObjectType_SplineBezierVector3f", - "ERamsesObjectType_SplineBezierVector4f", - "ERamsesObjectType_SplineBezierVector2i", - "ERamsesObjectType_SplineBezierVector3i", - "ERamsesObjectType_SplineBezierVector4i", "ERamsesObjectType_Resource", "ERamsesObjectType_Texture2D", "ERamsesObjectType_Texture3D", @@ -72,28 +40,16 @@ namespace ramses "ERamsesObjectType_TextureSamplerMS", "ERamsesObjectType_RenderBuffer", "ERamsesObjectType_RenderTarget", - "ERamsesObjectType_DataBufferObject", + "ERamsesObjectType_ArrayBufferObject", "ERamsesObjectType_Texture2DBuffer", "ERamsesObjectType_DataObject", - "ERamsesObjectType_DataFloat", - "ERamsesObjectType_DataVector2f", - "ERamsesObjectType_DataVector3f", - "ERamsesObjectType_DataVector4f", - "ERamsesObjectType_DataMatrix22f", - "ERamsesObjectType_DataMatrix33f", - "ERamsesObjectType_DataMatrix44f", - "ERamsesObjectType_DataInt32", - "ERamsesObjectType_DataVector2i", - "ERamsesObjectType_DataVector3i", - "ERamsesObjectType_DataVector4i", - "ERamsesObjectType_StreamTexture", "ERamsesObjectType_SceneReference", "ERamsesObjectType_TextureSamplerExternal" }; - ENUM_TO_STRING(ERamsesObjectType, RamsesObjectTypeNames, ERamsesObjectType_NUMBER_OF_TYPES); + ENUM_TO_STRING(ERamsesObjectType, RamsesObjectTypeNames, ERamsesObjectType::NUMBER_OF_TYPES); - static_assert(ERamsesObjectType_NUMBER_OF_TYPES == (sizeof(RamsesObjectTraits) / sizeof(RamsesObjectTraits[0])), "Every RamsesObject type must register its traits!"); + static_assert(static_cast(ERamsesObjectType::NUMBER_OF_TYPES) == RamsesObjectTraits.size(), "Every RamsesObject type must register its traits!"); const char* RamsesObjectTypeUtils::GetRamsesObjectTypeName(ERamsesObjectType type) { @@ -102,14 +58,15 @@ namespace ramses bool RamsesObjectTypeUtils::IsTypeMatchingBaseType(ERamsesObjectType type, ERamsesObjectType baseType) { - while (type != ERamsesObjectType_Invalid) + while (type != ERamsesObjectType::Invalid) { if (type == baseType) { return true; } - assert(RamsesObjectTraits[type].typeID == type && "Wrong order of RamsesObject traits!"); - type = RamsesObjectTraits[type].baseClassTypeID; + const auto index = static_cast(type); + assert(RamsesObjectTraits[index].typeID == type && "Wrong order of RamsesObject traits!"); + type = RamsesObjectTraits[index].baseClassTypeID; } return false; @@ -117,7 +74,8 @@ namespace ramses bool RamsesObjectTypeUtils::IsConcreteType(ERamsesObjectType type) { - assert(RamsesObjectTraits[type].typeID == type && "Wrong order of RamsesObject traits!"); - return RamsesObjectTraits[type].isConcreteType; + const auto index = static_cast(type); + assert(RamsesObjectTraits[index].typeID == type && "Wrong order of RamsesObject traits!"); + return RamsesObjectTraits[index].isConcreteType; } } diff --git a/client/ramses-client/impl/RamsesVersion.cpp b/client/ramses-client/impl/RamsesVersion.cpp index 5e082a992..a71ec694d 100644 --- a/client/ramses-client/impl/RamsesVersion.cpp +++ b/client/ramses-client/impl/RamsesVersion.cpp @@ -7,7 +7,6 @@ // ------------------------------------------------------------------------- #include "RamsesVersion.h" -#include "Collections/String.h" #include "Collections/IOutputStream.h" #include "Collections/IInputStream.h" #include "Collections/StringOutputStream.h" @@ -15,24 +14,26 @@ #include "Utils/LogMacros.h" #include "ramses-sdk-build-config.h" +#include + namespace ramses_internal { namespace RamsesVersion { - void WriteToStream(IOutputStream& stream, const String& versionString, const String& gitHash) + void WriteToStream(IOutputStream& stream, std::string_view versionString, std::string_view gitHash, ramses::EFeatureLevel featureLevel) { - LOG_INFO(CONTEXT_CLIENT, "RamsesVersion::WriteToStream: Version: " << versionString << " Git Hash: " << gitHash); + LOG_INFO(CONTEXT_CLIENT, "RamsesVersion::WriteToStream: Version: " << versionString << " Git Hash: " << gitHash << " Feature Level: " << featureLevel); StringOutputStream out; - out << "[RamsesVersion:" << versionString << "]\n[GitHash:" << gitHash << "]\n"; + out << "[RamsesVersion:" << versionString << "]\n[GitHash:" << gitHash << "]\n[FeatureLevel:" << featureLevel << "]\n"; stream.write(out.c_str(), out.size()); } - static bool ReadUntilNewline(IInputStream& stream, UInt32 readLimit, String& out) + static bool ReadUntilNewline(IInputStream& stream, uint32_t readLimit, std::string& out) { out.reserve(readLimit); - for (UInt32 i = 0; i < readLimit; ++i) + for (uint32_t i = 0; i < readLimit; ++i) { - Char character = 0; + char character = 0; stream.read(&character, 1); if (stream.getState() != EStatus::Ok || character == 0) { @@ -47,13 +48,13 @@ namespace ramses_internal return false; } - static bool ExpectString(const String& inputString, UInt& matchIndexInOut, const char* expectedString) + static bool ExpectString(const std::string& inputString, size_t& matchIndexInOut, const char* expectedString) { - const UInt expectedLength = std::strlen(expectedString); + const size_t expectedLength = std::strlen(expectedString); if (expectedLength + matchIndexInOut > inputString.size()) return false; - for (UInt i = 0; i < expectedLength; ++i) + for (size_t i = 0; i < expectedLength; ++i) { if (inputString[matchIndexInOut] != expectedString[i]) return false; @@ -62,7 +63,7 @@ namespace ramses_internal return true; } - static bool ExpectAndGetNumber(const String& inputString, UInt& matchIndexInOut, UInt32& numberOut) + static bool ExpectAndGetNumber(const std::string& inputString, size_t& matchIndexInOut, uint32_t& numberOut) { const char* startptr = inputString.data() + matchIndexInOut; char* endptr = nullptr; @@ -70,31 +71,31 @@ namespace ramses_internal if (startptr == endptr || readNumber < 0) return false; matchIndexInOut += (endptr - startptr); - numberOut = static_cast(readNumber); + numberOut = static_cast(readNumber); return true; } - static bool ExpectHexString(const String& inputString, UInt& matchIndexInOut) + static bool ExpectHexString(const std::string& inputString, size_t& matchIndexInOut) { - const UInt startIdx = matchIndexInOut; + const size_t startIdx = matchIndexInOut; while (matchIndexInOut < inputString.size() && std::isxdigit(inputString[matchIndexInOut])) ++matchIndexInOut; return startIdx != matchIndexInOut; } - static bool ExpectRamsesVersion(const String& versionString, VersionInfo& outVersion) + static bool ExpectRamsesVersion(const std::string& versionString, VersionInfo& outVersion) { - UInt idx = 0; + size_t idx = 0; if (!ExpectString(versionString, idx, "[RamsesVersion:")) return false; - const UInt versionStart = idx; + const size_t versionStart = idx; if (!ExpectAndGetNumber(versionString, idx, outVersion.major) || !ExpectString(versionString, idx, ".") || !ExpectAndGetNumber(versionString, idx, outVersion.minor) || !ExpectString(versionString, idx, ".")) return false; - const UInt trailingVersionStart = idx; + const size_t trailingVersionStart = idx; while (idx < versionString.size() && versionString[idx] != ']') ++idx; if (idx == trailingVersionStart) // may not be empty @@ -108,12 +109,12 @@ namespace ramses_internal return true; } - static bool ExpectGitHash(const String& gitHashString, VersionInfo& outVersion) + static bool ExpectGitHash(const std::string& gitHashString, VersionInfo& outVersion) { - UInt idx = 0; + size_t idx = 0; if (!ExpectString(gitHashString, idx, "[GitHash:")) return false; - const UInt hashStart = idx; + const size_t hashStart = idx; if (!ExpectString(gitHashString, idx, "(unknown)") && !ExpectHexString(gitHashString, idx)) return false; @@ -126,25 +127,51 @@ namespace ramses_internal return true; } - bool ReadFromStream(IInputStream& stream, VersionInfo& outVersion) + static bool ExpectFeatureLevel(const std::string& featureLevelString, ramses::EFeatureLevel& outFeatureLevel) + { + size_t idx = 0; + if (!ExpectString(featureLevelString, idx, "[FeatureLevel:")) + return false; + + uint32_t num = 0; + if (!ExpectAndGetNumber(featureLevelString, idx, num)) + return false; + + if (std::find(ramses::AllFeatureLevels.cbegin(), ramses::AllFeatureLevels.cend(), num) == ramses::AllFeatureLevels.cend()) + { + LOG_ERROR(CONTEXT_CLIENT, "RamsesVersion::ReadFromStream: Unknown feature level " << num << " in file, either file corrupt or file exported with future version"); + return false; + } + outFeatureLevel = static_cast(num); + + if (!ExpectString(featureLevelString, idx, "]") || idx != featureLevelString.size()) + return false; + + return true; + } + + bool ReadFromStream(IInputStream& stream, VersionInfo& outVersion, ramses::EFeatureLevel& outFeatureLevel) { - String versionString; - String gitHashString; + std::string versionString; + std::string gitHashString; + std::string featureLevelString; if (!ReadUntilNewline(stream, 100, versionString) || - !ReadUntilNewline(stream, 100, gitHashString)) + !ReadUntilNewline(stream, 100, gitHashString) || + !ReadUntilNewline(stream, 100, featureLevelString)) { LOG_ERROR(CONTEXT_CLIENT, "RamsesVersion::ReadFromStream: Can not read version information, file probably corrupt or invalid"); return false; } if (!ExpectRamsesVersion(versionString, outVersion) || - !ExpectGitHash(gitHashString, outVersion)) + !ExpectGitHash(gitHashString, outVersion) || + !ExpectFeatureLevel(featureLevelString, outFeatureLevel)) return false; return true; } - bool MatchesMajorMinor(UInt32 currentMajor, UInt32 currentMinor, const VersionInfo& in) + bool MatchesMajorMinor(uint32_t currentMajor, uint32_t currentMinor, const VersionInfo& in) { return (in.major == currentMajor && in.minor == currentMinor) || diff --git a/client/ramses-client/impl/RamsesVersion.h b/client/ramses-client/impl/RamsesVersion.h index 90d7b3b7b..57c43b82a 100644 --- a/client/ramses-client/impl/RamsesVersion.h +++ b/client/ramses-client/impl/RamsesVersion.h @@ -9,7 +9,11 @@ #ifndef RAMSES_INTERNAL_RAMSESVERSION_H #define RAMSES_INTERNAL_RAMSESVERSION_H -#include "Collections/String.h" +#include "ramses-framework-api/EFeatureLevel.h" +#include "PlatformAbstraction/PlatformTypes.h" + +#include +#include namespace ramses_internal { @@ -20,15 +24,15 @@ namespace ramses_internal { struct VersionInfo { - String gitHash; - String versionString; - UInt32 major; - UInt32 minor; + std::string gitHash; + std::string versionString; + uint32_t major; + uint32_t minor; }; - void WriteToStream(IOutputStream& stream, const String& versionString, const String& gitHash); - bool ReadFromStream(IInputStream& stream, VersionInfo& outVersion); - bool MatchesMajorMinor(UInt32 currentMajor, UInt32 currentMinor, const VersionInfo& in); + void WriteToStream(IOutputStream& stream, std::string_view versionString, std::string_view gitHash, ramses::EFeatureLevel featureLevel); + bool ReadFromStream(IInputStream& stream, VersionInfo& outVersion, ramses::EFeatureLevel& outFeatureLevel); + bool MatchesMajorMinor(uint32_t currentMajor, uint32_t currentMinor, const VersionInfo& in); } } diff --git a/client/ramses-client/impl/RenderBufferImpl.cpp b/client/ramses-client/impl/RenderBufferImpl.cpp index 3d3a41dae..1f23cd437 100644 --- a/client/ramses-client/impl/RenderBufferImpl.cpp +++ b/client/ramses-client/impl/RenderBufferImpl.cpp @@ -12,8 +12,8 @@ namespace ramses { - RenderBufferImpl::RenderBufferImpl(SceneImpl& scene, const char* name) - : SceneObjectImpl(scene, ERamsesObjectType_RenderBuffer, name) + RenderBufferImpl::RenderBufferImpl(SceneImpl& scene, std::string_view name) + : SceneObjectImpl(scene, ERamsesObjectType::RenderBuffer, name) { } @@ -163,20 +163,20 @@ namespace ramses // explicitly warn about usage of potentially uninitialized buffer if (usedAsTexture && !(usedInRenderPass || usedAsBlitDestination)) { - addValidationMessage(EValidationSeverity_Warning, "RenderBuffer is used in a TextureSampler for reading but is not set as destination in any RenderPass or BlitPass, this can lead to usage of uninitialized data."); + addValidationMessage(EValidationSeverity::Warning, "RenderBuffer is used in a TextureSampler for reading but is not set as destination in any RenderPass or BlitPass, this can lead to usage of uninitialized data."); hasIssue = true; } if (!usedInRenderPass && !usedAsBlitDestination) { hasIssue = true; - addValidationMessage(EValidationSeverity_Warning, "RenderBuffer is not set as destination in any RenderPass or BlitPass, destroy it if not needed."); + addValidationMessage(EValidationSeverity::Warning, "RenderBuffer is not set as destination in any RenderPass or BlitPass, destroy it if not needed."); } if (!usedAsTexture && !usedAsBlitSource && isColorBuffer) // depth/stencil buffer does not need to be validated for usage as texture { hasIssue = true; - addValidationMessage(EValidationSeverity_Warning, "RenderBuffer is neither used in a TextureSampler for reading nor set as source in a BlitPass, destroy it if not needed."); + addValidationMessage(EValidationSeverity::Warning, "RenderBuffer is neither used in a TextureSampler for reading nor set as source in a BlitPass, destroy it if not needed."); } if (hasIssue) @@ -184,7 +184,7 @@ namespace ramses ramses_internal::StringOutputStream rbDesc; const ramses_internal::RenderBuffer& rb = getIScene().getRenderBuffer(m_renderBufferHandle); rbDesc << " [" << rb.width << "x" << rb.height << "; " << ramses_internal::EnumToString(rb.type) << "; " << ramses_internal::EnumToString(rb.format) << "; " << ramses_internal::EnumToString(rb.accessMode) << "; " << rb.sampleCount << " samples]"; - return addValidationMessage(EValidationSeverity_Warning, rbDesc.c_str()); + return addValidationMessage(EValidationSeverity::Warning, rbDesc.c_str()); } return status; diff --git a/client/ramses-client/impl/RenderBufferImpl.h b/client/ramses-client/impl/RenderBufferImpl.h index a45b3374e..b2b613748 100644 --- a/client/ramses-client/impl/RenderBufferImpl.h +++ b/client/ramses-client/impl/RenderBufferImpl.h @@ -17,20 +17,22 @@ // ramses framework #include "SceneAPI/Handles.h" +#include + namespace ramses { class RenderBufferImpl final : public SceneObjectImpl { public: - RenderBufferImpl(SceneImpl& scene, const char* name); - virtual ~RenderBufferImpl() override; + RenderBufferImpl(SceneImpl& scene, std::string_view name); + ~RenderBufferImpl() override; void initializeFrameworkData(uint32_t width, uint32_t height, ERenderBufferType bufferType, ERenderBufferFormat bufferFormat, ERenderBufferAccessMode accessMode, uint32_t sampleCount); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + status_t validate() const override; uint32_t getWidth() const; uint32_t getHeight() const; diff --git a/client/ramses-client/impl/RenderGroupImpl.cpp b/client/ramses-client/impl/RenderGroupImpl.cpp index e0592ce8b..78540ff2b 100644 --- a/client/ramses-client/impl/RenderGroupImpl.cpp +++ b/client/ramses-client/impl/RenderGroupImpl.cpp @@ -22,8 +22,8 @@ namespace ramses { - RenderGroupImpl::RenderGroupImpl(SceneImpl& scene, const char* name) - : SceneObjectImpl(scene, ERamsesObjectType_RenderGroup, name) + RenderGroupImpl::RenderGroupImpl(SceneImpl& scene, std::string_view name) + : SceneObjectImpl(scene, ERamsesObjectType::RenderGroup, name) { } @@ -110,8 +110,8 @@ namespace ramses { status_t status = SceneObjectImpl::validate(); - if (m_meshes.size() == 0u) - status = addValidationMessage(EValidationSeverity_Warning, "rendergroup does not contain any meshes"); + if (m_meshes.empty() && m_renderGroups.empty()) + status = addValidationMessage(EValidationSeverity::Warning, "rendergroup does not contain any meshes"); for (const auto& element : m_meshes) status = std::max(status, addValidationOfDependentObject(*element)); diff --git a/client/ramses-client/impl/RenderGroupImpl.h b/client/ramses-client/impl/RenderGroupImpl.h index 9315d3ee5..968d3add1 100644 --- a/client/ramses-client/impl/RenderGroupImpl.h +++ b/client/ramses-client/impl/RenderGroupImpl.h @@ -11,7 +11,9 @@ #include "SceneObjectImpl.h" #include "SceneAPI/Handles.h" + #include +#include namespace ramses { @@ -24,15 +26,15 @@ namespace ramses class RenderGroupImpl final : public SceneObjectImpl { public: - RenderGroupImpl(SceneImpl& scene, const char* name); - virtual ~RenderGroupImpl() override; + RenderGroupImpl(SceneImpl& scene, std::string_view name); + ~RenderGroupImpl() override; void initializeFrameworkData(); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; + status_t validate() const override; status_t addMeshNode(const MeshNodeImpl& mesh, int32_t orderWithinGroup); status_t remove(const MeshNodeImpl& mesh); diff --git a/client/ramses-client/impl/RenderGroupMeshIterator.cpp b/client/ramses-client/impl/RenderGroupMeshIterator.cpp index fb74dc9f9..78845e39a 100644 --- a/client/ramses-client/impl/RenderGroupMeshIterator.cpp +++ b/client/ramses-client/impl/RenderGroupMeshIterator.cpp @@ -17,22 +17,17 @@ namespace ramses { RenderGroupMeshIterator::RenderGroupMeshIterator(const RenderGroup& renderGroup) - : impl(new IteratorImpl(renderGroup.impl.getAllMeshes())) + : m_impl{ std::make_unique>(renderGroup.m_impl.getAllMeshes()) } { } - RenderGroupMeshIterator::~RenderGroupMeshIterator() - { - delete impl; - } + RenderGroupMeshIterator::~RenderGroupMeshIterator() = default; const MeshNode* RenderGroupMeshIterator::getNext() { - const MeshNodeImpl* meshNode = impl->getNext(); + const MeshNodeImpl* meshNode = m_impl->getNext(); if (meshNode == nullptr) - { return nullptr; - } return &RamsesObjectTypeUtils::ConvertTo(meshNode->getRamsesObject()); } diff --git a/client/ramses-client/impl/RenderPassGroupIterator.cpp b/client/ramses-client/impl/RenderPassGroupIterator.cpp index 5b4f3ffee..d9928e152 100644 --- a/client/ramses-client/impl/RenderPassGroupIterator.cpp +++ b/client/ramses-client/impl/RenderPassGroupIterator.cpp @@ -17,22 +17,17 @@ namespace ramses { RenderPassGroupIterator::RenderPassGroupIterator(const RenderPass& renderPass) - : impl(new IteratorImpl(renderPass.impl.getAllRenderGroups())) + : m_impl{ std::make_unique>(renderPass.m_impl.getAllRenderGroups()) } { } - RenderPassGroupIterator::~RenderPassGroupIterator() - { - delete impl; - } + RenderPassGroupIterator::~RenderPassGroupIterator() = default; const RenderGroup* RenderPassGroupIterator::getNext() { - const RenderGroupImpl* renderGroup = impl->getNext(); + const RenderGroupImpl* renderGroup = m_impl->getNext(); if (renderGroup == nullptr) - { return nullptr; - } return &RamsesObjectTypeUtils::ConvertTo(renderGroup->getRamsesObject()); } diff --git a/client/ramses-client/impl/RenderPassImpl.cpp b/client/ramses-client/impl/RenderPassImpl.cpp index da3f24858..4cf1f2b72 100644 --- a/client/ramses-client/impl/RenderPassImpl.cpp +++ b/client/ramses-client/impl/RenderPassImpl.cpp @@ -19,10 +19,12 @@ #include "Scene/ClientScene.h" +#include + namespace ramses { - RenderPassImpl::RenderPassImpl(SceneImpl& scene, const char* renderpassName) - : SceneObjectImpl(scene, ERamsesObjectType_RenderPass, renderpassName) + RenderPassImpl::RenderPassImpl(SceneImpl& scene, std::string_view renderpassName) + : SceneObjectImpl(scene, ERamsesObjectType::RenderPass, renderpassName) , m_cameraImpl(nullptr) , m_renderTargetImpl(nullptr) { @@ -114,10 +116,10 @@ namespace ramses status_t status = SceneObjectImpl::validate(); if (nullptr == m_cameraImpl) - status = addValidationMessage(EValidationSeverity_Warning, "renderpass does not have a camera set"); + status = addValidationMessage(EValidationSeverity::Warning, "renderpass does not have a camera set"); if (0 == m_renderGroups.size()) - status = addValidationMessage(EValidationSeverity_Warning, "renderpass does not contain any rendergroups"); + status = addValidationMessage(EValidationSeverity::Warning, "renderpass does not contain any rendergroups"); else { for (const auto& renderGroup : m_renderGroups) @@ -127,7 +129,7 @@ namespace ramses if (nullptr != m_renderTargetImpl) status = std::max(status, addValidationOfDependentObject(*m_renderTargetImpl)); else if (getClearFlags() != ramses_internal::EClearFlags_None) - status = std::max(status, addValidationMessage(EValidationSeverity_Warning, "renderpass has clear flags enabled whithout any rendertarget, clear flags will have no effect")); + status = std::max(status, addValidationMessage(EValidationSeverity::Warning, "renderpass has clear flags enabled whithout any rendertarget, clear flags will have no effect")); return status; } @@ -157,9 +159,9 @@ namespace ramses } else { - ramses_internal::String str = "RenderPass::setCamera failed - camera is not valid, maybe camera was not initialized:\n"; - str += cameraImpl.getValidationReport(EValidationSeverity_Warning); - return addErrorEntry(str.c_str()); + std::string str = "RenderPass::setCamera failed - camera is not valid, maybe camera was not initialized:\n"; + str += cameraImpl.getValidationReport(EValidationSeverity::Warning); + return addErrorEntry(str); } return cameraValidity; @@ -181,13 +183,13 @@ namespace ramses return const_cast((const_cast(*this)).getCamera()); } - status_t RenderPassImpl::setClearColor(const ramses_internal::Vector4& clearColor) + status_t RenderPassImpl::setClearColor(const glm::vec4& clearColor) { getIScene().setRenderPassClearColor(m_renderPassHandle, clearColor); return StatusOK; } - const ramses_internal::Vector4& RenderPassImpl::getClearColor() const + const glm::vec4& RenderPassImpl::getClearColor() const { return getIScene().getRenderPass(m_renderPassHandle).clearColor; } diff --git a/client/ramses-client/impl/RenderPassImpl.h b/client/ramses-client/impl/RenderPassImpl.h index b6faeb261..8b5af6196 100644 --- a/client/ramses-client/impl/RenderPassImpl.h +++ b/client/ramses-client/impl/RenderPassImpl.h @@ -11,12 +11,14 @@ #include "SceneObjectImpl.h" #include "SceneAPI/Handles.h" +#include "DataTypesImpl.h" + #include +#include namespace ramses_internal { class IScene; - class Vector4; } namespace ramses @@ -34,22 +36,22 @@ namespace ramses class RenderPassImpl final : public SceneObjectImpl { public: - RenderPassImpl(SceneImpl& scene, const char* renderpassName); - virtual ~RenderPassImpl() override; + RenderPassImpl(SceneImpl& scene, std::string_view renderpassName); + ~RenderPassImpl() override; void initializeFrameworkData(); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t resolveDeserializationDependencies(DeserializationContext& serializationContext) override; + status_t validate() const override; status_t setCamera(const CameraNodeImpl& cameraImpl); const Camera* getCamera()const; Camera* getCamera(); - status_t setClearColor(const ramses_internal::Vector4& clearColor); - const ramses_internal::Vector4& getClearColor() const; + status_t setClearColor(const glm::vec4& clearColor); + const glm::vec4& getClearColor() const; status_t setClearFlags(uint32_t clearFlags); uint32_t getClearFlags() const; diff --git a/client/ramses-client/impl/RenderTargetDescriptionImpl.cpp b/client/ramses-client/impl/RenderTargetDescriptionImpl.cpp index 1fd5a9c80..78b93530d 100644 --- a/client/ramses-client/impl/RenderTargetDescriptionImpl.cpp +++ b/client/ramses-client/impl/RenderTargetDescriptionImpl.cpp @@ -28,14 +28,14 @@ namespace ramses status_t status = StatusObjectImpl::validate(); if (m_renderBuffers.empty()) - status = addValidationMessage(EValidationSeverity_Warning, "there is no RenderBuffer added"); + status = addValidationMessage(EValidationSeverity::Warning, "there is no RenderBuffer added"); else { assert(m_scene != nullptr); for(const auto& rb : m_renderBuffers) { if (!m_scene->getIScene().isRenderBufferAllocated(rb)) - status = addValidationMessage(EValidationSeverity_Error, "referencing one or more RenderBuffers that do not exist in scene anymore"); + status = addValidationMessage(EValidationSeverity::Error, "referencing one or more RenderBuffers that do not exist in scene anymore"); } } diff --git a/client/ramses-client/impl/RenderTargetDescriptionImpl.h b/client/ramses-client/impl/RenderTargetDescriptionImpl.h index 936319075..8cc1d1a08 100644 --- a/client/ramses-client/impl/RenderTargetDescriptionImpl.h +++ b/client/ramses-client/impl/RenderTargetDescriptionImpl.h @@ -21,9 +21,9 @@ namespace ramses { public: RenderTargetDescriptionImpl(); - virtual ~RenderTargetDescriptionImpl() override; + ~RenderTargetDescriptionImpl() override; - virtual status_t validate() const override; + status_t validate() const override; status_t addRenderBuffer(const RenderBufferImpl& renderBuffer); diff --git a/client/ramses-client/impl/RenderTargetImpl.cpp b/client/ramses-client/impl/RenderTargetImpl.cpp index 9ab84e50f..fdcedad0f 100644 --- a/client/ramses-client/impl/RenderTargetImpl.cpp +++ b/client/ramses-client/impl/RenderTargetImpl.cpp @@ -12,8 +12,8 @@ namespace ramses { - RenderTargetImpl::RenderTargetImpl(SceneImpl& scene, const char* name) - : SceneObjectImpl(scene, ERamsesObjectType_RenderTarget, name) + RenderTargetImpl::RenderTargetImpl(SceneImpl& scene, std::string_view name) + : SceneObjectImpl(scene, ERamsesObjectType::RenderTarget, name) , m_renderTargetHandle( ramses_internal::RenderTargetHandle::Invalid() ) { } diff --git a/client/ramses-client/impl/RenderTargetImpl.h b/client/ramses-client/impl/RenderTargetImpl.h index 1f6e57477..922c942dd 100644 --- a/client/ramses-client/impl/RenderTargetImpl.h +++ b/client/ramses-client/impl/RenderTargetImpl.h @@ -15,6 +15,8 @@ // ramses framework #include "SceneAPI/Handles.h" +#include + namespace ramses { class RenderTargetDescriptionImpl; @@ -22,13 +24,13 @@ namespace ramses class RenderTargetImpl final : public SceneObjectImpl { public: - RenderTargetImpl(SceneImpl& scene, const char* name); - virtual ~RenderTargetImpl() override; + RenderTargetImpl(SceneImpl& scene, std::string_view name); + ~RenderTargetImpl() override; void initializeFrameworkData(const RenderTargetDescriptionImpl& rtDesc); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; uint32_t getWidth() const; uint32_t getHeight() const; diff --git a/client/ramses-client/impl/ResourceDataPoolImpl.cpp b/client/ramses-client/impl/ResourceDataPoolImpl.cpp deleted file mode 100644 index f0869a694..000000000 --- a/client/ramses-client/impl/ResourceDataPoolImpl.cpp +++ /dev/null @@ -1,419 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ResourceDataPoolImpl.h" - -#include "Collections/IInputStream.h" -#include "ramses-client-api/Resource.h" -#include "ramses-client-api/ArrayResource.h" -#include "ramses-client-api/Texture2D.h" -#include "ramses-client-api/Texture3D.h" -#include "ramses-client-api/TextureCube.h" -#include "ramses-client-api/Effect.h" - -#include "Utils/LogMacros.h" -#include "Components/ResourceTableOfContents.h" -#include "SerializationContext.h" -#include "RamsesClientImpl.h" -#include "ResourceImpl.h" -#include "RamsesClientTypesImpl.h" -#include "Components/FileInputStreamContainer.h" - -namespace ramses -{ - class Texture3DImpl; - class TextureCubeImpl; - - ResourceDataPoolImpl::ResourceDataPoolImpl(RamsesClientImpl& client) - : m_client(client) - { - } - - resourceId_t ResourceDataPoolImpl::addResourceDataToPool(ramses_internal::ManagedResource const& res, const char* name, ERamsesObjectType type) - { - if (!res) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::addResourceDataToPool: couldn't create managed resource for type " << type); - return {}; - } - - auto id = ResourceImpl::CreateResourceHash(res->getHash(), name, type); - - auto it = m_resourcePoolData.find(id); - if (it != m_resourcePoolData.end()) - { - assert(res == it->value.managedResource); - assert(it->value.refCount > 0); - it->value.refCount++; - } - else - m_resourcePoolData.put(id, { res, name ? name : "", 1 }); - - return id; - } - - resourceId_t ResourceDataPoolImpl::addArrayResourceData(EDataType type, uint32_t numElements, const void* arrayData, resourceCacheFlag_t cacheFlag, const char* name) - { - auto res = m_client.createManagedArrayResource(numElements, type, arrayData, cacheFlag, name); - return addResourceDataToPool(res, name, ERamsesObjectType_ArrayResource); - } - - ramses::resourceId_t ResourceDataPoolImpl::addTexture2DData(ETextureFormat format, uint32_t width, uint32_t height, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name) - { - auto res = m_client.createManagedTexture(ramses_internal::EResourceType_Texture2D, width, height, 1u, format, mipMapCount, mipLevelData, generateMipChain, swizzle, cacheFlag, name); - return addResourceDataToPool(res, name, ERamsesObjectType_Texture2D); - } - - ramses::resourceId_t ResourceDataPoolImpl::addTexture3DData(ETextureFormat format, uint32_t width, uint32_t height, uint32_t depth, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, resourceCacheFlag_t cacheFlag, const char* name) - { - auto res = m_client.createManagedTexture(ramses_internal::EResourceType_Texture3D, width, height, depth, format, mipMapCount, mipLevelData, generateMipChain, {}, cacheFlag, name); - return addResourceDataToPool(res, name, ERamsesObjectType_Texture3D); - } - - ramses::resourceId_t ResourceDataPoolImpl::addTextureCubeData(ETextureFormat format, uint32_t size, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name) - { - auto res = m_client.createManagedTexture(ramses_internal::EResourceType_TextureCube, size, 1u, 1u, format, mipMapCount, mipLevelData, generateMipChain, swizzle, cacheFlag, name); - return addResourceDataToPool(res, name, ERamsesObjectType_TextureCube); - } - - ramses::resourceId_t ResourceDataPoolImpl::addEffectData(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, const char* name) - { - auto res = m_client.createManagedEffect(effectDesc, cacheFlag, name, m_effectErrorMessages); - return addResourceDataToPool(res, name, ERamsesObjectType_Effect); - } - - std::string ResourceDataPoolImpl::getLastEffectErrorMessages() const - { - return m_effectErrorMessages; - } - - bool ResourceDataPoolImpl::removeResourceData(resourceId_t const& id) - { - auto it = m_resourcePoolData.find(id); - if (it != m_resourcePoolData.end()) - { - assert(it->value.refCount > 0); - if (--it->value.refCount == 0) - m_resourcePoolData.remove(it); - - return true; - } - else - return false; - } - - bool ResourceDataPoolImpl::addResourceDataFile(std::string const& filename) - { - LOG_INFO(CONTEXT_CLIENT, "ResourceDataPool::addResourceDataFile '" << filename << "'"); - - if (m_filenameToHandle.find(filename) != m_filenameToHandle.end()) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::addResourceDataFile '" << filename << "' failed, resource file has been already added."); - return false; - } - - ramses_internal::InputStreamContainerSPtr resourceFileStream = std::make_shared(ramses_internal::String(filename)); - ramses_internal::IInputStream& inputStream = resourceFileStream->getStream(); - if (inputStream.getState() != ramses_internal::EStatus::Ok) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::addResourceDataFile '" << filename << "' failed, file could not be opened."); - return false; - } - - const ramses_internal::String fileInfo = ramses_internal::String("resource data file '") + filename + ramses_internal::String("'"); - if (!RamsesClientImpl::ReadRamsesVersionAndPrintWarningOnMismatch(inputStream, fileInfo)) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::addResourceDataFile '" << filename << "' failed, file is invalid"); - return false; - } - - uint64_t offsetForHLToc = 0; - uint64_t offsetForLLResourceBlock = 0; - inputStream >> offsetForHLToc; - inputStream >> offsetForLLResourceBlock; - - // register resource file for on-demand loading (LL-Resources) - inputStream.seek(static_cast(offsetForLLResourceBlock), ramses_internal::IInputStream::Seek::FromBeginning); - ramses_internal::ResourceTableOfContents loadedTOC; - if (!loadedTOC.readTOCPosAndTOCFromStream(inputStream)) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::addResourceDataFile '" << filename << "' failed, file did not contain valid resource data"); - return false; - } - - const ramses_internal::SceneFileHandle fileHandle = m_client.getClientApplication().addResourceFile(resourceFileStream, loadedTOC); - m_filenameToHandle.put(filename, fileHandle); - - LOG_INFO_P(CONTEXT_CLIENT, "ResourceDataPool::addResourceDataFile: Filename '{}' has handle {}", filename, fileHandle); - - // read HL Resources - inputStream.seek(static_cast(offsetForHLToc), ramses_internal::IInputStream::Seek::FromBeginning); - - uint64_t totalCount = 0u; - inputStream >> totalCount; - - for (uint64_t i = 0; i < totalCount; ++i) - { - resourceId_t id; - uint64_t offset; - inputStream >> id.highPart >> id.lowPart >> offset; - - m_resourceFileAddressRegister[id].push_back({ resourceFileStream, offset, fileHandle }); - m_resourceDataFileContent[filename].push_back(id); - } - - return true; - } - - bool ResourceDataPoolImpl::forceLoadResourcesFromResourceDataFile(std::string const& filename) - { - LOG_INFO(CONTEXT_CLIENT, "ResourceDataPool::forceLoadResourcesFromResourceDataFile '" << filename << "'"); - - const auto it = m_filenameToHandle.find(filename); - if (it == m_filenameToHandle.end()) - return false; - m_client.getClientApplication().loadResourceFromFile(it->value); - return true; - } - - bool ResourceDataPoolImpl::removeResourceDataFile(std::string const& filename) - { - LOG_INFO(CONTEXT_CLIENT, "ResourceDataPool::removeResourceDataFile '" << filename << "'"); - - const auto handleIt = m_filenameToHandle.find(filename); - if (handleIt == m_filenameToHandle.end()) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::removeResourceDataFile: file '" << filename << "' is unknown and can't be closed."); - return false; - } - const ramses_internal::SceneFileHandle fileHandle = handleIt->value; - - // delete from file content map - auto fileIt = m_resourceDataFileContent.find(filename); - auto resources = std::move(fileIt->value); - m_resourceDataFileContent.remove(fileIt); - - for (auto const& id : resources) - { - auto it = m_resourceFileAddressRegister.find(id); - if (it != m_resourceFileAddressRegister.end()) - { - auto& addresses = it->value; - auto address = std::remove_if(addresses.begin(), addresses.end(), - [&fileHandle](auto const& entry) { - return entry.fileHandle == fileHandle; - }); - addresses.erase(address, addresses.end()); - - if (addresses.empty()) - m_resourceFileAddressRegister.remove(it); - } - } - - m_client.getClientApplication().removeResourceFile(handleIt->value); - m_filenameToHandle.remove(handleIt); - - return true; - } - - bool ResourceDataPoolImpl::saveResourceDataFile(std::string const& filename, ResourceObjects const& resources, bool compress) const - { - LOG_INFO(ramses_internal::CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: " << filename << " numResources:" << resources.size()); - - if (m_filenameToHandle.find(filename) != m_filenameToHandle.end()) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: Cannot write resources to file '" << filename << "', its already opened by the resource data pool."); - return false; - } - - ramses_internal::File resourceDataFileOut(filename.c_str()); - ramses_internal::BinaryFileOutputStream resourceDataFileOutStream(resourceDataFileOut); - if (!resourceDataFileOut.isOpen()) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: Could not open file '" << filename << "' for writing resources"); - return false; - } - - RamsesClientImpl::WriteCurrentBuildVersionToStream(resourceDataFileOutStream); - - ramses_internal::UInt offsetDataStart = 0; - if (!resourceDataFileOut.getPos(offsetDataStart)) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: File handling error."); - return false; - } - - // calculate offsets for hl toc and resources - const uint64_t offsetHLTocStart = offsetDataStart + sizeof(uint64_t) * 2u; - const uint64_t offsetHLResourcesStart = - offsetHLTocStart + // start of toc - sizeof(uint64_t) + // total resource count - resources.size() * (sizeof(resourceId_t) + sizeof(uint64_t)); // number of entries with (resourceId_t and offset) - - if (!resourceDataFileOut.seek(static_cast(offsetHLResourcesStart), ramses_internal::File::SeekOrigin::BeginningOfFile)) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: File handling error."); - return false; - } - - std::vector> fileContent; - SerializationContext serializationContext; - serializationContext.serializeSceneObjectIds(false); // we take the resources out of the context of its scene, so don't write sceneObjectIds - for (const auto& res : resources) - { - ramses_internal::UInt filepos = 0; - if (!resourceDataFileOut.getPos(filepos)) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: File handling error."); - return false; - } - - fileContent.push_back({ res->getResourceId(), static_cast(filepos) }); - - resourceDataFileOutStream << static_cast(res->getType()); - if (StatusOK != res->impl.serialize(resourceDataFileOutStream, serializationContext)) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: Error serializing resource " << res->getResourceId().highPart << ":" << res->getResourceId().lowPart); - return false; - } - } - - ramses_internal::UInt offsetLLResourcesStart = 0; - if (!resourceDataFileOut.getPos(offsetLLResourcesStart)) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: File handling error."); - return false; - } - - m_client.writeLowLevelResourcesToStream(resources, resourceDataFileOutStream, compress); - - if (!resourceDataFileOut.seek(offsetDataStart, ramses_internal::File::SeekOrigin::BeginningOfFile)) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: File handling error."); - return false; - } - - resourceDataFileOutStream << static_cast(offsetHLTocStart); - resourceDataFileOutStream << static_cast(offsetLLResourcesStart); - resourceDataFileOutStream << static_cast(fileContent.size()); - for (auto const& entry : fileContent) - resourceDataFileOutStream << entry.first.highPart << entry.first.lowPart << entry.second; - - ramses_internal::UInt check = 0; - if (!resourceDataFileOut.getPos(check)) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: File handling error."); - return false; - } - - if (check != offsetHLResourcesStart) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: Error writing resource data to file. Table of content does not fit before hl resources"); - return false; - } - - if (!resourceDataFileOut.close()) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::saveResourceDataFile: Could not close resource file"); - return false; - } - - return true; - } - - ramses::Resource* ResourceDataPoolImpl::createResourceForScene(Scene& scene, resourceId_t const& id) - { - if (scene.getResource(id)) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::createResourceForScene failed, resource already exists for scene."); - return nullptr; - } - - ramses::Resource* ret = nullptr; - auto poolData = m_resourcePoolData.get(id); - auto addresses = m_resourceFileAddressRegister.get(id); - - if (poolData) // in case resource exists in both data and file we prefer the cheaper pool data variant - ret = createResourceForScene(scene, *poolData); - - if (!ret && addresses && !addresses->empty()) - ret = createResourceForScene(scene, addresses->front(), id); - - if (!ret) - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::createResourceForScene: Resource " << id << " not found in resource data pool"); - - assert(!ret || ret->getResourceId() == id); - return ret; - } - - ramses::Resource* ResourceDataPoolImpl::createResourceForScene(Scene& scene, ResourceFileAddress const& address, resourceId_t const& id) const - { - ramses_internal::IInputStream& inputStream = address.file->getStream(); - if (inputStream.seek(static_cast(address.offset), ramses_internal::IInputStream::Seek::FromBeginning) != ramses_internal::EStatus::Ok) - { - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::createResourceForScene: Error accessing file for Resource " << id); - return nullptr; - } - - uint64_t type; - inputStream >> type; - - status_t status = StatusOK; - DeserializationContext context; - switch (ERamsesObjectType(type)) - { - case ERamsesObjectType_Effect: - status = scene.impl.createAndDeserializeObjectImpls(inputStream, context, 1); - break; - case ERamsesObjectType_Texture2D: - status = scene.impl.createAndDeserializeObjectImpls(inputStream, context, 1); - break; - case ERamsesObjectType_Texture3D: - status = scene.impl.createAndDeserializeObjectImpls(inputStream, context, 1); - break; - case ERamsesObjectType_TextureCube: - status = scene.impl.createAndDeserializeObjectImpls(inputStream, context, 1); - break; - case ERamsesObjectType_ArrayResource: - status = scene.impl.createAndDeserializeObjectImpls(inputStream, context, 1); - break; - default: - LOG_ERROR(CONTEXT_CLIENT, "ResourceDataPool::createResourceForScene failed, unexpected object type in file stream " << type); - } - - return status == StatusOK ? scene.getResource(id) : nullptr; - } - - ramses::Resource* ResourceDataPoolImpl::createResourceForScene(Scene& scene, ResourceData const& poolData) const - { - switch (poolData.managedResource->getTypeID()) - { - case ramses_internal::EResourceType_VertexArray: - case ramses_internal::EResourceType_IndexArray: - return scene.impl.createHLArrayResource(poolData.managedResource, poolData.name.c_str()); - case ramses_internal::EResourceType_Texture2D: - return scene.impl.createHLTexture2D(poolData.managedResource, poolData.name.c_str()); - case ramses_internal::EResourceType_Texture3D: - return scene.impl.createHLTexture3D(poolData.managedResource, poolData.name.c_str()); - case ramses_internal::EResourceType_TextureCube: - return scene.impl.createHLTextureCube(poolData.managedResource, poolData.name.c_str()); - case ramses_internal::EResourceType_Effect: - return scene.impl.createHLEffect(poolData.managedResource, poolData.name.c_str()); - default: - assert(0); - return nullptr; - } - } - - void ResourceDataPoolImpl::getAllResourceDataFileResourceIds(std::vector& resources) const - { - resources.reserve(m_resourceFileAddressRegister.size()); - for (auto const& entry : m_resourceFileAddressRegister) - resources.push_back(entry.key); - } -} diff --git a/client/ramses-client/impl/ResourceDataPoolImpl.h b/client/ramses-client/impl/ResourceDataPoolImpl.h deleted file mode 100644 index 68e58bd11..000000000 --- a/client/ramses-client/impl/ResourceDataPoolImpl.h +++ /dev/null @@ -1,90 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_RESOURCEDATAPOOLIMPL_H -#define RAMSES_RESOURCEDATAPOOLIMPL_H - -#include "ramses-client-api/EDataType.h" -#include "ramses-client-api/MipLevelData.h" -#include "ramses-client-api/TextureEnums.h" -#include "ramses-client-api/TextureSwizzle.h" -#include "ramses-client-api/RamsesObjectTypes.h" - -#include "ramses-framework-api/RamsesFrameworkTypes.h" - -#include "Components/InputStreamContainer.h" -#include "Components/SceneFileHandle.h" -#include "Collections/HashMap.h" -#include "ResourceObjects.h" -#include "Components/ManagedResource.h" - -#include -#include - -namespace ramses -{ - class EffectDescription; - class Resource; - class Scene; - class RamsesClientImpl; - - class ResourceDataPoolImpl - { - public: - explicit ResourceDataPoolImpl(RamsesClientImpl& client); - - resourceId_t addArrayResourceData(EDataType type, uint32_t numElements, const void* arrayData, resourceCacheFlag_t cacheFlag, const char* name); - resourceId_t addTexture2DData(ETextureFormat format, uint32_t width, uint32_t height, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name); - resourceId_t addTexture3DData(ETextureFormat format, uint32_t width, uint32_t height, uint32_t depth, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, resourceCacheFlag_t cacheFlag, const char* name); - resourceId_t addTextureCubeData(ETextureFormat format, uint32_t size, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name); - resourceId_t addEffectData(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, const char* name); - - std::string getLastEffectErrorMessages() const; - - bool removeResourceData(resourceId_t const& id); - - bool addResourceDataFile(std::string const& filename); - bool forceLoadResourcesFromResourceDataFile(std::string const& filename); - bool removeResourceDataFile(std::string const& filename); - bool saveResourceDataFile(std::string const& filename, ResourceObjects const& resources, bool compress) const; - - Resource* createResourceForScene(Scene& scene, resourceId_t const& id); - - void getAllResourceDataFileResourceIds(std::vector& resources) const; - - private: - struct ResourceData - { - ramses_internal::ManagedResource managedResource; - std::string name; - int refCount = 0; - }; - - struct ResourceFileAddress - { - ramses_internal::InputStreamContainerSPtr file; - uint64_t offset; - ramses_internal::SceneFileHandle fileHandle; - }; - - ramses::Resource* createResourceForScene(Scene& scene, ResourceFileAddress const& address, resourceId_t const& id) const; - ramses::Resource* createResourceForScene(Scene& scene, ResourceData const& poolData) const; - - resourceId_t addResourceDataToPool(ramses_internal::ManagedResource const& res, const char* name, ERamsesObjectType type); - - RamsesClientImpl& m_client; - std::string m_effectErrorMessages; - - ramses_internal::HashMap> m_resourceFileAddressRegister; - ramses_internal::HashMap> m_resourceDataFileContent; - ramses_internal::HashMap m_resourcePoolData; - ramses_internal::HashMap m_filenameToHandle; - }; -} - -#endif diff --git a/client/ramses-client/impl/ResourceImpl.cpp b/client/ramses-client/impl/ResourceImpl.cpp index 1be903212..da36203db 100644 --- a/client/ramses-client/impl/ResourceImpl.cpp +++ b/client/ramses-client/impl/ResourceImpl.cpp @@ -18,18 +18,19 @@ #include "Components/ManagedResource.h" #include "Collections/StringOutputStream.h" -#include "Utils/StringUtils.h" #include "Utils/BinaryOutputStream.h" #include "RamsesObjectTypeUtils.h" #include "city.h" #include "Utils/LogMacros.h" +#include + namespace ramses { ResourceImpl::ResourceImpl(ERamsesObjectType type, ramses_internal::ResourceHashUsage hashUsage, SceneImpl& scene, - const char* name) + std::string_view name) : SceneObjectImpl(scene, type, name) , m_hashUsage(std::move(hashUsage)) { @@ -50,7 +51,7 @@ namespace ramses return m_resourceId; } - resourceId_t ResourceImpl::CreateResourceHash(ramses_internal::ResourceContentHash llhash, ramses_internal::String const& name, ERamsesObjectType type) + resourceId_t ResourceImpl::CreateResourceHash(ramses_internal::ResourceContentHash llhash, const std::string& name, ERamsesObjectType type) { resourceId_t hash; @@ -114,11 +115,11 @@ namespace ramses ramses_internal::StringOutputStream stringStream; stringStream << "Resource ID: " << m_resourceId; stringStream << " Resource Hash: " << m_hashUsage.getHash(); - addValidationMessage(EValidationSeverity_Info, ramses_internal::String{ stringStream.release() }); + addValidationMessage(EValidationSeverity::Info, stringStream.release()); return status; } - status_t ResourceImpl::setName(RamsesObject& object, const char* name) + status_t ResourceImpl::setName(RamsesObject& object, std::string_view name) { const status_t status = SceneObjectImpl::setName(object, name); diff --git a/client/ramses-client/impl/ResourceImpl.h b/client/ramses-client/impl/ResourceImpl.h index d3661d8c0..57f0fd66e 100644 --- a/client/ramses-client/impl/ResourceImpl.h +++ b/client/ramses-client/impl/ResourceImpl.h @@ -17,6 +17,8 @@ #include "Components/ManagedResource.h" #include "Components/ResourceHashUsage.h" +#include +#include namespace ramses_internal { @@ -31,20 +33,20 @@ namespace ramses ResourceImpl(ERamsesObjectType type, ramses_internal::ResourceHashUsage hashUsage, SceneImpl& scene, - const char* name); - virtual ~ResourceImpl() override; + std::string_view name); + ~ResourceImpl() override; - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; resourceId_t getResourceId() const; ramses_internal::ResourceContentHash getLowlevelResourceHash() const; - virtual status_t validate() const override; - virtual status_t setName(RamsesObject& object, const char* name) override; + status_t validate() const override; + status_t setName(RamsesObject& object, std::string_view name) override; - static resourceId_t CreateResourceHash(ramses_internal::ResourceContentHash llhash, ramses_internal::String const& name, ERamsesObjectType type); + static resourceId_t CreateResourceHash(ramses_internal::ResourceContentHash llhash, const std::string& name, ERamsesObjectType type); private: void updateResourceHash(); diff --git a/client/ramses-client/impl/RotationConventionUtils.h b/client/ramses-client/impl/RotationConventionUtils.h deleted file mode 100644 index dcd9511f1..000000000 --- a/client/ramses-client/impl/RotationConventionUtils.h +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_CLIENT_ROTATIONCONVENTIONUTILS_H -#define RAMSES_CLIENT_ROTATIONCONVENTIONUTILS_H - -#include "ramses-client-api/ERotationConvention.h" -#include "SceneAPI/ERotationConvention.h" -#include - -namespace ramses -{ - namespace RotationConventionUtils - { - inline ERotationConvention ConvertDataTypeFromInternal(ramses_internal::ERotationConvention convention) - { - assert(convention != ramses_internal::ERotationConvention::Legacy_ZYX); - - switch (convention) - { - case ramses_internal::ERotationConvention::XYZ: return ERotationConvention::XYZ; - case ramses_internal::ERotationConvention::XZY: return ERotationConvention::XZY; - case ramses_internal::ERotationConvention::YXZ: return ERotationConvention::YXZ; - case ramses_internal::ERotationConvention::YZX: return ERotationConvention::YZX; - case ramses_internal::ERotationConvention::ZXY: return ERotationConvention::ZXY; - case ramses_internal::ERotationConvention::ZYX: return ERotationConvention::ZYX; - case ramses_internal::ERotationConvention::XYX: return ERotationConvention::XYX; - case ramses_internal::ERotationConvention::XZX: return ERotationConvention::XZX; - case ramses_internal::ERotationConvention::YXY: return ERotationConvention::YXY; - case ramses_internal::ERotationConvention::YZY: return ERotationConvention::YZY; - case ramses_internal::ERotationConvention::ZXZ: return ERotationConvention::ZXZ; - case ramses_internal::ERotationConvention::ZYZ: return ERotationConvention::ZYZ; - default: - assert(false); - return ERotationConvention::ZYZ; - } - } - - inline ramses_internal::ERotationConvention ConvertRotationConventionToInternal(ERotationConvention convention) - { - switch (convention) - { - case ERotationConvention::XYZ: return ramses_internal::ERotationConvention::XYZ; - case ERotationConvention::XZY: return ramses_internal::ERotationConvention::XZY; - case ERotationConvention::YXZ: return ramses_internal::ERotationConvention::YXZ; - case ERotationConvention::YZX: return ramses_internal::ERotationConvention::YZX; - case ERotationConvention::ZXY: return ramses_internal::ERotationConvention::ZXY; - case ERotationConvention::ZYX: return ramses_internal::ERotationConvention::ZYX; - case ERotationConvention::XYX: return ramses_internal::ERotationConvention::XYX; - case ERotationConvention::XZX: return ramses_internal::ERotationConvention::XZX; - case ERotationConvention::YXY: return ramses_internal::ERotationConvention::YXY; - case ERotationConvention::YZY: return ramses_internal::ERotationConvention::YZY; - case ERotationConvention::ZXZ: return ramses_internal::ERotationConvention::ZXZ; - case ERotationConvention::ZYZ: return ramses_internal::ERotationConvention::ZYZ; - } - - assert(false); - return ramses_internal::ERotationConvention::ZYZ; - } - }; -} - -#endif diff --git a/client/ramses-client/impl/RotationTypeUtils.h b/client/ramses-client/impl/RotationTypeUtils.h new file mode 100644 index 000000000..6d1387c1a --- /dev/null +++ b/client/ramses-client/impl/RotationTypeUtils.h @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2020 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#pragma once + +#include "ramses-client-api/ERotationType.h" +#include "SceneAPI/ERotationType.h" +#include + +namespace ramses +{ + namespace RotationTypeUtils + { + inline ERotationType ConvertRotationTypeFromInternal(ramses_internal::ERotationType rotationType) + { + switch (rotationType) + { + case ramses_internal::ERotationType::Euler_ZYX: return ERotationType::Euler_ZYX; + case ramses_internal::ERotationType::Euler_YZX: return ERotationType::Euler_YZX; + case ramses_internal::ERotationType::Euler_ZXY: return ERotationType::Euler_ZXY; + case ramses_internal::ERotationType::Euler_XZY: return ERotationType::Euler_XZY; + case ramses_internal::ERotationType::Euler_YXZ: return ERotationType::Euler_YXZ; + case ramses_internal::ERotationType::Euler_XYZ: return ERotationType::Euler_XYZ; + case ramses_internal::ERotationType::Euler_XYX: return ERotationType::Euler_XYX; + case ramses_internal::ERotationType::Euler_XZX: return ERotationType::Euler_XZX; + case ramses_internal::ERotationType::Euler_YXY: return ERotationType::Euler_YXY; + case ramses_internal::ERotationType::Euler_YZY: return ERotationType::Euler_YZY; + case ramses_internal::ERotationType::Euler_ZXZ: return ERotationType::Euler_ZXZ; + case ramses_internal::ERotationType::Euler_ZYZ: return ERotationType::Euler_ZYZ; + case ramses_internal::ERotationType::Quaternion: return ERotationType::Quaternion; + } + + assert(false); + return ERotationType::Euler_XYZ; + } + + inline ramses_internal::ERotationType ConvertRotationTypeToInternal(ERotationType rotationType) + { + switch (rotationType) + { + case ERotationType::Euler_ZYX: return ramses_internal::ERotationType::Euler_ZYX; + case ERotationType::Euler_YZX: return ramses_internal::ERotationType::Euler_YZX; + case ERotationType::Euler_ZXY: return ramses_internal::ERotationType::Euler_ZXY; + case ERotationType::Euler_XZY: return ramses_internal::ERotationType::Euler_XZY; + case ERotationType::Euler_YXZ: return ramses_internal::ERotationType::Euler_YXZ; + case ERotationType::Euler_XYZ: return ramses_internal::ERotationType::Euler_XYZ; + case ERotationType::Euler_XYX: return ramses_internal::ERotationType::Euler_XYX; + case ERotationType::Euler_XZX: return ramses_internal::ERotationType::Euler_XZX; + case ERotationType::Euler_YXY: return ramses_internal::ERotationType::Euler_YXY; + case ERotationType::Euler_YZY: return ramses_internal::ERotationType::Euler_YZY; + case ERotationType::Euler_ZXZ: return ramses_internal::ERotationType::Euler_ZXZ; + case ERotationType::Euler_ZYZ: return ramses_internal::ERotationType::Euler_ZYZ; + case ERotationType::Quaternion: return ramses_internal::ERotationType::Quaternion; + } + + assert(false); + return ramses_internal::ERotationType::Euler_ZYZ; + } + }; +} + diff --git a/client/ramses-client/impl/SceneConfigImpl.h b/client/ramses-client/impl/SceneConfigImpl.h index f7464dd8c..8f552c5d5 100644 --- a/client/ramses-client/impl/SceneConfigImpl.h +++ b/client/ramses-client/impl/SceneConfigImpl.h @@ -23,7 +23,7 @@ namespace ramses EScenePublicationMode getPublicationMode() const; private: - EScenePublicationMode m_publicationMode = EScenePublicationMode_LocalAndRemote; + EScenePublicationMode m_publicationMode = EScenePublicationMode::LocalAndRemote; }; } diff --git a/client/ramses-client/impl/SceneDumper.cpp b/client/ramses-client/impl/SceneDumper.cpp index 6bdfc0103..2868c0ed7 100644 --- a/client/ramses-client/impl/SceneDumper.cpp +++ b/client/ramses-client/impl/SceneDumper.cpp @@ -15,7 +15,6 @@ #include "ramses-client-api/TextureSamplerMS.h" #include "ramses-client-api/TextureSamplerExternal.h" #include "ramses-client-api/TextureSampler.h" -#include "ramses-client-api/StreamTexture.h" #include "ramses-client-api/RenderTarget.h" #include "ramses-client-api/RenderBuffer.h" #include "ramses-client-api/RenderGroup.h" @@ -32,7 +31,6 @@ #include "GeometryBindingImpl.h" #include "Texture2DBufferImpl.h" #include "ObjectIteratorImpl.h" -#include "StreamTextureImpl.h" #include "RamsesClientImpl.h" #include "RenderBufferImpl.h" #include "RenderTargetImpl.h" @@ -68,11 +66,6 @@ namespace ramses return impl.getTextureBufferHandle(); } - template <> ramses_internal::StreamTextureHandle SceneDumper::GetHandle(const StreamTextureImpl& impl) - { - return impl.getHandle(); - } - template void SceneDumper::setupMap(ramses_internal::HashMap& map) { @@ -86,7 +79,7 @@ namespace ramses RamsesObjectRegistryIterator iterator(m_objectRegistry, TYPE_ID_OF_RAMSES_OBJECT::ID); while (const ObjectType* object = iterator.getNext()) { - map.put(GetHandle(object->impl), &object->impl); + map.put(GetHandle(object->m_impl), &object->m_impl); } } @@ -99,9 +92,6 @@ namespace ramses void SceneDumper::dumpUnrequiredObjects(ramses_internal::StringOutputStream& output) { setupMaps(); - markAllObjectsOfTypeAsRequired(ERamsesObjectType_AnimationObject); - markAllObjectsOfTypeAsRequired(ERamsesObjectType_AnimationSystem); - markAllObjectsOfTypeAsRequired(ERamsesObjectType_AnimationSystemRealTime); RenderPassSet requiredRenderPasses = markRequiredScreenRenderPasses(); while (requiredRenderPasses.size() > 0) @@ -122,7 +112,6 @@ namespace ramses markRequiredRenderTargets(requiredRenderPasses); RenderBufferSet requiredRenderBuffers = markRequiredRenderBuffer(requiredTextureSamplers); markRequiredTextureBuffer(requiredTextureSamplers); - markRequiredStreamTextures(requiredTextureSamplers); requiredRenderPasses = getRequiredRenderPasses(requiredRenderBuffers); } @@ -142,7 +131,6 @@ namespace ramses addToMap(m_textureSamplerHandleToObjectMap); setupMap(m_renderBufferHandleToObjectMap); setupMap(m_textureBufferHandleToObjectMap); - setupMap(m_streamTextureHandleToObjectMap); setupResourceMap(); setupRenderPassMap(); @@ -153,11 +141,11 @@ namespace ramses { m_resourceContentHashToObjectMap.clear(); - ObjectIteratorImpl iter(m_objectRegistry, ERamsesObjectType_Resource); + ObjectIteratorImpl iter(m_objectRegistry, ERamsesObjectType::Resource); auto resourceAsObject = iter.getNext(); while (resourceAsObject) { - const ResourceImpl* resource = &RamsesObjectTypeUtils::ConvertTo(*resourceAsObject).impl; + const ResourceImpl* resource = &RamsesObjectTypeUtils::ConvertTo(*resourceAsObject).m_impl; m_resourceContentHashToObjectMap.put(resource->getLowlevelResourceHash(), resource); resourceAsObject = iter.getNext(); } @@ -166,16 +154,16 @@ namespace ramses void SceneDumper::setupRenderPassMap() { m_destinationRenderBufferHandleToRenderPassSetMap.clear(); - RamsesObjectRegistryIterator renderPassIterator(m_objectRegistry, ERamsesObjectType_RenderPass); + RamsesObjectRegistryIterator renderPassIterator(m_objectRegistry, ERamsesObjectType::RenderPass); while (const RenderPass* renderPass = renderPassIterator.getNext()) { const RenderTarget* renderTarget = renderPass->getRenderTarget(); if (nullptr != renderTarget) { - const ramses_internal::ClientScene& clientScene = renderTarget->impl.getIScene(); + const ramses_internal::ClientScene& clientScene = renderTarget->m_impl.getIScene(); const ramses_internal::RenderTargetHandle renderTargetHandle = - renderTarget->impl.getRenderTargetHandle(); + renderTarget->m_impl.getRenderTargetHandle(); uint32_t renderBufferCount = clientScene.getRenderTargetRenderBufferCount(renderTargetHandle); for (uint32_t i = 0; i < renderBufferCount; i++) @@ -183,7 +171,7 @@ namespace ramses const ramses_internal::RenderBufferHandle renderBufferHandle = clientScene.getRenderTargetRenderBuffer(renderTargetHandle, i); - m_destinationRenderBufferHandleToRenderPassSetMap[renderBufferHandle].put(&renderPass->impl); + m_destinationRenderBufferHandleToRenderPassSetMap[renderBufferHandle].put(&renderPass->m_impl); } } } @@ -192,11 +180,11 @@ namespace ramses void SceneDumper::setupRenderBufferSetMap() { m_blitPassDestinationRenderBufferToSourceRenderBuffersMap.clear(); - RamsesObjectRegistryIterator blitPassIterator(m_objectRegistry, ERamsesObjectType_BlitPass); + RamsesObjectRegistryIterator blitPassIterator(m_objectRegistry, ERamsesObjectType::BlitPass); while (const BlitPass* blitPass = blitPassIterator.getNext()) { - const ramses_internal::ClientScene& clientScene = blitPass->impl.getIScene(); - const ramses_internal::BlitPassHandle blitPassHandle = blitPass->impl.getBlitPassHandle(); + const ramses_internal::ClientScene& clientScene = blitPass->m_impl.getIScene(); + const ramses_internal::BlitPassHandle blitPassHandle = blitPass->m_impl.getBlitPassHandle(); const ramses_internal::BlitPass& blitPassData = clientScene.getBlitPass(blitPassHandle); const RenderBufferImpl** sourceRenderBuffer = m_renderBufferHandleToObjectMap.get(blitPassData.sourceRenderBuffer); @@ -215,16 +203,6 @@ namespace ramses } } - void SceneDumper::markAllObjectsOfTypeAsRequired(ERamsesObjectType objectType) - { - RamsesObjectVector objects; - m_objectRegistry.getObjectsOfType(objects, objectType); - for (auto object : objects) - { - m_requiredObjects.put(&object->impl); - } - } - bool SceneDumper::addToRequiredObjects(const RamsesObjectImpl& object) { if (!m_requiredObjects.contains(&object)) @@ -242,14 +220,14 @@ namespace ramses { RenderPassSet requiredRenderPasses; - RamsesObjectRegistryIterator objectIterator(m_objectRegistry, ERamsesObjectType_RenderPass); + RamsesObjectRegistryIterator objectIterator(m_objectRegistry, ERamsesObjectType::RenderPass); while (const RenderPass* renderPass = objectIterator.getNext()) { if (renderPass->isEnabled() && nullptr == renderPass->getRenderTarget()) { - if (addToRequiredObjects(renderPass->impl)) + if (addToRequiredObjects(renderPass->m_impl)) { - requiredRenderPasses.put(&renderPass->impl); + requiredRenderPasses.put(&renderPass->m_impl); } } } @@ -484,9 +462,9 @@ namespace ramses const Camera* camera = renderPass->getCamera(); if (nullptr != camera) { - if (addToRequiredObjects(camera->impl)) + if (addToRequiredObjects(camera->m_impl)) { - requiredCameras.put(&camera->impl); + requiredCameras.put(&camera->m_impl); } } } @@ -521,7 +499,7 @@ namespace ramses const RenderTarget* renderTarget = renderPass->getRenderTarget(); if (nullptr != renderTarget) { - addToRequiredObjects(renderTarget->impl); + addToRequiredObjects(renderTarget->m_impl); } } } @@ -585,40 +563,6 @@ namespace ramses } } - SceneDumper::StreamTextureSet - SceneDumper::markRequiredStreamTextures(const TextureSamplerSet& requiredTextureSamplers) - { - SceneDumper::StreamTextureSet requiredStreamTextures; - ramses_internal::HashSet requiredTextureResourceHashes; - for (auto textureSampler : requiredTextureSamplers) - { - const auto sampler = textureSampler->getIScene().getTextureSampler(textureSampler->getTextureSamplerHandle()); - if (ramses_internal::TextureSampler::ContentType::StreamTexture == sampler.contentType && ramses_internal::StreamTextureHandle::Invalid() != sampler.contentHandle) - { - const ramses_internal::StreamTextureHandle streamTextureHandle(sampler.contentHandle); - const StreamTextureImpl** streamTexture = m_streamTextureHandleToObjectMap.get(streamTextureHandle); - - if (nullptr != streamTexture) - { - assert(*streamTexture); - if (addToRequiredObjects(**streamTexture)) - { - requiredStreamTextures.put(*streamTexture); - } - } - else - { - LOG_ERROR(ramses_internal::CONTEXT_CLIENT, - "SceneDumper::getRequiredStreamTextures Could not lookup stream texture handle: " - << streamTextureHandle << " !!!"); - assert(false); - } - } - } - - return requiredStreamTextures; - } - void SceneDumper::addNodeWithAllParentNodes(const NodeImpl* node) { while (nullptr != node) @@ -649,7 +593,7 @@ namespace ramses { RamsesObjectSet objects; RamsesObjectVector sceneObjects; - m_objectRegistry.getObjectsOfType(sceneObjects, ERamsesObjectType_RamsesObject); + m_objectRegistry.getObjectsOfType(sceneObjects, ERamsesObjectType::RamsesObject); for (auto sceneObject : sceneObjects) { objects.put(sceneObject); @@ -672,7 +616,7 @@ namespace ramses ramses_internal::HashMap typeStatistic; for (RamsesObject* object : objects) { - const RamsesObjectImpl* objectImpl = &object->impl; + const RamsesObjectImpl* objectImpl = &object->m_impl; ERamsesObjectType type = object->getType(); if (false == m_requiredObjects.contains(objectImpl)) { @@ -687,7 +631,7 @@ namespace ramses AddString("OBEJCT TYPE", output, 39); AddString("#UNREQUIRED / #TOTAL\n\n", output); - for (uint32_t type = 0; type < ERamsesObjectType::ERamsesObjectType_NUMBER_OF_TYPES; type++) + for (uint32_t type = 0; type < static_cast(ERamsesObjectType::NUMBER_OF_TYPES); type++) { if (RamsesObjectTypeUtils::IsConcreteType(ERamsesObjectType(type))) { @@ -711,7 +655,7 @@ namespace ramses } } - void SceneDumper::AddString(const ramses_internal::String& stringToAppend, + void SceneDumper::AddString(std::string_view stringToAppend, ramses_internal::StringOutputStream& string, uint32_t width, bool rightAlign) @@ -738,7 +682,7 @@ namespace ramses ramses_internal::StringOutputStream& outputStream) { outputStream << "Not required " << RamsesObjectTypeUtils::GetRamsesObjectTypeName(object.getType()); - outputStream << " name: \"" << object.getName() << "\" handle: " << object.impl.getObjectRegistryHandle() + outputStream << " name: \"" << object.getName() << "\" handle: " << object.m_impl.getObjectRegistryHandle() << "\n"; } diff --git a/client/ramses-client/impl/SceneDumper.h b/client/ramses-client/impl/SceneDumper.h index 43ff1af4f..71b09243c 100644 --- a/client/ramses-client/impl/SceneDumper.h +++ b/client/ramses-client/impl/SceneDumper.h @@ -18,6 +18,8 @@ #include "TextureSamplerImpl.h" +#include + namespace ramses { class RamsesObjectRegistry; @@ -27,7 +29,6 @@ namespace ramses class GeometryBindingImpl; class Texture2DBufferImpl; class TextureSamplerImpl; - class StreamTextureImpl; class TextureBufferImpl; class RamsesObjectImpl; class RamsesClientImpl; @@ -49,11 +50,10 @@ namespace ramses using RamsesObjectImplSet = ramses_internal::HashSet; explicit SceneDumper(const ramses::SceneImpl& scene); void dumpUnrequiredObjects(ramses_internal::StringOutputStream& output); - const RamsesObjectImplSet& getRequiredObjects() const; + [[nodiscard]] const RamsesObjectImplSet& getRequiredObjects() const; private: using GeometryBindingSet = ramses_internal::HashSet; using TextureSamplerSet = ramses_internal::HashSet; - using StreamTextureSet = ramses_internal::HashSet; using RenderTargetSet = ramses_internal::HashSet; using RenderBufferSet = ramses_internal::HashSet; using RenderGroupSet = ramses_internal::HashSet; @@ -70,7 +70,6 @@ namespace ramses using TextureSamplerHandleToObjectMap = ramses_internal::HashMap; using TextureBufferHandleToObjectMap = ramses_internal::HashMap; - using StreamTextureHandleToObjectMap = ramses_internal::HashMap; using RenderBufferHandleToObjectMap = ramses_internal::HashMap; using RenderBufferHandleRenderBufferSetMap = ramses_internal::HashMap; using ResourceContentHashToObjectMap = ramses_internal::HashMap; @@ -92,7 +91,6 @@ namespace ramses void setupResourceMap(); bool addToRequiredObjects(const RamsesObjectImpl& object); - void markAllObjectsOfTypeAsRequired(ERamsesObjectType objectType); RenderPassSet markRequiredScreenRenderPasses(); RenderPassSet getRequiredRenderPasses(const RenderBufferSet& requiredRenderBuffers); @@ -109,7 +107,6 @@ namespace ramses void markRequiredRenderTargets(const RenderPassSet& requiredRenderPasses); RenderBufferSet markRequiredRenderBuffer(const TextureSamplerSet& requiredTextureSamplers); void markRequiredTextureBuffer(const TextureSamplerSet& requiredTextureSamplers); - StreamTextureSet markRequiredStreamTextures(const TextureSamplerSet& requiredTextureSamplers); RenderBufferSet getRequiredRenderBuffers(RenderBufferSet& requiredRenderBuffers); void markRequiredResourcesFromHash(const ResourceContentHashSet& requiredResourceHashes); @@ -123,7 +120,7 @@ namespace ramses void addNodeWithAllParentNodes(const NodeImpl* node); void addRenderBufferRecursive(const RenderBufferImpl& renderBuffer, RenderBufferSet& requiredRenderBuffers); - static void AddString(const ramses_internal::String& stringToAppend, + static void AddString(std::string_view stringToAppend, ramses_internal::StringOutputStream& string, uint32_t width = 0, bool rightAlign = false); @@ -137,7 +134,6 @@ namespace ramses TextureSamplerHandleToObjectMap m_textureSamplerHandleToObjectMap; ResourceContentHashToObjectMap m_resourceContentHashToObjectMap; TextureBufferHandleToObjectMap m_textureBufferHandleToObjectMap; - StreamTextureHandleToObjectMap m_streamTextureHandleToObjectMap; RenderBufferHandleToObjectMap m_renderBufferHandleToObjectMap; }; diff --git a/client/ramses-client/impl/SceneFactory.cpp b/client/ramses-client/impl/SceneFactory.cpp index 36a5a0981..a9448fa99 100644 --- a/client/ramses-client/impl/SceneFactory.cpp +++ b/client/ramses-client/impl/SceneFactory.cpp @@ -12,41 +12,31 @@ namespace ramses_internal { - SceneFactory::SceneFactory() - { - } - - SceneFactory::~SceneFactory() - { - for (const auto& item : m_scenes) - { - delete item.value; - } - } - ClientScene* SceneFactory::createScene(const SceneInfo& sceneInfo) { - if (m_scenes.contains(sceneInfo.sceneID)) + if (m_scenes.count(sceneInfo.sceneID) != 0) { LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "SceneFactory::createScene: scene with id " << sceneInfo.sceneID << " already exists, scene ID must be unique!"); return nullptr; } - ClientScene* newScene = new ClientScene(sceneInfo); - m_scenes.put(newScene->getSceneId(), newScene); + auto newScene = std::make_unique(sceneInfo); + auto* newScenePtr = newScene.get(); + m_scenes.insert({ newScene->getSceneId(), std::move(newScene) }); - return newScene; + return newScenePtr; } - ClientScene* SceneFactory::releaseScene(SceneId id) + InternalSceneOwningPtr SceneFactory::releaseScene(SceneId id) { - ClientScene* ret = nullptr; - SceneMap::Iterator it = m_scenes.find(id); + InternalSceneOwningPtr ret; + auto it = m_scenes.find(id); if (it != m_scenes.end()) { - ret = it->value; - m_scenes.remove(it); + ret = std::move(it->second); + m_scenes.erase(it); } + return ret; } } diff --git a/client/ramses-client/impl/SceneFactory.h b/client/ramses-client/impl/SceneFactory.h index fca493123..1d869e29a 100644 --- a/client/ramses-client/impl/SceneFactory.h +++ b/client/ramses-client/impl/SceneFactory.h @@ -9,26 +9,23 @@ #ifndef RAMSES_SCENEFACTORY_H #define RAMSES_SCENEFACTORY_H -#include "Collections/HashSet.h" #include "SceneAPI/IScene.h" +#include namespace ramses_internal { class ClientScene; + using InternalSceneOwningPtr = std::unique_ptr; class SceneFactory { public: - explicit SceneFactory(); - ~SceneFactory(); - ClientScene* createScene(const SceneInfo& sceneInfo); - ClientScene* releaseScene(SceneId id); + InternalSceneOwningPtr releaseScene(SceneId id); private: - using SceneMap = HashMap; + using SceneMap = std::unordered_map; SceneMap m_scenes; - }; } diff --git a/client/ramses-client/impl/SceneGraphIteratorImpl.cpp b/client/ramses-client/impl/SceneGraphIteratorImpl.cpp index da29baf45..a3f92fd60 100644 --- a/client/ramses-client/impl/SceneGraphIteratorImpl.cpp +++ b/client/ramses-client/impl/SceneGraphIteratorImpl.cpp @@ -18,7 +18,7 @@ namespace ramses : m_traversalStyle(traversalStyle) , m_objectFilterType(objectFilterType) { - m_nodesToExpand.push_back(&startNode.impl); + m_nodesToExpand.push_back(&startNode.m_impl); } Node* SceneGraphIteratorImpl::getNext() @@ -38,8 +38,8 @@ namespace ramses void SceneGraphIteratorImpl::expandBreadthFirst(NodeImpl& node) { - const uint32_t childCount = node.getChildCount(); - for (uint32_t i = 0; i < childCount; ++i) + const size_t childCount = node.getChildCount(); + for (size_t i = 0; i < childCount; ++i) { m_nodesToExpand.push_back(&node.getChildImpl(i)); } @@ -66,14 +66,12 @@ namespace ramses switch (m_traversalStyle) { - case ETreeTraversalStyle_BreadthFirst: + case ETreeTraversalStyle::BreadthFirst: expandBreadthFirst(*next); break; - case ETreeTraversalStyle_DepthFirst: + case ETreeTraversalStyle::DepthFirst: expandDepthFirst(*next); break; - default: - assert(false); } return next; diff --git a/client/ramses-client/impl/SceneImpl.cpp b/client/ramses-client/impl/SceneImpl.cpp index c0124584a..46f0586d5 100644 --- a/client/ramses-client/impl/SceneImpl.cpp +++ b/client/ramses-client/impl/SceneImpl.cpp @@ -17,33 +17,19 @@ #include "ramses-client-api/MeshNode.h" #include "ramses-client-api/Texture2D.h" #include "ramses-client-api/Texture3D.h" -#include "ramses-client-api/StreamTexture.h" #include "ramses-client-api/TextureCube.h" #include "ramses-client-api/RenderGroup.h" #include "ramses-client-api/RenderPass.h" #include "ramses-client-api/BlitPass.h" -#include "ramses-client-api/AnimationSystemEnums.h" #include "ramses-client-api/PerspectiveCamera.h" #include "ramses-client-api/OrthographicCamera.h" #include "ramses-client-api/Appearance.h" #include "ramses-client-api/Effect.h" #include "ramses-client-api/GeometryBinding.h" -#include "ramses-client-api/AnimationSystem.h" -#include "ramses-client-api/AnimationSystemRealTime.h" #include "ramses-client-api/Effect.h" #include "ramses-client-api/EScenePublicationMode.h" #include "ramses-client-api/AttributeInput.h" -#include "ramses-client-api/DataFloat.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-client-api/DataVector4f.h" -#include "ramses-client-api/DataMatrix22f.h" -#include "ramses-client-api/DataMatrix33f.h" -#include "ramses-client-api/DataMatrix44f.h" -#include "ramses-client-api/DataInt32.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector3i.h" -#include "ramses-client-api/DataVector4i.h" +#include "ramses-client-api/DataObject.h" #include "ramses-client-api/ArrayBuffer.h" #include "ramses-client-api/Texture2DBuffer.h" #include "ramses-client-api/PickableObject.h" @@ -65,11 +51,6 @@ #include "NodeImpl.h" #include "AppearanceImpl.h" #include "GeometryBindingImpl.h" -#include "StreamTextureImpl.h" -#include "AnimationSystemImpl.h" -#include "AnimationAPI/IAnimationSystem.h" -#include "Animation/ActionCollectingAnimationSystem.h" -#include "Animation/AnimationSystemFactory.h" #include "SerializationContext.h" #include "Collections/IOutputStream.h" #include "Collections/IInputStream.h" @@ -105,6 +86,7 @@ #include "DataTypeUtils.h" #include "RamsesVersion.h" #include "Scene/ScenePersistation.h" +#include "AppearanceUtils.h" #include "Resource/ArrayResource.h" #include "Resource/TextureResource.h" @@ -114,7 +96,6 @@ #include "Components/EffectUniformTime.h" #include "PlatformAbstraction/PlatformMath.h" #include "Utils/TextureMathUtils.h" -#include "ResourceDataPoolImpl.h" #include "Components/FlushTimeInformation.h" #include "fmt/format.h" @@ -123,31 +104,23 @@ namespace ramses { SceneImpl::SceneImpl(ramses_internal::ClientScene& scene, const SceneConfigImpl& sceneConfig, RamsesClient& ramsesClient) - : ClientObjectImpl(ramsesClient.impl, ERamsesObjectType_Scene, scene.getName().c_str()) + : ClientObjectImpl(ramsesClient.m_impl, ERamsesObjectType::Scene, scene.getName().c_str()) , m_scene(scene) , m_nextSceneVersion(InvalidSceneVersionTag) , m_futurePublicationMode(sceneConfig.getPublicationMode()) , m_hlClient(ramsesClient) { LOG_INFO(ramses_internal::CONTEXT_CLIENT, "Scene::Scene: sceneId " << scene.getSceneId() << - ", publicationMode " << (sceneConfig.getPublicationMode() == EScenePublicationMode_LocalAndRemote ? "LocalAndRemote" : "LocalOnly")); + ", publicationMode " << (sceneConfig.getPublicationMode() == EScenePublicationMode::LocalAndRemote ? "LocalAndRemote" : "LocalOnly")); getClientImpl().getFramework().getPeriodicLogger().registerStatisticCollectionScene(m_scene.getSceneId(), m_scene.getStatisticCollection()); - const bool enableLocalOnlyOptimization = sceneConfig.getPublicationMode() == EScenePublicationMode_LocalOnly; + const bool enableLocalOnlyOptimization = sceneConfig.getPublicationMode() == EScenePublicationMode::LocalOnly; getClientImpl().getClientApplication().createScene(scene, enableLocalOnlyOptimization); } SceneImpl::~SceneImpl() { LOG_INFO(CONTEXT_CLIENT, "SceneImpl::~SceneImpl"); - RamsesObjectVector objects; - m_objectRegistry.getObjectsOfType(objects, ERamsesObjectType_SceneObject); - for (const auto it : objects) - { - delete &RamsesObjectTypeUtils::ConvertTo(*it); - } - closeSceneFile(); - getClientImpl().getFramework().getPeriodicLogger().removeStatisticCollectionScene(m_scene.getSceneId()); } @@ -172,20 +145,25 @@ namespace ramses return StatusOK; } - template ::value, T>::type* = nullptr> - T& createImplHelper(SceneImpl& scene, ERamsesObjectType) + template ::value, T>::type* = nullptr> + std::unique_ptr createImplHelper(SceneImpl& scene, ERamsesObjectType) + { + return std::make_unique(scene, ""); + } + template ::value, T>::type* = nullptr> + std::unique_ptr createImplHelper(SceneImpl& scene, ERamsesObjectType type) { - return *new T(scene, ""); + return std::make_unique(scene, type, ""); } - template ::value, T>::type* = nullptr> - T& createImplHelper(SceneImpl& scene, ERamsesObjectType type) + template ::value, T>::type* = nullptr> + std::unique_ptr createImplHelper(SceneImpl& scene, ERamsesObjectType) { - return *new T(scene, type, ""); + return std::make_unique(ramses_internal::ResourceHashUsage{}, scene, ""); } - template ::value, T>::type* = nullptr> - T& createImplHelper(SceneImpl& scene, ERamsesObjectType) + template ::value, T>::type* = nullptr> + std::unique_ptr createImplHelper(SceneImpl& scene, ERamsesObjectType type) { - return *new T({}, scene, ""); + return std::make_unique(scene, type, EDataType::Int32, ""); } template @@ -193,25 +171,18 @@ namespace ramses { for (uint32_t i = 0u; i < count; ++i) { - ObjectImplType& impl = createImplHelper(*this, TYPE_ID_OF_RAMSES_OBJECT::ID); + auto impl = createImplHelper(*this, TYPE_ID_OF_RAMSES_OBJECT::ID); ObjectIDType objectID = SerializationHelper::DeserializeObjectID(inStream); - auto status = impl.deserialize(inStream, serializationContext); + auto status = impl->deserialize(inStream, serializationContext); if (status != StatusOK) - { - delete &impl; return status; - } - ObjectType* object = new ObjectType(impl); - m_objectRegistry.addObject(*object); - if (auto resource = RamsesUtils::TryConvert(*object)) - m_resources.insert({ resource->getResourceId(), resource }); + serializationContext.registerObjectImpl(impl.get(), objectID); - if (!serializationContext.registerObjectImpl(&impl, objectID)) - { - delete object; - return addErrorEntry("Deserialization of object failed, object data serialized with wrong ID or data in file corrupted."); - } + auto& object = m_objectRegistry.createAndRegisterObject(std::move(impl)); + + if constexpr (std::is_base_of_v) + m_resources.insert({ object.getResourceId(), &object }); } return StatusOK; @@ -231,7 +202,7 @@ namespace ramses serializationContext.resize(totalCount, m_scene.getNodeCount()); m_objectRegistry.reserveAdditionalGeneralCapacity(totalCount); - std::array objectCounts = {}; + std::array(ERamsesObjectType::NUMBER_OF_TYPES)> objectCounts = {}; for (uint32_t i = 0u; i < typesCount; ++i) { @@ -239,124 +210,92 @@ namespace ramses const ERamsesObjectType type = SerializationHelper::DeserializeObjectTypeAndCount(inStream, count); assert(m_objectRegistry.getNumberOfObjects(type) == 0u); m_objectRegistry.reserveAdditionalObjectCapacity(type, count); - objectCounts[type] = count; + objectCounts[i] = count; status_t status = StatusOK; switch (type) { - case ERamsesObjectType_Node: + case ERamsesObjectType::Node: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_MeshNode: + case ERamsesObjectType::MeshNode: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_PerspectiveCamera: + case ERamsesObjectType::PerspectiveCamera: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_OrthographicCamera: + case ERamsesObjectType::OrthographicCamera: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_Appearance: + case ERamsesObjectType::Appearance: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_GeometryBinding: + case ERamsesObjectType::GeometryBinding: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_StreamTexture: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_AnimationSystem: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_AnimationSystemRealTime: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_RenderGroup: + case ERamsesObjectType::RenderGroup: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_RenderPass: + case ERamsesObjectType::RenderPass: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_BlitPass: + case ERamsesObjectType::BlitPass: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_PickableObject: + case ERamsesObjectType::PickableObject: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_SceneReference: + case ERamsesObjectType::SceneReference: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_RenderBuffer: + case ERamsesObjectType::RenderBuffer: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_RenderTarget: + case ERamsesObjectType::RenderTarget: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_TextureSampler: + case ERamsesObjectType::TextureSampler: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_TextureSamplerMS: + case ERamsesObjectType::TextureSamplerMS: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_TextureSamplerExternal: + case ERamsesObjectType::TextureSamplerExternal: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_DataFloat: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_DataVector2f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_DataVector3f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_DataVector4f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_DataMatrix22f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_DataMatrix33f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); + case ERamsesObjectType::DataObject: + status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_DataMatrix44f: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_DataInt32: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_DataVector2i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_DataVector3i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_DataVector4i: - status = createAndDeserializeObjectImpls(inStream, serializationContext, count); - break; - case ERamsesObjectType_DataBufferObject: + case ERamsesObjectType::ArrayBufferObject: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_Texture2DBuffer: + case ERamsesObjectType::Texture2DBuffer: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_ArrayResource: + case ERamsesObjectType::ArrayResource: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_Texture2D: + case ERamsesObjectType::Texture2D: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_Texture3D: + case ERamsesObjectType::Texture3D: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_TextureCube: + case ERamsesObjectType::TextureCube: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - case ERamsesObjectType_Effect: + case ERamsesObjectType::Effect: status = createAndDeserializeObjectImpls(inStream, serializationContext, count); break; - - default: + case ERamsesObjectType::NUMBER_OF_TYPES: + case ERamsesObjectType::Invalid: + case ERamsesObjectType::ClientObject: + case ERamsesObjectType::RamsesObject: + case ERamsesObjectType::SceneObject: + case ERamsesObjectType::Resource: + case ERamsesObjectType::Camera: + case ERamsesObjectType::Client: + case ERamsesObjectType::Scene: return addErrorEntry("Scene::deserialize failed, unexpected object type in file stream."); } @@ -367,7 +306,7 @@ namespace ramses LOG_DEBUG_F(ramses_internal::CONTEXT_PROFILING, ([&](ramses_internal::StringOutputStream& sos) { sos << "SceneImpl::deserialize: HL scene object counts for SceneID " << m_scene.getSceneId() << "\n"; - for (uint32_t i = 0; i < ERamsesObjectType_NUMBER_OF_TYPES; i++) + for (uint32_t i = 0; i < objectCounts.size(); i++) { if (objectCounts[i] > 0) { @@ -385,34 +324,32 @@ namespace ramses { status_t status = ClientObjectImpl::validate(); - uint32_t objectCount[ERamsesObjectType_NUMBER_OF_TYPES]; - for (uint32_t i = 0u; i < ERamsesObjectType_NUMBER_OF_TYPES; ++i) + std::array(ERamsesObjectType::NUMBER_OF_TYPES)> objectCount; + for (uint32_t i = 0u; i < objectCount.size(); ++i) { const ERamsesObjectType type = static_cast(i); - if (RamsesObjectTypeUtils::IsTypeMatchingBaseType(type, ERamsesObjectType_SceneObject) - && !RamsesObjectTypeUtils::IsTypeMatchingBaseType(type, ERamsesObjectType_AnimationObject) + if (RamsesObjectTypeUtils::IsTypeMatchingBaseType(type, ERamsesObjectType::SceneObject) && RamsesObjectTypeUtils::IsConcreteType(type)) { objectCount[i] = 0u; RamsesObjectRegistryIterator iter(getObjectRegistry(), ERamsesObjectType(i)); while (const RamsesObject* obj = iter.getNext()) { - status = std::max(status, addValidationOfDependentObject(obj->impl)); + status = std::max(status, addValidationOfDependentObject(obj->m_impl)); ++objectCount[i]; } } } - for (uint32_t i = 0u; i < ERamsesObjectType_NUMBER_OF_TYPES; ++i) + for (uint32_t i = 0u; i < objectCount.size(); ++i) { const ERamsesObjectType type = static_cast(i); - if (RamsesObjectTypeUtils::IsTypeMatchingBaseType(type, ERamsesObjectType_SceneObject) - && !RamsesObjectTypeUtils::IsTypeMatchingBaseType(type, ERamsesObjectType_AnimationObject) + if (RamsesObjectTypeUtils::IsTypeMatchingBaseType(type, ERamsesObjectType::SceneObject) && RamsesObjectTypeUtils::IsConcreteType(type)) { ramses_internal::StringOutputStream msg; msg << "Number of " << RamsesObjectTypeUtils::GetRamsesObjectTypeName(type) << " instances: " << objectCount[i]; - addValidationMessage(EValidationSeverity_Info, msg.c_str()); + addValidationMessage(EValidationSeverity::Info, msg.c_str()); } } @@ -426,8 +363,8 @@ namespace ramses { const auto consumerId = it.second->id; if (texConsumerIds.count(consumerId) > 0u) - status = addValidationMessage(EValidationSeverity_Error, - fmt::format("Duplicate texture consumer ID '{}' is not allowed and will result in unknown behavior when linking on renderer", consumerId.getValue()).c_str()); + status = addValidationMessage(EValidationSeverity::Error, + fmt::format("Duplicate texture consumer ID '{}' is not allowed and will result in unknown behavior when linking on renderer", consumerId.getValue())); texConsumerIds.insert(consumerId); } } @@ -435,157 +372,119 @@ namespace ramses return status; } - PerspectiveCamera* SceneImpl::createPerspectiveCamera(const char* name) + PerspectiveCamera* SceneImpl::createPerspectiveCamera(std::string_view name) { - CameraNodeImpl& pimpl = *new CameraNodeImpl(*this, ERamsesObjectType_PerspectiveCamera, name); - pimpl.initializeFrameworkData(); - PerspectiveCamera* newCamera = new PerspectiveCamera(pimpl); - registerCreatedObject(*newCamera); + auto pimpl = std::make_unique(*this, ERamsesObjectType::PerspectiveCamera, name); + pimpl->initializeFrameworkData(); - return newCamera; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } - OrthographicCamera* SceneImpl::createOrthographicCamera(const char* name) + OrthographicCamera* SceneImpl::createOrthographicCamera(std::string_view name) { - CameraNodeImpl& pimpl = *new CameraNodeImpl(*this, ERamsesObjectType_OrthographicCamera, name); - pimpl.initializeFrameworkData(); - OrthographicCamera* newCamera = new OrthographicCamera(pimpl); - registerCreatedObject(*newCamera); + auto pimpl = std::make_unique(*this, ERamsesObjectType::OrthographicCamera, name); + pimpl->initializeFrameworkData(); - return newCamera; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } - Appearance* SceneImpl::createAppearance(const Effect& effect, const char* name) + Appearance* SceneImpl::createAppearance(const Effect& effect, std::string_view name) { - if (this != &effect.impl.getSceneImpl()) + if (this != &effect.m_impl.getSceneImpl()) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createAppearance failed, effect is not from this scene."); return nullptr; } - AppearanceImpl* pimpl = createAppearanceImpl(name); - pimpl->initializeFrameworkData(effect.impl); - Appearance* appearance = new Appearance(*pimpl); - registerCreatedObject(*appearance); - - return appearance; - } - - AppearanceImpl* SceneImpl::createAppearanceImpl(const char* name) - { - return new AppearanceImpl(*this, name); - } - - StreamTexture* SceneImpl::createStreamTexture(const Texture2D& fallbackTexture, waylandIviSurfaceId_t source, const char* name) - { - if (this != &fallbackTexture.impl.getSceneImpl()) - { - LOG_ERROR(CONTEXT_CLIENT, "Scene::createStreamTexture failed, fallbackTexture is not from this scene."); - return nullptr; - } - - StreamTextureImpl& pimpl = *new StreamTextureImpl( *this, name); - pimpl.initializeFrameworkData(source, fallbackTexture.impl); - StreamTexture* texture = new StreamTexture(pimpl); - registerCreatedObject(*texture); + auto pimpl = std::make_unique(*this, name); + pimpl->initializeFrameworkData(effect.m_impl); - return texture; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } - GeometryBinding* SceneImpl::createGeometryBinding(const Effect& effect, const char* name) + GeometryBinding* SceneImpl::createGeometryBinding(const Effect& effect, std::string_view name) { - if (this != &effect.impl.getSceneImpl()) + if (this != &effect.m_impl.getSceneImpl()) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createGeometryBinding failed, effect is not from this scene."); return nullptr; } - GeometryBindingImpl& pimpl = *new GeometryBindingImpl(*this, name); - pimpl.initializeFrameworkData(effect.impl); - GeometryBinding* geometry = new GeometryBinding(pimpl); - registerCreatedObject(*geometry); + auto pimpl = std::make_unique(*this, name); + pimpl->initializeFrameworkData(effect.m_impl); - return geometry; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } status_t SceneImpl::destroy(SceneObject& object) { - if (!containsSceneObject(object.impl)) - { + if (!containsSceneObject(object.m_impl)) return addErrorEntry("Scene::destroy failed, object is not in this scene."); - } status_t returnStatus = StatusOK; switch (object.getType()) { - case ERamsesObjectType_RenderTarget: + case ERamsesObjectType::RenderTarget: returnStatus = destroyRenderTarget(RamsesObjectTypeUtils::ConvertTo(object)); break; - case ERamsesObjectType_OrthographicCamera: - case ERamsesObjectType_PerspectiveCamera: + case ERamsesObjectType::OrthographicCamera: + case ERamsesObjectType::PerspectiveCamera: returnStatus = destroyCamera(RamsesObjectTypeUtils::ConvertTo(object)); break; - case ERamsesObjectType_RenderGroup: + case ERamsesObjectType::RenderGroup: returnStatus = destroyRenderGroup(RamsesObjectTypeUtils::ConvertTo(object)); break; - case ERamsesObjectType_Node: - case ERamsesObjectType_PickableObject: + case ERamsesObjectType::Node: + case ERamsesObjectType::PickableObject: returnStatus = destroyNode(RamsesObjectTypeUtils::ConvertTo(object)); break; - case ERamsesObjectType_MeshNode: + case ERamsesObjectType::MeshNode: returnStatus = destroyMeshNode(RamsesObjectTypeUtils::ConvertTo(object)); break; - case ERamsesObjectType_AnimationSystem: - case ERamsesObjectType_AnimationSystemRealTime: - returnStatus = destroyAnimationSystem(RamsesObjectTypeUtils::ConvertTo(object)); - break; - case ERamsesObjectType_DataFloat: - case ERamsesObjectType_DataVector2f: - case ERamsesObjectType_DataVector3f: - case ERamsesObjectType_DataVector4f: - case ERamsesObjectType_DataMatrix22f: - case ERamsesObjectType_DataMatrix33f: - case ERamsesObjectType_DataMatrix44f: - case ERamsesObjectType_DataInt32: - case ERamsesObjectType_DataVector2i: - case ERamsesObjectType_DataVector3i: - case ERamsesObjectType_DataVector4i: + case ERamsesObjectType::DataObject: returnStatus = destroyDataObject(RamsesObjectTypeUtils::ConvertTo(object)); break; - case ERamsesObjectType_TextureSampler: + case ERamsesObjectType::TextureSampler: destroyTextureSampler(RamsesObjectTypeUtils::ConvertTo(object)); break; - case ERamsesObjectType_TextureSamplerMS: + case ERamsesObjectType::TextureSamplerMS: destroyTextureSampler(RamsesObjectTypeUtils::ConvertTo(object)); break; - case ERamsesObjectType_TextureSamplerExternal: + case ERamsesObjectType::TextureSamplerExternal: destroyTextureSampler(RamsesObjectTypeUtils::ConvertTo(object)); break; - case ERamsesObjectType_Appearance: - case ERamsesObjectType_GeometryBinding: - case ERamsesObjectType_RenderPass: - case ERamsesObjectType_BlitPass: - case ERamsesObjectType_RenderBuffer: - case ERamsesObjectType_StreamTexture: - case ERamsesObjectType_DataBufferObject: - case ERamsesObjectType_Texture2DBuffer: + case ERamsesObjectType::Appearance: + case ERamsesObjectType::GeometryBinding: + case ERamsesObjectType::RenderPass: + case ERamsesObjectType::BlitPass: + case ERamsesObjectType::RenderBuffer: + case ERamsesObjectType::ArrayBufferObject: + case ERamsesObjectType::Texture2DBuffer: returnStatus = destroyObject(object); break; - case ERamsesObjectType_SceneReference: { + case ERamsesObjectType::SceneReference: { auto& sceneReference = RamsesObjectTypeUtils::ConvertTo(object); LOG_INFO_P(ramses_internal::CONTEXT_CLIENT, "Scene::destroySceneReference: (master {} / ref {})", getSceneId(), sceneReference.getReferencedSceneId()); m_sceneReferences.remove(sceneReference.getReferencedSceneId()); returnStatus = destroyObject(object); break; } - case ERamsesObjectType_Texture2D: - case ERamsesObjectType_Texture3D: - case ERamsesObjectType_TextureCube: - case ERamsesObjectType_Effect: - case ERamsesObjectType_ArrayResource: + case ERamsesObjectType::Texture2D: + case ERamsesObjectType::Texture3D: + case ERamsesObjectType::TextureCube: + case ERamsesObjectType::Effect: + case ERamsesObjectType::ArrayResource: returnStatus = destroyResource(RamsesObjectTypeUtils::ConvertTo(object)); break; - default: + case ERamsesObjectType::Invalid: + case ERamsesObjectType::NUMBER_OF_TYPES: + case ERamsesObjectType::ClientObject: + case ERamsesObjectType::RamsesObject: + case ERamsesObjectType::SceneObject: + case ERamsesObjectType::Resource: + case ERamsesObjectType::Camera: + case ERamsesObjectType::Client: + case ERamsesObjectType::Scene: assert(false); returnStatus = addErrorEntry("Scene::destroy internal error, cannot destroy object!"); break; @@ -596,11 +495,11 @@ namespace ramses status_t SceneImpl::destroyRenderTarget(RenderTarget& renderTarget) { - RamsesObjectRegistryIterator iterator(m_objectRegistry, ERamsesObjectType_RenderPass); + RamsesObjectRegistryIterator iterator(m_objectRegistry, ERamsesObjectType::RenderPass); const RenderPass* renderPass = nullptr; while ((renderPass = iterator.getNext()) != nullptr) { - if (&renderTarget == renderPass->impl.getRenderTarget()) + if (&renderTarget == renderPass->m_impl.getRenderTarget()) { return addErrorEntry("Scene::destroy can not destroy render target while it is still assigned to a render pass!"); } @@ -634,9 +533,9 @@ namespace ramses void SceneImpl::markAllChildrenDirty(Node& node) { - for (uint32_t i = 0u; i < node.getChildCount(); ++i) + for (size_t i = 0u; i < node.getChildCount(); ++i) { - m_objectRegistry.setNodeDirty(node.impl.getChildImpl(i), true); + m_objectRegistry.setNodeDirty(node.m_impl.getChildImpl(i), true); } } @@ -661,7 +560,7 @@ namespace ramses status_t SceneImpl::destroyDataObject(DataObject& dataObject) { - const ramses_internal::DataInstanceHandle dataRef = dataObject.impl.getDataReference(); + const ramses_internal::DataInstanceHandle dataRef = dataObject.m_impl.getDataReference(); const uint32_t slotHandleCount = m_scene.getDataSlotCount(); for (ramses_internal::DataSlotHandle slotHandle(0u); slotHandle < slotHandleCount; slotHandle++) { @@ -678,7 +577,7 @@ namespace ramses template status_t SceneImpl::destroyTextureSampler(SAMPLER& sampler) { - const ramses_internal::TextureSamplerHandle& samplerHandle = sampler.impl.getTextureSamplerHandle(); + const ramses_internal::TextureSamplerHandle& samplerHandle = sampler.m_impl.getTextureSamplerHandle(); const uint32_t slotHandleCount = m_scene.getDataSlotCount(); for (ramses_internal::DataSlotHandle slotHandle(0u); slotHandle < slotHandleCount; slotHandle++) { @@ -694,7 +593,7 @@ namespace ramses status_t SceneImpl::destroyResource(Resource& resource) { - const resourceId_t resId = resource.impl.getResourceId(); + const resourceId_t resId = resource.m_impl.getResourceId(); const bool found = removeResourceWithIdFromResources(resId, resource); if (!found) assert(false); @@ -704,9 +603,8 @@ namespace ramses status_t SceneImpl::destroyObject(SceneObject& object) { - object.impl.deinitializeFrameworkData(); - m_objectRegistry.removeObject(object); - delete &object; + object.m_impl.deinitializeFrameworkData(); + m_objectRegistry.destroyAndUnregisterObject(object); return StatusOK; } @@ -716,11 +614,11 @@ namespace ramses { return addErrorEntry((ramses_internal::StringOutputStream() << "Scene(" << m_scene.getSceneId() << ")::publish: ignored, scene is already published").c_str()); } - if (requestedPublicationMode == EScenePublicationMode_LocalAndRemote && m_futurePublicationMode == EScenePublicationMode_LocalOnly) + if (requestedPublicationMode == EScenePublicationMode::LocalAndRemote && m_futurePublicationMode == EScenePublicationMode::LocalOnly) { return addErrorEntry((ramses_internal::StringOutputStream() << "Scene(" << m_scene.getSceneId() << ")::publish: Enabled local only optimisations from SceneConfig, cannot remote publish later").c_str()); } - if (requestedPublicationMode != EScenePublicationMode_LocalOnly && !getClientImpl().getFramework().isConnected()) + if (requestedPublicationMode != EScenePublicationMode::LocalOnly && !getClientImpl().getFramework().isConnected()) { LOG_INFO(ramses_internal::CONTEXT_CLIENT, "Scene(" << m_scene.getSceneId() << ")::publish(LocalAndRemote): Scene is only published locally until framework is connected (RamsesFramework::connect)"); } @@ -755,50 +653,44 @@ namespace ramses return m_futurePublicationMode; } - Node* SceneImpl::createNode(const char* name) + Node* SceneImpl::createNode(std::string_view name) { - NodeImpl* pimpl = new NodeImpl(*this, ERamsesObjectType_Node, name); + auto pimpl = std::make_unique(*this, ERamsesObjectType::Node, name); pimpl->initializeFrameworkData(); - Node* newNode = new Node(*pimpl); - registerCreatedObject(*newNode); - return newNode; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } - MeshNode* SceneImpl::createMeshNode(const char* name) + MeshNode* SceneImpl::createMeshNode(std::string_view name) { - MeshNodeImpl* pimpl = new MeshNodeImpl(*this, name); + auto pimpl = std::make_unique(*this, name); pimpl->initializeFrameworkData(); - MeshNode* newNode = new MeshNode(*pimpl); - registerCreatedObject(*newNode); - return newNode; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } - RenderGroup* SceneImpl::createRenderGroup(const char* name /*= 0*/) + RenderGroup* SceneImpl::createRenderGroup(std::string_view name) { - RenderGroupImpl& pimpl = *new RenderGroupImpl(*this, name); - pimpl.initializeFrameworkData(); - RenderGroup* renderGroup = new RenderGroup(pimpl); - registerCreatedObject(*renderGroup); + auto pimpl = std::make_unique(*this, name); + pimpl->initializeFrameworkData(); - return renderGroup; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } - RenderPass* SceneImpl::createRenderPass(const char* name /*= 0*/) + RenderPass* SceneImpl::createRenderPass(std::string_view name /*= {}*/) { return createRenderPassInternal(name); } - BlitPass* SceneImpl::createBlitPass(const RenderBuffer& sourceRenderBuffer, const RenderBuffer& destinationRenderBuffer, const char* name) + BlitPass* SceneImpl::createBlitPass(const RenderBuffer& sourceRenderBuffer, const RenderBuffer& destinationRenderBuffer, std::string_view name) { - if (!containsSceneObject(sourceRenderBuffer.impl)) + if (!containsSceneObject(sourceRenderBuffer.m_impl)) { LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "Scene(" << m_scene.getSceneId() << ")::createBlitPass failed, source render buffer is not from this scene."); return nullptr; } - if (!containsSceneObject(destinationRenderBuffer.impl)) + if (!containsSceneObject(destinationRenderBuffer.m_impl)) { LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "Scene(" << m_scene.getSceneId() << ")::createBlitPass failed, destination render buffer is not from this scene."); return nullptr; @@ -829,17 +721,15 @@ namespace ramses return nullptr; } - BlitPassImpl& pimpl = *new BlitPassImpl(*this, name); - pimpl.initializeFrameworkData(sourceRenderBuffer.impl, destinationRenderBuffer.impl); - BlitPass* blitPass = new BlitPass(pimpl); - registerCreatedObject(*blitPass); + auto pimpl = std::make_unique(*this, name); + pimpl->initializeFrameworkData(sourceRenderBuffer.m_impl, destinationRenderBuffer.m_impl); - return blitPass; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } - PickableObject* SceneImpl::createPickableObject(const ArrayBuffer& geometryBuffer, const pickableObjectId_t id, const char* name) + PickableObject* SceneImpl::createPickableObject(const ArrayBuffer& geometryBuffer, const pickableObjectId_t id, std::string_view name) { - if (!containsSceneObject(geometryBuffer.impl)) + if (!containsSceneObject(geometryBuffer.m_impl)) { LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "Scene(" << m_scene.getSceneId() @@ -847,7 +737,7 @@ namespace ramses return nullptr; } - if (geometryBuffer.getDataType() != EDataType::Vector3F || 0 != (geometryBuffer.impl.getElementCount() % 3)) + if (geometryBuffer.getDataType() != EDataType::Vector3F || 0 != (geometryBuffer.m_impl.getElementCount() % 3)) { LOG_ERROR(ramses_internal::CONTEXT_CLIENT, "Scene(" << m_scene.getSceneId() @@ -855,15 +745,13 @@ namespace ramses return nullptr; } - PickableObjectImpl& pimpl = *new PickableObjectImpl(*this, name); - pimpl.initializeFrameworkData(geometryBuffer.impl, id); - PickableObject* pickableObject = new PickableObject(pimpl); - registerCreatedObject(*pickableObject); + auto pimpl = std::make_unique(*this, name); + pimpl->initializeFrameworkData(geometryBuffer.m_impl, id); - return pickableObject; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } - RenderBuffer* SceneImpl::createRenderBuffer(uint32_t width, uint32_t height, ERenderBufferType bufferType, ERenderBufferFormat bufferFormat, ERenderBufferAccessMode accessMode, uint32_t sampleCount, const char* name) + RenderBuffer* SceneImpl::createRenderBuffer(uint32_t width, uint32_t height, ERenderBufferType bufferType, ERenderBufferFormat bufferFormat, ERenderBufferAccessMode accessMode, uint32_t sampleCount, std::string_view name) { if (0 == width || 0 == height) { @@ -877,15 +765,13 @@ namespace ramses return nullptr; } - RenderBufferImpl& pimpl = *new RenderBufferImpl(*this, name); - pimpl.initializeFrameworkData(width, height, bufferType, bufferFormat, accessMode, sampleCount); - RenderBuffer* buffer = new RenderBuffer(pimpl); - registerCreatedObject(*buffer); + auto pimpl = std::make_unique(*this, name); + pimpl->initializeFrameworkData(width, height, bufferType, bufferFormat, accessMode, sampleCount); - return buffer; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } - RenderTarget* SceneImpl::createRenderTarget(const RenderTargetDescriptionImpl& rtDesc, const char* name) + RenderTarget* SceneImpl::createRenderTarget(const RenderTargetDescriptionImpl& rtDesc, std::string_view name) { if (rtDesc.validate() != StatusOK) { @@ -893,12 +779,10 @@ namespace ramses return nullptr; } - RenderTargetImpl& pimpl = *new RenderTargetImpl(*this, name); - pimpl.initializeFrameworkData(rtDesc); - RenderTarget* rt = new RenderTarget(pimpl); - registerCreatedObject(*rt); + auto pimpl = std::make_unique(*this, name); + pimpl->initializeFrameworkData(rtDesc); - return rt; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } TextureSampler* SceneImpl::createTextureSampler( @@ -908,22 +792,22 @@ namespace ramses ETextureSamplingMethod magSamplingMethod, uint32_t anisotropyLevel, const Texture2D& texture, - const char* name /*= 0*/) + std::string_view name /*= {}*/) { - if (this != &texture.impl.getSceneImpl()) + if (this != &texture.m_impl.getSceneImpl()) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSampler failed, texture 2D is not from this scene."); return nullptr; } return createTextureSamplerImpl( - wrapUMode, wrapVMode, ETextureAddressMode_Clamp, + wrapUMode, wrapVMode, ETextureAddressMode::Clamp, minSamplingMethod, magSamplingMethod, anisotropyLevel, - ERamsesObjectType_Texture2D, + ERamsesObjectType::Texture2D, ramses_internal::TextureSampler::ContentType::ClientTexture, - texture.impl.getLowlevelResourceHash(), + texture.m_impl.getLowlevelResourceHash(), ramses_internal::InvalidMemoryHandle, name); } @@ -935,9 +819,9 @@ namespace ramses ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, const Texture3D& texture, - const char* name /*= 0*/) + std::string_view name /*= {}*/) { - if (this != &texture.impl.getSceneImpl()) + if (this != &texture.m_impl.getSceneImpl()) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSampler failed, texture 3D is not from this scene."); return nullptr; @@ -945,9 +829,9 @@ namespace ramses return createTextureSamplerImpl( wrapUMode, wrapVMode, wrapRMode, minSamplingMethod, magSamplingMethod, 1u, - ERamsesObjectType_Texture3D, + ERamsesObjectType::Texture3D, ramses_internal::TextureSampler::ContentType::ClientTexture, - texture.impl.getLowlevelResourceHash(), + texture.m_impl.getLowlevelResourceHash(), ramses_internal::InvalidMemoryHandle, name); } @@ -959,19 +843,19 @@ namespace ramses ETextureSamplingMethod magSamplingMethod, uint32_t anisotropyLevel, const TextureCube& texture, - const char* name /*= 0*/) + std::string_view name /*= {}*/) { - if (this != &texture.impl.getSceneImpl()) + if (this != &texture.m_impl.getSceneImpl()) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSampler failed, texture Cube is not from this scene."); return nullptr; } return createTextureSamplerImpl( - wrapUMode, wrapVMode, ETextureAddressMode_Clamp, minSamplingMethod, magSamplingMethod, anisotropyLevel, - ERamsesObjectType_TextureCube, + wrapUMode, wrapVMode, ETextureAddressMode::Clamp, minSamplingMethod, magSamplingMethod, anisotropyLevel, + ERamsesObjectType::TextureCube, ramses_internal::TextureSampler::ContentType::ClientTexture, - texture.impl.getLowlevelResourceHash(), + texture.m_impl.getLowlevelResourceHash(), ramses_internal::InvalidMemoryHandle, name); } @@ -983,7 +867,7 @@ namespace ramses ETextureSamplingMethod magSamplingMethod , uint32_t anisotropyLevel, const RenderBuffer& renderBuffer, - const char* name) + std::string_view name) { if (renderBuffer.getSampleCount() > 0) { @@ -991,24 +875,24 @@ namespace ramses return nullptr; } - if (!containsSceneObject(renderBuffer.impl)) + if (!containsSceneObject(renderBuffer.m_impl)) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSampler failed, render buffer is not from this scene."); return nullptr; } - if (ERenderBufferAccessMode_WriteOnly == renderBuffer.impl.getAccessMode()) + if (ERenderBufferAccessMode::WriteOnly == renderBuffer.m_impl.getAccessMode()) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSampler failed, render buffer has access mode write only."); return nullptr; } return createTextureSamplerImpl( - wrapUMode, wrapVMode, ETextureAddressMode_Clamp, minSamplingMethod, magSamplingMethod, anisotropyLevel, - ERamsesObjectType_RenderBuffer, + wrapUMode, wrapVMode, ETextureAddressMode::Clamp, minSamplingMethod, magSamplingMethod, anisotropyLevel, + ERamsesObjectType::RenderBuffer, ramses_internal::TextureSampler::ContentType::RenderBuffer, ramses_internal::ResourceContentHash::Invalid(), - renderBuffer.impl.getRenderBufferHandle().asMemoryHandle(), + renderBuffer.m_impl.getRenderBufferHandle().asMemoryHandle(), name); } @@ -1019,76 +903,51 @@ namespace ramses ETextureSamplingMethod magSamplingMethod, uint32_t anisotropyLevel, const Texture2DBuffer& textureBuffer, - const char* name) + std::string_view name) { - if (!containsSceneObject(textureBuffer.impl)) + if (!containsSceneObject(textureBuffer.m_impl)) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSampler failed, texture2D buffer is not from this scene."); return nullptr; } return createTextureSamplerImpl( - wrapUMode, wrapVMode, ETextureAddressMode_Clamp, minSamplingMethod, magSamplingMethod, anisotropyLevel, - ERamsesObjectType_Texture2DBuffer, + wrapUMode, wrapVMode, ETextureAddressMode::Clamp, minSamplingMethod, magSamplingMethod, anisotropyLevel, + ERamsesObjectType::Texture2DBuffer, ramses_internal::TextureSampler::ContentType::TextureBuffer, ramses_internal::ResourceContentHash::Invalid(), - textureBuffer.impl.getTextureBufferHandle().asMemoryHandle(), - name); - } - - ramses::TextureSampler* SceneImpl::createTextureSampler( - ETextureAddressMode wrapUMode, - ETextureAddressMode wrapVMode, - ETextureSamplingMethod minSamplingMethod, - ETextureSamplingMethod magSamplingMethod, - const StreamTexture& streamTexture, - const char* name) - { - if (!containsSceneObject(streamTexture.impl)) - { - LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSampler failed, streamTexture is not from this scene."); - return nullptr; - } - - return createTextureSamplerImpl( - wrapUMode, wrapVMode, ETextureAddressMode_Clamp, minSamplingMethod, magSamplingMethod, 1u, - ERamsesObjectType_StreamTexture, - ramses_internal::TextureSampler::ContentType::StreamTexture, - ramses_internal::ResourceContentHash::Invalid(), - streamTexture.impl.getHandle().asMemoryHandle(), + textureBuffer.m_impl.getTextureBufferHandle().asMemoryHandle(), name); } - ramses::TextureSamplerMS* SceneImpl::createTextureSamplerMS(const RenderBuffer& renderBuffer, const char* name) + ramses::TextureSamplerMS* SceneImpl::createTextureSamplerMS(const RenderBuffer& renderBuffer, std::string_view name) { - if (!containsSceneObject(renderBuffer.impl)) + if (!containsSceneObject(renderBuffer.m_impl)) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSampler failed, render buffer is not from this scene."); return nullptr; } - if (ERenderBufferAccessMode_WriteOnly == renderBuffer.impl.getAccessMode()) + if (ERenderBufferAccessMode::WriteOnly == renderBuffer.m_impl.getAccessMode()) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSampler failed, render buffer has access mode write only."); return nullptr; } - TextureSamplerImpl& samplerImpl = *new TextureSamplerImpl(*this, ERamsesObjectType_TextureSamplerMS, name); - samplerImpl.initializeFrameworkData( + auto samplerImpl = std::make_unique(*this, ERamsesObjectType::TextureSamplerMS, name); + samplerImpl->initializeFrameworkData( {}, - ERamsesObjectType_RenderBuffer, + ERamsesObjectType::RenderBuffer, ramses_internal::TextureSampler::ContentType::RenderBufferMS, ramses_internal::ResourceContentHash::Invalid(), - renderBuffer.impl.getRenderBufferHandle().asMemoryHandle()); + renderBuffer.m_impl.getRenderBufferHandle().asMemoryHandle()); - TextureSamplerMS* sampler = new TextureSamplerMS(samplerImpl); - registerCreatedObject(*sampler); - return sampler; + return &m_objectRegistry.createAndRegisterObject(std::move(samplerImpl)); } - ramses::TextureSamplerExternal* SceneImpl::createTextureSamplerExternal(ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, const char *name) + ramses::TextureSamplerExternal* SceneImpl::createTextureSamplerExternal(ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, std::string_view name) { - if (ETextureSamplingMethod_Nearest != magSamplingMethod && ETextureSamplingMethod_Linear != magSamplingMethod) + if (ETextureSamplingMethod::Nearest != magSamplingMethod && ETextureSamplingMethod::Linear != magSamplingMethod) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSamplerExternal failed, mag sampling method must be set to Nearest or Linear."); return nullptr; @@ -1096,15 +955,15 @@ namespace ramses //Restrictions in spec section 3.7.14, https://registry.khronos.org/OpenGL/extensions/OES/OES_EGL_image_external.txt //According to spec min filtering can only be linear or nearest - if (ETextureSamplingMethod_Nearest != minSamplingMethod && ETextureSamplingMethod_Linear != minSamplingMethod) + if (ETextureSamplingMethod::Nearest != minSamplingMethod && ETextureSamplingMethod::Linear != minSamplingMethod) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSamplerExternal failed, min sampling method must be set to Nearest or Linear for external textures."); return nullptr; } //According to spec clamp to edge so the only allowed wrap mode - constexpr ETextureAddressMode wrapUMode = ETextureAddressMode_Clamp; - constexpr ETextureAddressMode wrapVMode = ETextureAddressMode_Clamp; + constexpr ETextureAddressMode wrapUMode = ETextureAddressMode::Clamp; + constexpr ETextureAddressMode wrapVMode = ETextureAddressMode::Clamp; ramses_internal::TextureSamplerStates samplerStates( TextureUtils::GetTextureAddressModeInternal(wrapUMode), @@ -1114,17 +973,15 @@ namespace ramses TextureUtils::GetTextureSamplingInternal(magSamplingMethod) ); - TextureSamplerImpl& samplerImpl = *new TextureSamplerImpl(*this, ERamsesObjectType_TextureSamplerExternal, name); - samplerImpl.initializeFrameworkData( + auto samplerImpl = std::make_unique(*this, ERamsesObjectType::TextureSamplerExternal, name); + samplerImpl->initializeFrameworkData( samplerStates, - ERamsesObjectType_TextureSamplerExternal, + ERamsesObjectType::TextureSamplerExternal, ramses_internal::TextureSampler::ContentType::ExternalTexture, ramses_internal::ResourceContentHash::Invalid(), ramses_internal::InvalidMemoryHandle); - TextureSamplerExternal* sampler = new TextureSamplerExternal(samplerImpl); - registerCreatedObject(*sampler); - return sampler; + return &m_objectRegistry.createAndRegisterObject(std::move(samplerImpl)); } ramses::TextureSampler* SceneImpl::createTextureSamplerImpl( @@ -1138,9 +995,9 @@ namespace ramses ramses_internal::TextureSampler::ContentType contentType, ramses_internal::ResourceContentHash textureResourceHash, ramses_internal::MemoryHandle contentHandle, - const char* name /*= 0*/) + std::string_view name /*= {}*/) { - if (ETextureSamplingMethod_Nearest != magSamplingMethod && ETextureSamplingMethod_Linear != magSamplingMethod) + if (ETextureSamplingMethod::Nearest != magSamplingMethod && ETextureSamplingMethod::Linear != magSamplingMethod) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureSampler failed, mag sampling method must be set to Nearest or Linear."); return nullptr; @@ -1161,127 +1018,29 @@ namespace ramses anisotropyLevel ); - TextureSamplerImpl& samplerImpl = *new TextureSamplerImpl(*this, ERamsesObjectType_TextureSampler, name); - samplerImpl.initializeFrameworkData(samplerStates, samplerType, contentType, textureResourceHash, contentHandle); - - TextureSampler* sampler = new TextureSampler(samplerImpl); - registerCreatedObject(*sampler); - return sampler; - } - - DataFloat* SceneImpl::createDataFloat(const char* name /* =0 */) - { - DataObjectImpl& pimpl = *new DataObjectImpl(*this, ERamsesObjectType_DataFloat, name); - pimpl.initializeFrameworkData(); - DataFloat* dataObject = new DataFloat(pimpl); - registerCreatedObject(*dataObject); - - return dataObject; - } - - DataVector2f* SceneImpl::createDataVector2f(const char* name /* =0 */) - { - DataObjectImpl& pimpl = *new DataObjectImpl(*this, ERamsesObjectType_DataVector2f, name); - pimpl.initializeFrameworkData(); - DataVector2f* dataObject = new DataVector2f(pimpl); - registerCreatedObject(*dataObject); - - return dataObject; - } - - DataVector3f* SceneImpl::createDataVector3f(const char* name /* =0 */) - { - DataObjectImpl& pimpl = *new DataObjectImpl(*this, ERamsesObjectType_DataVector3f, name); - pimpl.initializeFrameworkData(); - DataVector3f* dataObject = new DataVector3f(pimpl); - registerCreatedObject(*dataObject); - - return dataObject; - } - - DataVector4f* SceneImpl::createDataVector4f(const char* name /* =0 */) - { - DataObjectImpl& pimpl = *new DataObjectImpl(*this, ERamsesObjectType_DataVector4f, name); - pimpl.initializeFrameworkData(); - DataVector4f* dataObject = new DataVector4f(pimpl); - registerCreatedObject(*dataObject); - - return dataObject; - } - - DataMatrix22f* SceneImpl::createDataMatrix22f(const char* name /* =0 */) - { - DataObjectImpl& pimpl = *new DataObjectImpl(*this, ERamsesObjectType_DataMatrix22f, name); - pimpl.initializeFrameworkData(); - DataMatrix22f* dataObject = new DataMatrix22f(pimpl); - registerCreatedObject(*dataObject); - - return dataObject; - } - - DataMatrix33f* SceneImpl::createDataMatrix33f(const char* name /* =0 */) - { - DataObjectImpl& pimpl = *new DataObjectImpl(*this, ERamsesObjectType_DataMatrix33f, name); - pimpl.initializeFrameworkData(); - DataMatrix33f* dataObject = new DataMatrix33f(pimpl); - registerCreatedObject(*dataObject); + auto samplerImpl = std::make_unique(*this, ERamsesObjectType::TextureSampler, name); + samplerImpl->initializeFrameworkData(samplerStates, samplerType, contentType, textureResourceHash, contentHandle); - return dataObject; + return &m_objectRegistry.createAndRegisterObject(std::move(samplerImpl)); } - DataMatrix44f* SceneImpl::createDataMatrix44f(const char* name /* =0 */) + DataObject* SceneImpl::createDataObject(EDataType dataType, std::string_view name /*= {}*/) { - DataObjectImpl& pimpl = *new DataObjectImpl(*this, ERamsesObjectType_DataMatrix44f, name); - pimpl.initializeFrameworkData(); - DataMatrix44f* dataObject = new DataMatrix44f(pimpl); - registerCreatedObject(*dataObject); - - return dataObject; - } - - DataInt32* SceneImpl::createDataInt32(const char* name /* =0 */) - { - DataObjectImpl& pimpl = *new DataObjectImpl(*this, ERamsesObjectType_DataInt32, name); - pimpl.initializeFrameworkData(); - DataInt32* dataObject = new DataInt32(pimpl); - registerCreatedObject(*dataObject); - - return dataObject; - } - - DataVector2i* SceneImpl::createDataVector2i(const char* name /* =0 */) - { - DataObjectImpl& pimpl = *new DataObjectImpl(*this, ERamsesObjectType_DataVector2i, name); - pimpl.initializeFrameworkData(); - DataVector2i* dataObject = new DataVector2i(pimpl); - registerCreatedObject(*dataObject); - - return dataObject; - } - - DataVector3i* SceneImpl::createDataVector3i(const char* name /* =0 */) - { - DataObjectImpl& pimpl = *new DataObjectImpl(*this, ERamsesObjectType_DataVector3i, name); - pimpl.initializeFrameworkData(); - DataVector3i* dataObject = new DataVector3i(pimpl); - registerCreatedObject(*dataObject); - - return dataObject; - } + if (!IsDataObjectDataType(dataType)) + { + LOG_ERROR(CONTEXT_CLIENT, "Scene::createDataObject data type is not supported, see IsDataObjectDataType."); + return nullptr; + } - DataVector4i* SceneImpl::createDataVector4i(const char* name /* =0 */) - { - DataObjectImpl& pimpl = *new DataObjectImpl(*this, ERamsesObjectType_DataVector4i, name); - pimpl.initializeFrameworkData(); - DataVector4i* dataObject = new DataVector4i(pimpl); - registerCreatedObject(*dataObject); + auto pimpl = std::make_unique(*this, ERamsesObjectType::DataObject, dataType, name); + pimpl->initializeFrameworkData(); - return dataObject; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } status_t SceneImpl::createTransformationDataProvider(const Node& node, dataProviderId_t id) { - if (!containsSceneObject(node.impl)) + if (!containsSceneObject(node.m_impl)) { return addErrorEntry("Scene::createTransformationDataProvider failed, node is not from this scene."); } @@ -1292,7 +1051,7 @@ namespace ramses return addErrorEntry("Scene::createTransformationDataProvider failed, duplicate data slot id"); } - const ramses_internal::NodeHandle nodeHandle = node.impl.getNodeHandle(); + const ramses_internal::NodeHandle nodeHandle = node.m_impl.getNodeHandle(); if (ramses_internal::DataSlotUtils::HasDataSlotIdForNode(m_scene, nodeHandle)) { return addErrorEntry("Scene::createTransformationDataProvider failed, Node already has a transformation data slot assigned"); @@ -1304,7 +1063,7 @@ namespace ramses status_t SceneImpl::createTransformationDataConsumer(const Node& node, dataConsumerId_t id) { - if (!containsSceneObject(node.impl)) + if (!containsSceneObject(node.m_impl)) { return addErrorEntry("Scene::createTransformationDataConsumer failed, Group Node is not from this scene."); } @@ -1315,7 +1074,7 @@ namespace ramses return addErrorEntry("Scene::createTransformationDataConsumer failed, duplicate data slot id"); } - const ramses_internal::NodeHandle nodeHandle = node.impl.getNodeHandle(); + const ramses_internal::NodeHandle nodeHandle = node.m_impl.getNodeHandle(); if (ramses_internal::DataSlotUtils::HasDataSlotIdForNode(m_scene, nodeHandle)) { return addErrorEntry("Scene::createTransformationDataConsumer failed, Node already has a transformation data slot assigned"); @@ -1327,7 +1086,7 @@ namespace ramses status_t SceneImpl::createDataProvider(const DataObject& dataObject, dataProviderId_t id) { - if (!containsSceneObject(dataObject.impl)) + if (!containsSceneObject(dataObject.m_impl)) { return addErrorEntry("Scene::createDataProvider failed, data object is not from this scene."); } @@ -1338,7 +1097,7 @@ namespace ramses return addErrorEntry("Scene::createDataProvider failed, duplicate data slot id"); } - const ramses_internal::DataInstanceHandle dataRef = dataObject.impl.getDataReference(); + const ramses_internal::DataInstanceHandle dataRef = dataObject.m_impl.getDataReference(); if (ramses_internal::DataSlotUtils::HasDataSlotIdForDataObject(m_scene, dataRef)) { return addErrorEntry("Scene::createDataProvider failed, data object already has a data slot assigned"); @@ -1350,7 +1109,7 @@ namespace ramses status_t SceneImpl::createDataConsumer(const DataObject& dataObject, dataConsumerId_t id) { - if (!containsSceneObject(dataObject.impl)) + if (!containsSceneObject(dataObject.m_impl)) { return addErrorEntry("Scene::createDataConsumer failed, data object is not from this scene."); } @@ -1361,7 +1120,7 @@ namespace ramses return addErrorEntry("Scene::createDataConsumer failed, duplicate data slot id"); } - const ramses_internal::DataInstanceHandle dataRef = dataObject.impl.getDataReference(); + const ramses_internal::DataInstanceHandle dataRef = dataObject.m_impl.getDataReference(); if (ramses_internal::DataSlotUtils::HasDataSlotIdForDataObject(m_scene, dataRef)) { return addErrorEntry("Scene::createDataConsumer failed, data object already has a data slot assigned"); @@ -1373,7 +1132,7 @@ namespace ramses status_t SceneImpl::createTextureProvider(const Texture2D& texture, dataProviderId_t id) { - if (this != &texture.impl.getSceneImpl()) + if (this != &texture.m_impl.getSceneImpl()) { return addErrorEntry("Scene::createTextureProvider failed, texture is not from this scene."); } @@ -1384,7 +1143,7 @@ namespace ramses return addErrorEntry("Scene::createTextureProvider failed, duplicate data slot id"); } - const ramses_internal::ResourceContentHash& textureHash = texture.impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash& textureHash = texture.m_impl.getLowlevelResourceHash(); if (ramses_internal::DataSlotUtils::HasDataSlotIdForTexture(m_scene, textureHash)) { return addErrorEntry("Scene::createTextureProvider failed, texture already has a data slot assigned in this scene"); @@ -1396,7 +1155,7 @@ namespace ramses status_t SceneImpl::updateTextureProvider(const Texture2D& texture, dataProviderId_t id) { - if (this != &texture.impl.getSceneImpl()) + if (this != &texture.m_impl.getSceneImpl()) { return addErrorEntry("Scene::updateTextureProvider failed, texture is not from this scene."); } @@ -1407,12 +1166,12 @@ namespace ramses return addErrorEntry("Scene::updateTextureProvider failed, provider has not been created before."); } - const ramses_internal::UInt32 slotHandleCount = m_scene.getDataSlotCount(); + const uint32_t slotHandleCount = m_scene.getDataSlotCount(); for (ramses_internal::DataSlotHandle slotHandle(0u); slotHandle < slotHandleCount; slotHandle++) { if (m_scene.isDataSlotAllocated(slotHandle) && m_scene.getDataSlot(slotHandle).id == internalDataSlotId) { - const ramses_internal::ResourceContentHash& textureHash = texture.impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash& textureHash = texture.m_impl.getLowlevelResourceHash(); if (m_scene.getDataSlot(slotHandle).attachedTexture != textureHash) { m_scene.setDataSlotTexture(slotHandle, textureHash); @@ -1426,9 +1185,9 @@ namespace ramses status_t SceneImpl::createTextureConsumer(const TextureSampler& sampler, dataConsumerId_t id) { - if (sampler.impl.getTextureType() != ERamsesObjectType_Texture2D && sampler.impl.getTextureType() != ERamsesObjectType_StreamTexture) + if (sampler.m_impl.getTextureType() != ERamsesObjectType::Texture2D) { - return addErrorEntry("Scene::createTextureConsumer failed, only texture sampler using 2D texture or StreamTexture can be used for linking.."); + return addErrorEntry("Scene::createTextureConsumer failed, only texture sampler using 2D texture can be used for linking.."); } return createTextureConsumerImpl(sampler, id); @@ -1452,7 +1211,7 @@ namespace ramses template status_t SceneImpl::createTextureConsumerImpl(const SAMPLER& sampler, dataConsumerId_t id, bool checkDuplicate) { - if (!containsSceneObject(sampler.impl)) + if (!containsSceneObject(sampler.m_impl)) { return addErrorEntry("Scene::createTextureConsumer failed, texture sampler is not from this scene."); } @@ -1464,7 +1223,7 @@ namespace ramses return addErrorEntry("Scene::createTextureConsumer failed, duplicate data slot id"); } - const ramses_internal::TextureSamplerHandle& samplerHandle = sampler.impl.getTextureSamplerHandle(); + const ramses_internal::TextureSamplerHandle& samplerHandle = sampler.m_impl.getTextureSamplerHandle(); if (ramses_internal::DataSlotUtils::HasDataSlotIdForTextureSampler(m_scene, samplerHandle)) { return addErrorEntry("Scene::createTextureConsumer failed, texture sampler already has a data slot assigned"); @@ -1522,57 +1281,6 @@ namespace ramses return ramses_internal::EffectUniformTime::GetMilliseconds(getIScene().getEffectTimeSync()); } - AnimationSystem* SceneImpl::createAnimationSystem(uint32_t flags, const char* name) - { - uint32_t creationFlags = ramses_internal::EAnimationSystemFlags_Default; - if ((flags & EAnimationSystemFlags_ClientSideProcessing) != 0) - { - creationFlags |= ramses_internal::EAnimationSystemFlags_FullProcessing; - } - if ((flags & EAnimationSystemFlags_SynchronizedClock) != 0) - { - LOG_WARN(CONTEXT_CLIENT, "Scene::createAnimationSystem: flag EAnimationSystemFlags_SynchronizedClock is relevant only for real-time animation system, it will be ignored"); - } - - AnimationSystemImpl& pimpl = createAnimationSystemImpl(creationFlags, ERamsesObjectType_AnimationSystem, name); - AnimationSystem* animationSystem = new AnimationSystem(pimpl); - registerCreatedObject(*animationSystem); - return animationSystem; - } - - AnimationSystemRealTime* SceneImpl::createRealTimeAnimationSystem(uint32_t flags, const char* name) - { - uint32_t creationFlags = ramses_internal::EAnimationSystemFlags_RealTime; - if ((flags & EAnimationSystemFlags_ClientSideProcessing) != 0) - { - creationFlags |= ramses_internal::EAnimationSystemFlags_FullProcessing; - } - if ((flags & EAnimationSystemFlags_SynchronizedClock) != 0) - { - creationFlags |= ramses_internal::EAnimationSystemFlags_SynchronizedClock; - } - - AnimationSystemImpl& pimpl = createAnimationSystemImpl(creationFlags, ERamsesObjectType_AnimationSystemRealTime, name); - AnimationSystemRealTime* animationSystem = new AnimationSystemRealTime(pimpl); - registerCreatedObject(*animationSystem); - return animationSystem; - } - - AnimationSystemImpl& SceneImpl::createAnimationSystemImpl(uint32_t flags, ERamsesObjectType type, const char* name) - { - ramses_internal::AnimationSystemFactory animSystemFactory(ramses_internal::EAnimationSystemOwner_Client, &m_scene.getSceneActionCollection()); - ramses_internal::IAnimationSystem* ianimationSystem = - animSystemFactory.createAnimationSystem(flags, ramses_internal::AnimationSystemSizeInformation()); - AnimationSystemImpl& pimpl = *new AnimationSystemImpl(*this, type, name); - pimpl.initializeFrameworkData(*ianimationSystem); - return pimpl; - } - - status_t SceneImpl::destroyAnimationSystem(AnimationSystem& animationSystem) - { - return destroyObject(animationSystem); - } - RamsesObjectRegistry& SceneImpl::getObjectRegistry() { return m_objectRegistry; @@ -1583,17 +1291,12 @@ namespace ramses return &object.getSceneImpl() == this; } - void SceneImpl::registerCreatedObject(SceneObject& object) - { - m_objectRegistry.addObject(object); - } - - const RamsesObject* SceneImpl::findObjectByName(const char* name) const + const RamsesObject* SceneImpl::findObjectByName(std::string_view name) const { return m_objectRegistry.findObjectByName(name); } - RamsesObject* SceneImpl::findObjectByName(const char* name) + RamsesObject* SceneImpl::findObjectByName(std::string_view name) { return m_objectRegistry.findObjectByName(name); } @@ -1621,14 +1324,14 @@ namespace ramses CONTAINER* container = nullptr; while ((container = iterator.getNextNonConst()) != nullptr) { - container->impl.removeIfContained(object.impl); + container->m_impl.removeIfContained(object.m_impl); } } void SceneImpl::removeAllDataSlotsForNode(const Node& node) { - const ramses_internal::NodeHandle nodeHandle = node.impl.getNodeHandle(); - const ramses_internal::UInt32 slotHandleCount = m_scene.getDataSlotCount(); + const ramses_internal::NodeHandle nodeHandle = node.m_impl.getNodeHandle(); + const uint32_t slotHandleCount = m_scene.getDataSlotCount(); for (ramses_internal::DataSlotHandle slotHandle(0u); slotHandle < slotHandleCount; slotHandle++) { if (m_scene.isDataSlotAllocated(slotHandle) && m_scene.getDataSlot(slotHandle).attachedNode == nodeHandle) @@ -1638,19 +1341,17 @@ namespace ramses } } - RenderPass* SceneImpl::createRenderPassInternal(const char* name) + RenderPass* SceneImpl::createRenderPassInternal(std::string_view name) { - RenderPassImpl& pimpl = *new RenderPassImpl(*this, name); - pimpl.initializeFrameworkData(); - RenderPass* renderPass = new RenderPass(pimpl); - registerCreatedObject(*renderPass); + auto pimpl = std::make_unique(*this, name); + pimpl->initializeFrameworkData(); - return renderPass; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } bool SceneImpl::cameraIsAssignedToRenderPasses(const Camera& camera) { - RamsesObjectRegistryIterator iterator(m_objectRegistry, ERamsesObjectType_RenderPass); + RamsesObjectRegistryIterator iterator(m_objectRegistry, ERamsesObjectType::RenderPass); const RenderPass* renderPass = nullptr; while ((renderPass = iterator.getNext()) != nullptr) { @@ -1678,7 +1379,7 @@ namespace ramses visibilityToApply = std::min(visibilityToApply, node.getVisibility()); const ERamsesObjectType nodeType = node.getType(); - if (nodeType == ERamsesObjectType_MeshNode) + if (nodeType == ERamsesObjectType::MeshNode) { MeshNodeImpl& meshNode = static_cast(node); const EVisibilityMode currentVisibility = meshNode.getFlattenedVisibility(); @@ -1689,8 +1390,8 @@ namespace ramses } } - const uint32_t numberOfChildren = node.getChildCount(); - for (uint32_t i = 0; i < numberOfChildren; i++) + const size_t numberOfChildren = node.getChildCount(); + for (size_t i = 0; i < numberOfChildren; i++) { NodeImpl& child = node.getChildImpl(i); m_dataStackForSubTreeVisibilityApplying.emplace_back(&child, visibilityToApply); @@ -1756,43 +1457,21 @@ namespace ramses m_nextSceneVersion = sceneVersion; } - ArrayBuffer* SceneImpl::createArrayBuffer(EDataType dataType, uint32_t maxNumElements, const char* name) + ArrayBuffer* SceneImpl::createArrayBuffer(EDataType dataType, uint32_t maxNumElements, std::string_view name) { - ArrayBufferImpl* pimpl = createArrayBufferImpl(dataType, maxNumElements, name); - - if (nullptr != pimpl) + if (!IsArrayResourceDataType(dataType)) { - ArrayBuffer* buffer = new ArrayBuffer(*pimpl); - registerCreatedObject(*buffer); - return buffer; + LOG_ERROR(CONTEXT_CLIENT, "Scene::createArrayBuffer failed: incompatible data type"); + return nullptr; } - return nullptr; - } - - ArrayBufferImpl* SceneImpl::createArrayBufferImpl(EDataType dataType, uint32_t numElements, const char* name) - { - ArrayBufferImpl* pimpl = new ArrayBufferImpl(*this, name); - pimpl->initializeFrameworkData(dataType, numElements); - - return pimpl; - } - - Texture2DBuffer* SceneImpl::createTexture2DBuffer(uint32_t mipLevels, uint32_t width, uint32_t height, ETextureFormat textureFormat, const char* name) - { - Texture2DBuffer* buffer = nullptr; - Texture2DBufferImpl* pimpl = createTexture2DBufferImpl(mipLevels, width, height, textureFormat, name); + auto pimpl = std::make_unique(*this, name); + pimpl->initializeFrameworkData(dataType, maxNumElements); - if(nullptr != pimpl) - { - buffer = new Texture2DBuffer(*pimpl); - registerCreatedObject(*buffer); - } - - return buffer; + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } - Texture2DBufferImpl* SceneImpl::createTexture2DBufferImpl(uint32_t mipLevels, uint32_t width, uint32_t height, ETextureFormat textureFormat, const char* name) + Texture2DBuffer* SceneImpl::createTexture2DBuffer(size_t mipLevels, uint32_t width, uint32_t height, ETextureFormat textureFormat, std::string_view name) { if (IsFormatCompressed(TextureUtils::GetTextureFormatInternal(textureFormat))) { @@ -1801,7 +1480,7 @@ namespace ramses } // More than one mips have size 1x1 -> error - const uint32_t maxMipCount = ramses_internal::TextureMathUtils::GetMipLevelCount(width, height, 1u); + const size_t maxMipCount = ramses_internal::TextureMathUtils::GetMipLevelCount(width, height, 1u); if (mipLevels > maxMipCount) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTexture2DBuffer failed, mipLevels too large for the provided texture size."); @@ -1812,7 +1491,7 @@ namespace ramses uint32_t currentWidth = width; uint32_t currentHeight = height; - for (uint32_t mipLevel = 0; mipLevel < mipLevels; ++mipLevel) + for (size_t mipLevel = 0; mipLevel < mipLevels; ++mipLevel) { mipMapSizes.push_back({ currentWidth, currentHeight }); @@ -1820,9 +1499,10 @@ namespace ramses currentHeight = std::max(1, currentHeight / 2); } - Texture2DBufferImpl* pimpl = new Texture2DBufferImpl(*this, name); + auto pimpl = std::make_unique(*this, name); pimpl->initializeFrameworkData(mipMapSizes, textureFormat); - return pimpl; + + return &m_objectRegistry.createAndRegisterObject(std::move(pimpl)); } ramses_internal::StatisticCollectionScene& SceneImpl::getStatisticCollection() @@ -1830,7 +1510,7 @@ namespace ramses return m_scene.getStatisticCollection(); } - ramses::SceneReference* SceneImpl::createSceneReference(sceneId_t referencedScene, const char* name) + ramses::SceneReference* SceneImpl::createSceneReference(sceneId_t referencedScene, std::string_view name) { if (!referencedScene.isValid()) { @@ -1844,8 +1524,7 @@ namespace ramses return nullptr; } - const auto scenes = getClientImpl().getListOfScenes(); - for (const auto scene : scenes) + for (const auto& scene : getClientImpl().getListOfScenes()) { if (getClientImpl().findSceneReference(scene->getSceneId(), referencedScene) != nullptr) { @@ -1857,12 +1536,12 @@ namespace ramses LOG_INFO_P(ramses_internal::CONTEXT_CLIENT, "Scene::createSceneReference: creating scene reference (master {} / ref {})", getSceneId(), referencedScene); - SceneReferenceImpl& pimpl = *new SceneReferenceImpl(*this, name); - pimpl.initializeFrameworkData(referencedScene); - SceneReference* sr = new SceneReference(pimpl); - registerCreatedObject(*sr); - m_sceneReferences.put(referencedScene, sr); - return sr; + auto pimpl = std::make_unique(*this, name); + pimpl->initializeFrameworkData(referencedScene); + auto& sr = m_objectRegistry.createAndRegisterObject(std::move(pimpl)); + m_sceneReferences.put(referencedScene, &sr); + + return &sr; } status_t SceneImpl::linkData(SceneReference* providerReference, dataProviderId_t providerId, SceneReference* consumerReference, dataConsumerId_t consumerId) @@ -1873,18 +1552,18 @@ namespace ramses if (consumerReference == providerReference) return addErrorEntry("Scene::linkData: can't link an object to another object in the same scene reference"); - if ((providerReference != nullptr && providerReference->impl.getSceneImpl().getSceneId() != getSceneId()) || - (consumerReference != nullptr && consumerReference->impl.getSceneImpl().getSceneId() != getSceneId())) + if ((providerReference != nullptr && providerReference->m_impl.getSceneImpl().getSceneId() != getSceneId()) || + (consumerReference != nullptr && consumerReference->m_impl.getSceneImpl().getSceneId() != getSceneId())) return addErrorEntry("Scene::linkData: can't link to object of a scene reference with a different master scene"); - if (providerReference && providerReference->impl.getReportedState() < RendererSceneState::Ready) + if (providerReference && providerReference->m_impl.getReportedState() < RendererSceneState::Ready) return addErrorEntry("Scene::linkData: Provider SceneReference state has to be at least Ready"); - if (consumerReference && consumerReference->impl.getReportedState() < RendererSceneState::Ready) + if (consumerReference && consumerReference->m_impl.getReportedState() < RendererSceneState::Ready) return addErrorEntry("Scene::linkData: Consumer SceneReference state has to be at least Ready"); - const auto providerScene = (providerReference ? providerReference->impl.getSceneReferenceHandle() : ramses_internal::SceneReferenceHandle{}); - const auto consumerScene = (consumerReference ? consumerReference->impl.getSceneReferenceHandle() : ramses_internal::SceneReferenceHandle{}); + const auto providerScene = (providerReference ? providerReference->m_impl.getSceneReferenceHandle() : ramses_internal::SceneReferenceHandle{}); + const auto consumerScene = (consumerReference ? consumerReference->m_impl.getSceneReferenceHandle() : ramses_internal::SceneReferenceHandle{}); getIScene().linkData(providerScene, ramses_internal::DataSlotId{ providerId.getValue() }, consumerScene, ramses_internal::DataSlotId{ consumerId.getValue() }); return StatusOK; @@ -1892,10 +1571,10 @@ namespace ramses status_t SceneImpl::unlinkData(SceneReference* consumerReference, dataConsumerId_t consumerId) { - if (consumerReference != nullptr && consumerReference->impl.getSceneImpl().getSceneId() != getSceneId()) + if (consumerReference != nullptr && consumerReference->m_impl.getSceneImpl().getSceneId() != getSceneId()) return addErrorEntry("Scene::unlinkData: can't unlink object of a scene reference with a different master scene"); - const auto consumerScene = (consumerReference ? consumerReference->impl.getSceneReferenceHandle() : ramses_internal::SceneReferenceHandle{}); + const auto consumerScene = (consumerReference ? consumerReference->m_impl.getSceneReferenceHandle() : ramses_internal::SceneReferenceHandle{}); getIScene().unlinkData(consumerScene, ramses_internal::DataSlotId{ consumerId.getValue() }); return StatusOK; @@ -1918,7 +1597,8 @@ namespace ramses return sceneObjectId_t{ m_lastSceneObjectId}; } - ArrayResource* SceneImpl::createArrayResource(EDataType type, uint32_t numElements, const void* arrayData, resourceCacheFlag_t cacheFlag, const char* name) + template + ArrayResource* SceneImpl::createArrayResource(uint32_t numElements, const T* arrayData, resourceCacheFlag_t cacheFlag, std::string_view name) { if (0u == numElements || nullptr == arrayData) { @@ -1926,7 +1606,7 @@ namespace ramses return nullptr; } - ramses_internal::ManagedResource res = getClientImpl().createManagedArrayResource(numElements, type, arrayData, cacheFlag, name); + ramses_internal::ManagedResource res = getClientImpl().createManagedArrayResource(numElements, GetEDataType(), arrayData, cacheFlag, name); if (!res) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createArrayResource: failed to create managed array resource"); @@ -1935,7 +1615,7 @@ namespace ramses return createHLArrayResource(res, name); } - ArrayResource* SceneImpl::createHLArrayResource(ramses_internal::ManagedResource const& resource, const char* name) + ArrayResource* SceneImpl::createHLArrayResource(ramses_internal::ManagedResource const& resource, std::string_view name) { assert(resource->getTypeID() == ramses_internal::EResourceType_IndexArray || resource->getTypeID() == ramses_internal::EResourceType_VertexArray); @@ -1943,17 +1623,17 @@ namespace ramses const auto arrayRes = resource->convertTo(); ramses_internal::ResourceHashUsage usage = getClientImpl().getClientApplication().getHashUsage(arrayRes->getHash()); - ArrayResourceImpl& pimpl = *new ArrayResourceImpl(usage, *this, name); - pimpl.initializeFromFrameworkData(arrayRes->getElementCount(), DataTypeUtils::ConvertDataTypeFromInternal(arrayRes->getElementType())); + auto pimpl = std::make_unique(usage, *this, name); + pimpl->initializeFromFrameworkData(arrayRes->getElementCount(), DataTypeUtils::ConvertDataTypeFromInternal(arrayRes->getElementType())); - ArrayResource* arrayResHL = new ArrayResource(pimpl); - registerCreatedResourceObject(*arrayResHL); - return arrayResHL; + return ®isterCreatedResourceObject(std::move(pimpl)); } - Texture2D* SceneImpl::createTexture2D(uint32_t width, uint32_t height, ETextureFormat format, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name) + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + Texture2D* SceneImpl::createTexture2D(uint32_t width, uint32_t height, ETextureFormat format, size_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, std::string_view name) { - ramses_internal::ManagedResource res = getClientImpl().createManagedTexture(ramses_internal::EResourceType_Texture2D, width, height, 1u, format, mipMapCount, mipLevelData, generateMipChain, swizzle, cacheFlag, name); + ramses_internal::ManagedResource res = getClientImpl().createManagedTexture( + ramses_internal::EResourceType_Texture2D, width, height, 1u, format, static_cast(mipMapCount), mipLevelData, generateMipChain, swizzle, cacheFlag, name); if (!res) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTexture2D: failed to create managed Texture2D resource"); @@ -1962,26 +1642,26 @@ namespace ramses return createHLTexture2D(res, name); } - Texture2D* SceneImpl::createHLTexture2D(ramses_internal::ManagedResource const& resource, const char* name) + Texture2D* SceneImpl::createHLTexture2D(ramses_internal::ManagedResource const& resource, std::string_view name) { assert(resource->getTypeID() == ramses_internal::EResourceType_Texture2D); const auto texRes = resource->convertTo(); ramses_internal::ResourceHashUsage hashUsage = getClientImpl().getClientApplication().getHashUsage(resource->getHash()); - Texture2DImpl& pimpl = *new Texture2DImpl(hashUsage, *this, name); - pimpl.initializeFromFrameworkData(texRes->getWidth(), texRes->getHeight(), + auto pimpl = std::make_unique(hashUsage, *this, name); + pimpl->initializeFromFrameworkData(texRes->getWidth(), texRes->getHeight(), TextureUtils::GetTextureFormatFromInternal(texRes->getTextureFormat()), TextureUtils::GetTextureSwizzleFromInternal(texRes->getTextureSwizzle())); - Texture2D* texture = new Texture2D(pimpl); - registerCreatedResourceObject(*texture); - return texture; + return ®isterCreatedResourceObject(std::move(pimpl)); } - Texture3D* SceneImpl::createTexture3D(uint32_t width, uint32_t height, uint32_t depth, ETextureFormat format, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, resourceCacheFlag_t cacheFlag, const char* name) + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + Texture3D* SceneImpl::createTexture3D(uint32_t width, uint32_t height, uint32_t depth, ETextureFormat format, size_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, resourceCacheFlag_t cacheFlag, std::string_view name) { - ramses_internal::ManagedResource res = getClientImpl().createManagedTexture(ramses_internal::EResourceType_Texture3D, width, height, depth, format, mipMapCount, mipLevelData, generateMipChain, {}, cacheFlag, name); + ramses_internal::ManagedResource res = getClientImpl().createManagedTexture( + ramses_internal::EResourceType_Texture3D, width, height, depth, format, static_cast(mipMapCount), mipLevelData, generateMipChain, {}, cacheFlag, name); if (!res) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTexture3D: failed to create managed Texture3D resource"); @@ -1990,24 +1670,24 @@ namespace ramses return createHLTexture3D(res, name); } - Texture3D* SceneImpl::createHLTexture3D(ramses_internal::ManagedResource const& resource, const char* name) + Texture3D* SceneImpl::createHLTexture3D(ramses_internal::ManagedResource const& resource, std::string_view name) { assert(resource->getTypeID() == ramses_internal::EResourceType_Texture3D); const auto texRes = resource->convertTo(); ramses_internal::ResourceHashUsage hashUsage = getClientImpl().getClientApplication().getHashUsage(resource->getHash()); - Texture3DImpl& pimpl = *new Texture3DImpl(hashUsage, *this, name); - pimpl.initializeFromFrameworkData(texRes->getWidth(), texRes->getHeight(), texRes->getDepth(), + auto pimpl = std::make_unique(hashUsage, *this, name); + pimpl->initializeFromFrameworkData(texRes->getWidth(), texRes->getHeight(), texRes->getDepth(), TextureUtils::GetTextureFormatFromInternal(texRes->getTextureFormat())); - Texture3D* texture = new Texture3D(pimpl); - registerCreatedResourceObject(*texture); - return texture; + return ®isterCreatedResourceObject(std::move(pimpl)); } - TextureCube* SceneImpl::createTextureCube(uint32_t size, ETextureFormat format, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name) + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + TextureCube* SceneImpl::createTextureCube(uint32_t size, ETextureFormat format, size_t mipMapCount, const CubeMipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, std::string_view name) { - ramses_internal::ManagedResource res = getClientImpl().createManagedTexture(ramses_internal::EResourceType_TextureCube, size, 1u, 1u, format, mipMapCount, mipLevelData, generateMipChain, swizzle, cacheFlag, name); + ramses_internal::ManagedResource res = getClientImpl().createManagedTexture( + ramses_internal::EResourceType_TextureCube, size, 1u, 1u, format, static_cast(mipMapCount), mipLevelData, generateMipChain, swizzle, cacheFlag, name); if (!res) { LOG_ERROR(CONTEXT_CLIENT, "Scene::createTextureCube: failed to create managed TextureCube resource"); @@ -2016,24 +1696,22 @@ namespace ramses return createHLTextureCube(res, name); } - TextureCube* SceneImpl::createHLTextureCube(ramses_internal::ManagedResource const& resource, const char* name) + TextureCube* SceneImpl::createHLTextureCube(ramses_internal::ManagedResource const& resource, std::string_view name) { assert(resource->getTypeID() == ramses_internal::EResourceType_TextureCube); const auto texRes = resource->convertTo(); ramses_internal::ResourceHashUsage hashUsage = getClientImpl().getClientApplication().getHashUsage(resource->getHash()); - TextureCubeImpl& pimpl = *new TextureCubeImpl(hashUsage, *this, name); - pimpl.initializeFromFrameworkData(texRes->getWidth(), + auto pimpl = std::make_unique(hashUsage, *this, name); + pimpl->initializeFromFrameworkData(texRes->getWidth(), TextureUtils::GetTextureFormatFromInternal(texRes->getTextureFormat()), TextureUtils::GetTextureSwizzleFromInternal(texRes->getTextureSwizzle())); - TextureCube* texture = new TextureCube(pimpl); - registerCreatedResourceObject(*texture); - return texture; + return ®isterCreatedResourceObject(std::move(pimpl)); } - Effect* SceneImpl::createEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, const char* name) + Effect* SceneImpl::createEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, std::string_view name) { ramses_internal::ManagedResource res = getClientImpl().createManagedEffect(effectDesc, cacheFlag, name, m_effectErrorMessages); if (!res) @@ -2045,26 +1723,29 @@ namespace ramses return createHLEffect(res, name); } - Effect* SceneImpl::createHLEffect(ramses_internal::ManagedResource const& resource, const char* name) + Effect* SceneImpl::createHLEffect(ramses_internal::ManagedResource const& resource, std::string_view name) { assert(resource->getTypeID() == ramses_internal::EResourceType_Effect); const auto effectRes = resource->convertTo(); ramses_internal::ResourceHashUsage hashUsage = getClientImpl().getClientApplication().getHashUsage(resource->getHash()); - ramses::EffectImpl& pimpl = *new ramses::EffectImpl(hashUsage, *this, name); - pimpl.initializeFromFrameworkData(effectRes->getUniformInputs(), effectRes->getAttributeInputs(), effectRes->getGeometryShaderInputType()); + auto pimpl = std::make_unique(hashUsage, *this, name); + pimpl->initializeFromFrameworkData(effectRes->getUniformInputs(), effectRes->getAttributeInputs(), effectRes->getGeometryShaderInputType()); - Effect* effect = new Effect(pimpl); - registerCreatedResourceObject(*effect); - return effect; + return ®isterCreatedResourceObject(std::move(pimpl)); } - void SceneImpl::registerCreatedResourceObject(Resource& resource) + template + T& SceneImpl::registerCreatedResourceObject(std::unique_ptr resourceImpl) { - registerCreatedObject(resource); + static_assert(std::is_base_of_v, "Meant for Resource instances only"); + T& resource = m_objectRegistry.createAndRegisterObject(std::move(resourceImpl)); + const resourceId_t resId = resource.getResourceId(); m_resources.insert({ resId, &resource }); + + return resource; } std::string SceneImpl::getLastEffectErrorMessages() const @@ -2082,7 +1763,7 @@ namespace ramses { for (const auto& res : m_resources) { - if (hash == res.second->impl.getLowlevelResourceHash()) + if (hash == res.second->m_impl.getLowlevelResourceHash()) return res.second; } @@ -2098,10 +1779,10 @@ namespace ramses return serialize(outputStream, serializationContext); } - status_t SceneImpl::saveToFile(const char* fileName, bool compress) const + status_t SceneImpl::saveToFile(std::string_view fileName, bool compress) const { - if (fileName == nullptr) - return addErrorEntry("Scene::saveToFile failed, filename was null"); + if (fileName.empty()) + return addErrorEntry("Scene::saveToFile failed: empty filename"); LOG_INFO_P(CONTEXT_CLIENT, "Scene::saveToFile: filename '{}', compress {}", fileName, compress); @@ -2110,9 +1791,10 @@ namespace ramses if (!outputFile.isOpen()) return addErrorEntry(fmt::format("Scene::saveToFile failed, could not open file for writing: '{}'", fileName)); - ramses_internal::RamsesVersion::WriteToStream(outputStream, ::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_STRING, ::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH); + const EFeatureLevel featureLevel = m_hlClient.m_impl.getFramework().getFeatureLevel(); + ramses_internal::RamsesVersion::WriteToStream(outputStream, ::ramses_sdk::RAMSES_SDK_RAMSES_VERSION, ::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH, featureLevel); - ramses_internal::UInt bytesForVersion = 0; + size_t bytesForVersion = 0; if (!outputFile.getPos(bytesForVersion)) return addErrorEntry(fmt::format("Scene::saveToFile failed, error getting save file position: '{}'", fileName)); @@ -2125,7 +1807,7 @@ namespace ramses const status_t status = writeSceneObjectsToStream(outputStream); - ramses_internal::UInt offsetLLResourcesStart = 0; + size_t offsetLLResourcesStart = 0; if (!outputFile.getPos(offsetLLResourcesStart)) return addErrorEntry(fmt::format("Scene::saveToFile failed, error getting save file position: '{}'", fileName)); @@ -2149,16 +1831,6 @@ namespace ramses return status; } - bool SceneImpl::saveResources(std::string const& fileName, bool compress) const - { - ResourceObjects resources; - resources.reserve(m_resources.size()); - for (auto entry : m_resources) - resources.push_back(entry.second); - - return getClientImpl().getResourceDataPool().impl.saveResourceDataFile(fileName, resources, compress); - } - void SceneImpl::setSceneFileHandle(ramses_internal::SceneFileHandle handle) { m_sceneFileHandle = handle; @@ -2199,4 +1871,12 @@ namespace ramses removeResourceWithIdFromResources(oldId, resourceWithNewId); m_resources.insert({ resourceWithNewId.getResourceId(), &resourceWithNewId }); } + + template ArrayResource* SceneImpl::createArrayResource(uint32_t, const uint16_t*, resourceCacheFlag_t, std::string_view); + template ArrayResource* SceneImpl::createArrayResource(uint32_t, const uint32_t*, resourceCacheFlag_t, std::string_view); + template ArrayResource* SceneImpl::createArrayResource(uint32_t, const float*, resourceCacheFlag_t, std::string_view); + template ArrayResource* SceneImpl::createArrayResource(uint32_t, const vec2f*, resourceCacheFlag_t, std::string_view); + template ArrayResource* SceneImpl::createArrayResource(uint32_t, const vec3f*, resourceCacheFlag_t, std::string_view); + template ArrayResource* SceneImpl::createArrayResource(uint32_t, const vec4f*, resourceCacheFlag_t, std::string_view); + template ArrayResource* SceneImpl::createArrayResource(uint32_t, const Byte*, resourceCacheFlag_t, std::string_view); } diff --git a/client/ramses-client/impl/SceneImpl.h b/client/ramses-client/impl/SceneImpl.h index e75f516ca..1f065bb00 100644 --- a/client/ramses-client/impl/SceneImpl.h +++ b/client/ramses-client/impl/SceneImpl.h @@ -11,11 +11,11 @@ #include "ramses-client-api/EScenePublicationMode.h" #include "ramses-client-api/EVisibilityMode.h" -#include "ramses-client-api/EDataType.h" #include "ramses-client-api/TextureEnums.h" #include "ramses-client-api/SceneReference.h" #include "ramses-client-api/MipLevelData.h" #include "ramses-client-api/TextureSwizzle.h" +#include "ramses-framework-api/EDataType.h" // internal #include "ClientObjectImpl.h" @@ -31,15 +31,16 @@ #include "SceneAPI/DataSlot.h" #include "SceneAPI/EDataSlotType.h" #include "SceneAPI/TextureSampler.h" -#include "AnimationAPI/IAnimationSystem.h" #include "Resource/ResourceTypes.h" #include "Components/ManagedResource.h" #include "Collections/Pair.h" #include "Utils/StatisticCollection.h" #include "RamsesFrameworkTypesImpl.h" + #include #include +#include namespace ramses_internal { @@ -60,27 +61,14 @@ namespace ramses class Node; class Effect; class MeshNode; - class AnimationSystem; - class AnimationSystemRealTime; class GeometryBinding; - class AnimationSystemImpl; class AttributeInput; class NodeImpl; class RenderGroup; class RenderPass; class RenderBuffer; class RenderTarget; - class DataFloat; - class DataVector2f; - class DataVector3f; - class DataVector4f; - class DataMatrix22f; - class DataMatrix33f; - class DataMatrix44f; - class DataInt32; - class DataVector2i; - class DataVector3i; - class DataVector4i; + class DataObject; class SceneConfigImpl; class RenderTargetDescriptionImpl; class BlitPass; @@ -88,7 +76,6 @@ namespace ramses class TextureSampler; class TextureSamplerMS; class TextureSamplerExternal; - class StreamTexture; class Texture2D; class Texture3D; class TextureCube; @@ -109,43 +96,34 @@ namespace ramses { public: SceneImpl(ramses_internal::ClientScene& scene, const SceneConfigImpl& sceneConfig, RamsesClient& ramsesClient); - virtual ~SceneImpl() override; + ~SceneImpl() override; void initializeFrameworkData(); - virtual void deinitializeFrameworkData() override final; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override final; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override final; - virtual status_t validate() const override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t validate() const override; - status_t publish(EScenePublicationMode publicationMode = EScenePublicationMode_LocalAndRemote); + status_t publish(EScenePublicationMode publicationMode = EScenePublicationMode::LocalAndRemote); status_t unpublish(); bool isPublished() const; sceneId_t getSceneId() const; EScenePublicationMode getPublicationModeSetFromSceneConfig() const; - status_t saveToFile(const char* fileName, bool compress) const; - bool saveResources(std::string const& fileName, bool compress) const; - - PerspectiveCamera* createPerspectiveCamera(const char* name); - OrthographicCamera* createOrthographicCamera(const char* name); - - Appearance* createAppearance(const Effect& effect, const char* name); - AppearanceImpl* createAppearanceImpl(const char* name); - - StreamTexture* createStreamTexture(const Texture2D& fallbackTexture, waylandIviSurfaceId_t source, const char* name); - GeometryBinding* createGeometryBinding(const Effect& effect, const char* name); - - Node* createNode(const char* name); - MeshNode* createMeshNode(const char* name); - - ramses::RenderGroup* createRenderGroup(const char* name); - ramses::RenderPass* createRenderPass(const char* name); - ramses::BlitPass* createBlitPass(const RenderBuffer& sourceRenderBuffer, const RenderBuffer& destinationRenderBuffer, const char* name); - - ramses::PickableObject* createPickableObject(const ArrayBuffer& geometryBuffer, const pickableObjectId_t id, const char* name); - - ramses::RenderBuffer* createRenderBuffer(uint32_t width, uint32_t height, ERenderBufferType bufferType, ERenderBufferFormat bufferFormat, ERenderBufferAccessMode accessMode, uint32_t sampleCount, const char* name); - ramses::RenderTarget* createRenderTarget(const RenderTargetDescriptionImpl& rtDesc, const char* name); + status_t saveToFile(std::string_view fileName, bool compress) const; + + PerspectiveCamera* createPerspectiveCamera(std::string_view name); + OrthographicCamera* createOrthographicCamera(std::string_view name); + Appearance* createAppearance(const Effect& effect, std::string_view name); + GeometryBinding* createGeometryBinding(const Effect& effect, std::string_view name); + Node* createNode(std::string_view name); + MeshNode* createMeshNode(std::string_view name); + ramses::RenderGroup* createRenderGroup(std::string_view name); + ramses::RenderPass* createRenderPass(std::string_view name); + ramses::BlitPass* createBlitPass(const RenderBuffer& sourceRenderBuffer, const RenderBuffer& destinationRenderBuffer, std::string_view name); + ramses::PickableObject* createPickableObject(const ArrayBuffer& geometryBuffer, const pickableObjectId_t id, std::string_view name); + ramses::RenderBuffer* createRenderBuffer(uint32_t width, uint32_t height, ERenderBufferType bufferType, ERenderBufferFormat bufferFormat, ERenderBufferAccessMode accessMode, uint32_t sampleCount, std::string_view name); + ramses::RenderTarget* createRenderTarget(const RenderTargetDescriptionImpl& rtDesc, std::string_view name); ramses::TextureSampler* createTextureSampler( ETextureAddressMode wrapUMode, @@ -154,7 +132,7 @@ namespace ramses ETextureSamplingMethod magSamplingMethod, uint32_t anisotropyLevel, const Texture2D& texture, - const char* name); + std::string_view name); ramses::TextureSampler* createTextureSampler( ETextureAddressMode wrapUMode, @@ -163,7 +141,7 @@ namespace ramses ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, const Texture3D& texture, - const char* name); + std::string_view name); ramses::TextureSampler* createTextureSampler( ETextureAddressMode wrapUMode, @@ -172,7 +150,7 @@ namespace ramses ETextureSamplingMethod magSamplingMethod, uint32_t anisotropyLevel, const TextureCube& texture, - const char* name); + std::string_view name); ramses::TextureSampler* createTextureSampler( ETextureAddressMode wrapUMode, @@ -181,7 +159,7 @@ namespace ramses ETextureSamplingMethod magSamplingMethod, uint32_t anisotropyLevel, const RenderBuffer& renderBuffer, - const char* name); + std::string_view name); ramses::TextureSampler* createTextureSampler( ETextureAddressMode wrapUMode, @@ -190,34 +168,16 @@ namespace ramses ETextureSamplingMethod magSamplingMethod, uint32_t anisotropyLevel, const Texture2DBuffer& textureBuffer, - const char* name); + std::string_view name); - ramses::TextureSampler* createTextureSampler( - ETextureAddressMode wrapUMode, - ETextureAddressMode wrapVMode, - ETextureSamplingMethod minSamplingMethod, - ETextureSamplingMethod magSamplingMethod, - const StreamTexture& streamTexture, - const char* name); - - ramses::TextureSamplerMS* createTextureSamplerMS(const RenderBuffer& renderBuffer, const char* name); + ramses::TextureSamplerMS* createTextureSamplerMS(const RenderBuffer& renderBuffer, std::string_view name); ramses::TextureSamplerExternal* createTextureSamplerExternal( ETextureSamplingMethod minSamplingMethod, ETextureSamplingMethod magSamplingMethod, - const char* name); - - DataFloat* createDataFloat(const char* name); - DataVector2f* createDataVector2f(const char* name); - DataVector3f* createDataVector3f(const char* name); - DataVector4f* createDataVector4f(const char* name); - DataMatrix22f* createDataMatrix22f(const char* name); - DataMatrix33f* createDataMatrix33f(const char* name); - DataMatrix44f* createDataMatrix44f(const char* name); - DataInt32* createDataInt32(const char* name); - DataVector2i* createDataVector2i(const char* name); - DataVector3i* createDataVector3i(const char* name); - DataVector4i* createDataVector4i(const char* name); + std::string_view name); + + DataObject* createDataObject(EDataType dataType, std::string_view name); status_t createTransformationDataProvider(const Node& node, dataProviderId_t id); status_t createTransformationDataConsumer(const Node& node, dataConsumerId_t id); @@ -229,16 +189,10 @@ namespace ramses status_t createTextureConsumer(const TextureSamplerMS& sampler, dataConsumerId_t id); status_t createTextureConsumer(const TextureSamplerExternal& sampler, dataConsumerId_t id); - AnimationSystem* createAnimationSystem(uint32_t flags, const char* name); - AnimationSystemRealTime* createRealTimeAnimationSystem(uint32_t flags, const char* name); + ArrayBuffer* createArrayBuffer(EDataType dataType, uint32_t maxNumElements, std::string_view name); + Texture2DBuffer* createTexture2DBuffer (size_t mipLevels, uint32_t width, uint32_t height, ETextureFormat textureFormat, std::string_view name); - ArrayBuffer* createArrayBuffer(EDataType dataType, uint32_t maxNumElements, const char* name); - ArrayBufferImpl* createArrayBufferImpl(EDataType dataType, uint32_t numElements, const char* name); - - Texture2DBuffer* createTexture2DBuffer (uint32_t mipLevels, uint32_t width, uint32_t height, ETextureFormat textureFormat, const char* name); - Texture2DBufferImpl* createTexture2DBufferImpl (uint32_t mipLevels, uint32_t width, uint32_t height, ETextureFormat textureFormat, const char* name); - - SceneReference* createSceneReference(sceneId_t referencedScene, const char* name); + SceneReference* createSceneReference(sceneId_t referencedScene, std::string_view name); status_t linkData(SceneReference* providerReference, dataProviderId_t providerId, SceneReference* consumerReference, dataConsumerId_t consumerId); status_t unlinkData(SceneReference* consumerReference, dataConsumerId_t consumerId); @@ -251,25 +205,30 @@ namespace ramses status_t resetUniformTimeMs(); int32_t getUniformTimeMs() const; - ArrayResource* createArrayResource(EDataType type, uint32_t numElements, const void* arrayData, resourceCacheFlag_t cacheFlag, const char* name); - Texture2D* createTexture2D(uint32_t width, uint32_t height, ETextureFormat format, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name); - Texture3D* createTexture3D(uint32_t width, uint32_t height, uint32_t depth, ETextureFormat format, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, resourceCacheFlag_t cacheFlag, const char* name); - TextureCube* createTextureCube(uint32_t size, ETextureFormat format, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, const char* name); - Effect* createEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, const char* name); + template + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + ArrayResource* createArrayResource(uint32_t numElements, const T* arrayData, resourceCacheFlag_t cacheFlag, std::string_view name); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + Texture2D* createTexture2D(uint32_t width, uint32_t height, ETextureFormat format, size_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, std::string_view name); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + Texture3D* createTexture3D(uint32_t width, uint32_t height, uint32_t depth, ETextureFormat format, size_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain, resourceCacheFlag_t cacheFlag, std::string_view name); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + TextureCube* createTextureCube(uint32_t size, ETextureFormat format, size_t mipMapCount, const CubeMipLevelData mipLevelData[], bool generateMipChain, const TextureSwizzle& swizzle, resourceCacheFlag_t cacheFlag, std::string_view name); + Effect* createEffect(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag, std::string_view name); std::string getLastEffectErrorMessages() const; - ArrayResource* createHLArrayResource(ramses_internal::ManagedResource const& resource, const char* name); - Texture2D* createHLTexture2D(ramses_internal::ManagedResource const& resource, const char* name); - Texture3D* createHLTexture3D(ramses_internal::ManagedResource const& resource, const char* name); - TextureCube* createHLTextureCube(ramses_internal::ManagedResource const& resource, const char* name); - Effect* createHLEffect(ramses_internal::ManagedResource const& resource, const char* name); + ArrayResource* createHLArrayResource(ramses_internal::ManagedResource const& resource, std::string_view name); + Texture2D* createHLTexture2D(ramses_internal::ManagedResource const& resource, std::string_view name); + Texture3D* createHLTexture3D(ramses_internal::ManagedResource const& resource, std::string_view name); + TextureCube* createHLTextureCube(ramses_internal::ManagedResource const& resource, std::string_view name); + Effect* createHLEffect(ramses_internal::ManagedResource const& resource, std::string_view name); const ramses_internal::ClientScene& getIScene() const; ramses_internal::ClientScene& getIScene(); bool containsSceneObject(const SceneObjectImpl& object) const; - const RamsesObject* findObjectByName(const char* name) const; - RamsesObject* findObjectByName(const char* name); + const RamsesObject* findObjectByName(std::string_view name) const; + RamsesObject* findObjectByName(std::string_view name); Resource* getResource(resourceId_t rid) const; const SceneObject* findObjectById(sceneObjectId_t id) const; @@ -313,15 +272,15 @@ namespace ramses ramses_internal::TextureSampler::ContentType contentType, ramses_internal::ResourceContentHash textureResourceHash, // The sampler stores either a texture, or... ramses_internal::MemoryHandle contentHandle, // a render target's color buffer, or a texture buffer, or a stream texture - const char* name /*= 0*/); + std::string_view name /*= 0*/); template status_t createTextureConsumerImpl(const SAMPLER& sampler, dataConsumerId_t id, bool checkDuplicate = true); - RenderPass* createRenderPassInternal(const char* name); - void registerCreatedObject(SceneObject& object); - void registerCreatedResourceObject(Resource& resource); - AnimationSystemImpl& createAnimationSystemImpl(uint32_t flags, ERamsesObjectType type, const char* name); + RenderPass* createRenderPassInternal(std::string_view name); + + template + T& registerCreatedResourceObject(std::unique_ptr resourceImpl); void removeAllDataSlotsForNode(const Node& node); @@ -339,7 +298,6 @@ namespace ramses status_t destroyCamera(Camera& camera); status_t destroyRenderGroup(RenderGroup& group); status_t destroyMeshNode(MeshNode& mesh); - status_t destroyAnimationSystem(AnimationSystem& animationSystem); status_t destroyNode(Node& node); status_t destroyDataObject(DataObject& dataObject); status_t destroyResource(Resource& resource); diff --git a/client/ramses-client/impl/SceneIterator.cpp b/client/ramses-client/impl/SceneIterator.cpp index 1c3b3aa05..c7660c07a 100644 --- a/client/ramses-client/impl/SceneIterator.cpp +++ b/client/ramses-client/impl/SceneIterator.cpp @@ -14,19 +14,15 @@ namespace ramses { - SceneIterator::SceneIterator(const RamsesClient& client) - : impl(new SceneIteratorImpl(client.impl.getListOfScenes())) + : m_impl{ std::make_unique(client.m_impl.getListOfScenes()) } { } - SceneIterator::~SceneIterator() - { - delete impl; - } + SceneIterator::~SceneIterator() = default; Scene* SceneIterator::getNext() { - return impl->getNext(); + return m_impl->getNext(); } } diff --git a/client/ramses-client/impl/SceneIteratorImpl.h b/client/ramses-client/impl/SceneIteratorImpl.h index 3c126f7fd..ee5dd0031 100644 --- a/client/ramses-client/impl/SceneIteratorImpl.h +++ b/client/ramses-client/impl/SceneIteratorImpl.h @@ -10,6 +10,7 @@ #define RAMSES_SCENEITERATORIMPL_H #include "IteratorImpl.h" +#include "RamsesClientImpl.h" #include "ramses-client-api/RamsesObjectTypes.h" #include "Collections/Vector.h" @@ -20,10 +21,21 @@ namespace ramses class SceneIteratorImpl : public IteratorImpl { public: - explicit SceneIteratorImpl(const std::vector& scenes) - : IteratorImpl(scenes) + explicit SceneIteratorImpl(const SceneVector& scenes) + : IteratorImpl{ TransformToScenePtrs(scenes) } { } + + private: + static std::vector TransformToScenePtrs(const SceneVector& scenes) + { + std::vector scenePtrs; + scenePtrs.reserve(scenes.size()); + for (auto& s : scenes) + scenePtrs.push_back(s.get()); + + return scenePtrs; + } }; } diff --git a/client/ramses-client/impl/SceneObjectImpl.cpp b/client/ramses-client/impl/SceneObjectImpl.cpp index ddc3051d4..fb43a61f6 100644 --- a/client/ramses-client/impl/SceneObjectImpl.cpp +++ b/client/ramses-client/impl/SceneObjectImpl.cpp @@ -13,7 +13,7 @@ namespace ramses { - SceneObjectImpl::SceneObjectImpl(SceneImpl& scene, ERamsesObjectType type, const char* name) + SceneObjectImpl::SceneObjectImpl(SceneImpl& scene, ERamsesObjectType type, std::string_view name) : ClientObjectImpl(scene.getClientImpl(), type, name) , m_scene(scene) , m_sceneObjectId(scene.getNextSceneObjectId()) diff --git a/client/ramses-client/impl/SceneObjectImpl.h b/client/ramses-client/impl/SceneObjectImpl.h index 6dcc70928..0096f8fbc 100644 --- a/client/ramses-client/impl/SceneObjectImpl.h +++ b/client/ramses-client/impl/SceneObjectImpl.h @@ -11,6 +11,8 @@ #include "ClientObjectImpl.h" +#include + namespace ramses_internal { class ClientScene; @@ -23,14 +25,14 @@ namespace ramses class SceneObjectImpl : public ClientObjectImpl { public: - explicit SceneObjectImpl(SceneImpl& scene, ERamsesObjectType type, const char* name); - virtual ~SceneObjectImpl() override; + explicit SceneObjectImpl(SceneImpl& scene, ERamsesObjectType type, std::string_view name); + ~SceneObjectImpl() override; // impl methods const SceneImpl& getSceneImpl() const; SceneImpl& getSceneImpl(); - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; const ramses_internal::ClientScene& getIScene() const; ramses_internal::ClientScene& getIScene(); diff --git a/client/ramses-client/impl/SceneObjectIterator.cpp b/client/ramses-client/impl/SceneObjectIterator.cpp index 54a386f46..eccabca8f 100644 --- a/client/ramses-client/impl/SceneObjectIterator.cpp +++ b/client/ramses-client/impl/SceneObjectIterator.cpp @@ -14,17 +14,14 @@ namespace ramses { SceneObjectIterator::SceneObjectIterator(const Scene& scene, ERamsesObjectType objectType) - : impl(new ObjectIteratorImpl(scene.impl.getObjectRegistry(), objectType)) + : m_impl{ std::make_unique(scene.m_impl.getObjectRegistry(), objectType) } { } - SceneObjectIterator::~SceneObjectIterator() - { - delete impl; - } + SceneObjectIterator::~SceneObjectIterator() = default; RamsesObject* SceneObjectIterator::getNext() { - return impl->getNext(); + return m_impl->getNext(); } } diff --git a/client/ramses-client/impl/SceneReferenceImpl.cpp b/client/ramses-client/impl/SceneReferenceImpl.cpp index 9e1ac21c5..71b35a87b 100644 --- a/client/ramses-client/impl/SceneReferenceImpl.cpp +++ b/client/ramses-client/impl/SceneReferenceImpl.cpp @@ -16,8 +16,8 @@ namespace ramses { - SceneReferenceImpl::SceneReferenceImpl(SceneImpl& scene, const char* name) - : SceneObjectImpl(scene, ERamsesObjectType_SceneReference, name) + SceneReferenceImpl::SceneReferenceImpl(SceneImpl& scene, std::string_view name) + : SceneObjectImpl(scene, ERamsesObjectType::SceneReference, name) { } diff --git a/client/ramses-client/impl/SceneReferenceImpl.h b/client/ramses-client/impl/SceneReferenceImpl.h index 0770d42ba..41b967b3e 100644 --- a/client/ramses-client/impl/SceneReferenceImpl.h +++ b/client/ramses-client/impl/SceneReferenceImpl.h @@ -18,18 +18,20 @@ #include "SceneAPI/Handles.h" #include "SceneAPI/RendererSceneState.h" +#include + namespace ramses { class SceneReferenceImpl final : public SceneObjectImpl { public: - SceneReferenceImpl(SceneImpl& scene, const char* name); + SceneReferenceImpl(SceneImpl& scene, std::string_view name); void initializeFrameworkData(sceneId_t referencedScene); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; status_t requestState(RendererSceneState requestedState); sceneId_t getReferencedSceneId() const; diff --git a/client/ramses-client/impl/SceneUtils.h b/client/ramses-client/impl/SceneUtils.h index 232263eb1..74db7beeb 100644 --- a/client/ramses-client/impl/SceneUtils.h +++ b/client/ramses-client/impl/SceneUtils.h @@ -23,14 +23,13 @@ namespace ramses { switch (publicationMode) { - case ramses::EScenePublicationMode_LocalAndRemote: + case ramses::EScenePublicationMode::LocalAndRemote: return ramses_internal::EScenePublicationMode_LocalAndRemote; - case ramses::EScenePublicationMode_LocalOnly: + case ramses::EScenePublicationMode::LocalOnly: return ramses_internal::EScenePublicationMode_LocalOnly; - default: - assert(false); - return ramses_internal::EScenePublicationMode_LocalAndRemote; } + assert(false); + return ramses_internal::EScenePublicationMode_LocalAndRemote; } }; } diff --git a/client/ramses-client/impl/SerializationContext.cpp b/client/ramses-client/impl/SerializationContext.cpp index d5782d275..7ba4c948b 100644 --- a/client/ramses-client/impl/SerializationContext.cpp +++ b/client/ramses-client/impl/SerializationContext.cpp @@ -57,14 +57,12 @@ namespace ramses return m_serializeSceneObjectIds; } - bool DeserializationContext::registerObjectImpl(RamsesObjectImpl* obj, ObjectIDType id) + void DeserializationContext::registerObjectImpl(RamsesObjectImpl* obj, ObjectIDType id) { if (m_objectImpls.size() <= id) - { m_objectImpls.resize(id + 1); - } + m_objectImpls[id] = obj; - return true; } void DeserializationContext::addForDependencyResolve(RamsesObjectImpl* obj) diff --git a/client/ramses-client/impl/SerializationContext.h b/client/ramses-client/impl/SerializationContext.h index a4458f416..ad591d40d 100644 --- a/client/ramses-client/impl/SerializationContext.h +++ b/client/ramses-client/impl/SerializationContext.h @@ -34,7 +34,7 @@ namespace ramses static ObjectIDType GetObjectIDNull(); // phase 1: deserialize - bool registerObjectImpl(RamsesObjectImpl* obj, ObjectIDType id); + void registerObjectImpl(RamsesObjectImpl* obj, ObjectIDType id); void addNodeHandleToNodeImplMapping(ramses_internal::NodeHandle nodeHandle, NodeImpl* node); template @@ -47,7 +47,7 @@ namespace ramses template void resolveDependencyIDImplAndStoreAsPointer(OBJECT_TYPE*& ptrId) const; - NodeImpl* getNodeImplForHandle(ramses_internal::NodeHandle) const; + [[nodiscard]] NodeImpl* getNodeImplForHandle(ramses_internal::NodeHandle) const; private: std::vector m_objectImpls; @@ -66,7 +66,7 @@ namespace ramses static ObjectIDType GetObjectIDNull(); void serializeSceneObjectIds(bool flag); - bool getSerializeSceneObjectIds() const; + [[nodiscard]] bool getSerializeSceneObjectIds() const; private: bool m_serializeSceneObjectIds = true; diff --git a/client/ramses-client/impl/SerializationHelper.h b/client/ramses-client/impl/SerializationHelper.h index 491a1c09a..a616c97ce 100644 --- a/client/ramses-client/impl/SerializationHelper.h +++ b/client/ramses-client/impl/SerializationHelper.h @@ -44,10 +44,10 @@ namespace ramses using TypeCountPair = std::pair; std::vector typesToSerialize; - typesToSerialize.reserve(ERamsesObjectType_NUMBER_OF_TYPES); + typesToSerialize.reserve(static_cast(ERamsesObjectType::NUMBER_OF_TYPES)); uint32_t totalCount = 0u; - for (uint32_t typeIdx = 0u; typeIdx < ERamsesObjectType_NUMBER_OF_TYPES; ++typeIdx) + for (uint32_t typeIdx = 0u; typeIdx < static_cast(ERamsesObjectType::NUMBER_OF_TYPES); ++typeIdx) { const ERamsesObjectType type = static_cast(typeIdx); if (RamsesObjectTypeUtils::IsConcreteType(type) && @@ -76,7 +76,7 @@ namespace ramses RamsesObjectRegistryIterator iter(registry, type); while (const ObjectsBaseType* obj = iter.getNext()) { - CHECK_RETURN_ERR(obj->impl.serialize(outStream, serializationContext)); + CHECK_RETURN_ERR(obj->m_impl.serialize(outStream, serializationContext)); } } @@ -91,7 +91,7 @@ namespace ramses static ERamsesObjectType DeserializeObjectTypeAndCount(ramses_internal::IInputStream& inStream, uint32_t& count) { - uint32_t typeInt = ERamsesObjectType_Invalid; + auto typeInt = static_cast(ERamsesObjectType::Invalid); inStream >> typeInt; inStream >> count; diff --git a/client/ramses-client/impl/SplineImpl.cpp b/client/ramses-client/impl/SplineImpl.cpp deleted file mode 100644 index ad80f43b5..000000000 --- a/client/ramses-client/impl/SplineImpl.cpp +++ /dev/null @@ -1,591 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "SplineImpl.h" -#include "AnimationSystemImpl.h" -#include "Math3d/Vector2.h" -#include "Math3d/Vector3.h" -#include "Math3d/Vector4.h" -#include "Math3d/Vector2i.h" -#include "Math3d/Vector3i.h" -#include "Math3d/Vector4i.h" -#include "SerializationContext.h" -#include "Animation/SplineBase.h" -#include "Animation/Spline.h" -#include "Animation/SplineKey.h" -#include "Animation/SplineKeyTangents.h" -#include "AnimationAPI/IAnimationSystem.h" - -namespace ramses -{ - SplineImpl::SplineImpl(AnimationSystemImpl& animationSystem, ERamsesObjectType type, const char* name) - : AnimationObjectImpl(animationSystem, type, name) - { - } - - SplineImpl::~SplineImpl() - { - } - - status_t SplineImpl::setSplineKeyStepBool(splineTimeStamp_t timeStamp, bool value) - { - getIAnimationSystem().setSplineKeyBasicBool(m_splineHandle, timeStamp, value); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyStepInt32(splineTimeStamp_t timeStamp, int32_t value) - { - getIAnimationSystem().setSplineKeyBasicInt32(m_splineHandle, timeStamp, value); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyStepFloat(splineTimeStamp_t timeStamp, float value) - { - getIAnimationSystem().setSplineKeyBasicFloat(m_splineHandle, timeStamp, value); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyStepVector2f(splineTimeStamp_t timeStamp, float x, float y) - { - getIAnimationSystem().setSplineKeyBasicVector2f(m_splineHandle, timeStamp, ramses_internal::Vector2(x, y)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyStepVector3f(splineTimeStamp_t timeStamp, float x, float y, float z) - { - getIAnimationSystem().setSplineKeyBasicVector3f(m_splineHandle, timeStamp, ramses_internal::Vector3(x, y, z)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyStepVector4f(splineTimeStamp_t timeStamp, float x, float y, float z, float w) - { - getIAnimationSystem().setSplineKeyBasicVector4f(m_splineHandle, timeStamp, ramses_internal::Vector4(x, y, z, w)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyStepVector2i(splineTimeStamp_t timeStamp, int32_t x, int32_t y) - { - getIAnimationSystem().setSplineKeyBasicVector2i(m_splineHandle, timeStamp, ramses_internal::Vector2i(x, y)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyStepVector3i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z) - { - getIAnimationSystem().setSplineKeyBasicVector3i(m_splineHandle, timeStamp, ramses_internal::Vector3i(x, y, z)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyStepVector4i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w) - { - getIAnimationSystem().setSplineKeyBasicVector4i(m_splineHandle, timeStamp, ramses_internal::Vector4i(x, y, z, w)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyLinearInt32(splineTimeStamp_t timeStamp, int32_t value) - { - getIAnimationSystem().setSplineKeyBasicInt32(m_splineHandle, timeStamp, value); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyLinearFloat(splineTimeStamp_t timeStamp, float value) - { - getIAnimationSystem().setSplineKeyBasicFloat(m_splineHandle, timeStamp, value); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyLinearVector2f(splineTimeStamp_t timeStamp, float x, float y) - { - getIAnimationSystem().setSplineKeyBasicVector2f(m_splineHandle, timeStamp, ramses_internal::Vector2(x, y)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyLinearVector3f(splineTimeStamp_t timeStamp, float x, float y, float z) - { - getIAnimationSystem().setSplineKeyBasicVector3f(m_splineHandle, timeStamp, ramses_internal::Vector3(x, y, z)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyLinearVector4f(splineTimeStamp_t timeStamp, float x, float y, float z, float w) - { - getIAnimationSystem().setSplineKeyBasicVector4f(m_splineHandle, timeStamp, ramses_internal::Vector4(x, y, z, w)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyLinearVector2i(splineTimeStamp_t timeStamp, int32_t x, int32_t y) - { - getIAnimationSystem().setSplineKeyBasicVector2i(m_splineHandle, timeStamp, ramses_internal::Vector2i(x, y)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyLinearVector3i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z) - { - getIAnimationSystem().setSplineKeyBasicVector3i(m_splineHandle, timeStamp, ramses_internal::Vector3i(x, y, z)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyLinearVector4i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w) - { - getIAnimationSystem().setSplineKeyBasicVector4i(m_splineHandle, timeStamp, ramses_internal::Vector4i(x, y, z, w)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyBezierInt32(splineTimeStamp_t timeStamp, int32_t value, float tanInX, float tanInY, float tanOutX, float tanOutY) - { - getIAnimationSystem().setSplineKeyTangentsInt32(m_splineHandle, timeStamp, value, ramses_internal::Vector2(tanInX, tanInY), ramses_internal::Vector2(tanOutX, tanOutY)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyBezierFloat(splineTimeStamp_t timeStamp, float value, float tanInX, float tanInY, float tanOutX, float tanOutY) - { - getIAnimationSystem().setSplineKeyTangentsFloat(m_splineHandle, timeStamp, value, ramses_internal::Vector2(tanInX, tanInY), ramses_internal::Vector2(tanOutX, tanOutY)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyBezierVector2f(splineTimeStamp_t timeStamp, float x, float y, float tanInX, float tanInY, float tanOutX, float tanOutY) - { - getIAnimationSystem().setSplineKeyTangentsVector2f(m_splineHandle, timeStamp, ramses_internal::Vector2(x, y), ramses_internal::Vector2(tanInX, tanInY), ramses_internal::Vector2(tanOutX, tanOutY)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyBezierVector3f(splineTimeStamp_t timeStamp, float x, float y, float z, float tanInX, float tanInY, float tanOutX, float tanOutY) - { - getIAnimationSystem().setSplineKeyTangentsVector3f(m_splineHandle, timeStamp, ramses_internal::Vector3(x, y, z), ramses_internal::Vector2(tanInX, tanInY), ramses_internal::Vector2(tanOutX, tanOutY)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyBezierVector4f(splineTimeStamp_t timeStamp, float x, float y, float z, float w, float tanInX, float tanInY, float tanOutX, float tanOutY) - { - getIAnimationSystem().setSplineKeyTangentsVector4f(m_splineHandle, timeStamp, ramses_internal::Vector4(x, y, z, w), ramses_internal::Vector2(tanInX, tanInY), ramses_internal::Vector2(tanOutX, tanOutY)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyBezierVector2i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, float tanInX, float tanInY, float tanOutX, float tanOutY) - { - getIAnimationSystem().setSplineKeyTangentsVector2i(m_splineHandle, timeStamp, ramses_internal::Vector2i(x, y), ramses_internal::Vector2(tanInX, tanInY), ramses_internal::Vector2(tanOutX, tanOutY)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyBezierVector3i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, float tanInX, float tanInY, float tanOutX, float tanOutY) - { - getIAnimationSystem().setSplineKeyTangentsVector3i(m_splineHandle, timeStamp, ramses_internal::Vector3i(x, y, z), ramses_internal::Vector2(tanInX, tanInY), ramses_internal::Vector2(tanOutX, tanOutY)); - return StatusOK; - } - - status_t SplineImpl::setSplineKeyBezierVector4i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w, float tanInX, float tanInY, float tanOutX, float tanOutY) - { - getIAnimationSystem().setSplineKeyTangentsVector4i(m_splineHandle, timeStamp, ramses_internal::Vector4i(x, y, z, w), ramses_internal::Vector2(tanInX, tanInY), ramses_internal::Vector2(tanOutX, tanOutY)); - return StatusOK; - } - - status_t SplineImpl::getSplineKeyBool(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, bool& value) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - value = getSplineKeyValue(spline, keyIndex); - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyInt32(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& value) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - value = getSplineKeyValue(spline, keyIndex); - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyFloat(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& value) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - value = getSplineKeyValue(spline, keyIndex); - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyVector2f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector2& value = getSplineKeyValue(spline, keyIndex); - x = value.x; - y = value.y; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyVector3f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector3& value = getSplineKeyValue(spline, keyIndex); - x = value.x; - y = value.y; - z = value.z; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyVector4f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& w) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector4& value = getSplineKeyValue(spline, keyIndex); - x = value.x; - y = value.y; - z = value.z; - w = value.w; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyVector2i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector2i& value = getSplineKeyValue(spline, keyIndex); - x = value.x; - y = value.y; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyVector3i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector3i& value = getSplineKeyValue(spline, keyIndex); - x = value.x; - y = value.y; - z = value.z; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyVector4i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, int32_t& w) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector4i& value = getSplineKeyValue(spline, keyIndex); - x = value.x; - y = value.y; - z = value.z; - w = value.w; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyTangentsInt32(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& value, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - value = getSplineKeyTangentsValue(spline, keyIndex, tanInX, tanInY, tanOutX, tanOutY); - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyTangentsFloat(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& value, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - value = getSplineKeyTangentsValue(spline, keyIndex, tanInX, tanInY, tanOutX, tanOutY); - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyTangentsVector2f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector2& value = getSplineKeyTangentsValue(spline, keyIndex, tanInX, tanInY, tanOutX, tanOutY); - x = value.x; - y = value.y; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyTangentsVector3f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector3& value = getSplineKeyTangentsValue(spline, keyIndex, tanInX, tanInY, tanOutX, tanOutY); - x = value.x; - y = value.y; - z = value.z; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyTangentsVector4f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& w, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector4& value = getSplineKeyTangentsValue(spline, keyIndex, tanInX, tanInY, tanOutX, tanOutY); - x = value.x; - y = value.y; - z = value.z; - w = value.w; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyTangentsVector2i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector2i& value = getSplineKeyTangentsValue(spline, keyIndex, tanInX, tanInY, tanOutX, tanOutY); - x = value.x; - y = value.y; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyTangentsVector3i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector3i& value = getSplineKeyTangentsValue(spline, keyIndex, tanInX, tanInY, tanOutX, tanOutY); - x = value.x; - y = value.y; - z = value.z; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::getSplineKeyTangentsVector4i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, int32_t& w, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - CHECK_RETURN_ERR(checkIfKeyIndexValid(spline, keyIndex)); - const ramses_internal::Vector4i& value = getSplineKeyTangentsValue(spline, keyIndex, tanInX, tanInY, tanOutX, tanOutY); - x = value.x; - y = value.y; - z = value.z; - w = value.w; - timeStamp = spline->getTimeStamp(keyIndex); - - return StatusOK; - } - - status_t SplineImpl::serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const - { - CHECK_RETURN_ERR(AnimationObjectImpl::serialize(outStream, serializationContext)); - - outStream << m_splineHandle; - - return StatusOK; - } - - status_t SplineImpl::deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) - { - CHECK_RETURN_ERR(AnimationObjectImpl::deserialize(inStream, serializationContext)); - - inStream >> m_splineHandle; - - return StatusOK; - } - - status_t SplineImpl::validate() const - { - status_t status = AnimationObjectImpl::validate(); - - const ramses_internal::SplineBase& spline = *getIAnimationSystem().getSpline(m_splineHandle); - if (spline.getNumKeys() == 0u) - status = std::max(status, addValidationMessage(EValidationSeverity_Warning, "spline has no keys assigned")); - - return status; - } - - void SplineImpl::initializeFrameworkData(ramses_internal::EInterpolationType interpolationType, ramses_internal::EDataTypeID dataTypeID) - { - const ramses_internal::ESplineKeyType keyType = GetKeyTypeForInterpolation(interpolationType); - m_splineHandle = getIAnimationSystem().allocateSpline(keyType, dataTypeID); - } - - void SplineImpl::deinitializeFrameworkData() - { - getIAnimationSystem().removeSpline(m_splineHandle); - m_splineHandle = ramses_internal::SplineHandle::Invalid(); - } - - ramses_internal::SplineHandle SplineImpl::getSplineHandle() const - { - return m_splineHandle; - } - - uint32_t SplineImpl::getNumKeys() const - { - const ramses_internal::SplineBase* spline = getIAnimationSystem().getSpline(m_splineHandle); - if (spline != nullptr) - { - return spline->getNumKeys(); - } - - return 0u; - } - - ramses_internal::EDataTypeID SplineImpl::GetDataTypeForSplineType(ERamsesObjectType splineType) - { - switch (splineType) - { - case ramses::ERamsesObjectType_SplineStepBool: - return ramses_internal::EDataTypeID_Boolean; - case ramses::ERamsesObjectType_SplineStepFloat: - case ramses::ERamsesObjectType_SplineLinearFloat: - case ramses::ERamsesObjectType_SplineBezierFloat: - return ramses_internal::EDataTypeID_Float; - case ramses::ERamsesObjectType_SplineStepInt32: - case ramses::ERamsesObjectType_SplineLinearInt32: - case ramses::ERamsesObjectType_SplineBezierInt32: - return ramses_internal::EDataTypeID_Int32; - case ramses::ERamsesObjectType_SplineStepVector2f: - case ramses::ERamsesObjectType_SplineLinearVector2f: - case ramses::ERamsesObjectType_SplineBezierVector2f: - return ramses_internal::EDataTypeID_Vector2f; - case ramses::ERamsesObjectType_SplineStepVector3f: - case ramses::ERamsesObjectType_SplineLinearVector3f: - case ramses::ERamsesObjectType_SplineBezierVector3f: - return ramses_internal::EDataTypeID_Vector3f; - case ramses::ERamsesObjectType_SplineStepVector4f: - case ramses::ERamsesObjectType_SplineLinearVector4f: - case ramses::ERamsesObjectType_SplineBezierVector4f: - return ramses_internal::EDataTypeID_Vector4f; - case ramses::ERamsesObjectType_SplineStepVector2i: - case ramses::ERamsesObjectType_SplineLinearVector2i: - case ramses::ERamsesObjectType_SplineBezierVector2i: - return ramses_internal::EDataTypeID_Vector2i; - case ramses::ERamsesObjectType_SplineStepVector3i: - case ramses::ERamsesObjectType_SplineLinearVector3i: - case ramses::ERamsesObjectType_SplineBezierVector3i: - return ramses_internal::EDataTypeID_Vector3i; - case ramses::ERamsesObjectType_SplineStepVector4i: - case ramses::ERamsesObjectType_SplineLinearVector4i: - case ramses::ERamsesObjectType_SplineBezierVector4i: - return ramses_internal::EDataTypeID_Vector4i; - default: - assert(false); - return ramses_internal::EDataTypeID_Invalid; - } - } - - ramses_internal::EInterpolationType SplineImpl::GetInterpolationTypeForSplineType(ERamsesObjectType splineType) - { - switch (splineType) - { - case ramses::ERamsesObjectType_SplineStepBool: - case ramses::ERamsesObjectType_SplineStepFloat: - case ramses::ERamsesObjectType_SplineStepInt32: - case ramses::ERamsesObjectType_SplineStepVector2f: - case ramses::ERamsesObjectType_SplineStepVector3f: - case ramses::ERamsesObjectType_SplineStepVector4f: - case ramses::ERamsesObjectType_SplineStepVector2i: - case ramses::ERamsesObjectType_SplineStepVector3i: - case ramses::ERamsesObjectType_SplineStepVector4i: - return ramses_internal::EInterpolationType_Step; - case ramses::ERamsesObjectType_SplineLinearFloat: - case ramses::ERamsesObjectType_SplineLinearInt32: - case ramses::ERamsesObjectType_SplineLinearVector2f: - case ramses::ERamsesObjectType_SplineLinearVector3f: - case ramses::ERamsesObjectType_SplineLinearVector4f: - case ramses::ERamsesObjectType_SplineLinearVector2i: - case ramses::ERamsesObjectType_SplineLinearVector3i: - case ramses::ERamsesObjectType_SplineLinearVector4i: - return ramses_internal::EInterpolationType_Linear; - case ramses::ERamsesObjectType_SplineBezierFloat: - case ramses::ERamsesObjectType_SplineBezierInt32: - case ramses::ERamsesObjectType_SplineBezierVector2f: - case ramses::ERamsesObjectType_SplineBezierVector3f: - case ramses::ERamsesObjectType_SplineBezierVector4f: - case ramses::ERamsesObjectType_SplineBezierVector2i: - case ramses::ERamsesObjectType_SplineBezierVector3i: - case ramses::ERamsesObjectType_SplineBezierVector4i: - return ramses_internal::EInterpolationType_Bezier; - default: - assert(false); - return ramses_internal::EInterpolationType_Invalid; - } - } - - ramses_internal::ESplineKeyType SplineImpl::GetKeyTypeForInterpolation(ramses_internal::EInterpolationType interpolationType) - { - switch (interpolationType) - { - case ramses_internal::EInterpolationType_Step: - case ramses_internal::EInterpolationType_Linear: - return ramses_internal::ESplineKeyType_Basic; - case ramses_internal::EInterpolationType_Bezier: - return ramses_internal::ESplineKeyType_Tangents; - default: - assert(false); - return ramses_internal::ESplineKeyType_Invalid; - } - } - - status_t SplineImpl::checkIfKeyIndexValid(const ramses_internal::SplineBase* spline, ramses_internal::SplineKeyIndex keyIndex) const - { - assert(spline != nullptr); - if (keyIndex >= spline->getNumKeys()) - { - return addErrorEntry("Spline::getKey failed, invalid spline key index."); - } - - return StatusOK; - } - - template - const EDataType& SplineImpl::getSplineKeyValue(const ramses_internal::SplineBase* spline, ramses_internal::SplineKeyIndex keyIndex) - { - using SplineKeyType = ramses_internal::SplineKey; - using SplineType = ramses_internal::Spline; - - assert(spline != nullptr); - assert(spline->getKeyType() == ramses_internal::ESplineKeyType_Basic); - assert(spline->getDataType() == ramses_internal::DataTypeToDataIDSelector::DataTypeID); - - const SplineType& splineTyped = static_cast(*spline); - const SplineKeyType& key = splineTyped.getKey(keyIndex); - return key.m_value; - } - - template - const EDataType& SplineImpl::getSplineKeyTangentsValue(const ramses_internal::SplineBase* spline, ramses_internal::SplineKeyIndex keyIndex, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) - { - using SplineKeyType = ramses_internal::SplineKeyTangents; - using SplineType = ramses_internal::Spline; - - assert(spline != nullptr); - assert(spline->getKeyType() == ramses_internal::ESplineKeyType_Tangents); - assert(spline->getDataType() == ramses_internal::DataTypeToDataIDSelector::DataTypeID); - - const SplineType& splineTyped = static_cast(*spline); - const SplineKeyType& key = splineTyped.getKey(keyIndex); - - tanInX = key.m_tangentIn.x; - tanInY = key.m_tangentIn.y; - tanOutX = key.m_tangentOut.x; - tanOutY = key.m_tangentOut.y; - - return key.m_value; - } -} diff --git a/client/ramses-client/impl/SplineImpl.h b/client/ramses-client/impl/SplineImpl.h deleted file mode 100644 index 14e3488da..000000000 --- a/client/ramses-client/impl/SplineImpl.h +++ /dev/null @@ -1,102 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINEIMPL_H -#define RAMSES_SPLINEIMPL_H - -// API -#include "ramses-client-api/AnimationTypes.h" - -// internal -#include "AnimationObjectImpl.h" -#include "Utils/DataTypeUtils.h" -#include "Animation/AnimationCommon.h" - -namespace ramses_internal -{ - class SplineBase; -} - -namespace ramses -{ - class SplineImpl final : public AnimationObjectImpl - { - public: - explicit SplineImpl(AnimationSystemImpl& animationSystem, ERamsesObjectType type, const char* name); - virtual ~SplineImpl() override; - - void initializeFrameworkData(ramses_internal::EInterpolationType interpolationType, ramses_internal::EDataTypeID dataTypeID); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t validate() const override; - - status_t setSplineKeyStepBool(splineTimeStamp_t timeStamp, bool value); - status_t setSplineKeyStepInt32(splineTimeStamp_t timeStamp, int32_t value); - status_t setSplineKeyStepFloat(splineTimeStamp_t timeStamp, float value); - status_t setSplineKeyStepVector2f(splineTimeStamp_t timeStamp, float x, float y); - status_t setSplineKeyStepVector3f(splineTimeStamp_t timeStamp, float x, float y, float z); - status_t setSplineKeyStepVector4f(splineTimeStamp_t timeStamp, float x, float y, float z, float w); - status_t setSplineKeyStepVector2i(splineTimeStamp_t timeStamp, int32_t x, int32_t y); - status_t setSplineKeyStepVector3i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z); - status_t setSplineKeyStepVector4i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w); - status_t setSplineKeyLinearInt32(splineTimeStamp_t timeStamp, int32_t value); - status_t setSplineKeyLinearFloat(splineTimeStamp_t timeStamp, float value); - status_t setSplineKeyLinearVector2f(splineTimeStamp_t timeStamp, float x, float y); - status_t setSplineKeyLinearVector3f(splineTimeStamp_t timeStamp, float x, float y, float z); - status_t setSplineKeyLinearVector4f(splineTimeStamp_t timeStamp, float x, float y, float z, float w); - status_t setSplineKeyLinearVector2i(splineTimeStamp_t timeStamp, int32_t x, int32_t y); - status_t setSplineKeyLinearVector3i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z); - status_t setSplineKeyLinearVector4i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w); - status_t setSplineKeyBezierInt32(splineTimeStamp_t timeStamp, int32_t value, float tanInX, float tanInY, float tanOutX, float tanOutY); - status_t setSplineKeyBezierFloat(splineTimeStamp_t timeStamp, float value, float tanInX, float tanInY, float tanOutX, float tanOutY); - status_t setSplineKeyBezierVector2f(splineTimeStamp_t timeStamp, float x, float y, float tanInX, float tanInY, float tanOutX, float tanOutY); - status_t setSplineKeyBezierVector3f(splineTimeStamp_t timeStamp, float x, float y, float z, float tanInX, float tanInY, float tanOutX, float tanOutY); - status_t setSplineKeyBezierVector4f(splineTimeStamp_t timeStamp, float x, float y, float z, float w, float tanInX, float tanInY, float tanOutX, float tanOutY); - status_t setSplineKeyBezierVector2i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, float tanInX, float tanInY, float tanOutX, float tanOutY); - status_t setSplineKeyBezierVector3i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, float tanInX, float tanInY, float tanOutX, float tanOutY); - status_t setSplineKeyBezierVector4i(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w, float tanInX, float tanInY, float tanOutX, float tanOutY); - - status_t getSplineKeyBool (splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, bool& value) const; - status_t getSplineKeyInt32 (splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& value) const; - status_t getSplineKeyFloat (splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& value) const; - status_t getSplineKeyVector2f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y) const; - status_t getSplineKeyVector3f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z) const; - status_t getSplineKeyVector4f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& w) const; - status_t getSplineKeyVector2i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y) const; - status_t getSplineKeyVector3i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z) const; - status_t getSplineKeyVector4i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, int32_t& w) const; - status_t getSplineKeyTangentsInt32(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& value, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const; - status_t getSplineKeyTangentsFloat(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& value, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const; - status_t getSplineKeyTangentsVector2f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const; - status_t getSplineKeyTangentsVector3f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const; - status_t getSplineKeyTangentsVector4f(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& w, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const; - status_t getSplineKeyTangentsVector2i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const; - status_t getSplineKeyTangentsVector3i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const; - status_t getSplineKeyTangentsVector4i(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, int32_t& w, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY) const; - - ramses_internal::SplineHandle getSplineHandle() const; - uint32_t getNumKeys() const; - - static ramses_internal::EDataTypeID GetDataTypeForSplineType(ERamsesObjectType splineType); - static ramses_internal::EInterpolationType GetInterpolationTypeForSplineType(ERamsesObjectType splineType); - static ramses_internal::ESplineKeyType GetKeyTypeForInterpolation(ramses_internal::EInterpolationType interpolationType); - - private: - status_t checkIfKeyIndexValid(const ramses_internal::SplineBase* spline, ramses_internal::SplineKeyIndex keyIndex) const; - - template - static const EDataType& getSplineKeyValue(const ramses_internal::SplineBase* spline, ramses_internal::SplineKeyIndex keyIndex); - template - static const EDataType& getSplineKeyTangentsValue(const ramses_internal::SplineBase* spline, ramses_internal::SplineKeyIndex keyIndex, float& tanInX, float& tanInY, float& tanOutX, float& tanOutY); - - ramses_internal::SplineHandle m_splineHandle; - }; -} - -#endif diff --git a/client/ramses-client/impl/StreamTextureImpl.cpp b/client/ramses-client/impl/StreamTextureImpl.cpp deleted file mode 100644 index 97306d338..000000000 --- a/client/ramses-client/impl/StreamTextureImpl.cpp +++ /dev/null @@ -1,104 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "StreamTextureImpl.h" -#include "Collections/IOutputStream.h" -#include "Collections/IInputStream.h" -#include "Scene/ClientScene.h" -#include "RamsesClientImpl.h" -#include "Texture2DImpl.h" -#include "ramses-client-api/Texture2D.h" -#include "RamsesObjectTypeUtils.h" -#include "SceneAPI/WaylandIviSurfaceId.h" - -namespace ramses -{ - StreamTextureImpl::StreamTextureImpl(SceneImpl& scene, const char* name) - : SceneObjectImpl(scene, ERamsesObjectType_StreamTexture, name ) - , m_streamTextureHandle(ramses_internal::StreamTextureHandle::Invalid()) - { - } - - StreamTextureImpl::~StreamTextureImpl() - { - } - - ramses::status_t StreamTextureImpl::serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const - { - CHECK_RETURN_ERR(SceneObjectImpl::serialize(outStream, serializationContext)); - - outStream << m_streamTextureHandle; - - return StatusOK; - } - - status_t StreamTextureImpl::deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) - { - CHECK_RETURN_ERR(SceneObjectImpl::deserialize(inStream, serializationContext)); - - inStream >> m_streamTextureHandle; - - return StatusOK; - } - - bool StreamTextureImpl::getForceFallbackImage() const - { - return getIScene().getStreamTexture(m_streamTextureHandle).forceFallbackTexture; - } - - waylandIviSurfaceId_t StreamTextureImpl::getStreamSource() const - { - return waylandIviSurfaceId_t{ getIScene().getStreamTexture(m_streamTextureHandle).source.getValue() }; - } - - void StreamTextureImpl::initializeFrameworkData(waylandIviSurfaceId_t source, const Texture2DImpl& fallbackTexture) - { - assert(!m_streamTextureHandle.isValid()); - m_streamTextureHandle = getIScene().allocateStreamTexture(ramses_internal::WaylandIviSurfaceId{ source.getValue() }, fallbackTexture.getLowlevelResourceHash()); - assert(m_streamTextureHandle.isValid()); - } - - void StreamTextureImpl::deinitializeFrameworkData() - { - assert(m_streamTextureHandle.isValid()); - getIScene().releaseStreamTexture(m_streamTextureHandle); - m_streamTextureHandle = ramses_internal::StreamTextureHandle::Invalid(); - } - - ramses_internal::StreamTextureHandle StreamTextureImpl::getHandle() const - { - return m_streamTextureHandle; - } - - ramses_internal::ResourceContentHash StreamTextureImpl::getFallbackTextureHash() const - { - return getIScene().getStreamTexture(m_streamTextureHandle).fallbackTexture; - } - - status_t StreamTextureImpl::validate() const - { - status_t status = SceneObjectImpl::validate(); - - const ramses_internal::ResourceContentHash fallbackTextureHash = getFallbackTextureHash(); - const Resource* resource = getSceneImpl().scanForResourceWithHash(fallbackTextureHash); - if (!resource) - return addValidationMessage(EValidationSeverity_Error, "StreamTexture is using a fallback texture which does not exist"); - - assert(resource->getType() == ERamsesObjectType_Texture2D); - const Texture2D& texture = RamsesObjectTypeUtils::ConvertTo(*resource); - return std::max(status, addValidationOfDependentObject(texture.impl)); - } - - status_t StreamTextureImpl::forceFallbackImage(bool forceFallbackImage) - { - getIScene().setForceFallbackImage(m_streamTextureHandle, forceFallbackImage); - return StatusOK; - } - -} - diff --git a/client/ramses-client/impl/StreamTextureImpl.h b/client/ramses-client/impl/StreamTextureImpl.h deleted file mode 100644 index 8ab9e4972..000000000 --- a/client/ramses-client/impl/StreamTextureImpl.h +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_STREAMTEXTUREIMPL_H -#define RAMSES_STREAMTEXTUREIMPL_H - -#include "SceneObjectImpl.h" -#include "SceneAPI/Handles.h" -#include "SceneAPI/ResourceContentHash.h" - -namespace ramses -{ - class Texture2DImpl; - - class StreamTextureImpl final : public SceneObjectImpl - { - public: - StreamTextureImpl(SceneImpl& scene, const char* name); - virtual ~StreamTextureImpl() override; - - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - - void initializeFrameworkData(waylandIviSurfaceId_t source, const Texture2DImpl& fallbackTexture); - virtual void deinitializeFrameworkData() override; - virtual status_t validate() const override; - - status_t forceFallbackImage(bool forceFallbackImage); - bool getForceFallbackImage() const; - - waylandIviSurfaceId_t getStreamSource() const; - ramses_internal::StreamTextureHandle getHandle() const; - ramses_internal::ResourceContentHash getFallbackTextureHash() const; - - private: - ramses_internal::StreamTextureHandle m_streamTextureHandle; - }; -} - -#endif - diff --git a/client/ramses-client/impl/Texture2DBufferImpl.cpp b/client/ramses-client/impl/Texture2DBufferImpl.cpp index ea4df424c..4d58fba08 100644 --- a/client/ramses-client/impl/Texture2DBufferImpl.cpp +++ b/client/ramses-client/impl/Texture2DBufferImpl.cpp @@ -14,8 +14,8 @@ namespace ramses { - Texture2DBufferImpl::Texture2DBufferImpl(SceneImpl& scene, const char* textureBufferName) - : SceneObjectImpl(scene, ERamsesObjectType_Texture2DBuffer, textureBufferName) + Texture2DBufferImpl::Texture2DBufferImpl(SceneImpl& scene, std::string_view textureBufferName) + : SceneObjectImpl(scene, ERamsesObjectType::Texture2DBuffer, textureBufferName) { } @@ -59,7 +59,7 @@ namespace ramses return StatusOK; } - status_t Texture2DBufferImpl::setData(const ramses_internal::Byte* data, uint32_t mipLevel, uint32_t offsetX, uint32_t offsetY, uint32_t width, uint32_t height) + status_t Texture2DBufferImpl::setData(const ramses_internal::Byte* data, size_t mipLevel, uint32_t offsetX, uint32_t offsetY, uint32_t width, uint32_t height) { if (width == 0 || height == 0) { @@ -79,14 +79,14 @@ namespace ramses return addErrorEntry("Texture2DBuffer::setData failed, updated subregion exceeds the size of the target mipLevel."); } - getIScene().updateTextureBuffer(m_textureBufferHandle, mipLevel, offsetX, offsetY, width, height, data); + getIScene().updateTextureBuffer(m_textureBufferHandle, static_cast(mipLevel), offsetX, offsetY, width, height, data); return StatusOK; } - uint32_t Texture2DBufferImpl::getMipLevelCount() const + size_t Texture2DBufferImpl::getMipLevelCount() const { - return static_cast(getIScene().getTextureBuffer(m_textureBufferHandle).mipMaps.size()); + return getIScene().getTextureBuffer(m_textureBufferHandle).mipMaps.size(); } ETextureFormat Texture2DBufferImpl::getTexelFormat() const @@ -94,7 +94,7 @@ namespace ramses return TextureUtils::GetTextureFormatFromInternal(getIScene().getTextureBuffer(m_textureBufferHandle).textureFormat); } - status_t Texture2DBufferImpl::getMipLevelData(uint32_t mipLevel, char* buffer, uint32_t bufferSize) const + status_t Texture2DBufferImpl::getMipLevelData(size_t mipLevel, char* buffer, size_t bufferSize) const { const auto& texBuffer = getIScene().getTextureBuffer(m_textureBufferHandle); if (mipLevel >= texBuffer.mipMaps.size()) @@ -103,13 +103,13 @@ namespace ramses } const auto& mipData = texBuffer.mipMaps[mipLevel].data; - const uint32_t dataSizeToCopy = std::min(bufferSize, static_cast(mipData.size())); + const size_t dataSizeToCopy = std::min(bufferSize, mipData.size()); ramses_internal::PlatformMemory::Copy(buffer, mipData.data(), dataSizeToCopy); return StatusOK; } - status_t Texture2DBufferImpl::getMipLevelSize(uint32_t mipLevel, uint32_t& widthOut, uint32_t& heightOut) const + status_t Texture2DBufferImpl::getMipLevelSize(size_t mipLevel, uint32_t& widthOut, uint32_t& heightOut) const { const auto& mipMaps = getIScene().getTextureBuffer(m_textureBufferHandle).mipMaps; if (mipLevel >= mipMaps.size()) @@ -123,7 +123,7 @@ namespace ramses return StatusOK; } - uint32_t Texture2DBufferImpl::getMipLevelDataSizeInBytes(uint32_t mipLevel) const + size_t Texture2DBufferImpl::getMipLevelDataSizeInBytes(size_t mipLevel) const { const auto& mipMaps = getIScene().getTextureBuffer(m_textureBufferHandle).mipMaps; if (mipLevel >= mipMaps.size()) @@ -145,10 +145,10 @@ namespace ramses }); if (usedAsInput && !isInitialized) - return addValidationMessage(EValidationSeverity_Warning, "TextureBuffer is used in a sampler but there is no data set, this could lead to graphical glitches if actually rendered."); + return addValidationMessage(EValidationSeverity::Warning, "TextureBuffer is used in a sampler but there is no data set, this could lead to graphical glitches if actually rendered."); if (!usedAsInput) - return addValidationMessage(EValidationSeverity_Warning, "TextureBuffer is not used anywhere, destroy it if not needed."); + return addValidationMessage(EValidationSeverity::Warning, "TextureBuffer is not used anywhere, destroy it if not needed."); return status; } diff --git a/client/ramses-client/impl/Texture2DBufferImpl.h b/client/ramses-client/impl/Texture2DBufferImpl.h index 45ddba87b..90d8194eb 100644 --- a/client/ramses-client/impl/Texture2DBufferImpl.h +++ b/client/ramses-client/impl/Texture2DBufferImpl.h @@ -12,29 +12,31 @@ #include "SceneObjectImpl.h" #include "SceneAPI/Handles.h" #include "SceneAPI/MipMapSize.h" -#include "ramses-client-api/EDataType.h" +#include "ramses-framework-api/EDataType.h" #include "ramses-client-api/TextureEnums.h" +#include + namespace ramses { class Texture2DBufferImpl : public SceneObjectImpl { public: - Texture2DBufferImpl(SceneImpl& scene, const char* textureBufferName); - virtual ~Texture2DBufferImpl() override; + Texture2DBufferImpl(SceneImpl& scene, std::string_view textureBufferName); + ~Texture2DBufferImpl() override; void initializeFrameworkData(const ramses_internal::MipMapDimensions& mipDimensions, ETextureFormat textureFormat); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t validate() const override; - status_t setData(const ramses_internal::Byte* data, uint32_t mipLevel, uint32_t offsetX, uint32_t offsetY, uint32_t width, uint32_t height); - uint32_t getMipLevelCount() const; + status_t setData(const ramses_internal::Byte* data, size_t mipLevel, uint32_t offsetX, uint32_t offsetY, uint32_t width, uint32_t height); + size_t getMipLevelCount() const; ETextureFormat getTexelFormat() const; - status_t getMipLevelData(uint32_t mipLevel, char* buffer, uint32_t bufferSize) const; - status_t getMipLevelSize(uint32_t mipLevel, uint32_t& widthOut, uint32_t& heightOut) const; - uint32_t getMipLevelDataSizeInBytes(uint32_t mipLevel) const; + status_t getMipLevelData(size_t mipLevel, char* buffer, size_t bufferSize) const; + status_t getMipLevelSize(size_t mipLevel, uint32_t& widthOut, uint32_t& heightOut) const; + size_t getMipLevelDataSizeInBytes(size_t mipLevel) const; ramses_internal::TextureBufferHandle getTextureBufferHandle() const; diff --git a/client/ramses-client/impl/Texture2DImpl.cpp b/client/ramses-client/impl/Texture2DImpl.cpp index 7799dabeb..f238f2ce4 100644 --- a/client/ramses-client/impl/Texture2DImpl.cpp +++ b/client/ramses-client/impl/Texture2DImpl.cpp @@ -12,16 +12,18 @@ #include "ResourceImpl.h" #include "ClientApplicationLogic.h" +#include + namespace ramses { Texture2DImpl::Texture2DImpl(ramses_internal::ResourceHashUsage resource, SceneImpl& scene, - const char* name, + std::string_view name, ERamsesObjectType overrideType /* = ERamsesObjectType_Texture2D*/) : ResourceImpl(overrideType, std::move(resource), scene, name) , m_width(0) , m_height(0) - , m_textureFormat(ETextureFormat::NUMBER_OF_ELEMENTS) + , m_textureFormat(ETextureFormat::Invalid) { } diff --git a/client/ramses-client/impl/Texture2DImpl.h b/client/ramses-client/impl/Texture2DImpl.h index 993465a70..ea3c3f03a 100644 --- a/client/ramses-client/impl/Texture2DImpl.h +++ b/client/ramses-client/impl/Texture2DImpl.h @@ -13,6 +13,8 @@ #include "ResourceImpl.h" #include "ramses-client-api/TextureSwizzle.h" +#include + namespace ramses_internal { class Texture2DResource; @@ -26,14 +28,14 @@ namespace ramses // overrideType is required if another texture type is reusing the impl, but needs a different type ID Texture2DImpl(ramses_internal::ResourceHashUsage resource, SceneImpl& scene, - const char* name, - ERamsesObjectType overrideType = ERamsesObjectType_Texture2D); + std::string_view name, + ERamsesObjectType overrideType = ERamsesObjectType::Texture2D); - virtual ~Texture2DImpl() override; + ~Texture2DImpl() override; void initializeFromFrameworkData(uint32_t width, uint32_t height, ETextureFormat textureFormat, const TextureSwizzle& swizzle); - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; uint32_t getWidth() const; uint32_t getHeight() const; diff --git a/client/ramses-client/impl/Texture3DImpl.cpp b/client/ramses-client/impl/Texture3DImpl.cpp index 142e1a94a..dffc750b4 100644 --- a/client/ramses-client/impl/Texture3DImpl.cpp +++ b/client/ramses-client/impl/Texture3DImpl.cpp @@ -11,12 +11,14 @@ #include "Components/ManagedResource.h" #include "ClientApplicationLogic.h" +#include + namespace ramses { Texture3DImpl::Texture3DImpl(ramses_internal::ResourceHashUsage resource, SceneImpl& scene, - const char* name) - : ResourceImpl(ERamsesObjectType_Texture3D, std::move(resource), scene, name) + std::string_view name) + : ResourceImpl(ERamsesObjectType::Texture3D, std::move(resource), scene, name) , m_width(0) , m_height(0) , m_depth(0) diff --git a/client/ramses-client/impl/Texture3DImpl.h b/client/ramses-client/impl/Texture3DImpl.h index 66c60d809..dc12f0937 100644 --- a/client/ramses-client/impl/Texture3DImpl.h +++ b/client/ramses-client/impl/Texture3DImpl.h @@ -12,6 +12,8 @@ #include "ramses-client-api/TextureEnums.h" #include "ResourceImpl.h" +#include + namespace ramses_internal { class Texture3DResource; @@ -24,13 +26,13 @@ namespace ramses public: Texture3DImpl(ramses_internal::ResourceHashUsage resource, SceneImpl& scene, - const char* name); + std::string_view name); - virtual ~Texture3DImpl() override; + ~Texture3DImpl() override; void initializeFromFrameworkData(uint32_t width, uint32_t height, uint32_t depth, ETextureFormat textureFormat); - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; uint32_t getWidth() const; uint32_t getHeight() const; diff --git a/client/ramses-client/impl/TextureCubeImpl.cpp b/client/ramses-client/impl/TextureCubeImpl.cpp index 89127abfd..99a66a0ea 100644 --- a/client/ramses-client/impl/TextureCubeImpl.cpp +++ b/client/ramses-client/impl/TextureCubeImpl.cpp @@ -13,8 +13,8 @@ namespace ramses { - TextureCubeImpl::TextureCubeImpl(ramses_internal::ResourceHashUsage texture, SceneImpl& scene, const char* name) - : ResourceImpl(ERamsesObjectType_TextureCube, std::move(texture), scene, name) + TextureCubeImpl::TextureCubeImpl(ramses_internal::ResourceHashUsage texture, SceneImpl& scene, std::string_view name) + : ResourceImpl(ERamsesObjectType::TextureCube, std::move(texture), scene, name) , m_size(0) , m_textureFormat(ETextureFormat::Invalid) { diff --git a/client/ramses-client/impl/TextureCubeImpl.h b/client/ramses-client/impl/TextureCubeImpl.h index 61195861e..69f8ee8a5 100644 --- a/client/ramses-client/impl/TextureCubeImpl.h +++ b/client/ramses-client/impl/TextureCubeImpl.h @@ -13,6 +13,8 @@ #include "ResourceImpl.h" #include "ramses-client-api/TextureSwizzle.h" +#include + namespace ramses_internal { class TextureCubeResource; @@ -26,12 +28,12 @@ namespace ramses /** * @brief This creates a Cube Texture. All texels are pre-allocated and initialized to 0. */ - TextureCubeImpl(ramses_internal::ResourceHashUsage texture, SceneImpl& scene, const char* name); - virtual ~TextureCubeImpl() override; + TextureCubeImpl(ramses_internal::ResourceHashUsage texture, SceneImpl& scene, std::string_view name); + ~TextureCubeImpl() override; void initializeFromFrameworkData(uint32_t size, ETextureFormat textureFormat, const TextureSwizzle& swizzle); - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; uint32_t getSize() const; ETextureFormat getTextureFormat() const; diff --git a/client/ramses-client/impl/TextureSamplerImpl.cpp b/client/ramses-client/impl/TextureSamplerImpl.cpp index 992baaec9..e8e9e33e8 100644 --- a/client/ramses-client/impl/TextureSamplerImpl.cpp +++ b/client/ramses-client/impl/TextureSamplerImpl.cpp @@ -12,7 +12,6 @@ #include "ramses-client-api/RenderBuffer.h" #include "ramses-client-api/Texture2DBuffer.h" #include "ramses-client-api/RenderTarget.h" -#include "ramses-client-api/StreamTexture.h" #include "Texture2DImpl.h" #include "Texture3DImpl.h" #include "TextureCubeImpl.h" @@ -21,7 +20,6 @@ #include "RenderBufferImpl.h" #include "Texture2DBufferImpl.h" #include "RenderTargetImpl.h" -#include "StreamTextureImpl.h" #include "SceneImpl.h" #include "Scene/ClientScene.h" #include "TextureUtils.h" @@ -30,9 +28,9 @@ namespace ramses { - TextureSamplerImpl::TextureSamplerImpl(SceneImpl& scene, ERamsesObjectType type, const char* name) + TextureSamplerImpl::TextureSamplerImpl(SceneImpl& scene, ERamsesObjectType type, std::string_view name) : SceneObjectImpl(scene, type, name) - , m_textureType(ERamsesObjectType_Invalid) + , m_textureType(ERamsesObjectType::Invalid) { } @@ -60,56 +58,48 @@ namespace ramses status_t TextureSamplerImpl::setTextureData(const Texture2D& texture) { - if (!isFromTheSameSceneAs(texture.impl)) + if (!isFromTheSameSceneAs(texture.m_impl)) return addErrorEntry("TextureSampler::setTextureData failed, client texture is not from the same client as this sampler."); - return setTextureDataInternal(ERamsesObjectType_Texture2D, ramses_internal::TextureSampler::ContentType::ClientTexture, texture.impl.getLowlevelResourceHash(), ramses_internal::InvalidMemoryHandle); + return setTextureDataInternal(ERamsesObjectType::Texture2D, ramses_internal::TextureSampler::ContentType::ClientTexture, texture.m_impl.getLowlevelResourceHash(), ramses_internal::InvalidMemoryHandle); } status_t TextureSamplerImpl::setTextureData(const Texture3D& texture) { - if (m_textureType != ERamsesObjectType_Texture3D) + if (m_textureType != ERamsesObjectType::Texture3D) return addErrorEntry("TextureSampler::setTextureData failed, changing data from non 3D texture to 3D texture is not supported. Create a new TextureSampler instead."); - if (!isFromTheSameSceneAs(texture.impl)) + if (!isFromTheSameSceneAs(texture.m_impl)) return addErrorEntry("TextureSampler::setTextureData failed, client texture is not from the same client as this sampler."); - return setTextureDataInternal(ERamsesObjectType_Texture3D, ramses_internal::TextureSampler::ContentType::ClientTexture, texture.impl.getLowlevelResourceHash(), ramses_internal::InvalidMemoryHandle); + return setTextureDataInternal(ERamsesObjectType::Texture3D, ramses_internal::TextureSampler::ContentType::ClientTexture, texture.m_impl.getLowlevelResourceHash(), ramses_internal::InvalidMemoryHandle); } status_t TextureSamplerImpl::setTextureData(const TextureCube& texture) { - if (!isFromTheSameSceneAs(texture.impl)) + if (!isFromTheSameSceneAs(texture.m_impl)) return addErrorEntry("TextureSampler::setTextureData failed, client texture is not from the same client as this sampler."); - return setTextureDataInternal(ERamsesObjectType_TextureCube, ramses_internal::TextureSampler::ContentType::ClientTexture, texture.impl.getLowlevelResourceHash(), ramses_internal::InvalidMemoryHandle); + return setTextureDataInternal(ERamsesObjectType::TextureCube, ramses_internal::TextureSampler::ContentType::ClientTexture, texture.m_impl.getLowlevelResourceHash(), ramses_internal::InvalidMemoryHandle); } status_t TextureSamplerImpl::setTextureData(const Texture2DBuffer& texture) { - if (!getSceneImpl().containsSceneObject(texture.impl)) + if (!getSceneImpl().containsSceneObject(texture.m_impl)) return addErrorEntry("TextureSampler::setTextureData failed, texture2D buffer is not from the same scene as this sampler."); - return setTextureDataInternal(ERamsesObjectType_Texture2DBuffer, ramses_internal::TextureSampler::ContentType::TextureBuffer, {}, texture.impl.getTextureBufferHandle().asMemoryHandle()); + return setTextureDataInternal(ERamsesObjectType::Texture2DBuffer, ramses_internal::TextureSampler::ContentType::TextureBuffer, {}, texture.m_impl.getTextureBufferHandle().asMemoryHandle()); } status_t TextureSamplerImpl::setTextureData(const RenderBuffer& texture) { - if (!getSceneImpl().containsSceneObject(texture.impl)) + if (!getSceneImpl().containsSceneObject(texture.m_impl)) return addErrorEntry("TextureSampler::setTextureData failed, render buffer is not from the same scene as this sampler."); - if (ERenderBufferAccessMode_WriteOnly == texture.impl.getAccessMode()) + if (ERenderBufferAccessMode::WriteOnly == texture.m_impl.getAccessMode()) return addErrorEntry("TextureSampler::setTextureData failed, render buffer has access mode write only."); - return setTextureDataInternal(ERamsesObjectType_RenderBuffer, ramses_internal::TextureSampler::ContentType::RenderBuffer, {}, texture.impl.getRenderBufferHandle().asMemoryHandle()); - } - - status_t TextureSamplerImpl::setTextureData(const StreamTexture& texture) - { - if (!getSceneImpl().containsSceneObject(texture.impl)) - return addErrorEntry("TextureSampler::setTextureData failed, stream texture is not from the same scene as this sampler."); - - return setTextureDataInternal(ERamsesObjectType_StreamTexture, ramses_internal::TextureSampler::ContentType::StreamTexture, {}, texture.impl.getHandle().asMemoryHandle()); + return setTextureDataInternal(ERamsesObjectType::RenderBuffer, ramses_internal::TextureSampler::ContentType::RenderBuffer, {}, texture.m_impl.getRenderBufferHandle().asMemoryHandle()); } status_t TextureSamplerImpl::setTextureDataInternal(ERamsesObjectType textureType, @@ -186,13 +176,12 @@ namespace ramses switch (m_textureType) { - case ERamsesObjectType_Texture2D: - case ERamsesObjectType_StreamTexture: - case ERamsesObjectType_RenderBuffer: - case ERamsesObjectType_Texture2DBuffer: return ramses_internal::EDataType::TextureSampler2D; - case ERamsesObjectType_Texture3D: return ramses_internal::EDataType::TextureSampler3D; - case ERamsesObjectType_TextureCube: return ramses_internal::EDataType::TextureSamplerCube; - case ERamsesObjectType_TextureSamplerExternal: return ramses_internal::EDataType::TextureSamplerExternal; + case ERamsesObjectType::Texture2D: + case ERamsesObjectType::RenderBuffer: + case ERamsesObjectType::Texture2DBuffer: return ramses_internal::EDataType::TextureSampler2D; + case ERamsesObjectType::Texture3D: return ramses_internal::EDataType::TextureSampler3D; + case ERamsesObjectType::TextureCube: return ramses_internal::EDataType::TextureSamplerCube; + case ERamsesObjectType::TextureSamplerExternal: return ramses_internal::EDataType::TextureSamplerExternal; default: break; } return ramses_internal::EDataType::Invalid; @@ -202,7 +191,7 @@ namespace ramses { CHECK_RETURN_ERR(SceneObjectImpl::serialize(outStream, serializationContext)); - outStream << static_cast(m_textureType); + outStream << static_cast(m_textureType); outStream << m_textureSamplerHandle; return StatusOK; @@ -212,7 +201,7 @@ namespace ramses { CHECK_RETURN_ERR(SceneObjectImpl::deserialize(inStream, serializationContext)); - ramses_internal::UInt32 value; + uint32_t value; inStream >> value; m_textureType = static_cast(value); @@ -234,7 +223,7 @@ namespace ramses case ramses_internal::TextureSampler::ContentType::ClientTexture: resource = getSceneImpl().scanForResourceWithHash(sampler.textureResource); if (resource == nullptr) - return addValidationMessage(EValidationSeverity_Error, "Client texture set in TextureSampler does not exist"); + return addValidationMessage(EValidationSeverity::Error, "Client texture set in TextureSampler does not exist"); contentStatus = validateResource(resource); break; case ramses_internal::TextureSampler::ContentType::RenderBuffer: @@ -244,16 +233,13 @@ namespace ramses case ramses_internal::TextureSampler::ContentType::TextureBuffer: contentStatus = validateTextureBuffer(ramses_internal::TextureBufferHandle(sampler.contentHandle)); break; - case ramses_internal::TextureSampler::ContentType::StreamTexture: - contentStatus = validateStreamTexture(ramses_internal::StreamTextureHandle(sampler.contentHandle)); - break; case ramses_internal::TextureSampler::ContentType::ExternalTexture: contentStatus = StatusOK; break; case ramses_internal::TextureSampler::ContentType::OffscreenBuffer: case ramses_internal::TextureSampler::ContentType::StreamBuffer: case ramses_internal::TextureSampler::ContentType::None: - return addValidationMessage(EValidationSeverity_Error, "There is no valid content source set in TextureSampler"); + return addValidationMessage(EValidationSeverity::Error, "There is no valid content source set in TextureSampler"); } return std::max(status, contentStatus); @@ -267,20 +253,20 @@ namespace ramses const bool isRenderBufferValid = getIScene().isRenderBufferAllocated(renderBufferHandle); if (isRenderBufferValid) { - RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType_RenderBuffer); + RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType::RenderBuffer); while (const RenderBuffer* renderBuffer = iter.getNext()) { - if (renderBufferHandle == renderBuffer->impl.getRenderBufferHandle()) + if (renderBufferHandle == renderBuffer->m_impl.getRenderBufferHandle()) { foundRenderBuffer = true; - status = addValidationOfDependentObject(renderBuffer->impl); + status = addValidationOfDependentObject(renderBuffer->m_impl); break; } } } if (!foundRenderBuffer) - return addValidationMessage(EValidationSeverity_Error, "Texture Sampler is using a RenderBuffer which does not exist"); + return addValidationMessage(EValidationSeverity::Error, "Texture Sampler is using a RenderBuffer which does not exist"); return status; } @@ -293,46 +279,20 @@ namespace ramses const bool isTextureBufferValid = getIScene().isTextureBufferAllocated(textureBufferHandle); if (isTextureBufferValid) { - RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType_Texture2DBuffer); + RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType::Texture2DBuffer); while (const Texture2DBuffer* textureBuffer = iter.getNext()) { - if (textureBufferHandle == textureBuffer->impl.getTextureBufferHandle()) + if (textureBufferHandle == textureBuffer->m_impl.getTextureBufferHandle()) { foundTextureBuffer = true; - status = addValidationOfDependentObject(textureBuffer->impl); + status = addValidationOfDependentObject(textureBuffer->m_impl); break; } } } if (!foundTextureBuffer) - return addValidationMessage(EValidationSeverity_Error, "Texture Sampler is using a TextureBuffer which does not exist"); - - return status; - } - - ramses::status_t TextureSamplerImpl::validateStreamTexture(ramses_internal::StreamTextureHandle streamTextureHandle) const - { - status_t status = StatusOK; - bool foundStreamTexture = false; - - const bool isStreamTextureValid = getIScene().isStreamTextureAllocated(streamTextureHandle); - if (isStreamTextureValid) - { - RamsesObjectRegistryIterator iter(getSceneImpl().getObjectRegistry(), ERamsesObjectType_StreamTexture); - while (const StreamTexture* streamTexture = iter.getNext()) - { - if (streamTextureHandle == streamTexture->impl.getHandle()) - { - foundStreamTexture = true; - status = addValidationOfDependentObject(streamTexture->impl); - break; - } - } - } - - if (!foundStreamTexture) - return addValidationMessage(EValidationSeverity_Error, "Texture Sampler is using a StreamTexture which does not exist"); + return addValidationMessage(EValidationSeverity::Error, "Texture Sampler is using a TextureBuffer which does not exist"); return status; } @@ -346,22 +306,22 @@ namespace ramses const ERamsesObjectType resourceType = resource->getType(); switch (resourceType) { - case ERamsesObjectType_Texture2D: + case ERamsesObjectType::Texture2D: { const Texture2D& texture = RamsesObjectTypeUtils::ConvertTo(*resource); - textureStatus = addValidationOfDependentObject(texture.impl); + textureStatus = addValidationOfDependentObject(texture.m_impl); break; } - case ERamsesObjectType_Texture3D: + case ERamsesObjectType::Texture3D: { const Texture3D& texture = RamsesObjectTypeUtils::ConvertTo(*resource); - textureStatus = addValidationOfDependentObject(texture.impl); + textureStatus = addValidationOfDependentObject(texture.m_impl); break; } - case ERamsesObjectType_TextureCube: + case ERamsesObjectType::TextureCube: { const TextureCube& texture = RamsesObjectTypeUtils::ConvertTo(*resource); - textureStatus = addValidationOfDependentObject(texture.impl); + textureStatus = addValidationOfDependentObject(texture.m_impl); break; } default: diff --git a/client/ramses-client/impl/TextureSamplerImpl.h b/client/ramses-client/impl/TextureSamplerImpl.h index e2fe40d12..456d3eabf 100644 --- a/client/ramses-client/impl/TextureSamplerImpl.h +++ b/client/ramses-client/impl/TextureSamplerImpl.h @@ -20,6 +20,8 @@ #include "SceneAPI/TextureSampler.h" #include "SceneAPI/EDataType.h" +#include + namespace ramses { class Texture2D; @@ -27,14 +29,13 @@ namespace ramses class TextureCube; class Texture2DBuffer; class RenderBuffer; - class StreamTexture; class Resource; class TextureSamplerImpl final : public SceneObjectImpl { public: - TextureSamplerImpl(SceneImpl& scene, ERamsesObjectType type, const char* name); - virtual ~TextureSamplerImpl() override; + TextureSamplerImpl(SceneImpl& scene, ERamsesObjectType type, std::string_view name); + ~TextureSamplerImpl() override; void initializeFrameworkData( const ramses_internal::TextureSamplerStates& samplerStates, @@ -43,10 +44,10 @@ namespace ramses ramses_internal::ResourceContentHash textureHash, ramses_internal::MemoryHandle contentHandle); - virtual void deinitializeFrameworkData() override; - virtual status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; - virtual status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; - virtual status_t validate() const override; + void deinitializeFrameworkData() override; + status_t serialize(ramses_internal::IOutputStream& outStream, SerializationContext& serializationContext) const override; + status_t deserialize(ramses_internal::IInputStream& inStream, DeserializationContext& serializationContext) override; + status_t validate() const override; ETextureAddressMode getWrapUMode() const; ETextureAddressMode getWrapVMode() const; @@ -60,7 +61,6 @@ namespace ramses status_t setTextureData(const TextureCube& texture); status_t setTextureData(const Texture2DBuffer& texture); status_t setTextureData(const RenderBuffer& texture); - status_t setTextureData(const StreamTexture& texture); ramses_internal::TextureSamplerHandle getTextureSamplerHandle() const; ramses_internal::EDataType getTextureDataType() const; @@ -73,7 +73,6 @@ namespace ramses ramses_internal::MemoryHandle contentHandle); status_t validateRenderBuffer(ramses_internal::RenderBufferHandle renderBufferHandle) const; - status_t validateStreamTexture(ramses_internal::StreamTextureHandle streamTextureHandle) const; status_t validateTextureBuffer(ramses_internal::TextureBufferHandle textureBufferHandle) const; status_t validateResource(const Resource* resource) const; diff --git a/client/ramses-client/impl/TextureUtils.cpp b/client/ramses-client/impl/TextureUtils.cpp index c71f996ca..679f24dca 100644 --- a/client/ramses-client/impl/TextureUtils.cpp +++ b/client/ramses-client/impl/TextureUtils.cpp @@ -14,6 +14,7 @@ namespace ramses { + // NOLINTNEXTLINE(modernize-avoid-c-arrays) void TextureUtils::FillMipDataSizes(ramses_internal::MipDataSizeVector& mipDataSizes, uint32_t mipMapCount, const MipLevelData mipLevelData[]) { assert(mipDataSizes.empty()); @@ -25,6 +26,7 @@ namespace ramses } } + // NOLINTNEXTLINE(modernize-avoid-c-arrays) void TextureUtils::FillMipDataSizes(ramses_internal::MipDataSizeVector& mipDataSizes, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[]) { assert(mipDataSizes.empty()); @@ -36,6 +38,7 @@ namespace ramses } } + // NOLINTNEXTLINE(modernize-avoid-c-arrays) void TextureUtils::FillMipData(uint8_t* dest, uint32_t mipMapCount, const MipLevelData mipLevelData[]) { for (uint32_t i = 0u; i < mipMapCount; ++i) @@ -46,6 +49,7 @@ namespace ramses } } + // NOLINTNEXTLINE(modernize-avoid-c-arrays) void TextureUtils::FillMipData(uint8_t* dest, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[]) { for (uint32_t i = 0u; i < mipMapCount; ++i) @@ -86,6 +90,7 @@ namespace ramses } } + // NOLINTNEXTLINE(modernize-avoid-c-arrays) bool TextureUtils::MipDataValid(uint32_t width, uint32_t height, uint32_t depth, uint32_t mipMapCount, const MipLevelData mipLevelData[], ETextureFormat format) { if (mipMapCount == 0u || mipLevelData == nullptr) @@ -120,13 +125,14 @@ namespace ramses if (!TextureUtils::IsTextureSizeSupportedByFormat(mipWidth, mipHeight, format)) { LOG_WARN(ramses_internal::CONTEXT_CLIENT, "Provided texture mip " << i << " might fail to be uploaded due to its size " - << mipWidth << "x" << mipHeight << " not supported by used format " << getTextureFormatString(format)); + << mipWidth << "x" << mipHeight << " not supported by used format " << toString(format)); } } return true; } + // NOLINTNEXTLINE(modernize-avoid-c-arrays) bool TextureUtils::MipDataValid(uint32_t width, uint32_t height, uint32_t depth, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[], ETextureFormat format) { // wrapper so that this function can be called from template following 2D/3D texture function signature @@ -135,6 +141,7 @@ namespace ramses return MipDataValid(width, mipMapCount, mipLevelData, format); } + // NOLINTNEXTLINE(modernize-avoid-c-arrays) bool TextureUtils::MipDataValid(uint32_t size, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[], ETextureFormat format) { if (mipMapCount == 0u || mipLevelData == nullptr) @@ -173,7 +180,7 @@ namespace ramses if (!TextureUtils::IsTextureSizeSupportedByFormat(mipSize, mipSize, format)) { LOG_WARN(ramses_internal::CONTEXT_CLIENT, "Provided texture mip " << i << " might fail to be uploaded due to its size " - << mipSize << "x" << mipSize << " not supported by used format " << getTextureFormatString(format)); + << mipSize << "x" << mipSize << " not supported by used format " << toString(format)); } } diff --git a/client/ramses-client/impl/TextureUtils.h b/client/ramses-client/impl/TextureUtils.h index be4e0118e..accfabe5c 100644 --- a/client/ramses-client/impl/TextureUtils.h +++ b/client/ramses-client/impl/TextureUtils.h @@ -128,11 +128,12 @@ namespace ramses return ramses_internal::ETextureFormat::SRGB8; case ETextureFormat::SRGB8_ALPHA8: return ramses_internal::ETextureFormat::SRGB8_ALPHA8; - - default: - assert(false); - return ramses_internal::ETextureFormat::RGBA8; + case ETextureFormat::Invalid: + break; } + + assert(false); + return ramses_internal::ETextureFormat::RGBA8; } static ETextureFormat GetTextureFormatFromInternal(ramses_internal::ETextureFormat textureformat) @@ -240,11 +241,22 @@ namespace ramses return ETextureFormat::SRGB8; case ramses_internal::ETextureFormat::SRGB8_ALPHA8: return ETextureFormat::SRGB8_ALPHA8; - - default: - assert(false); - return ETextureFormat::RGBA8; + case ramses_internal::ETextureFormat::Invalid: + case ramses_internal::ETextureFormat::R16: + case ramses_internal::ETextureFormat::RG16: + case ramses_internal::ETextureFormat::RGB16: + case ramses_internal::ETextureFormat::RGBA16: + case ramses_internal::ETextureFormat::DXT1RGB: + case ramses_internal::ETextureFormat::DXT3RGBA: + case ramses_internal::ETextureFormat::DXT5RGBA: + case ramses_internal::ETextureFormat::Depth16: + case ramses_internal::ETextureFormat::Depth24: + case ramses_internal::ETextureFormat::Depth24_Stencil8: + case ramses_internal::ETextureFormat::NUMBER_OF_TYPES: + break; } + assert(false); + return ETextureFormat::RGBA8; } static ETextureChannelColor GetTextureChannelColorFromInternal(ramses_internal::ETextureChannelColor textureChannelColor) @@ -394,7 +406,6 @@ namespace ramses return (width % 12 == 0) && (height % 12 == 0); case ETextureFormat::Invalid: - case ETextureFormat::NUMBER_OF_ELEMENTS: assert(false); return false; } @@ -407,16 +418,15 @@ namespace ramses { switch (addressMode) { - case ETextureAddressMode_Clamp: + case ETextureAddressMode::Clamp: return ramses_internal::EWrapMethod::Clamp; - case ETextureAddressMode_Repeat: + case ETextureAddressMode::Repeat: return ramses_internal::EWrapMethod::Repeat; - case ETextureAddressMode_Mirror: + case ETextureAddressMode::Mirror: return ramses_internal::EWrapMethod::RepeatMirrored; - default: - assert(false); - return ramses_internal::EWrapMethod::Clamp; } + assert(false); + return ramses_internal::EWrapMethod::Clamp; } static ETextureAddressMode GetTextureAddressModeFromInternal(ramses_internal::EWrapMethod addressMode) @@ -424,59 +434,58 @@ namespace ramses switch (addressMode) { case ramses_internal::EWrapMethod::Clamp: - return ETextureAddressMode_Clamp; + return ETextureAddressMode::Clamp; case ramses_internal::EWrapMethod::Repeat: - return ETextureAddressMode_Repeat; + return ETextureAddressMode::Repeat; case ramses_internal::EWrapMethod::RepeatMirrored: - return ETextureAddressMode_Mirror; - default: - assert(false); - return ETextureAddressMode_Clamp; + return ETextureAddressMode::Mirror; + case ramses_internal::EWrapMethod::NUMBER_OF_ELEMENTS: + break; } + assert(false); + return ETextureAddressMode::Clamp; } static ramses_internal::ETextureCubeFace GetTextureCubeFaceInternal(ETextureCubeFace face) { switch (face) { - case ETextureCubeFace_PositiveX: + case ETextureCubeFace::PositiveX: return ramses_internal::ETextureCubeFace_PositiveX; - case ETextureCubeFace_NegativeX: + case ETextureCubeFace::NegativeX: return ramses_internal::ETextureCubeFace_NegativeX; - case ETextureCubeFace_PositiveY: + case ETextureCubeFace::PositiveY: return ramses_internal::ETextureCubeFace_PositiveY; - case ETextureCubeFace_NegativeY: + case ETextureCubeFace::NegativeY: return ramses_internal::ETextureCubeFace_NegativeY; - case ETextureCubeFace_PositiveZ: + case ETextureCubeFace::PositiveZ: return ramses_internal::ETextureCubeFace_PositiveZ; - case ETextureCubeFace_NegativeZ: + case ETextureCubeFace::NegativeZ: return ramses_internal::ETextureCubeFace_NegativeZ; - default: - assert(false); - return ramses_internal::ETextureCubeFace_PositiveX; } + assert(false); + return ramses_internal::ETextureCubeFace_PositiveX; } static ramses_internal::ESamplingMethod GetTextureSamplingInternal(ETextureSamplingMethod sampling) { switch (sampling) { - case ETextureSamplingMethod_Nearest: + case ETextureSamplingMethod::Nearest: return ramses_internal::ESamplingMethod::Nearest; - case ETextureSamplingMethod_Nearest_MipMapNearest: + case ETextureSamplingMethod::Nearest_MipMapNearest: return ramses_internal::ESamplingMethod::Nearest_MipMapNearest; - case ETextureSamplingMethod_Nearest_MipMapLinear: + case ETextureSamplingMethod::Nearest_MipMapLinear: return ramses_internal::ESamplingMethod::Nearest_MipMapLinear; - case ETextureSamplingMethod_Linear: + case ETextureSamplingMethod::Linear: return ramses_internal::ESamplingMethod::Linear; - case ETextureSamplingMethod_Linear_MipMapNearest: + case ETextureSamplingMethod::Linear_MipMapNearest: return ramses_internal::ESamplingMethod::Linear_MipMapNearest; - case ETextureSamplingMethod_Linear_MipMapLinear: + case ETextureSamplingMethod::Linear_MipMapLinear: return ramses_internal::ESamplingMethod::Linear_MipMapLinear; - default: - assert(false); - return ramses_internal::ESamplingMethod::Linear; } + assert(false); + return ramses_internal::ESamplingMethod::Linear; } static ETextureSamplingMethod GetTextureSamplingFromInternal(ramses_internal::ESamplingMethod sampling) @@ -484,37 +493,37 @@ namespace ramses switch (sampling) { case ramses_internal::ESamplingMethod::Nearest: - return ETextureSamplingMethod_Nearest; + return ETextureSamplingMethod::Nearest; case ramses_internal::ESamplingMethod::Nearest_MipMapNearest: - return ETextureSamplingMethod_Nearest_MipMapNearest; + return ETextureSamplingMethod::Nearest_MipMapNearest; case ramses_internal::ESamplingMethod::Nearest_MipMapLinear: - return ETextureSamplingMethod_Nearest_MipMapLinear; + return ETextureSamplingMethod::Nearest_MipMapLinear; case ramses_internal::ESamplingMethod::Linear: - return ETextureSamplingMethod_Linear; + return ETextureSamplingMethod::Linear; case ramses_internal::ESamplingMethod::Linear_MipMapNearest: - return ETextureSamplingMethod_Linear_MipMapNearest; + return ETextureSamplingMethod::Linear_MipMapNearest; case ramses_internal::ESamplingMethod::Linear_MipMapLinear: - return ETextureSamplingMethod_Linear_MipMapLinear; - default: - assert(false); - return ETextureSamplingMethod_Linear; + return ETextureSamplingMethod::Linear_MipMapLinear; + case ramses_internal::ESamplingMethod::NUMBER_OF_ELEMENTS: + break; } + assert(false); + return ETextureSamplingMethod::Linear; } static ramses_internal::ERenderBufferType GetRenderBufferTypeInternal(ERenderBufferType bufferType) { switch (bufferType) { - case ERenderBufferType_Color: + case ERenderBufferType::Color: return ramses_internal::ERenderBufferType_ColorBuffer; - case ERenderBufferType_Depth: + case ERenderBufferType::Depth: return ramses_internal::ERenderBufferType_DepthBuffer; - case ERenderBufferType_DepthStencil: + case ERenderBufferType::DepthStencil: return ramses_internal::ERenderBufferType_DepthStencilBuffer; - default: - assert(false); - return ramses_internal::ERenderBufferType_InvalidBuffer; } + assert(false); + return ramses_internal::ERenderBufferType_InvalidBuffer; } static ERenderBufferType GetRenderBufferTypeFromInternal(ramses_internal::ERenderBufferType bufferType) @@ -522,69 +531,69 @@ namespace ramses switch (bufferType) { case ramses_internal::ERenderBufferType_ColorBuffer: - return ERenderBufferType_Color; + return ERenderBufferType::Color; case ramses_internal::ERenderBufferType_DepthBuffer: - return ERenderBufferType_Depth; + return ERenderBufferType::Depth; case ramses_internal::ERenderBufferType_DepthStencilBuffer: - return ERenderBufferType_DepthStencil; - default: - assert(false); - return ERenderBufferType_Color; + return ERenderBufferType::DepthStencil; + case ramses_internal::ERenderBufferType_InvalidBuffer: + case ramses_internal::ERenderBufferType_NUMBER_OF_ELEMENTS: + break; } + assert(false); + return ERenderBufferType::Color; } static ramses_internal::ETextureFormat GetRenderBufferFormatInternal(ERenderBufferFormat bufferFormat) { switch (bufferFormat) { - case ramses::ERenderBufferFormat_RGBA4: + case ramses::ERenderBufferFormat::RGBA4: return ramses_internal::ETextureFormat::RGBA4; - case ramses::ERenderBufferFormat_R8: + case ramses::ERenderBufferFormat::R8: return ramses_internal::ETextureFormat::R8; - case ramses::ERenderBufferFormat_RG8: + case ramses::ERenderBufferFormat::RG8: return ramses_internal::ETextureFormat::RG8; - case ramses::ERenderBufferFormat_RGB8: + case ramses::ERenderBufferFormat::RGB8: return ramses_internal::ETextureFormat::RGB8; - case ramses::ERenderBufferFormat_RGBA8: + case ramses::ERenderBufferFormat::RGBA8: return ramses_internal::ETextureFormat::RGBA8; - case ramses::ERenderBufferFormat_R16F: + case ramses::ERenderBufferFormat::R16F: return ramses_internal::ETextureFormat::R16F; - case ramses::ERenderBufferFormat_R32F: + case ramses::ERenderBufferFormat::R32F: return ramses_internal::ETextureFormat::R32F; - case ramses::ERenderBufferFormat_RG16F: + case ramses::ERenderBufferFormat::RG16F: return ramses_internal::ETextureFormat::RG16F; - case ramses::ERenderBufferFormat_RG32F: + case ramses::ERenderBufferFormat::RG32F: return ramses_internal::ETextureFormat::RG32F; - case ramses::ERenderBufferFormat_RGB16F: + case ramses::ERenderBufferFormat::RGB16F: return ramses_internal::ETextureFormat::RGB16F; - case ramses::ERenderBufferFormat_RGB32F: + case ramses::ERenderBufferFormat::RGB32F: return ramses_internal::ETextureFormat::RGB32F; - case ramses::ERenderBufferFormat_RGBA16F: + case ramses::ERenderBufferFormat::RGBA16F: return ramses_internal::ETextureFormat::RGBA16F; - case ramses::ERenderBufferFormat_RGBA32F: + case ramses::ERenderBufferFormat::RGBA32F: return ramses_internal::ETextureFormat::RGBA32F; - case ramses::ERenderBufferFormat_Depth24: + case ramses::ERenderBufferFormat::Depth24: return ramses_internal::ETextureFormat::Depth24; - case ramses::ERenderBufferFormat_Depth24_Stencil8: + case ramses::ERenderBufferFormat::Depth24_Stencil8: return ramses_internal::ETextureFormat::Depth24_Stencil8; - default: - assert(false); - return ramses_internal::ETextureFormat::Invalid; } + assert(false); + return ramses_internal::ETextureFormat::Invalid; } static ramses_internal::ERenderBufferAccessMode GetRenderBufferAccessModeInternal(ERenderBufferAccessMode accessMode) { switch (accessMode) { - case ramses::ERenderBufferAccessMode_WriteOnly: + case ramses::ERenderBufferAccessMode::WriteOnly: return ramses_internal::ERenderBufferAccessMode_WriteOnly; - case ramses::ERenderBufferAccessMode_ReadWrite: + case ramses::ERenderBufferAccessMode::ReadWrite: return ramses_internal::ERenderBufferAccessMode_ReadWrite; - default: - assert(false); - return ramses_internal::ERenderBufferAccessMode_Invalid; } + assert(false); + return ramses_internal::ERenderBufferAccessMode_Invalid; } static ERenderBufferFormat GetRenderBufferFormatFromInternal(ramses_internal::ETextureFormat bufferFormat) @@ -592,40 +601,84 @@ namespace ramses switch (bufferFormat) { case ramses_internal::ETextureFormat::RGBA4: - return ERenderBufferFormat_RGBA4; + return ERenderBufferFormat::RGBA4; case ramses_internal::ETextureFormat::R8: - return ERenderBufferFormat_R8; + return ERenderBufferFormat::R8; case ramses_internal::ETextureFormat::RG8: - return ERenderBufferFormat_RG8; + return ERenderBufferFormat::RG8; case ramses_internal::ETextureFormat::RGB8: - return ERenderBufferFormat_RGB8; + return ERenderBufferFormat::RGB8; case ramses_internal::ETextureFormat::RGBA8: - return ERenderBufferFormat_RGBA8; + return ERenderBufferFormat::RGBA8; case ramses_internal::ETextureFormat::R16F: - return ERenderBufferFormat_R16F; + return ERenderBufferFormat::R16F; case ramses_internal::ETextureFormat::R32F: - return ERenderBufferFormat_R32F; + return ERenderBufferFormat::R32F; case ramses_internal::ETextureFormat::RG16F: - return ERenderBufferFormat_RG16F; + return ERenderBufferFormat::RG16F; case ramses_internal::ETextureFormat::RG32F: - return ERenderBufferFormat_RG32F; + return ERenderBufferFormat::RG32F; case ramses_internal::ETextureFormat::RGB16F: - return ERenderBufferFormat_RGB16F; + return ERenderBufferFormat::RGB16F; case ramses_internal::ETextureFormat::RGB32F: - return ERenderBufferFormat_RGB32F; + return ERenderBufferFormat::RGB32F; case ramses_internal::ETextureFormat::RGBA16F: - return ERenderBufferFormat_RGBA16F; + return ERenderBufferFormat::RGBA16F; case ramses_internal::ETextureFormat::RGBA32F: - return ERenderBufferFormat_RGBA32F; + return ERenderBufferFormat::RGBA32F; case ramses_internal::ETextureFormat::Depth24: - return ERenderBufferFormat_Depth24; + return ERenderBufferFormat::Depth24; case ramses_internal::ETextureFormat::Depth24_Stencil8: - return ERenderBufferFormat_Depth24_Stencil8; - default: - assert(false); - return ERenderBufferFormat_RGBA8; + return ERenderBufferFormat::Depth24_Stencil8; + case ramses_internal::ETextureFormat::Invalid: + case ramses_internal::ETextureFormat::RGB565: + case ramses_internal::ETextureFormat::RGBA5551: + case ramses_internal::ETextureFormat::ETC2RGB: + case ramses_internal::ETextureFormat::ETC2RGBA: + case ramses_internal::ETextureFormat::ASTC_RGBA_4x4: + case ramses_internal::ETextureFormat::ASTC_RGBA_5x4: + case ramses_internal::ETextureFormat::ASTC_RGBA_5x5: + case ramses_internal::ETextureFormat::ASTC_RGBA_6x5: + case ramses_internal::ETextureFormat::ASTC_RGBA_6x6: + case ramses_internal::ETextureFormat::ASTC_RGBA_8x5: + case ramses_internal::ETextureFormat::ASTC_RGBA_8x6: + case ramses_internal::ETextureFormat::ASTC_RGBA_8x8: + case ramses_internal::ETextureFormat::ASTC_RGBA_10x5: + case ramses_internal::ETextureFormat::ASTC_RGBA_10x6: + case ramses_internal::ETextureFormat::ASTC_RGBA_10x8: + case ramses_internal::ETextureFormat::ASTC_RGBA_10x10: + case ramses_internal::ETextureFormat::ASTC_RGBA_12x10: + case ramses_internal::ETextureFormat::ASTC_RGBA_12x12: + case ramses_internal::ETextureFormat::ASTC_SRGBA_4x4: + case ramses_internal::ETextureFormat::ASTC_SRGBA_5x4: + case ramses_internal::ETextureFormat::ASTC_SRGBA_5x5: + case ramses_internal::ETextureFormat::ASTC_SRGBA_6x5: + case ramses_internal::ETextureFormat::ASTC_SRGBA_6x6: + case ramses_internal::ETextureFormat::ASTC_SRGBA_8x5: + case ramses_internal::ETextureFormat::ASTC_SRGBA_8x6: + case ramses_internal::ETextureFormat::ASTC_SRGBA_8x8: + case ramses_internal::ETextureFormat::ASTC_SRGBA_10x5: + case ramses_internal::ETextureFormat::ASTC_SRGBA_10x6: + case ramses_internal::ETextureFormat::ASTC_SRGBA_10x8: + case ramses_internal::ETextureFormat::ASTC_SRGBA_10x10: + case ramses_internal::ETextureFormat::ASTC_SRGBA_12x10: + case ramses_internal::ETextureFormat::ASTC_SRGBA_12x12: + case ramses_internal::ETextureFormat::SRGB8: + case ramses_internal::ETextureFormat::SRGB8_ALPHA8: + case ramses_internal::ETextureFormat::R16: + case ramses_internal::ETextureFormat::RG16: + case ramses_internal::ETextureFormat::RGB16: + case ramses_internal::ETextureFormat::RGBA16: + case ramses_internal::ETextureFormat::DXT1RGB: + case ramses_internal::ETextureFormat::DXT3RGBA: + case ramses_internal::ETextureFormat::DXT5RGBA: + case ramses_internal::ETextureFormat::Depth16: + case ramses_internal::ETextureFormat::NUMBER_OF_TYPES: + break; } + assert(false); + return ERenderBufferFormat::RGBA8; } static ERenderBufferAccessMode GetRenderBufferAccessModeFromInternal(ramses_internal::ERenderBufferAccessMode accessMode) @@ -633,50 +686,58 @@ namespace ramses switch (accessMode) { case ramses_internal::ERenderBufferAccessMode_WriteOnly: - return ERenderBufferAccessMode_WriteOnly; + return ERenderBufferAccessMode::WriteOnly; case ramses_internal::ERenderBufferAccessMode_ReadWrite: - return ERenderBufferAccessMode_ReadWrite; - default: - assert(false); - return ERenderBufferAccessMode_ReadWrite; + return ERenderBufferAccessMode::ReadWrite; + case ramses_internal::ERenderBufferAccessMode_Invalid: + case ramses_internal::ERenderBufferAccessMode_NUMBER_OF_ELEMENTS: + break; } + assert(false); + return ERenderBufferAccessMode::ReadWrite; } static bool IsRenderBufferTypeCompatibleWithFormat(ERenderBufferType bufferType, ERenderBufferFormat bufferFormat) { switch (bufferType) { - case ERenderBufferType_Color: + case ERenderBufferType::Color: return - bufferFormat == ERenderBufferFormat_RGBA4 || - bufferFormat == ERenderBufferFormat_R8 || - bufferFormat == ERenderBufferFormat_RG8 || - bufferFormat == ERenderBufferFormat_RGB8 || - bufferFormat == ERenderBufferFormat_RGBA8 || - bufferFormat == ERenderBufferFormat_R16F || - bufferFormat == ERenderBufferFormat_R32F || - bufferFormat == ERenderBufferFormat_RG16F || - bufferFormat == ERenderBufferFormat_RG32F || - bufferFormat == ERenderBufferFormat_RGB16F || - bufferFormat == ERenderBufferFormat_RGB32F || - bufferFormat == ERenderBufferFormat_RGBA16F || - bufferFormat == ERenderBufferFormat_RGBA32F; - case ERenderBufferType_Depth: - return bufferFormat == ERenderBufferFormat_Depth24; - case ERenderBufferType_DepthStencil: - return bufferFormat == ERenderBufferFormat_Depth24_Stencil8; - default: - assert(false); - return false; + bufferFormat == ERenderBufferFormat::RGBA4 || + bufferFormat == ERenderBufferFormat::R8 || + bufferFormat == ERenderBufferFormat::RG8 || + bufferFormat == ERenderBufferFormat::RGB8 || + bufferFormat == ERenderBufferFormat::RGBA8 || + bufferFormat == ERenderBufferFormat::R16F || + bufferFormat == ERenderBufferFormat::R32F || + bufferFormat == ERenderBufferFormat::RG16F || + bufferFormat == ERenderBufferFormat::RG32F || + bufferFormat == ERenderBufferFormat::RGB16F || + bufferFormat == ERenderBufferFormat::RGB32F || + bufferFormat == ERenderBufferFormat::RGBA16F || + bufferFormat == ERenderBufferFormat::RGBA32F; + case ERenderBufferType::Depth: + return bufferFormat == ERenderBufferFormat::Depth24; + case ERenderBufferType::DepthStencil: + return bufferFormat == ERenderBufferFormat::Depth24_Stencil8; } + assert(false); + return false; } + // NOLINTNEXTLINE(modernize-avoid-c-arrays) static void FillMipDataSizes(ramses_internal::MipDataSizeVector& mipDataSizes, uint32_t mipMapCount, const MipLevelData mipLevelData[]); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) static void FillMipDataSizes(ramses_internal::MipDataSizeVector& mipDataSizes, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[]); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) static void FillMipData(uint8_t* dest, uint32_t mipMapCount, const MipLevelData mipLevelData[]); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) static void FillMipData(uint8_t* dest, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[]); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) static bool MipDataValid(uint32_t width, uint32_t height, uint32_t depth, uint32_t mipMapCount, const MipLevelData mipLevelData[], ETextureFormat format); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) static bool MipDataValid(uint32_t width, uint32_t height, uint32_t depth, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[], ETextureFormat format); + // NOLINTNEXTLINE(modernize-avoid-c-arrays) static bool MipDataValid(uint32_t size, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[], ETextureFormat format); static bool TextureParametersValid(uint32_t width, uint32_t height, uint32_t depth, uint32_t mipMapCount); }; diff --git a/client/ramses-client/impl/VisibilityModeUtils.cpp b/client/ramses-client/impl/VisibilityModeUtils.cpp index 39649df81..99aeb9e5d 100644 --- a/client/ramses-client/impl/VisibilityModeUtils.cpp +++ b/client/ramses-client/impl/VisibilityModeUtils.cpp @@ -41,7 +41,7 @@ namespace ramses return ramses_internal::EVisibilityMode::Visible; } - const char* const VisibilityModeNames[] = + const std::array VisibilityModeNames = { "Off", "Invisible", diff --git a/client/ramses-client/impl/glslEffectBlock/GLSlang.h b/client/ramses-client/impl/glslEffectBlock/GLSlang.h index 1c331f2f7..44a39b268 100644 --- a/client/ramses-client/impl/glslEffectBlock/GLSlang.h +++ b/client/ramses-client/impl/glslEffectBlock/GLSlang.h @@ -25,20 +25,12 @@ WARNING_DISABLE_LINUX(-Wnon-virtual-dtor) WARNING_DISABLE_GCC(-Wsuggest-override) WARNING_DISABLE_GCC9(-Wdeprecated-copy) -#ifdef __ghs__ -#define __inline inline -#endif - // INCLUDE PROBLEMATIC HEADERS #include "Include/intermediate.h" #include "Include/InitializeGlobals.h" #include "MachineIndependent/localintermediate.h" #include "OGLCompilersDLL/InitializeDll.h" -#ifdef __ghs__ -#undef __inline -#endif - WARNINGS_POP #include "Public/ShaderLang.h" diff --git a/client/ramses-client/impl/glslEffectBlock/GlslEffect.cpp b/client/ramses-client/impl/glslEffectBlock/GlslEffect.cpp index 64183be47..f468b9b94 100644 --- a/client/ramses-client/impl/glslEffectBlock/GlslEffect.cpp +++ b/client/ramses-client/impl/glslEffectBlock/GlslEffect.cpp @@ -10,7 +10,6 @@ #include "Resource/EffectResource.h" #include "glslEffectBlock/GlslToEffectConverter.h" #include "GlslLimits.h" -#include "absl/algorithm/container.h" #include namespace ramses_internal @@ -39,12 +38,12 @@ namespace ramses_internal static GlslangInitAndFinalizeOnceHelper glslangInitializer; - GlslEffect::GlslEffect(const String& vertexShader, - const String& fragmentShader, - const String& geometryShader, - const std::vector& compilerDefines, - const HashMap& semanticInputs, - const String& name) + GlslEffect::GlslEffect(std::string_view vertexShader, + std::string_view fragmentShader, + std::string_view geometryShader, + const std::vector& compilerDefines, + const HashMap& semanticInputs, + std::string_view name) : m_vertexShader(vertexShader) , m_fragmentShader(fragmentShader) , m_geometryShader(geometryShader) @@ -67,7 +66,7 @@ namespace ramses_internal return m_effectResource; } - String defineString = createDefineString(); + std::string defineString = createDefineString(); ShaderParts vertexShaderParts; ShaderParts fragmentShaderParts; @@ -123,30 +122,30 @@ namespace ramses_internal const EffectInputInformationVector& uniformInputs = glslToEffectConverter.getUniformInputs(); const EffectInputInformationVector& attributeInputs = glslToEffectConverter.getAttributeInputs(); - absl::optional geomInputType = glslToEffectConverter.getGeometryShaderInputType(); + const auto geomInputType = glslToEffectConverter.getGeometryShaderInputType(); m_effectResource = new EffectResource(mergeShaderParts(vertexShaderParts), mergeShaderParts(fragmentShaderParts), mergeShaderParts(geometryShaderParts), geomInputType, uniformInputs, attributeInputs, m_name, cacheFlag); return m_effectResource; } - String GlslEffect::createDefineString() const + std::string GlslEffect::createDefineString() const { StringOutputStream result; for(const auto& defineString : m_compilerDefines) { result << "#define " << defineString << "\n"; } - return String(result.release()); + return result.release(); } - bool GlslEffect::createShaderParts(ShaderParts& outParts, const String& defineString, const String& userShader) const + bool GlslEffect::createShaderParts(ShaderParts& outParts, const std::string& defineString, const std::string& userShader) const { size_t versionStringStart; - String versionString; - if ((versionStringStart = userShader.find("#version")) != String::npos) + std::string versionString; + if ((versionStringStart = userShader.find("#version")) != std::string::npos) { size_t versionStringEnd = userShader.find('\n', versionStringStart); - if (versionStringEnd == String::npos) + if (versionStringEnd == std::string::npos) { m_errorMessages << "[GLSL Compiler] " << m_name << " Shader contains #version without newline \n"; return false; @@ -165,17 +164,17 @@ namespace ramses_internal return true; } - String GlslEffect::mergeShaderParts(const ShaderParts& shaderParts) const + std::string GlslEffect::mergeShaderParts(const ShaderParts& shaderParts) const { StringOutputStream str; str << shaderParts.version << shaderParts.defines << shaderParts.userCode; - return String(str.release()); + return str.release(); } - bool GlslEffect::parseShader(glslang::TShader& tShader, const TBuiltInResource& glslCompilationResources, const ShaderParts& shaderParts, const String& shaderName) + bool GlslEffect::parseShader(glslang::TShader& tShader, const TBuiltInResource& glslCompilationResources, const ShaderParts& shaderParts, const std::string& shaderName) { - const char* fragmentShaderCodeCString[] = { shaderParts.version.c_str(), shaderParts.defines.c_str(), shaderParts.userCode.c_str() }; - tShader.setStrings(fragmentShaderCodeCString, 3); + const std::array fragmentShaderCodeCString = { shaderParts.version.c_str(), shaderParts.defines.c_str(), shaderParts.userCode.c_str() }; + tShader.setStrings(fragmentShaderCodeCString.data(), 3); bool parsingSuccessful = tShader.parse(&glslCompilationResources, 100, false, EShMsgDefault); if (!parsingSuccessful) { @@ -222,9 +221,9 @@ namespace ramses_internal return false; } - const UInt32 vertexShaderVersion = program->getIntermediate(EShLangVertex)->getVersion(); - const UInt32 fragmentShaderVersion = program->getIntermediate(EShLangFragment)->getVersion(); - const UInt32 geometryShaderVersion = hasGeometryShader ? geometryShaderIntermediate->getVersion() : 0u; + const uint32_t vertexShaderVersion = program->getIntermediate(EShLangVertex)->getVersion(); + const uint32_t fragmentShaderVersion = program->getIntermediate(EShLangFragment)->getVersion(); + const uint32_t geometryShaderVersion = hasGeometryShader ? geometryShaderIntermediate->getVersion() : 0u; const uint32_t maximumSupportedVersion = 320u; @@ -275,21 +274,21 @@ namespace ramses_internal return success; } - UInt32 GlslEffect::getShadingLanguageVersion() const + uint32_t GlslEffect::getShadingLanguageVersion() const { assert(m_effectResource); return m_shadingLanguageVersion; } - ramses_internal::String GlslEffect::getEffectErrorMessages() const + std::string GlslEffect::getEffectErrorMessages() const { - return ramses_internal::String(m_errorMessages.c_str()); + return m_errorMessages.data(); } - bool GlslEffect::isSupportedExtension(const String& extension) const + bool GlslEffect::isSupportedExtension(const std::string& extension) const { - if (String("GL_OES_EGL_image_external") == extension - || String("GL_OES_EGL_image_external_essl3") == extension) + if (extension == "GL_OES_EGL_image_external" + || extension == "GL_OES_EGL_image_external_essl3") { return true; } diff --git a/client/ramses-client/impl/glslEffectBlock/GlslEffect.h b/client/ramses-client/impl/glslEffectBlock/GlslEffect.h index b8bfce789..b86868b3e 100644 --- a/client/ramses-client/impl/glslEffectBlock/GlslEffect.h +++ b/client/ramses-client/impl/glslEffectBlock/GlslEffect.h @@ -15,7 +15,9 @@ #include "SceneAPI/EFixedSemantics.h" #include "SceneAPI/RenderState.h" #include "Resource/IResource.h" -#include "absl/types/optional.h" + +#include +#include struct TBuiltInResource; namespace glslang @@ -32,47 +34,47 @@ namespace ramses_internal class GlslEffect { public: - GlslEffect(const String& vertexShader, - const String& fragmentShader, - const String& geometryShader, - const std::vector& compilerDefines, - const HashMap& semanticInputs, - const String& name); + GlslEffect(std::string_view vertexShader, + std::string_view fragmentShader, + std::string_view geometryShader, + const std::vector& compilerDefines, + const HashMap& semanticInputs, + std::string_view name); ~GlslEffect(); EffectResource* createEffectResource(ResourceCacheFlag cacheFlag); - UInt32 getShadingLanguageVersion() const; - String getEffectErrorMessages() const; + uint32_t getShadingLanguageVersion() const; + std::string getEffectErrorMessages() const; private: struct ShaderParts { - String version; - String defines; - String userCode; + std::string version; + std::string defines; + std::string userCode; }; - const String m_vertexShader; - const String m_fragmentShader; - const String m_geometryShader; - const std::vector m_compilerDefines; - const HashMap m_semanticInputs; - const String m_name; + const std::string m_vertexShader; + const std::string m_fragmentShader; + const std::string m_geometryShader; + const std::vector m_compilerDefines; + const HashMap m_semanticInputs; + const std::string m_name; mutable StringOutputStream m_errorMessages; EffectResource* m_effectResource; - UInt32 m_shadingLanguageVersion; + uint32_t m_shadingLanguageVersion; - String createDefineString() const; - bool createShaderParts(ShaderParts& outParts, const String& defineString, const String& userShader) const; - String mergeShaderParts(const ShaderParts& shaderParts) const; - bool parseShader(glslang::TShader& tShader, const TBuiltInResource& glslCompilationResources, const ShaderParts& shaderParts, const String& shaderName); + std::string createDefineString() const; + bool createShaderParts(ShaderParts& outParts, const std::string& defineString, const std::string& userShader) const; + std::string mergeShaderParts(const ShaderParts& shaderParts) const; + bool parseShader(glslang::TShader& tShader, const TBuiltInResource& glslCompilationResources, const ShaderParts& shaderParts, const std::string& shaderName); glslang::TProgram* linkProgram(glslang::TShader* vertexShader, glslang::TShader* fragmentShader, glslang::TShader* geometryShader) const; bool extractAndCheckShaderVersions(const glslang::TProgram* program); bool extractAndCheckExtensions(const glslang::TProgram* program); - bool isSupportedExtension(const String& extension) const; + bool isSupportedExtension(const std::string& extension) const; }; } diff --git a/client/ramses-client/impl/glslEffectBlock/GlslLimits.h b/client/ramses-client/impl/glslEffectBlock/GlslLimits.h index 1d347e9f4..cc1de272c 100644 --- a/client/ramses-client/impl/glslEffectBlock/GlslLimits.h +++ b/client/ramses-client/impl/glslEffectBlock/GlslLimits.h @@ -9,6 +9,11 @@ #ifndef RAMSES_GLSLLIMITS_H #define RAMSES_GLSLLIMITS_H +#include "PlatformAbstraction/PlatformTypes.h" + +#include +#include + struct TBuiltInResource; namespace ramses_internal @@ -17,12 +22,12 @@ namespace ramses_internal { public: - static void InitCompilationResources(TBuiltInResource& glslCompilationResources, const String& glslVersion) + static void InitCompilationResources(TBuiltInResource& glslCompilationResources, std::string_view glslVersion) { SetDefaults(glslCompilationResources); - const UInt32 version = GetVersionFromString(glslVersion); - const Bool isES = IsESVersion(glslVersion); + const uint32_t version = GetVersionFromString(glslVersion); + const bool isES = IsESVersion(glslVersion); if (isES) { @@ -49,21 +54,21 @@ namespace ramses_internal } } - static UInt32 GetVersionFromString(const String& glslVersion) + static uint32_t GetVersionFromString(std::string_view glslVersion) { - Char buffer[16]; - UInt32 n = 0; + std::array buffer; + uint32_t n = 0; for (size_t i = 0; i < glslVersion.size(); i++) { - const Char& c = glslVersion[i]; + const char& c = glslVersion[i]; if (isdigit(c)) { buffer[n++] = c; } - if (n >= sizeof(buffer)) + if (n >= buffer.size()) { // Avoid out of bounds access, in case the shader contains some crazy version number return 0; @@ -71,14 +76,14 @@ namespace ramses_internal } buffer[n] = 0; - return atoi(buffer); + return std::atoi(buffer.data()); } - static Bool IsESVersion(const String& glslVersion) + static bool IsESVersion(std::string_view glslVersion) { // The input parameter can contain newlines characters, so checking for endsWith() can fail. - static String esIdentifier(" es"); - return glslVersion.find(esIdentifier, 0u) != String::npos; + static constexpr auto esIdentifier{" es"}; + return glslVersion.find(esIdentifier, 0u) != std::string_view::npos; } private: diff --git a/client/ramses-client/impl/glslEffectBlock/GlslToEffectConverter.cpp b/client/ramses-client/impl/glslEffectBlock/GlslToEffectConverter.cpp index aaa894b25..182c680ce 100644 --- a/client/ramses-client/impl/glslEffectBlock/GlslToEffectConverter.cpp +++ b/client/ramses-client/impl/glslEffectBlock/GlslToEffectConverter.cpp @@ -12,7 +12,7 @@ namespace ramses_internal { - GlslToEffectConverter::GlslToEffectConverter(const HashMap& semanticInputs) + GlslToEffectConverter::GlslToEffectConverter(const HashMap& semanticInputs) : m_semanticInputs(semanticInputs) { } @@ -21,7 +21,7 @@ namespace ramses_internal { } - Bool GlslToEffectConverter::parseShaderProgram(glslang::TProgram* program) + bool GlslToEffectConverter::parseShaderProgram(glslang::TProgram* program) { CHECK_RETURN_ERR(parseLinkerObjectsForStage(program->getIntermediate(EShLangVertex)->getTreeRoot(), EShaderStage::Vertex)); // Parse data for vertex stage CHECK_RETURN_ERR(parseLinkerObjectsForStage(program->getIntermediate(EShLangFragment)->getTreeRoot(), EShaderStage::Fragment)); // Parse data for fragment stage @@ -35,6 +35,7 @@ namespace ramses_internal { const glslang::TLayoutGeometry prim = geomShader->getInputPrimitive(); + // only basic 'variant' (i.e. no strip/fan) of a primitive is allowed as GS input declaration switch (prim) { case glslang::ElgPoints: @@ -61,14 +62,13 @@ namespace ramses_internal return true; } - ramses_internal::String GlslToEffectConverter::getStatusMessage() const + std::string GlslToEffectConverter::getStatusMessage() const { - String msg = m_message.c_str(); - if (msg.size() == 0) + if (m_message.size() == 0) { - return String("Ok"); + return "Ok"; } - return msg; + return m_message.data(); } const EffectInputInformationVector& GlslToEffectConverter::getUniformInputs() const @@ -81,7 +81,7 @@ namespace ramses_internal return m_attributeInputs; } - absl::optional GlslToEffectConverter::getGeometryShaderInputType() const + std::optional GlslToEffectConverter::getGeometryShaderInputType() const { return m_geometryShaderInputType; } @@ -130,11 +130,11 @@ namespace ramses_internal if (storageQualifier == glslang::EvqVaryingIn && stage == EShaderStage::Vertex) // 'VaryingIn' means vertex attribute { - return setInputTypeFromType(symbol->getType(), String(symbol->getName().c_str()), m_attributeInputs); + return setInputTypeFromType(symbol->getType(), symbol->getName(), m_attributeInputs); } else if (storageQualifier == glslang::EvqUniform) { - return setInputTypeFromType(symbol->getType(), String(symbol->getName().c_str()), m_uniformInputs); + return setInputTypeFromType(symbol->getType(), symbol->getName(), m_uniformInputs); } return true; @@ -145,13 +145,13 @@ namespace ramses_internal EffectInputInformationVector temp; temp.swap(m_uniformInputs); // Clear current uniforms - will be added back later if they are ok - for (UInt i = 0; i < temp.size(); i++) + for (size_t i = 0; i < temp.size(); i++) { const EffectInputInformation& A = temp[i]; bool add = true; - for (UInt j = i + 1; j < temp.size(); j++) + for (size_t j = i + 1; j < temp.size(); j++) { const EffectInputInformation& B = temp[j]; @@ -179,7 +179,7 @@ namespace ramses_internal return true; } - bool GlslToEffectConverter::setInputTypeFromType(const glslang::TType& type, const String& inputName, EffectInputInformationVector& outputVector) const + bool GlslToEffectConverter::setInputTypeFromType(const glslang::TType& type, std::string_view inputName, EffectInputInformationVector& outputVector) const { uint32_t elementCount; CHECK_RETURN_ERR(getElementCountFromType(type, inputName, elementCount)); @@ -200,8 +200,7 @@ namespace ramses_internal for(const auto& structField : *structFields) { const glslang::TType& fieldType = *structField.type; - const String fieldName = String(fieldType.getFieldName().c_str()); - const String subName = getStructFieldIdentifier(inputName, fieldName, type.isArray() ? static_cast(i) : -1); + const auto subName = getStructFieldIdentifier(inputName, fieldType.getFieldName(), type.isArray() ? static_cast(i) : -1); // Get the element count for the field uint32_t newElementCount; @@ -223,7 +222,7 @@ namespace ramses_internal return true; } - const String GlslToEffectConverter::getStructFieldIdentifier(const String& baseName, const String& fieldName, const int32_t arrayIndex) const + std::string GlslToEffectConverter::getStructFieldIdentifier(std::string_view baseName, std::string_view fieldName, const int32_t arrayIndex) const { StringOutputStream stream; stream << baseName; @@ -238,10 +237,10 @@ namespace ramses_internal stream << '.'; stream << fieldName; - return String(stream.release()); + return stream.release(); } - bool GlslToEffectConverter::createEffectInputType(const glslang::TType& type, const String& inputName, uint32_t elementCount, EffectInputInformationVector& outputVector) const + bool GlslToEffectConverter::createEffectInputType(const glslang::TType& type, std::string_view inputName, uint32_t elementCount, EffectInputInformationVector& outputVector) const { EffectInputInformation input; input.inputName = inputName; @@ -424,7 +423,7 @@ namespace ramses_internal return true; } - bool GlslToEffectConverter::getElementCountFromType(const glslang::TType& type, const String& inputName, uint32_t& elementCount) const + bool GlslToEffectConverter::getElementCountFromType(const glslang::TType& type, std::string_view inputName, uint32_t& elementCount) const { if (type.isArray()) { diff --git a/client/ramses-client/impl/glslEffectBlock/GlslToEffectConverter.h b/client/ramses-client/impl/glslEffectBlock/GlslToEffectConverter.h index 5c235d139..872e20f3d 100644 --- a/client/ramses-client/impl/glslEffectBlock/GlslToEffectConverter.h +++ b/client/ramses-client/impl/glslEffectBlock/GlslToEffectConverter.h @@ -12,27 +12,29 @@ #include "glslEffectBlock/GLSlang.h" #include "Collections/StringOutputStream.h" #include "Collections/HashMap.h" -#include "Collections/String.h" #include "SceneAPI/EFixedSemantics.h" #include "Resource/EffectInputInformation.h" #include "SceneAPI/RenderState.h" -#include "absl/types/optional.h" +#include + +#include +#include namespace ramses_internal { class GlslToEffectConverter { public: - explicit GlslToEffectConverter(const HashMap& semanticInputs); + explicit GlslToEffectConverter(const HashMap& semanticInputs); ~GlslToEffectConverter(); - Bool parseShaderProgram(glslang::TProgram* program); - String getStatusMessage() const; + bool parseShaderProgram(glslang::TProgram* program); + std::string getStatusMessage() const; const EffectInputInformationVector& getUniformInputs() const; const EffectInputInformationVector& getAttributeInputs() const; - absl::optional getGeometryShaderInputType() const; + std::optional getGeometryShaderInputType() const; private: enum class EShaderStage @@ -43,26 +45,26 @@ namespace ramses_internal Invalid }; - HashMap m_semanticInputs; + HashMap m_semanticInputs; mutable StringOutputStream m_message; EffectInputInformationVector m_uniformInputs; EffectInputInformationVector m_attributeInputs; - absl::optional m_geometryShaderInputType; + std::optional m_geometryShaderInputType; bool parseLinkerObjectsForStage(const TIntermNode* node, EShaderStage stage); const glslang::TIntermSequence* getLinkerObjectSequence(const TIntermNode* node) const; bool handleSymbol(const glslang::TIntermSymbol* symbol, EShaderStage stage); - bool getElementCountFromType(const glslang::TType& type, const String& inputName, uint32_t& elementCount) const; - bool setInputTypeFromType(const glslang::TType& type, const String& inputName, EffectInputInformationVector& outputVector) const; + bool getElementCountFromType(const glslang::TType& type, std::string_view inputName, uint32_t& elementCount) const; + bool setInputTypeFromType(const glslang::TType& type, std::string_view inputName, EffectInputInformationVector& outputVector) const; bool setInputTypeFromType(const glslang::TType& type, EffectInputInformation& input) const; bool replaceVertexAttributeWithBufferVariant(); bool setSemanticsOnInput(EffectInputInformation& input) const; bool makeUniformsUnique(); - bool createEffectInputType(const glslang::TType& type, const String& inputName, uint32_t elementCount, EffectInputInformationVector& outputVector) const; - const String getStructFieldIdentifier(const String& baseName, const String& fieldName, const int32_t arrayIndex) const; + bool createEffectInputType(const glslang::TType& type, std::string_view inputName, uint32_t elementCount, EffectInputInformationVector& outputVector) const; + std::string getStructFieldIdentifier(std::string_view baseName, std::string_view fieldName, const int32_t arrayIndex) const; }; } diff --git a/client/ramses-client/impl/ramses-hmi-utils.cpp b/client/ramses-client/impl/ramses-hmi-utils.cpp deleted file mode 100644 index e8336b239..000000000 --- a/client/ramses-client/impl/ramses-hmi-utils.cpp +++ /dev/null @@ -1,82 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-hmi-utils.h" - -// API -#include "ramses-client-api/Node.h" -#include "ramses-client-api/RamsesClient.h" -#include "ramses-client-api/Scene.h" -#include "ramses-client-api/SceneObjectIterator.h" -#include "ramses-client-api/MeshNode.h" -#include "ramses-client-api/SceneGraphIterator.h" -#include "ramses-client-api/RenderGroup.h" -#include "ramses-client-api/ArrayResource.h" -#include "ramses-client-api/ResourceDataPool.h" - -// internal -#include "SceneImpl.h" -#include "RamsesClientImpl.h" -#include "MeshNodeImpl.h" -#include "SceneUtils/ResourceUtils.h" -#include "Scene/ClientScene.h" -#include "RamsesClientImpl.h" -#include "RamsesObjectTypeUtils.h" -#include "ResourceImpl.h" -#include "SceneDumper.h" -#include - -namespace ramses -{ - void RamsesHMIUtils::DumpUnrequiredSceneObjects(const Scene& scene) - { - LOG_INFO_F(ramses_internal::CONTEXT_CLIENT, [&] (ramses_internal::StringOutputStream& output) {SceneDumper sceneDumper(scene.impl); sceneDumper.dumpUnrequiredObjects(output); }); - } - - void RamsesHMIUtils::DumpUnrequiredSceneObjectsToFile(const Scene& scene, std::ostream& out) - { - ramses_internal::StringOutputStream output; - SceneDumper sceneDumper(scene.impl); - sceneDumper.dumpUnrequiredObjects(output); - out << output.release().c_str(); - } - - bool RamsesHMIUtils::AllResourcesForSceneKnown(const Scene& scene) - { - SceneObjectIterator iter(scene, ERamsesObjectType_Resource); - ramses_internal::HashSet knownHashes; - auto obj = iter.getNext(); - while (obj) - { - const ramses::Resource* res = &ramses::RamsesObjectTypeUtils::ConvertTo(*obj); - knownHashes.put(res->impl.getLowlevelResourceHash()); - obj = iter.getNext(); - } - - ramses_internal::ResourceContentHashVector resourcesUsedInScene; - ramses_internal::ResourceUtils::GetAllResourcesFromScene(resourcesUsedInScene, scene.impl.getIScene()); - for (const auto hash : resourcesUsedInScene) - { - if (!knownHashes.contains(hash)) - { - return false; - } - } - return true; - } - - ResourceDataPool& RamsesHMIUtils::GetResourceDataPoolForClient(RamsesClient& client) - { - return client.impl.getResourceDataPool(); - } - - bool RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(Scene const& scene, std::string const& filename, bool compress) - { - return scene.impl.saveResources(filename, compress); - } -} diff --git a/client/ramses-client/impl/ramses-text/FontRegistryImpl.cpp b/client/ramses-client/impl/ramses-text/FontRegistryImpl.cpp index 77c9405df..168d6f358 100644 --- a/client/ramses-client/impl/ramses-text/FontRegistryImpl.cpp +++ b/client/ramses-client/impl/ramses-text/FontRegistryImpl.cpp @@ -57,10 +57,10 @@ namespace ramses return it != m_fontInstances.cend() ? it->second.get() : nullptr; } - FontId FontRegistryImpl::createFreetype2Font(const char* fontPath) + FontId FontRegistryImpl::createFreetype2Font(std::string_view fontPath) { // basic checks - if (fontPath == nullptr || !ramses_internal::File(fontPath).exists()) + if (fontPath.empty() || !ramses_internal::File(fontPath).exists()) { LOG_ERROR(CONTEXT_TEXT, "FontRegistry::createFreetype2Font: Font file not found " << fontPath); return {}; diff --git a/client/ramses-client/impl/ramses-text/FontRegistryImpl.h b/client/ramses-client/impl/ramses-text/FontRegistryImpl.h index 42e2249ce..c3a9661e3 100644 --- a/client/ramses-client/impl/ramses-text/FontRegistryImpl.h +++ b/client/ramses-client/impl/ramses-text/FontRegistryImpl.h @@ -12,8 +12,10 @@ #include "ramses-text-api/IFontInstance.h" #include "ramses-text/Freetype2Wrapper.h" #include "ramses-text/FreetypeFontFace.h" + #include #include +#include namespace ramses { @@ -35,9 +37,9 @@ namespace ramses public: FontRegistryImpl() = default; - IFontInstance* getFontInstance(FontInstanceId fontInstanceId) const; + [[nodiscard]] IFontInstance* getFontInstance(FontInstanceId fontInstanceId) const; - FontId createFreetype2Font(const char* fontPath); + FontId createFreetype2Font(std::string_view fontPath); FontId createFreetype2FontFromFileDescriptor(int fd, size_t offset, size_t length); FontInstanceId createFreetype2FontInstance(FontId fontId, uint32_t size, bool forceAutohinting); diff --git a/client/ramses-client/impl/ramses-text/Freetype2FontInstance.cpp b/client/ramses-client/impl/ramses-text/Freetype2FontInstance.cpp index df2d14234..3203e7ba4 100644 --- a/client/ramses-client/impl/ramses-text/Freetype2FontInstance.cpp +++ b/client/ramses-client/impl/ramses-text/Freetype2FontInstance.cpp @@ -210,6 +210,7 @@ namespace ramses int32_t Freetype2FontInstance::getKerningAdvance(GlyphId glyphIdentifier1, GlyphId glyphIdentifier2) const { + // NOLINTNEXTLINE(hicpp-signed-bitwise) if (FT_HAS_KERNING(m_face)) { if (glyphIdentifier1.getValue() != 0 && glyphIdentifier2.getValue() != 0) diff --git a/client/ramses-client/impl/ramses-text/Freetype2FontInstance.h b/client/ramses-client/impl/ramses-text/Freetype2FontInstance.h index 90b0268b5..ce3bef69d 100644 --- a/client/ramses-client/impl/ramses-text/Freetype2FontInstance.h +++ b/client/ramses-client/impl/ramses-text/Freetype2FontInstance.h @@ -23,18 +23,18 @@ namespace ramses { public: Freetype2FontInstance(FontInstanceId id, FT_Face fontFace, uint32_t pixelSize, bool forceAutohinting); - virtual ~Freetype2FontInstance() override; + ~Freetype2FontInstance() override; - virtual bool supportsCharacter(char32_t character) const override final; - virtual int getHeight() const override; - virtual int getAscender() const override; - virtual int getDescender() const override; + [[nodiscard]] bool supportsCharacter(char32_t character) const final override; + [[nodiscard]] int getHeight() const override; + [[nodiscard]] int getAscender() const override; + [[nodiscard]] int getDescender() const override; - virtual void loadAndAppendGlyphMetrics(std::u32string::const_iterator charsBegin, std::u32string::const_iterator charsEnd, GlyphMetricsVector& positionedGlyphs) override; - virtual GlyphData loadGlyphBitmapData(GlyphId glyphId, uint32_t& sizeX, uint32_t& sizeY) override final; + void loadAndAppendGlyphMetrics(std::u32string::const_iterator charsBegin, std::u32string::const_iterator charsEnd, GlyphMetricsVector& positionedGlyphs) override; + GlyphData loadGlyphBitmapData(GlyphId glyphId, uint32_t& sizeX, uint32_t& sizeY) final override; std::unordered_set getAllSupportedCharacters() override; - GlyphId getGlyphId(char32_t character) const; + [[nodiscard]] GlyphId getGlyphId(char32_t character) const; protected: struct GlyphBitmapData; @@ -43,7 +43,7 @@ namespace ramses const GlyphBitmapData* getGlyphBitmapData(GlyphId glyphId); bool loadGlyph(GlyphId glyphId); void activateSize() const; - int32_t getKerningAdvance(GlyphId glyphIdentifier1, GlyphId glyphIdentifier2) const; + [[nodiscard]] int32_t getKerningAdvance(GlyphId glyphIdentifier1, GlyphId glyphIdentifier2) const; void cacheAllSupportedCharacters(); FontInstanceId m_id; diff --git a/client/ramses-client/impl/ramses-text/FreetypeFontFace.cpp b/client/ramses-client/impl/ramses-text/FreetypeFontFace.cpp index 77880c75d..dfd6d304a 100644 --- a/client/ramses-client/impl/ramses-text/FreetypeFontFace.cpp +++ b/client/ramses-client/impl/ramses-text/FreetypeFontFace.cpp @@ -71,11 +71,11 @@ namespace ramses_internal // ============================================================= - FreetypeFontFaceFilePath::FreetypeFontFaceFilePath(const char* fontPath, FT_Library freetypeLib) + FreetypeFontFaceFilePath::FreetypeFontFaceFilePath(std::string_view fontPath, FT_Library freetypeLib) : FreetypeFontFace(freetypeLib) , m_fontPath(fontPath) { - assert(fontPath); + assert(!fontPath.empty()); } bool FreetypeFontFaceFilePath::init() diff --git a/client/ramses-client/impl/ramses-text/FreetypeFontFace.h b/client/ramses-client/impl/ramses-text/FreetypeFontFace.h index 018b4e64b..5071b968c 100644 --- a/client/ramses-client/impl/ramses-text/FreetypeFontFace.h +++ b/client/ramses-client/impl/ramses-text/FreetypeFontFace.h @@ -11,7 +11,9 @@ #include "ramses-text/Freetype2Wrapper.h" #include "Utils/BinaryOffsetFileInputStream.h" + #include +#include namespace ramses_internal { @@ -40,7 +42,7 @@ namespace ramses_internal class FreetypeFontFaceFilePath : public FreetypeFontFace { public: - FreetypeFontFaceFilePath(const char* fontPath, FT_Library freetypeLib); + FreetypeFontFaceFilePath(std::string_view fontPath, FT_Library freetypeLib); bool init() override; diff --git a/client/ramses-client/impl/ramses-text/GlyphGeometry.h b/client/ramses-client/impl/ramses-text/GlyphGeometry.h index dcdd95842..54eea4144 100644 --- a/client/ramses-client/impl/ramses-text/GlyphGeometry.h +++ b/client/ramses-client/impl/ramses-text/GlyphGeometry.h @@ -9,6 +9,7 @@ #ifndef RAMSES_GLYPHGEOMETRY_H #define RAMSES_GLYPHGEOMETRY_H +#include "ramses-framework-api/DataTypes.h" #include "Utils/AssertMovable.h" #include @@ -20,8 +21,8 @@ namespace ramses struct GlyphGeometry { std::vector indices; - std::vector positions; - std::vector texcoords; + std::vector positions; + std::vector texcoords; size_t atlasPage = std::numeric_limits::max(); }; diff --git a/client/ramses-client/impl/ramses-text/GlyphTextureAtlas.cpp b/client/ramses-client/impl/ramses-text/GlyphTextureAtlas.cpp index f388cb2d8..4c48eef23 100644 --- a/client/ramses-client/impl/ramses-text/GlyphTextureAtlas.cpp +++ b/client/ramses-client/impl/ramses-text/GlyphTextureAtlas.cpp @@ -188,14 +188,10 @@ namespace ramses const float maxX = minX + glyph.width + 1.0f; const float maxY = minY + glyph.height + 1.0f; - geometry.positions.push_back(minX); // LowerLeft - geometry.positions.push_back(minY); - geometry.positions.push_back(minX); // UpperLeft - geometry.positions.push_back(maxY); - geometry.positions.push_back(maxX); // UpperRight - geometry.positions.push_back(maxY); - geometry.positions.push_back(maxX); // LowerRight - geometry.positions.push_back(minY); + geometry.positions.push_back(vec2f{ minX, minY }); // LowerLeft + geometry.positions.push_back(vec2f{ minX, maxY }); // UpperLeft + geometry.positions.push_back(vec2f{ maxX, maxY }); // UpperRight + geometry.positions.push_back(vec2f{ maxX, minY }); // LowerRight const Quad& glyphQuad = m_glyphInfoMap.at(glyphKey).glyphMapping.at(atlasPage).quad; const float lowerLeftX = glyphQuad.getOrigin().x + 0.5f; @@ -214,14 +210,10 @@ namespace ramses const float upperRightX_textureSpace = upperRightX / textureWidth; const float upperRightY_textureSpace = upperRightY / textureHeight; - geometry.texcoords.push_back(lowerLeftX_textureSpace); - geometry.texcoords.push_back(upperRightY_textureSpace); - geometry.texcoords.push_back(lowerLeftX_textureSpace); - geometry.texcoords.push_back(lowerLeftY_textureSpace); - geometry.texcoords.push_back(upperRightX_textureSpace); - geometry.texcoords.push_back(lowerLeftY_textureSpace); - geometry.texcoords.push_back(upperRightX_textureSpace); - geometry.texcoords.push_back(upperRightY_textureSpace); + geometry.texcoords.push_back(vec2f{ lowerLeftX_textureSpace, upperRightY_textureSpace }); + geometry.texcoords.push_back(vec2f{ lowerLeftX_textureSpace, lowerLeftY_textureSpace }); + geometry.texcoords.push_back(vec2f{ upperRightX_textureSpace, lowerLeftY_textureSpace }); + geometry.texcoords.push_back(vec2f{ upperRightX_textureSpace, upperRightY_textureSpace }); geometry.indices.push_back(indicesCounter + 2); geometry.indices.push_back(indicesCounter + 1); diff --git a/client/ramses-client/impl/ramses-text/GlyphTexturePage.cpp b/client/ramses-client/impl/ramses-text/GlyphTexturePage.cpp index 6b1b78b62..d3180f335 100644 --- a/client/ramses-client/impl/ramses-text/GlyphTexturePage.cpp +++ b/client/ramses-client/impl/ramses-text/GlyphTexturePage.cpp @@ -42,10 +42,10 @@ namespace ramses 1u, "")) , m_textureSampler(*scene.createTextureSampler( - ETextureAddressMode_Clamp, - ETextureAddressMode_Clamp, - ETextureSamplingMethod_Linear, - ETextureSamplingMethod_Linear, + ETextureAddressMode::Clamp, + ETextureAddressMode::Clamp, + ETextureSamplingMethod::Linear, + ETextureSamplingMethod::Linear, m_textureBuffer)) { m_freeQuads.push_back(Quad(QuadOffset(0, 0), size)); diff --git a/client/ramses-client/impl/ramses-text/GlyphTexturePage.h b/client/ramses-client/impl/ramses-text/GlyphTexturePage.h index 9a07872d3..67dcc1333 100644 --- a/client/ramses-client/impl/ramses-text/GlyphTexturePage.h +++ b/client/ramses-client/impl/ramses-text/GlyphTexturePage.h @@ -32,15 +32,15 @@ namespace ramses using GlyphPageData = std::vector; // Free space management - const Quads& getFreeSpace() const; + [[nodiscard]] const Quads& getFreeSpace() const; QuadOffset claimSpace(QuadIndex freeQuadIndex, const QuadSize& subportionSize); void releaseSpace(Quad box); - QuadIndex findFreeSpace(QuadSize const& size) const; + [[nodiscard]] QuadIndex findFreeSpace(QuadSize const& size) const; // Texture data management void updateDataWithPadding(const Quad& targetQuad, const uint8_t* sourceData, GlyphPageData& cacheForDataUpdate); - const Texture2DBuffer& getTextureBuffer() const; - const TextureSampler& getSampler() const; + [[nodiscard]] const Texture2DBuffer& getTextureBuffer() const; + [[nodiscard]] const TextureSampler& getSampler() const; private: bool mergeFreeQuad(Quad& freeQuadInAndOut); diff --git a/client/ramses-client/impl/ramses-text/HarfbuzzFontInstance.h b/client/ramses-client/impl/ramses-text/HarfbuzzFontInstance.h index dcc6026fe..41c3768d3 100644 --- a/client/ramses-client/impl/ramses-text/HarfbuzzFontInstance.h +++ b/client/ramses-client/impl/ramses-text/HarfbuzzFontInstance.h @@ -20,9 +20,9 @@ namespace ramses { public: HarfbuzzFontInstance(FontInstanceId id, FT_Face fontFace, uint32_t pixelSize, bool forceAutohinting); - virtual ~HarfbuzzFontInstance() override; + ~HarfbuzzFontInstance() override; - virtual void loadAndAppendGlyphMetrics(std::u32string::const_iterator charsBegin, std::u32string::const_iterator charsEnd, GlyphMetricsVector& positionedGlyphs) override final; + void loadAndAppendGlyphMetrics(std::u32string::const_iterator charsBegin, std::u32string::const_iterator charsEnd, GlyphMetricsVector& positionedGlyphs) override; HarfbuzzFontInstance(const HarfbuzzFontInstance&) = delete; HarfbuzzFontInstance(HarfbuzzFontInstance&&) = delete; diff --git a/client/ramses-client/impl/ramses-text/Quad.h b/client/ramses-client/impl/ramses-text/Quad.h index e855b79d3..0cec8b3e6 100644 --- a/client/ramses-client/impl/ramses-text/Quad.h +++ b/client/ramses-client/impl/ramses-text/Quad.h @@ -48,7 +48,7 @@ namespace ramses { } - uint32_t getArea() const + [[nodiscard]] uint32_t getArea() const { return x * y; } @@ -63,17 +63,17 @@ namespace ramses { } - const QuadSize& getSize() const + [[nodiscard]] const QuadSize& getSize() const { return m_size; } - const QuadOffset& getOrigin() const + [[nodiscard]] const QuadOffset& getOrigin() const { return m_offset; } - bool hasCommonEdge(const Quad& other) const + [[nodiscard]] bool hasCommonEdge(const Quad& other) const { const uint32_t min1x = m_offset.x; const uint32_t min1y = m_offset.y; @@ -126,7 +126,7 @@ namespace ramses return true; } - bool intersects(Quad const& other) const + [[nodiscard]] bool intersects(Quad const& other) const { return !(m_offset.x >= other.m_offset.x + other.m_size.x || m_offset.y >= other.m_offset.y + other.m_size.y || diff --git a/client/ramses-client/impl/ramses-text/TextCacheImpl.cpp b/client/ramses-client/impl/ramses-text/TextCacheImpl.cpp index 59a1a1be9..c7621f1cb 100644 --- a/client/ramses-client/impl/ramses-text/TextCacheImpl.cpp +++ b/client/ramses-client/impl/ramses-text/TextCacheImpl.cpp @@ -134,8 +134,7 @@ namespace ramses textLine.indices = m_scene.createArrayBuffer(ramses::EDataType::UInt16, numIndices, ""); textLine.indices->updateData(0u, numIndices, geometry.indices.data()); - assert(geometry.positions.size() % 2 == 0); // two floats per Vector2F - const uint32_t numVertexElements = static_cast(geometry.positions.size()) / 2; + const uint32_t numVertexElements = static_cast(geometry.positions.size()); textLine.positions = m_scene.createArrayBuffer(ramses::EDataType::Vector2F, numVertexElements, ""); textLine.positions->updateData(0u, numVertexElements, geometry.positions.data()); diff --git a/client/ramses-client/impl/ramses-utils.cpp b/client/ramses-client/impl/ramses-utils.cpp index 1d4b386fe..cf1dd0167 100644 --- a/client/ramses-client/impl/ramses-utils.cpp +++ b/client/ramses-client/impl/ramses-utils.cpp @@ -18,8 +18,7 @@ #include "ramses-client-api/MipLevelData.h" #include "ramses-client-api/PickableObject.h" #include "ramses-client-api/TextureSwizzle.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector4f.h" +#include "ramses-client-api/DataObject.h" #include "EffectImpl.h" #include "MeshNodeImpl.h" @@ -30,9 +29,12 @@ #include "Math3d/ProjectionParams.h" #include "Utils/File.h" #include "Utils/LogMacros.h" +#include "SceneDumper.h" #include "PlatformAbstraction/PlatformMemory.h" #include "PlatformAbstraction/PlatformMath.h" #include "lodepng.h" +#include +#include namespace ramses { @@ -58,7 +60,7 @@ namespace ramses return nullptr; } - Texture2D* RamsesUtils::CreateTextureResourceFromPng(const char* pngFilePath, Scene& scene, const TextureSwizzle& swizzle, const char* name/* = 0*/) + Texture2D* RamsesUtils::CreateTextureResourceFromPng(const char* pngFilePath, Scene& scene, const TextureSwizzle& swizzle, std::string_view name/* = 0*/) { if (!pngFilePath) { @@ -82,7 +84,7 @@ namespace ramses return scene.createTexture2D(ETextureFormat::RGBA8, width, height, 1, &mipLevelData, false, swizzle, ResourceCacheFlag_DoNotCache, name); } - Texture2D* RamsesUtils::CreateTextureResourceFromPngBuffer(const std::vector& pngData, Scene& scene, const TextureSwizzle& swizzle, const char* name) + Texture2D* RamsesUtils::CreateTextureResourceFromPngBuffer(const std::vector& pngData, Scene& scene, const TextureSwizzle& swizzle, std::string_view name) { unsigned int width = 0; unsigned int height = 0; @@ -174,27 +176,27 @@ namespace ramses bool IsPowerOfTwo(uint32_t val) { - while (((val & 1) == 0) && val > 1) + while (((val & 1u) == 0u) && val > 1u) { - val >>= 1; + val >>= 1u; } - return (val == 1); + return (val == 1u); } uint32_t Log2(uint32_t val) { uint32_t pow = 0; - while (((val & 1) == 0) && val > 1) + while (((val & 1u) == 0u) && val > 1u) { pow++; - val >>= 1; + val >>= 1u; } return pow; } - MipLevelData* RamsesUtils::GenerateMipMapsTexture2D(uint32_t originalWidth, uint32_t originalHeight, uint8_t bytesPerPixel, uint8_t* data, uint32_t& mipMapCount) + MipLevelData* RamsesUtils::GenerateMipMapsTexture2D(uint32_t originalWidth, uint32_t originalHeight, uint8_t bytesPerPixel, uint8_t* data, size_t& mipMapCount) { // copy original data const uint32_t originalSize = originalWidth * originalHeight * bytesPerPixel; @@ -217,7 +219,7 @@ namespace ramses mipLevelData[0].m_size = originalSize; mipLevelData[0].m_data = originalData; - uint32_t currentMipMapIndex = 1u; + size_t currentMipMapIndex = 1u; while (currentMipMapIndex < mipMapCount) { uint32_t nextWidth = std::max(originalWidth >> 1, 1u); @@ -280,11 +282,11 @@ namespace ramses return mipLevelData; } - CubeMipLevelData* RamsesUtils::GenerateMipMapsTextureCube(uint32_t faceWidth, uint32_t faceHeight, uint8_t bytesPerPixel, uint8_t* data, uint32_t& mipMapCount) + CubeMipLevelData* RamsesUtils::GenerateMipMapsTextureCube(uint32_t faceWidth, uint32_t faceHeight, uint8_t bytesPerPixel, uint8_t* data, size_t& mipMapCount) { const uint32_t faceSize = faceWidth * faceHeight * bytesPerPixel; mipMapCount = 0u; - MipLevelData* faceMips[6]; + std::array faceMips; faceMips[0] = GenerateMipMapsTexture2D(faceWidth, faceHeight, bytesPerPixel, &data[faceSize * 0], mipMapCount); faceMips[1] = GenerateMipMapsTexture2D(faceWidth, faceHeight, bytesPerPixel, &data[faceSize * 1], mipMapCount); faceMips[2] = GenerateMipMapsTexture2D(faceWidth, faceHeight, bytesPerPixel, &data[faceSize * 2], mipMapCount); @@ -293,7 +295,7 @@ namespace ramses faceMips[5] = GenerateMipMapsTexture2D(faceWidth, faceHeight, bytesPerPixel, &data[faceSize * 5], mipMapCount); CubeMipLevelData* cubeMipMaps = new CubeMipLevelData[mipMapCount]; - for (uint32_t level = 0; level < mipMapCount; level++) + for (size_t level = 0; level < mipMapCount; level++) { new (&cubeMipMaps[level]) CubeMipLevelData( faceMips[0][level].m_size, @@ -312,9 +314,9 @@ namespace ramses return cubeMipMaps; } - void RamsesUtils::DeleteGeneratedMipMaps(MipLevelData*& data, uint32_t mipMapCount) + void RamsesUtils::DeleteGeneratedMipMaps(MipLevelData*& data, size_t mipMapCount) { - for (uint32_t i = 0; i < mipMapCount; i++) + for (size_t i = 0; i < mipMapCount; i++) { delete[] data[i].m_data; } @@ -323,7 +325,7 @@ namespace ramses data = nullptr; } - void RamsesUtils::DeleteGeneratedMipMaps(CubeMipLevelData*& data, uint32_t mipMapCount) + void RamsesUtils::DeleteGeneratedMipMaps(CubeMipLevelData*& data, size_t mipMapCount) { for (size_t level = 0u; level < mipMapCount; level++) { @@ -340,10 +342,10 @@ namespace ramses nodeId_t RamsesUtils::GetNodeId(const Node& node) { - return nodeId_t(node.impl.getNodeHandle().asMemoryHandle()); + return nodeId_t(node.m_impl.getNodeHandle().asMemoryHandle()); } - bool RamsesUtils::SetPerspectiveCameraFrustumToDataObjects(float fov, float aspectRatio, float nearPlane, float farPlane, DataVector4f& frustumPlanesData, DataVector2f& nearFarPlanesData) + bool RamsesUtils::SetPerspectiveCameraFrustumToDataObjects(float fov, float aspectRatio, float nearPlane, float farPlane, DataObject& frustumPlanesData, DataObject& nearFarPlanesData) { const auto params = ramses_internal::ProjectionParams::Perspective(fov, aspectRatio, nearPlane, farPlane); if (!params.isValid()) @@ -352,20 +354,31 @@ namespace ramses return false; } - if (frustumPlanesData.setValue(params.leftPlane, params.rightPlane, params.bottomPlane, params.topPlane) != StatusOK || - nearFarPlanesData.setValue(params.nearPlane, params.farPlane) != StatusOK) + if (frustumPlanesData.setValue(ramses::vec4f{ params.leftPlane, params.rightPlane, params.bottomPlane, params.topPlane }) != StatusOK || + nearFarPlanesData.setValue(ramses::vec2f{ params.nearPlane, params.farPlane }) != StatusOK) return false; return true; } + + void RamsesUtils::DumpUnrequiredSceneObjects(const Scene& scene) + { + LOG_INFO_F(ramses_internal::CONTEXT_CLIENT, [&](ramses_internal::StringOutputStream& output) { + SceneDumper sceneDumper{ scene.m_impl }; + sceneDumper.dumpUnrequiredObjects(output); + }); + } + + void RamsesUtils::DumpUnrequiredSceneObjectsToFile(const Scene& scene, std::ostream& out) + { + ramses_internal::StringOutputStream output; + SceneDumper sceneDumper{ scene.m_impl }; + sceneDumper.dumpUnrequiredObjects(output); + out << output.release(); + } } // include all RamsesObject to instantiate conversion templates -#include "ramses-client-api/AnimatedProperty.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/AnimationSystem.h" -#include "ramses-client-api/AnimationSystemRealTime.h" #include "ramses-client-api/Appearance.h" #include "ramses-client-api/Camera.h" #include "ramses-client-api/GeometryBinding.h" @@ -375,49 +388,13 @@ namespace ramses #include "ramses-client-api/PerspectiveCamera.h" #include "ramses-client-api/Scene.h" #include "ramses-client-api/BlitPass.h" -#include "ramses-client-api/DataFloat.h" -#include "ramses-client-api/DataInt32.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-client-api/DataVector3i.h" -#include "ramses-client-api/DataVector4f.h" -#include "ramses-client-api/DataVector4i.h" -#include "ramses-client-api/DataMatrix22f.h" -#include "ramses-client-api/DataMatrix33f.h" -#include "ramses-client-api/DataMatrix44f.h" +#include "ramses-client-api/DataObject.h" #include "ramses-client-api/RenderBuffer.h" #include "ramses-client-api/RenderGroup.h" #include "ramses-client-api/RenderPass.h" #include "ramses-client-api/RenderTarget.h" #include "ramses-client-api/PickableObject.h" #include "ramses-client-api/SceneReference.h" -#include "ramses-client-api/SplineBezierFloat.h" -#include "ramses-client-api/SplineBezierInt32.h" -#include "ramses-client-api/SplineBezierVector2f.h" -#include "ramses-client-api/SplineBezierVector2i.h" -#include "ramses-client-api/SplineBezierVector3f.h" -#include "ramses-client-api/SplineBezierVector3i.h" -#include "ramses-client-api/SplineBezierVector4f.h" -#include "ramses-client-api/SplineBezierVector4i.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/SplineLinearInt32.h" -#include "ramses-client-api/SplineLinearVector2f.h" -#include "ramses-client-api/SplineLinearVector2i.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/SplineLinearVector3i.h" -#include "ramses-client-api/SplineLinearVector4f.h" -#include "ramses-client-api/SplineLinearVector4i.h" -#include "ramses-client-api/SplineStepBool.h" -#include "ramses-client-api/SplineStepFloat.h" -#include "ramses-client-api/SplineStepInt32.h" -#include "ramses-client-api/SplineStepVector2f.h" -#include "ramses-client-api/SplineStepVector2i.h" -#include "ramses-client-api/SplineStepVector3f.h" -#include "ramses-client-api/SplineStepVector3i.h" -#include "ramses-client-api/SplineStepVector4f.h" -#include "ramses-client-api/SplineStepVector4i.h" -#include "ramses-client-api/StreamTexture.h" #include "ramses-client-api/Texture2D.h" #include "ramses-client-api/Texture3D.h" #include "ramses-client-api/TextureCube.h" @@ -435,49 +412,17 @@ namespace ramses INSTANTIATE_CONVERT_TEMPLATE(ClientObject) INSTANTIATE_CONVERT_TEMPLATE(RamsesObject) INSTANTIATE_CONVERT_TEMPLATE(SceneObject) -INSTANTIATE_CONVERT_TEMPLATE(AnimationObject) INSTANTIATE_CONVERT_TEMPLATE(RamsesClient) INSTANTIATE_CONVERT_TEMPLATE(Scene) -INSTANTIATE_CONVERT_TEMPLATE(AnimationSystem) -INSTANTIATE_CONVERT_TEMPLATE(AnimationSystemRealTime) INSTANTIATE_CONVERT_TEMPLATE(Node) INSTANTIATE_CONVERT_TEMPLATE(MeshNode) INSTANTIATE_CONVERT_TEMPLATE(Camera) INSTANTIATE_CONVERT_TEMPLATE(PerspectiveCamera) INSTANTIATE_CONVERT_TEMPLATE(OrthographicCamera) INSTANTIATE_CONVERT_TEMPLATE(Effect) -INSTANTIATE_CONVERT_TEMPLATE(AnimatedProperty) -INSTANTIATE_CONVERT_TEMPLATE(Animation) -INSTANTIATE_CONVERT_TEMPLATE(AnimationSequence) INSTANTIATE_CONVERT_TEMPLATE(Appearance) INSTANTIATE_CONVERT_TEMPLATE(GeometryBinding) INSTANTIATE_CONVERT_TEMPLATE(PickableObject) -INSTANTIATE_CONVERT_TEMPLATE(Spline) -INSTANTIATE_CONVERT_TEMPLATE(SplineStepBool) -INSTANTIATE_CONVERT_TEMPLATE(SplineStepFloat) -INSTANTIATE_CONVERT_TEMPLATE(SplineStepInt32) -INSTANTIATE_CONVERT_TEMPLATE(SplineStepVector2f) -INSTANTIATE_CONVERT_TEMPLATE(SplineStepVector3f) -INSTANTIATE_CONVERT_TEMPLATE(SplineStepVector4f) -INSTANTIATE_CONVERT_TEMPLATE(SplineStepVector2i) -INSTANTIATE_CONVERT_TEMPLATE(SplineStepVector3i) -INSTANTIATE_CONVERT_TEMPLATE(SplineStepVector4i) -INSTANTIATE_CONVERT_TEMPLATE(SplineLinearFloat) -INSTANTIATE_CONVERT_TEMPLATE(SplineLinearInt32) -INSTANTIATE_CONVERT_TEMPLATE(SplineLinearVector2f) -INSTANTIATE_CONVERT_TEMPLATE(SplineLinearVector3f) -INSTANTIATE_CONVERT_TEMPLATE(SplineLinearVector4f) -INSTANTIATE_CONVERT_TEMPLATE(SplineLinearVector2i) -INSTANTIATE_CONVERT_TEMPLATE(SplineLinearVector3i) -INSTANTIATE_CONVERT_TEMPLATE(SplineLinearVector4i) -INSTANTIATE_CONVERT_TEMPLATE(SplineBezierFloat) -INSTANTIATE_CONVERT_TEMPLATE(SplineBezierInt32) -INSTANTIATE_CONVERT_TEMPLATE(SplineBezierVector2f) -INSTANTIATE_CONVERT_TEMPLATE(SplineBezierVector3f) -INSTANTIATE_CONVERT_TEMPLATE(SplineBezierVector4f) -INSTANTIATE_CONVERT_TEMPLATE(SplineBezierVector2i) -INSTANTIATE_CONVERT_TEMPLATE(SplineBezierVector3i) -INSTANTIATE_CONVERT_TEMPLATE(SplineBezierVector4i) INSTANTIATE_CONVERT_TEMPLATE(Resource) INSTANTIATE_CONVERT_TEMPLATE(Texture2D) INSTANTIATE_CONVERT_TEMPLATE(Texture3D) @@ -492,18 +437,6 @@ INSTANTIATE_CONVERT_TEMPLATE(TextureSamplerExternal) INSTANTIATE_CONVERT_TEMPLATE(RenderBuffer) INSTANTIATE_CONVERT_TEMPLATE(RenderTarget) INSTANTIATE_CONVERT_TEMPLATE(DataObject) -INSTANTIATE_CONVERT_TEMPLATE(DataFloat) -INSTANTIATE_CONVERT_TEMPLATE(DataVector2f) -INSTANTIATE_CONVERT_TEMPLATE(DataVector3f) -INSTANTIATE_CONVERT_TEMPLATE(DataVector4f) -INSTANTIATE_CONVERT_TEMPLATE(DataMatrix22f) -INSTANTIATE_CONVERT_TEMPLATE(DataMatrix33f) -INSTANTIATE_CONVERT_TEMPLATE(DataMatrix44f) -INSTANTIATE_CONVERT_TEMPLATE(DataInt32) -INSTANTIATE_CONVERT_TEMPLATE(DataVector2i) -INSTANTIATE_CONVERT_TEMPLATE(DataVector3i) -INSTANTIATE_CONVERT_TEMPLATE(DataVector4i) INSTANTIATE_CONVERT_TEMPLATE(ArrayBuffer) INSTANTIATE_CONVERT_TEMPLATE(Texture2DBuffer) -INSTANTIATE_CONVERT_TEMPLATE(StreamTexture) INSTANTIATE_CONVERT_TEMPLATE(SceneReference) diff --git a/client/ramses-client/ramses-client-api/Animation.cpp b/client/ramses-client/ramses-client-api/Animation.cpp deleted file mode 100644 index 3e39b544e..000000000 --- a/client/ramses-client/ramses-client-api/Animation.cpp +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/Animation.h" - -// internal -#include "AnimationImpl.h" - -namespace ramses -{ - Animation::Animation(AnimationImpl& pimpl) - : AnimationObject(pimpl) - , impl(pimpl) - { - } - - Animation::~Animation() - { - } - - globalTimeStamp_t Animation::getStartTime() const - { - return impl.getStartTime(); - } - - globalTimeStamp_t Animation::getStopTime() const - { - return impl.getStopTime(); - } -} diff --git a/client/ramses-client/ramses-client-api/AnimationSequence.cpp b/client/ramses-client/ramses-client-api/AnimationSequence.cpp deleted file mode 100644 index 749280e30..000000000 --- a/client/ramses-client/ramses-client-api/AnimationSequence.cpp +++ /dev/null @@ -1,160 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/Animation.h" - -// internal -#include "AnimationSequenceImpl.h" -#include "AnimationImpl.h" -#include "Animation/AnimationTime.h" -#include "Animation/Animation.h" -#include "PlatformAbstraction/PlatformTime.h" - -namespace ramses -{ - AnimationSequence::AnimationSequence(AnimationSequenceImpl& pimpl) - : AnimationObject(pimpl) - , impl(pimpl) - { - } - - AnimationSequence::~AnimationSequence() - { - } - - status_t AnimationSequence::addAnimation(const Animation& animation, sequenceTimeStamp_t startTimeInSequence, sequenceTimeStamp_t stopTimeInSequence) - { - const status_t status = impl.addAnimation(animation, startTimeInSequence, stopTimeInSequence); - LOG_HL_CLIENT_API3(status, LOG_API_RAMSESOBJECT_STRING(animation), startTimeInSequence, stopTimeInSequence); - return status; - } - - status_t AnimationSequence::removeAnimation(const Animation& animation) - { - const status_t status = impl.removeAnimation(animation); - LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(animation)); - return status; - } - - status_t AnimationSequence::start(timeMilliseconds_t offset) - { - const status_t status = impl.start(offset); - LOG_HL_CLIENT_API1(status, offset); - return status; - } - - status_t AnimationSequence::startReverse(timeMilliseconds_t offset) - { - const status_t status = impl.startReverse(offset); - LOG_HL_CLIENT_API1(status, offset); - return status; - } - - status_t AnimationSequence::startAt(globalTimeStamp_t timeStamp) - { - const status_t status = impl.startAt(timeStamp); - LOG_HL_CLIENT_API1(status, timeStamp); - return status; - } - - status_t AnimationSequence::startReverseAt(globalTimeStamp_t timeStamp) - { - const status_t status = impl.startReverseAt(timeStamp); - LOG_HL_CLIENT_API1(status, timeStamp); - return status; - } - - status_t AnimationSequence::stop(timeMilliseconds_t delay) - { - const status_t status = impl.stop(delay); - LOG_HL_CLIENT_API1(status, delay); - return status; - } - - status_t AnimationSequence::stopAt(globalTimeStamp_t timeStamp) - { - const status_t status = impl.stopAt(timeStamp); - LOG_HL_CLIENT_API1(status, timeStamp); - return status; - } - - status_t AnimationSequence::setPlaybackSpeed(float playbackSpeed) - { - const status_t status = impl.setPlaybackSpeed(playbackSpeed); - LOG_HL_CLIENT_API1(status, playbackSpeed); - return status; - } - - float AnimationSequence::getPlaybackSpeed() const - { - return impl.getPlaybackSpeed(); - } - - status_t AnimationSequence::setAnimationLooping(const Animation& animation, timeMilliseconds_t loopDuration) - { - const status_t status = impl.setAnimationLooping(animation, loopDuration); - LOG_HL_CLIENT_API2(status, LOG_API_RAMSESOBJECT_STRING(animation), loopDuration); - return status; - } - - bool AnimationSequence::isAnimationLooping(const Animation& animation) const - { - return impl.isAnimationLooping(animation); - } - - timeMilliseconds_t AnimationSequence::getAnimationLoopDuration(const Animation& animation) const - { - return impl.getAnimationLoopDuration(animation); - } - - status_t AnimationSequence::setAnimationRelative(const Animation& animation) - { - const status_t status = impl.setAnimationRelative(animation); - LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(animation)); - return status; - } - - status_t AnimationSequence::setAnimationAbsolute(const Animation& animation) - { - const status_t status = impl.setAnimationAbsolute(animation); - LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(animation)); - return status; - } - - bool AnimationSequence::isAnimationRelative(const Animation& animation) const - { - return impl.isAnimationRelative(animation); - } - - uint32_t AnimationSequence::getNumberOfAnimations() const - { - return impl.getNumAnimations(); - } - - bool AnimationSequence::containsAnimation(const Animation& animation) const - { - return impl.containsAnimation(animation); - } - - sequenceTimeStamp_t AnimationSequence::getAnimationStartTimeInSequence(const Animation& animation) const - { - return impl.getAnimationStartTimeInSequence(animation); - } - - sequenceTimeStamp_t AnimationSequence::getAnimationStopTimeInSequence(const Animation& animation) const - { - return impl.getAnimationStopTimeInSequence(animation); - } - - sequenceTimeStamp_t AnimationSequence::getAnimationSequenceStopTime() const - { - return impl.getAnimationSequenceStopTime(); - } -} diff --git a/client/ramses-client/ramses-client-api/AnimationSystem.cpp b/client/ramses-client/ramses-client-api/AnimationSystem.cpp deleted file mode 100644 index c4d6b78ee..000000000 --- a/client/ramses-client/ramses-client-api/AnimationSystem.cpp +++ /dev/null @@ -1,311 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/AnimationSystem.h" -#include "ramses-client-api/Node.h" -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/DataObject.h" -#include "ramses-client-api/SplineStepBool.h" -#include "ramses-client-api/SplineStepInt32.h" -#include "ramses-client-api/SplineStepFloat.h" -#include "ramses-client-api/SplineStepVector2f.h" -#include "ramses-client-api/SplineStepVector3f.h" -#include "ramses-client-api/SplineStepVector4f.h" -#include "ramses-client-api/SplineStepVector2i.h" -#include "ramses-client-api/SplineStepVector3i.h" -#include "ramses-client-api/SplineStepVector4i.h" -#include "ramses-client-api/SplineLinearInt32.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/SplineLinearVector2f.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/SplineLinearVector4f.h" -#include "ramses-client-api/SplineLinearVector2i.h" -#include "ramses-client-api/SplineLinearVector3i.h" -#include "ramses-client-api/SplineLinearVector4i.h" -#include "ramses-client-api/SplineBezierInt32.h" -#include "ramses-client-api/SplineBezierFloat.h" -#include "ramses-client-api/SplineBezierVector2f.h" -#include "ramses-client-api/SplineBezierVector3f.h" -#include "ramses-client-api/SplineBezierVector4f.h" -#include "ramses-client-api/SplineBezierVector2i.h" -#include "ramses-client-api/SplineBezierVector3i.h" -#include "ramses-client-api/SplineBezierVector4i.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/PickableObject.h" - -// internal -#include "AnimationSystemImpl.h" - -namespace ramses -{ - AnimationSystem::AnimationSystem(AnimationSystemImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - AnimationSystem::~AnimationSystem() - { - } - - status_t AnimationSystem::setTime(globalTimeStamp_t timeStamp) - { - const status_t status = impl.setTime(timeStamp); - LOG_HL_CLIENT_API1(status, timeStamp); - return status; - } - - globalTimeStamp_t AnimationSystem::getTime() const - { - return impl.getTime(); - } - - SplineStepBool* AnimationSystem::createSplineStepBool(const char* name) - { - SplineStepBool* spline = impl.createSplineStepBool(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineStepInt32* AnimationSystem::createSplineStepInt32(const char* name) - { - SplineStepInt32* spline = impl.createSplineStepInt32(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineStepFloat* AnimationSystem::createSplineStepFloat(const char* name) - { - SplineStepFloat* spline = impl.createSplineStepFloat(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineStepVector2f* AnimationSystem::createSplineStepVector2f(const char* name) - { - SplineStepVector2f* spline = impl.createSplineStepVector2f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineStepVector3f* AnimationSystem::createSplineStepVector3f(const char* name) - { - SplineStepVector3f* spline = impl.createSplineStepVector3f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineStepVector4f* AnimationSystem::createSplineStepVector4f(const char* name) - { - SplineStepVector4f* spline = impl.createSplineStepVector4f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineStepVector2i* AnimationSystem::createSplineStepVector2i(const char* name) - { - SplineStepVector2i* spline = impl.createSplineStepVector2i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineStepVector3i* AnimationSystem::createSplineStepVector3i(const char* name) - { - SplineStepVector3i* spline = impl.createSplineStepVector3i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineStepVector4i* AnimationSystem::createSplineStepVector4i(const char* name) - { - SplineStepVector4i* spline = impl.createSplineStepVector4i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineLinearInt32* AnimationSystem::createSplineLinearInt32(const char* name) - { - SplineLinearInt32* spline = impl.createSplineLinearInt32(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineLinearFloat* AnimationSystem::createSplineLinearFloat(const char* name) - { - SplineLinearFloat* spline = impl.createSplineLinearFloat(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineLinearVector2f* AnimationSystem::createSplineLinearVector2f(const char* name) - { - SplineLinearVector2f* spline = impl.createSplineLinearVector2f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineLinearVector3f* AnimationSystem::createSplineLinearVector3f(const char* name) - { - SplineLinearVector3f* spline = impl.createSplineLinearVector3f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineLinearVector4f* AnimationSystem::createSplineLinearVector4f(const char* name) - { - SplineLinearVector4f* spline = impl.createSplineLinearVector4f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineLinearVector2i* AnimationSystem::createSplineLinearVector2i(const char* name) - { - SplineLinearVector2i* spline = impl.createSplineLinearVector2i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineLinearVector3i* AnimationSystem::createSplineLinearVector3i(const char* name) - { - SplineLinearVector3i* spline = impl.createSplineLinearVector3i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineLinearVector4i* AnimationSystem::createSplineLinearVector4i(const char* name) - { - SplineLinearVector4i* spline = impl.createSplineLinearVector4i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineBezierInt32* AnimationSystem::createSplineBezierInt32(const char* name) - { - SplineBezierInt32* spline = impl.createSplineBezierInt32(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineBezierFloat* AnimationSystem::createSplineBezierFloat(const char* name) - { - SplineBezierFloat* spline = impl.createSplineBezierFloat(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineBezierVector2f* AnimationSystem::createSplineBezierVector2f(const char* name) - { - SplineBezierVector2f* spline = impl.createSplineBezierVector2f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineBezierVector3f* AnimationSystem::createSplineBezierVector3f(const char* name) - { - SplineBezierVector3f* spline = impl.createSplineBezierVector3f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineBezierVector4f* AnimationSystem::createSplineBezierVector4f(const char* name) - { - SplineBezierVector4f* spline = impl.createSplineBezierVector4f(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineBezierVector2i* AnimationSystem::createSplineBezierVector2i(const char* name) - { - SplineBezierVector2i* spline = impl.createSplineBezierVector2i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineBezierVector3i* AnimationSystem::createSplineBezierVector3i(const char* name) - { - SplineBezierVector3i* spline = impl.createSplineBezierVector3i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - SplineBezierVector4i* AnimationSystem::createSplineBezierVector4i(const char* name) - { - SplineBezierVector4i* spline = impl.createSplineBezierVector4i(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(spline), name); - return spline; - } - - Animation* AnimationSystem::createAnimation(const AnimatedProperty& animatedProperty, const Spline& spline, const char* name) - { - Animation* animation = impl.createAnimation(animatedProperty, spline, name); - LOG_HL_CLIENT_API3(LOG_API_RAMSESOBJECT_PTR_STRING(animation), LOG_API_RAMSESOBJECT_STRING(animatedProperty), LOG_API_RAMSESOBJECT_STRING(spline), name); - return animation; - } - - AnimationSequence* AnimationSystem::createAnimationSequence(const char* name) - { - AnimationSequence* animationSequence = impl.createAnimationSequence(name); - LOG_HL_CLIENT_API1(LOG_API_RAMSESOBJECT_PTR_STRING(animationSequence), name); - return animationSequence; - } - - status_t AnimationSystem::destroy(AnimationObject& animationObject) - { - const status_t status = impl.destroy(animationObject); - LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(animationObject)); - return status; - } - - AnimatedProperty* AnimationSystem::createAnimatedProperty(const Node& propertyOwner, EAnimatedProperty property, EAnimatedPropertyComponent propertyComponent, const char* name) - { - AnimatedProperty* animatedProperty = impl.createAnimatedProperty(propertyOwner.impl, property, propertyComponent, name); - LOG_HL_CLIENT_API4(LOG_API_RAMSESOBJECT_PTR_STRING(animatedProperty), LOG_API_RAMSESOBJECT_STRING(propertyOwner), property, propertyComponent, name); - return animatedProperty; - } - - AnimatedProperty* AnimationSystem::createAnimatedProperty(const UniformInput& propertyOwner, const Appearance& appearance, EAnimatedPropertyComponent propertyComponent, const char* name) - { - AnimatedProperty* animatedProperty = impl.createAnimatedProperty(propertyOwner, appearance, propertyComponent, name); - LOG_HL_CLIENT_API4(LOG_API_RAMSESOBJECT_PTR_STRING(animatedProperty), LOG_API_GENERIC_OBJECT_STRING(propertyOwner), LOG_API_RAMSESOBJECT_STRING(appearance), propertyComponent, name); - return animatedProperty; - } - - AnimatedProperty* AnimationSystem::createAnimatedProperty(const DataObject& propertyOwner, EAnimatedPropertyComponent propertyComponent, const char* name) - { - AnimatedProperty* animatedProperty = impl.createAnimatedProperty(propertyOwner, propertyComponent, name); - LOG_HL_CLIENT_API3(LOG_API_RAMSESOBJECT_PTR_STRING(animatedProperty), LOG_API_RAMSESOBJECT_STRING(propertyOwner), propertyComponent, name); - return animatedProperty; - } - - uint32_t AnimationSystem::getNumberOfFinishedAnimationsSincePreviousUpdate() const - { - return impl.getNumberOfFinishedAnimationsSincePreviousUpdate(); - } - - const Animation* AnimationSystem::getFinishedAnimationSincePreviousUpdate(uint32_t index) const - { - return impl.getFinishedAnimationSincePreviousUpdate(index); - } - - Animation* AnimationSystem::getFinishedAnimationSincePreviousUpdate(uint32_t index) - { - return impl.getFinishedAnimationSincePreviousUpdate(index); - } - - const RamsesObject* AnimationSystem::findObjectByName(const char* name) const - { - return impl.findObjectByName(name); - } - - RamsesObject* AnimationSystem::findObjectByName(const char* name) - { - return impl.findObjectByName(name); - } -} diff --git a/client/ramses-client/ramses-client-api/AnimationSystemRealTime.cpp b/client/ramses-client/ramses-client-api/AnimationSystemRealTime.cpp deleted file mode 100644 index fea55376d..000000000 --- a/client/ramses-client/ramses-client-api/AnimationSystemRealTime.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/AnimationSystemRealTime.h" - -// internal -#include "AnimationSystemImpl.h" - -namespace ramses -{ - AnimationSystemRealTime::AnimationSystemRealTime(AnimationSystemImpl& pimpl) - : AnimationSystem(pimpl) - { - } - - AnimationSystemRealTime::~AnimationSystemRealTime() - { - } - - status_t AnimationSystemRealTime::updateLocalTime(globalTimeStamp_t systemTime) - { - const status_t status = impl.updateLocalTime(systemTime); - LOG_HL_CLIENT_API1(status, systemTime); - return status; - } -} diff --git a/client/ramses-client/ramses-client-api/Appearance.cpp b/client/ramses-client/ramses-client-api/Appearance.cpp deleted file mode 100644 index b972d535b..000000000 --- a/client/ramses-client/ramses-client-api/Appearance.cpp +++ /dev/null @@ -1,495 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/Appearance.h" -#include "ramses-client-api/TextureSampler.h" -#include "ramses-client-api/TextureSamplerMS.h" -#include "ramses-client-api/TextureSamplerExternal.h" -#include "ramses-client-api/UniformInput.h" -#include "ramses-client-api/DataObject.h" - -// internal -#include "AppearanceImpl.h" -#include "Math3d/Vector2i.h" -#include "Math3d/Vector3i.h" -#include "Math3d/Vector4i.h" -#include "Math3d/Vector2.h" -#include "Math3d/Vector3.h" -#include "Math3d/Vector4.h" - -namespace ramses -{ - Appearance::Appearance(AppearanceImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - Appearance::~Appearance() - { - } - - status_t Appearance::setBlendingFactors(EBlendFactor srcColor, EBlendFactor destColor, EBlendFactor srcAlpha, EBlendFactor destAlpha) - { - const status_t status = impl.setBlendingFactors(srcColor, destColor, srcAlpha, destAlpha); - LOG_HL_CLIENT_API4(status, srcColor, destColor, srcAlpha, destAlpha); - return status; - } - - status_t Appearance::getBlendingFactors(EBlendFactor& srcColor, EBlendFactor& destColor, EBlendFactor& srcAlpha, EBlendFactor& destAlpha) const - { - return impl.getBlendingFactors(srcColor, destColor, srcAlpha, destAlpha); - } - - status_t Appearance::setBlendingOperations(EBlendOperation operationColor, EBlendOperation operationAlpha) - { - const status_t status = impl.setBlendingOperations(operationColor, operationAlpha); - LOG_HL_CLIENT_API2(status, operationColor, operationAlpha); - return status; - } - - status_t Appearance::getBlendingOperations(EBlendOperation& operationColor, EBlendOperation& operationAlpha) const - { - return impl.getBlendingOperations(operationColor, operationAlpha); - } - - ramses::status_t Appearance::setBlendingColor(float red, float green, float blue, float alpha) - { - const status_t status = impl.setBlendingColor(red, green, blue, alpha); - LOG_HL_CLIENT_API4(status, red, green, blue, alpha); - return status; - } - - ramses::status_t Appearance::getBlendingColor(float& red, float& green, float& blue, float& alpha) const - { - return impl.getBlendingColor(red, green, blue, alpha); - } - - status_t Appearance::setDepthWrite(EDepthWrite mode) - { - const status_t status = impl.setDepthWrite(mode); - LOG_HL_CLIENT_API1(status, mode); - return status; - } - - status_t Appearance::getDepthWriteMode(EDepthWrite& mode) const - { - return impl.getDepthWriteMode(mode); - } - - status_t Appearance::setDepthFunction(EDepthFunc func) - { - const status_t status = impl.setDepthFunction(func); - LOG_HL_CLIENT_API1(status, func); - return status; - } - - status_t Appearance::getDepthFunction(EDepthFunc& func) const - { - return impl.getDepthFunction(func); - } - - status_t Appearance::setScissorTest(EScissorTest state, int16_t x, int16_t y, uint16_t width, uint16_t height) - { - const status_t status = impl.setScissorTest(state, x, y, width, height); - LOG_HL_CLIENT_API5(status, state, x, y, width, height); - return status; - } - - status_t Appearance::getScissorTestState(EScissorTest& state) const - { - return impl.getScissorTestState(state); - } - - status_t Appearance::getScissorRegion(int16_t& x, int16_t& y, uint16_t& width, uint16_t& height) const - { - return impl.getScissorRegion(x, y, width, height); - } - - status_t Appearance::setStencilFunction(EStencilFunc func, uint8_t ref, uint8_t mask) - { - const status_t status = impl.setStencilFunc(func, ref, mask); - LOG_HL_CLIENT_API3(status, func, ref, mask); - return status; - } - - status_t Appearance::getStencilFunction(EStencilFunc& func, uint8_t& ref, uint8_t& mask) const - { - return impl.getStencilFunc(func, ref, mask); - } - - status_t Appearance::setStencilOperation(EStencilOperation sfail, EStencilOperation dpfail, EStencilOperation dppass) - { - const status_t status = impl.setStencilOperation(sfail, dpfail, dppass); - LOG_HL_CLIENT_API3(status, sfail, dpfail, dppass); - return status; - } - - status_t Appearance::getStencilOperation(EStencilOperation& sfail, EStencilOperation& dpfail, EStencilOperation& dppass) const - { - return impl.getStencilOperation(sfail, dpfail, dppass); - } - - status_t Appearance::setCullingMode(ECullMode mode) - { - const status_t status = impl.setCullingMode(mode); - LOG_HL_CLIENT_API1(status, mode); - return status; - } - - status_t Appearance::getCullingMode(ECullMode& mode) const - { - return impl.getCullingMode(mode); - } - - status_t Appearance::setDrawMode(EDrawMode mode) - { - const status_t status = impl.setDrawMode(mode); - LOG_HL_CLIENT_API1(status, mode); - return status; - } - - status_t Appearance::getDrawMode(EDrawMode& mode) const - { - return impl.getDrawMode(mode); - } - - status_t Appearance::setColorWriteMask(bool writeRed, bool writeGreen, bool writeBlue, bool writeAlpha) - { - const status_t status = impl.setColorWriteMask(writeRed, writeGreen, writeBlue, writeAlpha); - LOG_HL_CLIENT_API4(status, writeRed, writeGreen, writeBlue, writeAlpha); - return status; - } - - status_t Appearance::getColorWriteMask(bool& writeRed, bool& writeGreen, bool& writeBlue, bool& writeAlpha) const - { - return impl.getColorWriteMask(writeRed, writeGreen, writeBlue, writeAlpha); - } - - status_t Appearance::setInputValueInt32(const UniformInput& input, int32_t i) - { - return impl.setInputValue(input.impl, 1u, &i); - } - - status_t Appearance::setInputValueInt32(const UniformInput& input, uint32_t elementCount, const int32_t* values) - { - return impl.setInputValue(input.impl, elementCount, values); - } - - status_t Appearance::getInputValueInt32(const UniformInput& input, int32_t& i) const - { - return impl.getInputValue(input.impl, 1u, &i); - } - - status_t Appearance::getInputValueInt32(const UniformInput& input, uint32_t elementCount, int32_t* valuesOut) const - { - return impl.getInputValue(input.impl, elementCount, valuesOut); - } - - status_t Appearance::setInputValueFloat(const UniformInput& input, float value) - { - return impl.setInputValue(input.impl, 1u, &value); - } - - status_t Appearance::setInputValueFloat(const UniformInput& input, uint32_t elementCount, const float* values) - { - return impl.setInputValue(input.impl, elementCount, values); - } - - status_t Appearance::getInputValueFloat(const UniformInput& input, float& valueOut) const - { - return impl.getInputValue(input.impl, 1u, &valueOut); - } - - status_t Appearance::getInputValueFloat(const UniformInput& input, uint32_t elementCount, float* valuesOut) const - { - return impl.getInputValue(input.impl, elementCount, valuesOut); - } - - status_t Appearance::setInputValueVector2i(const UniformInput& input, int32_t x, int32_t y) - { - ramses_internal::Vector2i vecVal(x, y); - return impl.setInputValue(input.impl, 1u, &vecVal); - } - - status_t Appearance::setInputValueVector2i(const UniformInput& input, uint32_t elementCount, const int32_t* values) - { - return impl.setInputValueWithElementTypeCast(input.impl, elementCount, values); - } - - status_t Appearance::getInputValueVector2i(const UniformInput& input, int32_t& x, int32_t& y) const - { - ramses_internal::Vector2i vecVal; - const status_t stat = impl.getInputValue(input.impl, 1u, &vecVal); - x = vecVal.x; - y = vecVal.y; - return stat; - } - - status_t Appearance::getInputValueVector2i(const UniformInput& input, uint32_t elementCount, int32_t* valuesOut) const - { - return impl.getInputValueWithElementTypeCast(input.impl, elementCount, valuesOut); - } - - status_t Appearance::setInputValueVector3i(const UniformInput& input, int32_t x, int32_t y, int32_t z) - { - ramses_internal::Vector3i vecVal(x, y, z); - return impl.setInputValue(input.impl, 1u, &vecVal); - } - - status_t Appearance::setInputValueVector3i(const UniformInput& input, uint32_t elementCount, const int32_t* values) - { - return impl.setInputValueWithElementTypeCast(input.impl, elementCount, values); - } - - status_t Appearance::getInputValueVector3i(const UniformInput& input, int32_t& x, int32_t& y, int32_t& z) const - { - ramses_internal::Vector3i vecVal; - const status_t stat = impl.getInputValue(input.impl, 1u, &vecVal); - x = vecVal.x; - y = vecVal.y; - z = vecVal.z; - return stat; - } - - status_t Appearance::getInputValueVector3i(const UniformInput& input, uint32_t elementCount, int32_t* valuesOut) const - { - return impl.getInputValueWithElementTypeCast(input.impl, elementCount, valuesOut); - } - - status_t Appearance::setInputValueVector4i(const UniformInput& input, int32_t x, int32_t y, int32_t z, int32_t w) - { - ramses_internal::Vector4i vecVal(x, y, z, w); - return impl.setInputValue(input.impl, 1u, &vecVal); - } - - status_t Appearance::setInputValueVector4i(const UniformInput& input, uint32_t elementCount, const int32_t* values) - { - return impl.setInputValueWithElementTypeCast(input.impl, elementCount, values); - } - - status_t Appearance::getInputValueVector4i(const UniformInput& input, int32_t& x, int32_t& y, int32_t& z, int32_t& w) const - { - ramses_internal::Vector4i vecVal; - const status_t stat = impl.getInputValue(input.impl, 1u, &vecVal); - x = vecVal.x; - y = vecVal.y; - z = vecVal.z; - w = vecVal.w; - return stat; - } - - status_t Appearance::getInputValueVector4i(const UniformInput& input, uint32_t elementCount, int32_t* valuesOut) const - { - return impl.getInputValueWithElementTypeCast(input.impl, elementCount, valuesOut); - } - - status_t Appearance::setInputValueVector2f(const UniformInput& input, float x, float y) - { - ramses_internal::Vector2 vecVal(x, y); - return impl.setInputValue(input.impl, 1u, &vecVal); - } - - status_t Appearance::setInputValueVector2f(const UniformInput& input, uint32_t elementCount, const float* values) - { - return impl.setInputValueWithElementTypeCast(input.impl, elementCount, values); - } - - status_t Appearance::getInputValueVector2f(const UniformInput& input, float& x, float& y) const - { - ramses_internal::Vector2 vecVal; - const status_t stat = impl.getInputValue(input.impl, 1u, &vecVal); - x = vecVal.x; - y = vecVal.y; - return stat; - } - - status_t Appearance::getInputValueVector2f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const - { - return impl.getInputValueWithElementTypeCast(input.impl, elementCount, valuesOut); - } - - status_t Appearance::setInputValueVector3f(const UniformInput& input, float x, float y, float z) - { - ramses_internal::Vector3 vecVal(x, y, z); - return impl.setInputValue(input.impl, 1u, &vecVal); - } - - status_t Appearance::setInputValueVector3f(const UniformInput& input, uint32_t elementCount, const float* values) - { - return impl.setInputValueWithElementTypeCast(input.impl, elementCount, values); - } - - status_t Appearance::getInputValueVector3f(const UniformInput& input, float& x, float& y, float& z) const - { - ramses_internal::Vector3 vecVal; - const status_t stat = impl.getInputValue(input.impl, 1u, &vecVal); - x = vecVal.x; - y = vecVal.y; - z = vecVal.z; - return stat; - } - - status_t Appearance::getInputValueVector3f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const - { - return impl.getInputValueWithElementTypeCast(input.impl, elementCount, valuesOut); - } - - status_t Appearance::setInputValueVector4f(const UniformInput& input, float x, float y, float z, float w) - { - ramses_internal::Vector4 vecVal(x, y, z, w); - return impl.setInputValue(input.impl, 1u, &vecVal); - } - - status_t Appearance::setInputValueVector4f(const UniformInput& input, uint32_t elementCount, const float* values) - { - return impl.setInputValueWithElementTypeCast(input.impl, elementCount, values); - } - - status_t Appearance::getInputValueVector4f(const UniformInput& input, float& x, float& y, float& z, float& w) const - { - ramses_internal::Vector4 vecVal; - const status_t stat = impl.getInputValue(input.impl, 1u, &vecVal); - x = vecVal.x; - y = vecVal.y; - z = vecVal.z; - w = vecVal.w; - return stat; - } - - status_t Appearance::getInputValueVector4f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const - { - return impl.getInputValueWithElementTypeCast(input.impl, elementCount, valuesOut); - } - - status_t Appearance::setInputValueMatrix22f(const UniformInput& input, const float values[4]) - { - return impl.setInputValueWithElementTypeCast(input.impl, 1u, values); - } - - status_t Appearance::setInputValueMatrix22f(const UniformInput& input, uint32_t elementCount, const float* values) - { - return impl.setInputValueWithElementTypeCast(input.impl, elementCount, values); - } - - status_t Appearance::getInputValueMatrix22f(const UniformInput& input, float valueOut[4]) const - { - return impl.getInputValueWithElementTypeCast(input.impl, 1u, valueOut); - } - - status_t Appearance::getInputValueMatrix22f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const - { - return impl.getInputValueWithElementTypeCast(input.impl, elementCount, valuesOut); - } - - status_t Appearance::setInputValueMatrix33f(const UniformInput& input, const float values[9]) - { - return impl.setInputValueWithElementTypeCast(input.impl, 1u, values); - } - - status_t Appearance::setInputValueMatrix33f(const UniformInput& input, uint32_t elementCount, const float* values) - { - return impl.setInputValueWithElementTypeCast(input.impl, elementCount, values); - } - - status_t Appearance::getInputValueMatrix33f(const UniformInput& input, float valueOut[9]) const - { - return impl.getInputValueWithElementTypeCast(input.impl, 1u, valueOut); - } - - status_t Appearance::getInputValueMatrix33f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const - { - return impl.getInputValueWithElementTypeCast(input.impl, elementCount, valuesOut); - } - - status_t Appearance::setInputValueMatrix44f(const UniformInput& input, const float values[16]) - { - return impl.setInputValueWithElementTypeCast(input.impl, 1u, values); - } - - status_t Appearance::setInputValueMatrix44f(const UniformInput& input, uint32_t elementCount, const float* values) - { - return impl.setInputValueWithElementTypeCast(input.impl, elementCount, values); - } - - status_t Appearance::getInputValueMatrix44f(const UniformInput& input, float valueOut[16]) const - { - return impl.getInputValueWithElementTypeCast(input.impl, 1u, valueOut); - } - - status_t Appearance::getInputValueMatrix44f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const - { - return impl.getInputValueWithElementTypeCast(input.impl, elementCount, valuesOut); - } - - status_t Appearance::setInputTexture(const UniformInput& input, const TextureSampler& textureSampler) - { - const status_t status = impl.setInputTexture(input.impl, textureSampler.impl); - LOG_HL_CLIENT_API2(status, LOG_API_GENERIC_OBJECT_STRING(input), LOG_API_RAMSESOBJECT_STRING(textureSampler)); - return status; - } - - status_t Appearance::getInputTexture(const UniformInput& input, const TextureSampler*& textureSampler) const - { - return impl.getInputTexture(input.impl, textureSampler); - } - - status_t Appearance::setInputTexture(const UniformInput& input, const TextureSamplerMS& textureSampler) - { - const status_t status = impl.setInputTexture(input.impl, textureSampler.impl); - LOG_HL_CLIENT_API2(status, LOG_API_GENERIC_OBJECT_STRING(input), LOG_API_RAMSESOBJECT_STRING(textureSampler)); - return status; - } - - status_t Appearance::getInputTextureMS(const UniformInput& input, const TextureSamplerMS*& textureSampler) const - { - return impl.getInputTextureMS(input.impl, textureSampler); - } - - status_t Appearance::setInputTexture(const UniformInput& input, const TextureSamplerExternal& textureSampler) - { - const status_t status = impl.setInputTexture(input.impl, textureSampler.impl); - LOG_HL_CLIENT_API2(status, LOG_API_GENERIC_OBJECT_STRING(input), LOG_API_RAMSESOBJECT_STRING(textureSampler)); - return status; - } - - status_t Appearance::getInputTextureExternal(const UniformInput& input, const TextureSamplerExternal*& textureSampler) const - { - return impl.getInputTextureExternal(input.impl, textureSampler); - } - - status_t Appearance::bindInput(const UniformInput& input, const DataObject& dataObject) - { - const status_t status = impl.bindInput(input.impl, dataObject.impl); - LOG_HL_CLIENT_API2(status, LOG_API_GENERIC_OBJECT_STRING(input), LOG_API_RAMSESOBJECT_STRING(dataObject)); - return status; - } - - status_t Appearance::unbindInput(const UniformInput& input) - { - const status_t status = impl.unbindInput(input.impl); - LOG_HL_CLIENT_API1(status, LOG_API_GENERIC_OBJECT_STRING(input)); - return status; - } - - bool Appearance::isInputBound(const UniformInput& input) const - { - return impl.isInputBound(input.impl); - } - - const DataObject* Appearance::getDataObjectBoundToInput(const UniformInput& input) const - { - return impl.getBoundDataObject(input.impl); - } - - const Effect& Appearance::getEffect() const - { - return impl.getEffect(); - } -} diff --git a/client/ramses-client/ramses-client-api/AppearanceEnums.cpp b/client/ramses-client/ramses-client-api/AppearanceEnums.cpp deleted file mode 100644 index 1dac35d65..000000000 --- a/client/ramses-client/ramses-client-api/AppearanceEnums.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/AppearanceEnums.h" -#include "Utils/LoggingUtils.h" - -namespace ramses -{ - static const char* const InputTypeNames[] = - { - "Invalid", - "Float", - "Vector2f", - "Vector3f", - "Vector4f", - "Int16", - "Int32", - "UInt16", - "UInt32", - "Vector2i", - "Vector3i", - "Vector4i", - "Matrix22f", - "Matrix23f", - "Matrix24f", - "Matrix32f", - "Matrix33f", - "Matrix34f", - "Matrix42f", - "Matrix43f", - "Matrix44f", - "TextureSampler", - "AttributeUInt16", - "AttributeFloat", - "AttributeVector2f", - "AttributeVector3f", - "AttributeVector4f", - }; - - static const char* const BlendOperationNames[] = - { - "Disabled", - "Add", - "Subtract", - "ReverseSubtract", - "Min", - "Max", - }; - - static const char* const BlendFactorNames[] = - { - "Zero", - "One", - "SrcAlpha", - "OneMinusSrcAlpha", - "DstAlpha", - "OneMinustDstAlpha", - "SrcColor", - "OneMinusSrcColor", - "DstColor", - "OneMinusDstColor", - "ConstColor", - "OneMinusConstColor", - "ConstAlpha", - "OneMinusConstAlpha", - "AlphaSaturate", - }; - - static const char* const CullModeNames[] = - { - "Disabled", - "FrontFacing", - "BackFacing", - "FrontAndBackFacing", - }; - - static const char* const DepthWriteNames[] = - { - "Disabled", - "Enabled", - }; - - static const char* const ScissorTestNames[] = - { - "Disabled", - "Enabled", - }; - - static const char* const DepthFuncNames[] = - { - "Disabled", - "Greater", - "GreaterEqual", - "Less", - "LessEqual", - "Equal", - "NotEqual", - "Always", - "Never", - }; - - static const char* const StencilFuncNames[] = - { - "Disabled", - "Never", - "Always", - "Equal", - "NotEqual", - "Less", - "LessEqual", - "Greater", - "GreaterEqual", - }; - - static const char* const StencilOperationNames[] = - { - "Keep", - "Zero", - "Replace", - "Increment", - "IncrementWrap", - "Decrement", - "DecrementWrap", - "Invert", - }; - - const char* const DrawModeNames[] = - { - "Points", - "Lines", - "LineLoop", - "Triangles", - "TriangleStrip", - "TriangleFan", - "LineStrip", - }; - - ENUM_TO_STRING(EInputType, InputTypeNames, EInputType_NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(EBlendOperation, BlendOperationNames, EBlendOperation_NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(EBlendFactor, BlendFactorNames, EBlendFactor_NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(ECullMode, CullModeNames, ECullMode_NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(EDepthWrite, DepthWriteNames, EDepthWrite_NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(EScissorTest, ScissorTestNames, EScissorTest_NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(EDepthFunc, DepthFuncNames, EDepthFunc_NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(EStencilFunc, StencilFuncNames, EStencilFunc_NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(EStencilOperation, StencilOperationNames, EStencilOperation_NUMBER_OF_ELEMENTS); - ENUM_TO_STRING(EDrawMode, DrawModeNames, EDrawMode_NUMBER_OF_ELEMENTS); - - - const char* getInputTypeString(EInputType inputType) - { - return EnumToString(inputType); - } - - const char* getBlendOperationString(EBlendOperation blendOperation) - { - return EnumToString(blendOperation); - } - - const char* getBlendFactorString(EBlendFactor blendFactor) - { - return EnumToString(blendFactor); - } - - const char* getCullModeString(ECullMode cullMode) - { - return EnumToString(cullMode); - } - - const char* getDepthWriteString(EDepthWrite depthWrite) - { - return EnumToString(depthWrite); - } - - const char* getScissorTestString(EScissorTest scissorTest) - { - return EnumToString(scissorTest); - } - - const char* getDepthFuncString(EDepthFunc depthFunc) - { - return EnumToString(depthFunc); - } - - const char* getStencilFuncString(EStencilFunc stencilFunc) - { - return EnumToString(stencilFunc); - } - - const char* getStencilOperationString(EStencilOperation stencilOp) - { - return EnumToString(stencilOp); - } - - const char* getDrawModeString(EDrawMode drawMode) - { - return EnumToString(drawMode); - } -} diff --git a/client/ramses-client/ramses-client-api/ArrayBuffer.cpp b/client/ramses-client/ramses-client-api/ArrayBuffer.cpp deleted file mode 100644 index 831911639..000000000 --- a/client/ramses-client/ramses-client-api/ArrayBuffer.cpp +++ /dev/null @@ -1,53 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2017 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/ArrayBuffer.h" - -// Internal -#include "ArrayBufferImpl.h" - -namespace ramses -{ - ArrayBuffer::ArrayBuffer(ArrayBufferImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - ArrayBuffer::~ArrayBuffer() - { - } - - status_t ArrayBuffer::updateData(uint32_t firstElement, uint32_t numElements, const void* bufferData) - { - const status_t status = impl.updateData(firstElement, numElements, static_cast(bufferData)); - LOG_HL_CLIENT_API3(status, firstElement, numElements, bufferData); - return status; - } - - uint32_t ArrayBuffer::getMaximumNumberOfElements() const - { - return impl.getMaximumNumberOfElements(); - } - - uint32_t ArrayBuffer::getUsedNumberOfElements() const - { - return impl.getUsedNumberOfElements(); - } - - EDataType ArrayBuffer::getDataType() const - { - return impl.getDataType(); - } - - status_t ArrayBuffer::getData(void* buffer, uint32_t numElements) const - { - return impl.getData(static_cast(buffer), numElements); - } -} diff --git a/client/ramses-client/ramses-client-api/AttributeInput.cpp b/client/ramses-client/ramses-client-api/AttributeInput.cpp deleted file mode 100644 index 21d9a78f3..000000000 --- a/client/ramses-client/ramses-client-api/AttributeInput.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/AttributeInput.h" -#include "EffectInputImpl.h" - -namespace ramses -{ - AttributeInput::AttributeInput() - : EffectInput(*(new EffectInputImpl())) - { - } - - EEffectInputDataType AttributeInput::getDataType() const - { - return impl.getAttributeInputDataType(); - } - - EEffectAttributeSemantic AttributeInput::getSemantics() const - { - return impl.getAttributeSemantics(); - } -} diff --git a/client/ramses-client/ramses-client-api/DataFloat.cpp b/client/ramses-client/ramses-client-api/DataFloat.cpp deleted file mode 100644 index 47aa3ec5f..000000000 --- a/client/ramses-client/ramses-client-api/DataFloat.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/DataFloat.h" -#include "DataObjectImpl.h" - -namespace ramses -{ - DataFloat::DataFloat(DataObjectImpl& pimpl) - : DataObject(pimpl) - { - } - - DataFloat::~DataFloat() - { - } - - status_t DataFloat::setValue(float value) - { - return impl.setValue(value); - } - - status_t DataFloat::getValue(float& value) const - { - return impl.getValue(value); - } -} diff --git a/client/ramses-client/ramses-client-api/DataInt32.cpp b/client/ramses-client/ramses-client-api/DataInt32.cpp deleted file mode 100644 index 00759dd12..000000000 --- a/client/ramses-client/ramses-client-api/DataInt32.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/DataInt32.h" -#include "DataObjectImpl.h" - -namespace ramses -{ - DataInt32::DataInt32(DataObjectImpl& pimpl) - : DataObject(pimpl) - { - } - - DataInt32::~DataInt32() - { - } - - status_t DataInt32::setValue(int32_t value) - { - return impl.setValue(value); - } - - status_t DataInt32::getValue(int32_t& value) const - { - return impl.getValue(value); - } -} diff --git a/client/ramses-client/ramses-client-api/DataMatrix22f.cpp b/client/ramses-client/ramses-client-api/DataMatrix22f.cpp deleted file mode 100644 index ce55ecc8e..000000000 --- a/client/ramses-client/ramses-client-api/DataMatrix22f.cpp +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/DataMatrix22f.h" -#include "DataObjectImpl.h" -#include "Math3d/Matrix22f.h" - -namespace ramses -{ - DataMatrix22f::DataMatrix22f(DataObjectImpl& pimpl) - : DataObject(pimpl) - { - } - - DataMatrix22f::~DataMatrix22f() - { - } - - status_t DataMatrix22f::setValue(const float(&matrixElements)[4]) - { - const ramses_internal::Matrix22f value(matrixElements[0], matrixElements[1], matrixElements[2], matrixElements[3]); - return impl.setValue(value); - } - - status_t DataMatrix22f::getValue(float(&matrixElements)[4]) const - { - ramses_internal::Matrix22f value; - const status_t status = impl.getValue(value); - - matrixElements[0] = value.m11; - matrixElements[1] = value.m12; - matrixElements[2] = value.m21; - matrixElements[3] = value.m22; - - return status; - } -} diff --git a/client/ramses-client/ramses-client-api/DataMatrix33f.cpp b/client/ramses-client/ramses-client-api/DataMatrix33f.cpp deleted file mode 100644 index ef8cd0e77..000000000 --- a/client/ramses-client/ramses-client-api/DataMatrix33f.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/DataMatrix33f.h" -#include "DataObjectImpl.h" - -namespace ramses -{ - DataMatrix33f::DataMatrix33f(DataObjectImpl& pimpl) - : DataObject(pimpl) - { - } - - DataMatrix33f::~DataMatrix33f() - { - } - - status_t DataMatrix33f::setValue(const float(&matrixElements)[9]) - { - const ramses_internal::Matrix33f value( - matrixElements[0], matrixElements[1], matrixElements[2], - matrixElements[3], matrixElements[4], matrixElements[5], - matrixElements[6], matrixElements[7], matrixElements[8]); - return impl.setValue(value); - } - - status_t DataMatrix33f::getValue(float(&matrixElements)[9]) const - { - ramses_internal::Matrix33f value; - const status_t status = impl.getValue(value); - - matrixElements[0] = value.m11; - matrixElements[1] = value.m12; - matrixElements[2] = value.m13; - - matrixElements[3] = value.m21; - matrixElements[4] = value.m22; - matrixElements[5] = value.m23; - - matrixElements[6] = value.m31; - matrixElements[7] = value.m32; - matrixElements[8] = value.m33; - - return status; - } -} diff --git a/client/ramses-client/ramses-client-api/DataMatrix44f.cpp b/client/ramses-client/ramses-client-api/DataMatrix44f.cpp deleted file mode 100644 index a35bccf19..000000000 --- a/client/ramses-client/ramses-client-api/DataMatrix44f.cpp +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/DataMatrix44f.h" -#include "DataObjectImpl.h" - -namespace ramses -{ - DataMatrix44f::DataMatrix44f(DataObjectImpl& pimpl) - : DataObject(pimpl) - { - } - - DataMatrix44f::~DataMatrix44f() - { - } - - status_t DataMatrix44f::setValue(const float(&matrixElements)[16]) - { - const ramses_internal::Matrix44f value(matrixElements); - return impl.setValue(value); - } - - status_t DataMatrix44f::getValue(float(&matrixElements)[16]) const - { - ramses_internal::Matrix44f value; - const status_t status = impl.getValue(value); - - matrixElements[0] = value.m11; - matrixElements[1] = value.m12; - matrixElements[2] = value.m13; - matrixElements[3] = value.m14; - - matrixElements[4] = value.m21; - matrixElements[5] = value.m22; - matrixElements[6] = value.m23; - matrixElements[7] = value.m24; - - matrixElements[ 8] = value.m31; - matrixElements[ 9] = value.m32; - matrixElements[10] = value.m33; - matrixElements[11] = value.m34; - - matrixElements[12] = value.m41; - matrixElements[13] = value.m42; - matrixElements[14] = value.m43; - matrixElements[15] = value.m44; - - return status; - } -} diff --git a/client/ramses-client/ramses-client-api/DataVector2f.cpp b/client/ramses-client/ramses-client-api/DataVector2f.cpp deleted file mode 100644 index fffc718bd..000000000 --- a/client/ramses-client/ramses-client-api/DataVector2f.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/DataVector2f.h" -#include "DataObjectImpl.h" - -namespace ramses -{ - DataVector2f::DataVector2f(DataObjectImpl& pimpl) - : DataObject(pimpl) - { - } - - DataVector2f::~DataVector2f() - { - } - - status_t DataVector2f::setValue(const float x, const float y) - { - const ramses_internal::Vector2 value(x, y); - return impl.setValue(value); - } - - status_t DataVector2f::getValue(float& x, float& y) const - { - ramses_internal::Vector2 value; - status_t success = impl.getValue(value); - x = value.x; - y = value.y; - - return success; - } -} diff --git a/client/ramses-client/ramses-client-api/DataVector2i.cpp b/client/ramses-client/ramses-client-api/DataVector2i.cpp deleted file mode 100644 index 235614f81..000000000 --- a/client/ramses-client/ramses-client-api/DataVector2i.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/DataVector2i.h" -#include "DataObjectImpl.h" - -namespace ramses -{ - DataVector2i::DataVector2i(DataObjectImpl& pimpl) - : DataObject(pimpl) - { - } - - DataVector2i::~DataVector2i() - { - } - - status_t DataVector2i::setValue(const int32_t x, const int32_t y) - { - const ramses_internal::Vector2i value(x, y); - return impl.setValue(value); - } - - status_t DataVector2i::getValue(int32_t& x, int32_t& y) const - { - ramses_internal::Vector2i value; - status_t success = impl.getValue(value); - x = value.x; - y = value.y; - - return success; - } -} diff --git a/client/ramses-client/ramses-client-api/DataVector3f.cpp b/client/ramses-client/ramses-client-api/DataVector3f.cpp deleted file mode 100644 index 7dfe5b6af..000000000 --- a/client/ramses-client/ramses-client-api/DataVector3f.cpp +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/DataVector3f.h" -#include "DataObjectImpl.h" - -namespace ramses -{ - DataVector3f::DataVector3f(DataObjectImpl& pimpl) - : DataObject(pimpl) - { - } - - DataVector3f::~DataVector3f() - { - } - - status_t DataVector3f::setValue(const float x, const float y, const float z) - { - const ramses_internal::Vector3 value(x, y, z); - return impl.setValue(value); - } - - status_t DataVector3f::getValue(float& x, float& y, float& z) const - { - ramses_internal::Vector3 value; - status_t success = impl.getValue(value); - x = value.x; - y = value.y; - z = value.z; - - return success; - } -} diff --git a/client/ramses-client/ramses-client-api/DataVector3i.cpp b/client/ramses-client/ramses-client-api/DataVector3i.cpp deleted file mode 100644 index 84afa93f2..000000000 --- a/client/ramses-client/ramses-client-api/DataVector3i.cpp +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/DataVector3i.h" -#include "DataObjectImpl.h" - -namespace ramses -{ - DataVector3i::DataVector3i(DataObjectImpl& pimpl) - : DataObject(pimpl) - { - } - - DataVector3i::~DataVector3i() - { - } - - status_t DataVector3i::setValue(const int32_t x, const int32_t y, const int32_t z) - { - const ramses_internal::Vector3i value(x, y, z); - return impl.setValue(value); - } - - status_t DataVector3i::getValue(int32_t& x, int32_t& y, int32_t& z) const - { - ramses_internal::Vector3i value; - status_t success = impl.getValue(value); - x = value.x; - y = value.y; - z = value.z; - - return success; - } -} diff --git a/client/ramses-client/ramses-client-api/DataVector4f.cpp b/client/ramses-client/ramses-client-api/DataVector4f.cpp deleted file mode 100644 index ea5a84e38..000000000 --- a/client/ramses-client/ramses-client-api/DataVector4f.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/DataVector4f.h" -#include "DataObjectImpl.h" - -namespace ramses -{ - DataVector4f::DataVector4f(DataObjectImpl& pimpl) - : DataObject(pimpl) - { - } - - DataVector4f::~DataVector4f() - { - } - - status_t DataVector4f::setValue(const float x, const float y, const float z, const float w) - { - const ramses_internal::Vector4 value(x, y, z, w); - return impl.setValue(value); - } - - status_t DataVector4f::getValue(float& x, float& y, float& z, float& w) const - { - ramses_internal::Vector4 value(x, y, z, w); - const status_t success = impl.getValue(value); - x = value.x; - y = value.y; - z = value.z; - w = value.w; - - return success; - } -} diff --git a/client/ramses-client/ramses-client-api/DataVector4i.cpp b/client/ramses-client/ramses-client-api/DataVector4i.cpp deleted file mode 100644 index 62b977640..000000000 --- a/client/ramses-client/ramses-client-api/DataVector4i.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/DataVector4i.h" -#include "DataObjectImpl.h" - -namespace ramses -{ - DataVector4i::DataVector4i(DataObjectImpl& pimpl) - : DataObject(pimpl) - { - } - - DataVector4i::~DataVector4i() - { - } - - status_t DataVector4i::setValue(const int32_t x, const int32_t y, const int32_t z, const int32_t w) - { - const ramses_internal::Vector4i value(x, y, z, w); - return impl.setValue(value); - } - - status_t DataVector4i::getValue(int32_t& x, int32_t& y, int32_t& z, int32_t& w) const - { - ramses_internal::Vector4i value; - status_t success = impl.getValue(value); - x = value.x; - y = value.y; - z = value.z; - w = value.w; - - return success; - } -} diff --git a/client/ramses-client/ramses-client-api/Effect.cpp b/client/ramses-client/ramses-client-api/Effect.cpp deleted file mode 100644 index 209182f97..000000000 --- a/client/ramses-client/ramses-client-api/Effect.cpp +++ /dev/null @@ -1,78 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/Effect.h" -#include "ramses-client-api/UniformInput.h" -#include "ramses-client-api/AttributeInput.h" - -// internal -#include "EffectImpl.h" - -namespace ramses -{ - Effect::Effect(EffectImpl& pimpl) - : Resource(pimpl) - , impl(pimpl) - { - } - - Effect::~Effect() - { - } - - uint32_t Effect::getUniformInputCount() const - { - return impl.getUniformInputCount(); - } - - uint32_t Effect::getAttributeInputCount() const - { - return impl.getAttributeInputCount(); - } - - status_t Effect::getUniformInput(uint32_t index, UniformInput& uniformInput) const - { - return impl.getUniformInput(index, uniformInput.impl); - } - - status_t Effect::findUniformInput(EEffectUniformSemantic uniformSemantic, UniformInput& uniformInput) const - { - return impl.findUniformInput(uniformSemantic, uniformInput.impl); - } - - status_t Effect::getAttributeInput(uint32_t index, AttributeInput& attributeInput) const - { - return impl.getAttributeInput(index, attributeInput.impl); - } - - status_t Effect::findAttributeInput(EEffectAttributeSemantic attributeSemantic, AttributeInput& attributeInput) const - { - return impl.findAttributeInput(attributeSemantic, attributeInput.impl); - } - - bool Effect::hasGeometryShader(const Effect& effect) - { - return effect.impl.hasGeometryShader(); - } - - status_t Effect::getGeometryShaderInputType(const Effect& effect, EDrawMode& inputType) - { - return effect.impl.getGeometryShaderInputType(inputType); - } - - status_t Effect::findUniformInput(const char* inputName, UniformInput& uniformInput) const - { - return impl.findUniformInput(inputName, uniformInput.impl); - } - - status_t Effect::findAttributeInput(const char* inputName, AttributeInput& attributeInput) const - { - return impl.findAttributeInput(inputName, attributeInput.impl); - } -} diff --git a/client/ramses-client/ramses-client-api/EffectDescription.cpp b/client/ramses-client/ramses-client-api/EffectDescription.cpp deleted file mode 100644 index b5c8ba6f8..000000000 --- a/client/ramses-client/ramses-client-api/EffectDescription.cpp +++ /dev/null @@ -1,110 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/EffectDescription.h" - -// internal -#include "EffectDescriptionImpl.h" - -namespace ramses -{ - EffectDescription::EffectDescription() - : StatusObject(*new EffectDescriptionImpl()) - , impl(static_cast(StatusObject::impl)) - { - } - - status_t EffectDescription::setVertexShader(const char* shaderSource) - { - const status_t status = impl.setVertexShader(shaderSource); - LOG_HL_CLIENT_API1(status, shaderSource); - return status; - } - - status_t EffectDescription::setFragmentShader(const char* shaderSource) - { - const status_t status = impl.setFragmentShader(shaderSource); - LOG_HL_CLIENT_API1(status, shaderSource); - return status; - } - - status_t EffectDescription::setGeometryShader(const char* shaderSource) - { - const status_t status = impl.setGeometryShader(shaderSource); - LOG_HL_CLIENT_API1(status, shaderSource); - return status; - } - - status_t EffectDescription::setVertexShaderFromFile(const char* shaderSourceFileName) - { - const status_t status = impl.setVertexShaderFromFile(shaderSourceFileName); - LOG_HL_CLIENT_API1(status, shaderSourceFileName); - return status; - } - - status_t EffectDescription::setFragmentShaderFromFile(const char* shaderSourceFileName) - { - const status_t status = impl.setFragmentShaderFromFile(shaderSourceFileName); - LOG_HL_CLIENT_API1(status, shaderSourceFileName); - return status; - } - - status_t EffectDescription::setGeometryShaderFromFile(const char* shaderSourceFileName) - { - const status_t status = impl.setGeometryShaderFromFile(shaderSourceFileName); - LOG_HL_CLIENT_API1(status, shaderSourceFileName); - return status; - } - - status_t EffectDescription::addCompilerDefine(const char* define) - { - const status_t status = impl.addCompilerDefine(define); - LOG_HL_CLIENT_API1(status, define); - return status; - } - - status_t EffectDescription::setUniformSemantic(const char* inputName, EEffectUniformSemantic semanticType) - { - const status_t status = impl.setUniformSemantic(inputName, semanticType); - LOG_HL_CLIENT_API2(status, inputName, semanticType); - return status; - } - - status_t EffectDescription::setAttributeSemantic(const char* inputName, EEffectAttributeSemantic semanticType) - { - const status_t status = impl.setAttributeSemantic(inputName, semanticType); - LOG_HL_CLIENT_API2(status, inputName, semanticType); - return status; - } - - const char* EffectDescription::getVertexShader() const - { - return impl.getVertexShader(); - } - - const char* EffectDescription::getFragmentShader() const - { - return impl.getFragmentShader(); - } - - const char* EffectDescription::getGeometryShader() const - { - return impl.getGeometryShader(); - } - - uint32_t EffectDescription::getNumberOfCompilerDefines() const - { - return impl.getNumberOfCompilerDefines(); - } - - const char* EffectDescription::getCompilerDefine(uint32_t index) const - { - return impl.getCompilerDefine(index); - } -} diff --git a/client/ramses-client/ramses-client-api/Node.cpp b/client/ramses-client/ramses-client-api/Node.cpp deleted file mode 100644 index 67103edbf..000000000 --- a/client/ramses-client/ramses-client-api/Node.cpp +++ /dev/null @@ -1,186 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/Node.h" - -// internal -#include "NodeImpl.h" -#include "VisibilityModeUtils.h" - -namespace ramses -{ - Node::Node(NodeImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - Node::~Node() - { - } - - bool Node::hasChild() const - { - return impl.hasChild(); - } - - uint32_t Node::getChildCount() const - { - return impl.getChildCount(); - } - - Node* Node::getChild(uint32_t index) - { - return impl.getChild(index); - } - - const Node* Node::getChild(uint32_t index) const - { - return impl.getChild(index); - } - - status_t Node::addChild(Node& node) - { - const status_t status = impl.addChild(node.impl); - LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(node)); - return status; - } - - status_t Node::removeChild(Node& node) - { - const status_t status = impl.removeChild(node.impl); - LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(node)); - return status; - } - - status_t Node::removeAllChildren() - { - const status_t status = impl.removeAllChildren(); - LOG_HL_CLIENT_API_NOARG(status); - return status; - } - - bool Node::hasParent() const - { - return impl.hasParent(); - } - - Node* Node::getParent() - { - return impl.getParent(); - } - - const Node* Node::getParent() const - { - return impl.getParent(); - } - - status_t Node::setParent(Node& node) - { - const status_t status = impl.setParent(node.impl); - LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(node)); - return status; - } - - status_t Node::removeParent() - { - return impl.removeParent(); - } - - status_t Node::getModelMatrix(float(&modelMatrix)[16]) const - { - return impl.getModelMatrix(modelMatrix); - } - - status_t Node::getInverseModelMatrix(float(&inverseModelMatrix)[16]) const - { - return impl.getInverseModelMatrix(inverseModelMatrix); - } - - ramses::status_t Node::rotate(float x, float y, float z) - { - const status_t status = impl.rotate(x, y, z); - LOG_HL_CLIENT_API3(status, x, y, z); - return status; - } - - ramses::status_t Node::setRotation(float x, float y, float z) - { - const status_t status = impl.setRotation(x, y, z); - LOG_HL_CLIENT_API3(status, x, y, z); - return status; - } - - status_t Node::setRotation(float x, float y, float z, ERotationConvention rotationConvention) - { - const status_t status = impl.setRotation(x, y, z, rotationConvention); - LOG_HL_CLIENT_API4(status, x, y, z, rotationConvention); - return status; - } - - ramses::status_t Node::getRotation(float& x, float& y, float& z) const - { - return impl.getRotation(x, y, z); - } - - ramses::status_t Node::getRotation(float& x, float& y, float& z, ERotationConvention& rotationConvention) const - { - return impl.getRotation(x, y, z, rotationConvention); - } - - ramses::status_t Node::translate(float x, float y, float z) - { - const status_t status = impl.translate(x, y, z); - LOG_HL_CLIENT_API3(status, x, y, z); - return status; - } - - ramses::status_t Node::setTranslation(float x, float y, float z) - { - const status_t status = impl.setTranslation(x, y, z); - LOG_HL_CLIENT_API3(status, x, y, z); - return status; - } - - ramses::status_t Node::getTranslation(float& x, float& y, float& z) const - { - return impl.getTranslation(x, y, z); - } - - ramses::status_t Node::scale(float x, float y, float z) - { - const status_t status = impl.scale(x, y, z); - LOG_HL_CLIENT_API3(status, x, y, z); - return status; - } - - ramses::status_t Node::setScaling(float x, float y, float z) - { - const status_t status = impl.setScaling(x, y, z); - LOG_HL_CLIENT_API3(status, x, y, z); - return status; - } - - ramses::status_t Node::getScaling(float& x, float& y, float& z) const - { - return impl.getScaling(x, y, z); - } - - ramses::status_t Node::setVisibility(EVisibilityMode mode) - { - const status_t status = impl.setVisibility(mode); - LOG_HL_CLIENT_API1(status, VisibilityModeUtils::ToString(mode)); - return status; - } - - EVisibilityMode Node::getVisibility() const - { - return impl.getVisibility(); - } -} diff --git a/client/ramses-client/ramses-client-api/RenderTargetDescription.cpp b/client/ramses-client/ramses-client-api/RenderTargetDescription.cpp deleted file mode 100644 index a4c84ad7b..000000000 --- a/client/ramses-client/ramses-client-api/RenderTargetDescription.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2017 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/RenderTargetDescription.h" -#include "ramses-client-api/RenderBuffer.h" - -// internal -#include "RenderTargetDescriptionImpl.h" - -namespace ramses -{ - RenderTargetDescription::RenderTargetDescription() - : StatusObject(*new RenderTargetDescriptionImpl()) - , impl(static_cast(StatusObject::impl)) - { - } - - status_t RenderTargetDescription::addRenderBuffer(const RenderBuffer& renderBuffer) - { - const status_t status = impl.addRenderBuffer(renderBuffer.impl); - LOG_HL_CLIENT_API1(status, LOG_API_RAMSESOBJECT_STRING(renderBuffer)); - return status; - } -} diff --git a/client/ramses-client/ramses-client-api/ResourceDataPool.cpp b/client/ramses-client/ramses-client-api/ResourceDataPool.cpp deleted file mode 100644 index 24ebb027e..000000000 --- a/client/ramses-client/ramses-client-api/ResourceDataPool.cpp +++ /dev/null @@ -1,104 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/ResourceDataPool.h" -#include "ramses-client-api/Scene.h" -#include "ramses-client-api/Resource.h" -#include "ResourceDataPoolImpl.h" -#include "APILoggingMacros.h" -#include "RamsesFrameworkTypesImpl.h" -#include "RamsesClientTypesImpl.h" - -namespace ramses -{ - - ResourceDataPool::ResourceDataPool(ResourceDataPoolImpl& pimpl) - : impl(pimpl) - { - } - - ResourceDataPool::~ResourceDataPool() - { - delete& impl; - } - - ramses::resourceId_t ResourceDataPool::addArrayResourceData(EDataType type, uint32_t numElements, const void* arrayData, resourceCacheFlag_t cacheFlag /*= ResourceCacheFlag_DoNotCache*/, const char* name /*= nullptr*/) - { - auto resourceId = impl.addArrayResourceData(type, numElements, arrayData, cacheFlag, name); - LOG_HL_CLIENT_API5(type, resourceId, numElements, LOG_API_GENERIC_PTR_STRING(arrayData), cacheFlag, name); - return resourceId; - } - - ramses::resourceId_t ResourceDataPool::addTexture2DData(ETextureFormat format, uint32_t width, uint32_t height, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain /*= false*/, const TextureSwizzle& swizzle /*= {}*/, resourceCacheFlag_t cacheFlag /*= ResourceCacheFlag_DoNotCache*/, const char* name /*= nullptr*/) - { - auto resourceId = impl.addTexture2DData(format, width, height, mipMapCount, mipLevelData, generateMipChain, swizzle, cacheFlag, name); - LOG_HL_CLIENT_API9(getTextureFormatString(format), resourceId, width, height, mipMapCount, LOG_API_GENERIC_PTR_STRING(mipLevelData), generateMipChain, swizzle, cacheFlag, name); - return resourceId; - } - - ramses::resourceId_t ResourceDataPool::addTexture3DData(ETextureFormat format, uint32_t width, uint32_t height, uint32_t depth, uint32_t mipMapCount, const MipLevelData mipLevelData[], bool generateMipChain /*= false*/, resourceCacheFlag_t cacheFlag /*= ResourceCacheFlag_DoNotCache*/, const char* name /*= nullptr*/) - { - auto resourceId = impl.addTexture3DData(format, width, height, depth, mipMapCount, mipLevelData, generateMipChain, cacheFlag, name); - LOG_HL_CLIENT_API9(getTextureFormatString(format), resourceId, width, height, depth, mipMapCount, LOG_API_GENERIC_PTR_STRING(mipLevelData), generateMipChain, cacheFlag, name); - return resourceId; - } - - ramses::resourceId_t ResourceDataPool::addTextureCubeData(ETextureFormat format, uint32_t size, uint32_t mipMapCount, const CubeMipLevelData mipLevelData[], bool generateMipChain /*= false*/, const TextureSwizzle& swizzle /*= {}*/, resourceCacheFlag_t cacheFlag /*= ResourceCacheFlag_DoNotCache*/, const char* name /*= nullptr*/) - { - auto resourceId = impl.addTextureCubeData(format, size, mipMapCount, mipLevelData, generateMipChain, swizzle, cacheFlag, name); - LOG_HL_CLIENT_API8(getTextureFormatString(format), resourceId, size, mipMapCount, LOG_API_GENERIC_PTR_STRING(mipLevelData), generateMipChain, swizzle, cacheFlag, name); - return resourceId; - } - - ramses::resourceId_t ResourceDataPool::addEffectData(const EffectDescription& effectDesc, resourceCacheFlag_t cacheFlag /*= ResourceCacheFlag_DoNotCache*/, const char* name /*= nullptr*/) - { - auto resourceId = impl.addEffectData(effectDesc, cacheFlag, name); - LOG_HL_CLIENT_API3(resourceId, LOG_API_GENERIC_OBJECT_STRING(effectDesc), cacheFlag, name); - return resourceId; - } - - std::string ResourceDataPool::getLastEffectErrorMessages() const - { - return impl.getLastEffectErrorMessages(); - } - - bool ResourceDataPool::removeResourceData(resourceId_t const& id) - { - bool success = impl.removeResourceData(id); - LOG_HL_CLIENT_API1(success, id); - return success; - } - - bool ResourceDataPool::addResourceDataFile(std::string const& filename) - { - bool success = impl.addResourceDataFile(filename); - LOG_HL_CLIENT_API1(success, filename); - return success; - } - - bool ResourceDataPool::forceLoadResourcesFromResourceDataFile(std::string const& filename) - { - bool success = impl.forceLoadResourcesFromResourceDataFile(filename); - LOG_HL_CLIENT_API1(success, filename); - return success; - } - - bool ResourceDataPool::removeResourceDataFile(std::string const& filename) - { - bool success = impl.removeResourceDataFile(filename); - LOG_HL_CLIENT_API1(success, filename); - return success; - } - - ramses::Resource* ResourceDataPool::createResourceForScene(Scene& scene, resourceId_t const& id) - { - auto resource = impl.createResourceForScene(scene, id); - LOG_HL_CLIENT_API2(LOG_API_RAMSESOBJECT_PTR_STRING(resource), LOG_API_GENERIC_OBJECT_STRING(scene), id); - return resource; - } -} diff --git a/client/ramses-client/ramses-client-api/SceneConfig.cpp b/client/ramses-client/ramses-client-api/SceneConfig.cpp deleted file mode 100644 index 48d5c0359..000000000 --- a/client/ramses-client/ramses-client-api/SceneConfig.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SceneConfig.h" - -// internal -#include "SceneConfigImpl.h" -#include "APILoggingMacros.h" - -namespace ramses -{ - SceneConfig::SceneConfig() - : StatusObject(*new SceneConfigImpl()) - , impl(static_cast(StatusObject::impl)) - { - } - - SceneConfig::SceneConfig(const SceneConfig& other) - : StatusObject(*new SceneConfigImpl(other.impl)) - , impl(static_cast(StatusObject::impl)) - { - } - - SceneConfig::~SceneConfig() - { - } - - status_t SceneConfig::setPublicationMode(EScenePublicationMode publicationMode) - { - const status_t status = impl.setPublicationMode(publicationMode); - LOG_HL_CLIENT_API1(status, publicationMode); - return status; - } -} diff --git a/client/ramses-client/ramses-client-api/SplineBezierFloat.cpp b/client/ramses-client/ramses-client-api/SplineBezierFloat.cpp deleted file mode 100644 index 75cac565f..000000000 --- a/client/ramses-client/ramses-client-api/SplineBezierFloat.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineBezierFloat.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineBezierFloat::SplineBezierFloat(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineBezierFloat::~SplineBezierFloat() - { - } - - status_t SplineBezierFloat::setKey(splineTimeStamp_t timeStamp, float value, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y) - { - const status_t status = impl.setSplineKeyBezierFloat(timeStamp, value, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - LOG_HL_CLIENT_API6(status, timeStamp, value, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - return status; - } - - status_t SplineBezierFloat::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& value, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const - { - return impl.getSplineKeyTangentsFloat(keyIndex, timeStamp, value, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineBezierInt32.cpp b/client/ramses-client/ramses-client-api/SplineBezierInt32.cpp deleted file mode 100644 index b6850f9b7..000000000 --- a/client/ramses-client/ramses-client-api/SplineBezierInt32.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineBezierInt32.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineBezierInt32::SplineBezierInt32(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineBezierInt32::~SplineBezierInt32() - { - } - - status_t SplineBezierInt32::setKey(splineTimeStamp_t timeStamp, int32_t value, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y) - { - const status_t status = impl.setSplineKeyBezierInt32(timeStamp, value, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - LOG_HL_CLIENT_API6(status, timeStamp, value, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - return status; - } - - status_t SplineBezierInt32::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& value, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const - { - return impl.getSplineKeyTangentsInt32(keyIndex, timeStamp, value, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineBezierVector2f.cpp b/client/ramses-client/ramses-client-api/SplineBezierVector2f.cpp deleted file mode 100644 index 57f205828..000000000 --- a/client/ramses-client/ramses-client-api/SplineBezierVector2f.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineBezierVector2f.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineBezierVector2f::SplineBezierVector2f(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineBezierVector2f::~SplineBezierVector2f() - { - } - - status_t SplineBezierVector2f::setKey(splineTimeStamp_t timeStamp, float x, float y, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y) - { - const status_t status = impl.setSplineKeyBezierVector2f(timeStamp, x, y, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - LOG_HL_CLIENT_API7(status, timeStamp, x, y, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - return status; - } - - status_t SplineBezierVector2f::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const - { - return impl.getSplineKeyTangentsVector2f(keyIndex, timeStamp, x, y, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineBezierVector2i.cpp b/client/ramses-client/ramses-client-api/SplineBezierVector2i.cpp deleted file mode 100644 index 194012b1b..000000000 --- a/client/ramses-client/ramses-client-api/SplineBezierVector2i.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineBezierVector2i.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineBezierVector2i::SplineBezierVector2i(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineBezierVector2i::~SplineBezierVector2i() - { - } - - status_t SplineBezierVector2i::setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y) - { - const status_t status = impl.setSplineKeyBezierVector2i(timeStamp, x, y, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - LOG_HL_CLIENT_API7(status, timeStamp, x, y, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - return status; - } - - status_t SplineBezierVector2i::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const - { - return impl.getSplineKeyTangentsVector2i(keyIndex, timeStamp, x, y, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineBezierVector3f.cpp b/client/ramses-client/ramses-client-api/SplineBezierVector3f.cpp deleted file mode 100644 index 3dada0cbc..000000000 --- a/client/ramses-client/ramses-client-api/SplineBezierVector3f.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineBezierVector3f.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineBezierVector3f::SplineBezierVector3f(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineBezierVector3f::~SplineBezierVector3f() - { - } - - status_t SplineBezierVector3f::setKey(splineTimeStamp_t timeStamp, float x, float y, float z, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y) - { - const status_t status = impl.setSplineKeyBezierVector3f(timeStamp, x, y, z, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - LOG_HL_CLIENT_API8(status, timeStamp, x, y, z, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - return status; - } - - status_t SplineBezierVector3f::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const - { - return impl.getSplineKeyTangentsVector3f(keyIndex, timeStamp, x, y, z, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineBezierVector3i.cpp b/client/ramses-client/ramses-client-api/SplineBezierVector3i.cpp deleted file mode 100644 index cefe4194a..000000000 --- a/client/ramses-client/ramses-client-api/SplineBezierVector3i.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineBezierVector3i.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineBezierVector3i::SplineBezierVector3i(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineBezierVector3i::~SplineBezierVector3i() - { - } - - status_t SplineBezierVector3i::setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y) - { - const status_t status = impl.setSplineKeyBezierVector3i(timeStamp, x, y, z, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - LOG_HL_CLIENT_API8(status, timeStamp, x, y, z, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - return status; - } - - status_t SplineBezierVector3i::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const - { - return impl.getSplineKeyTangentsVector3i(keyIndex, timeStamp, x, y, z, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineBezierVector4f.cpp b/client/ramses-client/ramses-client-api/SplineBezierVector4f.cpp deleted file mode 100644 index 9df7fac4b..000000000 --- a/client/ramses-client/ramses-client-api/SplineBezierVector4f.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineBezierVector4f.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineBezierVector4f::SplineBezierVector4f(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineBezierVector4f::~SplineBezierVector4f() - { - } - - status_t SplineBezierVector4f::setKey(splineTimeStamp_t timeStamp, float x, float y, float z, float w, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y) - { - const status_t status = impl.setSplineKeyBezierVector4f(timeStamp, x, y, z, w, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - LOG_HL_CLIENT_API9(status, timeStamp, x, y, z, w, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - return status; - } - - status_t SplineBezierVector4f::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& w, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const - { - return impl.getSplineKeyTangentsVector4f(keyIndex, timeStamp, x, y, z, w, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineBezierVector4i.cpp b/client/ramses-client/ramses-client-api/SplineBezierVector4i.cpp deleted file mode 100644 index ca71682e9..000000000 --- a/client/ramses-client/ramses-client-api/SplineBezierVector4i.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineBezierVector4i.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineBezierVector4i::SplineBezierVector4i(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineBezierVector4i::~SplineBezierVector4i() - { - } - - status_t SplineBezierVector4i::setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y) - { - const status_t status = impl.setSplineKeyBezierVector4i(timeStamp, x, y, z, w, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - LOG_HL_CLIENT_API9(status, timeStamp, x, y, z, w, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - return status; - } - - status_t SplineBezierVector4i::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, int32_t& w, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const - { - return impl.getSplineKeyTangentsVector4i(keyIndex, timeStamp, x, y, z, w, tangentIn_x, tangentIn_y, tangentOut_x, tangentOut_y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineLinearFloat.cpp b/client/ramses-client/ramses-client-api/SplineLinearFloat.cpp deleted file mode 100644 index 0f11986a7..000000000 --- a/client/ramses-client/ramses-client-api/SplineLinearFloat.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineLinearFloat.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineLinearFloat::SplineLinearFloat(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineLinearFloat::~SplineLinearFloat() - { - } - - status_t SplineLinearFloat::setKey(splineTimeStamp_t timeStamp, float value) - { - const status_t status = impl.setSplineKeyLinearFloat(timeStamp, value); - LOG_HL_CLIENT_API2(status, timeStamp, value); - return status; - } - - status_t SplineLinearFloat::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& value) const - { - return impl.getSplineKeyFloat(keyIndex, timeStamp, value); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineLinearInt32.cpp b/client/ramses-client/ramses-client-api/SplineLinearInt32.cpp deleted file mode 100644 index 11517090a..000000000 --- a/client/ramses-client/ramses-client-api/SplineLinearInt32.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineLinearInt32.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineLinearInt32::SplineLinearInt32(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineLinearInt32::~SplineLinearInt32() - { - } - - status_t SplineLinearInt32::setKey(splineTimeStamp_t timeStamp, int32_t value) - { - const status_t status = impl.setSplineKeyLinearInt32(timeStamp, value); - LOG_HL_CLIENT_API2(status, timeStamp, value); - return status; - } - - status_t SplineLinearInt32::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& value) const - { - return impl.getSplineKeyInt32(keyIndex, timeStamp, value); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineLinearVector2f.cpp b/client/ramses-client/ramses-client-api/SplineLinearVector2f.cpp deleted file mode 100644 index 462b88103..000000000 --- a/client/ramses-client/ramses-client-api/SplineLinearVector2f.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineLinearVector2f.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineLinearVector2f::SplineLinearVector2f(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineLinearVector2f::~SplineLinearVector2f() - { - } - - status_t SplineLinearVector2f::setKey(splineTimeStamp_t timeStamp, float x, float y) - { - const status_t status = impl.setSplineKeyLinearVector2f(timeStamp, x, y); - LOG_HL_CLIENT_API3(status, timeStamp, x, y); - return status; - } - - status_t SplineLinearVector2f::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y) const - { - return impl.getSplineKeyVector2f(keyIndex, timeStamp, x, y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineLinearVector2i.cpp b/client/ramses-client/ramses-client-api/SplineLinearVector2i.cpp deleted file mode 100644 index 117a55329..000000000 --- a/client/ramses-client/ramses-client-api/SplineLinearVector2i.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineLinearVector2i.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineLinearVector2i::SplineLinearVector2i(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineLinearVector2i::~SplineLinearVector2i() - { - } - - status_t SplineLinearVector2i::setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y) - { - const status_t status = impl.setSplineKeyLinearVector2i(timeStamp, x, y); - LOG_HL_CLIENT_API3(status, timeStamp, x, y); - return status; - } - - status_t SplineLinearVector2i::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y) const - { - return impl.getSplineKeyVector2i(keyIndex, timeStamp, x, y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineLinearVector3f.cpp b/client/ramses-client/ramses-client-api/SplineLinearVector3f.cpp deleted file mode 100644 index 1ce87d215..000000000 --- a/client/ramses-client/ramses-client-api/SplineLinearVector3f.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineLinearVector3f.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineLinearVector3f::SplineLinearVector3f(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineLinearVector3f::~SplineLinearVector3f() - { - } - - status_t SplineLinearVector3f::setKey(splineTimeStamp_t timeStamp, float x, float y, float z) - { - const status_t status = impl.setSplineKeyLinearVector3f(timeStamp, x, y, z); - LOG_HL_CLIENT_API4(status, timeStamp, x, y, z); - return status; - } - - status_t SplineLinearVector3f::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z) const - { - return impl.getSplineKeyVector3f(keyIndex, timeStamp, x, y, z); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineLinearVector3i.cpp b/client/ramses-client/ramses-client-api/SplineLinearVector3i.cpp deleted file mode 100644 index 67d9ecdb1..000000000 --- a/client/ramses-client/ramses-client-api/SplineLinearVector3i.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineLinearVector3i.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineLinearVector3i::SplineLinearVector3i(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineLinearVector3i::~SplineLinearVector3i() - { - } - - status_t SplineLinearVector3i::setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z) - { - const status_t status = impl.setSplineKeyLinearVector3i(timeStamp, x, y, z); - LOG_HL_CLIENT_API4(status, timeStamp, x, y, z); - return status; - } - - status_t SplineLinearVector3i::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z) const - { - return impl.getSplineKeyVector3i(keyIndex, timeStamp, x, y, z); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineLinearVector4f.cpp b/client/ramses-client/ramses-client-api/SplineLinearVector4f.cpp deleted file mode 100644 index 1257de35b..000000000 --- a/client/ramses-client/ramses-client-api/SplineLinearVector4f.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineLinearVector4f.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineLinearVector4f::SplineLinearVector4f(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineLinearVector4f::~SplineLinearVector4f() - { - } - - status_t SplineLinearVector4f::setKey(splineTimeStamp_t timeStamp, float x, float y, float z, float w) - { - const status_t status = impl.setSplineKeyLinearVector4f(timeStamp, x, y, z, w); - LOG_HL_CLIENT_API5(status, timeStamp, x, y, z, w); - return status; - } - - status_t SplineLinearVector4f::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& w) const - { - return impl.getSplineKeyVector4f(keyIndex, timeStamp, x, y, z, w); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineLinearVector4i.cpp b/client/ramses-client/ramses-client-api/SplineLinearVector4i.cpp deleted file mode 100644 index 7d21f29ff..000000000 --- a/client/ramses-client/ramses-client-api/SplineLinearVector4i.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineLinearVector4i.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineLinearVector4i::SplineLinearVector4i(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineLinearVector4i::~SplineLinearVector4i() - { - } - - status_t SplineLinearVector4i::setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w) - { - const status_t status = impl.setSplineKeyLinearVector4i(timeStamp, x, y, z, w); - LOG_HL_CLIENT_API5(status, timeStamp, x, y, z, w); - return status; - } - - status_t SplineLinearVector4i::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, int32_t& w) const - { - return impl.getSplineKeyVector4i(keyIndex, timeStamp, x, y, z, w); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineStepBool.cpp b/client/ramses-client/ramses-client-api/SplineStepBool.cpp deleted file mode 100644 index f8ed873c6..000000000 --- a/client/ramses-client/ramses-client-api/SplineStepBool.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineStepBool.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineStepBool::SplineStepBool(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineStepBool::~SplineStepBool() - { - } - - status_t SplineStepBool::setKey(splineTimeStamp_t timeStamp, bool value) - { - const status_t status = impl.setSplineKeyStepBool(timeStamp, value); - LOG_HL_CLIENT_API2(status, timeStamp, value); - return status; - } - - status_t SplineStepBool::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, bool& value) const - { - return impl.getSplineKeyBool(keyIndex, timeStamp, value); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineStepFloat.cpp b/client/ramses-client/ramses-client-api/SplineStepFloat.cpp deleted file mode 100644 index 17b02f1e1..000000000 --- a/client/ramses-client/ramses-client-api/SplineStepFloat.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineStepFloat.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineStepFloat::SplineStepFloat(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineStepFloat::~SplineStepFloat() - { - } - - status_t SplineStepFloat::setKey(splineTimeStamp_t timeStamp, float value) - { - const status_t status = impl.setSplineKeyStepFloat(timeStamp, value); - LOG_HL_CLIENT_API2(status, timeStamp, value); - return status; - } - - status_t SplineStepFloat::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& value) const - { - return impl.getSplineKeyFloat(keyIndex, timeStamp, value); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineStepInt32.cpp b/client/ramses-client/ramses-client-api/SplineStepInt32.cpp deleted file mode 100644 index 2f688fa50..000000000 --- a/client/ramses-client/ramses-client-api/SplineStepInt32.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineStepInt32.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineStepInt32::SplineStepInt32(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineStepInt32::~SplineStepInt32() - { - } - - status_t SplineStepInt32::setKey(splineTimeStamp_t timeStamp, int32_t value) - { - const status_t status = impl.setSplineKeyStepInt32(timeStamp, value); - LOG_HL_CLIENT_API2(status, timeStamp, value); - return status; - } - - status_t SplineStepInt32::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& value) const - { - return impl.getSplineKeyInt32(keyIndex, timeStamp, value); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineStepVector2f.cpp b/client/ramses-client/ramses-client-api/SplineStepVector2f.cpp deleted file mode 100644 index 4c55d80f6..000000000 --- a/client/ramses-client/ramses-client-api/SplineStepVector2f.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineStepVector2f.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineStepVector2f::SplineStepVector2f(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineStepVector2f::~SplineStepVector2f() - { - } - - status_t SplineStepVector2f::setKey(splineTimeStamp_t timeStamp, float x, float y) - { - const status_t status = impl.setSplineKeyStepVector2f(timeStamp, x, y); - LOG_HL_CLIENT_API3(status, timeStamp, x, y); - return status; - } - - status_t SplineStepVector2f::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y) const - { - return impl.getSplineKeyVector2f(keyIndex, timeStamp, x, y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineStepVector2i.cpp b/client/ramses-client/ramses-client-api/SplineStepVector2i.cpp deleted file mode 100644 index 5b77bfd4f..000000000 --- a/client/ramses-client/ramses-client-api/SplineStepVector2i.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineStepVector2i.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineStepVector2i::SplineStepVector2i(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineStepVector2i::~SplineStepVector2i() - { - } - - status_t SplineStepVector2i::setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y) - { - const status_t status = impl.setSplineKeyStepVector2i(timeStamp, x, y); - LOG_HL_CLIENT_API3(status, timeStamp, x, y); - return status; - } - - status_t SplineStepVector2i::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y) const - { - return impl.getSplineKeyVector2i(keyIndex, timeStamp, x, y); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineStepVector3f.cpp b/client/ramses-client/ramses-client-api/SplineStepVector3f.cpp deleted file mode 100644 index 8a6479434..000000000 --- a/client/ramses-client/ramses-client-api/SplineStepVector3f.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineStepVector3f.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineStepVector3f::SplineStepVector3f(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineStepVector3f::~SplineStepVector3f() - { - } - - status_t SplineStepVector3f::setKey(splineTimeStamp_t timeStamp, float x, float y, float z) - { - const status_t status = impl.setSplineKeyStepVector3f(timeStamp, x, y, z); - LOG_HL_CLIENT_API4(status, timeStamp, x, y, z); - return status; - } - - status_t SplineStepVector3f::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z) const - { - return impl.getSplineKeyVector3f(keyIndex, timeStamp, x, y, z); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineStepVector3i.cpp b/client/ramses-client/ramses-client-api/SplineStepVector3i.cpp deleted file mode 100644 index 324318b4c..000000000 --- a/client/ramses-client/ramses-client-api/SplineStepVector3i.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineStepVector3i.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineStepVector3i::SplineStepVector3i(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineStepVector3i::~SplineStepVector3i() - { - } - - status_t SplineStepVector3i::setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z) - { - const status_t status = impl.setSplineKeyStepVector3i(timeStamp, x, y, z); - LOG_HL_CLIENT_API4(status, timeStamp, x, y, z); - return status; - } - - status_t SplineStepVector3i::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z) const - { - return impl.getSplineKeyVector3i(keyIndex, timeStamp, x, y, z); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineStepVector4f.cpp b/client/ramses-client/ramses-client-api/SplineStepVector4f.cpp deleted file mode 100644 index 3e95059a0..000000000 --- a/client/ramses-client/ramses-client-api/SplineStepVector4f.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineStepVector4f.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineStepVector4f::SplineStepVector4f(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineStepVector4f::~SplineStepVector4f() - { - } - - status_t SplineStepVector4f::setKey(splineTimeStamp_t timeStamp, float x, float y, float z, float w) - { - const status_t status = impl.setSplineKeyStepVector4f(timeStamp, x, y, z, w); - LOG_HL_CLIENT_API5(status, timeStamp, x, y, z, w); - return status; - } - - status_t SplineStepVector4f::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& w) const - { - return impl.getSplineKeyVector4f(keyIndex, timeStamp, x, y, z, w); - } -} diff --git a/client/ramses-client/ramses-client-api/SplineStepVector4i.cpp b/client/ramses-client/ramses-client-api/SplineStepVector4i.cpp deleted file mode 100644 index 348b680a3..000000000 --- a/client/ramses-client/ramses-client-api/SplineStepVector4i.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/SplineStepVector4i.h" - -// internal -#include "SplineImpl.h" - -namespace ramses -{ - SplineStepVector4i::SplineStepVector4i(SplineImpl& pimpl) - : Spline(pimpl) - { - } - - SplineStepVector4i::~SplineStepVector4i() - { - } - - status_t SplineStepVector4i::setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w) - { - const status_t status = impl.setSplineKeyStepVector4i(timeStamp, x, y, z, w); - LOG_HL_CLIENT_API5(status, timeStamp, x, y, z, w); - return status; - } - - status_t SplineStepVector4i::getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, int32_t& w) const - { - return impl.getSplineKeyVector4i(keyIndex, timeStamp, x, y, z, w); - } -} diff --git a/client/ramses-client/ramses-client-api/StreamTexture.cpp b/client/ramses-client/ramses-client-api/StreamTexture.cpp deleted file mode 100644 index e0eda1770..000000000 --- a/client/ramses-client/ramses-client-api/StreamTexture.cpp +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "StreamTextureImpl.h" -#include "ramses-client-api/StreamTexture.h" - -namespace ramses -{ - status_t StreamTexture::forceFallbackImage(bool forceFallbackImage) - { - const status_t status = impl.forceFallbackImage(forceFallbackImage); - LOG_HL_CLIENT_API1(status, forceFallbackImage); - return status; - } - - bool StreamTexture::getForceFallbackImage() const - { - return impl.getForceFallbackImage(); - } - - StreamTexture::StreamTexture(StreamTextureImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - StreamTexture::~StreamTexture() - { - } - - waylandIviSurfaceId_t StreamTexture::getStreamSourceId() const - { - return impl.getStreamSource(); - } -} - diff --git a/client/ramses-client/ramses-client-api/Texture2DBuffer.cpp b/client/ramses-client/ramses-client-api/Texture2DBuffer.cpp deleted file mode 100644 index cca5d954d..000000000 --- a/client/ramses-client/ramses-client-api/Texture2DBuffer.cpp +++ /dev/null @@ -1,59 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2017 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include "ramses-client-api/Texture2DBuffer.h" - -// Internal -#include "Texture2DBufferImpl.h" - -namespace ramses -{ - Texture2DBuffer::Texture2DBuffer(Texture2DBufferImpl& pimpl) - : SceneObject(pimpl) - , impl(pimpl) - { - } - - Texture2DBuffer::~Texture2DBuffer() - { - } - - status_t Texture2DBuffer::updateData(uint32_t mipLevel, uint32_t offsetX, uint32_t offsetY, uint32_t width, uint32_t height, const void* data) - { - const status_t status = impl.setData(static_cast(data), mipLevel, offsetX, offsetY, width, height); - LOG_HL_CLIENT_API6(status, LOG_API_GENERIC_PTR_STRING(data), mipLevel, offsetX, offsetY, width, height); - return status; - } - - uint32_t Texture2DBuffer::getMipLevelCount() const - { - return impl.getMipLevelCount(); - } - - status_t Texture2DBuffer::getMipLevelSize(uint32_t mipLevel, uint32_t& widthOut, uint32_t& heightOut) const - { - return impl.getMipLevelSize(mipLevel, widthOut, heightOut); - } - - uint32_t Texture2DBuffer::getMipLevelDataSizeInBytes(uint32_t mipLevel) const - { - return impl.getMipLevelDataSizeInBytes(mipLevel); - } - - ETextureFormat Texture2DBuffer::getTexelFormat() const - { - return impl.getTexelFormat(); - } - - status_t Texture2DBuffer::getMipLevelData(uint32_t mipLevel, void* buffer, uint32_t bufferSize) const - { - return impl.getMipLevelData(mipLevel, static_cast(buffer), bufferSize); - } - -} diff --git a/client/ramses-client/ramses-client-api/UniformInput.cpp b/client/ramses-client/ramses-client-api/UniformInput.cpp deleted file mode 100644 index b065c7c5e..000000000 --- a/client/ramses-client/ramses-client-api/UniformInput.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/UniformInput.h" -#include "EffectInputImpl.h" -#include "EffectInputSemanticUtils.h" - -namespace ramses -{ - UniformInput::UniformInput() - : EffectInput(*(new EffectInputImpl())) - { - } - - EEffectInputDataType UniformInput::getDataType() const - { - return impl.getUniformInputDataType(); - } - - EEffectUniformSemantic UniformInput::getSemantics() const - { - return impl.getUniformSemantics(); - } - - uint32_t UniformInput::getElementCount() const - { - return impl.getElementCount(); - } -} diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimatedProperty.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimatedProperty.h deleted file mode 100644 index 97da4a566..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimatedProperty.h +++ /dev/null @@ -1,82 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATEDPROPERTY_H -#define RAMSES_ANIMATEDPROPERTY_H - -#include "ramses-client-api/AnimationObject.h" - -namespace ramses -{ - /** - * @brief The AnimatedProperty holds a reference to data that can be animated. - */ - class RAMSES_API AnimatedProperty : public AnimationObject - { - public: - /** - * Stores internal data for implementation specifics of AnimatedProperty. - */ - class AnimatedPropertyImpl& impl; - - /** - * @brief Deleted copy constructor - * @param other unused - */ - AnimatedProperty(const AnimatedProperty& other) = delete; - - /** - * @brief Deleted copy assignment - * @param other unused - * @return unused - */ - AnimatedProperty& operator=(const AnimatedProperty& other) = delete; - - protected: - /** - * @brief AnimatedPropertyFactory is the factory for creating animated properties. - */ - friend class AnimatedPropertyFactory; - /** - * @brief AnimationSystemData is the factory for creating animated properties. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of the animated property - * - * @param[in] pimpl Internal data for implementation specifics of AnimatedProperty (sink - instance becomes owner) - */ - explicit AnimatedProperty(AnimatedPropertyImpl& pimpl); - - /** - * @brief Destructor of the animated property - */ - virtual ~AnimatedProperty() override; - }; - - /// Vector component ID for binding single/multi component data - enum EAnimatedPropertyComponent - { - EAnimatedPropertyComponent_X = 0, - EAnimatedPropertyComponent_Y, - EAnimatedPropertyComponent_Z, - EAnimatedPropertyComponent_W, - EAnimatedPropertyComponent_All - }; - - /// Property to animate for objects that have more than one property that can be animated - enum EAnimatedProperty - { - EAnimatedProperty_Translation = 0, - EAnimatedProperty_Rotation, - EAnimatedProperty_Scaling - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/Animation.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/Animation.h deleted file mode 100644 index 27e2578f7..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/Animation.h +++ /dev/null @@ -1,78 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATION_H -#define RAMSES_ANIMATION_H - -#include "ramses-client-api/AnimationObject.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The Animation combines spline with one or more AnimatedProperty instances - * and allows control of the animation. - */ - class RAMSES_API Animation : public AnimationObject - { - public: - /** - * @brief Gets global time stamp for when animation is set to start. - * - * @return Start global time stamp. - */ - globalTimeStamp_t getStartTime() const; - - /** - * @brief Gets global time stamp for when animation is set to stop. - * - * @return Stop global time stamp. - */ - globalTimeStamp_t getStopTime() const; - - /** - * @brief Stores internal data for implementation specifics of Animation. - */ - class AnimationImpl& impl; - - protected: - /** - * @brief AnimationSystemData is the factory for creating Animation. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of the Animation - * - * @param[in] pimpl Internal data for implementation specifics of Animation (sink - instance becomes owner) - */ - explicit Animation(AnimationImpl& pimpl); - - /** - * @brief Copy constructor of Animation - * - * @param[in] other Other instance of Animation class - */ - Animation(const Animation& other); - - /** - * @brief Assignment operator of Animation. - * - * @param[in] other Other instance of Animation class - * @return This instance after assignment - */ - Animation& operator=(const Animation& other); - - /** - * @brief Destructor of the Animation - */ - virtual ~Animation(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationObject.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationObject.h deleted file mode 100644 index 88355248a..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationObject.h +++ /dev/null @@ -1,62 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONOBJECT_H -#define RAMSES_ANIMATIONOBJECT_H - -#include "ramses-client-api/SceneObject.h" - -namespace ramses -{ - /** - * @brief The AnimationObject is a base class for all client API objects owned by an AnimationSystem. - */ - class RAMSES_API AnimationObject : public SceneObject - { - public: - /** - * Stores internal data for implementation specifics of AnimationObject. - */ - class AnimationObjectImpl& impl; - - protected: - /** - * @brief Constructor for AnimationObject. - * - * @param[in] pimpl Internal data for implementation specifics of AnimationObject (sink - instance becomes owner) - */ - explicit AnimationObject(AnimationObjectImpl& pimpl); - - /** - * @brief Destructor of the AnimationObject - */ - virtual ~AnimationObject(); - - /** - * @brief AnimationSystemData is the factory for creating animated properties. - */ - friend class AnimationSystemData; - - private: - /** - * @brief Copy constructor of AnimationObject - */ - AnimationObject(const AnimationObject& other); - - /** - * @brief Assignment operator of AnimationObject. - * - * @param[in] other Instance to assign from - * @return This instance after assignment - */ - AnimationObject& operator=(const AnimationObject& other); - }; -} - -#endif - diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSequence.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSequence.h deleted file mode 100644 index 8526daa9c..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSequence.h +++ /dev/null @@ -1,277 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONSEQUENCE_H -#define RAMSES_ANIMATIONSEQUENCE_H - -#include "ramses-client-api/AnimationObject.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - class Animation; - - /** - * @brief The AnimationSequence is a container for multiple animations. - * AnimationSequence has its own virtual time line where all its animations - * are put onto with given offsets. The sequence of animations can then be - * started/stopped altogether. - */ - class RAMSES_API AnimationSequence : public AnimationObject - { - public: - /** - * @brief Add animation to the sequence. - * Animation will be placed on to sequence time line at given time stamp if provided. - * The animation must not be added to multiple sequences, otherwise this will result in an undefined behavior - * - * @param animation Animation to be added to sequence. If animation already exists in sequence, only its start/stop time is updated. - * @param startTimeInSequence Time stamp for animation to start within sequence time line. By default animation is added to the beginning of sequence. - * @param stopTimeInSequence Time stamp for animation to stop within sequence time line. By default animation stops when its last spline key is reached. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t addAnimation(const Animation& animation, sequenceTimeStamp_t startTimeInSequence = 0u, sequenceTimeStamp_t stopTimeInSequence = 0u); - - /** - * @brief Remove animation from sequence. - * - * @param animation Animation to be removed from sequence. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t removeAnimation(const Animation& animation); - - /** - * @brief Starts the sequence of animations. - * Offset can be used to postpone start if positive or start sequence - * from a certain point if negative. - * - * @param offset Offset start in milliseconds. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t start(timeMilliseconds_t offset = 0); - - /** - * @brief Starts the sequence of animations at given time. - * - * @param timeStamp Global time stamp for sequence to start. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t startAt(globalTimeStamp_t timeStamp); - - /** - * @brief Starts the sequence of animations in reverse. - * Offset can be used to postpone start if positive or start sequence - * from a certain point if negative. - * - * @param offset Offset start in milliseconds. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t startReverse(timeMilliseconds_t offset = 0); - - /** - * @brief Starts the sequence of animations at given time in reverse. - * - * @param timeStamp Global time stamp for sequence to start in reverse. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t startReverseAt(globalTimeStamp_t timeStamp); - - /** - * @brief Stops the sequence of animations. - * Delay can be used to postpone the stop. - * - * @param delay Delay stop in milliseconds. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t stop(timeMilliseconds_t delay = 0); - - /** - * @brief Stops the sequence of animations at given time. - * - * @param timeStamp Global time stamp for sequence to stop. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t stopAt(globalTimeStamp_t timeStamp); - - /** - * @brief Sets sequence playback speed affecting all animations within the sequence. - * Default sequence playback speed is 1, higher than 1 means faster (2 is double speed), - * lower than 1 means slower (0.5 is half speed). - * Zero and negative values are not allowed. - * - * @param playbackSpeed Playback speed multiplier. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setPlaybackSpeed(float playbackSpeed); - - /** - * @brief Gets sequence current playback speed. - * See setPlaybackSpeed() for meanings of the values. - * - * @return Playback speed. - */ - float getPlaybackSpeed() const; - - /** - * @brief Sets animation relative. - * By default animation is absolute, ie. data values from spline keys are directly - * assigned to animated property. - * Relative animation takes value of the animated property at the point when animation starts - * and adds spline key value to it. - * - * Note: Changing from absolute to relative animation during playback will cause undefined results. - * - * @param animation Animation in sequence for which relative is to be set - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setAnimationRelative(const Animation& animation); - - /** - * @brief Sets animation absolute. - * By default animation is absolute, ie. data values from spline keys are directly - * assigned to animated property. - * - * Note: Changing from relative to absolute animation during playback will cause undefined results. - * - * @param animation Animation in sequence for which absolute is to be set - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setAnimationAbsolute(const Animation& animation); - - /** - * @brief Enables animation looping. - * When animation reaches last spline key (or loopDuration passes) it begins playback from start. - * Looping animation can only be stopped by calling stop(). - * - * @param animation Animation in sequence for which looping is to be set - * @param loopDuration Duration of one loop iteration. If 0 one loop iteration equals duration - * of whole animation (determined from last spline key). - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setAnimationLooping(const Animation& animation, timeMilliseconds_t loopDuration = 0); - - /** - * @brief Returns true if animation is set to relative. - * See setAnimationRelative(). - * - * @param animation Animation in sequence for which relative state has to be returned - * @return True if animation is relative, false otherwise. - */ - bool isAnimationRelative(const Animation& animation) const; - - /** - * @brief Returns true if animation is set to loop. - * - * @param animation Animation in sequence for which looping state has to be returned - * @return True if animation is set to loop, false otherwise. - */ - bool isAnimationLooping(const Animation& animation) const; - - /** - * @brief Gets loop duration for animation. - * See setAnimationLooping() for meaning of the value. - * - * @param animation Animation in sequence for which looping duration has to be returned - * @return Loop duration of animation. - */ - timeMilliseconds_t getAnimationLoopDuration(const Animation& animation) const; - - /** - * @brief Returns number of animations within the sequence. - * - * @return Number of animations. - */ - uint32_t getNumberOfAnimations() const; - - /** - * @brief Checks if given animation is in the AnimationSequence. - * - * @param animation Animation to check. - * @return True if animation is contained by the AnimationSequence, false otherwise. - */ - bool containsAnimation(const Animation& animation) const; - - /** - * @brief Gives animation start time within the AnimationSequence. - * - * @param animation Animation to check. - * @return Animation start time stamp in the AnimationSequence time line. - * InvalidSequenceTimeStamp if animation is not in this AnimationSequence. - */ - sequenceTimeStamp_t getAnimationStartTimeInSequence(const Animation& animation) const; - - /** - * @brief Gives animation stop time within the AnimationSequence. - * - * @param animation Animation to check. - * @return Animation stop time stamp in the AnimationSequence time line. - * InvalidSequenceTimeStamp if animation is not in this AnimationSequence. - */ - sequenceTimeStamp_t getAnimationStopTimeInSequence(const Animation& animation) const; - - /** - * @brief Gets the maximum timestamp of all animations in this sequence - * - * @return max time stamp - */ - sequenceTimeStamp_t getAnimationSequenceStopTime() const; - - /** - * @brief Stores internal data for implementation specifics of AnimationSequence. - */ - class AnimationSequenceImpl& impl; - - protected: - /** - * @brief AnimationSystemData is the factory for creating AnimationSequence. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of the AnimationSequence. - * - * @param[in] pimpl Internal data for implementation specifics of AnimationSequence (sink - instance becomes owner) - */ - explicit AnimationSequence(AnimationSequenceImpl& pimpl); - - /** - * @brief Copy constructor of AnimationSequence. - * - * @param[in] other Other instance of AnimationSequence class - */ - AnimationSequence(const AnimationSequence& other); - - /** - * @brief Assignment operator of AnimationSequence. - * - * @param[in] other Other instance of AnimationSequence class - * @return This instance after assignment - */ - AnimationSequence& operator=(const AnimationSequence& other); - - /** - * @brief Destructor of the AnimationSequence. - */ - virtual ~AnimationSequence(); - - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystem.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystem.h deleted file mode 100644 index 2a5dd74bf..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystem.h +++ /dev/null @@ -1,418 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONSYSTEM_H -#define RAMSES_ANIMATIONSYSTEM_H - -#include "ramses-client-api/AnimationTypes.h" -#include "ramses-client-api/SceneObject.h" -#include "ramses-client-api/AnimatedProperty.h" - -namespace ramses -{ - class Spline; - class SplineStepBool; - class SplineStepInt32; - class SplineStepFloat; - class SplineStepVector2f; - class SplineStepVector3f; - class SplineStepVector4f; - class SplineStepVector2i; - class SplineStepVector3i; - class SplineStepVector4i; - class SplineLinearInt32; - class SplineLinearFloat; - class SplineLinearVector2f; - class SplineLinearVector3f; - class SplineLinearVector4f; - class SplineLinearVector2i; - class SplineLinearVector3i; - class SplineLinearVector4i; - class SplineBezierInt32; - class SplineBezierFloat; - class SplineBezierVector2f; - class SplineBezierVector3f; - class SplineBezierVector4f; - class SplineBezierVector2i; - class SplineBezierVector3i; - class SplineBezierVector4i; - class Animation; - class AnimationSequence; - class Node; - class UniformInput; - class DataObject; - class Appearance; - class AnimationObject; - class PickableObject; - - /** - * @brief The AnimationSystem holds all animation related data. - */ - class RAMSES_API AnimationSystem : public SceneObject - { - public: - /** - * @brief Sets the animation system to a given time. - * Any unsigned integral values that are used in an incrementing fashion - * can be used. This time stamp is distributed to renderer and can be used - * as a synchronization point. - * - * @param[in] timeStamp The global time stamp to be set in animation system - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setTime(globalTimeStamp_t timeStamp); - - /** - * @brief Gets the current animation system time. - * The time stamp retrieved is the time stamp that was previously set by calling setTime. - * - * @return Current time stamp of the animation system - */ - globalTimeStamp_t getTime() const; - - /** - * @brief Creates a spline in this animation system using bool data type and step interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineStepBool* createSplineStepBool(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using int32_t data type and step interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineStepInt32* createSplineStepInt32(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using float data type and step interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineStepFloat* createSplineStepFloat(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector2f data type and step interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineStepVector2f* createSplineStepVector2f(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector3f data type and step interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineStepVector3f* createSplineStepVector3f(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector4f data type and step interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineStepVector4f* createSplineStepVector4f(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector2i data type and step interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineStepVector2i* createSplineStepVector2i(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector3i data type and step interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineStepVector3i* createSplineStepVector3i(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector4i data type and step interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineStepVector4i* createSplineStepVector4i(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using int32_t data type and linear interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineLinearInt32* createSplineLinearInt32(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using float data type and linear interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineLinearFloat* createSplineLinearFloat(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector2f data type and linear interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineLinearVector2f* createSplineLinearVector2f(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector3f data type and linear interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineLinearVector3f* createSplineLinearVector3f(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector4f data type and linear interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineLinearVector4f* createSplineLinearVector4f(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector2i data type and linear interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineLinearVector2i* createSplineLinearVector2i(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector3i data type and linear interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineLinearVector3i* createSplineLinearVector3i(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector4i data type and linear interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineLinearVector4i* createSplineLinearVector4i(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using int32_t data type and Bezier interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineBezierInt32* createSplineBezierInt32(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using float data type and Bezier interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineBezierFloat* createSplineBezierFloat(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector2f data type and Bezier interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineBezierVector2f* createSplineBezierVector2f(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector3f data type and Bezier interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineBezierVector3f* createSplineBezierVector3f(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector4f data type and Bezier interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineBezierVector4f* createSplineBezierVector4f(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector2i data type and Bezier interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineBezierVector2i* createSplineBezierVector2i(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector3i data type and Bezier interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineBezierVector3i* createSplineBezierVector3i(const char* name = nullptr); - - /** - * @brief Creates a spline in this animation system using Vector4i data type and Bezier interpolation - * - * @param[in] name The optional name of the spline - * @return Pointer to the created spline, null on failure - */ - SplineBezierVector4i* createSplineBezierVector4i(const char* name = nullptr); - - /** - * @brief Creates Animation that can animate given property using given spline - * - * @param[in] animatedProperty AnimatedProperty to animate with this Animation - * @param[in] spline Spline to be used for animation - * @param[in] name The optional name of the Animation - * @return Pointer to the created Animation, null on failure - */ - Animation* createAnimation(const AnimatedProperty& animatedProperty, const Spline& spline, const char* name = nullptr); - - /** - * @brief Creates AnimationSequence that can hold references to multiple animations and control them together. - * - * @param[in] name The optional name of the AnimationSequence - * @return Pointer to the created AnimationSequence, null on failure - */ - AnimationSequence* createAnimationSequence(const char* name = nullptr); - - /** - * @brief Release an animation system object and its data. - * The object must be owned by this animation system. - * The reference to this object is no longer valid after it is destroyed. - * - * @param[in] animationObject Animation object to be released. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t destroy(AnimationObject& animationObject); - - /** - * @brief Create a new animated property for Node - * - * @param[in] propertyOwner Reference to an entity that contains data to animate. - * @param[in] property Property to animate. - * @param[in] propertyComponent The optional component to animate in case of vector property. - * @param[in] name The optional name of the AnimatedProperty. - * @return AnimatedProperty referring to Node's property, null on failure - */ - AnimatedProperty* createAnimatedProperty(const Node& propertyOwner, EAnimatedProperty property, EAnimatedPropertyComponent propertyComponent = EAnimatedPropertyComponent_All, const char* name = nullptr); - - /** - * @brief Create a new animated property for Appearance's input - * - * @param[in] propertyOwner Reference to an entity that contains data to animate. - * @param[in] appearance Reference to the appearance to which the input belongs. - * @param[in] propertyComponent The optional component to animate in case of vector property. - * @param[in] name The optional name of the AnimatedProperty. - * @return AnimatedProperty referring to Appearance input, null on failure - */ - AnimatedProperty* createAnimatedProperty(const UniformInput& propertyOwner, const Appearance& appearance, EAnimatedPropertyComponent propertyComponent = EAnimatedPropertyComponent_All, const char* name = nullptr); - - /** - * @brief Create a new animated property for a DataObject - * - * @param[in] propertyOwner Reference to an entity that contains data to animate. - * @param[in] propertyComponent The optional component to animate in case of vector property. - * @param[in] name The optional name of the AnimatedProperty. - * @return AnimatedProperty referring to DataObject, null on failure - */ - AnimatedProperty* createAnimatedProperty(const DataObject& propertyOwner, EAnimatedPropertyComponent propertyComponent = EAnimatedPropertyComponent_All, const char* name = nullptr); - - /** - * @brief Get number of animations that were finished in current update round. - * The animation system collects finished animations between - * current and previous setTime/updateLocalTime call. - * - * @return Number of finished animations in current update round. - */ - uint32_t getNumberOfFinishedAnimationsSincePreviousUpdate() const; - - /** - * @brief Get animation that was finished in current update round. - * The animation system collects finished animations between - * current and previous setTime/updateLocalTime call. - * - * @param[in] index Index of the finished animation. - * Use getNumberOfFinishedAnimationsSincePreviousUpdate() to get the count. - * @return Animation finished in current update round. - */ - const Animation* getFinishedAnimationSincePreviousUpdate(uint32_t index) const; - - /** - * @copydoc getFinishedAnimationSincePreviousUpdate(uint32_t index) const - **/ - Animation* getFinishedAnimationSincePreviousUpdate(uint32_t index); - - /** - * @brief Get an object from the animation system by name - * - * @param[in] name The name of the object to get. - * @return Pointer to the object if found, nullptr otherwise. - */ - const RamsesObject* findObjectByName(const char* name) const; - - /** - * @copydoc findObjectByName(const char* name) const - **/ - RamsesObject* findObjectByName(const char* name); - - /** - * Stores internal data for implementation specifics of animation system. - */ - class AnimationSystemImpl& impl; - - protected: - /** - * @brief SceneImpl is the factory for creating animation system instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of the animation system - * - * @param[in] pimpl Internal data for implementation specifics of AnimationSystem (sink - instance becomes owner) - */ - explicit AnimationSystem(AnimationSystemImpl& pimpl); - - /** - * @brief Copy constructor of animation system - * - * @param[in] other Other instance of animation system class - */ - AnimationSystem(const AnimationSystem& other); - - /** - * @brief Assignment operator of animation system. - * - * @param[in] other Other instance of animation system class - * @return This instance after assignment - */ - AnimationSystem& operator=(const AnimationSystem& other); - - /** - * @brief Destructor of the animation system - */ - virtual ~AnimationSystem(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemEnums.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemEnums.h deleted file mode 100644 index 87b9bd04d..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemEnums.h +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONSYSTEMENUMS_H -#define RAMSES_ANIMATIONSYSTEMENUMS_H - - -namespace ramses -{ - - /// AnimationSystem creation flags - enum EAnimationSystemFlags - { - EAnimationSystemFlags_Default = 0, - EAnimationSystemFlags_ClientSideProcessing, - EAnimationSystemFlags_SynchronizedClock ///< uses a synchronized time source for realtime animations (for renderer and client running on different machines) - }; - -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemObjectIterator.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemObjectIterator.h deleted file mode 100644 index 763539568..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemObjectIterator.h +++ /dev/null @@ -1,57 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONSYSTEMOBJECTITERATOR_H -#define RAMSES_ANIMATIONSYSTEMOBJECTITERATOR_H - -#include "ramses-client-api/RamsesObject.h" - -namespace ramses -{ - class AnimationSystemIteratorImpl; - class AnimationSystem; - - /** - * @brief The AnimationSystemObjectIterator iterates over objects of given type in an AnimationSystem. - * - * It provides a way to traverse all objects of given type owned by a given animation system. - */ - class RAMSES_API AnimationSystemObjectIterator - { - public: - /** - * - * @brief A AnimationSystemObjectIterator can iterate through objects of given type within an animation system. - * @param[in] animationSystem AnimationSystem whose objects to iterate through. - * @param[in] objectType Optional type of objects to iterate through. - **/ - explicit AnimationSystemObjectIterator(const AnimationSystem& animationSystem, ERamsesObjectType objectType = ERamsesObjectType_RamsesObject); - - /** - * - * @brief Destructor - **/ - ~AnimationSystemObjectIterator(); - - /** - * - * @brief Iterate through all objects of given type - * @return next object, null if no more objects available - * - * Iterator is invalid and may no longer be used if any objects are added or removed. - **/ - RamsesObject* getNext(); - - private: - AnimationSystemObjectIterator(const AnimationSystemObjectIterator& iterator); - AnimationSystemObjectIterator& operator=(const AnimationSystemObjectIterator& iterator); - AnimationSystemIteratorImpl* impl; - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemRealTime.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemRealTime.h deleted file mode 100644 index 78dde6fbf..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationSystemRealTime.h +++ /dev/null @@ -1,93 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONSYSTEMREALTIME_H -#define RAMSES_ANIMATIONSYSTEMREALTIME_H - -#include "ramses-client-api/AnimationSystem.h" - -namespace ramses -{ - /** - * @brief The AnimationSystemRealTime is a special version of AnimationSystem - * that is designed to use system time for animations. - * - * In a Ramses distributed system the client and the renderer have their own copies - * of animation system. Real time animation system guarantees smooth animations without - * stuttering by giving the control of time updates to the renderer. - * The renderer updates the animation system using system time every frame. - * - * The client animation system does not have to worry about updates as long as it is not - * making changes to the states and properties. Many animation related commands - * rely on the current time of the local animation system, it uses it as time stamp - * for some commands that are then distributed to the renderer. - * Therefore it is important to properly update the time of the local animation system - * before any change is made. - */ - class RAMSES_API AnimationSystemRealTime : public AnimationSystem - { - public: - - /** - * @brief Sets the local animation system to a given time. - * The time used should always be system time, because the renderer uses - * the system time implicitly. Slight offset can be used to delay or hide - * latency of the distribution system. - * - * @param[in] systemTime The time stamp to be set in the local animation system. - * If 0 then system time is used. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t updateLocalTime(globalTimeStamp_t systemTime = 0u); - - protected: - /** - * @brief SceneImpl is the factory for creating animation system instances. - */ - friend class SceneImpl; - - /** - * @brief Hidden method from AnimationSystem base class as time cannot be set to AnimationSystemRealTime - * - * @param[in] timeStamp Explicit time cannot be used with AnimationSystemRealTime - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setTime(globalTimeStamp_t timeStamp); - - /** - * @brief Constructor of the animation system - * - * @param[in] pimpl Internal data for implementation specifics of AnimationSystem (sink - instance becomes owner) - */ - explicit AnimationSystemRealTime(AnimationSystemImpl& pimpl); - - /** - * @brief Copy constructor of animation system - * - * @param[in] other Other instance of animation system class - */ - AnimationSystemRealTime(const AnimationSystemRealTime& other); - - /** - * @brief Assignment operator of animation system. - * - * @param[in] other Other instance of animation system class - * @return This instance after assignment - */ - AnimationSystemRealTime& operator=(const AnimationSystemRealTime& other); - - /** - * @brief Destructor of the animation system - */ - virtual ~AnimationSystemRealTime(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationTypes.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationTypes.h deleted file mode 100644 index 97ee60c23..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/AnimationTypes.h +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ANIMATIONTYPES_H -#define RAMSES_ANIMATIONTYPES_H - -#include "ramses-framework-api/RamsesFrameworkTypes.h" - -namespace ramses -{ - using timeMilliseconds_t = int64_t; /**< Time in milliseconds type */ - using globalTimeStamp_t = uint64_t; /**< Global time stamp type */ - using splineTimeStamp_t = uint32_t; /**< Spline key time stamp type */ - using sequenceTimeStamp_t = uint32_t; /**< Local sequence time stamp for animation within sequence */ - using splineKeyIndex_t = uint32_t; /**< Spline key index type */ - - /// Time stamp within a sequence local time denoting an invalid value - const sequenceTimeStamp_t InvalidSequenceTimeStamp = sequenceTimeStamp_t(-1); -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/Appearance.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/Appearance.h deleted file mode 100644 index 2c9300f2b..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/Appearance.h +++ /dev/null @@ -1,929 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_APPEARANCE_H -#define RAMSES_APPEARANCE_H - -#include "ramses-client-api/SceneObject.h" -#include "ramses-client-api/AppearanceEnums.h" - -namespace ramses -{ - class SceneImpl; - class UniformInput; - class DataObject; - class TextureSampler; - class TextureSamplerMS; - class TextureSamplerExternal; - class Effect; - - /** - * @brief The Appearance describes how an object should look like. This includes GLSL uniform values, - * and GPU states such as blending, buffer configurations, masks etc. The API to set uniform values - * is aligned to the glUniformX API of OpenGL. Beware that boolean values are reported and handled - * as int (0 is false, anything else is true) - similar to OpenGL conventions. - * - * It provides mechanisms for creating effects - */ - class RAMSES_API Appearance : public SceneObject - { - public: - /** - * @brief Sets blending factors for source/destination color/alpha. - * Blending operations need to be set as well in order to enable blending. - * - * @param[in] srcColor Source color blending factor - * @param[in] destColor Destination color blending factor - * @param[in] srcAlpha Source alpha blending factor - * @param[in] destAlpha Destination alpha blending factor - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setBlendingFactors(EBlendFactor srcColor, EBlendFactor destColor, EBlendFactor srcAlpha, EBlendFactor destAlpha); - - /** - * @brief Gets blending factors for source/destination color/alpha. - * - * @param[out] srcColor Source color blending factor - * @param[out] destColor Destination color blending factor - * @param[out] srcAlpha Source alpha blending factor - * @param[out] destAlpha Destination alpha blending factor - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getBlendingFactors(EBlendFactor& srcColor, EBlendFactor& destColor, EBlendFactor& srcAlpha, EBlendFactor& destAlpha) const; - - /** - * @brief Sets blending operation for color and alpha. - * Blending factors need to be set as well in order to enable blending. - * - * @param[in] operationColor Blending operation for color - * @param[in] operationAlpha Blending operation for alpha - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setBlendingOperations(EBlendOperation operationColor, EBlendOperation operationAlpha); - - /** - * @brief Gets blending operation for color and alpha. - * - * @param[out] operationColor Blending operation for color - * @param[out] operationAlpha Blending operation for alpha - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getBlendingOperations(EBlendOperation& operationColor, EBlendOperation& operationAlpha) const; - - /** - * @brief Sets blending color that can be used as blending color constant for some blending factors. - * The default value is (0,0,0,0) - * - * @param[in] red Red channel in blending color - * @param[in] green Green channel in blending color - * @param[in] blue Blue channel in blending color - * @param[in] alpha Alpha channel in blending color - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setBlendingColor(float red, float green, float blue, float alpha); - - /** - * @brief Gets blending color set via setBlendingColor - * - * @param[out] red Red channel in blending color - * @param[out] green Green channel in blending color - * @param[out] blue Blue channel in blending color - * @param[out] alpha Alpha channel in blending color - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getBlendingColor(float& red, float& green, float& blue, float& alpha) const; - - /** - * @brief Enables or disables writing to depth buffer. - * - * @param[in] mode Flag denoting enabling or disabling depth writes. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setDepthWrite(EDepthWrite mode); - - /** - * @brief Gets the current state of writing to depth buffer. - * @param[out] mode Depth write mode - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getDepthWriteMode(EDepthWrite& mode) const; - - /** - * @brief Sets depth comparison function. - * Depth writing has to be enabled in order for this to have any effect. - * Default depth comparison function is less or equal. - * - * @param[in] func Depth comparison function to be used - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setDepthFunction(EDepthFunc func); - - /** - * @brief Gets depth comparison function. - * - * @param[out] func Depth comparison function to be used - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getDepthFunction(EDepthFunc& func) const; - - /** - * @brief Enables or disables scissor test and sets region for scissor test - * - * @param[in] state Flag denoting enabling or disabling scissor test. - * @param[in] x Offset of scissor region on x-axis. - * @param[in] y Offset of scissor region on y-axis. - * @param[in] width Width of scissor region. - * @param[in] height Height of scissor region. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setScissorTest(EScissorTest state, int16_t x, int16_t y, uint16_t width, uint16_t height); - - /** - * @brief Gets the current state of scissor test. - * @param[out] state State of scissor test - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getScissorTestState(EScissorTest& state) const; - - /** - * @brief Gets region for scissor test - * - * @param[out] x Offset of scissor region on x-axis. - * @param[out] y Offset of scissor region on y-axis. - * @param[out] width Width of scissor region. - * @param[out] height Height of scissor region. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getScissorRegion(int16_t& x, int16_t& y, uint16_t& width, uint16_t& height) const; - - /** - * @brief Sets stencil function, reference and mask value for stencil testing. - * Stencil is disabled by default. - * - * @param[in] func Stencil function to be used - * @param[in] ref Stencil reference value to be used - * @param[in] mask Stencil mask value to be used - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setStencilFunction(EStencilFunc func, uint8_t ref, uint8_t mask); - - /** - * @brief Gets stencil function, reference and mask value - * - * @param[out] func Stencil function currently set - * @param[out] ref Stencil reference value currently set - * @param[out] mask Stencil mask value currently set - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getStencilFunction(EStencilFunc& func, uint8_t& ref, uint8_t& mask) const; - - /** - * @brief Sets stencil operations for stencil testing. - * Default stencil operation values are keep. - * - * @param[in] sfail Stencil operation when stencil test fails - * @param[in] dpfail Stencil operation when the stencil test passes, but the depth test fails - * @param[in] dppass Stencil operation when both the stencil test and the depth test pass - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setStencilOperation(EStencilOperation sfail, EStencilOperation dpfail, EStencilOperation dppass); - - /** - * @brief Gets stencil operations - * - * @param[out] sfail Stencil operation when stencil test fails - * @param[out] dpfail Stencil operation when the stencil test passes, but the depth test fails - * @param[out] dppass Stencil operation when both the stencil test and the depth test pass - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getStencilOperation(EStencilOperation& sfail, EStencilOperation& dpfail, EStencilOperation& dppass) const; - - /** - * @brief Sets the culling mode indicating which side of mesh will be removed before rasterization. - * Default culling mode is BackFaceCulling. - * - * @param[in] mode Culling mode to be used. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setCullingMode(ECullMode mode); - - /** - * @brief Sets the draw mode indicating by which primitive the mesh will be rendered - * Default draw mode is Triangles. - * - * @param[in] mode Draw mode to be used. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setDrawMode(EDrawMode mode); - - /** - * @brief Gets the culling mode indicating which side of mesh will be removed before rasterization. - * - * @param[out] mode Culling mode to be used. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getCullingMode(ECullMode& mode) const; - - /** - * @brief Gets the draw mode indicating by which primitive the mesh will be rendered - * - * @param[out] mode draw mode to be used. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getDrawMode(EDrawMode& mode) const; - - /** - * @brief Sets color write mask. - * If needed certain color channels can stay untouched using the color write mask. - * By default writing to all color channels is enabled. - * - * @param[in] writeRed Enable/disable flag for red channel - * @param[in] writeGreen Enable/disable flag for green channel - * @param[in] writeBlue Enable/disable flag for blue channel - * @param[in] writeAlpha Enable/disable flag for alpha channel - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setColorWriteMask(bool writeRed, bool writeGreen, bool writeBlue, bool writeAlpha); - - /** - * @brief Gets color write mask. - * - * @param[out] writeRed Enable/disable flag for red channel - * @param[out] writeGreen Enable/disable flag for green channel - * @param[out] writeBlue Enable/disable flag for blue channel - * @param[out] writeAlpha Enable/disable flag for alpha channel - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getColorWriteMask(bool& writeRed, bool& writeGreen, bool& writeBlue, bool& writeAlpha) const; - - /** - * Stores internal data for implementation specifics of Appearance. - */ - class AppearanceImpl& impl; - - /** - * @brief Sets value of the input. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] i The integer 32bit value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueInt32(const UniformInput& input, int32_t i); - - /** - * @brief Sets values of the input elements. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] elementCount The number of values that are used from values. Must match UniformInput::getElementCount() - * @param[in] values Pointer the the values - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueInt32(const UniformInput& input, uint32_t elementCount, const int32_t* values); - - /** - * @brief Gets the value of the input. - * - * @param[in] input The effect uniform input - * @param[out] i The signed int 32-bit value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueInt32(const UniformInput& input, int32_t& i) const; - - /** - * @brief Gets current values of the input. - * - * @param[in] input The effect uniform input - * @param[in] elementCount the number of values that are copied to valuesOut. Must match UniformInput::getElementCount() - * @param[out] valuesOut location where the values are copied to - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueInt32(const UniformInput& input, uint32_t elementCount, int32_t* valuesOut) const; - - - /** - * @brief Sets value of the input. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] value The float value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueFloat(const UniformInput& input, float value); - - /** - * @brief Sets values of the input elements. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] elementCount The number of values that are used from values. Must match UniformInput::getElementCount() - * @param[in] values Pointer the the values - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueFloat(const UniformInput& input, uint32_t elementCount, const float* values); - - /** - * @brief Gets the value of the input. - * - * @param[in] input The effect uniform input - * @param[out] valueOut The float value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueFloat(const UniformInput& input, float& valueOut) const; - - /** - * @brief Gets current values of the input. - * - * @param[in] input The effect uniform input - * @param[in] elementCount the number of values that are copied to valuesOut. Must match UniformInput::getElementCount() - * @param[out] valuesOut location where the values are copied to - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueFloat(const UniformInput& input, uint32_t elementCount, float* valuesOut) const; - - /** - * @brief Sets value of the input. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] x The X component value - * @param[in] y The Y component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector2i(const UniformInput& input, int32_t x, int32_t y); - - /** - * @brief Sets values of the input elements. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] elementCount The number of values that are used from values. Must match UniformInput::getElementCount() - * @param[in] values Pointer the the values - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector2i(const UniformInput& input, uint32_t elementCount, const int32_t* values); - - /** - * @brief Gets the value of the input. - * - * @param[in] input The effect uniform input - * @param[out] x The X component value - * @param[out] y The Y component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector2i(const UniformInput& input, int32_t& x, int32_t& y) const; - - /** - * @brief Gets current values of the input. - * - * @param[in] input The effect uniform input - * @param[in] elementCount the number of values that are copied to valuesOut. Must match UniformInput::getElementCount() - * @param[out] valuesOut location where the values are copied to - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector2i(const UniformInput& input, uint32_t elementCount, int32_t* valuesOut) const; - - /** - * @brief Sets value of the input. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] x The X component value - * @param[in] y The Y component value - * @param[in] z The Z component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector3i(const UniformInput& input, int32_t x, int32_t y, int32_t z); - - /** - * @brief Sets values of the input elements. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] elementCount The number of values that are used from values. Must match UniformInput::getElementCount() - * @param[in] values Pointer the the values - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector3i(const UniformInput& input, uint32_t elementCount, const int32_t* values); - - /** - * @brief Gets the value of the input. - * - * @param[in] input The effect uniform input - * @param[out] x The X component value - * @param[out] y The Y component value - * @param[out] z The Z component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector3i(const UniformInput& input, int32_t& x, int32_t& y, int32_t& z) const; - - /** - * @brief Gets current values of the input. - * - * @param[in] input The effect uniform input - * @param[in] elementCount the number of values that are copied to valuesOut. Must match UniformInput::getElementCount() - * @param[out] valuesOut location where the values are copied to - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector3i(const UniformInput& input, uint32_t elementCount, int32_t* valuesOut) const; - - /** - * @brief Sets value of the input. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] x The X component value - * @param[in] y The Y component value - * @param[in] z The Z component value - * @param[in] w The W component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector4i(const UniformInput& input, int32_t x, int32_t y, int32_t z, int32_t w); - - /** - * @brief Sets values of the input elements. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] elementCount The number of values that are used from values. Must match UniformInput::getElementCount() - * @param[in] values Pointer the the values - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector4i(const UniformInput& input, uint32_t elementCount, const int32_t* values); - - /** - * @brief Gets the value of the input. - * - * @param[in] input The effect uniform input - * @param[out] x The X component value - * @param[out] y The Y component value - * @param[out] z The Z component value - * @param[out] w The W component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector4i(const UniformInput& input, int32_t& x, int32_t& y, int32_t& z, int32_t& w) const; - - /** - * @brief Gets current values of the input. - * - * @param[in] input The effect uniform input - * @param[in] elementCount the number of values that are copied to valuesOut. Must match UniformInput::getElementCount() - * @param[out] valuesOut location where the values are copied to - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector4i(const UniformInput& input, uint32_t elementCount, int32_t* valuesOut) const; - - /** - * @brief Sets value of the input. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] x The X component value - * @param[in] y The Y component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector2f(const UniformInput& input, float x, float y); - - /** - * @brief Sets values of the input elements. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] elementCount The number of values that are used from values. Must match UniformInput::getElementCount() - * @param[in] values Pointer the the values - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector2f(const UniformInput& input, uint32_t elementCount, const float* values); - - /** - * @brief Gets the value of the input. - * - * @param[in] input The effect uniform input - * @param[out] x The X component value - * @param[out] y The Y component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector2f(const UniformInput& input, float& x, float& y) const; - - /** - * @brief Gets current values of the input. - * - * @param[in] input The effect uniform input - * @param[in] elementCount the number of values that are copied to valuesOut. Must match UniformInput::getElementCount() - * @param[out] valuesOut location where the values are copied to - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector2f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const; - - /** - * @brief Sets value of the input. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] x The X component value - * @param[in] y The Y component value - * @param[in] z The Z component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector3f(const UniformInput& input, float x, float y, float z); - - /** - * @brief Sets values of the input elements. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] elementCount The number of values that are used from values. Must match UniformInput::getElementCount() - * @param[in] values Pointer the the values - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector3f(const UniformInput& input, uint32_t elementCount, const float* values); - - /** - * @brief Gets the value of the input. - * - * @param[in] input The effect uniform input - * @param[out] x The X component value - * @param[out] y The Y component value - * @param[out] z The Z component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector3f(const UniformInput& input, float& x, float& y, float& z) const; - - /** - * @brief Gets current values of the input. - * - * @param[in] input The effect uniform input - * @param[in] elementCount the number of values that are copied to valuesOut. Must match UniformInput::getElementCount() - * @param[out] valuesOut location where the values are copied to - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector3f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const; - - /** - * @brief Sets value of the input. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] x The X component value - * @param[in] y The Y component value - * @param[in] z The Z component value - * @param[in] w The W component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector4f(const UniformInput& input, float x, float y, float z, float w); - - /** - * @brief Sets values of the input elements. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] elementCount The number of values that are used from values. Must match UniformInput::getElementCount() - * @param[in] values Pointer the the values - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueVector4f(const UniformInput& input, uint32_t elementCount, const float* values); - - /** - * @brief Gets the value of the input. - * - * @param[in] input The effect uniform input - * @param[out] x The X component value - * @param[out] y The Y component value - * @param[out] z The Z component value - * @param[out] w The W component value - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector4f(const UniformInput& input, float& x, float& y, float& z, float& w) const; - - /** - * @brief Gets current values of the input. - * - * @param[in] input The effect uniform input - * @param[in] elementCount the number of values that are copied to valuesOut. Must match UniformInput::getElementCount() - * @param[out] valuesOut location where the values are copied to - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueVector4f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const; - - /** - * @brief Sets value of the input. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] values The matrix 2x2 value (column-wise) - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueMatrix22f(const UniformInput& input, const float values[4]); - - /** - * @brief Sets values of the input elements. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] elementCount The number of values that are used from values. Must match UniformInput::getElementCount() - * @param[in] values Pointer the the values (will be stored column-wise) - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueMatrix22f(const UniformInput& input, uint32_t elementCount, const float* values); - - /** - * @brief Gets the value of the input. - * - * @param[in] input The effect uniform input - * @param[out] valueOut The matrix 2x2 value (column-wise) - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueMatrix22f(const UniformInput& input, float valueOut[4]) const; - - /** - * @brief Gets current values of the input. - * - * @param[in] input The effect uniform input - * @param[in] elementCount the number of values that are copied to valuesOut. Must match UniformInput::getElementCount() - * @param[out] valuesOut location where the values (column-wise) are copied to - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueMatrix22f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const; - - /** - * @brief Sets value of the input. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] values The matrix 3x3 value (column-wise) - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueMatrix33f(const UniformInput& input, const float values[9]); - - /** - * @brief Sets values of the input elements. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] elementCount The number of values that are used from values. Must match UniformInput::getElementCount() - * @param[in] values Pointer the the values (will be stored column-wise) - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueMatrix33f(const UniformInput& input, uint32_t elementCount, const float* values); - - /** - * @brief Gets the value of the input. - * - * @param[in] input The effect uniform input - * @param[out] valueOut The matrix 3x3 value (column-wise) - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueMatrix33f(const UniformInput& input, float valueOut[9]) const; - - /** - * @brief Gets current values of the input. - * - * @param[in] input The effect uniform input - * @param[in] elementCount the number of values that are copied to valuesOut. Must match UniformInput::getElementCount() - * @param[out] valuesOut location where the values (column-wise) are copied to - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueMatrix33f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const; - - /** - * @brief Sets value of the input. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] values The matrix 4x4 value (column-wise) - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueMatrix44f(const UniformInput& input, const float values[16]); - - /** - * @brief Sets values of the input elements. - * - * @param[in] input The effect uniform input to set the value to - * @param[in] elementCount The number of values that are used from values. Must match UniformInput::getElementCount() - * @param[in] values Pointer the the values (will be stored column-wise) - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputValueMatrix44f(const UniformInput& input, uint32_t elementCount, const float* values); - - /** - * @brief Gets the value of the input. - * - * @param[in] input The effect uniform input - * @param[out] valueOut The matrix 4x4 value (column-wise) - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueMatrix44f(const UniformInput& input, float valueOut[16]) const; - - /** - * @brief Gets current values of the input. - * - * @param[in] input The effect uniform input - * @param[in] elementCount the number of values that are copied to valuesOut. Must match UniformInput::getElementCount() - * @param[out] valuesOut location where the values (column-wise) are copied to - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputValueMatrix44f(const UniformInput& input, uint32_t elementCount, float* valuesOut) const; - - /** - * @brief Sets texture sampler to the input - * - * @param[in] input The effect uniform input to set the value to - * @param[in] textureSampler The texture sampler - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputTexture(const UniformInput& input, const TextureSampler& textureSampler); - - /** - * @brief Sets multisampled texture sampler to the input - * - * @param[in] input The multisampled texture sampler uniform input to set the value to - * @param[in] textureSampler The multisampled texture sampler - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputTexture(const UniformInput& input, const TextureSamplerMS& textureSampler); - - /** - * @brief Sets external texture sampler to the input - * - * @param[in] input The external texture sampler uniform input to set the value to - * @param[in] textureSampler The external texture sampler - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setInputTexture(const UniformInput& input, const TextureSamplerExternal& textureSampler); - - /** - * @brief Gets texture sampler currently set to the input - * - * @param[in] input The effect uniform input - * @param[out] textureSampler Will set texture sampler pointer to the TextureSampler object set to the uniform input, - * nullptr if none set or there was an error - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputTexture(const UniformInput& input, const TextureSampler*& textureSampler) const; - - /** - * @brief Gets texture sampler currently set to the input - * - * @param[in] input The effect uniform input - * @param[out] textureSampler Will set texture sampler pointer to the TextureSamplerMS object set to the uniform input, - * nullptr if none set or there was an error - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputTextureMS(const UniformInput& input, const TextureSamplerMS*& textureSampler) const; - - /** - * @brief Gets texture sampler currently set to the input - * - * @param[in] input The effect uniform input - * @param[out] textureSampler Will set texture sampler pointer to the TextureSamplerExternal object set to the uniform input, - * nullptr if none set or there was an error - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInputTextureExternal(const UniformInput& input, const TextureSamplerExternal*& textureSampler) const; - - /** - * @brief Bind a DataObject to the Appearance's uniform input. - * The value from the DataObject will be used and any change made on the DataObject - * will be reflected in the Appearance. One DataObject can be bound to multiple Appearances. - * The data type of the DataObject must match the uniform input data type otherwise - * the call will fail and report error. - * DataObject cannot be bound to an input with semantics, texture input or array input. - * Once a DataObject is bound to an input the value cannot be set or get using \c set/getInputValue*() anymore. - * Binding a DataObject to an already bound input will unbind the old one and bind the new one. - * - * @param[in] input The effect uniform input to bind the DataObject to - * @param[in] dataObject The DataObject to be bound - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t bindInput(const UniformInput& input, const DataObject& dataObject); - - /** - * @brief Unbind a previously bound DataObject from the Appearance's uniform input. - * Any previously set value that was set before binding will now be used. - * Appropriate \c set/getInputValue*() method must be used to set or get the value - * or another DataObject can be bound. - * - * @param[in] input The effect uniform input to unbind the DataObject from - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t unbindInput(const UniformInput& input); - - /** - * @brief Check whether a uniform input has any DataObject bound to it. - * - * @param[in] input The effect uniform input to check - * @return \c true if there is any DataObject bound to the input, false otherwise - */ - bool isInputBound(const UniformInput& input) const; - - /** - * @brief Gets the data object bound to a uniform input. - * - * @param[in] input The effect uniform input to get the bound data object for - * @return \c The data object bound the uniform input if existing, otherwise returns nullptr - */ - const DataObject* getDataObjectBoundToInput(const UniformInput& input) const; - - /** - * @brief Gets the effect used to create this appearance - * - * @return The effect used to create the appearance. - */ - const Effect& getEffect() const; - - protected: - /** - * @brief Scene is the factory for creating Appearance instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of Appearance - * - * @param[in] pimpl Internal data for implementation specifics of Appearance (sink - instance becomes owner) - */ - explicit Appearance(AppearanceImpl& pimpl); - - /** - * @brief Copy constructor of Appearance - * - * @param[in] other Other instance of appearance class - */ - Appearance(const Appearance& other); - - /** - * @brief Assignment operator of Appearance. - * - * @param[in] other Other instance of appearance class - * @return This instance after assignment - */ - Appearance& operator=(const Appearance& other); - - /** - * @brief Destructor of the Appearance - */ - virtual ~Appearance(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/AppearanceEnums.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/AppearanceEnums.h deleted file mode 100644 index ca233f1e0..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/AppearanceEnums.h +++ /dev/null @@ -1,279 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2012 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_APPEARANCEENUMS_H -#define RAMSES_APPEARANCEENUMS_H - -#include "ramses-framework-api/APIExport.h" - -namespace ramses -{ - /** - * Specifies the data type of an input. - */ - enum EInputType - { - EInputType_Invalid = 0, - EInputType_Float, - EInputType_Vector2f, - EInputType_Vector3f, - EInputType_Vector4f, - EInputType_Int16, - EInputType_Int32, - EInputType_UInt16, - EInputType_UInt32, - EInputType_Vector2i, - EInputType_Vector3i, - EInputType_Vector4i, - EInputType_Matrix22f, - EInputType_Matrix23f, - EInputType_Matrix24f, - EInputType_Matrix32f, - EInputType_Matrix33f, - EInputType_Matrix34f, - EInputType_Matrix42f, - EInputType_Matrix43f, - EInputType_Matrix44f, - EInputType_TextureSampler, - - EInputType_AttributeUInt16, - EInputType_AttributeFloat, - EInputType_AttributeVector2f, - EInputType_AttributeVector3f, - EInputType_AttributeVector4f, - EInputType_NUMBER_OF_ELEMENTS - }; - - /** - * Specifies the blending operation. - */ - enum EBlendOperation - { - EBlendOperation_Disabled = 0, - EBlendOperation_Add, - EBlendOperation_Subtract, - EBlendOperation_ReverseSubtract, - EBlendOperation_Min, - EBlendOperation_Max, - EBlendOperation_NUMBER_OF_ELEMENTS - }; - - /** - * Specifies the blending factor used with blending operation. - */ - enum EBlendFactor - { - EBlendFactor_Zero = 0, - EBlendFactor_One, - EBlendFactor_SrcAlpha, - EBlendFactor_OneMinusSrcAlpha, - EBlendFactor_DstAlpha, - EBlendFactor_OneMinusDstAlpha, - EBlendFactor_SrcColor, - EBlendFactor_OneMinusSrcColor, - EBlendFactor_DstColor, - EBlendFactor_OneMinusDstColor, - EBlendFactor_ConstColor, - EBlendFactor_OneMinusConstColor, - EBlendFactor_ConstAlpha, - EBlendFactor_OneMinusConstAlpha, - EBlendFactor_AlphaSaturate, - EBlendFactor_NUMBER_OF_ELEMENTS - }; - - /** - * Specifies the culling mode. - */ - enum ECullMode - { - ECullMode_Disabled = 0, - ECullMode_FrontFacing, - ECullMode_BackFacing, - ECullMode_FrontAndBackFacing, - ECullMode_NUMBER_OF_ELEMENTS - }; - - /** - * Specifies the depth write state. - */ - enum EDepthWrite - { - EDepthWrite_Disabled = 0, - EDepthWrite_Enabled, - EDepthWrite_NUMBER_OF_ELEMENTS - }; - - /** - * Specifies the scissor test state. - */ - enum EScissorTest - { - EScissorTest_Disabled = 0, - EScissorTest_Enabled, - EScissorTest_NUMBER_OF_ELEMENTS - }; - - /** - * Specifies the depth function. - */ - enum EDepthFunc - { - EDepthFunc_Disabled = 0, - EDepthFunc_Greater, - EDepthFunc_GreaterEqual, - EDepthFunc_Less, - EDepthFunc_LessEqual, - EDepthFunc_Equal, - EDepthFunc_NotEqual, - EDepthFunc_Always, - EDepthFunc_Never, - EDepthFunc_NUMBER_OF_ELEMENTS - }; - - /** - * Specifies the stencil function. - */ - enum EStencilFunc - { - EStencilFunc_Disabled = 0, - EStencilFunc_Never, - EStencilFunc_Always, - EStencilFunc_Equal, - EStencilFunc_NotEqual, - EStencilFunc_Less, - EStencilFunc_LessEqual, - EStencilFunc_Greater, - EStencilFunc_GreaterEqual, - EStencilFunc_NUMBER_OF_ELEMENTS - }; - - /** - * Specifies the stencil operation. - */ - enum EStencilOperation - { - EStencilOperation_Keep = 0, - EStencilOperation_Zero, - EStencilOperation_Replace, - EStencilOperation_Increment, - EStencilOperation_IncrementWrap, - EStencilOperation_Decrement, - EStencilOperation_DecrementWrap, - EStencilOperation_Invert, - EStencilOperation_NUMBER_OF_ELEMENTS - }; - - /** - * Specifies the drawing mode. - */ - enum EDrawMode - { - EDrawMode_Points = 0, - EDrawMode_Lines, - EDrawMode_Triangles, - EDrawMode_TriangleStrip, - EDrawMode_LineLoop, - EDrawMode_TriangleFan, - EDrawMode_LineStrip, - EDrawMode_NUMBER_OF_ELEMENTS - }; - - - - /** - * @brief Returns string representation for input type - * @details Useful for logging, etc. - * - * @param inputType The enum parameter for which you will get the string - * @return String representation of the input type - */ - RAMSES_API const char* getInputTypeString(EInputType inputType); - - /** - * @brief Returns string representation for blend operation - * @details Useful for logging, etc. - * - * @param blendOperation The enum parameter for which you will get the string - * @return String representation of the blend operation - */ - RAMSES_API const char* getBlendOperationString(EBlendOperation blendOperation); - - /** - * @brief Returns string representation for blend factor - * @details Useful for logging, etc. - * - * @param blendFactor The enum parameter for which you will get the string - * @return String representation of the blend factor - */ - RAMSES_API const char* getBlendFactorString(EBlendFactor blendFactor); - - /** - * @brief Returns string representation for cull mode - * @details Useful for logging, etc. - * - * @param cullMode The enum parameter for which you will get the string - * @return String representation of the cull mode - */ - RAMSES_API const char* getCullModeString(ECullMode cullMode); - - /** - * @brief Returns string representation for depth write - * @details Useful for logging, etc. - * - * @param depthWrite The enum parameter for which you will get the string - * @return String representation of the depth write - */ - RAMSES_API const char* getDepthWriteString(EDepthWrite depthWrite); - - /** - * @brief Returns string representation for scissor test - * @details Useful for logging, etc. - * - * @param scissorTest The enum parameter for which you will get the string - * @return String representation of the scissor test - */ - RAMSES_API const char* getScissorTestString(EScissorTest scissorTest); - - /** - * @brief Returns string representation for depth function - * @details Useful for logging, etc. - * - * @param depthFunc The enum parameter for which you will get the string - * @return String representation of the depth function - */ - RAMSES_API const char* getDepthFuncString(EDepthFunc depthFunc); - - /** - * @brief Returns string representation for stencil function - * @details Useful for logging, etc. - * - * @param stencilFunc The enum parameter for which you will get the string - * @return String representation of the stencil function - */ - RAMSES_API const char* getStencilFuncString(EStencilFunc stencilFunc); - - /** - * @brief Returns string representation for stencil operation - * @details Useful for logging, etc. - * - * @param stencilOp The enum parameter for which you will get the string - * @return String representation of the stencil operation - */ - RAMSES_API const char* getStencilOperationString(EStencilOperation stencilOp); - - /** - * @brief Returns string representation for draw mode - * @details Useful for logging, etc. - * - * @param drawMode The enum parameter for which you will get the string - * @return String representation of the draw mode - */ - RAMSES_API const char* getDrawModeString(EDrawMode drawMode); -} - -#endif //RAMSES_APPEARANCEENUMS_H diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/AttributeInput.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/AttributeInput.h deleted file mode 100644 index baee254a1..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/AttributeInput.h +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_ATTRIBUTEINPUT_H -#define RAMSES_ATTRIBUTEINPUT_H - -#include "ramses-client-api/EffectInput.h" -#include "ramses-client-api/EffectInputDataType.h" -#include "ramses-client-api/EffectInputSemantic.h" - -namespace ramses -{ - /** - * @brief The AttributeInput is a description of an attribute effect input - */ - class RAMSES_API AttributeInput : public EffectInput - { - public: - /** - * @brief Constructor of AttributeInput. - */ - AttributeInput(); - - /** - * @brief Returns the effect input data type. - * - * @return Effect input data type - */ - EEffectInputDataType getDataType() const; - - /** - * @brief Returns the effect input semantics. - * - * @return Effect input semantics - */ - EEffectAttributeSemantic getSemantics() const; - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataFloat.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataFloat.h deleted file mode 100644 index 93cc374bb..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataFloat.h +++ /dev/null @@ -1,62 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAFLOAT_H -#define RAMSES_DATAFLOAT_H - -#include "ramses-client-api/DataObject.h" - -namespace ramses -{ - class DataObjectImpl; - - /** - * @brief The DataFloat data object stores a float value within a scene. - */ - class RAMSES_API DataFloat : public DataObject - { - public: - /** - * @brief Sets/updates the stored value. - * - * @param[in] value new value. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setValue(float value); - - /** - * @brief Gets the stored value. - * - * @param[out] value stored value. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getValue(float& value) const; - - protected: - /** - * @brief Scene is the factory for creating DataFloat instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of DataFloat - * - * @param[in] pimpl Internal data for implementation specifics of a DataObject (sink - instance becomes owner) - */ - explicit DataFloat(DataObjectImpl& pimpl); - - /** - * @brief Destructor of the DataFloat - */ - virtual ~DataFloat(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataInt32.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataInt32.h deleted file mode 100644 index f16cf1bbb..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataInt32.h +++ /dev/null @@ -1,62 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAINT32_H -#define RAMSES_DATAINT32_H - -#include "ramses-client-api/DataObject.h" - -namespace ramses -{ - class DataObjectImpl; - - /** - * @brief The DataInt32 data object stores an 32-bit integer value within a scene. - */ - class RAMSES_API DataInt32 : public DataObject - { - public: - /** - * @brief Sets/updates the stored value. - * - * @param[in] value new value. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setValue(int32_t value); - - /** - * @brief Gets the stored value. - * - * @param[out] value stored value. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getValue(int32_t& value) const; - - protected: - /** - * @brief Scene is the factory for creating DataInt32 instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of DataInt32 - * - * @param[in] pimpl Internal data for implementation specifics of a DataObject (sink - instance becomes owner) - */ - explicit DataInt32(DataObjectImpl& pimpl); - - /** - * @brief Destructor of the DataInt32 - */ - virtual ~DataInt32(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix22f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix22f.h deleted file mode 100644 index 60829263a..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix22f.h +++ /dev/null @@ -1,62 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAMATRIX22F_H -#define RAMSES_DATAMATRIX22F_H - -#include "ramses-client-api/DataObject.h" - -namespace ramses -{ - class DataObjectImpl; - - /** - * @brief The DataMatrix22f data object stores a matrix with 4 float components (2 rows, 2 columns) within a scene. - */ - class RAMSES_API DataMatrix22f : public DataObject - { - public: - /** - * @brief Sets/updates the stored values of the matrix. - * - * @param[in] matrixElements new matrix values (row-wise). - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setValue(const float(&matrixElements)[4]); - - /** - * @brief Gets all stored values of the matrix. - * - * @param[out] matrixElements of the matrix (row-wise). - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getValue(float(&matrixElements)[4]) const; - - protected: - /** - * @brief Scene is the factory for creating DataMatrix22f instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of DataMatrix22f - * - * @param[in] pimpl Internal data for implementation specifics of a DataObject (sink - instance becomes owner) - */ - explicit DataMatrix22f(DataObjectImpl& pimpl); - - /** - * @brief Destructor of the UniformDataVector4f - */ - virtual ~DataMatrix22f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix33f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix33f.h deleted file mode 100644 index c112cff8e..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix33f.h +++ /dev/null @@ -1,62 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAMATRIX33F_H -#define RAMSES_DATAMATRIX33F_H - -#include "ramses-client-api/DataObject.h" - -namespace ramses -{ - class DataObjectImpl; - - /** - * @brief The DataMatrix33f data object stores a matrix with 9 float components (3 rows, 3 columns) within a scene. - */ - class RAMSES_API DataMatrix33f : public DataObject - { - public: - /** - * @brief Sets/updates the stored values of the matrix. - * - * @param[in] matrixElements new matrix values (row-wise). - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setValue(const float(&matrixElements)[9]); - - /** - * @brief Gets all stored values of the matrix. - * - * @param[out] matrixElements of the matrix (row-wise). - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getValue(float(&matrixElements)[9]) const; - - protected: - /** - * @brief Scene is the factory for creating DataMatrix33f instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of DataMatrix33f - * - * @param[in] pimpl Internal data for implementation specifics of a DataObject (sink - instance becomes owner) - */ - explicit DataMatrix33f(DataObjectImpl& pimpl); - - /** - * @brief Destructor of the UniformDataVector4f - */ - virtual ~DataMatrix33f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix44f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix44f.h deleted file mode 100644 index 6e9de4dd5..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataMatrix44f.h +++ /dev/null @@ -1,62 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAMATRIX44F_H -#define RAMSES_DATAMATRIX44F_H - -#include "ramses-client-api/DataObject.h" - -namespace ramses -{ - class DataObjectImpl; - - /** - * @brief The DataMatrix44f data object stores a matrix with 16 float components (4 rows, 4 columns) within a scene. - */ - class RAMSES_API DataMatrix44f : public DataObject - { - public: - /** - * @brief Sets/updates the stored values of the matrix. - * - * @param[in] matrixElements new matrix values (row-wise). - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setValue(const float(&matrixElements)[16]); - - /** - * @brief Gets all stored values of the matrix. - * - * @param[out] matrixElements of the matrix (row-wise). - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getValue(float(&matrixElements)[16]) const; - - protected: - /** - * @brief Scene is the factory for creating DataMatrix44f instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of DataMatrix44f - * - * @param[in] pimpl Internal data for implementation specifics of a DataObject (sink - instance becomes owner) - */ - explicit DataMatrix44f(DataObjectImpl& pimpl); - - /** - * @brief Destructor of the UniformDataVector4f - */ - virtual ~DataMatrix44f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataObject.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataObject.h deleted file mode 100644 index 49552e4aa..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataObject.h +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAOBJECT_H -#define RAMSES_DATAOBJECT_H - -#include "ramses-client-api/SceneObject.h" - -namespace ramses -{ - /** - * @brief The DataObject is a base class for data container for storing data in a scene. - * @details A concretely typed data object (see derived classes) can be bound to some inputs - * of some object types (e.g. #ramses::Appearance::bindInput or #ramses::Camera::bindViewportOffset). - * When a data object is bound to an input the data object value overrides whatever was previously - * set to that input using its direct setter. - * A single #DataObject can be bound to multiple inputs (also to other #ramses::RamsesObject types - * where applicable) providing a way to distribute value across instances/inputs. - * Using #DataObject also allows use of data linking across scenes between data object provider and consumer - * (#ramses::Scene::createDataProvider and #ramses::Scene::createDataConsumer) see SDK examples for typical use cases. - * A value set to a #DataObject is then propagated everywhere it is bound to and it is linked to. - */ - class RAMSES_API DataObject : public SceneObject - { - public: - /** - * Stores internal data for implementation specifics - */ - class DataObjectImpl& impl; - - protected: - /** - * @brief Scene is the factory for creating DataObject instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of DataObject - * - * @param[in] pimpl Internal data for implementation specifics of DataObject - */ - explicit DataObject(DataObjectImpl& pimpl); - - /** - * @brief Destructor for a DataObject - */ - virtual ~DataObject(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector2f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector2f.h deleted file mode 100644 index 766207367..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector2f.h +++ /dev/null @@ -1,64 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAVECTOR2F_H -#define RAMSES_DATAVECTOR2F_H - -#include "ramses-client-api/DataObject.h" - -namespace ramses -{ - class DataObjectImpl; - - /** - * @brief The DataVector2f data object stores a vector with 2 float components within a scene. - */ - class RAMSES_API DataVector2f : public DataObject - { - public: - /** - * @brief Sets/updates the stored values of the vector. - * - * @param[in] x new value for the first component of the vector. - * @param[in] y new value for the second component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setValue(float x, float y); - - /** - * @brief Gets all stored values of the vector. - * - * @param[out] x value of the first component of the vector. - * @param[out] y value of the second component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getValue(float& x, float& y) const; - - protected: - /** - * @brief Scene is the factory for creating DataVector2f instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of DataVector2f - * - * @param[in] pimpl Internal data for implementation specifics of a DataObject (sink - instance becomes owner) - */ - explicit DataVector2f(DataObjectImpl& pimpl); - - /** - * @brief Destructor of the DataVector2f - */ - virtual ~DataVector2f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector2i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector2i.h deleted file mode 100644 index e92248cf1..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector2i.h +++ /dev/null @@ -1,64 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAVECTOR2I_H -#define RAMSES_DATAVECTOR2I_H - -#include "ramses-client-api/DataObject.h" - -namespace ramses -{ - class DataObjectImpl; - - /** - * @brief The DataVector2i data object stores a vector with two 32-bit-integer components within a scene. - */ - class RAMSES_API DataVector2i : public DataObject - { - public: - /** - * @brief Sets/updates the stored values of the vector. - * - * @param[in] x new value for the first component of the vector. - * @param[in] y new value for the second component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setValue(int32_t x, int32_t y); - - /** - * @brief Gets all stored values of the vector. - * - * @param[out] x value of the first component of the vector. - * @param[out] y value of the second component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getValue(int32_t& x, int32_t& y) const; - - protected: - /** - * @brief Scene is the factory for creating DataVector2i instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of DataVector2i - * - * @param[in] pimpl Internal data for implementation specifics of a DataObject (sink - instance becomes owner) - */ - explicit DataVector2i(DataObjectImpl& pimpl); - - /** - * @brief Destructor of the DataVector2i - */ - virtual ~DataVector2i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector3f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector3f.h deleted file mode 100644 index 05c37c6b4..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector3f.h +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAVECTOR3F_H -#define RAMSES_DATAVECTOR3F_H - -#include "ramses-client-api/DataObject.h" - -namespace ramses -{ - class DataObjectImpl; - - /** - * @brief The DataVector3f data object stores a vector with 3 float components within a scene. - */ - class RAMSES_API DataVector3f : public DataObject - { - public: - /** - * @brief Sets/updates the stored values of the vector. - * - * @param[in] x new value for the first component of the vector. - * @param[in] y new value for the second component of the vector. - * @param[in] z new value for the third component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setValue(float x, float y, float z); - - /** - * @brief Gets all stored values of the vector. - * - * @param[out] x value of the first component of the vector. - * @param[out] y value of the second component of the vector. - * @param[out] z value of the third component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getValue(float& x, float& y, float& z) const; - - protected: - /** - * @brief Scene is the factory for creating DataVector3f instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of UniformVector3f - * - * @param[in] pimpl Internal data for implementation specifics of a DataObject (sink - instance becomes owner) - */ - explicit DataVector3f(DataObjectImpl& pimpl); - - /** - * @brief Destructor of the DataVector3f - */ - virtual ~DataVector3f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector3i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector3i.h deleted file mode 100644 index c791f2887..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector3i.h +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAVECTOR3I_H -#define RAMSES_DATAVECTOR3I_H - -#include "ramses-client-api/DataObject.h" - -namespace ramses -{ - class DataObjectImpl; - - /** - * @brief The DataVector3i data object stores a vector with 3 integer components within a scene. - */ - class RAMSES_API DataVector3i : public DataObject - { - public: - /** - * @brief Sets/updates the stored values of the vector. - * - * @param[in] x new value for the first component of the vector. - * @param[in] y new value for the second component of the vector. - * @param[in] z new value for the third component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setValue(int32_t x, int32_t y, int32_t z); - - /** - * @brief Gets all stored values of the vector. - * - * @param[out] x value of the first component of the vector. - * @param[out] y value of the second component of the vector. - * @param[out] z value of the third component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getValue(int32_t& x, int32_t& y, int32_t& z) const; - - protected: - /** - * @brief Scene is the factory for creating DataVector3i instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of DataVector3i - * - * @param[in] pimpl Internal data for implementation specifics of a DataObject (sink - instance becomes owner) - */ - explicit DataVector3i(DataObjectImpl& pimpl); - - /** - * @brief Destructor of the DataVector3i - */ - virtual ~DataVector3i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector4f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector4f.h deleted file mode 100644 index e2dd90777..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector4f.h +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAVECTOR4F_H -#define RAMSES_DATAVECTOR4F_H - -#include "ramses-client-api/DataObject.h" - -namespace ramses -{ - class DataObjectImpl; - - /** - * @brief The DataVector4f data object stores a vector with 4 float components within a scene. - */ - class RAMSES_API DataVector4f : public DataObject - { - public: - /** - * @brief Sets/updates the stored values of the vector. - * - * @param[in] x new value for the first component of the vector. - * @param[in] y new value for the second component of the vector. - * @param[in] z new value for the third component of the vector. - * @param[in] w new value for the fourth component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setValue(float x, float y, float z, float w); - - /** - * @brief Gets all stored values of the vector. - * - * @param[out] x value of the first component of the vector. - * @param[out] y value of the second component of the vector. - * @param[out] z value of the third component of the vector. - * @param[out] w value of the fourth component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getValue(float& x, float& y, float& z, float& w) const; - - protected: - /** - * @brief Scene is the factory for creating DataVector4f instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of DataVector4f - * - * @param[in] pimpl Internal data for implementation specifics of a DataObject (sink - instance becomes owner) - */ - explicit DataVector4f(DataObjectImpl& pimpl); - - /** - * @brief Destructor of the DataVector4f - */ - virtual ~DataVector4f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector4i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector4i.h deleted file mode 100644 index df972e9dc..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/DataVector4i.h +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_DATAVECTOR4I_H -#define RAMSES_DATAVECTOR4I_H - -#include "ramses-client-api/DataObject.h" - -namespace ramses -{ - class DataObjectImpl; - - /** - * @brief The DataVector4i data object stores a vector with 4 integer components within a scene. - */ - class RAMSES_API DataVector4i : public DataObject - { - public: - /** - * @brief Sets/updates the stored values of the vector. - * - * @param[in] x new value for the first component of the vector. - * @param[in] y new value for the second component of the vector. - * @param[in] z new value for the third component of the vector. - * @param[in] w new value for the fourth component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setValue(int32_t x, int32_t y, int32_t z, int32_t w); - - /** - * @brief Gets all stored values of the vector. - * - * @param[out] x value of the first component of the vector. - * @param[out] y value of the second component of the vector. - * @param[out] z value of the third component of the vector. - * @param[out] w value of the fourth component of the vector. - * @return status == 0 for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getValue(int32_t& x, int32_t& y, int32_t& z, int32_t& w) const; - - protected: - /** - * @brief Scene is the factory for creating DataVector4i instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of DataVector4i - * - * @param[in] pimpl Internal data for implementation specifics of a DataObject (sink - instance becomes owner) - */ - explicit DataVector4i(DataObjectImpl& pimpl); - - /** - * @brief Destructor of the DataVector4i - */ - virtual ~DataVector4i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/EDataType.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/EDataType.h deleted file mode 100644 index 886aa4f6b..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/EDataType.h +++ /dev/null @@ -1,78 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2017 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_EDATATYPE_H -#define RAMSES_EDATATYPE_H -#include // for integer datatypes -#include // for size_t - -namespace ramses -{ - /** - * @brief Specifies the data type used for creating data buffers - */ - enum class EDataType - { - UInt16 = 0, ///< one component of type uint16_t per data element - UInt32, ///< one component of type uint32_t per data element - Float, ///< one component of type float per data element - Vector2F, ///< two components of type float per data element - Vector3F, ///< three components of type float per data element - Vector4F, ///< four components of type float per data element - ByteBlob, ///< array of raw bytes which gets typed later (e.g. interleaved vertex buffer) where one element is always sized as 1 byte - }; - - /** - * @brief Retrieve number of components per element of specified data type. - * - * @param[in] dataType Data type to be queried - * @return number of components per element - */ - constexpr size_t GetNumberOfComponents(EDataType dataType) - { - return (dataType == EDataType::UInt16) ? 1u : - (dataType == EDataType::UInt32) ? 1u : - (dataType == EDataType::Float) ? 1u : - (dataType == EDataType::Vector2F) ? 2u : - (dataType == EDataType::Vector3F) ? 3u : - (dataType == EDataType::Vector4F) ? 4u : - (dataType == EDataType::ByteBlob) ? 1u : 0u; - } - - /** - * @brief Retrieve size of one component of specified data type in bytes. - * - * @details For #EDataType::ByteBlob element size is defined to be 1 byte. - * - * @param[in] dataType Data type to be queried - * @return Size of a component of the data type in bytes - */ - constexpr size_t GetSizeOfComponent(EDataType dataType) - { - return (dataType == EDataType::Float) ? sizeof(float) : - (dataType == EDataType::UInt16) ? sizeof(uint16_t) : - (dataType == EDataType::UInt32) ? sizeof(uint32_t) : - (dataType == EDataType::Vector2F) ? sizeof(float) : - (dataType == EDataType::Vector3F) ? sizeof(float) : - (dataType == EDataType::Vector4F) ? sizeof(float) : - (dataType == EDataType::ByteBlob) ? 1u : 0u; - } - - /** - * @brief Retrieve size of one element of specified data type in bytes. - * - * @param[in] dataType Data type to be queried - * @return Size of one element of the data type in bytes - */ - constexpr size_t GetSizeOfDataType(EDataType dataType) - { - return GetNumberOfComponents(dataType) * GetSizeOfComponent(dataType); - } -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/ERotationConvention.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/ERotationConvention.h deleted file mode 100644 index 7486055c2..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/ERotationConvention.h +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_EROTATIONCONVENTION_H -#define RAMSES_EROTATIONCONVENTION_H - -namespace ramses -{ - /** - * Specifies the rotation convention used in calculation of transforms in a right-handed coordinate system. The - * order of the letters in each enum value represents the position of the corresponding rotation matrix - * in the multiplication chain. For example XYZ means the rotation matrix will be of the shape Rx * Ry * Rz * v - * where v is a vector or another matrix and R are right-handed rotation matrices which rotate around an - * axis specified by the rotation order. Check the specific enum documentation for the exact rotation order - * in terms of 'which rotation is applied first'. - */ - enum class ERotationConvention - { - XYZ, ///< rotates around Z then Y then X axis - XZY, ///< rotates around Y then Z then X axis - YXZ, ///< rotates around Z then X then Y axis - YZX, ///< rotates around X then Z then Y axis - ZXY, ///< rotates around Y then X then Z axis - ZYX, ///< rotates around X then Y then Z axis - XYX, ///< rotates around X then Y then X axis - XZX, ///< rotates around X then Z then X axis - YXY, ///< rotates around Y then X then Y axis - YZY, ///< rotates around Y then Z then Y axis - ZXZ, ///< rotates around Z then X then Z axis - ZYZ, ///< rotates around Z then Y then Z axis - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/EffectInputDataType.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/EffectInputDataType.h deleted file mode 100644 index 9958a9903..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/EffectInputDataType.h +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_EFFECTINPUTDATATYPE_H -#define RAMSES_EFFECTINPUTDATATYPE_H - -namespace ramses -{ - /** - * @brief Data type of effect input - */ - enum EEffectInputDataType - { - EEffectInputDataType_Invalid = 0, ///< Invalid or Unknown data type - EEffectInputDataType_Int32, ///< Integer 32bit data type - EEffectInputDataType_UInt16, ///< Unsigned integer 16bit data type - EEffectInputDataType_UInt32, ///< Unsigned integer 32bit data type - EEffectInputDataType_Float, ///< Float data type - EEffectInputDataType_Vector2F, ///< Vector2 float data type - EEffectInputDataType_Vector3F, ///< Vector3 float data type - EEffectInputDataType_Vector4F, ///< Vector4 float data type - EEffectInputDataType_Vector2I, ///< Vector2 integer data type - EEffectInputDataType_Vector3I, ///< Vector3 integer data type - EEffectInputDataType_Vector4I, ///< Vector4 integer data type - EEffectInputDataType_Matrix22F, ///< Matrix2x2 data type - EEffectInputDataType_Matrix33F, ///< Matrix3x3 data type - EEffectInputDataType_Matrix44F, ///< Matrix4x4 data type - EEffectInputDataType_TextureSampler2D, ///< 2D Texture sampler data type - EEffectInputDataType_TextureSampler2DMS, ///< 2D Texture sampler multi sampled data type - EEffectInputDataType_TextureSampler3D, ///< 3D Texture sampler data type - EEffectInputDataType_TextureSamplerCube, ///< Cube Texture sampler data type - EEffectInputDataType_TextureSamplerExternal, ///< External Texture sampler data type - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/Node.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/Node.h deleted file mode 100644 index 96fee854b..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/Node.h +++ /dev/null @@ -1,355 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_NODE_H -#define RAMSES_NODE_H - -#include "ramses-client-api/SceneObject.h" -#include "ramses-client-api/EVisibilityMode.h" -#include "ramses-client-api/ERotationConvention.h" - -namespace ramses -{ - /** - * @brief The Node is the base class of all nodes and provides - * scene graph functionality which propagates to its children. - */ - class RAMSES_API Node : public SceneObject - { - public: - /** - * @brief Returns if node has at least one child Node. - * - * @return true if this Node has at least one child Node, false otherwise. - */ - bool hasChild() const; - - /** - * @brief Gets the number of child Nodes of this node. - * - * @return Number of child Nodes of this Node. - */ - uint32_t getChildCount() const; - - /** - * @brief Gets child node at provided index. - * - * @param[in] index Index of the child Node to be returned. Index range is [0..getChildCount()-1]. - * - * @return Pointer to Node, if child at index exists. - * @return nullptr if child at index does not exist. - */ - Node* getChild(uint32_t index); - /** @copydoc getChild(uint32_t) */ - const Node* getChild(uint32_t index) const; - - /** - * @brief Adds child Node to this node. - * - * @param[in] node Node to be set as child for this Node. - * - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t addChild(Node& node); - - /** - * @brief Removes a child Node from this node. - * - * @param[in] node Node to be removed as child from this Node. - * - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t removeChild(Node& node); - - /** - * @brief Removes all child Nodes from this node. - * - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t removeAllChildren(); - - /** - * @brief Returns if Node has a parent Node. - * - * @return true if Node has a parent Node, false otherwise. - */ - bool hasParent() const; - - /** - * @brief Gets parent Node of this Node. - * - * @return Pointer to parent Node, if parent exists, nullptr otherwise. - */ - Node* getParent(); - /** @copydoc getParent() */ - const Node* getParent() const; - - /** - * @brief Sets parent Node for this node. - * - * @param[in] node Node to be set as parent for this Node. - * - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setParent(Node& node); - - /** - * @brief Removes the parent Node from this Node. - * - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t removeParent(); - - /** - * @brief Gets model (world in scene space) matrix computed from the scene graph. - * Performance note: this call will cause computation of a transformation chain, - * however the result is cached therefore subsequent calls will have little to none - * performance overhead as long as topology or transformation in the chain does not change. - * - * @param[out] modelMatrix Will be filled with the model matrix 4x4 column-major - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getModelMatrix(float(&modelMatrix)[16]) const; - - /** - * @brief Gets inverse model (world in scene space) matrix computed from the scene graph. - * Performance note: this call will cause computation of a transformation chain, - * however the result is cached therefore subsequent calls will have little to none - * performance overhead as long as topology or transformation in the chain does not change. - * - * @param[out] inverseModelMatrix Will be filled with the inverse model matrix 4x4 column-major - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getInverseModelMatrix(float(&inverseModelMatrix)[16]) const; - - /** - * @brief Rotates in all three directions with the given values without affecting - * the currently set rotation convention. - * - * This function can be used only if #ramses::Node::setRotation(float,float,float) is used to set - * node rotation, which implicitly uses left-handed Euler ZYX rotation convention, otherwise it will fail. - * - * @deprecated This function is deprecated and will be removed in one of the next - * major releases. It is encouraged if possible to already migrate to using - * #ramses::Node::setRotation(float,float,float,ERotationConvention) and #ramses::Node::getRotation(float&,float&,float&,ERotationConvention&)const. - * - * @param[in] x Value in degrees which is added to the current rotation around x-axis - * @param[in] y Value in degrees which is added to the current rotation around y-axis - * @param[in] z Value in degrees which is added to the current rotation around z-axis - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t rotate(float x, float y, float z); - - /** - * @brief Sets the absolute rotation in all three directions for left-handed rotation - * using Euler ZYX rotation convention. If this function is used to set, then only - * #ramses::Node::getRotation(float&,float&,float&)const can be used to get node rotation, which - * implicitly uses left-handed Euler ZYX rotation convention. - * - * @deprecated This function is deprecated and will be removed in one of the next - * major releases. It is encouraged if possible to already migrate to using - * #ramses::Node::setRotation(float,float,float,ERotationConvention) and #ramses::Node::getRotation(float&,float&,float&,ERotationConvention&)const. - * If this function is used it is strongly advised to use it for node rotation in the whole scene, i.e., to completely avoid using set/get rotation - * with custom convention within same scene. - * - * @param[in] x The value in degrees for the rotation around x-axis. - * @param[in] y The value in degrees for the rotation around y-axis. - * @param[in] z The value in degrees for the rotation around z-axis. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setRotation(float x, float y, float z); - - /** - * @brief Sets the absolute rotation in all three directions for right-handed rotation using the chosen Euler - * angles rotation convention. If this function is used to set, then only - * #ramses::Node::getRotation(float&,float&,float&,ERotationConvention&)const can be used to get node rotation. - * - * It is possible to use different rotation conventions on different nodes within the same scene. However, it is strongly - * advised not to mix this function with set/get rotation with legacy convention within same scene. - * - * @param[in] x The value in degrees for the rotation around x-axis in case of Tait-Bryan conventions, for Proper Euler conventions specifies - the rotation angle for the first angle in the convention name. - * @param[in] y The value in degrees for the rotation around y-axis in case of Tait-Bryan conventions, for Proper Euler conventions specifies - the rotation angle for the second angle in the convention name. - * @param[in] z The value in degrees for the rotation around z-axis in case of Tait-Bryan conventions, for Proper Euler conventions specifies - the rotation angle for the third angle in the convention name. - * @param[in] rotationConvention The rotation convention to use for calculation of rotation matrix. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setRotation(float x, float y, float z, ERotationConvention rotationConvention); - - /** - * @brief Retrieves the absolute rotation for left-handed rotation using - * Euler ZYX rotation convention. - * - * This function can be used only if #ramses::Node::setRotation(float,float,float) is used to set - * node rotation, which implicitly uses left-handed Euler ZYX rotation convention, otherwise it will fail. - * - * Default value is 0 for all components. - * - * @deprecated This function is deprecated and will be removed in one of the next - * major releases. It is encouraged if possible to already migrate to using - * #ramses::Node::setRotation(float,float,float,ERotationConvention) and #ramses::Node::getRotation(float&,float&,float&,ERotationConvention&)const. - * If this function is used it is strongly advised to use it for node rotation in the whole scene, i.e., to completely avoid using set/get rotation - * with custom convention. - * - * @param[out] x Current value in degrees on x-axis. - * @param[out] y Current value in degrees on y-axis. - * @param[out] z Current value in degrees on z-axis. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getRotation(float& x, float& y, float& z) const; - - /** - * @brief Retrieves the absolute rotation for right-handed rotation in all three directions and the used Euler - * angles rotation convention. This function can be used only - * if #ramses::Node::setRotation(float,float,float,ERotationConvention) is used to set node rotation. - * - * Default value is 0 for all components and ERotationConvention::XYZ. - * - * It is possible to use different rotation conventions on different nodes within the same scene. However, it is strongly - * advised not to mix this function with set/get rotation with legacy convention within same scene. - * - * @param[out] x Current value in degrees for rotation around x-axis in case of Tait-Bryan conventions, in case of Proper Euler conventions gets - the rotation angle for the first angle in the convention name. - * @param[out] y Current value in degrees for rotation around y-axis in case of Tait-Bryan conventions, in case of Proper Euler conventions gets - the rotation angle for the second angle in the convention name. - * @param[out] z Current value in degrees for rotation around z-axis in case of Tait-Bryan conventions, in case of Proper Euler conventions gets - the rotation angle for the third angle in the convention name. - * @param[out] rotationConvention The rotation convention to use for calculation of rotation matrix. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getRotation(float& x, float& y, float& z, ERotationConvention& rotationConvention) const; - - /** - * @brief Translates in all three directions with the given values. - * - * @param[in] x Float with relative translation from origin on x-axis. - * @param[in] y Float with relative translation from origin on y-axis. - * @param[in] z Float with relative translation from origin on z-axis. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t translate(float x, float y, float z); - - /** - * @brief Sets the absolute translation the absolute values. - * - * @param[in] x Float with absolute translation value in x-axis. - * @param[in] y Float with absolute translation value in y-axis. - * @param[in] z Float with absolute translation value in z-axis. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setTranslation(float x, float y, float z); - - /** - * @brief Retrieves the current absolute translation. - * - * @param[out] x Current translation on x-axis. - * @param[out] y Current translation on y-axis. - * @param[out] z Current translation on z-axis. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getTranslation(float& x, float& y, float& z) const; - - /** - * @brief Scales in all three directions with the given values. - * - * @param[in] x The relative scaling factor which in x-dimension. - * @param[in] y The relative scaling factor which in y-dimension. - * @param[in] z The relative scaling factor which in z-dimension. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t scale(float x, float y, float z); - - /** - * @brief Sets the absolute scale in all three dimensions. - * - * @param[in] x The scaling factor in x-dimension. - * @param[in] y The scaling factor in y-dimension. - * @param[in] z The scaling factor in z-dimension. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setScaling(float x, float y, float z); - - /** - * @brief Retrieves the current absolute scale in all three dimensions. - * - * @param[out] x The current scaling in x-dimension. - * @param[out] y The current scaling in y-dimension. - * @param[out] z The current scaling in z-dimension. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getScaling(float& x, float& y, float& z) const; - - /** - * @brief Sets the visibility of the Node. - * Visibility of a node determines if a renderable is rendered or not and if - * its resources are loaded. See \ref EVisibilityMode for more details. - * Those attributes are propagated down to the node's children recursively. A node - * can only be rendered, if none of its parents are Invisible or Off, the node's - * resources are only loaded if none of its parents are Off. - * - * @param[in] mode Visibility mode for this node (default is Visible) - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setVisibility(EVisibilityMode mode); - - /** - * @brief Gets the visibility property of the Node. This is just the visibility - * state of this node object, NOT the hierarchically accumulated visibility of its parents. - * - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - EVisibilityMode getVisibility() const; - - /** - * Stores internal data for implementation specifics of Node. - */ - class NodeImpl& impl; - - protected: - /** - * @brief Scene is the factory for creating Node instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor for Node. - * - * @param[in] pimpl Internal data for implementation specifics of Node (sink - instance becomes owner) - */ - explicit Node(NodeImpl& pimpl); - - /** - * @brief Destructor of the Node - */ - virtual ~Node(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/RamsesObjectTypes.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/RamsesObjectTypes.h deleted file mode 100644 index 8313f6b8a..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/RamsesObjectTypes.h +++ /dev/null @@ -1,104 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_RAMSESOBJECTTYPES_H -#define RAMSES_RAMSESOBJECTTYPES_H - -#include "ramses-framework-api/RamsesFrameworkTypes.h" - -namespace ramses -{ - /// RamsesObject type ID - enum ERamsesObjectType - { - ERamsesObjectType_Invalid = 0, - ERamsesObjectType_ClientObject, // base type - ERamsesObjectType_RamsesObject, // base type - ERamsesObjectType_SceneObject, // base type - ERamsesObjectType_AnimationObject, // base type - ERamsesObjectType_Client, - ERamsesObjectType_Scene, - ERamsesObjectType_AnimationSystem, // base type and concrete type - ERamsesObjectType_AnimationSystemRealTime, - ERamsesObjectType_Node, // base type - ERamsesObjectType_MeshNode, - ERamsesObjectType_Camera, - ERamsesObjectType_PerspectiveCamera, - ERamsesObjectType_OrthographicCamera, - ERamsesObjectType_Effect, - ERamsesObjectType_AnimatedProperty, - ERamsesObjectType_Animation, - ERamsesObjectType_AnimationSequence, - ERamsesObjectType_Appearance, - ERamsesObjectType_GeometryBinding, - ERamsesObjectType_PickableObject, - ERamsesObjectType_Spline, // base type - ERamsesObjectType_SplineStepBool, - ERamsesObjectType_SplineStepFloat, - ERamsesObjectType_SplineStepInt32, - ERamsesObjectType_SplineStepVector2f, - ERamsesObjectType_SplineStepVector3f, - ERamsesObjectType_SplineStepVector4f, - ERamsesObjectType_SplineStepVector2i, - ERamsesObjectType_SplineStepVector3i, - ERamsesObjectType_SplineStepVector4i, - ERamsesObjectType_SplineLinearFloat, - ERamsesObjectType_SplineLinearInt32, - ERamsesObjectType_SplineLinearVector2f, - ERamsesObjectType_SplineLinearVector3f, - ERamsesObjectType_SplineLinearVector4f, - ERamsesObjectType_SplineLinearVector2i, - ERamsesObjectType_SplineLinearVector3i, - ERamsesObjectType_SplineLinearVector4i, - ERamsesObjectType_SplineBezierFloat, - ERamsesObjectType_SplineBezierInt32, - ERamsesObjectType_SplineBezierVector2f, - ERamsesObjectType_SplineBezierVector3f, - ERamsesObjectType_SplineBezierVector4f, - ERamsesObjectType_SplineBezierVector2i, - ERamsesObjectType_SplineBezierVector3i, - ERamsesObjectType_SplineBezierVector4i, - ERamsesObjectType_Resource, // base type - ERamsesObjectType_Texture2D, - ERamsesObjectType_Texture3D, - ERamsesObjectType_TextureCube, - ERamsesObjectType_ArrayResource, - ERamsesObjectType_RenderGroup, - ERamsesObjectType_RenderPass, - ERamsesObjectType_BlitPass, - ERamsesObjectType_TextureSampler, - ERamsesObjectType_TextureSamplerMS, - ERamsesObjectType_RenderBuffer, - ERamsesObjectType_RenderTarget, - ERamsesObjectType_DataBufferObject, - ERamsesObjectType_Texture2DBuffer, - ERamsesObjectType_DataObject, // base type - ERamsesObjectType_DataFloat, - ERamsesObjectType_DataVector2f, - ERamsesObjectType_DataVector3f, - ERamsesObjectType_DataVector4f, - ERamsesObjectType_DataMatrix22f, - ERamsesObjectType_DataMatrix33f, - ERamsesObjectType_DataMatrix44f, - ERamsesObjectType_DataInt32, - ERamsesObjectType_DataVector2i, - ERamsesObjectType_DataVector3i, - ERamsesObjectType_DataVector4i, - ERamsesObjectType_StreamTexture, - ERamsesObjectType_SceneReference, - - ERamsesObjectType_TextureSamplerExternal, - // Whenever new type of object is added - // its traits must be registered in RamsesObjectTypeTraits.h using helper macros - // and added to appropriate test type list(s) in RamsesObjectTestTypes.h - // and added a conversion template instantiation in RamsesObjectTypeUtils.cpp - ERamsesObjectType_NUMBER_OF_TYPES - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/ResourceDataPool.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/ResourceDataPool.h deleted file mode 100644 index a38d3c6ec..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/ResourceDataPool.h +++ /dev/null @@ -1,266 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_RESOURCEDATAPOOL_H -#define RAMSES_RESOURCEDATAPOOL_H - -#include "ramses-client-api/EDataType.h" -#include "ramses-client-api/MipLevelData.h" -#include "ramses-client-api/TextureEnums.h" -#include "ramses-client-api/TextureSwizzle.h" - -#include "ramses-framework-api/RamsesFrameworkTypes.h" - -#include - -namespace ramses -{ - class EffectDescription; - class Resource; - class Scene; - class ResourceDataPoolImpl; - - /** - * @brief The ResourceDataPool holds resource data which can be instantiated for a given scene. Resource data can either be - * added by calling the add functions or by attaching a resource data file to the pool. The same resource data can be - * instantiated by multiple scenes at the same time. - * - * @deprecated This class was being introduced to cover legacy ramses use cases. Using this class is discouraged - * and it might be removed without warning in the future. - */ - class RAMSES_API ResourceDataPool - { - public: - /** - * @brief Add ArrayResource data to the pool. The pool is taking ownership of the given range of data of a certain type and keeps it - * to instantiate resource from it later via createResourceForScene. See #ramses::ArrayResource for more details. - * Readding the same resource data again will return the previous resource id, but not recreate the resource data. Readding - * a same resource requires removing it again with removeResourceData. - * - * @param[in] type The data type of the array elements. - * @param[in] numElements The number of elements of the given data type to use for the resource. - * @param[in] arrayData Pointer to the data to be used to create the array from. - * @param[in] cacheFlag The optional flag sent to the renderer. The value describes how the cache implementation should handle the resource. - * @param[in] name The optional name of the ArrayResource. - * @return The resource id of the created pool ArrayResource - */ - resourceId_t addArrayResourceData( - EDataType type, - uint32_t numElements, - const void* arrayData, - resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache, - const char* name = nullptr); - - /** - * @brief Add Texture2D data to the pool. It is taking ownership of the given range of texture data in the specified pixel format - * and keeps it to instantiate resource from it later via createResourceForScene. See #ramses::Texture2D for more details. - * - * Readding the same resource data again will return the previous resource id, but not recreate the resource data. Readding - * a same resource requires removing it again with removeResourceData. - * - * @param[in] format Pixel format of the Texture2D data. - * @param[in] width Width of the texture (mipmap level 0). - * @param[in] height Height of the texture (mipmap level 0). - * @param[in] mipMapCount Number of mipmap levels contained in mipLevelData array. - * @param[in] mipLevelData Array of MipLevelData structs defining mipmap levels - * to use. Amount and sizes of supplied mipmap levels have to - * conform to GL specification. Order is lowest level (biggest - * resolution) to highest level (smallest resolution). - * @param[in] swizzle Describes how RGBA channels of the texture are swizzled, - * where each member of the struct represents one destination channel that the source channel should get sampled from. - * @param[in] generateMipChain Auto generate mipmap levels. Cannot be used if custom data for lower mipmap levels provided. - * @param[in] cacheFlag The optional flag sent to the renderer. The value describes how the cache implementation should handle the resource. - * @param[in] name The name of the Texture2D. - * @return The resource id of the pool Texture2D resource - */ - resourceId_t addTexture2DData( - ETextureFormat format, - uint32_t width, - uint32_t height, - uint32_t mipMapCount, - const MipLevelData mipLevelData[], - bool generateMipChain = false, - const TextureSwizzle& swizzle = {}, - resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache, - const char* name = nullptr); - - /** - * @brief Add Texture3D data to the pool. It is taking ownership of the given range of texture data in the specified pixel format - * and keeps it to instantiate resource from it later via createResourceForScene. See #ramses::Texture3D for more details. - * Readding the same resource data again will return the previous resource id, but not recreate the resource data. Readding - * a same resource requires removing it again with removeResourceData. - * - * @param[in] format Pixel format of the Texture3D data. - * @param[in] width Width of the texture (mipmap level 0). - * @param[in] height Height of the texture (mipmap level 0). - * @param[in] depth Depth of the texture. - * @param[in] mipMapCount Number of mipmap levels contained in mipLevelData array. - * @param[in] mipLevelData Array of MipLevelData structs defining mipmap levels - * to use. Amount and sizes of supplied mipmap levels have to - * conform to GL specification. Order is lowest level (biggest - * resolution) to highest level (smallest resolution). - * @param[in] generateMipChain Auto generate mipmap levels. Cannot be used if custom data for lower mipmap levels provided. - * @param[in] cacheFlag The optional flag sent to the renderer. The value describes how the cache implementation should handle the resource. - * @param[in] name The name of the Texture3D. - * @return The resource id of the pool Texture3D resource - */ - resourceId_t addTexture3DData( - ETextureFormat format, - uint32_t width, - uint32_t height, - uint32_t depth, - uint32_t mipMapCount, - const MipLevelData mipLevelData[], - bool generateMipChain = false, - resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache, - const char* name = nullptr); - - - /** - * @brief Add Cube Texture data to the pool. It is taking ownership of the given range of texture data in the specified pixel format - * and keeps it to instantiate resource from it later via createResourceForScene. All texel values are initially initialized to 0. - * See #ramses::TextureCube for more details. - * Readding the same resource data again will return the previous resource id, but not recreate the resource data. Readding - * a same resource requires removing it again with removeResourceData. - * - * @param[in] format Pixel format of the Cube Texture data. - * @param[in] size edge length of one quadratic cube face, belonging to the texture. - * @param[in] mipMapCount Number of mipmaps contained in mipLevelData array. - * @param[in] mipLevelData Array of MipLevelData structs defining mipmap levels - * to use. Amount and sizes of supplied mipmap levels have to - * conform to GL specification. Order ist lowest level (biggest - * resolution) to highest level (smallest resolution). - * @param[in] generateMipChain Auto generate mipmap levels. Cannot be used if custom data for lower mipmap levels provided. - * @param[in] swizzle Describes how RGBA channels of the texture are swizzled, - * @param[in] cacheFlag The optional flag sent to the renderer. The value describes how the cache implementation should handle the resource. - * @param[in] name The name of the Cube Texture. - * @return The resource id of the pool Cube Texture resource - */ - resourceId_t addTextureCubeData( - ETextureFormat format, - uint32_t size, - uint32_t mipMapCount, - const CubeMipLevelData mipLevelData[], - bool generateMipChain = false, - const TextureSwizzle& swizzle = {}, - resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache, - const char* name = nullptr); - - /** - * @brief Add Effect data to the pool by parsing a GLSL shader described by an EffectDescription instance. The data can be used to - * instantiate a resource via createResourceForScene. Refer to RamsesClient::getLastEffectErrorMessages in case of parsing error. - * See #ramses::Effect for more details. - * Readding the same resource data again will return the previous resource id, but not recreate the resource data. Readding - * a same resource requires removing it again with removeResourceData. - * - * @param[in] effectDesc Effect description. - * @param[in] cacheFlag The optional flag sent to the renderer. The value describes how the cache implementation should handle the resource. - * @param[in] name The name of the created Effect. - * @return The resource id of the pool effect - */ - resourceId_t addEffectData( - const EffectDescription& effectDesc, - resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache, - const char* name = nullptr); - - /** - * @brief Get the GLSL error messages that were produced at the creation of the last Effect data - * - * @return A string containing the GLSL error messages of the last effect - */ - std::string getLastEffectErrorMessages() const; - - /** - * @brief Removes data which was added to the pool. The provided resource id might not be used anymore to instantiate a - * resource via createResourceForScene, depending on how many times corresponding addResourceData was called before. - * Already instantiated resources will not be affected by removeResourceData. - * - * @param[in] id The resource id of the previously added resource data. - * @return true for success. - */ - bool removeResourceData(resourceId_t const& id); - - /** - * @brief Adds a file to the resource data pool, so the contained data can be instantiated via - * createResourceForScene. - * - * @param[in] filename The file name of the resource file to be added to pool. - * @return true for success. - */ - bool addResourceDataFile(std::string const& filename); - - /** - * @brief Loads all resources in a file currently in use by any scene from that file to memory. - * - * @details When resources are created via createResourceForScene, not the full resource data is loaded immediately, - * but lazily at a later time when the data is actually needed. This function will fully load all resources - * which are currently instantiated in any scene and make sure resources are complete and independent of the - * resource file. - * - * This operation can be used to trigger the potentially heavy resource loading at a user chosen, most - * convenient point in time to ensure fast resource handling once the resource is actually used for rendering. - * - * This operation is recommended to be called before removeResourceDataFile to avoid removing a resource file - * whose resource data hasn't been fully loaded yet for all its resources in any scene. - * - * @param[in] filename The file name of the resource file resource are supposed to force loaded from. - * @return true for success. - */ - bool forceLoadResourcesFromResourceDataFile(std::string const& filename); - - /** - * @brief Removes a resource file from the pool. The contained data can then not be used anymore - * to instantiate a resource via createResourceForScene. - * - * @details Calling this function might make resource data contained in the file unavailable for loading which - * leads to a scene not being rendered or rendered in a wrong way. It is recommended to call - * forceLoadResourcesFromResourceDataFile before to make sure all resource data is in memory and - * don't do any other resource operation before calling removeResourceDataFile. - * - * @param[in] filename The file name of the resource file to be removed from pool. - * @return true for success. - */ - bool removeResourceDataFile(std::string const& filename); - - /** - * @brief Creates a resource for a scene out of pool data. The resource can then be used in - * scene as if created with the scenes create resource functions. - * - * @param[in] scene The scene to instantiate the resource in. - * @param[in] id The resource id of the previously added resource. - * @return Pointer to the created resource. - */ - Resource* createResourceForScene(Scene& scene, resourceId_t const& id); - - /** - * Stores internal data for implementation specifics of ResourceDataPool. - */ - ResourceDataPoolImpl& impl; - - private: - - /** - * @brief RamsesClientImpl is the factory for creating the ResourceDataPool instance. - */ - friend class RamsesClientImpl; - - /** - * @brief Constructor of the ResourceDataPool - * - * @param[in] pimpl Internal data for implementation specifics of ResourceDataPool - */ - explicit ResourceDataPool(ResourceDataPoolImpl& pimpl); - - /** - * @brief Destructor of the ResourceDataPool - */ - ~ResourceDataPool(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/Spline.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/Spline.h deleted file mode 100644 index 9450bfe0e..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/Spline.h +++ /dev/null @@ -1,54 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINE_H -#define RAMSES_SPLINE_H - -#include "ramses-client-api/AnimationObject.h" - -namespace ramses -{ - /** - * @brief The Spline is a set of keys describing an animation - */ - class RAMSES_API Spline : public AnimationObject - { - public: - /** - * Returns number of keys stored in this spline. - * - * @return Number of keys - */ - uint32_t getNumberOfKeys() const; - - /** - * Stores internal data for implementation specifics of Spline. - */ - class SplineImpl& impl; - - protected: - /** - * @brief AnimationSystemData is the factory for creating Spline instances. - */ - friend class AnimationSystemData; - - /** - * @brief Default constructor of Spline. - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit Spline(SplineImpl& pimpl); - - /** - * @brief Destructor of Spline - */ - virtual ~Spline(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierFloat.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierFloat.h deleted file mode 100644 index 2eb5216a4..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierFloat.h +++ /dev/null @@ -1,72 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINEBEZIERFLOAT_H -#define RAMSES_SPLINEBEZIERFLOAT_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineBezierFloat stores spline keys of type float that can be used for animation with Bezier interpolation. - */ - class RAMSES_API SplineBezierFloat : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] value The value for the key data. - * @param[in] tangentIn_x The X component of incoming tangent vector. - * @param[in] tangentIn_y The Y component of incoming tangent vector. - * @param[in] tangentOut_x The X component of outgoing tangent vector. - * @param[in] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float value, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] value The value of the key data. - * @param[out] tangentIn_x The X component of incoming tangent vector. - * @param[out] tangentIn_y The Y component of incoming tangent vector. - * @param[out] tangentOut_x The X component of outgoing tangent vector. - * @param[out] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& value, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineBezierFloat instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineBezierFloat - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineBezierFloat(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineBezierFloat - */ - virtual ~SplineBezierFloat(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierInt32.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierInt32.h deleted file mode 100644 index ef82fa87f..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierInt32.h +++ /dev/null @@ -1,72 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINEBEZIERINT32_H -#define RAMSES_SPLINEBEZIERINT32_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineBezierInt32 stores spline keys of type int32_t that can be used for animation with Bezier interpolation. - */ - class RAMSES_API SplineBezierInt32 : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] value The value for the key data. - * @param[in] tangentIn_x The X component of incoming tangent vector. - * @param[in] tangentIn_y The Y component of incoming tangent vector. - * @param[in] tangentOut_x The X component of outgoing tangent vector. - * @param[in] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t value, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] value The value of the key data. - * @param[out] tangentIn_x The X component of incoming tangent vector. - * @param[out] tangentIn_y The Y component of incoming tangent vector. - * @param[out] tangentOut_x The X component of outgoing tangent vector. - * @param[out] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& value, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineBezierInt32 instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineBezierInt32 - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineBezierInt32(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineBezierInt32 - */ - virtual ~SplineBezierInt32(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector2f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector2f.h deleted file mode 100644 index 8d8df74ec..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector2f.h +++ /dev/null @@ -1,74 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINEBEZIERVECTOR2F_H -#define RAMSES_SPLINEBEZIERVECTOR2F_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineBezierVector2f stores spline keys of type Vector2f that can be used for animation with Bezier interpolation. - */ - class RAMSES_API SplineBezierVector2f : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] tangentIn_x The X component of incoming tangent vector. - * @param[in] tangentIn_y The Y component of incoming tangent vector. - * @param[in] tangentOut_x The X component of outgoing tangent vector. - * @param[in] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float x, float y, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] tangentIn_x The X component of incoming tangent vector. - * @param[out] tangentIn_y The Y component of incoming tangent vector. - * @param[out] tangentOut_x The X component of outgoing tangent vector. - * @param[out] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineBezierVector2f instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineBezierVector2f - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineBezierVector2f(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineBezierVector2f - */ - virtual ~SplineBezierVector2f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector2i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector2i.h deleted file mode 100644 index 0a6a653b9..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector2i.h +++ /dev/null @@ -1,74 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINEBEZIERVECTOR2I_H -#define RAMSES_SPLINEBEZIERVECTOR2I_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineBezierVector2i stores spline keys of type Vector2i that can be used for animation with Bezier interpolation. - */ - class RAMSES_API SplineBezierVector2i : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] tangentIn_x The X component of incoming tangent vector. - * @param[in] tangentIn_y The Y component of incoming tangent vector. - * @param[in] tangentOut_x The X component of outgoing tangent vector. - * @param[in] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] tangentIn_x The X component of incoming tangent vector. - * @param[out] tangentIn_y The Y component of incoming tangent vector. - * @param[out] tangentOut_x The X component of outgoing tangent vector. - * @param[out] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineBezierVector2i instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineBezierVector2i - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineBezierVector2i(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineBezierVector2i - */ - virtual ~SplineBezierVector2i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector3f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector3f.h deleted file mode 100644 index ea69c7184..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector3f.h +++ /dev/null @@ -1,76 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINEBEZIERVECTOR3F_H -#define RAMSES_SPLINEBEZIERVECTOR3F_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineBezierVector3f stores spline keys of type Vector3f that can be used for animation with Bezier interpolation. - */ - class RAMSES_API SplineBezierVector3f : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @param[in] tangentIn_x The X component of incoming tangent vector. - * @param[in] tangentIn_y The Y component of incoming tangent vector. - * @param[in] tangentOut_x The X component of outgoing tangent vector. - * @param[in] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float x, float y, float z, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @param[out] tangentIn_x The X component of incoming tangent vector. - * @param[out] tangentIn_y The Y component of incoming tangent vector. - * @param[out] tangentOut_x The X component of outgoing tangent vector. - * @param[out] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineBezierVector3f instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineBezierVector3f - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineBezierVector3f(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineBezierVector3f - */ - virtual ~SplineBezierVector3f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector3i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector3i.h deleted file mode 100644 index 12ed3a824..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector3i.h +++ /dev/null @@ -1,76 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINEBEZIERVECTOR3I_H -#define RAMSES_SPLINEBEZIERVECTOR3I_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineBezierVector3i stores spline keys of type Vector3i that can be used for animation with Bezier interpolation. - */ - class RAMSES_API SplineBezierVector3i : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @param[in] tangentIn_x The X component of incoming tangent vector. - * @param[in] tangentIn_y The Y component of incoming tangent vector. - * @param[in] tangentOut_x The X component of outgoing tangent vector. - * @param[in] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @param[out] tangentIn_x The X component of incoming tangent vector. - * @param[out] tangentIn_y The Y component of incoming tangent vector. - * @param[out] tangentOut_x The X component of outgoing tangent vector. - * @param[out] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineBezierVector3i instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineBezierVector3i - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineBezierVector3i(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineBezierVector3i - */ - virtual ~SplineBezierVector3i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector4f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector4f.h deleted file mode 100644 index 6f5a33a30..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector4f.h +++ /dev/null @@ -1,78 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINEBEZIERVECTOR4F_H -#define RAMSES_SPLINEBEZIERVECTOR4F_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineBezierVector4f stores spline keys of type Vector4f that can be used for animation with Bezier interpolation. - */ - class RAMSES_API SplineBezierVector4f : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @param[in] w The fourth value for the key data. - * @param[in] tangentIn_x The X component of incoming tangent vector. - * @param[in] tangentIn_y The Y component of incoming tangent vector. - * @param[in] tangentOut_x The X component of outgoing tangent vector. - * @param[in] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float x, float y, float z, float w, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @param[out] w The fourth value of the key data. - * @param[out] tangentIn_x The X component of incoming tangent vector. - * @param[out] tangentIn_y The Y component of incoming tangent vector. - * @param[out] tangentOut_x The X component of outgoing tangent vector. - * @param[out] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& w, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineBezierVector4f instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineBezierVector4f - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineBezierVector4f(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineBezierVector4f - */ - virtual ~SplineBezierVector4f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector4i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector4i.h deleted file mode 100644 index 4ab99f971..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineBezierVector4i.h +++ /dev/null @@ -1,78 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINEBEZIERVECTOR4I_H -#define RAMSES_SPLINEBEZIERVECTOR4I_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineBezierVector4i stores spline keys of type Vector4i that can be used for animation with Bezier interpolation. - */ - class RAMSES_API SplineBezierVector4i : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @param[in] w The fourth value for the key data. - * @param[in] tangentIn_x The X component of incoming tangent vector. - * @param[in] tangentIn_y The Y component of incoming tangent vector. - * @param[in] tangentOut_x The X component of outgoing tangent vector. - * @param[in] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w, float tangentIn_x, float tangentIn_y, float tangentOut_x, float tangentOut_y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @param[out] w The fourth value of the key data. - * @param[out] tangentIn_x The X component of incoming tangent vector. - * @param[out] tangentIn_y The Y component of incoming tangent vector. - * @param[out] tangentOut_x The X component of outgoing tangent vector. - * @param[out] tangentOut_y The Y component of outgoing tangent vector. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, int32_t& w, float& tangentIn_x, float& tangentIn_y, float& tangentOut_x, float& tangentOut_y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineBezierVector4i instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineBezierVector4i - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineBezierVector4i(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineBezierVector4i - */ - virtual ~SplineBezierVector4i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearFloat.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearFloat.h deleted file mode 100644 index 45e626aa3..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearFloat.h +++ /dev/null @@ -1,64 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINELINEARFLOAT_H -#define RAMSES_SPLINELINEARFLOAT_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineLinearFloat stores spline keys of type float that can be used for animation with linear interpolation. - */ - class RAMSES_API SplineLinearFloat : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] value The value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float value); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] value The value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& value) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineLinearFloat instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineLinearFloat - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineLinearFloat(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineLinearFloat - */ - virtual ~SplineLinearFloat(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearInt32.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearInt32.h deleted file mode 100644 index c6bb9c79e..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearInt32.h +++ /dev/null @@ -1,64 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINELINEARINT32_H -#define RAMSES_SPLINELINEARINT32_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineLinearInt32 stores spline keys of type int32_t that can be used for animation with linear interpolation. - */ - class RAMSES_API SplineLinearInt32 : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] value The value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t value); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] value The value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& value) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineLinearInt32 instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineLinearInt32 - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineLinearInt32(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineLinearInt32 - */ - virtual ~SplineLinearInt32(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector2f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector2f.h deleted file mode 100644 index b5188710e..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector2f.h +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINELINEARVECTOR2F_H -#define RAMSES_SPLINELINEARVECTOR2F_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineLinearVector2f stores spline keys of type Vector2f that can be used for animation with linear interpolation. - */ - class RAMSES_API SplineLinearVector2f : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float x, float y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineLinearVector2f instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineLinearVector2f - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineLinearVector2f(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineLinearVector2f - */ - virtual ~SplineLinearVector2f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector2i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector2i.h deleted file mode 100644 index 975b2a3c4..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector2i.h +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINELINEARVECTOR2I_H -#define RAMSES_SPLINELINEARVECTOR2I_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineLinearVector2i stores spline keys of type Vector2i that can be used for animation with linear interpolation. - */ - class RAMSES_API SplineLinearVector2i : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineLinearVector2i instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineLinearVector2i - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineLinearVector2i(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineLinearVector2i - */ - virtual ~SplineLinearVector2i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector3f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector3f.h deleted file mode 100644 index 8aba758a7..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector3f.h +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINELINEARVECTOR3F_H -#define RAMSES_SPLINELINEARVECTOR3F_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineLinearVector3f stores spline keys of type Vector3f that can be used for animation with linear interpolation. - */ - class RAMSES_API SplineLinearVector3f : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float x, float y, float z); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineLinearVector3f instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineLinearVector3f - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineLinearVector3f(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineLinearVector3f - */ - virtual ~SplineLinearVector3f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector3i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector3i.h deleted file mode 100644 index 3df4fefec..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector3i.h +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINELINEARVECTOR3I_H -#define RAMSES_SPLINELINEARVECTOR3I_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineLinearVector3i stores spline keys of type Vector3i that can be used for animation with linear interpolation. - */ - class RAMSES_API SplineLinearVector3i : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineLinearVector3i instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineLinearVector3i - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineLinearVector3i(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineLinearVector3i - */ - virtual ~SplineLinearVector3i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector4f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector4f.h deleted file mode 100644 index 725e0f230..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector4f.h +++ /dev/null @@ -1,70 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINELINEARVECTOR4F_H -#define RAMSES_SPLINELINEARVECTOR4F_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineLinearVector4f stores spline keys of type Vector4f that can be used for animation with linear interpolation. - */ - class RAMSES_API SplineLinearVector4f : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @param[in] w The fourth value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float x, float y, float z, float w); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @param[out] w The fourth value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& w) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineLinearVector4f instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineLinearVector4f - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineLinearVector4f(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineLinearVector4f - */ - virtual ~SplineLinearVector4f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector4i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector4i.h deleted file mode 100644 index 27fee875f..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineLinearVector4i.h +++ /dev/null @@ -1,70 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINELINEARVECTOR4I_H -#define RAMSES_SPLINELINEARVECTOR4I_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineLinearVector4i stores spline keys of type Vector4i that can be used for animation with linear interpolation. - */ - class RAMSES_API SplineLinearVector4i : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @param[in] w The fourth value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @param[out] w The fourth value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, int32_t& w) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineLinearVector4i instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineLinearVector4i - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineLinearVector4i(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineLinearVector4i - */ - virtual ~SplineLinearVector4i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepBool.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepBool.h deleted file mode 100644 index 8bb78f482..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepBool.h +++ /dev/null @@ -1,64 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINESTEPBOOL_H -#define RAMSES_SPLINESTEPBOOL_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineStepBool stores spline keys of type bool that can be used for animation with step interpolation. - */ - class RAMSES_API SplineStepBool : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] value The value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, bool value); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] value The value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, bool& value) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineStepBool instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineStepBool - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineStepBool(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineStepBool - */ - virtual ~SplineStepBool(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepFloat.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepFloat.h deleted file mode 100644 index 66cf2a519..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepFloat.h +++ /dev/null @@ -1,64 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINESTEPFLOAT_H -#define RAMSES_SPLINESTEPFLOAT_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineStepFloat stores spline keys of type float that can be used for animation with step interpolation. - */ - class RAMSES_API SplineStepFloat : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] value The value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float value); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] value The value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& value) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineStepFloat instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineStepFloat - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineStepFloat(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineStepFloat - */ - virtual ~SplineStepFloat(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepInt32.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepInt32.h deleted file mode 100644 index f5aba8d54..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepInt32.h +++ /dev/null @@ -1,64 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINESTEPINT32_H -#define RAMSES_SPLINESTEPINT32_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineStepInt32 stores spline keys of type int32_t that can be used for animation with step interpolation. - */ - class RAMSES_API SplineStepInt32 : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] value The value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t value); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] value The value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& value) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineStepInt32 instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineStepInt32 - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineStepInt32(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineStepInt32 - */ - virtual ~SplineStepInt32(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector2f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector2f.h deleted file mode 100644 index 0ea3e6940..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector2f.h +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINESTEPVECTOR2F_H -#define RAMSES_SPLINESTEPVECTOR2F_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineStepVector2f stores spline keys of type Vector2f that can be used for animation with step interpolation. - */ - class RAMSES_API SplineStepVector2f : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float x, float y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineStepVector2f instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineStepVector2f - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineStepVector2f(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineStepVector2f - */ - virtual ~SplineStepVector2f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector2i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector2i.h deleted file mode 100644 index ac8769e21..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector2i.h +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINESTEPVECTOR2I_H -#define RAMSES_SPLINESTEPVECTOR2I_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineStepVector2i stores spline keys of type Vector2i that can be used for animation with step interpolation. - */ - class RAMSES_API SplineStepVector2i : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineStepVector2i instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineStepVector2i - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineStepVector2i(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineStepVector2i - */ - virtual ~SplineStepVector2i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector3f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector3f.h deleted file mode 100644 index 7830b34dc..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector3f.h +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINESTEPVECTOR3F_H -#define RAMSES_SPLINESTEPVECTOR3F_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineStepVector3f stores spline keys of type Vector3f that can be used for animation with step interpolation. - */ - class RAMSES_API SplineStepVector3f : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float x, float y, float z); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineStepVector3f instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineStepVector3f - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineStepVector3f(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineStepVector3f - */ - virtual ~SplineStepVector3f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector3i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector3i.h deleted file mode 100644 index 7425a8627..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector3i.h +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINESTEPVECTOR3I_H -#define RAMSES_SPLINESTEPVECTOR3I_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineStepVector3i stores spline keys of type Vector3i that can be used for animation with step interpolation. - */ - class RAMSES_API SplineStepVector3i : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineStepVector3i instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineStepVector3i - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineStepVector3i(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineStepVector3i - */ - virtual ~SplineStepVector3i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector4f.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector4f.h deleted file mode 100644 index 0ab0fcf16..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector4f.h +++ /dev/null @@ -1,70 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINESTEPVECTOR4F_H -#define RAMSES_SPLINESTEPVECTOR4F_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineStepVector4f stores spline keys of type Vector4f that can be used for animation with step interpolation. - */ - class RAMSES_API SplineStepVector4f : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @param[in] w The fourth value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, float x, float y, float z, float w); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @param[out] w The fourth value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, float& x, float& y, float& z, float& w) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineStepVector4f instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineStepVector4f - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineStepVector4f(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineStepVector4f - */ - virtual ~SplineStepVector4f(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector4i.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector4i.h deleted file mode 100644 index cb2a59ac6..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/SplineStepVector4i.h +++ /dev/null @@ -1,70 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2014 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINESTEPVECTOR4I_H -#define RAMSES_SPLINESTEPVECTOR4I_H - -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/AnimationTypes.h" - -namespace ramses -{ - /** - * @brief The SplineStepVector4i stores spline keys of type Vector4i that can be used for animation with step interpolation. - */ - class RAMSES_API SplineStepVector4i : public Spline - { - public: - /** - * @brief Sets a spline key at given time with given value. - * - * @param[in] timeStamp The time stamp for the key to be set - * @param[in] x The first value for the key data. - * @param[in] y The second value for the key data. - * @param[in] z The third value for the key data. - * @param[in] w The fourth value for the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t setKey(splineTimeStamp_t timeStamp, int32_t x, int32_t y, int32_t z, int32_t w); - - /** - * @brief Gets key value and time stamp for a given key index. - * - * @param[in] keyIndex Index of a key to get values from. - * @param[out] timeStamp The time stamp of the key. - * @param[out] x The first value of the key data. - * @param[out] y The second value of the key data. - * @param[out] z The third value of the key data. - * @param[out] w The fourth value of the key data. - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t getKeyValues(splineKeyIndex_t keyIndex, splineTimeStamp_t& timeStamp, int32_t& x, int32_t& y, int32_t& z, int32_t& w) const; - - protected: - /** - * @brief AnimationSystemData is the factory for creating SplineStepVector4i instances. - */ - friend class AnimationSystemData; - - /** - * @brief Constructor of SplineStepVector4i - * - * @param[in] pimpl Internal data for implementation specifics of Spline (sink - instance becomes owner) - */ - explicit SplineStepVector4i(SplineImpl& pimpl); - - /** - * @brief Destructor of the SplineStepVector4i - */ - virtual ~SplineStepVector4i(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-client-api/StreamTexture.h b/client/ramses-client/ramses-client-api/include/ramses-client-api/StreamTexture.h deleted file mode 100644 index fc19b238c..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-client-api/StreamTexture.h +++ /dev/null @@ -1,77 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_STREAMTEXTURE_H -#define RAMSES_STREAMTEXTURE_H - -#include "ramses-client-api/SceneObject.h" - -namespace ramses -{ - /** - * @brief StreamTexture is a special kind of texture, which holds a reference to a - * "fallback texture" and a stream source id. The content of the StreamTexture is - * dynamic and is determined within the renderer, based on whether the stream with - * the specified id is available or not. If it is available, the content of the - * StreamTexture is taken from the stream. Otherwise, the StreamTexture is replaced - * by the "fallback texture". - */ - class RAMSES_API StreamTexture : public SceneObject - { - public: - /** - * Stores internal data for implementation specifics of StreamTexture. - */ - class StreamTextureImpl& impl; - - /** - * @brief Force usage of fallback texture even if compositing source is available - * - * @param[in] forceFallbackImage If set to \c true the fallback image will always be shown even if streaming source for - * embedded compositing is or becomes available - * - * @return StatusOK for success, otherwise the returned status can be used - * to resolve error message using getStatusMessage(). - */ - status_t forceFallbackImage(bool forceFallbackImage); - - /** - * @brief Get the current value, if fallback image is forced or not. - * - * @return If forcing of fallback image is currently enabled or not. - */ - bool getForceFallbackImage() const; - - /** - * @brief Get the stream source id used for compositing on the stream texture - * - * @return Stream source id - */ - waylandIviSurfaceId_t getStreamSourceId() const; - - protected: - /** - * @brief RamsesClient is the factory for creating StreamTexture instances. - */ - friend class SceneImpl; - - /** - * @brief Constructor of StreamTexture - * - * @param[in] impl Internal data for implementation specifics of StreamTexture (sink - instance becomes owner) - */ - explicit StreamTexture(StreamTextureImpl& impl); - - /** - * @brief Destructor of the StreamTexture - */ - virtual ~StreamTexture(); - }; -} - -#endif diff --git a/client/ramses-client/ramses-client-api/include/ramses-hmi-utils.h b/client/ramses-client/ramses-client-api/include/ramses-hmi-utils.h deleted file mode 100644 index 5463963f0..000000000 --- a/client/ramses-client/ramses-client-api/include/ramses-hmi-utils.h +++ /dev/null @@ -1,80 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2017 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_RAMSES_HMI_UTILS_H -#define RAMSES_RAMSES_HMI_UTILS_H - -#include "ramses-framework-api/APIExport.h" -#include "ramses-client-api/Scene.h" -#include - -namespace ramses -{ - class ResourceDataPool; - - /** - * @brief Utility functions especially created for functions not yet designated for direct API integration. - **/ - class RAMSES_API RamsesHMIUtils - { - public: - /** - * @brief Checks if all resources for scene are locally available - * - * This method can be called to verify that at the time of calling all resources that are used inside scene are locally - * available. That means this scene could be mapped on a renderer and the renderer would be able to retrieve all needed - * resources. - * It does not check for resources in remote providers or resource caches. - * - * @param[in] scene The scene to verify resources for - * @result True when all resources are available, false if not - */ - static bool AllResourcesForSceneKnown(const Scene& scene); - - /** - * @brief Dumps all objects of a scene which do not contribute to the visual appearance of the scene on screen. - * This includes disabled RenderPass-es, invisible MeshNode-s, client resources which are not used by the scene, and so on. - * The output is in text form, starts with a list of all unrequired objects and their names and concludes with a - * statistic (number of unrequired objects out of all objects of that type) - * - * @param[in] scene the source scene - */ - static void DumpUnrequiredSceneObjects(const Scene& scene); - - /** - * @brief As RamsesHMIUtils::DumpUnrequiredSceneObjects but write to given stream. - * - * @param[in] scene the source scene - * @param[out] out stream to write to - */ - static void DumpUnrequiredSceneObjectsToFile(const Scene& scene, std::ostream& out); - - /** - * @brief Returns the ResourceDataPool for this client. - * @deprecated This function and the returned class are deprecated. See #ramses::ResourceDataPool for details. - * - * @param[in] client The client to get the ResourceDataPool from. - * return The reference to the ResourceDataPool. - */ - static ResourceDataPool& GetResourceDataPoolForClient(RamsesClient& client); - - /** - * @brief Saves all resources of a scene to a resource file, which can be opened in a ResourceDataPool. - * @deprecated This functionality is deprecated. See #ramses::ResourceDataPool for details. - * - * @param[in] scene The scene to save resource of. - * @param[in] filename The file path to write the resource file to. - * @param[in] compress if set to true, resources might be compressed before saving - * otherwise, uncompressed data will be saved - * return True if operation succeeded. - */ - static bool SaveResourcesOfSceneToResourceFile(Scene const& scene, std::string const& filename, bool compress); - }; -} - -#endif diff --git a/client/ramses-client/test/AnimatedPropertyTest.cpp b/client/ramses-client/test/AnimatedPropertyTest.cpp deleted file mode 100644 index 86ce9cccb..000000000 --- a/client/ramses-client/test/AnimatedPropertyTest.cpp +++ /dev/null @@ -1,308 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include - -#include "ramses-client-api/MeshNode.h" -#include "ramses-client-api/PerspectiveCamera.h" -#include "ramses-client-api/OrthographicCamera.h" -#include "ramses-client-api/UniformInput.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-client-api/DataMatrix44f.h" -#include "ramses-client-api/PickableObject.h" - -#include "Collections/Vector.h" - -#include "ClientTestUtils.h" -#include "EffectInputImpl.h" -#include "AnimatedPropertyImpl.h" -#include "AnimationSystemImpl.h" -#include "TestEffectCreator.h" -#include "RamsesObjectTestTypes.h" - -using namespace testing; - -namespace ramses -{ - class AnimatedProperty; - - static TestEffectCreator* sharedEffectCreator = nullptr; - static AnimationSystem* sharedAnimationSystem = nullptr; - - template - const AnimatedProperty* createAnimatedProperty(AnimationSystem& animSystem, AnimatableType& animatable, EAnimatedPropertyComponent propertyComponent, EAnimatedProperty property, Appearance&) - { - return animSystem.createAnimatedProperty(animatable, property, propertyComponent); - } - - template <> - const AnimatedProperty* createAnimatedProperty(AnimationSystem& animSystem, DataVector2f& animatable, EAnimatedPropertyComponent propertyComponent, EAnimatedProperty, Appearance&) - { - return animSystem.createAnimatedProperty(animatable, propertyComponent); - } - - template <> - const AnimatedProperty* createAnimatedProperty(AnimationSystem& animSystem, DataVector3f& animatable, EAnimatedPropertyComponent propertyComponent, EAnimatedProperty, Appearance&) - { - return animSystem.createAnimatedProperty(animatable, propertyComponent); - } - - template <> - const AnimatedProperty* createAnimatedProperty(AnimationSystem& animSystem, UniformInput& animatable, EAnimatedPropertyComponent propertyComponent, EAnimatedProperty, Appearance& appearance) - { - return animSystem.createAnimatedProperty(animatable, appearance, propertyComponent); - } - - template - class AnimatedPropertyTest : public testing::Test - { - public: - using ComponentVector = std::vector; - using PropertyComponentVector = std::vector; - using PropertyVector = std::vector; - - static void SetUpTestCase() - { - sharedEffectCreator = new TestEffectCreator; - sharedAnimationSystem = sharedEffectCreator->getScene().createAnimationSystem(); - } - - static void TearDownTestCase() - { - sharedAnimationSystem = nullptr; - delete sharedEffectCreator; - sharedEffectCreator = nullptr; - } - - void SetUp() override - { - EXPECT_TRUE(sharedEffectCreator != nullptr); - EXPECT_TRUE(sharedAnimationSystem != nullptr); - } - - AnimatedPropertyTest() - : animatableProperties({ EAnimatedProperty_Translation, EAnimatedProperty_Rotation, EAnimatedProperty_Scaling }) - { - // define all possible valid combinations for every property - - ComponentVector compsScalar; - compsScalar.push_back(EAnimatedPropertyComponent_All); - ComponentVector compsVec3; - compsVec3.push_back(EAnimatedPropertyComponent_All); - compsVec3.push_back(EAnimatedPropertyComponent_X); - compsVec3.push_back(EAnimatedPropertyComponent_Y); - compsVec3.push_back(EAnimatedPropertyComponent_Z); - ComponentVector compsVec4; - compsVec4.push_back(EAnimatedPropertyComponent_All); - compsVec4.push_back(EAnimatedPropertyComponent_X); - compsVec4.push_back(EAnimatedPropertyComponent_Y); - compsVec4.push_back(EAnimatedPropertyComponent_Z); - compsVec4.push_back(EAnimatedPropertyComponent_W); - - propertiesDefaultScalar.push_back(compsScalar); - propertiesDefaultVec3.push_back(compsVec3); - propertiesDefaultVec4.push_back(compsVec4); - } - - template - void checkPropertyCreation(AnimatableType& animatable, const PropertyComponentVector& comps) - { - for (const auto& propComp : comps) - { - for (const auto comp : propComp) - { - for (const auto property : animatableProperties) - { - const AnimatedProperty* prop = createAnimatedProperty(*sharedAnimationSystem, animatable, comp, property, *sharedEffectCreator->appearance); - EXPECT_FALSE(prop == nullptr); - } - } - } - } - - template - void checkPropertyCreationFail(AnimatableType& animatable) - { - for (const auto property : animatableProperties) - { - const AnimatedProperty* prop = createAnimatedProperty(*sharedAnimationSystem, animatable, EAnimatedPropertyComponent_W, property, *sharedEffectCreator->appearance); - EXPECT_TRUE(prop == nullptr); - } - } - - void getValidUniformInput(UniformInput& uniform, const char* inputName) - { - EXPECT_EQ(StatusOK, sharedEffectCreator->effect->findUniformInput(inputName, uniform)); - EXPECT_TRUE(uniform.isValid()); - } - - protected: - PropertyComponentVector propertiesDefaultScalar; - PropertyComponentVector propertiesDefaultVec3; - PropertyComponentVector propertiesDefaultVec4; - PropertyVector animatableProperties; - }; - - // dummy type instantiation for tests with explicit animatable type within the test case itself - using AnAnimatedProperty = AnimatedPropertyTest; - - TEST_F(AnAnimatedProperty, createAnimatedPropertyWithAllCombinationsOfValidParams_UniformInput_Vector4F) - { - UniformInput animatable; - getValidUniformInput(animatable, "vec4fInput"); - checkPropertyCreation(animatable, propertiesDefaultVec4); - } - - TEST_F(AnAnimatedProperty, createAnimatedPropertyWithAllCombinationsOfValidParams_DataObject_Vector3F) - { - DataVector3f& animatable = sharedEffectCreator->createObject("animatable"); - checkPropertyCreation(animatable, propertiesDefaultVec3); - } - - TEST_F(AnAnimatedProperty, createAnimatedPropertyWithInvalidParams_UniformInput_Vector2F_Z_Animation) - { - UniformInput animatable; - getValidUniformInput(animatable, "vec2fInput"); - AnimatedProperty* prop = sharedAnimationSystem->createAnimatedProperty(animatable, *sharedEffectCreator->appearance, EAnimatedPropertyComponent_Z); - EXPECT_TRUE(prop == nullptr); - } - - TEST_F(AnAnimatedProperty, createAnimatedPropertyWithInvalidParams_DataObject_Vector2F_Z_Animation) - { - DataVector2f& animatable = sharedEffectCreator->createObject("animatable"); - AnimatedProperty* prop = sharedAnimationSystem->createAnimatedProperty(animatable, EAnimatedPropertyComponent_Z); - EXPECT_TRUE(prop == nullptr); - } - - TEST_F(AnAnimatedProperty, failsToCreateAnimatedPropertyForInvalidUniformInput) - { - UniformInput animatable; - AnimatedProperty* prop = sharedAnimationSystem->createAnimatedProperty(animatable, *sharedEffectCreator->appearance); - EXPECT_TRUE(prop == nullptr); - } - - TEST_F(AnAnimatedProperty, failsToCreateAnimatedPropertyForUniformInputBelongingToDifferentAppearance) - { - UniformInput animatable; - getValidUniformInput(animatable, "vec4fInput"); - AnimatedProperty* prop = sharedAnimationSystem->createAnimatedProperty(animatable, sharedEffectCreator->createObject()); - EXPECT_TRUE(prop == nullptr); - } - - TEST_F(AnAnimatedProperty, failsToCreateIfPropertyIsFromAnotherScene_Node) - { - Scene* otherScene = sharedEffectCreator->getClient().createScene(sceneId_t(12u)); - Node& animatable = *otherScene->createNode(); - - AnimatedProperty* prop = sharedAnimationSystem->createAnimatedProperty(animatable, EAnimatedProperty_Translation); - EXPECT_TRUE(prop == nullptr); - } - - TEST_F(AnAnimatedProperty, failsToCreateIfPropertyIsFromAnotherScene_Uniform) - { - UniformInput animatable; - getValidUniformInput(animatable, "vec4fInput"); - - Scene* otherScene = sharedEffectCreator->getClient().createScene(sceneId_t(13u)); - AnimationSystem* otherAnimationSystem = otherScene->createAnimationSystem(); - - AnimatedProperty* prop = otherAnimationSystem->createAnimatedProperty(animatable, *sharedEffectCreator->appearance); - EXPECT_TRUE(prop == nullptr); - } - - TEST_F(AnAnimatedProperty, failsToCreateIfPropertyIsFromAnotherScene_DataObject) - { - DataVector3f& animatable = sharedEffectCreator->createObject("animatable"); - - Scene* otherScene = sharedEffectCreator->getClient().createScene(sceneId_t(14u)); - AnimationSystem* otherAnimationSystem = otherScene->createAnimationSystem(); - - AnimatedProperty* prop = otherAnimationSystem->createAnimatedProperty(animatable); - EXPECT_TRUE(prop == nullptr); - } - - TEST_F(AnAnimatedProperty, failsToCreateAnimatedPropertyForUniformInputWithUnsupportedDataType) - { - UniformInput animatable; - getValidUniformInput(animatable, "matrix44fInput"); - AnimatedProperty* prop = sharedAnimationSystem->createAnimatedProperty(animatable, *sharedEffectCreator->appearance); - EXPECT_TRUE(prop == nullptr); - } - - TEST_F(AnAnimatedProperty, failsToCreateAnimatedPropertyForDataObjectWithUnsupportedDataType) - { - DataMatrix44f& animatable = sharedEffectCreator->createObject("animatable"); - - AnimatedProperty* prop = sharedAnimationSystem->createAnimatedProperty(animatable); - EXPECT_TRUE(prop == nullptr); - } - - TEST_F(AnAnimatedProperty, removalOfAnimatedPropertyReleasesItsDataBind) - { - Node& animatable = sharedEffectCreator->createObject("animatable"); - AnimatedProperty* prop = sharedAnimationSystem->createAnimatedProperty(animatable, EAnimatedProperty_Translation, EAnimatedPropertyComponent_All); - ASSERT_TRUE(prop != nullptr); - const ramses_internal::DataBindHandle dataBind = prop->impl.getDataBindHandle(); - EXPECT_TRUE(sharedAnimationSystem->impl.getIAnimationSystem().containsDataBinding(dataBind)); - - sharedAnimationSystem->destroy(*prop); - - EXPECT_FALSE(sharedAnimationSystem->impl.getIAnimationSystem().containsDataBinding(dataBind)); - } - - TEST_F(AnAnimatedProperty, canBeValidated) - { - Node& animatable = sharedEffectCreator->createObject("animatable"); - AnimatedProperty* prop = sharedAnimationSystem->createAnimatedProperty(animatable, EAnimatedProperty_Translation, EAnimatedPropertyComponent_All); - ASSERT_TRUE(prop != nullptr); - EXPECT_EQ(StatusOK, prop->validate()); - } - - TEST_F(AnAnimatedProperty, failsValidationWhenItsPropertyRemoved) - { - Node& animatable = sharedEffectCreator->createObject("animatable"); - AnimatedProperty* prop = sharedAnimationSystem->createAnimatedProperty(animatable, EAnimatedProperty_Translation, EAnimatedPropertyComponent_All); - ASSERT_TRUE(prop != nullptr); - - sharedEffectCreator->getScene().destroy(animatable); - - EXPECT_NE(StatusOK, prop->validate()); - } - - TEST_F(AnAnimatedProperty, failsValidationWhenUsedInMultipleAnimations) - { - Node& animatable = sharedEffectCreator->createObject("animatable"); - AnimatedProperty* prop = sharedAnimationSystem->createAnimatedProperty(animatable, EAnimatedProperty_Translation, EAnimatedPropertyComponent_All); - ASSERT_TRUE(prop != nullptr); - - Spline* spline = sharedAnimationSystem->createSplineLinearVector3f(); - ASSERT_TRUE(spline != nullptr); - - sharedAnimationSystem->createAnimation(*prop, *spline); - sharedAnimationSystem->createAnimation(*prop, *spline); - - EXPECT_NE(StatusOK, prop->validate()); - } - - // templated tests for all node types - TYPED_TEST_SUITE(AnimatedPropertyTest, NodeTypes); - - TYPED_TEST(AnimatedPropertyTest, createNodeAnimatedPropertyWithAllCombinationsOfValidParams) - { - TypeParam& animatable = sharedEffectCreator->createObject("animatable"); - this->checkPropertyCreation(animatable, this->propertiesDefaultVec3); - } - - TYPED_TEST(AnimatedPropertyTest, createNodeAnimatedPropertyWithInvalidParams) - { - TypeParam& animatable = sharedEffectCreator->createObject("animatable"); - this->checkPropertyCreationFail(animatable); - } -} diff --git a/client/ramses-client/test/AnimationSequenceTest.cpp b/client/ramses-client/test/AnimationSequenceTest.cpp deleted file mode 100644 index b95809cf4..000000000 --- a/client/ramses-client/test/AnimationSequenceTest.cpp +++ /dev/null @@ -1,639 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include - -#include "ClientTestUtils.h" -#include "ramses-client-api/Node.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/AnimatedProperty.h" -#include "AnimationSystemImpl.h" -#include "AnimationImpl.h" - -#include "Animation/AnimationTime.h" - -using namespace testing; - -namespace ramses -{ - class AnimationSequenceTest : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test - { - public: - AnimationSequenceTest() - { - m_spline = animationSystem.createSplineLinearFloat("spline"); - m_spline->setKey(0u, 1.f); - m_spline->setKey(SplineDuration, 0.f); - Node* node = m_scene.createNode(); - m_animProperty = animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation, EAnimatedPropertyComponent_X, "prop"); - m_animation = animationSystem.createAnimation(*m_animProperty, *m_spline, "animation"); - m_sequence = animationSystem.createAnimationSequence("sequence"); - } - - protected: - using AnimationVector = std::vector; - void createSomeAnimationsAndPutToSequence(AnimationVector& animations) - { - animations.clear(); - Node* node = m_scene.createNode(); - AnimatedProperty* animProperty2 = animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation, EAnimatedPropertyComponent_X, "prop2"); - AnimatedProperty* animProperty3 = animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation, EAnimatedPropertyComponent_X, "prop3"); - AnimatedProperty* animProperty4 = animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation, EAnimatedPropertyComponent_X, "prop4"); - AnimatedProperty* animProperty5 = animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation, EAnimatedPropertyComponent_X, "prop5"); - Animation* animation2 = animationSystem.createAnimation(*animProperty2, *m_spline, "animation2"); - Animation* animation3 = animationSystem.createAnimation(*animProperty3, *m_spline, "animation3"); - Animation* animation4 = animationSystem.createAnimation(*animProperty4, *m_spline, "animation4"); - Animation* animation5 = animationSystem.createAnimation(*animProperty5, *m_spline, "animation5"); - animations.push_back(m_animation); - animations.push_back(animation2); - animations.push_back(animation3); - animations.push_back(animation4); - animations.push_back(animation5); - - m_sequence->addAnimation(*m_animation); - m_sequence->addAnimation(*animation2, 3000u); - m_sequence->addAnimation(*animation3, m_sequence->getAnimationStopTimeInSequence(*animation2)); - m_sequence->addAnimation(*animation4, 1000u + m_sequence->getAnimationStopTimeInSequence(*animation3)); - m_sequence->addAnimation(*animation5, 9999u); - } - - using AnimationSequenceTimes = std::vector; - static AnimationSequenceTimes GetStartAndStopTimesWithinSequence(const AnimationVector& animations, const AnimationSequence& sequence) - { - AnimationSequenceTimes times; - times.reserve(animations.size() * 2u); - for (const auto& anim : animations) - { - const sequenceTimeStamp_t startTimeInSequence = sequence.getAnimationStartTimeInSequence(*anim); - const sequenceTimeStamp_t stopTimeInSequence = sequence.getAnimationStopTimeInSequence(*anim); - times.push_back(startTimeInSequence); - times.push_back(stopTimeInSequence); - } - - return times; - } - - static void ExpectStartAndStopTimesWithinSequenceAdjustedBySpeedFactor(const AnimationSequenceTimes& expectedTimes, float speedFactor, const AnimationVector& animations, const AnimationSequence& sequence) - { - ASSERT_TRUE(expectedTimes.size() == animations.size() * 2u); - for (ramses_internal::UInt i = 0u; i < animations.size(); ++i) - { - const Animation& anim = *animations[i]; - const sequenceTimeStamp_t startTimeInSequence = sequence.getAnimationStartTimeInSequence(anim); - const sequenceTimeStamp_t stopTimeInSequence = sequence.getAnimationStopTimeInSequence(anim); - const sequenceTimeStamp_t expectedStartTimeInSequence = static_cast(expectedTimes[i * 2u] * speedFactor); - const sequenceTimeStamp_t expectedStopTimeInSequence = static_cast(expectedTimes[i * 2u + 1u] * speedFactor); - EXPECT_EQ(expectedStartTimeInSequence, startTimeInSequence); - EXPECT_EQ(expectedStopTimeInSequence, stopTimeInSequence); - } - } - - static void ExpectStartTimeWRTSequence(globalTimeStamp_t sequenceStartTime, const AnimationVector& animations, const AnimationSequence& sequence) - { - for (const auto& anim : animations) - { - const sequenceTimeStamp_t timeInSeq = sequence.getAnimationStartTimeInSequence(*anim); - EXPECT_EQ(sequenceStartTime + timeInSeq, anim->getStartTime()); - } - } - - static void ExpectStartTimeWRTSequence(globalTimeStamp_t sequenceStartTime, const AnimationVector& animations, const AnimationSequenceTimes& expectedTimes) - { - ASSERT_TRUE(expectedTimes.size() == animations.size() * 2u); - for (ramses_internal::UInt i = 0u; i < animations.size(); ++i) - { - const sequenceTimeStamp_t timeInSeq = expectedTimes[i * 2u]; - EXPECT_EQ(sequenceStartTime + timeInSeq, animations[i]->getStartTime()); - } - } - - static void ExpectStopTimeWRTSequence(globalTimeStamp_t sequenceStartTime, const AnimationVector& animations, const AnimationSequence& sequence) - { - for (const auto& anim : animations) - { - const sequenceTimeStamp_t timeInSeq = sequence.getAnimationStopTimeInSequence(*anim); - EXPECT_EQ(sequenceStartTime + timeInSeq, anim->getStopTime()); - } - } - - static void ExpectStopTimeWRTSequence(globalTimeStamp_t sequenceStartTime, const AnimationVector& animations, const AnimationSequenceTimes& expectedTimes) - { - ASSERT_TRUE(expectedTimes.size() == animations.size() * 2u); - for (ramses_internal::UInt i = 0u; i < animations.size(); ++i) - { - const sequenceTimeStamp_t timeInSeq = expectedTimes[i * 2u + 1u]; - EXPECT_EQ(sequenceStartTime + timeInSeq, animations[i]->getStopTime()); - } - } - - static void ExpectStopTime(globalTimeStamp_t timeStamp, const AnimationVector& animations) - { - for (const auto& anim : animations) - { - EXPECT_EQ(timeStamp, anim->getStopTime()); - } - } - - static const timeMilliseconds_t SplineDuration = 2000; - - SplineLinearFloat* m_spline; - AnimatedProperty* m_animProperty; - Animation* m_animation; - AnimationSequence* m_sequence; - }; - - TEST_F(AnimationSequenceTest, initialValues) - { - EXPECT_EQ(0u, m_sequence->getNumberOfAnimations()); - EXPECT_FALSE(m_sequence->containsAnimation(*m_animation)); - EXPECT_EQ(1.f, m_sequence->getPlaybackSpeed()); - EXPECT_FALSE(m_sequence->isAnimationLooping(*m_animation)); - EXPECT_EQ(0, m_sequence->getAnimationLoopDuration(*m_animation)); - EXPECT_FALSE(m_sequence->isAnimationRelative(*m_animation)); - EXPECT_EQ(InvalidSequenceTimeStamp, m_sequence->getAnimationStartTimeInSequence(*m_animation)); - EXPECT_EQ(InvalidSequenceTimeStamp, m_sequence->getAnimationStopTimeInSequence(*m_animation)); - } - - TEST_F(AnimationSequenceTest, addAnimationDefault) - { - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation)); - EXPECT_EQ(1u, m_sequence->getNumberOfAnimations()); - EXPECT_TRUE(m_sequence->containsAnimation(*m_animation)); - EXPECT_EQ(0u, m_sequence->getAnimationStartTimeInSequence(*m_animation)); - EXPECT_EQ(timeMilliseconds_t(SplineDuration), m_sequence->getAnimationStopTimeInSequence(*m_animation)); - } - - TEST_F(AnimationSequenceTest, addAnimationWithStartStopTimes) - { - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation, 2500u, 5000u)); - EXPECT_EQ(1u, m_sequence->getNumberOfAnimations()); - EXPECT_TRUE(m_sequence->containsAnimation(*m_animation)); - EXPECT_EQ(2500u, m_sequence->getAnimationStartTimeInSequence(*m_animation)); - EXPECT_EQ(5000u, m_sequence->getAnimationStopTimeInSequence(*m_animation)); - } - - TEST_F(AnimationSequenceTest, addAnimationAfter) - { - Animation* animation2 = animationSystem.createAnimation(*m_animProperty, *m_spline, "animation"); - - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation, 2500u, 5000u)); - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*animation2, m_sequence->getAnimationStopTimeInSequence(*m_animation) + 3000u)); - EXPECT_EQ(2u, m_sequence->getNumberOfAnimations()); - EXPECT_TRUE(m_sequence->containsAnimation(*m_animation)); - EXPECT_TRUE(m_sequence->containsAnimation(*animation2)); - EXPECT_EQ(2500u, m_sequence->getAnimationStartTimeInSequence(*m_animation)); - EXPECT_EQ(5000u, m_sequence->getAnimationStopTimeInSequence(*m_animation)); - EXPECT_EQ(8000u, m_sequence->getAnimationStartTimeInSequence(*animation2)); - EXPECT_EQ(8000u + SplineDuration, m_sequence->getAnimationStopTimeInSequence(*animation2)); - } - - TEST_F(AnimationSequenceTest, removeAnimation) - { - Animation* animation2 = animationSystem.createAnimation(*m_animProperty, *m_spline, "animation"); - Animation* animation3 = animationSystem.createAnimation(*m_animProperty, *m_spline, "animation"); - - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation)); - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*animation2)); - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*animation3)); - EXPECT_EQ(3u, m_sequence->getNumberOfAnimations()); - EXPECT_TRUE(m_sequence->containsAnimation(*m_animation)); - EXPECT_TRUE(m_sequence->containsAnimation(*animation2)); - EXPECT_TRUE(m_sequence->containsAnimation(*animation3)); - EXPECT_EQ(StatusOK, m_sequence->removeAnimation(*animation2)); - EXPECT_TRUE(m_sequence->containsAnimation(*m_animation)); - EXPECT_FALSE(m_sequence->containsAnimation(*animation2)); - EXPECT_TRUE(m_sequence->containsAnimation(*animation3)); - } - - TEST_F(AnimationSequenceTest, canGetSequenceStopTime) - { - Animation* animation2 = animationSystem.createAnimation(*m_animProperty, *m_spline, "animation"); - - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation, 2500u, 3000u)); - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*animation2, 1500u, 5000u)); - EXPECT_EQ(5000u, m_sequence->getAnimationSequenceStopTime()); - } - - TEST_F(AnimationSequenceTest, canGetSequenceStopTimeAfterLastAnimationRemoved) - { - Animation* animation2 = animationSystem.createAnimation(*m_animProperty, *m_spline, "animation"); - - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation, 2500u, 3000u)); - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*animation2, 1500u, 5000u)); - EXPECT_EQ(StatusOK, m_sequence->removeAnimation(*animation2)); - EXPECT_EQ(3000u, m_sequence->getAnimationSequenceStopTime()); - } - - TEST_F(AnimationSequenceTest, canGetSequenceStopTimeAfterSpeedIncrease) - { - Animation* animation2 = animationSystem.createAnimation(*m_animProperty, *m_spline, "animation"); - - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation, 2500u, 3000u)); - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*animation2, 1500u, 5000u)); - EXPECT_EQ(StatusOK, m_sequence->setPlaybackSpeed(2.f)); - EXPECT_EQ(2500u, m_sequence->getAnimationSequenceStopTime()); - } - - TEST_F(AnimationSequenceTest, canGetSequenceStopTimeAfterSpeedDecrease) - { - Animation* animation2 = animationSystem.createAnimation(*m_animProperty, *m_spline, "animation"); - - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation, 2500u, 3000u)); - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*animation2, 1500u, 5000u)); - EXPECT_EQ(StatusOK, m_sequence->setPlaybackSpeed(0.5f)); - EXPECT_EQ(10000u, m_sequence->getAnimationSequenceStopTime()); - } - - TEST_F(AnimationSequenceTest, startSetsStartTimeFromAnimationSystemForAllAnimationsInSequence) - { - EXPECT_EQ(StatusOK, animationSystem.setTime(3333u)); - - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - - EXPECT_EQ(StatusOK, m_sequence->start()); - - ExpectStartTimeWRTSequence(3333u, animations, *m_sequence); - } - - TEST_F(AnimationSequenceTest, startWithOffsetSetsStartTimeFromAnimationSystemForAllAnimationsInSequence) - { - EXPECT_EQ(StatusOK, animationSystem.setTime(3333u)); - - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - - EXPECT_EQ(StatusOK, m_sequence->start(555)); - - ExpectStartTimeWRTSequence(3333u + 555u, animations, *m_sequence); - } - - TEST_F(AnimationSequenceTest, startAtSetsStartTimeForAllAnimationsInSequence) - { - EXPECT_EQ(StatusOK, animationSystem.setTime(3333u)); - - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - - EXPECT_EQ(StatusOK, m_sequence->startAt(6666u)); - - ExpectStartTimeWRTSequence(6666u, animations, *m_sequence); - } - - TEST_F(AnimationSequenceTest, stopSetsStopTimeFromAnimationSystemForAllAnimationsInSequence) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - - EXPECT_EQ(StatusOK, animationSystem.setTime(3333u)); - EXPECT_EQ(StatusOK, m_sequence->start()); - - EXPECT_EQ(StatusOK, animationSystem.setTime(6666u)); - EXPECT_EQ(StatusOK, m_sequence->stop()); - - ExpectStopTime(6666u, animations); - } - - TEST_F(AnimationSequenceTest, stopWithOffsetSetsStopTimeFromAnimationSystemForAllAnimationsInSequence) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - - EXPECT_EQ(StatusOK, animationSystem.setTime(3333u)); - EXPECT_EQ(StatusOK, m_sequence->start()); - - EXPECT_EQ(StatusOK, animationSystem.setTime(6666u)); - EXPECT_EQ(StatusOK, m_sequence->stop(999u)); - - ExpectStopTime(6666u + 999u, animations); - } - - TEST_F(AnimationSequenceTest, stopAtSetsStopTimeForAllAnimationsInSequence) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - - EXPECT_EQ(StatusOK, m_sequence->start()); - EXPECT_EQ(StatusOK, m_sequence->stopAt(9999u)); - - ExpectStopTime(9999u, animations); - } - - TEST_F(AnimationSequenceTest, restartSetsProperStartAndStopTimeForAllAnimationsInSequence) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - - EXPECT_EQ(StatusOK, animationSystem.setTime(100u)); - EXPECT_EQ(StatusOK, m_sequence->start()); - EXPECT_EQ(StatusOK, animationSystem.setTime(4500u)); - EXPECT_EQ(StatusOK, m_sequence->stop()); - EXPECT_EQ(StatusOK, m_sequence->start()); - EXPECT_EQ(StatusOK, m_sequence->stopAt(9999u)); - - ExpectStartTimeWRTSequence(4500u, animations, *m_sequence); - ExpectStopTime(9999u, animations); - } - - TEST_F(AnimationSequenceTest, defaultStopTimeStampMatchesSplineDuration) - { - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation)); - - EXPECT_EQ(StatusOK, m_sequence->startAt(5000u)); - EXPECT_EQ(static_cast(5000u + SplineDuration), m_animation->getStopTime()); - } - - TEST_F(AnimationSequenceTest, stopWithoutStartDoesNothing) - { - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation)); - - const globalTimeStamp_t invalidTime = ramses_internal::AnimationTime::InvalidTimeStamp; - EXPECT_EQ(StatusOK, m_sequence->stop()); - EXPECT_EQ(invalidTime, m_animation->getStartTime()); - EXPECT_EQ(invalidTime, m_animation->getStopTime()); - EXPECT_EQ(StatusOK, m_sequence->stopAt(999u)); - EXPECT_EQ(invalidTime, m_animation->getStartTime()); - EXPECT_EQ(invalidTime, m_animation->getStopTime()); - } - - TEST_F(AnimationSequenceTest, setSequencePlaybackSpeedInvalidArg) - { - EXPECT_NE(StatusOK, m_sequence->setPlaybackSpeed(-1.f)); - EXPECT_NE(StatusOK, m_sequence->setPlaybackSpeed(0.f)); - } - - TEST_F(AnimationSequenceTest, setSequencePlaybackSpeed) - { - EXPECT_EQ(StatusOK, m_sequence->setPlaybackSpeed(10.f)); - EXPECT_EQ(10.f, m_sequence->getPlaybackSpeed()); - EXPECT_EQ(StatusOK, m_sequence->setPlaybackSpeed(1.f)); - EXPECT_EQ(1.f, m_sequence->getPlaybackSpeed()); - } - - TEST_F(AnimationSequenceTest, stoppingWhenAlreadyStoppedDoesNotChangeAnything) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - const AnimationSequenceTimes originalSequenceTimes = GetStartAndStopTimesWithinSequence(animations, *m_sequence); - - const globalTimeStamp_t sequenceStart = 10000u; - EXPECT_EQ(StatusOK, animationSystem.setTime(sequenceStart)); - EXPECT_EQ(StatusOK, m_sequence->start()); - - const globalTimeStamp_t sequenceStop = 16000u; - EXPECT_EQ(StatusOK, animationSystem.setTime(sequenceStop)); - EXPECT_EQ(StatusOK, m_sequence->stop()); - - EXPECT_EQ(StatusOK, animationSystem.setTime(sequenceStop + 1000u)); - EXPECT_EQ(StatusOK, m_sequence->stop()); - - ExpectStartAndStopTimesWithinSequenceAdjustedBySpeedFactor(originalSequenceTimes, 1.f, animations, *m_sequence); - ExpectStartTimeWRTSequence(sequenceStart, animations, *m_sequence); - ExpectStopTime(sequenceStop, animations); - } - - TEST_F(AnimationSequenceTest, stoppingAfterFinishedDoesNotChangeAnything) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - const AnimationSequenceTimes originalSequenceTimes = GetStartAndStopTimesWithinSequence(animations, *m_sequence); - - const globalTimeStamp_t sequenceStart = 10000u; - EXPECT_EQ(StatusOK, animationSystem.setTime(sequenceStart)); - EXPECT_EQ(StatusOK, m_sequence->start()); - - EXPECT_EQ(StatusOK, animationSystem.setTime(100000u)); - EXPECT_EQ(StatusOK, m_sequence->stop()); - - ExpectStartAndStopTimesWithinSequenceAdjustedBySpeedFactor(originalSequenceTimes, 1.f, animations, *m_sequence); - ExpectStartTimeWRTSequence(sequenceStart, animations, *m_sequence); - ExpectStopTimeWRTSequence(sequenceStart, animations, *m_sequence); - } - - TEST_F(AnimationSequenceTest, changeSequencePlaybackSpeedBeforeStart) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - const AnimationSequenceTimes originalSequenceTimes = GetStartAndStopTimesWithinSequence(animations, *m_sequence); - - const float newSpeed = 0.5f; - const float speedFactor = 1.f / newSpeed; - EXPECT_EQ(StatusOK, m_sequence->setPlaybackSpeed(newSpeed)); - - const globalTimeStamp_t sequenceStart = 10000u; - EXPECT_EQ(StatusOK, m_sequence->start(sequenceStart)); - - ExpectStartAndStopTimesWithinSequenceAdjustedBySpeedFactor(originalSequenceTimes, speedFactor, animations, *m_sequence); - ExpectStartTimeWRTSequence(sequenceStart, animations, *m_sequence); - ExpectStopTimeWRTSequence(sequenceStart, animations, *m_sequence); - } - - TEST_F(AnimationSequenceTest, changeSequencePlaybackSpeedAfterStart) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - const AnimationSequenceTimes originalSequenceTimes = GetStartAndStopTimesWithinSequence(animations, *m_sequence); - - const globalTimeStamp_t sequenceStart = 10000u; - EXPECT_EQ(StatusOK, animationSystem.setTime(sequenceStart)); - EXPECT_EQ(StatusOK, m_sequence->start()); - - const float newSpeed = 0.5f; - const float speedFactor = 1.f / newSpeed; - EXPECT_EQ(StatusOK, m_sequence->setPlaybackSpeed(newSpeed)); - - ExpectStartAndStopTimesWithinSequenceAdjustedBySpeedFactor(originalSequenceTimes, speedFactor, animations, *m_sequence); - ExpectStartTimeWRTSequence(sequenceStart, animations, *m_sequence); - ExpectStopTimeWRTSequence(sequenceStart, animations, *m_sequence); - } - - TEST_F(AnimationSequenceTest, changeSequencePlaybackSpeedDuringRestart) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - const AnimationSequenceTimes originalSequenceTimes = GetStartAndStopTimesWithinSequence(animations, *m_sequence); - - EXPECT_EQ(StatusOK, m_sequence->start()); - EXPECT_EQ(StatusOK, animationSystem.setTime(8500u)); - EXPECT_EQ(StatusOK, m_sequence->stop()); - - const globalTimeStamp_t sequenceStart = 10000u; - EXPECT_EQ(StatusOK, animationSystem.setTime(sequenceStart)); - - const float newSpeed = 2.f; - const float speedFactor = 1.f / newSpeed; - EXPECT_EQ(StatusOK, m_sequence->setPlaybackSpeed(newSpeed)); - EXPECT_EQ(StatusOK, m_sequence->start()); - - ExpectStartAndStopTimesWithinSequenceAdjustedBySpeedFactor(originalSequenceTimes, speedFactor, animations, *m_sequence); - ExpectStartTimeWRTSequence(sequenceStart, animations, *m_sequence); - ExpectStopTimeWRTSequence(sequenceStart, animations, *m_sequence); - } - - TEST_F(AnimationSequenceTest, slowDownSequencePlaybackSpeedWhilePlaying) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - const AnimationSequenceTimes originalSequenceTimes = GetStartAndStopTimesWithinSequence(animations, *m_sequence); - - const globalTimeStamp_t sequenceStart = 10000u; - EXPECT_EQ(StatusOK, m_sequence->start(sequenceStart)); - - const globalTimeStamp_t currentTime = 16000u; - EXPECT_EQ(StatusOK, animationSystem.setTime(currentTime)); - - const float newSpeed = 0.5f; - const float speedFactor = 1.f / newSpeed; - EXPECT_EQ(StatusOK, m_sequence->setPlaybackSpeed(newSpeed)); - - ExpectStartAndStopTimesWithinSequenceAdjustedBySpeedFactor(originalSequenceTimes, speedFactor, animations, *m_sequence); - const globalTimeStamp_t newSequenceStart = currentTime - static_cast((currentTime - sequenceStart) * speedFactor); - ExpectStartTimeWRTSequence(newSequenceStart, animations, *m_sequence); - ExpectStopTimeWRTSequence(newSequenceStart, animations, *m_sequence); - } - - TEST_F(AnimationSequenceTest, speedUpSequencePlaybackSpeedWhilePlaying) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - const AnimationSequenceTimes originalSequenceTimes = GetStartAndStopTimesWithinSequence(animations, *m_sequence); - - const globalTimeStamp_t sequenceStart = 10000u; - EXPECT_EQ(StatusOK, m_sequence->start(sequenceStart)); - - const globalTimeStamp_t currentTime = 16000u; - EXPECT_EQ(StatusOK, animationSystem.setTime(currentTime)); - - const float newSpeed = 2.f; - const float speedFactor = 1.f / newSpeed; - EXPECT_EQ(StatusOK, m_sequence->setPlaybackSpeed(newSpeed)); - - ExpectStartAndStopTimesWithinSequenceAdjustedBySpeedFactor(originalSequenceTimes, speedFactor, animations, *m_sequence); - const globalTimeStamp_t newSequenceStart = currentTime - static_cast((currentTime - sequenceStart) * speedFactor); - ExpectStartTimeWRTSequence(newSequenceStart, animations, *m_sequence); - ExpectStopTimeWRTSequence(newSequenceStart, animations, *m_sequence); - } - - TEST_F(AnimationSequenceTest, changingSequencePlaybackSpeedAfterItFinishedDoesNotChangeGlobalTimeRangesOfItsAnimations) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - const AnimationSequenceTimes originalSequenceTimes = GetStartAndStopTimesWithinSequence(animations, *m_sequence); - - const globalTimeStamp_t sequenceStart = 10000u; - EXPECT_EQ(StatusOK, animationSystem.setTime(sequenceStart)); - EXPECT_EQ(StatusOK, m_sequence->start()); - - EXPECT_EQ(StatusOK, animationSystem.setTime(100000u)); - - const float newSpeed = 0.5f; - const float speedFactor = 1.f / newSpeed; - EXPECT_EQ(StatusOK, m_sequence->setPlaybackSpeed(newSpeed)); - - ExpectStartAndStopTimesWithinSequenceAdjustedBySpeedFactor(originalSequenceTimes, speedFactor, animations, *m_sequence); - ExpectStartTimeWRTSequence(sequenceStart, animations, originalSequenceTimes); - ExpectStopTimeWRTSequence(sequenceStart, animations, originalSequenceTimes); - } - - TEST_F(AnimationSequenceTest, changingSequencePlaybackSpeedAfterItStoppedDoesNotChangeGlobalTimeRangesOfItsAnimations) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - const AnimationSequenceTimes originalSequenceTimes = GetStartAndStopTimesWithinSequence(animations, *m_sequence); - - const globalTimeStamp_t sequenceStart = 10000u; - EXPECT_EQ(StatusOK, animationSystem.setTime(sequenceStart)); - EXPECT_EQ(StatusOK, m_sequence->start()); - - const globalTimeStamp_t sequenceStop = 16000u; - EXPECT_EQ(StatusOK, animationSystem.setTime(sequenceStop)); - EXPECT_EQ(StatusOK, m_sequence->stop()); - - const float newSpeed = 0.5f; - const float speedFactor = 1.f / newSpeed; - EXPECT_EQ(StatusOK, m_sequence->setPlaybackSpeed(newSpeed)); - - ExpectStartAndStopTimesWithinSequenceAdjustedBySpeedFactor(originalSequenceTimes, speedFactor, animations, *m_sequence); - ExpectStartTimeWRTSequence(sequenceStart, animations, originalSequenceTimes); - ExpectStopTime(sequenceStop, animations); - } - - TEST_F(AnimationSequenceTest, setAnimationLooping) - { - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation)); - - EXPECT_EQ(StatusOK, m_sequence->setAnimationLooping(*m_animation)); - EXPECT_TRUE(m_sequence->isAnimationLooping(*m_animation)); - EXPECT_EQ(0, m_sequence->getAnimationLoopDuration(*m_animation)); - } - - TEST_F(AnimationSequenceTest, setAnimationLoopingWithDuration) - { - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation)); - - EXPECT_EQ(StatusOK, m_sequence->setAnimationLooping(*m_animation, 2000)); - EXPECT_TRUE(m_sequence->isAnimationLooping(*m_animation)); - EXPECT_EQ(2000, m_sequence->getAnimationLoopDuration(*m_animation)); - } - - TEST_F(AnimationSequenceTest, setAnimationLoopingWithNegativeDurationTreatedAsZeroDuration) - { - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation)); - - EXPECT_EQ(StatusOK, m_sequence->setAnimationLooping(*m_animation, -2000)); - EXPECT_TRUE(m_sequence->isAnimationLooping(*m_animation)); - EXPECT_EQ(0, m_sequence->getAnimationLoopDuration(*m_animation)); - } - - TEST_F(AnimationSequenceTest, setAnimationRelative) - { - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation)); - - EXPECT_EQ(StatusOK, m_sequence->setAnimationRelative(*m_animation)); - EXPECT_TRUE(m_sequence->isAnimationRelative(*m_animation)); - } - - TEST_F(AnimationSequenceTest, setAnimationAbsolute) - { - EXPECT_EQ(StatusOK, m_sequence->addAnimation(*m_animation)); - - EXPECT_EQ(StatusOK, m_sequence->setAnimationRelative(*m_animation)); - EXPECT_EQ(StatusOK, m_sequence->setAnimationAbsolute(*m_animation)); - EXPECT_FALSE(m_sequence->isAnimationRelative(*m_animation)); - } - - TEST_F(AnimationSequenceTest, canValidate) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - EXPECT_EQ(StatusOK, m_sequence->validate()); - } - - TEST_F(AnimationSequenceTest, failsValidationIfEmpty) - { - EXPECT_NE(StatusOK, m_sequence->validate()); - } - - TEST_F(AnimationSequenceTest, failsValidationIfOneOfAnimationsDangling) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - this->animationSystem.destroy(*animations.back()); - EXPECT_NE(StatusOK, m_sequence->validate()); - } - - TEST_F(AnimationSequenceTest, failsValidationIfOneOfAnimationsInvalid) - { - AnimationVector animations; - createSomeAnimationsAndPutToSequence(animations); - this->animationSystem.destroy(*m_spline); - EXPECT_NE(StatusOK, m_sequence->validate()); - } -} diff --git a/client/ramses-client/test/AnimationSystemObjectIteratorTest.cpp b/client/ramses-client/test/AnimationSystemObjectIteratorTest.cpp deleted file mode 100644 index 7da984964..000000000 --- a/client/ramses-client/test/AnimationSystemObjectIteratorTest.cpp +++ /dev/null @@ -1,90 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -// API -#include -#include "RamsesClientImpl.h" -#include "Utils/File.h" -#include "ramses-client-api/RamsesClient.h" -#include "RamsesObjectTestTypes.h" -#include "RamsesObjectTestTypes.h" -#include "ClientTestUtils.h" -#include "ramses-client-api/AnimationSystemObjectIterator.h" -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/SplineStepBool.h" -#include "ramses-client-api/SplineStepFloat.h" -#include "ramses-client-api/SplineStepVector4i.h" -#include "ramses-client-api/SplineStepVector4f.h" -#include "ramses-client-api/SplineStepVector3i.h" -#include "ramses-client-api/SplineStepVector3f.h" -#include "ramses-client-api/SplineStepVector2i.h" -#include "ramses-client-api/SplineStepVector2f.h" -#include "ramses-client-api/SplineStepInt32.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/SplineLinearVector4i.h" -#include "ramses-client-api/SplineLinearVector4f.h" -#include "ramses-client-api/SplineLinearVector3i.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/SplineLinearVector2i.h" -#include "ramses-client-api/SplineLinearVector2f.h" -#include "ramses-client-api/SplineLinearInt32.h" -#include "ramses-client-api/SplineBezierFloat.h" -#include "ramses-client-api/SplineBezierVector4i.h" -#include "ramses-client-api/SplineBezierVector4f.h" -#include "ramses-client-api/SplineBezierVector3i.h" -#include "ramses-client-api/SplineBezierVector3f.h" -#include "ramses-client-api/SplineBezierVector2i.h" -#include "ramses-client-api/SplineBezierVector2f.h" -#include "ramses-client-api/SplineBezierInt32.h" -#include "ramses-utils.h" -#include "RamsesObjectTypeTraits.h" - -namespace ramses -{ - template - class AnimationSystemObjectIteratorTest : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test - { - public: - AnimationSystemObjectIteratorTest() : LocalTestClientWithSceneAndAnimationSystem() - { - } - ramses::ERamsesObjectType getTypeIDForObjectType() - { - return ramses::TYPE_ID_OF_RAMSES_OBJECT::ID; - } - }; - - TYPED_TEST_SUITE(AnimationSystemObjectIteratorTest, ramses::AnimationObjectTypes); - - TYPED_TEST(AnimationSystemObjectIteratorTest, ObjectIteratorReturnsMultipleObjects) - { - const RamsesObject* ro1 = &this->template createObject("ro1"); - const RamsesObject* ro2 = &this->template createObject("ro2"); - - AnimationSystemObjectIterator iterator(this->animationSystem, this->getTypeIDForObjectType()); - - RamsesObject* iteratedObject1 = iterator.getNext(); - RamsesObject* iteratedObject2 = iterator.getNext(); - - ASSERT_TRUE(nullptr != iteratedObject1); - ASSERT_TRUE(nullptr != iteratedObject2); - EXPECT_TRUE(ro1 == iteratedObject1); - EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(*ro1)); - EXPECT_TRUE(ro2 == iteratedObject2); - EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(*ro2)); - EXPECT_TRUE(nullptr == iterator.getNext()); - } - - TYPED_TEST(AnimationSystemObjectIteratorTest, ObjectIteratorDirectlyReturnsNullIfNoObjectsAvailable) - { - AnimationSystemObjectIterator iterator(this->animationSystem, this->getTypeIDForObjectType()); - - EXPECT_TRUE(nullptr == iterator.getNext()); - } -} diff --git a/client/ramses-client/test/AnimationSystemTest.cpp b/client/ramses-client/test/AnimationSystemTest.cpp deleted file mode 100644 index a7d80bedc..000000000 --- a/client/ramses-client/test/AnimationSystemTest.cpp +++ /dev/null @@ -1,534 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include - -#include "ClientTestUtils.h" -#include "TestEffectCreator.h" -#include "AnimationSystemImpl.h" -#include "AnimationAPI/IAnimationSystem.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSystemRealTime.h" -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/UniformInput.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-utils.h" - -using namespace testing; - -namespace ramses -{ - class AnimatedProperty; - class Animation; - - class AnimationSystemTest : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test - { - public: - explicit AnimationSystemTest(uint32_t flags = EAnimationSystemFlags_Default) - : LocalTestClientWithSceneAndAnimationSystem(flags) - { - } - - protected: - AnimationSequence* createAnimationSequence(Animation** animation = nullptr, Node** animatedNode = nullptr, EAnimatedProperty propertyToAnimate = EAnimatedProperty_Translation) - { - SplineLinearVector3f* spline = animationSystem.createSplineLinearVector3f("spline"); - EXPECT_FALSE(spline == nullptr); - - spline->setKey(0u, 0.0f, 0.0f, 0.0f); - spline->setKey(5000u, 1.0f, 1.0f, 1.0f); - - Node* node = m_scene.createNode("node"); - EXPECT_FALSE(node == nullptr); - node->setTranslation(1.0f, 1.0f, 1.0f); - node->setRotation(1.f, 2.f, 3.f); - - AnimatedProperty* prop = animationSystem.createAnimatedProperty(*node, propertyToAnimate, EAnimatedPropertyComponent_All); - EXPECT_FALSE(prop == nullptr); - - Animation* anim = animationSystem.createAnimation(*prop, *spline, "anim"); - EXPECT_FALSE(anim == nullptr); - - AnimationSequence* sequence = animationSystem.createAnimationSequence("sequence"); - EXPECT_FALSE(sequence == nullptr); - - sequence->addAnimation(*anim); - - if (animation != nullptr) - { - *animation = anim; - } - if (animatedNode != nullptr) - { - *animatedNode = node; - } - - return sequence; - } - }; - - class AnimationSystemTestClientSideProcessing : public AnimationSystemTest - { - public: - AnimationSystemTestClientSideProcessing() - : AnimationSystemTest(EAnimationSystemFlags_ClientSideProcessing) - { - } - }; - - // Valid creation cases are tested in ownership test and other global RamsesObject tests. - - TEST_F(AnimationSystemTest, initialTime) - { - EXPECT_EQ(0u, this->animationSystem.getTime()); - } - - TEST_F(AnimationSystemTest, setGetTime) - { - EXPECT_EQ(StatusOK, this->animationSystem.setTime(333u)); - EXPECT_EQ(333u, this->animationSystem.getTime()); - } - - TEST_F(AnimationSystemTest, createRealTimeAnimationSystem) - { - AnimationSystemRealTime& rtAnimSystem = *m_scene.createRealTimeAnimationSystem(ramses::EAnimationSystemFlags_ClientSideProcessing, "rt anim system"); - EXPECT_EQ(StatusOK, rtAnimSystem.updateLocalTime()); - EXPECT_NE(0u, rtAnimSystem.getTime()); - EXPECT_TRUE(rtAnimSystem.impl.getIAnimationSystem().isRealTime()); - EXPECT_FALSE(rtAnimSystem.impl.getIAnimationSystem().useSynchronizedClock()); - } - - TEST_F(AnimationSystemTest, createRealTimeAnimationSystemWithPtpTime) - { - AnimationSystemRealTime& rtAnimSystem = *m_scene.createRealTimeAnimationSystem(ramses::EAnimationSystemFlags_SynchronizedClock, "rt anim system"); - EXPECT_EQ(StatusOK, rtAnimSystem.updateLocalTime()); - EXPECT_NE(0u, rtAnimSystem.getTime()); - EXPECT_TRUE(rtAnimSystem.impl.getIAnimationSystem().isRealTime()); - EXPECT_TRUE(rtAnimSystem.impl.getIAnimationSystem().useSynchronizedClock()); - } - - TEST_F(AnimationSystemTest, createAnimationSystemIgnoreSynchronizedClock) - { - AnimationSystem& animSystem = *m_scene.createAnimationSystem(ramses::EAnimationSystemFlags_SynchronizedClock, "anim system"); - EXPECT_EQ(0u, animSystem.getTime()); - EXPECT_FALSE(animSystem.impl.getIAnimationSystem().isRealTime()); - EXPECT_FALSE(animSystem.impl.getIAnimationSystem().useSynchronizedClock()); - } - - TEST_F(AnimationSystemTest, realTimeAnimationSystemSetsLocalTime) - { - AnimationSystemRealTime& rtAnimSystem = this->createObject("rt anim system"); - EXPECT_EQ(StatusOK, rtAnimSystem.updateLocalTime(333u)); - EXPECT_EQ(333u, rtAnimSystem.getTime()); - } - - TEST_F(AnimationSystemTest, createAnimationWithSplineFromDifferentAnimationSystemFails) - { - AnimationSystem* otherAnimationSystem = this->getScene().createAnimationSystem(); - SplineLinearVector3f* spline = otherAnimationSystem->createSplineLinearVector3f("spline"); - EXPECT_FALSE(spline == nullptr); - Node* node = this->m_scene.createNode("node"); - EXPECT_FALSE(node == nullptr); - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation); - EXPECT_FALSE(prop == nullptr); - - Animation* anim = this->animationSystem.createAnimation(*prop, *spline, "anim"); - EXPECT_TRUE(anim == nullptr); - } - - TEST_F(AnimationSystemTest, createAnimationWithAnimatedPropertyFromDifferentAnimationSystemFails) - { - AnimationSystem* otherAnimationSystem = this->getScene().createAnimationSystem(); - SplineLinearVector3f* spline = this->animationSystem.createSplineLinearVector3f("spline"); - EXPECT_FALSE(spline == nullptr); - Node* node = this->m_scene.createNode("node"); - EXPECT_FALSE(node == nullptr); - AnimatedProperty* prop = otherAnimationSystem->createAnimatedProperty(*node, EAnimatedProperty_Translation); - EXPECT_FALSE(prop == nullptr); - - Animation* anim = this->animationSystem.createAnimation(*prop, *spline, "anim"); - EXPECT_TRUE(anim == nullptr); - } - - TEST_F(AnimationSystemTest, createAnimationWithSplineAndPropertyDataMismatchFails) - { - SplineLinearFloat* spline = this->animationSystem.createSplineLinearFloat("spline"); - EXPECT_FALSE(spline == nullptr); - Node* node = this->m_scene.createNode("node"); - EXPECT_FALSE(node == nullptr); - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation); - EXPECT_FALSE(prop == nullptr); - - Animation* anim = this->animationSystem.createAnimation(*prop, *spline, "anim"); - EXPECT_TRUE(anim == nullptr); - } - - TEST_F(AnimationSystemTest, createAnimationPropertyToRotateNonLegacyRotationFails) - { - Node* node = this->m_scene.createNode("node"); - EXPECT_FALSE(node == nullptr); - node->setRotation(1.f, 2.f, 3.f, ramses::ERotationConvention::XYX); - - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Rotation); - EXPECT_TRUE(prop == nullptr); - } - - TEST_F(AnimationSystemTest, createAnimationPropertyToTranslateOrScaleNonLegacyRotationSucceeds) - { - Node* node = this->m_scene.createNode("node"); - EXPECT_FALSE(node == nullptr); - node->setRotation(1.f, 2.f, 3.f, ramses::ERotationConvention::XYX); - - AnimatedProperty* prop1 = this->animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation); - EXPECT_FALSE(prop1 == nullptr); - - AnimatedProperty* prop2 = this->animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Scaling); - EXPECT_FALSE(prop2 == nullptr); - } - - TEST_F(AnimationSystemTest, createAnimationWithSplineAndPropertyComponentMismatchFails) - { - SplineLinearVector3f* spline = this->animationSystem.createSplineLinearVector3f("spline"); - EXPECT_FALSE(spline == nullptr); - Node* node = this->m_scene.createNode("node"); - EXPECT_FALSE(node == nullptr); - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation, EAnimatedPropertyComponent_Y); - EXPECT_FALSE(prop == nullptr); - - Animation* anim = this->animationSystem.createAnimation(*prop, *spline, "anim"); - EXPECT_TRUE(anim == nullptr); - } - - TEST_F(AnimationSystemTest, realTimeAnimationSystemCanBeConvertedToAnimationSystem) - { - RamsesObject& obj = this->createObject("anim system"); - EXPECT_TRUE(RamsesUtils::TryConvert(obj) != nullptr); - const RamsesObject& constObj = obj; - EXPECT_TRUE(RamsesUtils::TryConvert(constObj) != nullptr); - } - - TEST_F(AnimationSystemTest, retrieveAnimationFromFinishedAnimationsWhenAnimationFinished) - { - Animation* animation = nullptr; - AnimationSequence* sequence = createAnimationSequence(&animation); - sequence->startAt(0u); - - EXPECT_EQ(0u, animationSystem.getNumberOfFinishedAnimationsSincePreviousUpdate()); - - animationSystem.setTime(5000u); - EXPECT_EQ(1u, animationSystem.getNumberOfFinishedAnimationsSincePreviousUpdate()); - EXPECT_EQ(animation, animationSystem.getFinishedAnimationSincePreviousUpdate(0u)); - } - - TEST_F(AnimationSystemTest, retrieveAnimationFromFinishedAnimationsWhenAnimationStopped) - { - Animation* animation = nullptr; - AnimationSequence* sequence = createAnimationSequence(&animation); - sequence->startAt(0u); - - EXPECT_EQ(0u, animationSystem.getNumberOfFinishedAnimationsSincePreviousUpdate()); - - animationSystem.setTime(1000u); - sequence->stop(); - - EXPECT_EQ(1u, animationSystem.getNumberOfFinishedAnimationsSincePreviousUpdate()); - EXPECT_EQ(animation, animationSystem.getFinishedAnimationSincePreviousUpdate(0u)); - } - - TEST_F(AnimationSystemTest, canValidate) - { - Animation* animation = nullptr; - createAnimationSequence(&animation); - EXPECT_EQ(StatusOK, animationSystem.validate()); - } - - TEST_F(AnimationSystemTest, canValidate_SucceedsIfTranslatingOrScalingNodeWithNonLegacyRotation) - { - Animation* animation = nullptr; - Node* node = nullptr; - - //translation - createAnimationSequence(&animation, &node, EAnimatedProperty_Translation); - node->setRotation(1.f, 2.f, 3.f, ramses::ERotationConvention::XYX); - EXPECT_EQ(StatusOK, animationSystem.validate()); - - //scaling - createAnimationSequence(&animation, &node, EAnimatedProperty_Scaling); - node->setRotation(1.f, 2.f, 3.f, ramses::ERotationConvention::XYX); - EXPECT_EQ(StatusOK, animationSystem.validate()); - } - - TEST_F(AnimationSystemTest, validationReportsContainsObjectCounts) - { - // 1 sequence - createAnimationSequence(); - - // 4 splines in total - SplineLinearVector3f* spline2 = animationSystem.createSplineLinearVector3f("spline2"); - SplineLinearVector3f* spline3 = animationSystem.createSplineLinearVector3f("spline3"); - SplineLinearVector3f* spline4 = animationSystem.createSplineLinearVector3f("spline4"); - spline2->setKey(0u, 0.0f, 0.0f, 0.0f); - spline3->setKey(0u, 0.0f, 0.0f, 0.0f); - spline4->setKey(0u, 0.0f, 0.0f, 0.0f); - - // 3 animated properties in total - Node* node2 = m_scene.createNode("node2"); - Node* node3 = m_scene.createNode("node3"); - AnimatedProperty* prop2 = animationSystem.createAnimatedProperty(*node2, EAnimatedProperty_Translation, EAnimatedPropertyComponent_All); - animationSystem.createAnimatedProperty(*node3, EAnimatedProperty_Translation, EAnimatedPropertyComponent_All); - - // 2 animations in total - animationSystem.createAnimation(*prop2, *spline2, "anim"); - - EXPECT_EQ(StatusOK, animationSystem.validate()); - const ramses_internal::String validationReport = animationSystem.getValidationReport(); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("Number of ERamsesObjectType_Animation instances: 2")); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("Number of ERamsesObjectType_AnimationSequence instances: 1")); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("Number of ERamsesObjectType_AnimatedProperty instances: 3")); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("Number of ERamsesObjectType_SplineLinearVector3f instances: 4")); - } - - TEST_F(AnimationSystemTest, failsToValidateIfOneOfItsObjectsNotValidated) - { - Animation* animation = nullptr; - Node* node = nullptr; - createAnimationSequence(&animation, &node); - this->animationSystem.destroy(*animation); - this->getScene().destroy(*node); - - EXPECT_NE(StatusOK, animationSystem.validate()); - - const ramses_internal::String validationReport = animationSystem.getValidationReport(); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("Number of ERamsesObjectType_Animation instances: 0")); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("Number of ERamsesObjectType_AnimationSequence instances: 1")); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("Number of ERamsesObjectType_AnimatedProperty instances: 1")); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("Number of ERamsesObjectType_SplineLinearVector3f instances: 1")); - } - - TEST_F(AnimationSystemTest, failsToValidateIfDataBindingIsNotValid) - { - Animation* animation = nullptr; - Node* node = nullptr; - createAnimationSequence(&animation, &node, EAnimatedProperty_Rotation); - this->getScene().destroy(*node); - - EXPECT_NE(StatusOK, animationSystem.validate()); - - const ramses_internal::String validationReport = animationSystem.getValidationReport(); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("Number of ERamsesObjectType_AnimationSequence instances: 1")); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("Number of ERamsesObjectType_AnimatedProperty instances: 1")); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("Number of ERamsesObjectType_SplineLinearVector3f instances: 1")); - } - - TEST_F(AnimationSystemTest, failsToValidateIfNodeRotatesUsingNonLegacyConvention) - { - Animation* animation = nullptr; - Node* node = nullptr; - createAnimationSequence(&animation, &node, EAnimatedProperty_Rotation); - node->setRotation(1.f, 2.f, 3.f, ramses::ERotationConvention::XYX); - - EXPECT_NE(StatusOK, animationSystem.validate()); - - const ramses_internal::String validationReport = animationSystem.getValidationReport(); - EXPECT_THAT(validationReport.stdRef(), HasSubstr("trying to animate rotation for node that does not use legacy rotation convention")); - } - - TEST_F(AnimationSystemTestClientSideProcessing, createAnimationSystemWithClientSideProcessingAndCheckInterpolationResults) - { - Node* node = nullptr; - AnimationSequence* sequence = createAnimationSequence(nullptr, &node); - sequence->startAt(0u); - - this->animationSystem.setTime(2500u); - - float x = 0.0f; - float y = 0.0f; - float z = 0.0f; - node->getTranslation(x, y, z); - - const float EPSILON = 1e-6f; - EXPECT_NEAR(x, 0.5f, EPSILON); - EXPECT_NEAR(y, 0.5f, EPSILON); - EXPECT_NEAR(z, 0.5f, EPSILON); - } - - TEST_F(AnimationSystemTestClientSideProcessing, canAnimatePropertyTranslateNode) - { - Node& animatable = this->createObject("animatable"); - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(animatable, EAnimatedProperty_Translation, EAnimatedPropertyComponent_All); - ASSERT_TRUE(prop != nullptr); - - SplineLinearVector3f* spline = this->animationSystem.createSplineLinearVector3f(); - ASSERT_TRUE(spline != nullptr); - const ramses_internal::Vector3 startValue(0.f); - const ramses_internal::Vector3 endValue(1.f); - EXPECT_EQ(StatusOK, spline->setKey(0u, startValue.x, startValue.y, startValue.z)); - EXPECT_EQ(StatusOK, spline->setKey(100u, endValue.x, endValue.y, endValue.z)); - - Animation* animation = this->animationSystem.createAnimation(*prop, *spline); - ASSERT_TRUE(animation != nullptr); - - AnimationSequence* animSequence = this->animationSystem.createAnimationSequence(); - ASSERT_TRUE(animSequence != nullptr); - EXPECT_EQ(StatusOK, animSequence->addAnimation(*animation)); - EXPECT_EQ(StatusOK, animSequence->startAt(0u)); - - ramses_internal::Vector3 value(startValue); - EXPECT_EQ(StatusOK, animatable.setTranslation(value.x, value.y, value.z)); - - EXPECT_EQ(StatusOK, this->animationSystem.setTime(50u)); - EXPECT_EQ(StatusOK, animatable.getTranslation(value.x, value.y, value.z)); - EXPECT_NE(startValue, value); - - EXPECT_EQ(StatusOK, this->animationSystem.setTime(150u)); - EXPECT_EQ(StatusOK, animatable.getTranslation(value.x, value.y, value.z)); - EXPECT_EQ(endValue, value); - } - - TEST_F(AnimationSystemTestClientSideProcessing, canAnimatePropertyRotateNode) - { - Node& animatable = this->createObject("animatable"); - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(animatable, EAnimatedProperty_Rotation, EAnimatedPropertyComponent_All); - ASSERT_TRUE(prop != nullptr); - - SplineLinearVector3f* spline = this->animationSystem.createSplineLinearVector3f(); - ASSERT_TRUE(spline != nullptr); - const ramses_internal::Vector3 startValue(0.f); - const ramses_internal::Vector3 endValue(1.f); - EXPECT_EQ(StatusOK, spline->setKey(0u, startValue.x, startValue.y, startValue.z)); - EXPECT_EQ(StatusOK, spline->setKey(100u, endValue.x, endValue.y, endValue.z)); - - Animation* animation = this->animationSystem.createAnimation(*prop, *spline); - ASSERT_TRUE(animation != nullptr); - - AnimationSequence* animSequence = this->animationSystem.createAnimationSequence(); - ASSERT_TRUE(animSequence != nullptr); - EXPECT_EQ(StatusOK, animSequence->addAnimation(*animation)); - EXPECT_EQ(StatusOK, animSequence->startAt(0u)); - - ramses_internal::Vector3 value(startValue); - EXPECT_EQ(StatusOK, animatable.setRotation(value.x, value.y, value.z)); - - EXPECT_EQ(StatusOK, this->animationSystem.setTime(50u)); - EXPECT_EQ(StatusOK, animatable.getRotation(value.x, value.y, value.z)); - EXPECT_NE(startValue, value); - - EXPECT_EQ(StatusOK, this->animationSystem.setTime(150u)); - EXPECT_EQ(StatusOK, animatable.getRotation(value.x, value.y, value.z)); - EXPECT_EQ(endValue, value); - } - - TEST_F(AnimationSystemTestClientSideProcessing, canAnimatePropertyScaleNode) - { - Node& animatable = this->createObject("animatable"); - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(animatable, EAnimatedProperty_Scaling, EAnimatedPropertyComponent_All); - ASSERT_TRUE(prop != nullptr); - - SplineLinearVector3f* spline = this->animationSystem.createSplineLinearVector3f(); - ASSERT_TRUE(spline != nullptr); - const ramses_internal::Vector3 startValue(0.f); - const ramses_internal::Vector3 endValue(1.f); - EXPECT_EQ(StatusOK, spline->setKey(0u, startValue.x, startValue.y, startValue.z)); - EXPECT_EQ(StatusOK, spline->setKey(100u, endValue.x, endValue.y, endValue.z)); - - Animation* animation = this->animationSystem.createAnimation(*prop, *spline); - ASSERT_TRUE(animation != nullptr); - - AnimationSequence* animSequence = this->animationSystem.createAnimationSequence(); - ASSERT_TRUE(animSequence != nullptr); - EXPECT_EQ(StatusOK, animSequence->addAnimation(*animation)); - EXPECT_EQ(StatusOK, animSequence->startAt(0u)); - - ramses_internal::Vector3 value(startValue); - EXPECT_EQ(StatusOK, animatable.setScaling(value.x, value.y, value.z)); - - EXPECT_EQ(StatusOK, this->animationSystem.setTime(50u)); - EXPECT_EQ(StatusOK, animatable.getScaling(value.x, value.y, value.z)); - EXPECT_NE(startValue, value); - - EXPECT_EQ(StatusOK, this->animationSystem.setTime(150u)); - EXPECT_EQ(StatusOK, animatable.getScaling(value.x, value.y, value.z)); - EXPECT_EQ(endValue, value); - } - - TEST_F(AnimationSystemTestClientSideProcessing, canAnimatePropertyUniformInput) - { - Effect* effect = TestEffectCreator::createEffect(this->m_scene, false); - ASSERT_TRUE(effect != nullptr); - UniformInput uniformInput; - EXPECT_EQ(StatusOK, effect->findUniformInput("vec3fInput", uniformInput)); - - Appearance* appearance = this->m_scene.createAppearance(*effect); - ASSERT_TRUE(appearance != nullptr); - - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(uniformInput, *appearance, EAnimatedPropertyComponent_All); - ASSERT_TRUE(prop != nullptr); - - SplineLinearVector3f* spline = this->animationSystem.createSplineLinearVector3f(); - ASSERT_TRUE(spline != nullptr); - const ramses_internal::Vector3 startValue(0.f); - const ramses_internal::Vector3 endValue(1.f); - EXPECT_EQ(StatusOK, spline->setKey(0u, startValue.x, startValue.y, startValue.z)); - EXPECT_EQ(StatusOK, spline->setKey(100u, endValue.x, endValue.y, endValue.z)); - - Animation* animation = this->animationSystem.createAnimation(*prop, *spline); - ASSERT_TRUE(animation != nullptr); - - AnimationSequence* animSequence = this->animationSystem.createAnimationSequence(); - ASSERT_TRUE(animSequence != nullptr); - EXPECT_EQ(StatusOK, animSequence->addAnimation(*animation)); - EXPECT_EQ(StatusOK, animSequence->startAt(0u)); - - ramses_internal::Vector3 value(startValue); - EXPECT_EQ(StatusOK, appearance->setInputValueVector3f(uniformInput, value.x, value.y, value.z)); - - EXPECT_EQ(StatusOK, this->animationSystem.setTime(50u)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector3f(uniformInput, value.x, value.y, value.z)); - EXPECT_NE(startValue, value); - - EXPECT_EQ(StatusOK, this->animationSystem.setTime(150u)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector3f(uniformInput, value.x, value.y, value.z)); - EXPECT_EQ(endValue, value); - } - - TEST_F(AnimationSystemTestClientSideProcessing, canAnimatePropertyDataObject) - { - DataVector3f& animatable = this->createObject("animatable"); - - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(animatable, EAnimatedPropertyComponent_All); - ASSERT_TRUE(prop != nullptr); - - SplineLinearVector3f* spline = this->animationSystem.createSplineLinearVector3f(); - ASSERT_TRUE(spline != nullptr); - const ramses_internal::Vector3 startValue(0.f); - const ramses_internal::Vector3 endValue(1.f); - EXPECT_EQ(StatusOK, spline->setKey(0u, startValue.x, startValue.y, startValue.z)); - EXPECT_EQ(StatusOK, spline->setKey(100u, endValue.x, endValue.y, endValue.z)); - - Animation* animation = this->animationSystem.createAnimation(*prop, *spline); - ASSERT_TRUE(animation != nullptr); - - AnimationSequence* animSequence = this->animationSystem.createAnimationSequence(); - ASSERT_TRUE(animSequence != nullptr); - EXPECT_EQ(StatusOK, animSequence->addAnimation(*animation)); - EXPECT_EQ(StatusOK, animSequence->startAt(0u)); - - ramses_internal::Vector3 value(startValue); - EXPECT_EQ(StatusOK, animatable.setValue(value.x, value.y, value.z)); - - EXPECT_EQ(StatusOK, this->animationSystem.setTime(50u)); - EXPECT_EQ(StatusOK, animatable.getValue(value.x, value.y, value.z)); - EXPECT_NE(startValue, value); - - EXPECT_EQ(StatusOK, this->animationSystem.setTime(150u)); - EXPECT_EQ(StatusOK, animatable.getValue(value.x, value.y, value.z)); - EXPECT_EQ(endValue, value); - } -} diff --git a/client/ramses-client/test/AnimationTest.cpp b/client/ramses-client/test/AnimationTest.cpp deleted file mode 100644 index cbadc5523..000000000 --- a/client/ramses-client/test/AnimationTest.cpp +++ /dev/null @@ -1,112 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include - -#include "ClientTestUtils.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/AnimatedProperty.h" -#include "ramses-client-api/DataFloat.h" -#include "AnimationSystemImpl.h" -#include "AnimationImpl.h" - -#include "Animation/AnimationTime.h" - -using namespace testing; - -namespace ramses -{ - class AnimationTest : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test - { - public: - virtual void SetUp() override - { - m_animatedNode = this->m_scene.createNode(); - - m_spline = this->animationSystem.createSplineLinearFloat("spline"); - m_animProperty = this->animationSystem.createAnimatedProperty(*m_animatedNode, EAnimatedProperty_Translation, EAnimatedPropertyComponent_X, "prop"); - m_animation = this->animationSystem.createAnimation(*m_animProperty, *m_spline, "animation"); - } - - protected: - void setUpSpline() - { - m_spline->setKey(0u, 1.f); - m_spline->setKey(2000, 0.f); - } - - Node* m_animatedNode; - SplineLinearFloat* m_spline; - AnimatedProperty* m_animProperty; - Animation* m_animation; - }; - - TEST_F(AnimationTest, initialValues) - { - const globalTimeStamp_t invalidTime = ramses_internal::AnimationTime::InvalidTimeStamp; - EXPECT_EQ(invalidTime, m_animation->getStartTime()); - EXPECT_EQ(invalidTime, m_animation->getStopTime()); - EXPECT_NE(ramses_internal::AnimationHandle::Invalid(), m_animation->impl.getAnimationHandle()); - } - - TEST_F(AnimationTest, canValidate) - { - setUpSpline(); - EXPECT_EQ(StatusOK, m_animation->validate()); - } - - TEST_F(AnimationTest, failsToValidateIfAnimatedPropertyDestroyed) - { - setUpSpline(); - this->animationSystem.destroy(*m_animProperty); - EXPECT_NE(StatusOK, m_animation->validate()); - } - - TEST_F(AnimationTest, failsToValidateIfAnimatedNodeDestroyed) - { - setUpSpline(); - this->m_scene.destroy(*m_animatedNode); - EXPECT_NE(StatusOK, m_animation->validate()); - } - - TEST_F(AnimationTest, failsToValidateIfSplineDestroyed) - { - setUpSpline(); - this->animationSystem.destroy(*m_spline); - EXPECT_NE(StatusOK, m_animation->validate()); - } - - TEST_F(AnimationTest, failsToValidateIfSplineEmpty) - { - EXPECT_NE(StatusOK, m_animation->validate()); - } - - TEST_F(AnimationTest, validatesWithAnimatedDataObject) - { - DataFloat& animatable = this->createObject(); - setUpSpline(); - - AnimatedProperty* animProperty = this->animationSystem.createAnimatedProperty(animatable, EAnimatedPropertyComponent_X, "prop"); - Animation* animation = this->animationSystem.createAnimation(*animProperty, *m_spline, "animation"); - EXPECT_EQ(StatusOK, animation->validate()); - } - - TEST_F(AnimationTest, failsToValidateIfAnimatedDataObjectDestroyed) - { - DataFloat& animatable = this->createObject(); - setUpSpline(); - - AnimatedProperty* animProperty = this->animationSystem.createAnimatedProperty(animatable, EAnimatedPropertyComponent_X, "prop"); - Animation* animation = this->animationSystem.createAnimation(*animProperty, *m_spline, "animation"); - - this->getScene().destroy(animatable); - - EXPECT_NE(StatusOK, animation->validate()); - } -} diff --git a/client/ramses-client/test/CreationHelper.cpp b/client/ramses-client/test/CreationHelper.cpp deleted file mode 100644 index 09e234529..000000000 --- a/client/ramses-client/test/CreationHelper.cpp +++ /dev/null @@ -1,466 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "gtest/gtest.h" -#include "CreationHelper.h" -#include "TestEffects.h" -#include "ramses-client-api/Node.h" -#include "ramses-client-api/Scene.h" -#include "ramses-client-api/AnimationSystemRealTime.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/RamsesClient.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/SplineStepBool.h" -#include "ramses-client-api/SplineStepInt32.h" -#include "ramses-client-api/SplineStepFloat.h" -#include "ramses-client-api/SplineStepVector2f.h" -#include "ramses-client-api/SplineStepVector3f.h" -#include "ramses-client-api/SplineStepVector4f.h" -#include "ramses-client-api/SplineStepVector2i.h" -#include "ramses-client-api/SplineStepVector3i.h" -#include "ramses-client-api/SplineStepVector4i.h" -#include "ramses-client-api/SplineLinearInt32.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/SplineLinearVector2f.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/SplineLinearVector4f.h" -#include "ramses-client-api/SplineLinearVector2i.h" -#include "ramses-client-api/SplineLinearVector3i.h" -#include "ramses-client-api/SplineLinearVector4i.h" -#include "ramses-client-api/SplineBezierInt32.h" -#include "ramses-client-api/SplineBezierFloat.h" -#include "ramses-client-api/SplineBezierVector2f.h" -#include "ramses-client-api/SplineBezierVector3f.h" -#include "ramses-client-api/SplineBezierVector4f.h" -#include "ramses-client-api/SplineBezierVector2i.h" -#include "ramses-client-api/SplineBezierVector3i.h" -#include "ramses-client-api/SplineBezierVector4i.h" -#include "ramses-client-api/Effect.h" -#include "ramses-client-api/EffectDescription.h" -#include "ramses-client-api/Texture2D.h" -#include "ramses-client-api/ArrayResource.h" -#include "ramses-client-api/RenderBuffer.h" -#include "ramses-client-api/RenderTarget.h" -#include "ramses-client-api/EffectInputSemantic.h" -#include "ramses-client-api/AttributeInput.h" -#include "ramses-client-api/RenderGroup.h" -#include "ramses-client-api/DataFloat.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-client-api/DataVector4f.h" -#include "ramses-client-api/DataMatrix22f.h" -#include "ramses-client-api/DataMatrix33f.h" -#include "ramses-client-api/DataMatrix44f.h" -#include "ramses-client-api/DataInt32.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector3i.h" -#include "ramses-client-api/DataVector4i.h" -#include "ramses-client-api/RenderTargetDescription.h" -#include "ramses-client-api/BlitPass.h" -#include "ramses-client-api/PickableObject.h" -#include "ramses-client-api/Camera.h" -#include "ramses-client-api/ArrayBuffer.h" -#include "ramses-client-api/PerspectiveCamera.h" - -namespace ramses -{ - CreationHelper::CreationHelper(Scene* scene, AnimationSystem* animSystem, RamsesClient* ramsesClient) - : m_scene(scene) - , m_animationSystem(animSystem) - , m_ramsesClient(ramsesClient) - { - } - - CreationHelper::~CreationHelper() - { - for(const auto& component : m_allocatedClientAndFrameworkComponents) - { - component.second->destroyClient(*component.first); - delete component.second; - } - - if (m_animationSystem != nullptr) - { - destroyAdditionalAllocatedAnimationSystemObjects(); - } - - if (m_scene != nullptr) - { - destroyAdditionalAllocatedSceneObjects(); - } - } - - void CreationHelper::setScene(Scene* scene) - { - m_scene = scene; - } - - void CreationHelper::setAnimationSystem(AnimationSystem* animSystem) - { - m_animationSystem = animSystem; - } - - void CreationHelper::destroyAdditionalAllocatedSceneObjects() - { - assert(m_scene != nullptr); - for(const auto& obj : m_additionalAllocatedSceneObjects) - { - ASSERT_TRUE(m_scene->destroy(*obj) == StatusOK); - } - m_additionalAllocatedSceneObjects.clear(); - } - - size_t CreationHelper::getAdditionalAllocatedNodeCount() const - { - size_t nodeCount = 0; - for (auto obj : m_additionalAllocatedSceneObjects) - { - if (obj->isOfType(ERamsesObjectType_Node)) - ++nodeCount; - } - return nodeCount; - } - - void CreationHelper::destroyAdditionalAllocatedAnimationSystemObjects() - { - assert(m_animationSystem != nullptr); - for (const auto& obj : m_additionalAllocatedAnimationSystemObjects) - { - ASSERT_TRUE(m_animationSystem->destroy(*obj) == StatusOK); - } - m_additionalAllocatedAnimationSystemObjects.clear(); - } - - template <> RamsesClient* CreationHelper::createObjectOfType(const char* name) - { - ramses::RamsesFramework* framework = new ramses::RamsesFramework; - RamsesClient* allocatedClient = framework->createClient(name); - if (allocatedClient) - m_allocatedClientAndFrameworkComponents.push_back(ClientAndFramework(allocatedClient, framework)); - return allocatedClient; - } - template <> Scene* CreationHelper::createObjectOfType(const char* name) - { - return m_ramsesClient->createScene(sceneId_t(999u), ramses::SceneConfig(), name); - } - template <> AnimationSystem* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createAnimationSystem(ramses::EAnimationSystemFlags_Default, name); - } - template <> AnimationSystemRealTime* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createRealTimeAnimationSystem(ramses::EAnimationSystemFlags_Default, name); - } - template <> Node* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createNode(name); - } - template <> MeshNode* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createMeshNode(name); - } - template <> PerspectiveCamera* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createPerspectiveCamera(name); - } - template <> OrthographicCamera* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createOrthographicCamera(name); - } - template <> Effect* CreationHelper::createObjectOfType(const char* name) - { - EffectDescription effectDescription; - effectDescription.setVertexShader("void main(void) {gl_Position=vec4(0);}"); - effectDescription.setFragmentShader("void main(void) {gl_FragColor=vec4(1);}"); - return m_scene->createEffect(effectDescription, ResourceCacheFlag_DoNotCache, name); - } - template <> AnimatedProperty* CreationHelper::createObjectOfType(const char* name) - { - Node& node = *m_scene->createNode("node"); - m_additionalAllocatedSceneObjects.push_back(&node); - return m_animationSystem->createAnimatedProperty(node, EAnimatedProperty_Translation, EAnimatedPropertyComponent_All, name); - } - template <> Animation* CreationHelper::createObjectOfType(const char* name) - { - Node& node = *m_scene->createNode("node"); - m_additionalAllocatedSceneObjects.push_back(&node); - AnimatedProperty& prop = *m_animationSystem->createAnimatedProperty(node, EAnimatedProperty_Translation); - SplineLinearVector3f& spline = *m_animationSystem->createSplineLinearVector3f("spline"); - m_additionalAllocatedAnimationSystemObjects.push_back(&prop); - m_additionalAllocatedAnimationSystemObjects.push_back(&spline); - return m_animationSystem->createAnimation(prop, spline, name); - } - template <> AnimationSequence* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createAnimationSequence(name); - } - template <> Appearance* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createAppearance(*TestEffects::CreateTestEffect(*m_scene), name); - } - template <> SplineStepBool* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineStepBool(name); - } - template <> SplineStepFloat* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineStepFloat(name); - } - template <> SplineStepInt32* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineStepInt32(name); - } - template <> SplineStepVector2f* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineStepVector2f(name); - } - template <> SplineStepVector3f* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineStepVector3f(name); - } - template <> SplineStepVector4f* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineStepVector4f(name); - } - template <> SplineStepVector2i* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineStepVector2i(name); - } - template <> SplineStepVector3i* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineStepVector3i(name); - } - template <> SplineStepVector4i* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineStepVector4i(name); - } - template <> SplineLinearFloat* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineLinearFloat(name); - } - template <> SplineLinearInt32* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineLinearInt32(name); - } - template <> SplineLinearVector2f* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineLinearVector2f(name); - } - template <> SplineLinearVector3f* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineLinearVector3f(name); - } - template <> SplineLinearVector4f* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineLinearVector4f(name); - } - template <> SplineLinearVector2i* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineLinearVector2i(name); - } - template <> SplineLinearVector3i* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineLinearVector3i(name); - } - template <> SplineLinearVector4i* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineLinearVector4i(name); - } - template <> SplineBezierFloat* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineBezierFloat(name); - } - template <> SplineBezierInt32* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineBezierInt32(name); - } - template <> SplineBezierVector2f* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineBezierVector2f(name); - } - template <> SplineBezierVector3f* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineBezierVector3f(name); - } - template <> SplineBezierVector4f* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineBezierVector4f(name); - } - template <> SplineBezierVector2i* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineBezierVector2i(name); - } - template <> SplineBezierVector3i* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineBezierVector3i(name); - } - template <> SplineBezierVector4i* CreationHelper::createObjectOfType(const char* name) - { - return m_animationSystem->createSplineBezierVector4i(name); - } - template <> Texture2D* CreationHelper::createObjectOfType(const char* name) - { - uint8_t data[4] = { 0u }; - MipLevelData mipLevelData(sizeof(data), data); - return m_scene->createTexture2D(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, {}, ResourceCacheFlag_DoNotCache, name); - } - template <> Texture3D* CreationHelper::createObjectOfType(const char* name) - { - uint8_t data[32] = { 0u }; - MipLevelData mipLevelData(sizeof(data), data); - return m_scene->createTexture3D(ETextureFormat::RGBA8, 1u, 2u, 4u, 1, &mipLevelData, false, ResourceCacheFlag_DoNotCache, name); - } - template <> TextureCube* CreationHelper::createObjectOfType(const char* name) - { - uint8_t data[4] = { 0u }; - CubeMipLevelData mipLevelData(sizeof(data), data, data, data, data, data, data); - return m_scene->createTextureCube(ETextureFormat::RGBA8, 1u, 1, &mipLevelData, false, {}, ResourceCacheFlag_DoNotCache, name); - } - template <> ArrayResource* CreationHelper::createObjectOfType(const char* name) - { - const uint16_t data = 0u; - return m_scene->createArrayResource(EDataType::UInt16, 1u, &data, ResourceCacheFlag_DoNotCache, name); - } - - template <> RenderGroup* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createRenderGroup(name); - } - template <> RenderPass* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createRenderPass(name); - } - - template <> BlitPass* CreationHelper::createObjectOfType(const char* name) - { - const RenderBuffer* sourceRenderBuffer = createObjectOfType("src render buffer"); - const RenderBuffer* destinationRenderBuffer = createObjectOfType("dst render buffer"); - return m_scene->createBlitPass(*sourceRenderBuffer, *destinationRenderBuffer, name); - } - - template <> TextureSampler* CreationHelper::createObjectOfType(const char* name) - { - uint8_t data[4] = { 0u }; - MipLevelData mipLevelData(sizeof(data), data); - Texture2D* texture = m_scene->createTexture2D(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, {}, ResourceCacheFlag_DoNotCache, "texture"); - return m_scene->createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Mirror, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Nearest, *texture, 1u, name); - } - template <> TextureSamplerMS* CreationHelper::createObjectOfType(const char* name) - { - RenderBuffer* renderBuffer = m_scene->createRenderBuffer(16, 16, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite, 4u, "renderBuffer"); - return m_scene->createTextureSamplerMS(*renderBuffer, name); - } - template <> RenderBuffer* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createRenderBuffer(16, 16, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite, 0u, name); - } - template <> RenderTarget* CreationHelper::createObjectOfType(const char* name) - { - RenderTargetDescription rtDesc; - rtDesc.addRenderBuffer(*createObjectOfType("rb")); - return m_scene->createRenderTarget(rtDesc, name); - } - - template <> GeometryBinding* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createGeometryBinding(*createObjectOfType("geometry_binding_effect"), name); - } - - template <> DataFloat* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createDataFloat(name); - } - - template <> DataVector2f* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createDataVector2f(name); - } - - template <> DataVector3f* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createDataVector3f(name); - } - - template <> DataVector4f* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createDataVector4f(name); - } - - template <> DataMatrix22f* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createDataMatrix22f(name); - } - - template <> DataMatrix33f* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createDataMatrix33f(name); - } - - template <> DataMatrix44f* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createDataMatrix44f(name); - } - - template <> DataInt32* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createDataInt32(name); - } - - template <> DataVector2i* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createDataVector2i(name); - } - - template <> DataVector3i* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createDataVector3i(name); - } - template <> DataVector4i* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createDataVector4i(name); - } - template <> StreamTexture* CreationHelper::createObjectOfType(const char* name) - { - uint8_t data[4] = { 0u }; - MipLevelData mipLevelData(sizeof(data), data); - Texture2D* fallback = m_scene->createTexture2D(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, {}, ResourceCacheFlag_DoNotCache, "fallbackTex"); - StreamTexture* streamTexture = m_scene->createStreamTexture(*fallback, waylandIviSurfaceId_t(0), name); - return streamTexture; - } - - template <> ArrayBuffer* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createArrayBuffer(EDataType::UInt32, 13u, name); - - } - template <> Texture2DBuffer* CreationHelper::createObjectOfType(const char* name) - { - return m_scene->createTexture2DBuffer(ETextureFormat::RGBA8, 3, 4, 2, name); - } - - template <> PickableObject* CreationHelper::createObjectOfType(const char* name) - { - const auto vb = m_scene->createArrayBuffer(EDataType::Vector3F, 3u, "vb"); - m_additionalAllocatedSceneObjects.push_back(vb); - PerspectiveCamera* camera = m_scene->createPerspectiveCamera("pickableCamera"); - m_additionalAllocatedSceneObjects.push_back(camera); - camera->setFrustum(-1.4f, 1.4f, -1.4f, 1.4f, 1.f, 100.f); - camera->setViewport(0, 0, 200, 200); - PickableObject* pickableObject = m_scene->createPickableObject(*vb, pickableObjectId_t{ 123u }, name); - pickableObject->setCamera(*camera); - return pickableObject; - } - - template <> SceneReference* CreationHelper::createObjectOfType(const char* name) - { - // SceneReference cannot refer to same sceneID, create a different one every time - ++m_lastReferencedSceneId.getReference(); - return m_scene->createSceneReference(m_lastReferencedSceneId, name); - } -} diff --git a/client/ramses-client/test/CreationHelper.h b/client/ramses-client/test/CreationHelper.h deleted file mode 100644 index 9758cf370..000000000 --- a/client/ramses-client/test/CreationHelper.h +++ /dev/null @@ -1,195 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_CREATIONHELPER_H -#define RAMSES_CREATIONHELPER_H - -#include "ramses-framework-api/RamsesFrameworkTypes.h" -#include "PlatformAbstraction/PlatformTypes.h" -#include "Collections/Vector.h" -#include "Collections/Pair.h" -#include - -namespace ramses -{ - class RamsesObject; - class SceneObject; - class AnimationObject; - class RamsesClient; - class RamsesFramework; - class TextureCubeInput; - class Scene; - class AnimationSystem; - class AnimationSystemRealTime; - class Node; - class MeshNode; - class PerspectiveCamera; - class OrthographicCamera; - class Effect; - class AnimatedProperty; - class Animation; - class AnimationSequence; - class Appearance; - class SplineStepBool; - class SplineStepFloat; - class SplineStepInt32; - class SplineStepVector2f; - class SplineStepVector3f; - class SplineStepVector4f; - class SplineStepVector2i; - class SplineStepVector3i; - class SplineStepVector4i; - class SplineLinearFloat; - class SplineLinearInt32; - class SplineLinearVector2f; - class SplineLinearVector3f; - class SplineLinearVector4f; - class SplineLinearVector2i; - class SplineLinearVector3i; - class SplineLinearVector4i; - class SplineBezierFloat; - class SplineBezierInt32; - class SplineBezierVector2f; - class SplineBezierVector3f; - class SplineBezierVector4f; - class SplineBezierVector2i; - class SplineBezierVector3i; - class SplineBezierVector4i; - class Texture2D; - class Texture3D; - class TextureCube; - class ArrayResource; - class RenderGroup; - class RenderPass; - class BlitPass; - class TextureSampler; - class TextureSamplerMS; - class RenderBuffer; - class RenderTarget; - class RenderGroup; - class ArrayBuffer; - class Texture2DBuffer; - class GeometryBinding; - class DataFloat; - class DataVector2f; - class DataVector3f; - class DataVector4f; - class DataMatrix22f; - class DataMatrix33f; - class DataMatrix44f; - class DataInt32; - class DataVector2i; - class DataVector3i; - class DataVector4i; - class StreamTexture; - class PickableObject; - class SceneReference; - - class CreationHelper - { - public: - CreationHelper(Scene* scene, AnimationSystem* animSystem, RamsesClient* ramsesClient); - ~CreationHelper(); - - void setScene(Scene* scene); - void setAnimationSystem(AnimationSystem* animSystem); - - template - ObjectType* createObjectOfType(const char* name) - { - UNUSED(name); - assert(false); - return NULL; - } - - void destroyAdditionalAllocatedSceneObjects(); - void destroyAdditionalAllocatedAnimationSystemObjects(); - size_t getAdditionalAllocatedNodeCount() const; - - private: - Scene* m_scene; - AnimationSystem* m_animationSystem; - RamsesClient* m_ramsesClient; - - using ClientAndFramework = std::pair; - using RamsesClientAndFrameworkComponentVector = std::vector; - RamsesClientAndFrameworkComponentVector m_allocatedClientAndFrameworkComponents; - std::vector m_additionalAllocatedSceneObjects; - sceneId_t m_lastReferencedSceneId{ 123u }; - std::vector m_additionalAllocatedAnimationSystemObjects; - }; - - template <> RamsesClient* CreationHelper::createObjectOfType(const char* name); - template <> Scene* CreationHelper::createObjectOfType(const char* name); - template <> AnimationSystem* CreationHelper::createObjectOfType(const char* name); - template <> AnimationSystemRealTime* CreationHelper::createObjectOfType(const char* name); - template <> Node* CreationHelper::createObjectOfType(const char* name); - template <> MeshNode* CreationHelper::createObjectOfType(const char* name); - template <> PerspectiveCamera* CreationHelper::createObjectOfType(const char* name); - template <> OrthographicCamera* CreationHelper::createObjectOfType(const char* name); - template <> Effect* CreationHelper::createObjectOfType(const char* name); - template <> AnimatedProperty* CreationHelper::createObjectOfType(const char* name); - template <> Animation* CreationHelper::createObjectOfType(const char* name); - template <> AnimationSequence* CreationHelper::createObjectOfType(const char* name); - template <> Appearance* CreationHelper::createObjectOfType(const char* name); - template <> SplineStepBool* CreationHelper::createObjectOfType(const char* name); - template <> SplineStepFloat* CreationHelper::createObjectOfType(const char* name); - template <> SplineStepInt32* CreationHelper::createObjectOfType(const char* name); - template <> SplineStepVector2f* CreationHelper::createObjectOfType(const char* name); - template <> SplineStepVector3f* CreationHelper::createObjectOfType(const char* name); - template <> SplineStepVector4f* CreationHelper::createObjectOfType(const char* name); - template <> SplineStepVector2i* CreationHelper::createObjectOfType(const char* name); - template <> SplineStepVector3i* CreationHelper::createObjectOfType(const char* name); - template <> SplineStepVector4i* CreationHelper::createObjectOfType(const char* name); - template <> SplineLinearFloat* CreationHelper::createObjectOfType(const char* name); - template <> SplineLinearInt32* CreationHelper::createObjectOfType(const char* name); - template <> SplineLinearVector2f* CreationHelper::createObjectOfType(const char* name); - template <> SplineLinearVector3f* CreationHelper::createObjectOfType(const char* name); - template <> SplineLinearVector4f* CreationHelper::createObjectOfType(const char* name); - template <> SplineLinearVector2i* CreationHelper::createObjectOfType(const char* name); - template <> SplineLinearVector3i* CreationHelper::createObjectOfType(const char* name); - template <> SplineLinearVector4i* CreationHelper::createObjectOfType(const char* name); - template <> SplineBezierFloat* CreationHelper::createObjectOfType(const char* name); - template <> SplineBezierInt32* CreationHelper::createObjectOfType(const char* name); - template <> SplineBezierVector2f* CreationHelper::createObjectOfType(const char* name); - template <> SplineBezierVector3f* CreationHelper::createObjectOfType(const char* name); - template <> SplineBezierVector4f* CreationHelper::createObjectOfType(const char* name); - template <> SplineBezierVector2i* CreationHelper::createObjectOfType(const char* name); - template <> SplineBezierVector3i* CreationHelper::createObjectOfType(const char* name); - template <> SplineBezierVector4i* CreationHelper::createObjectOfType(const char* name); - template <> Texture2D* CreationHelper::createObjectOfType(const char* name); - template <> Texture3D* CreationHelper::createObjectOfType(const char* name); - template <> TextureCube* CreationHelper::createObjectOfType(const char* name); - template <> ArrayResource* CreationHelper::createObjectOfType(const char* name); - template <> RenderGroup* CreationHelper::createObjectOfType(const char* name); - template <> RenderPass* CreationHelper::createObjectOfType(const char* name); - template <> BlitPass* CreationHelper::createObjectOfType(const char* name); - template <> TextureSampler* CreationHelper::createObjectOfType(const char* name); - template <> TextureSamplerMS* CreationHelper::createObjectOfType(const char* name); - template <> RenderBuffer* CreationHelper::createObjectOfType(const char* name); - template <> RenderTarget* CreationHelper::createObjectOfType(const char* name); - template <> GeometryBinding* CreationHelper::createObjectOfType(const char* name); - template <> DataFloat* CreationHelper::createObjectOfType(const char* name); - template <> DataVector2f* CreationHelper::createObjectOfType(const char* name); - template <> DataVector3f* CreationHelper::createObjectOfType(const char* name); - template <> DataVector4f* CreationHelper::createObjectOfType(const char* name); - template <> DataMatrix22f* CreationHelper::createObjectOfType(const char* name); - template <> DataMatrix33f* CreationHelper::createObjectOfType(const char* name); - template <> DataMatrix44f* CreationHelper::createObjectOfType(const char* name); - template <> DataInt32* CreationHelper::createObjectOfType(const char* name); - template <> DataVector2i* CreationHelper::createObjectOfType(const char* name); - template <> DataVector3i* CreationHelper::createObjectOfType(const char* name); - template <> DataVector4i* CreationHelper::createObjectOfType(const char* name); - template <> StreamTexture* CreationHelper::createObjectOfType(const char* name); - template <> ArrayBuffer* CreationHelper::createObjectOfType(const char* name); - template <> Texture2DBuffer* CreationHelper::createObjectOfType(const char* name); - template <> PickableObject* CreationHelper::createObjectOfType(const char* name); - template <> SceneReference* CreationHelper::createObjectOfType(const char* name); -} - -#endif diff --git a/client/ramses-client/test/DataBufferTest.cpp b/client/ramses-client/test/DataBufferTest.cpp deleted file mode 100644 index ca0fef526..000000000 --- a/client/ramses-client/test/DataBufferTest.cpp +++ /dev/null @@ -1,233 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2017 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include -#include - -#include "ClientTestUtils.h" -#include "SceneImpl.h" -#include "ramses-utils.h" -#include "Scene/ClientScene.h" -#include "ramses-client-api/ArrayBuffer.h" -#include "ramses-client-api/AttributeInput.h" -#include "ArrayBufferImpl.h" -#include "RamsesObjectTypeUtils.h" -#include "DataTypeUtils.h" - -using namespace testing; -using namespace ramses_internal; - -namespace ramses -{ - class ADataBuffer : public LocalTestClientWithScene, public ::testing::TestWithParam - { - protected: - ADataBuffer() - : LocalTestClientWithScene() - , elementSizeInBytes(EnumToSize(DataTypeUtils::ConvertDataTypeToInternal(GetParam()))) - , dataBuffer(*m_scene.createArrayBuffer(GetParam(), 13, "data buffer")) - , dataBufferHandle(dataBuffer.impl.getDataBufferHandle()) - , singleElementData(elementSizeInBytes) - { - std::iota(singleElementData.begin(), singleElementData.end(), static_cast(1)); - } - - EDataType getCreationDataType() { return GetParam(); } - - const uint32_t elementSizeInBytes; - ArrayBuffer& dataBuffer; - const ramses_internal::DataBufferHandle dataBufferHandle; - std::vector singleElementData; - }; - - TEST_P(ADataBuffer, IsAllocatedOnInternalSceneAfterCreation) - { - EXPECT_TRUE(this->dataBufferHandle.isValid()); - EXPECT_TRUE(this->m_scene.impl.getIScene().isDataBufferAllocated(this->dataBufferHandle)); - } - - TEST_P(ADataBuffer, PropagatesDataChangesToInternalScene) - { - EXPECT_EQ(StatusOK, this->dataBuffer.updateData(0u, 1u, singleElementData.data())); - - const Byte* dataBufferData = this->m_scene.impl.getIScene().getDataBuffer(this->dataBufferHandle).data.data(); - EXPECT_EQ(0, std::memcmp(singleElementData.data(), dataBufferData, singleElementData.size())); - - std::vector twoElementsDataBuffer(elementSizeInBytes*2); - std::iota(twoElementsDataBuffer.begin(), twoElementsDataBuffer.end(), static_cast(20)); - EXPECT_EQ(StatusOK, this->dataBuffer.updateData(1u, 2u, twoElementsDataBuffer.data())); - - std::vector expectedThreeeElements = singleElementData; - expectedThreeeElements.insert(expectedThreeeElements.end(), twoElementsDataBuffer.begin(), twoElementsDataBuffer.end()); - ASSERT_EQ(dataBufferData, this->m_scene.impl.getIScene().getDataBuffer(this->dataBufferHandle).data.data()); - EXPECT_EQ(0, std::memcmp(expectedThreeeElements.data(), dataBufferData, expectedThreeeElements.size())); - } - - TEST_P(ADataBuffer, CanNotBeUpdatedWhenDataSizeBiggerThanMaximumSize) - { - std::vector data(elementSizeInBytes*14); - EXPECT_NE(StatusOK, this->dataBuffer.updateData(0u, 14u, data.data())); - } - - TEST_P(ADataBuffer, CanNotBeUpdatdWhenDataSizeAndOffsetBiggerThanMaximumSize) - { - std::vector data(elementSizeInBytes*4); - EXPECT_NE(StatusOK, this->dataBuffer.updateData(10u, 4u, data.data())); - } - - TEST_P(ADataBuffer, CanBeValidated) - { - const auto effect = TestEffects::CreateTestEffectWithAttribute(this->m_scene); - GeometryBinding& geom = this->createValidGeometry(effect); - if (DataTypeUtils::IsValidIndicesType(this->dataBuffer.getDataType())) - EXPECT_EQ(StatusOK, geom.setIndices(RamsesObjectTypeUtils::ConvertTo(this->dataBuffer))); - else if (EDataType::ByteBlob == getCreationDataType()) - { - AttributeInput attrInput1; - effect->findAttributeInput("a_position", attrInput1); - AttributeInput attrInput2; - effect->findAttributeInput("a_vec2", attrInput2); - constexpr uint16_t nonZeroStride = 56u; - EXPECT_EQ(StatusOK, geom.setInputBuffer(attrInput1, RamsesObjectTypeUtils::ConvertTo(this->dataBuffer), 0u, nonZeroStride)); - EXPECT_EQ(StatusOK, geom.setInputBuffer(attrInput2, RamsesObjectTypeUtils::ConvertTo(this->dataBuffer), sizeof(float), nonZeroStride) ); - } - else - { - const char* validInputName = nullptr; - switch (getCreationDataType()) - { - case EDataType::Float: - validInputName = "a_position"; - break; - case EDataType::Vector2F: - validInputName = "a_vec2"; - break; - case EDataType::Vector3F: - validInputName = "a_vec3"; - break; - case EDataType::Vector4F: - validInputName = "a_vec4"; - break; - default: assert(false); - } - AttributeInput attrInput; - effect->findAttributeInput(validInputName, attrInput); - EXPECT_EQ(StatusOK, geom.setInputBuffer(attrInput, RamsesObjectTypeUtils::ConvertTo(this->dataBuffer))); - } - EXPECT_EQ(StatusOK, this->dataBuffer.updateData(0u, 1u, singleElementData.data())); - EXPECT_EQ(StatusOK, this->dataBuffer.validate()); - } - - TEST_P(ADataBuffer, IsValidIfNotUsedByAnyMeshButUsedByPickableObject) - { - ArrayBuffer* geometryBuffer = this->m_scene.createArrayBuffer(EDataType::Vector3F, 3, "geometryBuffer"); - const float data[] = { 0.f, 0.f, 0.f }; - geometryBuffer->updateData(0u, 1u, data); - const pickableObjectId_t id(2); - this->m_scene.createPickableObject(*geometryBuffer, id, "PickableObject"); - - EXPECT_EQ(StatusOK, geometryBuffer->validate()); - } - - TEST_P(ADataBuffer, ReportsWarningIfNotUsedInGeometry) - { - EXPECT_EQ(StatusOK, this->dataBuffer.updateData(0u, 1u, singleElementData.data())); - EXPECT_NE(StatusOK, this->dataBuffer.validate()); - } - - TEST_P(ADataBuffer, ReportsWarningIfUsedInGeometryButNotInitialized) - { - const auto effect = TestEffects::CreateTestEffectWithAttribute(this->m_scene); - GeometryBinding& geom = this->createValidGeometry(effect); - if (DataTypeUtils::IsValidIndicesType(this->dataBuffer.getDataType())) - geom.setIndices(RamsesObjectTypeUtils::ConvertTo(this->dataBuffer)); - else - { - AttributeInput attrInput; - effect->findAttributeInput("a_position", attrInput); - geom.setInputBuffer(attrInput, RamsesObjectTypeUtils::ConvertTo(this->dataBuffer)); - } - EXPECT_NE(StatusOK, this->dataBuffer.validate()); - } - - TEST_P(ADataBuffer, CanGetDataType) - { - EXPECT_EQ(this->getCreationDataType(), this->dataBuffer.getDataType()); - } - - TEST_P(ADataBuffer, CanGetMaximumSize) - { - EXPECT_EQ(13u, this->dataBuffer.getMaximumNumberOfElements()); - EXPECT_EQ(13u, this->dataBuffer.impl.getElementCount()); - } - - TEST_P(ADataBuffer, CanGetElementSize) - { - uint32_t expectedSize = 0u; - switch (getCreationDataType()) - { - case EDataType::UInt16: - expectedSize = sizeof(uint16_t); - break; - case EDataType::UInt32: - expectedSize = sizeof(uint32_t); - break; - case EDataType::Float: - expectedSize = sizeof(float); - break; - case EDataType::Vector2F: - expectedSize = 2 * sizeof(float); - break; - case EDataType::Vector3F: - expectedSize = 3 * sizeof(float); - break; - case EDataType::Vector4F: - expectedSize = 4 * sizeof(float); - break; - case EDataType::ByteBlob: - expectedSize = 1u; - break; - default: - assert(false); - } - - EXPECT_EQ(expectedSize, this->elementSizeInBytes); - } - - TEST_P(ADataBuffer, CanGetUsedSize) - { - EXPECT_EQ(StatusOK, this->dataBuffer.updateData(0u, 1u, singleElementData.data())); - EXPECT_EQ(13u, this->dataBuffer.getMaximumNumberOfElements()); - EXPECT_EQ(13u, this->dataBuffer.impl.getElementCount()); - EXPECT_EQ(1u, this->dataBuffer.getUsedNumberOfElements()); - EXPECT_EQ(1u, this->dataBuffer.impl.getUsedElementCount()); - } - - TEST_P(ADataBuffer, CanGetData) - { - EXPECT_EQ(StatusOK, this->dataBuffer.updateData(0u, 1u, singleElementData.data())); - - std::vector dataBufferOut(singleElementData); - EXPECT_EQ(StatusOK, this->dataBuffer.getData(dataBufferOut.data(), 1)); - EXPECT_EQ(singleElementData, dataBufferOut); - - std::vector twoElementsDataBuffer(elementSizeInBytes*2); - std::iota(twoElementsDataBuffer.begin(), twoElementsDataBuffer.end(), static_cast(20)); - EXPECT_EQ(StatusOK, this->dataBuffer.updateData(1u, 2u, twoElementsDataBuffer.data())); - - std::vector threeElementsDataBufferOut(elementSizeInBytes*3); - EXPECT_EQ(StatusOK, this->dataBuffer.getData(threeElementsDataBufferOut.data(), 3u)); - - std::vector expectedThreeeElements = singleElementData; - expectedThreeeElements.insert(expectedThreeeElements.end(), twoElementsDataBuffer.begin(), twoElementsDataBuffer.end()); - EXPECT_EQ(expectedThreeeElements, threeElementsDataBufferOut); - } - - INSTANTIATE_TEST_SUITE_P(ADataBufferTest, ADataBuffer, - ::testing::Values(EDataType::UInt16, EDataType::UInt32, EDataType::Float, EDataType::Vector2F, EDataType::Vector3F, EDataType::Vector4F, EDataType::ByteBlob)); -} diff --git a/client/ramses-client/test/DataObjectTest.cpp b/client/ramses-client/test/DataObjectTest.cpp deleted file mode 100644 index 8ad713ed5..000000000 --- a/client/ramses-client/test/DataObjectTest.cpp +++ /dev/null @@ -1,405 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include - -#include "ClientTestUtils.h" -#include "ramses-client-api/DataObject.h" -#include "ramses-client-api/DataFloat.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-client-api/DataVector4f.h" -#include "ramses-client-api/DataMatrix22f.h" -#include "ramses-client-api/DataMatrix33f.h" -#include "ramses-client-api/DataMatrix44f.h" -#include "ramses-client-api/DataInt32.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector3i.h" -#include "ramses-client-api/DataVector4i.h" -#include "ramses-utils.h" - -using namespace testing; -using namespace ramses_internal; - -namespace ramses -{ - class ADataObject : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test - { - protected: - template - void checkConversion(T& dataObj) - { - DataObject* baseObj = RamsesUtils::TryConvert(dataObj); - ASSERT_TRUE(baseObj != nullptr); - T* concreteObj = RamsesUtils::TryConvert(*baseObj); - EXPECT_TRUE(concreteObj != nullptr); - EXPECT_EQ(&dataObj, concreteObj); - - const T& constDataObj = dataObj; - const DataObject* constBaseObj = RamsesUtils::TryConvert(constDataObj); - ASSERT_TRUE(constBaseObj != nullptr); - const T* constConcreteObj = RamsesUtils::TryConvert(*constBaseObj); - EXPECT_TRUE(constConcreteObj != nullptr); - EXPECT_EQ(&constDataObj, constConcreteObj); - } - }; - - - // DataFloat - - TEST_F(ADataObject, isInitializedOnCreationOfDataFloat) - { - DataFloat* data = m_scene.createDataFloat("dataFloat"); - - float getVal = 1.5f; - ASSERT_EQ(StatusOK, data->getValue(getVal)); - EXPECT_EQ(0.0f, getVal); - } - - TEST_F(ADataObject, canSetAndGetAFloatValue) - { - DataFloat* data = m_scene.createDataFloat("dataFloat"); - float setVal = 3.1415f; - ASSERT_EQ(StatusOK, data->setValue(setVal)); - - float getVal = 0.0f; - ASSERT_EQ(StatusOK, data->getValue(getVal)); - EXPECT_EQ(setVal, getVal); - } - - TEST_F(ADataObject, canConvertDataFloatToDataObjectAndBack) - { - DataFloat* data = m_scene.createDataFloat("dataFloat"); - DataObject* dataObj = RamsesUtils::TryConvert(*data); - EXPECT_TRUE(dataObj); - data = RamsesUtils::TryConvert(*dataObj); - EXPECT_TRUE(data); - } - - // DataVector2f - - TEST_F(ADataObject, isInitializedOnCreationOfDataVector2f) - { - DataVector2f* data = m_scene.createDataVector2f("dataVector2f"); - - float getVal[] = { 3.1415f, 1.732f }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1])); - EXPECT_EQ(0.0f, getVal[0]); - EXPECT_EQ(0.0f, getVal[1]); - } - - TEST_F(ADataObject, canSetAndGetAVector2fValue) - { - DataVector2f* data = m_scene.createDataVector2f("dataVector2f"); - float setVal[] = { 3.1415f, 1.732f }; - ASSERT_EQ(StatusOK, data->setValue(setVal[0], setVal[1])); - - float getVal[] = { 0.0f, 0.0f }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1])); - EXPECT_EQ(setVal[0], getVal[0]); - EXPECT_EQ(setVal[1], getVal[1]); - } - - TEST_F(ADataObject, canConvertDataVector2fToDataObjectAndBack) - { - DataVector2f* data = m_scene.createDataVector2f("dataVector2f"); - this->checkConversion(*data); - } - - // DataVector3f - - TEST_F(ADataObject, isInitializedOnCreationOfDataVector3f) - { - DataVector3f* data = m_scene.createDataVector3f("dataVector3f"); - - float getVal[] = { 3.1415f, 1.732f, 2.71828f }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1], getVal[2])); - EXPECT_EQ(0.0f, getVal[0]); - EXPECT_EQ(0.0f, getVal[1]); - EXPECT_EQ(0.0f, getVal[2]); - } - - TEST_F(ADataObject, canSetAndGetAVector3fValue) - { - DataVector3f* data = m_scene.createDataVector3f("dataVector3f"); - float setVal[] = { 3.1415f, 1.732f, 2.71828f }; - ASSERT_EQ(StatusOK, data->setValue(setVal[0], setVal[1], setVal[2])); - - float getVal[] = { 0.0f, 0.0f, 0.0f }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1], getVal[2])); - EXPECT_EQ(setVal[0], getVal[0]); - EXPECT_EQ(setVal[1], getVal[1]); - EXPECT_EQ(setVal[2], getVal[2]); - } - - TEST_F(ADataObject, canConvertDataVector3fToDataObjectAndBack) - { - DataVector3f* data = m_scene.createDataVector3f("dataVector3f"); - this->checkConversion(*data); - } - - // DataVector4f - - TEST_F(ADataObject, isInitializedOnCreationOfDataVector4f) - { - DataVector4f* data = m_scene.createDataVector4f("dataVector4f"); - - float getVal[] = { 3.1415f, 1.732f, 2.71828f, 1.41429f }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1], getVal[2], getVal[3])); - EXPECT_EQ(0.0f, getVal[0]); - EXPECT_EQ(0.0f, getVal[1]); - EXPECT_EQ(0.0f, getVal[2]); - EXPECT_EQ(0.0f, getVal[3]); - } - - TEST_F(ADataObject, canSetAndGetAVector4fValue) - { - DataVector4f* data = m_scene.createDataVector4f("dataVector4f"); - float setVal[] = { 3.1415f, 1.732f, 2.71828f, 1.41429f }; - ASSERT_EQ(StatusOK, data->setValue(setVal[0], setVal[1], setVal[2], setVal[3])); - - float getVal[] = { 0.0f, 0.0f, 0.0f, 0.0f }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1], getVal[2], getVal[3])); - EXPECT_EQ(setVal[0], getVal[0]); - EXPECT_EQ(setVal[1], getVal[1]); - EXPECT_EQ(setVal[2], getVal[2]); - EXPECT_EQ(setVal[3], getVal[3]); - } - - TEST_F(ADataObject, canConvertDataVector4fToDataObjectAndBack) - { - DataVector4f* data = m_scene.createDataVector4f("dataVector4f"); - this->checkConversion(*data); - } - - // DataMatrix22f - - TEST_F(ADataObject, isInitializedOnCreationOfDataMatrix22f) - { - DataMatrix22f* data = m_scene.createDataMatrix22f("dataMatrix22f"); - - float getVal[] = { 1.0f, 2.0f, 3.0f, 4.0f }; - ASSERT_EQ(StatusOK, data->getValue(getVal)); - for (uint32_t i = 0; i < 4u; i++) - { - EXPECT_EQ(0.0f, getVal[i]); - } - } - - TEST_F(ADataObject, canSetAndGetAMatrix22fValue) - { - DataMatrix22f* data = m_scene.createDataMatrix22f("dataMatrix22f"); - const float setVal[] = { 1.0f, 2.0f, 3.0f, 4.0f }; - ASSERT_EQ(StatusOK, data->setValue(setVal)); - - float getVal[] = { 0.0f, 0.0f, 0.0f, 0.0f }; - ASSERT_EQ(StatusOK, data->getValue(getVal)); - for (uint32_t i = 0; i < 4u; i++) - { - EXPECT_EQ(setVal[i], getVal[i]); - } - } - - TEST_F(ADataObject, canConvertDataMatrix22fToDataObjectAndBack) - { - DataMatrix22f* data = m_scene.createDataMatrix22f("dataMatrix22f"); - this->checkConversion(*data); - } - - // DataMatrix33f - - TEST_F(ADataObject, isInitializedOnCreationOfDataMatrix33f) - { - DataMatrix33f* data = m_scene.createDataMatrix33f("dataMatrix33f"); - - float getVal[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f }; - ASSERT_EQ(StatusOK, data->getValue(getVal)); - for (uint32_t i = 0; i < 9u; i++) - { - EXPECT_EQ(0.0f, getVal[i]); - } - } - - TEST_F(ADataObject, canSetAndGetAMatrix33fValue) - { - DataMatrix33f* data = m_scene.createDataMatrix33f("dataMatrix33f"); - const float setVal[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f }; - ASSERT_EQ(StatusOK, data->setValue(setVal)); - - float getVal[] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; - ASSERT_EQ(StatusOK, data->getValue(getVal)); - for (uint32_t i = 0; i < 9u; i++) - { - EXPECT_EQ(setVal[i], getVal[i]); - } - } - - TEST_F(ADataObject, canConvertDataMatrix33fToDataObjectAndBack) - { - DataMatrix33f* data = m_scene.createDataMatrix33f("dataMatrix33f"); - this->checkConversion(*data); - } - - // DataMatrix44f - - TEST_F(ADataObject, isInitializedOnCreationOfDataMatrix44f) - { - DataMatrix44f* data = m_scene.createDataMatrix44f("dataMatrix44f"); - - float getVal[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f, 16.0f }; - ASSERT_EQ(StatusOK, data->getValue(getVal)); - for (uint32_t i = 0; i < 16; i++) - { - EXPECT_EQ(0.0f, getVal[i]); - } - } - - TEST_F(ADataObject, canSetAndGetAMatrix44fValue) - { - DataMatrix44f* data = m_scene.createDataMatrix44f("dataMatrix44f"); - float setVal[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f, 16.0f }; - ASSERT_EQ(StatusOK, data->setValue(setVal)); - - float getVal[] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; - ASSERT_EQ(StatusOK, data->getValue(getVal)); - for (uint32_t i = 0; i < 16; i++) - { - EXPECT_EQ(setVal[i], getVal[i]); - } - } - - TEST_F(ADataObject, canConvertDataMatrix44fToDataObjectAndBack) - { - DataMatrix44f* data = m_scene.createDataMatrix44f("dataMatrix44f"); - this->checkConversion(*data); - } - - // DataInt32 - - TEST_F(ADataObject, isInitializedOnCreationOfDataInt32) - { - DataInt32* data = m_scene.createDataInt32("dataInt32"); - - int32_t getVal = 1; - ASSERT_EQ(StatusOK, data->getValue(getVal)); - EXPECT_EQ(0.0f, getVal); - } - - TEST_F(ADataObject, canSetAndGetAInt32Value) - { - DataInt32* data = m_scene.createDataInt32("dataInt32"); - int32_t setVal = 3; - ASSERT_EQ(StatusOK, data->setValue(setVal)); - - int32_t getVal = 0; - ASSERT_EQ(StatusOK, data->getValue(getVal)); - EXPECT_EQ(setVal, getVal); - } - - TEST_F(ADataObject, canConvertDataInt32ToDataObjectAndBack) - { - DataInt32* data = m_scene.createDataInt32("dataInt32"); - this->checkConversion(*data); - } - - // DataVector2i - - TEST_F(ADataObject, isInitializedOnCreationOfDataVector2i) - { - DataVector2i* data = m_scene.createDataVector2i("dataVector2i"); - - int32_t getVal[] = { 3, 1 }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1])); - EXPECT_EQ(0.0f, getVal[0]); - EXPECT_EQ(0.0f, getVal[1]); - } - - TEST_F(ADataObject, canSetAndGetAVector2iValue) - { - DataVector2i* data = m_scene.createDataVector2i("dataVector2i"); - int32_t setVal[] = { 3, 2 }; - ASSERT_EQ(StatusOK, data->setValue(setVal[0], setVal[1])); - - int32_t getVal[] = { 0, 0 }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1])); - EXPECT_EQ(setVal[0], getVal[0]); - EXPECT_EQ(setVal[1], getVal[1]); - } - - TEST_F(ADataObject, canConvertDataVector2iToDataObjectAndBack) - { - DataVector2i* data = m_scene.createDataVector2i("dataVector2i"); - this->checkConversion(*data); - } - - // DataVector3i - - TEST_F(ADataObject, isInitializedOnCreationOfDataVector3i) - { - DataVector3i* data = m_scene.createDataVector3i("dataVector3i"); - - int32_t getVal[] = { 1, 2, 3 }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1], getVal[2])); - EXPECT_EQ(0.0f, getVal[0]); - EXPECT_EQ(0.0f, getVal[1]); - EXPECT_EQ(0.0f, getVal[2]); - } - - TEST_F(ADataObject, canSetAndGetAVector3iValue) - { - DataVector3i* data = m_scene.createDataVector3i("dataVector3i"); - int32_t setVal[] = { 1, 2, 3 }; - ASSERT_EQ(StatusOK, data->setValue(setVal[0], setVal[1], setVal[2])); - - int32_t getVal[] = { 0, 0, 0 }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1], getVal[2])); - EXPECT_EQ(setVal[0], getVal[0]); - EXPECT_EQ(setVal[1], getVal[1]); - EXPECT_EQ(setVal[2], getVal[2]); - } - - TEST_F(ADataObject, canConvertDataVector3iToDataObjectAndBack) - { - DataVector3i* data = m_scene.createDataVector3i("dataVector3i"); - this->checkConversion(*data); - } - - // DataVector4i - - TEST_F(ADataObject, isInitializedOnCreationOfDataVector4i) - { - DataVector4i* data = m_scene.createDataVector4i("dataVector4i"); - - int32_t getVal[] = { 1, 2, 3, 4 }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1], getVal[2], getVal[3])); - EXPECT_EQ(0.0f, getVal[0]); - EXPECT_EQ(0.0f, getVal[1]); - EXPECT_EQ(0.0f, getVal[2]); - EXPECT_EQ(0.0f, getVal[3]); - } - - TEST_F(ADataObject, canSetAndGetAVector4iValue) - { - DataVector4i* data = m_scene.createDataVector4i("dataVector4i"); - int32_t setVal[] = { 1, 2, 3, 4 }; - ASSERT_EQ(StatusOK, data->setValue(setVal[0], setVal[1], setVal[2], setVal[3])); - - int32_t getVal[] = { 0, 0, 0, 0 }; - ASSERT_EQ(StatusOK, data->getValue(getVal[0], getVal[1], getVal[2], getVal[3])); - EXPECT_EQ(setVal[0], getVal[0]); - EXPECT_EQ(setVal[1], getVal[1]); - EXPECT_EQ(setVal[2], getVal[2]); - EXPECT_EQ(setVal[3], getVal[3]); - } - - TEST_F(ADataObject, canConvertDataVector4iToDataObjectAndBack) - { - DataVector4i* data = m_scene.createDataVector4i("dataVector4i"); - this->checkConversion(*data); - } -} diff --git a/client/ramses-client/test/EDataTypeTest.cpp b/client/ramses-client/test/EDataTypeTest.cpp deleted file mode 100644 index 909bfbbbd..000000000 --- a/client/ramses-client/test/EDataTypeTest.cpp +++ /dev/null @@ -1,58 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include -#include "ramses-client-api/EDataType.h" -#include "SceneAPI/EDataType.h" - -namespace ramses -{ - using namespace testing; - - TEST(EDatatype, CanReturnNumberOfComponentsAtCompiletime) - { - static_assert(1u == GetNumberOfComponents(EDataType::Float), "EDataType::Float should have 1 component"); - static_assert(2u == GetNumberOfComponents(EDataType::Vector2F), "EDataType::Vector2F should have 2 components"); - } - - TEST(EDatatype, CanReturnSizeOfComponentAtCompiletime) - { - static_assert(sizeof(uint16_t) == GetSizeOfComponent(EDataType::UInt16), "component of EDataType::UInt16 should have size of uint16_t"); - static_assert(sizeof(uint32_t) == GetSizeOfComponent(EDataType::UInt32), "component of EDataType::UInt32 should have size of uint32_t"); - static_assert(sizeof(float) == GetSizeOfComponent(EDataType::Float), "component of EDataType::Float should have size of float"); - static_assert(sizeof(float) == GetSizeOfComponent(EDataType::Vector2F), "component of EDataType::Vector2F should have size of float"); - static_assert(sizeof(float) == GetSizeOfComponent(EDataType::Vector3F), "component of EDataType::Vector3F should have size of float"); - static_assert(sizeof(float) == GetSizeOfComponent(EDataType::Vector4F), "component of EDataType::Vector4F should have size of float"); - } - - TEST(EDatatype, CanReturnSizesAtCompiletime) - { - static_assert(sizeof(float) == GetSizeOfDataType(EDataType::Float), "EDataType::Float is sizeof float"); - static_assert(sizeof(float) * 2u == GetSizeOfDataType(EDataType::Vector2F), "EDataType::Vector2F should be as large as two floats"); - } - - TEST(EDataType, EnsureGivesSameSizeOnPublicAPI) - { - EXPECT_EQ(ramses::GetSizeOfDataType(ramses::EDataType::UInt16), ramses_internal::EnumToSize(ramses_internal::EDataType::UInt16)); - EXPECT_EQ(ramses::GetSizeOfDataType(ramses::EDataType::UInt32), ramses_internal::EnumToSize(ramses_internal::EDataType::UInt32)); - EXPECT_EQ(ramses::GetSizeOfDataType(ramses::EDataType::Float), ramses_internal::EnumToSize(ramses_internal::EDataType::Float)); - EXPECT_EQ(ramses::GetSizeOfDataType(ramses::EDataType::Vector2F), ramses_internal::EnumToSize(ramses_internal::EDataType::Vector2F)); - EXPECT_EQ(ramses::GetSizeOfDataType(ramses::EDataType::Vector3F), ramses_internal::EnumToSize(ramses_internal::EDataType::Vector3F)); - EXPECT_EQ(ramses::GetSizeOfDataType(ramses::EDataType::Vector4F), ramses_internal::EnumToSize(ramses_internal::EDataType::Vector4F)); - } - - TEST(EDataType, EnsureGivesSameNumberOfComponentsOnPublicAPI) - { - EXPECT_EQ(ramses::GetNumberOfComponents(ramses::EDataType::UInt16), ramses_internal::EnumToNumComponents(ramses_internal::EDataType::UInt16)); - EXPECT_EQ(ramses::GetNumberOfComponents(ramses::EDataType::UInt32), ramses_internal::EnumToNumComponents(ramses_internal::EDataType::UInt32)); - EXPECT_EQ(ramses::GetNumberOfComponents(ramses::EDataType::Float), ramses_internal::EnumToNumComponents(ramses_internal::EDataType::Float)); - EXPECT_EQ(ramses::GetNumberOfComponents(ramses::EDataType::Vector2F), ramses_internal::EnumToNumComponents(ramses_internal::EDataType::Vector2F)); - EXPECT_EQ(ramses::GetNumberOfComponents(ramses::EDataType::Vector3F), ramses_internal::EnumToNumComponents(ramses_internal::EDataType::Vector3F)); - EXPECT_EQ(ramses::GetNumberOfComponents(ramses::EDataType::Vector4F), ramses_internal::EnumToNumComponents(ramses_internal::EDataType::Vector4F)); - } -} diff --git a/client/ramses-client/test/EffectInputTest.cpp b/client/ramses-client/test/EffectInputTest.cpp deleted file mode 100644 index 2cf7c111f..000000000 --- a/client/ramses-client/test/EffectInputTest.cpp +++ /dev/null @@ -1,256 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include - -#include "ramses-client-api/UniformInput.h" -#include "ramses-client-api/AttributeInput.h" -#include "EffectInputImpl.h" -#include "SceneAPI/IScene.h" -#include "Utils/File.h" -#include "Utils/BinaryFileOutputStream.h" -#include "Utils/BinaryFileInputStream.h" - -using namespace testing; - -namespace ramses -{ - class AnEffectInput : public ::testing::Test - { - }; - - TEST_F(AnEffectInput, UniformInputIsInitializedToDefaultUponCreation) - { - UniformInput input; - EXPECT_STREQ("", input.getName()); - EXPECT_EQ(0u, input.getElementCount()); - EXPECT_EQ(ramses_internal::ResourceContentHash::Invalid(), input.impl.getEffectHash()); - EXPECT_EQ(EEffectInputDataType_Invalid, input.getDataType()); - EXPECT_EQ(ramses_internal::EDataType::Invalid, input.impl.getDataType()); - EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.impl.getSemantics()); - EXPECT_EQ(EEffectUniformSemantic::Invalid, input.getSemantics()); - EXPECT_EQ(static_cast(-1), input.impl.getInputIndex()); - EXPECT_FALSE(input.isValid()); - } - - TEST_F(AnEffectInput, AttributeInputIsInitializedToDefaultUponCreation) - { - AttributeInput input; - EXPECT_STREQ("", input.getName()); - EXPECT_EQ(ramses_internal::ResourceContentHash::Invalid(), input.impl.getEffectHash()); - EXPECT_EQ(EEffectInputDataType_Invalid, input.getDataType()); - EXPECT_EQ(ramses_internal::EDataType::Invalid, input.impl.getDataType()); - EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.impl.getSemantics()); - EXPECT_EQ(static_cast(-1), input.impl.getInputIndex()); - EXPECT_FALSE(input.isValid()); - } - - TEST_F(AnEffectInput, UniformInputIsInitializedToGivenValues) - { - const ramses_internal::ResourceContentHash effectHash(1u, 0); - const ramses_internal::String inputName("test"); - const ramses_internal::EDataType dataType = ramses_internal::EDataType::Int32; - const ramses_internal::EFixedSemantics semantics = ramses_internal::EFixedSemantics::ModelMatrix; - const uint32_t elementCount = 9u; - const uint32_t index = 66u; - - UniformInput input; - input.impl.initialize(effectHash, inputName, dataType, semantics, elementCount, index); - - EXPECT_STREQ(inputName.c_str(), input.getName()); - EXPECT_EQ(elementCount, input.getElementCount()); - EXPECT_EQ(effectHash, input.impl.getEffectHash()); - EXPECT_EQ(dataType, input.impl.getDataType()); - EXPECT_EQ(semantics, input.impl.getSemantics()); - EXPECT_EQ(EEffectUniformSemantic::ModelMatrix, input.getSemantics()); - EXPECT_EQ(index, input.impl.getInputIndex()); - EXPECT_TRUE(input.isValid()); - } - - TEST_F(AnEffectInput, AttributeInputIsInitializedToGivenValues) - { - const ramses_internal::ResourceContentHash effectHash(1u, 0); - const ramses_internal::String inputName("test"); - const ramses_internal::EDataType dataType = ramses_internal::EDataType::Vector2Buffer; - const ramses_internal::EFixedSemantics semantics = ramses_internal::EFixedSemantics::TextPositionsAttribute; - const uint32_t index = 66u; - - AttributeInput input; - input.impl.initialize(effectHash, inputName, dataType, semantics, 1u, index); - - EXPECT_STREQ(inputName.c_str(), input.getName()); - EXPECT_EQ(effectHash, input.impl.getEffectHash()); - EXPECT_EQ(dataType, input.impl.getDataType()); - EXPECT_EQ(semantics, input.impl.getSemantics()); - EXPECT_EQ(index, input.impl.getInputIndex()); - EXPECT_TRUE(input.isValid()); - } - - TEST_F(AnEffectInput, CanBeSerialized) - { - const ramses_internal::ResourceContentHash effectHash(1u, 0); - const ramses_internal::String inputName("test"); - const ramses_internal::EDataType dataType = ramses_internal::EDataType::Vector2Buffer; - const ramses_internal::EFixedSemantics semantics = ramses_internal::EFixedSemantics::TextTextureCoordinatesAttribute; - const uint32_t index = 66u; - - EffectInputImpl input; - input.initialize(effectHash, inputName, dataType, semantics, 1u, index); - - const ramses_internal::String fileName("someTemporaryFile.ram"); - - ramses_internal::File outputFile(fileName); - ramses_internal::BinaryFileOutputStream outputStream(outputFile); - assert(outputFile.isOpen()); - - input.serialize(outputStream); - - outputFile.close(); - - ramses_internal::File inputFile(fileName); - ramses_internal::BinaryFileInputStream inputStream(inputFile); - assert(inputFile.isOpen()); - - EffectInputImpl inputRead; - - inputRead.deserialize(inputStream); - - inputFile.close(); - - EXPECT_EQ(inputName, inputRead.getName()); - EXPECT_EQ(effectHash, inputRead.getEffectHash()); - EXPECT_EQ(dataType, inputRead.getDataType()); - EXPECT_EQ(semantics, inputRead.getSemantics()); - EXPECT_EQ(index, inputRead.getInputIndex()); - EXPECT_TRUE(inputRead.isValid()); - } - - TEST_F(AnEffectInput, CanBeComparedForEquality) - { - const ramses_internal::ResourceContentHash effectHash(1u, 0); - const ramses_internal::String inputName("test"); - const ramses_internal::EDataType dataType = ramses_internal::EDataType::Vector2Buffer; - const ramses_internal::EFixedSemantics semantics = ramses_internal::EFixedSemantics::TextPositionsAttribute; - const uint32_t index = 66u; - - EffectInputImpl input1; - input1.initialize(effectHash, inputName, dataType, semantics, 1u, index); - - { - EffectInputImpl input2; - input2.initialize(effectHash, inputName, dataType, semantics, 1u, index); - EXPECT_TRUE(input1 == input2); - EXPECT_FALSE(input1 != input2); - } - - { - EffectInputImpl input2; - input2.initialize(ramses_internal::ResourceContentHash(2u, 0), inputName, dataType, semantics, 1u, index); - EXPECT_FALSE(input1 == input2); - EXPECT_TRUE(input1 != input2); - } - - { - EffectInputImpl input2; - input2.initialize(effectHash, "test2", dataType, semantics, 1u, index); - EXPECT_FALSE(input1 == input2); - EXPECT_TRUE(input1 != input2); - } - - { - EffectInputImpl input2; - input2.initialize(effectHash, inputName, ramses_internal::EDataType::Vector3Buffer, semantics, 1u, index); - EXPECT_FALSE(input1 == input2); - EXPECT_TRUE(input1 != input2); - } - - { - EffectInputImpl input2; - input2.initialize(effectHash, inputName, dataType, ramses_internal::EFixedSemantics::TextTextureCoordinatesAttribute, 1u, index); - EXPECT_FALSE(input1 == input2); - EXPECT_TRUE(input1 != input2); - } - - { - EffectInputImpl input2; - input2.initialize(effectHash, inputName, dataType, semantics, 2u, index); - EXPECT_FALSE(input1 == input2); - EXPECT_TRUE(input1 != input2); - } - - { - EffectInputImpl input2; - input2.initialize(effectHash, inputName, dataType, semantics, 1u, 67u); - EXPECT_FALSE(input1 == input2); - EXPECT_TRUE(input1 != input2); - } - } - - TEST_F(AnEffectInput, ReturnsCorrectDataType) - { - const ramses_internal::ResourceContentHash effectHash(1u, 0); - const ramses_internal::String inputName("test"); - const ramses_internal::EFixedSemantics semantics = ramses_internal::EFixedSemantics::ModelMatrix; - const uint32_t index = 66u; - UniformInput input; - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::UInt16, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_UInt16); - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::UInt32, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_UInt32); - - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::Int32, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_Int32); - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::Vector2I, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_Vector2I); - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::Vector3I, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_Vector3I); - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::Vector4I, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_Vector4I); - - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::Float, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_Float); - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::Vector2F, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_Vector2F); - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::Vector3F, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_Vector3F); - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::Vector4F, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_Vector4F); - - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::Matrix22F, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_Matrix22F); - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::Matrix33F, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_Matrix33F); - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::Matrix44F, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_Matrix44F); - - - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::TextureSampler2D, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_TextureSampler2D); - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::TextureSampler2DMS, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_TextureSampler2DMS); - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::TextureSampler3D, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_TextureSampler3D); - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::TextureSamplerCube, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_TextureSamplerCube); - input.impl.initialize(effectHash, inputName, ramses_internal::EDataType::TextureSamplerExternal, semantics, 1u, index); - EXPECT_EQ(input.getDataType(), EEffectInputDataType_TextureSamplerExternal); - } -} diff --git a/client/ramses-client/test/NodeTransformationTest.cpp b/client/ramses-client/test/NodeTransformationTest.cpp deleted file mode 100644 index 1d45d73af..000000000 --- a/client/ramses-client/test/NodeTransformationTest.cpp +++ /dev/null @@ -1,289 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include - -#include "ramses-client-api/MeshNode.h" -#include "ramses-client-api/PerspectiveCamera.h" -#include "ramses-client-api/OrthographicCamera.h" -#include "ramses-client-api/PickableObject.h" - -#include "ClientTestUtils.h" -#include "Math3d/Vector3.h" -#include "RamsesObjectTestTypes.h" -#include "TestEqualHelper.h" - -namespace ramses -{ - using namespace testing; - - template - class NodeTransformationTest : public LocalTestClientWithScene, public testing::Test - { - protected: - virtual void SetUp() override - { - m_node = &this->template createObject("node"); - } - - T* m_node; - }; - - TYPED_TEST_SUITE(NodeTransformationTest, NodeTypes); - - TYPED_TEST(NodeTransformationTest, setTranslate) - { - ramses_internal::Vector3 initialTranslation(0.f, 0.f, 0.f); - ramses_internal::Vector3 actualTranslation; - EXPECT_EQ(StatusOK, this->m_node->getTranslation(actualTranslation.x, actualTranslation.y, actualTranslation.z)); - EXPECT_EQ(initialTranslation, actualTranslation); - - ramses_internal::Vector3 translationVector(1.2f, 2.3f, 4.5f); - EXPECT_EQ(StatusOK, this->m_node->setTranslation(translationVector.x, translationVector.y, translationVector.z)); - EXPECT_EQ(StatusOK, this->m_node->getTranslation(actualTranslation.x, actualTranslation.y, actualTranslation.z)); - EXPECT_EQ(translationVector, actualTranslation); - } - - TYPED_TEST(NodeTransformationTest, translate) - { - ramses_internal::Vector3 initialTranslation(0.f, 0.f, 0.f); - ramses_internal::Vector3 actualTranslation; - EXPECT_EQ(StatusOK, this->m_node->getTranslation(actualTranslation.x, actualTranslation.y, actualTranslation.z)); - EXPECT_EQ(initialTranslation, actualTranslation); - - ramses_internal::Vector3 translationVector(1.2f, 2.3f, 4.5f); - EXPECT_EQ(StatusOK, this->m_node->translate(translationVector.x, translationVector.y, translationVector.z)); - EXPECT_EQ(StatusOK, this->m_node->translate(translationVector.x, translationVector.y, translationVector.z)); - EXPECT_EQ(StatusOK, this->m_node->getTranslation(actualTranslation.x, actualTranslation.y, actualTranslation.z)); - EXPECT_EQ(2 * translationVector, actualTranslation); - } - - TYPED_TEST(NodeTransformationTest, setRotation) - { - ramses_internal::Vector3 initialRotation(0.f, 0.f, 0.f); - ramses_internal::Vector3 actualRotation; - EXPECT_EQ(StatusOK, this->m_node->getRotation(actualRotation.x, actualRotation.y, actualRotation.z)); - EXPECT_EQ(initialRotation, actualRotation); - - ramses_internal::Vector3 rotationVector_1(1.2f, 2.3f, 4.5f); - EXPECT_EQ(StatusOK, this->m_node->setRotation(rotationVector_1.x, rotationVector_1.y, rotationVector_1.z)); - EXPECT_EQ(StatusOK, this->m_node->getRotation(actualRotation.x, actualRotation.y, actualRotation.z)); - EXPECT_EQ(rotationVector_1, actualRotation); - - ramses_internal::Vector3 rotationVector_2(2.2f, 3.3f, 5.5f); - EXPECT_EQ(StatusOK, this->m_node->setRotation(rotationVector_2.x, rotationVector_2.y, rotationVector_2.z)); - EXPECT_EQ(StatusOK, this->m_node->getRotation(actualRotation.x, actualRotation.y, actualRotation.z)); - EXPECT_EQ(rotationVector_2, actualRotation); - } - - TYPED_TEST(NodeTransformationTest, rotate) - { - ramses_internal::Vector3 initialRotation(0.f, 0.f, 0.f); - ramses_internal::Vector3 actualRotation; - EXPECT_EQ(StatusOK, this->m_node->getRotation(actualRotation.x, actualRotation.y, actualRotation.z)); - EXPECT_EQ(initialRotation, actualRotation); - - ramses_internal::Vector3 rotationVector_1(1.f, 2.f, 3.f); - EXPECT_EQ(StatusOK, this->m_node->rotate(rotationVector_1.x, rotationVector_1.y, rotationVector_1.z)); - EXPECT_EQ(StatusOK, this->m_node->getRotation(actualRotation.x, actualRotation.y, actualRotation.z)); - EXPECT_EQ(rotationVector_1, actualRotation); - - ramses_internal::Vector3 rotationVector_2(4.5f, 2.5f, 0.5f); - EXPECT_EQ(StatusOK, this->m_node->rotate(rotationVector_2.x, rotationVector_2.y, rotationVector_2.z)); - - ramses_internal::Vector3 resultVector(5.5f, 4.5f, 3.5f); - EXPECT_EQ(StatusOK, this->m_node->getRotation(actualRotation.x, actualRotation.y, actualRotation.z)); - EXPECT_EQ(resultVector, actualRotation); - } - - TYPED_TEST(NodeTransformationTest, setScaling) - { - ramses_internal::Vector3 initialScaling(1.f, 1.f, 1.f); - ramses_internal::Vector3 actualScale; - EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale.x, actualScale.y, actualScale.z)); - EXPECT_EQ(initialScaling, actualScale); - - ramses_internal::Vector3 scalingVector_1(1.2f, 2.3f, 4.5f); - EXPECT_EQ(StatusOK, this->m_node->setScaling(scalingVector_1.x, scalingVector_1.y, scalingVector_1.z)); - EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale.x, actualScale.y, actualScale.z)); - EXPECT_EQ(scalingVector_1, actualScale); - - ramses_internal::Vector3 scalingVector_2(2.2f, 3.3f, 5.5f); - EXPECT_EQ(StatusOK, this->m_node->setScaling(scalingVector_2.x, scalingVector_2.y, scalingVector_2.z)); - EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale.x, actualScale.y, actualScale.z)); - EXPECT_EQ(scalingVector_2, actualScale); - } - - TYPED_TEST(NodeTransformationTest, scale) - { - ramses_internal::Vector3 initialScaling(1.f, 1.f, 1.f); - ramses_internal::Vector3 actualScale; - EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale.x, actualScale.y, actualScale.z)); - EXPECT_EQ(initialScaling, actualScale); - - ramses_internal::Vector3 scalingVector_1(4.f, 6.f, 8.f); - EXPECT_EQ(StatusOK, this->m_node->scale(scalingVector_1.x, scalingVector_1.y, scalingVector_1.z)); - EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale.x, actualScale.y, actualScale.z)); - EXPECT_EQ(scalingVector_1, actualScale); - - ramses_internal::Vector3 scalingVector_2(0.5f, 0.5f, 0.5f); - EXPECT_EQ(StatusOK, this->m_node->scale(scalingVector_2.x, scalingVector_2.y, scalingVector_2.z)); - - ramses_internal::Vector3 resultVector(2.f, 3.f, 4.f); - EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale.x, actualScale.y, actualScale.z)); - EXPECT_EQ(resultVector, actualScale); - } - - TYPED_TEST(NodeTransformationTest, rotateLegacyZYXAndNonLegacyConventions) - { - const ramses_internal::Vector3 rotationAngles(30.f, 60.f, 120.f); - EXPECT_EQ(StatusOK, this->m_node->setRotation(rotationAngles.x, rotationAngles.y, rotationAngles.z)); - const auto expectedLegacyZYXMatrix = ramses_internal::Matrix44f::RotationEuler(rotationAngles, ramses_internal::ERotationConvention::Legacy_ZYX); - - ramses_internal::Matrix44f resultNodeMatrix; - this->m_node->getModelMatrix(resultNodeMatrix.data); - expectMatrixFloatEqual(expectedLegacyZYXMatrix, resultNodeMatrix); - - TypeParam* childNode = &this->template createObject("child node"); - this->m_node->addChild(*childNode); - - ramses_internal::Matrix44f resultChlidNodeMatrix; - childNode->getModelMatrix(resultChlidNodeMatrix.data); - expectMatrixFloatEqual(expectedLegacyZYXMatrix, resultChlidNodeMatrix); - - EXPECT_EQ(StatusOK, childNode->setRotation(rotationAngles.x, rotationAngles.y, rotationAngles.z, ramses::ERotationConvention::XYZ)); - childNode->getModelMatrix(resultChlidNodeMatrix.data); - expectMatrixFloatEqual(ramses_internal::Matrix44f::Identity, resultChlidNodeMatrix); - - const ramses_internal::Vector3 childRotationAngles(35.f, 65.f, 125.f); - EXPECT_EQ(StatusOK, childNode->setRotation(childRotationAngles.x, childRotationAngles.y, childRotationAngles.z, ramses::ERotationConvention::XYX)); - childNode->getModelMatrix(resultChlidNodeMatrix.data); - const auto expectedChildRotateMatrix = expectedLegacyZYXMatrix * - ramses_internal::Matrix44f::RotationEuler(childRotationAngles, ramses_internal::ERotationConvention::XYX); - expectMatrixFloatEqual(expectedChildRotateMatrix, resultChlidNodeMatrix); - } - - TYPED_TEST(NodeTransformationTest, rotateMixConventions) - { - /* - node - (10, 0, 0, XYZ) - / \ - chlid0 child1 - (0, 20, 0, ZYX) (0, 20, 30, ZYZ) - | - grandChild - (0, 0, 30, YZX) - */ - TypeParam* child0 = &this->template createObject("child0 node"); - TypeParam* child1 = &this->template createObject("child1 node"); - TypeParam* grandChild = &this->template createObject("grand child node"); - this->m_node->addChild(*child0); - this->m_node->addChild(*child1); - child0->addChild(*grandChild); - - EXPECT_EQ(StatusOK, this->m_node ->setRotation(10.f, 0.f , 0.f , ramses::ERotationConvention::XYZ)); - EXPECT_EQ(StatusOK, child0 ->setRotation(0.f , 20.f, 0.f , ramses::ERotationConvention::ZYX)); - EXPECT_EQ(StatusOK, child1 ->setRotation(0.f , 20.f, 30.f, ramses::ERotationConvention::ZYZ)); - EXPECT_EQ(StatusOK, grandChild ->setRotation(0.f , 0.f, 30.f, ramses::ERotationConvention::YZX)); - - //expected matrices for rotation after transformation chain is applied - const auto expectedNodeRotationMatrix = ramses_internal::Matrix44f::RotationEuler({ 10.f , 0.f , 0.f }, ramses_internal::ERotationConvention::XYZ); - const auto expectedChild0RorationMatrix = ramses_internal::Matrix44f::RotationEuler({ 10.f , 20.f, 0.f }, ramses_internal::ERotationConvention::XYZ); - const auto expectedChild1RorationMatrix = ramses_internal::Matrix44f::RotationEuler({ 10.f , 20.f, 30.f }, ramses_internal::ERotationConvention::XYZ); - const auto expectedGrandChildRorationMatrix = ramses_internal::Matrix44f::RotationEuler({ 10.f , 20.f, 30.f }, ramses_internal::ERotationConvention::XYZ); - - ramses_internal::Matrix44f resultNodeMatrix; - ramses_internal::Matrix44f resultChild0Matrix; - ramses_internal::Matrix44f resultChild1Matrix; - ramses_internal::Matrix44f resultGrandChildMatrix; - this->m_node->getModelMatrix(resultNodeMatrix.data); - child0 ->getModelMatrix(resultChild0Matrix.data); - child1 ->getModelMatrix(resultChild1Matrix.data); - grandChild ->getModelMatrix(resultGrandChildMatrix.data); - - expectMatrixFloatEqual(expectedNodeRotationMatrix , resultNodeMatrix); - expectMatrixFloatEqual(expectedChild0RorationMatrix , resultChild0Matrix); - expectMatrixFloatEqual(expectedChild1RorationMatrix , resultChild1Matrix); - expectMatrixFloatEqual(expectedGrandChildRorationMatrix , resultGrandChildMatrix); - } - - template - class NodeTransformationTestWithPublishedScene : public LocalTestClientWithScene, public testing::Test - { - protected: - virtual void SetUp() override - { - const ramses_internal::IScene& iscene = this->m_scene.impl.getIScene(); - ramses_internal::SceneInfo info(iscene.getSceneId(), iscene.getName()); - EXPECT_CALL(this->sceneActionsCollector, handleNewSceneAvailable(info, _)); - EXPECT_CALL(this->sceneActionsCollector, handleInitializeScene(info, _)); - EXPECT_EQ(StatusOK, m_scene.publish(EScenePublicationMode_LocalOnly)); - - m_node = &this->template createObject("node"); - } - - virtual void TearDown() override - { - EXPECT_CALL(this->sceneActionsCollector, handleSceneBecameUnavailable(ramses_internal::SceneId(this->m_scene.impl.getSceneId().getValue()), _)); - EXPECT_EQ(StatusOK, m_scene.unpublish()); - } - - T* m_node; - }; - - TYPED_TEST_SUITE(NodeTransformationTestWithPublishedScene, NodeTypes); - - TYPED_TEST(NodeTransformationTestWithPublishedScene, setTranslateWithValuesEqualToCurrentValuesDoesNotCreateSceneActions) - { - ramses_internal::Vector3 translationVector(1.2f, 2.3f, 4.5f); - EXPECT_CALL(this->sceneActionsCollector, handleSceneUpdate_rvr(ramses_internal::SceneId(this->m_scene.impl.getSceneId().getValue()), _, _)); - EXPECT_EQ(StatusOK, this->m_node->setTranslation(translationVector.x, translationVector.y, translationVector.z)); - this->m_scene.flush(); - EXPECT_LE(1u, this->sceneActionsCollector.getNumberOfActions()); - - Mock::VerifyAndClearExpectations(this); - this->sceneActionsCollector.resetCollecting(); - - EXPECT_EQ(StatusOK, this->m_node->setTranslation(translationVector.x, translationVector.y, translationVector.z)); - this->m_scene.flush(); - EXPECT_EQ(0u, this->sceneActionsCollector.getNumberOfActions()); // flush empty and optimized away - } - - TYPED_TEST(NodeTransformationTestWithPublishedScene, setRotationWithValuesEqualToCurrentValuesDoesNotCreateSceneActions) - { - ramses_internal::Vector3 rotationVector(1.2f, 2.3f, 4.5f); - EXPECT_CALL(this->sceneActionsCollector, handleSceneUpdate_rvr(ramses_internal::SceneId(this->m_scene.impl.getSceneId().getValue()), _, _)); - EXPECT_EQ(StatusOK, this->m_node->setRotation(rotationVector.x, rotationVector.y, rotationVector.z)); - this->m_scene.flush(); - EXPECT_LE(1u, this->sceneActionsCollector.getNumberOfActions()); - - Mock::VerifyAndClearExpectations(this); - this->sceneActionsCollector.resetCollecting(); - - EXPECT_EQ(StatusOK, this->m_node->setRotation(rotationVector.x, rotationVector.y, rotationVector.z)); - this->m_scene.flush(); - EXPECT_EQ(0u, this->sceneActionsCollector.getNumberOfActions()); // flush empty and optimized away - } - - TYPED_TEST(NodeTransformationTestWithPublishedScene, setScalingWithValuesEqualToCurrentValuesDoesNotCreateSceneActions) - { - ramses_internal::Vector3 scalingVector(1.2f, 2.3f, 4.5f); - EXPECT_CALL(this->sceneActionsCollector, handleSceneUpdate_rvr(ramses_internal::SceneId(this->m_scene.impl.getSceneId().getValue()), _, _)); - EXPECT_EQ(StatusOK, this->m_node->setScaling(scalingVector.x, scalingVector.y, scalingVector.z)); - this->m_scene.flush(); - EXPECT_LE(1u, this->sceneActionsCollector.getNumberOfActions()); - - Mock::VerifyAndClearExpectations(this); - this->sceneActionsCollector.resetCollecting(); - - EXPECT_EQ(StatusOK, this->m_node->setScaling(scalingVector.x, scalingVector.y, scalingVector.z)); - this->m_scene.flush(); - EXPECT_EQ(0u, this->sceneActionsCollector.getNumberOfActions()); // flush empty and optimized away - } -} diff --git a/client/ramses-client/test/RamsesAnimationObjectOwnershipTest.cpp b/client/ramses-client/test/RamsesAnimationObjectOwnershipTest.cpp deleted file mode 100644 index 3ebd4aea5..000000000 --- a/client/ramses-client/test/RamsesAnimationObjectOwnershipTest.cpp +++ /dev/null @@ -1,139 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include - -#include "ramses-client-api/AnimationSystem.h" -#include "ramses-client-api/AnimationSystemRealTime.h" -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/SplineStepBool.h" -#include "ramses-client-api/SplineStepInt32.h" -#include "ramses-client-api/SplineStepFloat.h" -#include "ramses-client-api/SplineStepVector2f.h" -#include "ramses-client-api/SplineStepVector3f.h" -#include "ramses-client-api/SplineStepVector4f.h" -#include "ramses-client-api/SplineStepVector2i.h" -#include "ramses-client-api/SplineStepVector3i.h" -#include "ramses-client-api/SplineStepVector4i.h" -#include "ramses-client-api/SplineLinearInt32.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/SplineLinearVector2f.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/SplineLinearVector4f.h" -#include "ramses-client-api/SplineLinearVector2i.h" -#include "ramses-client-api/SplineLinearVector3i.h" -#include "ramses-client-api/SplineLinearVector4i.h" -#include "ramses-client-api/SplineBezierInt32.h" -#include "ramses-client-api/SplineBezierFloat.h" -#include "ramses-client-api/SplineBezierVector2f.h" -#include "ramses-client-api/SplineBezierVector3f.h" -#include "ramses-client-api/SplineBezierVector4f.h" -#include "ramses-client-api/SplineBezierVector2i.h" -#include "ramses-client-api/SplineBezierVector3i.h" -#include "ramses-client-api/SplineBezierVector4i.h" - -#include "RamsesClientImpl.h" -#include "SceneImpl.h" -#include "AnimationSystemImpl.h" -#include "ClientTestUtils.h" -#include "RamsesObjectTestTypes.h" - -namespace ramses -{ - using namespace testing; - - template - class AnimationSystemOwnershipTest : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test - { - protected: - void expectNoFrameworkObjectsAllocated() - { - m_creationHelper.destroyAdditionalAllocatedAnimationSystemObjects(); - const ramses_internal::IAnimationSystem& animSystem = this->animationSystem.impl.getIAnimationSystem(); - for (ramses_internal::SplineHandle i(0); i < animSystem.getTotalSplineCount(); ++i) - { - EXPECT_FALSE(animSystem.containsSpline(i)); - } - for (ramses_internal::DataBindHandle i(0); i < animSystem.getTotalDataBindCount(); ++i) - { - EXPECT_FALSE(animSystem.containsDataBinding(i)); - } - for (ramses_internal::AnimationInstanceHandle i(0); i < animSystem.getTotalAnimationInstanceCount(); ++i) - { - EXPECT_FALSE(animSystem.containsAnimationInstance(i)); - } - for (ramses_internal::AnimationHandle i(0); i < animSystem.getTotalAnimationCount(); ++i) - { - EXPECT_FALSE(animSystem.containsAnimation(i)); - } - } - }; - - TYPED_TEST_SUITE(AnimationSystemOwnershipTest, AnimationObjectTypes); - - TYPED_TEST(AnimationSystemOwnershipTest, animationSystemObjectsAreOfTypeAnimationObject) - { - const AnimationObject* obj = &this->template createObject("objectName"); - ASSERT_TRUE(nullptr != obj); - EXPECT_TRUE(obj->isOfType(ERamsesObjectType_AnimationObject)); - EXPECT_TRUE(obj->isOfType(ERamsesObjectType_SceneObject)); - EXPECT_TRUE(obj->isOfType(ERamsesObjectType_ClientObject)); - } - - TYPED_TEST(AnimationSystemOwnershipTest, animationSystemObjectsHaveReferenceToTheirAnimationSystem) - { - const AnimationObject* obj = &this->template createObject("objectName"); - ASSERT_TRUE(nullptr != obj); - EXPECT_EQ(&this->animationSystem.impl, &obj->impl.getAnimationSystemImpl()); - EXPECT_EQ(&this->animationSystem.impl.getIAnimationSystem(), &obj->impl.getIAnimationSystem()); - EXPECT_EQ(&this->m_scene.impl, &obj->impl.getSceneImpl()); - EXPECT_EQ(&this->m_scene.impl.getIScene(), &obj->impl.getIScene()); - EXPECT_EQ(&this->client.impl, &obj->impl.getClientImpl()); - } - - TYPED_TEST(AnimationSystemOwnershipTest, animationSystemContainsCreatedObject) - { - const RamsesObject* obj = &this->template createObject("objectName"); - ASSERT_TRUE(nullptr != obj); - const RamsesObject* sn = this->animationSystem.findObjectByName("objectName"); - ASSERT_TRUE(nullptr != sn); - EXPECT_EQ(obj, sn); - } - - TYPED_TEST(AnimationSystemOwnershipTest, animationSystemDoesNotContainDestroyedObject) - { - AnimationObject* obj = &this->template createObject("objectName"); - ASSERT_TRUE(nullptr != obj); - EXPECT_EQ(StatusOK, this->animationSystem.destroy(*obj)); - const RamsesObject* sn = this->animationSystem.findObjectByName("objectName"); - ASSERT_TRUE(nullptr == sn); - this->expectNoFrameworkObjectsAllocated(); - } - - TYPED_TEST(AnimationSystemOwnershipTest, animationObjectNameChanged) - { - RamsesObject* obj = &this->template createObject("objectName"); - ASSERT_TRUE(nullptr != obj); - obj->setName("otherObjectName"); - RamsesObject* sn = this->animationSystem.findObjectByName("otherObjectName"); - ASSERT_TRUE(nullptr != sn); - EXPECT_EQ(obj, sn); - } - - TYPED_TEST(AnimationSystemOwnershipTest, animationSystemNotContainsDestroyedObject) - { - RamsesObject& obj = this->template createObject("objectName"); - TypeParam& objTyped = static_cast(obj); - - this->animationSystem.destroy(objTyped); - const RamsesObject* sn = this->animationSystem.findObjectByName("objectName"); - EXPECT_EQ(nullptr, sn); - } -} diff --git a/client/ramses-client/test/RamsesHmiUtilsTest.cpp b/client/ramses-client/test/RamsesHmiUtilsTest.cpp deleted file mode 100644 index 923ba9c77..000000000 --- a/client/ramses-client/test/RamsesHmiUtilsTest.cpp +++ /dev/null @@ -1,182 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-hmi-utils.h" -#include "ramses-client-api/RenderGroup.h" -#include "RenderGroupImpl.h" -#include "ramses-client-api/Texture2D.h" -#include "ramses-client-api/UniformInput.h" -#include "ramses-client-api/Effect.h" -#include "ramses-client-api/MeshNode.h" -#include "TestEffects.h" -#include "gmock/gmock.h" -#include - -using namespace testing; - -namespace -{ - const char* vertexShader = - "#version 100 \n" - "uniform highp mat4 mvpMatrix; \n" - "attribute vec3 a_position; \n" - "attribute vec2 a_texcoord; \n" - "varying vec2 v_texcoord; \n" - " \n" - "void main() \n" - "{ \n" - " gl_Position = mvpMatrix * vec4(a_position, 1.0); \n" - " v_texcoord = a_texcoord; \n" - "} \n"; - - const char* fragmentShader = - "#version 100 \n" - "uniform sampler2D textureSampler; \n" - "uniform sampler2D textureSampler2; \n" - "varying lowp vec2 v_texcoord; \n" - " \n" - "void main(void) \n" - "{ \n" - " gl_FragColor = texture2D(textureSampler, v_texcoord); \n" - "} \n"; -} - -namespace ramses -{ - class ARamsesHmiUtils : public ::testing::Test - { - public: - static Texture2D* CreateTestTexture(Scene& scene, uint8_t num) - { - uint8_t data[4] = { num }; - MipLevelData mipLevelData(sizeof(data), data); - Texture2D* texture = scene.createTexture2D(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, {}, ResourceCacheFlag_DoNotCache); - EXPECT_TRUE(texture != nullptr); - return texture; - } - - static void SaveSceneAndResourcesToFiles(bool createMissingResources = false) - { - RamsesFramework frameworkForSaving; - RamsesClient& clientForSaving(*frameworkForSaving.createClient("clientForSaving")); - Scene* scene = clientForSaving.createScene(sceneId_t(1)); - - Texture2D* texture1 = CreateTestTexture(*scene, 1u); - Texture2D* texture2 = CreateTestTexture(*scene, 2u); - CreateTestTexture(*scene, 3u); - - auto sampler1 = scene->createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, *texture1, 1, "sampler1"); - auto sampler2 = scene->createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, *texture2, 1, "sampler2"); - - ramses::EffectDescription effectDesc; - effectDesc.setVertexShader(vertexShader); - effectDesc.setFragmentShader(fragmentShader); - effectDesc.setUniformSemantic("mvpMatrix", ramses::EEffectUniformSemantic::ModelViewProjectionMatrix); - - ramses::Effect* effectTex = scene->createEffect(effectDesc, ramses::ResourceCacheFlag_DoNotCache); - ramses::Appearance* appearance = scene->createAppearance(*effectTex); - ramses::UniformInput textureInput; - effectTex->findUniformInput("textureSampler", textureInput); - appearance->setInputTexture(textureInput, *sampler1); - ramses::UniformInput textureInput2; - effectTex->findUniformInput("textureSampler2", textureInput2); - appearance->setInputTexture(textureInput2, *sampler2); - - ramses::MeshNode* meshNode = scene->createMeshNode(); - meshNode->setAppearance(*appearance); - - if (createMissingResources) - scene->destroy(*texture1); - - EXPECT_EQ(StatusOK, scene->saveToFile("scene.ramscene", false)); - } - - ARamsesHmiUtils() - : framework() - , client(*framework.createClient("client")) - { - } - - RamsesFramework framework; - RamsesClient& client; - }; - - TEST_F(ARamsesHmiUtils, allResourcesKnownWhenLoadingScene) - { - SaveSceneAndResourcesToFiles(); - - Scene* scene = client.loadSceneFromFile("scene.ramscene"); - ASSERT_TRUE(scene != nullptr); - - EXPECT_TRUE(RamsesHMIUtils::AllResourcesForSceneKnown(*scene)); - } - - TEST_F(ARamsesHmiUtils, resourcesMissingWhenLoadingSceneWhichWasMissingResource) - { - SaveSceneAndResourcesToFiles(true); - - Scene* scene = client.loadSceneFromFile("scene.ramscene"); - ASSERT_TRUE(scene != nullptr); - - EXPECT_FALSE(RamsesHMIUtils::AllResourcesForSceneKnown(*scene)); - } - - TEST_F(ARamsesHmiUtils, allResourcesKnownWhenCreatingMissingResourcesLater) - { - SaveSceneAndResourcesToFiles(true); - - Scene* scene = client.loadSceneFromFile("scene.ramscene"); - ASSERT_TRUE(scene != nullptr); - EXPECT_FALSE(RamsesHMIUtils::AllResourcesForSceneKnown(*scene)); - - EXPECT_TRUE(CreateTestTexture(*scene, 1u)); - EXPECT_TRUE(RamsesHMIUtils::AllResourcesForSceneKnown(*scene)); - } - - TEST_F(ARamsesHmiUtils, canDumpUnrequiredSceneObjects) - { - SaveSceneAndResourcesToFiles(); - - Scene* scene = client.loadSceneFromFile("scene.ramscene"); - ASSERT_TRUE(scene != nullptr); - - // method only has side effects, only tests possible is that it does not crash - RamsesHMIUtils::DumpUnrequiredSceneObjects(*scene); - } - - TEST_F(ARamsesHmiUtils, canDumpUnrequiredSceneObjectsToFile) - { - SaveSceneAndResourcesToFiles(); - - Scene* scene = client.loadSceneFromFile("scene.ramscene"); - ASSERT_TRUE(scene != nullptr); - - { - std::ofstream outf("unreqObjects.txt", std::ios::out | std::ios::trunc); - RamsesHMIUtils::DumpUnrequiredSceneObjectsToFile(*scene, outf); - } - - std::ifstream inf("unreqObjects.txt", std::ios::in); - std::string content((std::istreambuf_iterator(inf)), std::istreambuf_iterator()); - // cannot check whole content because order of entries not deterministic - EXPECT_THAT(content, HasSubstr("Not required ERamsesObjectType_TextureSampler name: \"sampler2\" handle: 1")); - } - - TEST_F(ARamsesHmiUtils, canGiveTheResourceDataPoolForAnyGivenClient) - { - RamsesClient& client1(*framework.createClient("client1")); - RamsesClient& client2(*framework.createClient("client2")); - - auto& rdp1 = RamsesHMIUtils::GetResourceDataPoolForClient(client1); - auto& rdp2 = RamsesHMIUtils::GetResourceDataPoolForClient(client2); - auto& rdp1again = RamsesHMIUtils::GetResourceDataPoolForClient(client1); - - EXPECT_EQ(&rdp1, &rdp1again); - EXPECT_NE(&rdp1, &rdp2); - } -} diff --git a/client/ramses-client/test/RamsesObjectRegistryTest.cpp b/client/ramses-client/test/RamsesObjectRegistryTest.cpp deleted file mode 100644 index 4d2e9bdf5..000000000 --- a/client/ramses-client/test/RamsesObjectRegistryTest.cpp +++ /dev/null @@ -1,208 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include -#include "ramses-client-api/RamsesObject.h" -#include "RamsesObjectRegistry.h" -#include "NodeImpl.h" -#include "ramses-client-api/Node.h" -#include "ClientTestUtils.h" -#include - -namespace ramses -{ - using namespace testing; - - using DummyObjectImpl = NodeImpl; - class DummyObject : public Node - { - public: - explicit DummyObject(SceneImpl& scene) - : Node(*new DummyObjectImpl(scene, ERamsesObjectType_Node, "")) - { - } - }; - - class ARamsesObjectRegistry : public testing::Test - { - public: - ARamsesObjectRegistry() - : m_dummyObject(*createDummyObject()) - { - } - - ~ARamsesObjectRegistry() - { - delete &m_dummyObject; - } - - DummyObject* createDummyObject() - { - return new DummyObject(m_dummyScene.getScene().impl); - } - - protected: - LocalTestClientWithScene m_dummyScene; - RamsesObjectRegistry m_registry; - DummyObject& m_dummyObject; - }; - - TEST_F(ARamsesObjectRegistry, isEmptyUponCreation) - { - RamsesObjectVector objects; - m_registry.getObjectsOfType(objects, ERamsesObjectType_RamsesObject); - EXPECT_TRUE(objects.empty()); - EXPECT_EQ(0u, m_registry.getNumberOfObjects(ERamsesObjectType_Node)); - } - - TEST_F(ARamsesObjectRegistry, canAddObject) - { - m_dummyObject.setName("name"); - m_registry.addObject(m_dummyObject); - EXPECT_EQ(&m_dummyObject, m_registry.findObjectByName("name")); - EXPECT_EQ(&m_dummyObject, m_registry.findObjectById(sceneObjectId_t{1u})); - EXPECT_EQ(&m_dummyObject, &m_dummyObject.impl.getRamsesObject()); - EXPECT_EQ(1u, m_registry.getNumberOfObjects(ERamsesObjectType_Node)); - EXPECT_EQ(0u, m_registry.getNumberOfObjects(ERamsesObjectType_RenderBuffer)); - RamsesObjectVector objects; - m_registry.getObjectsOfType(objects, ERamsesObjectType_RamsesObject); - ASSERT_EQ(1u, objects.size()); - EXPECT_EQ(&m_dummyObject, objects[0]); - } - - TEST_F(ARamsesObjectRegistry, sceneObjectsGetUniqueIds) - { - const std::unique_ptr dummy1(createDummyObject()); - const std::unique_ptr dummy2(createDummyObject()); - const std::unique_ptr dummy3(createDummyObject()); - const std::unique_ptr dummy4(createDummyObject()); - const std::unique_ptr dummy5(createDummyObject()); - - const std::unordered_set sceneObjectIds - { - dummy1->getSceneObjectId(), - dummy2->getSceneObjectId(), - dummy3->getSceneObjectId(), - dummy4->getSceneObjectId(), - dummy5->getSceneObjectId() - }; - - EXPECT_EQ(sceneObjectIds.size(), 5u); - } - - TEST_F(ARamsesObjectRegistry, canRemoveObject) - { - m_dummyObject.setName("name"); - m_registry.addObject(m_dummyObject); - m_registry.removeObject(m_dummyObject); - EXPECT_TRUE(nullptr == m_registry.findObjectByName("name")); - EXPECT_TRUE(nullptr == m_registry.findObjectById(sceneObjectId_t{1u})); - EXPECT_EQ(0u, m_registry.getNumberOfObjects(ERamsesObjectType_Node)); - EXPECT_EQ(0u, m_registry.getNumberOfObjects(ERamsesObjectType_RenderBuffer)); - RamsesObjectVector objects; - m_registry.getObjectsOfType(objects, ERamsesObjectType_RamsesObject); - EXPECT_TRUE(objects.empty()); - } - - - TEST_F(ARamsesObjectRegistry, canAddAndRetrieveObjectInfo) - { - m_dummyObject.setName("name"); - m_registry.addObject(m_dummyObject); - EXPECT_EQ(&m_dummyObject, m_registry.findObjectByName("name")); - EXPECT_EQ(&m_dummyObject, m_registry.findObjectById(sceneObjectId_t{1u})); - } - - TEST_F(ARamsesObjectRegistry, cannotRetrieveObjectInfoAfterObjectDeleted) - { - m_dummyObject.setName("name"); - m_registry.addObject(m_dummyObject); - m_registry.removeObject(m_dummyObject); - EXPECT_TRUE(nullptr == m_registry.findObjectByName("name")); - EXPECT_TRUE(nullptr == m_registry.findObjectById(sceneObjectId_t{1u})); - } - - TEST_F(ARamsesObjectRegistry, cannotFindObjectByOldNameWhenNameUpdatedAfterAdding) - { - m_dummyObject.setName("name_old"); - m_registry.addObject(m_dummyObject); - m_dummyObject.setName("name_new"); - EXPECT_TRUE(nullptr == m_registry.findObjectByName("name_old")); - } - - TEST_F(ARamsesObjectRegistry, canAddAndRetrieveObjectInfoWhenUpdatedAfterAdding) - { - m_registry.addObject(m_dummyObject); - m_dummyObject.setName("name"); - EXPECT_EQ(&m_dummyObject, m_registry.findObjectByName("name")); - } - - TEST_F(ARamsesObjectRegistry, canRetrieveObjectFromImpl) - { - m_registry.addObject(m_dummyObject); - EXPECT_EQ(&m_dummyObject, &m_dummyObject.impl.getRamsesObject()); - } - - TEST_F(ARamsesObjectRegistry, hasNoDirtyNodesAfterInitialization) - { - EXPECT_EQ(0u, m_registry.getDirtyNodes().size()); - } - - TEST_F(ARamsesObjectRegistry, marksNodeDirty) - { - m_registry.addObject(m_dummyObject); - m_registry.setNodeDirty(m_dummyObject.impl, true); - EXPECT_EQ(1u, m_registry.getDirtyNodes().size()); - EXPECT_TRUE(m_registry.getDirtyNodes().contains(&m_dummyObject.impl)); - EXPECT_TRUE(m_registry.isNodeDirty(m_dummyObject.impl)); - } - - TEST_F(ARamsesObjectRegistry, marksNodeClean) - { - std::unique_ptr object1(createDummyObject()); - m_registry.addObject(*object1); - m_registry.setNodeDirty(object1->impl, true); - - std::unique_ptr object2(createDummyObject()); - m_registry.addObject(*object2); - m_registry.setNodeDirty(object2->impl, true); - - EXPECT_EQ(2u, m_registry.getDirtyNodes().size()); - - m_registry.setNodeDirty(object1->impl, false); - EXPECT_EQ(1u, m_registry.getDirtyNodes().size()); - EXPECT_FALSE(m_registry.getDirtyNodes().contains(&object1->impl)); - EXPECT_FALSE(m_registry.isNodeDirty(object1->impl)); - } - - TEST_F(ARamsesObjectRegistry, clearsDirtyNodes) - { - m_registry.addObject(m_dummyObject); - m_registry.setNodeDirty(m_dummyObject.impl, true); - m_registry.clearDirtyNodes(); - EXPECT_EQ(0u, m_registry.getDirtyNodes().size()); - EXPECT_FALSE(m_registry.isNodeDirty(m_dummyObject.impl)); - } - - TEST_F(ARamsesObjectRegistry, marksNodeCleanWhenDeleted) - { - m_registry.addObject(m_dummyObject); - m_registry.setNodeDirty(m_dummyObject.impl, true); - m_registry.removeObject(m_dummyObject); - EXPECT_EQ(0u, m_registry.getDirtyNodes().size()); - } - - TEST_F(ARamsesObjectRegistry, nodeAppearsInDirtyListAfterMarkedDirty) - { - m_registry.addObject(m_dummyObject); - m_registry.setNodeDirty(m_dummyObject.impl, true); - EXPECT_EQ(1u, m_registry.getDirtyNodes().size()); - EXPECT_TRUE(m_registry.getDirtyNodes().contains(&m_dummyObject.impl)); - EXPECT_TRUE(m_registry.isNodeDirty(m_dummyObject.impl)); - } -} diff --git a/client/ramses-client/test/RamsesObjectTest1.cpp b/client/ramses-client/test/RamsesObjectTest1.cpp deleted file mode 100644 index 2048f9397..000000000 --- a/client/ramses-client/test/RamsesObjectTest1.cpp +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/MeshNode.h" -#include "ramses-client-api/PerspectiveCamera.h" -#include "ramses-client-api/OrthographicCamera.h" -#include "ramses-client-api/AnimationSystem.h" -#include "ramses-client-api/AnimationSystemRealTime.h" -#include "ramses-client-api/Spline.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSequence.h" -#include "ramses-client-api/SplineStepBool.h" -#include "ramses-client-api/SplineStepInt32.h" -#include "ramses-client-api/SplineStepFloat.h" -#include "ramses-client-api/SplineStepVector2f.h" -#include "ramses-client-api/SplineStepVector3f.h" -#include "ramses-client-api/SplineStepVector4f.h" -#include "ramses-client-api/SplineStepVector2i.h" -#include "ramses-client-api/SplineStepVector3i.h" -#include "ramses-client-api/SplineStepVector4i.h" -#include "ramses-client-api/SplineLinearInt32.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/SplineLinearVector2f.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/SplineLinearVector4f.h" -#include "ramses-client-api/SplineLinearVector2i.h" -#include "ramses-client-api/SplineLinearVector3i.h" -#include "ramses-client-api/SplineLinearVector4i.h" -#include "ramses-client-api/SplineBezierInt32.h" -#include "ramses-client-api/SplineBezierFloat.h" -#include "ramses-client-api/SplineBezierVector2f.h" -#include "ramses-client-api/SplineBezierVector3f.h" -#include "ramses-client-api/SplineBezierVector4f.h" -#include "ramses-client-api/SplineBezierVector2i.h" -#include "ramses-client-api/SplineBezierVector3i.h" -#include "ramses-client-api/SplineBezierVector4i.h" -#include "ramses-client-api/Texture2D.h" -#include "ramses-client-api/Texture3D.h" -#include "ramses-client-api/TextureCube.h" -#include "ramses-client-api/StreamTexture.h" - -#include "ramses-utils.h" -#include "RamsesObjectTestTypes.h" -#include "RamsesObjectTestCommon.h" - -using namespace testing; - -namespace ramses -{ - INSTANTIATE_TYPED_TEST_SUITE_P(RamsesObjectTest1, RamsesObjectTest, RamsesObjectTypes1); -} diff --git a/client/ramses-client/test/RamsesObjectTest2.cpp b/client/ramses-client/test/RamsesObjectTest2.cpp deleted file mode 100644 index dc77e24d3..000000000 --- a/client/ramses-client/test/RamsesObjectTest2.cpp +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/ArrayResource.h" -#include "ramses-client-api/TextureSampler.h" -#include "ramses-client-api/TextureSamplerMS.h" -#include "ramses-client-api/RenderBuffer.h" -#include "ramses-client-api/RenderTarget.h" -#include "ramses-client-api/RenderGroup.h" -#include "ramses-client-api/RenderPass.h" -#include "ramses-client-api/BlitPass.h" -#include "ramses-client-api/EffectDescription.h" -#include "ramses-client-api/GeometryBinding.h" -#include "ramses-client-api/DataFloat.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-client-api/DataVector4f.h" -#include "ramses-client-api/DataMatrix22f.h" -#include "ramses-client-api/DataMatrix33f.h" -#include "ramses-client-api/DataMatrix44f.h" -#include "ramses-client-api/DataInt32.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector3i.h" -#include "ramses-client-api/DataVector4i.h" -#include "ramses-client-api/ArrayBuffer.h" -#include "ramses-client-api/Texture2DBuffer.h" -#include "ramses-client-api/PickableObject.h" -#include "ramses-client-api/SceneReference.h" - -#include "ramses-utils.h" -#include "RamsesObjectTestTypes.h" -#include "RamsesObjectTestCommon.h" - -using namespace testing; - -namespace ramses -{ - INSTANTIATE_TYPED_TEST_SUITE_P(RamsesObjectTest2, RamsesObjectTest, RamsesObjectTypes2); -} diff --git a/client/ramses-client/test/RamsesObjectTestCommon.h b/client/ramses-client/test/RamsesObjectTestCommon.h deleted file mode 100644 index 3cb3fd6c2..000000000 --- a/client/ramses-client/test/RamsesObjectTestCommon.h +++ /dev/null @@ -1,157 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_RAMSESOBJECTTESTCOMMON_H -#define RAMSES_RAMSESOBJECTTESTCOMMON_H - -#include "ClientTestUtils.h" -#include "RamsesObjectTypeUtils.h" -#include "Collections/String.h" - -namespace ramses -{ - template - class RamsesObjectTest : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test - { - }; - - TYPED_TEST_SUITE_P(RamsesObjectTest); - - TYPED_TEST_P(RamsesObjectTest, getType) - { - const RamsesObject& obj = this->template createObject("object"); - const ERamsesObjectType type = TYPE_ID_OF_RAMSES_OBJECT::ID; - EXPECT_EQ(type, obj.getType()); - EXPECT_TRUE(obj.isOfType(ERamsesObjectType_RamsesObject)); - } - - TYPED_TEST_P(RamsesObjectTest, isOfTypeForAllDefinedBaseClasses) - { - const RamsesObject& obj = this->template createObject("object"); - ERamsesObjectType type = TYPE_ID_OF_RAMSES_OBJECT::ID; - - while (type != ERamsesObjectType_Invalid) - { - EXPECT_TRUE(obj.isOfType(type)); - type = RamsesObjectTraits[type].baseClassTypeID; - } - } - - TYPED_TEST_P(RamsesObjectTest, getSetName) - { - RamsesObject& obj = this->template createObject("object"); - ASSERT_STREQ("object", obj.getName()); - EXPECT_EQ(StatusOK, obj.setName("newName")); - ASSERT_STREQ("newName", obj.getName()); - } - - TYPED_TEST_P(RamsesObjectTest, convertToTypes) - { - RamsesObject& obj = this->template createObject("object"); - const RamsesObject& constObj = obj; - - EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(obj)); - EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(constObj)); - - EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(obj)); - EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(constObj)); - } - - TYPED_TEST_P(RamsesObjectTest, convertToItsClosestBaseClass) - { - using BaseClassType = typename CLASS_OF_RAMSES_OBJECT_TYPE::ID>::BaseTypeID>::ClassType; - - RamsesObject& obj = this->template createObject("object"); - const RamsesObject& constObj = obj; - - if (TYPE_ID_OF_RAMSES_OBJECT::ID != ERamsesObjectType_Invalid) - { - EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(obj)); - EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(constObj)); - } - } - - TYPED_TEST_P(RamsesObjectTest, getRamsesObjectFromImpl) - { - RamsesObject& obj = this->template createObject("object"); - const RamsesObject& constObj = obj; - - RamsesObjectImpl& objImpl = obj.impl; - const RamsesObjectImpl& constObjImpl = constObj.impl; - - EXPECT_EQ(&obj, &objImpl.getRamsesObject()); - EXPECT_EQ(&constObj, &constObjImpl.getRamsesObject()); - } - - TYPED_TEST_P(RamsesObjectTest, validationStringIsNonEmptyAfterCall) - { - const RamsesObject& obj = this->template createObject("object"); - - EXPECT_STREQ("", obj.getValidationReport()); - obj.validate(); - EXPECT_STRNE("", obj.getValidationReport()); - } - - TYPED_TEST_P(RamsesObjectTest, validationStringContainsObjectTypeAndName) - { - const RamsesObject& obj = this->template createObject("object"); - - EXPECT_NE(StatusOK, obj.impl.addErrorEntry("dummy")); - obj.validate(); - const ramses_internal::String validationReport = obj.getValidationReport(EValidationSeverity_Info); - - EXPECT_THAT(validationReport.stdRef(), ::testing::HasSubstr(RamsesObjectTypeUtils::GetRamsesObjectTypeName(obj.getType()))); - EXPECT_THAT(validationReport.stdRef(), ::testing::HasSubstr(obj.getName())); - } - - TYPED_TEST_P(RamsesObjectTest, validationIgnoresWrongAPIUsage) - { - RamsesObject& obj = this->template createObject("object"); - - // simulate some API usage error - EXPECT_NE(StatusOK, obj.impl.addErrorEntry("dummy error msg")); - - obj.validate(); - EXPECT_THAT(obj.getValidationReport(EValidationSeverity::EValidationSeverity_Warning), ::testing::Not(::testing::HasSubstr("dummy error msg"))); - - // will not appear in validation of scene either - this->m_scene.validate(); - EXPECT_THAT(this->m_scene.getValidationReport(EValidationSeverity::EValidationSeverity_Warning), ::testing::Not(::testing::HasSubstr("dummy error msg"))); - } - - TYPED_TEST_P(RamsesObjectTest, convertToWrongType) - { - RamsesObject& obj = this->template createObject("object"); - const RamsesObject& constObj = obj; - - if (constObj.getType() != ERamsesObjectType_SplineStepBool) - { - EXPECT_TRUE(nullptr == RamsesUtils::TryConvert(obj)); - EXPECT_TRUE(nullptr == RamsesUtils::TryConvert(constObj)); - } - else - { - EXPECT_TRUE(nullptr == RamsesUtils::TryConvert(obj)); - EXPECT_TRUE(nullptr == RamsesUtils::TryConvert(constObj)); - } - } - - REGISTER_TYPED_TEST_SUITE_P(RamsesObjectTest, - getType, - isOfTypeForAllDefinedBaseClasses, - getSetName, - convertToTypes, - convertToItsClosestBaseClass, - convertToWrongType, - getRamsesObjectFromImpl, - validationStringIsNonEmptyAfterCall, - validationStringContainsObjectTypeAndName, - validationIgnoresWrongAPIUsage); -} - -#endif diff --git a/client/ramses-client/test/RamsesObjectTestTypes.h b/client/ramses-client/test/RamsesObjectTestTypes.h deleted file mode 100644 index 02679543f..000000000 --- a/client/ramses-client/test/RamsesObjectTestTypes.h +++ /dev/null @@ -1,275 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_RAMSESOBJECTTESTTYPES_H -#define RAMSES_RAMSESOBJECTTESTTYPES_H - -#include - -namespace ramses -{ - class RamsesClient; - class Scene; - class Appearance; - class Camera; - class PerspectiveCamera; - class OrthographicCamera; - class Appearance; - class Node; - class Effect; - class MeshNode; - class AnimationSystem; - class AnimationSystemRealTime; - class ArrayBuffer; - class Texture2DBuffer; - class GeometryBinding; - class RenderGroup; - class RenderPass; - class RenderBuffer; - class RenderTarget; - class DataFloat; - class DataVector2f; - class DataVector3f; - class DataVector4f; - class DataMatrix22f; - class DataMatrix33f; - class DataMatrix44f; - class DataInt32; - class DataVector2i; - class DataVector3i; - class DataVector4i; - class BlitPass; - class TextureSampler; - class TextureSamplerMS; - class StreamTexture; - class Texture2D; - class Texture3D; - class TextureCube; - class AnimatedProperty; - class Animation; - class AnimationSequence; - class Spline; - class SplineStepBool; - class SplineStepInt32; - class SplineStepFloat; - class SplineStepVector2f; - class SplineStepVector3f; - class SplineStepVector4f; - class SplineStepVector2i; - class SplineStepVector3i; - class SplineStepVector4i; - class SplineLinearInt32; - class SplineLinearFloat; - class SplineLinearVector2f; - class SplineLinearVector3f; - class SplineLinearVector4f; - class SplineLinearVector2i; - class SplineLinearVector3i; - class SplineLinearVector4i; - class SplineBezierInt32; - class SplineBezierFloat; - class SplineBezierVector2f; - class SplineBezierVector3f; - class SplineBezierVector4f; - class SplineBezierVector2i; - class SplineBezierVector3i; - class SplineBezierVector4i; - class PickableObject; - class SceneReference; - class ArrayResource; - - // Objects derived from Node class - using NodeTypes = ::testing::Types< - Node, - MeshNode, - PerspectiveCamera, - OrthographicCamera, - PickableObject>; - - // Objects derived from Resource class - using ResourceTypes = ::testing::Types< - ArrayResource, - Texture2D, - Texture3D, - TextureCube, - Effect>; - - // Objects owned by Scene - using SceneObjectTypes = ::testing::Types< - AnimationSystem, - AnimationSystemRealTime, - Node, - MeshNode, - PerspectiveCamera, - OrthographicCamera, - Appearance, - GeometryBinding, - RenderGroup, - RenderPass, - BlitPass, - TextureSampler, - TextureSamplerMS, - RenderBuffer, - RenderTarget, - DataFloat, - DataVector2f, - DataVector3f, - DataVector4f, - DataMatrix22f, - DataMatrix33f, - DataMatrix44f, - DataInt32, - DataVector2i, - DataVector3i, - DataVector4i, - StreamTexture, - ArrayBuffer, - Texture2DBuffer, - PickableObject, - SceneReference, - Texture2D, - Texture3D, - TextureCube, - ArrayResource, - Effect>; - - // Objects owned by AnimationSystem - using AnimationObjectTypes = ::testing::Types< - AnimatedProperty, - Animation, - AnimationSequence, - SplineStepBool, - SplineStepFloat, - SplineStepInt32, - SplineStepVector2f, - SplineStepVector3f, - SplineStepVector4f, - SplineStepVector2i, - SplineStepVector3i, - SplineStepVector4i, - SplineLinearFloat, - SplineLinearInt32, - SplineLinearVector2f, - SplineLinearVector3f, - SplineLinearVector4f, - SplineLinearVector2i, - SplineLinearVector3i, - SplineLinearVector4i, - SplineBezierFloat, - SplineBezierInt32, - SplineBezierVector2f, - SplineBezierVector3f, - SplineBezierVector4f, - SplineBezierVector2i, - SplineBezierVector3i, - SplineBezierVector4i>; - - // Objects owned by client - using ClientObjectTypes = ::testing::Types; - - // All Ramses objects - set #1 - using RamsesObjectTypes1 = ::testing::Types< - RamsesClient, - Scene, - AnimationSystem, - AnimationSystemRealTime, - Node, - MeshNode, - PerspectiveCamera, - OrthographicCamera, - Effect, - AnimatedProperty, - Animation, - AnimationSequence, - Appearance, - SplineStepBool, - SplineStepFloat, - SplineStepInt32, - SplineStepVector2f, - SplineStepVector3f, - SplineStepVector4f, - SplineStepVector2i, - SplineStepVector3i, - SplineStepVector4i, - SplineLinearFloat, - SplineLinearInt32, - SplineLinearVector2f, - SplineLinearVector3f, - SplineLinearVector4f, - SplineLinearVector2i, - SplineLinearVector3i, - SplineBezierFloat, - SplineBezierInt32, - SplineBezierVector2f, - SplineBezierVector3f, - SplineBezierVector4f, - SplineBezierVector2i, - SplineBezierVector3i, - SplineBezierVector4i, - Texture2D, - Texture3D, - TextureCube, - StreamTexture>; - - // All Ramses objects - set #2 - using RamsesObjectTypes2 = ::testing::Types< - ArrayResource, - RenderGroup, - RenderPass, - BlitPass, - TextureSampler, - TextureSamplerMS, - RenderBuffer, - RenderTarget, - ArrayBuffer, - Texture2DBuffer, - GeometryBinding, - DataFloat, - DataVector2f, - DataVector3f, - DataVector4f, - DataMatrix22f, - DataMatrix33f, - DataMatrix44f, - DataInt32, - DataVector2i, - DataVector3i, - DataVector4i, - PickableObject, - SceneReference>; - - // Spline types - using SplineTypes = ::testing::Types< - SplineStepBool, - SplineStepFloat, - SplineStepInt32, - SplineStepVector2f, - SplineStepVector3f, - SplineStepVector4f, - SplineStepVector2i, - SplineStepVector3i, - SplineStepVector4i, - SplineLinearFloat, - SplineLinearInt32, - SplineLinearVector2f, - SplineLinearVector3f, - SplineLinearVector4f, - SplineLinearVector2i, - SplineLinearVector3i, - SplineLinearVector4i, - SplineBezierFloat, - SplineBezierInt32, - SplineBezierVector2f, - SplineBezierVector3f, - SplineBezierVector4f, - SplineBezierVector2i, - SplineBezierVector3i, - SplineBezierVector4i>; -} - -#endif diff --git a/client/ramses-client/test/RenderTargetDescriptionTest.cpp b/client/ramses-client/test/RenderTargetDescriptionTest.cpp deleted file mode 100644 index 23dc58fc3..000000000 --- a/client/ramses-client/test/RenderTargetDescriptionTest.cpp +++ /dev/null @@ -1,160 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include -#include "ramses-client-api/RenderBuffer.h" -#include "ramses-client-api/RenderTargetDescription.h" -#include "RenderBufferImpl.h" -#include "RenderTargetDescriptionImpl.h" -#include "ClientTestUtils.h" - -using namespace testing; - -namespace ramses -{ - class RenderTargetDescriptionTest : public LocalTestClientWithScene, public testing::Test - { - public: - protected: - RenderTargetDescription rtDesc; - }; - - TEST_F(RenderTargetDescriptionTest, initialState) - { - EXPECT_TRUE(rtDesc.impl.getRenderBuffers().empty()); - } - - TEST_F(RenderTargetDescriptionTest, validatesAsWarningIfEmpty) - { - EXPECT_NE(StatusOK, rtDesc.validate()); - } - - TEST_F(RenderTargetDescriptionTest, canAddRenderBuffer) - { - const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); - - ASSERT_EQ(1u, rtDesc.impl.getRenderBuffers().size()); - EXPECT_EQ(colorRb.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[0]); - - EXPECT_EQ(StatusOK, rtDesc.validate()); - } - - TEST_F(RenderTargetDescriptionTest, canAddMultipleColorRenderBuffers) - { - const RenderBuffer& colorRb1 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - const RenderBuffer& colorRb2 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb1)); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb2)); - - ASSERT_EQ(2u, rtDesc.impl.getRenderBuffers().size()); - EXPECT_EQ(colorRb1.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[0]); - EXPECT_EQ(colorRb2.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[1]); - - EXPECT_EQ(StatusOK, rtDesc.validate()); - } - - TEST_F(RenderTargetDescriptionTest, canAddMultipleColorRenderBuffersWithDepthBuffer) - { - const RenderBuffer& colorRb1 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - const RenderBuffer& colorRb2 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - const RenderBuffer& depthRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Depth, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_ReadWrite); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb1)); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb2)); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(depthRb)); - - ASSERT_EQ(3u, rtDesc.impl.getRenderBuffers().size()); - EXPECT_EQ(colorRb1.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[0]); - EXPECT_EQ(colorRb2.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[1]); - EXPECT_EQ(depthRb.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[2]); - - EXPECT_EQ(StatusOK, rtDesc.validate()); - } - - TEST_F(RenderTargetDescriptionTest, failsToAddSameRenderBufferTwice) - { - const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); - EXPECT_NE(StatusOK, rtDesc.addRenderBuffer(colorRb)); - - ASSERT_EQ(1u, rtDesc.impl.getRenderBuffers().size()); - EXPECT_EQ(colorRb.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[0]); - } - - TEST_F(RenderTargetDescriptionTest, failsToAddRenderBufferFromAnotherScene) - { - const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); - - Scene& otherScene = *client.createScene(sceneId_t(666u)); - const RenderBuffer& otherRb = *otherScene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - EXPECT_NE(StatusOK, rtDesc.addRenderBuffer(otherRb)); - - ASSERT_EQ(1u, rtDesc.impl.getRenderBuffers().size()); - EXPECT_EQ(colorRb.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[0]); - } - - TEST_F(RenderTargetDescriptionTest, failsToAddRenderBufferWithDifferentResolution) - { - const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); - - const RenderBuffer& otherRb = *m_scene.createRenderBuffer(1u, 2u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - EXPECT_NE(StatusOK, rtDesc.addRenderBuffer(otherRb)); - - ASSERT_EQ(1u, rtDesc.impl.getRenderBuffers().size()); - EXPECT_EQ(colorRb.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[0]); - } - - TEST_F(RenderTargetDescriptionTest, failsToAddMoreThanOneDepthBuffer) - { - const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - const RenderBuffer& depthRb1 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Depth, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_ReadWrite); - const RenderBuffer& depthRb2 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_DepthStencil, ERenderBufferFormat_Depth24_Stencil8, ERenderBufferAccessMode_ReadWrite); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(depthRb1)); - EXPECT_NE(StatusOK, rtDesc.addRenderBuffer(depthRb2)); - - ASSERT_EQ(2u, rtDesc.impl.getRenderBuffers().size()); - EXPECT_EQ(colorRb.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[0]); - EXPECT_EQ(depthRb1.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[1]); - } - - TEST_F(RenderTargetDescriptionTest, failsToValidateAfterAddedRenderBufferDestroyed) - { - RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); - EXPECT_EQ(StatusOK, rtDesc.validate()); - - m_scene.destroy(colorRb); - EXPECT_NE(StatusOK, rtDesc.validate()); - } - - TEST_F(RenderTargetDescriptionTest, canAddRenderBuffersWithMsaaSampleCountNotZero) - { - const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_WriteOnly, 4u); - const RenderBuffer& depthRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Depth, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_WriteOnly, 4u); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(depthRb)); - - ASSERT_EQ(2u, rtDesc.impl.getRenderBuffers().size()); - EXPECT_EQ(colorRb.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[0]); - EXPECT_EQ(depthRb.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[1]); - } - - TEST_F(RenderTargetDescriptionTest, canNotAddRenderBuffersWithDifferentMsaaSampleCount) - { - const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_WriteOnly, 3u); - const RenderBuffer& depthRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Depth, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_WriteOnly, 4u); - EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); - EXPECT_NE(StatusOK, rtDesc.addRenderBuffer(depthRb)); - - ASSERT_EQ(1u, rtDesc.impl.getRenderBuffers().size()); - EXPECT_EQ(colorRb.impl.getRenderBufferHandle(), rtDesc.impl.getRenderBuffers()[0]); - } -} diff --git a/client/ramses-client/test/ResourceDataPoolPersistationTest.cpp b/client/ramses-client/test/ResourceDataPoolPersistationTest.cpp deleted file mode 100644 index 02399c6c4..000000000 --- a/client/ramses-client/test/ResourceDataPoolPersistationTest.cpp +++ /dev/null @@ -1,518 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client-api/ArrayResource.h" -#include "ramses-client-api/Texture2D.h" -#include "ramses-client-api/Texture3D.h" -#include "ramses-client-api/TextureCube.h" -#include "ramses-client-api/EffectDescription.h" -#include "ramses-client.h" -#include "ramses-utils.h" - -#include "ResourceDataPoolPersistationTest.h" -#include "RamsesObjectTestTypes.h" -#include "EffectImpl.h" -#include "ArrayResourceImpl.h" -#include "Texture2DImpl.h" -#include "Texture3DImpl.h" -#include "TextureCubeImpl.h" -#include "ramses-sdk-build-config.h" -#include "PlatformAbstraction/PlatformMath.h" - -namespace ramses -{ - using namespace testing; - - template - class ResourceDataPoolPersistationTest : public ResourceDataPoolPersistation {}; - - TYPED_TEST_SUITE(ResourceDataPoolPersistationTest, ResourceTypes); - - TYPED_TEST(ResourceDataPoolPersistationTest, canReadWriteResources) - { - TypeParam* resource = this->template createResource("resourceName"); - ASSERT_TRUE(resource != nullptr); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const TypeParam* loadedResource = this->template getObjectForTesting(resource->getResourceId(), "resourceName"); - - // Make sure the hash is not Invalid to prevent default constructed resources to pass - EXPECT_NE(ramses_internal::ResourceContentHash::Invalid(), loadedResource->impl.getLowlevelResourceHash()); - EXPECT_EQ(resource->impl.getLowlevelResourceHash(), loadedResource->impl.getLowlevelResourceHash()); - EXPECT_EQ(resource->getType(), loadedResource->getType()); - } - - TYPED_TEST(ResourceDataPoolPersistationTest, canReadWriteMultipleResources) - { - TypeParam* resource1 = this->template createResource("resourceName1"); - ASSERT_TRUE(resource1 != nullptr); - - TypeParam* resource2 = this->template createResource("resourceName2"); - ASSERT_TRUE(resource2 != nullptr); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const TypeParam* loadedResource1 = this->template getObjectForTesting(resource1->getResourceId(), "resourceName1"); - const TypeParam* loadedResource2 = this->template getObjectForTesting(resource2->getResourceId(), "resourceName2"); - - EXPECT_EQ(resource1->impl.getLowlevelResourceHash(), loadedResource1->impl.getLowlevelResourceHash()); - EXPECT_EQ(resource2->impl.getLowlevelResourceHash(), loadedResource2->impl.getLowlevelResourceHash()); - EXPECT_EQ(resource1->getType(), loadedResource1->getType()); - EXPECT_EQ(resource2->getType(), loadedResource2->getType()); - } - - TYPED_TEST(ResourceDataPoolPersistationTest, failsToCreateResourceMultipleTimes) - { - TypeParam* resource = this->template createResource("resourceName"); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const TypeParam* loadedResource = this->template getObjectForTesting(resource->getResourceId(), "resourceName"); - EXPECT_EQ(resource->impl.getLowlevelResourceHash(), loadedResource->impl.getLowlevelResourceHash()); - - EXPECT_FALSE(RamsesHMIUtils::GetResourceDataPoolForClient(this->m_clientForLoading).createResourceForScene(*this->m_sceneLoaded, resource->getResourceId())); - } - - TYPED_TEST(ResourceDataPoolPersistationTest, canGetResourceMultipleTimes) - { - TypeParam* resource = this->template createResource("resourceName"); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const TypeParam* loadedResource = this->template getObjectForTesting(resource->getResourceId(), "resourceName"); - EXPECT_EQ(resource->impl.getLowlevelResourceHash(), loadedResource->impl.getLowlevelResourceHash()); - - auto res2 = this->m_sceneLoaded->getResource(resource->getResourceId()); - EXPECT_EQ(loadedResource, res2); - } - - TYPED_TEST(ResourceDataPoolPersistationTest, cantGetResourceBeforeAddingFileToResourcePool) - { - TypeParam* resource = this->template createResource("resourceName"); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - if (!this->m_sceneLoaded) - this->m_sceneLoaded = this->m_clientForLoading.createScene(sceneId_t{ 0xf00b42 }); - EXPECT_FALSE(this->m_sceneLoaded->getResource(resource->getResourceId())); - } - - TEST_F(ResourceDataPoolPersistation, canReadWriteArrayResource) - { - ArrayResource* resource = this->template createResource("resourceName"); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const ArrayResource* loadedResource = this->template getObjectForTesting(resource->getResourceId(), "resourceName"); - EXPECT_EQ(resource->getNumberOfElements(), loadedResource->getNumberOfElements()); - EXPECT_EQ(resource->getDataType(), loadedResource->getDataType()); - } - - TEST_F(ResourceDataPoolPersistation, canReadWriteArrayResourceWithOtherTypeAndSize) - { - const float data[6] = { .0f }; - ArrayResource* resource = m_scene.createArrayResource(EDataType::Vector3F, 2, data, ResourceCacheFlag_DoNotCache, "resourceName"); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const ArrayResource* loadedResource = this->getObjectForTesting(resource->getResourceId(), "resourceName"); - EXPECT_EQ(resource->getNumberOfElements(), loadedResource->getNumberOfElements()); - EXPECT_EQ(resource->getDataType(), loadedResource->getDataType()); - } - - TEST_F(ResourceDataPoolPersistation, canReadWriteTexture2D) - { - Texture2D* resource = this->createResource("resourceName"); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const Texture2D* loadedResource = this->getObjectForTesting(resource->getResourceId(), "resourceName"); - EXPECT_EQ(resource->impl.getHeight(), loadedResource->impl.getHeight()); - EXPECT_EQ(resource->impl.getWidth(), loadedResource->impl.getWidth()); - EXPECT_EQ(resource->impl.getTextureFormat(), loadedResource->impl.getTextureFormat()); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelRed, loadedResource->impl.getTextureSwizzle().channelRed); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelGreen, loadedResource->impl.getTextureSwizzle().channelGreen); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelBlue, loadedResource->impl.getTextureSwizzle().channelBlue); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelAlpha, loadedResource->impl.getTextureSwizzle().channelAlpha); - } - - TEST_F(ResourceDataPoolPersistation, canReadWriteTexture2DwithNonDefaultSwizzle) - { - const TextureSwizzle swizzle = {ETextureChannelColor::Alpha, ETextureChannelColor::Red, ETextureChannelColor::Green, ETextureChannelColor::Blue}; - const uint8_t data[4] = { 0u }; - const MipLevelData mipLevelData(sizeof(data), data); - Texture2D* resource = m_scene.createTexture2D(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, swizzle, ResourceCacheFlag_DoNotCache, "resourceName"); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const Texture2D* loadedResource = this->getObjectForTesting(resource->getResourceId(), "resourceName"); - EXPECT_EQ(resource->impl.getHeight(), loadedResource->impl.getHeight()); - EXPECT_EQ(resource->impl.getWidth(), loadedResource->impl.getWidth()); - EXPECT_EQ(resource->impl.getTextureFormat(), loadedResource->impl.getTextureFormat()); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelRed, loadedResource->impl.getTextureSwizzle().channelRed); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelGreen, loadedResource->impl.getTextureSwizzle().channelGreen); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelBlue, loadedResource->impl.getTextureSwizzle().channelBlue); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelAlpha, loadedResource->impl.getTextureSwizzle().channelAlpha); - } - - TEST_F(ResourceDataPoolPersistation, canReadWriteTexture3D) - { - Texture3D* resource = this->createResource("resourceName"); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const Texture3D* loadedResource = this->getObjectForTesting(resource->getResourceId(), "resourceName"); - EXPECT_EQ(resource->impl.getHeight(), loadedResource->impl.getHeight()); - EXPECT_EQ(resource->impl.getWidth(), loadedResource->impl.getWidth()); - EXPECT_EQ(resource->impl.getDepth(), loadedResource->impl.getDepth()); - EXPECT_EQ(resource->impl.getTextureFormat(), loadedResource->impl.getTextureFormat()); - } - - TEST_F(ResourceDataPoolPersistation, canReadWriteTextureCube) - { - TextureCube* resource = this->createResource("resourceName"); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const TextureCube* loadedResource = this->getObjectForTesting(resource->getResourceId(), "resourceName"); - EXPECT_EQ(resource->impl.getSize(), loadedResource->impl.getSize()); - EXPECT_EQ(resource->impl.getTextureFormat(), loadedResource->impl.getTextureFormat()); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelRed, loadedResource->impl.getTextureSwizzle().channelRed); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelGreen, loadedResource->impl.getTextureSwizzle().channelGreen); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelBlue, loadedResource->impl.getTextureSwizzle().channelBlue); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelAlpha, loadedResource->impl.getTextureSwizzle().channelAlpha); - } - - TEST_F(ResourceDataPoolPersistation, canReadWriteTextureCubeWithNonDefaultSwizzle) - { - const TextureSwizzle swizzle = { ETextureChannelColor::Alpha, ETextureChannelColor::Red, ETextureChannelColor::Green, ETextureChannelColor::Blue }; - const uint8_t data[4] = { 0u }; - const CubeMipLevelData mipLevelData(sizeof(data), data, data, data, data, data, data); - TextureCube* resource = m_scene.createTextureCube(ETextureFormat::RGBA8, 1u, 1, &mipLevelData, false, swizzle, ResourceCacheFlag_DoNotCache, "resourceName"); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const TextureCube* loadedResource = this->getObjectForTesting(resource->getResourceId(), "resourceName"); - EXPECT_EQ(resource->impl.getSize(), loadedResource->impl.getSize()); - EXPECT_EQ(resource->impl.getTextureFormat(), loadedResource->impl.getTextureFormat()); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelRed, loadedResource->impl.getTextureSwizzle().channelRed); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelGreen, loadedResource->impl.getTextureSwizzle().channelGreen); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelBlue, loadedResource->impl.getTextureSwizzle().channelBlue); - EXPECT_EQ(resource->impl.getTextureSwizzle().channelAlpha, loadedResource->impl.getTextureSwizzle().channelAlpha); - } - - TEST_F(ResourceDataPoolPersistation, canReadWriteEffect) - { - Effect* resource = this->createResource("resourceName1"); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - const Effect* loadedResource = this->getObjectForTesting(resource->getResourceId(), "resourceName1"); - EXPECT_TRUE(loadedResource); - } - - TEST_F(ResourceDataPoolPersistation, canLoadFromEffectFileAndSaveOutDirectly) - { - Effect* resource = this->createResource("resourceName1"); - - EXPECT_TRUE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(m_scene, "someTemporaryFile.ram", false)); - - // load created effect resource in another client - ramses::RamsesFramework anotherFramework; - ramses::RamsesClient& anotherClient(*anotherFramework.createClient("ramses client")); - auto& scene(*anotherClient.createScene(sceneId_t{ 0xf00 })); - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(anotherClient).addResourceDataFile("someTemporaryFile.ram")); - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(anotherClient).createResourceForScene(scene, resource->getResourceId())); - - // write out the loaded resource immediately - EXPECT_TRUE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(scene, "someOtherTempFile.ram", false)); - } - - TEST_F(ResourceDataPoolPersistation, saveResourcesToFile_InvalidFilename) - { - EXPECT_FALSE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(m_scene, "?XYZ:/dummyFile", false)); - } - - TEST_F(ResourceDataPoolPersistation, saveResourcesToFile_NoFilename) - { - EXPECT_FALSE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(m_scene, "", false)); - } - - TEST_F(ResourceDataPoolPersistation, saveResourcesToFile_FileExists) - { - EXPECT_TRUE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(m_scene, "dummyFile.dat", false)); - - ramses_internal::File file("dummyFile.dat"); - EXPECT_TRUE(file.remove()); - } - - TEST_F(ResourceDataPoolPersistation, addResourceDataFile_InvalidFilename) - { - EXPECT_FALSE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).addResourceDataFile("?XYZ:/dummyFile")); - } - - TEST_F(ResourceDataPoolPersistation, addResourceDataFile_NoFilename) - { - EXPECT_FALSE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).addResourceDataFile("")); - } - - TEST_F(ResourceDataPoolPersistation, addResourceDataFile_NonexistantFilename) - { - EXPECT_FALSE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).addResourceDataFile("ZEGETWTWAGTGSDGEg_thisfilename_in_this_directory_should_not_exist_DSAFDSFSTEZHDXHB")); - } - - static void checkVersionStrings(ramses_internal::BinaryFileInputStream& stream) - { - ramses_internal::String expectedVersionString = - ramses_internal::String("[RamsesVersion:") + - ramses_internal::String(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_STRING) + - ramses_internal::String("]\n[GitHash:") + - ramses_internal::String(::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH) + - ramses_internal::String("]\n"); - - const ramses_internal::UInt32 stringSize = static_cast(expectedVersionString.size()); - - ramses_internal::Char* versionStringInFile = new ramses_internal::Char[stringSize + 1]; - stream.read(versionStringInFile, stringSize); - versionStringInFile[stringSize] = '\0'; - - EXPECT_STREQ(versionStringInFile, expectedVersionString.c_str()); - - delete[] versionStringInFile; - } - - TEST_F(ResourceDataPoolPersistation, storesRamsesVersionToSceneAndResourceFile) - { - // Save empty scene and empty resources - { - ramses::Scene* emptyScene = client.createScene(sceneId_t(15u)); - ramses::status_t status = emptyScene->saveToFile("emptyScene.ramscene", false); - ASSERT_EQ(ramses::StatusOK, status); - - RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(*emptyScene, "emptyResourceAsset.ramres", false); - client.destroy(*emptyScene); - } - - // Load and check version strings - { - ramses_internal::File sceneFile("emptyScene.ramscene"); - ramses_internal::BinaryFileInputStream sceneStream(sceneFile); - - checkVersionStrings(sceneStream); - - ramses_internal::File resourceFile("emptyResourceAsset.ramres"); - ramses_internal::BinaryFileInputStream resourceStream(resourceFile); - - checkVersionStrings(resourceStream); - } - } - - template - const ObjectType* getObjectFromScene(Scene* scene, const char* name) - { - const RamsesObject* objectPerName = scene->findObjectByName(name); - EXPECT_STREQ(name, objectPerName->getName()); - - const ObjectType* specificObject = RamsesUtils::TryConvert(*objectPerName); - EXPECT_TRUE(nullptr != specificObject); - return specificObject; - } - - TEST_F(ResourceDataPoolPersistation, compressedFileIsSmallerThanUncompressed) - { - const std::vector data(1000u, 0u); - this->m_scene.createArrayResource(EDataType::UInt16, static_cast(data.size()), data.data()); - - EXPECT_TRUE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(m_scene, "someTemporaryFile.ram", false)); - - ramses_internal::File file("someTemporaryFile.ram"); - EXPECT_TRUE(file.exists()); - ramses_internal::UInt fileSize = 0; - EXPECT_TRUE(file.getSizeInBytes(fileSize)); - EXPECT_TRUE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(m_scene, "someTemporaryFile.ram", true)); - - ramses_internal::File file2("someTemporaryFile.ram"); - EXPECT_TRUE(file2.exists()); - ramses_internal::UInt fileSize2 = 0; - EXPECT_TRUE(file2.getSizeInBytes(fileSize2)); - - EXPECT_GT(fileSize, fileSize2); - } - - TEST_F(ResourceDataPoolPersistation, savedResourceDataFilesAreConsistent) - { - this->createResource("resourceName1"); - this->createResource("resourceName2"); - this->createResource("resourceTex"); - this->createResource("resourceTex"); - - for (ramses_internal::String name : { "ts1.ramres", "ts2.ramres", "ts3.ramres", "ts4.ramres", "ts5.ramres", "ts6.ramres" }) - { - EXPECT_TRUE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(m_scene, name.c_str(), false)); - } - - for (ramses_internal::String name : { "ts2.ramres", "ts3.ramres", "ts4.ramres", "ts5.ramres", "ts6.ramres" }) - { - EXPECT_TRUE(ramses::ClientTestUtils::CompareBinaryFiles("ts1.ramres", name.c_str())); - } - } - - class ResourceSavingTestClient : public LocalTestClientWithSceneAndAnimationSystem - { - public: - template - ResourceType* createResource(const char* name) - { - return m_creationHelper.createObjectOfType(name); - } - }; - - TEST(AResourceDataPoolPersistation, keepsResourceDataFilesConsistentAcrossMultipleClients) - { - for (ramses_internal::String name : { "ts1.ramres", "ts2.ramres", "ts3.ramres", "ts4.ramres", "ts5.ramres", "ts6.ramres" }) - { - ResourceSavingTestClient testClient; - testClient.createResource("resourceTex"); - testClient.createResource("resourceTex"); - testClient.createResource("resourceName1"); - testClient.createResource("resourceName2"); - testClient.createResource("resourceTex"); - testClient.createResource("resourceTex"); - testClient.createResource("resourceTex"); - EXPECT_TRUE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(testClient.getScene(), name.c_str(), false)); - } - - for (ramses_internal::String name : { "ts2.ramres", "ts3.ramres", "ts4.ramres", "ts5.ramres", "ts6.ramres" }) - { - EXPECT_TRUE(ramses::ClientTestUtils::CompareBinaryFiles("ts1.ramres", name.c_str())); - } - } - - TEST_F(ResourceDataPoolPersistation, doesNotLoadResourceOnResourceFileClose) - { - ArrayResource* resource = this->template createResource("resourceName"); - ASSERT_TRUE(resource != nullptr); - - this->doWriteReadCycle("someTemporaryFile.ram"); - this->template getObjectForTesting(resource->getResourceId(), "resourceName"); - - auto llhash = static_cast(resource)->impl.getLowlevelResourceHash(); - EXPECT_FALSE(m_clientForLoading.impl.getClientApplication().getResource(llhash)); - - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).removeResourceDataFile("someTemporaryFile.ram")); - Scene& scene2(*m_clientForLoading.createScene(sceneId_t{ 0x00110101 })); - EXPECT_FALSE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).createResourceForScene(scene2, resource->getResourceId())); - EXPECT_FALSE(m_clientForLoading.impl.getClientApplication().getResource(llhash)); - } - - TEST_F(ResourceDataPoolPersistation, canForceLoadLowLevelResource) - { - ArrayResource* resource = this->template createResource("resourceName"); - ASSERT_TRUE(resource != nullptr); - - this->doWriteReadCycle("someTemporaryFile.ram"); - this->template getObjectForTesting(resource->getResourceId(), "resourceName"); - - auto llhash = static_cast(resource)->impl.getLowlevelResourceHash(); - EXPECT_FALSE(m_clientForLoading.impl.getClientApplication().getResource(llhash)); - - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).forceLoadResourcesFromResourceDataFile("someTemporaryFile.ram")); - Scene& scene2(*m_clientForLoading.createScene(sceneId_t{ 0x00110101 })); - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).createResourceForScene(scene2, resource->getResourceId())); - EXPECT_TRUE(m_clientForLoading.impl.getClientApplication().getResource(llhash)); - } - - TEST_F(ResourceDataPoolPersistation, canCloseAResourceFileWhileOtherFileHoldsResourceAndDoesNotLoadLowLevelResource) - { - ArrayResource* resource = this->template createResource("resourceName"); - ASSERT_TRUE(resource != nullptr); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - Texture2D* resource2 = this->template createResource("resourceName"); - ASSERT_TRUE(resource != nullptr); - this->doWriteReadCycle("someTemporaryFile2.ram"); - this->template getObjectForTesting(resource->getResourceId(), "resourceName"); - - auto llhash = static_cast(resource)->impl.getLowlevelResourceHash(); - EXPECT_FALSE(m_clientForLoading.impl.getClientApplication().getResource(llhash)); - - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).removeResourceDataFile("someTemporaryFile2.ram")); - Scene& scene2(*m_clientForLoading.createScene(sceneId_t{ 0x00110101 })); - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).createResourceForScene(scene2, resource->getResourceId())); - EXPECT_FALSE(m_clientForLoading.impl.getClientApplication().getResource(llhash)); - EXPECT_FALSE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).createResourceForScene(scene2, resource2->getResourceId())); - auto llhash2 = static_cast(resource2)->impl.getLowlevelResourceHash(); - EXPECT_FALSE(m_clientForLoading.impl.getClientApplication().getResource(llhash2)); - } - - TEST_F(ResourceDataPoolPersistation, failsToRemoveResourceFileItDoesNotHave) - { - ArrayResource* resource = this->template createResource("resourceName"); - ASSERT_TRUE(resource != nullptr); - - this->doWriteReadCycle("someTemporaryFile.ram"); - EXPECT_FALSE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).removeResourceDataFile("someTemporaryFile2.ram")); - } - - TEST_F(ResourceDataPoolPersistation, failsToRemoveResourceFileTwice) - { - ArrayResource* resource = this->template createResource("resourceName"); - ASSERT_TRUE(resource != nullptr); - - this->doWriteReadCycle("someTemporaryFile.ram"); - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).removeResourceDataFile("someTemporaryFile.ram")); - EXPECT_FALSE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).removeResourceDataFile("someTemporaryFile.ram")); - } - - TEST_F(ResourceDataPoolPersistation, loadsResourceOnDoubleResourceFileDeletionAndMakesResourceNotInstantiable) - { - ArrayResource* resource = this->template createResource("resourceName"); - ASSERT_TRUE(resource != nullptr); - - this->doWriteReadCycle("someTemporaryFile.ram"); - this->doWriteReadCycle("someTemporaryFile2.ram"); - - // make sure resource is used - this->template getObjectForTesting(resource->getResourceId(), "resourceName"); - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).forceLoadResourcesFromResourceDataFile("someTemporaryFile.ram")); - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).removeResourceDataFile("someTemporaryFile.ram")); - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).removeResourceDataFile("someTemporaryFile2.ram")); - - Scene& newScene(*m_clientForLoading.createScene(sceneId_t{ 0xf00ba2 })); - EXPECT_TRUE(m_clientForLoading.impl.getClientApplication().getResource(resource->impl.getLowlevelResourceHash())); - EXPECT_FALSE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).createResourceForScene(newScene, resource->getResourceId())); - } - - TEST_F(ResourceDataPoolPersistation, doesNotSaveSceneObjectIdsForResourcesButLoadingAssignsNewIds) - { - // bump up sceneObjectId counter a bit - this->template createResource("dummy1"); - this->template createResource("dummy2"); - this->template createResource("dummy3"); - this->template createResource("dummy4"); - - auto arrayRes = this->template createResource("resourceName1"); - ASSERT_TRUE(arrayRes); - auto texRes = this->template createResource("resourceName2"); - ASSERT_TRUE(texRes); - EXPECT_LT(arrayRes->getSceneObjectId().getValue(), texRes->getSceneObjectId().getValue()); - - this->doWriteReadCycle("someTemporaryFile.ram"); - - auto loadedTexRes = this->template getObjectForTesting(texRes->getResourceId(), "resourceName2"); - auto loadedArrayRes = this->template getObjectForTesting(arrayRes->getResourceId(), "resourceName1"); - - EXPECT_NE(loadedTexRes->getSceneObjectId(), sceneObjectId_t::Invalid()); - EXPECT_NE(loadedTexRes->getSceneObjectId(), texRes->getSceneObjectId()); - EXPECT_NE(loadedArrayRes->getSceneObjectId(), sceneObjectId_t::Invalid()); - EXPECT_NE(loadedArrayRes->getSceneObjectId(), arrayRes->getSceneObjectId()); - EXPECT_GT(loadedArrayRes->getSceneObjectId().getValue(), loadedTexRes->getSceneObjectId().getValue()); - } -} diff --git a/client/ramses-client/test/ResourceDataPoolPersistationTest.h b/client/ramses-client/test/ResourceDataPoolPersistationTest.h deleted file mode 100644 index 6399337dd..000000000 --- a/client/ramses-client/test/ResourceDataPoolPersistationTest.h +++ /dev/null @@ -1,59 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_RESOURCEDATAPOOLPERSISTATIONTEST_H -#define RAMSES_RESOURCEDATAPOOLPERSISTATIONTEST_H - -#include -#include "ramses-client-api/RamsesClient.h" -#include "RamsesClientImpl.h" -#include "CreationHelper.h" -#include "ScenePersistationTest.h" -#include "ramses-hmi-utils.h" - -namespace ramses -{ - class ResourceDataPoolPersistation : public ASceneAndAnimationSystemLoadedFromFile - { - public: - template - ResourceType* createResource(const char* name) - { - return m_creationHelper.createObjectOfType(name); - } - - void doWriteReadCycle(std::string const& fileName) - { - EXPECT_TRUE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(m_scene, fileName, false)); - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).addResourceDataFile(fileName)); - } - - template - const ResourceType* getObjectForTesting(resourceId_t const& id, const char* name) - { - if (!m_sceneLoaded) - m_sceneLoaded = m_clientForLoading.createScene(sceneId_t{ 0xf00b42 }); - RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).createResourceForScene(*m_sceneLoaded, id); - Resource* objectForId = this->m_sceneLoaded->getResource(id); - EXPECT_TRUE(objectForId); - RamsesObject* objectPerName = this->m_sceneLoaded->findObjectByName(name); - auto type = TYPE_ID_OF_RAMSES_OBJECT::ID; - EXPECT_EQ(objectPerName->getType(), type); - EXPECT_EQ(objectPerName, objectForId); - EXPECT_TRUE(objectPerName); - if (!objectPerName) - return nullptr; - - const ResourceType* specificObject = RamsesUtils::TryConvert(*objectPerName); - EXPECT_TRUE(specificObject); - return specificObject; - } - }; -} - -#endif diff --git a/client/ramses-client/test/ResourceDataPoolTest.cpp b/client/ramses-client/test/ResourceDataPoolTest.cpp deleted file mode 100644 index 42081dbc8..000000000 --- a/client/ramses-client/test/ResourceDataPoolTest.cpp +++ /dev/null @@ -1,396 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-framework-api/RamsesFramework.h" -#include "ramses-client-api/ResourceDataPool.h" -#include "ramses-client-api/RamsesClient.h" -#include "ramses-client-api/Scene.h" -#include "ramses-client-api/Resource.h" -#include "ramses-client-api/ArrayResource.h" -#include "ramses-client-api/Texture2D.h" -#include "ramses-client-api/Texture3D.h" -#include "ramses-client-api/TextureCube.h" -#include "ramses-client-api/EffectDescription.h" -#include "ramses-client-api/Effect.h" -#include "ramses-hmi-utils.h" - -#include "ResourceDataPoolImpl.h" -#include "gtest/gtest.h" -#include "CreationHelper.h" -#include "RamsesObjectTypeTraits.h" -#include "RamsesObjectTestTypes.h" -#include "RamsesClientImpl.h" -#include "ResourceImpl.h" - -namespace ramses -{ - class AResourceDataPool : public testing::Test - { - public: - AResourceDataPool() - : framework() - , client(*framework.createClient("")) - , scene(*client.createScene(sceneId_t{ 0xf00 })) - , otherScene(*client.createScene(sceneId_t{ 0xf00b42 })) - , rdp(RamsesHMIUtils::GetResourceDataPoolForClient(client)) - , helper(&scene, nullptr, &client) - { - brokenEffectDesc.setVertexShader("#version 100\n" - "attribute float inp;\n" - "void main(void)\n" - "{\n" - " gl_Position = vec4(0.0)\n" - "}\n"); - brokenEffectDesc.setFragmentShader("precision highp float;" - "void main(void)\n" - "{" - " gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);" - "}"); - workingEffectDesc.setVertexShader("#version 100\n" - "attribute float inp;\n" - "void main(void)\n" - "{\n" - " gl_Position = vec4(0.0)\n;" - "}\n"); - workingEffectDesc.setFragmentShader("precision highp float;" - "void main(void)\n" - "{" - " gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);" - "}"); - } - - template - resourceId_t createResourceData() - { - switch (TYPE_ID_OF_RAMSES_OBJECT::ID) - { - case ERamsesObjectType_ArrayResource: - return createArrayResourceData(); - case ERamsesObjectType_Texture2D: - return createTexture2DData(); - case ERamsesObjectType_Texture3D: - return createTexture3DData(); - case ERamsesObjectType_TextureCube: - return createTextureCubeData(); - case ERamsesObjectType_Effect: - return createEffectData(); - default: - return {}; - } - } - - resourceId_t createArrayResourceData() - { - uint16_t data = 0; - return rdp.addArrayResourceData(EDataType::UInt16, 1, &data, ResourceCacheFlag_DoNotCache, resName); - } - - resourceId_t createTexture2DData() - { - uint8_t data[4] = { 0u }; - MipLevelData mipLevelData(sizeof(data), data); - TextureSwizzle swizzle; - return rdp.addTexture2DData(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, swizzle, ResourceCacheFlag_DoNotCache, resName); - } - - resourceId_t createTexture3DData() - { - uint8_t data[32] = { 0u }; - MipLevelData mipLevelData(sizeof(data), data); - return rdp.addTexture3DData(ETextureFormat::RGBA8, 1u, 2u, 4u, 1, &mipLevelData, false, ResourceCacheFlag_DoNotCache, resName); - } - - resourceId_t createTextureCubeData() - { - uint8_t data[4] = { 0u }; - CubeMipLevelData mipLevelData(sizeof(data), data, data, data, data, data, data); - TextureSwizzle swizzle; - return rdp.addTextureCubeData(ETextureFormat::RGBA8, 1u, 1, &mipLevelData, false, swizzle, ResourceCacheFlag_DoNotCache, resName); - } - - resourceId_t createEffectData() - { - EffectDescription effectDescription; - effectDescription.setVertexShader("void main(void) {gl_Position=vec4(0);}"); - effectDescription.setFragmentShader("void main(void) {gl_FragColor=vec4(1);}"); - return rdp.addEffectData(effectDescription, ResourceCacheFlag_DoNotCache, resName); - } - - template - ObjectType* instantiateVerifyAndCastResource(resourceId_t const& id, bool useOtherScene = false) - { - auto& usedScene = useOtherScene ? otherScene : scene; - EXPECT_NE(id, resourceId_t::Invalid()); - auto res = rdp.createResourceForScene(usedScene, id); - - EXPECT_TRUE(res); - EXPECT_EQ(res, usedScene.getResource(id)); - if (resName) - { - EXPECT_EQ(res, usedScene.findObjectByName(resName)); - EXPECT_STREQ(res->getName(), resName); - } - else - EXPECT_STREQ(res->getName(), ""); - - EXPECT_EQ(res->getResourceId(), id); - auto type = TYPE_ID_OF_RAMSES_OBJECT::ID; - EXPECT_EQ(res->getType(), type); - EXPECT_TRUE(client.impl.getClientApplication().getResource(res->impl.getLowlevelResourceHash())); - - return static_cast(res); - } - - RamsesFramework framework; - RamsesClient& client; - Scene& scene; - Scene& otherScene; - - ResourceDataPool& rdp; - - CreationHelper helper; - - const char* resName = "myResource"; - - EffectDescription brokenEffectDesc; - EffectDescription workingEffectDesc; - }; - - template - class AResourceDataPoolTyped : public AResourceDataPool - { - public: - }; - - TYPED_TEST_SUITE(AResourceDataPoolTyped, ResourceTypes); - - TEST_F(AResourceDataPool, createResourceForSceneReturnsNullptrWithInvalidId) - { - EXPECT_FALSE(rdp.createResourceForScene(scene, {})); - } - - TEST_F(AResourceDataPool, createResourceForSceneReturnsNullptrWithNonExistingId) - { - EXPECT_FALSE(rdp.createResourceForScene(scene, {0xf00, 0xba2})); - } - - TEST_F(AResourceDataPool, canCreateArrayResourceDataAndInstantiateItForScene) - { - auto id = createArrayResourceData(); - auto res = instantiateVerifyAndCastResource(id); - - EXPECT_EQ(res->getDataType(), EDataType::UInt16); - EXPECT_EQ(res->getNumberOfElements(), 1u); - } - - TEST_F(AResourceDataPool, canCreateAnotherKindOfArrayResourceDataAndInstantiateItForScene) - { - float data[4] = { .0f, .0f, .0f, .0f }; - auto id = rdp.addArrayResourceData(EDataType::Vector2F, 2, &data, ResourceCacheFlag_DoNotCache, resName); - auto res = instantiateVerifyAndCastResource(id); - - EXPECT_EQ(res->getDataType(), EDataType::Vector2F); - EXPECT_EQ(res->getNumberOfElements(), 2u); - } - - TEST_F(AResourceDataPool, canCreateTexture2DDataAndInstantiateItForScene) - { - auto id = createTexture2DData(); - auto res = instantiateVerifyAndCastResource(id); - - EXPECT_EQ(res->getWidth(), 1u); - EXPECT_EQ(res->getHeight(), 1u); - EXPECT_EQ(res->getTextureFormat(), ETextureFormat::RGBA8); - TextureSwizzle swizzle; - EXPECT_EQ(res->getTextureSwizzle().channelRed, swizzle.channelRed); - EXPECT_EQ(res->getTextureSwizzle().channelGreen, swizzle.channelGreen); - EXPECT_EQ(res->getTextureSwizzle().channelBlue, swizzle.channelBlue); - EXPECT_EQ(res->getTextureSwizzle().channelAlpha, swizzle.channelAlpha); - } - - TEST_F(AResourceDataPool, canCreateTexture3DDataAndInstantiateItForScene) - { - auto id = createTexture3DData(); - auto res = instantiateVerifyAndCastResource(id); - - EXPECT_EQ(res->getWidth(), 1u); - EXPECT_EQ(res->getHeight(), 2u); - EXPECT_EQ(res->getDepth(), 4u); - EXPECT_EQ(res->getTextureFormat(), ETextureFormat::RGBA8); - } - - TEST_F(AResourceDataPool, canCreateTextureCubeDataAndInstantiateItForScene) - { - auto id = createTextureCubeData(); - auto res = instantiateVerifyAndCastResource(id); - - EXPECT_EQ(res->getSize(), 1u); - EXPECT_EQ(res->getTextureFormat(), ETextureFormat::RGBA8); - TextureSwizzle swizzle; - EXPECT_EQ(res->getTextureSwizzle().channelRed, swizzle.channelRed); - EXPECT_EQ(res->getTextureSwizzle().channelGreen, swizzle.channelGreen); - EXPECT_EQ(res->getTextureSwizzle().channelBlue, swizzle.channelBlue); - EXPECT_EQ(res->getTextureSwizzle().channelAlpha, swizzle.channelAlpha); - } - - TEST_F(AResourceDataPool, canCreateEffectDataAndInstantiateItForScene) - { - auto id = createEffectData(); - auto res = instantiateVerifyAndCastResource(id); - - EXPECT_EQ(res->getUniformInputCount(), 0u); - EXPECT_EQ(res->getAttributeInputCount(), 0u); - - EXPECT_EQ(rdp.getLastEffectErrorMessages(), ""); - } - - TEST_F(AResourceDataPool, willReportFailuresOfEffectCreationIntoEffectErrorMessagesAndResetIt) - { - EXPECT_EQ(rdp.addEffectData(brokenEffectDesc), resourceId_t::Invalid()); - EXPECT_NE(rdp.getLastEffectErrorMessages(), ""); - EXPECT_NE(rdp.addEffectData(workingEffectDesc), resourceId_t::Invalid()); - EXPECT_EQ(rdp.getLastEffectErrorMessages(), ""); - } - - TEST_F(AResourceDataPool, doesNotMixUpSceneEffectErrorMessagesAndHisOwn) - { - EXPECT_NE(rdp.addEffectData(workingEffectDesc), resourceId_t::Invalid()); - EXPECT_FALSE(scene.createEffect(brokenEffectDesc)); - EXPECT_EQ(rdp.getLastEffectErrorMessages(), ""); - EXPECT_NE(rdp.addEffectData(workingEffectDesc), resourceId_t::Invalid()); - EXPECT_NE(scene.getLastEffectErrorMessages(), ""); - } - - TEST_F(AResourceDataPool, doesNotMixUpSceneEffectErrorMessagesAndHisOwnViceVersa) - { - EXPECT_TRUE(scene.createEffect(workingEffectDesc)); - EXPECT_EQ(rdp.addEffectData(brokenEffectDesc), resourceId_t::Invalid()); - EXPECT_EQ(scene.getLastEffectErrorMessages(), ""); - EXPECT_TRUE(scene.createEffect(workingEffectDesc)); - EXPECT_NE(rdp.getLastEffectErrorMessages(), ""); - } - - TYPED_TEST(AResourceDataPoolTyped, acceptsNullptrAsName) - { - this->resName = nullptr; - auto id = this->template createResourceData(); - - this->template instantiateVerifyAndCastResource(id); - } - - TYPED_TEST(AResourceDataPoolTyped, canCreateArrayResourceDataTwiceWithSameParametersWillResultInSameID) - { - auto id = this->template createResourceData(); - auto id2 = this->template createResourceData(); - EXPECT_EQ(id, id2); - - this->template instantiateVerifyAndCastResource(id2); - } - - TYPED_TEST(AResourceDataPoolTyped, cantCreatePoolResourceFromSceneCreatedResourceID) - { - auto sceneResource = this->helper.template createObjectOfType(this->resName); - EXPECT_FALSE(this->rdp.createResourceForScene(this->scene, sceneResource->getResourceId())); - EXPECT_FALSE(this->rdp.createResourceForScene(this->otherScene, sceneResource->getResourceId())); - } - - TYPED_TEST(AResourceDataPoolTyped, canCreateSceneCreatedObjectAndIdenticalPoolResourceAndThenInstantiateItInAnotherScene) - { - auto sceneResource = this->helper.template createObjectOfType(this->resName); - auto id = this->template createResourceData(); - EXPECT_EQ(sceneResource->getResourceId(), id); - EXPECT_FALSE(this->rdp.createResourceForScene(this->scene, id)); - EXPECT_TRUE(this->rdp.createResourceForScene(this->otherScene, id)); - } - - TYPED_TEST(AResourceDataPoolTyped, canCreateResourceTwiceForDifferentScenesFromOnePoolData) - { - auto id = this->template createResourceData(); - auto res1 = this->template instantiateVerifyAndCastResource(id, false); - auto res2 = this->template instantiateVerifyAndCastResource(id, true); - EXPECT_NE(res1, res2); - EXPECT_EQ(res1->getResourceId(), res2->getResourceId()); - EXPECT_EQ(res1->getType(), res2->getType()); - EXPECT_STREQ(res1->getName(), res2->getName()); - } - - TYPED_TEST(AResourceDataPoolTyped, canRecreateResourceAgainAfterDeletingFromScene) - { - auto id = this->template createResourceData(); - auto res = this->template instantiateVerifyAndCastResource(id); - this->scene.destroy(*res); - this->template instantiateVerifyAndCastResource(id); - } - - TEST_F(AResourceDataPool, failsWhenTryingToRemoveUnknownResource) - { - EXPECT_FALSE(this->rdp.removeResourceData({ 0xf00, 0xba2 })); - } - - TYPED_TEST(AResourceDataPoolTyped, canAddAndThenRemoveFromPoolFailsToCreateResourceAfterwards) - { - auto id = this->template createResourceData(); - EXPECT_TRUE(this->rdp.removeResourceData(id)); - EXPECT_FALSE(this->rdp.createResourceForScene(this->scene, id)); - } - - TYPED_TEST(AResourceDataPoolTyped, canAddAndThenRemoveFromPoolFailsToRemoveTwice) - { - auto id = this->template createResourceData(); - EXPECT_TRUE(this->rdp.removeResourceData(id)); - EXPECT_FALSE(this->rdp.removeResourceData(id)); - } - - TYPED_TEST(AResourceDataPoolTyped, canAddAndInstantiateSceneKeepsLLResourceAlive) - { - auto id = this->template createResourceData(); - auto res = this->template instantiateVerifyAndCastResource(id); - EXPECT_TRUE(this->rdp.removeResourceData(id)); - EXPECT_FALSE(this->rdp.createResourceForScene(this->scene, id)); - - EXPECT_EQ(res->getResourceId(), id); - auto llhash = static_cast(res)->impl.getLowlevelResourceHash(); - EXPECT_TRUE(this->client.impl.getClientApplication().getResource(llhash)); - this->scene.destroy(*res); - EXPECT_FALSE(this->client.impl.getClientApplication().getResource(llhash)); - } - - TYPED_TEST(AResourceDataPoolTyped, canAddAndInstantiatePoolKeepsLLResourceAlive) - { - auto id = this->template createResourceData(); - auto res = this->template instantiateVerifyAndCastResource(id); - auto llhash = static_cast(res)->impl.getLowlevelResourceHash(); - this->scene.destroy(*res); - - EXPECT_TRUE(this->client.impl.getClientApplication().getResource(llhash)); - EXPECT_TRUE(this->rdp.removeResourceData(id)); - EXPECT_FALSE(this->client.impl.getClientApplication().getResource(llhash)); - } - - TYPED_TEST(AResourceDataPoolTyped, canAddResourceMultipleTimesAndThenAllowsRemovingExactlyAsOften) - { - auto id = this->template createResourceData(); - auto id2 = this->template createResourceData(); - ASSERT_EQ(id, id2); - EXPECT_TRUE(this->rdp.removeResourceData(id)); - EXPECT_TRUE(this->rdp.removeResourceData(id)); - EXPECT_FALSE(this->rdp.removeResourceData(id)); - - this->template createResourceData(); - EXPECT_TRUE(this->rdp.removeResourceData(id)); - EXPECT_FALSE(this->rdp.removeResourceData(id)); - - this->template createResourceData(); - this->template createResourceData(); - EXPECT_TRUE(this->rdp.removeResourceData(id)); - this->template createResourceData(); - EXPECT_TRUE(this->rdp.removeResourceData(id)); - EXPECT_TRUE(this->rdp.removeResourceData(id)); - EXPECT_FALSE(this->rdp.removeResourceData(id)); - } -} diff --git a/client/ramses-client/test/SplineTest.cpp b/client/ramses-client/test/SplineTest.cpp deleted file mode 100644 index 789e01fa1..000000000 --- a/client/ramses-client/test/SplineTest.cpp +++ /dev/null @@ -1,261 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include - -#include "ramses-client-api/SplineStepBool.h" -#include "ramses-client-api/SplineStepInt32.h" -#include "ramses-client-api/SplineStepFloat.h" -#include "ramses-client-api/SplineStepVector2f.h" -#include "ramses-client-api/SplineStepVector3f.h" -#include "ramses-client-api/SplineStepVector4f.h" -#include "ramses-client-api/SplineStepVector2i.h" -#include "ramses-client-api/SplineStepVector3i.h" -#include "ramses-client-api/SplineStepVector4i.h" -#include "ramses-client-api/SplineLinearInt32.h" -#include "ramses-client-api/SplineLinearFloat.h" -#include "ramses-client-api/SplineLinearVector2f.h" -#include "ramses-client-api/SplineLinearVector3f.h" -#include "ramses-client-api/SplineLinearVector4f.h" -#include "ramses-client-api/SplineLinearVector2i.h" -#include "ramses-client-api/SplineLinearVector3i.h" -#include "ramses-client-api/SplineLinearVector4i.h" -#include "ramses-client-api/SplineBezierInt32.h" -#include "ramses-client-api/SplineBezierFloat.h" -#include "ramses-client-api/SplineBezierVector2f.h" -#include "ramses-client-api/SplineBezierVector3f.h" -#include "ramses-client-api/SplineBezierVector4f.h" -#include "ramses-client-api/SplineBezierVector2i.h" -#include "ramses-client-api/SplineBezierVector3i.h" -#include "ramses-client-api/SplineBezierVector4i.h" -#include "SplineImpl.h" - -#include "ClientTestUtils.h" -#include "SplineTestMacros.h" -#include "Math3d/Vector2.h" -#include "RamsesObjectTestTypes.h" - -using namespace testing; - -namespace ramses -{ - class SplineTest : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test - { - public: - SplineTest() - : LocalTestClientWithSceneAndAnimationSystem(), testing::Test() - , m_tan1(-1.f, 1.f) - , m_tan2(-1.f, 1.f) - { - } - - protected: - const ramses_internal::Vector2 m_tan1; - const ramses_internal::Vector2 m_tan2; - }; - - template - class SplineInitTest : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test {}; - - TYPED_TEST_SUITE(SplineInitTest, SplineTypes); - - TYPED_TEST(SplineInitTest, splineCreatedWithNoKeys) - { - const Spline& spline = this->template createObject("spline"); - EXPECT_EQ(0u, spline.getNumberOfKeys()); - EXPECT_NE(ramses_internal::SplineHandle::Invalid(), spline.impl.getSplineHandle()); - } - - TYPED_TEST(SplineInitTest, splineCreatedWithNoKeysGivesValidationWarning) - { - const Spline& spline = this->template createObject("spline"); - EXPECT_NE(StatusOK, spline.validate()); - } - - TEST_F(SplineTest, splineWithKeyCanBeValidated) - { - SplineStepBool& spline = *this->animationSystem.createSplineStepBool(); - CHECK_SET_GET_KEY1(spline, bool, 0u, 1000u, true); - EXPECT_EQ(StatusOK, spline.validate()); - } - - TEST_F(SplineTest, setGetKey_SplineStepBool) - { - SplineStepBool& spline = *this->animationSystem.createSplineStepBool(); - CHECK_SET_GET_KEY1(spline, bool, 0u, 1000u, true); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineStepFloat) - { - SplineStepFloat& spline = *this->animationSystem.createSplineStepFloat(); - CHECK_SET_GET_KEY1(spline, float, 0u, 1000u, 99.f); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineStepInt32) - { - SplineStepInt32& spline = *this->animationSystem.createSplineStepInt32(); - CHECK_SET_GET_KEY1(spline, int32_t, 0u, 1000u, 99); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineStepVector2f) - { - SplineStepVector2f& spline = *this->animationSystem.createSplineStepVector2f(); - CHECK_SET_GET_KEY2(spline, float, 0u, 1000u, 99.f, -111.f); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineStepVector3f) - { - SplineStepVector3f& spline = *this->animationSystem.createSplineStepVector3f(); - CHECK_SET_GET_KEY3(spline, float, 0u, 1000u, 99.f, 111.f, -111.f); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineStepVector4f) - { - SplineStepVector4f& spline = *this->animationSystem.createSplineStepVector4f(); - CHECK_SET_GET_KEY4(spline, float, 0u, 1000u, 99.f, 111.f, -111.f, -999.f); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineStepVector2i) - { - SplineStepVector2i& spline = *this->animationSystem.createSplineStepVector2i(); - CHECK_SET_GET_KEY2(spline, int32_t, 0u, 1000u, 99, -111); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineStepVector3i) - { - SplineStepVector3i& spline = *this->animationSystem.createSplineStepVector3i(); - CHECK_SET_GET_KEY3(spline, int32_t, 0u, 1000u, 111, -111, -999); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineStepVector4i) - { - SplineStepVector4i& spline = *this->animationSystem.createSplineStepVector4i(); - CHECK_SET_GET_KEY4(spline, int32_t, 0u, 1000u, 99, 111, -111, -999); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineLinearFloat) - { - SplineLinearFloat& spline = *this->animationSystem.createSplineLinearFloat(); - CHECK_SET_GET_KEY1(spline, float, 0u, 1000u, 99.f); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineLinearInt32) - { - SplineLinearInt32& spline = *this->animationSystem.createSplineLinearInt32(); - CHECK_SET_GET_KEY1(spline, int32_t, 0u, 1000u, 99); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineLinearVector2f) - { - SplineLinearVector2f& spline = *this->animationSystem.createSplineLinearVector2f(); - CHECK_SET_GET_KEY2(spline, float, 0u, 1000u, 99.f, -111.f); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineLinearVector3f) - { - SplineLinearVector3f& spline = *this->animationSystem.createSplineLinearVector3f(); - CHECK_SET_GET_KEY3(spline, float, 0u, 1000u, 99.f, 111.f, -111.f); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineLinearVector4f) - { - SplineLinearVector4f& spline = *this->animationSystem.createSplineLinearVector4f(); - CHECK_SET_GET_KEY4(spline, float, 0u, 1000u, 99.f, 111.f, -111.f, -999.f); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineLinearVector2i) - { - SplineLinearVector2i& spline = *this->animationSystem.createSplineLinearVector2i(); - CHECK_SET_GET_KEY2(spline, int32_t, 0u, 1000u, 99, -111); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineLinearVector3i) - { - SplineLinearVector3i& spline = *this->animationSystem.createSplineLinearVector3i(); - CHECK_SET_GET_KEY3(spline, int32_t, 0u, 1000u, 99, 111, -111); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineLinearVector4i) - { - SplineLinearVector4i& spline = *this->animationSystem.createSplineLinearVector4i(); - CHECK_SET_GET_KEY4(spline, int32_t, 0u, 1000u, 99, 111, -111, -999); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineBezierFloat) - { - SplineBezierFloat& spline = *this->animationSystem.createSplineBezierFloat(); - CHECK_SET_GET_KEY_TANGENTS1(spline, float, 0u, 1000u, 99.f, m_tan1, m_tan2); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineBezierInt32) - { - SplineBezierInt32& spline = *this->animationSystem.createSplineBezierInt32(); - CHECK_SET_GET_KEY_TANGENTS1(spline, int32_t, 0u, 1000u, 99, m_tan1, m_tan2); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineBezierVector2f) - { - SplineBezierVector2f& spline = *this->animationSystem.createSplineBezierVector2f(); - CHECK_SET_GET_KEY_TANGENTS2(spline, float, 0u, 1000u, 99.f, -111.f, m_tan1, m_tan2); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineBezierVector3f) - { - SplineBezierVector3f& spline = *this->animationSystem.createSplineBezierVector3f(); - CHECK_SET_GET_KEY_TANGENTS3(spline, float, 0u, 1000u, 99.f, 111.f, -111.f, m_tan1, m_tan2); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineBezierVector4f) - { - SplineBezierVector4f& spline = *this->animationSystem.createSplineBezierVector4f(); - CHECK_SET_GET_KEY_TANGENTS4(spline, float, 0u, 1000u, 99.f, 111.f, -111.f, -999.f, m_tan1, m_tan2); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineBezierVector2i) - { - SplineBezierVector2i& spline = *this->animationSystem.createSplineBezierVector2i(); - CHECK_SET_GET_KEY_TANGENTS2(spline, int32_t, 0u, 1000u, 99, -111, m_tan1, m_tan2); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineBezierVector3i) - { - SplineBezierVector3i& spline = *this->animationSystem.createSplineBezierVector3i(); - CHECK_SET_GET_KEY_TANGENTS3(spline, int32_t, 0u, 1000u, 99, 111, -111, m_tan1, m_tan2); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } - - TEST_F(SplineTest, setGetKey_SplineBezierVector4i) - { - SplineBezierVector4i& spline = *this->animationSystem.createSplineBezierVector4i(); - CHECK_SET_GET_KEY_TANGENTS4(spline, int32_t, 0u, 1000u, 99, 111, -111, -999, m_tan1, m_tan2); - EXPECT_EQ(1u, spline.getNumberOfKeys()); - } -} diff --git a/client/ramses-client/test/SplineTestMacros.h b/client/ramses-client/test/SplineTestMacros.h deleted file mode 100644 index c9582c984..000000000 --- a/client/ramses-client/test/SplineTestMacros.h +++ /dev/null @@ -1,148 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_SPLINETESTMACROS_H -#define RAMSES_SPLINETESTMACROS_H - -#define CHECK_SET_GET_KEY1(_spline, _componentType, _keyIdx, _timeStamp, _val1) \ -{ \ - EXPECT_EQ(StatusOK, _spline.setKey(_timeStamp, _val1)); \ - _componentType val1actual = _componentType(0); \ - splineTimeStamp_t timeStampActual = 0u; \ - EXPECT_EQ(StatusOK, _spline.getKeyValues(_keyIdx, timeStampActual, val1actual)); \ - EXPECT_EQ(_timeStamp, timeStampActual); \ - EXPECT_EQ(_val1, val1actual); \ -} - -#define CHECK_SET_GET_KEY2(_spline, _componentType, _keyIdx, _timeStamp, _val1, _val2) \ -{ \ - EXPECT_EQ(StatusOK, _spline.setKey(_timeStamp, _val1, _val2)); \ - _componentType val1actual = _componentType(0); \ - _componentType val2actual = _componentType(0); \ - splineTimeStamp_t timeStampActual = 0u; \ - EXPECT_EQ(StatusOK, _spline.getKeyValues(_keyIdx, timeStampActual, val1actual, val2actual)); \ - EXPECT_EQ(_timeStamp, timeStampActual); \ - EXPECT_EQ(_val1, val1actual); \ - EXPECT_EQ(_val2, val2actual); \ -} - -#define CHECK_SET_GET_KEY3(_spline, _componentType, _keyIdx, _timeStamp, _val1, _val2, _val3) \ -{ \ - EXPECT_EQ(StatusOK, _spline.setKey(_timeStamp, _val1, _val2, _val3)); \ - _componentType val1actual = _componentType(0); \ - _componentType val2actual = _componentType(0); \ - _componentType val3actual = _componentType(0); \ - splineTimeStamp_t timeStampActual = 0u; \ - EXPECT_EQ(StatusOK, _spline.getKeyValues(_keyIdx, timeStampActual, val1actual, val2actual, val3actual)); \ - EXPECT_EQ(_timeStamp, timeStampActual); \ - EXPECT_EQ(_val1, val1actual); \ - EXPECT_EQ(_val2, val2actual); \ - EXPECT_EQ(_val3, val3actual); \ -} - -#define CHECK_SET_GET_KEY4(_spline, _componentType, _keyIdx, _timeStamp, _val1, _val2, _val3, _val4) \ -{ \ - EXPECT_EQ(StatusOK, _spline.setKey(_timeStamp, _val1, _val2, _val3, _val4)); \ - _componentType val1actual = _componentType(0); \ - _componentType val2actual = _componentType(0); \ - _componentType val3actual = _componentType(0); \ - _componentType val4actual = _componentType(0); \ - splineTimeStamp_t timeStampActual = 0u; \ - EXPECT_EQ(StatusOK, _spline.getKeyValues(_keyIdx, timeStampActual, val1actual, val2actual, val3actual, val4actual)); \ - EXPECT_EQ(_timeStamp, timeStampActual); \ - EXPECT_EQ(_val1, val1actual); \ - EXPECT_EQ(_val2, val2actual); \ - EXPECT_EQ(_val3, val3actual); \ - EXPECT_EQ(_val4, val4actual); \ -} - -#define CHECK_SET_GET_KEY_TANGENTS1(_spline, _componentType, _keyIdx, _timeStamp, _val1, _tan1, _tan2) \ -{ \ - EXPECT_EQ(StatusOK, _spline.setKey(_timeStamp, _val1, _tan1.x, _tan1.y, _tan2.x, _tan2.y)); \ - _componentType val1actual = _componentType(0); \ - float tan1xActual = 0.f; \ - float tan1yActual = 0.f; \ - float tan2xActual = 0.f; \ - float tan2yActual = 0.f; \ - splineTimeStamp_t timeStampActual = 0u; \ - EXPECT_EQ(StatusOK, _spline.getKeyValues(_keyIdx, timeStampActual, val1actual, tan1xActual, tan1yActual, tan2xActual, tan2yActual)); \ - EXPECT_EQ(_timeStamp, timeStampActual); \ - EXPECT_EQ(_val1, val1actual); \ - EXPECT_EQ(_tan1.x, tan1xActual); \ - EXPECT_EQ(_tan1.y, tan1yActual); \ - EXPECT_EQ(_tan2.x, tan2xActual); \ - EXPECT_EQ(_tan2.y, tan2yActual); \ -} - -#define CHECK_SET_GET_KEY_TANGENTS2(_spline, _componentType, _keyIdx, _timeStamp, _val1, _val2, _tan1, _tan2) \ -{ \ - EXPECT_EQ(StatusOK, _spline.setKey(_timeStamp, _val1, _val2, _tan1.x, _tan1.y, _tan2.x, _tan2.y)); \ - _componentType val1actual = _componentType(0); \ - _componentType val2actual = _componentType(0); \ - float tan1xActual = 0.f; \ - float tan1yActual = 0.f; \ - float tan2xActual = 0.f; \ - float tan2yActual = 0.f; \ - splineTimeStamp_t timeStampActual = 0u; \ - EXPECT_EQ(StatusOK, _spline.getKeyValues(_keyIdx, timeStampActual, val1actual, val2actual, tan1xActual, tan1yActual, tan2xActual, tan2yActual)); \ - EXPECT_EQ(_timeStamp, timeStampActual); \ - EXPECT_EQ(_val1, val1actual); \ - EXPECT_EQ(_val2, val2actual); \ - EXPECT_EQ(_tan1.x, tan1xActual); \ - EXPECT_EQ(_tan1.y, tan1yActual); \ - EXPECT_EQ(_tan2.x, tan2xActual); \ - EXPECT_EQ(_tan2.y, tan2yActual); \ -} - -#define CHECK_SET_GET_KEY_TANGENTS3(_spline, _componentType, _keyIdx, _timeStamp, _val1, _val2, _val3, _tan1, _tan2) \ -{ \ - EXPECT_EQ(StatusOK, _spline.setKey(_timeStamp, _val1, _val2, _val3, _tan1.x, _tan1.y, _tan2.x, _tan2.y)); \ - _componentType val1actual = _componentType(0); \ - _componentType val2actual = _componentType(0); \ - _componentType val3actual = _componentType(0); \ - float tan1xActual = 0.f; \ - float tan1yActual = 0.f; \ - float tan2xActual = 0.f; \ - float tan2yActual = 0.f; \ - splineTimeStamp_t timeStampActual = 0u; \ - EXPECT_EQ(StatusOK, _spline.getKeyValues(_keyIdx, timeStampActual, val1actual, val2actual, val3actual, tan1xActual, tan1yActual, tan2xActual, tan2yActual)); \ - EXPECT_EQ(_timeStamp, timeStampActual); \ - EXPECT_EQ(_val1, val1actual); \ - EXPECT_EQ(_val2, val2actual); \ - EXPECT_EQ(_val3, val3actual); \ - EXPECT_EQ(_tan1.x, tan1xActual); \ - EXPECT_EQ(_tan1.y, tan1yActual); \ - EXPECT_EQ(_tan2.x, tan2xActual); \ - EXPECT_EQ(_tan2.y, tan2yActual); \ -} - -#define CHECK_SET_GET_KEY_TANGENTS4(_spline, _componentType, _keyIdx, _timeStamp, _val1, _val2, _val3, _val4, _tan1, _tan2) \ -{ \ - EXPECT_EQ(StatusOK, _spline.setKey(_timeStamp, _val1, _val2, _val3, _val4, _tan1.x, _tan1.y, _tan2.x, _tan2.y)); \ - _componentType val1actual = _componentType(0); \ - _componentType val2actual = _componentType(0); \ - _componentType val3actual = _componentType(0); \ - _componentType val4actual = _componentType(0); \ - float tan1xActual = 0.f; \ - float tan1yActual = 0.f; \ - float tan2xActual = 0.f; \ - float tan2yActual = 0.f; \ - splineTimeStamp_t timeStampActual = 0u; \ - EXPECT_EQ(StatusOK, _spline.getKeyValues(_keyIdx, timeStampActual, val1actual, val2actual, val3actual, val4actual, tan1xActual, tan1yActual, tan2xActual, tan2yActual)); \ - EXPECT_EQ(_timeStamp, timeStampActual); \ - EXPECT_EQ(_val1, val1actual); \ - EXPECT_EQ(_val2, val2actual); \ - EXPECT_EQ(_val3, val3actual); \ - EXPECT_EQ(_val4, val4actual); \ - EXPECT_EQ(_tan1.x, tan1xActual); \ - EXPECT_EQ(_tan1.y, tan1yActual); \ - EXPECT_EQ(_tan2.x, tan2xActual); \ - EXPECT_EQ(_tan2.y, tan2yActual); \ -} - -#endif diff --git a/client/ramses-client/test/StreamTextureTest.cpp b/client/ramses-client/test/StreamTextureTest.cpp deleted file mode 100644 index 41078a7e3..000000000 --- a/client/ramses-client/test/StreamTextureTest.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2015 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include - -#include "ClientTestUtils.h" -#include "CreationHelper.h" -#include "ramses-client-api/Texture2D.h" -#include "ramses-client-api/StreamTexture.h" - -using namespace testing; - -namespace ramses -{ - class StreamTextureTest : public LocalTestClientWithScene, public ::testing::Test - { - public: - StreamTextureTest() - { - } - }; - - TEST_F(StreamTextureTest, createStreamTexture) - { - const Texture2D& fallbackTexture = createObject("fallbackTexture"); - StreamTexture* streamTexture = this->m_scene.createStreamTexture(fallbackTexture, waylandIviSurfaceId_t(0), "testStreamTexture"); - - ASSERT_NE(static_cast(nullptr), streamTexture); - } - - TEST_F(StreamTextureTest, reportsErrorWhenValidatedWithInvalidFallbackTexture) - { - Texture2D& fallbackTexture = createObject("fallbackTexture"); - StreamTexture* streamTexture = this->m_scene.createStreamTexture(fallbackTexture, waylandIviSurfaceId_t(0), "testStreamTexture"); - ASSERT_TRUE(nullptr != streamTexture); - EXPECT_EQ(StatusOK, streamTexture->validate()); - - EXPECT_EQ(StatusOK, m_scene.destroy(fallbackTexture)); - EXPECT_NE(StatusOK, streamTexture->validate()); - } - - TEST_F(StreamTextureTest, canToggleForceFallbackImage) - { - const Texture2D& fallbackTexture = createObject("fallbackTexture"); - StreamTexture* streamTexture = this->m_scene.createStreamTexture(fallbackTexture, waylandIviSurfaceId_t(0), "testStreamTexture"); - - ASSERT_NE(static_cast(nullptr), streamTexture); - - EXPECT_FALSE(streamTexture->getForceFallbackImage()); - - EXPECT_EQ(StatusOK, streamTexture->forceFallbackImage(true)); - EXPECT_TRUE(streamTexture->getForceFallbackImage()); - - EXPECT_EQ(StatusOK, streamTexture->forceFallbackImage(false)); - EXPECT_FALSE(streamTexture->getForceFallbackImage()); - } - - TEST_F(StreamTextureTest, canGetStreamSourceId) - { - const Texture2D& fallbackTexture = createObject("fallbackTexture"); - StreamTexture* streamTexture = this->m_scene.createStreamTexture(fallbackTexture, waylandIviSurfaceId_t(123u), "testStreamTexture"); - - EXPECT_EQ(waylandIviSurfaceId_t(123u), streamTexture->getStreamSourceId()); - } -} diff --git a/client/ramses-client/ramses-text-api/FontRegistry.cpp b/client/ramses-text-api/FontRegistry.cpp similarity index 96% rename from client/ramses-client/ramses-text-api/FontRegistry.cpp rename to client/ramses-text-api/FontRegistry.cpp index 8be847d6f..784570d5c 100644 --- a/client/ramses-client/ramses-text-api/FontRegistry.cpp +++ b/client/ramses-text-api/FontRegistry.cpp @@ -26,7 +26,7 @@ namespace ramses return impl.getFontInstance(fontInstanceId); } - FontId FontRegistry::createFreetype2Font(const char* fontPath) + FontId FontRegistry::createFreetype2Font(std::string_view fontPath) { return impl.createFreetype2Font(fontPath); } diff --git a/client/ramses-client/ramses-text-api/TextCache.cpp b/client/ramses-text-api/TextCache.cpp similarity index 100% rename from client/ramses-client/ramses-text-api/TextCache.cpp rename to client/ramses-text-api/TextCache.cpp diff --git a/client/ramses-client/ramses-text-api/UtfUtils.cpp b/client/ramses-text-api/UtfUtils.cpp similarity index 98% rename from client/ramses-client/ramses-text-api/UtfUtils.cpp rename to client/ramses-text-api/UtfUtils.cpp index 7ad8774e5..0e9be1bb5 100644 --- a/client/ramses-client/ramses-text-api/UtfUtils.cpp +++ b/client/ramses-text-api/UtfUtils.cpp @@ -8,7 +8,9 @@ #include "ramses-text-api/UtfUtils.h" #include "Utils/LogMacros.h" + #include +#include namespace ramses { @@ -23,7 +25,7 @@ namespace ramses const uint32_t cUniSurLowEnd = 0xdfff; const uint32_t cHalfBase = 0x0010000; - const char TrailingBytesForUTF8[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + const std::array TrailingBytesForUTF8 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -189,7 +191,7 @@ namespace ramses else { //2 bytes -> last 5 bits are relevant; 3 bytes -> last 4 bits, etc. - const uint8_t mask = (1 << (7 - trailingBytesRemaining)) - 1; + const uint8_t mask = (1u << (7u - trailingBytesRemaining)) - 1u; result.codePoint = currentByte & mask; currentCharacterNumBytes = trailingBytesRemaining + 1; } diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/FontCascade.h b/client/ramses-text-api/include/ramses-text-api/FontCascade.h similarity index 99% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/FontCascade.h rename to client/ramses-text-api/include/ramses-text-api/FontCascade.h index 2e35ae536..3f418a7a8 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/FontCascade.h +++ b/client/ramses-text-api/include/ramses-text-api/FontCascade.h @@ -22,6 +22,7 @@ namespace ramses using OrderedFontList = std::vector; /** + * @ingroup TextAPI * @brief struct to define a font cascade * * Allows to define a list of fonts that are tried sequentially for character diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/FontInstanceId.h b/client/ramses-text-api/include/ramses-text-api/FontInstanceId.h similarity index 93% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/FontInstanceId.h rename to client/ramses-text-api/include/ramses-text-api/FontInstanceId.h index 134552a1c..bb7643afd 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/FontInstanceId.h +++ b/client/ramses-text-api/include/ramses-text-api/FontInstanceId.h @@ -10,7 +10,7 @@ #define RAMSES_FONTINSTANCEID_H #include "ramses-framework-api/StronglyTypedValue.h" -#include +#include #include namespace ramses @@ -21,16 +21,19 @@ namespace ramses struct FontIdTag {}; /** + * @ingroup TextAPI * @brief A strongly typed integer to distinguish between different fonts */ using FontId = StronglyTypedValue::max(), FontIdTag>; /** + * @ingroup TextAPI * @brief An empty struct to make FontInstanceId a strong type */ struct FontInstanceIdTag {}; /** + * @ingroup TextAPI * @brief A strongly typed integer to distinguish between different font instances */ using FontInstanceId = StronglyTypedValue::max(), FontInstanceIdTag>; diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/FontInstanceOffsets.h b/client/ramses-text-api/include/ramses-text-api/FontInstanceOffsets.h similarity index 98% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/FontInstanceOffsets.h rename to client/ramses-text-api/include/ramses-text-api/FontInstanceOffsets.h index dc59438c5..144f48946 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/FontInstanceOffsets.h +++ b/client/ramses-text-api/include/ramses-text-api/FontInstanceOffsets.h @@ -15,6 +15,7 @@ namespace ramses { /** + * @ingroup TextAPI * @brief A list of tuples from font instances and offsets into a string. * * Used to tell the text API which font should be used for which substring diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/FontRegistry.h b/client/ramses-text-api/include/ramses-text-api/FontRegistry.h similarity index 94% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/FontRegistry.h rename to client/ramses-text-api/include/ramses-text-api/FontRegistry.h index 42cd295c1..fb6d803e1 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/FontRegistry.h +++ b/client/ramses-text-api/include/ramses-text-api/FontRegistry.h @@ -12,9 +12,12 @@ #include "ramses-text-api/IFontAccessor.h" #include "ramses-text-api/IFontInstance.h" +#include + namespace ramses { /** + * @ingroup TextAPI * @brief Font registry can be used to load Freetype2 fonts and create font instances (optionally with Harfbuzz). * These are owned and managed by FontRegistry. */ @@ -29,7 +32,7 @@ namespace ramses /** * @brief Destructor for FontRegistry. Destroys all created fonts */ - virtual ~FontRegistry() override; + ~FontRegistry() override; /** * @brief Get font instance object corresponding to given id @@ -37,7 +40,7 @@ namespace ramses * @param[in] fontInstanceId The id of font instance * @return The font instance object, nullptr if not found */ - virtual IFontInstance* getFontInstance(FontInstanceId fontInstanceId) const override; + [[nodiscard]] IFontInstance* getFontInstance(FontInstanceId fontInstanceId) const override; /** * @brief Load Freetype2 font from file @@ -45,7 +48,7 @@ namespace ramses * @param[in] fontPath The file path to the font * @return The font id, FontId::Invalid() on error */ - FontId createFreetype2Font(const char* fontPath); + FontId createFreetype2Font(std::string_view fontPath); /** * @brief Load Freetype2 font from an open file descriptor. diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/Glyph.h b/client/ramses-text-api/include/ramses-text-api/Glyph.h similarity index 99% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/Glyph.h rename to client/ramses-text-api/include/ramses-text-api/Glyph.h index 4fd7e7165..a3af7c13d 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/Glyph.h +++ b/client/ramses-text-api/include/ramses-text-api/Glyph.h @@ -33,6 +33,7 @@ namespace ramses using GlyphId = StronglyTypedValue::max(), GlyphIdTag>; /** + * @ingroup TextAPI * @brief GlyphKey identifies a glyph with a specific font instance. */ struct GlyphKey diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/GlyphMetrics.h b/client/ramses-text-api/include/ramses-text-api/GlyphMetrics.h similarity index 98% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/GlyphMetrics.h rename to client/ramses-text-api/include/ramses-text-api/GlyphMetrics.h index 778e6bf61..37eec33bb 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/GlyphMetrics.h +++ b/client/ramses-text-api/include/ramses-text-api/GlyphMetrics.h @@ -14,6 +14,7 @@ namespace ramses { /** + * @ingroup TextAPI * @brief GlyphMetrics describes a glyph's position and dimensions in the rasterized glyph bitmap space. * * Width and height are the pixel dimensions of the glyph - must match the pixel data of diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/IFontAccessor.h b/client/ramses-text-api/include/ramses-text-api/IFontAccessor.h similarity index 91% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/IFontAccessor.h rename to client/ramses-text-api/include/ramses-text-api/IFontAccessor.h index 1746f3545..8c4b825f1 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/IFontAccessor.h +++ b/client/ramses-text-api/include/ramses-text-api/IFontAccessor.h @@ -16,6 +16,7 @@ namespace ramses class IFontInstance; /** + * @ingroup TextAPI * @brief Interface for getting font instances using font instance ids. * * This interface allows overriding the standard Freetype/Harfbuzz logic @@ -36,7 +37,7 @@ namespace ramses * @param[in] fontInstanceId The id of font instance * @return The font instance object */ - virtual IFontInstance* getFontInstance(FontInstanceId fontInstanceId) const = 0; + [[nodiscard]] virtual IFontInstance* getFontInstance(FontInstanceId fontInstanceId) const = 0; }; } diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/IFontInstance.h b/client/ramses-text-api/include/ramses-text-api/IFontInstance.h similarity index 90% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/IFontInstance.h rename to client/ramses-text-api/include/ramses-text-api/IFontInstance.h index c309267ef..261dc51c2 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/IFontInstance.h +++ b/client/ramses-text-api/include/ramses-text-api/IFontInstance.h @@ -10,13 +10,14 @@ #define RAMSES_TEXT_IFONTINSTANCE_H #include "ramses-text-api/GlyphMetrics.h" -#include +#include #include #include namespace ramses { /** + * @ingroup TextAPI * @brief Interface for font instance that can be used to query glyph metadata and bitmaps */ class RAMSES_API IFontInstance @@ -32,7 +33,7 @@ namespace ramses * @param[in] character The UTF32 char code of the character * @return True if the character is supported, false otherwise */ - virtual bool supportsCharacter(char32_t character) const = 0; + [[nodiscard]] virtual bool supportsCharacter(char32_t character) const = 0; /** * @brief Get all characters that are supported (mapped to a glyph) by IFontInstance @@ -44,19 +45,19 @@ namespace ramses * @brief Get the line height of the font instance * @return Line height (in rasterized texels of glyphs) */ - virtual int getHeight() const = 0; + [[nodiscard]] virtual int getHeight() const = 0; /** * @brief Get the line ascender of the font instance (see Freetype2 docs) * @return Line ascender (in rasterized texels of glyphs) */ - virtual int getAscender() const = 0; + [[nodiscard]] virtual int getAscender() const = 0; /** * @brief Get the line descender of the font instance (see Freetype2 docs) * @return Line descender (in rasterized texels of glyphs) */ - virtual int getDescender() const = 0; + [[nodiscard]] virtual int getDescender() const = 0; /** * @brief Load the glyphs metrics for all characters in a string and appends them to the provided vector of GLYPHS diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/LayoutUtils.h b/client/ramses-text-api/include/ramses-text-api/LayoutUtils.h similarity index 97% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/LayoutUtils.h rename to client/ramses-text-api/include/ramses-text-api/LayoutUtils.h index 1e4a0ff94..af778ba47 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/LayoutUtils.h +++ b/client/ramses-text-api/include/ramses-text-api/LayoutUtils.h @@ -16,6 +16,7 @@ namespace ramses namespace LayoutUtils { /** + * @ingroup TextAPI * @brief Helper data structure describing multiple glyphs metrics. * * StringBoundingBox is essentially a union of a set of GlyphMetrics. @@ -41,6 +42,7 @@ namespace ramses }; /** + * @ingroup TextAPI * @brief Compute a bounding box for a string represented by a range of GlyphMetrics. * @param[in] first Beginning of range of GlyphMetrics to compute bounding box * @param[in] last End of range of GlyphMetrics to compute bounding box, glyph pointed to by last is not included @@ -49,6 +51,7 @@ namespace ramses RAMSES_API StringBoundingBox GetBoundingBoxForString(GlyphMetricsVector::const_iterator first, GlyphMetricsVector::const_iterator last); /** + * @ingroup TextAPI * @copydoc GetBoundingBoxForString(GlyphMetricsVector::const_iterator first, GlyphMetricsVector::const_iterator last) **/ RAMSES_API StringBoundingBox GetBoundingBoxForString(const GlyphMetricsVector::const_reverse_iterator& first, const GlyphMetricsVector::const_reverse_iterator& last); diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/TextCache.h b/client/ramses-text-api/include/ramses-text-api/TextCache.h similarity index 94% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/TextCache.h rename to client/ramses-text-api/include/ramses-text-api/TextCache.h index 4b6d5c7b3..e966208f2 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/TextCache.h +++ b/client/ramses-text-api/include/ramses-text-api/TextCache.h @@ -21,6 +21,7 @@ namespace ramses class TextCacheImpl; /** + * @ingroup TextAPI * @brief Stores text data - texture atlas, meshes, glyph bitmap data. It is a cache because the * content can be re-generated when necessary, e.g. when cached glyphs take up too much memory. * @@ -118,7 +119,7 @@ namespace ramses * constructor of the text cache. * @return The glyph metrics vector created */ - GlyphMetricsVector getPositionedGlyphs(const std::u32string& str, FontInstanceId font); + GlyphMetricsVector getPositionedGlyphs(const std::u32string& str, FontInstanceId font); /** * @brief Create and get glyph metrics for a string using a list of font instances and offsets @@ -132,7 +133,7 @@ namespace ramses * constructor of the text cache. Also see docs of FontInstanceOffsets * @return The glyph metrics created */ - GlyphMetricsVector getPositionedGlyphs(const std::u32string& str, const FontInstanceOffsets& fontOffsets); + GlyphMetricsVector getPositionedGlyphs(const std::u32string& str, const FontInstanceOffsets& fontOffsets); /** * @brief Create the scene objects, e.g., mesh and appearance...etc, needed for rendering a text line (represented by glyph metrics). @@ -152,28 +153,28 @@ namespace ramses * @param[in] effect The effect used for creating the appearance of the text line and rendering the meshes * @return Id of the text line created */ - TextLineId createTextLine(const GlyphMetricsVector& glyphs, const Effect& effect); + TextLineId createTextLine(const GlyphMetricsVector& glyphs, const Effect& effect); /** * @brief Get a const pointer to a (previously created) text line object * @param[in] textId Id of the text line object to get * @return A pointer to the text line object, or nullptr on failure */ - TextLine const* getTextLine(TextLineId textId) const; + [[nodiscard]] TextLine const* getTextLine(TextLineId textId) const; /** * @brief Get a (non-const) pointer to a (previously created) text line object * @param[in] textId Id of the text line object to get * @return A pointer to the text line object, or nullptr on failure */ - TextLine* getTextLine(TextLineId textId); + TextLine* getTextLine(TextLineId textId); /** * @brief Delete an existing text line object * @param[in] textId Id of the text line object to delete * @return True on success, false otherwise */ - bool deleteTextLine(TextLineId textId); + bool deleteTextLine(TextLineId textId); /** * @brief Check if provided GlyphMetricsVector contains at least one renderable glyph @@ -182,7 +183,7 @@ namespace ramses * @param[in] glyphMetrics GlyphMetrics to be checked * @ return True if the provided vector contains at least one renderable glyph */ - static bool ContainsRenderableGlyphs(const GlyphMetricsVector& glyphMetrics); + static bool ContainsRenderableGlyphs(const GlyphMetricsVector& glyphMetrics); /** * @brief Apply character tracking (positive or negative) to each glyph in provided GlyphMetricsVector. @@ -197,7 +198,7 @@ namespace ramses * @param[in] trackingFactor factor of tracking intensity (higher --> bigger tracking effect) * @param[in] fontSize size of the font that contains the glyphs to be tracked */ - static void ApplyTrackingToGlyphs(GlyphMetricsVector& glyphMetrics, int32_t trackingFactor, int32_t fontSize); + static void ApplyTrackingToGlyphs(GlyphMetricsVector& glyphMetrics, int32_t trackingFactor, int32_t fontSize); /** * Stores internal data for implementation specifics of TextCache. diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/TextLine.h b/client/ramses-text-api/include/ramses-text-api/TextLine.h similarity index 87% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/TextLine.h rename to client/ramses-text-api/include/ramses-text-api/TextLine.h index c40955778..1a48907be 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/TextLine.h +++ b/client/ramses-text-api/include/ramses-text-api/TextLine.h @@ -29,21 +29,22 @@ namespace ramses /** * @brief Groups the scene objects needed to render a text line + * @ingroup TextAPI */ struct TextLine { /// Mesh node that represents the text - MeshNode* meshNode = nullptr; + MeshNode* meshNode = nullptr; /// Index to the atlas page containing the glyphs - std::size_t atlasPage = std::numeric_limits::max(); + std::size_t atlasPage = std::numeric_limits::max(); /// Glyph metrics of the original string characters - GlyphMetricsVector glyphs; + GlyphMetricsVector glyphs; /// Stores vertex data for the text line quads ArrayBuffer* positions = nullptr; /// Stores texture coordinate data for the text line quads ArrayBuffer* textureCoordinates = nullptr; /// Stores index data for the text line quads - ArrayBuffer* indices = nullptr; + ArrayBuffer* indices = nullptr; }; static_assert(std::is_move_constructible::value, "TextLine must be movable"); diff --git a/client/ramses-client/ramses-text-api/include/ramses-text-api/UtfUtils.h b/client/ramses-text-api/include/ramses-text-api/UtfUtils.h similarity index 98% rename from client/ramses-client/ramses-text-api/include/ramses-text-api/UtfUtils.h rename to client/ramses-text-api/include/ramses-text-api/UtfUtils.h index 41bc71345..04d5ed21c 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text-api/UtfUtils.h +++ b/client/ramses-text-api/include/ramses-text-api/UtfUtils.h @@ -10,12 +10,13 @@ #define RAMSES_UTFUTILS_H #include "ramses-framework-api/APIExport.h" -#include +#include #include namespace ramses { /** + * @ingroup TextAPI * @brief Stores an extracted Unicode/UTF32 code-point and corresponding meta-data resulting from the conversion to Unicode (see also UtfUtils) */ struct ExtractedUnicodePoint @@ -29,6 +30,7 @@ namespace ramses }; /** + * @ingroup TextAPI * @brief Converts UTF and Unicode according to the Unicode standard */ namespace UtfUtils diff --git a/client/ramses-client/ramses-text-api/include/ramses-text.h b/client/ramses-text-api/include/ramses-text.h similarity index 90% rename from client/ramses-client/ramses-text-api/include/ramses-text.h rename to client/ramses-text-api/include/ramses-text.h index 635f804eb..91487e6c0 100644 --- a/client/ramses-client/ramses-text-api/include/ramses-text.h +++ b/client/ramses-text-api/include/ramses-text.h @@ -9,6 +9,11 @@ #ifndef RAMSES_RAMSES_TEXT_H #define RAMSES_RAMSES_TEXT_H +/** + * @defgroup TextAPI The Ramses Text API + * This group contains all of the Ramses Text API types. + */ + #include "ramses-text-api/FontInstanceId.h" #include "ramses-text-api/FontInstanceOffsets.h" #include "ramses-text-api/FontRegistry.h" diff --git a/client/ramses-client/test/AppearanceTest.cpp b/client/test/AppearanceTest.cpp similarity index 61% rename from client/ramses-client/test/AppearanceTest.cpp rename to client/test/AppearanceTest.cpp index bfb584623..001da9a78 100644 --- a/client/ramses-client/test/AppearanceTest.cpp +++ b/client/test/AppearanceTest.cpp @@ -11,21 +11,17 @@ #include "ramses-client-api/TextureCube.h" #include "ramses-client-api/UniformInput.h" #include "ramses-client-api/Texture2D.h" -#include "ramses-client-api/DataFloat.h" -#include "ramses-client-api/DataMatrix44f.h" +#include "ramses-client-api/DataObject.h" #include "ramses-client-api/TextureSamplerMS.h" #include "ramses-client-api/RenderBuffer.h" #include "ramses-client-api/TextureSamplerExternal.h" #include "TestEffectCreator.h" #include "ClientTestUtils.h" -#include "Math3d/Vector2.h" -#include "Math3d/Vector2i.h" -#include "Math3d/Vector3i.h" -#include "Math3d/Vector4i.h" #include "EffectImpl.h" #include "DataObjectImpl.h" #include "TextureSamplerImpl.h" #include "AppearanceImpl.h" +#include "AppearanceUtils.h" namespace ramses { @@ -73,7 +69,7 @@ namespace ramses EXPECT_TRUE(texture != nullptr); info.texture2D = texture; - TextureSampler* sampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* sampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); EXPECT_TRUE(sampler != nullptr); info.sampler = sampler; } @@ -82,7 +78,7 @@ namespace ramses { EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("texture2dMSInput", info.input)); - RenderBuffer* renderBuffer = sharedTestState->getScene().createRenderBuffer(2u, 2u, ERenderBufferType_Color, ERenderBufferFormat_RGB8, ERenderBufferAccessMode_ReadWrite, 4u); + RenderBuffer* renderBuffer = sharedTestState->getScene().createRenderBuffer(2u, 2u, ERenderBufferType::Color, ERenderBufferFormat::RGB8, ERenderBufferAccessMode::ReadWrite, 4u); EXPECT_TRUE(renderBuffer != nullptr); info.renderBuffer = renderBuffer; @@ -101,7 +97,7 @@ namespace ramses EXPECT_TRUE(texture != nullptr); info.texture3D = texture; - TextureSampler* sampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* sampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); EXPECT_TRUE(sampler != nullptr); info.sampler = sampler; } @@ -116,7 +112,7 @@ namespace ramses EXPECT_TRUE(texture != nullptr); info.textureCube = texture; - TextureSampler* sampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* sampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); EXPECT_TRUE(sampler != nullptr); info.sampler = sampler; } @@ -125,7 +121,7 @@ namespace ramses { EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("textureExternalInput", info.input)); - TextureSamplerExternal* sampler = sharedTestState->getScene().createTextureSamplerExternal(ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear); + TextureSamplerExternal* sampler = sharedTestState->getScene().createTextureSamplerExternal(ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear); EXPECT_TRUE(sampler != nullptr); info.samplerExternal = sampler; } @@ -150,85 +146,77 @@ namespace ramses const Effect& effect = appearance->getEffect(); EXPECT_EQ(&effect, sharedTestState->effect); - const ramses_internal::DataLayout uniformLayout = sharedTestState->getInternalScene().getDataLayout(appearance->impl.getUniformDataLayout()); + const ramses_internal::DataLayout uniformLayout = sharedTestState->getInternalScene().getDataLayout(appearance->m_impl.getUniformDataLayout()); const ramses_internal::ResourceContentHash& effectHashFromUniformLayout = uniformLayout.getEffectHash(); - EXPECT_EQ(appearance->getEffect().impl.getLowlevelResourceHash(), effectHashFromUniformLayout); + EXPECT_EQ(appearance->getEffect().m_impl.getLowlevelResourceHash(), effectHashFromUniformLayout); } TEST_F(AAppearanceTest, setGetBlendingFactors) { - status_t stat = appearance->setBlendingFactors(EBlendFactor_One, EBlendFactor_SrcAlpha, EBlendFactor_OneMinusSrcAlpha, EBlendFactor_DstAlpha); + status_t stat = appearance->setBlendingFactors(EBlendFactor::One, EBlendFactor::SrcAlpha, EBlendFactor::OneMinusSrcAlpha, EBlendFactor::DstAlpha); EXPECT_EQ(StatusOK, stat); - EBlendFactor srcColor = EBlendFactor_Zero; - EBlendFactor destColor = EBlendFactor_Zero; - EBlendFactor srcAlpha = EBlendFactor_Zero; - EBlendFactor destAlpha = EBlendFactor_Zero; + EBlendFactor srcColor = EBlendFactor::Zero; + EBlendFactor destColor = EBlendFactor::Zero; + EBlendFactor srcAlpha = EBlendFactor::Zero; + EBlendFactor destAlpha = EBlendFactor::Zero; stat = appearance->getBlendingFactors(srcColor, destColor, srcAlpha, destAlpha); EXPECT_EQ(StatusOK, stat); - EXPECT_EQ(EBlendFactor_One, srcColor); - EXPECT_EQ(EBlendFactor_SrcAlpha, destColor); - EXPECT_EQ(EBlendFactor_OneMinusSrcAlpha, srcAlpha); - EXPECT_EQ(EBlendFactor_DstAlpha, destAlpha); + EXPECT_EQ(EBlendFactor::One, srcColor); + EXPECT_EQ(EBlendFactor::SrcAlpha, destColor); + EXPECT_EQ(EBlendFactor::OneMinusSrcAlpha, srcAlpha); + EXPECT_EQ(EBlendFactor::DstAlpha, destAlpha); } TEST_F(AAppearanceTest, setGetBlendingColor) { - float r = std::numeric_limits::max(); - float g = std::numeric_limits::max(); - float b = std::numeric_limits::max(); - float a = std::numeric_limits::max(); + vec4f color{ std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max() }; //default values - status_t stat = appearance->getBlendingColor(r, g, b, a); + status_t stat = appearance->getBlendingColor(color); EXPECT_EQ(StatusOK, stat); - EXPECT_EQ(0.0f, r); - EXPECT_EQ(0.0f, g); - EXPECT_EQ(0.0f, b); - EXPECT_EQ(0.0f, a); + EXPECT_EQ(color, vec4f(0.f, 0.f, 0.f, 0.f)); - stat = appearance->setBlendingColor(0.1f, 0.2f, 0.3f, 0.4f); + const vec4f colorToSet{ 0.1f, 0.2f, 0.3f, 0.4f }; + stat = appearance->setBlendingColor(colorToSet); EXPECT_EQ(StatusOK, stat); - stat = appearance->getBlendingColor(r, g, b, a); + stat = appearance->getBlendingColor(color); EXPECT_EQ(StatusOK, stat); - EXPECT_EQ(0.1f, r); - EXPECT_EQ(0.2f, g); - EXPECT_EQ(0.3f, b); - EXPECT_EQ(0.4f, a); + EXPECT_EQ(colorToSet, color); } TEST_F(AAppearanceTest, setGetDepthWrite) { - EDepthWrite depthWriteMode = EDepthWrite_Disabled; - EXPECT_EQ(StatusOK, appearance->setDepthWrite(EDepthWrite_Enabled)); + EDepthWrite depthWriteMode = EDepthWrite::Disabled; + EXPECT_EQ(StatusOK, appearance->setDepthWrite(EDepthWrite::Enabled)); EXPECT_EQ(StatusOK, appearance->getDepthWriteMode(depthWriteMode)); - EXPECT_EQ(EDepthWrite_Enabled, depthWriteMode); + EXPECT_EQ(EDepthWrite::Enabled, depthWriteMode); - EXPECT_EQ(StatusOK, appearance->setDepthWrite(EDepthWrite_Disabled)); + EXPECT_EQ(StatusOK, appearance->setDepthWrite(EDepthWrite::Disabled)); EXPECT_EQ(StatusOK, appearance->getDepthWriteMode(depthWriteMode)); - EXPECT_EQ(EDepthWrite_Disabled, depthWriteMode); + EXPECT_EQ(EDepthWrite::Disabled, depthWriteMode); - EXPECT_EQ(StatusOK, appearance->setDepthWrite(EDepthWrite_Enabled)); + EXPECT_EQ(StatusOK, appearance->setDepthWrite(EDepthWrite::Enabled)); EXPECT_EQ(StatusOK, appearance->getDepthWriteMode(depthWriteMode)); - EXPECT_EQ(EDepthWrite_Enabled, depthWriteMode); + EXPECT_EQ(EDepthWrite::Enabled, depthWriteMode); } TEST_F(AAppearanceTest, setGetDepthFunction) { - EDepthFunc depthFunc = EDepthFunc_Disabled; + EDepthFunc depthFunc = EDepthFunc::Disabled; EXPECT_EQ(StatusOK, appearance->getDepthFunction(depthFunc)); - EXPECT_EQ(EDepthFunc_LessEqual, depthFunc); + EXPECT_EQ(EDepthFunc::LessEqual, depthFunc); - EXPECT_EQ(StatusOK, appearance->setDepthFunction(EDepthFunc_GreaterEqual)); + EXPECT_EQ(StatusOK, appearance->setDepthFunction(EDepthFunc::GreaterEqual)); EXPECT_EQ(StatusOK, appearance->getDepthFunction(depthFunc)); - EXPECT_EQ(EDepthFunc_GreaterEqual, depthFunc); + EXPECT_EQ(EDepthFunc::GreaterEqual, depthFunc); } TEST_F(AAppearanceTest, setGetScissorTest) { - EScissorTest mode = EScissorTest_Disabled; - EXPECT_EQ(StatusOK, appearance->setScissorTest(EScissorTest_Enabled, 1, 2, 3u, 4u)); + EScissorTest mode = EScissorTest::Disabled; + EXPECT_EQ(StatusOK, appearance->setScissorTest(EScissorTest::Enabled, 1, 2, 3u, 4u)); EXPECT_EQ(StatusOK, appearance->getScissorTestState(mode)); - EXPECT_EQ(EScissorTest_Enabled, mode); + EXPECT_EQ(EScissorTest::Enabled, mode); int16_t x = 0; int16_t y = 0; @@ -243,66 +231,66 @@ namespace ramses TEST_F(AAppearanceTest, setGetStencilFunc) { - status_t stat = appearance->setStencilFunction(EStencilFunc_Equal, 2u, 0xef); + status_t stat = appearance->setStencilFunction(EStencilFunc::Equal, 2u, 0xef); EXPECT_EQ(StatusOK, stat); - EStencilFunc func = EStencilFunc_Disabled; + EStencilFunc func = EStencilFunc::Disabled; uint8_t ref = 0; uint8_t mask = 0; stat = appearance->getStencilFunction(func, ref, mask); EXPECT_EQ(StatusOK, stat); - EXPECT_EQ(EStencilFunc_Equal, func); + EXPECT_EQ(EStencilFunc::Equal, func); EXPECT_EQ(2u, ref); EXPECT_EQ(0xef, mask); } TEST_F(AAppearanceTest, setGetStencilOperation) { - status_t stat = appearance->setStencilOperation(EStencilOperation_Decrement, EStencilOperation_Increment, EStencilOperation_DecrementWrap); + status_t stat = appearance->setStencilOperation(EStencilOperation::Decrement, EStencilOperation::Increment, EStencilOperation::DecrementWrap); EXPECT_EQ(StatusOK, stat); - EStencilOperation sfail = EStencilOperation_Zero; - EStencilOperation dpfail = EStencilOperation_Zero; - EStencilOperation dppass = EStencilOperation_Zero; + EStencilOperation sfail = EStencilOperation::Zero; + EStencilOperation dpfail = EStencilOperation::Zero; + EStencilOperation dppass = EStencilOperation::Zero; stat = appearance->getStencilOperation(sfail, dpfail, dppass); EXPECT_EQ(StatusOK, stat); - EXPECT_EQ(EStencilOperation_Decrement, sfail); - EXPECT_EQ(EStencilOperation_Increment, dpfail); - EXPECT_EQ(EStencilOperation_DecrementWrap, dppass); + EXPECT_EQ(EStencilOperation::Decrement, sfail); + EXPECT_EQ(EStencilOperation::Increment, dpfail); + EXPECT_EQ(EStencilOperation::DecrementWrap, dppass); } TEST_F(AAppearanceTest, setGetBlendOperations) { - EXPECT_EQ(StatusOK, appearance->setBlendingOperations(EBlendOperation_Subtract, EBlendOperation_Max)); - EBlendOperation opColor = EBlendOperation_Disabled; - EBlendOperation opAlpha = EBlendOperation_Disabled; + EXPECT_EQ(StatusOK, appearance->setBlendingOperations(EBlendOperation::Subtract, EBlendOperation::Max)); + EBlendOperation opColor = EBlendOperation::Disabled; + EBlendOperation opAlpha = EBlendOperation::Disabled; EXPECT_EQ(StatusOK, appearance->getBlendingOperations(opColor, opAlpha)); - EXPECT_EQ(EBlendOperation_Subtract, opColor); - EXPECT_EQ(EBlendOperation_Max, opAlpha); + EXPECT_EQ(EBlendOperation::Subtract, opColor); + EXPECT_EQ(EBlendOperation::Max, opAlpha); } TEST_F(AAppearanceTest, setGetCullMode) { - ECullMode mode = ECullMode_Disabled; - EXPECT_EQ(StatusOK, appearance->setCullingMode(ECullMode_FrontFacing)); + ECullMode mode = ECullMode::Disabled; + EXPECT_EQ(StatusOK, appearance->setCullingMode(ECullMode::FrontFacing)); EXPECT_EQ(StatusOK, appearance->getCullingMode(mode)); - EXPECT_EQ(ECullMode_FrontFacing, mode); - EXPECT_EQ(StatusOK, appearance->setCullingMode(ECullMode_Disabled)); + EXPECT_EQ(ECullMode::FrontFacing, mode); + EXPECT_EQ(StatusOK, appearance->setCullingMode(ECullMode::Disabled)); EXPECT_EQ(StatusOK, appearance->getCullingMode(mode)); - EXPECT_EQ(ECullMode_Disabled, mode); + EXPECT_EQ(ECullMode::Disabled, mode); } TEST_F(AAppearanceTest, hasDrawModeTrianglesByDefault) { EDrawMode mode; EXPECT_EQ(StatusOK, appearance->getDrawMode(mode)); - EXPECT_EQ(EDrawMode_Triangles, mode); + EXPECT_EQ(EDrawMode::Triangles, mode); } TEST_F(AAppearanceTest, setGetDrawMode) { - EDrawMode mode = EDrawMode_Lines; - EXPECT_EQ(StatusOK, appearance->setDrawMode(EDrawMode_Points)); + EDrawMode mode = EDrawMode::Lines; + EXPECT_EQ(StatusOK, appearance->setDrawMode(EDrawMode::Points)); EXPECT_EQ(StatusOK, appearance->getDrawMode(mode)); - EXPECT_EQ(EDrawMode_Points, mode); + EXPECT_EQ(EDrawMode::Points, mode); } TEST_F(AAppearanceTest, setGetColorWriteMask) @@ -333,8 +321,8 @@ namespace ramses const float value = 42; float getValue = 0; - EXPECT_NE(StatusOK, appearance->setInputValueFloat(inputObject, value)); - EXPECT_NE(StatusOK, appearance->getInputValueFloat(inputObject, getValue)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, getValue)); } TEST_F(AAppearanceTest, reportsErrorWhenGetSetMismatchingInputTypeArray) @@ -345,8 +333,8 @@ namespace ramses const float values[] = { 42, 43, 44, 45, 46, 47 }; float getValues[6]; - EXPECT_NE(StatusOK, appearance->setInputValueVector2f(inputObject, 3u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueVector2f(inputObject, 3u, getValues)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 6u, values)); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 6u, getValues)); } TEST_F(AAppearanceTest, reportsErrorWhenGetSetMismatchingInputTypeTexture) @@ -359,7 +347,7 @@ namespace ramses Texture2D* texture = sharedTestState->getScene().createTexture2D(ETextureFormat::RGB8, 1u, 1u, 1u, &mipData, false); ASSERT_TRUE(texture != nullptr); - TextureSampler* textureSampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* textureSampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); ASSERT_TRUE(textureSampler != nullptr); EXPECT_NE(StatusOK, appearance->setInputTexture(inputObject, *textureSampler)); @@ -379,7 +367,7 @@ namespace ramses ramses::Scene& anotherScene = *sharedTestState->getClient().createScene(sceneId_t(1u)); Texture2D* texture = anotherScene.createTexture2D(ETextureFormat::RGB8, 1u, 1u, 1u, &mipData, false); ASSERT_TRUE(texture != nullptr); - TextureSampler* textureSampler = anotherScene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* textureSampler = anotherScene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); ASSERT_TRUE(textureSampler != nullptr); EXPECT_NE(StatusOK, appearance->setInputTexture(inputObject, *textureSampler)); @@ -397,7 +385,7 @@ namespace ramses const MipLevelData mipData(3u, texData); Texture2D* texture = sharedTestState->getScene().createTexture2D(ETextureFormat::RGB8, 1u, 1u, 1u, &mipData, false); ASSERT_TRUE(texture != nullptr); - TextureSampler* textureSampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* textureSampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); ASSERT_TRUE(textureSampler != nullptr); EXPECT_EQ(StatusOK, appearance->setInputTexture(inputObject, *textureSampler)); @@ -476,11 +464,17 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("integerInput", inputObject)); - const int32_t value = 42; - int32_t getValue = 0; + int32_t value = 42; + int32_t& valueR = value; + const int32_t& valueCR = value; + auto valueM = value; + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueCR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, std::move(valueM))); - EXPECT_EQ(StatusOK, appearance->setInputValueInt32(inputObject, value)); - EXPECT_EQ(StatusOK, appearance->getInputValueInt32(inputObject, getValue)); + int32_t getValue = 0; + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, getValue)); EXPECT_EQ(value, getValue); } @@ -493,14 +487,14 @@ namespace ramses int32_t values[] = { value, value * 2, value * 3 }; int32_t getValues[3] = { 0 }; - EXPECT_EQ(StatusOK, appearance->setInputValueInt32(inputObject, 3u, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueInt32(inputObject, 3u, getValues)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 3u, values)); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, 3u, getValues)); EXPECT_EQ(values, absl::MakeSpan(getValues)); - EXPECT_NE(StatusOK, appearance->setInputValueInt32(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->setInputValueInt32(inputObject, 11u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueInt32(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueInt32(inputObject, 11u, values)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 0u, values)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 11u, values)); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 0u, values)); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 11u, values)); } /// Float @@ -509,11 +503,17 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("floatInput", inputObject)); - const float value = 42; - float getValue = 0; + float value = 42; + float& valueR = value; + const float& valueCR = value; + auto valueM = value; + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueCR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, std::move(valueM))); - EXPECT_EQ(StatusOK, appearance->setInputValueFloat(inputObject, value)); - EXPECT_EQ(StatusOK, appearance->getInputValueFloat(inputObject, getValue)); + float getValue = 0; + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, getValue)); EXPECT_EQ(value, getValue); } @@ -526,27 +526,32 @@ namespace ramses const float values[] = { value, value * 2, value * 3 }; float getValues[3] = { 0 }; - EXPECT_EQ(StatusOK, appearance->setInputValueFloat(inputObject, 3u, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueFloat(inputObject, 3u, getValues)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 3u, values)); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, 3u, getValues)); EXPECT_EQ(values, absl::MakeSpan(getValues)); - EXPECT_NE(StatusOK, appearance->setInputValueFloat(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->setInputValueFloat(inputObject, 11u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueFloat(inputObject, 0u, getValues)); - EXPECT_NE(StatusOK, appearance->getInputValueFloat(inputObject, 11u, getValues)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 0u, values)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 11u, values)); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 0u, getValues)); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 11u, getValues)); } - /// Vector2i TEST_F(AAppearanceTest, canHandleUniformInputsOfTypeVector2i) { UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec2iInput", inputObject)); - const ramses_internal::Vector2i value(42, 24); - ramses_internal::Vector2i getValue; - - EXPECT_EQ(StatusOK, appearance->setInputValueVector2i(inputObject, value.x, value.y)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector2i(inputObject, getValue.x, getValue.y)); + vec2i value{ 42, 24 }; + vec2i& valueR = value; + const vec2i& valueCR = value; + auto valueM = value; + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueCR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, std::move(valueM))); + + vec2i getValue; + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, getValue)); EXPECT_EQ(value, getValue); } @@ -555,30 +560,35 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec2iInputArray", inputObject)); - const int32_t values[] = { 42, 43, 44, 45, 46, 47 }; - int32_t getValues[6] = { 0 }; + const std::vector values = { vec2i{42, 43}, vec2i{44, 45}, vec2i{46, 47} }; + std::vector getValues(values.size()); - EXPECT_EQ(StatusOK, appearance->setInputValueVector2i(inputObject, 3u, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector2i(inputObject, 3u, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 3u, values.data())); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, 3u, getValues.data())); + EXPECT_EQ(values, getValues); - EXPECT_NE(StatusOK, appearance->setInputValueVector2i(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->setInputValueVector2i(inputObject, 11u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueVector2i(inputObject, 0u, getValues)); - EXPECT_NE(StatusOK, appearance->getInputValueVector2i(inputObject, 11u, getValues)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 0u, values.data())); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 11u, values.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 0u, getValues.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 11u, getValues.data())); } - /// Vector3i TEST_F(AAppearanceTest, canHandleUniformInputsOfTypeVector3i) { UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec3iInput", inputObject)); - const ramses_internal::Vector3i value(42, 24, 4422); - ramses_internal::Vector3i getValue; - - EXPECT_EQ(StatusOK, appearance->setInputValueVector3i(inputObject, value.x, value.y, value.z)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector3i(inputObject, getValue.x, getValue.y, getValue.z)); + vec3i value{ 42, 24, 4422 }; + vec3i& valueR = value; + const vec3i& valueCR = value; + auto valueM = value; + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueCR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, std::move(valueM))); + + vec3i getValue; + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, getValue)); EXPECT_EQ(value, getValue); } @@ -587,30 +597,35 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec3iInputArray", inputObject)); - const int32_t values[] = { 42, 43, 44, 45, 46, 47, 48, 49, 50 }; - int32_t getValues[9] = { 0 }; + const std::vector values = { vec3i{42, 43, 444}, vec3i{44, 45, 555}, vec3i{46, 47, 666} }; + std::vector getValues(values.size()); - EXPECT_EQ(StatusOK, appearance->setInputValueVector3i(inputObject, 3u, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector3i(inputObject, 3u, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 3u, values.data())); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, 3u, getValues.data())); + EXPECT_EQ(values, getValues); - EXPECT_NE(StatusOK, appearance->setInputValueVector3i(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->setInputValueVector3i(inputObject, 11u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueVector3i(inputObject, 0u, getValues)); - EXPECT_NE(StatusOK, appearance->getInputValueVector3i(inputObject, 11u, getValues)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 0u, values.data())); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 11u, values.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 0u, getValues.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 11u, getValues.data())); } - /// Vector4i TEST_F(AAppearanceTest, canHandleUniformInputsOfTypeVector4i) { UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec4iInput", inputObject)); - const ramses_internal::Vector4i value(42, 24, 44, 22); - ramses_internal::Vector4i getValue; - - EXPECT_EQ(StatusOK, appearance->setInputValueVector4i(inputObject, value.x, value.y, value.z, value.w)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector4i(inputObject, getValue.x, getValue.y, getValue.z, getValue.w)); + vec4i value{ 42, 24, 44, 22 }; + vec4i& valueR = value; + const vec4i& valueCR = value; + auto valueM = value; + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueCR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, std::move(valueM))); + + vec4i getValue; + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, getValue)); EXPECT_EQ(value, getValue); } @@ -619,35 +634,35 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec4iInputArray", inputObject)); - const int32_t values[] = - { - 42, 43, 44, 45, - 46, 47, 48, 49, - 50, 51, 52, 53 - }; - int32_t getValues[12] = { 0 }; + const std::vector values = { vec4i{42, 43, 444, 555}, vec4i{44, 45, 666, 777}, vec4i{46, 47, 888, 999} }; + std::vector getValues(values.size()); - EXPECT_EQ(StatusOK, appearance->setInputValueVector4i(inputObject, 3u, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector4i(inputObject, 3u, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 3u, values.data())); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, 3u, getValues.data())); + EXPECT_EQ(values, getValues); - EXPECT_NE(StatusOK, appearance->setInputValueVector4i(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->setInputValueVector4i(inputObject, 11u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueVector4i(inputObject, 0u, getValues)); - EXPECT_NE(StatusOK, appearance->getInputValueVector4i(inputObject, 11u, getValues)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 0u, values.data())); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 11u, values.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 0u, getValues.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 11u, getValues.data())); } - /// Vector2 TEST_F(AAppearanceTest, canHandleUniformInputsOfTypeVector2f) { UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec2fInput", inputObject)); - const ramses_internal::Vector2 value(42, 24); - ramses_internal::Vector2 getValue; - - EXPECT_EQ(StatusOK, appearance->setInputValueVector2f(inputObject, value.x, value.y)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector2f(inputObject, getValue.x, getValue.y)); + vec2f value{ 42.f, 24.f }; + vec2f& valueR = value; + const vec2f& valueCR = value; + auto valueM = value; + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueCR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, std::move(valueM))); + + vec2f getValue; + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, getValue)); EXPECT_EQ(value, getValue); } @@ -656,30 +671,35 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec2fInputArray", inputObject)); - const float values[] = { 42, 43, 44, 45, 46, 47 }; - float getValues[6] = { 0 }; + const std::vector values = { vec2f{42.f, 43.f}, vec2f{44.f, 45.f}, vec2f{46.f, 47.f} }; + std::vector getValues(values.size()); - EXPECT_EQ(StatusOK, appearance->setInputValueVector2f(inputObject, 3u, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector2f(inputObject, 3u, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 3u, values.data())); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, 3u, getValues.data())); + EXPECT_EQ(values, getValues); - EXPECT_NE(StatusOK, appearance->setInputValueVector2f(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->setInputValueVector2f(inputObject, 11u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueVector2f(inputObject, 0u, getValues)); - EXPECT_NE(StatusOK, appearance->getInputValueVector2f(inputObject, 11u, getValues)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 0u, values.data())); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 11u, values.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 0u, getValues.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 11u, getValues.data())); } - /// Vector3 TEST_F(AAppearanceTest, canHandleUniformInputsOfTypeVector3f) { UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec3fInput", inputObject)); - const ramses_internal::Vector3 value(42, 24, 44); - ramses_internal::Vector3 getValue; - - EXPECT_EQ(StatusOK, appearance->setInputValueVector3f(inputObject, value.x, value.y, value.z)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector3f(inputObject, getValue.x, getValue.y, getValue.z)); + vec3f value{ 42.f, 24.f, 44.f }; + vec3f& valueR = value; + const vec3f& valueCR = value; + auto valueM = value; + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueCR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, std::move(valueM))); + + vec3f getValue; + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, getValue)); EXPECT_EQ(value, getValue); } @@ -688,35 +708,35 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec3fInputArray", inputObject)); - const float values[] = - { - 42, 43, 44, - 45, 46, 47, - 48, 49, 50 - }; - float getValues[9] = { 0 }; + const std::vector values = { vec3f{42.f, 43.f, 444.f}, vec3f{44.f, 45.f, 666.f}, vec3f{46.f, 47.f, 888.f} }; + std::vector getValues(values.size()); - EXPECT_EQ(StatusOK, appearance->setInputValueVector3f(inputObject, 3u, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector3f(inputObject, 3u, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 3u, values.data())); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, 3u, getValues.data())); + EXPECT_EQ(values, getValues); - EXPECT_NE(StatusOK, appearance->setInputValueVector3f(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->setInputValueVector3f(inputObject, 11u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueVector3f(inputObject, 0u, getValues)); - EXPECT_NE(StatusOK, appearance->getInputValueVector3f(inputObject, 11u, getValues)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 0u, values.data())); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 11u, values.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 0u, getValues.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 11u, getValues.data())); } - /// Vector4 TEST_F(AAppearanceTest, canHandleUniformInputsOfTypeVector4f) { UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec4fInput", inputObject)); - const ramses_internal::Vector4 value(42, 24, 44, 55); - ramses_internal::Vector4 getValue; - - EXPECT_EQ(StatusOK, appearance->setInputValueVector4f(inputObject, value.x, value.y, value.z, value.w)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector4f(inputObject, getValue.x, getValue.y, getValue.z, getValue.w)); + vec4f value{ 42.f, 24.f, 44.f, 55.f }; + vec4f& valueR = value; + const vec4f& valueCR = value; + auto valueM = value; + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueCR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, std::move(valueM))); + + vec4f getValue; + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, getValue)); EXPECT_EQ(value, getValue); } @@ -725,39 +745,37 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec4fInputArray", inputObject)); - const float values[] = - { - 42, 43, 44, 45, - 46, 47, 48, 49, - 50, 51, 52, 53 - }; - float getValues[12] = { 0 }; + const std::vector values = { vec4f{42.f, 43.f, 444.f, 555.f}, vec4f{44.f, 45.f, 666.f, 777.f}, vec4f{46.f, 47.f, 888.f, 999.f} }; + std::vector getValues(values.size()); - EXPECT_EQ(StatusOK, appearance->setInputValueVector4f(inputObject, 3u, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueVector4f(inputObject, 3u, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 3u, values.data())); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, 3u, getValues.data())); + EXPECT_EQ(values, getValues); - EXPECT_NE(StatusOK, appearance->setInputValueVector4f(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->setInputValueVector4f(inputObject, 11u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueVector4f(inputObject, 0u, getValues)); - EXPECT_NE(StatusOK, appearance->getInputValueVector4f(inputObject, 11u, getValues)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 0u, values.data())); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 11u, values.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 0u, getValues.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 11u, getValues.data())); } - /// Matrix22f + /// matrix22f TEST_F(AAppearanceTest, canHandleUniformInputsOfTypeMatrix22f) { UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("matrix22fInput", inputObject)); - const float values[4] = - { - 42, 43, 44, 45 - }; - float getValues[4] = { 0 }; - - EXPECT_EQ(StatusOK, appearance->setInputValueMatrix22f(inputObject, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueMatrix22f(inputObject, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + matrix22f value{ 42.f, 43.f, 44.f, 45.f }; + matrix22f& valueR = value; + const matrix22f& valueCR = value; + auto valueM = value; + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueCR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, std::move(valueM))); + + matrix22f getValue; + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, getValue)); + EXPECT_EQ(value, getValue); } TEST_F(AAppearanceTest, canHandleUniformInputsOfTypeMatrix22fArray) @@ -765,22 +783,22 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("matrix22fInputArray", inputObject)); - const float values[12] = + const std::vector values = { - 42, 43, 44, 45, - 46, 47, 48, 49, - 50, 51, 52, 53 + matrix22f{42.f, 43.f, 44.f, 45.f}, + matrix22f{46.f, 47.f, 48.f, 49.f}, + matrix22f{50.f, 51.f, 52.f, 53.f} }; - float getValues[12] = { 0 }; + std::vector getValues(values.size()); - EXPECT_EQ(StatusOK, appearance->setInputValueMatrix22f(inputObject, 3u, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueMatrix22f(inputObject, 3u, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 3u, values.data())); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, 3u, getValues.data())); + EXPECT_EQ(values, getValues); - EXPECT_NE(StatusOK, appearance->setInputValueMatrix22f(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->setInputValueMatrix22f(inputObject, 11u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueMatrix22f(inputObject, 0u, getValues)); - EXPECT_NE(StatusOK, appearance->getInputValueMatrix22f(inputObject, 11u, getValues)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 0u, values.data())); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 11u, values.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 0u, getValues.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 11u, getValues.data())); } /// Matrix33f @@ -789,17 +807,18 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("matrix33fInput", inputObject)); - const float values[9] = - { - 42, 43, 44, - 45, 46, 47, - 48, 49, 50 - }; - float getValues[9] = { 0 }; - - EXPECT_EQ(StatusOK, appearance->setInputValueMatrix33f(inputObject, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueMatrix33f(inputObject, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + matrix33f value{ 42.f, 43.f, 44.f, 45.f, 46.f, 47.f, 48.f, 49.f, 50.f }; + matrix33f& valueR = value; + const matrix33f& valueCR = value; + auto valueM = value; + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueCR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, std::move(valueM))); + + matrix33f getValue; + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, getValue)); + EXPECT_EQ(value, getValue); } TEST_F(AAppearanceTest, canHandleUniformInputsOfTypeMatrix33fArray) @@ -807,28 +826,22 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("matrix33fInputArray", inputObject)); - const float values[27] = + const std::vector values = { - 42, 43, 44, - 46, 47, 48, - 50, 51, 52, - 54, 55, 56, - 46, 47, 48, - 42, 43, 44, - 54, 55, 56, - 46, 47, 48, - 50, 51, 52 + matrix33f{42.f, 43.f, 44.f, 45.f, 11.f, 22.f, 33.f, 44.f, 55.f}, + matrix33f{46.f, 47.f, 48.f, 49.f, 66.f, 77.f, 88.f, 99.f, 10.f}, + matrix33f{50.f, 51.f, 52.f, 53.f, 20.f, 30.f, 40.f, 50.f, 60.f} }; - float getValues[27] = { 0 }; + std::vector getValues(values.size()); - EXPECT_EQ(StatusOK, appearance->setInputValueMatrix33f(inputObject, 3u, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueMatrix33f(inputObject, 3u, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 3u, values.data())); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, 3u, getValues.data())); + EXPECT_EQ(values, getValues); - EXPECT_NE(StatusOK, appearance->setInputValueMatrix33f(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->setInputValueMatrix33f(inputObject, 11u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueMatrix33f(inputObject, 0u, getValues)); - EXPECT_NE(StatusOK, appearance->getInputValueMatrix33f(inputObject, 11u, getValues)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 0u, values.data())); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 11u, values.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 0u, getValues.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 11u, getValues.data())); } /// Matrix44f @@ -837,18 +850,18 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("matrix44fInput", inputObject)); - const float values[16] = - { - 42, 43, 44, 45, - 46, 47, 48, 49, - 50, 51, 52, 53, - 54, 55, 56, 57 - }; - float getValues[16]; - - EXPECT_EQ(StatusOK, appearance->setInputValueMatrix44f(inputObject, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueMatrix44f(inputObject, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + matrix44f value{ 42.f, 43.f, 44.f, 45.f, 46.f, 47.f, 48.f, 49.f, 50.f, 51.f, 52.f, 53.f, 54.f, 55.f, 56.f, 57.f }; + matrix44f& valueR = value; + const matrix44f& valueCR = value; + auto valueM = value; + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, valueCR)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, std::move(valueM))); + + matrix44f getValue; + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, getValue)); + EXPECT_EQ(value, getValue); } TEST_F(AAppearanceTest, canHandleUniformInputsOfTypeMatrix44fArray) @@ -856,31 +869,22 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("matrix44fInputArray", inputObject)); - const float values[] = + const std::vector values = { - 42, 43, 44, 45, - 46, 47, 48, 49, - 50, 51, 52, 53, - 54, 55, 56, 57, - 46, 47, 48, 49, - 42, 43, 44, 45, - 54, 55, 56, 57, - 46, 47, 48, 49, - 50, 51, 52, 53, - 54, 55, 56, 57, - 42, 43, 44, 45, - 50, 51, 52, 53 + matrix44f{42.f, 43.f, 44.f, 45.f, 11.f, 22.f, 33.f, 44.f, 55.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f}, + matrix44f{46.f, 47.f, 48.f, 49.f, 66.f, 77.f, 88.f, 99.f, 10.f, 8.f, 9.f, 10.f, 11.f, 12.f, 13.f, 14.f}, + matrix44f{50.f, 51.f, 52.f, 53.f, 20.f, 30.f, 40.f, 50.f, 60.f, 15.f, 16.f, 17.f, 18.f, 19.f, 20.f, 21.f} }; - float getValues[48] = { 0 }; + std::vector getValues(values.size()); - EXPECT_EQ(StatusOK, appearance->setInputValueMatrix44f(inputObject, 3u, values)); - EXPECT_EQ(StatusOK, appearance->getInputValueMatrix44f(inputObject, 3u, getValues)); - EXPECT_EQ(values, absl::MakeSpan(getValues)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 3u, values.data())); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, 3u, getValues.data())); + EXPECT_EQ(values, getValues); - EXPECT_NE(StatusOK, appearance->setInputValueMatrix44f(inputObject, 0u, values)); - EXPECT_NE(StatusOK, appearance->setInputValueMatrix44f(inputObject, 11u, values)); - EXPECT_NE(StatusOK, appearance->getInputValueMatrix44f(inputObject, 0u, getValues)); - EXPECT_NE(StatusOK, appearance->getInputValueMatrix44f(inputObject, 11u, getValues)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 0u, values.data())); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, 11u, values.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 0u, getValues.data())); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, 11u, getValues.data())); } /// Texture2D @@ -894,7 +898,7 @@ namespace ramses Texture2D* texture = sharedTestState->getScene().createTexture2D(ETextureFormat::RGB8, 1u, 1u, 1u, &mipData, false); ASSERT_TRUE(texture != nullptr); - TextureSampler* textureSampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* textureSampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); ASSERT_TRUE(textureSampler != nullptr); EXPECT_EQ(StatusOK, appearance->setInputTexture(inputObject, *textureSampler)); @@ -913,11 +917,11 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("texture2dMSInput", inputObject)); - RenderBuffer* renderBuffer = sharedTestState->getScene().createRenderBuffer(2u, 2u, ERenderBufferType_Color, ERenderBufferFormat_RGB8, ERenderBufferAccessMode_ReadWrite, 4u); + RenderBuffer* renderBuffer = sharedTestState->getScene().createRenderBuffer(2u, 2u, ERenderBufferType::Color, ERenderBufferFormat::RGB8, ERenderBufferAccessMode::ReadWrite, 4u); TextureSamplerMS* textureSampler = sharedTestState->getScene().createTextureSamplerMS(*renderBuffer, "renderBuffer"); ASSERT_TRUE(textureSampler != nullptr); - EXPECT_EQ(textureSampler->impl.getTextureDataType(), ramses_internal::EDataType::TextureSampler2DMS); + EXPECT_EQ(textureSampler->m_impl.getTextureDataType(), ramses_internal::EDataType::TextureSampler2DMS); EXPECT_EQ(StatusOK, appearance->setInputTexture(inputObject, *textureSampler)); const TextureSamplerMS* actualSampler = nullptr; @@ -939,7 +943,7 @@ namespace ramses TextureCube* texture = sharedTestState->getScene().createTextureCube(ETextureFormat::RGB8, 1u, 1u, &mipData, false); ASSERT_TRUE(texture != nullptr); - TextureSampler* textureSampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, *texture); + TextureSampler* textureSampler = sharedTestState->getScene().createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, *texture); ASSERT_TRUE(textureSampler != nullptr); EXPECT_EQ(StatusOK, appearance->setInputTexture(inputObject, *textureSampler)); @@ -958,10 +962,10 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("textureExternalInput", inputObject)); - TextureSamplerExternal* textureSampler = sharedTestState->getScene().createTextureSamplerExternal(ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear); + TextureSamplerExternal* textureSampler = sharedTestState->getScene().createTextureSamplerExternal(ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear); ASSERT_TRUE(textureSampler != nullptr); - EXPECT_EQ(textureSampler->impl.getTextureDataType(), ramses_internal::EDataType::TextureSamplerExternal); + EXPECT_EQ(textureSampler->m_impl.getTextureDataType(), ramses_internal::EDataType::TextureSamplerExternal); EXPECT_EQ(StatusOK, appearance->setInputTexture(inputObject, *textureSampler)); const TextureSamplerExternal* actualSampler = nullptr; @@ -984,12 +988,12 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("floatInput", inputObject)); - DataFloat* dataObject = sharedTestState->getScene().createDataFloat(); + auto dataObject = sharedTestState->getScene().createDataObject(EDataType::Float); ASSERT_TRUE(dataObject != nullptr); EXPECT_EQ(StatusOK, appearance->bindInput(inputObject, *dataObject)); EXPECT_TRUE(appearance->isInputBound(inputObject)); - EXPECT_EQ(dataObject->impl.getDataReference(), appearance->getDataObjectBoundToInput(inputObject)->impl.getDataReference()); + EXPECT_EQ(dataObject->m_impl.getDataReference(), appearance->getDataObjectBoundToInput(inputObject)->m_impl.getDataReference()); EXPECT_EQ(StatusOK, appearance->unbindInput(inputObject)); EXPECT_FALSE(appearance->isInputBound(inputObject)); @@ -1000,9 +1004,9 @@ namespace ramses { UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("floatInput", inputObject)); - EXPECT_EQ(StatusOK, appearance->setInputValueFloat(inputObject, 666.f)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 666.f)); - DataFloat* dataObject = sharedTestState->getScene().createDataFloat(); + auto dataObject = sharedTestState->getScene().createDataObject(EDataType::Float); ASSERT_TRUE(dataObject != nullptr); dataObject->setValue(333.f); @@ -1010,16 +1014,16 @@ namespace ramses const float setValue = 0.111f; float value = 0.f; - EXPECT_NE(StatusOK, appearance->setInputValueFloat(inputObject, setValue)); + EXPECT_NE(StatusOK, appearance->setInputValue(inputObject, setValue)); EXPECT_EQ(StatusOK, dataObject->getValue(value)); EXPECT_FLOAT_EQ(333.f, value); // failed setter does not modify data object value = 0.f; - EXPECT_NE(StatusOK, appearance->getInputValueFloat(inputObject, value)); + EXPECT_NE(StatusOK, appearance->getInputValue(inputObject, value)); EXPECT_FLOAT_EQ(0.f, value); // failed getter does not modify out parameter EXPECT_EQ(StatusOK, appearance->unbindInput(inputObject)); - EXPECT_EQ(StatusOK, appearance->getInputValueFloat(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, value)); EXPECT_EQ(666.f, value); // failed setter did not modify previously set value } @@ -1028,7 +1032,7 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("floatInputArray", inputObject)); - DataFloat* dataObject = sharedTestState->getScene().createDataFloat(); + auto dataObject = sharedTestState->getScene().createDataObject(EDataType::Float); ASSERT_TRUE(dataObject != nullptr); EXPECT_NE(StatusOK, appearance->bindInput(inputObject, *dataObject)); @@ -1040,7 +1044,7 @@ namespace ramses EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("floatInput", inputObject)); ramses::Scene& anotherScene = *sharedTestState->getClient().createScene(sceneId_t(1u)); - DataFloat* dataObject = anotherScene.createDataFloat(); + auto dataObject = anotherScene.createDataObject(EDataType::Float); ASSERT_TRUE(dataObject != nullptr); EXPECT_NE(StatusOK, appearance->bindInput(inputObject, *dataObject)); @@ -1053,10 +1057,11 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("vec4fInput", inputObject)); - DataFloat* dataObject = sharedTestState->getScene().createDataFloat(); + auto dataObject = sharedTestState->getScene().createDataObject(EDataType::Float); ASSERT_TRUE(dataObject != nullptr); EXPECT_NE(StatusOK, appearance->bindInput(inputObject, *dataObject)); + EXPECT_FALSE(appearance->isInputBound(inputObject)); } TEST_F(AAppearanceTestWithSemanticUniforms, failsToBindDataObjectIfInputHasSemantics) @@ -1064,7 +1069,7 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("matrix44fInput", inputObject)); - DataMatrix44f* dataObject = sharedTestState->getScene().createDataMatrix44f(); + auto dataObject = sharedTestState->getScene().createDataObject(EDataType::Matrix44F); ASSERT_TRUE(dataObject != nullptr); EXPECT_NE(StatusOK, appearance->bindInput(inputObject, *dataObject)); @@ -1074,9 +1079,9 @@ namespace ramses { UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("floatInput", inputObject)); - EXPECT_EQ(StatusOK, appearance->setInputValueFloat(inputObject, 666.f)); + EXPECT_EQ(StatusOK, appearance->setInputValue(inputObject, 666.f)); - DataFloat* dataObject = sharedTestState->getScene().createDataFloat(); + auto dataObject = sharedTestState->getScene().createDataObject(EDataType::Float); ASSERT_TRUE(dataObject != nullptr); EXPECT_EQ(StatusOK, appearance->bindInput(inputObject, *dataObject)); @@ -1084,7 +1089,7 @@ namespace ramses EXPECT_EQ(StatusOK, appearance->unbindInput(inputObject)); float value = 0.f; - EXPECT_EQ(StatusOK, appearance->getInputValueFloat(inputObject, value)); + EXPECT_EQ(StatusOK, appearance->getInputValue(inputObject, value)); EXPECT_FLOAT_EQ(666.f, value); } @@ -1201,7 +1206,7 @@ namespace ramses UniformInput inputObject; EXPECT_EQ(StatusOK, sharedTestState->effect->findUniformInput("floatInput", inputObject)); - DataFloat* dataObject = sharedTestState->getScene().createDataFloat(); + auto dataObject = sharedTestState->getScene().createDataObject(EDataType::Float); ASSERT_TRUE(dataObject != nullptr); EXPECT_EQ(StatusOK, newAppearance->bindInput(inputObject, *dataObject)); @@ -1221,20 +1226,20 @@ namespace ramses TEST_F(AAppearanceTest, failsWhenWrongBlendingOperationsSet) { - status_t stat = appearance->setBlendingOperations(EBlendOperation_Subtract, EBlendOperation_ReverseSubtract); + status_t stat = appearance->setBlendingOperations(EBlendOperation::Subtract, EBlendOperation::ReverseSubtract); EXPECT_EQ(StatusOK, stat); - stat = appearance->setBlendingOperations(EBlendOperation_Add, EBlendOperation_Disabled); + stat = appearance->setBlendingOperations(EBlendOperation::Add, EBlendOperation::Disabled); EXPECT_NE(StatusOK, stat); - stat = appearance->setBlendingOperations(EBlendOperation_Disabled, EBlendOperation_Add); + stat = appearance->setBlendingOperations(EBlendOperation::Disabled, EBlendOperation::Add); EXPECT_NE(StatusOK, stat); - EBlendOperation opColor = EBlendOperation_Disabled; - EBlendOperation opAlpha = EBlendOperation_Disabled; + EBlendOperation opColor = EBlendOperation::Disabled; + EBlendOperation opAlpha = EBlendOperation::Disabled; EXPECT_EQ(StatusOK, appearance->getBlendingOperations(opColor, opAlpha)); - EXPECT_EQ(EBlendOperation_Subtract, opColor); - EXPECT_EQ(EBlendOperation_ReverseSubtract, opAlpha); + EXPECT_EQ(EBlendOperation::Subtract, opColor); + EXPECT_EQ(EBlendOperation::ReverseSubtract, opAlpha); } class AnAppearanceWithGeometryShader : public AAppearanceTest @@ -1251,26 +1256,60 @@ namespace ramses { EDrawMode mode; EXPECT_EQ(StatusOK, appearance->getDrawMode(mode)); - EXPECT_EQ(EDrawMode_Lines, mode); + EXPECT_EQ(EDrawMode::Lines, mode); } TEST_F(AnAppearanceWithGeometryShader, RefusesToChangeDrawingMode_WhenIncompatibleToGeometryShader) { // Shader uses lines, can't change to incompatible types - EXPECT_NE(StatusOK, appearance->setDrawMode(EDrawMode::EDrawMode_Points)); - EXPECT_NE(StatusOK, appearance->setDrawMode(EDrawMode::EDrawMode_Triangles)); - EXPECT_NE(StatusOK, appearance->setDrawMode(EDrawMode::EDrawMode_TriangleFan)); + EXPECT_NE(StatusOK, appearance->setDrawMode(EDrawMode::Points)); + EXPECT_NE(StatusOK, appearance->setDrawMode(EDrawMode::Triangles)); + EXPECT_NE(StatusOK, appearance->setDrawMode(EDrawMode::TriangleFan)); EDrawMode mode; EXPECT_EQ(StatusOK, appearance->getDrawMode(mode)); - EXPECT_EQ(EDrawMode_Lines, mode); + EXPECT_EQ(EDrawMode::Lines, mode); } TEST_F(AnAppearanceWithGeometryShader, AllowsChangingDrawMode_IfNewModeIsStillCompatible) { // Shader uses lines, change to line strip is ok - still produces lines for the geometry stage - EXPECT_EQ(StatusOK, appearance->setDrawMode(EDrawMode::EDrawMode_LineStrip)); + EXPECT_EQ(StatusOK, appearance->setDrawMode(EDrawMode::LineStrip)); EDrawMode mode; EXPECT_EQ(StatusOK, appearance->getDrawMode(mode)); - EXPECT_EQ(EDrawMode_LineStrip, mode); + EXPECT_EQ(EDrawMode::LineStrip, mode); + } + + TEST(AnAppearanceUtils, ChecksValidGeometryShaderModes) + { + // valid combinations of appearance draw mode (first) and GS input type (second) + const std::initializer_list> validDrawModeToGSModeCombinations = { + { EDrawMode::Points, EDrawMode::Points }, + { EDrawMode::Lines, EDrawMode::Lines }, + { EDrawMode::LineStrip, EDrawMode::Lines }, + { EDrawMode::LineLoop, EDrawMode::Lines }, + { EDrawMode::Triangles, EDrawMode::Triangles }, + { EDrawMode::TriangleStrip, EDrawMode::Triangles }, + { EDrawMode::TriangleFan, EDrawMode::Triangles } + }; + + for (int drawModeVal = 0; drawModeVal <= static_cast(EDrawMode::LineStrip); ++drawModeVal) + { + for (int gsInputTypeVal = 0; gsInputTypeVal <= static_cast(EDrawMode::LineStrip); ++gsInputTypeVal) + { + const auto drawMode = static_cast(drawModeVal); + const auto gsInputType = static_cast(gsInputTypeVal); + + // gs input mode restricted to only these types + if (gsInputType != EDrawMode::Points && gsInputType != EDrawMode::Lines && gsInputType != EDrawMode::Triangles) + continue; + + const auto it = std::find_if(validDrawModeToGSModeCombinations.begin(), validDrawModeToGSModeCombinations.end(), [&](const auto& modePair) { + return modePair.first == drawMode && modePair.second == gsInputType; + }); + const bool isValid = (it != validDrawModeToGSModeCombinations.end()); + + EXPECT_EQ(isValid, AppearanceUtils::GeometryShaderCompatibleWithDrawMode(gsInputType, drawMode)); + } + } } } diff --git a/client/test/ArrayBufferTest.cpp b/client/test/ArrayBufferTest.cpp new file mode 100644 index 000000000..805dd72e0 --- /dev/null +++ b/client/test/ArrayBufferTest.cpp @@ -0,0 +1,292 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2017 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include +#include + +#include "ClientTestUtils.h" +#include "SceneImpl.h" +#include "ramses-utils.h" +#include "Scene/ClientScene.h" +#include "ramses-client-api/ArrayBuffer.h" +#include "ramses-client-api/AttributeInput.h" +#include "ArrayBufferImpl.h" +#include "RamsesObjectTypeUtils.h" +#include "DataTypeUtils.h" +#include "glm/gtx/range.hpp" + +using namespace testing; +using namespace ramses_internal; + +namespace ramses +{ + template + std::vector SomeDataVector(); + template <> std::vector SomeDataVector() { return { 1, 2, 3 }; } + template <> std::vector SomeDataVector() { return { 1, 2, 3 }; } + template <> std::vector SomeDataVector() { return { 1.f, 2.f, 3.f }; } + template <> std::vector SomeDataVector() { return { vec2f{1.f, 2.f}, vec2f{3.f, 4.f} }; } + template <> std::vector SomeDataVector() { return { vec3f{1.f, 2.f, 3.f}, vec3f{3.f, 4.f, 5.f} }; } + template <> std::vector SomeDataVector() { return { vec4f{1.f, 2.f, 3.f, 4.f}, vec4f{3.f, 4.f, 5.f, 6.f} }; } + template <> std::vector SomeDataVector() { return { 1, 2, 3, 4, 5, 6 }; } + + template + class AnArrayBuffer : public LocalTestClientWithScene, public ::testing::Test + { + protected: + static constexpr EDataType GetDataType() { return GetEDataType(); } + + static constexpr uint32_t ElementSizeInBytes = EnumToSize(DataTypeUtils::ConvertDataTypeToInternal(GetDataType())); + + std::vector m_data{ SomeDataVector() }; + ArrayBuffer& m_dataBuffer{ *m_scene.createArrayBuffer(GetDataType(), static_cast(m_data.size()), "m_data buffer") }; + }; + + using DataTypes = ::testing::Types< + uint16_t, + uint32_t, + float, + vec2f, + vec3f, + vec4f, + Byte + >; + + TYPED_TEST_SUITE(AnArrayBuffer, DataTypes); + + TYPED_TEST(AnArrayBuffer, IsAllocatedOnInternalSceneAfterCreation) + { + EXPECT_TRUE(this->m_dataBuffer.m_impl.getDataBufferHandle().isValid()); + EXPECT_TRUE(this->m_scene.m_impl.getIScene().isDataBufferAllocated(this->m_dataBuffer.m_impl.getDataBufferHandle())); + } + + TYPED_TEST(AnArrayBuffer, ContainsZeroedDataAfterCreation) + { + EXPECT_EQ(this->m_data.size(), this->m_dataBuffer.getMaximumNumberOfElements()); + EXPECT_EQ(0u, this->m_dataBuffer.getUsedNumberOfElements()); + + std::vector dataInBuffer(this->m_data.size()); + EXPECT_EQ(StatusOK, this->m_dataBuffer.getData(dataInBuffer.data(), this->m_dataBuffer.getMaximumNumberOfElements())); + for (auto v : dataInBuffer) + { + if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) + { + for (auto e : v) + EXPECT_FLOAT_EQ(0.f, e); + } + else + { + EXPECT_EQ(static_cast(0), v); + } + } + } + + TYPED_TEST(AnArrayBuffer, CanUpdateWholeBuffer) + { + EXPECT_EQ(StatusOK, this->m_dataBuffer.updateData(0u, this->m_dataBuffer.getMaximumNumberOfElements(), this->m_data.data())); + + std::vector dataInBuffer(this->m_data.size()); + EXPECT_EQ(StatusOK, this->m_dataBuffer.getData(dataInBuffer.data(), this->m_dataBuffer.getMaximumNumberOfElements())); + EXPECT_EQ(this->m_data, dataInBuffer); + } + + TYPED_TEST(AnArrayBuffer, CanUpdateSingleElement) + { + // set whole buffer first + EXPECT_EQ(StatusOK, this->m_dataBuffer.updateData(0u, this->m_dataBuffer.getMaximumNumberOfElements(), this->m_data.data())); + + // update only second element with first element from test data + EXPECT_EQ(StatusOK, this->m_dataBuffer.updateData(1u, 1u, &this->m_data[0])); + std::vector dataInBuffer(this->m_data.size()); + EXPECT_EQ(StatusOK, this->m_dataBuffer.getData(dataInBuffer.data(), this->m_dataBuffer.getMaximumNumberOfElements())); + EXPECT_EQ(this->m_data[0], dataInBuffer[1]); + for (size_t i = 0u; i < this->m_data.size(); ++i) + { + if (i != 1u) + { + EXPECT_EQ(this->m_data[i], dataInBuffer[i]); + } + } + + // now update only first element with second element from test data + EXPECT_EQ(StatusOK, this->m_dataBuffer.updateData(0u, 1u, &this->m_data[1])); + EXPECT_EQ(StatusOK, this->m_dataBuffer.getData(dataInBuffer.data(), this->m_dataBuffer.getMaximumNumberOfElements())); + EXPECT_EQ(this->m_data[0], dataInBuffer[1]); + EXPECT_EQ(this->m_data[1], dataInBuffer[0]); + for (size_t i = 0u; i < this->m_data.size(); ++i) + { + if (i != 0u && i != 1u) + { + EXPECT_EQ(this->m_data[i], dataInBuffer[i]); + } + } + } + + TYPED_TEST(AnArrayBuffer, CanNotBeUpdatedWhenDataSizeBiggerThanMaximumSize) + { + this->m_data.push_back(this->m_data.front()); + EXPECT_NE(StatusOK, this->m_dataBuffer.updateData(0u, uint32_t(this->m_data.size()), this->m_data.data())); + } + + TYPED_TEST(AnArrayBuffer, CanNotBeUpdatedWhenDataSizeAndOffsetBiggerThanMaximumSize) + { + EXPECT_NE(StatusOK, this->m_dataBuffer.updateData(1u, uint32_t(this->m_data.size()), this->m_data.data())); + } + + TYPED_TEST(AnArrayBuffer, CanBeValidated) + { + const auto effect = TestEffects::CreateTestEffectWithAttribute(this->m_scene); + GeometryBinding& geom = this->createValidGeometry(effect); + if (DataTypeUtils::IsValidIndicesType(this->m_dataBuffer.getDataType())) + EXPECT_EQ(StatusOK, geom.setIndices(this->m_dataBuffer)); + else if (EDataType::ByteBlob == this->GetDataType()) + { + AttributeInput attrInput1; + effect->findAttributeInput("a_position", attrInput1); + AttributeInput attrInput2; + effect->findAttributeInput("a_vec2", attrInput2); + constexpr uint16_t nonZeroStride = 56u; + EXPECT_EQ(StatusOK, geom.setInputBuffer(attrInput1, this->m_dataBuffer, 0u, nonZeroStride)); + EXPECT_EQ(StatusOK, geom.setInputBuffer(attrInput2, this->m_dataBuffer, sizeof(float), nonZeroStride) ); + } + else + { + const char* validInputName = nullptr; + switch (this->GetDataType()) + { + case EDataType::Float: + validInputName = "a_position"; + break; + case EDataType::Vector2F: + validInputName = "a_vec2"; + break; + case EDataType::Vector3F: + validInputName = "a_vec3"; + break; + case EDataType::Vector4F: + validInputName = "a_vec4"; + break; + default: assert(false); + } + AttributeInput attrInput; + effect->findAttributeInput(validInputName, attrInput); + EXPECT_EQ(StatusOK, geom.setInputBuffer(attrInput, this->m_dataBuffer)); + } + EXPECT_EQ(StatusOK, this->m_dataBuffer.updateData(0u, 1u, this->m_data.data())); + EXPECT_EQ(StatusOK, this->m_dataBuffer.validate()); + } + + TYPED_TEST(AnArrayBuffer, IsValidIfNotUsedByAnyMeshButUsedByPickableObject) + { + ArrayBuffer* geometryBuffer = this->m_scene.createArrayBuffer(EDataType::Vector3F, 3, "geometryBuffer"); + EXPECT_EQ(StatusOK, geometryBuffer->updateData(0u, 1u, SomeDataVector().data())); + const pickableObjectId_t id(2); + ASSERT_TRUE(this->m_scene.createPickableObject(*geometryBuffer, id, "PickableObject")); + + EXPECT_EQ(StatusOK, geometryBuffer->validate()); + } + + TYPED_TEST(AnArrayBuffer, ReportsWarningIfNotUsedInGeometry) + { + EXPECT_EQ(StatusOK, this->m_dataBuffer.updateData(0u, 1u, this->m_data.data())); + EXPECT_NE(StatusOK, this->m_dataBuffer.validate()); + } + + TYPED_TEST(AnArrayBuffer, ReportsWarningIfUsedInGeometryButNotInitialized) + { + const auto effect = TestEffects::CreateTestEffectWithAttribute(this->m_scene); + GeometryBinding& geom = this->createValidGeometry(effect); + if (DataTypeUtils::IsValidIndicesType(this->m_dataBuffer.getDataType())) + geom.setIndices(this->m_dataBuffer); + else + { + AttributeInput attrInput; + effect->findAttributeInput("a_position", attrInput); + geom.setInputBuffer(attrInput, this->m_dataBuffer); + } + EXPECT_NE(StatusOK, this->m_dataBuffer.validate()); + } + + TYPED_TEST(AnArrayBuffer, CanGetDataType) + { + EXPECT_EQ(this->GetDataType(), this->m_dataBuffer.getDataType()); + } + + TYPED_TEST(AnArrayBuffer, CanGetMaximumSize) + { + EXPECT_EQ(this->m_data.size(), this->m_dataBuffer.getMaximumNumberOfElements()); + EXPECT_EQ(this->m_data.size(), this->m_dataBuffer.m_impl.getElementCount()); + } + + TYPED_TEST(AnArrayBuffer, CanGetUsedSize) + { + EXPECT_EQ(0u, this->m_dataBuffer.getUsedNumberOfElements()); + + // set 1 element + EXPECT_EQ(StatusOK, this->m_dataBuffer.updateData(0u, 1u, this->m_data.data())); + EXPECT_EQ(1u, this->m_dataBuffer.getUsedNumberOfElements()); + // set same element again - no change in used count + EXPECT_EQ(StatusOK, this->m_dataBuffer.updateData(0u, 1u, this->m_data.data())); + EXPECT_EQ(1u, this->m_dataBuffer.getUsedNumberOfElements()); + + // set 2 elements + EXPECT_EQ(StatusOK, this->m_dataBuffer.updateData(0u, 2u, this->m_data.data())); + EXPECT_EQ(2u, this->m_dataBuffer.getUsedNumberOfElements()); + // set first element again - no change in used count + EXPECT_EQ(StatusOK, this->m_dataBuffer.updateData(0u, 1u, this->m_data.data())); + EXPECT_EQ(2u, this->m_dataBuffer.getUsedNumberOfElements()); + + // set all elements + EXPECT_EQ(StatusOK, this->m_dataBuffer.updateData(0u, uint32_t(this->m_data.size()), this->m_data.data())); + EXPECT_EQ(uint32_t(this->m_data.size()), this->m_dataBuffer.getUsedNumberOfElements()); + EXPECT_EQ(this->m_dataBuffer.getMaximumNumberOfElements(), this->m_dataBuffer.getUsedNumberOfElements()); + } + + TYPED_TEST(AnArrayBuffer, FailsToUpdateIfUsingWrongType) + { + if (this->GetDataType() != EDataType::ByteBlob) + { + EXPECT_NE(StatusOK, this->m_dataBuffer.updateData(0u, 1u, SomeDataVector().data())); + } + else + { + EXPECT_NE(StatusOK, this->m_dataBuffer.updateData(0u, 1u, SomeDataVector().data())); + } + } + + TYPED_TEST(AnArrayBuffer, FailsToGetDataIfUsingWrongType) + { + if (this->GetDataType() != EDataType::ByteBlob) + { + auto data = SomeDataVector(); + EXPECT_NE(StatusOK, this->m_dataBuffer.getData(data.data(), 1u)); + } + else + { + auto data = SomeDataVector(); + EXPECT_NE(StatusOK, this->m_dataBuffer.getData(data.data(), 1u)); + } + } + + TEST(AnArrayBuffer, FailsToCreateForUnsupportedDataType) + { + LocalTestClientWithScene testScene; + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::Int32, 1u)); + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::Vector2I, 1u)); + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::Vector3I, 1u)); + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::Vector4I, 1u)); + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::Matrix22F, 1u)); + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::Matrix33F, 1u)); + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::Matrix44F, 1u)); + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::TextureSampler2D, 1u)); + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::TextureSampler2DMS, 1u)); + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::TextureSampler3D, 1u)); + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::TextureSamplerCube, 1u)); + EXPECT_EQ(nullptr, testScene.getScene().createArrayBuffer(EDataType::TextureSamplerExternal, 1u)); + } +} diff --git a/client/ramses-client/test/BlitPassTest.cpp b/client/test/BlitPassTest.cpp similarity index 75% rename from client/ramses-client/test/BlitPassTest.cpp rename to client/test/BlitPassTest.cpp index 15bbd064c..c263d3d38 100644 --- a/client/ramses-client/test/BlitPassTest.cpp +++ b/client/test/BlitPassTest.cpp @@ -22,21 +22,21 @@ using namespace ramses_internal; namespace ramses { - class ABlitPass : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test + class ABlitPass : public LocalTestClientWithScene, public testing::Test { protected: ABlitPass() - : LocalTestClientWithSceneAndAnimationSystem() + : LocalTestClientWithScene() , sourceRenderBuffer(*createRenderBuffer()) , destinationRenderBuffer(*createRenderBuffer()) , blitpass(*m_scene.createBlitPass(sourceRenderBuffer, destinationRenderBuffer, "BlitPass")) - , blitpassHandle(blitpass.impl.getBlitPassHandle()) + , blitpassHandle(blitpass.m_impl.getBlitPassHandle()) { } RenderBuffer* createRenderBuffer() { - RenderBuffer* renderBuffer = m_scene.createRenderBuffer(16u, 12u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + RenderBuffer* renderBuffer = m_scene.createRenderBuffer(16u, 12u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); return renderBuffer; } @@ -49,25 +49,25 @@ namespace ramses TEST_F(ABlitPass, CanGetSourceAndDestinationBuffers) { const RenderBuffer& srcRB = blitpass.getSourceRenderBuffer(); - EXPECT_EQ(srcRB.impl.getRenderBufferHandle(), sourceRenderBuffer.impl.getRenderBufferHandle()); + EXPECT_EQ(srcRB.m_impl.getRenderBufferHandle(), sourceRenderBuffer.m_impl.getRenderBufferHandle()); const RenderBuffer& dstRB = blitpass.getDestinationRenderBuffer(); - EXPECT_EQ(dstRB.impl.getRenderBufferHandle(), destinationRenderBuffer.impl.getRenderBufferHandle()); + EXPECT_EQ(dstRB.m_impl.getRenderBufferHandle(), destinationRenderBuffer.m_impl.getRenderBufferHandle()); } TEST_F(ABlitPass, BlittingRegionIsSetToCompleteBufferByDefault) { - const ramses_internal::PixelRectangle& sourceRegion = m_scene.impl.getIScene().getBlitPass(blitpassHandle).sourceRegion; + const ramses_internal::PixelRectangle& sourceRegion = m_scene.m_impl.getIScene().getBlitPass(blitpassHandle).sourceRegion; EXPECT_EQ(0u, sourceRegion.x); EXPECT_EQ(0u, sourceRegion.y); - EXPECT_EQ(static_cast(sourceRenderBuffer.getWidth()), sourceRegion.width); - EXPECT_EQ(static_cast(sourceRenderBuffer.getHeight()), sourceRegion.height); + EXPECT_EQ(static_cast(sourceRenderBuffer.getWidth()), sourceRegion.width); + EXPECT_EQ(static_cast(sourceRenderBuffer.getHeight()), sourceRegion.height); - const ramses_internal::PixelRectangle& destinationRegion = m_scene.impl.getIScene().getBlitPass(blitpassHandle).destinationRegion; + const ramses_internal::PixelRectangle& destinationRegion = m_scene.m_impl.getIScene().getBlitPass(blitpassHandle).destinationRegion; EXPECT_EQ(0u, destinationRegion.x); EXPECT_EQ(0u, destinationRegion.y); - EXPECT_EQ(static_cast(destinationRenderBuffer.getWidth()), destinationRegion.width); - EXPECT_EQ(static_cast(destinationRenderBuffer.getHeight()), destinationRegion.height); + EXPECT_EQ(static_cast(destinationRenderBuffer.getWidth()), destinationRegion.width); + EXPECT_EQ(static_cast(destinationRenderBuffer.getHeight()), destinationRegion.height); { //client HL api @@ -97,13 +97,13 @@ namespace ramses const int32_t height(6u); EXPECT_EQ(StatusOK, blitpass.setBlittingRegion(sourceX, sourceY, destinationX, destinationY, width, height)); - const ramses_internal::PixelRectangle& sourceRegion = m_scene.impl.getIScene().getBlitPass(blitpassHandle).sourceRegion; + const ramses_internal::PixelRectangle& sourceRegion = m_scene.m_impl.getIScene().getBlitPass(blitpassHandle).sourceRegion; EXPECT_EQ(sourceX, sourceRegion.x); EXPECT_EQ(sourceY, sourceRegion.y); EXPECT_EQ(width, sourceRegion.width); EXPECT_EQ(height, sourceRegion.height); - const ramses_internal::PixelRectangle& destinationRegion = m_scene.impl.getIScene().getBlitPass(blitpassHandle).destinationRegion; + const ramses_internal::PixelRectangle& destinationRegion = m_scene.m_impl.getIScene().getBlitPass(blitpassHandle).destinationRegion; EXPECT_EQ(destinationX, destinationRegion.x); EXPECT_EQ(destinationY, destinationRegion.y); EXPECT_EQ(width, destinationRegion.width); @@ -136,17 +136,17 @@ namespace ramses EXPECT_NE(StatusOK, blitpass.setBlittingRegion(0u, 0u, sourceRenderBuffer.getWidth(), 0u, 10, 10u)); EXPECT_NE(StatusOK, blitpass.setBlittingRegion(0u, 0u, 0, sourceRenderBuffer.getHeight(), 10, 10u)); - const ramses_internal::PixelRectangle& sourceRegion = m_scene.impl.getIScene().getBlitPass(blitpassHandle).sourceRegion; + const ramses_internal::PixelRectangle& sourceRegion = m_scene.m_impl.getIScene().getBlitPass(blitpassHandle).sourceRegion; EXPECT_EQ(0u, sourceRegion.x); EXPECT_EQ(0u, sourceRegion.y); - EXPECT_EQ(static_cast(sourceRenderBuffer.getWidth()), sourceRegion.width); - EXPECT_EQ(static_cast(sourceRenderBuffer.getHeight()), sourceRegion.height); + EXPECT_EQ(static_cast(sourceRenderBuffer.getWidth()), sourceRegion.width); + EXPECT_EQ(static_cast(sourceRenderBuffer.getHeight()), sourceRegion.height); - const ramses_internal::PixelRectangle& destinationRegion = m_scene.impl.getIScene().getBlitPass(blitpassHandle).destinationRegion; + const ramses_internal::PixelRectangle& destinationRegion = m_scene.m_impl.getIScene().getBlitPass(blitpassHandle).destinationRegion; EXPECT_EQ(0u, destinationRegion.x); EXPECT_EQ(0u, destinationRegion.y); - EXPECT_EQ(static_cast(destinationRenderBuffer.getWidth()), destinationRegion.width); - EXPECT_EQ(static_cast(destinationRenderBuffer.getHeight()), destinationRegion.height); + EXPECT_EQ(static_cast(destinationRenderBuffer.getWidth()), destinationRegion.width); + EXPECT_EQ(static_cast(destinationRenderBuffer.getHeight()), destinationRegion.height); } TEST_F(ABlitPass, canValidate) diff --git a/client/ramses-client/test/CameraTest.cpp b/client/test/CameraTest.cpp similarity index 75% rename from client/ramses-client/test/CameraTest.cpp rename to client/test/CameraTest.cpp index 638d68311..db08898f2 100644 --- a/client/ramses-client/test/CameraTest.cpp +++ b/client/test/CameraTest.cpp @@ -11,9 +11,7 @@ #include "ClientTestUtils.h" #include "ramses-client-api/PerspectiveCamera.h" #include "ramses-client-api/OrthographicCamera.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector4f.h" +#include "ramses-client-api/DataObject.h" #include "Math3d/CameraMatrixHelper.h" #include "DataObjectImpl.h" #include "CameraNodeImpl.h" @@ -103,7 +101,7 @@ namespace ramses TYPED_TEST(ACamera, reportsErrorIfTryingToRetrieveProjectionMatrixWithoutBeingInitialized) { - float projMat[16] = { 0.f }; + matrix44f projMat; EXPECT_NE(StatusOK, this->camera->getProjectionMatrix(projMat)); } @@ -143,15 +141,15 @@ namespace ramses { EXPECT_EQ(StatusOK, this->camera->setFrustum(0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f)); - const auto projType = (this->camera->getType() == ERamsesObjectType_PerspectiveCamera ? ramses_internal::ECameraProjectionType::Perspective : ramses_internal::ECameraProjectionType::Orthographic); - const ramses_internal::Matrix44f expectedMatrix = ramses_internal::CameraMatrixHelper::ProjectionMatrix(ramses_internal::ProjectionParams::Frustum(projType, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f)); + const auto projType = (this->camera->getType() == ERamsesObjectType::PerspectiveCamera ? ramses_internal::ECameraProjectionType::Perspective : ramses_internal::ECameraProjectionType::Orthographic); + const auto expectedMatrix = ramses_internal::CameraMatrixHelper::ProjectionMatrix(ramses_internal::ProjectionParams::Frustum(projType, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f)); - float projMatData[16] = { 0.f }; + matrix44f projMatData; EXPECT_EQ(StatusOK, this->camera->getProjectionMatrix(projMatData)); for (uint32_t i = 0u; i < 16u; ++i) { - EXPECT_FLOAT_EQ(expectedMatrix.data[i], projMatData[i]); + EXPECT_FLOAT_EQ(expectedMatrix[i/4][i%4], projMatData[i/4][i%4]); } } @@ -164,8 +162,8 @@ namespace ramses TYPED_TEST(ACamera, canBindViewportData) { - const auto do1 = this->m_scene.createDataVector2i(); - const auto do2 = this->m_scene.createDataVector2i(); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2I); EXPECT_EQ(StatusOK, this->camera->bindViewportOffset(*do1)); EXPECT_EQ(StatusOK, this->camera->bindViewportSize(*do2)); @@ -176,17 +174,27 @@ namespace ramses TYPED_TEST(ACamera, canBindFrustumPlanesData) { - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); EXPECT_EQ(StatusOK, this->camera->bindFrustumPlanes(*do1, *do2)); EXPECT_TRUE(this->camera->isFrustumPlanesBound()); } + TYPED_TEST(ACamera, failsToBindViewportIfWrongType) + { + const auto wrongDataObject = this->m_scene.createDataObject(EDataType::Vector3F); + EXPECT_NE(StatusOK, this->camera->bindViewportOffset(*wrongDataObject)); + EXPECT_NE(StatusOK, this->camera->bindViewportSize(*wrongDataObject)); + + EXPECT_FALSE(this->camera->isViewportOffsetBound()); + EXPECT_FALSE(this->camera->isViewportSizeBound()); + } + TYPED_TEST(ACamera, failsToBindViewportDataIfDataObjectFromDifferentScene) { auto otherScene = this->client.createScene(sceneId_t{ this->m_scene.getSceneId().getValue() + 1 }); - const auto do1 = otherScene->createDataVector2i(); - const auto do2 = otherScene->createDataVector2i(); + const auto do1 = otherScene->createDataObject(EDataType::Vector2I); + const auto do2 = otherScene->createDataObject(EDataType::Vector2I); EXPECT_NE(StatusOK, this->camera->bindViewportOffset(*do1)); EXPECT_NE(StatusOK, this->camera->bindViewportSize(*do2)); @@ -195,11 +203,23 @@ namespace ramses EXPECT_FALSE(this->camera->isViewportSizeBound()); } + TYPED_TEST(ACamera, failsToBindFrustumIfWrongType) + { + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); + const auto wrongDataObject = this->m_scene.createDataObject(EDataType::Vector3F); + EXPECT_NE(StatusOK, this->camera->bindFrustumPlanes(*do1, *wrongDataObject)); + EXPECT_FALSE(this->camera->isFrustumPlanesBound()); + + EXPECT_NE(StatusOK, this->camera->bindFrustumPlanes(*wrongDataObject, *do2)); + EXPECT_FALSE(this->camera->isFrustumPlanesBound()); + } + TYPED_TEST(ACamera, failsToBindFrustumDataIfDataObjectFromDifferentScene) { auto otherScene = this->client.createScene(sceneId_t{ this->m_scene.getSceneId().getValue() + 1 }); - const auto do1 = otherScene->createDataVector4f(); - const auto do2 = otherScene->createDataVector2f(); + const auto do1 = otherScene->createDataObject(EDataType::Vector4F); + const auto do2 = otherScene->createDataObject(EDataType::Vector2F); EXPECT_NE(StatusOK, this->camera->bindFrustumPlanes(*do1, *do2)); EXPECT_FALSE(this->camera->isFrustumPlanesBound()); @@ -207,21 +227,21 @@ namespace ramses TYPED_TEST(ACamera, canUnbindViewportData) { - const auto do1 = this->m_scene.createDataVector2i(); - const auto do2 = this->m_scene.createDataVector2i(); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2I); EXPECT_EQ(StatusOK, this->camera->bindViewportOffset(*do1)); EXPECT_EQ(StatusOK, this->camera->bindViewportSize(*do2)); - EXPECT_EQ(do1->impl.getDataReference(), this->camera->impl.getViewportOffsetHandle()); - EXPECT_EQ(do2->impl.getDataReference(), this->camera->impl.getViewportSizeHandle()); + EXPECT_EQ(do1->m_impl.getDataReference(), this->camera->m_impl.getViewportOffsetHandle()); + EXPECT_EQ(do2->m_impl.getDataReference(), this->camera->m_impl.getViewportSizeHandle()); EXPECT_TRUE(this->camera->isViewportOffsetBound()); EXPECT_TRUE(this->camera->isViewportSizeBound()); EXPECT_EQ(StatusOK, this->camera->unbindViewportOffset()); EXPECT_EQ(StatusOK, this->camera->unbindViewportSize()); - EXPECT_NE(do1->impl.getDataReference(), this->camera->impl.getViewportOffsetHandle()); - EXPECT_NE(do2->impl.getDataReference(), this->camera->impl.getViewportSizeHandle()); + EXPECT_NE(do1->m_impl.getDataReference(), this->camera->m_impl.getViewportOffsetHandle()); + EXPECT_NE(do2->m_impl.getDataReference(), this->camera->m_impl.getViewportSizeHandle()); EXPECT_FALSE(this->camera->isViewportOffsetBound()); EXPECT_FALSE(this->camera->isViewportSizeBound()); @@ -229,26 +249,26 @@ namespace ramses TYPED_TEST(ACamera, canUnbindFrustumPlanesData) { - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); EXPECT_EQ(StatusOK, this->camera->bindFrustumPlanes(*do1, *do2)); EXPECT_TRUE(this->camera->isFrustumPlanesBound()); - EXPECT_EQ(do1->impl.getDataReference(), this->camera->impl.getFrustrumPlanesHandle()); - EXPECT_EQ(do2->impl.getDataReference(), this->camera->impl.getFrustrumNearFarPlanesHandle()); + EXPECT_EQ(do1->m_impl.getDataReference(), this->camera->m_impl.getFrustrumPlanesHandle()); + EXPECT_EQ(do2->m_impl.getDataReference(), this->camera->m_impl.getFrustrumNearFarPlanesHandle()); EXPECT_EQ(StatusOK, this->camera->unbindFrustumPlanes()); EXPECT_FALSE(this->camera->isFrustumPlanesBound()); - EXPECT_NE(do1->impl.getDataReference(), this->camera->impl.getFrustrumPlanesHandle()); - EXPECT_NE(do2->impl.getDataReference(), this->camera->impl.getFrustrumNearFarPlanesHandle()); + EXPECT_NE(do1->m_impl.getDataReference(), this->camera->m_impl.getFrustrumPlanesHandle()); + EXPECT_NE(do2->m_impl.getDataReference(), this->camera->m_impl.getFrustrumNearFarPlanesHandle()); } TYPED_TEST(ACamera, getsReferencedViewportValuesWhenBound) { - const auto do1 = this->m_scene.createDataVector2i(); - const auto do2 = this->m_scene.createDataVector2i(); - do1->setValue(1, 2); - do2->setValue(3, 4); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2I); + do1->setValue(vec2i{ 1, 2 }); + do2->setValue(vec2i{ 3, 4 }); EXPECT_EQ(StatusOK, this->camera->setViewport(-1, -2, 16, 32)); @@ -265,10 +285,10 @@ namespace ramses TYPED_TEST(ACamera, getsReferencedFrustumPlanesValuesWhenBound) { - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); - do1->setValue(1, 2, 3, 4); - do2->setValue(5, 6); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); + do1->setValue(vec4f{ 1.f, 2.f, 3.f, 4.f }); + do2->setValue(vec2f{ 5.f, 6.f }); EXPECT_EQ(StatusOK, this->camera->setFrustum(-1.f, 1.f, -1.f, 1.f, 0.1f, 1.f)); @@ -285,10 +305,10 @@ namespace ramses TYPED_TEST(ACamera, getsCorrectViewportValuesWhenBoundFromOneDataObjectToAnother) { - const auto do1 = this->m_scene.createDataVector2i(); - const auto do2 = this->m_scene.createDataVector2i(); - do1->setValue(1, 2); - do2->setValue(3, 4); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2I); + do1->setValue(vec2i{ 1, 2 }); + do2->setValue(vec2i{ 3, 4 }); EXPECT_EQ(StatusOK, this->camera->setViewport(-1, -2, 16, 32)); @@ -315,20 +335,20 @@ namespace ramses TYPED_TEST(ACamera, getsCorrectFrustumValuesWhenBoundFromOneDataObjectToAnother) { - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); - do1->setValue(1, 2, 3, 4); - do2->setValue(5, 6); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); + do1->setValue(vec4f{ 1.f, 2.f, 3.f, 4.f }); + do2->setValue(vec2f{ 5.f, 6.f }); EXPECT_EQ(StatusOK, this->camera->setFrustum(-1.f, 1.f, -1.f, 1.f, 0.1f, 1.f)); EXPECT_EQ(StatusOK, this->camera->bindFrustumPlanes(*do1, *do2)); EXPECT_TRUE(this->camera->isFrustumPlanesBound()); - const auto do3 = this->m_scene.createDataVector4f(); - const auto do4 = this->m_scene.createDataVector2f(); - do3->setValue(-1, -2, -3, -4); - do4->setValue(-5, -6); + const auto do3 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do4 = this->m_scene.createDataObject(EDataType::Vector2F); + do3->setValue(vec4f{ -1.f, -2.f, -3.f, -4.f }); + do4->setValue(vec2f{ -5.f, -6.f }); EXPECT_EQ(StatusOK, this->camera->bindFrustumPlanes(*do3, *do4)); EXPECT_TRUE(this->camera->isFrustumPlanesBound()); @@ -343,10 +363,10 @@ namespace ramses TYPED_TEST(ACamera, getsReferencedViewportValuesWhenBound_alsoWhenChangedInDataObject) { - const auto do1 = this->m_scene.createDataVector2i(); - const auto do2 = this->m_scene.createDataVector2i(); - do1->setValue(1, 2); - do2->setValue(3, 4); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2I); + do1->setValue(vec2i{ 1, 2 }); + do2->setValue(vec2i{ 3, 4 }); EXPECT_EQ(StatusOK, this->camera->setViewport(-1, -2, 16, 32)); @@ -360,8 +380,8 @@ namespace ramses EXPECT_EQ(3u, this->camera->getViewportWidth()); EXPECT_EQ(4u, this->camera->getViewportHeight()); - do1->setValue(11, 22); - do2->setValue(33, 44); + do1->setValue(vec2i{ 11, 22 }); + do2->setValue(vec2i{ 33, 44 }); EXPECT_EQ(11, this->camera->getViewportX()); EXPECT_EQ(22, this->camera->getViewportY()); @@ -371,18 +391,18 @@ namespace ramses TYPED_TEST(ACamera, getsReferencedFrustumValuesWhenBound_alsoWhenChangedInDataObject) { - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); - do1->setValue(1, 2, 3, 4); - do2->setValue(5, 6); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); + do1->setValue(vec4f{ 1.f, 2.f, 3.f, 4.f }); + do2->setValue(vec2f{ 5.f, 6.f }); EXPECT_EQ(StatusOK, this->camera->setFrustum(-1.f, 1.f, -1.f, 1.f, 0.1f, 1.f)); EXPECT_EQ(StatusOK, this->camera->bindFrustumPlanes(*do1, *do2)); EXPECT_TRUE(this->camera->isFrustumPlanesBound()); - do1->setValue(-1, -2, -3, -4); - do2->setValue(-5, -6); + do1->setValue(vec4f{ -1.f, -2.f, -3.f, -4.f }); + do2->setValue(vec2f{ -5.f, -6.f }); EXPECT_EQ(-1.f, this->camera->getLeftPlane()); EXPECT_EQ(-2.f, this->camera->getRightPlane()); @@ -396,8 +416,8 @@ namespace ramses { auto camera2 = &this->template createObject("camera"); - const auto do1 = this->m_scene.createDataVector2i(); - do1->setValue(3, 4); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + do1->setValue(vec2i{ 3, 4 }); EXPECT_EQ(StatusOK, this->camera->setViewport(-1, -2, 16, 32)); EXPECT_EQ(StatusOK, camera2->setViewport(-11, -22, 166, 322)); @@ -416,7 +436,7 @@ namespace ramses EXPECT_EQ(3u, camera2->getViewportWidth()); EXPECT_EQ(4u, camera2->getViewportHeight()); - do1->setValue(11, 22); + do1->setValue(vec2i{ 11, 22 }); EXPECT_EQ(11, this->camera->getViewportX()); EXPECT_EQ(22, this->camera->getViewportY()); @@ -430,10 +450,10 @@ namespace ramses TYPED_TEST(ACamera, canBindDataToTwoCamerasFrustum) { - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); - do1->setValue(1, 2, 3, 4); - do2->setValue(5, 6); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); + do1->setValue(vec4f{ 1.f, 2.f, 3.f, 4.f }); + do2->setValue(vec2f{ 5.f, 6.f }); EXPECT_EQ(StatusOK, this->camera->setFrustum(-1.f, 1.f, -1.f, 1.f, 0.1f, 1.f)); auto camera2 = &this->template createObject("camera"); @@ -457,8 +477,8 @@ namespace ramses EXPECT_EQ(5.f, camera2->getNearPlane()); EXPECT_EQ(6.f, camera2->getFarPlane()); - do1->setValue(-1, -2, -3, -4); - do2->setValue(-5, -6); + do1->setValue(vec4f{ -1.f, -2.f, -3.f, -4.f }); + do2->setValue(vec2f{ -5.f, -6.f }); EXPECT_EQ(-1.f, this->camera->getLeftPlane()); EXPECT_EQ(-2.f, this->camera->getRightPlane()); EXPECT_EQ(-3.f, this->camera->getBottomPlane()); @@ -475,10 +495,10 @@ namespace ramses TYPED_TEST(ACamera, getsOriginalViewportValuesWhenUnbound) { - const auto do1 = this->m_scene.createDataVector2i(); - const auto do2 = this->m_scene.createDataVector2i(); - do1->setValue(1, 2); - do2->setValue(3, 4); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2I); + do1->setValue(vec2i{ 1, 2 }); + do2->setValue(vec2i{ 3, 4 }); EXPECT_EQ(StatusOK, this->camera->setViewport(-1, -2, 16, 32)); @@ -503,10 +523,10 @@ namespace ramses TYPED_TEST(ACamera, getsOriginalFrustumValuesWhenUnbound) { - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); - do1->setValue(1, 2, 3, 4); - do2->setValue(5, 6); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); + do1->setValue(vec4f{ 1.f, 2.f, 3.f, 4.f }); + do2->setValue(vec2f{ 5.f, 6.f }); EXPECT_EQ(StatusOK, this->camera->setFrustum(-1.f, 1.f, -1.f, 1.f, 0.1f, 1.f)); @@ -525,8 +545,8 @@ namespace ramses TYPED_TEST(ACamera, viewportValueSetIfBoundOnlyAffectsOriginalValueThatBecomesActiveAfterUnbound) { - const auto do1 = this->m_scene.createDataVector2i(); - do1->setValue(1, 2); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + do1->setValue(vec2i{ 1, 2 }); EXPECT_EQ(StatusOK, this->camera->setViewport(-1, -2, 16, 32)); @@ -556,10 +576,10 @@ namespace ramses TYPED_TEST(ACamera, frustumValueSetIfBoundOnlyAffectOriginalValuesThatBecomeActiveAfterUnbound) { - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); - do1->setValue(1, 2, 3, 4); - do2->setValue(5, 6); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); + do1->setValue(vec4f{ 1.f, 2.f, 3.f, 4.f }); + do2->setValue(vec2f{ 5.f, 6.f }); EXPECT_EQ(StatusOK, this->camera->setFrustum(-1.f, 1.f, -1.f, 1.f, 0.1f, 1.f)); @@ -592,10 +612,10 @@ namespace ramses // make frustum valid - not testing frustum validity here EXPECT_EQ(StatusOK, this->camera->setFrustum(-1, 1, -1, 1, 0.1f, 0.2f)); - const auto do1 = this->m_scene.createDataVector2i(); - const auto do2 = this->m_scene.createDataVector2i(); - do1->setValue(1, 2); - do2->setValue(3, 4); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2I); + do1->setValue(vec2i{ 1, 2 }); + do2->setValue(vec2i{ 3, 4 }); EXPECT_EQ(StatusOK, this->camera->bindViewportOffset(*do1)); EXPECT_EQ(StatusOK, this->camera->bindViewportSize(*do2)); EXPECT_EQ(StatusOK, this->camera->validate()); @@ -606,10 +626,10 @@ namespace ramses // make frustum valid - not testing frustum validity here EXPECT_EQ(StatusOK, this->camera->setFrustum(-1, 1, -1, 1, 0.1f, 0.2f)); - const auto do1 = this->m_scene.createDataVector2i(); - const auto do2 = this->m_scene.createDataVector2i(); - do1->setValue(1, 2); - do2->setValue(3, 4); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2I); + do1->setValue(vec2i{ 1, 2 }); + do2->setValue(vec2i{ 3, 4 }); EXPECT_EQ(StatusOK, this->camera->bindViewportOffset(*do1)); EXPECT_EQ(StatusOK, this->camera->bindViewportSize(*do2)); EXPECT_EQ(StatusOK, this->camera->validate()); @@ -626,10 +646,10 @@ namespace ramses // make viewport valid - not testing viewport validity here EXPECT_EQ(StatusOK, this->camera->setViewport(0, 0, 16, 16)); - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); - do1->setValue(-1, 1, -1, 1); - do2->setValue(1, 10); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); + do1->setValue(vec4f{ -1.f, 1.f, -1.f, 1.f }); + do2->setValue(vec2f{ 1.f, 10.f }); EXPECT_EQ(StatusOK, this->camera->bindFrustumPlanes(*do1, *do2)); // bound values valid EXPECT_EQ(StatusOK, this->camera->validate()); @@ -644,10 +664,10 @@ namespace ramses // make frustum valid - not testing frustum validity here EXPECT_EQ(StatusOK, this->camera->setFrustum(-1, 1, -1, 1, 0.1f, 0.2f)); - const auto do1 = this->m_scene.createDataVector2i(); - const auto do2 = this->m_scene.createDataVector2i(); - do1->setValue(1, 2); - do2->setValue(0, 4); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2I); + do1->setValue(vec2i{ 1, 2 }); + do2->setValue(vec2i{ 0, 4 }); EXPECT_EQ(StatusOK, this->camera->bindViewportOffset(*do1)); EXPECT_EQ(StatusOK, this->camera->bindViewportSize(*do2)); EXPECT_NE(StatusOK, this->camera->validate()); @@ -658,10 +678,10 @@ namespace ramses // make viewport valid - not testing viewport validity here EXPECT_EQ(StatusOK, this->camera->setViewport(0, 0, 16, 16)); - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); - do1->setValue(1, -1, 1, -1); - do2->setValue(1, 10); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); + do1->setValue(vec4f{ 1.f, -1.f, 1.f, -1.f }); + do2->setValue(vec2f{ 1.f, 10.f }); EXPECT_EQ(StatusOK, this->camera->bindFrustumPlanes(*do1, *do2)); EXPECT_NE(StatusOK, this->camera->validate()); } @@ -673,10 +693,10 @@ namespace ramses EXPECT_EQ(StatusOK, this->camera->setViewport(0, 0, 16, 16)); - const auto do1 = this->m_scene.createDataVector2i(); - const auto do2 = this->m_scene.createDataVector2i(); - do1->setValue(1, 2); - do2->setValue(0, 4); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector2I); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2I); + do1->setValue(vec2i{ 1, 2 }); + do2->setValue(vec2i{ 0, 4 }); EXPECT_EQ(StatusOK, this->camera->bindViewportOffset(*do1)); EXPECT_EQ(StatusOK, this->camera->bindViewportSize(*do2)); EXPECT_NE(StatusOK, this->camera->validate()); @@ -689,10 +709,10 @@ namespace ramses EXPECT_EQ(StatusOK, this->camera->setFrustum(-1, 1, -1, 1, 0.1f, 0.2f)); - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); - do1->setValue(1, -1, 1, -1); - do2->setValue(1, 10); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); + do1->setValue(vec4f{ 1.f, -1.f, 1.f, -1.f }); + do2->setValue(vec2f{ 1.f, 10.f }); EXPECT_EQ(StatusOK, this->camera->bindFrustumPlanes(*do1, *do2)); EXPECT_NE(StatusOK, this->camera->validate()); } @@ -740,8 +760,8 @@ namespace ramses TEST_F(APerspectiveCamera, canBindFrustumAndGetOriginalValuesWhenUnbound) { - const auto do1 = this->m_scene.createDataVector4f(); - const auto do2 = this->m_scene.createDataVector2f(); + const auto do1 = this->m_scene.createDataObject(EDataType::Vector4F); + const auto do2 = this->m_scene.createDataObject(EDataType::Vector2F); EXPECT_TRUE(RamsesUtils::SetPerspectiveCameraFrustumToDataObjects(45.f, 2.3f, 2.f, 20.f, *do1, *do2)); EXPECT_EQ(StatusOK, this->camera->setFrustum(19.f, 1.33f, 0.1f, 1.f)); diff --git a/client/ramses-client/test/ClientApplicationLogicTest.cpp b/client/test/ClientApplicationLogicTest.cpp similarity index 90% rename from client/ramses-client/test/ClientApplicationLogicTest.cpp rename to client/test/ClientApplicationLogicTest.cpp index 9401249a3..8b2378c1c 100644 --- a/client/ramses-client/test/ClientApplicationLogicTest.cpp +++ b/client/test/ClientApplicationLogicTest.cpp @@ -106,7 +106,7 @@ TEST_F(AClientApplicationLogic, forwardsAddingOfResourceFileToResourceComponent) const ResourceInfo resourceInfo(EResourceType_VertexArray, ResourceContentHash(44u, 0), 2u, 1u); ResourceTableOfContents resourceToc; resourceToc.registerContents(resourceInfo, 0u, 1u); - const String fileName("resourceFile"); + const std::string fileName("resourceFile"); InputStreamContainerSPtr resourceFileStream(std::make_shared(fileName)); EXPECT_CALL(resourceComponent, addResourceFile(resourceFileStream, Ref(resourceToc))); @@ -118,7 +118,7 @@ TEST_F(AClientApplicationLogic, triesToGetRequestedResourceFromResourceComponent const ResourceContentHash dummyResourceHash(44u, 0); ON_CALL(resourceComponent, getResource(_)).WillByDefault(Return(ManagedResource())); EXPECT_CALL(resourceComponent, getResource(dummyResourceHash)); - logic.getResource(dummyResourceHash); + EXPECT_EQ(ManagedResource(), logic.getResource(dummyResourceHash)); } TEST_F(AClientApplicationLogic, triesToGetHashUsageFromResourceComponent) @@ -126,7 +126,7 @@ TEST_F(AClientApplicationLogic, triesToGetHashUsageFromResourceComponent) const ResourceContentHash dummyResourceHash(44u, 0); ON_CALL(resourceComponent, getResourceHashUsage(_)).WillByDefault(Return(ResourceHashUsage())); EXPECT_CALL(resourceComponent, getResourceHashUsage(dummyResourceHash)); - logic.getHashUsage(dummyResourceHash); + EXPECT_FALSE(logic.getHashUsage(dummyResourceHash).isValid()); } TEST_F(AClientApplicationLogic, addsAndRemovesResourceFilesFromComponent) @@ -183,7 +183,7 @@ class AClientApplicationLogicWithRealComponents : public ::testing::Test public: AClientApplicationLogicWithRealComponents() : resComp(stats, fwlock) - , sceneComp(clientId, commSystem, connStatusUpdateNotifier, resComp, fwlock) + , sceneComp(clientId, commSystem, connStatusUpdateNotifier, resComp, fwlock, ramses::EFeatureLevel_Latest) , logic(clientId, fwlock) { logic.init(resComp, sceneComp); @@ -210,7 +210,7 @@ TEST_F(AClientApplicationLogicWithRealComponents, keepsResourcesAliveForNewSubsc ClientScene clientScene{ SceneInfo(sceneId) }; logic.createScene(clientScene, false); logic.publishScene(sceneId, EScenePublicationMode_LocalAndRemote); - auto res = new TextureResource(EResourceType_Texture2D, TextureMetaInfo(1u, 1u, 1u, ETextureFormat::R8, false, {}, { 1u }), ResourceCacheFlag_DoNotCache, String()); + auto res = new TextureResource(EResourceType_Texture2D, TextureMetaInfo(1u, 1u, 1u, ETextureFormat::R8, false, {}, { 1u }), ResourceCacheFlag_DoNotCache, {}); res->setResourceData(ResourceBlob{ 1 }, { 1u, 1u }); auto hash = res->getHash(); { @@ -220,13 +220,13 @@ TEST_F(AClientApplicationLogicWithRealComponents, keepsResourcesAliveForNewSubsc auto hashUsage = logic.getHashUsage(hash); // use texture2d - auto stHandle = clientScene.allocateStreamTexture(WaylandIviSurfaceId{ 1u }, hash); + const auto dataSlotHandle = clientScene.allocateDataSlot({ EDataSlotType_TextureProvider, DataSlotId(0u), {}, {}, hash, {} }); sceneComp.handleSubscribeScene(sceneId, renderer1); EXPECT_TRUE(logic.flush(sceneId, {}, {})); - // release stream texture along with its texture2d (by leaving scope) - clientScene.releaseStreamTexture(stHandle); + // release data slot along with its texture2d (by leaving scope) + clientScene.releaseDataSlot(dataSlotHandle); } EXPECT_EQ(resComp.getResource(hash).get(), res); @@ -238,9 +238,9 @@ TEST_F(AClientApplicationLogicWithRealComponents, keepsAlsoOldResourcesAliveForN ClientScene clientScene{ SceneInfo(sceneId) }; logic.createScene(clientScene, false); logic.publishScene(sceneId, EScenePublicationMode_LocalAndRemote); - auto res = new TextureResource(EResourceType_Texture2D, TextureMetaInfo(1u, 1u, 1u, ETextureFormat::R8, false, {}, { 1u }), ResourceCacheFlag_DoNotCache, String()); + auto res = new TextureResource(EResourceType_Texture2D, TextureMetaInfo(1u, 1u, 1u, ETextureFormat::R8, false, {}, { 1u }), ResourceCacheFlag_DoNotCache, {}); res->setResourceData(ResourceBlob{ 1 }, { 1u, 1u }); - auto res2 = new TextureResource(EResourceType_Texture2D, TextureMetaInfo(2u, 2u, 1u, ETextureFormat::R8, true, {}, { 4u }), ResourceCacheFlag_DoNotCache, String()); + auto res2 = new TextureResource(EResourceType_Texture2D, TextureMetaInfo(2u, 2u, 1u, ETextureFormat::R8, true, {}, { 4u }), ResourceCacheFlag_DoNotCache, {}); res2->setResourceData(ResourceBlob{ 2 }, { 2u, 2u }); auto hash = res->getHash(); auto hash2 = res2->getHash(); @@ -250,7 +250,7 @@ TEST_F(AClientApplicationLogicWithRealComponents, keepsAlsoOldResourcesAliveForN auto hashUsage = logic.getHashUsage(hash); // use texture2d - auto stHandle = clientScene.allocateStreamTexture(WaylandIviSurfaceId{ 1u }, hash); + const auto dataSlotHandle = clientScene.allocateDataSlot({ EDataSlotType_TextureProvider, DataSlotId(0u), {}, {}, hash, {} }); sceneComp.handleSubscribeScene(sceneId, renderer1); EXPECT_TRUE(logic.flush(sceneId, {}, {})); @@ -260,12 +260,12 @@ TEST_F(AClientApplicationLogicWithRealComponents, keepsAlsoOldResourcesAliveForN auto hashUsage2 = logic.getHashUsage(hash2); // use texture2d - clientScene.allocateStreamTexture(WaylandIviSurfaceId{ 2u }, hash2); + clientScene.allocateDataSlot({ EDataSlotType_TextureProvider, DataSlotId(1u), {}, {}, hash2, {} }); EXPECT_TRUE(logic.flush(sceneId, {}, {})); - // release first stream texture along with its texture2d (by leaving scope) - clientScene.releaseStreamTexture(stHandle); + // release first data slot along with its texture2d (by leaving scope) + clientScene.releaseDataSlot(dataSlotHandle); } EXPECT_EQ(resComp.getResource(hash).get(), res); diff --git a/client/ramses-client/test/ClientCommands/SceneCommandBufferTest.cpp b/client/test/ClientCommands/SceneCommandBufferTest.cpp similarity index 86% rename from client/ramses-client/test/ClientCommands/SceneCommandBufferTest.cpp rename to client/test/ClientCommands/SceneCommandBufferTest.cpp index 8a1f49d8b..ec76d7056 100644 --- a/client/ramses-client/test/ClientCommands/SceneCommandBufferTest.cpp +++ b/client/test/ClientCommands/SceneCommandBufferTest.cpp @@ -20,10 +20,6 @@ namespace ramses_internal class MockSceneCommandVisitor { public: - void operator()(const SceneCommandForceFallback& cmd) - { - handleSceneCommandForceFallback(cmd); - } void operator()(const SceneCommandFlushSceneVersion& cmd) { handleSceneCommandFlushSceneVersion(cmd); @@ -41,7 +37,6 @@ namespace ramses_internal handleSceneCommandLogResourceMemoryUsage(cmd); } - MOCK_METHOD(void, handleSceneCommandForceFallback, (const SceneCommandForceFallback&)); MOCK_METHOD(void, handleSceneCommandFlushSceneVersion, (const SceneCommandFlushSceneVersion&)); MOCK_METHOD(void, handleSceneCommandValidationRequest, (const SceneCommandValidationRequest&)); MOCK_METHOD(void, handleSceneCommandDumpSceneToFile, (const SceneCommandDumpSceneToFile&), (const)); @@ -51,11 +46,6 @@ namespace ramses_internal } // local comparison operators - static bool operator==(const SceneCommandForceFallback& a, const SceneCommandForceFallback& b) - { - return a.streamTextureName == b.streamTextureName && a.forceFallback == b.forceFallback; - } - static bool operator==(const SceneCommandFlushSceneVersion& a, const SceneCommandFlushSceneVersion& b) { return a.sceneVersion == b.sceneVersion; @@ -87,18 +77,13 @@ namespace ramses_internal TEST_F(ASceneCommandBuffer, canUseAllCommands) { InSequence seq; - { - SceneCommandForceFallback cmd{"foo", true}; - buffer.enqueueCommand(cmd); - EXPECT_CALL(visitor, handleSceneCommandForceFallback(cmd)); - } { SceneCommandFlushSceneVersion cmd{12345u}; buffer.enqueueCommand(cmd); EXPECT_CALL(visitor, handleSceneCommandFlushSceneVersion(cmd)); } { - SceneCommandValidationRequest cmd{ramses::EValidationSeverity_Error, "bar"}; + SceneCommandValidationRequest cmd{ramses::EValidationSeverity::Error, "bar"}; buffer.enqueueCommand(cmd); EXPECT_CALL(visitor, handleSceneCommandValidationRequest(cmd)); } @@ -138,14 +123,13 @@ namespace ramses_internal std::thread t1([&]() { setupDoneBarrier.wait(); - buffer.enqueueCommand(SceneCommandForceFallback{"foo", true}); + buffer.enqueueCommand(SceneCommandValidationRequest{ramses::EValidationSeverity::Error, "bar"}); buffer.enqueueCommand(SceneCommandFlushSceneVersion{12345u}); writersDoneBarrier.wait(); allDone.wait(); }); std::thread t2([&]() { setupDoneBarrier.wait(); - buffer.enqueueCommand(SceneCommandValidationRequest{ramses::EValidationSeverity_Error, "bar"}); buffer.enqueueCommand(SceneCommandDumpSceneToFile{"somename", false}); buffer.enqueueCommand(SceneCommandLogResourceMemoryUsage{}); writersDoneBarrier.wait(); @@ -154,9 +138,8 @@ namespace ramses_internal std::thread t3([&]() { Sequence seqT1; Sequence seqT2; - EXPECT_CALL(visitor, handleSceneCommandForceFallback(SceneCommandForceFallback{"foo", true})).InSequence(seqT1); + EXPECT_CALL(visitor, handleSceneCommandValidationRequest(SceneCommandValidationRequest{ramses::EValidationSeverity::Error, "bar"})).InSequence(seqT1); EXPECT_CALL(visitor, handleSceneCommandFlushSceneVersion(SceneCommandFlushSceneVersion{12345u})).InSequence(seqT1); - EXPECT_CALL(visitor, handleSceneCommandValidationRequest(SceneCommandValidationRequest{ramses::EValidationSeverity_Error, "bar"})).InSequence(seqT2); EXPECT_CALL(visitor, handleSceneCommandDumpSceneToFile(SceneCommandDumpSceneToFile{"somename", false})).InSequence(seqT2); EXPECT_CALL(visitor, handleSceneCommandLogResourceMemoryUsage(SceneCommandLogResourceMemoryUsage{})).InSequence(seqT2); setupDoneBarrier.wait(); diff --git a/client/ramses-client/test/ClientEventHandlerMock.cpp b/client/test/ClientEventHandlerMock.cpp similarity index 100% rename from client/ramses-client/test/ClientEventHandlerMock.cpp rename to client/test/ClientEventHandlerMock.cpp diff --git a/client/ramses-client/test/ClientEventHandlerMock.h b/client/test/ClientEventHandlerMock.h similarity index 83% rename from client/ramses-client/test/ClientEventHandlerMock.h rename to client/test/ClientEventHandlerMock.h index 1b3098296..d04b79847 100644 --- a/client/ramses-client/test/ClientEventHandlerMock.h +++ b/client/test/ClientEventHandlerMock.h @@ -13,16 +13,18 @@ #include "ramses-client-api/IClientEventHandler.h" #include "ramses-client-api/SceneReference.h" +#include + namespace ramses { class ClientEventHandlerMock : public IClientEventHandler { public: ClientEventHandlerMock(); - virtual ~ClientEventHandlerMock() override; + ~ClientEventHandlerMock() override; - MOCK_METHOD(void, sceneFileLoadFailed, (const char* filename), (override)); - MOCK_METHOD(void, sceneFileLoadSucceeded, (const char* filename, Scene* loadedScene), (override)); + MOCK_METHOD(void, sceneFileLoadFailed, (std::string_view filename), (override)); + MOCK_METHOD(void, sceneFileLoadSucceeded, (std::string_view filename, Scene* loadedScene), (override)); MOCK_METHOD(void, sceneReferenceStateChanged, (SceneReference& sceneRef, RendererSceneState state), (override)); MOCK_METHOD(void, sceneReferenceFlushed, (SceneReference& sceneRef, sceneVersionTag_t versionTag), (override)); MOCK_METHOD(void, dataLinked, (sceneId_t providerScene, dataProviderId_t providerId, sceneId_t consumerScene, dataConsumerId_t consumerId, bool success), (override)); diff --git a/client/ramses-client/test/ClientTestUtils.h b/client/test/ClientTestUtils.h similarity index 74% rename from client/ramses-client/test/ClientTestUtils.h rename to client/test/ClientTestUtils.h index d44807ed9..01f7cbec7 100644 --- a/client/ramses-client/test/ClientTestUtils.h +++ b/client/test/ClientTestUtils.h @@ -11,11 +11,11 @@ #include "ramses-client-api/RamsesClient.h" #include "ramses-client-api/Scene.h" -#include "ramses-client-api/AnimationSystem.h" #include "CreationHelper.h" #include "MockActionCollector.h" #include "RamsesFrameworkImpl.h" +#include "RamsesFrameworkConfigImpl.h" #include "MeshNodeImpl.h" #include "SceneImpl.h" #include "Scene/ClientScene.h" @@ -27,6 +27,8 @@ #include "ramses-client-api/EffectDescription.h" #include "TestEffects.h" +#include + namespace ramses_internal { class ClientScene; @@ -34,25 +36,21 @@ namespace ramses_internal namespace ramses { - //disabling the periodic logs is important as otherwise race conditions can occur in the tests that check - //that the statistic counters are updated (periodic logger resets the counters) - static const char* clientArgs[] = { "LocalTestClient", "-l", "0", "-fakeConnection", "-disablePeriodicLogs" }; - class LocalTestClient { public: LocalTestClient() - : framework(sizeof(clientArgs) / sizeof(char*), clientArgs) + : framework{ GetDefaultFrameworkConfig() } , client(*framework.createClient("localTestClient")) - , m_creationHelper(nullptr, nullptr, &client) + , m_creationHelper(nullptr, &client) , sceneActionsCollector() { - sceneActionsCollector.init(framework.impl.getScenegraphComponent()); - framework.impl.getScenegraphComponent().setSceneRendererHandler(&sceneActionsCollector); + sceneActionsCollector.init(framework.m_impl.getScenegraphComponent()); + framework.m_impl.getScenegraphComponent().setSceneRendererHandler(&sceneActionsCollector); } template - ObjectType& createObject(const char* name = nullptr) + ObjectType& createObject(std::string_view name = {}) { return *m_creationHelper.createObjectOfType(name); } @@ -67,6 +65,17 @@ namespace ramses return framework; } + static const RamsesFrameworkConfig& GetDefaultFrameworkConfig() + { + //disabling the periodic logs is important as otherwise race conditions can occur in the tests that check + //that the statistic counters are updated (periodic logger resets the counters) + static RamsesFrameworkConfig config{EFeatureLevel_Latest}; + config.setLogLevel(ramses::ELogLevel::Off); + config.setConnectionSystem(ramses::EConnectionSystem::Off); + config.setPeriodicLogInterval(std::chrono::seconds(0)); + return config; + } + protected: RamsesFramework framework; RamsesClient& client; @@ -81,7 +90,7 @@ namespace ramses : LocalTestClient() , m_scene(*client.createScene(sceneId_t(123u), sceneConfig)) , m_constRefToScene(m_scene) - , m_internalScene(m_scene.impl.getIScene()) + , m_internalScene(m_scene.m_impl.getIScene()) { m_creationHelper.setScene(&m_scene); } @@ -145,25 +154,6 @@ namespace ramses ramses_internal::ClientScene& m_internalScene; }; - class LocalTestClientWithSceneAndAnimationSystem : public LocalTestClientWithScene - { - protected: - explicit LocalTestClientWithSceneAndAnimationSystem(uint32_t animationSystemCreationFlags = EAnimationSystemFlags_Default) - : LocalTestClientWithScene() - , animationSystem(*m_scene.createAnimationSystem(animationSystemCreationFlags, "animation system")) - { - m_creationHelper.setAnimationSystem(&animationSystem); - } - - ~LocalTestClientWithSceneAndAnimationSystem() - { - m_creationHelper.destroyAdditionalAllocatedAnimationSystemObjects(); - m_scene.destroy(animationSystem); - } - - AnimationSystem& animationSystem; - }; - class ClientTestUtils { public: @@ -180,8 +170,8 @@ namespace ramses return false; } - UInt length1 = 0u; - UInt length2 = 0u; + size_t length1 = 0u; + size_t length2 = 0u; EXPECT_TRUE(file1.getSizeInBytes(length1)); EXPECT_TRUE(file2.getSizeInBytes(length2)); if (length1 != length2) @@ -189,16 +179,16 @@ namespace ramses return false; } - const UInt bufferLength = 1024 * 1024; - std::vector buf1(bufferLength); - std::vector buf2(bufferLength); + const size_t bufferLength = 1024 * 1024; + std::vector buf1(bufferLength); + std::vector buf2(bufferLength); - UInt offset = 0; - UInt dummy = 0; + size_t offset = 0; + size_t dummy = 0; bool equal = true; while (offset < length1 && equal) { - UInt left = std::min(length1 - offset, bufferLength); + size_t left = std::min(length1 - offset, bufferLength); EXPECT_EQ(EStatus::Ok, file1.read(buf1.data(), left, dummy)); EXPECT_EQ(EStatus::Ok, file2.read(buf2.data(), left, dummy)); equal = (PlatformMemory::Compare(buf1.data(), buf2.data(), left) == 0); diff --git a/client/test/CreationHelper.cpp b/client/test/CreationHelper.cpp new file mode 100644 index 000000000..19e6a8285 --- /dev/null +++ b/client/test/CreationHelper.cpp @@ -0,0 +1,222 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2015 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "gtest/gtest.h" +#include "CreationHelper.h" +#include "TestEffects.h" +#include "ramses-client-api/Node.h" +#include "ramses-client-api/Scene.h" +#include "ramses-client-api/RamsesClient.h" +#include "ramses-client-api/Effect.h" +#include "ramses-client-api/EffectDescription.h" +#include "ramses-client-api/Texture2D.h" +#include "ramses-client-api/ArrayResource.h" +#include "ramses-client-api/RenderBuffer.h" +#include "ramses-client-api/RenderTarget.h" +#include "ramses-client-api/EffectInputSemantic.h" +#include "ramses-client-api/AttributeInput.h" +#include "ramses-client-api/RenderGroup.h" +#include "ramses-client-api/DataObject.h" +#include "ramses-client-api/RenderTargetDescription.h" +#include "ramses-client-api/BlitPass.h" +#include "ramses-client-api/PickableObject.h" +#include "ramses-client-api/Camera.h" +#include "ramses-client-api/ArrayBuffer.h" +#include "ramses-client-api/PerspectiveCamera.h" + +namespace ramses +{ + CreationHelper::CreationHelper(Scene* scene, RamsesClient* ramsesClient) + : m_scene(scene) + , m_ramsesClient(ramsesClient) + { + } + + CreationHelper::~CreationHelper() + { + for(const auto& component : m_allocatedClientAndFrameworkComponents) + { + component.second->destroyClient(*component.first); + delete component.second; + } + + if (m_scene != nullptr) + { + destroyAdditionalAllocatedSceneObjects(); + } + } + + void CreationHelper::setScene(Scene* scene) + { + m_scene = scene; + } + + void CreationHelper::destroyAdditionalAllocatedSceneObjects() + { + assert(m_scene != nullptr); + for(const auto& obj : m_additionalAllocatedSceneObjects) + { + ASSERT_TRUE(m_scene->destroy(*obj) == StatusOK); + } + m_additionalAllocatedSceneObjects.clear(); + } + + size_t CreationHelper::getAdditionalAllocatedNodeCount() const + { + size_t nodeCount = 0; + for (auto obj : m_additionalAllocatedSceneObjects) + { + if (obj->isOfType(ERamsesObjectType::Node)) + ++nodeCount; + } + return nodeCount; + } + + template <> RamsesClient* CreationHelper::createObjectOfType(std::string_view name) + { + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework* framework = new RamsesFramework(config); + RamsesClient* allocatedClient = framework->createClient(name); + if (allocatedClient) + m_allocatedClientAndFrameworkComponents.push_back(ClientAndFramework(allocatedClient, framework)); + return allocatedClient; + } + template <> Scene* CreationHelper::createObjectOfType(std::string_view name) + { + return m_ramsesClient->createScene(sceneId_t(999u), ramses::SceneConfig(), name); + } + template <> Node* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createNode(name); + } + template <> MeshNode* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createMeshNode(name); + } + template <> PerspectiveCamera* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createPerspectiveCamera(name); + } + template <> OrthographicCamera* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createOrthographicCamera(name); + } + template <> Effect* CreationHelper::createObjectOfType(std::string_view name) + { + EffectDescription effectDescription; + effectDescription.setVertexShader("void main(void) {gl_Position=vec4(0);}"); + effectDescription.setFragmentShader("void main(void) {gl_FragColor=vec4(1);}"); + return m_scene->createEffect(effectDescription, ResourceCacheFlag_DoNotCache, name); + } + template <> Appearance* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createAppearance(*TestEffects::CreateTestEffect(*m_scene, "appearance effect"), name); + } + template <> Texture2D* CreationHelper::createObjectOfType(std::string_view name) + { + std::array data = { 0u }; + MipLevelData mipLevelData(static_cast(data.size()), data.data()); + return m_scene->createTexture2D(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, {}, ResourceCacheFlag_DoNotCache, name); + } + template <> Texture3D* CreationHelper::createObjectOfType(std::string_view name) + { + std::array data = { 0u }; + MipLevelData mipLevelData(static_cast(data.size()), data.data()); + return m_scene->createTexture3D(ETextureFormat::RGBA8, 1u, 2u, 4u, 1, &mipLevelData, false, ResourceCacheFlag_DoNotCache, name); + } + template <> TextureCube* CreationHelper::createObjectOfType(std::string_view name) + { + uint8_t data[4] = { 0u }; // NOLINT(modernize-avoid-c-arrays) + CubeMipLevelData mipLevelData(sizeof(data), data, data, data, data, data, data); + return m_scene->createTextureCube(ETextureFormat::RGBA8, 1u, 1, &mipLevelData, false, {}, ResourceCacheFlag_DoNotCache, name); + } + template <> ArrayResource* CreationHelper::createObjectOfType(std::string_view name) + { + const uint16_t data = 0u; + return m_scene->createArrayResource(1u, &data, ResourceCacheFlag_DoNotCache, name); + } + + template <> RenderGroup* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createRenderGroup(name); + } + template <> RenderPass* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createRenderPass(name); + } + + template <> BlitPass* CreationHelper::createObjectOfType(std::string_view name) + { + const RenderBuffer* sourceRenderBuffer = createObjectOfType("src render buffer"); + const RenderBuffer* destinationRenderBuffer = createObjectOfType("dst render buffer"); + return m_scene->createBlitPass(*sourceRenderBuffer, *destinationRenderBuffer, name); + } + + template <> TextureSampler* CreationHelper::createObjectOfType(std::string_view name) + { + std::array data = { 0u }; + MipLevelData mipLevelData(static_cast(data.size()), data.data()); + Texture2D* texture = m_scene->createTexture2D(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, {}, ResourceCacheFlag_DoNotCache, "texture"); + return m_scene->createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Mirror, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Nearest, *texture, 1u, name); + } + template <> TextureSamplerMS* CreationHelper::createObjectOfType(std::string_view name) + { + RenderBuffer* renderBuffer = m_scene->createRenderBuffer(16, 16, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite, 4u, "renderBuffer"); + return m_scene->createTextureSamplerMS(*renderBuffer, name); + } + template <> RenderBuffer* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createRenderBuffer(16, 16, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite, 0u, name); + } + template <> RenderTarget* CreationHelper::createObjectOfType(std::string_view name) + { + RenderTargetDescription rtDesc; + rtDesc.addRenderBuffer(*createObjectOfType("rb")); + return m_scene->createRenderTarget(rtDesc, name); + } + + template <> GeometryBinding* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createGeometryBinding(*createObjectOfType("geometry_binding_effect"), name); + } + + template <> DataObject* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createDataObject(EDataType::Float, name); + } + + template <> ArrayBuffer* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createArrayBuffer(EDataType::UInt32, 13u, name); + + } + template <> Texture2DBuffer* CreationHelper::createObjectOfType(std::string_view name) + { + return m_scene->createTexture2DBuffer(ETextureFormat::RGBA8, 3, 4, 2, name); + } + + template <> PickableObject* CreationHelper::createObjectOfType(std::string_view name) + { + const auto vb = m_scene->createArrayBuffer(EDataType::Vector3F, 3u, "vb"); + m_additionalAllocatedSceneObjects.push_back(vb); + PerspectiveCamera* camera = m_scene->createPerspectiveCamera("pickableCamera"); + m_additionalAllocatedSceneObjects.push_back(camera); + camera->setFrustum(-1.4f, 1.4f, -1.4f, 1.4f, 1.f, 100.f); + camera->setViewport(0, 0, 200, 200); + PickableObject* pickableObject = m_scene->createPickableObject(*vb, pickableObjectId_t{ 123u }, name); + pickableObject->setCamera(*camera); + return pickableObject; + } + + template <> SceneReference* CreationHelper::createObjectOfType(std::string_view name) + { + // SceneReference cannot refer to same sceneID, create a different one every time + ++m_lastReferencedSceneId.getReference(); + return m_scene->createSceneReference(m_lastReferencedSceneId, name); + } +} diff --git a/client/test/CreationHelper.h b/client/test/CreationHelper.h new file mode 100644 index 000000000..8edd9d3f7 --- /dev/null +++ b/client/test/CreationHelper.h @@ -0,0 +1,110 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2015 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#ifndef RAMSES_CREATIONHELPER_H +#define RAMSES_CREATIONHELPER_H + +#include "ramses-framework-api/RamsesFrameworkTypes.h" +#include "PlatformAbstraction/PlatformTypes.h" +#include "Collections/Vector.h" +#include "Collections/Pair.h" + +#include +#include + +namespace ramses +{ + class RamsesObject; + class SceneObject; + class RamsesClient; + class RamsesFramework; + class TextureCubeInput; + class Scene; + class Node; + class MeshNode; + class PerspectiveCamera; + class OrthographicCamera; + class Effect; + class Appearance; + class Texture2D; + class Texture3D; + class TextureCube; + class ArrayResource; + class RenderGroup; + class RenderPass; + class BlitPass; + class TextureSampler; + class TextureSamplerMS; + class RenderBuffer; + class RenderTarget; + class RenderGroup; + class ArrayBuffer; + class Texture2DBuffer; + class GeometryBinding; + class DataObject; + class PickableObject; + class SceneReference; + + class CreationHelper + { + public: + CreationHelper(Scene* scene, RamsesClient* ramsesClient); + ~CreationHelper(); + + void setScene(Scene* scene); + + template + ObjectType* createObjectOfType(std::string_view name) + { + UNUSED(name); + assert(false); + return NULL; + } + + void destroyAdditionalAllocatedSceneObjects(); + [[nodiscard]] size_t getAdditionalAllocatedNodeCount() const; + + private: + Scene* m_scene; + RamsesClient* m_ramsesClient; + + using ClientAndFramework = std::pair; + using RamsesClientAndFrameworkComponentVector = std::vector; + RamsesClientAndFrameworkComponentVector m_allocatedClientAndFrameworkComponents; + std::vector m_additionalAllocatedSceneObjects; + sceneId_t m_lastReferencedSceneId{ 123u }; + }; + + template <> RamsesClient* CreationHelper::createObjectOfType(std::string_view name); + template <> Scene* CreationHelper::createObjectOfType(std::string_view name); + template <> Node* CreationHelper::createObjectOfType(std::string_view name); + template <> MeshNode* CreationHelper::createObjectOfType(std::string_view name); + template <> PerspectiveCamera* CreationHelper::createObjectOfType(std::string_view name); + template <> OrthographicCamera* CreationHelper::createObjectOfType(std::string_view name); + template <> Effect* CreationHelper::createObjectOfType(std::string_view name); + template <> Appearance* CreationHelper::createObjectOfType(std::string_view name); + template <> Texture2D* CreationHelper::createObjectOfType(std::string_view name); + template <> Texture3D* CreationHelper::createObjectOfType(std::string_view name); + template <> TextureCube* CreationHelper::createObjectOfType(std::string_view name); + template <> ArrayResource* CreationHelper::createObjectOfType(std::string_view name); + template <> RenderGroup* CreationHelper::createObjectOfType(std::string_view name); + template <> RenderPass* CreationHelper::createObjectOfType(std::string_view name); + template <> BlitPass* CreationHelper::createObjectOfType(std::string_view name); + template <> TextureSampler* CreationHelper::createObjectOfType(std::string_view name); + template <> TextureSamplerMS* CreationHelper::createObjectOfType(std::string_view name); + template <> RenderBuffer* CreationHelper::createObjectOfType(std::string_view name); + template <> RenderTarget* CreationHelper::createObjectOfType(std::string_view name); + template <> GeometryBinding* CreationHelper::createObjectOfType(std::string_view name); + template <> DataObject* CreationHelper::createObjectOfType(std::string_view name); + template <> ArrayBuffer* CreationHelper::createObjectOfType(std::string_view name); + template <> Texture2DBuffer* CreationHelper::createObjectOfType(std::string_view name); + template <> PickableObject* CreationHelper::createObjectOfType(std::string_view name); + template <> SceneReference* CreationHelper::createObjectOfType(std::string_view name); +} + +#endif diff --git a/client/test/DataObjectTest.cpp b/client/test/DataObjectTest.cpp new file mode 100644 index 000000000..d35e9f222 --- /dev/null +++ b/client/test/DataObjectTest.cpp @@ -0,0 +1,117 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2016 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include + +#include "ClientTestUtils.h" +#include "ramses-client-api/DataObject.h" +#include "ramses-utils.h" +#include "glm/gtx/range.hpp" + +using namespace testing; +using namespace ramses_internal; + +namespace ramses +{ + template + class ADataObject : public LocalTestClientWithScene, public testing::Test + { + }; + + template + T SomeValue(); + template <> int32_t SomeValue() { return 1; } + template <> float SomeValue() { return 2.f; } + template <> vec2f SomeValue() { return vec2f{ 1.f, 2.f }; } + template <> vec3f SomeValue() { return vec3f{ 1.f, 2.f, 3.f }; } + template <> vec4f SomeValue() { return vec4f{ 1.f, 2.f, 3.f, 4.f }; } + template <> vec2i SomeValue() { return vec2i{ 1, 2 }; } + template <> vec3i SomeValue() { return vec3i{ 1, 2, 3 }; } + template <> vec4i SomeValue() { return vec4i{ 1, 2, 3, 4 }; } + template <> matrix22f SomeValue() { matrix22f ret; std::iota(begin(ret), end(ret), 1.f); return ret; } + template <> matrix33f SomeValue() { matrix33f ret; std::iota(begin(ret), end(ret), 1.f); return ret; } + template <> matrix44f SomeValue() { matrix44f ret; std::iota(begin(ret), end(ret), 1.f); return ret; } + + using DataTypes = ::testing::Types< + int32_t, + float, + vec2f, + vec3f, + vec4f, + vec2i, + vec3i, + vec4i, + matrix22f, + matrix33f, + matrix44f + >; + TYPED_TEST_SUITE(ADataObject, DataTypes); + + TYPED_TEST(ADataObject, canGetDataType) + { + const auto dataObject = this->m_scene.createDataObject(GetEDataType(), "data"); + ASSERT_TRUE(dataObject); + EXPECT_EQ(GetEDataType(), dataObject->getDataType()); + } + + TYPED_TEST(ADataObject, hasDefaultValueAfterCreation) + { + const auto dataObject = this->m_scene.createDataObject(GetEDataType(), "data"); + ASSERT_TRUE(dataObject); + + TypeParam getVal; + EXPECT_EQ(StatusOK, dataObject->getValue(getVal)); + EXPECT_EQ(TypeParam{}, getVal); + } + + TYPED_TEST(ADataObject, canSetAndGetAValue) + { + const auto dataObject = this->m_scene.createDataObject(GetEDataType(), "data"); + ASSERT_TRUE(dataObject); + + EXPECT_EQ(StatusOK, dataObject->setValue(SomeValue())); + + TypeParam getVal; + EXPECT_EQ(StatusOK, dataObject->getValue(getVal)); + EXPECT_EQ(SomeValue(), getVal); + } + + TYPED_TEST(ADataObject, failsToSetAndGetAValueIfUsingWrongDataType) + { + const auto dataObject = this->m_scene.createDataObject(GetEDataType(), "data"); + ASSERT_TRUE(dataObject); + + if constexpr (std::is_same_v) + { + EXPECT_NE(StatusOK, dataObject->setValue(SomeValue())); + float getVal; + EXPECT_NE(StatusOK, dataObject->getValue(getVal)); + } + else + { + EXPECT_NE(StatusOK, dataObject->setValue(SomeValue())); + int32_t getVal; + EXPECT_NE(StatusOK, dataObject->getValue(getVal)); + } + } + + TEST(ADataObject, failsToCreateIfUsingWrongDataType) + { + RamsesFramework framework{ RamsesFrameworkConfig{EFeatureLevel_Latest} }; + auto client = framework.createClient("test"); + auto scene = client->createScene(sceneId_t{ 123 }); + EXPECT_EQ(nullptr, scene->createDataObject(EDataType::UInt16)); + EXPECT_EQ(nullptr, scene->createDataObject(EDataType::UInt32)); + EXPECT_EQ(nullptr, scene->createDataObject(EDataType::ByteBlob)); + EXPECT_EQ(nullptr, scene->createDataObject(EDataType::TextureSampler2D)); + EXPECT_EQ(nullptr, scene->createDataObject(EDataType::TextureSampler2DMS)); + EXPECT_EQ(nullptr, scene->createDataObject(EDataType::TextureSampler3D)); + EXPECT_EQ(nullptr, scene->createDataObject(EDataType::TextureSamplerCube)); + EXPECT_EQ(nullptr, scene->createDataObject(EDataType::TextureSamplerExternal)); + } +} diff --git a/client/ramses-client/test/EffectDescriptionTest.cpp b/client/test/EffectDescriptionTest.cpp similarity index 71% rename from client/ramses-client/test/EffectDescriptionTest.cpp rename to client/test/EffectDescriptionTest.cpp index 6176b7fee..d7f71df14 100644 --- a/client/ramses-client/test/EffectDescriptionTest.cpp +++ b/client/test/EffectDescriptionTest.cpp @@ -11,6 +11,8 @@ #include "EffectDescriptionImpl.h" #include "EffectInputSemanticUtils.h" +#include + using namespace testing; namespace ramses @@ -19,9 +21,9 @@ namespace ramses { public: protected: - EEffectUniformSemantic getSemanticForUniform(const char* inputName) + EEffectUniformSemantic getSemanticForUniform(std::string_view inputName) { - ramses_internal::EFixedSemantics* internalSemantic = effectDesc.impl.getSemanticsMap().get(inputName); + ramses_internal::EFixedSemantics* internalSemantic = effectDesc.m_impl.get().getSemanticsMap().get(std::string{inputName}); if (internalSemantic == nullptr) { return EEffectUniformSemantic::Invalid; @@ -30,9 +32,9 @@ namespace ramses return EffectInputSemanticUtils::GetEffectUniformSemanticFromInternal(*internalSemantic); } - EEffectAttributeSemantic getSemanticForAttribute(const char* inputName) + EEffectAttributeSemantic getSemanticForAttribute(std::string_view inputName) { - ramses_internal::EFixedSemantics* internalSemantic = effectDesc.impl.getSemanticsMap().get(inputName); + ramses_internal::EFixedSemantics* internalSemantic = effectDesc.m_impl.get().getSemanticsMap().get(std::string{inputName}); if (internalSemantic == nullptr) { return EEffectAttributeSemantic::Invalid; @@ -50,16 +52,16 @@ namespace ramses EXPECT_STREQ("", effectDesc.getFragmentShader()); EXPECT_STREQ("", effectDesc.getGeometryShader()); EXPECT_EQ(0u, effectDesc.getNumberOfCompilerDefines()); - EXPECT_EQ(0u, effectDesc.impl.getSemanticsMap().size()); + EXPECT_EQ(0u, effectDesc.m_impl.get().getSemanticsMap().size()); } TEST_F(EffectDescriptionTest, setShaderWithNULLRetrievesEmptyString) { - EXPECT_EQ(StatusOK, effectDesc.setVertexShader(nullptr)); + EXPECT_EQ(StatusOK, effectDesc.setVertexShader({})); EXPECT_STREQ("", effectDesc.getVertexShader()); - EXPECT_EQ(StatusOK, effectDesc.setFragmentShader(nullptr)); + EXPECT_EQ(StatusOK, effectDesc.setFragmentShader({})); EXPECT_STREQ("", effectDesc.getFragmentShader()); - EXPECT_EQ(StatusOK, effectDesc.setGeometryShader(nullptr)); + EXPECT_EQ(StatusOK, effectDesc.setGeometryShader({})); EXPECT_STREQ("", effectDesc.getGeometryShader()); } @@ -98,7 +100,7 @@ namespace ramses TEST_F(EffectDescriptionTest, addDefineAsNULLReportsError) { - EXPECT_NE(StatusOK, effectDesc.addCompilerDefine(nullptr)); + EXPECT_NE(StatusOK, effectDesc.addCompilerDefine({})); EXPECT_EQ(nullptr, effectDesc.getCompilerDefine(0u)); EXPECT_EQ(0u, effectDesc.getNumberOfCompilerDefines()); } @@ -120,8 +122,8 @@ namespace ramses TEST_F(EffectDescriptionTest, addSemanticNameAsNULLReportsError) { - EXPECT_NE(StatusOK, effectDesc.setUniformSemantic(nullptr, EEffectUniformSemantic::ViewMatrix)); - EXPECT_EQ(0u, effectDesc.impl.getSemanticsMap().size()); + EXPECT_NE(StatusOK, effectDesc.setUniformSemantic({}, EEffectUniformSemantic::ViewMatrix)); + EXPECT_EQ(0u, effectDesc.m_impl.get().getSemanticsMap().size()); } TEST_F(EffectDescriptionTest, canAddAndRetrieveSemanticInput) @@ -135,18 +137,18 @@ namespace ramses EXPECT_EQ(StatusOK, effectDesc.setUniformSemantic(semantic1, semanticType1)); EXPECT_EQ(semanticType1, getSemanticForUniform(semantic1)); - EXPECT_EQ(1u, effectDesc.impl.getSemanticsMap().size()); + EXPECT_EQ(1u, effectDesc.m_impl.get().getSemanticsMap().size()); EXPECT_EQ(StatusOK, effectDesc.setUniformSemantic(semantic2, semanticType2)); EXPECT_EQ(semanticType1, getSemanticForUniform(semantic1)); EXPECT_EQ(semanticType2, getSemanticForUniform(semantic2)); - EXPECT_EQ(2u, effectDesc.impl.getSemanticsMap().size()); + EXPECT_EQ(2u, effectDesc.m_impl.get().getSemanticsMap().size()); EXPECT_EQ(StatusOK, effectDesc.setAttributeSemantic(semantic3, semanticType3)); EXPECT_EQ(semanticType1, getSemanticForUniform(semantic1)); EXPECT_EQ(semanticType2, getSemanticForUniform(semantic2)); EXPECT_EQ(semanticType3, getSemanticForAttribute(semantic3)); - EXPECT_EQ(3u, effectDesc.impl.getSemanticsMap().size()); + EXPECT_EQ(3u, effectDesc.m_impl.get().getSemanticsMap().size()); } TEST_F(EffectDescriptionTest, retrievesUnknownTypeForSemanticNotAdded) @@ -175,12 +177,55 @@ namespace ramses TEST_F(EffectDescriptionTest, reportsErrorWhenLoadingNonexistingFile) { - const char* fileName = "nofile%&"; + const auto fileName = "nofile%&"; EXPECT_NE(StatusOK, effectDesc.setVertexShaderFromFile(fileName)); EXPECT_NE(StatusOK, effectDesc.setFragmentShaderFromFile(fileName)); EXPECT_NE(StatusOK, effectDesc.setFragmentShaderFromFile(fileName)); - EXPECT_NE(StatusOK, effectDesc.setVertexShaderFromFile(nullptr)); - EXPECT_NE(StatusOK, effectDesc.setFragmentShaderFromFile(nullptr)); - EXPECT_NE(StatusOK, effectDesc.setFragmentShaderFromFile(nullptr)); + EXPECT_NE(StatusOK, effectDesc.setVertexShaderFromFile({})); + EXPECT_NE(StatusOK, effectDesc.setFragmentShaderFromFile({})); + EXPECT_NE(StatusOK, effectDesc.setFragmentShaderFromFile({})); + } + + TEST_F(EffectDescriptionTest, CanBeCopyAndMoveConstructed) + { + ASSERT_EQ(StatusOK, effectDesc.setVertexShader("test")); + + EffectDescription effectDescCopy{ effectDesc }; + EXPECT_STREQ("test", effectDescCopy.getVertexShader()); + + EffectDescription effectDescMove{ std::move(effectDesc) }; + EXPECT_STREQ("test", effectDescMove.getVertexShader()); + } + + TEST_F(EffectDescriptionTest, CanBeCopyAndMoveAssigned) + { + ASSERT_EQ(StatusOK, effectDesc.setVertexShader("test")); + + EffectDescription effectDescCopy; + effectDescCopy = effectDesc; + EXPECT_STREQ("test", effectDescCopy.getVertexShader()); + + EffectDescription effectDescMove; + effectDescMove = std::move(effectDesc); + EXPECT_STREQ("test", effectDescMove.getVertexShader()); + } + + TEST_F(EffectDescriptionTest, CanBeSelfAssigned) + { + ASSERT_EQ(StatusOK, effectDesc.setVertexShader("test")); + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wself-move" +#pragma clang diagnostic ignored "-Wself-assign-overloaded" +#endif + effectDesc = effectDesc; + EXPECT_STREQ("test", effectDesc.getVertexShader()); + effectDesc = std::move(effectDesc); + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_STREQ("test", effectDesc.getVertexShader()); +#ifdef __clang__ +#pragma clang diagnostic pop +#endif } } diff --git a/client/test/EffectInputTest.cpp b/client/test/EffectInputTest.cpp new file mode 100644 index 000000000..b5c5878fa --- /dev/null +++ b/client/test/EffectInputTest.cpp @@ -0,0 +1,369 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2015 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include + +#include "ramses-client-api/UniformInput.h" +#include "ramses-client-api/AttributeInput.h" +#include "EffectInputImpl.h" +#include "SceneAPI/IScene.h" +#include "Utils/File.h" +#include "Utils/BinaryFileOutputStream.h" +#include "Utils/BinaryFileInputStream.h" + +using namespace testing; + +namespace ramses +{ + class AnEffectInput : public ::testing::Test + { + protected: + void initializeInput(EffectInput& input) + { + constexpr ramses_internal::ResourceContentHash effectHash{ 1u, 0u }; + constexpr ramses_internal::EFixedSemantics semantics = ramses_internal::EFixedSemantics::ModelMatrix; + input.m_impl.get().initialize(effectHash, "test", ramses_internal::EDataType::Matrix44F, semantics, 1u, 66u); + } + + void checkInput(const EffectInput& input) + { + AttributeInput compInput; + initializeInput(compInput); + + EXPECT_STREQ(compInput.getName(), input.getName()); + EXPECT_EQ(compInput.getDataType(), input.getDataType()); + EXPECT_EQ(compInput.m_impl.get().getSemantics(), input.m_impl.get().getSemantics()); + EXPECT_EQ(compInput.m_impl.get().getElementCount(), input.m_impl.get().getElementCount()); + EXPECT_EQ(compInput.m_impl.get().getInputIndex(), input.m_impl.get().getInputIndex()); + EXPECT_EQ(compInput.m_impl.get().getEffectHash(), input.m_impl.get().getEffectHash()); + } + }; + + TEST_F(AnEffectInput, UniformInputIsInitializedToDefaultUponCreation) + { + UniformInput input; + EXPECT_STREQ("", input.getName()); + EXPECT_EQ(0u, input.getElementCount()); + EXPECT_EQ(ramses_internal::ResourceContentHash::Invalid(), input.m_impl.get().getEffectHash()); + EXPECT_FALSE(input.getDataType()); + EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.m_impl.get().getSemantics()); + EXPECT_EQ(EEffectUniformSemantic::Invalid, input.getSemantics()); + EXPECT_EQ(static_cast(-1), input.m_impl.get().getInputIndex()); + EXPECT_FALSE(input.isValid()); + } + + TEST_F(AnEffectInput, AttributeInputIsInitializedToDefaultUponCreation) + { + AttributeInput input; + EXPECT_STREQ("", input.getName()); + EXPECT_EQ(ramses_internal::ResourceContentHash::Invalid(), input.m_impl.get().getEffectHash()); + EXPECT_FALSE(input.getDataType()); + EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.m_impl.get().getSemantics()); + EXPECT_EQ(static_cast(-1), input.m_impl.get().getInputIndex()); + EXPECT_FALSE(input.isValid()); + } + + TEST_F(AnEffectInput, UniformInputIsInitializedToGivenValues) + { + const ramses_internal::ResourceContentHash effectHash(1u, 0); + const std::string inputName("test"); + const ramses_internal::EDataType dataType = ramses_internal::EDataType::Int32; + const ramses_internal::EFixedSemantics semantics = ramses_internal::EFixedSemantics::ModelMatrix; + const size_t elementCount = 9u; + const size_t index = 66u; + + UniformInput input; + input.m_impl.get().initialize(effectHash, inputName, dataType, semantics, elementCount, index); + + EXPECT_STREQ(inputName.c_str(), input.getName()); + EXPECT_EQ(elementCount, input.getElementCount()); + EXPECT_EQ(EDataType::Int32, *input.getDataType()); + EXPECT_EQ(effectHash, input.m_impl.get().getEffectHash()); + EXPECT_EQ(dataType, input.m_impl.get().getInternalDataType()); + EXPECT_EQ(semantics, input.m_impl.get().getSemantics()); + EXPECT_EQ(EEffectUniformSemantic::ModelMatrix, input.getSemantics()); + EXPECT_EQ(index, input.m_impl.get().getInputIndex()); + EXPECT_TRUE(input.isValid()); + } + + TEST_F(AnEffectInput, AttributeInputIsInitializedToGivenValues) + { + const ramses_internal::ResourceContentHash effectHash(1u, 0); + const std::string inputName("test"); + const ramses_internal::EDataType dataType = ramses_internal::EDataType::Vector2Buffer; + const ramses_internal::EFixedSemantics semantics = ramses_internal::EFixedSemantics::TextPositionsAttribute; + const size_t index = 66u; + + AttributeInput input; + input.m_impl.get().initialize(effectHash, inputName, dataType, semantics, 1u, index); + + EXPECT_STREQ(inputName.c_str(), input.getName()); + EXPECT_EQ(EDataType::Vector2F, *input.getDataType()); + EXPECT_EQ(effectHash, input.m_impl.get().getEffectHash()); + EXPECT_EQ(dataType, input.m_impl.get().getInternalDataType()); + EXPECT_EQ(semantics, input.m_impl.get().getSemantics()); + EXPECT_EQ(index, input.m_impl.get().getInputIndex()); + EXPECT_TRUE(input.isValid()); + } + + TEST_F(AnEffectInput, CanBeSerialized) + { + const ramses_internal::ResourceContentHash effectHash(1u, 0); + const std::string inputName("test"); + const ramses_internal::EDataType dataType = ramses_internal::EDataType::Vector2Buffer; + const ramses_internal::EFixedSemantics semantics = ramses_internal::EFixedSemantics::TextTextureCoordinatesAttribute; + const size_t index = 66u; + + EffectInputImpl input; + input.initialize(effectHash, inputName, dataType, semantics, 1u, index); + + const std::string fileName("someTemporaryFile.ram"); + + ramses_internal::File outputFile(fileName); + ramses_internal::BinaryFileOutputStream outputStream(outputFile); + assert(outputFile.isOpen()); + + input.serialize(outputStream); + + outputFile.close(); + + ramses_internal::File inputFile(fileName); + ramses_internal::BinaryFileInputStream inputStream(inputFile); + assert(inputFile.isOpen()); + + EffectInputImpl inputRead; + + inputRead.deserialize(inputStream); + + inputFile.close(); + + EXPECT_EQ(inputName, inputRead.getName()); + EXPECT_EQ(input.getDataType(), inputRead.getDataType()); + EXPECT_EQ(effectHash, inputRead.getEffectHash()); + EXPECT_EQ(dataType, inputRead.getInternalDataType()); + EXPECT_EQ(semantics, inputRead.getSemantics()); + EXPECT_EQ(index, inputRead.getInputIndex()); + EXPECT_TRUE(inputRead.isValid()); + } + + TEST_F(AnEffectInput, CanBeComparedForEquality) + { + const ramses_internal::ResourceContentHash effectHash(1u, 0); + const std::string inputName("test"); + const ramses_internal::EDataType dataType = ramses_internal::EDataType::Vector2Buffer; + const ramses_internal::EFixedSemantics semantics = ramses_internal::EFixedSemantics::TextPositionsAttribute; + const size_t index = 66u; + + EffectInputImpl input1; + input1.initialize(effectHash, inputName, dataType, semantics, 1u, index); + + { + EffectInputImpl input2; + input2.initialize(effectHash, inputName, dataType, semantics, 1u, index); + EXPECT_TRUE(input1 == input2); + EXPECT_FALSE(input1 != input2); + } + + { + EffectInputImpl input2; + input2.initialize(ramses_internal::ResourceContentHash(2u, 0), inputName, dataType, semantics, 1u, index); + EXPECT_FALSE(input1 == input2); + EXPECT_TRUE(input1 != input2); + } + + { + EffectInputImpl input2; + input2.initialize(effectHash, "test2", dataType, semantics, 1u, index); + EXPECT_FALSE(input1 == input2); + EXPECT_TRUE(input1 != input2); + } + + { + EffectInputImpl input2; + input2.initialize(effectHash, inputName, ramses_internal::EDataType::Vector3Buffer, semantics, 1u, index); + EXPECT_FALSE(input1 == input2); + EXPECT_TRUE(input1 != input2); + } + + { + EffectInputImpl input2; + input2.initialize(effectHash, inputName, dataType, ramses_internal::EFixedSemantics::TextTextureCoordinatesAttribute, 1u, index); + EXPECT_FALSE(input1 == input2); + EXPECT_TRUE(input1 != input2); + } + + { + EffectInputImpl input2; + input2.initialize(effectHash, inputName, dataType, semantics, 2u, index); + EXPECT_FALSE(input1 == input2); + EXPECT_TRUE(input1 != input2); + } + + { + EffectInputImpl input2; + input2.initialize(effectHash, inputName, dataType, semantics, 1u, 67u); + EXPECT_FALSE(input1 == input2); + EXPECT_TRUE(input1 != input2); + } + } + + TEST_F(AnEffectInput, ReturnsCorrectDataType) + { + const ramses_internal::ResourceContentHash effectHash(1u, 0); + const std::string inputName("test"); + const ramses_internal::EFixedSemantics semantics = ramses_internal::EFixedSemantics::ModelMatrix; + const size_t index = 66u; + UniformInput input; + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::UInt16, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::UInt16); + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::UInt32, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::UInt32); + + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::Int32, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::Int32); + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::Vector2I, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::Vector2I); + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::Vector3I, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::Vector3I); + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::Vector4I, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::Vector4I); + + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::Float, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::Float); + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::Vector2F, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::Vector2F); + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::Vector3F, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::Vector3F); + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::Vector4F, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::Vector4F); + + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::Matrix22F, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::Matrix22F); + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::Matrix33F, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::Matrix33F); + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::Matrix44F, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::Matrix44F); + + + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::TextureSampler2D, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::TextureSampler2D); + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::TextureSampler2DMS, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::TextureSampler2DMS); + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::TextureSampler3D, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::TextureSampler3D); + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::TextureSamplerCube, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::TextureSamplerCube); + input.m_impl.get().initialize(effectHash, inputName, ramses_internal::EDataType::TextureSamplerExternal, semantics, 1u, index); + EXPECT_EQ(*input.getDataType(), EDataType::TextureSamplerExternal); + } + + TEST_F(AnEffectInput, CanBeCopyAndMoveConstructed_Uniform) + { + UniformInput input; + initializeInput(input); + + UniformInput inputCopy{ input }; + checkInput(inputCopy); + + UniformInput inputMove{ std::move(input) }; + checkInput(inputMove); + } + + TEST_F(AnEffectInput, CanBeCopyAndMoveAssigned_Uniform) + { + UniformInput input; + initializeInput(input); + + UniformInput inputCopy; + inputCopy = input; + checkInput(inputCopy); + + UniformInput inputMove; + inputMove = std::move(input); + checkInput(inputMove); + } + + TEST_F(AnEffectInput, CanBeSelfAssigned_Uniform) + { + UniformInput input; + initializeInput(input); + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wself-move" +#pragma clang diagnostic ignored "-Wself-assign-overloaded" +#endif + input = input; + checkInput(input); + input = std::move(input); + // NOLINTNEXTLINE(bugprone-use-after-move) + checkInput(input); +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + } + + TEST_F(AnEffectInput, CanBeCopyAndMoveConstructed_Attribute) + { + AttributeInput input; + initializeInput(input); + + AttributeInput inputCopy{ input }; + checkInput(inputCopy); + + AttributeInput inputMove{ std::move(input) }; + checkInput(inputMove); + } + + TEST_F(AnEffectInput, CanBeCopyAndMoveAssigned_Attribute) + { + AttributeInput input; + initializeInput(input); + + AttributeInput inputCopy; + inputCopy = input; + checkInput(inputCopy); + + AttributeInput inputMove; + inputMove = std::move(input); + checkInput(inputMove); + } + + TEST_F(AnEffectInput, CanBeSelfAssigned_Attribute) + { + AttributeInput input; + initializeInput(input); + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wself-move" +#pragma clang diagnostic ignored "-Wself-assign-overloaded" +#endif + input = input; + checkInput(input); + input = std::move(input); + // NOLINTNEXTLINE(bugprone-use-after-move) + checkInput(input); +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + } +} diff --git a/client/ramses-client/test/EffectTest.cpp b/client/test/EffectTest.cpp similarity index 77% rename from client/ramses-client/test/EffectTest.cpp rename to client/test/EffectTest.cpp index a3930e2cf..7c5915211 100644 --- a/client/ramses-client/test/EffectTest.cpp +++ b/client/test/EffectTest.cpp @@ -60,13 +60,13 @@ namespace ramses TEST_F(AnEffect, hasNoGeometryShaderWhenNotCreatedWithSuch) { - EXPECT_FALSE(Effect::hasGeometryShader(*sharedTestState->effect)); + EXPECT_FALSE(sharedTestState->effect->hasGeometryShader()); } TEST_F(AnEffect, reportsErrorWhenAskedForGeometryInputType_ButNoGeometryShaderProvided) { EDrawMode mode; - EXPECT_NE(StatusOK, Effect::getGeometryShaderInputType(*sharedTestState->effect, mode)); + EXPECT_NE(StatusOK, sharedTestState->effect->getGeometryShaderInputType(mode)); } TEST_F(AnEffect, reportsErrorWhenAskingForNonExistingUniformInput) @@ -125,12 +125,12 @@ namespace ramses EXPECT_STREQ("vec3fInputArray", input.getName()); EXPECT_EQ(3u, input.getElementCount()); - EXPECT_EQ(effect->impl.getLowlevelResourceHash(), input.impl.getEffectHash()); - EXPECT_EQ(EEffectInputDataType_Vector3F, input.getDataType()); - EXPECT_EQ(ramses_internal::EDataType::Vector3F, input.impl.getDataType()); - EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.impl.getSemantics()); + EXPECT_EQ(effect->m_impl.getLowlevelResourceHash(), input.m_impl.get().getEffectHash()); + EXPECT_EQ(EDataType::Vector3F, *input.getDataType()); + EXPECT_EQ(ramses_internal::EDataType::Vector3F, input.m_impl.get().getInternalDataType()); + EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.m_impl.get().getSemantics()); EXPECT_EQ(EEffectUniformSemantic::Invalid, input.getSemantics()); - EXPECT_EQ(5u, input.impl.getInputIndex()); + EXPECT_EQ(5u, input.m_impl.get().getInputIndex()); EXPECT_TRUE(input.isValid()); } @@ -142,10 +142,10 @@ namespace ramses EXPECT_STREQ("matrix44fInput", input.getName()); EXPECT_EQ(1u, input.getElementCount()); - EXPECT_EQ(effect->impl.getLowlevelResourceHash(), input.impl.getEffectHash()); - EXPECT_EQ(EEffectInputDataType_Matrix44F, input.getDataType()); - EXPECT_EQ(ramses_internal::EDataType::Matrix44F, input.impl.getDataType()); - EXPECT_EQ(ramses_internal::EFixedSemantics::ModelViewMatrix, input.impl.getSemantics()); + EXPECT_EQ(effect->m_impl.getLowlevelResourceHash(), input.m_impl.get().getEffectHash()); + EXPECT_EQ(EDataType::Matrix44F, input.getDataType()); + EXPECT_EQ(ramses_internal::EDataType::Matrix44F, input.m_impl.get().getInternalDataType()); + EXPECT_EQ(ramses_internal::EFixedSemantics::ModelViewMatrix, input.m_impl.get().getSemantics()); EXPECT_EQ(EEffectUniformSemantic::ModelViewMatrix, input.getSemantics()); EXPECT_TRUE(input.isValid()); } @@ -157,12 +157,12 @@ namespace ramses EXPECT_EQ(StatusOK, effect->getAttributeInput(1u, input)); EXPECT_STREQ("vec2fArrayInput", input.getName()); - EXPECT_EQ(effect->impl.getLowlevelResourceHash(), input.impl.getEffectHash()); - EXPECT_EQ(EEffectInputDataType_Vector2F, input.getDataType()); - EXPECT_EQ(ramses_internal::EDataType::Vector2Buffer, input.impl.getDataType()); - EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.impl.getSemantics()); + EXPECT_EQ(effect->m_impl.getLowlevelResourceHash(), input.m_impl.get().getEffectHash()); + EXPECT_EQ(EDataType::Vector2F, *input.getDataType()); + EXPECT_EQ(ramses_internal::EDataType::Vector2Buffer, input.m_impl.get().getInternalDataType()); + EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.m_impl.get().getSemantics()); EXPECT_EQ(EEffectAttributeSemantic::Invalid, input.getSemantics()); - EXPECT_EQ(1u, input.impl.getInputIndex()); + EXPECT_EQ(1u, input.m_impl.get().getInputIndex()); EXPECT_TRUE(input.isValid()); } @@ -173,12 +173,12 @@ namespace ramses EXPECT_EQ(StatusOK, effect->getAttributeInput(1u, input)); EXPECT_STREQ("vec2fArrayInput", input.getName()); - EXPECT_EQ(effect->impl.getLowlevelResourceHash(), input.impl.getEffectHash()); - EXPECT_EQ(EEffectInputDataType_Vector2F, input.getDataType()); - EXPECT_EQ(ramses_internal::EDataType::Vector2Buffer, input.impl.getDataType()); - EXPECT_EQ(ramses_internal::EFixedSemantics::TextPositionsAttribute, input.impl.getSemantics()); + EXPECT_EQ(effect->m_impl.getLowlevelResourceHash(), input.m_impl.get().getEffectHash()); + EXPECT_EQ(EDataType::Vector2F, *input.getDataType()); + EXPECT_EQ(ramses_internal::EDataType::Vector2Buffer, input.m_impl.get().getInternalDataType()); + EXPECT_EQ(ramses_internal::EFixedSemantics::TextPositionsAttribute, input.m_impl.get().getSemantics()); EXPECT_EQ(EEffectAttributeSemantic::TextPositions, input.getSemantics()); - EXPECT_EQ(1u, input.impl.getInputIndex()); + EXPECT_EQ(1u, input.m_impl.get().getInputIndex()); EXPECT_TRUE(input.isValid()); } @@ -189,10 +189,10 @@ namespace ramses EXPECT_EQ(StatusOK, effect->findAttributeInput(ramses::EEffectAttributeSemantic::TextPositions, input)); EXPECT_STREQ("vec2fArrayInput", input.getName()); - EXPECT_EQ(effect->impl.getLowlevelResourceHash(), input.impl.getEffectHash()); - EXPECT_EQ(EEffectInputDataType_Vector2F, input.getDataType()); - EXPECT_EQ(ramses_internal::EDataType::Vector2Buffer, input.impl.getDataType()); - EXPECT_EQ(ramses_internal::EFixedSemantics::TextPositionsAttribute, input.impl.getSemantics()); + EXPECT_EQ(effect->m_impl.getLowlevelResourceHash(), input.m_impl.get().getEffectHash()); + EXPECT_EQ(EDataType::Vector2F, *input.getDataType()); + EXPECT_EQ(ramses_internal::EDataType::Vector2Buffer, input.m_impl.get().getInternalDataType()); + EXPECT_EQ(ramses_internal::EFixedSemantics::TextPositionsAttribute, input.m_impl.get().getSemantics()); EXPECT_EQ(EEffectAttributeSemantic::TextPositions, input.getSemantics()); EXPECT_TRUE(input.isValid()); } @@ -205,12 +205,12 @@ namespace ramses EXPECT_STREQ("vec3fInputArray", input.getName()); EXPECT_EQ(3u, input.getElementCount()); - EXPECT_EQ(effect->impl.getLowlevelResourceHash(), input.impl.getEffectHash()); - EXPECT_EQ(EEffectInputDataType_Vector3F, input.getDataType()); - EXPECT_EQ(ramses_internal::EDataType::Vector3F, input.impl.getDataType()); - EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.impl.getSemantics()); + EXPECT_EQ(effect->m_impl.getLowlevelResourceHash(), input.m_impl.get().getEffectHash()); + EXPECT_EQ(EDataType::Vector3F, *input.getDataType()); + EXPECT_EQ(ramses_internal::EDataType::Vector3F, input.m_impl.get().getInternalDataType()); + EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.m_impl.get().getSemantics()); EXPECT_EQ(EEffectUniformSemantic::Invalid, input.getSemantics()); - EXPECT_EQ(5u, input.impl.getInputIndex()); + EXPECT_EQ(5u, input.m_impl.get().getInputIndex()); EXPECT_TRUE(input.isValid()); } @@ -226,13 +226,13 @@ namespace ramses UniformInput inputExternalTexture; EXPECT_EQ(StatusOK, effect->findUniformInput("textureExternalInput", inputExternalTexture)); - EXPECT_EQ(EEffectInputDataType_TextureSampler2D, input2d.getDataType()); + EXPECT_EQ(EDataType::TextureSampler2D, input2d.getDataType()); EXPECT_TRUE(input2d.isValid()); - EXPECT_EQ(EEffectInputDataType_TextureSampler3D, input3d.getDataType()); + EXPECT_EQ(EDataType::TextureSampler3D, input3d.getDataType()); EXPECT_TRUE(input3d.isValid()); - EXPECT_EQ(EEffectInputDataType_TextureSamplerCube, inputcube.getDataType()); + EXPECT_EQ(EDataType::TextureSamplerCube, inputcube.getDataType()); EXPECT_TRUE(inputcube.isValid()); - EXPECT_EQ(EEffectInputDataType_TextureSamplerExternal, inputExternalTexture.getDataType()); + EXPECT_EQ(EDataType::TextureSamplerExternal, inputExternalTexture.getDataType()); EXPECT_TRUE(inputExternalTexture.isValid()); } @@ -243,12 +243,12 @@ namespace ramses EXPECT_EQ(StatusOK, effect->findAttributeInput("vec2fArrayInput", input)); EXPECT_STREQ("vec2fArrayInput", input.getName()); - EXPECT_EQ(effect->impl.getLowlevelResourceHash(), input.impl.getEffectHash()); - EXPECT_EQ(EEffectInputDataType_Vector2F, input.getDataType()); - EXPECT_EQ(ramses_internal::EDataType::Vector2Buffer, input.impl.getDataType()); - EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.impl.getSemantics()); + EXPECT_EQ(effect->m_impl.getLowlevelResourceHash(), input.m_impl.get().getEffectHash()); + EXPECT_EQ(EDataType::Vector2F, *input.getDataType()); + EXPECT_EQ(ramses_internal::EDataType::Vector2Buffer, input.m_impl.get().getInternalDataType()); + EXPECT_EQ(ramses_internal::EFixedSemantics::Invalid, input.m_impl.get().getSemantics()); EXPECT_EQ(EEffectAttributeSemantic::Invalid, input.getSemantics()); - EXPECT_EQ(1u, input.impl.getInputIndex()); + EXPECT_EQ(1u, input.m_impl.get().getInputIndex()); EXPECT_TRUE(input.isValid()); } @@ -259,12 +259,12 @@ namespace ramses EXPECT_EQ(StatusOK, effect->findAttributeInput("vec2fArrayInput", input)); EXPECT_STREQ("vec2fArrayInput", input.getName()); - EXPECT_EQ(effect->impl.getLowlevelResourceHash(), input.impl.getEffectHash()); - EXPECT_EQ(EEffectInputDataType_Vector2F, input.getDataType()); - EXPECT_EQ(ramses_internal::EDataType::Vector2Buffer, input.impl.getDataType()); - EXPECT_EQ(ramses_internal::EFixedSemantics::TextPositionsAttribute, input.impl.getSemantics()); + EXPECT_EQ(effect->m_impl.getLowlevelResourceHash(), input.m_impl.get().getEffectHash()); + EXPECT_EQ(EDataType::Vector2F, *input.getDataType()); + EXPECT_EQ(ramses_internal::EDataType::Vector2Buffer, input.m_impl.get().getInternalDataType()); + EXPECT_EQ(ramses_internal::EFixedSemantics::TextPositionsAttribute, input.m_impl.get().getSemantics()); EXPECT_EQ(EEffectAttributeSemantic::TextPositions, input.getSemantics()); - EXPECT_EQ(1u, input.impl.getInputIndex()); + EXPECT_EQ(1u, input.m_impl.get().getInputIndex()); EXPECT_TRUE(input.isValid()); } @@ -275,12 +275,12 @@ namespace ramses EXPECT_EQ(StatusOK, effect->findUniformInput("texture2dInput", input)); EXPECT_STREQ("texture2dInput", input.getName()); - EXPECT_EQ(effect->impl.getLowlevelResourceHash(), input.impl.getEffectHash()); - EXPECT_EQ(EEffectInputDataType_TextureSampler2D, input.getDataType()); - EXPECT_EQ(ramses_internal::EDataType::TextureSampler2D, input.impl.getDataType()); - EXPECT_EQ(ramses_internal::EFixedSemantics::TextTexture, input.impl.getSemantics()); + EXPECT_EQ(effect->m_impl.getLowlevelResourceHash(), input.m_impl.get().getEffectHash()); + EXPECT_EQ(EDataType::TextureSampler2D, *input.getDataType()); + EXPECT_EQ(ramses_internal::EDataType::TextureSampler2D, input.m_impl.get().getInternalDataType()); + EXPECT_EQ(ramses_internal::EFixedSemantics::TextTexture, input.m_impl.get().getSemantics()); EXPECT_EQ(EEffectUniformSemantic::TextTexture, input.getSemantics()); - EXPECT_EQ(22u, input.impl.getInputIndex()); + EXPECT_EQ(22u, input.m_impl.get().getInputIndex()); EXPECT_TRUE(input.isValid()); } @@ -302,12 +302,12 @@ namespace ramses "}"); /// Can create ... - EXPECT_NE(static_cast(nullptr), sharedTestState->getScene().impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); + EXPECT_NE(static_cast(nullptr), sharedTestState->getScene().m_impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); effectDesc.setAttributeSemantic("a_position", EEffectAttributeSemantic::TextPositions); /// Can not create ... - EXPECT_EQ(static_cast(nullptr), sharedTestState->getScene().impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); + EXPECT_EQ(static_cast(nullptr), sharedTestState->getScene().m_impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); } TEST_F(AnEffect, canRetrieveGLSLErrorMessageFromClient) @@ -387,12 +387,12 @@ namespace ramses "}"); /// Can create ... - EXPECT_NE(static_cast(nullptr), sharedTestState->getScene().impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); + EXPECT_NE(static_cast(nullptr), sharedTestState->getScene().m_impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); effectDesc.setAttributeSemantic("a_texcoord", EEffectAttributeSemantic::TextTextureCoordinates); /// Can not create ... - EXPECT_EQ(static_cast(nullptr), sharedTestState->getScene().impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); + EXPECT_EQ(static_cast(nullptr), sharedTestState->getScene().m_impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); } TEST_F(AnEffect, canNotCreateEffectWhenTextTextureSemanticsHasWrongType) @@ -413,12 +413,12 @@ namespace ramses "}"); /// Can create ... - EXPECT_NE(static_cast(nullptr), sharedTestState->getScene().impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); + EXPECT_NE(static_cast(nullptr), sharedTestState->getScene().m_impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); effectDesc.setUniformSemantic("u_texture", EEffectUniformSemantic::TextTexture); /// Can not create ... - EXPECT_EQ(static_cast(nullptr), sharedTestState->getScene().impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); + EXPECT_EQ(static_cast(nullptr), sharedTestState->getScene().m_impl.createEffect(effectDesc, ResourceCacheFlag_DoNotCache, "")); } TEST_F(AnEffect, supportsBoolUniforms) @@ -462,7 +462,7 @@ namespace ramses Appearance* appearance = sharedTestState->getScene().createAppearance(*effect); ASSERT_NE(nullptr, appearance); - EXPECT_EQ(StatusOK, appearance->setInputValueInt32(uniform, 0)); + EXPECT_EQ(StatusOK, appearance->setInputValue(uniform, 0)); } class AnEffectWithGeometryShader : public AnEffect @@ -504,9 +504,10 @@ namespace ramses Effect* effect = sharedTestState->getScene().createEffect(effectDesc); ASSERT_NE(nullptr, effect); + EXPECT_TRUE(effect->hasGeometryShader()); EDrawMode geometryShaderInput; - EXPECT_EQ(StatusOK, Effect::getGeometryShaderInputType(*effect, geometryShaderInput)); - EXPECT_EQ(geometryShaderInput, EDrawMode::EDrawMode_Points); + EXPECT_EQ(StatusOK, effect->getGeometryShaderInputType(geometryShaderInput)); + EXPECT_EQ(geometryShaderInput, EDrawMode::Points); } TEST_F(AnEffectWithGeometryShader, providesExpectedGeometryInputType_Lines) @@ -526,9 +527,10 @@ namespace ramses Effect* effect = sharedTestState->getScene().createEffect(effectDesc); ASSERT_NE(nullptr, effect); + EXPECT_TRUE(effect->hasGeometryShader()); EDrawMode geometryShaderInput; - EXPECT_EQ(StatusOK, Effect::getGeometryShaderInputType(*effect, geometryShaderInput)); - EXPECT_EQ(geometryShaderInput, EDrawMode::EDrawMode_Lines); + EXPECT_EQ(StatusOK, effect->getGeometryShaderInputType(geometryShaderInput)); + EXPECT_EQ(geometryShaderInput, EDrawMode::Lines); } TEST_F(AnEffectWithGeometryShader, providesExpectedGeometryInputType_Triangles) @@ -549,7 +551,33 @@ namespace ramses Effect* effect = sharedTestState->getScene().createEffect(effectDesc); ASSERT_NE(nullptr, effect); EDrawMode geometryShaderInput; - EXPECT_EQ(StatusOK, Effect::getGeometryShaderInputType(*effect, geometryShaderInput)); - EXPECT_EQ(geometryShaderInput, EDrawMode::EDrawMode_Triangles); + EXPECT_EQ(StatusOK, effect->getGeometryShaderInputType(geometryShaderInput)); + EXPECT_EQ(geometryShaderInput, EDrawMode::Triangles); + } + + TEST_F(AnEffectWithGeometryShader, providesExpectedGeometryInputType_whenMultipleIdenticalEffectsCreated) + { + EffectDescription effectDesc; + effectDesc.setVertexShader(m_vertShader); + effectDesc.setFragmentShader(m_fragShader); + effectDesc.setGeometryShader(R"SHADER( + #version 320 es + layout(triangles) in; + layout(points, max_vertices = 1) out; + void main() { + gl_Position = vec4(0.0); + EmitVertex(); + } + )SHADER"); + + Effect* effect1 = sharedTestState->getScene().createEffect(effectDesc); + Effect* effect2 = sharedTestState->getScene().createEffect(effectDesc); + ASSERT_NE(nullptr, effect1); + ASSERT_NE(nullptr, effect2); + EDrawMode geometryShaderInput; + EXPECT_EQ(StatusOK, effect1->getGeometryShaderInputType(geometryShaderInput)); + EXPECT_EQ(geometryShaderInput, EDrawMode::Triangles); + EXPECT_EQ(StatusOK, effect2->getGeometryShaderInputType(geometryShaderInput)); + EXPECT_EQ(geometryShaderInput, EDrawMode::Triangles); } } diff --git a/client/ramses-client/test/GeometryBindingTest.cpp b/client/test/GeometryBindingTest.cpp similarity index 84% rename from client/ramses-client/test/GeometryBindingTest.cpp rename to client/test/GeometryBindingTest.cpp index 342430cec..df1ca551c 100644 --- a/client/ramses-client/test/GeometryBindingTest.cpp +++ b/client/test/GeometryBindingTest.cpp @@ -21,7 +21,7 @@ #include "GeometryBindingImpl.h" #include "ArrayBufferImpl.h" #include "Resource/EffectResource.h" -#include "ramses-client-api/EDataType.h" +#include "ramses-framework-api/EDataType.h" #include "SceneAPI/EDataType.h" #include "SceneAPI/ResourceContentHash.h" @@ -49,26 +49,26 @@ namespace ramses } protected: - void checkHashSetToInternalScene(const GeometryBinding& geometryBinding, ramses_internal::DataFieldHandle field, const Resource& resource, ramses_internal::UInt32 expectedInstancingDivisor) const + void checkHashSetToInternalScene(const GeometryBinding& geometryBinding, ramses_internal::DataFieldHandle field, const Resource& resource, uint32_t expectedInstancingDivisor) const { - const ramses_internal::ResourceContentHash expectedHash = resource.impl.getLowlevelResourceHash(); - const ramses_internal::ResourceField& actualDataResource = sharedTestState->getInternalScene().getDataResource(geometryBinding.impl.getAttributeDataInstance(), field); + const ramses_internal::ResourceContentHash expectedHash = resource.m_impl.getLowlevelResourceHash(); + const ramses_internal::ResourceField& actualDataResource = sharedTestState->getInternalScene().getDataResource(geometryBinding.m_impl.getAttributeDataInstance(), field); EXPECT_EQ(expectedHash, actualDataResource.hash); EXPECT_EQ(expectedInstancingDivisor, actualDataResource.instancingDivisor); } - void checkDataBufferSetToInternalScene(const GeometryBinding& geometryBinding, ramses_internal::DataFieldHandle field, const ArrayBufferImpl& dataBuffer, ramses_internal::UInt32 expectedInstancingDivisor) const + void checkDataBufferSetToInternalScene(const GeometryBinding& geometryBinding, ramses_internal::DataFieldHandle field, const ArrayBufferImpl& dataBuffer, uint32_t expectedInstancingDivisor) const { const ramses_internal::DataBufferHandle dataBufferHandle = dataBuffer.getDataBufferHandle(); - const ramses_internal::ResourceField& actualDataResource = sharedTestState->getInternalScene().getDataResource(geometryBinding.impl.getAttributeDataInstance(), field); + const ramses_internal::ResourceField& actualDataResource = sharedTestState->getInternalScene().getDataResource(geometryBinding.m_impl.getAttributeDataInstance(), field); EXPECT_EQ(dataBufferHandle, actualDataResource.dataBuffer); EXPECT_EQ(expectedInstancingDivisor, actualDataResource.instancingDivisor); } ArrayResource* setVec3fArrayInput(GeometryBinding& geometry) { - float verts[9] = { 0.f }; - auto vertices = sharedTestState->getScene().createArrayResource(EDataType::Vector3F, 3u, verts, ramses::ResourceCacheFlag_DoNotCache, "vec3Vertices"); + const vec3f vert{ 0.f, 1.f, 2.f }; + auto vertices = sharedTestState->getScene().createArrayResource(1u, &vert, ramses::ResourceCacheFlag_DoNotCache, "vec3Vertices"); EXPECT_TRUE(vertices != nullptr); assert(vertices); @@ -81,8 +81,8 @@ namespace ramses ArrayResource* setVec2fArrayInput(GeometryBinding& geometry) { - float verts[8] = { 0.2f }; - auto vertices = sharedTestState->getScene().createArrayResource(EDataType::Vector2F, 4u, verts, ramses::ResourceCacheFlag_DoNotCache, "vec2Vertices"); + const vec2f vert{ 0.f, 1.f }; + auto vertices = sharedTestState->getScene().createArrayResource(1u, &vert, ramses::ResourceCacheFlag_DoNotCache, "vec2Vertices"); EXPECT_TRUE(vertices != nullptr); assert(vertices); @@ -95,8 +95,8 @@ namespace ramses ArrayResource* setVec4fArrayInput(GeometryBinding& geometry) { - float verts[8] = { 0.4f }; - auto vertices = sharedTestState->getScene().createArrayResource(EDataType::Vector4F, 2u, verts, ramses::ResourceCacheFlag_DoNotCache, "vec4Vertices"); + const vec4f vert{ 0.f, 1.f, 2.f, 3.f }; + auto vertices = sharedTestState->getScene().createArrayResource(1u, &vert, ramses::ResourceCacheFlag_DoNotCache, "vec4Vertices"); EXPECT_TRUE(vertices != nullptr); assert(vertices); @@ -110,7 +110,7 @@ namespace ramses ArrayResource* setFloatArrayInput(GeometryBinding& geometry) { float verts[8] = { 0.1f }; - auto vertices = sharedTestState->getScene().createArrayResource(EDataType::Float, 8u, verts, ResourceCacheFlag_DoNotCache, "floatVertices"); + auto vertices = sharedTestState->getScene().createArrayResource(8u, verts, ResourceCacheFlag_DoNotCache, "floatVertices"); EXPECT_TRUE(vertices != nullptr); assert(vertices); @@ -124,11 +124,11 @@ namespace ramses ArrayResource* setIndicesInput(GeometryBinding& geometry) { uint32_t inds[3] = { 0u }; - auto indices = sharedTestState->getScene().createArrayResource(EDataType::UInt32, 3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); + auto indices = sharedTestState->getScene().createArrayResource(3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); EXPECT_TRUE(indices != nullptr); assert(indices); - EXPECT_EQ(0u, geometry.impl.getIndicesCount()); + EXPECT_EQ(0u, geometry.m_impl.getIndicesCount()); EXPECT_EQ(StatusOK, geometry.setIndices(*indices)); return indices; @@ -148,9 +148,9 @@ namespace ramses const Effect& resultEffect = geometry->getEffect(); EXPECT_EQ(resultEffect.getResourceId(), emptyEffect->getResourceId()); - EXPECT_EQ(resultEffect.impl.getLowlevelResourceHash(), emptyEffect->impl.getLowlevelResourceHash()); + EXPECT_EQ(resultEffect.m_impl.getLowlevelResourceHash(), emptyEffect->m_impl.getLowlevelResourceHash()); - const uint32_t fieldCount = sharedTestState->getInternalScene().getDataLayout(geometry->impl.getAttributeDataLayout()).getFieldCount(); + const uint32_t fieldCount = sharedTestState->getInternalScene().getDataLayout(geometry->m_impl.getAttributeDataLayout()).getFieldCount(); EXPECT_EQ(1u, fieldCount); sharedTestState->getScene().destroy(*geometry); @@ -164,7 +164,7 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*emptyEffect, "geometry"); ASSERT_TRUE(geometry != nullptr); - const uint32_t fieldCount = sharedTestState->getInternalScene().getDataLayout(geometry->impl.getAttributeDataLayout()).getFieldCount(); + const uint32_t fieldCount = sharedTestState->getInternalScene().getDataLayout(geometry->m_impl.getAttributeDataLayout()).getFieldCount(); EXPECT_EQ(1u, fieldCount); sharedTestState->getScene().destroy(*geometry); @@ -178,10 +178,10 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*emptyEffect, "geometry"); ASSERT_TRUE(geometry != nullptr); - const ramses_internal::DataLayout geometryLayout = sharedTestState->getInternalScene().getDataLayout(geometry->impl.getAttributeDataLayout()); + const ramses_internal::DataLayout geometryLayout = sharedTestState->getInternalScene().getDataLayout(geometry->m_impl.getAttributeDataLayout()); const ramses_internal::ResourceContentHash& effectHashFromGeometryLayout = geometryLayout.getEffectHash(); - EXPECT_EQ(emptyEffect->impl.getLowlevelResourceHash(), effectHashFromGeometryLayout); + EXPECT_EQ(emptyEffect->m_impl.getLowlevelResourceHash(), effectHashFromGeometryLayout); sharedTestState->getScene().destroy(*geometry); sharedTestState->getScene().destroy(*emptyEffect); @@ -193,7 +193,7 @@ namespace ramses ASSERT_TRUE(geometry != nullptr); const ramses_internal::DataFieldHandle indicesField(GeometryBindingImpl::IndicesDataFieldIndex); - const ramses_internal::EFixedSemantics semantics = sharedTestState->getInternalScene().getDataLayout(geometry->impl.getAttributeDataLayout()).getField(indicesField).semantics; + const ramses_internal::EFixedSemantics semantics = sharedTestState->getInternalScene().getDataLayout(geometry->m_impl.getAttributeDataLayout()).getField(indicesField).semantics; EXPECT_EQ(ramses_internal::EFixedSemantics::Indices, semantics); sharedTestState->getScene().destroy(*geometry); @@ -204,8 +204,8 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry != nullptr); - static const float verts[9] = { 0.f }; - ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(EDataType::Vector3F, 3u, verts, ramses::ResourceCacheFlag_DoNotCache, "vertices"); + const vec3f vert{ 0.f, 1.f, 2.f }; + ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(1u, &vert, ramses::ResourceCacheFlag_DoNotCache, "vertices"); ASSERT_TRUE(vertices != nullptr); AttributeInput input; @@ -222,8 +222,8 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry != nullptr); - static const float verts[9] = { 0.f }; - ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(EDataType::Vector3F, 3u, verts, ramses::ResourceCacheFlag_DoNotCache, "vertices"); + const vec3f vert{ 0.f, 1.f, 2.f }; + ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(1u, &vert, ramses::ResourceCacheFlag_DoNotCache, "vertices"); ASSERT_TRUE(vertices != nullptr); AttributeInput input; @@ -238,8 +238,8 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry != nullptr); - static const float verts[8] = { 0.f }; - ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(EDataType::Vector2F, 4u, verts, ramses::ResourceCacheFlag_DoNotCache, "vertices"); + const vec2f vert{ 1.f, 2.f }; + ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(1u, &vert, ramses::ResourceCacheFlag_DoNotCache, "vertices"); ASSERT_TRUE(vertices != nullptr); AttributeInput input; @@ -255,9 +255,9 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry != nullptr); - float verts[8] = { 0.f }; + const vec2f vert{ 1.f, 2.f }; Scene& anotherScene(*sharedTestState->getClient().createScene(sceneId_t{ 0xf00 })); - ArrayResource* const vertices = anotherScene.createArrayResource(EDataType::Vector2F, 4u, verts, ramses::ResourceCacheFlag_DoNotCache, "vec2Vertices"); + ArrayResource* const vertices = anotherScene.createArrayResource(1u, &vert, ramses::ResourceCacheFlag_DoNotCache, "vec2Vertices"); ASSERT_TRUE(vertices != nullptr); AttributeInput input; @@ -268,42 +268,6 @@ namespace ramses sharedTestState->getClient().destroy(anotherScene); } - TEST_F(GeometryBindingTest, reportsErrorWhenSettingVec3ArrayResourceFromAnotherScene) - { - GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); - ASSERT_TRUE(geometry != nullptr); - - float verts[9] = { 0.f }; - Scene& anotherScene(*sharedTestState->getClient().createScene(sceneId_t{ 0xf00 })); - ArrayResource* const vertices = anotherScene.createArrayResource(EDataType::Vector3F, 3u, verts, ramses::ResourceCacheFlag_DoNotCache, "vec3Vertices"); - ASSERT_TRUE(vertices != nullptr); - - AttributeInput input; - EXPECT_EQ(StatusOK, sharedTestState->effect->findAttributeInput("vec3fArrayInput", input)); - EXPECT_NE(StatusOK, geometry->setInputBuffer(input, *vertices)); - - sharedTestState->getScene().destroy(*geometry); - sharedTestState->getClient().destroy(anotherScene); - } - - TEST_F(GeometryBindingTest, reportsErrorWhenSettingVec4ArrayResourceFromAnotherScene) - { - GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); - ASSERT_TRUE(geometry != nullptr); - - float verts[8] = { 0.f }; - Scene& anotherScene(*sharedTestState->getClient().createScene(sceneId_t{ 0xf00 })); - ArrayResource* const vertices = anotherScene.createArrayResource(EDataType::Vector4F, 2u, verts, ramses::ResourceCacheFlag_DoNotCache, "vec2Vertices"); - ASSERT_TRUE(vertices != nullptr); - - AttributeInput input; - EXPECT_EQ(StatusOK, sharedTestState->effect->findAttributeInput("vec4fArrayInput", input)); - EXPECT_NE(StatusOK, geometry->setInputBuffer(input, *vertices)); - - sharedTestState->getScene().destroy(*geometry); - sharedTestState->getClient().destroy(anotherScene); - } - TEST_F(GeometryBindingTest, reportsErrorWhenSettingIndexDataBufferFromAnotherScene) { GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); @@ -375,8 +339,8 @@ namespace ramses Scene* otherScene = sharedTestState->getClient().createScene(sceneId_t(777u)); ASSERT_NE(nullptr, otherScene); - uint32_t data[4] = { 0u }; - ArrayResource* const vertices = otherScene->createArrayResource(EDataType::ByteBlob, sizeof(data), data); + const ramses::Byte data[4] = { 0 }; + ArrayResource* const vertices = otherScene->createArrayResource(sizeof(data), data); ASSERT_NE(nullptr, vertices); AttributeInput inputVec2; @@ -393,24 +357,6 @@ namespace ramses sharedTestState->getClient().destroy(*otherScene); } - TEST_F(GeometryBindingTest, reportsErrorWhenSettingFloatArrayResourceFromAnotherScene) - { - GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); - ASSERT_TRUE(geometry != nullptr); - - float verts[8] = { 0.f }; - Scene& anotherScene(*sharedTestState->getClient().createScene(sceneId_t{ 0xf00 })); - ArrayResource* const vertices = anotherScene.createArrayResource(EDataType::Float, 8u, verts, ResourceCacheFlag_DoNotCache, "floatVertices"); - ASSERT_TRUE(vertices != nullptr); - - AttributeInput input; - EXPECT_EQ(StatusOK, sharedTestState->effect->findAttributeInput("floatArrayInput", input)); - EXPECT_NE(StatusOK, geometry->setInputBuffer(input, *vertices)); - - sharedTestState->getScene().destroy(*geometry); - sharedTestState->getClient().destroy(anotherScene); - } - TEST_F(GeometryBindingTest, reportsErrorWhenSettingUint16ArrayIndicesFromAnotherScene) { GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); @@ -418,27 +364,10 @@ namespace ramses uint16_t inds[3] = { 0u }; Scene& anotherScene(*sharedTestState->getClient().createScene(sceneId_t{ 0xf00 })); - ArrayResource* const indices = anotherScene.createArrayResource(EDataType::UInt16, 3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); + ArrayResource* const indices = anotherScene.createArrayResource(3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); ASSERT_TRUE(indices != nullptr); - EXPECT_EQ(0u, geometry->impl.getIndicesCount()); - EXPECT_NE(StatusOK, geometry->setIndices(*indices)); - - EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*geometry)); - sharedTestState->getClient().destroy(anotherScene); - } - - TEST_F(GeometryBindingTest, reportsErrorWhenSettingUint32ArrayIndicesFromAnotherScene) - { - GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); - ASSERT_TRUE(geometry != nullptr); - - uint32_t inds[3] = { 0u }; - Scene& anotherScene(*sharedTestState->getClient().createScene(sceneId_t{ 0xf00 })); - ArrayResource* const indices = anotherScene.createArrayResource(EDataType::UInt32, 3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); - ASSERT_TRUE(indices != nullptr); - - EXPECT_EQ(0u, geometry->impl.getIndicesCount()); + EXPECT_EQ(0u, geometry->m_impl.getIndicesCount()); EXPECT_NE(StatusOK, geometry->setIndices(*indices)); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*geometry)); @@ -451,13 +380,13 @@ namespace ramses ASSERT_TRUE(geometry != nullptr); const uint16_t inds[3] = { 0u }; - ArrayResource* const indices = sharedTestState->getScene().createArrayResource(EDataType::UInt16, 3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); + ArrayResource* const indices = sharedTestState->getScene().createArrayResource(3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); ASSERT_TRUE(indices != nullptr); - EXPECT_EQ(0u, geometry->impl.getIndicesCount()); + EXPECT_EQ(0u, geometry->m_impl.getIndicesCount()); EXPECT_EQ(StatusOK, geometry->setIndices(*indices)); checkHashSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(0u), *indices, 0u); - EXPECT_EQ(indices->impl.getElementCount(), geometry->impl.getIndicesCount()); + EXPECT_EQ(indices->m_impl.getElementCount(), geometry->m_impl.getIndicesCount()); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*geometry)); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*indices)); @@ -469,13 +398,13 @@ namespace ramses ASSERT_TRUE(geometry != nullptr); const uint32_t inds[3] = { 0u }; - ArrayResource* const indices = sharedTestState->getScene().createArrayResource(EDataType::UInt32, 3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); + ArrayResource* const indices = sharedTestState->getScene().createArrayResource(3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); ASSERT_TRUE(indices != nullptr); - EXPECT_EQ(0u, geometry->impl.getIndicesCount()); + EXPECT_EQ(0u, geometry->m_impl.getIndicesCount()); EXPECT_EQ(StatusOK, geometry->setIndices(*indices)); checkHashSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(0u), *indices, 0u); - EXPECT_EQ(indices->impl.getElementCount(), geometry->impl.getIndicesCount()); + EXPECT_EQ(indices->m_impl.getElementCount(), geometry->m_impl.getIndicesCount()); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*geometry)); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*indices)); @@ -489,10 +418,10 @@ namespace ramses ArrayBuffer* const indices = sharedTestState->getScene().createArrayBuffer(EDataType::UInt16, 1u, "index data buffer"); ASSERT_TRUE(indices != nullptr); - EXPECT_EQ(0u, geometry->impl.getIndicesCount()); + EXPECT_EQ(0u, geometry->m_impl.getIndicesCount()); EXPECT_EQ(StatusOK, geometry->setIndices(*indices)); - checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(0u), indices->impl, 0u); - EXPECT_EQ(indices->impl.getElementCount(), geometry->impl.getIndicesCount()); + checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(0u), indices->m_impl, 0u); + EXPECT_EQ(indices->m_impl.getElementCount(), geometry->m_impl.getIndicesCount()); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*geometry)); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*indices)); @@ -506,10 +435,10 @@ namespace ramses ArrayBuffer* const indices = sharedTestState->getScene().createArrayBuffer(EDataType::UInt32, 1u, "index data buffer"); ASSERT_TRUE(indices != nullptr); - EXPECT_EQ(0u, geometry->impl.getIndicesCount()); + EXPECT_EQ(0u, geometry->m_impl.getIndicesCount()); EXPECT_EQ(StatusOK, geometry->setIndices(*indices)); - checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(0u), indices->impl, 0u); - EXPECT_EQ(indices->impl.getElementCount(), geometry->impl.getIndicesCount()); + checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(0u), indices->m_impl, 0u); + EXPECT_EQ(indices->m_impl.getElementCount(), geometry->m_impl.getIndicesCount()); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*geometry)); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*indices)); @@ -521,7 +450,7 @@ namespace ramses ASSERT_TRUE(geometry); const float inds[4] = { .0f, .0f, .0f, .0f }; - ArrayResource* const indices = sharedTestState->getScene().createArrayResource(EDataType::Float, 1u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); + ArrayResource* const indices = sharedTestState->getScene().createArrayResource(4u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); ASSERT_TRUE(indices); EXPECT_NE(StatusOK, geometry->setIndices(*indices)); @@ -535,8 +464,8 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry); - const float inds[4] = { .0f, .0f, .0f, .0f }; - ArrayResource* const indices = sharedTestState->getScene().createArrayResource(EDataType::Vector2F, 1u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); + const vec2f indice{ 1.f, 2.f }; + ArrayResource* const indices = sharedTestState->getScene().createArrayResource(1u, &indice, ramses::ResourceCacheFlag_DoNotCache, "indices"); ASSERT_TRUE(indices); EXPECT_NE(StatusOK, geometry->setIndices(*indices)); @@ -550,8 +479,8 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry); - const float inds[4] = { .0f, .0f, .0f, .0f }; - ArrayResource* const indices = sharedTestState->getScene().createArrayResource(EDataType::Vector3F, 1u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); + const vec3f indice{ 1.f, 2.f, 3.f }; + ArrayResource* const indices = sharedTestState->getScene().createArrayResource(1u, &indice, ramses::ResourceCacheFlag_DoNotCache, "indices"); ASSERT_TRUE(indices); EXPECT_NE(StatusOK, geometry->setIndices(*indices)); @@ -565,8 +494,8 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry); - const float inds[4] = { .0f, .0f, .0f, .0f }; - ArrayResource* const indices = sharedTestState->getScene().createArrayResource(EDataType::Vector4F, 1u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); + const vec4f indice{ 1.f, 2.f, 3.f, 4.f }; + ArrayResource* const indices = sharedTestState->getScene().createArrayResource(1u, &indice, ramses::ResourceCacheFlag_DoNotCache, "indices"); ASSERT_TRUE(indices); EXPECT_NE(StatusOK, geometry->setIndices(*indices)); @@ -658,8 +587,8 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry); - const float inds[4] = { .0f, .0f, .0f, .0f }; - ArrayResource* const indices = sharedTestState->getScene().createArrayResource(EDataType::ByteBlob, sizeof(inds), inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); + const ramses::Byte inds[4] = { 0, 1, 2, 3 }; + ArrayResource* const indices = sharedTestState->getScene().createArrayResource(sizeof(inds), inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); ASSERT_TRUE(indices); EXPECT_NE(StatusOK, geometry->setIndices(*indices)); @@ -676,8 +605,8 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry != nullptr); - const float verts[9] = { 0.f }; - ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(EDataType::Vector3F, 3u, verts, ramses::ResourceCacheFlag_DoNotCache, "vertices"); + const vec3f vert{ 0.f, 1.f, 2.f }; + ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(1u, &vert, ramses::ResourceCacheFlag_DoNotCache, "vertices"); ASSERT_TRUE(vertices != nullptr); EXPECT_EQ(StatusOK, geometry->setInputBuffer(input, *vertices)); @@ -699,7 +628,7 @@ namespace ramses ASSERT_TRUE(vertices != nullptr); EXPECT_EQ(StatusOK, geometry->setInputBuffer(input, *vertices, 16u)); - checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(3u), vertices->impl, 16u); + checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(3u), vertices->m_impl, 16u); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*geometry)); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*vertices)); @@ -721,8 +650,8 @@ namespace ramses constexpr uint16_t nonZeroStride = 17u; EXPECT_EQ(StatusOK, geometry->setInputBuffer(inputVec2, *interleavedVertices, 0u, nonZeroStride)); EXPECT_EQ(StatusOK, geometry->setInputBuffer(inputVec3, *interleavedVertices, 2 * sizeof(float), nonZeroStride)); - checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(2u), interleavedVertices->impl, 0u); - checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(3u), interleavedVertices->impl, 0u); + checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(2u), interleavedVertices->m_impl, 0u); + checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(3u), interleavedVertices->m_impl, 0u); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*geometry)); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*interleavedVertices)); @@ -738,8 +667,9 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry != nullptr); - float data[10] = { 1.f }; - ArrayResource* const interleavedVertices = sharedTestState->getScene().createArrayResource(EDataType::ByteBlob, sizeof(data), data); + const float data[10] = { 1.f }; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) interleaved vertices passed as byte blob + ArrayResource* const interleavedVertices = sharedTestState->getScene().createArrayResource(sizeof(data), reinterpret_cast(data)); ASSERT_TRUE(interleavedVertices != nullptr); constexpr uint16_t nonZeroStride = 17u; @@ -764,7 +694,7 @@ namespace ramses ASSERT_TRUE(vertices); EXPECT_EQ(StatusOK, geometry->setInputBuffer(input, *vertices)); - checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(2u), vertices->impl, 0u); + checkDataBufferSetToInternalScene(*geometry, ramses_internal::DataFieldHandle(2u), vertices->m_impl, 0u); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*geometry)); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*vertices)); @@ -778,8 +708,9 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry); - float data[10] = { 1.f }; - ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(EDataType::ByteBlob, sizeof(data), data); + const float data[10] = { 1.f }; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) interleaved vertices passed as byte blob + ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(sizeof(data), reinterpret_cast(data)); ASSERT_TRUE(vertices); EXPECT_EQ(StatusOK, geometry->setInputBuffer(input, *vertices)); @@ -814,8 +745,8 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry); - const float verts[2] = { 0.f }; - const auto vertices = sharedTestState->getScene().createArrayResource(EDataType::Vector2F, 1u, verts, ramses::ResourceCacheFlag_DoNotCache, "vertices"); + const vec2f vert{ 0.f, 1.f }; + const auto vertices = sharedTestState->getScene().createArrayResource(1u, &vert, ramses::ResourceCacheFlag_DoNotCache, "vertices"); ASSERT_TRUE(vertices); EXPECT_NE(StatusOK, geometry->setInputBuffer(input, *vertices, 1u, 2u)); @@ -832,8 +763,8 @@ namespace ramses GeometryBinding* const geometry = sharedTestState->getScene().createGeometryBinding(*sharedTestState->effect, "geometry"); ASSERT_TRUE(geometry != nullptr); - const float verts[9] = { 0.f }; - ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(EDataType::Vector3F, 3u, verts, ramses::ResourceCacheFlag_DoNotCache, "vertices"); + const vec3f vert{ 0.f, 1.f, 2.f }; + ArrayResource* const vertices = sharedTestState->getScene().createArrayResource(1u, &vert, ramses::ResourceCacheFlag_DoNotCache, "vertices"); ASSERT_TRUE(vertices != nullptr); EXPECT_NE(StatusOK, geometry->setInputBuffer(input, *vertices)); @@ -1045,7 +976,8 @@ namespace ramses ASSERT_TRUE(geometry != nullptr); uint32_t data[4] = { 0u }; - ArrayResource* const interleavedVertices = sharedTestState->getScene().createArrayResource(EDataType::ByteBlob, sizeof(data), data); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) interleaved vertices passed as byte blob + ArrayResource* const interleavedVertices = sharedTestState->getScene().createArrayResource(sizeof(data), reinterpret_cast(data)); ASSERT_TRUE(interleavedVertices != nullptr); auto floatArray = setFloatArrayInput(*geometry); @@ -1115,11 +1047,11 @@ namespace ramses EXPECT_EQ(StatusOK, geometry->validate()); //delete data buffer and create new one with same handle - ramses_internal::DataBufferHandle dataBufferHandle = vertexDataBuffer->impl.getDataBufferHandle(); + ramses_internal::DataBufferHandle dataBufferHandle = vertexDataBuffer->m_impl.getDataBufferHandle(); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*vertexDataBuffer)); - ASSERT_FALSE(sharedTestState->getScene().impl.getIScene().isDataBufferAllocated(dataBufferHandle)); - sharedTestState->getScene().impl.getIScene().allocateDataBuffer(ramses_internal::EDataBufferType::VertexBuffer, ramses_internal::EDataType::Vector2F, 10 * sizeof(float), dataBufferHandle); - ASSERT_TRUE(sharedTestState->getScene().impl.getIScene().isDataBufferAllocated(dataBufferHandle)); + ASSERT_FALSE(sharedTestState->getScene().m_impl.getIScene().isDataBufferAllocated(dataBufferHandle)); + sharedTestState->getScene().m_impl.getIScene().allocateDataBuffer(ramses_internal::EDataBufferType::VertexBuffer, ramses_internal::EDataType::Vector2F, 10 * sizeof(float), dataBufferHandle); + ASSERT_TRUE(sharedTestState->getScene().m_impl.getIScene().isDataBufferAllocated(dataBufferHandle)); EXPECT_NE(StatusOK, geometry->validate()); EXPECT_EQ(StatusOK, sharedTestState->getScene().destroy(*geometry)); diff --git a/client/ramses-client/test/GlslEffectTest.cpp b/client/test/GlslEffectTest.cpp similarity index 95% rename from client/ramses-client/test/GlslEffectTest.cpp rename to client/test/GlslEffectTest.cpp index b802d00f2..5a012b9e9 100644 --- a/client/ramses-client/test/GlslEffectTest.cpp +++ b/client/test/GlslEffectTest.cpp @@ -10,7 +10,10 @@ #include "PlatformAbstraction/PlatformThread.h" #include "Resource/EffectResource.h" #include "gmock/gmock.h" + #include +#include +#include using namespace ramses_internal; @@ -21,21 +24,21 @@ class AGlslEffect : public ::testing::Test { } - const String basicVertexShader = R"SHADER( + const std::string basicVertexShader = R"SHADER( #version 320 es void main(void) { gl_Position = vec4(0.0); } )SHADER"; - const String basicFragmentShader = R"SHADER( + const std::string basicFragmentShader = R"SHADER( #version 320 es out lowp vec4 colorOut; void main(void) { colorOut = vec4(0.0); })SHADER"; - const String basicGeometryShader = R"SHADER( + const std::string basicGeometryShader = R"SHADER( #version 320 es layout(points) in; layout(points, max_vertices = 1) out; @@ -44,13 +47,13 @@ class AGlslEffect : public ::testing::Test EmitVertex(); } )SHADER"; - const std::vector emptyCompilerDefines; - const HashMap emptySemanticInputs; + const std::vector emptyCompilerDefines; + const HashMap emptySemanticInputs; protected: - static void VerifyUniformInputExists(const EffectResource& effect, const char* uniformName) + static void VerifyUniformInputExists(const EffectResource& effect, std::string_view uniformName) { - const DataFieldHandle effecthandle = effect.getUniformDataFieldHandleByName(String(uniformName)); + const DataFieldHandle effecthandle = effect.getUniformDataFieldHandleByName(std::string(uniformName)); EXPECT_TRUE(effecthandle.isValid()); }; }; @@ -64,7 +67,7 @@ TEST_F(AGlslEffect, canParseBasicShaders) EXPECT_EQ(0u, res->getUniformInputs().size()); EXPECT_EQ(0u, res->getAttributeInputs().size()); EXPECT_FALSE(res->getGeometryShaderInputType().has_value()); - EXPECT_EQ(String(), res->getName()); + EXPECT_EQ(std::string(), res->getName()); } TEST_F(AGlslEffect, canParseBasicShaders_WithGeometryShader) @@ -75,13 +78,13 @@ TEST_F(AGlslEffect, canParseBasicShaders_WithGeometryShader) EXPECT_EQ(0u, res->getUniformInputs().size()); EXPECT_EQ(0u, res->getAttributeInputs().size()); - EXPECT_EQ(EDrawMode::Points, *res->getGeometryShaderInputType()); - EXPECT_EQ(String(), res->getName()); + EXPECT_EQ(EDrawMode::Points, res->getGeometryShaderInputType()); + EXPECT_EQ(std::string(), res->getName()); } TEST_F(AGlslEffect, canParseGeometryShaderWithTriangles) { - const String geometryShaderTriangles = R"SHADER( + const std::string geometryShaderTriangles = R"SHADER( #version 320 es layout(triangles) in; layout(points, max_vertices = 1) out; @@ -96,8 +99,8 @@ TEST_F(AGlslEffect, canParseGeometryShaderWithTriangles) EXPECT_EQ(0u, res->getUniformInputs().size()); EXPECT_EQ(0u, res->getAttributeInputs().size()); - EXPECT_EQ(EDrawMode::Triangles, *res->getGeometryShaderInputType()); - EXPECT_EQ(String(), res->getName()); + EXPECT_EQ(EDrawMode::Triangles, res->getGeometryShaderInputType()); + EXPECT_EQ(std::string(), res->getName()); } TEST_F(AGlslEffect, usesPassedName) @@ -106,7 +109,7 @@ TEST_F(AGlslEffect, usesPassedName) std::unique_ptr res(ge.createEffectResource(ResourceCacheFlag(0u))); ASSERT_TRUE(res); - EXPECT_EQ(String("someName"), res->getName()); + EXPECT_EQ(std::string("someName"), res->getName()); } TEST_F(AGlslEffect, rejectsEmptyVertexShader) @@ -163,17 +166,17 @@ TEST_F(AGlslEffect, usesProvidedDefines) "{\n" " gl_FragColor = DEFINE_ONE;\n" "}\n"; - std::vector compilerDefines; + std::vector compilerDefines; compilerDefines.push_back("DEFINE_ZERO vec4(0.0)"); compilerDefines.push_back("DEFINE_ONE vec4(1.0)"); GlslEffect ge(vertexShader, fragmentShader, "", compilerDefines, emptySemanticInputs, ""); std::unique_ptr res(ge.createEffectResource(ResourceCacheFlag(0u))); ASSERT_TRUE(res); - EXPECT_THAT(res->getVertexShader(), ::testing::HasSubstr(compilerDefines[0].stdRef())); - EXPECT_THAT(res->getVertexShader(), ::testing::HasSubstr(compilerDefines[1].stdRef())); - EXPECT_THAT(res->getFragmentShader(), ::testing::HasSubstr(compilerDefines[0].stdRef())); - EXPECT_THAT(res->getFragmentShader(), ::testing::HasSubstr(compilerDefines[1].stdRef())); + EXPECT_THAT(res->getVertexShader(), ::testing::HasSubstr(compilerDefines[0])); + EXPECT_THAT(res->getVertexShader(), ::testing::HasSubstr(compilerDefines[1])); + EXPECT_THAT(res->getFragmentShader(), ::testing::HasSubstr(compilerDefines[0])); + EXPECT_THAT(res->getFragmentShader(), ::testing::HasSubstr(compilerDefines[1])); } TEST_F(AGlslEffect, generatedShaderWithDefaultVersionAndDefinesEmbedded) @@ -189,7 +192,7 @@ TEST_F(AGlslEffect, generatedShaderWithDefaultVersionAndDefinesEmbedded) "{\n" " gl_FragColor = vec4(0.0);\n" "}\n"; - std::vector compilerDefines; + std::vector compilerDefines; compilerDefines.push_back("FIRST_DEFINE foo"); compilerDefines.push_back("OTHER_DEFINE bar"); GlslEffect ge(vertexShader, fragmentShader, "", compilerDefines, emptySemanticInputs, ""); @@ -289,7 +292,7 @@ TEST_F(AGlslEffect, acceptsGLSLESShaders_Version310esWithGeometryShaderExtension color = vec4(v_position, 1.0); })SHADER"; - const String geometryShader = R"SHADER( + const std::string geometryShader = R"SHADER( #version 310 es #extension GL_EXT_geometry_shader : enable layout(points) in; @@ -303,7 +306,7 @@ TEST_F(AGlslEffect, acceptsGLSLESShaders_Version310esWithGeometryShaderExtension GlslEffect ge(vertexShader, fragmentShader, geometryShader, emptyCompilerDefines, emptySemanticInputs, ""); std::unique_ptr res(ge.createEffectResource(ResourceCacheFlag(0u))); - EXPECT_EQ(EDrawMode::Points, *res->getGeometryShaderInputType()); + EXPECT_EQ(EDrawMode::Points, res->getGeometryShaderInputType()); EXPECT_TRUE(res); } @@ -383,7 +386,7 @@ TEST_F(AGlslEffect, canParseShaderInputs) { colorOut = vec4(0.0); })SHADER"; - const String geometryShader = R"SHADER( + const std::string geometryShader = R"SHADER( #version 320 es precision highp float; layout(points) in; @@ -399,7 +402,7 @@ TEST_F(AGlslEffect, canParseShaderInputs) } )SHADER"; - HashMap semantics; + HashMap semantics; semantics.put("uniformWithSemantic", EFixedSemantics::ModelViewProjectionMatrix); semantics.put("attributeWithSemantic", EFixedSemantics::CameraWorldPosition); @@ -583,7 +586,7 @@ TEST_F(AGlslEffect, failsWithWrongSemanticForType) " gl_FragColor = vec4(0.0);\n" "}\n"; - HashMap semantics; + HashMap semantics; semantics.put("uniformWithWrongSemantic", EFixedSemantics::ModelViewProjectionMatrix); GlslEffect ge(vertexShader, fragmentShader, "", emptyCompilerDefines, semantics, ""); @@ -777,10 +780,10 @@ class GlslEffectTestRunnable : public Runnable { } - virtual void run() override + void run() override { - const std::vector defs; - const ramses_internal::HashMap sems; + const std::vector defs; + const ramses_internal::HashMap sems; const char* v = "void main(){gl_Position=vec4(0);}"; const char* f = "void main(){gl_FragColor=vec4(0);}"; @@ -789,7 +792,7 @@ class GlslEffectTestRunnable : public Runnable createdSuccessfully = static_cast(resource); } - Bool createdSuccessfully; + bool createdSuccessfully; }; TEST_F(AGlslEffect, UseGlslEffectFromDifferentThread) @@ -810,13 +813,13 @@ TEST_F(AGlslEffect, UseGlslEffectFromMainThread) TEST_F(AGlslEffect, hasDefaultShaderVersion100) { - const String basicVertexShader_v100 = R"SHADER( + const std::string basicVertexShader_v100 = R"SHADER( void main(void) { gl_Position = vec4(0.0); } )SHADER"; - const String basicFragmentShader_v100 = R"SHADER( + const std::string basicFragmentShader_v100 = R"SHADER( void main(void) { gl_FragColor = vec4(0.0); @@ -1004,23 +1007,24 @@ TEST_F(AGlslEffect, rejectsEffectWithUniformDeclaredInBothStagesWithSameNameButD EXPECT_FALSE(res); } -TEST_F(AGlslEffect, DISABLED_acceptsActiveAsKeyword) +// This is a bug in glslang, or maybe wrong configuration in ramses. In gles 100, active should be allowed +TEST_F(AGlslEffect, doesNotAcceptActiveAsKeywordInVersion100) { - const String basicVertexShader_v100 = R"SHADER( + const std::string basicVertexShader_v100 = R"SHADER( attribute float active; void main(void) { gl_Position = vec4(0.0); } )SHADER"; - const String basicFragmentShader_v100 = R"SHADER( + const std::string basicFragmentShader_v100 = R"SHADER( void main(void) { gl_FragColor = vec4(0.0); })SHADER"; GlslEffect ge(basicVertexShader_v100, basicFragmentShader_v100, "", emptyCompilerDefines, emptySemanticInputs, ""); std::unique_ptr res(ge.createEffectResource(ResourceCacheFlag(0u))); - EXPECT_TRUE(res); + EXPECT_FALSE(res); } TEST_F(AGlslEffect, canRetrieveGLSLErrorMessage) diff --git a/client/ramses-client/test/GlslLimitsTest.cpp b/client/test/GlslLimitsTest.cpp similarity index 74% rename from client/ramses-client/test/GlslLimitsTest.cpp rename to client/test/GlslLimitsTest.cpp index 0b1893ce7..2632b4d15 100644 --- a/client/ramses-client/test/GlslLimitsTest.cpp +++ b/client/test/GlslLimitsTest.cpp @@ -7,7 +7,6 @@ // ------------------------------------------------------------------------- #include -#include "Collections/String.h" #include "glslEffectBlock/GLSlang.h" #include "glslEffectBlock/GlslLimits.h" @@ -26,15 +25,15 @@ namespace ramses_internal TEST_F(GlslLimitsTest, parseGlslVersionFromString) { - EXPECT_EQ(100u, GlslLimits::GetVersionFromString(String("#version 100"))); - EXPECT_EQ(330u, GlslLimits::GetVersionFromString(String("#version 330"))); - EXPECT_EQ(0u, GlslLimits::GetVersionFromString(String("Some invalid string"))); + EXPECT_EQ(100u, GlslLimits::GetVersionFromString("#version 100")); + EXPECT_EQ(330u, GlslLimits::GetVersionFromString("#version 330")); + EXPECT_EQ(0u, GlslLimits::GetVersionFromString("Some invalid string")); } TEST_F(GlslLimitsTest, testLimitsOpenGL_20) { TBuiltInResource res; - GlslLimits::InitCompilationResources(res, String("#version 110")); + GlslLimits::InitCompilationResources(res, "#version 110"); EXPECT_EQ(res.maxClipPlanes, 6); EXPECT_EQ(res.maxTextureUnits, 2); @@ -44,7 +43,7 @@ namespace ramses_internal TEST_F(GlslLimitsTest, testLimitsOpenGL_ES_30) { TBuiltInResource res; - GlslLimits::InitCompilationResources(res, String("#version 300 es")); + GlslLimits::InitCompilationResources(res, "#version 300 es"); EXPECT_EQ(res.maxFragmentInputVectors, 15); EXPECT_EQ(res.maxTessEvaluationUniformComponents, 0); // Should not be present @@ -53,7 +52,7 @@ namespace ramses_internal TEST_F(GlslLimitsTest, testLimitsOpenGL_42) { TBuiltInResource res; - GlslLimits::InitCompilationResources(res, String("#version 420")); + GlslLimits::InitCompilationResources(res, "#version 420"); EXPECT_EQ(res.maxAtomicCounterBufferSize, 16384); EXPECT_EQ(res.minProgramTexelOffset, -8); diff --git a/client/ramses-client/test/IteratorTest.cpp b/client/test/IteratorTest.cpp similarity index 100% rename from client/ramses-client/test/IteratorTest.cpp rename to client/test/IteratorTest.cpp diff --git a/client/ramses-client/test/MeshNodeTest.cpp b/client/test/MeshNodeTest.cpp similarity index 94% rename from client/ramses-client/test/MeshNodeTest.cpp rename to client/test/MeshNodeTest.cpp index 4304701f7..798fba976 100644 --- a/client/ramses-client/test/MeshNodeTest.cpp +++ b/client/test/MeshNodeTest.cpp @@ -25,17 +25,17 @@ using namespace ramses_internal; namespace ramses { - class MeshNodeTest : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test + class MeshNodeTest : public LocalTestClientWithScene, public testing::Test { protected: - virtual void SetUp() override + void SetUp() override { m_meshNode = m_scene.createMeshNode("node"); } AssertionResult renderableDataInstancesAreSetInScene(DataInstanceHandle uniformDataInstance, DataInstanceHandle attributeDataInstance) { - const RenderableHandle renderableHandle = m_meshNode->impl.getRenderableHandle(); + const RenderableHandle renderableHandle = m_meshNode->m_impl.getRenderableHandle(); const auto actualDataInstances = m_internalScene.getRenderable(renderableHandle).dataInstances; if (uniformDataInstance != actualDataInstances[ERenderableDataSlotType_Uniforms]) { @@ -50,8 +50,8 @@ namespace ramses AssertionResult effectResourceIsSetInScene(const Appearance& appearance) { - const EffectImpl* effect = appearance.impl.getEffectImpl(); - const Renderable renderable = m_internalScene.getRenderable(m_meshNode->impl.getRenderableHandle()); + const EffectImpl* effect = appearance.m_impl.getEffectImpl(); + const Renderable renderable = m_internalScene.getRenderable(m_meshNode->m_impl.getRenderableHandle()); const DataLayoutHandle& dataLayout = m_internalScene.getLayoutOfDataInstance(renderable.dataInstances[ERenderableDataSlotType_Geometry]); const ResourceContentHash& effectHash = m_internalScene.getDataLayout(dataLayout).getEffectHash(); @@ -62,16 +62,16 @@ namespace ramses AssertionResult renderableStateIsSetInScene(const Appearance& appearance) { - const RenderableHandle renderableHandle = m_meshNode->impl.getRenderableHandle(); + const RenderableHandle renderableHandle = m_meshNode->m_impl.getRenderableHandle(); - EXPECT_EQ(appearance.impl.getRenderStateHandle(), m_internalScene.getRenderable(renderableHandle).renderState); + EXPECT_EQ(appearance.m_impl.getRenderStateHandle(), m_internalScene.getRenderable(renderableHandle).renderState); return AssertionSuccess(); } AssertionResult meshNodeUniformAndAttributesIsSetInScene(const Appearance& appearance, const GeometryBinding& geometry) { - EXPECT_TRUE(renderableDataInstancesAreSetInScene(appearance.impl.getUniformDataInstance(), geometry.impl.getAttributeDataInstance())); + EXPECT_TRUE(renderableDataInstancesAreSetInScene(appearance.m_impl.getUniformDataInstance(), geometry.m_impl.getAttributeDataInstance())); EXPECT_TRUE(effectResourceIsSetInScene(appearance)); EXPECT_TRUE(renderableStateIsSetInScene(appearance)); @@ -192,7 +192,7 @@ namespace ramses ASSERT_TRUE(geometry != nullptr); const uint16_t indices = 0; - ArrayResource& indexArray = *anotherScene.createArrayResource(EDataType::UInt16, 1, &indices); + ArrayResource& indexArray = *anotherScene.createArrayResource(1u, &indices); EXPECT_EQ(StatusOK, geometry->setIndices(indexArray)); EXPECT_NE(StatusOK, m_meshNode->setGeometryBinding(*geometry)); @@ -205,7 +205,7 @@ namespace ramses ArrayResource& indexArray = createValidIndexArray(); EXPECT_EQ(StatusOK, geometry.setIndices(indexArray)); EXPECT_EQ(StatusOK, m_meshNode->setGeometryBinding(geometry)); - EXPECT_EQ(indexArray.impl.getElementCount(), m_meshNode->getIndexCount()); + EXPECT_EQ(indexArray.m_impl.getElementCount(), m_meshNode->getIndexCount()); } TEST_F(MeshNodeTest, settingGeometryHavingDifferentEffectFromAppearanceReportsError) @@ -282,7 +282,7 @@ namespace ramses EXPECT_TRUE(nullptr == m_meshNode->getAppearance()); EXPECT_TRUE(nullptr == m_meshNode->getGeometryBinding()); - const RenderableHandle renderableHandle = m_meshNode->impl.getRenderableHandle(); + const RenderableHandle renderableHandle = m_meshNode->m_impl.getRenderableHandle(); EXPECT_FALSE(this->m_internalScene.getRenderable(renderableHandle).dataInstances[ERenderableDataSlotType_Uniforms].isValid()); EXPECT_FALSE(this->m_internalScene.getRenderable(renderableHandle).dataInstances[ERenderableDataSlotType_Geometry].isValid()); EXPECT_FALSE(this->m_internalScene.getRenderable(renderableHandle).renderState.isValid()); @@ -313,7 +313,7 @@ namespace ramses TEST_F(MeshNodeTest, checksIfMeshNodeIsInitiallyVisible) { - EXPECT_EQ(m_meshNode->impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_meshNode->m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(MeshNodeTest, setsAndGetsSameStartIndex) diff --git a/client/ramses-client/test/MockActionCollector.h b/client/test/MockActionCollector.h similarity index 82% rename from client/ramses-client/test/MockActionCollector.h rename to client/test/MockActionCollector.h index 2dd5558bd..052191305 100644 --- a/client/ramses-client/test/MockActionCollector.h +++ b/client/test/MockActionCollector.h @@ -59,19 +59,19 @@ namespace ramses ramses_internal::ISceneGraphConsumerComponent* m_sceneGraphConsumer; - virtual void handleInitializeScene(const ramses_internal::SceneInfo& sceneInfo, const ramses_internal::Guid& providerID) override + void handleInitializeScene(const ramses_internal::SceneInfo& sceneInfo, const ramses_internal::Guid& providerID) override { ramses_internal::SceneRendererHandlerMock::handleInitializeScene(sceneInfo, providerID); } - virtual void handleSceneUpdate(const ramses_internal::SceneId& sceneId, ramses_internal::SceneUpdate&& sceneUpdate, const ramses_internal::Guid& providerID) override + void handleSceneUpdate(const ramses_internal::SceneId& sceneId, ramses_internal::SceneUpdate&& sceneUpdate, const ramses_internal::Guid& providerID) override { m_collectedActions.append(sceneUpdate.actions); ramses_internal::SceneRendererHandlerMock::handleSceneUpdate(sceneId, std::move(sceneUpdate), providerID); ++m_numReceivedActionLists; } - virtual void handleNewSceneAvailable(const ramses_internal::SceneInfo& newScene, const ramses_internal::Guid& providerID) override + void handleNewSceneAvailable(const ramses_internal::SceneInfo& newScene, const ramses_internal::Guid& providerID) override { ramses_internal::SceneRendererHandlerMock::handleNewSceneAvailable(newScene, providerID); if (m_sceneGraphConsumer != nullptr) @@ -80,7 +80,7 @@ namespace ramses } } - virtual void handleSceneBecameUnavailable(const ramses_internal::SceneId& unavailableScene, const ramses_internal::Guid& providerID) override + void handleSceneBecameUnavailable(const ramses_internal::SceneId& unavailableScene, const ramses_internal::Guid& providerID) override { ramses_internal::SceneRendererHandlerMock::handleSceneBecameUnavailable(unavailableScene, providerID); } diff --git a/client/ramses-client/test/NodeLazyTransformTest.cpp b/client/test/NodeLazyTransformTest.cpp similarity index 63% rename from client/ramses-client/test/NodeLazyTransformTest.cpp rename to client/test/NodeLazyTransformTest.cpp index c9a587d9c..e10e2b1d6 100644 --- a/client/ramses-client/test/NodeLazyTransformTest.cpp +++ b/client/test/NodeLazyTransformTest.cpp @@ -21,14 +21,14 @@ namespace ramses class NodeLazyTransformTest : public LocalTestClientWithScene, public testing::Test { protected: - virtual void SetUp() override + void SetUp() override { m_node = &this->template createObject("node"); } - ramses_internal::UInt32 getActualTransformCount() const + uint32_t getActualTransformCount() const { - ramses_internal::UInt32 count = 0u; + uint32_t count = 0u; for (ramses_internal::TransformHandle handle(0u); handle < this->m_internalScene.getTransformCount(); ++handle) { if (this->m_internalScene.isTransformAllocated(handle)) @@ -52,83 +52,73 @@ namespace ramses TYPED_TEST(NodeLazyTransformTest, callingSetterWithIdentityCreatesNoTransform) { - EXPECT_EQ(this->m_node->setTranslation(0.0f, 0.0f, 0.0f), StatusOK); - EXPECT_EQ(this->m_node->translate (0.0f, 0.0f, 0.0f), StatusOK); - EXPECT_EQ(this->m_node->setRotation (0.0f, 0.0f, 0.0f), StatusOK); - EXPECT_EQ(this->m_node->rotate (0.0f, 0.0f, 0.0f), StatusOK); - EXPECT_EQ(this->m_node->setScaling (1.0f, 1.0f, 1.0f), StatusOK); - EXPECT_EQ(this->m_node->scale (1.0f, 1.0f, 1.0f), StatusOK); + EXPECT_EQ(this->m_node->setTranslation({0.0f, 0.0f, 0.0f}), StatusOK); + EXPECT_EQ(this->m_node->translate({0.0f, 0.0f, 0.0f}), StatusOK); + EXPECT_EQ(this->m_node->setRotation({0.0f, 0.0f, 0.0f}, ERotationType::Euler_ZYX), StatusOK); + EXPECT_EQ(this->m_node->setRotation({0.0f, 0.0f, 0.0f}, ERotationType::Euler_XYZ), StatusOK); + EXPECT_EQ(this->m_node->setRotation({0.0f, 0.0f, 0.0f}, ERotationType::Euler_ZYZ), StatusOK); + EXPECT_EQ(this->m_node->setScaling({1.0f, 1.0f, 1.0f}), StatusOK); + EXPECT_EQ(this->m_node->scale({1.0f, 1.0f, 1.0f}), StatusOK); EXPECT_EQ(this->getActualTransformCount(), 0u); } TYPED_TEST(NodeLazyTransformTest, callingGettersWithoutTransformReturnIdentity) { - float x; - float y; - float z; - - EXPECT_EQ(this->m_node->getTranslation(x, y, z), StatusOK); - EXPECT_EQ(x, 0.0f); - EXPECT_EQ(y, 0.0f); - EXPECT_EQ(z, 0.0f); - - EXPECT_EQ(this->m_node->getRotation(x, y, z), StatusOK); - EXPECT_EQ(x, 0.0f); - EXPECT_EQ(y, 0.0f); - EXPECT_EQ(z, 0.0f); - - EXPECT_EQ(this->m_node->getScaling(x, y, z), StatusOK); - EXPECT_EQ(x, 1.0f); - EXPECT_EQ(y, 1.0f); - EXPECT_EQ(z, 1.0f); + vec3f value; + + EXPECT_EQ(this->m_node->getTranslation(value), StatusOK); + EXPECT_EQ(value, vec3f(0.f)); + + EXPECT_EQ(this->m_node->getRotation(value), StatusOK); + EXPECT_EQ(value, vec3f(0.f)); + + quat q; + EXPECT_EQ(this->m_node->getRotation(q), StatusOK); + EXPECT_EQ(q, glm::identity()); + + EXPECT_EQ(this->m_node->getScaling(value), StatusOK); + EXPECT_EQ(value, vec3f(1.f)); } TYPED_TEST(NodeLazyTransformTest, callingSetTranslationWithValueCreatesTransform) { EXPECT_EQ(this->getActualTransformCount(), 0u); - EXPECT_EQ(this->m_node->setTranslation(1.0f, 2.0f, 3.0f), StatusOK); + EXPECT_EQ(this->m_node->setTranslation({1.0f, 2.0f, 3.0f}), StatusOK); EXPECT_EQ(this->getActualTransformCount(), 1u); } TYPED_TEST(NodeLazyTransformTest, callingTranslateWithValueCreatesTransform) { EXPECT_EQ(this->getActualTransformCount(), 0u); - EXPECT_EQ(this->m_node->translate(1.0f, 2.0f, 3.0f), StatusOK); + EXPECT_EQ(this->m_node->translate({1.0f, 2.0f, 3.0f}), StatusOK); EXPECT_EQ(this->getActualTransformCount(), 1u); } TYPED_TEST(NodeLazyTransformTest, callingSetRotationWithValueCreatesTransform) { EXPECT_EQ(this->getActualTransformCount(), 0u); - EXPECT_EQ(this->m_node->setRotation(1.0f, 2.0f, 3.0f), StatusOK); - EXPECT_EQ(this->getActualTransformCount(), 1u); - } - - TYPED_TEST(NodeLazyTransformTest, callingRotateWithValueCreatesTransform) - { - EXPECT_EQ(this->getActualTransformCount(), 0u); - EXPECT_EQ(this->m_node->rotate(1.0f, 2.0f, 3.0f), StatusOK); + EXPECT_EQ(this->m_node->setRotation({1.0f, 2.0f, 3.0f}, ERotationType::Euler_YXZ), StatusOK); EXPECT_EQ(this->getActualTransformCount(), 1u); } TYPED_TEST(NodeLazyTransformTest, callingSetScalingWithValueCreatesTransform) { EXPECT_EQ(this->getActualTransformCount(), 0u); - EXPECT_EQ(this->m_node->setScaling(1.0f, 2.0f, 3.0f), StatusOK); + EXPECT_EQ(this->m_node->setScaling({1.0f, 2.0f, 3.0f}), StatusOK); EXPECT_EQ(this->getActualTransformCount(), 1u); } TYPED_TEST(NodeLazyTransformTest, callingScaleWithValueCreatesTransform) { EXPECT_EQ(this->getActualTransformCount(), 0u); - EXPECT_EQ(this->m_node->scale(1.0f, 2.0f, 3.0f), StatusOK); + EXPECT_EQ(this->m_node->scale({1.0f, 2.0f, 3.0f}), StatusOK); EXPECT_EQ(this->getActualTransformCount(), 1u); } TYPED_TEST(NodeLazyTransformTest, destructionDestroysTransform) { - EXPECT_EQ(this->m_node->setTranslation(1.0f, 2.0f, 3.0f), StatusOK); + EXPECT_EQ(this->m_node->setTranslation({1.0f, 2.0f, 3.0f}), StatusOK); EXPECT_EQ(this->getActualTransformCount(), 1u); this->m_scene.destroy(*this->m_node); diff --git a/client/ramses-client/test/NodeTest.cpp b/client/test/NodeTest.cpp similarity index 60% rename from client/ramses-client/test/NodeTest.cpp rename to client/test/NodeTest.cpp index 8e688d1d5..d9aadf0a2 100644 --- a/client/ramses-client/test/NodeTest.cpp +++ b/client/test/NodeTest.cpp @@ -16,16 +16,20 @@ #include "ClientTestUtils.h" #include "RamsesObjectTestTypes.h" +#include "Math3d/Rotation.h" +#include "glm/gtx/transform.hpp" + +#include using namespace testing; namespace ramses { template - class NodeTest : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test + class NodeTest : public LocalTestClientWithScene, public testing::Test { public: - NodeType& createNode(const char* name) + NodeType& createNode(std::string_view name) { return (this->template createObject(name)); } @@ -71,7 +75,7 @@ namespace ramses TYPED_TEST(NodeTest, shouldNotAddNodeFromOneSceneAsChildToNodeInOtherScene) { Scene& otherScene = *this->client.createScene(sceneId_t(1234u)); - CreationHelper otherSceneCreationHelper(&otherScene, nullptr, &this->client); + CreationHelper otherSceneCreationHelper(&otherScene, &this->client); Node& parent = *otherSceneCreationHelper.template createObjectOfType("parent"); EXPECT_EQ(0u, parent.getChildCount()); @@ -193,9 +197,9 @@ namespace ramses EXPECT_EQ(nullptr, child1.getParent()); EXPECT_EQ(nullptr, child2.getParent()); EXPECT_EQ(nullptr, child3.getParent()); - EXPECT_TRUE(child1.impl.isDirty()); - EXPECT_TRUE(child2.impl.isDirty()); - EXPECT_TRUE(child3.impl.isDirty()); + EXPECT_TRUE(child1.m_impl.isDirty()); + EXPECT_TRUE(child2.m_impl.isDirty()); + EXPECT_TRUE(child3.m_impl.isDirty()); } TYPED_TEST(NodeTest, reportsErrorWhenRemovinNonExistingParent) @@ -308,7 +312,7 @@ namespace ramses { Scene& anotherScene = *this->client.createScene(sceneId_t(12u)); - CreationHelper otherSceneCreationHelper(&anotherScene, nullptr, &this->client); + CreationHelper otherSceneCreationHelper(&anotherScene, &this->client); Node& parent = *otherSceneCreationHelper.template createObjectOfType("parent"); Node& node = this->createNode("node"); @@ -324,14 +328,14 @@ namespace ramses TYPED_TEST(NodeTest, isNotDirtyInitially) { Node& node = this->createNode("node"); - EXPECT_FALSE(node.impl.isDirty()); + EXPECT_FALSE(node.m_impl.isDirty()); } TYPED_TEST(NodeTest, canBeMarkedDirty) { Node& node = this->createNode("node"); - node.impl.markDirty(); - EXPECT_TRUE(node.impl.isDirty()); + node.m_impl.markDirty(); + EXPECT_TRUE(node.m_impl.isDirty()); } TYPED_TEST(NodeTest, isMarkedDirtyWhenSettingParent) @@ -339,7 +343,7 @@ namespace ramses Node& parent = this->createNode("parent"); Node& child = this->createNode("child"); EXPECT_EQ(StatusOK, child.setParent(parent)); - EXPECT_TRUE(child.impl.isDirty()); + EXPECT_TRUE(child.m_impl.isDirty()); } TYPED_TEST(NodeTest, isMarkedDirtyWhenAddingToParent) @@ -347,7 +351,7 @@ namespace ramses Node& parent = this->createNode("parent"); Node& child = this->createNode("child"); EXPECT_EQ(StatusOK, parent.addChild(child)); - EXPECT_TRUE(child.impl.isDirty()); + EXPECT_TRUE(child.m_impl.isDirty()); } TYPED_TEST(NodeTest, isMarkedDirtyWhenRemovedFromParent) @@ -358,7 +362,7 @@ namespace ramses this->m_scene.flush(); // to clear dirty state EXPECT_EQ(StatusOK, parent.removeChild(child)); - EXPECT_TRUE(child.impl.isDirty()); + EXPECT_TRUE(child.m_impl.isDirty()); } TYPED_TEST(NodeTest, staysCleanWhenSettingChild) @@ -366,7 +370,7 @@ namespace ramses Node& parent = this->createNode("parent"); Node& child = this->createNode("child"); EXPECT_EQ(StatusOK, parent.addChild(child)); - EXPECT_FALSE(parent.impl.isDirty()); + EXPECT_FALSE(parent.m_impl.isDirty()); } TYPED_TEST(NodeTest, isMarkedDirtyWhenParentDestroyed) @@ -377,271 +381,250 @@ namespace ramses this->m_scene.flush(); // to clear dirty state this->m_scene.destroy(parent); - EXPECT_TRUE(child.impl.isDirty()); + EXPECT_TRUE(child.m_impl.isDirty()); } TYPED_TEST(NodeTest, staysCleanWhenRemovingNonExistingParent) { Node& nodeWithNoParent = this->createNode("node"); EXPECT_NE(StatusOK, nodeWithNoParent.removeParent()); - EXPECT_FALSE(nodeWithNoParent.impl.isDirty()); + EXPECT_FALSE(nodeWithNoParent.m_impl.isDirty()); } - void expectMatricesEqual(const float mat1[16], const float mat2[16]) + void expectMatricesEqual(const glm::mat4 mat1, const glm::mat4 mat2) { for (uint32_t i = 0u; i < 16u; ++i) { - EXPECT_FLOAT_EQ(mat1[i], mat2[i]); + EXPECT_FLOAT_EQ(mat1[i / 4][i % 4], mat2[i / 4][i % 4]); } } TYPED_TEST(NodeTest, getsIdentityModelMatrixInitially) { Node& node = this->createNode("node"); - float modelMat[16] = { 0.f }; + matrix44f modelMat; EXPECT_EQ(StatusOK, node.getModelMatrix(modelMat)); - expectMatricesEqual(ramses_internal::Matrix44f::Identity.data, modelMat); + expectMatricesEqual(glm::identity(), modelMat); } TYPED_TEST(NodeTest, setsRotationVectorAndConvention) { Node& node = this->createNode("node"); - EXPECT_EQ(ramses::StatusOK, node.setRotation(1.f, 2.f, 3.f, ERotationConvention::XZX)); + EXPECT_EQ(ramses::StatusOK, node.setRotation({1.f, 2.f, 3.f}, ERotationType::Euler_XZX)); - const auto transformHandle = node.impl.getTransformHandle(); + const auto transformHandle = node.m_impl.getTransformHandle(); - float x; - float y; - float z; - ERotationConvention convention; - EXPECT_EQ(ramses::StatusOK, node.getRotation(x, y, z, convention)); - EXPECT_EQ(x, 1.f); - EXPECT_EQ(y, 2.f); - EXPECT_EQ(z, 3.f); - EXPECT_EQ(ERotationConvention::XZX, convention); - EXPECT_EQ(ramses_internal::Vector3(1.f, 2.f, 3.f), this->m_scene.impl.getIScene().getRotation(transformHandle)); - EXPECT_EQ(ramses_internal::ERotationConvention::XZX, this->m_scene.impl.getIScene().getRotationConvention(transformHandle)); + vec3f euler; + EXPECT_EQ(ramses::StatusOK, node.getRotation(euler)); + EXPECT_EQ(euler.x, 1.f); + EXPECT_EQ(euler.y, 2.f); + EXPECT_EQ(euler.z, 3.f); + EXPECT_EQ(ERotationType::Euler_XZX, node.getRotationType()); + EXPECT_EQ(glm::vec3(1.f, 2.f, 3.f), glm::vec3(this->m_scene.m_impl.getIScene().getRotation(transformHandle))); + EXPECT_EQ(ramses_internal::ERotationType::Euler_XZX, this->m_scene.m_impl.getIScene().getRotationType(transformHandle)); - EXPECT_EQ(ramses::StatusOK, node.setRotation(11.f, 12.f, 13.f, ERotationConvention::ZYX)); + EXPECT_EQ(ramses::StatusOK, node.setRotation({11.f, 12.f, 13.f}, ERotationType::Euler_XYZ)); - EXPECT_EQ(ramses::StatusOK, node.getRotation(x, y, z, convention)); - EXPECT_EQ(x, 11.f); - EXPECT_EQ(y, 12.f); - EXPECT_EQ(z, 13.f); - EXPECT_EQ(ERotationConvention::ZYX, convention); - EXPECT_EQ(ramses_internal::Vector3(11.f, 12.f, 13.f), this->m_scene.impl.getIScene().getRotation(transformHandle)); - EXPECT_EQ(ramses_internal::ERotationConvention::ZYX, this->m_scene.impl.getIScene().getRotationConvention(transformHandle)); + EXPECT_EQ(ramses::StatusOK, node.getRotation(euler)); + EXPECT_EQ(euler.x, 11.f); + EXPECT_EQ(euler.y, 12.f); + EXPECT_EQ(euler.z, 13.f); + EXPECT_EQ(ERotationType::Euler_XYZ, node.getRotationType()); + EXPECT_EQ(glm::vec3(11.f, 12.f, 13.f), glm::vec3(this->m_scene.m_impl.getIScene().getRotation(transformHandle))); + EXPECT_EQ(ramses_internal::ERotationType::Euler_XYZ, this->m_scene.m_impl.getIScene().getRotationType(transformHandle)); } - TYPED_TEST(NodeTest, nonLegacyGetRotationFailsWhenLegacyRotationIsSet) + TYPED_TEST(NodeTest, returnsErrorWhenSettingQuaternionConvention) { Node& node = this->createNode("node"); - EXPECT_EQ(ramses::StatusOK, node.setRotation(1.f, 2.f, 3.f)); + EXPECT_NE(StatusOK, node.setRotation({10.f, 20.f, 30.f}, ERotationType::Quaternion)); - float x; - float y; - float z; - ERotationConvention convention; - EXPECT_NE(ramses::StatusOK, node.getRotation(x, y, z, convention)); - } + // default value is still set + EXPECT_EQ(ERotationType::Euler_XYZ, node.getRotationType()); - TYPED_TEST(NodeTest, getRotationReturnsDefaultValuesWithoutSetBefore) - { - Node& node = this->createNode("node"); + quat quaternion; + EXPECT_EQ(StatusOK, node.getRotation(quaternion)); + EXPECT_EQ(quaternion, glm::identity()); + + vec3f euler; + EXPECT_EQ(StatusOK, node.getRotation(euler)); + EXPECT_EQ(euler, vec3f{0.f}); - float x; - float y; - float z; - EXPECT_EQ(ramses::StatusOK, node.getRotation(x, y, z)); - EXPECT_EQ(0.f, x); - EXPECT_EQ(0.f, y); - EXPECT_EQ(0.f, z); + const auto transformHandle = node.m_impl.getTransformHandle(); + EXPECT_FALSE(transformHandle.isValid()); } - TYPED_TEST(NodeTest, getRotationWithConventionReturnsDefaultValuesWithoutSetBefore) + TYPED_TEST(NodeTest, setsRotationQuaternion) { Node& node = this->createNode("node"); - float x; - float y; - float z; - ERotationConvention convention; - EXPECT_EQ(ramses::StatusOK, node.getRotation(x, y, z, convention)); - EXPECT_EQ(0.f, x); - EXPECT_EQ(0.f, y); - EXPECT_EQ(0.f, z); - EXPECT_EQ(ERotationConvention::XYZ, convention); + const quat q{0.830048f, -0.2907008f, 0.4666782f, -0.093407f}; + EXPECT_EQ(StatusOK, node.setRotation(q)); + + const auto transformHandle = node.m_impl.getTransformHandle(); + + quat qOut; + EXPECT_EQ(ramses::StatusOK, node.getRotation(qOut)); + EXPECT_EQ(qOut.x, q.x); + EXPECT_EQ(qOut.y, q.y); + EXPECT_EQ(qOut.z, q.z); + EXPECT_EQ(qOut.w, q.w); + EXPECT_EQ(glm::vec4(q.x, q.y, q.z, q.w), this->m_scene.m_impl.getIScene().getRotation(transformHandle)); + EXPECT_EQ(ramses_internal::ERotationType::Quaternion, this->m_scene.m_impl.getIScene().getRotationType(transformHandle)); } - TYPED_TEST(NodeTest, rotateFailsWhenNonLegacyRotationIsSet) + TYPED_TEST(NodeTest, setsRotationQuaternionThenEuler) { Node& node = this->createNode("node"); - EXPECT_EQ(ramses::StatusOK, node.setRotation(1.f, 2.f, 3.f, ERotationConvention::ZYX)); - - const auto transformHandle = node.impl.getTransformHandle(); + const quat q{0.830048f, -0.2907008f, 0.4666782f, -0.093407f}; + EXPECT_EQ(StatusOK, node.setRotation(q)); + quat qOut; + EXPECT_EQ(ramses::StatusOK, node.getRotation(qOut)); - EXPECT_NE(ramses::StatusOK, node.rotate(1.f , 2.f, 3.f)); + const auto transformHandle = node.m_impl.getTransformHandle(); + EXPECT_EQ(ramses::StatusOK, node.setRotation({11.f, 12.f, 13.f}, ERotationType::Euler_XYZ)); - //check no change on LL scene - EXPECT_EQ(ramses_internal::Vector3(1.f, 2.f, 3.f), this->m_scene.impl.getIScene().getRotation(transformHandle)); - EXPECT_EQ(ramses_internal::ERotationConvention::ZYX, this->m_scene.impl.getIScene().getRotationConvention(transformHandle)); + vec3f euler; + EXPECT_EQ(ramses::StatusOK, node.getRotation(euler)); + EXPECT_EQ(ERotationType::Euler_XYZ, node.getRotationType()); + EXPECT_EQ(euler.x, 11.f); + EXPECT_EQ(euler.y, 12.f); + EXPECT_EQ(euler.z, 13.f); + EXPECT_EQ(glm::vec3(11.f, 12.f, 13.f), glm::vec3(this->m_scene.m_impl.getIScene().getRotation(transformHandle))); + EXPECT_EQ(ramses_internal::ERotationType::Euler_XYZ, this->m_scene.m_impl.getIScene().getRotationType(transformHandle)); + EXPECT_NE(ramses::StatusOK, node.getRotation(qOut)); } - TYPED_TEST(NodeTest, legacyGetRotationFailsWhenNonLegacyRotationIsSet) + TYPED_TEST(NodeTest, setsRotationEulerThenQuaternion) { Node& node = this->createNode("node"); - EXPECT_EQ(ramses::StatusOK, node.setRotation(1.f, 2.f, 3.f, ERotationConvention::ZYX)); + EXPECT_EQ(ramses::StatusOK, node.setRotation({1.f, 2.f, 3.f}, ERotationType::Euler_XZX)); - float x; - float y; - float z; - EXPECT_NE(ramses::StatusOK, node.getRotation(x, y, z)); - } + vec3f euler; + EXPECT_EQ(ramses::StatusOK, node.getRotation(euler)); + EXPECT_EQ(ERotationType::Euler_XZX, node.getRotationType()); - TYPED_TEST(NodeTest, setsRotationVectorAndConvention_LegacyConvention) - { - Node& node = this->createNode("node"); - EXPECT_EQ(ramses::StatusOK, node.setRotation(1.f, 2.f, 3.f)); + const quat q{0.830048f, -0.2907008f, 0.4666782f, -0.093407f}; + EXPECT_EQ(StatusOK, node.setRotation(q)); - float x; - float y; - float z; - EXPECT_EQ(ramses::StatusOK, node.getRotation(x, y, z)); - EXPECT_EQ(x, 1.f); - EXPECT_EQ(y, 2.f); - EXPECT_EQ(z, 3.f); + const auto transformHandle = node.m_impl.getTransformHandle(); - const auto transformHandle = node.impl.getTransformHandle(); - EXPECT_EQ(ramses_internal::Vector3(1.f, 2.f, 3.f), this->m_scene.impl.getIScene().getRotation(transformHandle)); - EXPECT_EQ(ramses_internal::ERotationConvention::Legacy_ZYX, this->m_scene.impl.getIScene().getRotationConvention(transformHandle)); + quat qOut; + EXPECT_EQ(ramses::StatusOK, node.getRotation(qOut)); + EXPECT_EQ(qOut.x, q.x); + EXPECT_EQ(qOut.y, q.y); + EXPECT_EQ(qOut.z, q.z); + EXPECT_EQ(qOut.w, q.w); + EXPECT_EQ(glm::vec4(q.x, q.y, q.z, q.w), this->m_scene.m_impl.getIScene().getRotation(transformHandle)); + EXPECT_EQ(ramses_internal::ERotationType::Quaternion, this->m_scene.m_impl.getIScene().getRotationType(transformHandle)); + + EXPECT_NE(ramses::StatusOK, node.getRotation(euler)); + EXPECT_EQ(ERotationType::Quaternion, node.getRotationType()); } - TYPED_TEST(NodeTest, canRotate_LegacyConvention) + TYPED_TEST(NodeTest, getRotationReturnsErrorIfQuaternion) { Node& node = this->createNode("node"); - EXPECT_EQ(ramses::StatusOK, node.rotate(1.f, 2.f, 3.f)); - EXPECT_EQ(ramses::StatusOK, node.rotate(1.f, 2.f, 3.f)); - - float x; - float y; - float z; - EXPECT_EQ(ramses::StatusOK, node.getRotation(x, y, z)); - EXPECT_EQ(x, 2.f); - EXPECT_EQ(y, 4.f); - EXPECT_EQ(z, 6.f); - - const auto transformHandle = node.impl.getTransformHandle(); - EXPECT_EQ(ramses_internal::Vector3(2.f, 4.f, 6.f), this->m_scene.impl.getIScene().getRotation(transformHandle)); - EXPECT_EQ(ramses_internal::ERotationConvention::Legacy_ZYX, this->m_scene.impl.getIScene().getRotationConvention(transformHandle)); + quat q{0.5f, 0.5f, 0.5f, -0.5f}; + EXPECT_EQ(StatusOK, node.setRotation(q)); + vec3f euler; + EXPECT_NE(StatusOK, node.getRotation(euler)); + EXPECT_EQ(ERotationType::Quaternion, node.getRotationType()); } - TYPED_TEST(NodeTest, identitySetRotationWithExplicitConventionCanNotBeDiscarded) + TYPED_TEST(NodeTest, getRotationQuaternionReturnsErrorIfEuler) { Node& node = this->createNode("node"); - EXPECT_EQ(ramses::StatusOK, node.setRotation(0.f, 0.f, 0.f, ERotationConvention::ZYX)); - - float x; - float y; - float z; - ERotationConvention conv; - EXPECT_EQ(ramses::StatusOK, node.getRotation(x, y, z, conv)); - EXPECT_EQ(ERotationConvention::ZYX, conv); + quat q; + EXPECT_EQ(StatusOK, node.setRotation({90.f, 0.f, 0.f})); + EXPECT_NE(StatusOK, node.getRotation(q)); } - TYPED_TEST(NodeTest, settingScaleOrTranlationMayNotBreakGetRotationWithConvention) + TYPED_TEST(NodeTest, getRotationReturnsDefaultValuesWithoutSetBefore) { Node& node = this->createNode("node"); - float x; - float y; - float z; - ERotationConvention convention; - EXPECT_EQ(ramses::StatusOK, node.getRotation(x, y, z, convention)); - EXPECT_EQ(0.f, x); - EXPECT_EQ(0.f, y); - EXPECT_EQ(0.f, z); - EXPECT_EQ(ERotationConvention::XYZ, convention); + vec3f euler; + EXPECT_EQ(ramses::StatusOK, node.getRotation(euler)); + EXPECT_EQ(euler, vec3f{0.f}); + EXPECT_EQ(ERotationType::Euler_XYZ, node.getRotationType()); + } - EXPECT_EQ(ramses::StatusOK, node.setTranslation(2, 2, 2)); - EXPECT_EQ(ramses::StatusOK, node.getRotation(x, y, z, convention)); - EXPECT_EQ(0.f, x); - EXPECT_EQ(0.f, y); - EXPECT_EQ(0.f, z); - EXPECT_EQ(ERotationConvention::XYZ, convention); + TYPED_TEST(NodeTest, getRotationQuaternionReturnsDefaultValuesWithoutSetBefore) + { + Node& node = this->createNode("node"); + quat q; + EXPECT_EQ(ERotationType::Euler_XYZ, node.getRotationType()); + EXPECT_EQ(ramses::StatusOK, node.getRotation(q)); + EXPECT_EQ(0.f, q.x); + EXPECT_EQ(0.f, q.y); + EXPECT_EQ(0.f, q.z); + EXPECT_EQ(1.f, q.w); } - TYPED_TEST(NodeTest, canSetAndGetNonLegacyRotationAfterSettingScaleOrTranlation) + TYPED_TEST(NodeTest, settingScaleOrTranlationMayNotBreakGetRotation) { Node& node = this->createNode("node"); - EXPECT_EQ(ramses::StatusOK, node.setTranslation(2, 2, 2)); - - EXPECT_EQ(ramses::StatusOK, node.setRotation(1.f, 2.f, 3.f, ERotationConvention::ZYZ)); + vec3f euler; + EXPECT_EQ(ramses::StatusOK, node.getRotation(euler)); + EXPECT_EQ(euler, vec3f{0.f}); + EXPECT_EQ(ERotationType::Euler_XYZ, node.getRotationType()); - float x; - float y; - float z; - ERotationConvention convention; + EXPECT_EQ(ramses::StatusOK, node.setTranslation({2, 2, 2})); + EXPECT_EQ(ramses::StatusOK, node.getRotation(euler)); - EXPECT_EQ(ramses::StatusOK, node.getRotation(x, y, z, convention)); - - EXPECT_EQ(1.f, x); - EXPECT_EQ(2.f, y); - EXPECT_EQ(3.f, z); - EXPECT_EQ(ERotationConvention::ZYZ, convention); + EXPECT_EQ(euler, vec3f{0.f}); + EXPECT_EQ(ERotationType::Euler_XYZ, node.getRotationType()); } - TYPED_TEST(NodeTest, canSetAndGetLegacyRotationAfterSettingScaleOrTranlation) + TYPED_TEST(NodeTest, canSetAndGetRotationAfterSettingScaleOrTranslation) { Node& node = this->createNode("node"); - EXPECT_EQ(ramses::StatusOK, node.setTranslation(2, 2, 2)); - - EXPECT_EQ(ramses::StatusOK, node.setRotation(1.f, 2.f, 3.f)); + EXPECT_EQ(ramses::StatusOK, node.setTranslation({2, 2, 2})); - float x; - float y; - float z; + EXPECT_EQ(ramses::StatusOK, node.setRotation({1.f, 2.f, 3.f}, ERotationType::Euler_ZYZ)); - EXPECT_EQ(ramses::StatusOK, node.getRotation(x, y, z)); + vec3f euler; + EXPECT_EQ(ramses::StatusOK, node.getRotation(euler)); - EXPECT_EQ(1.f, x); - EXPECT_EQ(2.f, y); - EXPECT_EQ(3.f, z); + EXPECT_EQ(euler, vec3f(1.f, 2.f, 3.f)); + EXPECT_EQ(ERotationType::Euler_ZYZ, node.getRotationType()); } TYPED_TEST(NodeTest, getsIdentityInverseModelMatrixInitially) { Node& node = this->createNode("node"); - float modelMat[16] = { 0.f }; + matrix44f modelMat; EXPECT_EQ(StatusOK, node.getInverseModelMatrix(modelMat)); - expectMatricesEqual(ramses_internal::Matrix44f::Identity.data, modelMat); + expectMatricesEqual(glm::identity(), modelMat); } TYPED_TEST(NodeTest, getsModelMatrixComputedFromTransformationChain_SingleNode) { Node& node = this->template createObject(); - node.setTranslation(1.f, 2.f, 3.f); - node.setScaling(4.f, 5.f, 6.f); - node.setRotation(7.f, 8.f, 9.f); + node.setTranslation({1.f, 2.f, 3.f}); + node.setScaling({4.f, 5.f, 6.f}); + node.setRotation({7.f, 8.f, 9.f}, ERotationType::Euler_ZYX); - const ramses_internal::Matrix44f transMat = ramses_internal::Matrix44f::Translation({ 1.f, 2.f, 3.f }); - const ramses_internal::Matrix44f scaleMat = ramses_internal::Matrix44f::Scaling({ 4.f, 5.f, 6.f }); - const ramses_internal::Matrix44f rotMat = ramses_internal::Matrix44f::RotationEuler({ 7.f, 8.f, 9.f }, ramses_internal::ERotationConvention::Legacy_ZYX); - const ramses_internal::Matrix44f expectedModelMat = transMat * rotMat * scaleMat; + const auto transMat = glm::translate(glm::vec3{ 1.f, 2.f, 3.f }); + const auto scaleMat = glm::scale(glm::vec3{ 4.f, 5.f, 6.f }); + const auto rotMat = ramses_internal::Math3d::Rotation({ 7.f, 8.f, 9.f, 1.f }, ramses_internal::ERotationType::Euler_ZYX); + const auto expectedModelMat = transMat * rotMat * scaleMat; - float modelMat[16] = { 0.f }; + matrix44f modelMat; EXPECT_EQ(StatusOK, node.getModelMatrix(modelMat)); - expectMatricesEqual(expectedModelMat.data, modelMat); + expectMatricesEqual(expectedModelMat, modelMat); } TYPED_TEST(NodeTest, getsModelMatrixComputedFromTransformationChain_MultipleNodes) @@ -650,9 +633,9 @@ namespace ramses Node& scaleNode = this->template createObject(); Node& rotateNode = this->template createObject(); - translationNode.setTranslation(1.f, 2.f, 3.f); - scaleNode.setScaling(4.f, 5.f, 6.f); - rotateNode.setRotation(7.f, 8.f, 9.f); + translationNode.setTranslation({1.f, 2.f, 3.f}); + scaleNode.setScaling({4.f, 5.f, 6.f}); + rotateNode.setRotation({7.f, 8.f, 9.f}, ERotationType::Euler_ZYX); translationNode.addChild(rotateNode); rotateNode.addChild(scaleNode); @@ -660,34 +643,34 @@ namespace ramses Node& node = this->createNode("node"); scaleNode.addChild(node); - const ramses_internal::Matrix44f transMat = ramses_internal::Matrix44f::Translation({ 1.f, 2.f, 3.f }); - const ramses_internal::Matrix44f scaleMat = ramses_internal::Matrix44f::Scaling({ 4.f, 5.f, 6.f }); - const ramses_internal::Matrix44f rotMat = ramses_internal::Matrix44f::RotationEuler({ 7.f, 8.f, 9.f }, ramses_internal::ERotationConvention::Legacy_ZYX); - const ramses_internal::Matrix44f expectedModelMat = transMat * rotMat * scaleMat; + const auto transMat = glm::translate(glm::vec3{ 1.f, 2.f, 3.f }); + const auto scaleMat = glm::scale(glm::vec3{ 4.f, 5.f, 6.f }); + const auto rotMat = ramses_internal::Math3d::Rotation({ 7.f, 8.f, 9.f, 1.f }, ramses_internal::ERotationType::Euler_ZYX); + const auto expectedModelMat = transMat * rotMat * scaleMat; - float modelMat[16] = { 0.f }; + matrix44f modelMat; EXPECT_EQ(StatusOK, node.getModelMatrix(modelMat)); - expectMatricesEqual(expectedModelMat.data, modelMat); + expectMatricesEqual(expectedModelMat, modelMat); } TYPED_TEST(NodeTest, getsInverseModelMatrixComputedFromTransformationChain_SingleNode) { Node& node = this->template createObject(); - node.setTranslation(1.f, 2.f, 3.f); - node.setScaling(4.f, 5.f, 6.f); - node.setRotation(7.f, 8.f, 9.f); + node.setTranslation({1.f, 2.f, 3.f}); + node.setScaling({4.f, 5.f, 6.f}); + node.setRotation({7.f, 8.f, 9.f}, ERotationType::Euler_YZX); - const ramses_internal::Matrix44f transMat = ramses_internal::Matrix44f::Translation({ -1.f, -2.f, -3.f }); - const ramses_internal::Matrix44f scaleMat = ramses_internal::Matrix44f::Scaling({ 4.f, 5.f, 6.f }).inverse(); - const ramses_internal::Matrix44f rotMat = ramses_internal::Matrix44f::RotationEuler({ 7.f, 8.f, 9.f }, ramses_internal::ERotationConvention::Legacy_ZYX).transpose(); - const ramses_internal::Matrix44f expectedInverseModelMat = scaleMat * rotMat * transMat; + const auto transMat = glm::translate(glm::vec3{ -1.f, -2.f, -3.f }); + const auto scaleMat = glm::inverse(glm::scale(glm::vec3{ 4.f, 5.f, 6.f })); + const auto rotMat = glm::transpose(ramses_internal::Math3d::Rotation({ 7.f, 8.f, 9.f, 1.f }, ramses_internal::ERotationType::Euler_YZX)); + const auto expectedInverseModelMat = scaleMat * rotMat * transMat; - float inverseModelMat[16] = { 0.f }; + matrix44f inverseModelMat; EXPECT_EQ(StatusOK, node.getInverseModelMatrix(inverseModelMat)); - expectMatricesEqual(expectedInverseModelMat.data, inverseModelMat); + expectMatricesEqual(expectedInverseModelMat, inverseModelMat); } TYPED_TEST(NodeTest, getsInverseModelMatrixComputedFromTransformationChain_MultipleNodes) @@ -696,9 +679,9 @@ namespace ramses Node& scaleNode = this->template createObject(); Node& rotateNode = this->template createObject(); - translationNode.setTranslation(1.f, 2.f, 3.f); - scaleNode.setScaling(4.f, 5.f, 6.f); - rotateNode.setRotation(7.f, 8.f, 9.f); + translationNode.setTranslation({1.f, 2.f, 3.f}); + scaleNode.setScaling({4.f, 5.f, 6.f}); + rotateNode.setRotation({7.f, 8.f, 9.f}, ERotationType::Euler_YXY); translationNode.addChild(rotateNode); rotateNode.addChild(scaleNode); @@ -706,15 +689,15 @@ namespace ramses Node& node = this->createNode("node"); scaleNode.addChild(node); - const ramses_internal::Matrix44f transMat = ramses_internal::Matrix44f::Translation({ -1.f, -2.f, -3.f }); - const ramses_internal::Matrix44f scaleMat = ramses_internal::Matrix44f::Scaling({ 4.f, 5.f, 6.f }).inverse(); - const ramses_internal::Matrix44f rotMat = ramses_internal::Matrix44f::RotationEuler({ 7.f, 8.f, 9.f }, ramses_internal::ERotationConvention::Legacy_ZYX).transpose(); - const ramses_internal::Matrix44f expectedInverseModelMat = scaleMat * rotMat * transMat; + const auto transMat = glm::translate(glm::vec3{ -1.f, -2.f, -3.f }); + const auto scaleMat = glm::inverse(glm::scale(glm::vec3{ 4.f, 5.f, 6.f })); + const auto rotMat = glm::transpose(ramses_internal::Math3d::Rotation({ 7.f, 8.f, 9.f, 1.f }, ramses_internal::ERotationType::Euler_YXY)); + const auto expectedInverseModelMat = scaleMat * rotMat * transMat; - float inverseModelMat[16] = { 0.f }; + matrix44f inverseModelMat; EXPECT_EQ(StatusOK, node.getInverseModelMatrix(inverseModelMat)); - expectMatricesEqual(expectedInverseModelMat.data, inverseModelMat); + expectMatricesEqual(expectedInverseModelMat, inverseModelMat); } TYPED_TEST(NodeTest, instantiationDoesNotCreateMultipleLLNodes) @@ -726,14 +709,14 @@ namespace ramses // E.g. PickableObject depends on Camera which is a node as well and would be wrongly counted here. const size_t additionalAllocatedNodeCount = this->m_creationHelper.getAdditionalAllocatedNodeCount(); - EXPECT_EQ(1u, node.impl.getIScene().getNodeCount() - additionalAllocatedNodeCount); - EXPECT_TRUE(this->m_internalScene.isNodeAllocated(node.impl.getNodeHandle())); + EXPECT_EQ(1u, node.m_impl.getIScene().getNodeCount() - additionalAllocatedNodeCount); + EXPECT_TRUE(this->m_internalScene.isNodeAllocated(node.m_impl.getNodeHandle())); } TYPED_TEST(NodeTest, destructionDestroysLLNode) { Node& node = this->createNode("node"); - const auto handle = node.impl.getNodeHandle(); + const auto handle = node.m_impl.getNodeHandle(); this->m_scene.destroy(node); EXPECT_FALSE(this->m_internalScene.isNodeAllocated(handle)); } diff --git a/client/test/NodeTransformationTest.cpp b/client/test/NodeTransformationTest.cpp new file mode 100644 index 000000000..7d5d3eb21 --- /dev/null +++ b/client/test/NodeTransformationTest.cpp @@ -0,0 +1,243 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2015 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include + +#include "ramses-client-api/MeshNode.h" +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/OrthographicCamera.h" +#include "ramses-client-api/PickableObject.h" + +#include "ClientTestUtils.h" +#include "RamsesObjectTestTypes.h" +#include "TestEqualHelper.h" +#include "Math3d/Rotation.h" + +namespace ramses +{ + using namespace testing; + + template + class NodeTransformationTest : public LocalTestClientWithScene, public testing::Test + { + protected: + void SetUp() override + { + m_node = &this->template createObject("node"); + } + + T* m_node; + }; + + TYPED_TEST_SUITE(NodeTransformationTest, NodeTypes); + + TYPED_TEST(NodeTransformationTest, setTranslate) + { + vec3f initialTranslation(0.f, 0.f, 0.f); + vec3f actualTranslation; + EXPECT_EQ(StatusOK, this->m_node->getTranslation(actualTranslation)); + EXPECT_EQ(initialTranslation, actualTranslation); + + vec3f translationVector(1.2f, 2.3f, 4.5f); + EXPECT_EQ(StatusOK, this->m_node->setTranslation(translationVector)); + EXPECT_EQ(StatusOK, this->m_node->getTranslation(actualTranslation)); + EXPECT_EQ(translationVector, actualTranslation); + } + + TYPED_TEST(NodeTransformationTest, translate) + { + vec3f initialTranslation(0.f, 0.f, 0.f); + vec3f actualTranslation; + EXPECT_EQ(StatusOK, this->m_node->getTranslation(actualTranslation)); + EXPECT_EQ(initialTranslation, actualTranslation); + + vec3f translationVector(1.2f, 2.3f, 4.5f); + EXPECT_EQ(StatusOK, this->m_node->translate(translationVector)); + EXPECT_EQ(StatusOK, this->m_node->translate(translationVector)); + EXPECT_EQ(StatusOK, this->m_node->getTranslation(actualTranslation)); + EXPECT_EQ(2.f * translationVector, actualTranslation); + } + + TYPED_TEST(NodeTransformationTest, setRotation) + { + vec3f initialRotation(0.f, 0.f, 0.f); + vec3f actualRotation; + EXPECT_EQ(StatusOK, this->m_node->getRotation(actualRotation)); + EXPECT_EQ(ERotationType::Euler_XYZ, this->m_node->getRotationType()); + EXPECT_EQ(initialRotation, actualRotation); + + vec3f rotationVector_1(1.2f, 2.3f, 4.5f); + EXPECT_EQ(StatusOK, this->m_node->setRotation(rotationVector_1, ERotationType::Euler_ZYX)); + EXPECT_EQ(StatusOK, this->m_node->getRotation(actualRotation)); + EXPECT_EQ(rotationVector_1, actualRotation); + EXPECT_EQ(ERotationType::Euler_ZYX, this->m_node->getRotationType()); + + vec3f rotationVector_2(2.2f, 3.3f, 5.5f); + EXPECT_EQ(StatusOK, this->m_node->setRotation(rotationVector_2, ERotationType::Euler_ZYZ)); + EXPECT_EQ(StatusOK, this->m_node->getRotation(actualRotation)); + EXPECT_EQ(rotationVector_2, actualRotation); + EXPECT_EQ(ERotationType::Euler_ZYZ, this->m_node->getRotationType()); + } + + TYPED_TEST(NodeTransformationTest, setScaling) + { + vec3f initialScaling(1.f, 1.f, 1.f); + vec3f actualScale; + EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale)); + EXPECT_EQ(initialScaling, actualScale); + + vec3f scalingVector_1(1.2f, 2.3f, 4.5f); + EXPECT_EQ(StatusOK, this->m_node->setScaling(scalingVector_1)); + EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale)); + EXPECT_EQ(scalingVector_1, actualScale); + + vec3f scalingVector_2(2.2f, 3.3f, 5.5f); + EXPECT_EQ(StatusOK, this->m_node->setScaling(scalingVector_2)); + EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale)); + EXPECT_EQ(scalingVector_2, actualScale); + } + + TYPED_TEST(NodeTransformationTest, scale) + { + vec3f initialScaling(1.f, 1.f, 1.f); + vec3f actualScale; + EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale)); + EXPECT_EQ(initialScaling, actualScale); + + vec3f scalingVector_1(4.f, 6.f, 8.f); + EXPECT_EQ(StatusOK, this->m_node->scale(scalingVector_1)); + EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale)); + EXPECT_EQ(scalingVector_1, actualScale); + + vec3f scalingVector_2(0.5f, 0.5f, 0.5f); + EXPECT_EQ(StatusOK, this->m_node->scale(scalingVector_2)); + + vec3f resultVector(2.f, 3.f, 4.f); + EXPECT_EQ(StatusOK, this->m_node->getScaling(actualScale)); + EXPECT_EQ(resultVector, actualScale); + } + + TYPED_TEST(NodeTransformationTest, rotateMixConventions) + { + /* + node + (10, 0, 0, Euler_ZYX) + / \ + chlid0 child1 + (0, 20, 0, Euler_XYZ) (0, 20, 30, Euler_ZYZ) + | + grandChild + (0, 0, 30, Euler_XZY) + */ + TypeParam* child0 = &this->template createObject("child0 node"); + TypeParam* child1 = &this->template createObject("child1 node"); + TypeParam* grandChild = &this->template createObject("grand child node"); + this->m_node->addChild(*child0); + this->m_node->addChild(*child1); + child0->addChild(*grandChild); + + EXPECT_EQ(StatusOK, this->m_node->setRotation({10.f, 0.f, 0.f}, ramses::ERotationType::Euler_ZYX)); + EXPECT_EQ(StatusOK, child0->setRotation({0.f, 20.f, 0.f}, ramses::ERotationType::Euler_XYZ)); + EXPECT_EQ(StatusOK, child1->setRotation({0.f, 20.f, 30.f}, ramses::ERotationType::Euler_ZYZ)); + EXPECT_EQ(StatusOK, grandChild->setRotation({0.f, 0.f, 30.f}, ramses::ERotationType::Euler_XZY)); + + //expected matrices for rotation after transformation chain is applied + const auto expectedNodeRotationMatrix = ramses_internal::Math3d::Rotation({ 10.f , 0.f , 0.f, 1.f }, ramses_internal::ERotationType::Euler_ZYX); + const auto expectedChild0RorationMatrix = ramses_internal::Math3d::Rotation({ 10.f , 20.f, 0.f, 1.f }, ramses_internal::ERotationType::Euler_ZYX); + const auto expectedChild1RorationMatrix = ramses_internal::Math3d::Rotation({ 10.f , 20.f, 30.f, 1.f }, ramses_internal::ERotationType::Euler_ZYX); + const auto expectedGrandChildRorationMatrix = ramses_internal::Math3d::Rotation({ 10.f , 20.f, 30.f, 1.f }, ramses_internal::ERotationType::Euler_ZYX); + + matrix44f resultNodeMatrix; + matrix44f resultChild0Matrix; + matrix44f resultChild1Matrix; + matrix44f resultGrandChildMatrix; + this->m_node->getModelMatrix(resultNodeMatrix); + child0 ->getModelMatrix(resultChild0Matrix); + child1 ->getModelMatrix(resultChild1Matrix); + grandChild ->getModelMatrix(resultGrandChildMatrix); + + ramses_internal::expectMatrixFloatEqual(expectedNodeRotationMatrix , resultNodeMatrix); + ramses_internal::expectMatrixFloatEqual(expectedChild0RorationMatrix , resultChild0Matrix); + ramses_internal::expectMatrixFloatEqual(expectedChild1RorationMatrix , resultChild1Matrix); + ramses_internal::expectMatrixFloatEqual(expectedGrandChildRorationMatrix , resultGrandChildMatrix); + } + + template + class NodeTransformationTestWithPublishedScene : public LocalTestClientWithScene, public testing::Test + { + protected: + void SetUp() override + { + const ramses_internal::IScene& iscene = this->m_scene.m_impl.getIScene(); + ramses_internal::SceneInfo info(iscene.getSceneId(), iscene.getName()); + EXPECT_CALL(this->sceneActionsCollector, handleNewSceneAvailable(info, _)); + EXPECT_CALL(this->sceneActionsCollector, handleInitializeScene(info, _)); + EXPECT_EQ(StatusOK, m_scene.publish(EScenePublicationMode::LocalOnly)); + + m_node = &this->template createObject("node"); + } + + void TearDown() override + { + EXPECT_CALL(this->sceneActionsCollector, handleSceneBecameUnavailable(ramses_internal::SceneId(this->m_scene.m_impl.getSceneId().getValue()), _)); + EXPECT_EQ(StatusOK, m_scene.unpublish()); + } + + T* m_node; + }; + + TYPED_TEST_SUITE(NodeTransformationTestWithPublishedScene, NodeTypes); + + TYPED_TEST(NodeTransformationTestWithPublishedScene, setTranslateWithValuesEqualToCurrentValuesDoesNotCreateSceneActions) + { + vec3f translationVector(1.2f, 2.3f, 4.5f); + EXPECT_CALL(this->sceneActionsCollector, handleSceneUpdate_rvr(ramses_internal::SceneId(this->m_scene.getSceneId().getValue()), _, _)); + EXPECT_EQ(StatusOK, this->m_node->setTranslation(translationVector)); + this->m_scene.flush(); + EXPECT_LE(1u, this->sceneActionsCollector.getNumberOfActions()); + + Mock::VerifyAndClearExpectations(this); + this->sceneActionsCollector.resetCollecting(); + + EXPECT_EQ(StatusOK, this->m_node->setTranslation(translationVector)); + this->m_scene.flush(); + EXPECT_EQ(0u, this->sceneActionsCollector.getNumberOfActions()); // flush empty and optimized away + } + + TYPED_TEST(NodeTransformationTestWithPublishedScene, setRotationWithValuesEqualToCurrentValuesDoesNotCreateSceneActions) + { + vec3f rotationVector(1.2f, 2.3f, 4.5f); + EXPECT_CALL(this->sceneActionsCollector, handleSceneUpdate_rvr(ramses_internal::SceneId(this->m_scene.getSceneId().getValue()), _, _)); + EXPECT_EQ(StatusOK, this->m_node->setRotation(rotationVector, ERotationType::Euler_YXZ)); + this->m_scene.flush(); + EXPECT_LE(1u, this->sceneActionsCollector.getNumberOfActions()); + + Mock::VerifyAndClearExpectations(this); + this->sceneActionsCollector.resetCollecting(); + + EXPECT_EQ(StatusOK, this->m_node->setRotation(rotationVector, ERotationType::Euler_YXZ)); + this->m_scene.flush(); + EXPECT_EQ(0u, this->sceneActionsCollector.getNumberOfActions()); // flush empty and optimized away + } + + TYPED_TEST(NodeTransformationTestWithPublishedScene, setScalingWithValuesEqualToCurrentValuesDoesNotCreateSceneActions) + { + vec3f scalingVector(1.2f, 2.3f, 4.5f); + EXPECT_CALL(this->sceneActionsCollector, handleSceneUpdate_rvr(ramses_internal::SceneId(this->m_scene.getSceneId().getValue()), _, _)); + EXPECT_EQ(StatusOK, this->m_node->setScaling(scalingVector)); + this->m_scene.flush(); + EXPECT_LE(1u, this->sceneActionsCollector.getNumberOfActions()); + + Mock::VerifyAndClearExpectations(this); + this->sceneActionsCollector.resetCollecting(); + + EXPECT_EQ(StatusOK, this->m_node->setScaling(scalingVector)); + this->m_scene.flush(); + EXPECT_EQ(0u, this->sceneActionsCollector.getNumberOfActions()); // flush empty and optimized away + } +} diff --git a/client/ramses-client/test/NodeVisibilityTest.cpp b/client/test/NodeVisibilityTest.cpp similarity index 80% rename from client/ramses-client/test/NodeVisibilityTest.cpp rename to client/test/NodeVisibilityTest.cpp index 915d9478f..139bdf35a 100644 --- a/client/ramses-client/test/NodeVisibilityTest.cpp +++ b/client/test/NodeVisibilityTest.cpp @@ -28,7 +28,7 @@ namespace ramses class ANodeVisibilityTest : public LocalTestClientWithScene, public testing::Test { protected: - virtual void SetUp() override + void SetUp() override { m_visibilityNode = &this->template createObject("node"); m_parentVisNode = &this->template createObject("parent"); @@ -39,7 +39,7 @@ namespace ramses void testFlattenedVisibility(EVisibilityMode mode) { - EXPECT_EQ(this->m_childMesh->impl.getFlattenedVisibility(), mode); + EXPECT_EQ(this->m_childMesh->m_impl.getFlattenedVisibility(), mode); } T* m_parentVisNode; @@ -51,45 +51,45 @@ namespace ramses TYPED_TEST(ANodeVisibilityTest, isVisibleInitially) { - EXPECT_EQ(this->m_visibilityNode->impl.getVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(this->m_visibilityNode->m_impl.getVisibility(), EVisibilityMode::Visible); } TYPED_TEST(ANodeVisibilityTest, canChangeVisibility) { this->m_visibilityNode->setVisibility(EVisibilityMode::Invisible); - EXPECT_EQ(this->m_visibilityNode->impl.getVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(this->m_visibilityNode->m_impl.getVisibility(), EVisibilityMode::Invisible); this->m_visibilityNode->setVisibility(EVisibilityMode::Off); - EXPECT_EQ(this->m_visibilityNode->impl.getVisibility(), EVisibilityMode::Off); + EXPECT_EQ(this->m_visibilityNode->m_impl.getVisibility(), EVisibilityMode::Off); } TYPED_TEST(ANodeVisibilityTest, isMarkedDirtyWhenChangingVisibility) { this->m_visibilityNode->setVisibility(EVisibilityMode::Invisible); - EXPECT_TRUE(this->m_visibilityNode->impl.isDirty()); + EXPECT_TRUE(this->m_visibilityNode->m_impl.isDirty()); } TYPED_TEST(ANodeVisibilityTest, staysCleanWhenSettingTheSameVisibility) { this->m_visibilityNode->setVisibility(EVisibilityMode::Invisible); - EXPECT_TRUE(this->m_visibilityNode->impl.isDirty()); + EXPECT_TRUE(this->m_visibilityNode->m_impl.isDirty()); this->m_scene.flush(); // to clear dirty state this->m_visibilityNode->setVisibility(EVisibilityMode::Invisible); - EXPECT_FALSE(this->m_visibilityNode->impl.isDirty()); + EXPECT_FALSE(this->m_visibilityNode->m_impl.isDirty()); } TYPED_TEST(ANodeVisibilityTest, confidenceTest_isMarkedDirtyWhenChangingVisibilityMultipleTimes) { this->m_visibilityNode->setVisibility(EVisibilityMode::Invisible); - EXPECT_TRUE(this->m_visibilityNode->impl.isDirty()); + EXPECT_TRUE(this->m_visibilityNode->m_impl.isDirty()); this->m_scene.flush(); // to clear dirty state this->m_visibilityNode->setVisibility(EVisibilityMode::Visible); - EXPECT_TRUE(this->m_visibilityNode->impl.isDirty()); + EXPECT_TRUE(this->m_visibilityNode->m_impl.isDirty()); this->m_scene.flush(); // to clear dirty state this->m_visibilityNode->setVisibility(EVisibilityMode::Off); - EXPECT_TRUE(this->m_visibilityNode->impl.isDirty()); + EXPECT_TRUE(this->m_visibilityNode->m_impl.isDirty()); } TYPED_TEST(ANodeVisibilityTest, hasFlattenedVisibilitySetInitially) @@ -102,7 +102,7 @@ namespace ramses TYPED_TEST(ANodeVisibilityTest, visibilityPropagatesFromParentToChildrenOnFlush) { this->m_visibilityNode->setVisibility(EVisibilityMode::Invisible); - EXPECT_TRUE(this->m_visibilityNode->impl.isDirty()); + EXPECT_TRUE(this->m_visibilityNode->m_impl.isDirty()); this->m_scene.flush(); this->testFlattenedVisibility(EVisibilityMode::Invisible); @@ -111,7 +111,7 @@ namespace ramses TYPED_TEST(ANodeVisibilityTest, offVisibilityPropagatesFromParentToChildrenOnFlush) { this->m_visibilityNode->setVisibility(EVisibilityMode::Off); - EXPECT_TRUE(this->m_visibilityNode->impl.isDirty()); + EXPECT_TRUE(this->m_visibilityNode->m_impl.isDirty()); this->m_scene.flush(); this->testFlattenedVisibility(EVisibilityMode::Off); @@ -120,7 +120,7 @@ namespace ramses TYPED_TEST(ANodeVisibilityTest, visibilityPropagatesFromGrandParentToChildrenOnFlush) { this->m_parentVisNode->setVisibility(EVisibilityMode::Invisible); - EXPECT_TRUE(this->m_visibilityNode->impl.isDirty()); + EXPECT_TRUE(this->m_visibilityNode->m_impl.isDirty()); this->m_scene.flush(); this->testFlattenedVisibility(EVisibilityMode::Invisible); @@ -129,7 +129,7 @@ namespace ramses TYPED_TEST(ANodeVisibilityTest, offVisibilityPropagatesFromGrandParentToChildrenOnFlush) { this->m_parentVisNode->setVisibility(EVisibilityMode::Off); - EXPECT_TRUE(this->m_visibilityNode->impl.isDirty()); + EXPECT_TRUE(this->m_visibilityNode->m_impl.isDirty()); this->m_scene.flush(); this->testFlattenedVisibility(EVisibilityMode::Off); @@ -152,9 +152,9 @@ namespace ramses { this->m_childMesh->setVisibility(EVisibilityMode::Invisible); this->m_scene.flush(); - EXPECT_EQ(this->m_childMesh->impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(this->m_childMesh->m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); this->m_childMesh->setVisibility(EVisibilityMode::Off); this->m_scene.flush(); - EXPECT_EQ(this->m_childMesh->impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(this->m_childMesh->m_impl.getFlattenedVisibility(), EVisibilityMode::Off); } } diff --git a/client/ramses-client/test/PickableObjectTest.cpp b/client/test/PickableObjectTest.cpp similarity index 90% rename from client/ramses-client/test/PickableObjectTest.cpp rename to client/test/PickableObjectTest.cpp index 877478179..86651c2f8 100644 --- a/client/ramses-client/test/PickableObjectTest.cpp +++ b/client/test/PickableObjectTest.cpp @@ -21,14 +21,14 @@ using namespace ramses_internal; namespace ramses { - class APickableObject : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test + class APickableObject : public LocalTestClientWithScene, public testing::Test { protected: APickableObject() - : LocalTestClientWithSceneAndAnimationSystem() + : LocalTestClientWithScene() , geometryBuffer(*createGeometryBuffer()) , pickableObject(*m_scene.createPickableObject(geometryBuffer, pickableObjectId_t(1u))) - , pickableObjectHandle(pickableObject.impl.getPickableObjectHandle()) + , pickableObjectHandle(pickableObject.m_impl.getPickableObjectHandle()) { } @@ -47,8 +47,8 @@ namespace ramses void setGeometryBufferData() { - const float data[] = { 0.f, 0.f, 0.f }; - geometryBuffer.updateData(0u, 1u, data); + const vec3f dummyVec{ 0.f, 0.f, 0.f }; + geometryBuffer.updateData(0u, 1u, &dummyVec); } PerspectiveCamera* pickableCamera = nullptr; @@ -64,7 +64,7 @@ namespace ramses const auto cam = pickableObject.getCamera(); EXPECT_EQ(cam, pickableObject.getCamera()); - EXPECT_EQ(cam->impl.getCameraHandle(), pickableCamera->impl.getCameraHandle()); + EXPECT_EQ(cam->m_impl.getCameraHandle(), pickableCamera->m_impl.getCameraHandle()); } TEST_F(APickableObject, CannotSetPickableCameraFromAnotherScene) @@ -89,7 +89,7 @@ namespace ramses { const ArrayBuffer& geoBuffer = pickableObject.getGeometryBuffer(); EXPECT_EQ(&geoBuffer, &geometryBuffer); - EXPECT_EQ(geoBuffer.impl.getDataBufferHandle(), geometryBuffer.impl.getDataBufferHandle()); + EXPECT_EQ(geoBuffer.m_impl.getDataBufferHandle(), geometryBuffer.m_impl.getDataBufferHandle()); } TEST_F(APickableObject, CanSetAndGetPickableObjectId) diff --git a/client/ramses-client/test/QuadTest.cpp b/client/test/QuadTest.cpp similarity index 100% rename from client/ramses-client/test/QuadTest.cpp rename to client/test/QuadTest.cpp diff --git a/client/ramses-client/test/RamsesClientTest.cpp b/client/test/RamsesClientTest.cpp similarity index 83% rename from client/ramses-client/test/RamsesClientTest.cpp rename to client/test/RamsesClientTest.cpp index 2cfc3c47a..e793a92d7 100644 --- a/client/ramses-client/test/RamsesClientTest.cpp +++ b/client/test/RamsesClientTest.cpp @@ -19,7 +19,6 @@ #include "ClientTestUtils.h" #include "ClientApplicationLogic.h" #include "SceneAPI/SceneId.h" -#include "Collections/String.h" #include "ClientEventHandlerMock.h" #include "SceneReferencing/SceneReferenceEvent.h" @@ -50,7 +49,7 @@ namespace ramses } protected: - RamsesFramework m_framework; + RamsesFramework m_framework{ RamsesFrameworkConfig{EFeatureLevel_Latest} }; RamsesClient& m_client; }; @@ -107,29 +106,33 @@ namespace ramses TEST(RamsesClient, canCreateClientWithNULLNameAndCmdLineArguments) { - ramses::RamsesFramework framework(0, static_cast(nullptr)); - EXPECT_NE(framework.createClient(nullptr), nullptr); + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework framework{config}; + EXPECT_NE(framework.createClient({}), nullptr); EXPECT_FALSE(framework.isConnected()); } TEST(RamsesClient, canCreateClientWithoutCmdLineArguments) { - ramses::RamsesFramework framework; - EXPECT_NE(framework.createClient(nullptr), nullptr); + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework framework{config}; + EXPECT_NE(framework.createClient({}), nullptr); EXPECT_FALSE(framework.isConnected()); } TEST(RamsesClient, createClientFailsWhenConnected) { - ramses::RamsesFramework framework; + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework framework{config}; EXPECT_EQ(ramses::StatusOK, framework.connect()); - EXPECT_EQ(framework.createClient(nullptr), nullptr); + EXPECT_EQ(framework.createClient({}), nullptr); } TEST(RamsesClient, destroyClientFailsWhenConnected) { - ramses::RamsesFramework framework; - auto* client = framework.createClient(nullptr); + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework framework{config}; + auto* client = framework.createClient({}); ASSERT_NE(client, nullptr); EXPECT_EQ(ramses::StatusOK, framework.connect()); EXPECT_NE(ramses::StatusOK, framework.destroyClient(*client)); @@ -221,24 +224,24 @@ namespace ramses using ramses_internal::SceneInfo; ramses_internal::SceneId internalSceneId(sceneId.getValue()); - EXPECT_CALL(sceneActionsCollector, handleNewSceneAvailable(SceneInfo(internalSceneId, ramses_internal::String(scene->getName())), _)); + EXPECT_CALL(sceneActionsCollector, handleNewSceneAvailable(SceneInfo(internalSceneId, scene->getName()), _)); EXPECT_CALL(sceneActionsCollector, handleInitializeScene(_, _)); EXPECT_CALL(sceneActionsCollector, handleSceneUpdate_rvr(ramses_internal::SceneId(sceneId.getValue()), _, _)); EXPECT_CALL(sceneActionsCollector, handleSceneBecameUnavailable(internalSceneId, _)); - scene->publish(ramses::EScenePublicationMode_LocalOnly); + scene->publish(ramses::EScenePublicationMode::LocalOnly); scene->flush(); - EXPECT_TRUE(client.impl.getClientApplication().isScenePublished(internalSceneId)); + EXPECT_TRUE(client.m_impl.getClientApplication().isScenePublished(internalSceneId)); client.destroy(*scene); - EXPECT_FALSE(client.impl.getClientApplication().isScenePublished(internalSceneId)); + EXPECT_FALSE(client.m_impl.getClientApplication().isScenePublished(internalSceneId)); } TEST_F(ALocalRamsesClient, returnsNullptrOnFindSceneReferenceIfThereIsNoSceneReference) { - EXPECT_EQ(client.impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 456 }), nullptr); + EXPECT_EQ(client.m_impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 456 }), nullptr); client.createScene(sceneId_t{ 123 }); - EXPECT_EQ(client.impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 456 }), nullptr); + EXPECT_EQ(client.m_impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 456 }), nullptr); } TEST_F(ALocalRamsesClient, returnsNullptrOnFindSceneReferenceIfWrongReferencedSceneIdIsProvided) @@ -246,7 +249,7 @@ namespace ramses auto scene = client.createScene(sceneId_t{ 123 }); scene->createSceneReference(sceneId_t{ 456 }); - EXPECT_EQ(client.impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 1 }), nullptr); + EXPECT_EQ(client.m_impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 1 }), nullptr); } TEST_F(ALocalRamsesClient, returnsNullptrOnFindSceneReferenceIfWrongMasterSceneIdIsProvided) @@ -256,9 +259,9 @@ namespace ramses auto scene2 = client.createScene(sceneId_t{ 1234 }); scene2->createSceneReference(sceneId_t{ 4567 }); - EXPECT_EQ(client.impl.findSceneReference(sceneId_t{ 1 }, sceneId_t{ 456 }), nullptr); - EXPECT_EQ(client.impl.findSceneReference(sceneId_t{ 1234 }, sceneId_t{ 456 }), nullptr); - EXPECT_EQ(client.impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 4567 }), nullptr); + EXPECT_EQ(client.m_impl.findSceneReference(sceneId_t{ 1 }, sceneId_t{ 456 }), nullptr); + EXPECT_EQ(client.m_impl.findSceneReference(sceneId_t{ 1234 }, sceneId_t{ 456 }), nullptr); + EXPECT_EQ(client.m_impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 4567 }), nullptr); } TEST_F(ALocalRamsesClient, returnsSceneReferenceOnFindSceneReference) @@ -269,9 +272,9 @@ namespace ramses auto sr2 = scene->createSceneReference(sceneId_t{ 12345 }); auto sr3 = scene2->createSceneReference(sceneId_t{ 12345 }); - EXPECT_EQ(client.impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 456 }), sr); - EXPECT_EQ(client.impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 12345 }), sr2); - EXPECT_EQ(client.impl.findSceneReference(sceneId_t{ 1234 }, sceneId_t{ 12345 }), sr3); + EXPECT_EQ(client.m_impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 456 }), sr); + EXPECT_EQ(client.m_impl.findSceneReference(sceneId_t{ 123 }, sceneId_t{ 12345 }), sr2); + EXPECT_EQ(client.m_impl.findSceneReference(sceneId_t{ 1234 }, sceneId_t{ 12345 }), sr3); } TEST_F(ALocalRamsesClient, callsAppropriateNotificationForSceneStateChangedEvent) @@ -287,7 +290,7 @@ namespace ramses event.type = ramses_internal::SceneReferenceEventType::SceneStateChanged; event.sceneState = ramses_internal::RendererSceneState::Rendered; - client.impl.getClientApplication().handleSceneReferenceEvent(event, {}); + client.m_impl.getClientApplication().handleSceneReferenceEvent(event, {}); testing::StrictMock handler; EXPECT_CALL(handler, sceneReferenceStateChanged(_, RendererSceneState::Rendered)).WillOnce([sr](SceneReference& ref, RendererSceneState) @@ -310,7 +313,7 @@ namespace ramses event.type = ramses_internal::SceneReferenceEventType::SceneFlushed; event.tag = ramses_internal::SceneVersionTag{ 567 }; - client.impl.getClientApplication().handleSceneReferenceEvent(event, {}); + client.m_impl.getClientApplication().handleSceneReferenceEvent(event, {}); testing::StrictMock handler; EXPECT_CALL(handler, sceneReferenceFlushed(_, sceneVersionTag_t{ 567 })).WillOnce([sr](SceneReference& ref, sceneVersionTag_t) @@ -336,7 +339,7 @@ namespace ramses event.dataConsumer = ramses_internal::DataSlotId{ 987 }; event.status = false; - client.impl.getClientApplication().handleSceneReferenceEvent(event, {}); + client.m_impl.getClientApplication().handleSceneReferenceEvent(event, {}); testing::StrictMock handler; EXPECT_CALL(handler, dataLinked(sceneId_t{ masterScene.getValue() }, dataProviderId_t{ 123 }, sceneId_t{ reffedScene.getValue() }, dataConsumerId_t{ 987 }, false)); @@ -357,7 +360,7 @@ namespace ramses event.dataConsumer = ramses_internal::DataSlotId{ 987 }; event.status = true; - client.impl.getClientApplication().handleSceneReferenceEvent(event, {}); + client.m_impl.getClientApplication().handleSceneReferenceEvent(event, {}); testing::StrictMock handler; EXPECT_CALL(handler, dataUnlinked(sceneId_t{ reffedScene.getValue() }, dataConsumerId_t{ 987 }, true)); @@ -376,11 +379,11 @@ namespace ramses event.referencedScene = reffedScene; event.type = ramses_internal::SceneReferenceEventType::SceneFlushed; event.tag = ramses_internal::SceneVersionTag{ 567 }; - client.impl.getClientApplication().handleSceneReferenceEvent(event, {}); + client.m_impl.getClientApplication().handleSceneReferenceEvent(event, {}); event.tag = ramses_internal::SceneVersionTag{ 568 }; - client.impl.getClientApplication().handleSceneReferenceEvent(event, {}); + client.m_impl.getClientApplication().handleSceneReferenceEvent(event, {}); event.tag = ramses_internal::SceneVersionTag{ 569 }; - client.impl.getClientApplication().handleSceneReferenceEvent(event, {}); + client.m_impl.getClientApplication().handleSceneReferenceEvent(event, {}); testing::StrictMock handler; EXPECT_CALL(handler, sceneReferenceFlushed(_, sceneVersionTag_t{ 567 })).WillOnce([sr](SceneReference& ref, sceneVersionTag_t) @@ -410,13 +413,13 @@ namespace ramses event.type = ramses_internal::SceneReferenceEventType::SceneFlushed; event.tag = ramses_internal::SceneVersionTag{ 567 }; - client.impl.getClientApplication().handleSceneReferenceEvent(event, {}); + client.m_impl.getClientApplication().handleSceneReferenceEvent(event, {}); event.referencedScene = reffedScene; event.type = ramses_internal::SceneReferenceEventType::SceneFlushed; event.tag = ramses_internal::SceneVersionTag{ 567 }; - client.impl.getClientApplication().handleSceneReferenceEvent(event, {}); + client.m_impl.getClientApplication().handleSceneReferenceEvent(event, {}); testing::StrictMock handler; client.dispatchEvents(handler); @@ -424,7 +427,8 @@ namespace ramses TEST(ARamsesFrameworkImplInAClientLib, canCreateAClient) { - ramses::RamsesFramework fw; + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework fw{config}; auto client = fw.createClient("client"); EXPECT_NE(client, nullptr); @@ -432,7 +436,8 @@ namespace ramses TEST(ARamsesFrameworkImplInAClientLib, canCreateMultipleClients) { - ramses::RamsesFramework fw; + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework fw{config}; auto client1 = fw.createClient("first client"); auto client2 = fw.createClient("second client"); @@ -442,7 +447,8 @@ namespace ramses TEST(ARamsesFrameworkImplInAClientLib, acceptsLocallyCreatedClientsForDestruction) { - ramses::RamsesFramework fw; + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework fw{config}; auto client1 = fw.createClient("first client"); auto client2 = fw.createClient("second client"); @@ -452,8 +458,9 @@ namespace ramses TEST(ARamsesFrameworkImplInAClientLib, doesNotAcceptForeignCreatedClientsForDestruction) { - ramses::RamsesFramework fw1; - ramses::RamsesFramework fw2; + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework fw1{config}; + RamsesFramework fw2{config}; auto client1 = fw1.createClient("first client"); auto client2 = fw2.createClient("second client"); @@ -463,7 +470,8 @@ namespace ramses TEST(ARamsesFrameworkImplInAClientLib, doesNotAcceptSameClientTwiceForDestruction) { - ramses::RamsesFramework fw; + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework fw{config}; auto client = fw.createClient("client"); EXPECT_EQ(StatusOK, fw.destroyClient(*client)); @@ -472,7 +480,8 @@ namespace ramses TEST(ARamsesFrameworkImplInAClientLib, canCreateDestroyAndRecreateAClient) { - ramses::RamsesFramework fw; + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework fw{config}; auto client = fw.createClient("client"); EXPECT_NE(nullptr, client); diff --git a/client/ramses-client/test/RamsesObjectOwnershipTest.cpp b/client/test/RamsesObjectOwnershipTest.cpp similarity index 77% rename from client/ramses-client/test/RamsesObjectOwnershipTest.cpp rename to client/test/RamsesObjectOwnershipTest.cpp index 9e67331a2..255b9c15e 100644 --- a/client/ramses-client/test/RamsesObjectOwnershipTest.cpp +++ b/client/test/RamsesObjectOwnershipTest.cpp @@ -8,8 +8,6 @@ #include -#include "ramses-client-api/AnimationSystemRealTime.h" -#include "ramses-client-api/AnimationSystem.h" #include "ramses-client-api/PerspectiveCamera.h" #include "ramses-client-api/OrthographicCamera.h" #include "ramses-client-api/GeometryBinding.h" @@ -27,18 +25,7 @@ #include "ramses-client-api/Texture3D.h" #include "ramses-client-api/TextureCube.h" #include "ramses-client-api/EffectDescription.h" -#include "ramses-client-api/DataFloat.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-client-api/DataVector4f.h" -#include "ramses-client-api/DataMatrix22f.h" -#include "ramses-client-api/DataMatrix33f.h" -#include "ramses-client-api/DataMatrix44f.h" -#include "ramses-client-api/DataInt32.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector3i.h" -#include "ramses-client-api/DataVector4i.h" -#include "ramses-client-api/StreamTexture.h" +#include "ramses-client-api/DataObject.h" #include "ramses-client-api/ArrayBuffer.h" #include "ramses-client-api/Texture2DBuffer.h" #include "ramses-client-api/PickableObject.h" @@ -47,7 +34,6 @@ #include "SceneImpl.h" #include "ClientTestUtils.h" #include "RamsesObjectTestTypes.h" -#include "AnimationSystemImpl.h" #include "NodeImpl.h" #include "MeshNodeImpl.h" #include "CameraNodeImpl.h" @@ -59,7 +45,6 @@ #include "RenderTargetImpl.h" #include "TextureSamplerImpl.h" #include "DataObjectImpl.h" -#include "StreamTextureImpl.h" #include "ArrayBufferImpl.h" #include "Texture2DBufferImpl.h" #include "PickableObjectImpl.h" @@ -77,17 +62,17 @@ namespace ramses using namespace testing; template - class SceneOwnershipTest : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test + class SceneOwnershipTest : public LocalTestClientWithScene, public testing::Test { public: - SceneOwnershipTest() : LocalTestClientWithSceneAndAnimationSystem() + SceneOwnershipTest() : LocalTestClientWithScene() { } void expectNoFrameworkObjectsAllocated() { m_creationHelper.destroyAdditionalAllocatedSceneObjects(); - const ramses_internal::IScene& scene = this->m_scene.impl.getIScene(); + const ramses_internal::IScene& scene = this->m_scene.m_impl.getIScene(); for (ramses_internal::NodeHandle i(0); i < scene.getNodeCount(); ++i) { @@ -151,17 +136,17 @@ namespace ramses { auto obj = &this->template createObject("objectName"); ASSERT_TRUE(nullptr != obj); - EXPECT_TRUE(obj->isOfType(ERamsesObjectType_SceneObject)); - EXPECT_TRUE(obj->isOfType(ERamsesObjectType_ClientObject)); + EXPECT_TRUE(obj->isOfType(ERamsesObjectType::SceneObject)); + EXPECT_TRUE(obj->isOfType(ERamsesObjectType::ClientObject)); } TYPED_TEST(SceneOwnershipTest, sceneObjectsHaveReferenceToTheirScene) { auto obj = &this->template createObject("objectName"); ASSERT_TRUE(nullptr != obj); - EXPECT_EQ(&this->m_scene.impl, &obj->impl.getSceneImpl()); - EXPECT_EQ(&this->m_scene.impl.getIScene(), &obj->impl.getIScene()); - EXPECT_EQ(&this->client.impl, &obj->impl.getClientImpl()); + EXPECT_EQ(&this->m_scene.m_impl, &obj->m_impl.getSceneImpl()); + EXPECT_EQ(&this->m_scene.m_impl.getIScene(), &obj->m_impl.getIScene()); + EXPECT_EQ(&this->client.m_impl, &obj->m_impl.getClientImpl()); } TYPED_TEST(SceneOwnershipTest, sceneContainsCreatedObject) @@ -191,26 +176,26 @@ namespace ramses TYPED_TEST(SceneOwnershipTest, creatingAndDestroyingObjectsUpdatesStatisticCounter) { - this->m_scene.impl.getStatisticCollection().nextTimeInterval(); //object number is updated by nextTimeInterval() - ramses_internal::UInt32 initialNumber = this->m_scene.impl.getStatisticCollection().statObjectsCount.getCounterValue(); - EXPECT_EQ(0u, this->m_scene.impl.getStatisticCollection().statObjectsCreated.getCounterValue()); - EXPECT_EQ(0u, this->m_scene.impl.getStatisticCollection().statObjectsDestroyed.getCounterValue()); + this->m_scene.m_impl.getStatisticCollection().nextTimeInterval(); //object number is updated by nextTimeInterval() + uint32_t initialNumber = this->m_scene.m_impl.getStatisticCollection().statObjectsCount.getCounterValue(); + EXPECT_EQ(0u, this->m_scene.m_impl.getStatisticCollection().statObjectsCreated.getCounterValue()); + EXPECT_EQ(0u, this->m_scene.m_impl.getStatisticCollection().statObjectsDestroyed.getCounterValue()); auto obj = &this->template createObject("objectName"); - ramses_internal::UInt32 numberCreated = this->m_scene.impl.getStatisticCollection().statObjectsCreated.getCounterValue(); + uint32_t numberCreated = this->m_scene.m_impl.getStatisticCollection().statObjectsCreated.getCounterValue(); EXPECT_LE(1u, numberCreated); //some types create multiple scene objects (e.g. RenderTarget) - EXPECT_EQ(0u, this->m_scene.impl.getStatisticCollection().statObjectsDestroyed.getCounterValue()); + EXPECT_EQ(0u, this->m_scene.m_impl.getStatisticCollection().statObjectsDestroyed.getCounterValue()); - this->m_scene.impl.getStatisticCollection().nextTimeInterval(); - EXPECT_EQ(initialNumber + numberCreated, this->m_scene.impl.getStatisticCollection().statObjectsCount.getCounterValue()); + this->m_scene.m_impl.getStatisticCollection().nextTimeInterval(); + EXPECT_EQ(initialNumber + numberCreated, this->m_scene.m_impl.getStatisticCollection().statObjectsCount.getCounterValue()); this->m_scene.destroy(*obj); - EXPECT_LE(1u, this->m_scene.impl.getStatisticCollection().statObjectsDestroyed.getCounterValue()); + EXPECT_LE(1u, this->m_scene.m_impl.getStatisticCollection().statObjectsDestroyed.getCounterValue()); - this->m_scene.impl.getStatisticCollection().nextTimeInterval(); - EXPECT_GT(initialNumber + numberCreated, this->m_scene.impl.getStatisticCollection().statObjectsCount.getCounterValue()); - EXPECT_LE(initialNumber, this->m_scene.impl.getStatisticCollection().statObjectsCount.getCounterValue()); + this->m_scene.m_impl.getStatisticCollection().nextTimeInterval(); + EXPECT_GT(initialNumber + numberCreated, this->m_scene.m_impl.getStatisticCollection().statObjectsCount.getCounterValue()); + EXPECT_LE(initialNumber, this->m_scene.m_impl.getStatisticCollection().statObjectsCount.getCounterValue()); } TYPED_TEST(ClientOwnershipTest, clientContainsCreatedObject) @@ -245,14 +230,14 @@ namespace ramses { const ClientObject* obj = &this->template createObject("objectName"); ASSERT_TRUE(nullptr != obj); - EXPECT_TRUE(obj->isOfType(ERamsesObjectType_ClientObject)); + EXPECT_TRUE(obj->isOfType(ERamsesObjectType::ClientObject)); } TYPED_TEST(ClientOwnershipTest, clientObjectsHaveReferenceToTheirClient) { const ClientObject* obj = &this->template createObject("objectName"); ASSERT_TRUE(nullptr != obj); - EXPECT_EQ(&this->client.impl, &obj->impl.getClientImpl()); + EXPECT_EQ(&this->client.m_impl, &obj->m_impl.getClientImpl()); } TYPED_TEST(ClientOwnershipTest, clientObjectNameChanged) @@ -280,6 +265,7 @@ namespace ramses RamsesObject* obj = &this->template createObject("objectName"); TypeParam& objTyped = static_cast(*obj); ASSERT_TRUE(obj); + this->client.destroy(objTyped); RamsesObject* sn = this->client.findSceneByName("objectName"); ASSERT_FALSE(sn); diff --git a/client/test/RamsesObjectRegistryTest.cpp b/client/test/RamsesObjectRegistryTest.cpp new file mode 100644 index 000000000..3aeb1206f --- /dev/null +++ b/client/test/RamsesObjectRegistryTest.cpp @@ -0,0 +1,182 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2015 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include +#include "ramses-client-api/RamsesObject.h" +#include "RamsesObjectRegistry.h" +#include "NodeImpl.h" +#include "ramses-client-api/Node.h" +#include "ClientTestUtils.h" +#include + +namespace ramses +{ + using namespace testing; + + class ARamsesObjectRegistry : public testing::Test + { + public: + Node* createAndRegisterDummyObject() + { + return &m_registry.createAndRegisterObject(std::make_unique(m_dummyScene.getScene().m_impl, ERamsesObjectType::Node, "")); + } + + protected: + LocalTestClientWithScene m_dummyScene; + RamsesObjectRegistry m_registry; + }; + + TEST_F(ARamsesObjectRegistry, isEmptyUponCreation) + { + RamsesObjectVector objects; + m_registry.getObjectsOfType(objects, ERamsesObjectType::RamsesObject); + EXPECT_TRUE(objects.empty()); + EXPECT_EQ(0u, m_registry.getNumberOfObjects(ERamsesObjectType::Node)); + } + + TEST_F(ARamsesObjectRegistry, canAddObject) + { + auto dummyObject = createAndRegisterDummyObject(); + dummyObject->setName("name"); + EXPECT_EQ(dummyObject, m_registry.findObjectByName("name")); + EXPECT_EQ(dummyObject, m_registry.findObjectById(sceneObjectId_t{1u})); + EXPECT_EQ(dummyObject, &dummyObject->m_impl.getRamsesObject()); + EXPECT_EQ(1u, m_registry.getNumberOfObjects(ERamsesObjectType::Node)); + EXPECT_EQ(0u, m_registry.getNumberOfObjects(ERamsesObjectType::RenderBuffer)); + RamsesObjectVector objects; + m_registry.getObjectsOfType(objects, ERamsesObjectType::RamsesObject); + ASSERT_EQ(1u, objects.size()); + EXPECT_EQ(dummyObject, objects[0]); + } + + TEST_F(ARamsesObjectRegistry, sceneObjectsGetUniqueIds) + { + const auto dummy1(createAndRegisterDummyObject()); + const auto dummy2(createAndRegisterDummyObject()); + const auto dummy3(createAndRegisterDummyObject()); + const auto dummy4(createAndRegisterDummyObject()); + const auto dummy5(createAndRegisterDummyObject()); + + const std::unordered_set sceneObjectIds + { + dummy1->getSceneObjectId(), + dummy2->getSceneObjectId(), + dummy3->getSceneObjectId(), + dummy4->getSceneObjectId(), + dummy5->getSceneObjectId() + }; + + EXPECT_EQ(sceneObjectIds.size(), 5u); + } + + TEST_F(ARamsesObjectRegistry, canRemoveObject) + { + auto dummyObject = createAndRegisterDummyObject(); + dummyObject->setName("name"); + m_registry.destroyAndUnregisterObject(*dummyObject); + EXPECT_TRUE(nullptr == m_registry.findObjectByName("name")); + EXPECT_TRUE(nullptr == m_registry.findObjectById(sceneObjectId_t{1u})); + EXPECT_EQ(0u, m_registry.getNumberOfObjects(ERamsesObjectType::Node)); + EXPECT_EQ(0u, m_registry.getNumberOfObjects(ERamsesObjectType::RenderBuffer)); + RamsesObjectVector objects; + m_registry.getObjectsOfType(objects, ERamsesObjectType::RamsesObject); + EXPECT_TRUE(objects.empty()); + } + + TEST_F(ARamsesObjectRegistry, canAddAndRetrieveObjectInfo) + { + auto dummyObject = createAndRegisterDummyObject(); + dummyObject->setName("name"); + EXPECT_EQ(dummyObject, m_registry.findObjectByName("name")); + EXPECT_EQ(dummyObject, m_registry.findObjectById(sceneObjectId_t{1u})); + } + + TEST_F(ARamsesObjectRegistry, canRenameObjectAndFindUnderNewName) + { + auto dummyObject = createAndRegisterDummyObject(); + dummyObject->setName("name"); + EXPECT_EQ(dummyObject, m_registry.findObjectByName("name")); + EXPECT_EQ(dummyObject, m_registry.findObjectById(sceneObjectId_t{ 1u })); + dummyObject->setName("newName"); + EXPECT_EQ(dummyObject, m_registry.findObjectByName("newName")); + EXPECT_EQ(dummyObject, m_registry.findObjectById(sceneObjectId_t{ 1u })); + // cannot find under old name + EXPECT_EQ(nullptr, m_registry.findObjectByName("name")); + } + + TEST_F(ARamsesObjectRegistry, cannotRetrieveObjectInfoAfterObjectDeleted) + { + auto dummyObject = createAndRegisterDummyObject(); + dummyObject->setName("name"); + m_registry.destroyAndUnregisterObject(*dummyObject); + EXPECT_TRUE(nullptr == m_registry.findObjectByName("name")); + EXPECT_TRUE(nullptr == m_registry.findObjectById(sceneObjectId_t{1u})); + } + + TEST_F(ARamsesObjectRegistry, canRetrieveObjectFromImpl) + { + auto dummyObject = createAndRegisterDummyObject(); + EXPECT_EQ(dummyObject, &dummyObject->m_impl.getRamsesObject()); + } + + TEST_F(ARamsesObjectRegistry, hasNoDirtyNodesAfterInitialization) + { + EXPECT_EQ(0u, m_registry.getDirtyNodes().size()); + } + + TEST_F(ARamsesObjectRegistry, marksNodeDirty) + { + auto dummyObject = createAndRegisterDummyObject(); + m_registry.setNodeDirty(dummyObject->m_impl, true); + EXPECT_EQ(1u, m_registry.getDirtyNodes().size()); + EXPECT_TRUE(m_registry.getDirtyNodes().contains(&dummyObject->m_impl)); + EXPECT_TRUE(m_registry.isNodeDirty(dummyObject->m_impl)); + } + + TEST_F(ARamsesObjectRegistry, marksNodeClean) + { + auto object1(createAndRegisterDummyObject()); + m_registry.setNodeDirty(object1->m_impl, true); + + auto object2(createAndRegisterDummyObject()); + m_registry.setNodeDirty(object2->m_impl, true); + + EXPECT_EQ(2u, m_registry.getDirtyNodes().size()); + + m_registry.setNodeDirty(object1->m_impl, false); + EXPECT_EQ(1u, m_registry.getDirtyNodes().size()); + EXPECT_FALSE(m_registry.getDirtyNodes().contains(&object1->m_impl)); + EXPECT_FALSE(m_registry.isNodeDirty(object1->m_impl)); + } + + TEST_F(ARamsesObjectRegistry, clearsDirtyNodes) + { + auto dummyObject = createAndRegisterDummyObject(); + m_registry.setNodeDirty(dummyObject->m_impl, true); + m_registry.clearDirtyNodes(); + EXPECT_EQ(0u, m_registry.getDirtyNodes().size()); + EXPECT_FALSE(m_registry.isNodeDirty(dummyObject->m_impl)); + } + + TEST_F(ARamsesObjectRegistry, marksNodeCleanWhenDeleted) + { + auto dummyObject = createAndRegisterDummyObject(); + m_registry.setNodeDirty(dummyObject->m_impl, true); + m_registry.destroyAndUnregisterObject(*dummyObject); + EXPECT_EQ(0u, m_registry.getDirtyNodes().size()); + } + + TEST_F(ARamsesObjectRegistry, nodeAppearsInDirtyListAfterMarkedDirty) + { + auto dummyObject = createAndRegisterDummyObject(); + m_registry.setNodeDirty(dummyObject->m_impl, true); + EXPECT_EQ(1u, m_registry.getDirtyNodes().size()); + EXPECT_TRUE(m_registry.getDirtyNodes().contains(&dummyObject->m_impl)); + EXPECT_TRUE(m_registry.isNodeDirty(dummyObject->m_impl)); + } +} diff --git a/client/test/RamsesObjectTest.cpp b/client/test/RamsesObjectTest.cpp new file mode 100644 index 000000000..940012571 --- /dev/null +++ b/client/test/RamsesObjectTest.cpp @@ -0,0 +1,171 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2015 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include "ramses-client-api/MeshNode.h" +#include "ramses-client-api/PerspectiveCamera.h" +#include "ramses-client-api/OrthographicCamera.h" +#include "ramses-client-api/Texture2D.h" +#include "ramses-client-api/Texture3D.h" +#include "ramses-client-api/TextureCube.h" +#include "ramses-client-api/ArrayResource.h" +#include "ramses-client-api/TextureSampler.h" +#include "ramses-client-api/TextureSamplerMS.h" +#include "ramses-client-api/RenderBuffer.h" +#include "ramses-client-api/RenderTarget.h" +#include "ramses-client-api/RenderGroup.h" +#include "ramses-client-api/RenderPass.h" +#include "ramses-client-api/BlitPass.h" +#include "ramses-client-api/EffectDescription.h" +#include "ramses-client-api/GeometryBinding.h" +#include "ramses-client-api/DataObject.h" +#include "ramses-client-api/ArrayBuffer.h" +#include "ramses-client-api/Texture2DBuffer.h" +#include "ramses-client-api/PickableObject.h" +#include "ramses-client-api/SceneReference.h" + +#include "ramses-utils.h" +#include "RamsesObjectTypeUtils.h" +#include "RamsesObjectTestTypes.h" +#include "ClientTestUtils.h" + +using namespace testing; + +namespace ramses +{ + template + class RamsesObjectTest : public LocalTestClientWithScene, public testing::Test + { + }; + + TYPED_TEST_SUITE(RamsesObjectTest, RamsesObjectTypes); + + TYPED_TEST(RamsesObjectTest, getType) + { + const RamsesObject& obj = this->template createObject("object"); + const ERamsesObjectType type = TYPE_ID_OF_RAMSES_OBJECT::ID; + EXPECT_EQ(type, obj.getType()); + EXPECT_TRUE(obj.isOfType(ERamsesObjectType::RamsesObject)); + } + + TYPED_TEST(RamsesObjectTest, isOfTypeForAllDefinedBaseClasses) + { + const RamsesObject& obj = this->template createObject("object"); + ERamsesObjectType type = TYPE_ID_OF_RAMSES_OBJECT::ID; + + while (type != ERamsesObjectType::Invalid) + { + EXPECT_TRUE(obj.isOfType(type)); + type = RamsesObjectTraits[static_cast(type)].baseClassTypeID; + } + } + + TYPED_TEST(RamsesObjectTest, getSetName) + { + RamsesObject& obj = this->template createObject("object"); + ASSERT_STREQ("object", obj.getName()); + EXPECT_EQ(StatusOK, obj.setName("newName")); + ASSERT_STREQ("newName", obj.getName()); + + if (obj.isOfType(ERamsesObjectType::SceneObject)) + { + EXPECT_EQ(nullptr, this->m_scene.findObjectByName("object")); + EXPECT_EQ(&obj, this->m_scene.findObjectByName("newName")); + } + } + + TYPED_TEST(RamsesObjectTest, convertToTypes) + { + RamsesObject& obj = this->template createObject("object"); + const RamsesObject& constObj = obj; + + EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(obj)); + EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(constObj)); + + EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(obj)); + EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(constObj)); + } + + TYPED_TEST(RamsesObjectTest, convertToItsClosestBaseClass) + { + using BaseClassType = typename CLASS_OF_RAMSES_OBJECT_TYPE::ID>::BaseTypeID>::ClassType; + + RamsesObject& obj = this->template createObject("object"); + const RamsesObject& constObj = obj; + + if (TYPE_ID_OF_RAMSES_OBJECT::ID != ERamsesObjectType::Invalid) + { + EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(obj)); + EXPECT_TRUE(nullptr != RamsesUtils::TryConvert(constObj)); + } + } + + TYPED_TEST(RamsesObjectTest, getRamsesObjectFromImpl) + { + RamsesObject& obj = this->template createObject("object"); + const RamsesObject& constObj = obj; + + RamsesObjectImpl& objImpl = obj.m_impl; + const RamsesObjectImpl& constObjImpl = constObj.m_impl; + + EXPECT_EQ(&obj, &objImpl.getRamsesObject()); + EXPECT_EQ(&constObj, &constObjImpl.getRamsesObject()); + } + + TYPED_TEST(RamsesObjectTest, validationStringIsNonEmptyAfterCall) + { + const RamsesObject& obj = this->template createObject("object"); + + EXPECT_STREQ("", obj.getValidationReport()); + std::ignore = obj.validate(); + EXPECT_STRNE("", obj.getValidationReport()); + } + + TYPED_TEST(RamsesObjectTest, validationStringContainsObjectTypeAndName) + { + const RamsesObject& obj = this->template createObject("object"); + + EXPECT_NE(StatusOK, obj.m_impl.addErrorEntry("dummy")); + std::ignore = obj.validate(); + const std::string validationReport = obj.getValidationReport(EValidationSeverity::Info); + + EXPECT_THAT(validationReport, ::testing::HasSubstr(RamsesObjectTypeUtils::GetRamsesObjectTypeName(obj.getType()))); + EXPECT_THAT(validationReport, ::testing::HasSubstr(obj.getName())); + } + + TYPED_TEST(RamsesObjectTest, validationIgnoresWrongAPIUsage) + { + RamsesObject& obj = this->template createObject("object"); + + // simulate some API usage error + EXPECT_NE(StatusOK, obj.m_impl.addErrorEntry("dummy error msg")); + + std::ignore = obj.validate(); + EXPECT_THAT(obj.getValidationReport(EValidationSeverity::Warning), ::testing::Not(::testing::HasSubstr("dummy error msg"))); + + // will not appear in validation of scene either + std::ignore = this->m_scene.validate(); + EXPECT_THAT(this->m_scene.getValidationReport(EValidationSeverity::Warning), ::testing::Not(::testing::HasSubstr("dummy error msg"))); + } + + TYPED_TEST(RamsesObjectTest, convertToWrongType) + { + RamsesObject& obj = this->template createObject("object"); + const RamsesObject& constObj = obj; + + if (constObj.getType() != ERamsesObjectType::PerspectiveCamera) + { + EXPECT_TRUE(nullptr == RamsesUtils::TryConvert(obj)); + EXPECT_TRUE(nullptr == RamsesUtils::TryConvert(constObj)); + } + else + { + EXPECT_TRUE(nullptr == RamsesUtils::TryConvert(obj)); + EXPECT_TRUE(nullptr == RamsesUtils::TryConvert(constObj)); + } + } +} diff --git a/client/test/RamsesObjectTestTypes.h b/client/test/RamsesObjectTestTypes.h new file mode 100644 index 000000000..c6b407b93 --- /dev/null +++ b/client/test/RamsesObjectTestTypes.h @@ -0,0 +1,118 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2015 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#ifndef RAMSES_RAMSESOBJECTTESTTYPES_H +#define RAMSES_RAMSESOBJECTTESTTYPES_H + +#include + +namespace ramses +{ + class RamsesClient; + class Scene; + class Appearance; + class Camera; + class PerspectiveCamera; + class OrthographicCamera; + class Appearance; + class Node; + class Effect; + class MeshNode; + class ArrayBuffer; + class Texture2DBuffer; + class GeometryBinding; + class RenderGroup; + class RenderPass; + class RenderBuffer; + class RenderTarget; + class DataObject; + class BlitPass; + class TextureSampler; + class TextureSamplerMS; + class Texture2D; + class Texture3D; + class TextureCube; + class PickableObject; + class SceneReference; + class ArrayResource; + + // Objects derived from Node class + using NodeTypes = ::testing::Types< + Node, + MeshNode, + PerspectiveCamera, + OrthographicCamera, + PickableObject>; + + // Objects derived from Resource class + using ResourceTypes = ::testing::Types< + ArrayResource, + Texture2D, + Texture3D, + TextureCube, + Effect>; + + // Objects owned by Scene + using SceneObjectTypes = ::testing::Types< + Node, + MeshNode, + PerspectiveCamera, + OrthographicCamera, + Appearance, + GeometryBinding, + RenderGroup, + RenderPass, + BlitPass, + TextureSampler, + TextureSamplerMS, + RenderBuffer, + RenderTarget, + DataObject, + ArrayBuffer, + Texture2DBuffer, + PickableObject, + SceneReference, + Texture2D, + Texture3D, + TextureCube, + ArrayResource, + Effect>; + + // Objects owned by client + using ClientObjectTypes = ::testing::Types; + + // All Ramses objects + using RamsesObjectTypes = ::testing::Types< + RamsesClient, + Scene, + Node, + MeshNode, + PerspectiveCamera, + OrthographicCamera, + Effect, + Appearance, + Texture2D, + Texture3D, + TextureCube, + ArrayResource, + RenderGroup, + RenderPass, + BlitPass, + TextureSampler, + TextureSamplerMS, + RenderBuffer, + RenderTarget, + ArrayBuffer, + Texture2DBuffer, + GeometryBinding, + DataObject, + PickableObject, + SceneReference>; +} + +#endif diff --git a/client/ramses-client/test/RamsesUtilsTest.cpp b/client/test/RamsesUtilsTest.cpp similarity index 93% rename from client/ramses-client/test/RamsesUtilsTest.cpp rename to client/test/RamsesUtilsTest.cpp index bd3c9ea34..022d5d261 100644 --- a/client/ramses-client/test/RamsesUtilsTest.cpp +++ b/client/test/RamsesUtilsTest.cpp @@ -12,12 +12,13 @@ #include "ramses-client-api/Texture2D.h" #include "ramses-client-api/MipLevelData.h" #include "ramses-client-api/TextureEnums.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector4f.h" +#include "ramses-client-api/DataObject.h" #include "Texture2DImpl.h" #include "Utils/File.h" #include "Math3d/ProjectionParams.h" +#include + using namespace testing; namespace ramses @@ -25,14 +26,14 @@ namespace ramses class ARamsesUtilsTest : public ::testing::Test, public LocalTestClientWithScene { public: - static void LoadFileToVector(const char* fname, std::vector& data) + static void LoadFileToVector(std::string_view fname, std::vector& data) { ramses_internal::File f(fname); ASSERT_TRUE(f.open(ramses_internal::File::Mode::ReadOnlyBinary)); - ramses_internal::UInt filesize = 0; + size_t filesize = 0; ASSERT_TRUE(f.getSizeInBytes(filesize)); data.resize(filesize); - ramses_internal::UInt numread = 0; + size_t numread = 0; ASSERT_EQ(ramses_internal::EStatus::Ok, f.read(data.data(), filesize, numread)); ASSERT_EQ(filesize, numread); } @@ -326,7 +327,7 @@ namespace ramses data[i] = i + 1u; } - uint32_t mipMapCount = 0u; + size_t mipMapCount = 0u; MipLevelData* mipData = RamsesUtils::GenerateMipMapsTexture2D(width, height, pixelSize, data, mipMapCount); EXPECT_TRUE(mipData); EXPECT_EQ(4u, mipMapCount); @@ -370,7 +371,7 @@ namespace ramses data[2*i + 1] = i + 1u + 32u; } - uint32_t mipMapCount = 0u; + size_t mipMapCount = 0u; MipLevelData* mipData = RamsesUtils::GenerateMipMapsTexture2D(width, height, pixelSize, data, mipMapCount); EXPECT_TRUE(mipData); EXPECT_EQ(4u, mipMapCount); @@ -418,7 +419,7 @@ namespace ramses data[i] = i + 1u; } - uint32_t mipMapCount = 0u; + size_t mipMapCount = 0u; CubeMipLevelData* mipData = RamsesUtils::GenerateMipMapsTextureCube(width, height, pixelSize, data, mipMapCount); EXPECT_TRUE(mipData); EXPECT_EQ(3u, mipMapCount); @@ -466,26 +467,27 @@ namespace ramses TEST_F(ARamsesUtilsTest, canSetFrustumOnDataObjects) { - auto& do1 = this->createObject(); - auto& do2 = this->createObject(); + auto& do1 = *m_scene.createDataObject(EDataType::Vector4F); + auto& do2 = *m_scene.createDataObject(EDataType::Vector2F); EXPECT_TRUE(RamsesUtils::SetPerspectiveCameraFrustumToDataObjects(33.f, 1.5f, 1.f, 2.f, do1, do2)); const auto expectedParams = ramses_internal::ProjectionParams::Perspective(33.f, 1.5f, 1.f, 2.f); - std::array planes; - EXPECT_EQ(StatusOK, do1.getValue(planes[0], planes[1], planes[2], planes[3])); - EXPECT_EQ(StatusOK, do2.getValue(planes[4], planes[5])); - EXPECT_EQ(expectedParams.leftPlane, planes[0]); - EXPECT_EQ(expectedParams.rightPlane, planes[1]); - EXPECT_EQ(expectedParams.bottomPlane, planes[2]); - EXPECT_EQ(expectedParams.topPlane, planes[3]); - EXPECT_EQ(expectedParams.nearPlane, planes[4]); - EXPECT_EQ(expectedParams.farPlane, planes[5]); + vec4f planes1; + vec2f planes2; + EXPECT_EQ(StatusOK, do1.getValue(planes1)); + EXPECT_EQ(StatusOK, do2.getValue(planes2)); + EXPECT_EQ(expectedParams.leftPlane, planes1[0]); + EXPECT_EQ(expectedParams.rightPlane, planes1[1]); + EXPECT_EQ(expectedParams.bottomPlane, planes1[2]); + EXPECT_EQ(expectedParams.topPlane, planes1[3]); + EXPECT_EQ(expectedParams.nearPlane, planes2[0]); + EXPECT_EQ(expectedParams.farPlane, planes2[1]); } TEST_F(ARamsesUtilsTest, failsToSetInvalidFrustumOnDataObjects) { - auto& do1 = this->createObject(); - auto& do2 = this->createObject(); + auto& do1 = *m_scene.createDataObject(EDataType::Vector4F); + auto& do2 = *m_scene.createDataObject(EDataType::Vector2F); EXPECT_FALSE(RamsesUtils::SetPerspectiveCameraFrustumToDataObjects(0.f, 1.5f, 1.f, 2.f, do1, do2)); EXPECT_FALSE(RamsesUtils::SetPerspectiveCameraFrustumToDataObjects(-33.f, 1.5f, 1.f, 2.f, do1, do2)); EXPECT_FALSE(RamsesUtils::SetPerspectiveCameraFrustumToDataObjects(33.f, 0.f, 1.f, 2.f, do1, do2)); @@ -497,6 +499,6 @@ namespace ramses TEST_F(ARamsesUtilsTest, getNodeHandleReturnsMemoryHandle) { MeshNode& node = this->createValidMeshNode(); - EXPECT_EQ(node.impl.getNodeHandle().asMemoryHandle(), RamsesUtils::GetNodeId(node).getValue()); + EXPECT_EQ(node.m_impl.getNodeHandle().asMemoryHandle(), RamsesUtils::GetNodeId(node).getValue()); } } diff --git a/client/ramses-client/test/RamsesVersionTest.cpp b/client/test/RamsesVersionTest.cpp similarity index 60% rename from client/ramses-client/test/RamsesVersionTest.cpp rename to client/test/RamsesVersionTest.cpp index 9b7fc5bb6..e7ee8573f 100644 --- a/client/ramses-client/test/RamsesVersionTest.cpp +++ b/client/test/RamsesVersionTest.cpp @@ -19,27 +19,29 @@ namespace ramses_internal class ARamsesVersion : public ::testing::Test { public: - static BinaryOutputStream CreateOutputStreamFromString(const String& str) + static BinaryOutputStream CreateOutputStreamFromString(const std::string& str) { BinaryOutputStream out; - out.write(str.data(), static_cast(str.size())); + out.write(str.data(), static_cast(str.size())); return out; } RamsesVersion::VersionInfo info; + ramses::EFeatureLevel featureLevel = ramses::EFeatureLevel_01; }; TEST_F(ARamsesVersion, canParseCurrentVersion) { BinaryOutputStream out; - RamsesVersion::WriteToStream(out, ::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_STRING, ::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH); + RamsesVersion::WriteToStream(out, ::ramses_sdk::RAMSES_SDK_RAMSES_VERSION, ::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH, ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); - EXPECT_STREQ(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_STRING, info.versionString.c_str()); + EXPECT_STREQ(::ramses_sdk::RAMSES_SDK_RAMSES_VERSION, info.versionString.c_str()); EXPECT_STREQ(::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH, info.gitHash.c_str()); - EXPECT_EQ(static_cast(std::atoi(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MAJOR)), info.major); - EXPECT_EQ(static_cast(std::atoi(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MINOR)), info.minor); + EXPECT_EQ(static_cast(std::atoi(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MAJOR)), info.major); + EXPECT_EQ(static_cast(std::atoi(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MINOR)), info.minor); + EXPECT_EQ(ramses::EFeatureLevel_Latest, featureLevel); } TEST_F(ARamsesVersion, canParseCurrentVersionFromFile) @@ -47,24 +49,25 @@ namespace ramses_internal { File fOut("ramsesVersionTest"); BinaryFileOutputStream out(fOut); - RamsesVersion::WriteToStream(out, ::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_STRING, ::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH); + RamsesVersion::WriteToStream(out, ::ramses_sdk::RAMSES_SDK_RAMSES_VERSION, ::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH, ramses::EFeatureLevel_Latest); } File fIn("ramsesVersionTest"); BinaryFileInputStream in(fIn); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); - EXPECT_STREQ(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_STRING, info.versionString.c_str()); + EXPECT_STREQ(::ramses_sdk::RAMSES_SDK_RAMSES_VERSION, info.versionString.c_str()); EXPECT_STREQ(::ramses_sdk::RAMSES_SDK_GIT_COMMIT_HASH, info.gitHash.c_str()); - EXPECT_EQ(static_cast(std::atoi(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MAJOR)), info.major); - EXPECT_EQ(static_cast(std::atoi(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MINOR)), info.minor); + EXPECT_EQ(static_cast(std::atoi(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MAJOR)), info.major); + EXPECT_EQ(static_cast(std::atoi(::ramses_sdk::RAMSES_SDK_PROJECT_VERSION_MINOR)), info.minor); + EXPECT_EQ(ramses::EFeatureLevel_Latest, featureLevel); } TEST_F(ARamsesVersion, canParseReleaseVersion) { BinaryOutputStream out; - RamsesVersion::WriteToStream(out, "17.0.1", "af98afa8e94"); + RamsesVersion::WriteToStream(out, "17.0.1", "af98afa8e94", ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); EXPECT_STREQ("17.0.1", info.versionString.c_str()); EXPECT_STREQ("af98afa8e94", info.gitHash.c_str()); @@ -75,9 +78,9 @@ namespace ramses_internal TEST_F(ARamsesVersion, canParseMasterVersion) { BinaryOutputStream out; - RamsesVersion::WriteToStream(out, "0.0.0-devMaster", "af98afa8e94"); + RamsesVersion::WriteToStream(out, "0.0.0-devMaster", "af98afa8e94", ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); EXPECT_STREQ("0.0.0-devMaster", info.versionString.c_str()); EXPECT_STREQ("af98afa8e94", info.gitHash.c_str()); @@ -88,9 +91,9 @@ namespace ramses_internal TEST_F(ARamsesVersion, canParseOtherVersionSuffix) { BinaryOutputStream out; - RamsesVersion::WriteToStream(out, "0.0.0-releaseCandidate", "af98afa8e94"); + RamsesVersion::WriteToStream(out, "0.0.0-releaseCandidate", "af98afa8e94", ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); EXPECT_STREQ("0.0.0-releaseCandidate", info.versionString.c_str()); EXPECT_STREQ("af98afa8e94", info.gitHash.c_str()); @@ -101,9 +104,9 @@ namespace ramses_internal TEST_F(ARamsesVersion, canParseVersionNumbersInCorrectOrder) { BinaryOutputStream out; - RamsesVersion::WriteToStream(out, "1.2.3-devMaster", "af98afa8e94"); + RamsesVersion::WriteToStream(out, "1.2.3-devMaster", "af98afa8e94", ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); EXPECT_STREQ("1.2.3-devMaster", info.versionString.c_str()); EXPECT_STREQ("af98afa8e94", info.gitHash.c_str()); @@ -114,9 +117,9 @@ namespace ramses_internal TEST_F(ARamsesVersion, canParseLongVersionNumbers) { BinaryOutputStream out; - RamsesVersion::WriteToStream(out, "11.22.333", "af98afa8e94"); + RamsesVersion::WriteToStream(out, "11.22.333", "af98afa8e94", ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); EXPECT_STREQ("11.22.333", info.versionString.c_str()); EXPECT_STREQ("af98afa8e94", info.gitHash.c_str()); @@ -127,9 +130,9 @@ namespace ramses_internal TEST_F(ARamsesVersion, canParseUnknownAsHashValue) { BinaryOutputStream out; - RamsesVersion::WriteToStream(out, "1.3.2", "(unknown)"); + RamsesVersion::WriteToStream(out, "1.3.2", "(unknown)", ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); EXPECT_STREQ("1.3.2", info.versionString.c_str()); EXPECT_STREQ("(unknown)", info.gitHash.c_str()); @@ -140,9 +143,9 @@ namespace ramses_internal TEST_F(ARamsesVersion, canParseFullHashValue) { BinaryOutputStream out; - RamsesVersion::WriteToStream(out, "1.3.2", "ce01334641b90dbfc02cbee5d072cec8f9000afe"); + RamsesVersion::WriteToStream(out, "1.3.2", "ce01334641b90dbfc02cbee5d072cec8f9000afe", ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); EXPECT_STREQ("1.3.2", info.versionString.c_str()); EXPECT_STREQ("ce01334641b90dbfc02cbee5d072cec8f9000afe", info.gitHash.c_str()); @@ -151,9 +154,9 @@ namespace ramses_internal TEST_F(ARamsesVersion, canParseMajorMinorPatchTweak) { BinaryOutputStream out; - RamsesVersion::WriteToStream(out, "1.3.2.5", "ce01334641b90dbfc02cbee5d072cec8f9000afe"); + RamsesVersion::WriteToStream(out, "1.3.2.5", "ce01334641b90dbfc02cbee5d072cec8f9000afe", ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); EXPECT_STREQ("1.3.2.5", info.versionString.c_str()); EXPECT_STREQ("ce01334641b90dbfc02cbee5d072cec8f9000afe", info.gitHash.c_str()); @@ -164,9 +167,9 @@ namespace ramses_internal TEST_F(ARamsesVersion, canParseDifferentSuffix) { BinaryOutputStream out; - RamsesVersion::WriteToStream(out, "1.3.2-foobar", "ce01334641b90dbfc02cbee5d072cec8f9000afe"); + RamsesVersion::WriteToStream(out, "1.3.2-foobar", "ce01334641b90dbfc02cbee5d072cec8f9000afe", ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); EXPECT_STREQ("1.3.2-foobar", info.versionString.c_str()); EXPECT_STREQ("ce01334641b90dbfc02cbee5d072cec8f9000afe", info.gitHash.c_str()); @@ -177,9 +180,9 @@ namespace ramses_internal TEST_F(ARamsesVersion, canParseRandomStringAfterMajorMinor) { BinaryOutputStream out; - RamsesVersion::WriteToStream(out, "1.3.a.b.c+hotfix3", "ce01334641b90dbfc02cbee5d072cec8f9000afe"); + RamsesVersion::WriteToStream(out, "1.3.a.b.c+hotfix3", "ce01334641b90dbfc02cbee5d072cec8f9000afe", ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); EXPECT_STREQ("1.3.a.b.c+hotfix3", info.versionString.c_str()); EXPECT_STREQ("ce01334641b90dbfc02cbee5d072cec8f9000afe", info.gitHash.c_str()); @@ -189,69 +192,69 @@ namespace ramses_internal TEST_F(ARamsesVersion, canParseHandCraftedVersionInformation) { - BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHash:d89398fd]\n")); + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHash:d89398fd]\n[FeatureLevel:1]\n")); BinaryInputStream in(out.getData()); - EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_TRUE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenHashEntryKeywordWrong) { - BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHashX:d89398fd]\n")); + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHashX:d89398fd]\n[FeatureLevel:1]\n")); BinaryInputStream in(out.getData()); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenHashEntryCharactersAreWrong) { - BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHash:d8XX9398fd]\n")); + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHash:d8XX9398fd]\n[FeatureLevel:1]\n")); BinaryInputStream in(out.getData()); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenHashMissing) { - BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHash:]\n")); + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHash:]\n[FeatureLevel:1]\n")); BinaryInputStream in(out.getData()); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenVersionMissing) { - BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:]\n[GitHash:d89398fd]\n")); + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:]\n[GitHash:d89398fd]\n[FeatureLevel:1]\n")); BinaryInputStream in(out.getData()); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenVersionTruncated) { - BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.]\n[GitHash:d89398fd]\n")); + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.]\n[GitHash:d89398fd]\n[FeatureLevel:1]\n")); BinaryInputStream in(out.getData()); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenVersionNotNumbers) { - BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.a.0]\n[GitHash:d89398fd]\n")); + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.a.0]\n[GitHash:d89398fd]\n[FeatureLevel:1]\n")); BinaryInputStream in(out.getData()); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenStartsWithZeroBytes) { - const std::string str = "[RamsesVersion:0.0.0]\n[GitHash:d89398fd]\n"; + const std::string str = "[RamsesVersion:0.0.0]\n[GitHash:d89398fd]\n[FeatureLevel:1]\n"; BinaryOutputStream out; uint32_t zeroData = 0; out.write(&zeroData, sizeof(zeroData)); out.write(str.data(), str.size()); BinaryInputStream in(out.getData()); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenStartsContainsZeroBytes) { const std::string str_1 = "[RamsesVersi"; - const std::string str_2 = "on:0.0.0]\n[GitHash:d89398fd]\n"; + const std::string str_2 = "on:0.0.0]\n[GitHash:d89398fd]\n[FeatureLevel:1]\n"; BinaryOutputStream out; uint32_t zeroData = 0; out.write(str_1.data(), str_1.size()); @@ -259,41 +262,41 @@ namespace ramses_internal out.write(str_2.data(), str_2.size()); BinaryInputStream in(out.getData()); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenStreamInvalid) { File f("this_file_should_not_exist"); BinaryFileInputStream in(f); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenNoNewlineFoundWithinReasonableDistanceFromStart) { - String s; + std::string s; for (int i = 0; i < 1000; ++i) { s += ' '; } BinaryOutputStream out(CreateOutputStreamFromString(s)); - RamsesVersion::WriteToStream(out, "17.0.1", "af98afa8eeee94"); + RamsesVersion::WriteToStream(out, "17.0.1", "af98afa8eeee94", ramses::EFeatureLevel_Latest); BinaryInputStream in(out.getData()); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenStreamNotLongEnoughToMatchVersionIndicator) { - BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersi\n[GitHash:d89398fd]\n")); + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersi\n[GitHash:d89398fd]\n[FeatureLevel:1]\n")); BinaryInputStream in(out.getData()); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, failsWhenVersionNumberNegative) { - BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:1.-1.0]\n[GitHash:d89398fd]\n")); + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:1.-1.0]\n[GitHash:d89398fd]\n[FeatureLevel:1]\n")); BinaryInputStream in(out.getData()); - EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info)); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); } TEST_F(ARamsesVersion, MatchesCurrentMajorMinorMatchesCurrentExactVersion) @@ -335,4 +338,56 @@ namespace ramses_internal vi.minor = 66 + 1; EXPECT_FALSE(RamsesVersion::MatchesMajorMinor(55, 66, vi)); } + + TEST_F(ARamsesVersion, failsWhenFeatureLevelCompletelyMissing) + { + { + // using actual file instead of byte stream because in this test EOF is reached + // which is only handled properly when using file stream + File f{ "tempFile" }; + ASSERT_TRUE(f.open(File::Mode::WriteNewBinary)); + const std::string data{ "[RamsesVersion:0.0.0]\n[GitHash:d89398fd]\n" }; + ASSERT_TRUE(f.write(data.data(), data.size())); + } + + File f{ "tempFile" }; + ASSERT_TRUE(f.open(File::Mode::ReadOnlyBinary)); + BinaryFileInputStream stream{ f }; + EXPECT_FALSE(RamsesVersion::ReadFromStream(stream, info, featureLevel)); + } + + TEST_F(ARamsesVersion, failsWhenFeatureLevelLabelMismatch) + { + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHash:d89398fd]\n[FeatureLevelx:1]\n")); + BinaryInputStream in(out.getData()); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); + } + + TEST_F(ARamsesVersion, failsWhenFeatureLevelLabelBracketMissing) + { + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHash:d89398fd]\n[FeatureLevel:1\n")); + BinaryInputStream in(out.getData()); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); + } + + TEST_F(ARamsesVersion, failsWhenFeatureLevelLabelHasExtraChars) + { + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHash:d89398fd]\n[FeatureLevel:1].\n")); + BinaryInputStream in(out.getData()); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); + } + + TEST_F(ARamsesVersion, failsWhenFeatureLevelNotANumber) + { + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHash:d89398fd]\n[FeatureLevel:x]\n")); + BinaryInputStream in(out.getData()); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); + } + + TEST_F(ARamsesVersion, failsWhenFeatureLevelNotSupported) + { + BinaryOutputStream out(CreateOutputStreamFromString("[RamsesVersion:0.0.0]\n[GitHash:d89398fd]\n[FeatureLevel:99]\n")); + BinaryInputStream in(out.getData()); + EXPECT_FALSE(RamsesVersion::ReadFromStream(in, info, featureLevel)); + } } diff --git a/client/ramses-client/test/RenderBufferTest.cpp b/client/test/RenderBufferTest.cpp similarity index 65% rename from client/ramses-client/test/RenderBufferTest.cpp rename to client/test/RenderBufferTest.cpp index 8874a620f..fea615deb 100644 --- a/client/ramses-client/test/RenderBufferTest.cpp +++ b/client/test/RenderBufferTest.cpp @@ -38,74 +38,74 @@ namespace ramses void useInBlitPassAsSource(const RenderBuffer& rb) { - const RenderBuffer* dummyRb = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* dummyRb = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); this->m_scene.createBlitPass(rb, *dummyRb); } void useInBlitPassAsDestination(const RenderBuffer& rb) { - const RenderBuffer* dummyRb = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* dummyRb = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); this->m_scene.createBlitPass(*dummyRb, rb); } }; TEST_F(RenderBufferTest, canCreateRenderBuffer) { - const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(600u, 400u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_WriteOnly, 4u, "RenderBuffer"); + const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(600u, 400u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::WriteOnly, 4u, "RenderBuffer"); ASSERT_TRUE(renderBuffer != nullptr); EXPECT_EQ(600u, renderBuffer->getWidth()); EXPECT_EQ(400u, renderBuffer->getHeight()); - EXPECT_EQ(ERenderBufferType_Color, renderBuffer->getBufferType()); - EXPECT_EQ(ERenderBufferFormat_RGBA8, renderBuffer->getBufferFormat()); - EXPECT_EQ(ERenderBufferAccessMode_WriteOnly, renderBuffer->getAccessMode()); + EXPECT_EQ(ERenderBufferType::Color, renderBuffer->getBufferType()); + EXPECT_EQ(ERenderBufferFormat::RGBA8, renderBuffer->getBufferFormat()); + EXPECT_EQ(ERenderBufferAccessMode::WriteOnly, renderBuffer->getAccessMode()); EXPECT_EQ(4u, renderBuffer->getSampleCount()); - const ramses_internal::RenderBufferHandle rbHandle = renderBuffer->impl.getRenderBufferHandle(); + const ramses_internal::RenderBufferHandle rbHandle = renderBuffer->m_impl.getRenderBufferHandle(); EXPECT_TRUE(m_internalScene.isRenderBufferAllocated(rbHandle)); } TEST_F(RenderBufferTest, failsToCreateRenderBufferWithZeroWidthOrHeight) { - RenderBuffer* rbWithZeroWidth = m_scene.createRenderBuffer(0u, 400u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + RenderBuffer* rbWithZeroWidth = m_scene.createRenderBuffer(0u, 400u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); EXPECT_EQ(nullptr, rbWithZeroWidth); - RenderBuffer* rbWithZeroHeight = m_scene.createRenderBuffer(400u, 0u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + RenderBuffer* rbWithZeroHeight = m_scene.createRenderBuffer(400u, 0u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); EXPECT_EQ(nullptr, rbWithZeroHeight); } TEST_F(RenderBufferTest, failsToCreateRenderBufferOfIncompatibleTypeAndFormat) { - EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_ReadWrite)); - EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Color, ERenderBufferFormat_Depth24_Stencil8, ERenderBufferAccessMode_ReadWrite)); - EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Depth, ERenderBufferFormat_Depth24_Stencil8, ERenderBufferAccessMode_ReadWrite)); - EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_Depth, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite)); - EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_DepthStencil, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_ReadWrite)); - EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType_DepthStencil, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite)); + EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::ReadWrite)); + EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::Depth24_Stencil8, ERenderBufferAccessMode::ReadWrite)); + EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Depth, ERenderBufferFormat::Depth24_Stencil8, ERenderBufferAccessMode::ReadWrite)); + EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Depth, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite)); + EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::DepthStencil, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::ReadWrite)); + EXPECT_TRUE(nullptr == m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::DepthStencil, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite)); } TEST_F(RenderBufferTest, canCreateReadWriteMSAARenderBuffer) { - const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(600u, 400u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite, 4u, "RenderBuffer"); + const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(600u, 400u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite, 4u, "RenderBuffer"); ASSERT_TRUE(renderBuffer != nullptr); EXPECT_EQ(600u, renderBuffer->getWidth()); EXPECT_EQ(400u, renderBuffer->getHeight()); - EXPECT_EQ(ERenderBufferType_Color, renderBuffer->getBufferType()); - EXPECT_EQ(ERenderBufferFormat_RGBA8, renderBuffer->getBufferFormat()); - EXPECT_EQ(ERenderBufferAccessMode_ReadWrite, renderBuffer->getAccessMode()); + EXPECT_EQ(ERenderBufferType::Color, renderBuffer->getBufferType()); + EXPECT_EQ(ERenderBufferFormat::RGBA8, renderBuffer->getBufferFormat()); + EXPECT_EQ(ERenderBufferAccessMode::ReadWrite, renderBuffer->getAccessMode()); EXPECT_EQ(4u, renderBuffer->getSampleCount()); - const ramses_internal::RenderBufferHandle rbHandle = renderBuffer->impl.getRenderBufferHandle(); + const ramses_internal::RenderBufferHandle rbHandle = renderBuffer->m_impl.getRenderBufferHandle(); EXPECT_TRUE(m_internalScene.isRenderBufferAllocated(rbHandle)); } TEST_F(RenderBufferTest, reportsErrorIfNotUsedInAnyRenderPassNorBlitPass) { - const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(renderBuffer != nullptr); EXPECT_NE(StatusOK, renderBuffer->validate()); } TEST_F(RenderBufferTest, reportsErrorIfUsedInRenderPassButNotReferencedByAnySamplerNorUsedAsBlitPassSource) { - const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(renderBuffer != nullptr); useInRenderPass(*renderBuffer); EXPECT_NE(StatusOK, renderBuffer->validate()); @@ -113,33 +113,33 @@ namespace ramses TEST_F(RenderBufferTest, reportsErrorIfReferencedBySamplerButNotUsedInAnyRenderPassNorUsedAsBlitPassDestination) { - const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(renderBuffer != nullptr); - m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Nearest, *renderBuffer); + m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Nearest, *renderBuffer); EXPECT_NE(StatusOK, renderBuffer->validate()); } TEST_F(RenderBufferTest, validatesWhenUsedInRenderPassAndReferencedBySampler) { - const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(renderBuffer != nullptr); useInRenderPass(*renderBuffer); - m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Nearest, *renderBuffer); + m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Nearest, *renderBuffer); EXPECT_EQ(StatusOK, renderBuffer->validate()); } TEST_F(RenderBufferTest, validatesWhenUsedAsBlitPassDestinationAndReferencedBySampler) { - const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(renderBuffer != nullptr); useInBlitPassAsDestination(*renderBuffer); - m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Nearest, *renderBuffer); + m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Nearest, *renderBuffer); EXPECT_EQ(StatusOK, renderBuffer->validate()); } TEST_F(RenderBufferTest, validatesWhenNOTReferencedBySamplerButUsedAsBlitPassSource) { - const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(renderBuffer != nullptr); useInRenderPass(*renderBuffer); useInBlitPassAsSource(*renderBuffer); @@ -148,7 +148,7 @@ namespace ramses TEST_F(RenderBufferTest, doesNotReportsErrorIfUsedInRenderPassButNotReferencedByAnySampler_DepthBufferType) { - const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType_Depth, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(400u, 400u, ERenderBufferType::Depth, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(renderBuffer != nullptr); useInRenderPass(*renderBuffer); EXPECT_EQ(StatusOK, renderBuffer->validate()); diff --git a/client/ramses-client/test/RenderGroupMeshIteratorTest.cpp b/client/test/RenderGroupMeshIteratorTest.cpp similarity index 100% rename from client/ramses-client/test/RenderGroupMeshIteratorTest.cpp rename to client/test/RenderGroupMeshIteratorTest.cpp diff --git a/client/ramses-client/test/RenderGroupTest.cpp b/client/test/RenderGroupTest.cpp similarity index 93% rename from client/ramses-client/test/RenderGroupTest.cpp rename to client/test/RenderGroupTest.cpp index 40e8c39a3..bf2a7eb7d 100644 --- a/client/ramses-client/test/RenderGroupTest.cpp +++ b/client/test/RenderGroupTest.cpp @@ -24,11 +24,11 @@ using namespace ramses_internal; namespace ramses { - class ARenderGroup : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test + class ARenderGroup : public LocalTestClientWithScene, public testing::Test { protected: ARenderGroup() - : LocalTestClientWithSceneAndAnimationSystem() + : LocalTestClientWithScene() , renderGroup(*m_scene.createRenderGroup("RenderGroup")) , renderGroup2(*m_scene.createRenderGroup("RenderGroup2")) { @@ -36,12 +36,12 @@ namespace ramses void expectNumMeshesContained(uint32_t numMeshes, const RenderGroup& group) { - EXPECT_EQ(numMeshes, static_cast(group.impl.getAllMeshes().size())); + EXPECT_EQ(numMeshes, static_cast(group.m_impl.getAllMeshes().size())); } void expectNumRenderGroupsContained(uint32_t numRenderGroups, const RenderGroup& group) { - EXPECT_EQ(numRenderGroups, static_cast(group.impl.getAllRenderGroups().size())); + EXPECT_EQ(numRenderGroups, static_cast(group.m_impl.getAllRenderGroups().size())); } void expectMeshContained(const MeshNode& mesh, int32_t order, const RenderGroup& group) @@ -51,9 +51,9 @@ namespace ramses EXPECT_EQ(StatusOK, group.getMeshNodeOrder(mesh, actualOrder)); EXPECT_EQ(order, actualOrder); - const auto& internalRg = m_internalScene.getRenderGroup(group.impl.getRenderGroupHandle()); - ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderable(mesh.impl.getRenderableHandle(), internalRg)); - EXPECT_EQ(order, ramses_internal::RenderGroupUtils::FindRenderableEntry(mesh.impl.getRenderableHandle(), internalRg)->order); + const auto& internalRg = m_internalScene.getRenderGroup(group.m_impl.getRenderGroupHandle()); + ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderable(mesh.m_impl.getRenderableHandle(), internalRg)); + EXPECT_EQ(order, ramses_internal::RenderGroupUtils::FindRenderableEntry(mesh.m_impl.getRenderableHandle(), internalRg)->order); } void expectRenderGroupContained(const RenderGroup& nestedRenderGroup, int32_t order, const RenderGroup& group) @@ -63,23 +63,23 @@ namespace ramses EXPECT_EQ(StatusOK, group.getRenderGroupOrder(nestedRenderGroup, actualOrder)); EXPECT_EQ(order, actualOrder); - const auto& internalRg = m_internalScene.getRenderGroup(group.impl.getRenderGroupHandle()); - ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(nestedRenderGroup.impl.getRenderGroupHandle(), internalRg)); - EXPECT_EQ(order, ramses_internal::RenderGroupUtils::FindRenderGroupEntry(nestedRenderGroup.impl.getRenderGroupHandle(), internalRg)->order); + const auto& internalRg = m_internalScene.getRenderGroup(group.m_impl.getRenderGroupHandle()); + ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(nestedRenderGroup.m_impl.getRenderGroupHandle(), internalRg)); + EXPECT_EQ(order, ramses_internal::RenderGroupUtils::FindRenderGroupEntry(nestedRenderGroup.m_impl.getRenderGroupHandle(), internalRg)->order); } void expectMeshNotContained(const MeshNode& mesh, const RenderGroup& group) { EXPECT_FALSE(group.containsMeshNode(mesh)); - const auto& internalRg = m_internalScene.getRenderGroup(group.impl.getRenderGroupHandle()); - EXPECT_FALSE(ramses_internal::RenderGroupUtils::ContainsRenderable(mesh.impl.getRenderableHandle(), internalRg)); + const auto& internalRg = m_internalScene.getRenderGroup(group.m_impl.getRenderGroupHandle()); + EXPECT_FALSE(ramses_internal::RenderGroupUtils::ContainsRenderable(mesh.m_impl.getRenderableHandle(), internalRg)); } void expectRenderGroupNotContained(const RenderGroup& nestedRenderGroup, const RenderGroup& group) { EXPECT_FALSE(group.containsRenderGroup(nestedRenderGroup)); - const auto& internalRg = m_internalScene.getRenderGroup(group.impl.getRenderGroupHandle()); - EXPECT_FALSE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(nestedRenderGroup.impl.getRenderGroupHandle(), internalRg)); + const auto& internalRg = m_internalScene.getRenderGroup(group.m_impl.getRenderGroupHandle()); + EXPECT_FALSE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(nestedRenderGroup.m_impl.getRenderGroupHandle(), internalRg)); } void addValidMeshToRenderGroup() @@ -128,6 +128,20 @@ namespace ramses EXPECT_NE(StatusOK, renderGroup.validate()); } + TEST_F(ARenderGroup, validatesIfEmptyButNestedRenderGroupIsNot) + { + // empty -> invalid + EXPECT_NE(StatusOK, renderGroup.validate()); + + // nested group also empty -> invalid + renderGroup.addRenderGroup(renderGroup2); + EXPECT_NE(StatusOK, renderGroup.validate()); + + // add mesh to nested group -> valid + EXPECT_EQ(StatusOK, renderGroup2.addMeshNode(createValidMeshNode(), 3)); + EXPECT_EQ(StatusOK, renderGroup.validate()); + } + TEST_F(ARenderGroup, validationGivesWarningIfRenderGroupContainsInvalidMesh) { addBrokenMeshToRenderGroup(); diff --git a/client/ramses-client/test/RenderPassGroupIteratorTest.cpp b/client/test/RenderPassGroupIteratorTest.cpp similarity index 100% rename from client/ramses-client/test/RenderPassGroupIteratorTest.cpp rename to client/test/RenderPassGroupIteratorTest.cpp diff --git a/client/ramses-client/test/RenderPassTest.cpp b/client/test/RenderPassTest.cpp similarity index 90% rename from client/ramses-client/test/RenderPassTest.cpp rename to client/test/RenderPassTest.cpp index 79137286d..56af06a56 100644 --- a/client/ramses-client/test/RenderPassTest.cpp +++ b/client/test/RenderPassTest.cpp @@ -30,11 +30,11 @@ using namespace ramses_internal; namespace ramses { - class ARenderPass : public LocalTestClientWithSceneAndAnimationSystem, public testing::Test + class ARenderPass : public LocalTestClientWithScene, public testing::Test { protected: ARenderPass() - : LocalTestClientWithSceneAndAnimationSystem() + : LocalTestClientWithScene() , renderpass(*m_scene.createRenderPass("RenderPass")) , renderpass2(*m_scene.createRenderPass("RenderPass2")) { @@ -59,7 +59,7 @@ namespace ramses RenderTarget* createRenderTarget() { - const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(16u, 12u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* renderBuffer = m_scene.createRenderBuffer(16u, 12u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); RenderTargetDescription rtDesc; rtDesc.addRenderBuffer(*renderBuffer); return m_scene.createRenderTarget(rtDesc); @@ -67,7 +67,7 @@ namespace ramses void expectNumGroupsContained(uint32_t numGroups, const RenderPass& pass) { - EXPECT_EQ(numGroups, static_cast(pass.impl.getAllRenderGroups().size())); + EXPECT_EQ(numGroups, static_cast(pass.m_impl.getAllRenderGroups().size())); } void expectGroupContained(const RenderGroup& group, int32_t order, const RenderPass& pass) @@ -77,17 +77,17 @@ namespace ramses EXPECT_EQ(StatusOK, pass.getRenderGroupOrder(group, actualOrder)); EXPECT_EQ(order, actualOrder); - const ramses_internal::RenderPass& internalRP = m_internalScene.getRenderPass(pass.impl.getRenderPassHandle()); - ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(group.impl.getRenderGroupHandle(), internalRP)); - EXPECT_EQ(group.impl.getRenderGroupHandle(), ramses_internal::RenderGroupUtils::FindRenderGroupEntry(group.impl.getRenderGroupHandle(), internalRP)->renderGroup); - EXPECT_EQ(order, ramses_internal::RenderGroupUtils::FindRenderGroupEntry(group.impl.getRenderGroupHandle(), internalRP)->order); + const ramses_internal::RenderPass& internalRP = m_internalScene.getRenderPass(pass.m_impl.getRenderPassHandle()); + ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(group.m_impl.getRenderGroupHandle(), internalRP)); + EXPECT_EQ(group.m_impl.getRenderGroupHandle(), ramses_internal::RenderGroupUtils::FindRenderGroupEntry(group.m_impl.getRenderGroupHandle(), internalRP)->renderGroup); + EXPECT_EQ(order, ramses_internal::RenderGroupUtils::FindRenderGroupEntry(group.m_impl.getRenderGroupHandle(), internalRP)->order); } void expectGroupNotContained(const RenderGroup& group, const RenderPass& pass) { EXPECT_FALSE(pass.containsRenderGroup(group)); - const ramses_internal::RenderPass& internalRP = m_internalScene.getRenderPass(pass.impl.getRenderPassHandle()); - EXPECT_FALSE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(group.impl.getRenderGroupHandle(), internalRP)); + const ramses_internal::RenderPass& internalRP = m_internalScene.getRenderPass(pass.m_impl.getRenderPassHandle()); + EXPECT_FALSE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(group.m_impl.getRenderGroupHandle(), internalRP)); } RenderPass& renderpass; @@ -353,14 +353,14 @@ namespace ramses TEST_F(ARenderPass, hasFrameBufferAsDefaultRenderTarget) { EXPECT_EQ(nullptr, renderpass.getRenderTarget()); - EXPECT_FALSE(m_scene.impl.getIScene().getRenderPass(renderpass.impl.getRenderPassHandle()).renderTarget.isValid()); + EXPECT_FALSE(m_scene.m_impl.getIScene().getRenderPass(renderpass.m_impl.getRenderPassHandle()).renderTarget.isValid()); } TEST_F(ARenderPass, canBeAssignedFrameBufferAsRenderTarget) { EXPECT_EQ(StatusOK, renderpass.setRenderTarget(nullptr)); EXPECT_EQ(nullptr, renderpass.getRenderTarget()); - EXPECT_FALSE(m_scene.impl.getIScene().getRenderPass(renderpass.impl.getRenderPassHandle()).renderTarget.isValid()); + EXPECT_FALSE(m_scene.m_impl.getIScene().getRenderPass(renderpass.m_impl.getRenderPassHandle()).renderTarget.isValid()); } TEST_F(ARenderPass, canSwitchFromRenderTargetBackToFramebuffer) @@ -375,14 +375,14 @@ namespace ramses // then set framebuffer rendering EXPECT_EQ(StatusOK, renderpass.setRenderTarget(nullptr)); EXPECT_EQ(nullptr, renderpass.getRenderTarget()); - EXPECT_FALSE(m_scene.impl.getIScene().getRenderPass(renderpass.impl.getRenderPassHandle()).renderTarget.isValid()); + EXPECT_FALSE(m_scene.m_impl.getIScene().getRenderPass(renderpass.m_impl.getRenderPassHandle()).renderTarget.isValid()); } TEST_F(ARenderPass, reportsErrorWhenSetRenderTargetFromAnotherScene) { Scene& anotherScene = *client.createScene(ramses::sceneId_t(12u)); - const RenderBuffer* renderBuffer = anotherScene.createRenderBuffer(16u, 12u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* renderBuffer = anotherScene.createRenderBuffer(16u, 12u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); RenderTargetDescription rtDesc; rtDesc.addRenderBuffer(*renderBuffer); RenderTarget* renderTarget = anotherScene.createRenderTarget(rtDesc); @@ -399,7 +399,7 @@ namespace ramses { RenderTarget* renderTarget = createRenderTarget(); EXPECT_NE(StatusOK, renderpass.setRenderTarget(renderTarget)); - EXPECT_NE(renderTarget->impl.getRenderTargetHandle(), m_scene.impl.getIScene().getRenderPass(renderpass.impl.getRenderPassHandle()).renderTarget); + EXPECT_NE(renderTarget->m_impl.getRenderTargetHandle(), m_scene.m_impl.getIScene().getRenderPass(renderpass.m_impl.getRenderPassHandle()).renderTarget); EXPECT_NE(renderTarget, renderpass.getRenderTarget()); } @@ -411,7 +411,7 @@ namespace ramses ASSERT_EQ(StatusOK, renderpass.setCamera(*orthoCamera)); EXPECT_EQ(StatusOK, renderpass.setRenderTarget(renderTarget)); - EXPECT_EQ(renderTarget->impl.getRenderTargetHandle(), m_scene.impl.getIScene().getRenderPass(renderpass.impl.getRenderPassHandle()).renderTarget); + EXPECT_EQ(renderTarget->m_impl.getRenderTargetHandle(), m_scene.m_impl.getIScene().getRenderPass(renderpass.m_impl.getRenderPassHandle()).renderTarget); EXPECT_EQ(renderTarget, renderpass.getRenderTarget()); } @@ -423,7 +423,7 @@ namespace ramses ASSERT_EQ(StatusOK, renderpass.setCamera(*perspectiveCamera)); EXPECT_EQ(StatusOK, renderpass.setRenderTarget(renderTarget)); - EXPECT_EQ(renderTarget->impl.getRenderTargetHandle(), m_scene.impl.getIScene().getRenderPass(renderpass.impl.getRenderPassHandle()).renderTarget); + EXPECT_EQ(renderTarget->m_impl.getRenderTargetHandle(), m_scene.m_impl.getIScene().getRenderPass(renderpass.m_impl.getRenderPassHandle()).renderTarget); EXPECT_EQ(renderTarget, renderpass.getRenderTarget()); } @@ -453,15 +453,14 @@ namespace ramses TEST_F(ARenderPass, setsItsClearColor) { - float c[4]; - renderpass.getClearColor(c[0], c[1], c[2], c[3]); + auto c = renderpass.getClearColor(); EXPECT_EQ(0.f, c[0]); EXPECT_EQ(0.f, c[1]); EXPECT_EQ(0.f, c[2]); EXPECT_EQ(1.f, c[3]); - EXPECT_EQ(StatusOK, renderpass.setClearColor(0.1f, 0.2f, 0.3f, 0.4f)); - renderpass.getClearColor(c[0], c[1], c[2], c[3]); + EXPECT_EQ(StatusOK, renderpass.setClearColor({0.1f, 0.2f, 0.3f, 0.4f})); + c = renderpass.getClearColor(); EXPECT_EQ(0.1f, c[0]); EXPECT_EQ(0.2f, c[1]); EXPECT_EQ(0.3f, c[2]); @@ -470,13 +469,13 @@ namespace ramses TEST_F(ARenderPass, setsItsClearFlag) { - EXPECT_EQ(static_cast(ramses::EClearFlags::EClearFlags_All), renderpass.getClearFlags()); + EXPECT_EQ(static_cast(ramses::EClearFlags::EClearFlags_All), renderpass.getClearFlags()); EXPECT_EQ(StatusOK, renderpass.setClearFlags(ramses::EClearFlags::EClearFlags_None)); - EXPECT_EQ(static_cast(ramses::EClearFlags::EClearFlags_None), renderpass.getClearFlags()); + EXPECT_EQ(static_cast(ramses::EClearFlags::EClearFlags_None), renderpass.getClearFlags()); EXPECT_EQ(StatusOK, renderpass.setClearFlags(ramses::EClearFlags::EClearFlags_Depth)); - EXPECT_EQ(static_cast(ramses::EClearFlags::EClearFlags_Depth), renderpass.getClearFlags()); + EXPECT_EQ(static_cast(ramses::EClearFlags::EClearFlags_Depth), renderpass.getClearFlags()); EXPECT_EQ(StatusOK, renderpass.setClearFlags(ramses::EClearFlags::EClearFlags_Color | ramses::EClearFlags::EClearFlags_Stencil)); EXPECT_EQ(static_cast(ramses::EClearFlags::EClearFlags_Color | ramses::EClearFlags::EClearFlags_Stencil), renderpass.getClearFlags()); @@ -624,8 +623,8 @@ namespace ramses EXPECT_EQ(StatusOK, m_scene.destroy(*group)); - EXPECT_TRUE(renderpass.impl.getAllRenderGroups().empty()); - EXPECT_TRUE(renderpass2.impl.getAllRenderGroups().empty()); + EXPECT_TRUE(renderpass.m_impl.getAllRenderGroups().empty()); + EXPECT_TRUE(renderpass2.m_impl.getAllRenderGroups().empty()); } TEST_F(ARenderPass, canRemoveAllRenderGroups) diff --git a/client/test/RenderTargetDescriptionTest.cpp b/client/test/RenderTargetDescriptionTest.cpp new file mode 100644 index 000000000..39128a670 --- /dev/null +++ b/client/test/RenderTargetDescriptionTest.cpp @@ -0,0 +1,206 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2015 BMW Car IT GmbH +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +#include +#include "ramses-client-api/RenderBuffer.h" +#include "ramses-client-api/RenderTargetDescription.h" +#include "RenderBufferImpl.h" +#include "RenderTargetDescriptionImpl.h" +#include "ClientTestUtils.h" + +using namespace testing; + +namespace ramses +{ + class RenderTargetDescriptionTest : public LocalTestClientWithScene, public testing::Test + { + public: + protected: + RenderTargetDescription rtDesc; + }; + + TEST_F(RenderTargetDescriptionTest, initialState) + { + EXPECT_TRUE(rtDesc.m_impl.get().getRenderBuffers().empty()); + } + + TEST_F(RenderTargetDescriptionTest, validatesAsWarningIfEmpty) + { + EXPECT_NE(StatusOK, rtDesc.validate()); + } + + TEST_F(RenderTargetDescriptionTest, canAddRenderBuffer) + { + const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); + + ASSERT_EQ(1u, rtDesc.m_impl.get().getRenderBuffers().size()); + EXPECT_EQ(colorRb.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[0]); + + EXPECT_EQ(StatusOK, rtDesc.validate()); + } + + TEST_F(RenderTargetDescriptionTest, canAddMultipleColorRenderBuffers) + { + const RenderBuffer& colorRb1 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + const RenderBuffer& colorRb2 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb1)); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb2)); + + ASSERT_EQ(2u, rtDesc.m_impl.get().getRenderBuffers().size()); + EXPECT_EQ(colorRb1.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[0]); + EXPECT_EQ(colorRb2.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[1]); + + EXPECT_EQ(StatusOK, rtDesc.validate()); + } + + TEST_F(RenderTargetDescriptionTest, canAddMultipleColorRenderBuffersWithDepthBuffer) + { + const RenderBuffer& colorRb1 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + const RenderBuffer& colorRb2 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + const RenderBuffer& depthRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Depth, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::ReadWrite); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb1)); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb2)); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(depthRb)); + + ASSERT_EQ(3u, rtDesc.m_impl.get().getRenderBuffers().size()); + EXPECT_EQ(colorRb1.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[0]); + EXPECT_EQ(colorRb2.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[1]); + EXPECT_EQ(depthRb.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[2]); + + EXPECT_EQ(StatusOK, rtDesc.validate()); + } + + TEST_F(RenderTargetDescriptionTest, failsToAddSameRenderBufferTwice) + { + const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); + EXPECT_NE(StatusOK, rtDesc.addRenderBuffer(colorRb)); + + ASSERT_EQ(1u, rtDesc.m_impl.get().getRenderBuffers().size()); + EXPECT_EQ(colorRb.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[0]); + } + + TEST_F(RenderTargetDescriptionTest, failsToAddRenderBufferFromAnotherScene) + { + const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); + + Scene& otherScene = *client.createScene(sceneId_t(666u)); + const RenderBuffer& otherRb = *otherScene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + EXPECT_NE(StatusOK, rtDesc.addRenderBuffer(otherRb)); + + ASSERT_EQ(1u, rtDesc.m_impl.get().getRenderBuffers().size()); + EXPECT_EQ(colorRb.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[0]); + } + + TEST_F(RenderTargetDescriptionTest, failsToAddRenderBufferWithDifferentResolution) + { + const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); + + const RenderBuffer& otherRb = *m_scene.createRenderBuffer(1u, 2u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + EXPECT_NE(StatusOK, rtDesc.addRenderBuffer(otherRb)); + + ASSERT_EQ(1u, rtDesc.m_impl.get().getRenderBuffers().size()); + EXPECT_EQ(colorRb.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[0]); + } + + TEST_F(RenderTargetDescriptionTest, failsToAddMoreThanOneDepthBuffer) + { + const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + const RenderBuffer& depthRb1 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Depth, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::ReadWrite); + const RenderBuffer& depthRb2 = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::DepthStencil, ERenderBufferFormat::Depth24_Stencil8, ERenderBufferAccessMode::ReadWrite); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(depthRb1)); + EXPECT_NE(StatusOK, rtDesc.addRenderBuffer(depthRb2)); + + ASSERT_EQ(2u, rtDesc.m_impl.get().getRenderBuffers().size()); + EXPECT_EQ(colorRb.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[0]); + EXPECT_EQ(depthRb1.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[1]); + } + + TEST_F(RenderTargetDescriptionTest, failsToValidateAfterAddedRenderBufferDestroyed) + { + RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); + EXPECT_EQ(StatusOK, rtDesc.validate()); + + m_scene.destroy(colorRb); + EXPECT_NE(StatusOK, rtDesc.validate()); + } + + TEST_F(RenderTargetDescriptionTest, canAddRenderBuffersWithMsaaSampleCountNotZero) + { + const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::WriteOnly, 4u); + const RenderBuffer& depthRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Depth, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::WriteOnly, 4u); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(depthRb)); + + ASSERT_EQ(2u, rtDesc.m_impl.get().getRenderBuffers().size()); + EXPECT_EQ(colorRb.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[0]); + EXPECT_EQ(depthRb.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[1]); + } + + TEST_F(RenderTargetDescriptionTest, canNotAddRenderBuffersWithDifferentMsaaSampleCount) + { + const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::WriteOnly, 3u); + const RenderBuffer& depthRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Depth, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::WriteOnly, 4u); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); + EXPECT_NE(StatusOK, rtDesc.addRenderBuffer(depthRb)); + + ASSERT_EQ(1u, rtDesc.m_impl.get().getRenderBuffers().size()); + EXPECT_EQ(colorRb.m_impl.getRenderBufferHandle(), rtDesc.m_impl.get().getRenderBuffers()[0]); + } + + TEST_F(RenderTargetDescriptionTest, CanBeCopyAndMoveConstructed) + { + const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); + + RenderTargetDescription rtDescCopy{ rtDesc }; + EXPECT_THAT(rtDescCopy.m_impl.get().getRenderBuffers(), ElementsAre(colorRb.m_impl.getRenderBufferHandle())); + + RenderTargetDescription rtDescMove{ std::move(rtDesc) }; + EXPECT_THAT(rtDescMove.m_impl.get().getRenderBuffers(), ElementsAre(colorRb.m_impl.getRenderBufferHandle())); + } + + TEST_F(RenderTargetDescriptionTest, CanBeCopyAndMoveAssigned) + { + const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); + + RenderTargetDescription rtDescCopy; + rtDescCopy = rtDesc; + EXPECT_THAT(rtDescCopy.m_impl.get().getRenderBuffers(), ElementsAre(colorRb.m_impl.getRenderBufferHandle())); + + RenderTargetDescription rtDescMove; + rtDescMove = std::move(rtDesc); + EXPECT_THAT(rtDescMove.m_impl.get().getRenderBuffers(), ElementsAre(colorRb.m_impl.getRenderBufferHandle())); + } + + TEST_F(RenderTargetDescriptionTest, CanBeSelfAssigned) + { + const RenderBuffer& colorRb = *m_scene.createRenderBuffer(1u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + EXPECT_EQ(StatusOK, rtDesc.addRenderBuffer(colorRb)); + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wself-move" +#pragma clang diagnostic ignored "-Wself-assign-overloaded" +#endif + rtDesc = rtDesc; + EXPECT_THAT(rtDesc.m_impl.get().getRenderBuffers(), ElementsAre(colorRb.m_impl.getRenderBufferHandle())); + rtDesc = std::move(rtDesc); + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_THAT(rtDesc.m_impl.get().getRenderBuffers(), ElementsAre(colorRb.m_impl.getRenderBufferHandle())); +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + } +} diff --git a/client/ramses-client/test/RenderTargetTest.cpp b/client/test/RenderTargetTest.cpp similarity index 87% rename from client/ramses-client/test/RenderTargetTest.cpp rename to client/test/RenderTargetTest.cpp index d8cf33c91..0a613755e 100644 --- a/client/ramses-client/test/RenderTargetTest.cpp +++ b/client/test/RenderTargetTest.cpp @@ -25,7 +25,7 @@ namespace ramses TEST_F(ARenderTarget, canBeCreated) { - const RenderBuffer& rb = *m_scene.createRenderBuffer(16u, 8u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer& rb = *m_scene.createRenderBuffer(16u, 8u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); RenderTargetDescription rtDesc; rtDesc.addRenderBuffer(rb); @@ -34,10 +34,10 @@ namespace ramses EXPECT_EQ(16u, renderTarget->getWidth()); EXPECT_EQ(8u, renderTarget->getHeight()); - const ramses_internal::RenderTargetHandle rtHandle = renderTarget->impl.getRenderTargetHandle(); + const ramses_internal::RenderTargetHandle rtHandle = renderTarget->m_impl.getRenderTargetHandle(); EXPECT_TRUE(m_internalScene.isRenderTargetAllocated(rtHandle)); ASSERT_EQ(1u, m_internalScene.getRenderTargetRenderBufferCount(rtHandle)); - EXPECT_EQ(rb.impl.getRenderBufferHandle(), m_internalScene.getRenderTargetRenderBuffer(rtHandle, 0u)); + EXPECT_EQ(rb.m_impl.getRenderBufferHandle(), m_internalScene.getRenderTargetRenderBuffer(rtHandle, 0u)); } TEST_F(ARenderTarget, failsToCreateUsingInvalidRenderTargetDescription) diff --git a/client/ramses-client/test/ResourceTest.cpp b/client/test/ResourceTest.cpp similarity index 78% rename from client/ramses-client/test/ResourceTest.cpp rename to client/test/ResourceTest.cpp index fbc0f612b..b585b5613 100644 --- a/client/ramses-client/test/ResourceTest.cpp +++ b/client/test/ResourceTest.cpp @@ -13,7 +13,6 @@ #include "ramses-client-api/EffectDescription.h" #include "ramses-client-api/TextureSwizzle.h" #include "ramses-client-api/SceneObjectIterator.h" -#include "ramses-hmi-utils.h" #include "ramses-utils.h" #include "Texture2DImpl.h" @@ -30,6 +29,7 @@ #include "UnsafeTestMemoryHelpers.h" #include +#include namespace ramses { @@ -43,12 +43,12 @@ namespace ramses m_oldLogLevel = ramses_internal::CONTEXT_HLAPI_CLIENT.getLogLevel(); ramses_internal::CONTEXT_HLAPI_CLIENT.setLogLevel(ramses_internal::ELogLevel::Trace); } - ~AResourceTestClient() { + ~AResourceTestClient() override { ramses_internal::CONTEXT_HLAPI_CLIENT.setLogLevel(m_oldLogLevel); } ramses_internal::ManagedResource getCreatedResource(const ramses_internal::ResourceContentHash& hash) { - return client.impl.getClientApplication().getResource(hash); + return client.m_impl.getClientApplication().getResource(hash); } ramses_internal::ELogLevel m_oldLogLevel; }; @@ -214,7 +214,7 @@ namespace ramses { const uint8_t data[4 * 10 * 12] = {}; MipLevelData mipLevelData(sizeof(data), data); - Texture2D* texture = m_scene.createTexture2D(ramses::ETextureFormat::RGBA8, 10, 12, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, nullptr); + Texture2D* texture = m_scene.createTexture2D(ramses::ETextureFormat::RGBA8, 10, 12, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, {}); ASSERT_TRUE(nullptr != texture); EXPECT_STREQ("", texture->getName()); } @@ -226,7 +226,7 @@ namespace ramses Texture2D* texture = m_scene.createTexture2D(ramses::ETextureFormat::RGBA8, 10, 12, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, "name"); ASSERT_TRUE(nullptr != texture); - const ramses_internal::ResourceContentHash hash = texture->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = texture->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash.isValid()); } @@ -238,14 +238,14 @@ namespace ramses Texture2D* texture = m_scene.createTexture2D(ramses::ETextureFormat::RGBA8, 10, 12, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, "name"); ASSERT_TRUE(nullptr != texture); - const ramses_internal::ResourceContentHash hash = texture->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = texture->m_impl.getLowlevelResourceHash(); uint8_t data2[4 * 10 * 12] = {}; data[20] = 42; MipLevelData mipLevelData2(sizeof(data2), data2); Texture2D* texture2 = m_scene.createTexture2D(ramses::ETextureFormat::RGBA8, 10, 12, 1, &mipLevelData2, false, {}, ramses::ResourceCacheFlag_DoNotCache, "name"); ASSERT_TRUE(nullptr != texture2); - const ramses_internal::ResourceContentHash hash2 = texture2->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash2 = texture2->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash != hash2); } @@ -253,9 +253,9 @@ namespace ramses { const uint8_t data[1 * 2 * 4] = { 1, 2, 3, 4, 5, 6, 7, 8 }; MipLevelData mipLevelData(sizeof(data), data); - Texture2D* texture = m_scene.createTexture2D(ramses::ETextureFormat::RGBA8, 2, 1, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, nullptr); + Texture2D* texture = m_scene.createTexture2D(ramses::ETextureFormat::RGBA8, 2, 1, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, {}); ASSERT_TRUE(nullptr != texture); - ramses_internal::ManagedResource res = getCreatedResource(texture->impl.getLowlevelResourceHash()); + ramses_internal::ManagedResource res = getCreatedResource(texture->m_impl.getLowlevelResourceHash()); ASSERT_EQ(sizeof(data), res->getDecompressedDataSize()); EXPECT_EQ(data, res->getResourceData().span()); @@ -265,10 +265,10 @@ namespace ramses { const uint8_t data[1 * 2 * 3] = { 1, 2, 3, 4, 5, 6 }; MipLevelData mipLevelData(sizeof(data), data); - Texture2D* texture = m_scene.createTexture2D(ramses::ETextureFormat::RGB8, 2, 1, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, nullptr); + Texture2D* texture = m_scene.createTexture2D(ramses::ETextureFormat::RGB8, 2, 1, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, {}); ASSERT_TRUE(nullptr != texture); - ramses_internal::ManagedResource res = getCreatedResource(texture->impl.getLowlevelResourceHash()); + ramses_internal::ManagedResource res = getCreatedResource(texture->m_impl.getLowlevelResourceHash()); ASSERT_EQ(sizeof(data), res->getDecompressedDataSize()); EXPECT_EQ(data, res->getResourceData().span()); @@ -279,10 +279,10 @@ namespace ramses const uint8_t data0[2 * 2 * 3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; const uint8_t data1[1 * 1 * 3] = { 13, 14, 15 }; const MipLevelData mipLevelData[2] = { { sizeof(data0), data0 },{ sizeof(data1), data1 } }; - const Texture2D* texture = m_scene.createTexture2D(ramses::ETextureFormat::RGB8, 2, 2, 2, mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, nullptr); + const Texture2D* texture = m_scene.createTexture2D(ramses::ETextureFormat::RGB8, 2, 2, 2, mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, {}); ASSERT_TRUE(nullptr != texture); - const ramses_internal::ManagedResource res = getCreatedResource(texture->impl.getLowlevelResourceHash()); + const ramses_internal::ManagedResource res = getCreatedResource(texture->m_impl.getLowlevelResourceHash()); ASSERT_EQ(sizeof(data0) + sizeof(data1), res->getDecompressedDataSize()); EXPECT_EQ(data0, res->getResourceData().span().subspan(0, sizeof(data0))); @@ -382,7 +382,7 @@ namespace ramses { const uint8_t data[4 * 10 * 10] = {}; CubeMipLevelData mipLevelData(sizeof(data), data, data, data, data, data, data); - TextureCube* texture = m_scene.createTextureCube(ramses::ETextureFormat::RGBA8, 10, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, nullptr); + TextureCube* texture = m_scene.createTextureCube(ramses::ETextureFormat::RGBA8, 10, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, {}); ASSERT_TRUE(nullptr != texture); EXPECT_STREQ("", texture->getName()); } @@ -393,7 +393,7 @@ namespace ramses CubeMipLevelData mipLevelData(sizeof(data), data, data, data, data, data, data); TextureCube* texture = m_scene.createTextureCube(ramses::ETextureFormat::RGBA8, 2, 1, &mipLevelData, false); ASSERT_TRUE(nullptr != texture); - ramses_internal::ManagedResource res = getCreatedResource(texture->impl.getLowlevelResourceHash()); + ramses_internal::ManagedResource res = getCreatedResource(texture->m_impl.getLowlevelResourceHash()); ASSERT_EQ(sizeof(data) * 6u, res->getDecompressedDataSize()); for (uint32_t i = 0u; i < 6u; ++i) @@ -406,7 +406,7 @@ namespace ramses CubeMipLevelData mipLevelData(sizeof(data), data, data, data, data, data, data); TextureCube* texture = m_scene.createTextureCube(ramses::ETextureFormat::RGB8, 2, 1, &mipLevelData, false); ASSERT_TRUE(nullptr != texture); - ramses_internal::ManagedResource res = getCreatedResource(texture->impl.getLowlevelResourceHash()); + ramses_internal::ManagedResource res = getCreatedResource(texture->m_impl.getLowlevelResourceHash()); ASSERT_EQ(sizeof(data) * 6u, res->getDecompressedDataSize()); for (uint32_t i = 0u; i < 6u; ++i) @@ -433,10 +433,10 @@ namespace ramses { sizeof(data0px), data0px, data0nx, data0py, data0ny, data0pz, data0nz }, { sizeof(data1px), data1px, data1nx, data1py, data1ny, data1pz, data1nz } }; - const TextureCube* texture = m_scene.createTextureCube(ramses::ETextureFormat::RGB8, 2u, 2, mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, nullptr); + const TextureCube* texture = m_scene.createTextureCube(ramses::ETextureFormat::RGB8, 2u, 2, mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, {}); ASSERT_TRUE(nullptr != texture); - const ramses_internal::ManagedResource res = getCreatedResource(texture->impl.getLowlevelResourceHash()); + const ramses_internal::ManagedResource res = getCreatedResource(texture->m_impl.getLowlevelResourceHash()); ASSERT_EQ(6u * (sizeof(data0px) + sizeof(data1px)), res->getDecompressedDataSize()); @@ -609,7 +609,7 @@ namespace ramses { const uint8_t data[4 * 10 * 12 * 14] = {}; MipLevelData mipLevelData(sizeof(data), data); - Texture3D* texture = m_scene.createTexture3D(ramses::ETextureFormat::RGBA8, 10, 12, 14, 1, &mipLevelData, false, ramses::ResourceCacheFlag_DoNotCache, nullptr); + Texture3D* texture = m_scene.createTexture3D(ramses::ETextureFormat::RGBA8, 10, 12, 14, 1, &mipLevelData, false, ramses::ResourceCacheFlag_DoNotCache, {}); ASSERT_TRUE(nullptr != texture); EXPECT_STREQ("", texture->getName()); } @@ -621,7 +621,7 @@ namespace ramses Texture3D* texture = m_scene.createTexture3D(ramses::ETextureFormat::RGBA8, 10, 12, 14, 1, &mipLevelData, false, ramses::ResourceCacheFlag_DoNotCache, "name"); ASSERT_TRUE(nullptr != texture); - const ramses_internal::ResourceContentHash hash = texture->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = texture->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash.isValid()); } @@ -633,14 +633,14 @@ namespace ramses Texture3D* texture = m_scene.createTexture3D(ramses::ETextureFormat::RGBA8, 10, 12, 14, 1, &mipLevelData1, false, ramses::ResourceCacheFlag_DoNotCache, "name"); ASSERT_TRUE(nullptr != texture); - const ramses_internal::ResourceContentHash hash = texture->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = texture->m_impl.getLowlevelResourceHash(); uint8_t data2[4 * 10 * 12 * 14] = {}; data[20] = 42; MipLevelData mipLevelData2(sizeof(data2), data2); Texture3D* texture2 = m_scene.createTexture3D(ramses::ETextureFormat::RGBA8, 10, 12, 14, 1, &mipLevelData2, false, ramses::ResourceCacheFlag_DoNotCache, "name"); ASSERT_TRUE(nullptr != texture2); - const ramses_internal::ResourceContentHash hash2 = texture2->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash2 = texture2->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash != hash2); } @@ -648,10 +648,10 @@ namespace ramses { const uint8_t data[1 * 2 * 2 * 4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; MipLevelData mipLevelData(sizeof(data), data); - Texture3D* texture = m_scene.createTexture3D(ramses::ETextureFormat::RGBA8, 2, 1, 2, 1, &mipLevelData, false, ramses::ResourceCacheFlag_DoNotCache, nullptr); + Texture3D* texture = m_scene.createTexture3D(ramses::ETextureFormat::RGBA8, 2, 1, 2, 1, &mipLevelData, false, ramses::ResourceCacheFlag_DoNotCache, {}); ASSERT_TRUE(nullptr != texture); - ramses_internal::ManagedResource res = getCreatedResource(texture->impl.getLowlevelResourceHash()); + ramses_internal::ManagedResource res = getCreatedResource(texture->m_impl.getLowlevelResourceHash()); ASSERT_EQ(sizeof(data), res->getDecompressedDataSize()); EXPECT_EQ(data, res->getResourceData().span()); @@ -661,10 +661,10 @@ namespace ramses { const uint8_t data[1 * 2 * 2 * 3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; MipLevelData mipLevelData(sizeof(data), data); - Texture3D* texture = m_scene.createTexture3D(ramses::ETextureFormat::RGB8, 2, 1, 2, 1, &mipLevelData, false, ramses::ResourceCacheFlag_DoNotCache, nullptr); + Texture3D* texture = m_scene.createTexture3D(ramses::ETextureFormat::RGB8, 2, 1, 2, 1, &mipLevelData, false, ramses::ResourceCacheFlag_DoNotCache, {}); ASSERT_TRUE(nullptr != texture); - ramses_internal::ManagedResource res = getCreatedResource(texture->impl.getLowlevelResourceHash()); + ramses_internal::ManagedResource res = getCreatedResource(texture->m_impl.getLowlevelResourceHash()); ASSERT_EQ(sizeof(data), res->getDecompressedDataSize()); EXPECT_EQ(data, res->getResourceData().span()); @@ -675,10 +675,10 @@ namespace ramses const uint8_t data0[2 * 2 * 2 * 3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120 }; const uint8_t data1[1 * 1 * 3] = { 13, 14, 15 }; const MipLevelData mipLevelData[2] = { { sizeof(data0), data0 },{ sizeof(data1), data1 } }; - const Texture3D* texture = m_scene.createTexture3D(ramses::ETextureFormat::RGB8, 2u, 2u, 2u, 2, mipLevelData, false, ramses::ResourceCacheFlag_DoNotCache, nullptr); + const Texture3D* texture = m_scene.createTexture3D(ramses::ETextureFormat::RGB8, 2u, 2u, 2u, 2, mipLevelData, false, ramses::ResourceCacheFlag_DoNotCache, {}); ASSERT_TRUE(nullptr != texture); - const ramses_internal::ManagedResource res = getCreatedResource(texture->impl.getLowlevelResourceHash()); + const ramses_internal::ManagedResource res = getCreatedResource(texture->m_impl.getLowlevelResourceHash()); ASSERT_EQ(sizeof(data0) + sizeof(data1), res->getDecompressedDataSize()); EXPECT_EQ(data0, res->getResourceData().span().subspan(0, sizeof(data0))); @@ -776,50 +776,50 @@ namespace ramses TEST_F(AResourceTestClient, creatingResourcesWithSameDataInternallyRefersToSameResourceHash) { - const float data[2 * 2] = {1,2,3,4}; - auto a = m_scene.createArrayResource(EDataType::Vector2F, 2, data); - auto b = m_scene.createArrayResource(EDataType::Vector2F, 2, data); + const vec2f data[2] = { vec2f{1.f,2.f}, vec2f{3.f,4.f} }; + auto a = m_scene.createArrayResource(2, data); + auto b = m_scene.createArrayResource(2, data); - ASSERT_EQ(a->impl.getLowlevelResourceHash(), b->impl.getLowlevelResourceHash()); + ASSERT_EQ(a->m_impl.getLowlevelResourceHash(), b->m_impl.getLowlevelResourceHash()); } TEST_F(AResourceTestClient, creatingResourcesWithSameDataInternallySharesData) { - const float data[2 * 2] = { 1,2,3,4 }; - auto a = m_scene.createArrayResource(EDataType::Vector2F, 2, data); - auto b = m_scene.createArrayResource(EDataType::Vector2F, 2, data); - ramses_internal::ManagedResource resourceA = client.impl.getResource(a->impl.getLowlevelResourceHash()); - ramses_internal::ManagedResource resourceB = client.impl.getResource(b->impl.getLowlevelResourceHash()); + const vec2f data[2] = { vec2f{1.f,2.f}, vec2f{3.f,4.f} }; + auto a = m_scene.createArrayResource(2, data); + auto b = m_scene.createArrayResource(2, data); + ramses_internal::ManagedResource resourceA = client.m_impl.getResource(a->m_impl.getLowlevelResourceHash()); + ramses_internal::ManagedResource resourceB = client.m_impl.getResource(b->m_impl.getLowlevelResourceHash()); ASSERT_EQ(resourceA, resourceB); } TEST_F(AResourceTestClient, creatingResourcesWithSameDataContentInternallySharesData) { - const float data[2 * 2] = { 1,2,3,4 }; - const float data2[2 * 2] = { 1,2,3,4 }; - auto a = m_scene.createArrayResource(EDataType::Vector2F, 2, data); - auto b = m_scene.createArrayResource(EDataType::Vector2F, 2, data2); - ramses_internal::ManagedResource resourceA = client.impl.getResource(a->impl.getLowlevelResourceHash()); - ramses_internal::ManagedResource resourceB = client.impl.getResource(b->impl.getLowlevelResourceHash()); + const vec2f data1[2] = { vec2f{1.f,2.f}, vec2f{3.f,4.f} }; + const vec2f data2[2] = { vec2f{1.f,2.f}, vec2f{3.f,4.f} }; + auto a = m_scene.createArrayResource(2, data1); + auto b = m_scene.createArrayResource(2, data2); + ramses_internal::ManagedResource resourceA = client.m_impl.getResource(a->m_impl.getLowlevelResourceHash()); + ramses_internal::ManagedResource resourceB = client.m_impl.getResource(b->m_impl.getLowlevelResourceHash()); ASSERT_EQ(resourceA, resourceB); } TEST_F(AResourceTestClient, destroyingDuplicateResourceDoesNotDeleteData) { - const float data[2 * 2] = { 1, 2, 3, 4 }; - const float data2[2 * 2] = { 1, 2, 3, 4 }; - auto a = m_scene.createArrayResource(EDataType::Vector2F, 2, data); - auto b = m_scene.createArrayResource(EDataType::Vector2F, 2, data2); + const vec2f data1[2] = { vec2f{1.f,2.f}, vec2f{3.f,4.f} }; + const vec2f data2[2] = { vec2f{1.f,2.f}, vec2f{3.f,4.f} }; + auto a = m_scene.createArrayResource(2, data1); + auto b = m_scene.createArrayResource(2, data2); m_scene.destroy(*b); - ramses_internal::ManagedResource aRes = client.impl.getResource(a->impl.getLowlevelResourceHash()); - EXPECT_TRUE(ramses_internal::UnsafeTestMemoryHelpers::CompareMemoryBlobToSpan(&data, sizeof(data), aRes->getResourceData().span())); + ramses_internal::ManagedResource aRes = client.m_impl.getResource(a->m_impl.getLowlevelResourceHash()); + EXPECT_TRUE(ramses_internal::UnsafeTestMemoryHelpers::CompareMemoryBlobToSpan(&data1, sizeof(data1), aRes->getResourceData().span())); } TEST_F(AResourceTestClient, createFloatArray) { const float data[2] = {}; - const auto a = m_scene.createArrayResource(EDataType::Float, 2, data); + const auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); EXPECT_EQ(a->getNumberOfElements(), 2u); EXPECT_EQ(a->getDataType(), EDataType::Float); @@ -828,10 +828,10 @@ namespace ramses TEST_F(AResourceTestClient, createFloatArrayHashIsValid) { const float data[2] = {}; - auto a = m_scene.createArrayResource(EDataType::Float, 2, data); + auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash.isValid()); } @@ -839,28 +839,28 @@ namespace ramses { float data[2] = {}; data[0] = 4; - auto a = m_scene.createArrayResource(EDataType::Float, 2, data); + auto a = m_scene.createArrayResource(2, data); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); float data2[2] = {}; data2[0] = 42.0f; - auto a2 = m_scene.createArrayResource(EDataType::Float, 2, data2); + auto a2 = m_scene.createArrayResource(2, data2); - const ramses_internal::ResourceContentHash hash2 = a2->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash2 = a2->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash != hash2); } TEST_F(AResourceTestClient, createEmptyFloatArrayFails) { - auto a = m_scene.createArrayResource(EDataType::Float, 0, nullptr); + auto a = m_scene.createArrayResource(0, nullptr); EXPECT_TRUE(nullptr == a); } TEST_F(AResourceTestClient, createVector2fArray) { - const float data[2 * 2] = {}; - const auto a = m_scene.createArrayResource(EDataType::Vector2F, 2, data); + const vec2f data[2] = { vec2f{1.f,2.f}, vec2f{3.f,4.f} }; + const auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); EXPECT_EQ(a->getNumberOfElements(), 2u); EXPECT_EQ(a->getDataType(), EDataType::Vector2F); @@ -868,40 +868,40 @@ namespace ramses TEST_F(AResourceTestClient, createVector2fArrayHashIsValid) { - const float data[2 * 2] = {}; - auto a = m_scene.createArrayResource(EDataType::Vector2F, 2, data); + const vec2f data[2] = { vec2f{1.f,2.f}, vec2f{3.f,4.f} }; + const auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash.isValid()); } TEST_F(AResourceTestClient, createVector2fArrayHashIsUnique) { - float data[2 * 2] = {}; - data[0] = 4; - auto a = m_scene.createArrayResource(EDataType::Vector2F, 2, data); + vec2f data[2] = { vec2f{1.f,2.f}, vec2f{3.f,4.f} }; + data[0][0] = 4; + auto a = m_scene.createArrayResource(2, data); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); - float data2[2 * 2] = {}; - data2[0] = 42.0f; - auto a2 = m_scene.createArrayResource(EDataType::Vector2F, 2, data2); + vec2f data2[2] = { vec2f{1.f,2.f}, vec2f{3.f,4.f} }; + data2[0][0] = 42.0f; + auto a2 = m_scene.createArrayResource(2, data2); - const ramses_internal::ResourceContentHash hash2 = a2->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash2 = a2->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash != hash2); } TEST_F(AResourceTestClient, createEmptyVector2fArrayFails) { - auto a = m_scene.createArrayResource(EDataType::Vector2F, 0, nullptr); + auto a = m_scene.createArrayResource(0, nullptr); EXPECT_TRUE(nullptr == a); } TEST_F(AResourceTestClient, createVector3fArray) { - const float data[2 * 3] = {}; - auto a = m_scene.createArrayResource(EDataType::Vector3F, 2, data); + const vec3f data[2] = { vec3f{1.f,2.f,3.f}, vec3f{3.f,4.f,5.f} }; + auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); EXPECT_EQ(a->getNumberOfElements(), 2u); EXPECT_EQ(a->getDataType(), EDataType::Vector3F); @@ -909,40 +909,40 @@ namespace ramses TEST_F(AResourceTestClient, createVector3fArrayHashIsValid) { - const float data[2 * 3] = {}; - auto a = m_scene.createArrayResource(EDataType::Vector3F, 2, data); + const vec3f data[2] = { vec3f{1.f,2.f,3.f}, vec3f{3.f,4.f,5.f} }; + auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash.isValid()); } TEST_F(AResourceTestClient, createVector3fArrayHashIsUnique) { - float data[2 * 3] = {}; - data[0] = 4.0f; - auto a = m_scene.createArrayResource(EDataType::Vector3F, 2, data); + vec3f data[2] = { vec3f{1.f,2.f,3.f}, vec3f{3.f,4.f,5.f} }; + data[0][0] = 4.0f; + auto a = m_scene.createArrayResource(2, data); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); - float data2[2 * 3] = {}; - data2[0] = 44.0f; - auto a2 = m_scene.createArrayResource(EDataType::Vector3F, 2, data2); + vec3f data2[2] = { vec3f{1.f,2.f,3.f}, vec3f{3.f,4.f,5.f} }; + data2[0][0] = 44.0f; + auto a2 = m_scene.createArrayResource(2, data2); - const ramses_internal::ResourceContentHash hash2 = a2->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash2 = a2->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash != hash2); } TEST_F(AResourceTestClient, createEmptyVector3fArrayFails) { - auto a = m_scene.createArrayResource(EDataType::Vector3F, 0, nullptr); + auto a = m_scene.createArrayResource(0, nullptr); EXPECT_TRUE(nullptr == a); } TEST_F(AResourceTestClient, createVector4fArray) { - const float data[2 * 4] = {}; - auto a = m_scene.createArrayResource(EDataType::Vector4F, 2, data); + const vec4f data[2] = { vec4f{1.f,2.f,3.f,4.f}, vec4f{3.f,4.f,5.f,6.f} }; + auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); EXPECT_EQ(a->getNumberOfElements(), 2u); EXPECT_EQ(a->getDataType(), EDataType::Vector4F); @@ -950,40 +950,40 @@ namespace ramses TEST_F(AResourceTestClient, createVector4fArrayHashIsValid) { - const float data[2 * 4] = {}; - auto a = m_scene.createArrayResource(EDataType::Vector4F, 2, data); + const vec4f data[2] = { vec4f{1.f,2.f,3.f,4.f}, vec4f{3.f,4.f,5.f,6.f} }; + auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash.isValid()); } TEST_F(AResourceTestClient, createVector4fArrayHashIsUnique) { - float data[2 * 4] = {}; - data[0] = 4.0f; - auto a = m_scene.createArrayResource(EDataType::Vector4F, 2, data); + vec4f data[2] = { vec4f{1.f,2.f,3.f,4.f}, vec4f{3.f,4.f,5.f,6.f} }; + data[0][0] = 4.0f; + auto a = m_scene.createArrayResource(2, data); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); - float data2[2 * 4] = {}; - data2[0] = 42.0f; - auto a2 = m_scene.createArrayResource(EDataType::Vector4F, 2, data2); + vec4f data2[2] = { vec4f{1.f,2.f,3.f,4.f}, vec4f{3.f,4.f,5.f,6.f} }; + data2[0][0] = 42.0f; + auto a2 = m_scene.createArrayResource(2, data2); - const ramses_internal::ResourceContentHash hash2 = a2->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash2 = a2->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash != hash2); } TEST_F(AResourceTestClient, createEmptyVector4fArrayFails) { - auto a = m_scene.createArrayResource(EDataType::Vector4F, 0, nullptr); + auto a = m_scene.createArrayResource(0, nullptr); EXPECT_TRUE(nullptr == a); } TEST_F(AResourceTestClient, createUInt16ArrayResource) { const uint16_t data[2] = {}; - const ArrayResource *a = m_scene.createArrayResource(EDataType::UInt16, 2, data); + const ArrayResource *a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); EXPECT_EQ(a->getNumberOfElements(), 2u); EXPECT_EQ(a->getDataType(), EDataType::UInt16); @@ -992,10 +992,10 @@ namespace ramses TEST_F(AResourceTestClient, createUInt16ArrayResourceHashIsValid) { const uint16_t data[2] = {}; - const ArrayResource* a = m_scene.createArrayResource(EDataType::UInt16, 2, data); + const ArrayResource* a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash.isValid()); } @@ -1003,28 +1003,28 @@ namespace ramses { uint16_t data[2] = {}; data[0] = 4; - auto a = m_scene.createArrayResource(EDataType::UInt16, 2, data); + auto a = m_scene.createArrayResource(2, data); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); uint16_t data2[2] = {}; data2[0] = 42; - auto a2 = m_scene.createArrayResource(EDataType::UInt16, 2, data2); + auto a2 = m_scene.createArrayResource(2, data2); - const ramses_internal::ResourceContentHash hash2 = a2->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash2 = a2->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash != hash2); } TEST_F(AResourceTestClient, createEmptyUInt16ArrayFails) { - auto a = m_scene.createArrayResource(EDataType::UInt16, 2, nullptr); + auto a = m_scene.createArrayResource(2, nullptr); EXPECT_TRUE(nullptr == a); } TEST_F(AResourceTestClient, createUInt32Array) { const uint32_t data[2] = {}; - auto a = m_scene.createArrayResource(EDataType::UInt32, 2, data); + auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); EXPECT_EQ(a->getNumberOfElements(), 2u); EXPECT_EQ(a->getDataType(), EDataType::UInt32); @@ -1033,10 +1033,10 @@ namespace ramses TEST_F(AResourceTestClient, createUInt32ArrayHashIsValid) { const uint32_t data[2] = {}; - auto a = m_scene.createArrayResource(EDataType::UInt32, 2, data); + auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash.isValid()); } @@ -1044,29 +1044,29 @@ namespace ramses { uint32_t data[2] = {}; data[0] = 4; - auto a = m_scene.createArrayResource(EDataType::UInt32, 2, data); + auto a = m_scene.createArrayResource(2, data); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); uint32_t data2[2] = {}; data2[0] = 42; - auto a2 = m_scene.createArrayResource(EDataType::UInt32, 2, data2); + auto a2 = m_scene.createArrayResource(2, data2); - const ramses_internal::ResourceContentHash hash2 = a2->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash2 = a2->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash != hash2); } TEST_F(AResourceTestClient, createEmptyUInt32ArrayFails) { - auto a = m_scene.createArrayResource(EDataType::UInt32, 0, nullptr); + auto a = m_scene.createArrayResource(0, nullptr); EXPECT_TRUE(nullptr == a); } TEST_F(AResourceTestClient, createByteBlobArray) { - const float data[2] = {}; - const auto a = m_scene.createArrayResource(EDataType::ByteBlob, 2, data); + const ramses::Byte data[2] = {}; + const auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); EXPECT_EQ(a->getNumberOfElements(), 2u); EXPECT_EQ(a->getDataType(), EDataType::ByteBlob); @@ -1074,33 +1074,33 @@ namespace ramses TEST_F(AResourceTestClient, createByteBlobArrayHashIsValid) { - const float data[2] = {}; - auto a = m_scene.createArrayResource(EDataType::ByteBlob, 2, data); + const ramses::Byte data[2] = {}; + const auto a = m_scene.createArrayResource(2, data); ASSERT_TRUE(nullptr != a); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash.isValid()); } TEST_F(AResourceTestClient, createByteBlobArrayHashIsUnique) { - float data[2] = {}; + ramses::Byte data[2] = {}; data[0] = 4; - auto a = m_scene.createArrayResource(EDataType::ByteBlob, sizeof(data), data); + auto a = m_scene.createArrayResource(sizeof(data), data); - const ramses_internal::ResourceContentHash hash = a->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash = a->m_impl.getLowlevelResourceHash(); - float data2[2] = {}; - data2[0] = 42.0f; - auto a2 = m_scene.createArrayResource(EDataType::ByteBlob, 2, data2); + ramses::Byte data2[2] = {}; + data2[0] = 5; + auto a2 = m_scene.createArrayResource(2, data2); - const ramses_internal::ResourceContentHash hash2 = a2->impl.getLowlevelResourceHash(); + const ramses_internal::ResourceContentHash hash2 = a2->m_impl.getLowlevelResourceHash(); EXPECT_TRUE(hash != hash2); } TEST_F(AResourceTestClient, createEmptyByteBlobArrayFails) { - auto a = m_scene.createArrayResource(EDataType::ByteBlob, 0, nullptr); + auto a = m_scene.createArrayResource(0, nullptr); EXPECT_TRUE(nullptr == a); } @@ -1138,7 +1138,7 @@ namespace ramses ASSERT_TRUE(effect1 != nullptr); auto effect2 = m_scene.createEffect(effectDesc, ramses::ResourceCacheFlag_DoNotCache, "effect2"); ASSERT_TRUE(effect2 != nullptr); - EXPECT_EQ(effect1->impl.getLowlevelResourceHash(), effect2->impl.getLowlevelResourceHash()); + EXPECT_EQ(effect1->m_impl.getLowlevelResourceHash(), effect2->m_impl.getLowlevelResourceHash()); m_scene.destroy(*effect1); m_scene.destroy(*effect2); } @@ -1147,7 +1147,7 @@ namespace ramses class ResourceTest : public LocalTestClientWithScene, public testing::Test { public: - ResourceType& createResource(const char* name) + ResourceType& createResource(std::string_view name) { return (this->template createObject(name)); } @@ -1165,17 +1165,17 @@ namespace ramses TYPED_TEST(ResourceTest, sameResourcesWithDifferentNamesShareSameHash) { - const ramses_internal::String name_A("name_A"); - const ramses_internal::String name_B("name_B"); - Resource* resource_A = static_cast(&this->createResource(name_A.c_str())); - Resource* resource_B = static_cast(&this->createResource(name_B.c_str())); + const std::string name_A("name_A"); + const std::string name_B("name_B"); + Resource* resource_A = static_cast(&this->createResource(name_A)); + Resource* resource_B = static_cast(&this->createResource(name_B)); - RamsesObject* foundObject_A = this->getScene().findObjectByName(name_A.c_str()); - RamsesObject* foundObject_B = this->getScene().findObjectByName(name_B.c_str()); + RamsesObject* foundObject_A = this->getScene().findObjectByName(name_A); + RamsesObject* foundObject_B = this->getScene().findObjectByName(name_B); ASSERT_TRUE(resource_A); ASSERT_TRUE(resource_B); - EXPECT_EQ(resource_A->impl.getLowlevelResourceHash(), resource_B->impl.getLowlevelResourceHash()); + EXPECT_EQ(resource_A->m_impl.getLowlevelResourceHash(), resource_B->m_impl.getLowlevelResourceHash()); ASSERT_TRUE(foundObject_A); ASSERT_TRUE(foundObject_B); @@ -1186,22 +1186,22 @@ namespace ramses TYPED_TEST(ResourceTest, statisticCounterIsUpdated) { - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesCreated.getCounterValue()); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesNumber.getCounterValue()); + EXPECT_EQ(0u, this->getFramework().m_impl.getStatisticCollection().statResourcesCreated.getCounterValue()); + EXPECT_EQ(0u, this->getFramework().m_impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); + EXPECT_EQ(0u, this->getFramework().m_impl.getStatisticCollection().statResourcesNumber.getCounterValue()); RamsesObject& obj = this->createResource("resource"); Resource* res = RamsesUtils::TryConvert(obj); - EXPECT_EQ(1u, this->getFramework().impl.getStatisticCollection().statResourcesCreated.getCounterValue()); + EXPECT_EQ(1u, this->getFramework().m_impl.getStatisticCollection().statResourcesCreated.getCounterValue()); - this->getFramework().impl.getStatisticCollection().nextTimeInterval(); //statResourcesNumber is updated by nextTimeInterval() - EXPECT_EQ(1u, this->getFramework().impl.getStatisticCollection().statResourcesNumber.getCounterValue()); + this->getFramework().m_impl.getStatisticCollection().nextTimeInterval(); //statResourcesNumber is updated by nextTimeInterval() + EXPECT_EQ(1u, this->getFramework().m_impl.getStatisticCollection().statResourcesNumber.getCounterValue()); this->getScene().destroy(*res); - EXPECT_EQ(1u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); + EXPECT_EQ(1u, this->getFramework().m_impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - this->getFramework().impl.getStatisticCollection().nextTimeInterval(); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesNumber.getCounterValue()); + this->getFramework().m_impl.getStatisticCollection().nextTimeInterval(); + EXPECT_EQ(0u, this->getFramework().m_impl.getStatisticCollection().statResourcesNumber.getCounterValue()); } TYPED_TEST(ResourceTest, canCreateResourceSetNameAndAfterwardsDestroyResourceCleanly) @@ -1209,7 +1209,7 @@ namespace ramses TypeParam& obj = this->createResource("resource"); obj.setName("otherName"); EXPECT_EQ(StatusOK, this->m_scene.destroy(obj)); - SceneObjectIterator iterator(this->m_scene, ERamsesObjectType_Resource); + SceneObjectIterator iterator(this->m_scene, ERamsesObjectType::Resource); EXPECT_EQ(iterator.getNext(), nullptr); EXPECT_EQ(StatusOK, this->m_scene.saveToFile("someFileName.ramses", false)); } @@ -1244,66 +1244,4 @@ namespace ramses EXPECT_EQ(StatusOK, this->m_scene.destroy(*obj2)); EXPECT_EQ(this->m_scene.getResource(id), nullptr); } - - TEST_F(AResourceTestClient, statisticCounterIsUpdatedWhenAddingToPoolViaFileAndCreatingForScene) - { - const float data[2 * 2] = { 1, 2, 3, 4 }; - auto res = m_scene.createArrayResource(EDataType::Vector2F, 2, data); - auto hash = res->getResourceId(); - - EXPECT_TRUE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(m_scene, "testfile.ramres", false)); - - m_scene.destroy(*res); - EXPECT_EQ(1u, this->getFramework().impl.getStatisticCollection().statResourcesCreated.getCounterValue()); - EXPECT_EQ(1u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - - this->getFramework().impl.getStatisticCollection().nextTimeInterval(); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesNumber.getCounterValue()); - - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesCreated.getCounterValue()); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(client).addResourceDataFile("testfile.ramres")); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesCreated.getCounterValue()); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - - Scene& scene(*client.createScene(sceneId_t{ 0xf00 })); - auto res2 = RamsesHMIUtils::GetResourceDataPoolForClient(client).createResourceForScene(scene, hash); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesCreated.getCounterValue()); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - { - auto managedRes = client.impl.getClientApplication().loadResource(res2->impl.getLowlevelResourceHash()); - EXPECT_EQ(1u, this->getFramework().impl.getStatisticCollection().statResourcesCreated.getCounterValue()); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - - scene.destroy(*res2); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - } - EXPECT_EQ(1u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - - this->getFramework().impl.getStatisticCollection().nextTimeInterval(); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesNumber.getCounterValue()); - } - - TEST_F(AResourceTestClient, statisticCounterIsUpdatedWhenAddingToPoolViaDataAndCreatingForScene) - { - const float data[2 * 2] = { 1, 2, 3, 4 }; - auto hash = RamsesHMIUtils::GetResourceDataPoolForClient(client).addArrayResourceData(EDataType::Vector2F, 2, data); - EXPECT_NE(hash, resourceId_t::Invalid()); - EXPECT_EQ(1u, this->getFramework().impl.getStatisticCollection().statResourcesCreated.getCounterValue()); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - - this->getFramework().impl.getStatisticCollection().nextTimeInterval(); - auto res = RamsesHMIUtils::GetResourceDataPoolForClient(client).createResourceForScene(this->getScene(), hash); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesCreated.getCounterValue()); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - - this->getScene().destroy(*res); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(client).removeResourceData(hash)); - EXPECT_EQ(1u, this->getFramework().impl.getStatisticCollection().statResourcesDestroyed.getCounterValue()); - - this->getFramework().impl.getStatisticCollection().nextTimeInterval(); - EXPECT_EQ(0u, this->getFramework().impl.getStatisticCollection().statResourcesNumber.getCounterValue()); - } } diff --git a/client/ramses-client/test/SceneDistributionTest.cpp b/client/test/SceneDistributionTest.cpp similarity index 86% rename from client/ramses-client/test/SceneDistributionTest.cpp rename to client/test/SceneDistributionTest.cpp index 411f05a90..15c35fac0 100644 --- a/client/ramses-client/test/SceneDistributionTest.cpp +++ b/client/test/SceneDistributionTest.cpp @@ -8,7 +8,6 @@ #include "gtest/gtest.h" #include "ClientTestUtils.h" -#include "ramses-client-api/SplineLinearVector3f.h" #include "ramses-client-api/Node.h" #include "SceneAPI/IScene.h" #include "Scene/SceneActionCollection.h" @@ -17,7 +16,7 @@ namespace ramses { using namespace testing; - class ADistributedScene : public LocalTestClientWithSceneAndAnimationSystem, public ::testing::Test + class ADistributedScene : public LocalTestClientWithScene, public ::testing::Test { public: ADistributedScene() @@ -25,13 +24,13 @@ namespace ramses framework.connect(); } - ~ADistributedScene() + ~ADistributedScene() override { } void publishScene() { - const ramses_internal::IScene& iscene = m_scene.impl.getIScene(); + const ramses_internal::IScene& iscene = m_scene.m_impl.getIScene(); ramses_internal::SceneInfo info(iscene.getSceneId(), iscene.getName()); EXPECT_CALL(sceneActionsCollector, handleNewSceneAvailable(info, _)); EXPECT_CALL(sceneActionsCollector, handleInitializeScene(info, _)); @@ -40,7 +39,7 @@ namespace ramses void unpublishScene() { - EXPECT_CALL(sceneActionsCollector, handleSceneBecameUnavailable(ramses_internal::SceneId(m_scene.impl.getSceneId().getValue()), _)); + EXPECT_CALL(sceneActionsCollector, handleSceneBecameUnavailable(ramses_internal::SceneId(m_scene.m_impl.getSceneId().getValue()), _)); EXPECT_EQ(StatusOK, m_scene.unpublish()); } @@ -50,11 +49,6 @@ namespace ramses Node* node = m_scene.createNode("node"); Node* nodeTrans = m_scene.createNode("nodetrans"); nodeTrans->setParent(*node); - - // do random animation stuff - SplineLinearVector3f* spline = animationSystem.createSplineLinearVector3f("spline"); - AnimatedProperty* prop = animationSystem.createAnimatedProperty(*nodeTrans, EAnimatedProperty_Translation); - animationSystem.createAnimation(*prop, *spline, "anim"); } void expectActionListsEqual(const ramses_internal::SceneActionCollection& list1, const ramses_internal::SceneActionCollection& list2) const @@ -68,12 +62,12 @@ namespace ramses void expectSceneOperationsSent() { - EXPECT_CALL(sceneActionsCollector, handleSceneUpdate_rvr(ramses_internal::SceneId(m_scene.impl.getSceneId().getValue()), _, _)); + EXPECT_CALL(sceneActionsCollector, handleSceneUpdate_rvr(ramses_internal::SceneId(m_scene.m_impl.getSceneId().getValue()), _, _)); } void expectSceneUnpublication() { - EXPECT_CALL(sceneActionsCollector, handleSceneBecameUnavailable(ramses_internal::SceneId(m_scene.impl.getSceneId().getValue()), _)); + EXPECT_CALL(sceneActionsCollector, handleSceneBecameUnavailable(ramses_internal::SceneId(m_scene.m_impl.getSceneId().getValue()), _)); } protected: @@ -139,7 +133,7 @@ namespace ramses const ramses_internal::SceneId sceneId(33u); Scene* otherScene = client.createScene(ramses::sceneId_t(sceneId.getValue())); ASSERT_TRUE(otherScene != nullptr); - const ramses_internal::IScene& otherIScene = otherScene->impl.getIScene(); + const ramses_internal::IScene& otherIScene = otherScene->m_impl.getIScene(); ramses_internal::SceneInfo sceneInfo(sceneId, otherIScene.getName()); EXPECT_CALL(sceneActionsCollector, handleNewSceneAvailable(sceneInfo, _)); diff --git a/client/ramses-client/test/SceneFactoryTest.cpp b/client/test/SceneFactoryTest.cpp similarity index 87% rename from client/ramses-client/test/SceneFactoryTest.cpp rename to client/test/SceneFactoryTest.cpp index 40fce7014..1b2e3f4e9 100644 --- a/client/ramses-client/test/SceneFactoryTest.cpp +++ b/client/test/SceneFactoryTest.cpp @@ -25,8 +25,8 @@ TEST_F(ASceneFactory, createsAndDeletesScene) { IScene* scene = factory.createScene(SceneInfo()); ASSERT_TRUE(nullptr != scene); - EXPECT_EQ(scene, factory.releaseScene(scene->getSceneId())); - delete scene; + auto sceneOwnPtr = factory.releaseScene(scene->getSceneId()); + EXPECT_EQ(scene, sceneOwnPtr.get()); } TEST_F(ASceneFactory, cannotCreateTwoScenesWithTheSameId) @@ -38,13 +38,13 @@ TEST_F(ASceneFactory, cannotCreateTwoScenesWithTheSameId) TEST_F(ASceneFactory, createsSceneWithProvidedOptions) { - const SceneSizeInformation sizeInfo(1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u, 11u, 12u, 13u, 14u, 15u, 16u, 17u, 18u, 19u, 20u); + const SceneSizeInformation sizeInfo(1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u, 11u, 12u, 13u, 14u, 15u, 16u, 17u, 18u); const SceneId sceneId(456u); const SceneInfo sceneInfo(sceneId, "sceneName"); Scene* scene = static_cast(factory.createScene(sceneInfo)); scene->preallocateSceneSize(sizeInfo); ASSERT_TRUE(scene != nullptr); - EXPECT_EQ(String("sceneName"), scene->getName()); + EXPECT_EQ(std::string("sceneName"), scene->getName()); EXPECT_EQ(sizeInfo, scene->getSceneSizeInformation()); EXPECT_EQ(sceneId, scene->getSceneId()); } diff --git a/client/ramses-client/test/SceneGraphIteratorTest.cpp b/client/test/SceneGraphIteratorTest.cpp similarity index 89% rename from client/ramses-client/test/SceneGraphIteratorTest.cpp rename to client/test/SceneGraphIteratorTest.cpp index 316e81e1a..af9ab555a 100644 --- a/client/ramses-client/test/SceneGraphIteratorTest.cpp +++ b/client/test/SceneGraphIteratorTest.cpp @@ -43,7 +43,7 @@ namespace ramses TEST_F(SceneGraphIteratorTest, traversesSceneGraph_DepthFirst) { - SceneGraphIterator iterator(m_root, ETreeTraversalStyle_DepthFirst); + SceneGraphIterator iterator(m_root, ETreeTraversalStyle::DepthFirst); std::vector expectedOrder; expectedOrder.push_back(&m_root); @@ -59,7 +59,7 @@ namespace ramses TEST_F(SceneGraphIteratorTest, traversesSceneGraph_BreadthFirst) { - SceneGraphIterator iterator(m_root, ETreeTraversalStyle_BreadthFirst); + SceneGraphIterator iterator(m_root, ETreeTraversalStyle::BreadthFirst); std::vector expectedOrder; expectedOrder.push_back(&m_root); @@ -75,14 +75,14 @@ namespace ramses TEST_F(SceneGraphIteratorTest, traverseOnlyOneLeaf_DepthFirst) { - SceneGraphIterator iter(m_mesh1b, ETreeTraversalStyle_DepthFirst); + SceneGraphIterator iter(m_mesh1b, ETreeTraversalStyle::DepthFirst); EXPECT_EQ(&m_mesh1b, iter.getNext()); EXPECT_EQ(nullptr, iter.getNext()); } TEST_F(SceneGraphIteratorTest, traverseOnlyOneLeaf_BreathFirst) { - SceneGraphIterator iter(m_mesh1b, ETreeTraversalStyle_BreadthFirst); + SceneGraphIterator iter(m_mesh1b, ETreeTraversalStyle::BreadthFirst); EXPECT_EQ(&m_mesh1b, iter.getNext()); EXPECT_EQ(nullptr, iter.getNext()); } diff --git a/client/ramses-client/test/SceneIteratorTest.cpp b/client/test/SceneIteratorTest.cpp similarity index 100% rename from client/ramses-client/test/SceneIteratorTest.cpp rename to client/test/SceneIteratorTest.cpp diff --git a/client/ramses-client/test/SceneObjectIteratorTest.cpp b/client/test/SceneObjectIteratorTest.cpp similarity index 84% rename from client/ramses-client/test/SceneObjectIteratorTest.cpp rename to client/test/SceneObjectIteratorTest.cpp index 6efbf9412..a351c0fad 100644 --- a/client/ramses-client/test/SceneObjectIteratorTest.cpp +++ b/client/test/SceneObjectIteratorTest.cpp @@ -11,7 +11,6 @@ #include "ramses-client-api/RenderBuffer.h" #include "ramses-client-api/RenderTarget.h" #include "ramses-client-api/SceneObjectIterator.h" -#include "ramses-client-api/AnimationSystemRealTime.h" #include "ramses-client-api/MeshNode.h" #include "ramses-client-api/PerspectiveCamera.h" #include "ramses-client-api/OrthographicCamera.h" @@ -20,18 +19,7 @@ #include "ramses-client-api/RenderPass.h" #include "ramses-client-api/BlitPass.h" #include "ramses-client-api/GeometryBinding.h" -#include "ramses-client-api/DataFloat.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-client-api/DataVector4f.h" -#include "ramses-client-api/DataMatrix22f.h" -#include "ramses-client-api/DataMatrix33f.h" -#include "ramses-client-api/DataMatrix44f.h" -#include "ramses-client-api/DataInt32.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector3i.h" -#include "ramses-client-api/DataVector4i.h" -#include "ramses-client-api/StreamTexture.h" +#include "ramses-client-api/DataObject.h" #include "ramses-client-api/ArrayBuffer.h" #include "ramses-client-api/Texture2DBuffer.h" #include "ramses-client-api/PickableObject.h" diff --git a/client/ramses-client/test/ScenePersistationTest.cpp b/client/test/ScenePersistationTest.cpp similarity index 57% rename from client/ramses-client/test/ScenePersistationTest.cpp rename to client/test/ScenePersistationTest.cpp index 385dd115d..528d82aab 100644 --- a/client/ramses-client/test/ScenePersistationTest.cpp +++ b/client/test/ScenePersistationTest.cpp @@ -10,17 +10,11 @@ #include "PlatformAbstraction/PlatformError.h" #include "ramses-client-api/MeshNode.h" -#include "ramses-client-api/AnimationSystemRealTime.h" #include "ramses-client-api/TextureSampler.h" #include "ramses-client-api/TextureSamplerMS.h" #include "ramses-client-api/RenderBuffer.h" #include "ramses-client-api/RenderTargetDescription.h" #include "ramses-client-api/RenderTarget.h" -#include "ramses-client-api/SplineStepBool.h" -#include "ramses-client-api/SplineStepFloat.h" -#include "ramses-client-api/AnimatedProperty.h" -#include "ramses-client-api/Animation.h" -#include "ramses-client-api/AnimationSequence.h" #include "ramses-client-api/PerspectiveCamera.h" #include "ramses-client-api/OrthographicCamera.h" #include "ramses-client-api/GeometryBinding.h" @@ -29,24 +23,15 @@ #include "ramses-client-api/BlitPass.h" #include "ramses-client-api/EffectDescription.h" #include "ramses-client-api/AttributeInput.h" -#include "ramses-client-api/DataFloat.h" -#include "ramses-client-api/DataVector2f.h" -#include "ramses-client-api/DataVector3f.h" -#include "ramses-client-api/DataVector4f.h" -#include "ramses-client-api/DataMatrix22f.h" -#include "ramses-client-api/DataMatrix33f.h" -#include "ramses-client-api/DataMatrix44f.h" -#include "ramses-client-api/DataInt32.h" -#include "ramses-client-api/DataVector2i.h" -#include "ramses-client-api/DataVector3i.h" -#include "ramses-client-api/DataVector4i.h" -#include "ramses-client-api/StreamTexture.h" +#include "ramses-client-api/DataObject.h" #include "ramses-client-api/Texture2D.h" #include "ramses-client-api/TextureSamplerExternal.h" #include "ramses-client-api/ArrayBuffer.h" #include "ramses-client-api/Texture2DBuffer.h" #include "ramses-client-api/PickableObject.h" #include "ramses-client-api/ArrayResource.h" +#include "ramses-client-api/UniformInput.h" +#include "ramses-client-api/Effect.h" #include "ramses-utils.h" #include "ScenePersistationTest.h" @@ -54,14 +39,7 @@ #include "AppearanceImpl.h" #include "DataObjectImpl.h" #include "GeometryBindingImpl.h" -#include "ramses-client-api/Effect.h" #include "EffectImpl.h" -#include "TestEffects.h" -#include "AnimationSystemImpl.h" -#include "SceneAPI/IScene.h" -#include "SceneAPI/BlitPass.h" -#include "SceneAPI/RenderGroupUtils.h" -#include "SceneAPI/RenderPass.h" #include "SceneImpl.h" #include "Texture2DImpl.h" #include "TextureSamplerImpl.h" @@ -71,25 +49,23 @@ #include "PickableObjectImpl.h" #include "RenderBufferImpl.h" #include "RenderTargetImpl.h" -#include "StreamTextureImpl.h" #include "MeshNodeImpl.h" -#include "SplineImpl.h" -#include "AnimatedPropertyImpl.h" -#include "AnimationImpl.h" -#include "AnimationSequenceImpl.h" -#include "Utils/File.h" -#include "ramses-utils.h" -#include "FileDescriptorHelper.h" -#include "UnsafeTestMemoryHelpers.h" - #include "ArrayBufferImpl.h" #include "Texture2DBufferImpl.h" -#include "RamsesObjectTestTypes.h" -#include "ramses-client-api/UniformInput.h" + +#include "Utils/File.h" +#include "SceneAPI/IScene.h" +#include "SceneAPI/BlitPass.h" +#include "SceneAPI/RenderGroupUtils.h" +#include "SceneAPI/RenderPass.h" #include "Scene/ESceneActionId.h" #include "Scene/ResourceChanges.h" #include "Scene/SceneActionApplier.h" -#include "ramses-hmi-utils.h" + +#include "TestEffects.h" +#include "FileDescriptorHelper.h" +#include "UnsafeTestMemoryHelpers.h" +#include "RamsesObjectTestTypes.h" #include #include @@ -99,13 +75,13 @@ namespace ramses { using namespace testing; - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canWriteAndReadSceneFromFile) + TEST_F(ASceneLoadedFromFile, canWriteAndReadSceneFromFile) { EXPECT_EQ(StatusOK, m_scene.saveToFile("someTemporaryFile.ram", false)); EXPECT_NE(nullptr, m_clientForLoading.loadSceneFromFile("someTemporaryFile.ram", false)); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canWriteAndReadSceneFromMemoryWithExplicitDeleter) + TEST_F(ASceneLoadedFromFile, canWriteAndReadSceneFromMemoryWithExplicitDeleter) { EXPECT_EQ(StatusOK, m_scene.saveToFile("someTemporaryFile.ram", false)); @@ -120,7 +96,7 @@ namespace ramses EXPECT_NE(nullptr, m_clientForLoading.loadSceneFromMemory(std::move(data), fileSize, false)); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canWriteAndReadSceneFromMemoryWithImplicitDeleter) + TEST_F(ASceneLoadedFromFile, canWriteAndReadSceneFromMemoryWithImplicitDeleter) { EXPECT_EQ(StatusOK, m_scene.saveToFile("someTemporaryFile.ram", false)); @@ -135,7 +111,7 @@ namespace ramses EXPECT_NE(nullptr, RamsesUtils::LoadSceneFromMemory(m_clientForLoading, std::move(data), fileSize, false)); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canWriteAndReadSceneFromFileDescriptor) + TEST_F(ASceneLoadedFromFile, canWriteAndReadSceneFromFileDescriptor) { EXPECT_EQ(StatusOK, m_scene.saveToFile("someTemporaryFile.ram", false)); @@ -164,7 +140,7 @@ namespace ramses EXPECT_EQ(123u, scene->getSceneId().getValue()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadSceneFromFileDescriptorCustomSceneId) + TEST_F(ASceneLoadedFromFile, canReadSceneFromFileDescriptorCustomSceneId) { const char* filename = "someTemporaryFile.ram"; EXPECT_EQ(StatusOK, m_scene.saveToFile(filename, false)); @@ -187,7 +163,7 @@ namespace ramses } } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, errorsReadingSceneFromFileDescriptorCustomSceneId) + TEST_F(ASceneLoadedFromFile, errorsReadingSceneFromFileDescriptorCustomSceneId) { const char* filename = "someTemporaryFile.ram"; EXPECT_EQ(StatusOK, m_scene.saveToFile(filename, false)); @@ -211,7 +187,7 @@ namespace ramses EXPECT_TRUE(invalidFileSize == nullptr); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAScene) + TEST_F(ASceneLoadedFromFile, canReadWriteAScene) { const status_t status = m_scene.saveToFile("someTemporaryFile.ram", false); EXPECT_EQ(StatusOK, status); @@ -225,15 +201,12 @@ namespace ramses fillObjectTypeHistogramFromScene(loadedSceneNumbers, *m_sceneLoaded); EXPECT_PRED_FORMAT2(AssertHistogramEqual, origSceneNumbers, loadedSceneNumbers); - ramses_internal::SceneSizeInformation origSceneSizeInfo = m_scene.impl.getIScene().getSceneSizeInformation(); - ramses_internal::SceneSizeInformation loadedSceneSizeInfo = m_sceneLoaded->impl.getIScene().getSceneSizeInformation(); + ramses_internal::SceneSizeInformation origSceneSizeInfo = m_scene.m_impl.getIScene().getSceneSizeInformation(); + ramses_internal::SceneSizeInformation loadedSceneSizeInfo = m_sceneLoaded->m_impl.getIScene().getSceneSizeInformation(); EXPECT_EQ(origSceneSizeInfo, loadedSceneSizeInfo); - - const auto animationSystemLoaded = RamsesUtils::TryConvert(*m_sceneLoaded->findObjectByName("animation system")); - ASSERT_TRUE(nullptr != animationSystemLoaded); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAPerspectiveCamera) + TEST_F(ASceneLoadedFromFile, canReadWriteAPerspectiveCamera) { PerspectiveCamera* camera = this->m_scene.createPerspectiveCamera("my cam"); camera->setFrustum(0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f); @@ -242,7 +215,7 @@ namespace ramses doWriteReadCycle(); PerspectiveCamera* loadedCamera = this->getObjectForTesting("my cam"); - EXPECT_EQ(camera->impl.getCameraHandle(), loadedCamera->impl.getCameraHandle()); + EXPECT_EQ(camera->m_impl.getCameraHandle(), loadedCamera->m_impl.getCameraHandle()); EXPECT_EQ(0.1f, loadedCamera->getLeftPlane()); EXPECT_EQ(0.2f, loadedCamera->getRightPlane()); EXPECT_EQ(0.3f, loadedCamera->getBottomPlane()); @@ -260,29 +233,7 @@ namespace ramses m_sceneLoaded->destroy(*loadedCamera); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteStreamTexture) - { - uint8_t data[4] = { 0u }; - MipLevelData mipLevelData(sizeof(data), data); - Texture2D* fallbackTexture = this->m_scene.createTexture2D(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, "fallbackTexture"); - - StreamTexture* streamTexture = this->m_scene.createStreamTexture(*fallbackTexture, waylandIviSurfaceId_t(3), "resourceName"); - StreamTexture* streamTextureWithForcedFallback = this->m_scene.createStreamTexture(*fallbackTexture, waylandIviSurfaceId_t(4), "resourceName2"); - streamTextureWithForcedFallback->forceFallbackImage(true); - - this->doWriteReadCycle(); - - const StreamTexture* loadedStreamTexture = this->getObjectForTesting("resourceName"); - const StreamTexture* loadedStreamTextureWithForcedFallback = this->getObjectForTesting("resourceName2"); - - EXPECT_EQ(streamTexture->impl.getStreamSource(), loadedStreamTexture->impl.getStreamSource()); - EXPECT_EQ(streamTexture->impl.getFallbackTextureHash(), loadedStreamTexture->impl.getFallbackTextureHash()); - EXPECT_EQ(fallbackTexture->impl.getLowlevelResourceHash(), loadedStreamTexture->impl.getFallbackTextureHash()); - EXPECT_FALSE(loadedStreamTexture->impl.getForceFallbackImage()); - EXPECT_TRUE(loadedStreamTextureWithForcedFallback->impl.getForceFallbackImage()); - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAnOrthographicCamera) + TEST_F(ASceneLoadedFromFile, canReadWriteAnOrthographicCamera) { OrthographicCamera* camera = this->m_scene.createOrthographicCamera("my cam"); camera->setFrustum(0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f); @@ -292,7 +243,7 @@ namespace ramses OrthographicCamera* loadedCamera = this->getObjectForTesting("my cam"); - EXPECT_EQ(camera->impl.getCameraHandle(), loadedCamera->impl.getCameraHandle()); + EXPECT_EQ(camera->m_impl.getCameraHandle(), loadedCamera->m_impl.getCameraHandle()); EXPECT_FLOAT_EQ(0.1f, loadedCamera->getLeftPlane()); EXPECT_FLOAT_EQ(0.2f, loadedCamera->getRightPlane()); @@ -311,7 +262,61 @@ namespace ramses m_sceneLoaded->destroy(*loadedCamera); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAnAppearance) + TEST_F(ASceneLoadedFromFile, canReadWriteAnEffect) + { + TestEffects::CreateTestEffectWithAllStages(this->m_scene, "eff"); + + doWriteReadCycle(); + + auto loadedEffect = this->getObjectForTesting("eff"); + ASSERT_TRUE(loadedEffect); + + // check uniforms + EXPECT_EQ(4u, loadedEffect->getUniformInputCount()); + UniformInput uniform; + EXPECT_EQ(StatusOK, loadedEffect->getUniformInput(0u, uniform)); + EXPECT_STREQ("vs_uniform", uniform.getName()); + EXPECT_EQ(EDataType::Float, *uniform.getDataType()); + EXPECT_EQ(1u, uniform.getElementCount()); + + EXPECT_EQ(StatusOK, loadedEffect->getUniformInput(1u, uniform)); + EXPECT_STREQ("colorRG", uniform.getName()); + EXPECT_EQ(EDataType::Vector2F, *uniform.getDataType()); + EXPECT_EQ(1u, uniform.getElementCount()); + + EXPECT_EQ(StatusOK, loadedEffect->getUniformInput(2u, uniform)); + EXPECT_STREQ("colorBA", uniform.getName()); + EXPECT_EQ(EDataType::Float, *uniform.getDataType()); + EXPECT_EQ(2u, uniform.getElementCount()); + + EXPECT_EQ(StatusOK, loadedEffect->getUniformInput(3u, uniform)); + EXPECT_STREQ("gs_uniform", uniform.getName()); + EXPECT_EQ(EDataType::Float, *uniform.getDataType()); + EXPECT_EQ(1u, uniform.getElementCount()); + + // check attributes + EXPECT_EQ(3u, loadedEffect->getAttributeInputCount()); + AttributeInput attrib; + EXPECT_EQ(StatusOK, loadedEffect->getAttributeInput(0u, attrib)); + EXPECT_STREQ("a_position1", attrib.getName()); + EXPECT_EQ(EDataType::Vector3F, *attrib.getDataType()); + + EXPECT_EQ(StatusOK, loadedEffect->getAttributeInput(1u, attrib)); + EXPECT_STREQ("a_position2", attrib.getName()); + EXPECT_EQ(EDataType::Float, *attrib.getDataType()); + + EXPECT_EQ(StatusOK, loadedEffect->getAttributeInput(2u, attrib)); + EXPECT_STREQ("a_position3", attrib.getName()); + EXPECT_EQ(EDataType::Float, *attrib.getDataType()); + + // GS + EXPECT_TRUE(loadedEffect->hasGeometryShader()); + EDrawMode gsInputType = EDrawMode::Points; + EXPECT_EQ(StatusOK, loadedEffect->getGeometryShaderInputType(gsInputType)); + EXPECT_EQ(EDrawMode::Lines, gsInputType); + } + + TEST_F(ASceneLoadedFromFile, canReadWriteAnAppearance) { Effect* effect = TestEffects::CreateTestEffect(this->m_scene); Appearance* appearance = this->m_scene.createAppearance(*effect, "appearance"); @@ -320,13 +325,75 @@ namespace ramses Appearance* loadedAppearance = this->getObjectForTesting("appearance"); - EXPECT_EQ(appearance->getEffect().impl.getLowlevelResourceHash(), loadedAppearance->getEffect().impl.getLowlevelResourceHash()); - EXPECT_EQ(appearance->impl.getRenderStateHandle(), loadedAppearance->impl.getRenderStateHandle()); - EXPECT_EQ(appearance->impl.getUniformDataInstance(), loadedAppearance->impl.getUniformDataInstance()); - EXPECT_EQ(appearance->impl.getIScene().getSceneId(), loadedAppearance->impl.getIScene().getSceneId()); + EXPECT_EQ(appearance->getEffect().m_impl.getLowlevelResourceHash(), loadedAppearance->getEffect().m_impl.getLowlevelResourceHash()); + EXPECT_EQ(appearance->m_impl.getRenderStateHandle(), loadedAppearance->m_impl.getRenderStateHandle()); + EXPECT_EQ(appearance->m_impl.getUniformDataInstance(), loadedAppearance->m_impl.getUniformDataInstance()); + EXPECT_EQ(appearance->m_impl.getIScene().getSceneId(), loadedAppearance->m_impl.getIScene().getSceneId()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, keepingTrackOfsceneObjectIdsAndFindObjectByIdWork) + TEST_F(ASceneLoadedFromFile, canReadWriteAnAppearanceWithGeometryShaderAndRestrictDrawMode) + { + const Effect* effect = TestEffects::CreateTestEffectWithAllStages(this->m_scene); + this->m_scene.createAppearance(*effect, "appearance"); + + doWriteReadCycle(); + + Appearance* loadedAppearance = this->getObjectForTesting("appearance"); + + EDrawMode drawMode = EDrawMode::Points; + EXPECT_EQ(StatusOK, loadedAppearance->getDrawMode(drawMode)); + EXPECT_EQ(EDrawMode::Lines, drawMode); + + EDrawMode gsInputType = EDrawMode::Points; + ASSERT_TRUE(loadedAppearance->getEffect().hasGeometryShader()); + EXPECT_EQ(StatusOK, loadedAppearance->getEffect().getGeometryShaderInputType(gsInputType)); + EXPECT_EQ(EDrawMode::Lines, gsInputType); + + // valid draw mode change + EXPECT_EQ(StatusOK, loadedAppearance->setDrawMode(EDrawMode::LineStrip)); + + // invalid draw mode change + EXPECT_NE(StatusOK, loadedAppearance->setDrawMode(EDrawMode::Points)); + } + + TEST_F(ASceneLoadedFromFile, canReadWriteAnAppearanceWithGeometryShaderAndRestrictDrawMode_usingSameEffect) + { + const Effect* effect1 = TestEffects::CreateTestEffectWithAllStages(this->m_scene); + const Effect* effect2 = TestEffects::CreateTestEffectWithAllStages(this->m_scene); + this->m_scene.createAppearance(*effect1, "appearance1"); + this->m_scene.createAppearance(*effect2, "appearance2"); + + doWriteReadCycle(); + + Appearance* loadedAppearance1 = this->getObjectForTesting("appearance1"); + Appearance* loadedAppearance2 = this->getObjectForTesting("appearance2"); + + // appearance 1 + EDrawMode drawMode = EDrawMode::Points; + EDrawMode gsInputType = EDrawMode::Points; + EXPECT_EQ(StatusOK, loadedAppearance1->getDrawMode(drawMode)); + EXPECT_EQ(EDrawMode::Lines, drawMode); + ASSERT_TRUE(loadedAppearance1->getEffect().hasGeometryShader()); + EXPECT_EQ(StatusOK, loadedAppearance1->getEffect().getGeometryShaderInputType(gsInputType)); + EXPECT_EQ(EDrawMode::Lines, gsInputType); + + // appearance 2 + EXPECT_EQ(StatusOK, loadedAppearance1->getDrawMode(drawMode)); + EXPECT_EQ(EDrawMode::Lines, drawMode); + ASSERT_TRUE(loadedAppearance1->getEffect().hasGeometryShader()); + EXPECT_EQ(StatusOK, loadedAppearance1->getEffect().getGeometryShaderInputType(gsInputType)); + EXPECT_EQ(EDrawMode::Lines, gsInputType); + + // valid draw mode change + EXPECT_EQ(StatusOK, loadedAppearance1->setDrawMode(EDrawMode::LineStrip)); + EXPECT_EQ(StatusOK, loadedAppearance2->setDrawMode(EDrawMode::LineStrip)); + + // invalid draw mode change + EXPECT_NE(StatusOK, loadedAppearance1->setDrawMode(EDrawMode::Points)); + EXPECT_NE(StatusOK, loadedAppearance2->setDrawMode(EDrawMode::Points)); + } + + TEST_F(ASceneLoadedFromFile, keepingTrackOfsceneObjectIdsAndFindObjectByIdWork) { Effect* effect = TestEffects::CreateTestEffect(this->m_scene); @@ -350,7 +417,7 @@ namespace ramses EXPECT_NE(geometryIdBeforeSaveAndLoad, camera->getSceneObjectId()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAnAppearanceWithUniformValuesSetOrBound) + TEST_F(ASceneLoadedFromFile, canReadWriteAnAppearanceWithUniformValuesSetOrBound) { Effect* effect = TestEffects::CreateTestEffect(this->m_scene); Appearance* appearance = this->m_scene.createAppearance(*effect, "appearance"); @@ -360,10 +427,10 @@ namespace ramses effect->findUniformInput("u_FragColorR", fragColorR); effect->findUniformInput("u_FragColorG", fragColorG); - DataFloat* fragColorDataObject = this->m_scene.createDataFloat(); + auto fragColorDataObject = this->m_scene.createDataObject(EDataType::Float); fragColorDataObject->setValue(123.f); - appearance->setInputValueFloat(fragColorR, 567.f); + appearance->setInputValue(fragColorR, 567.f); appearance->bindInput(fragColorG, *fragColorDataObject); doWriteReadCycle(); @@ -377,25 +444,25 @@ namespace ramses loadedEffect.findUniformInput("u_FragColorG", fragColorGOut); float resultR = 0.f; - loadedAppearance->getInputValueFloat(fragColorROut, resultR); + loadedAppearance->getInputValue(fragColorROut, resultR); EXPECT_FLOAT_EQ(567.f, resultR); ASSERT_TRUE(loadedAppearance->isInputBound(fragColorGOut)); float resultG = 0.f; const DataObject& fragColorDataObjectOut = *loadedAppearance->getDataObjectBoundToInput(fragColorGOut); - RamsesObjectTypeUtils::ConvertTo(fragColorDataObjectOut).getValue(resultG); + fragColorDataObjectOut.getValue(resultG); EXPECT_EQ(123.f, resultG); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, multipleAppearancesSharingSameEffectAreCorrectlyWrittenAndLoaded) + TEST_F(ASceneLoadedFromFile, multipleAppearancesSharingSameEffectAreCorrectlyWrittenAndLoaded) { Effect* effect = TestEffects::CreateTestEffect(this->m_scene); const Appearance* appearance1 = m_scene.createAppearance(*effect, "appearance1"); const Appearance* appearance2 = m_scene.createAppearance(*effect, "appearance2"); // check data layout ref count on LL - EXPECT_EQ(appearance1->impl.getUniformDataLayout(), appearance2->impl.getUniformDataLayout()); - const uint32_t numReferences = m_scene.impl.getIScene().getNumDataLayoutReferences(appearance1->impl.getUniformDataLayout()); + EXPECT_EQ(appearance1->m_impl.getUniformDataLayout(), appearance2->m_impl.getUniformDataLayout()); + const uint32_t numReferences = m_scene.m_impl.getIScene().getNumDataLayoutReferences(appearance1->m_impl.getUniformDataLayout()); EXPECT_EQ(2u, numReferences); doWriteReadCycle(); @@ -404,18 +471,18 @@ namespace ramses Appearance* loadedAppearance2 = this->getObjectForTesting("appearance2"); // check data layout ref count on LL in loaded scene - EXPECT_EQ(loadedAppearance1->impl.getUniformDataLayout(), loadedAppearance2->impl.getUniformDataLayout()); - const uint32_t numReferencesLoaded = m_scene.impl.getIScene().getNumDataLayoutReferences(loadedAppearance1->impl.getUniformDataLayout()); + EXPECT_EQ(loadedAppearance1->m_impl.getUniformDataLayout(), loadedAppearance2->m_impl.getUniformDataLayout()); + const uint32_t numReferencesLoaded = m_scene.m_impl.getIScene().getNumDataLayoutReferences(loadedAppearance1->m_impl.getUniformDataLayout()); EXPECT_EQ(numReferences, numReferencesLoaded); m_sceneLoaded->destroy(*loadedAppearance2); m_sceneLoaded->destroy(*loadedAppearance1); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteGeometryBinding) + TEST_F(ASceneLoadedFromFile, canReadWriteGeometryBinding) { static const uint16_t inds[3] = { 0, 1, 2 }; - ArrayResource* const indices = this->m_scene.createArrayResource(EDataType::UInt16, 3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); + ArrayResource* const indices = this->m_scene.createArrayResource(3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); Effect* effect = TestEffects::CreateTestEffectWithAttribute(this->m_scene); @@ -427,21 +494,21 @@ namespace ramses const GeometryBinding* loadedGeometry = this->getObjectForTesting("geometry"); - EXPECT_EQ(geometry->impl.getEffectHash(), loadedGeometry->impl.getEffectHash()); - EXPECT_EQ(geometry->impl.getAttributeDataLayout(), loadedGeometry->impl.getAttributeDataLayout()); - EXPECT_EQ(geometry->impl.getAttributeDataInstance(), loadedGeometry->impl.getAttributeDataInstance()); - EXPECT_EQ(geometry->impl.getIndicesCount(), loadedGeometry->impl.getIndicesCount()); + EXPECT_EQ(geometry->m_impl.getEffectHash(), loadedGeometry->m_impl.getEffectHash()); + EXPECT_EQ(geometry->m_impl.getAttributeDataLayout(), loadedGeometry->m_impl.getAttributeDataLayout()); + EXPECT_EQ(geometry->m_impl.getAttributeDataInstance(), loadedGeometry->m_impl.getAttributeDataInstance()); + EXPECT_EQ(geometry->m_impl.getIndicesCount(), loadedGeometry->m_impl.getIndicesCount()); const Effect& loadedEffect = loadedGeometry->getEffect(); EXPECT_EQ(effect->getResourceId(), loadedEffect.getResourceId()); - EXPECT_EQ(effect->impl.getLowlevelResourceHash(), loadedEffect.impl.getLowlevelResourceHash()); - EXPECT_EQ(effect->impl.getObjectRegistryHandle(), loadedEffect.impl.getObjectRegistryHandle()); + EXPECT_EQ(effect->m_impl.getLowlevelResourceHash(), loadedEffect.m_impl.getLowlevelResourceHash()); + EXPECT_EQ(effect->m_impl.getObjectRegistryHandle(), loadedEffect.m_impl.getObjectRegistryHandle()); EXPECT_EQ(4u, loadedEffect.getAttributeInputCount()); AttributeInput attributeInputOut; EXPECT_EQ(StatusOK, loadedEffect.findAttributeInput("a_position", attributeInputOut)); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAMeshNode) + TEST_F(ASceneLoadedFromFile, canReadWriteAMeshNode) { MeshNode* meshNode = this->m_scene.createMeshNode("a meshnode"); @@ -453,10 +520,10 @@ namespace ramses EXPECT_EQ(meshNode->getGeometryBinding(), loadedMeshNode->getGeometryBinding()); EXPECT_EQ(meshNode->getStartIndex(), loadedMeshNode->getStartIndex()); EXPECT_EQ(meshNode->getIndexCount(), loadedMeshNode->getIndexCount()); - EXPECT_EQ(meshNode->impl.getFlattenedVisibility(), loadedMeshNode->impl.getFlattenedVisibility()); + EXPECT_EQ(meshNode->m_impl.getFlattenedVisibility(), loadedMeshNode->m_impl.getFlattenedVisibility()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAMeshNode_withVisibilityParent) + TEST_F(ASceneLoadedFromFile, canReadWriteAMeshNode_withVisibilityParent) { MeshNode* meshNode = this->m_scene.createMeshNode("a meshnode"); @@ -465,17 +532,17 @@ namespace ramses visibilityParent->addChild(*meshNode); // Apply visibility state only with flush, not before - EXPECT_EQ(meshNode->impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(meshNode->m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); this->m_scene.flush(); - EXPECT_EQ(meshNode->impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(meshNode->m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); doWriteReadCycle(); MeshNode* loadedMeshNode = this->getObjectForTesting("a meshnode"); - EXPECT_EQ(loadedMeshNode->impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(loadedMeshNode->m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAMeshNode_withVisibilityParentOff) + TEST_F(ASceneLoadedFromFile, canReadWriteAMeshNode_withVisibilityParentOff) { MeshNode* meshNode = this->m_scene.createMeshNode("a meshnode"); @@ -484,24 +551,24 @@ namespace ramses visibilityParent->addChild(*meshNode); // Apply visibility state only with flush, not before - EXPECT_EQ(meshNode->impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(meshNode->m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); this->m_scene.flush(); - EXPECT_EQ(meshNode->impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(meshNode->m_impl.getFlattenedVisibility(), EVisibilityMode::Off); doWriteReadCycle(); MeshNode* loadedMeshNode = this->getObjectForTesting("a meshnode"); - EXPECT_EQ(loadedMeshNode->impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(loadedMeshNode->m_impl.getFlattenedVisibility(), EVisibilityMode::Off); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAMeshNode_withValues) + TEST_F(ASceneLoadedFromFile, canReadWriteAMeshNode_withValues) { Effect* effect = TestEffects::CreateTestEffect(this->m_scene); Appearance* appearance = this->m_scene.createAppearance(*effect, "appearance"); GeometryBinding* geometry = this->m_scene.createGeometryBinding(*effect, "geometry"); const uint16_t data = 0u; - ArrayResource* indices = this->m_scene.createArrayResource(EDataType::UInt16, 1u, &data, ramses::ResourceCacheFlag_DoNotCache, "indices"); + ArrayResource* indices = this->m_scene.createArrayResource(1u, &data, ramses::ResourceCacheFlag_DoNotCache, "indices"); geometry->setIndices(*indices); MeshNode* meshNode = this->m_scene.createMeshNode("a meshnode"); @@ -511,7 +578,7 @@ namespace ramses EXPECT_EQ(StatusOK, meshNode->setGeometryBinding(*geometry)); EXPECT_EQ(StatusOK, meshNode->setStartIndex(456)); EXPECT_EQ(StatusOK, meshNode->setIndexCount(678u)); - EXPECT_EQ(StatusOK, meshNode->impl.setFlattenedVisibility(EVisibilityMode::Off)); + EXPECT_EQ(StatusOK, meshNode->m_impl.setFlattenedVisibility(EVisibilityMode::Off)); doWriteReadCycle(); MeshNode* loadedMeshNode = this->getObjectForTesting("a meshnode"); @@ -523,10 +590,10 @@ namespace ramses EXPECT_EQ(meshNode->getSceneObjectId(), loadedMeshNode->getSceneObjectId()); EXPECT_EQ(456u, loadedMeshNode->getStartIndex()); EXPECT_EQ(678u, loadedMeshNode->getIndexCount()); - EXPECT_EQ(loadedMeshNode->impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(loadedMeshNode->m_impl.getFlattenedVisibility(), EVisibilityMode::Off); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteANodeWithVisibility) + TEST_F(ASceneLoadedFromFile, canReadWriteANodeWithVisibility) { Node* visibilityNode = this->m_scene.createNode("a visibilitynode"); @@ -539,7 +606,7 @@ namespace ramses EXPECT_EQ(loadedVisibilityNode->getVisibility(), EVisibilityMode::Invisible); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteARenderGroup) + TEST_F(ASceneLoadedFromFile, canReadWriteARenderGroup) { RenderGroup* renderGroup = this->m_scene.createRenderGroup("a rendergroup"); @@ -558,18 +625,18 @@ namespace ramses EXPECT_STREQ(renderGroup->getName(), loadedRenderGroup->getName()); EXPECT_EQ(renderGroup->getSceneObjectId(), loadedRenderGroup->getSceneObjectId()); - EXPECT_EQ(2u, loadedRenderGroup->impl.getAllMeshes().size()); - EXPECT_EQ(&loadedMeshA->impl, loadedRenderGroup->impl.getAllMeshes()[0]); - EXPECT_EQ(&loadedMeshB->impl, loadedRenderGroup->impl.getAllMeshes()[1]); + EXPECT_EQ(2u, loadedRenderGroup->m_impl.getAllMeshes().size()); + EXPECT_EQ(&loadedMeshA->m_impl, loadedRenderGroup->m_impl.getAllMeshes()[0]); + EXPECT_EQ(&loadedMeshB->m_impl, loadedRenderGroup->m_impl.getAllMeshes()[1]); - const auto& internalRg = m_sceneLoaded->impl.getIScene().getRenderGroup(renderGroup->impl.getRenderGroupHandle()); - ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderable(meshA->impl.getRenderableHandle(), internalRg)); - ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderable(meshB->impl.getRenderableHandle(), internalRg)); - EXPECT_EQ(1, ramses_internal::RenderGroupUtils::FindRenderableEntry(meshA->impl.getRenderableHandle(), internalRg)->order); - EXPECT_EQ(2, ramses_internal::RenderGroupUtils::FindRenderableEntry(meshB->impl.getRenderableHandle(), internalRg)->order); + const auto& internalRg = m_sceneLoaded->m_impl.getIScene().getRenderGroup(renderGroup->m_impl.getRenderGroupHandle()); + ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderable(meshA->m_impl.getRenderableHandle(), internalRg)); + ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderable(meshB->m_impl.getRenderableHandle(), internalRg)); + EXPECT_EQ(1, ramses_internal::RenderGroupUtils::FindRenderableEntry(meshA->m_impl.getRenderableHandle(), internalRg)->order); + EXPECT_EQ(2, ramses_internal::RenderGroupUtils::FindRenderableEntry(meshB->m_impl.getRenderableHandle(), internalRg)->order); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteANestedRenderGroup) + TEST_F(ASceneLoadedFromFile, canReadWriteANestedRenderGroup) { RenderGroup* renderGroup = this->m_scene.createRenderGroup("a rendergroup"); RenderGroup* nestedRenderGroup = this->m_scene.createRenderGroup("a nested rendergroup"); @@ -591,28 +658,28 @@ namespace ramses EXPECT_STREQ(nestedRenderGroup->getName(), loadedNestedRenderGroup->getName()); EXPECT_EQ(nestedRenderGroup->getSceneObjectId(), loadedNestedRenderGroup->getSceneObjectId()); - EXPECT_EQ(1u, loadedRenderGroup->impl.getAllMeshes().size()); - EXPECT_EQ(1u, loadedRenderGroup->impl.getAllRenderGroups().size()); - EXPECT_EQ(1u, loadedNestedRenderGroup->impl.getAllMeshes().size()); + EXPECT_EQ(1u, loadedRenderGroup->m_impl.getAllMeshes().size()); + EXPECT_EQ(1u, loadedRenderGroup->m_impl.getAllRenderGroups().size()); + EXPECT_EQ(1u, loadedNestedRenderGroup->m_impl.getAllMeshes().size()); - EXPECT_EQ(&loadedMeshA->impl, loadedRenderGroup->impl.getAllMeshes()[0]); - EXPECT_EQ(&loadedMeshB->impl, loadedNestedRenderGroup->impl.getAllMeshes()[0]); + EXPECT_EQ(&loadedMeshA->m_impl, loadedRenderGroup->m_impl.getAllMeshes()[0]); + EXPECT_EQ(&loadedMeshB->m_impl, loadedNestedRenderGroup->m_impl.getAllMeshes()[0]); - EXPECT_EQ(&loadedNestedRenderGroup->impl, loadedRenderGroup->impl.getAllRenderGroups()[0]); + EXPECT_EQ(&loadedNestedRenderGroup->m_impl, loadedRenderGroup->m_impl.getAllRenderGroups()[0]); - const auto& internalRg = m_sceneLoaded->impl.getIScene().getRenderGroup(renderGroup->impl.getRenderGroupHandle()); - ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderable(meshA->impl.getRenderableHandle(), internalRg)); - EXPECT_EQ(1, ramses_internal::RenderGroupUtils::FindRenderableEntry(meshA->impl.getRenderableHandle(), internalRg)->order); + const auto& internalRg = m_sceneLoaded->m_impl.getIScene().getRenderGroup(renderGroup->m_impl.getRenderGroupHandle()); + ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderable(meshA->m_impl.getRenderableHandle(), internalRg)); + EXPECT_EQ(1, ramses_internal::RenderGroupUtils::FindRenderableEntry(meshA->m_impl.getRenderableHandle(), internalRg)->order); - ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(nestedRenderGroup->impl.getRenderGroupHandle(), internalRg)); - EXPECT_EQ(1, ramses_internal::RenderGroupUtils::FindRenderGroupEntry(nestedRenderGroup->impl.getRenderGroupHandle(), internalRg)->order); + ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(nestedRenderGroup->m_impl.getRenderGroupHandle(), internalRg)); + EXPECT_EQ(1, ramses_internal::RenderGroupUtils::FindRenderGroupEntry(nestedRenderGroup->m_impl.getRenderGroupHandle(), internalRg)->order); - const auto& internalRgNested = m_sceneLoaded->impl.getIScene().getRenderGroup(nestedRenderGroup->impl.getRenderGroupHandle()); - ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderable(meshB->impl.getRenderableHandle(), internalRgNested)); - EXPECT_EQ(2, ramses_internal::RenderGroupUtils::FindRenderableEntry(meshB->impl.getRenderableHandle(), internalRgNested)->order); + const auto& internalRgNested = m_sceneLoaded->m_impl.getIScene().getRenderGroup(nestedRenderGroup->m_impl.getRenderGroupHandle()); + ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderable(meshB->m_impl.getRenderableHandle(), internalRgNested)); + EXPECT_EQ(2, ramses_internal::RenderGroupUtils::FindRenderableEntry(meshB->m_impl.getRenderableHandle(), internalRgNested)->order); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteABasicRenderPass) + TEST_F(ASceneLoadedFromFile, canReadWriteABasicRenderPass) { const int32_t renderOrder = 1; @@ -632,7 +699,7 @@ namespace ramses EXPECT_TRUE(loadedRenderPass->isRenderOnce()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteARenderPassWithACamera) + TEST_F(ASceneLoadedFromFile, canReadWriteARenderPassWithACamera) { RenderPass* renderPass = this->m_scene.createRenderPass("a renderpass"); @@ -652,7 +719,7 @@ namespace ramses EXPECT_EQ(StatusOK, loadedCamera->validate()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteARenderPassWhichHasRenderGroups) + TEST_F(ASceneLoadedFromFile, canReadWriteARenderPassWhichHasRenderGroups) { RenderPass* renderPass = this->m_scene.createRenderPass("a renderpass"); RenderGroup* groupA = this->m_scene.createRenderGroup("groupA"); @@ -668,24 +735,24 @@ namespace ramses EXPECT_STREQ(renderPass->getName(), loadedRenderPass->getName()); EXPECT_EQ(renderPass->getSceneObjectId(), loadedRenderPass->getSceneObjectId()); - EXPECT_EQ(2u, loadedRenderPass->impl.getAllRenderGroups().size()); - EXPECT_EQ(&loadedMeshA->impl, loadedRenderPass->impl.getAllRenderGroups()[0]); - EXPECT_EQ(&loadedMeshB->impl, loadedRenderPass->impl.getAllRenderGroups()[1]); + EXPECT_EQ(2u, loadedRenderPass->m_impl.getAllRenderGroups().size()); + EXPECT_EQ(&loadedMeshA->m_impl, loadedRenderPass->m_impl.getAllRenderGroups()[0]); + EXPECT_EQ(&loadedMeshB->m_impl, loadedRenderPass->m_impl.getAllRenderGroups()[1]); - const ramses_internal::RenderPass& internalRP = m_sceneLoaded->impl.getIScene().getRenderPass(renderPass->impl.getRenderPassHandle()); - ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(groupA->impl.getRenderGroupHandle(), internalRP)); - ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(groupB->impl.getRenderGroupHandle(), internalRP)); - EXPECT_EQ(groupA->impl.getRenderGroupHandle(), ramses_internal::RenderGroupUtils::FindRenderGroupEntry(groupA->impl.getRenderGroupHandle(), internalRP)->renderGroup); - EXPECT_EQ(groupB->impl.getRenderGroupHandle(), ramses_internal::RenderGroupUtils::FindRenderGroupEntry(groupB->impl.getRenderGroupHandle(), internalRP)->renderGroup); - EXPECT_EQ(1, ramses_internal::RenderGroupUtils::FindRenderGroupEntry(groupA->impl.getRenderGroupHandle(), internalRP)->order); - EXPECT_EQ(2, ramses_internal::RenderGroupUtils::FindRenderGroupEntry(groupB->impl.getRenderGroupHandle(), internalRP)->order); + const ramses_internal::RenderPass& internalRP = m_sceneLoaded->m_impl.getIScene().getRenderPass(renderPass->m_impl.getRenderPassHandle()); + ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(groupA->m_impl.getRenderGroupHandle(), internalRP)); + ASSERT_TRUE(ramses_internal::RenderGroupUtils::ContainsRenderGroup(groupB->m_impl.getRenderGroupHandle(), internalRP)); + EXPECT_EQ(groupA->m_impl.getRenderGroupHandle(), ramses_internal::RenderGroupUtils::FindRenderGroupEntry(groupA->m_impl.getRenderGroupHandle(), internalRP)->renderGroup); + EXPECT_EQ(groupB->m_impl.getRenderGroupHandle(), ramses_internal::RenderGroupUtils::FindRenderGroupEntry(groupB->m_impl.getRenderGroupHandle(), internalRP)->renderGroup); + EXPECT_EQ(1, ramses_internal::RenderGroupUtils::FindRenderGroupEntry(groupA->m_impl.getRenderGroupHandle(), internalRP)->order); + EXPECT_EQ(2, ramses_internal::RenderGroupUtils::FindRenderGroupEntry(groupB->m_impl.getRenderGroupHandle(), internalRP)->order); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteBlitPass) + TEST_F(ASceneLoadedFromFile, canReadWriteBlitPass) { const int32_t renderOrder = 1; - const RenderBuffer* srcRenderBuffer = this->m_scene.createRenderBuffer(23, 42, ERenderBufferType_Depth, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_WriteOnly, 0u, "src renderBuffer"); - const RenderBuffer* dstRenderBuffer = this->m_scene.createRenderBuffer(23, 42, ERenderBufferType_Depth, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_WriteOnly, 0u, "dst renderBuffer"); + const RenderBuffer* srcRenderBuffer = this->m_scene.createRenderBuffer(23, 42, ERenderBufferType::Depth, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::WriteOnly, 0u, "src renderBuffer"); + const RenderBuffer* dstRenderBuffer = this->m_scene.createRenderBuffer(23, 42, ERenderBufferType::Depth, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::WriteOnly, 0u, "dst renderBuffer"); BlitPass* blitPass = this->m_scene.createBlitPass(*srcRenderBuffer, *dstRenderBuffer, "a blitpass"); EXPECT_EQ(StatusOK, blitPass->setRenderOrder(renderOrder)); @@ -700,25 +767,25 @@ namespace ramses EXPECT_EQ(renderOrder, loadedBlitPass->getRenderOrder()); EXPECT_FALSE(loadedBlitPass->isEnabled()); - const ramses_internal::BlitPassHandle loadedBlitPassHandle = loadedBlitPass->impl.getBlitPassHandle(); - const ramses_internal::BlitPass& blitPassInternal = m_sceneLoaded->impl.getIScene().getBlitPass(loadedBlitPassHandle); + const ramses_internal::BlitPassHandle loadedBlitPassHandle = loadedBlitPass->m_impl.getBlitPassHandle(); + const ramses_internal::BlitPass& blitPassInternal = m_sceneLoaded->m_impl.getIScene().getBlitPass(loadedBlitPassHandle); EXPECT_EQ(renderOrder, blitPassInternal.renderOrder); EXPECT_FALSE(blitPassInternal.isEnabled); - EXPECT_EQ(srcRenderBuffer->impl.getRenderBufferHandle(), blitPassInternal.sourceRenderBuffer); - EXPECT_EQ(dstRenderBuffer->impl.getRenderBufferHandle(), blitPassInternal.destinationRenderBuffer); + EXPECT_EQ(srcRenderBuffer->m_impl.getRenderBufferHandle(), blitPassInternal.sourceRenderBuffer); + EXPECT_EQ(dstRenderBuffer->m_impl.getRenderBufferHandle(), blitPassInternal.destinationRenderBuffer); EXPECT_EQ(renderOrder, blitPassInternal.renderOrder); const ramses_internal::PixelRectangle& sourceRegion = blitPassInternal.sourceRegion; EXPECT_EQ(0u, sourceRegion.x); EXPECT_EQ(0u, sourceRegion.y); - EXPECT_EQ(static_cast(srcRenderBuffer->getWidth()), sourceRegion.width); - EXPECT_EQ(static_cast(srcRenderBuffer->getHeight()), sourceRegion.height); + EXPECT_EQ(static_cast(srcRenderBuffer->getWidth()), sourceRegion.width); + EXPECT_EQ(static_cast(srcRenderBuffer->getHeight()), sourceRegion.height); const ramses_internal::PixelRectangle& destinationRegion = blitPassInternal.destinationRegion; EXPECT_EQ(0u, destinationRegion.x); EXPECT_EQ(0u, destinationRegion.y); - EXPECT_EQ(static_cast(dstRenderBuffer->getWidth()), destinationRegion.width); - EXPECT_EQ(static_cast(dstRenderBuffer->getHeight()), destinationRegion.height); + EXPECT_EQ(static_cast(dstRenderBuffer->getWidth()), destinationRegion.width); + EXPECT_EQ(static_cast(dstRenderBuffer->getHeight()), destinationRegion.height); //client HL api { @@ -737,14 +804,14 @@ namespace ramses EXPECT_EQ(dstRenderBuffer->getHeight(), heightOut); } - EXPECT_EQ(srcRenderBuffer->impl.getRenderBufferHandle(), loadedBlitPass->getSourceRenderBuffer().impl.getRenderBufferHandle()); - EXPECT_EQ(srcRenderBuffer->impl.getObjectRegistryHandle(), loadedBlitPass->getSourceRenderBuffer().impl.getObjectRegistryHandle()); + EXPECT_EQ(srcRenderBuffer->m_impl.getRenderBufferHandle(), loadedBlitPass->getSourceRenderBuffer().m_impl.getRenderBufferHandle()); + EXPECT_EQ(srcRenderBuffer->m_impl.getObjectRegistryHandle(), loadedBlitPass->getSourceRenderBuffer().m_impl.getObjectRegistryHandle()); - EXPECT_EQ(dstRenderBuffer->impl.getRenderBufferHandle(), loadedBlitPass->getDestinationRenderBuffer().impl.getRenderBufferHandle()); - EXPECT_EQ(dstRenderBuffer->impl.getObjectRegistryHandle(), loadedBlitPass->getDestinationRenderBuffer().impl.getObjectRegistryHandle()); + EXPECT_EQ(dstRenderBuffer->m_impl.getRenderBufferHandle(), loadedBlitPass->getDestinationRenderBuffer().m_impl.getRenderBufferHandle()); + EXPECT_EQ(dstRenderBuffer->m_impl.getObjectRegistryHandle(), loadedBlitPass->getDestinationRenderBuffer().m_impl.getObjectRegistryHandle()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWritePickableObject) + TEST_F(ASceneLoadedFromFile, canReadWritePickableObject) { const EDataType geometryBufferDataType = EDataType::Vector3F; const ArrayBuffer* geometryBuffer = this->m_scene.createArrayBuffer(geometryBufferDataType, 3u, "geometryBuffer"); @@ -773,23 +840,23 @@ namespace ramses EXPECT_EQ(this->getObjectForTesting("pickableCamera"), loadedPickableObject->getCamera()); EXPECT_EQ(this->getObjectForTesting("geometryBuffer"), &loadedPickableObject->getGeometryBuffer()); - const ramses_internal::PickableObjectHandle loadedPickableObjectPassHandle = loadedPickableObject->impl.getPickableObjectHandle(); - const ramses_internal::PickableObject& pickableObjectInternal = m_sceneLoaded->impl.getIScene().getPickableObject(loadedPickableObjectPassHandle); + const ramses_internal::PickableObjectHandle loadedPickableObjectPassHandle = loadedPickableObject->m_impl.getPickableObjectHandle(); + const ramses_internal::PickableObject& pickableObjectInternal = m_sceneLoaded->m_impl.getIScene().getPickableObject(loadedPickableObjectPassHandle); EXPECT_EQ(id.getValue(), pickableObjectInternal.id.getValue()); EXPECT_FALSE(pickableObjectInternal.isEnabled); - EXPECT_EQ(geometryBuffer->impl.getDataBufferHandle(), pickableObjectInternal.geometryHandle); - EXPECT_EQ(pickableCamera->impl.getCameraHandle(), pickableObjectInternal.cameraHandle); + EXPECT_EQ(geometryBuffer->m_impl.getDataBufferHandle(), pickableObjectInternal.geometryHandle); + EXPECT_EQ(pickableCamera->m_impl.getCameraHandle(), pickableObjectInternal.cameraHandle); - EXPECT_EQ(geometryBuffer->impl.getDataBufferHandle(), loadedPickableObject->getGeometryBuffer().impl.getDataBufferHandle()); - EXPECT_EQ(geometryBuffer->impl.getObjectRegistryHandle(), loadedPickableObject->getGeometryBuffer().impl.getObjectRegistryHandle()); + EXPECT_EQ(geometryBuffer->m_impl.getDataBufferHandle(), loadedPickableObject->getGeometryBuffer().m_impl.getDataBufferHandle()); + EXPECT_EQ(geometryBuffer->m_impl.getObjectRegistryHandle(), loadedPickableObject->getGeometryBuffer().m_impl.getObjectRegistryHandle()); - EXPECT_EQ(pickableCamera->impl.getCameraHandle(), loadedPickableObject->getCamera()->impl.getCameraHandle()); - EXPECT_EQ(pickableCamera->impl.getObjectRegistryHandle(), loadedPickableObject->getCamera()->impl.getObjectRegistryHandle()); + EXPECT_EQ(pickableCamera->m_impl.getCameraHandle(), loadedPickableObject->getCamera()->m_impl.getCameraHandle()); + EXPECT_EQ(pickableCamera->m_impl.getObjectRegistryHandle(), loadedPickableObject->getCamera()->m_impl.getObjectRegistryHandle()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteRenderBuffer) + TEST_F(ASceneLoadedFromFile, canReadWriteRenderBuffer) { - RenderBuffer* renderBuffer = this->m_scene.createRenderBuffer(23, 42, ERenderBufferType_Depth, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_WriteOnly, 4u, "a renderTarget"); + RenderBuffer* renderBuffer = this->m_scene.createRenderBuffer(23, 42, ERenderBufferType::Depth, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::WriteOnly, 4u, "a renderTarget"); doWriteReadCycle(); @@ -804,14 +871,14 @@ namespace ramses EXPECT_EQ(renderBuffer->getAccessMode(), loadedRenderBuffer->getAccessMode()); EXPECT_EQ(renderBuffer->getSampleCount(), loadedRenderBuffer->getSampleCount()); - EXPECT_EQ(renderBuffer->impl.getRenderBufferHandle(), loadedRenderBuffer->impl.getRenderBufferHandle()); + EXPECT_EQ(renderBuffer->m_impl.getRenderBufferHandle(), loadedRenderBuffer->m_impl.getRenderBufferHandle()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteARenderPassWithARenderTargetAndCamera) + TEST_F(ASceneLoadedFromFile, canReadWriteARenderPassWithARenderTargetAndCamera) { RenderPass* renderPass = this->m_scene.createRenderPass("a renderpass"); - RenderBuffer* renderBuffer = this->m_scene.createRenderBuffer(23u, 42u, ERenderBufferType_Depth, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_ReadWrite, 0u, "a renderBuffer"); + RenderBuffer* renderBuffer = this->m_scene.createRenderBuffer(23u, 42u, ERenderBufferType::Depth, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::ReadWrite, 0u, "a renderBuffer"); RenderTargetDescription rtDesc; rtDesc.addRenderBuffer(*renderBuffer); RenderTarget* renderTarget = this->m_scene.createRenderTarget(rtDesc, "target"); @@ -835,9 +902,9 @@ namespace ramses EXPECT_EQ(StatusOK, loadedCamera->validate()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteRenderTarget) + TEST_F(ASceneLoadedFromFile, canReadWriteRenderTarget) { - const RenderBuffer& rb = *m_scene.createRenderBuffer(16u, 8u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer& rb = *m_scene.createRenderBuffer(16u, 8u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); RenderTargetDescription rtDesc; rtDesc.addRenderBuffer(rb); @@ -852,10 +919,10 @@ namespace ramses EXPECT_EQ(renderTarget->getWidth(), loadedRenderTarget->getWidth()); EXPECT_EQ(renderTarget->getHeight(), loadedRenderTarget->getHeight()); - EXPECT_EQ(renderTarget->impl.getRenderTargetHandle(), loadedRenderTarget->impl.getRenderTargetHandle()); + EXPECT_EQ(renderTarget->m_impl.getRenderTargetHandle(), loadedRenderTarget->m_impl.getRenderTargetHandle()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteIndexDataBuffer) + TEST_F(ASceneLoadedFromFile, canReadWriteIndexDataBuffer) { ArrayBuffer& buffer = *m_scene.createArrayBuffer(EDataType::UInt32, 6u, "indexDB"); buffer.updateData(3u, 2u, std::array{ {6, 7} }.data()); @@ -865,11 +932,11 @@ namespace ramses const ArrayBuffer* loadedBuffer = this->getObjectForTesting("indexDB"); EXPECT_STREQ(buffer.getName(), loadedBuffer->getName()); - EXPECT_EQ(buffer.impl.getDataBufferHandle(), loadedBuffer->impl.getDataBufferHandle()); - EXPECT_EQ(6 * sizeof(uint32_t), m_scene.impl.getIScene().getDataBuffer(loadedBuffer->impl.getDataBufferHandle()).data.size()); - EXPECT_EQ(5 * sizeof(uint32_t), m_scene.impl.getIScene().getDataBuffer(loadedBuffer->impl.getDataBufferHandle()).usedSize); + EXPECT_EQ(buffer.m_impl.getDataBufferHandle(), loadedBuffer->m_impl.getDataBufferHandle()); + EXPECT_EQ(6 * sizeof(uint32_t), m_scene.m_impl.getIScene().getDataBuffer(loadedBuffer->m_impl.getDataBufferHandle()).data.size()); + EXPECT_EQ(5 * sizeof(uint32_t), m_scene.m_impl.getIScene().getDataBuffer(loadedBuffer->m_impl.getDataBufferHandle()).usedSize); - const ramses_internal::Byte* loadedDataBufferData = m_scene.impl.getIScene().getDataBuffer(loadedBuffer->impl.getDataBufferHandle()).data.data(); + const ramses_internal::Byte* loadedDataBufferData = m_scene.m_impl.getIScene().getDataBuffer(loadedBuffer->m_impl.getDataBufferHandle()).data.data(); EXPECT_EQ(6u, ramses_internal::UnsafeTestMemoryHelpers::GetTypedValueFromMemoryBlob(loadedDataBufferData, 3)); EXPECT_EQ(7u, ramses_internal::UnsafeTestMemoryHelpers::GetTypedValueFromMemoryBlob(loadedDataBufferData, 4)); @@ -882,7 +949,7 @@ namespace ramses EXPECT_EQ(7u, bufferDataOut[4]); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteTexture2DBuffer) + TEST_F(ASceneLoadedFromFile, canReadWriteTexture2DBuffer) { Texture2DBuffer& buffer = *m_scene.createTexture2DBuffer(ETextureFormat::RGBA8, 3, 4, 2, "textureBuffer"); buffer.updateData(0, 0, 0, 2, 2, std::array{ {12, 23, 34, 56} }.data()); @@ -894,10 +961,10 @@ namespace ramses EXPECT_STREQ(buffer.getName(), loadedBuffer->getName()); EXPECT_EQ(buffer.getSceneObjectId(), loadedBuffer->getSceneObjectId()); - EXPECT_EQ(buffer.impl.getTextureBufferHandle(), loadedBuffer->impl.getTextureBufferHandle()); + EXPECT_EQ(buffer.m_impl.getTextureBufferHandle(), loadedBuffer->m_impl.getTextureBufferHandle()); //iscene - const ramses_internal::TextureBuffer& loadedInternalBuffer = m_scene.impl.getIScene().getTextureBuffer(loadedBuffer->impl.getTextureBufferHandle()); + const ramses_internal::TextureBuffer& loadedInternalBuffer = m_scene.m_impl.getIScene().getTextureBuffer(loadedBuffer->m_impl.getTextureBufferHandle()); ASSERT_EQ(2u, loadedInternalBuffer.mipMaps.size()); EXPECT_EQ(3u, loadedInternalBuffer.mipMaps[0].width); EXPECT_EQ(4u, loadedInternalBuffer.mipMaps[0].height); @@ -937,27 +1004,7 @@ namespace ramses EXPECT_EQ(78u, bufferForMip1[0]); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAnAnimationSystem) - { - AnimationSystem* animSystem = this->m_scene.createAnimationSystem(ramses::EAnimationSystemFlags_Default, "anim system"); - - doWriteReadCycle(); - - AnimationSystem* animSystemLoaded = this->getObjectForTesting("anim system"); - EXPECT_EQ(animSystem->impl.getAnimationSystemHandle(), animSystemLoaded->impl.getAnimationSystemHandle()); - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAnAnimationSystemRealTime) - { - AnimationSystemRealTime* animSystem = this->m_scene.createRealTimeAnimationSystem(ramses::EAnimationSystemFlags_Default, "anim system"); - - doWriteReadCycle(); - - AnimationSystemRealTime* animSystemLoaded = this->getObjectForTesting("anim system"); - EXPECT_EQ(animSystem->impl.getAnimationSystemHandle(), animSystemLoaded->impl.getAnimationSystemHandle()); - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteANode) + TEST_F(ASceneLoadedFromFile, canReadWriteANode) { //generic node cannot be created, therefore using group node Node* grandParent = this->m_scene.createNode("node1"); @@ -967,10 +1014,10 @@ namespace ramses grandParent->addChild(*parent); child->setParent(*parent); - child->setTranslation(1, 2, 3); + child->setTranslation({1, 2, 3}); child->setVisibility(EVisibilityMode::Invisible); - child->setRotation(1, 2, 3); - child->setScaling(1, 2, 3); + child->setRotation({1, 2, 3}, ERotationType::Euler_XYX); + child->setScaling({1, 2, 3}); doWriteReadCycle(); @@ -987,39 +1034,34 @@ namespace ramses EXPECT_EQ(1u, loadedParent->getChildCount()); EXPECT_EQ(0u, loadedChild->getChildCount()); - float tx = 0; - float ty = 0; - float tz = 0; - EXPECT_EQ(StatusOK, loadedChild->getTranslation(tx, ty, tz)); - EXPECT_FLOAT_EQ(1, tx); - EXPECT_FLOAT_EQ(2, ty); - EXPECT_FLOAT_EQ(3, tz); + vec3f translation; + EXPECT_EQ(StatusOK, loadedChild->getTranslation(translation)); + EXPECT_FLOAT_EQ(1, translation.x); + EXPECT_FLOAT_EQ(2, translation.y); + EXPECT_FLOAT_EQ(3, translation.z); EXPECT_EQ(loadedChild->getVisibility(), EVisibilityMode::Invisible); - float rx = 0; - float ry = 0; - float rz = 0; - EXPECT_EQ(StatusOK, loadedChild->getRotation(rx, ry, rz)); - EXPECT_FLOAT_EQ(1, rx); - EXPECT_FLOAT_EQ(2, ry); - EXPECT_FLOAT_EQ(3, rz); + vec3f rotation; + EXPECT_EQ(StatusOK, loadedChild->getRotation(rotation)); + EXPECT_FLOAT_EQ(1, rotation.x); + EXPECT_FLOAT_EQ(2, rotation.y); + EXPECT_FLOAT_EQ(3, rotation.z); + EXPECT_EQ(ERotationType::Euler_XYX, loadedChild->getRotationType()); - float sx = 0; - float sy = 0; - float sz = 0; - EXPECT_EQ(StatusOK, loadedChild->getScaling(sx, sy, sz)); - EXPECT_FLOAT_EQ(1, sx); - EXPECT_FLOAT_EQ(2, sy); - EXPECT_FLOAT_EQ(3, sz); + vec3f scale; + EXPECT_EQ(StatusOK, loadedChild->getScaling(scale)); + EXPECT_FLOAT_EQ(1, scale.x); + EXPECT_FLOAT_EQ(2, scale.y); + EXPECT_FLOAT_EQ(3, scale.z); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteANodeWithTranslation) + TEST_F(ASceneLoadedFromFile, canReadWriteANodeWithTranslation) { Node* node = this->m_scene.createNode("translate node 1"); Node* child = this->m_scene.createNode("groupnode child"); - node->setTranslation(1, 2, 3); + node->setTranslation({1, 2, 3}); node->addChild(*child); doWriteReadCycle(); @@ -1032,21 +1074,19 @@ namespace ramses EXPECT_EQ(1u, node->getChildCount()); EXPECT_EQ(loadedChild, loadedTranslateNode->getChild(0u)); - float x = 0; - float y = 0; - float z = 0; - EXPECT_EQ(StatusOK, node->getTranslation(x, y, z)); - EXPECT_FLOAT_EQ(1, x); - EXPECT_FLOAT_EQ(2, y); - EXPECT_FLOAT_EQ(3, z); + vec3f translation; + EXPECT_EQ(StatusOK, node->getTranslation(translation)); + EXPECT_FLOAT_EQ(1, translation.x); + EXPECT_FLOAT_EQ(2, translation.y); + EXPECT_FLOAT_EQ(3, translation.z); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteANodeWithRotation) + TEST_F(ASceneLoadedFromFile, canReadWriteANodeWithRotation) { Node* node = this->m_scene.createNode("rotate node 1"); Node* child = this->m_scene.createNode("groupnode child"); - node->setRotation(1, 2, 3); + node->setRotation({1, 2, 3}, ERotationType::Euler_ZYX); child->setParent(*node); doWriteReadCycle(); @@ -1058,21 +1098,20 @@ namespace ramses EXPECT_EQ(1u, loadedRotateNode->getChildCount()); EXPECT_EQ(loadedChild, loadedRotateNode->getChild(0u)); - float x = 0; - float y = 0; - float z = 0; - EXPECT_EQ(StatusOK, loadedRotateNode->getRotation(x, y, z)); - EXPECT_FLOAT_EQ(1, x); - EXPECT_FLOAT_EQ(2, y); - EXPECT_FLOAT_EQ(3, z); + vec3f rotation; + EXPECT_EQ(StatusOK, loadedRotateNode->getRotation(rotation)); + EXPECT_FLOAT_EQ(1, rotation.x); + EXPECT_FLOAT_EQ(2, rotation.y); + EXPECT_FLOAT_EQ(3, rotation.z); + EXPECT_EQ(ERotationType::Euler_ZYX, loadedRotateNode->getRotationType()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteANodeWithScaling) + TEST_F(ASceneLoadedFromFile, canReadWriteANodeWithScaling) { Node* node = this->m_scene.createNode("scale node"); Node* child = this->m_scene.createNode("groupnode child"); - node->setScaling(1, 2, 3); + node->setScaling({1, 2, 3}); child->setParent(*node); doWriteReadCycle(); @@ -1084,21 +1123,19 @@ namespace ramses EXPECT_EQ(1u, loadedScaleNode->getChildCount()); EXPECT_EQ(loadedChild, loadedScaleNode->getChild(0u)); - float x = 0; - float y = 0; - float z = 0; - EXPECT_EQ(StatusOK, loadedScaleNode->getScaling(x, y, z)); - EXPECT_FLOAT_EQ(1, x); - EXPECT_FLOAT_EQ(2, y); - EXPECT_FLOAT_EQ(3, z); + vec3f scale; + EXPECT_EQ(StatusOK, loadedScaleNode->getScaling(scale)); + EXPECT_FLOAT_EQ(1, scale.x); + EXPECT_FLOAT_EQ(2, scale.y); + EXPECT_FLOAT_EQ(3, scale.z); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteATextureSampler) + TEST_F(ASceneLoadedFromFile, canReadWriteATextureSampler) { - const ETextureAddressMode wrapUMode = ETextureAddressMode_Mirror; - const ETextureAddressMode wrapVMode = ETextureAddressMode_Repeat; - const ETextureSamplingMethod minSamplingMethod = ETextureSamplingMethod_Linear_MipMapNearest; - const ETextureSamplingMethod magSamplingMethod = ETextureSamplingMethod_Linear; + const ETextureAddressMode wrapUMode = ETextureAddressMode::Mirror; + const ETextureAddressMode wrapVMode = ETextureAddressMode::Repeat; + const ETextureSamplingMethod minSamplingMethod = ETextureSamplingMethod::Linear_MipMapNearest; + const ETextureSamplingMethod magSamplingMethod = ETextureSamplingMethod::Linear; const uint8_t data[4] = { 0u }; const MipLevelData mipLevelData(sizeof(data), data); Texture2D* texture = this->m_scene.createTexture2D(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, "texture"); @@ -1116,13 +1153,13 @@ namespace ramses EXPECT_EQ(minSamplingMethod, loadedSampler->getMinSamplingMethod()); EXPECT_EQ(magSamplingMethod, loadedSampler->getMagSamplingMethod()); EXPECT_EQ(8u, loadedSampler->getAnisotropyLevel()); - EXPECT_EQ(texture->impl.getLowlevelResourceHash(), this->m_sceneLoaded->impl.getIScene().getTextureSampler(loadedSampler->impl.getTextureSamplerHandle()).textureResource); - EXPECT_EQ(ERamsesObjectType_Texture2D, loadedSampler->impl.getTextureType()); + EXPECT_EQ(texture->m_impl.getLowlevelResourceHash(), this->m_sceneLoaded->m_impl.getIScene().getTextureSampler(loadedSampler->m_impl.getTextureSamplerHandle()).textureResource); + EXPECT_EQ(ERamsesObjectType::Texture2D, loadedSampler->m_impl.getTextureType()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteATextureSamplerMS) + TEST_F(ASceneLoadedFromFile, canReadWriteATextureSamplerMS) { - RenderBuffer* renderBuffer = m_scene.createRenderBuffer(4u, 4u, ERenderBufferType_Color, ERenderBufferFormat_RGB8, ERenderBufferAccessMode_ReadWrite, 4u); + RenderBuffer* renderBuffer = m_scene.createRenderBuffer(4u, 4u, ERenderBufferType::Color, ERenderBufferFormat::RGB8, ERenderBufferAccessMode::ReadWrite, 4u); TextureSamplerMS* sampler = this->m_scene.createTextureSamplerMS(*renderBuffer, "sampler"); ASSERT_TRUE(nullptr != sampler); @@ -1132,12 +1169,12 @@ namespace ramses TextureSamplerMS* loadedSampler = getObjectForTesting("sampler"); ASSERT_TRUE(nullptr != loadedSampler); - EXPECT_EQ(ERamsesObjectType_RenderBuffer, loadedSampler->impl.getTextureType()); + EXPECT_EQ(ERamsesObjectType::RenderBuffer, loadedSampler->m_impl.getTextureType()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteATextureSamplerExternal) + TEST_F(ASceneLoadedFromFile, canReadWriteATextureSamplerExternal) { - TextureSamplerExternal* sampler = this->m_scene.createTextureSamplerExternal(ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, "sampler"); + TextureSamplerExternal* sampler = this->m_scene.createTextureSamplerExternal(ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, "sampler"); ASSERT_TRUE(nullptr != sampler); doWriteReadCycle(); @@ -1145,10 +1182,10 @@ namespace ramses TextureSamplerExternal* loadedSampler = getObjectForTesting("sampler"); ASSERT_TRUE(nullptr != loadedSampler); - EXPECT_EQ(ERamsesObjectType_TextureSamplerExternal, loadedSampler->impl.getTextureType()); + EXPECT_EQ(ERamsesObjectType::TextureSamplerExternal, loadedSampler->m_impl.getTextureType()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteSceneId) + TEST_F(ASceneLoadedFromFile, canReadWriteSceneId) { const sceneId_t sceneId = ramses::sceneId_t(1ULL << 63); ramses::Scene& mScene(*client.createScene(sceneId)); @@ -1162,52 +1199,52 @@ namespace ramses EXPECT_EQ(sceneId, m_sceneLoaded->getSceneId()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, defaultsToLocalAndRemotePublicationMode) + TEST_F(ASceneLoadedFromFile, defaultsToLocalAndRemotePublicationMode) { const sceneId_t sceneId(81); EXPECT_EQ(StatusOK, client.createScene(sceneId)->saveToFile("someTempararyFile.ram", false)); m_sceneLoaded = m_clientForLoading.loadSceneFromFile("someTempararyFile.ram"); ASSERT_TRUE(nullptr != m_sceneLoaded); - EXPECT_EQ(EScenePublicationMode_LocalAndRemote, m_sceneLoaded->impl.getPublicationModeSetFromSceneConfig()); + EXPECT_EQ(EScenePublicationMode::LocalAndRemote, m_sceneLoaded->m_impl.getPublicationModeSetFromSceneConfig()); } // TODO(tobias) add to store this option to file format as soon as changes are allowed - TEST_F(ASceneAndAnimationSystemLoadedFromFile, DISABLED_respectsPublicationModeSetOnCreatingFileBeforeSave) + TEST_F(ASceneLoadedFromFile, DISABLED_respectsPublicationModeSetOnCreatingFileBeforeSave) { const sceneId_t sceneId(81); SceneConfig config; - config.setPublicationMode(ramses::EScenePublicationMode_LocalOnly); + config.setPublicationMode(ramses::EScenePublicationMode::LocalOnly); EXPECT_EQ(StatusOK, client.createScene(sceneId, config)->saveToFile("someTempararyFile.ram", false)); m_sceneLoaded = m_clientForLoading.loadSceneFromFile("someTempararyFile.ram"); ASSERT_TRUE(nullptr != m_sceneLoaded); - EXPECT_EQ(EScenePublicationMode_LocalOnly, m_sceneLoaded->impl.getPublicationModeSetFromSceneConfig()); + EXPECT_EQ(EScenePublicationMode::LocalOnly, m_sceneLoaded->m_impl.getPublicationModeSetFromSceneConfig()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canOverwritePublicationModeForLoadedFiles) + TEST_F(ASceneLoadedFromFile, canOverwritePublicationModeForLoadedFiles) { const sceneId_t sceneId(80); EXPECT_EQ(StatusOK, client.createScene(sceneId)->saveToFile("someTempararyFile.ram", false)); m_sceneLoaded = m_clientForLoading.loadSceneFromFile("someTempararyFile.ram", true); ASSERT_TRUE(nullptr != m_sceneLoaded); - EXPECT_EQ(EScenePublicationMode_LocalOnly, m_sceneLoaded->impl.getPublicationModeSetFromSceneConfig()); + EXPECT_EQ(EScenePublicationMode::LocalOnly, m_sceneLoaded->m_impl.getPublicationModeSetFromSceneConfig()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, reportsErrorWhenSavingSceneToFileWithInvalidFileName) + TEST_F(ASceneLoadedFromFile, reportsErrorWhenSavingSceneToFileWithInvalidFileName) { - ramses::status_t status = m_scene.saveToFile("?XYZ:/dummyFile", false); + ramses::status_t status = m_scene.saveToFile("?Euler_ZYX:/dummyFile", false); EXPECT_NE(ramses::StatusOK, status); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, reportsErrorWhenSavingSceneToFileWithNoFileName) + TEST_F(ASceneLoadedFromFile, reportsErrorWhenSavingSceneToFileWithNoFileName) { - ramses::status_t status = m_scene.saveToFile(nullptr, false); + ramses::status_t status = m_scene.saveToFile({}, false); EXPECT_NE(ramses::StatusOK, status); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, overwritesExistingFileWhenSavingSceneToIt) + TEST_F(ASceneLoadedFromFile, overwritesExistingFileWhenSavingSceneToIt) { { ramses_internal::File existingFile("dummyFile.dat"); @@ -1220,7 +1257,7 @@ namespace ramses { ramses_internal::File fileShouldBeOverwritten("dummyFile.dat"); EXPECT_TRUE(fileShouldBeOverwritten.open(ramses_internal::File::Mode::ReadOnly)); - ramses_internal::UInt fileSize = 0; + size_t fileSize = 0; EXPECT_TRUE(fileShouldBeOverwritten.getSizeInBytes(fileSize)); EXPECT_NE(0u, fileSize); } @@ -1228,19 +1265,19 @@ namespace ramses EXPECT_TRUE(ramses_internal::File("dummyFile.dat").remove()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, doesNotLoadSceneFromFileWithInvalidFileName) + TEST_F(ASceneLoadedFromFile, doesNotLoadSceneFromFileWithInvalidFileName) { - ramses::Scene* scene = client.loadSceneFromFile("?XYZ:/dummyFile"); + ramses::Scene* scene = client.loadSceneFromFile("?Euler_ZYX:/dummyFile"); EXPECT_TRUE(nullptr == scene); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, doesNotLoadSceneFromFileWithoutFileName) + TEST_F(ASceneLoadedFromFile, doesNotLoadSceneFromFileWithoutFileName) { - EXPECT_EQ(nullptr, client.loadSceneFromFile(nullptr)); + EXPECT_EQ(nullptr, client.loadSceneFromFile({})); EXPECT_EQ(nullptr, client.loadSceneFromFile("")); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, doesNotLoadSceneFromInvalidMemory) + TEST_F(ASceneLoadedFromFile, doesNotLoadSceneFromInvalidMemory) { auto deleter = [](const unsigned char* ptr) { delete[] ptr; }; EXPECT_EQ(nullptr, client.loadSceneFromMemory(std::unique_ptr(nullptr, deleter), 1, false)); @@ -1250,25 +1287,25 @@ namespace ramses EXPECT_EQ(nullptr, RamsesUtils::LoadSceneFromMemory(client, std::unique_ptr(new unsigned char[1]), 0, false)); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, doesNotLoadSceneFromInvalidFileDescriptor) + TEST_F(ASceneLoadedFromFile, doesNotLoadSceneFromInvalidFileDescriptor) { EXPECT_EQ(nullptr, client.loadSceneFromFileDescriptor(-1, 0, 1, false)); EXPECT_EQ(nullptr, client.loadSceneFromFileDescriptor(1, 0, 0, false)); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, doesNotLoadSceneFromUnexistingFile) + TEST_F(ASceneLoadedFromFile, doesNotLoadSceneFromUnexistingFile) { ramses::Scene* scene = client.loadSceneFromFile("ZEGETWTWAGTGSDGEg_thisfilename_in_this_directory_should_not_exist_DSAFDSFSTEZHDXHB"); EXPECT_TRUE(nullptr == scene); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canHandleAllZeroFileOnSceneLoad) + TEST_F(ASceneLoadedFromFile, canHandleAllZeroFileOnSceneLoad) { const char* filename = "allzerofile.dat"; { ramses_internal::File file(filename); EXPECT_TRUE(file.open(ramses_internal::File::Mode::WriteNew)); - std::vector zerovector(4096); + std::vector zerovector(4096); EXPECT_TRUE(file.write(&zerovector[0], zerovector.size())); file.close(); } @@ -1277,7 +1314,7 @@ namespace ramses EXPECT_TRUE(scene == nullptr); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, cannotLoadSameFileTwice) + TEST_F(ASceneLoadedFromFile, cannotLoadSameFileTwice) { const sceneId_t sceneId = ramses::sceneId_t(1ULL << 63); ramses::Scene* scene = client.createScene(sceneId); @@ -1290,7 +1327,7 @@ namespace ramses EXPECT_EQ(nullptr, m_clientForLoading.loadSceneFromFile("someTempararyFile.ram")); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, cannotLoadScenesWithSameSceneIdTwice) + TEST_F(ASceneLoadedFromFile, cannotLoadScenesWithSameSceneIdTwice) { const sceneId_t sceneId = ramses::sceneId_t(1ULL << 63); @@ -1313,331 +1350,111 @@ namespace ramses EXPECT_EQ(nullptr, m_clientForLoading.loadSceneFromFile("someTempararyFile_2.ram")); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canHandleAllZeroFileOnResourceLoad) + TEST_F(ASceneLoadedFromFile, cannotLoadSceneWithMismatchingFeatureLevel) { - const char* filename = "allzerofile.dat"; - { - ramses_internal::File file(filename); - EXPECT_TRUE(file.open(ramses_internal::File::Mode::WriteNew)); - std::vector zerovector(4096); - EXPECT_TRUE(file.write(&zerovector[0], zerovector.size())); - file.close(); - } - - EXPECT_FALSE(RamsesHMIUtils::GetResourceDataPoolForClient(m_clientForLoading).addResourceDataFile(filename)); + saveSceneWithFeatureLevelToFile(EFeatureLevel(99), "someTemporaryFile.ram"); + EXPECT_EQ(nullptr, m_clientForLoading.loadSceneFromFile("someTemporaryFile.ram")); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteSpline) + TEST_F(ASceneLoadedFromFile, canGetFeatureLevelFromSceneFile) { - SplineStepBool* spline = this->animationSystem.createSplineStepBool("spline"); + saveSceneWithFeatureLevelToFile(EFeatureLevel_Latest, "someTemporaryFile.ram"); - doWriteReadCycle(); - - const SplineStepBool* splineLoaded = this->getAnimationObjectForTesting("spline"); - EXPECT_EQ(spline->getNumberOfKeys(), splineLoaded->getNumberOfKeys()); - EXPECT_EQ(spline->impl.getSplineHandle(), splineLoaded->impl.getSplineHandle()); + EFeatureLevel featureLevel = EFeatureLevel_01; + EXPECT_TRUE(RamsesClient::GetFeatureLevelFromFile("someTemporaryFile.ram", featureLevel)); + EXPECT_EQ(EFeatureLevel_Latest, featureLevel); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteMultipleSplinesOfSameType) + TEST_F(ASceneLoadedFromFile, failsToGetFeatureLevelFromFileWithUnknownFeatureLevel) { - const SplineStepBool* spline1 = this->animationSystem.createSplineStepBool("spline1"); - const SplineStepBool* spline2 = this->animationSystem.createSplineStepBool("spline2"); - const SplineStepBool* spline3 = this->animationSystem.createSplineStepBool("spline3"); - - doWriteReadCycle(); + saveSceneWithFeatureLevelToFile(EFeatureLevel(99), "someTemporaryFile.ram"); - const SplineStepBool* splineLoaded1 = this->getAnimationObjectForTesting("spline1"); - const SplineStepBool* splineLoaded2 = this->getAnimationObjectForTesting("spline2"); - const SplineStepBool* splineLoaded3 = this->getAnimationObjectForTesting("spline3"); - EXPECT_EQ(spline1->getNumberOfKeys(), splineLoaded1->getNumberOfKeys()); - EXPECT_EQ(spline2->getNumberOfKeys(), splineLoaded2->getNumberOfKeys()); - EXPECT_EQ(spline3->getNumberOfKeys(), splineLoaded3->getNumberOfKeys()); - EXPECT_EQ(spline1->impl.getSplineHandle(), splineLoaded1->impl.getSplineHandle()); - EXPECT_EQ(spline2->impl.getSplineHandle(), splineLoaded2->impl.getSplineHandle()); - EXPECT_EQ(spline3->impl.getSplineHandle(), splineLoaded3->impl.getSplineHandle()); + EFeatureLevel featureLevel = EFeatureLevel_01; + EXPECT_FALSE(RamsesClient::GetFeatureLevelFromFile("someTemporaryFile.ram", featureLevel)); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAnimatedProperty) + TEST_F(ASceneLoadedFromFile, failsToGetFeatureLevelFromNonexistingFile) { - Node* node = this->m_scene.createNode(); - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation, EAnimatedPropertyComponent_X, "property"); - - doWriteReadCycle(); - - const AnimatedProperty* propLoaded = this->getAnimationObjectForTesting("property"); - EXPECT_EQ(prop->impl.getDataBindHandle(), propLoaded->impl.getDataBindHandle()); - EXPECT_EQ(prop->impl.getVectorComponent(), propLoaded->impl.getVectorComponent()); - EXPECT_EQ(prop->impl.getDataTypeID(), propLoaded->impl.getDataTypeID()); + EFeatureLevel featureLevel = EFeatureLevel_01; + EXPECT_FALSE(RamsesClient::GetFeatureLevelFromFile("doesnt.Exist", featureLevel)); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAnimation) + TEST_F(ASceneLoadedFromFile, canGetFeatureLevelFromSceneFileViaFileDescriptor) { - SplineStepFloat* spline = this->animationSystem.createSplineStepFloat("spline"); - Node* node = this->m_scene.createNode(); - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation, EAnimatedPropertyComponent_X); - Animation* anim = this->animationSystem.createAnimation(*prop, *spline, "animation"); + saveSceneWithFeatureLevelToFile(EFeatureLevel_Latest, "someTemporaryFile.ram"); - doWriteReadCycle(); + size_t fileSize = 0; + { + // write to a file with some offset + ramses_internal::File inFile("someTemporaryFile.ram"); + EXPECT_TRUE(inFile.getSizeInBytes(fileSize)); + std::vector data(fileSize); + size_t numBytesRead = 0; + EXPECT_TRUE(inFile.open(ramses_internal::File::Mode::ReadOnlyBinary)); + EXPECT_EQ(ramses_internal::EStatus::Ok, inFile.read(data.data(), fileSize, numBytesRead)); - const Animation* animLoaded = this->getAnimationObjectForTesting("animation"); - EXPECT_EQ(anim->impl.getAnimationInstanceHandle(), animLoaded->impl.getAnimationInstanceHandle()); - EXPECT_EQ(anim->impl.getAnimationHandle(), animLoaded->impl.getAnimationHandle()); - } + ramses_internal::File outFile("someTemporaryFileWithOffset.ram"); + EXPECT_TRUE(outFile.open(ramses_internal::File::Mode::WriteOverWriteOldBinary)); - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteAnimationSequence) - { - SplineStepFloat* spline = this->animationSystem.createSplineStepFloat("spline"); - Node* node = this->m_scene.createNode(); - AnimatedProperty* prop = this->animationSystem.createAnimatedProperty(*node, EAnimatedProperty_Translation, EAnimatedPropertyComponent_X); - Animation* anim = this->animationSystem.createAnimation(*prop, *spline, "animation"); - AnimationSequence* seq = this->animationSystem.createAnimationSequence("seq"); - seq->addAnimation(*anim, 50u, 100u); - seq->setPlaybackSpeed(2.0f); - seq->setAnimationLooping(*anim, 50); - seq->setAnimationAbsolute(*anim); + uint32_t zeroData = 0; + EXPECT_TRUE(outFile.write(&zeroData, sizeof(zeroData))); + EXPECT_TRUE(outFile.write(data.data(), data.size())); + EXPECT_TRUE(outFile.write(&zeroData, sizeof(zeroData))); + } + const int fd = ramses_internal::FileDescriptorHelper::OpenFileDescriptorBinary("someTemporaryFileWithOffset.ram"); - doWriteReadCycle(); + EFeatureLevel featureLevel = EFeatureLevel_01; + EXPECT_TRUE(RamsesClient::GetFeatureLevelFromFile(fd, 4u, fileSize, featureLevel)); + EXPECT_EQ(EFeatureLevel_Latest, featureLevel); + } - const AnimationSequence* seqLoaded = this->getAnimationObjectForTesting("seq"); - EXPECT_EQ(seq->getNumberOfAnimations(), seqLoaded->getNumberOfAnimations()); - EXPECT_EQ(seq->getPlaybackSpeed(), seqLoaded->getPlaybackSpeed()); - EXPECT_EQ(seq->isAnimationLooping(*anim), seqLoaded->isAnimationLooping(*anim)); - EXPECT_EQ(seq->getAnimationLoopDuration(*anim), seqLoaded->getAnimationLoopDuration(*anim)); - EXPECT_EQ(seq->isAnimationRelative(*anim), seqLoaded->isAnimationRelative(*anim)); - EXPECT_EQ(25u, seq->getAnimationStartTimeInSequence(*anim)); - EXPECT_EQ(50u, seq->getAnimationStopTimeInSequence(*anim)); + TEST_F(ASceneLoadedFromFile, failsToGetFeatureLevelFromInvalidFileDescriptor) + { + EFeatureLevel featureLevel = EFeatureLevel_01; + EXPECT_FALSE(RamsesClient::GetFeatureLevelFromFile(-1, 0u, 10u, featureLevel)); + EXPECT_FALSE(RamsesClient::GetFeatureLevelFromFile(1, 0u, 0u, featureLevel)); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteTransformDataSlot) + TEST_F(ASceneLoadedFromFile, canReadWriteTransformDataSlot) { Node* node = this->m_scene.createNode("node"); - EXPECT_EQ(StatusOK, this->m_scene.impl.createTransformationDataConsumer(*node, dataConsumerId_t(2u))); - ASSERT_EQ(1u, this->m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(StatusOK, this->m_scene.m_impl.createTransformationDataConsumer(*node, dataConsumerId_t(2u))); + ASSERT_EQ(1u, this->m_scene.m_impl.getIScene().getDataSlotCount()); ramses_internal::DataSlotHandle slotHandle(0u); - EXPECT_TRUE(this->m_scene.impl.getIScene().isDataSlotAllocated(slotHandle)); + EXPECT_TRUE(this->m_scene.m_impl.getIScene().isDataSlotAllocated(slotHandle)); doWriteReadCycle(); const Node* nodeLoaded = this->getObjectForTesting("node"); - ramses_internal::NodeHandle nodeHandle = nodeLoaded->impl.getNodeHandle(); - ASSERT_EQ(1u, this->m_sceneLoaded->impl.getIScene().getDataSlotCount()); + ramses_internal::NodeHandle nodeHandle = nodeLoaded->m_impl.getNodeHandle(); + ASSERT_EQ(1u, this->m_sceneLoaded->m_impl.getIScene().getDataSlotCount()); - EXPECT_TRUE(this->m_sceneLoaded->impl.getIScene().isDataSlotAllocated(slotHandle)); - EXPECT_EQ(nodeHandle, this->m_sceneLoaded->impl.getIScene().getDataSlot(slotHandle).attachedNode); - EXPECT_EQ(ramses_internal::DataSlotId(2u), this->m_sceneLoaded->impl.getIScene().getDataSlot(slotHandle).id); - EXPECT_EQ(ramses_internal::EDataSlotType_TransformationConsumer, this->m_sceneLoaded->impl.getIScene().getDataSlot(slotHandle).type); + EXPECT_TRUE(this->m_sceneLoaded->m_impl.getIScene().isDataSlotAllocated(slotHandle)); + EXPECT_EQ(nodeHandle, this->m_sceneLoaded->m_impl.getIScene().getDataSlot(slotHandle).attachedNode); + EXPECT_EQ(ramses_internal::DataSlotId(2u), this->m_sceneLoaded->m_impl.getIScene().getDataSlot(slotHandle).id); + EXPECT_EQ(ramses_internal::EDataSlotType_TransformationConsumer, this->m_sceneLoaded->m_impl.getIScene().getDataSlot(slotHandle).type); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteDataFloat) + TEST_F(ASceneLoadedFromFile, canReadWriteDataObject) { float setValue = 5.0f; - DataFloat* data = this->m_scene.createDataFloat("floatData"); + auto data = this->m_scene.createDataObject(EDataType::Float, "floatData"); EXPECT_EQ(StatusOK, data->setValue(setValue)); doWriteReadCycle(); - const DataFloat* loadedData = this->getObjectForTesting("floatData"); + const auto loadedData = this->getObjectForTesting("floatData"); ASSERT_TRUE(loadedData); float loadedValue = 0.0f; + EXPECT_EQ(EDataType::Float, loadedData->getDataType()); EXPECT_EQ(StatusOK, loadedData->getValue(loadedValue)); EXPECT_EQ(setValue, loadedValue); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteDataVector2f) - { - float setValue[] = { 1.0f, 2.0f }; - DataVector2f* data = this->m_scene.createDataVector2f("vec2fData"); - - EXPECT_EQ(StatusOK, data->setValue(setValue[0], setValue[1])); - - doWriteReadCycle(); - - const DataVector2f* loadedData = this->getObjectForTesting("vec2fData"); - ASSERT_TRUE(loadedData); - float loadedValue[] = { 0.0f, 0.0f }; - EXPECT_EQ(StatusOK, loadedData->getValue(loadedValue[0], loadedValue[1])); - - for (uint32_t i = 0; i < 2; i++) - { - EXPECT_EQ(setValue[i], loadedValue[i]); - } - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteDataVector3f) - { - float setValue[] = { 1.0f, 2.0f, 3.0f }; - DataVector3f* data = this->m_scene.createDataVector3f("vec3fData"); - - EXPECT_EQ(StatusOK, data->setValue(setValue[0], setValue[1], setValue[2])); - - doWriteReadCycle(); - - const DataVector3f* loadedData = this->getObjectForTesting("vec3fData"); - ASSERT_TRUE(loadedData); - float loadedValue[] = { 0.0f, 0.0f, 0.0f }; - EXPECT_EQ(StatusOK, loadedData->getValue(loadedValue[0], loadedValue[1], loadedValue[2])); - - for (uint32_t i = 0; i < 3; i++) - { - EXPECT_EQ(setValue[i], loadedValue[i]); - } - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteDataVector4f) - { - float setValue[] = { 1.0f, 2.0f, 3.0f, 4.0f }; - DataVector4f* data = this->m_scene.createDataVector4f("vec4fData"); - - EXPECT_EQ(StatusOK, data->setValue(setValue[0], setValue[1], setValue[2], setValue[3])); - - doWriteReadCycle(); - - const DataVector4f* loadedData = this->getObjectForTesting("vec4fData"); - ASSERT_TRUE(loadedData); - float loadedValue[] = { 0.0f, 0.0f, 0.0f, 0.0f }; - EXPECT_EQ(StatusOK, loadedData->getValue(loadedValue[0], loadedValue[1], loadedValue[2], loadedValue[3])); - for (uint32_t i = 0; i < 4; i++) - { - EXPECT_EQ(setValue[i], loadedValue[i]); - } - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteDataMatrix22f) - { - const float setValue[] = { 1.0f, 2.0f, 3.0f, 4.0f }; - DataMatrix22f* data = this->m_scene.createDataMatrix22f("matrix22Data"); - - EXPECT_EQ(StatusOK, data->setValue(setValue)); - - doWriteReadCycle(); - - const DataMatrix22f* loadedData = this->getObjectForTesting("matrix22Data"); - ASSERT_TRUE(loadedData); - float loadedValue[] = { 0.0f, 0.0f, 0.0f, 0.0f }; - EXPECT_EQ(StatusOK, loadedData->getValue(loadedValue)); - for (uint32_t i = 0; i < 4u; i++) - { - EXPECT_EQ(setValue[i], loadedValue[i]); - } - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteDataMatrix33f) - { - const float setValue[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f }; - DataMatrix33f* data = this->m_scene.createDataMatrix33f("matrix33Data"); - - EXPECT_EQ(StatusOK, data->setValue(setValue)); - - doWriteReadCycle(); - - const DataMatrix33f* loadedData = this->getObjectForTesting("matrix33Data"); - ASSERT_TRUE(loadedData); - float loadedValue[] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; - EXPECT_EQ(StatusOK, loadedData->getValue(loadedValue)); - for (uint32_t i = 0; i < 9u; i++) - { - EXPECT_EQ(setValue[i], loadedValue[i]); - } - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteDataMatrix44f) - { - float setValue[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f, 16.0f }; - DataMatrix44f* data = this->m_scene.createDataMatrix44f("matrix44Data"); - - EXPECT_EQ(StatusOK, data->setValue(setValue)); - - doWriteReadCycle(); - - const DataMatrix44f* loadedData = this->getObjectForTesting("matrix44Data"); - ASSERT_TRUE(loadedData); - float loadedValue[] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, }; - EXPECT_EQ(StatusOK, loadedData->getValue(loadedValue)); - for (uint32_t i = 0; i < 16; i++) - { - EXPECT_EQ(setValue[i], loadedValue[i]); - } - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteDataInt32) - { - int32_t setValue = 5; - DataInt32* data = this->m_scene.createDataInt32("int32Data"); - - EXPECT_EQ(StatusOK, data->setValue(setValue)); - - doWriteReadCycle(); - - const DataInt32* loadedData = this->getObjectForTesting("int32Data"); - ASSERT_TRUE(loadedData); - int32_t loadedValue = 0; - EXPECT_EQ(StatusOK, loadedData->getValue(loadedValue)); - EXPECT_EQ(setValue, loadedValue); - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteDataVector2i) - { - int32_t setValue[] = { 1, 2 }; - DataVector2i* data = this->m_scene.createDataVector2i("vec2iData"); - - EXPECT_EQ(StatusOK, data->setValue(setValue[0], setValue[1])); - - doWriteReadCycle(); - - const DataVector2i* loadedData = this->getObjectForTesting("vec2iData"); - ASSERT_TRUE(loadedData); - int32_t loadedValue[] = { 0, 0 }; - EXPECT_EQ(StatusOK, loadedData->getValue(loadedValue[0], loadedValue[1])); - - for (uint32_t i = 0; i < 2; i++) - { - EXPECT_EQ(setValue[i], loadedValue[i]); - } - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteDataVector3i) - { - int32_t setValue[] = { 1, 2, 3 }; - DataVector3i* data = this->m_scene.createDataVector3i("vec3iData"); - - EXPECT_EQ(StatusOK, data->setValue(setValue[0], setValue[1], setValue[2])); - - doWriteReadCycle(); - - const DataVector3i* loadedData = this->getObjectForTesting("vec3iData"); - ASSERT_TRUE(loadedData); - int32_t loadedValue[] = { 0, 0, 0 }; - EXPECT_EQ(StatusOK, loadedData->getValue(loadedValue[0], loadedValue[1], loadedValue[2])); - - for (uint32_t i = 0; i < 3; i++) - { - EXPECT_EQ(setValue[i], loadedValue[i]); - } - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteDataVector4i) - { - int32_t setValue[] = { 1, 2, 3, 4 }; - DataVector4i* data = this->m_scene.createDataVector4i("vec4iData"); - - EXPECT_EQ(StatusOK, data->setValue(setValue[0], setValue[1], setValue[2], setValue[3])); - - doWriteReadCycle(); - - const DataVector4i* loadedData = this->getObjectForTesting("vec4iData"); - ASSERT_TRUE(loadedData); - int32_t loadedValue[] = { 0, 0, 0, 0 }; - EXPECT_EQ(StatusOK, loadedData->getValue(loadedValue[0], loadedValue[1], loadedValue[2], loadedValue[3])); - for (uint32_t i = 0; i < 4; i++) - { - EXPECT_EQ(setValue[i], loadedValue[i]); - } - } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, canReadWriteSceneReferences) + TEST_F(ASceneLoadedFromFile, canReadWriteSceneReferences) { constexpr ramses::sceneId_t referencedSceneId(444); auto sr1 = this->m_scene.createSceneReference(referencedSceneId, "scene ref"); @@ -1660,16 +1477,16 @@ namespace ramses EXPECT_EQ(ramses::RendererSceneState::Rendered, loadedSceneRef2->getRequestedState()); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, savesLLResourceOnlyOnceIfTwoHLResourcesReferToIt) + TEST_F(ASceneLoadedFromFile, savesLLResourceOnlyOnceIfTwoHLResourcesReferToIt) { std::vector inds(300); std::iota(inds.begin(), inds.end(), static_cast(0u)); - this->m_scene.createArrayResource(EDataType::UInt16, 300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices"); - this->m_scene.createArrayResource(EDataType::UInt16, 300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices"); - this->m_scene.createArrayResource(EDataType::UInt16, 300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices"); - this->m_scene.createArrayResource(EDataType::UInt16, 300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices2"); - this->m_scene.createArrayResource(EDataType::UInt16, 300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices2"); - this->m_scene.createArrayResource(EDataType::UInt16, 300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices2"); + this->m_scene.createArrayResource(300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices"); + this->m_scene.createArrayResource(300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices"); + this->m_scene.createArrayResource(300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices"); + this->m_scene.createArrayResource(300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices2"); + this->m_scene.createArrayResource(300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices2"); + this->m_scene.createArrayResource(300u, inds.data(), ramses::ResourceCacheFlag_DoNotCache, "indices2"); doWriteReadCycle(); std::ifstream in("someTemporaryFile.ram", std::ifstream::ate | std::ifstream::binary); @@ -1680,14 +1497,14 @@ namespace ramses template struct TestHelper { - static T* create(ASceneAndAnimationSystemLoadedFromFileTemplated* fixture, ramses::RamsesClient&, ramses::Scene&) + static T* create(ASceneLoadedFromFileTemplated* fixture, ramses::RamsesClient&, ramses::Scene&) { return &fixture->template createObject("a node"); } }; - TYPED_TEST_SUITE(ASceneAndAnimationSystemLoadedFromFileTemplated, NodeTypes); - TYPED_TEST(ASceneAndAnimationSystemLoadedFromFileTemplated, canReadWriteAllNodes) + TYPED_TEST_SUITE(ASceneLoadedFromFileTemplated, NodeTypes); + TYPED_TEST(ASceneLoadedFromFileTemplated, canReadWriteAllNodes) { auto node = TestHelper::create(this, this->client, this->m_scene); @@ -1696,9 +1513,9 @@ namespace ramses auto child = &this->template createObject("child"); auto parent = &this->template createObject("parent"); - node->setTranslation(1, 2, 3); - node->setRotation(4, 5, 6); - node->setScaling(7, 8, 9); + node->setTranslation({1, 2, 3}); + node->setRotation({4, 5, 6}, ERotationType::Euler_XZX); + node->setScaling({7, 8, 9}); node->addChild(*child); node->setParent(*parent); @@ -1717,63 +1534,61 @@ namespace ramses ASSERT_EQ(1u, loadedSuperNode->getChildCount()); EXPECT_EQ(loadedChild, loadedSuperNode->getChild(0u)); EXPECT_EQ(loadedParent, loadedSuperNode->getParent()); - float x = 0; - float y = 0; - float z = 0; - EXPECT_EQ(StatusOK, loadedSuperNode->getTranslation(x, y, z)); - EXPECT_FLOAT_EQ(1, x); - EXPECT_FLOAT_EQ(2, y); - EXPECT_FLOAT_EQ(3, z); - EXPECT_EQ(StatusOK, loadedSuperNode->getRotation(x, y, z)); - EXPECT_FLOAT_EQ(4, x); - EXPECT_FLOAT_EQ(5, y); - EXPECT_FLOAT_EQ(6, z); - EXPECT_EQ(StatusOK, loadedSuperNode->getScaling(x, y, z)); - EXPECT_FLOAT_EQ(7, x); - EXPECT_FLOAT_EQ(8, y); - EXPECT_FLOAT_EQ(9, z); + vec3f value; + EXPECT_EQ(StatusOK, loadedSuperNode->getTranslation(value)); + EXPECT_FLOAT_EQ(1, value.x); + EXPECT_FLOAT_EQ(2, value.y); + EXPECT_FLOAT_EQ(3, value.z); + EXPECT_EQ(StatusOK, loadedSuperNode->getRotation(value)); + EXPECT_FLOAT_EQ(4, value.x); + EXPECT_FLOAT_EQ(5, value.y); + EXPECT_FLOAT_EQ(6, value.z); + EXPECT_EQ(ERotationType::Euler_XZX, loadedSuperNode->getRotationType()); + EXPECT_EQ(StatusOK, loadedSuperNode->getScaling(value)); + EXPECT_FLOAT_EQ(7, value.x); + EXPECT_FLOAT_EQ(8, value.y); + EXPECT_FLOAT_EQ(9, value.z); EXPECT_EQ(loadedSuperNode->getVisibility(), EVisibilityMode::Invisible); } - - TEST_F(ASceneAndAnimationSystemLoadedFromFile, compressedFileIsSmallerThanUncompressedWhenUsingSaveSceneToFile) + TEST_F(ASceneLoadedFromFile, compressedFileIsSmallerThanUncompressedWhenUsingSaveSceneToFile) { Scene* scene = client.createScene(sceneId_t(1)); const std::vector data(1000u, 0u); - EXPECT_TRUE(scene->createArrayResource(EDataType::UInt16, static_cast(data.size()), data.data())); + EXPECT_TRUE(scene->createArrayResource(static_cast(data.size()), data.data())); EXPECT_EQ(StatusOK, scene->saveToFile("testscene.ramscene", false)); ramses_internal::File file("testscene.ramscene"); EXPECT_TRUE(file.exists()); - ramses_internal::UInt uncompressedFileSize = 0; + size_t uncompressedFileSize = 0; EXPECT_TRUE(file.getSizeInBytes(uncompressedFileSize)); EXPECT_EQ(StatusOK, scene->saveToFile("testscene.ramscene", true)); ramses_internal::File file2("testscene.ramscene"); EXPECT_TRUE(file2.exists()); - ramses_internal::UInt compressedFileSize = 0; + size_t compressedFileSize = 0; EXPECT_TRUE(file2.getSizeInBytes(compressedFileSize)); EXPECT_GT(uncompressedFileSize, compressedFileSize); } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, savedFilesAreConsistent) + TEST_F(ASceneLoadedFromFile, savedFilesAreConsistent) { - for (ramses_internal::String name : { "ts1.ramscene", "ts2.ramscene", "ts3.ramscene", "ts4.ramscene", "ts5.ramscene", "ts6.ramscene" }) + for (const auto& name : { "ts1.ramscene", "ts2.ramscene", "ts3.ramscene", "ts4.ramscene", "ts5.ramscene", "ts6.ramscene" }) { - EXPECT_EQ(StatusOK, this->m_scene.saveToFile(name.c_str(), false)); + EXPECT_EQ(StatusOK, this->m_scene.saveToFile(name, false)); } - for (ramses_internal::String name : { "ts2.ramscene", "ts3.ramscene", "ts4.ramscene", "ts5.ramscene", "ts6.ramscene" }) + for (const auto& name : { "ts2.ramscene", "ts3.ramscene", "ts4.ramscene", "ts5.ramscene", "ts6.ramscene" }) { - EXPECT_TRUE(ClientTestUtils::CompareBinaryFiles("ts1.ramscene", name.c_str())); + EXPECT_TRUE(ClientTestUtils::CompareBinaryFiles("ts1.ramscene", name)); } } - TEST_F(ASceneAndAnimationSystemLoadedFromFile, closesSceneFileAndLowLevelResourceWhenDestroyed) + TEST_F(ASceneLoadedFromFile, closesSceneFileAndLowLevelResourceWhenDestroyed) { const status_t status = m_scene.saveToFile("someTemporaryFile.ram", false); EXPECT_EQ(StatusOK, status); @@ -1781,8 +1596,8 @@ namespace ramses m_sceneLoaded = m_clientForLoading.loadSceneFromFile("someTemporaryFile.ram", {}); ASSERT_TRUE(nullptr != m_sceneLoaded); - const ramses_internal::SceneFileHandle handle = m_sceneLoaded->impl.getSceneFileHandle(); - EXPECT_TRUE(m_clientForLoading.impl.getClientApplication().hasResourceFile(handle)); + const ramses_internal::SceneFileHandle handle = m_sceneLoaded->m_impl.getSceneFileHandle(); + EXPECT_TRUE(m_clientForLoading.m_impl.getClientApplication().hasResourceFile(handle)); m_clientForLoading.destroy(*m_sceneLoaded); // scene gets destroyed asynchronously, so we can't just test after the destroy @@ -1792,10 +1607,9 @@ namespace ramses for (; ticks > 0; --ticks) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); - if (!m_clientForLoading.impl.getClientApplication().hasResourceFile(handle)) + if (!m_clientForLoading.m_impl.getClientApplication().hasResourceFile(handle)) break; } EXPECT_GT(ticks, 0u); } - } diff --git a/client/ramses-client/test/ScenePersistationTest.h b/client/test/ScenePersistationTest.h similarity index 70% rename from client/ramses-client/test/ScenePersistationTest.h rename to client/test/ScenePersistationTest.h index 1167ddd0f..0b1fea252 100644 --- a/client/ramses-client/test/ScenePersistationTest.h +++ b/client/test/ScenePersistationTest.h @@ -15,28 +15,25 @@ #include "SceneImpl.h" #include "RamsesObjectTypeUtils.h" #include "RamsesClientImpl.h" +#include "RamsesFrameworkConfigImpl.h" + +#include namespace ramses { - class ASceneAndAnimationSystemLoadedFromFile : public LocalTestClientWithSceneAndAnimationSystem, public ::testing::Test + class ASceneLoadedFromFile : public LocalTestClientWithScene, public ::testing::Test { public: - explicit ASceneAndAnimationSystemLoadedFromFile(uint32_t animationSystemCreationFlags = EAnimationSystemFlags_Default) - : LocalTestClientWithSceneAndAnimationSystem(animationSystemCreationFlags) + ASceneLoadedFromFile() + : LocalTestClientWithScene() , m_clientForLoading(*m_frameworkForLoader.createClient("client")) , m_sceneLoaded(nullptr) - , m_animationSystemLoaded(nullptr) - { - m_frameworkForLoader.impl.getScenegraphComponent().setSceneRendererHandler(&sceneActionsCollector); - } - - ~ASceneAndAnimationSystemLoadedFromFile() { + m_frameworkForLoader.m_impl.getScenegraphComponent().setSceneRendererHandler(&sceneActionsCollector); } using ObjectTypeHistogram = ramses_internal::HashMap< ERamsesObjectType, uint32_t >; - void fillObjectTypeHistorgramFromObjectVector(ObjectTypeHistogram& counter, const RamsesObjectVector& objects) { for (const auto obj : objects) @@ -58,11 +55,11 @@ namespace ramses void fillObjectTypeHistogramFromScene( ObjectTypeHistogram& counter, const ramses::Scene& scene ) { RamsesObjectVector objects; - scene.impl.getObjectRegistry().getObjectsOfType(objects, ERamsesObjectType_SceneObject); + scene.m_impl.getObjectRegistry().getObjectsOfType(objects, ERamsesObjectType::SceneObject); fillObjectTypeHistorgramFromObjectVector( counter, objects ); } - ::testing::AssertionResult AssertHistogramEqual( const char* m_expr, const char* n_expr, const ObjectTypeHistogram& m, const ObjectTypeHistogram& n ) + ::testing::AssertionResult AssertHistogramEqual( std::string_view m_expr, std::string_view n_expr, const ObjectTypeHistogram& m, const ObjectTypeHistogram& n ) { bool isEqual = true; ::testing::AssertionResult wrongHistogramCount = ::testing::AssertionFailure(); @@ -133,67 +130,51 @@ namespace ramses // this can not be enabled for text serialization, because those are destroying/creating meshes if (expectSameSceneSizeInfo) { - ramses_internal::SceneSizeInformation origSceneSizeInfo = m_scene.impl.getIScene().getSceneSizeInformation(); - ramses_internal::SceneSizeInformation loadedSceneSizeInfo = m_sceneLoaded->impl.getIScene().getSceneSizeInformation(); + ramses_internal::SceneSizeInformation origSceneSizeInfo = m_scene.m_impl.getIScene().getSceneSizeInformation(); + ramses_internal::SceneSizeInformation loadedSceneSizeInfo = m_sceneLoaded->m_impl.getIScene().getSceneSizeInformation(); EXPECT_EQ(origSceneSizeInfo, loadedSceneSizeInfo); } - - m_animationSystemLoaded = RamsesUtils::TryConvert(*m_sceneLoaded->findObjectByName("animation system")); - ASSERT_TRUE(nullptr != m_animationSystemLoaded); } - template - T* getObjectForTesting(const char* name) + void saveSceneWithFeatureLevelToFile(EFeatureLevel featureLevel, std::string_view fileName) { - RamsesObject* objectPerName = this->m_sceneLoaded->findObjectByName(name); - EXPECT_TRUE(objectPerName != nullptr); - if (!objectPerName) - return nullptr; - EXPECT_STREQ(name, objectPerName->getName()); + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + config.m_impl.get().setFeatureLevelNoCheck(featureLevel); + RamsesFramework someFramework{ config }; + RamsesClient* someClient = someFramework.createClient("someClient"); - T* specificObject = RamsesUtils::TryConvert(*objectPerName); - EXPECT_TRUE(nullptr != specificObject); - return specificObject; + ramses::Scene* scene = someClient->createScene(sceneId_t{ 123u }); + const status_t status = scene->saveToFile(fileName, false); + EXPECT_EQ(StatusOK, status); } template - T* getAnimationObjectForTesting(const char* name) + T* getObjectForTesting(std::string_view name) { - RamsesObject* objectPerName = this->m_animationSystemLoaded->findObjectByName(name); + RamsesObject* objectPerName = this->m_sceneLoaded->findObjectByName(name); EXPECT_TRUE(objectPerName != nullptr); if (!objectPerName) return nullptr; - EXPECT_STREQ(name, objectPerName->getName()); + EXPECT_STREQ(std::string{name}.c_str(), objectPerName->getName()); T* specificObject = RamsesUtils::TryConvert(*objectPerName); EXPECT_TRUE(nullptr != specificObject); return specificObject; } - ramses::RamsesFramework m_frameworkForLoader; + ramses::RamsesFramework m_frameworkForLoader{ RamsesFrameworkConfig{EFeatureLevel_Latest} }; ramses::RamsesClient& m_clientForLoading; ramses::Scene* m_sceneLoaded; - ramses::AnimationSystem* m_animationSystemLoaded; }; - class ASceneAndAnimationSystemLoadedFromFileWithDefaultRenderPass : public ASceneAndAnimationSystemLoadedFromFile + class ASceneLoadedFromFileWithDefaultRenderPass : public ASceneLoadedFromFile { - public: - ASceneAndAnimationSystemLoadedFromFileWithDefaultRenderPass() - : ASceneAndAnimationSystemLoadedFromFile(0) - { - } }; template - class ASceneAndAnimationSystemLoadedFromFileTemplated : public ASceneAndAnimationSystemLoadedFromFile + class ASceneLoadedFromFileTemplated : public ASceneLoadedFromFile { - public: - ASceneAndAnimationSystemLoadedFromFileTemplated() - : ASceneAndAnimationSystemLoadedFromFile(0) - { - } }; } diff --git a/client/ramses-client/test/ScenePersistationThreadedTest.cpp b/client/test/ScenePersistationThreadedTest.cpp similarity index 66% rename from client/ramses-client/test/ScenePersistationThreadedTest.cpp rename to client/test/ScenePersistationThreadedTest.cpp index 92d5891bb..961896eff 100644 --- a/client/ramses-client/test/ScenePersistationThreadedTest.cpp +++ b/client/test/ScenePersistationThreadedTest.cpp @@ -18,17 +18,17 @@ #include "ramses-client-api/RenderGroup.h" #include "ramses-client-api/EffectDescription.h" #include "ramses-client-api/ArrayResource.h" -#include "ramses-client-api/StreamTexture.h" #include "ramses-client-api/Texture2D.h" #include "ramses-client-api/Effect.h" #include "ramses-utils.h" -#include "ramses-hmi-utils.h" #include "ClientEventHandlerMock.h" #include "TestEffects.h" #include "PlatformAbstraction/PlatformThread.h" #include "Utils/File.h" +#include + namespace ramses { using namespace testing; @@ -39,27 +39,15 @@ namespace ramses class RamsesFileCreator { public: - static void SaveSceneToFile(sceneId_t sceneId, const ramses_internal::String& sceneFilename) + static void SaveSceneToFile(sceneId_t sceneId, std::string_view sceneFilename) { - RamsesFramework framework; + RamsesFramework framework{ RamsesFrameworkConfig{EFeatureLevel_Latest} }; RamsesClient& client(*framework.createClient("RamsesFileCreator")); Scene* scene = CreateScene(client, sceneId); ASSERT_TRUE(scene != nullptr); - ASSERT_EQ(StatusOK, scene->saveToFile(sceneFilename.c_str(), false)); - } - - static void SaveResourcesOfSceneToFile(sceneId_t sceneId, const ramses_internal::String& resourceFilename) - { - RamsesFramework framework; - RamsesClient& client(*framework.createClient("RamsesFileCreator")); - - Scene* scene = CreateScene(client, sceneId); - - ASSERT_TRUE(scene != nullptr); - - ASSERT_TRUE(RamsesHMIUtils::SaveResourcesOfSceneToResourceFile(*scene, resourceFilename.c_str(), false)); + ASSERT_EQ(StatusOK, scene->saveToFile(sceneFilename, false)); } private: @@ -74,19 +62,13 @@ namespace ramses RenderGroup* renderGroup = scene->createRenderGroup("a rendergroup"); renderGroup->addMeshNode(*scene->createMeshNode(), 3); - // stream texture - uint8_t data[4] = { 0u }; - MipLevelData mipLevelData(sizeof(data), data); - Texture2D* fallbackTexture = scene->createTexture2D(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, "fallbackTexture"); - scene->createStreamTexture(*fallbackTexture, waylandIviSurfaceId_t(3), "resourceName"); - //appearance Effect* effect = TestEffects::CreateTestEffect(*scene); scene->createAppearance(*effect, "appearance"); // geometry binding static const uint16_t inds[3] = { 0, 1, 2 }; - ArrayResource* const indices = scene->createArrayResource(EDataType::UInt16, 3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); + ArrayResource* const indices = scene->createArrayResource(3u, inds, ramses::ResourceCacheFlag_DoNotCache, "indices"); GeometryBinding* geometry = scene->createGeometryBinding(*effect, "geometry"); assert(geometry != nullptr); @@ -102,18 +84,12 @@ namespace ramses static const char* sceneFile = "ARamsesFileLoadedInSeveralThread_1.ramscene"; static const char* otherSceneFile = "ARamsesFileLoadedInSeveralThread_2.ramscene"; -// static const char* nonexistingSceneFile = "this_file_should_really_not_exist.ramscene"; - - static const char* resFile = "ARamsesFileLoadedInSeveralThread_1.ramres"; - static const char* otherResFile = "ARamsesFileLoadedInSeveralThread_2.ramres"; - //static const char* nonexistingResFile = "this_file_should_really_not_exist.ramres"; class ARamsesFileLoadedInSeveralThread : public ::testing::Test { public: ARamsesFileLoadedInSeveralThread() - : framework() - , client(*framework.createClient("ARamsesFileLoadedInSeveralThread")) + : client(*framework.createClient("ARamsesFileLoadedInSeveralThread")) , numClientEvents(0) , loadedScene(nullptr) { @@ -126,10 +102,10 @@ namespace ramses ++numClientEvents; } - bool waitForNumClientEvents(int num, UInt32 timeoutMs = 60000) + bool waitForNumClientEvents(int num, uint32_t timeoutMs = 60000) { - const UInt32 sleepMs = 50; - for (UInt32 cnt = 0; cnt < (timeoutMs / sleepMs); ++cnt) + const uint32_t sleepMs = 50; + for (uint32_t cnt = 0; cnt < (timeoutMs / sleepMs); ++cnt) { client.dispatchEvents(eventHandler); if (numClientEvents >= num) @@ -141,7 +117,7 @@ namespace ramses return numClientEvents == num; } - RamsesFramework framework; + RamsesFramework framework{ RamsesFrameworkConfig{EFeatureLevel_Latest} }; RamsesClient& client; StrictMock eventHandler; int numClientEvents; @@ -151,15 +127,12 @@ namespace ramses { RamsesFileCreator::SaveSceneToFile(sceneId_t(123u), sceneFile); RamsesFileCreator::SaveSceneToFile(sceneId_t(124u), otherSceneFile); - RamsesFileCreator::SaveResourcesOfSceneToFile(sceneId_t(125u), resFile); } static void TearDownTestCase() { File(sceneFile).remove(); File(otherSceneFile).remove(); - File(resFile).remove(); - File(otherResFile).remove(); } }; @@ -173,18 +146,6 @@ namespace ramses EXPECT_EQ(sceneId_t(123u), loadedScene->getSceneId()); } - TEST_F(ARamsesFileLoadedInSeveralThread, canAsyncLoadSceneParallelToSynchronousResourceLoad) - { - EXPECT_CALL(eventHandler, sceneFileLoadSucceeded(StrEq(sceneFile), _)); - EXPECT_EQ(StatusOK, client.loadSceneFromFileAsync(sceneFile)); - EXPECT_TRUE(RamsesHMIUtils::GetResourceDataPoolForClient(client).addResourceDataFile(resFile)); - - ASSERT_TRUE(waitForNumClientEvents(1)); - - ASSERT_TRUE(loadedScene != nullptr); - EXPECT_EQ(sceneId_t(123u), loadedScene->getSceneId()); - } - TEST_F(ARamsesFileLoadedInSeveralThread, asyncLoadSceneFileWithoutEverCallingDispatchDoesNotLeakMemory) { EXPECT_EQ(StatusOK, client.loadSceneFromFileAsync(sceneFile)); diff --git a/client/ramses-client/test/SceneReferenceTest.cpp b/client/test/SceneReferenceTest.cpp similarity index 90% rename from client/ramses-client/test/SceneReferenceTest.cpp rename to client/test/SceneReferenceTest.cpp index b87a0ddc0..a29bcefa8 100644 --- a/client/ramses-client/test/SceneReferenceTest.cpp +++ b/client/test/SceneReferenceTest.cpp @@ -26,7 +26,7 @@ namespace ramses SceneReference* sceneReference = this->m_scene.createSceneReference(sceneId_t(444),"testSceneReference"); ASSERT_NE(nullptr, sceneReference); - const auto sceneRefHandle = sceneReference->impl.getSceneReferenceHandle(); + const auto sceneRefHandle = sceneReference->m_impl.getSceneReferenceHandle(); EXPECT_TRUE(sceneRefHandle.isValid()); ASSERT_TRUE(this->getInternalScene().isSceneReferenceAllocated(sceneRefHandle)); const auto& sceneRef = this->getInternalScene().getSceneReference(sceneRefHandle); @@ -80,7 +80,7 @@ namespace ramses sceneReference->requestState(RendererSceneState::Ready); ASSERT_EQ(RendererSceneState::Ready, sceneReference->getRequestedState()); - const auto sceneRefHandle = sceneReference->impl.getSceneReferenceHandle(); + const auto sceneRefHandle = sceneReference->m_impl.getSceneReferenceHandle(); ASSERT_TRUE(this->getInternalScene().isSceneReferenceAllocated(sceneRefHandle)); EXPECT_EQ(ramses_internal::RendererSceneState::Ready, this->getInternalScene().getSceneReference(sceneRefHandle).requestedState); } @@ -97,15 +97,15 @@ namespace ramses EXPECT_NE(StatusOK, m_scene.linkData(nullptr, dataProviderId_t(1), reference2, dataConsumerId_t(2))); EXPECT_NE(StatusOK, m_scene.linkData(reference2, dataProviderId_t(1), nullptr, dataConsumerId_t(2))); - reference1->impl.setReportedState(RendererSceneState::Available); + reference1->m_impl.setReportedState(RendererSceneState::Available); EXPECT_NE(StatusOK, m_scene.linkData(reference1, dataProviderId_t(1), reference2, dataConsumerId_t(2))); EXPECT_NE(StatusOK, m_scene.linkData(reference2, dataProviderId_t(1), reference1, dataConsumerId_t(2))); - reference1->impl.setReportedState(RendererSceneState::Ready); + reference1->m_impl.setReportedState(RendererSceneState::Ready); EXPECT_NE(StatusOK, m_scene.linkData(reference1, dataProviderId_t(1), reference2, dataConsumerId_t(2))); EXPECT_NE(StatusOK, m_scene.linkData(reference2, dataProviderId_t(1), reference1, dataConsumerId_t(2))); - reference1->impl.setReportedState(RendererSceneState::Rendered); + reference1->m_impl.setReportedState(RendererSceneState::Rendered); EXPECT_NE(StatusOK, m_scene.linkData(reference1, dataProviderId_t(1), reference2, dataConsumerId_t(2))); EXPECT_NE(StatusOK, m_scene.linkData(reference2, dataProviderId_t(1), reference1, dataConsumerId_t(2))); } @@ -114,8 +114,8 @@ namespace ramses { auto reference1 = m_scene.createSceneReference(sceneId_t(111)); auto reference2 = m_scene.createSceneReference(sceneId_t(222)); - reference1->impl.setReportedState(RendererSceneState::Ready); - reference2->impl.setReportedState(RendererSceneState::Rendered); + reference1->m_impl.setReportedState(RendererSceneState::Ready); + reference2->m_impl.setReportedState(RendererSceneState::Rendered); EXPECT_EQ(StatusOK, m_scene.linkData(reference1, dataProviderId_t(1), nullptr, dataConsumerId_t(2))); EXPECT_EQ(StatusOK, m_scene.linkData(nullptr, dataProviderId_t(1), reference1, dataConsumerId_t(2))); @@ -130,9 +130,9 @@ namespace ramses auto reference1 = m_scene.createSceneReference(sceneId_t(333)); - reference1->impl.setReportedState(RendererSceneState::Ready); - referenceFromOtherScene1->impl.setReportedState(RendererSceneState::Rendered); - referenceFromOtherScene2->impl.setReportedState(RendererSceneState::Rendered); + reference1->m_impl.setReportedState(RendererSceneState::Ready); + referenceFromOtherScene1->m_impl.setReportedState(RendererSceneState::Rendered); + referenceFromOtherScene2->m_impl.setReportedState(RendererSceneState::Rendered); EXPECT_NE(StatusOK, m_scene.linkData(nullptr, dataProviderId_t(1), nullptr, dataConsumerId_t(2))); EXPECT_NE(StatusOK, m_scene.linkData(reference1, dataProviderId_t(1), reference1, dataConsumerId_t(2))); @@ -162,7 +162,7 @@ namespace ramses constexpr sceneId_t sceneId{ 444 }; auto reference = m_scene.createSceneReference(sceneId); ASSERT_NE(nullptr, reference); - const auto sceneRefHandle = reference->impl.getSceneReferenceHandle(); + const auto sceneRefHandle = reference->m_impl.getSceneReferenceHandle(); ASSERT_TRUE(this->getInternalScene().isSceneReferenceAllocated(sceneRefHandle)); EXPECT_EQ(StatusOK, reference->requestNotificationsForSceneVersionTags(true)); @@ -180,7 +180,7 @@ namespace ramses EXPECT_EQ(StatusOK, reference->setRenderOrder(-13)); - const auto sceneRefHandle = reference->impl.getSceneReferenceHandle(); + const auto sceneRefHandle = reference->m_impl.getSceneReferenceHandle(); ASSERT_TRUE(this->getInternalScene().isSceneReferenceAllocated(sceneRefHandle)); EXPECT_EQ(-13, this->getInternalScene().getSceneReference(sceneRefHandle).renderOrder); } @@ -195,10 +195,10 @@ namespace ramses auto reference2 = m_scene.createSceneReference(sceneId2); ASSERT_NE(nullptr, reference1); ASSERT_NE(nullptr, reference2); - const auto sceneRefHandle1 = reference1->impl.getSceneReferenceHandle(); - const auto sceneRefHandle2 = reference2->impl.getSceneReferenceHandle(); - reference1->impl.setReportedState(RendererSceneState::Ready); - reference2->impl.setReportedState(RendererSceneState::Ready); + const auto sceneRefHandle1 = reference1->m_impl.getSceneReferenceHandle(); + const auto sceneRefHandle2 = reference2->m_impl.getSceneReferenceHandle(); + reference1->m_impl.setReportedState(RendererSceneState::Ready); + reference2->m_impl.setReportedState(RendererSceneState::Ready); EXPECT_EQ(StatusOK, m_scene.linkData(reference1, providerId, reference2, consumerId)); EXPECT_EQ(StatusOK, m_scene.linkData(reference1, providerId, nullptr, consumerId)); @@ -232,7 +232,7 @@ namespace ramses constexpr dataConsumerId_t consumerId{ 13 }; auto reference = m_scene.createSceneReference(sceneId); ASSERT_NE(nullptr, reference); - const auto sceneRefHandle = reference->impl.getSceneReferenceHandle(); + const auto sceneRefHandle = reference->m_impl.getSceneReferenceHandle(); EXPECT_EQ(StatusOK, m_scene.unlinkData(reference, consumerId)); EXPECT_EQ(StatusOK, m_scene.unlinkData(nullptr, consumerId)); diff --git a/client/ramses-client/test/SceneTest.cpp b/client/test/SceneTest.cpp similarity index 71% rename from client/ramses-client/test/SceneTest.cpp rename to client/test/SceneTest.cpp index 5d557879b..fbb175c12 100644 --- a/client/ramses-client/test/SceneTest.cpp +++ b/client/test/SceneTest.cpp @@ -13,14 +13,13 @@ #include "ramses-client-api/RenderPass.h" #include "ramses-client-api/BlitPass.h" #include "ramses-client-api/MeshNode.h" -#include "ramses-client-api/DataFloat.h" +#include "ramses-client-api/DataObject.h" #include "ramses-client-api/TextureSampler.h" #include "ramses-client-api/TextureSamplerMS.h" #include "ramses-client-api/TextureSamplerExternal.h" #include "ramses-client-api/Texture2D.h" #include "ramses-client-api/Texture3D.h" -#include "ramses-client-api/StreamTexture.h" -#include "ramses-client-api/EDataType.h" +#include "ramses-framework-api/EDataType.h" #include "ramses-client-api/ArrayBuffer.h" #include "ramses-utils.h" @@ -31,7 +30,7 @@ #include "BlitPassImpl.h" #include "TextureSamplerImpl.h" #include "Texture2DImpl.h" -#include "StreamTextureImpl.h" +#include "SceneConfigImpl.h" #include "ClientTestUtils.h" #include "SimpleSceneTopology.h" #include "Components/FlushTimeInformation.h" @@ -57,8 +56,9 @@ namespace ramses TEST(SceneOjects, canGiveSceneID) { - RamsesFramework framework; - RamsesClient& client(*framework.createClient(nullptr)); + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework framework{config}; + RamsesClient& client(*framework.createClient({})); Scene* sceneA = client.createScene(sceneId_t(1u)); Scene* sceneB = client.createScene(sceneId_t(2u)); @@ -72,6 +72,52 @@ namespace ramses EXPECT_EQ(sceneA, client.getScene(fromSceneA->getSceneId())); } + TEST(ASceneConfig, CanBeCopyAndMoveConstructed) + { + SceneConfig config; + config.setPublicationMode(EScenePublicationMode::LocalAndRemote); + + SceneConfig configCopy{ config }; + EXPECT_EQ(EScenePublicationMode::LocalAndRemote, configCopy.m_impl.get().getPublicationMode()); + + SceneConfig configMove{ std::move(config) }; + EXPECT_EQ(EScenePublicationMode::LocalAndRemote, configMove.m_impl.get().getPublicationMode()); + } + + TEST(ASceneConfig, CanBeCopyAndMoveAssigned) + { + SceneConfig config; + config.setPublicationMode(EScenePublicationMode::LocalAndRemote); + + SceneConfig configCopy; + configCopy = config; + EXPECT_EQ(EScenePublicationMode::LocalAndRemote, configCopy.m_impl.get().getPublicationMode()); + + SceneConfig configMove; + configMove = std::move(config); + EXPECT_EQ(EScenePublicationMode::LocalAndRemote, configMove.m_impl.get().getPublicationMode()); + } + + TEST(ASceneConfig, CanBeSelfAssigned) + { + SceneConfig config; + config.setPublicationMode(EScenePublicationMode::LocalAndRemote); + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wself-move" +#pragma clang diagnostic ignored "-Wself-assign-overloaded" +#endif + config = config; + EXPECT_EQ(EScenePublicationMode::LocalAndRemote, config.m_impl.get().getPublicationMode()); + config = std::move(config); + // NOLINTNEXTLINE(bugprone-use-after-move) + EXPECT_EQ(EScenePublicationMode::LocalAndRemote, config.m_impl.get().getPublicationMode()); +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + } + class ASceneWithContent : public SimpleSceneTopology { }; @@ -103,66 +149,66 @@ namespace ramses TEST(DistributedSceneTest, givesErrorWhenRemotePublishingAfterEnablingLocalOnlyOptmisations) { - RamsesFramework framework(sizeof(clientArgs) / sizeof(char*), clientArgs); - RamsesClient& remoteClient(*framework.createClient(nullptr)); + RamsesFramework framework{ LocalTestClient::GetDefaultFrameworkConfig() }; + RamsesClient& remoteClient(*framework.createClient({})); framework.connect(); SceneConfig config; - config.setPublicationMode(EScenePublicationMode_LocalOnly); + config.setPublicationMode(EScenePublicationMode::LocalOnly); Scene* distributedScene = remoteClient.createScene(sceneId_t(1u), config); - EXPECT_NE(StatusOK, distributedScene->publish(EScenePublicationMode_LocalAndRemote)); + EXPECT_NE(StatusOK, distributedScene->publish(EScenePublicationMode::LocalAndRemote)); } TEST(DistributedSceneTest, canPublishRemotelyIfSetInConfig) { - RamsesFramework framework(sizeof(clientArgs) / sizeof(char*), clientArgs); - RamsesClient& remoteClient(*framework.createClient(nullptr)); + RamsesFramework framework{ LocalTestClient::GetDefaultFrameworkConfig() }; + RamsesClient& remoteClient(*framework.createClient({})); framework.connect(); SceneConfig config; - config.setPublicationMode(EScenePublicationMode_LocalAndRemote); + config.setPublicationMode(EScenePublicationMode::LocalAndRemote); Scene* distributedScene = remoteClient.createScene(sceneId_t(1u), config); - EXPECT_EQ(StatusOK, distributedScene->publish(EScenePublicationMode_LocalAndRemote)); + EXPECT_EQ(StatusOK, distributedScene->publish(EScenePublicationMode::LocalAndRemote)); } TEST(DistributedSceneTest, canPublishRemotelyIfNoSettingInConfig) { - RamsesFramework framework(sizeof(clientArgs) / sizeof(char*), clientArgs); - RamsesClient& remoteClient(*framework.createClient(nullptr)); + RamsesFramework framework{ LocalTestClient::GetDefaultFrameworkConfig() }; + RamsesClient& remoteClient(*framework.createClient({})); framework.connect(); SceneConfig config; Scene* distributedScene = remoteClient.createScene(sceneId_t(1u), config); - EXPECT_EQ(StatusOK, distributedScene->publish(EScenePublicationMode_LocalAndRemote)); + EXPECT_EQ(StatusOK, distributedScene->publish(EScenePublicationMode::LocalAndRemote)); } TEST(DistributedSceneTest, canPublishLocallyWhenEnablingLocalOnlyOptimisations) { - RamsesFramework framework(sizeof(clientArgs) / sizeof(char*), clientArgs); - RamsesClient& remoteClient(*framework.createClient(nullptr)); + RamsesFramework framework{ LocalTestClient::GetDefaultFrameworkConfig() }; + RamsesClient& remoteClient(*framework.createClient({})); framework.connect(); SceneConfig config; - config.setPublicationMode(EScenePublicationMode_LocalOnly); + config.setPublicationMode(EScenePublicationMode::LocalOnly); Scene* distributedScene = remoteClient.createScene(sceneId_t(1u), config); - EXPECT_EQ(StatusOK, distributedScene->publish(EScenePublicationMode_LocalOnly)); + EXPECT_EQ(StatusOK, distributedScene->publish(EScenePublicationMode::LocalOnly)); } TEST(DistributedSceneTest, isPublishedAfterDisconnect) { - RamsesFramework framework(sizeof(clientArgs) / sizeof(char*), clientArgs); - RamsesClient& remoteClient(*framework.createClient(nullptr)); + RamsesFramework framework{ LocalTestClient::GetDefaultFrameworkConfig() }; + RamsesClient& remoteClient(*framework.createClient({})); framework.connect(); SceneConfig config; Scene* distributedScene = remoteClient.createScene(sceneId_t(1u), config); - EXPECT_EQ(StatusOK, distributedScene->publish(EScenePublicationMode_LocalAndRemote)); + EXPECT_EQ(StatusOK, distributedScene->publish(EScenePublicationMode::LocalAndRemote)); framework.disconnect(); EXPECT_TRUE(distributedScene->isPublished()); framework.connect(); EXPECT_TRUE(distributedScene->isPublished()); - EXPECT_NE(StatusOK, distributedScene->publish(EScenePublicationMode_LocalAndRemote)); + EXPECT_NE(StatusOK, distributedScene->publish(EScenePublicationMode::LocalAndRemote)); } TEST(DistributedSceneTest, canPublishSceneIfNotConnected) { - RamsesFramework framework(sizeof(clientArgs) / sizeof(char*), clientArgs); - RamsesClient& remoteClient(*framework.createClient(nullptr)); + RamsesFramework framework{ LocalTestClient::GetDefaultFrameworkConfig() }; + RamsesClient& remoteClient(*framework.createClient({})); Scene* distributedScene = remoteClient.createScene(sceneId_t(1u)); EXPECT_EQ(StatusOK, distributedScene->publish()); EXPECT_TRUE(distributedScene->isPublished()); @@ -210,8 +256,8 @@ namespace ramses m_scene.destroy(*mesh); - EXPECT_TRUE(group->impl.getAllMeshes().empty()); - EXPECT_TRUE(group2->impl.getAllMeshes().empty()); + EXPECT_TRUE(group->m_impl.getAllMeshes().empty()); + EXPECT_TRUE(group2->m_impl.getAllMeshes().empty()); } TEST_F(AScene, failsToCreateAppearanceWhenEffectIsFromAnotherScene) @@ -243,16 +289,16 @@ namespace ramses Texture2D* texture = anotherScene.createTexture2D(ETextureFormat::RGB8, 1u, 1u, 1u, &mipData, false); ASSERT_TRUE(texture != nullptr); - TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); EXPECT_TRUE(nullptr == textureSampler); } { const uint8_t data[1 * 2 * 2 * 4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; MipLevelData mipLevelData(sizeof(data), data); - Texture3D* texture = anotherScene.createTexture3D(ETextureFormat::RGBA8, 2, 1, 2, 1, &mipLevelData, false, ramses::ResourceCacheFlag_DoNotCache, nullptr); + Texture3D* texture = anotherScene.createTexture3D(ETextureFormat::RGBA8, 2, 1, 2, 1, &mipLevelData, false, ramses::ResourceCacheFlag_DoNotCache, {}); ASSERT_TRUE(nullptr != texture); - TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); EXPECT_TRUE(nullptr == textureSampler); } { @@ -261,11 +307,11 @@ namespace ramses TextureCube* texture = anotherScene.createTextureCube(ETextureFormat::RGBA8, 10, 1, &mipLevelData, false); ASSERT_TRUE(nullptr != texture); - TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); EXPECT_TRUE(nullptr == textureSampler); } { - RenderBuffer* renderBuffer = anotherScene.createRenderBuffer(4u, 4u, ERenderBufferType_Color, ERenderBufferFormat_RGB8, ERenderBufferAccessMode_ReadWrite, 4u); + RenderBuffer* renderBuffer = anotherScene.createRenderBuffer(4u, 4u, ERenderBufferType::Color, ERenderBufferFormat::RGB8, ERenderBufferAccessMode::ReadWrite, 4u); ASSERT_TRUE(nullptr != renderBuffer); TextureSamplerMS* textureSampler = m_scene.createTextureSamplerMS(*renderBuffer, "sampler"); @@ -276,10 +322,10 @@ namespace ramses TEST_F(AScene, failsToCreateTextureSamplerWhenRenderTargetIsFromAnotherScene) { Scene& anotherScene = *client.createScene(sceneId_t(12u)); - RenderBuffer* renderBuffer = anotherScene.createRenderBuffer(100u, 100u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + RenderBuffer* renderBuffer = anotherScene.createRenderBuffer(100u, 100u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(nullptr != renderBuffer); - TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *renderBuffer); + TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *renderBuffer); EXPECT_TRUE(nullptr == textureSampler); client.destroy(anotherScene); @@ -288,7 +334,7 @@ namespace ramses TEST_F(AScene, failsToCreateTextureSamplerMSWhenRenderTargetIsFromAnotherScene) { Scene& anotherScene = *client.createScene(sceneId_t(12u)); - RenderBuffer* renderBuffer = anotherScene.createRenderBuffer(100u, 100u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + RenderBuffer* renderBuffer = anotherScene.createRenderBuffer(100u, 100u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(nullptr != renderBuffer); TextureSamplerMS* textureSampler = m_scene.createTextureSamplerMS(*renderBuffer, "sampler"); @@ -297,34 +343,18 @@ namespace ramses client.destroy(anotherScene); } - TEST_F(AScene, failsToCreateTextureSamplerWhenStreamTextureIsFromAnotherScene) - { - Scene& anotherScene = *client.createScene(sceneId_t(12u)); - uint8_t data[4] = { 0u }; - MipLevelData mipLevelData(sizeof(data), data); - const Texture2D& fallbackTexture = *anotherScene.createTexture2D(ETextureFormat::RGBA8, 1u, 1u, 1, &mipLevelData, false, {}, ResourceCacheFlag_DoNotCache, ""); - - StreamTexture* streamTexture = anotherScene.createStreamTexture(fallbackTexture, waylandIviSurfaceId_t(1), "testStreamTexture"); - ASSERT_TRUE(nullptr != streamTexture); - - TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *streamTexture); - EXPECT_TRUE(nullptr == textureSampler); - - client.destroy(anotherScene); - } - TEST_F(AScene, failsToCreateTextureSamplerWhenRenderBufferIsNotReadable) { - RenderBuffer* renderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_WriteOnly); + RenderBuffer* renderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::WriteOnly); ASSERT_TRUE(nullptr != renderBuffer); - TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *renderBuffer); + TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *renderBuffer); EXPECT_TRUE(nullptr == textureSampler); } TEST_F(AScene, failsToCreateTextureSamplerMSWhenRenderBufferIsNotReadable) { - RenderBuffer* renderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_WriteOnly); + RenderBuffer* renderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::WriteOnly); ASSERT_TRUE(nullptr != renderBuffer); TextureSamplerMS* textureSampler = m_scene.createTextureSamplerMS(*renderBuffer, "sampler"); @@ -360,27 +390,27 @@ namespace ramses m_vis1.setVisibility(EVisibilityMode::Invisible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh2a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh2b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh2a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh2b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); m_vis2.setVisibility(EVisibilityMode::Invisible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh2a.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh2b.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh2a.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh2b.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); m_vis1.setVisibility(EVisibilityMode::Off); m_vis2.setVisibility(EVisibilityMode::Visible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Off); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Off); - EXPECT_EQ(m_mesh2a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh2b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh2a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh2b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(ASceneWithContent, checksMeshVisibilityAddedToInvisibleParent) @@ -391,7 +421,7 @@ namespace ramses addMeshNode->setParent(m_vis2); m_scene.flush(); - EXPECT_EQ(addMeshNode->impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(addMeshNode->m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); //cleanup addMeshNode->removeParent(); @@ -405,7 +435,7 @@ namespace ramses addMeshNode->setParent(m_vis2); m_scene.flush(); - EXPECT_EQ(addMeshNode->impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(addMeshNode->m_impl.getFlattenedVisibility(), EVisibilityMode::Off); //cleanup addMeshNode->removeParent(); @@ -420,43 +450,43 @@ namespace ramses EXPECT_EQ(StatusOK, m_node9.setParent(m_node8)); //Newly added mesh should be visible by default - EXPECT_EQ(m_node9.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_node9.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); m_vis1.setVisibility(EVisibilityMode::Invisible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh2a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh2b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_node9.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh2a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh2b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_node9.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); m_node8.setVisibility(EVisibilityMode::Invisible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh2a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh2b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_node9.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh2a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh2b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_node9.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); m_vis1.setVisibility(EVisibilityMode::Visible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh2a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh2b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_node9.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh2a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh2b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_node9.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); m_node8.setVisibility(EVisibilityMode::Visible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh2a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh2b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_node9.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh2a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh2b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_node9.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(ASceneWithContent, subTreeStaysInvisibleIfSettingVisibilityNodeVisibleParentedByInvisibleNode) @@ -476,16 +506,16 @@ namespace ramses // should stay invisible m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); } TEST_F(ASceneWithContent, nodesStayVisibleWhenRemovedVisibleParent) { m_scene.destroy(m_vis1); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(ASceneWithContent, nodesBecomeVisibleWhenRemovedInvisibleParent) @@ -495,8 +525,8 @@ namespace ramses m_scene.destroy(m_vis1); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(ASceneWithContent, nodesBecomeVisibleWhenRemovedOffParent) @@ -506,8 +536,8 @@ namespace ramses m_scene.destroy(m_vis1); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(ASceneWithContent, visibleMeshBecomesInvisibleWhenMovedUnderInvisibleNode) @@ -517,7 +547,7 @@ namespace ramses m_mesh2a.setParent(m_vis1); m_scene.flush(); - EXPECT_EQ(m_mesh2a.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh2a.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); } TEST_F(ASceneWithContent, visibleMeshBecomesOffWhenMovedUnderOffNode) @@ -527,7 +557,7 @@ namespace ramses m_mesh2a.setParent(m_vis1); m_scene.flush(); - EXPECT_EQ(m_mesh2a.impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh2a.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); } TEST_F(ASceneWithContent, visibleMeshWithoutParentBecomesInvisibleWhenSetInvisibleNodeAsParent) @@ -539,7 +569,7 @@ namespace ramses meshOrphan.setParent(m_vis1); m_scene.flush(); - EXPECT_EQ(meshOrphan.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(meshOrphan.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); } TEST_F(ASceneWithContent, visibleMeshWithoutParentBecomesOffWhenSetOffNodeAsParent) @@ -551,7 +581,7 @@ namespace ramses meshOrphan.setParent(m_vis1); m_scene.flush(); - EXPECT_EQ(meshOrphan.impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(meshOrphan.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); } TEST_F(ASceneWithContent, visibleMeshWithoutParentBecomesInvisibleWhenMovedUnderInvisibleNode) @@ -563,7 +593,7 @@ namespace ramses m_vis1.addChild(meshOrphan); m_scene.flush(); - EXPECT_EQ(meshOrphan.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(meshOrphan.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); } TEST_F(ASceneWithContent, visibleMeshWithoutParentBecomesOffWhenMovedUnderOffNode) @@ -575,7 +605,7 @@ namespace ramses m_vis1.addChild(meshOrphan); m_scene.flush(); - EXPECT_EQ(meshOrphan.impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(meshOrphan.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); } TEST_F(ASceneWithContent, meshInInvisibleBranchBecomesVisibleWhenMovedUnderVisibleNode) @@ -585,7 +615,7 @@ namespace ramses m_mesh1a.setParent(m_vis2); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(ASceneWithContent, meshInOffBranchBecomesVisibleWhenMovedUnderVisibleNode) @@ -595,7 +625,7 @@ namespace ramses m_mesh1a.setParent(m_vis2); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(ASceneWithContent, nodesStayVisibleIfParentBecomesInvisibleAndIsDeletedInSameCommit) @@ -603,8 +633,8 @@ namespace ramses m_vis1.setVisibility(EVisibilityMode::Invisible); m_scene.destroy(m_vis1); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(ASceneWithContent, nodesStayVisibleIfParentBecomesOffAndIsDeletedInSameCommit) @@ -612,28 +642,28 @@ namespace ramses m_vis1.setVisibility(EVisibilityMode::Off); m_scene.destroy(m_vis1); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(ASceneWithContent, nodeBecomesVisibleIfInvisibleParentIsRemovedFromIt) { m_vis1.setVisibility(EVisibilityMode::Invisible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); m_mesh1a.removeParent(); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(ASceneWithContent, nodeBecomesVisibleIfOffParentIsRemovedFromIt) { m_vis1.setVisibility(EVisibilityMode::Off); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); m_mesh1a.removeParent(); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } TEST_F(ASceneWithContent, nodeBecomesInvisibleIfAnyOfItsAncestorsIsInvisible) @@ -648,20 +678,20 @@ namespace ramses // set parent invisible vis1a.setVisibility(EVisibilityMode::Invisible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); // revert vis1a.setVisibility(EVisibilityMode::Visible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); // set grandparent invisible m_vis1.setVisibility(EVisibilityMode::Invisible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); } TEST_F(ASceneWithContent, nodeBecomesOffIfAnyOfItsAncestorsIsOff) @@ -676,20 +706,20 @@ namespace ramses // set parent invisible vis1a.setVisibility(EVisibilityMode::Off); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Off); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); // revert vis1a.setVisibility(EVisibilityMode::Visible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); // set grandparent invisible m_vis1.setVisibility(EVisibilityMode::Off); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Off); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); } TEST_F(ASceneWithContent, nodeBecomesOffIfAnyOfItsAncestorsIsOffAndInvisible) @@ -705,20 +735,20 @@ namespace ramses vis1a.setVisibility(EVisibilityMode::Off); m_vis1.setVisibility(EVisibilityMode::Invisible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Off); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); // revert vis1a.setVisibility(EVisibilityMode::Invisible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); // set grandparent invisible m_vis1.setVisibility(EVisibilityMode::Off); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Off); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); } TEST_F(ASceneWithContent, offNodeStaysOffIndependentOfParentVisibility) @@ -731,35 +761,35 @@ namespace ramses m_mesh1b.setVisibility(EVisibilityMode::Invisible); m_vis1.setVisibility(EVisibilityMode::Visible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Off); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); // revert m_vis1.setVisibility(EVisibilityMode::Invisible); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Off); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Invisible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Invisible); // set grandparent invisible m_vis1.setVisibility(EVisibilityMode::Off); m_scene.flush(); - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Off); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Off); } TEST_F(AScene, canCreateATransformDataSlot) { Node* node = m_scene.createNode(); - EXPECT_EQ(0u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(0u, m_scene.m_impl.getIScene().getDataSlotCount()); EXPECT_EQ(StatusOK, m_scene.createTransformationDataConsumer(*node, dataConsumerId_t(666u))); - EXPECT_EQ(1u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(1u, m_scene.m_impl.getIScene().getDataSlotCount()); ramses_internal::DataSlotHandle slotHandle(0u); - ASSERT_TRUE(m_scene.impl.getIScene().isDataSlotAllocated(slotHandle)); - EXPECT_EQ(node->impl.getNodeHandle(), m_scene.impl.getIScene().getDataSlot(slotHandle).attachedNode); - EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.impl.getIScene().getDataSlot(slotHandle).id); - EXPECT_EQ(ramses_internal::EDataSlotType_TransformationConsumer, m_scene.impl.getIScene().getDataSlot(slotHandle).type); + ASSERT_TRUE(m_scene.m_impl.getIScene().isDataSlotAllocated(slotHandle)); + EXPECT_EQ(node->m_impl.getNodeHandle(), m_scene.m_impl.getIScene().getDataSlot(slotHandle).attachedNode); + EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.m_impl.getIScene().getDataSlot(slotHandle).id); + EXPECT_EQ(ramses_internal::EDataSlotType_TransformationConsumer, m_scene.m_impl.getIScene().getDataSlot(slotHandle).type); } TEST_F(AScene, removesTransformDataSlotsOfNodeOnDestruction) @@ -767,13 +797,13 @@ namespace ramses Node* node = m_scene.createNode(); m_scene.createTransformationDataProvider(*node, dataProviderId_t(1412u)); - EXPECT_EQ(1u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(1u, m_scene.m_impl.getIScene().getDataSlotCount()); ramses_internal::DataSlotHandle linkHandle(0u); - ASSERT_TRUE(m_scene.impl.getIScene().isDataSlotAllocated(linkHandle)); + ASSERT_TRUE(m_scene.m_impl.getIScene().isDataSlotAllocated(linkHandle)); m_scene.destroy(*node); - EXPECT_FALSE(m_scene.impl.getIScene().isDataSlotAllocated(linkHandle)); + EXPECT_FALSE(m_scene.m_impl.getIScene().isDataSlotAllocated(linkHandle)); } TEST_F(AScene, canNotCreateMoreThanOneTransformationDataSlotForANode) @@ -801,7 +831,7 @@ namespace ramses { Scene& anotherScene = *client.createScene(sceneId_t(12u)); - DataFloat* dataObject = anotherScene.createDataFloat(); + auto dataObject = anotherScene.createDataObject(EDataType::Float); ASSERT_TRUE(nullptr != dataObject); EXPECT_NE(StatusOK, m_scene.createDataProvider(*dataObject, dataProviderId_t{1u})); @@ -813,7 +843,7 @@ namespace ramses { Scene& anotherScene = *client.createScene(sceneId_t(12u)); - DataFloat* dataObject = anotherScene.createDataFloat(); + auto dataObject = anotherScene.createDataObject(EDataType::Float); ASSERT_TRUE(nullptr != dataObject); EXPECT_NE(StatusOK, m_scene.createDataConsumer(*dataObject, dataConsumerId_t{1u})); @@ -823,36 +853,36 @@ namespace ramses TEST_F(AScene, canCreateADataObjectDataSlot) { - DataFloat* dataObject = m_scene.createDataFloat(); - EXPECT_EQ(0u, m_scene.impl.getIScene().getDataSlotCount()); + auto dataObject = m_scene.createDataObject(EDataType::Float); + EXPECT_EQ(0u, m_scene.m_impl.getIScene().getDataSlotCount()); EXPECT_EQ(StatusOK, m_scene.createDataConsumer(*dataObject, dataConsumerId_t(666u))); - EXPECT_EQ(1u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(1u, m_scene.m_impl.getIScene().getDataSlotCount()); ramses_internal::DataSlotHandle slotHandle(0u); - ASSERT_TRUE(m_scene.impl.getIScene().isDataSlotAllocated(slotHandle)); - EXPECT_EQ(dataObject->impl.getDataReference(), m_scene.impl.getIScene().getDataSlot(slotHandle).attachedDataReference); - EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.impl.getIScene().getDataSlot(slotHandle).id); - EXPECT_EQ(ramses_internal::EDataSlotType_DataConsumer, m_scene.impl.getIScene().getDataSlot(slotHandle).type); + ASSERT_TRUE(m_scene.m_impl.getIScene().isDataSlotAllocated(slotHandle)); + EXPECT_EQ(dataObject->m_impl.getDataReference(), m_scene.m_impl.getIScene().getDataSlot(slotHandle).attachedDataReference); + EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.m_impl.getIScene().getDataSlot(slotHandle).id); + EXPECT_EQ(ramses_internal::EDataSlotType_DataConsumer, m_scene.m_impl.getIScene().getDataSlot(slotHandle).type); } TEST_F(AScene, removesDataSlotsOfDataObjectOnDestruction) { - DataFloat* dataObject = m_scene.createDataFloat(); + auto dataObject = m_scene.createDataObject(EDataType::Float); m_scene.createDataProvider(*dataObject, dataProviderId_t(1412u)); - EXPECT_EQ(1u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(1u, m_scene.m_impl.getIScene().getDataSlotCount()); ramses_internal::DataSlotHandle linkHandle(0u); - ASSERT_TRUE(m_scene.impl.getIScene().isDataSlotAllocated(linkHandle)); + ASSERT_TRUE(m_scene.m_impl.getIScene().isDataSlotAllocated(linkHandle)); m_scene.destroy(*dataObject); - EXPECT_FALSE(m_scene.impl.getIScene().isDataSlotAllocated(linkHandle)); + EXPECT_FALSE(m_scene.m_impl.getIScene().isDataSlotAllocated(linkHandle)); } TEST_F(AScene, canNotCreateMoreThanOneDataSlotForADataObject) { - DataFloat* dataObject = m_scene.createDataFloat(); + auto dataObject = m_scene.createDataObject(EDataType::Float); EXPECT_EQ(StatusOK, m_scene.createDataProvider(*dataObject, dataProviderId_t(1u))); EXPECT_NE(StatusOK, m_scene.createDataProvider(*dataObject, dataProviderId_t(2u))); EXPECT_NE(StatusOK, m_scene.createDataConsumer(*dataObject, dataConsumerId_t(3u))); @@ -860,8 +890,8 @@ namespace ramses TEST_F(AScene, canNotCreateMoreThanOneDataSlotWithTheSameId) { - DataFloat* dataObject1 = m_scene.createDataFloat(); - DataFloat* dataObject2 = m_scene.createDataFloat(); + auto dataObject1 = m_scene.createDataObject(EDataType::Float); + auto dataObject2 = m_scene.createDataObject(EDataType::Float); EXPECT_EQ(StatusOK, m_scene.createDataProvider(*dataObject1, dataProviderId_t(1u))); EXPECT_NE(StatusOK, m_scene.createDataProvider(*dataObject2, dataProviderId_t(1u))); EXPECT_NE(StatusOK, m_scene.createDataConsumer(*dataObject2, dataConsumerId_t(1u))); @@ -873,7 +903,8 @@ namespace ramses TEST_F(AScene, reportsErrorWhenCreateTextureProviderWithTextureFromAnotherScene) { - RamsesFramework anotherFramework; + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework anotherFramework{config}; Scene& anotherScene(*client.createScene(sceneId_t{ 0xf00 })); uint8_t data = 0u; MipLevelData mipData(1u, &data); @@ -891,7 +922,7 @@ namespace ramses Texture2D* texture = anotherScene.createTexture2D(ETextureFormat::R8, 1u, 1u, 1u, &mipData, false); ASSERT_TRUE(nullptr != texture); - const TextureSampler* sampler = anotherScene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + const TextureSampler* sampler = anotherScene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); ASSERT_TRUE(nullptr != sampler); EXPECT_NE(StatusOK, m_scene.createTextureConsumer(*sampler, dataConsumerId_t{1u})); @@ -902,7 +933,7 @@ namespace ramses TEST_F(AScene, reportsErrorWhenCreateTextureConsumerWithSamplerMSFromAnotherScene) { Scene& anotherScene = *client.createScene(sceneId_t(12u)); - RenderBuffer* renderBuffer = anotherScene.createRenderBuffer(4u, 4u, ERenderBufferType_Color, ERenderBufferFormat_RGB8, ERenderBufferAccessMode_ReadWrite, 4u); + RenderBuffer* renderBuffer = anotherScene.createRenderBuffer(4u, 4u, ERenderBufferType::Color, ERenderBufferFormat::RGB8, ERenderBufferAccessMode::ReadWrite, 4u); ASSERT_TRUE(nullptr != renderBuffer); const TextureSamplerMS* sampler = anotherScene.createTextureSamplerMS(*renderBuffer, "sampler"); @@ -916,7 +947,7 @@ namespace ramses TEST_F(AScene, reportsErrorWhenCreateTextureConsumerWithSamplerExternalFromAnotherScene) { Scene& anotherScene = *client.createScene(sceneId_t(12u)); - const TextureSamplerExternal* sampler = anotherScene.createTextureSamplerExternal(ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear); + const TextureSamplerExternal* sampler = anotherScene.createTextureSamplerExternal(ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear); ASSERT_TRUE(nullptr != sampler); EXPECT_NE(StatusOK, m_scene.createTextureConsumer(*sampler, dataConsumerId_t{ 1u })); @@ -926,7 +957,7 @@ namespace ramses TEST_F(AScene, canCreateATextureProviderDataSlot) { - EXPECT_EQ(0u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(0u, m_scene.m_impl.getIScene().getDataSlotCount()); uint8_t data = 0u; MipLevelData mipData(1u, &data); @@ -935,17 +966,17 @@ namespace ramses EXPECT_EQ(StatusOK, m_scene.createTextureProvider(*texture, dataProviderId_t{666u})); - EXPECT_EQ(1u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(1u, m_scene.m_impl.getIScene().getDataSlotCount()); ramses_internal::DataSlotHandle slotHandle(0u); - ASSERT_TRUE(m_scene.impl.getIScene().isDataSlotAllocated(slotHandle)); - EXPECT_EQ(texture->impl.getLowlevelResourceHash(), m_scene.impl.getIScene().getDataSlot(slotHandle).attachedTexture); - EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.impl.getIScene().getDataSlot(slotHandle).id); - EXPECT_EQ(ramses_internal::EDataSlotType_TextureProvider, m_scene.impl.getIScene().getDataSlot(slotHandle).type); + ASSERT_TRUE(m_scene.m_impl.getIScene().isDataSlotAllocated(slotHandle)); + EXPECT_EQ(texture->m_impl.getLowlevelResourceHash(), m_scene.m_impl.getIScene().getDataSlot(slotHandle).attachedTexture); + EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.m_impl.getIScene().getDataSlot(slotHandle).id); + EXPECT_EQ(ramses_internal::EDataSlotType_TextureProvider, m_scene.m_impl.getIScene().getDataSlot(slotHandle).type); } TEST_F(AScene, canUpdateATextureProviderDataSlot) { - EXPECT_EQ(0u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(0u, m_scene.m_impl.getIScene().getDataSlotCount()); uint8_t data1 = 0u; MipLevelData mipData1(1u, &data1); @@ -960,41 +991,41 @@ namespace ramses EXPECT_EQ(StatusOK, m_scene.createTextureProvider(*texture1, dataProviderId_t{666u})); EXPECT_EQ(StatusOK, m_scene.updateTextureProvider(*texture2, dataProviderId_t{666u})); - EXPECT_EQ(1u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(1u, m_scene.m_impl.getIScene().getDataSlotCount()); ramses_internal::DataSlotHandle slotHandle(0u); - ASSERT_TRUE(m_scene.impl.getIScene().isDataSlotAllocated(slotHandle)); - EXPECT_EQ(texture2->impl.getLowlevelResourceHash(), m_scene.impl.getIScene().getDataSlot(slotHandle).attachedTexture); - EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.impl.getIScene().getDataSlot(slotHandle).id); - EXPECT_EQ(ramses_internal::EDataSlotType_TextureProvider, m_scene.impl.getIScene().getDataSlot(slotHandle).type); + ASSERT_TRUE(m_scene.m_impl.getIScene().isDataSlotAllocated(slotHandle)); + EXPECT_EQ(texture2->m_impl.getLowlevelResourceHash(), m_scene.m_impl.getIScene().getDataSlot(slotHandle).attachedTexture); + EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.m_impl.getIScene().getDataSlot(slotHandle).id); + EXPECT_EQ(ramses_internal::EDataSlotType_TextureProvider, m_scene.m_impl.getIScene().getDataSlot(slotHandle).type); } TEST_F(AScene, canCreateATextureConsumerDataSlot) { - EXPECT_EQ(0u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(0u, m_scene.m_impl.getIScene().getDataSlotCount()); uint8_t data = 0u; MipLevelData mipData(1u, &data); Texture2D* texture = m_scene.createTexture2D(ETextureFormat::R8, 1u, 1u, 1u, &mipData, false); ASSERT_TRUE(nullptr != texture); - const TextureSampler* sampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + const TextureSampler* sampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); ASSERT_TRUE(nullptr != sampler); EXPECT_EQ(StatusOK, m_scene.createTextureConsumer(*sampler, dataConsumerId_t{666u})); - EXPECT_EQ(1u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(1u, m_scene.m_impl.getIScene().getDataSlotCount()); ramses_internal::DataSlotHandle slotHandle(0u); - ASSERT_TRUE(m_scene.impl.getIScene().isDataSlotAllocated(slotHandle)); - EXPECT_EQ(sampler->impl.getTextureSamplerHandle(), m_scene.impl.getIScene().getDataSlot(slotHandle).attachedTextureSampler); - EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.impl.getIScene().getDataSlot(slotHandle).id); - EXPECT_EQ(ramses_internal::EDataSlotType_TextureConsumer, m_scene.impl.getIScene().getDataSlot(slotHandle).type); + ASSERT_TRUE(m_scene.m_impl.getIScene().isDataSlotAllocated(slotHandle)); + EXPECT_EQ(sampler->m_impl.getTextureSamplerHandle(), m_scene.m_impl.getIScene().getDataSlot(slotHandle).attachedTextureSampler); + EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.m_impl.getIScene().getDataSlot(slotHandle).id); + EXPECT_EQ(ramses_internal::EDataSlotType_TextureConsumer, m_scene.m_impl.getIScene().getDataSlot(slotHandle).type); } TEST_F(AScene, canCreateATextureConsumerDataSlotWithTextureSamplerMS) { - EXPECT_EQ(0u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(0u, m_scene.m_impl.getIScene().getDataSlotCount()); - RenderBuffer* renderBuffer = m_scene.createRenderBuffer(4u, 4u, ERenderBufferType_Color, ERenderBufferFormat_RGB8, ERenderBufferAccessMode_ReadWrite, 4u); + RenderBuffer* renderBuffer = m_scene.createRenderBuffer(4u, 4u, ERenderBufferType::Color, ERenderBufferFormat::RGB8, ERenderBufferAccessMode::ReadWrite, 4u); ASSERT_TRUE(nullptr != renderBuffer); const TextureSamplerMS* sampler = m_scene.createTextureSamplerMS(*renderBuffer, "sampler"); @@ -1003,42 +1034,42 @@ namespace ramses EXPECT_EQ(StatusOK, m_scene.createTextureConsumer(*sampler, dataConsumerId_t{ 666u })); - EXPECT_EQ(1u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(1u, m_scene.m_impl.getIScene().getDataSlotCount()); ramses_internal::DataSlotHandle slotHandle(0u); - ASSERT_TRUE(m_scene.impl.getIScene().isDataSlotAllocated(slotHandle)); - EXPECT_EQ(sampler->impl.getTextureSamplerHandle(), m_scene.impl.getIScene().getDataSlot(slotHandle).attachedTextureSampler); - EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.impl.getIScene().getDataSlot(slotHandle).id); - EXPECT_EQ(ramses_internal::EDataSlotType_TextureConsumer, m_scene.impl.getIScene().getDataSlot(slotHandle).type); + ASSERT_TRUE(m_scene.m_impl.getIScene().isDataSlotAllocated(slotHandle)); + EXPECT_EQ(sampler->m_impl.getTextureSamplerHandle(), m_scene.m_impl.getIScene().getDataSlot(slotHandle).attachedTextureSampler); + EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.m_impl.getIScene().getDataSlot(slotHandle).id); + EXPECT_EQ(ramses_internal::EDataSlotType_TextureConsumer, m_scene.m_impl.getIScene().getDataSlot(slotHandle).type); } TEST_F(AScene, canCreateATextureConsumerDataSlotWithTextureSamplerExternal) { - EXPECT_EQ(0u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(0u, m_scene.m_impl.getIScene().getDataSlotCount()); - const TextureSamplerExternal* sampler = m_scene.createTextureSamplerExternal(ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear); + const TextureSamplerExternal* sampler = m_scene.createTextureSamplerExternal(ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear); ASSERT_TRUE(nullptr != sampler); EXPECT_EQ(StatusOK, m_scene.createTextureConsumer(*sampler, dataConsumerId_t{ 666u })); - EXPECT_EQ(1u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(1u, m_scene.m_impl.getIScene().getDataSlotCount()); ramses_internal::DataSlotHandle slotHandle(0u); - ASSERT_TRUE(m_scene.impl.getIScene().isDataSlotAllocated(slotHandle)); - EXPECT_EQ(sampler->impl.getTextureSamplerHandle(), m_scene.impl.getIScene().getDataSlot(slotHandle).attachedTextureSampler); - EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.impl.getIScene().getDataSlot(slotHandle).id); - EXPECT_EQ(ramses_internal::EDataSlotType_TextureConsumer, m_scene.impl.getIScene().getDataSlot(slotHandle).type); + ASSERT_TRUE(m_scene.m_impl.getIScene().isDataSlotAllocated(slotHandle)); + EXPECT_EQ(sampler->m_impl.getTextureSamplerHandle(), m_scene.m_impl.getIScene().getDataSlot(slotHandle).attachedTextureSampler); + EXPECT_EQ(ramses_internal::DataSlotId(666u), m_scene.m_impl.getIScene().getDataSlot(slotHandle).id); + EXPECT_EQ(ramses_internal::EDataSlotType_TextureConsumer, m_scene.m_impl.getIScene().getDataSlot(slotHandle).type); } TEST_F(AScene, canNotCreateATextureConsumerDataSlotIfOtherThan2DTextureAssigned) { - EXPECT_EQ(0u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(0u, m_scene.m_impl.getIScene().getDataSlotCount()); uint8_t data = 0u; MipLevelData mipData(1u, &data); Texture3D* texture = m_scene.createTexture3D(ETextureFormat::R8, 1u, 1u, 1u, 1u, &mipData, false); ASSERT_TRUE(nullptr != texture); - const TextureSampler* sampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + const TextureSampler* sampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); ASSERT_TRUE(nullptr != sampler); EXPECT_NE(StatusOK, m_scene.createTextureConsumer(*sampler, dataConsumerId_t{666u})); @@ -1051,18 +1082,18 @@ namespace ramses Texture2D* texture = m_scene.createTexture2D(ETextureFormat::R8, 1u, 1u, 1u, &mipData, false); ASSERT_TRUE(nullptr != texture); - TextureSampler* sampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* sampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); ASSERT_TRUE(nullptr != sampler); EXPECT_EQ(StatusOK, m_scene.createTextureConsumer(*sampler, dataConsumerId_t{666u})); - EXPECT_EQ(1u, m_scene.impl.getIScene().getDataSlotCount()); + EXPECT_EQ(1u, m_scene.m_impl.getIScene().getDataSlotCount()); ramses_internal::DataSlotHandle linkHandle(0u); - ASSERT_TRUE(m_scene.impl.getIScene().isDataSlotAllocated(linkHandle)); + ASSERT_TRUE(m_scene.m_impl.getIScene().isDataSlotAllocated(linkHandle)); m_scene.destroy(*sampler); - EXPECT_FALSE(m_scene.impl.getIScene().isDataSlotAllocated(linkHandle)); + EXPECT_FALSE(m_scene.m_impl.getIScene().isDataSlotAllocated(linkHandle)); } TEST_F(AScene, canNotCreateMoreThanOneConsumerForATextureSampler) @@ -1072,7 +1103,7 @@ namespace ramses Texture2D* texture = m_scene.createTexture2D(ETextureFormat::R8, 1u, 1u, 1u, &mipData, false); ASSERT_TRUE(nullptr != texture); - TextureSampler* sampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* sampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); ASSERT_TRUE(nullptr != sampler); EXPECT_EQ(StatusOK, m_scene.createTextureConsumer(*sampler, dataConsumerId_t{666u})); @@ -1081,7 +1112,7 @@ namespace ramses TEST_F(AScene, canNotCreateMoreThanOneConsumerForATextureSamplerMS) { - RenderBuffer* renderBuffer = m_scene.createRenderBuffer(4u, 4u, ERenderBufferType_Color, ERenderBufferFormat_RGB8, ERenderBufferAccessMode_ReadWrite, 4u); + RenderBuffer* renderBuffer = m_scene.createRenderBuffer(4u, 4u, ERenderBufferType::Color, ERenderBufferFormat::RGB8, ERenderBufferAccessMode::ReadWrite, 4u); ASSERT_TRUE(nullptr != renderBuffer); const TextureSamplerMS* sampler = m_scene.createTextureSamplerMS(*renderBuffer, "sampler"); @@ -1093,7 +1124,7 @@ namespace ramses TEST_F(AScene, canNotCreateMoreThanOneConsumerForATextureSamplerExternal) { - const TextureSamplerExternal* sampler = m_scene.createTextureSamplerExternal(ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear); + const TextureSamplerExternal* sampler = m_scene.createTextureSamplerExternal(ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear); ASSERT_TRUE(nullptr != sampler); EXPECT_EQ(StatusOK, m_scene.createTextureConsumer(*sampler, dataConsumerId_t{ 666u })); @@ -1120,9 +1151,9 @@ namespace ramses Texture2D* texture2 = m_scene.createTexture2D(ETextureFormat::R8, 1u, 1u, 1u, &mipData, false); ASSERT_TRUE(nullptr != texture2); - TextureSampler* sampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture); + TextureSampler* sampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture); ASSERT_TRUE(nullptr != sampler); - TextureSampler* sampler2 = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, *texture2); + TextureSampler* sampler2 = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, *texture2); ASSERT_TRUE(nullptr != sampler2); EXPECT_EQ(StatusOK, m_scene.createTextureProvider(*texture, dataProviderId_t(1u))); @@ -1144,100 +1175,75 @@ namespace ramses EXPECT_NE(StatusOK, m_scene.updateTextureProvider(*texture, dataProviderId_t(1u))); } - TEST_F(AScene, canCreateStreamTextureWithFallbackTextureAndStreamSource) - { - const Texture2D& texture2D = createObject("testTexture2D"); - const waylandIviSurfaceId_t source(1); - StreamTexture* streamTexture = this->m_scene.createStreamTexture(texture2D, source, "testStreamTexture"); - - ASSERT_NE(static_cast(nullptr), streamTexture); - EXPECT_EQ(source, streamTexture->impl.getStreamSource()); - EXPECT_EQ(texture2D.impl.getLowlevelResourceHash(), streamTexture->impl.getFallbackTextureHash()); - } - - TEST_F(AScene, cannotCreateStreamTextureWithFallbackTextureFromDifferentScene) - { - const uint8_t data[4 * 10 * 12] = {}; - MipLevelData mipLevelData(sizeof(data), data); - - Scene& anotherScene(*client.createScene(sceneId_t{ 0xf00 })); - - Texture2D* anotherTexture = anotherScene.createTexture2D(ramses::ETextureFormat::RGBA8, 10, 12, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, "name"); - ASSERT_TRUE(nullptr != anotherTexture); - - StreamTexture* streamTexture = this->m_scene.createStreamTexture(*anotherTexture, waylandIviSurfaceId_t(0), "StreamTexture"); - EXPECT_TRUE(nullptr == streamTexture); - } - TEST_F(AScene, canCreateTextureSamplerForTexture2DWithDefaultAnisotropyLevel) { const Texture2D& texture2D = createObject("testTexture2D"); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, texture2D); + const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, texture2D); ASSERT_NE(static_cast(nullptr), sampler); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Nearest, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureSamplingMethod::Nearest, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_Texture2D, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::Texture2D, sampler->getTextureType()); } TEST_F(AScene, canCreateTextureSamplerForRenderBufferWithDefaultAnisotropyLevel) { const RenderBuffer& renderBuffer = createObject("renderBuffer"); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, renderBuffer); + const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, renderBuffer); ASSERT_NE(static_cast(nullptr), sampler); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Nearest, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureSamplingMethod::Nearest, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_RenderBuffer, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::RenderBuffer, sampler->getTextureType()); } TEST_F(AScene, canCreateTextureSamplerForTextureCubeWithDefaultAnisotropyLevel) { const TextureCube& textureCube = createObject("testTextureCube"); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, textureCube); + const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, textureCube); ASSERT_NE(static_cast(nullptr), sampler); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_TextureCube, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::TextureCube, sampler->getTextureType()); } TEST_F(AScene, cantCreateTextureSamplerForTexture2DWithWrongAnisotropyValue) { const Texture2D& texture = createObject("testTexture2D"); - TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, texture, 0); + TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, texture, 0); EXPECT_TRUE(nullptr == textureSampler); } TEST_F(AScene, cantCreateTextureSamplerForTextureCubeWithWrongAnisotropyValue) { const TextureCube& textureCube = createObject("testTextureCube"); - const TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, textureCube, 0); + const TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, textureCube, 0); EXPECT_TRUE(nullptr == textureSampler); } TEST_F(AScene, cantCreateTextureSamplerForRenderBufferWithWrongAnisotropyValue) { const RenderBuffer& renderBuffer = createObject("renderBuffer"); - const TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Nearest, ETextureSamplingMethod_Linear, renderBuffer, 0); + const TextureSampler* textureSampler = m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Nearest, ETextureSamplingMethod::Linear, renderBuffer, 0); EXPECT_TRUE(nullptr == textureSampler); } TEST_F(AScene, cantCreateTextureSamplerWithWrongMagSamplingMethod) { const TextureCube& textureCube = createObject("testTextureCube"); - EXPECT_EQ(nullptr, this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear_MipMapLinear, textureCube)); - EXPECT_EQ(nullptr, this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear_MipMapNearest, textureCube)); - EXPECT_EQ(nullptr, this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Nearest_MipMapNearest, textureCube)); + EXPECT_EQ(nullptr, this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear_MipMapLinear, textureCube)); + EXPECT_EQ(nullptr, this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear_MipMapNearest, textureCube)); + EXPECT_EQ(nullptr, this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Nearest_MipMapNearest, textureCube)); } TEST_F(AScene, canCreateBlitPass) @@ -1251,7 +1257,7 @@ namespace ramses TEST_F(AScene, cannotCreateBlitPass_WithSourceRenderBufferFromDifferentScene) { Scene& anotherScene = *client.createScene(sceneId_t(12u)); - const RenderBuffer* sourceRenderBuffer = anotherScene.createRenderBuffer(100u, 100u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* sourceRenderBuffer = anotherScene.createRenderBuffer(100u, 100u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(nullptr != sourceRenderBuffer); const RenderBuffer& destinationRenderBuffer = createObject("dst renderBuffer"); @@ -1262,7 +1268,7 @@ namespace ramses TEST_F(AScene, cannotCreateBlitPass_WithSourceAndDestinationRenderBufferFromDifferentScenes) { Scene& anotherScene = *client.createScene(sceneId_t(12u)); - const RenderBuffer* destinationRenderBuffer = anotherScene.createRenderBuffer(100u, 100u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* destinationRenderBuffer = anotherScene.createRenderBuffer(100u, 100u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(nullptr != destinationRenderBuffer); const RenderBuffer& sourceRenderBuffer = createObject("src renderBuffer"); @@ -1272,8 +1278,8 @@ namespace ramses TEST_F(AScene, cannotCreateBlitPass_WithSourceAndDestinationRenderBuffersHavingDifferentType) { - const RenderBuffer* sourceRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType_Color, ERenderBufferFormat_R8, ERenderBufferAccessMode_ReadWrite); - const RenderBuffer* destinationRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType_Depth, ERenderBufferFormat_Depth24, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* sourceRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType::Color, ERenderBufferFormat::R8, ERenderBufferAccessMode::ReadWrite); + const RenderBuffer* destinationRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType::Depth, ERenderBufferFormat::Depth24, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(nullptr != sourceRenderBuffer); ASSERT_TRUE(nullptr != destinationRenderBuffer); @@ -1283,8 +1289,8 @@ namespace ramses TEST_F(AScene, cannotCreateBlitPass_WithSourceAndDestinationRenderBuffersHavingDifferentFormat) { - const RenderBuffer* sourceRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType_Color, ERenderBufferFormat_R8, ERenderBufferAccessMode_ReadWrite); - const RenderBuffer* destinationRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* sourceRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType::Color, ERenderBufferFormat::R8, ERenderBufferAccessMode::ReadWrite); + const RenderBuffer* destinationRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(nullptr != sourceRenderBuffer); ASSERT_TRUE(nullptr != destinationRenderBuffer); @@ -1294,8 +1300,8 @@ namespace ramses TEST_F(AScene, cannotCreateBlitPass_WithSourceAndDestinationRenderBuffersHavinghDifferentWidth) { - const RenderBuffer* sourceRenderBuffer = m_scene.createRenderBuffer(1u, 100u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - const RenderBuffer* destinationRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* sourceRenderBuffer = m_scene.createRenderBuffer(1u, 100u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + const RenderBuffer* destinationRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(nullptr != sourceRenderBuffer); ASSERT_TRUE(nullptr != destinationRenderBuffer); @@ -1305,8 +1311,8 @@ namespace ramses TEST_F(AScene, cannotCreateBlitPass_WithSourceAndDestinationRenderBuffersHavingDifferentHeight) { - const RenderBuffer* sourceRenderBuffer = m_scene.createRenderBuffer(100u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); - const RenderBuffer* destinationRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* sourceRenderBuffer = m_scene.createRenderBuffer(100u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); + const RenderBuffer* destinationRenderBuffer = m_scene.createRenderBuffer(100u, 100u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(nullptr != sourceRenderBuffer); ASSERT_TRUE(nullptr != destinationRenderBuffer); @@ -1316,7 +1322,7 @@ namespace ramses TEST_F(AScene, cannotCreateBlitPass_WithSameSourceAndDestinationRenderBuffers) { - const RenderBuffer* rb = m_scene.createRenderBuffer(100u, 1u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite); + const RenderBuffer* rb = m_scene.createRenderBuffer(100u, 1u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite); ASSERT_TRUE(nullptr != rb); const BlitPass* blitPass = m_scene.createBlitPass(*rb, *rb, "blitpass"); EXPECT_TRUE(nullptr == blitPass); @@ -1402,9 +1408,9 @@ namespace ramses TEST_F(AScene, flushIncreasesStatisticCounter) { - EXPECT_EQ(0u, m_scene.impl.getStatisticCollection().statFlushesTriggered.getCounterValue()); + EXPECT_EQ(0u, m_scene.m_impl.getStatisticCollection().statFlushesTriggered.getCounterValue()); m_scene.flush(); - EXPECT_EQ(1u, m_scene.impl.getStatisticCollection().statFlushesTriggered.getCounterValue()); + EXPECT_EQ(1u, m_scene.m_impl.getStatisticCollection().statFlushesTriggered.getCounterValue()); } TEST_F(AScene, canGetResourceByID) @@ -1589,14 +1595,16 @@ namespace ramses TEST_F(AScene, returnsFalseOnFlushWhenResourcesAreMissing) { - m_creationHelper.createObjectOfType("meh"); + m_creationHelper.createObjectOfType("meh"); EXPECT_EQ(StatusOK, m_scene.flush()); // test legal scene state => flush success - m_scene.destroy(*RamsesUtils::TryConvert(*m_scene.findObjectByName("fallbackTex"))); + m_scene.destroy(*RamsesUtils::TryConvert(*m_scene.findObjectByName("appearance effect"))); m_scene.destroy(*RamsesUtils::TryConvert(*m_scene.findObjectByName("meh"))); EXPECT_EQ(StatusOK, m_scene.flush()); // resource is deleted, but nobody needs it => flush success - m_creationHelper.createObjectOfType("meh2"); - m_scene.destroy(*RamsesUtils::TryConvert(*m_scene.findObjectByName("fallbackTex"))); + auto* appearnace = m_creationHelper.createObjectOfType("meh2"); + auto* mesh = m_creationHelper.createObjectOfType("meh3"); + mesh->setAppearance(*appearnace); + m_scene.destroy(*RamsesUtils::TryConvert(*m_scene.findObjectByName("appearance effect"))); EXPECT_NE(StatusOK, m_scene.flush()); // test scene with resource missing => flush failed } @@ -1612,9 +1620,9 @@ namespace ramses TEST_F(AScene, resetUniformTimeMs) { - EXPECT_EQ(ramses_internal::FlushTime::InvalidTimestamp, m_scene.impl.getIScene().getEffectTimeSync()); + EXPECT_EQ(ramses_internal::FlushTime::InvalidTimestamp, m_scene.m_impl.getIScene().getEffectTimeSync()); EXPECT_EQ(StatusOK, m_scene.resetUniformTimeMs()); - const auto timeSync = m_scene.impl.getIScene().getEffectTimeSync(); + const auto timeSync = m_scene.m_impl.getIScene().getEffectTimeSync(); EXPECT_NE(0, timeSync.time_since_epoch().count()); EXPECT_EQ(StatusOK, m_scene.flush()); @@ -1629,7 +1637,7 @@ namespace ramses EXPECT_EQ(timeSync, timeInfo.internalTimestamp); }); - EXPECT_EQ(StatusOK, m_scene.publish(ramses::EScenePublicationMode_LocalOnly)); + EXPECT_EQ(StatusOK, m_scene.publish(ramses::EScenePublicationMode::LocalOnly)); Mock::VerifyAndClearExpectations(&sceneActionsCollector); // internal timestamp contains flush time diff --git a/client/ramses-client/test/SerializationHelperTest.cpp b/client/test/SerializationHelperTest.cpp similarity index 100% rename from client/ramses-client/test/SerializationHelperTest.cpp rename to client/test/SerializationHelperTest.cpp diff --git a/client/ramses-client/test/SimpleSceneTopology.h b/client/test/SimpleSceneTopology.h similarity index 85% rename from client/ramses-client/test/SimpleSceneTopology.h rename to client/test/SimpleSceneTopology.h index c50a54190..ee943ca32 100644 --- a/client/ramses-client/test/SimpleSceneTopology.h +++ b/client/test/SimpleSceneTopology.h @@ -50,10 +50,10 @@ namespace ramses EXPECT_EQ(StatusOK, m_vis2.addChild(m_mesh2b)); //Every Mesh should be visible by default - EXPECT_EQ(m_mesh1a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh1b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh2a.impl.getFlattenedVisibility(), EVisibilityMode::Visible); - EXPECT_EQ(m_mesh2b.impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh1b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh2a.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); + EXPECT_EQ(m_mesh2b.m_impl.getFlattenedVisibility(), EVisibilityMode::Visible); } Node& m_root; diff --git a/client/ramses-client/test/TestEffectCreator.h b/client/test/TestEffectCreator.h similarity index 97% rename from client/ramses-client/test/TestEffectCreator.h rename to client/test/TestEffectCreator.h index d2fa5c175..0ab6987cc 100644 --- a/client/ramses-client/test/TestEffectCreator.h +++ b/client/test/TestEffectCreator.h @@ -36,14 +36,14 @@ namespace ramses assert(appearance); } - virtual ~TestEffectCreator() + ~TestEffectCreator() override { EXPECT_EQ(StatusOK, this->m_scene.destroy(*appearance)); } static Effect* createEffect(Scene& scene, bool withSemantics, bool withGeometryShader = false) { - ramses_internal::String VertexShader( + std::string VertexShader( "#version 320 es\n" "uniform lowp float floatInput;\n" "uniform lowp float floatInputArray[3];\n" @@ -92,7 +92,7 @@ namespace ramses " gl_Position = values;\n" "}\n"); - ramses_internal::String FragmentShader( + std::string FragmentShader( "#version 320 es\n" "precision mediump float;\n" "uniform sampler2D texture2dInput;\n" diff --git a/client/ramses-client/test/TestEffects.h b/client/test/TestEffects.h similarity index 59% rename from client/ramses-client/test/TestEffects.h rename to client/test/TestEffects.h index 33bc510cf..5cb56a025 100644 --- a/client/ramses-client/test/TestEffects.h +++ b/client/test/TestEffects.h @@ -18,7 +18,7 @@ namespace ramses class TestEffects { public: - static Effect* CreateTestEffect(Scene& scene, const char* name = nullptr, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache) + static Effect* CreateTestEffect(Scene& scene, std::string_view name = {}, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache) { EffectDescription effectDesc; effectDesc.setVertexShader( @@ -39,7 +39,7 @@ namespace ramses return effect; } - static Effect* CreateDifferentTestEffect(Scene& scene, const char* name = nullptr, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache) + static Effect* CreateDifferentTestEffect(Scene& scene, std::string_view name = {}, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache) { EffectDescription effectDesc; effectDesc.setVertexShader( @@ -57,7 +57,7 @@ namespace ramses return effect; } - static Effect* CreateTestEffectWithAttribute(Scene& scene, const char* name = nullptr, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache) + static Effect* CreateTestEffectWithAttribute(Scene& scene, std::string_view name = {}, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache) { EffectDescription effectDesc; effectDesc.setVertexShader( @@ -78,6 +78,50 @@ namespace ramses assert(effect != nullptr); return effect; } + + static Effect* CreateTestEffectWithAllStages(Scene& scene, std::string_view name = {}, resourceCacheFlag_t cacheFlag = ResourceCacheFlag_DoNotCache) + { + const char* vs = R"SHADER( + #version 320 es + in vec3 a_position1; + in float a_position2; + in float a_position3; + uniform highp float vs_uniform; + void main(void) + { + gl_Position = vec4(vs_uniform, a_position1.y, a_position2, a_position3); + } + )SHADER"; + + const char* gs = R"SHADER( + #version 320 es + layout(lines) in; + layout(points, max_vertices = 1) out; + uniform highp float gs_uniform; + void main() { + gl_Position = vec4(gs_uniform, 0.0, 0.0, 1.0); + EmitVertex(); + } + )SHADER"; + + const char* fs = R"SHADER( + #version 320 es + uniform highp vec2 colorRG; + uniform highp float colorBA[2]; + out lowp vec4 colorOut; + void main(void) + { + colorOut = vec4(colorRG, colorBA[0], colorBA[1]); + })SHADER"; + + EffectDescription effectDesc; + effectDesc.setVertexShader(vs); + effectDesc.setFragmentShader(fs); + effectDesc.setGeometryShader(gs); + Effect* effect = scene.createEffect(effectDesc, cacheFlag, name); + assert(effect != nullptr); + return effect; + } }; } diff --git a/client/ramses-client/test/Texture2DBufferTest.cpp b/client/test/Texture2DBufferTest.cpp similarity index 92% rename from client/ramses-client/test/Texture2DBufferTest.cpp rename to client/test/Texture2DBufferTest.cpp index e8004d4f4..b162de09f 100644 --- a/client/ramses-client/test/Texture2DBufferTest.cpp +++ b/client/test/Texture2DBufferTest.cpp @@ -29,10 +29,10 @@ namespace ramses TEST_F(ATexture2DBuffer, IsAllocatedOnInternalSceneAfterCreation) { Texture2DBuffer& textureBuffer = *m_scene.createTexture2DBuffer(ETextureFormat::RGBA8, 3, 4, 2); - const ramses_internal::TextureBufferHandle textureBufferHandle = textureBuffer.impl.getTextureBufferHandle(); + const ramses_internal::TextureBufferHandle textureBufferHandle = textureBuffer.m_impl.getTextureBufferHandle(); EXPECT_TRUE(textureBufferHandle.isValid()); - EXPECT_TRUE(this->m_scene.impl.getIScene().isTextureBufferAllocated(textureBufferHandle)); + EXPECT_TRUE(this->m_scene.m_impl.getIScene().isTextureBufferAllocated(textureBufferHandle)); } TEST_F(ATexture2DBuffer, CanGetTexelFormat) @@ -45,8 +45,8 @@ namespace ramses TEST_F(ATexture2DBuffer, PropagatesItsPropertiesToInternalScene) { Texture2DBuffer& textureBuffer = *m_scene.createTexture2DBuffer(ETextureFormat::RGBA8, 3, 4, 2); - const ramses_internal::TextureBufferHandle textureBufferHandle = textureBuffer.impl.getTextureBufferHandle(); - const ramses_internal::TextureBuffer& internalTexBuffer = this->m_scene.impl.getIScene().getTextureBuffer(textureBufferHandle); + const ramses_internal::TextureBufferHandle textureBufferHandle = textureBuffer.m_impl.getTextureBufferHandle(); + const ramses_internal::TextureBuffer& internalTexBuffer = this->m_scene.m_impl.getIScene().getTextureBuffer(textureBufferHandle); EXPECT_EQ(ramses_internal::ETextureFormat::RGBA8, internalTexBuffer.textureFormat); ASSERT_EQ(2u, internalTexBuffer.mipMaps.size()); @@ -85,11 +85,11 @@ namespace ramses TEST_F(ATexture2DBuffer, PropagatesDataUpdatesToInternalScene) { Texture2DBuffer& textureBuffer = *m_scene.createTexture2DBuffer(ETextureFormat::RGBA8, 3, 4, 2); - const ramses_internal::TextureBufferHandle textureBufferHandle = textureBuffer.impl.getTextureBufferHandle(); + const ramses_internal::TextureBufferHandle textureBufferHandle = textureBuffer.m_impl.getTextureBufferHandle(); // update mipLevel = 0 EXPECT_EQ(StatusOK, textureBuffer.updateData(0, 0, 0, 2, 2, std::array{ {12, 23, 34, 56} }.data())); - const Byte* textureBufferDataMip0 = this->m_scene.impl.getIScene().getTextureBuffer(textureBufferHandle).mipMaps[0].data.data(); + const Byte* textureBufferDataMip0 = this->m_scene.m_impl.getIScene().getTextureBuffer(textureBufferHandle).mipMaps[0].data.data(); EXPECT_EQ(12u, UnsafeTestMemoryHelpers::GetTypedValueFromMemoryBlob(textureBufferDataMip0, 0)); EXPECT_EQ(23u, UnsafeTestMemoryHelpers::GetTypedValueFromMemoryBlob(textureBufferDataMip0, 1)); @@ -98,7 +98,7 @@ namespace ramses // update mipLevel = 1 EXPECT_EQ(StatusOK, textureBuffer.updateData(1, 0, 0, 1, 1, std::array{ {78} }.data())); - const Byte* textureBufferDataMip1 = this->m_scene.impl.getIScene().getTextureBuffer(textureBufferHandle).mipMaps[1].data.data(); + const Byte* textureBufferDataMip1 = this->m_scene.m_impl.getIScene().getTextureBuffer(textureBufferHandle).mipMaps[1].data.data(); EXPECT_EQ(78u, UnsafeTestMemoryHelpers::GetTypedValueFromMemoryBlob(textureBufferDataMip1, 0)); } @@ -217,7 +217,7 @@ namespace ramses { Texture2DBuffer& textureBuffer = *m_scene.createTexture2DBuffer(ETextureFormat::RGBA8, 4, 4, 1); EXPECT_EQ(StatusOK, textureBuffer.updateData(0, 0, 0, 2, 2, std::array{ {12, 23, 34, 56} }.data())); - m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, textureBuffer); + m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, textureBuffer); EXPECT_EQ(StatusOK, textureBuffer.validate()); } @@ -231,7 +231,7 @@ namespace ramses TEST_F(ATexture2DBuffer, ReportsWarningIfUsedInSamplerButNotInitialized) { Texture2DBuffer& textureBuffer = *m_scene.createTexture2DBuffer(ETextureFormat::RGBA8, 4, 4, 1); - m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, textureBuffer); + m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, textureBuffer); EXPECT_NE(StatusOK, textureBuffer.validate()); } } diff --git a/client/ramses-client/test/TextureSamplerTest.cpp b/client/test/TextureSamplerTest.cpp similarity index 57% rename from client/ramses-client/test/TextureSamplerTest.cpp rename to client/test/TextureSamplerTest.cpp index b7ab5740d..bb7764860 100644 --- a/client/ramses-client/test/TextureSamplerTest.cpp +++ b/client/test/TextureSamplerTest.cpp @@ -19,12 +19,10 @@ #include "ramses-client-api/Texture3D.h" #include "ramses-client-api/TextureCube.h" #include "ramses-client-api/Texture2DBuffer.h" -#include "ramses-client-api/StreamTexture.h" #include "ramses-client-api/RenderBuffer.h" #include "ramses-client-api/RenderTargetDescription.h" #include "ramses-client-api/RenderPass.h" #include "ramses-client-api/OrthographicCamera.h" -#include "StreamTextureImpl.h" #include "TextureSamplerImpl.h" #include "RenderBufferImpl.h" #include "Texture2DImpl.h" @@ -43,18 +41,18 @@ namespace ramses TEST_F(TextureSamplerTest, createSamplerForTexture2D) { const Texture2D& texture2D = createObject("testTexture2D"); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture2D, 1u, "testSampler2D"); + const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture2D, 1u, "testSampler2D"); ASSERT_NE(static_cast(nullptr), sampler); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_Texture2D, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::Texture2D, sampler->getTextureType()); - const ramses_internal::TextureSamplerHandle samplerHandle = sampler->impl.getTextureSamplerHandle(); - const auto texHash = texture2D.impl.getLowlevelResourceHash(); + const ramses_internal::TextureSamplerHandle samplerHandle = sampler->m_impl.getTextureSamplerHandle(); + const auto texHash = texture2D.m_impl.getLowlevelResourceHash(); ASSERT_TRUE(m_internalScene.isTextureSamplerAllocated(samplerHandle)); EXPECT_EQ(ramses_internal::TextureSampler::ContentType::ClientTexture, m_internalScene.getTextureSampler(samplerHandle).contentType); EXPECT_EQ(texHash, m_internalScene.getTextureSampler(samplerHandle).textureResource); @@ -63,75 +61,75 @@ namespace ramses TEST_F(TextureSamplerTest, createSamplerForTexture2DWithAnisotropy) { const Texture2D& texture2D = createObject("testTexture2D"); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture2D, 16u, "testSampler2D"); + const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture2D, 16u, "testSampler2D"); ASSERT_NE(static_cast(nullptr), sampler); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(16u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_Texture2D, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::Texture2D, sampler->getTextureType()); } TEST_F(TextureSamplerTest, createSamplerForTexture3D) { const Texture3D& texture3D = createObject("testTexture3D"); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureAddressMode_Mirror, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture3D, "testSampler3D"); + const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureAddressMode::Mirror, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture3D, "testSampler3D"); ASSERT_NE(static_cast(nullptr), sampler); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureAddressMode_Mirror, sampler->getWrapRMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureAddressMode::Mirror, sampler->getWrapRMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_Texture3D, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::Texture3D, sampler->getTextureType()); } TEST_F(TextureSamplerTest, createSamplerForTextureCube) { const TextureCube& textureCube = createObject("testTextureCube"); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, textureCube, 1u, "testSamplerCube"); + const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, textureCube, 1u, "testSamplerCube"); ASSERT_NE(static_cast(nullptr), sampler); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_TextureCube, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::TextureCube, sampler->getTextureType()); } TEST_F(TextureSamplerTest, createSamplerForTextureCubeWithAnisotropy) { const TextureCube& textureCube = createObject("testTextureCube"); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, textureCube, 16u, "testSamplerCube"); + const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, textureCube, 16u, "testSamplerCube"); ASSERT_NE(static_cast(nullptr), sampler); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(16u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_TextureCube, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::TextureCube, sampler->getTextureType()); } TEST_F(TextureSamplerTest, createSamplerForRenderBuffer) { const RenderBuffer& renderBuffer = createObject(); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, renderBuffer, 16u); + TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, renderBuffer, 16u); ASSERT_NE(static_cast(nullptr), sampler); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(16u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_RenderBuffer, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::RenderBuffer, sampler->getTextureType()); - const ramses_internal::TextureSamplerHandle samplerHandle = sampler->impl.getTextureSamplerHandle(); - const ramses_internal::RenderBufferHandle renderBufferHandle = renderBuffer.impl.getRenderBufferHandle(); + const ramses_internal::TextureSamplerHandle samplerHandle = sampler->m_impl.getTextureSamplerHandle(); + const ramses_internal::RenderBufferHandle renderBufferHandle = renderBuffer.m_impl.getRenderBufferHandle(); ASSERT_TRUE(m_internalScene.isTextureSamplerAllocated(samplerHandle)); EXPECT_EQ(ramses_internal::TextureSampler::ContentType::RenderBuffer, m_internalScene.getTextureSampler(samplerHandle).contentType); EXPECT_EQ(renderBufferHandle.asMemoryHandle(), m_internalScene.getTextureSampler(samplerHandle).contentHandle); @@ -144,8 +142,8 @@ namespace ramses ASSERT_NE(static_cast(nullptr), sampler); - const ramses_internal::TextureSamplerHandle samplerHandle = sampler->impl.getTextureSamplerHandle(); - const ramses_internal::RenderBufferHandle renderBufferHandle = renderBuffer.impl.getRenderBufferHandle(); + const ramses_internal::TextureSamplerHandle samplerHandle = sampler->m_impl.getTextureSamplerHandle(); + const ramses_internal::RenderBufferHandle renderBufferHandle = renderBuffer.m_impl.getRenderBufferHandle(); ASSERT_TRUE(m_internalScene.isTextureSamplerAllocated(samplerHandle)); EXPECT_EQ(ramses_internal::TextureSampler::ContentType::RenderBufferMS, m_internalScene.getTextureSampler(samplerHandle).contentType); EXPECT_EQ(renderBufferHandle.asMemoryHandle(), m_internalScene.getTextureSampler(samplerHandle).contentHandle); @@ -153,10 +151,10 @@ namespace ramses TEST_F(TextureSamplerTest, createSamplerForTextureExternal) { - const TextureSamplerExternal* sampler = this->m_scene.createTextureSamplerExternal(ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, "testSampler2DExternal"); + const TextureSamplerExternal* sampler = this->m_scene.createTextureSamplerExternal(ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, "testSampler2DExternal"); ASSERT_NE(static_cast(nullptr), sampler); - const ramses_internal::TextureSamplerHandle samplerHandle = sampler->impl.getTextureSamplerHandle(); + const ramses_internal::TextureSamplerHandle samplerHandle = sampler->m_impl.getTextureSamplerHandle(); ASSERT_TRUE(m_internalScene.isTextureSamplerAllocated(samplerHandle)); const auto& samplerInternal = m_internalScene.getTextureSampler(samplerHandle); @@ -175,21 +173,21 @@ namespace ramses TEST_F(TextureSamplerTest, doesNotCreateSamplerForTextureExternalWithNotAllowedFilteringMethod) { - EXPECT_EQ(nullptr, this->m_scene.createTextureSamplerExternal(ETextureSamplingMethod_Linear_MipMapLinear, ETextureSamplingMethod_Linear, "testSampler2DExternal")); - EXPECT_EQ(nullptr, this->m_scene.createTextureSamplerExternal(ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear_MipMapNearest, "testSampler2DExternal")); + EXPECT_EQ(nullptr, this->m_scene.createTextureSamplerExternal(ETextureSamplingMethod::Linear_MipMapLinear, ETextureSamplingMethod::Linear, "testSampler2DExternal")); + EXPECT_EQ(nullptr, this->m_scene.createTextureSamplerExternal(ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear_MipMapNearest, "testSampler2DExternal")); } TEST_F(TextureSamplerTest, canCreateSamplerConsumersWithSameConsumerIdButFailValidation) { - auto sampler1 = m_scene.createTextureSamplerExternal(ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear); + auto sampler1 = m_scene.createTextureSamplerExternal(ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear); const dataConsumerId_t consumerId{ 123u }; EXPECT_EQ(StatusOK, m_scene.createTextureConsumer(*sampler1, consumerId)); EXPECT_EQ(StatusOK, m_scene.validate()); - const auto sampler2 = m_scene.createTextureSamplerExternal(ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear); + const auto sampler2 = m_scene.createTextureSamplerExternal(ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear); EXPECT_EQ(StatusOK, m_scene.createTextureConsumer(*sampler2, consumerId)); EXPECT_NE(StatusOK, m_scene.validate()); - const std::string report = m_scene.getValidationReport(EValidationSeverity_Error); + const std::string report = m_scene.getValidationReport(EValidationSeverity::Error); EXPECT_THAT(report, HasSubstr("Duplicate texture consumer ID '123'")); // validates fine again after duplicate removed @@ -199,46 +197,24 @@ namespace ramses TEST_F(TextureSamplerTest, cannotCreateSamplerForRenderBufferWithSamples) { - const RenderBuffer& renderBuffer = *m_scene.createRenderBuffer(4u, 4u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_ReadWrite, 4u); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, renderBuffer, 16u); + const RenderBuffer& renderBuffer = *m_scene.createRenderBuffer(4u, 4u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::ReadWrite, 4u); + TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, renderBuffer, 16u); EXPECT_EQ(nullptr, sampler); } TEST_F(TextureSamplerTest, cannotCreateSamplerMSForWriteOnlyRenderBuffer) { - const RenderBuffer& renderBuffer = *m_scene.createRenderBuffer(4u, 4u, ERenderBufferType_Color, ERenderBufferFormat_RGBA8, ERenderBufferAccessMode_WriteOnly, 4u); + const RenderBuffer& renderBuffer = *m_scene.createRenderBuffer(4u, 4u, ERenderBufferType::Color, ERenderBufferFormat::RGBA8, ERenderBufferAccessMode::WriteOnly, 4u); TextureSamplerMS* sampler = this->m_scene.createTextureSamplerMS(renderBuffer, "renderBuffer"); EXPECT_EQ(nullptr, sampler); } - TEST_F(TextureSamplerTest, createSamplerForStreamTexture) - { - const StreamTexture& streamTexture = createObject(); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, streamTexture); - - ASSERT_NE(static_cast(nullptr), sampler); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); - EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_StreamTexture, sampler->getTextureType()); - - - const ramses_internal::TextureSamplerHandle samplerHandle = sampler->impl.getTextureSamplerHandle(); - const ramses_internal::StreamTextureHandle streamTextureHandle = streamTexture.impl.getHandle(); - - ASSERT_TRUE(m_internalScene.isTextureSamplerAllocated(samplerHandle)); - EXPECT_EQ(ramses_internal::TextureSampler::ContentType::StreamTexture, m_internalScene.getTextureSampler(samplerHandle).contentType); - EXPECT_EQ(streamTextureHandle.asMemoryHandle(), m_internalScene.getTextureSampler(samplerHandle).contentHandle); - } - TEST_F(TextureSamplerTest, reportsErrorWhenValidatedWithInvalidTexture2D) { Texture2D& texture2D = createObject("testTexture2D"); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture2D, 1u, "testSampler2D"); + const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture2D, 1u, "testSampler2D"); ASSERT_TRUE(nullptr != sampler); EXPECT_EQ(StatusOK, sampler->validate()); @@ -249,7 +225,7 @@ namespace ramses TEST_F(TextureSamplerTest, reportsErrorWhenValidatedWithInvalidTexture3D) { Texture3D& texture3D = createObject("testTexture3D"); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureAddressMode_Mirror, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture3D, "testSampler3D"); + const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureAddressMode::Mirror, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture3D, "testSampler3D"); ASSERT_TRUE(nullptr != sampler); EXPECT_EQ(StatusOK, sampler->validate()); @@ -260,7 +236,7 @@ namespace ramses TEST_F(TextureSamplerTest, reportsErrorWhenValidatedWithInvalidTextureCube) { TextureCube& textureCube = createObject("testTextureCube"); - const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, textureCube, 1u, "testSamplerCube"); + const TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, textureCube, 1u, "testSamplerCube"); ASSERT_TRUE(nullptr != sampler); EXPECT_EQ(StatusOK, sampler->validate()); @@ -281,7 +257,7 @@ namespace ramses renderPass.setCamera(*orthoCam); renderPass.setRenderTarget(rt); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, renderBuffer); + TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, renderBuffer); ASSERT_TRUE(nullptr != sampler); EXPECT_EQ(StatusOK, sampler->validate()); @@ -310,20 +286,9 @@ namespace ramses EXPECT_NE(StatusOK, sampler->validate()); } - TEST_F(TextureSamplerTest, reportsErrorWhenValidatedWithInvalidStreamTexture) - { - StreamTexture& streamTexture = createObject(); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, streamTexture); - ASSERT_TRUE(nullptr != sampler); - EXPECT_EQ(StatusOK, sampler->validate()); - - EXPECT_EQ(StatusOK, m_scene.destroy(streamTexture)); - EXPECT_NE(StatusOK, sampler->validate()); - } - TEST_F(TextureSamplerTest, doesNotReportErrorWhenValidatedWithExternalTexture) { - TextureSamplerExternal* sampler = this->m_scene.createTextureSamplerExternal(ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear); + TextureSamplerExternal* sampler = this->m_scene.createTextureSamplerExternal(ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear); ASSERT_TRUE(nullptr != sampler); EXPECT_EQ(StatusOK, sampler->validate()); } @@ -331,55 +296,55 @@ namespace ramses TEST_F(TextureSamplerTest, setTextureDataToTexture2D) { const Texture3D& texture3D = createObject(); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture3D); + TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture3D); ASSERT_NE(nullptr, sampler); const Texture2D& texture2D = createObject(); EXPECT_EQ(StatusOK, sampler->setTextureData(texture2D)); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapRMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapRMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_Texture2D, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::Texture2D, sampler->getTextureType()); - const ramses_internal::TextureSamplerHandle samplerHandle = sampler->impl.getTextureSamplerHandle(); + const ramses_internal::TextureSamplerHandle samplerHandle = sampler->m_impl.getTextureSamplerHandle(); ASSERT_TRUE(m_internalScene.isTextureSamplerAllocated(samplerHandle)); EXPECT_EQ(1u, m_internalScene.getTextureSamplerCount()); EXPECT_EQ(ramses_internal::TextureSampler::ContentType::ClientTexture, m_internalScene.getTextureSampler(samplerHandle).contentType); - EXPECT_EQ(texture2D.impl.getLowlevelResourceHash(), m_internalScene.getTextureSampler(samplerHandle).textureResource); + EXPECT_EQ(texture2D.m_impl.getLowlevelResourceHash(), m_internalScene.getTextureSampler(samplerHandle).textureResource); } TEST_F(TextureSamplerTest, setTextureDataFromTexture3DToAnotherTexture3D) { const Texture3D& texture3Doriginal = createObject(); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture3Doriginal); + TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture3Doriginal); ASSERT_NE(nullptr, sampler); const Texture3D& texture3D = createObject(); EXPECT_EQ(StatusOK, sampler->setTextureData(texture3D)); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapRMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapRMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_Texture3D, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::Texture3D, sampler->getTextureType()); - const ramses_internal::TextureSamplerHandle samplerHandle = sampler->impl.getTextureSamplerHandle(); + const ramses_internal::TextureSamplerHandle samplerHandle = sampler->m_impl.getTextureSamplerHandle(); ASSERT_TRUE(m_internalScene.isTextureSamplerAllocated(samplerHandle)); EXPECT_EQ(1u, m_internalScene.getTextureSamplerCount()); EXPECT_EQ(ramses_internal::TextureSampler::ContentType::ClientTexture, m_internalScene.getTextureSampler(samplerHandle).contentType); - EXPECT_EQ(texture3D.impl.getLowlevelResourceHash(), m_internalScene.getTextureSampler(samplerHandle).textureResource); + EXPECT_EQ(texture3D.m_impl.getLowlevelResourceHash(), m_internalScene.getTextureSampler(samplerHandle).textureResource); } TEST_F(TextureSamplerTest, failsToSetTextureDataFromTexture2DToTexture3D) { const Texture2D& texture2D = createObject(); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture2D, 1u); + TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture2D, 1u); ASSERT_NE(nullptr, sampler); const Texture3D& texture3D = createObject(); @@ -389,100 +354,77 @@ namespace ramses TEST_F(TextureSamplerTest, setTextureDataToTextureCube) { const Texture3D& texture3D = createObject(); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture3D); + TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture3D); ASSERT_NE(nullptr, sampler); const TextureCube& textureCube = createObject(); EXPECT_EQ(StatusOK, sampler->setTextureData(textureCube)); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapRMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapRMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_TextureCube, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::TextureCube, sampler->getTextureType()); - const ramses_internal::TextureSamplerHandle samplerHandle = sampler->impl.getTextureSamplerHandle(); + const ramses_internal::TextureSamplerHandle samplerHandle = sampler->m_impl.getTextureSamplerHandle(); ASSERT_TRUE(m_internalScene.isTextureSamplerAllocated(samplerHandle)); EXPECT_EQ(1u, m_internalScene.getTextureSamplerCount()); EXPECT_EQ(ramses_internal::TextureSampler::ContentType::ClientTexture, m_internalScene.getTextureSampler(samplerHandle).contentType); - EXPECT_EQ(textureCube.impl.getLowlevelResourceHash(), m_internalScene.getTextureSampler(samplerHandle).textureResource); + EXPECT_EQ(textureCube.m_impl.getLowlevelResourceHash(), m_internalScene.getTextureSampler(samplerHandle).textureResource); } TEST_F(TextureSamplerTest, setTextureDataToTexture2DBuffer) { const Texture2D& texture2D = createObject(); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture2D, 1u); + TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture2D, 1u); ASSERT_NE(nullptr, sampler); const Texture2DBuffer& textureBuffer = createObject(); EXPECT_EQ(StatusOK, sampler->setTextureData(textureBuffer)); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_Texture2DBuffer, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::Texture2DBuffer, sampler->getTextureType()); - const ramses_internal::TextureSamplerHandle samplerHandle = sampler->impl.getTextureSamplerHandle(); + const ramses_internal::TextureSamplerHandle samplerHandle = sampler->m_impl.getTextureSamplerHandle(); ASSERT_TRUE(m_internalScene.isTextureSamplerAllocated(samplerHandle)); EXPECT_EQ(1u, m_internalScene.getTextureSamplerCount()); EXPECT_EQ(ramses_internal::TextureSampler::ContentType::TextureBuffer, m_internalScene.getTextureSampler(samplerHandle).contentType); - EXPECT_EQ(textureBuffer.impl.getTextureBufferHandle().asMemoryHandle(), m_internalScene.getTextureSampler(samplerHandle).contentHandle); + EXPECT_EQ(textureBuffer.m_impl.getTextureBufferHandle().asMemoryHandle(), m_internalScene.getTextureSampler(samplerHandle).contentHandle); } TEST_F(TextureSamplerTest, setTextureDataToRenderBuffer) { const Texture2D& texture2D = createObject(); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture2D, 1u); + TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture2D, 1u); ASSERT_NE(nullptr, sampler); const RenderBuffer& buffer = createObject(); EXPECT_EQ(StatusOK, sampler->setTextureData(buffer)); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapUMode()); + EXPECT_EQ(ETextureAddressMode::Clamp, sampler->getWrapVMode()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMinSamplingMethod()); + EXPECT_EQ(ETextureSamplingMethod::Linear, sampler->getMagSamplingMethod()); EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_RenderBuffer, sampler->getTextureType()); + EXPECT_EQ(ERamsesObjectType::RenderBuffer, sampler->getTextureType()); - const ramses_internal::TextureSamplerHandle samplerHandle = sampler->impl.getTextureSamplerHandle(); + const ramses_internal::TextureSamplerHandle samplerHandle = sampler->m_impl.getTextureSamplerHandle(); ASSERT_TRUE(m_internalScene.isTextureSamplerAllocated(samplerHandle)); EXPECT_EQ(1u, m_internalScene.getTextureSamplerCount()); EXPECT_EQ(ramses_internal::TextureSampler::ContentType::RenderBuffer, m_internalScene.getTextureSampler(samplerHandle).contentType); - EXPECT_EQ(buffer.impl.getRenderBufferHandle().asMemoryHandle(), m_internalScene.getTextureSampler(samplerHandle).contentHandle); - } - - TEST_F(TextureSamplerTest, setTextureDataToStreamTexture) - { - const Texture2D& texture2D = createObject(); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture2D, 1u); - ASSERT_NE(nullptr, sampler); - - const StreamTexture& texture = createObject(); - EXPECT_EQ(StatusOK, sampler->setTextureData(texture)); - - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapUMode()); - EXPECT_EQ(ETextureAddressMode_Clamp, sampler->getWrapVMode()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMinSamplingMethod()); - EXPECT_EQ(ETextureSamplingMethod_Linear, sampler->getMagSamplingMethod()); - EXPECT_EQ(1u, sampler->getAnisotropyLevel()); - EXPECT_EQ(ERamsesObjectType_StreamTexture, sampler->getTextureType()); - - const ramses_internal::TextureSamplerHandle samplerHandle = sampler->impl.getTextureSamplerHandle(); - ASSERT_TRUE(m_internalScene.isTextureSamplerAllocated(samplerHandle)); - EXPECT_EQ(1u, m_internalScene.getTextureSamplerCount()); - EXPECT_EQ(ramses_internal::TextureSampler::ContentType::StreamTexture, m_internalScene.getTextureSampler(samplerHandle).contentType); - EXPECT_EQ(texture.impl.getHandle().asMemoryHandle(), m_internalScene.getTextureSampler(samplerHandle).contentHandle); + EXPECT_EQ(buffer.m_impl.getRenderBufferHandle().asMemoryHandle(), m_internalScene.getTextureSampler(samplerHandle).contentHandle); } TEST_F(TextureSamplerTest, failsToSetTextureDataToSamplerMarkedAsConsumer) { const auto& texture2D = createObject(); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture2D); + TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture2D); ASSERT_NE(nullptr, sampler); ASSERT_EQ(StatusOK, this->m_scene.createTextureConsumer(*sampler, dataConsumerId_t{666u})); @@ -495,28 +437,24 @@ namespace ramses EXPECT_NE(StatusOK, sampler->setTextureData(textureBuffer)); const auto& renderBuffer = createObject(); EXPECT_NE(StatusOK, sampler->setTextureData(renderBuffer)); - const auto& streamTexture = createObject(); - EXPECT_NE(StatusOK, sampler->setTextureData(streamTexture)); } TEST_F(TextureSamplerTest, failsToSetTextureDataFromOtherSceneThanSampler) { const auto& texture2D = createObject(); - TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode_Clamp, ETextureAddressMode_Clamp, ETextureSamplingMethod_Linear, ETextureSamplingMethod_Linear, texture2D); + TextureSampler* sampler = this->m_scene.createTextureSampler(ETextureAddressMode::Clamp, ETextureAddressMode::Clamp, ETextureSamplingMethod::Linear, ETextureSamplingMethod::Linear, texture2D); ASSERT_NE(nullptr, sampler); RamsesClient& otherClient(*this->framework.createClient("other")); - CreationHelper creationHelper(otherClient.createScene(sceneId_t(666u)), nullptr, &otherClient); + CreationHelper creationHelper(otherClient.createScene(sceneId_t(666u)), &otherClient); - const auto& texture2Dother = *creationHelper.createObjectOfType(nullptr); + const auto& texture2Dother = *creationHelper.createObjectOfType({}); EXPECT_NE(StatusOK, sampler->setTextureData(texture2Dother)); - const auto& textureCube = *creationHelper.createObjectOfType(nullptr); + const auto& textureCube = *creationHelper.createObjectOfType({}); EXPECT_NE(StatusOK, sampler->setTextureData(textureCube)); - const auto& textureBuffer = *creationHelper.createObjectOfType(nullptr); + const auto& textureBuffer = *creationHelper.createObjectOfType({}); EXPECT_NE(StatusOK, sampler->setTextureData(textureBuffer)); - const auto& renderBuffer = *creationHelper.createObjectOfType(nullptr); + const auto& renderBuffer = *creationHelper.createObjectOfType({}); EXPECT_NE(StatusOK, sampler->setTextureData(renderBuffer)); - const auto& streamTexture = *creationHelper.createObjectOfType(nullptr); - EXPECT_NE(StatusOK, sampler->setTextureData(streamTexture)); } } diff --git a/client/ramses-client/test/res/ramses-client-test_minimalShader.frag b/client/test/res/ramses-client-test_minimalShader.frag similarity index 100% rename from client/ramses-client/test/res/ramses-client-test_minimalShader.frag rename to client/test/res/ramses-client-test_minimalShader.frag diff --git a/client/ramses-client/test/res/ramses-client-test_minimalShader.geom b/client/test/res/ramses-client-test_minimalShader.geom similarity index 100% rename from client/ramses-client/test/res/ramses-client-test_minimalShader.geom rename to client/test/res/ramses-client-test_minimalShader.geom diff --git a/client/ramses-client/test/res/ramses-client-test_minimalShader.vert b/client/test/res/ramses-client-test_minimalShader.vert similarity index 100% rename from client/ramses-client/test/res/ramses-client-test_minimalShader.vert rename to client/test/res/ramses-client-test_minimalShader.vert diff --git a/client/ramses-client/test/res/ramses-client-test_shader.vert b/client/test/res/ramses-client-test_shader.vert similarity index 100% rename from client/ramses-client/test/res/ramses-client-test_shader.vert rename to client/test/res/ramses-client-test_shader.vert diff --git a/client/ramses-client/test/res/ramses-text-DroidKufi-Regular.ttf b/client/test/res/ramses-text-DroidKufi-Regular.ttf similarity index 100% rename from client/ramses-client/test/res/ramses-text-DroidKufi-Regular.ttf rename to client/test/res/ramses-text-DroidKufi-Regular.ttf diff --git a/client/ramses-client/test/res/ramses-text-Roboto-Bold.ttf b/client/test/res/ramses-text-Roboto-Bold.ttf similarity index 100% rename from client/ramses-client/test/res/ramses-text-Roboto-Bold.ttf rename to client/test/res/ramses-text-Roboto-Bold.ttf diff --git a/client/ramses-client/test/res/ramses-text-Roboto-Regular.ttf b/client/test/res/ramses-text-Roboto-Regular.ttf similarity index 100% rename from client/ramses-client/test/res/ramses-text-Roboto-Regular.ttf rename to client/test/res/ramses-text-Roboto-Regular.ttf diff --git a/client/ramses-client/test/res/rgba8_expectedFlipped.png b/client/test/res/rgba8_expectedFlipped.png similarity index 100% rename from client/ramses-client/test/res/rgba8_expectedFlipped.png rename to client/test/res/rgba8_expectedFlipped.png diff --git a/client/ramses-client/test/res/sampleTexture.png b/client/test/res/sampleTexture.png similarity index 100% rename from client/ramses-client/test/res/sampleTexture.png rename to client/test/res/sampleTexture.png diff --git a/client/ramses-client/test/res/sampleTexture_invalid.png b/client/test/res/sampleTexture_invalid.png similarity index 100% rename from client/ramses-client/test/res/sampleTexture_invalid.png rename to client/test/res/sampleTexture_invalid.png diff --git a/client/ramses-client/test/FontCascadeTest.cpp b/client/test/text/FontCascadeTest.cpp similarity index 98% rename from client/ramses-client/test/FontCascadeTest.cpp rename to client/test/text/FontCascadeTest.cpp index 9d0e0ff88..9bc348561 100644 --- a/client/ramses-client/test/FontCascadeTest.cpp +++ b/client/test/text/FontCascadeTest.cpp @@ -86,7 +86,8 @@ namespace ramses FontInstanceOffsets fontOffsets; const auto filteredStr = FontCascade::FilterAndFindFontInstancesForString(m_fontCascadeLatinThenArabic, str, fontOffsets); - RamsesFramework framework; + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + RamsesFramework framework{config}; RamsesClient& client(*framework.createClient("test")); TextCache textCache(*client.createScene(sceneId_t(1u)), *FRegistry, 64u, 64u); diff --git a/client/ramses-client/test/FontRegistryTest.cpp b/client/test/text/FontRegistryTest.cpp similarity index 100% rename from client/ramses-client/test/FontRegistryTest.cpp rename to client/test/text/FontRegistryTest.cpp diff --git a/client/ramses-client/test/Freetype2FontInstanceTest.cpp b/client/test/text/Freetype2FontInstanceTest.cpp similarity index 100% rename from client/ramses-client/test/Freetype2FontInstanceTest.cpp rename to client/test/text/Freetype2FontInstanceTest.cpp diff --git a/client/ramses-client/test/GlyphTextureAtlasTest.cpp b/client/test/text/GlyphTextureAtlasTest.cpp similarity index 87% rename from client/ramses-client/test/GlyphTextureAtlasTest.cpp rename to client/test/text/GlyphTextureAtlasTest.cpp index c4211d71d..58afd91fc 100644 --- a/client/ramses-client/test/GlyphTextureAtlasTest.cpp +++ b/client/test/text/GlyphTextureAtlasTest.cpp @@ -49,15 +49,15 @@ namespace ramses return false; } - const float x1_lo = glyphGeometry1.texcoords[2]; - const float y1_lo = glyphGeometry1.texcoords[3]; - const float x1_hi = glyphGeometry1.texcoords[6]; - const float y1_hi = glyphGeometry1.texcoords[7]; + const float x1_lo = glyphGeometry1.texcoords[1][0]; + const float y1_lo = glyphGeometry1.texcoords[1][1]; + const float x1_hi = glyphGeometry1.texcoords[3][0]; + const float y1_hi = glyphGeometry1.texcoords[3][1]; - const float x2_lo = glyphGeometry2.texcoords[2]; - const float y2_lo = glyphGeometry2.texcoords[3]; - const float x2_hi = glyphGeometry2.texcoords[6]; - const float y2_hi = glyphGeometry2.texcoords[7]; + const float x2_lo = glyphGeometry2.texcoords[1][0]; + const float y2_lo = glyphGeometry2.texcoords[1][1]; + const float x2_hi = glyphGeometry2.texcoords[3][0]; + const float y2_hi = glyphGeometry2.texcoords[3][0]; const float x_lo = std::max(x1_lo, x2_lo); const float y_lo = std::max(y1_lo, y2_lo); @@ -69,17 +69,17 @@ namespace ramses void verifyGlyphMapping(const GlyphGeometry& glyphGeometry, float x_lo_expected, float y_lo_expected, float x_hi_expected, float y_hi_expected) const { - EXPECT_FLOAT_EQ(glyphGeometry.texcoords[2], x_lo_expected); - EXPECT_FLOAT_EQ(glyphGeometry.texcoords[3], y_lo_expected); - EXPECT_FLOAT_EQ(glyphGeometry.texcoords[6], x_hi_expected); - EXPECT_FLOAT_EQ(glyphGeometry.texcoords[7], y_hi_expected); + EXPECT_FLOAT_EQ(glyphGeometry.texcoords[1][0], x_lo_expected); + EXPECT_FLOAT_EQ(glyphGeometry.texcoords[1][1], y_lo_expected); + EXPECT_FLOAT_EQ(glyphGeometry.texcoords[3][0], x_hi_expected); + EXPECT_FLOAT_EQ(glyphGeometry.texcoords[3][1], y_hi_expected); } void expectGeometrySize(size_t numGlyphs, GlyphGeometry const& glyphGeometry) { - EXPECT_EQ(glyphGeometry.positions.size(), numGlyphs * 8u); + EXPECT_EQ(glyphGeometry.positions.size(), numGlyphs * 4u); EXPECT_EQ(glyphGeometry.indices.size(), numGlyphs * 6u); - EXPECT_EQ(glyphGeometry.texcoords.size(), numGlyphs * 8u); + EXPECT_EQ(glyphGeometry.texcoords.size(), numGlyphs * 4u); } void expectGeometry(int left, int right, int bottom, int top, GlyphGeometry const& geo, int i = 0, int xoffset = 0, int yoffset = 0) @@ -87,17 +87,17 @@ namespace ramses constexpr int padding = 1; constexpr float halfPadding = padding / 2.f; - EXPECT_EQ(geo.positions[i * 8 + 0], left - halfPadding); - EXPECT_EQ(geo.positions[i * 8 + 1], bottom - halfPadding); + EXPECT_EQ(geo.positions[i * 4][0], left - halfPadding); + EXPECT_EQ(geo.positions[i * 4][1], bottom - halfPadding); - EXPECT_EQ(geo.positions[i * 8 + 2], left - halfPadding); - EXPECT_EQ(geo.positions[i * 8 + 3], top + halfPadding); + EXPECT_EQ(geo.positions[i * 4 + 1][0], left - halfPadding); + EXPECT_EQ(geo.positions[i * 4 + 1][1], top + halfPadding); - EXPECT_EQ(geo.positions[i * 8 + 4], right + halfPadding); - EXPECT_EQ(geo.positions[i * 8 + 5], top + halfPadding); + EXPECT_EQ(geo.positions[i * 4 + 2][0], right + halfPadding); + EXPECT_EQ(geo.positions[i * 4 + 2][1], top + halfPadding); - EXPECT_EQ(geo.positions[i * 8 + 6], right + halfPadding); - EXPECT_EQ(geo.positions[i * 8 + 7], bottom - halfPadding); + EXPECT_EQ(geo.positions[i * 4 + 3][0], right + halfPadding); + EXPECT_EQ(geo.positions[i * 4 + 3][1], bottom - halfPadding); EXPECT_EQ(geo.indices[i * 6 + 0], i * 4 + 2); EXPECT_EQ(geo.indices[i * 6 + 1], i * 4 + 1); @@ -107,21 +107,21 @@ namespace ramses EXPECT_EQ(geo.indices[i * 6 + 4], i * 4 + 2); EXPECT_EQ(geo.indices[i * 6 + 5], i * 4 + 0); - EXPECT_NEAR(geo.texcoords[i * 8 + 0], (xoffset + padding - halfPadding) / AtlasTextureWidth, 0.0001f); - EXPECT_NEAR(geo.texcoords[i * 8 + 1], (yoffset + top - bottom + padding + halfPadding) / AtlasTextureHeight, 0.0001f); + EXPECT_NEAR(geo.texcoords[i * 4][0], (xoffset + padding - halfPadding) / AtlasTextureWidth, 0.0001f); + EXPECT_NEAR(geo.texcoords[i * 4][1], (yoffset + top - bottom + padding + halfPadding) / AtlasTextureHeight, 0.0001f); - EXPECT_NEAR(geo.texcoords[i * 8 + 2], (xoffset + padding - halfPadding) / AtlasTextureWidth, 0.0001f); - EXPECT_NEAR(geo.texcoords[i * 8 + 3], (yoffset + padding - halfPadding) / AtlasTextureHeight, 0.0001f); + EXPECT_NEAR(geo.texcoords[i * 4 + 1][0], (xoffset + padding - halfPadding) / AtlasTextureWidth, 0.0001f); + EXPECT_NEAR(geo.texcoords[i * 4 + 1][1], (yoffset + padding - halfPadding) / AtlasTextureHeight, 0.0001f); - EXPECT_NEAR(geo.texcoords[i * 8 + 4], (xoffset + right - left + padding + halfPadding) / AtlasTextureWidth, 0.0001f); - EXPECT_NEAR(geo.texcoords[i * 8 + 5], (yoffset + padding - halfPadding) / AtlasTextureHeight, 0.0001f); + EXPECT_NEAR(geo.texcoords[i * 4 + 2][0], (xoffset + right - left + padding + halfPadding) / AtlasTextureWidth, 0.0001f); + EXPECT_NEAR(geo.texcoords[i * 4 + 2][1], (yoffset + padding - halfPadding) / AtlasTextureHeight, 0.0001f); - EXPECT_NEAR(geo.texcoords[i * 8 + 6], (xoffset + right - left + padding + halfPadding) / AtlasTextureWidth, 0.0001f); - EXPECT_NEAR(geo.texcoords[i * 8 + 7], (yoffset + top - bottom + padding + halfPadding) / AtlasTextureHeight, 0.0001f); + EXPECT_NEAR(geo.texcoords[i * 4 + 3][0], (xoffset + right - left + padding + halfPadding) / AtlasTextureWidth, 0.0001f); + EXPECT_NEAR(geo.texcoords[i * 4 + 3][1], (yoffset + top - bottom + padding + halfPadding) / AtlasTextureHeight, 0.0001f); } protected: - RamsesFramework m_framework; + RamsesFramework m_framework{ RamsesFrameworkConfig{EFeatureLevel_Latest} }; RamsesClient& m_client; Scene& m_scene; diff --git a/client/ramses-client/test/GlyphTexturePageTest.cpp b/client/test/text/GlyphTexturePageTest.cpp similarity index 97% rename from client/ramses-client/test/GlyphTexturePageTest.cpp rename to client/test/text/GlyphTexturePageTest.cpp index 0a5ffcec3..fef4c1933 100644 --- a/client/ramses-client/test/GlyphTexturePageTest.cpp +++ b/client/test/text/GlyphTexturePageTest.cpp @@ -23,13 +23,13 @@ namespace ramses static constexpr uint32_t PageHeight = 16; AGlyphTexturePage(); - ~AGlyphTexturePage() + ~AGlyphTexturePage() override { delete m_glyphPage; } protected: - RamsesFramework m_framework; + RamsesFramework m_framework{ RamsesFrameworkConfig{EFeatureLevel_Latest} }; RamsesClient& m_client; Scene& m_scene; GlyphTexturePage* m_glyphPage; @@ -60,7 +60,7 @@ namespace ramses SinglePixel }; - GlyphTexturePage::QuadIndex getFittingFreeQuadIndex(EClaimedQuadSize size) const + [[nodiscard]] GlyphTexturePage::QuadIndex getFittingFreeQuadIndex(EClaimedQuadSize size) const { QuadSize quadSize = getQuadSize(size); GlyphTexturePage::QuadIndex i(0); @@ -75,12 +75,12 @@ namespace ramses return std::numeric_limits::max(); } - bool canClaimQuad(EClaimedQuadSize size) const + [[nodiscard]] bool canClaimQuad(EClaimedQuadSize size) const { return getFittingFreeQuadIndex(size) != std::numeric_limits::max(); } - QuadSize getQuadSize(EClaimedQuadSize size) const + [[nodiscard]] QuadSize getQuadSize(EClaimedQuadSize size) const { switch (size) { @@ -155,16 +155,13 @@ namespace ramses void expectRamsesObjects(bool expect) { - SceneObjectIterator bufferIter(m_scene, ERamsesObjectType_Texture2DBuffer); - SceneObjectIterator samplerIter(m_scene, ERamsesObjectType_TextureSampler); + SceneObjectIterator bufferIter(m_scene, ERamsesObjectType::Texture2DBuffer); + SceneObjectIterator samplerIter(m_scene, ERamsesObjectType::TextureSampler); EXPECT_EQ(!bufferIter.getNext(), !expect); EXPECT_EQ(!samplerIter.getNext(), !expect); } }; - constexpr uint32_t AGlyphTexturePage::PageWidth; - constexpr uint32_t AGlyphTexturePage::PageHeight; - AGlyphTexturePage::AGlyphTexturePage() : m_client(*m_framework.createClient("text test")) , m_scene(*m_client.createScene(sceneId_t(1u))) diff --git a/client/ramses-client/test/HarfbuzzFontInstanceTest.cpp b/client/test/text/HarfbuzzFontInstanceTest.cpp similarity index 100% rename from client/ramses-client/test/HarfbuzzFontInstanceTest.cpp rename to client/test/text/HarfbuzzFontInstanceTest.cpp diff --git a/client/ramses-client/test/LayoutUtilsHMITest.cpp b/client/test/text/LayoutUtilsHMITest.cpp similarity index 96% rename from client/ramses-client/test/LayoutUtilsHMITest.cpp rename to client/test/text/LayoutUtilsHMITest.cpp index 629bcdd68..f2bfe8ba2 100644 --- a/client/ramses-client/test/LayoutUtilsHMITest.cpp +++ b/client/test/text/LayoutUtilsHMITest.cpp @@ -26,7 +26,8 @@ namespace ramses static void SetUpTestCase() { - Framework = new RamsesFramework; + RamsesFrameworkConfig config{EFeatureLevel_Latest}; + Framework = new RamsesFramework{config}; Client = ALayoutUtilsHMI::Framework->createClient("test"); SScene = Client->createScene(sceneId_t(1u)); diff --git a/client/ramses-client/test/LayoutUtilsTest.cpp b/client/test/text/LayoutUtilsTest.cpp similarity index 100% rename from client/ramses-client/test/LayoutUtilsTest.cpp rename to client/test/text/LayoutUtilsTest.cpp diff --git a/client/ramses-client/test/TextCacheTest.cpp b/client/test/text/TextCacheTest.cpp similarity index 99% rename from client/ramses-client/test/TextCacheTest.cpp rename to client/test/text/TextCacheTest.cpp index ab691eec9..7737a7ac0 100644 --- a/client/ramses-client/test/TextCacheTest.cpp +++ b/client/test/text/TextCacheTest.cpp @@ -92,7 +92,7 @@ namespace ramses return effect; } - RamsesFramework m_framework; + RamsesFramework m_framework{ RamsesFrameworkConfig{EFeatureLevel_Latest} }; RamsesClient& m_client; Scene& m_scene; diff --git a/client/ramses-client/test/UtfUtilsTest.cpp b/client/test/text/UtfUtilsTest.cpp similarity index 100% rename from client/ramses-client/test/UtfUtilsTest.cpp rename to client/test/text/UtfUtilsTest.cpp diff --git a/cmake/modules/FindAndroidSDK.cmake b/cmake/modules/FindAndroidSDK.cmake index 71c8bb4f4..f71fed5eb 100644 --- a/cmake/modules/FindAndroidSDK.cmake +++ b/cmake/modules/FindAndroidSDK.cmake @@ -10,15 +10,15 @@ if (AndroidSDK_FOUND) return() endif() -SET(AndroidSDK_FOUND FALSE) +set(AndroidSDK_FOUND FALSE) -FIND_LIBRARY(ANDROID_LIB android) -IF(ANDROID_LIB) - SET(AndroidSDK_LIBRARIES ${ANDROID_LIB} log) +find_library(ANDROID_LIB android) +if(ANDROID_LIB) + set(AndroidSDK_LIBRARIES ${ANDROID_LIB} log) - MARK_AS_ADVANCED( + mark_as_advanced( AndroidSDK_LIBRARIES ) - SET(AndroidSDK_FOUND TRUE) -ENDIF() + set(AndroidSDK_FOUND TRUE) +endif() diff --git a/cmake/modules/FindBoost.cmake b/cmake/modules/FindBoost.cmake index 43f140a9e..0aafa1a95 100644 --- a/cmake/modules/FindBoost.cmake +++ b/cmake/modules/FindBoost.cmake @@ -148,8 +148,8 @@ IF (Boost_FOUND) message(FATAL_ERROR "boost version could not be extracted from ${Boost_INCLUDE_DIR}/boost/version.hpp (found: ${BOOST_version})") endif() - ACME_INFO("+ Boost (${Boost_SOURCE_TYPE}, ${Boost_LIBRARY_TYPE}, ${BOOST_version})") + message(STATUS "+ Boost (${Boost_SOURCE_TYPE}, ${Boost_LIBRARY_TYPE}, ${BOOST_version})") ELSE() - ACME_INFO("- Boost") + message(STATUS "- Boost") ENDIF() diff --git a/cmake/modules/FindEGL.cmake b/cmake/modules/FindEGL.cmake index 8892d47cb..79228788f 100644 --- a/cmake/modules/FindEGL.cmake +++ b/cmake/modules/FindEGL.cmake @@ -6,35 +6,32 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. # ------------------------------------------------------------------------- -SET( EGL_FOUND FALSE ) - -IF (TARGET_OS MATCHES "Windows") +set( EGL_FOUND FALSE ) +if (CMAKE_SYSTEM_NAME MATCHES "Windows") # Windows does not suppot EGL natively -ELSEIF(TARGET_OS MATCHES "Linux" OR TARGET_OS MATCHES "Android") - FIND_PATH(EGL_INCLUDE_DIRS EGL/egl.h +elseif(CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android") + find_path(EGL_INCLUDE_DIRS EGL/egl.h /usr/include ) - FIND_LIBRARY(EGL_LIBRARY + find_library(EGL_LIBRARY NAMES EGL PATHS ) - SET( EGL_FOUND "NO" ) - IF(EGL_LIBRARY) - SET( EGL_LIBRARIES ${EGL_LIBRARY} ) - SET( EGL_FOUND TRUE ) - #message(STATUS "Found EGL libs: ${EGL_LIBRARIES}") - #message(STATUS "Found EGL includes: ${EGL_INCLUDE_DIRS}") - ENDIF(EGL_LIBRARY) + set( EGL_FOUND "NO" ) + if(EGL_LIBRARY) + set( EGL_LIBRARIES ${EGL_LIBRARY} ) + set( EGL_FOUND TRUE ) + endif() - MARK_AS_ADVANCED( + mark_as_advanced( EGL_INCLUDE_DIRS EGL_LIBRARIES EGL_LIBRARY EGL_FOUND ) -ENDIF() +endif() diff --git a/cmake/modules/FindLinuxInput.cmake b/cmake/modules/FindLinuxInput.cmake index 6cfc4c386..cfe984f6b 100644 --- a/cmake/modules/FindLinuxInput.cmake +++ b/cmake/modules/FindLinuxInput.cmake @@ -6,21 +6,19 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. # ------------------------------------------------------------------------- -SET( LinuxInput_FOUND FALSE) +set( LinuxInput_FOUND FALSE) -IF (TARGET_OS MATCHES "Linux") - - FIND_PATH(LinuxInput_INCLUDE_DIRS linux/input.h +if (CMAKE_SYSTEM_NAME MATCHES "Linux") + find_path(LinuxInput_INCLUDE_DIRS linux/input.h /usr/include ) - IF(LinuxInput_INCLUDE_DIRS) + if(LinuxInput_INCLUDE_DIRS) SET(LinuxInput_FOUND TRUE) - ENDIF() + endif() - MARK_AS_ADVANCED( + mark_as_advanced( LinuxInput_INCLUDE_DIRS LinuxInput_FOUND ) - -ENDIF() +endif() diff --git a/cmake/modules/FindOpenGL.cmake b/cmake/modules/FindOpenGL.cmake index 0060e4307..89b32ff13 100644 --- a/cmake/modules/FindOpenGL.cmake +++ b/cmake/modules/FindOpenGL.cmake @@ -15,7 +15,7 @@ IF(CMAKE_SYSTEM_NAME MATCHES "Windows") # Windows has all OpenGL/WGL symbols in one lib - opengl32.lib SET(OpenGL_LIBRARIES opengl32) - SET(OpenGL_DEFINITIONS "-DDESKTOP_GL") + set(OpenGL_DEFINITIONS "-DDESKTOP_GL") MARK_AS_ADVANCED( OpenGL_INCLUDE_DIRS diff --git a/cmake/modules/FindSphinx.cmake b/cmake/modules/FindSphinx.cmake new file mode 100644 index 000000000..4201947b9 --- /dev/null +++ b/cmake/modules/FindSphinx.cmake @@ -0,0 +1,22 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +# Sphinx is a pip module, has to be on the path +find_program(SPHINX_EXECUTABLE + NAMES sphinx-build + DOC "Path to sphinx-build executable") + +find_program(BREATHE_EXECUTABLE + NAMES breathe-apidoc + DOC "Path to breathe-apidoc executable") + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Sphinx + "Failed to find sphinx-build executable" + SPHINX_EXECUTABLE + BREATHE_EXECUTABLE) diff --git a/cmake/ramses/addCheckerTargets.cmake b/cmake/ramses/addCheckerTargets.cmake index 6c5823644..2283ae3c0 100644 --- a/cmake/ramses/addCheckerTargets.cmake +++ b/cmake/ramses/addCheckerTargets.cmake @@ -6,17 +6,17 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. # ------------------------------------------------------------------------- -IF (ramses-sdk_PYTHON3 AND CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) +if (ramses-sdk_PYTHON3 AND CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) file(GLOB_RECURSE CHECKER_SCRIPTS ${PROJECT_SOURCE_DIR}/scripts/code_style_checker/*.py) - ADD_CUSTOM_TARGET(CHECK_CODE_STYLE + add_custom_target(CHECK_CODE_STYLE COMMAND ${ramses-sdk_PYTHON3} ${PROJECT_SOURCE_DIR}/scripts/code_style_checker/check_all_styles.py WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/scripts/code_style_checker SOURCES ${CHECKER_SCRIPTS} ) - SET_PROPERTY(TARGET CHECK_CODE_STYLE PROPERTY FOLDER "CMakePredefinedTargets") - ACME_INFO("+ CHECK_CODE_STYLE") + set_property(TARGET CHECK_CODE_STYLE PROPERTY FOLDER "CMakePredefinedTargets") + message(STATUS "+ CHECK_CODE_STYLE") -ELSE() - ACME_INFO("- CHECK_CODE_STYLE [missing python]") -ENDIF() +else() + message(STATUS "- CHECK_CODE_STYLE [missing python]") +endif() diff --git a/cmake/ramses/addSubdirectory.cmake b/cmake/ramses/addSubdirectory.cmake new file mode 100644 index 000000000..f3aaaec00 --- /dev/null +++ b/cmake/ramses/addSubdirectory.cmake @@ -0,0 +1,29 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2023 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + + +function(addSubdirectory) + cmake_parse_arguments(SUBDIR "" "PATH;MODE" "" ${ARGV}) + + if(SUBDIR_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unparsed addSubdirectories properties: '${SUBDIR_UNPARSED_ARGUMENTS}'") + endif() + + # TODO this global variable is not needed, FIND_PACKAGE has its own caching + set(GLOBAL_WILL_NOT_FIND_DEPENDENCY "" CACHE INTERNAL "") + + if(${SUBDIR_MODE} STREQUAL "AUTO") + set(GLOBAL_MODULE_DEPENDENCY_CHECK ON) + elseif(${SUBDIR_MODE} STREQUAL "ON") + set(GLOBAL_MODULE_DEPENDENCY_CHECK OFF) + else() + message(FATAL_ERROR "Unsupported mode passed to addSubdirectories: `${SUBDIR_MODE}`") + endif() + + add_subdirectory(${SUBDIR_PATH}) +endfunction() diff --git a/cmake/ramses/buildConfig.cmake b/cmake/ramses/buildConfig.cmake index ef9e63a9b..4e56611c3 100644 --- a/cmake/ramses/buildConfig.cmake +++ b/cmake/ramses/buildConfig.cmake @@ -14,3 +14,75 @@ if(ramses-sdk_PARALLEL_LINK_JOBS) set_property(GLOBAL APPEND PROPERTY JOB_POOLS link_job_pool=${ramses-sdk_PARALLEL_LINK_JOBS}) set(CMAKE_JOB_POOL_LINK link_job_pool) endif() + +function(createBuildConfig) + + set(TARGET_DIRECTORY ${CMAKE_BINARY_DIR}/BuildConfig) + + set(EXPORTED_VARIABLES + PROJECT_DESCRIPTION + PROJECT_HOMEPAGE_URL + RAMSES_VERSION + PROJECT_VERSION_MAJOR + PROJECT_VERSION_MINOR + PROJECT_VERSION_PATCH + CMAKE_BUILD_TYPE + CMAKE_CXX_COMPILER + CMAKE_CXX_COMPILER_ID + CMAKE_CXX_FLAGS + CMAKE_CXX_FLAGS_DEBUG + CMAKE_CXX_FLAGS_RELEASE + CMAKE_SYSTEM_NAME + CMAKE_SYSTEM_VERSION + CMAKE_TOOLCHAIN_FILE + CMAKE_VERSION + GIT_COMMIT_COUNT + GIT_COMMIT_HASH + BUILD_ENV_VERSION_INFO_FULL + ) + + set(EXPORTED_INT_VARIABLES + PROJECT_VERSION_MAJOR + PROJECT_VERSION_MINOR + PROJECT_VERSION_PATCH) + + if (NOT DEFINED GIT_COMMIT_COUNT OR NOT DEFINED GIT_COMMIT_HASH) + message(FATAL_ERROR "GIT_COMMIT_COUNT and GIT_COMMIT_HASH must be set") + endif() + + set(EXPORTED_RAMSES_CONFIG_SYMBOLS "") + + foreach(VAR ${EXPORTED_VARIABLES}) + if(NOT DEFINED ${VAR}) + set(${VAR} "(unknown)") + endif() + + # escape '\' + string(REPLACE "\\" "\\\\" TMP "${${VAR}}") + string(REPLACE "\"" "\\\"" TMP "${TMP}") + set(ESCAPED_${VAR} "${TMP}") + + set(EXPORTED_RAMSES_CONFIG_SYMBOLS "${EXPORTED_RAMSES_CONFIG_SYMBOLS}\nconst char* const RAMSES_SDK_${VAR} = \"@ESCAPED_${VAR}@\";") + endforeach() + + foreach(VAR ${EXPORTED_INT_VARIABLES}) + if(DEFINED ${VAR}) + set(EXPORTED_RAMSES_CONFIG_SYMBOLS "${EXPORTED_RAMSES_CONFIG_SYMBOLS}\nconst int RAMSES_SDK_${VAR}_INT = @${VAR}@;") + endif() + endforeach() + + configure_file( + ${PROJECT_SOURCE_DIR}/cmake/templates/build-config.h.in + ${TARGET_DIRECTORY}/ramses-sdk-build-config.h.in + ) + + configure_file( + ${TARGET_DIRECTORY}/ramses-sdk-build-config.h.in + ${TARGET_DIRECTORY}/ramses-sdk-build-config.h + ) + + message(STATUS "G RamsesBuildConfig.h") + + # TODO Violin replace this with a target, using global includes like this is dangerous + include_directories(${TARGET_DIRECTORY}) +endfunction() diff --git a/cmake/ramses/createPackage.cmake b/cmake/ramses/createPackage.cmake new file mode 100644 index 000000000..fe56f8b7b --- /dev/null +++ b/cmake/ramses/createPackage.cmake @@ -0,0 +1,45 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2023 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +set(CPACK_GENERATOR ${ramses-sdk_CPACK_GENERATOR}) + +set(CPACK_SOURCE_GENERATOR "TGZ") + +if (NOT DEFINED GIT_COMMIT_COUNT OR NOT DEFINED GIT_COMMIT_HASH) + message(FATAL_ERROR "GIT_COMMIT_COUNT and GIT_COMMIT_HASH must be set") +endif() +set(SCM_VERSION "${GIT_COMMIT_COUNT}-${GIT_COMMIT_HASH}") + +if("${CPACK_PACKAGE_NAME}" STREQUAL "") + set(CPACK_PACKAGE_NAME "ramses") +endif() + +set(CPACK_PACKAGE_VERSION "${RAMSES_VERSION}-${SCM_VERSION}") +set(CPACK_SOURCE_STRIP_FILES TRUE) +set(CPACK_STRIP_FILES FALSE) +set(CPACK_PACKAGE_CONTACT "ramses-oss@list.bmw.com") +set(CPACK_PACKAGE_VENDOR "ramses") + +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A distributed 3D rendering framework for embedded systems") +set(CPACK_PACKAGE_DESCRIPTION "A packaged version of ramses. Generated using CPack.") + +if(ramses-sdk_CPACK_GENERATOR STREQUAL "DEB") + # Enables CPack to add proper dependency info to the package, see docs for more info + set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) +endif() + +# Allows providing custom package suffix, use "-" by default +if(RAMSES_CUSTOM_PACKAGE_SUFFIX) + set(PACKAGE_SUFFIX ${RAMSES_CUSTOM_PACKAGE_SUFFIX}) +else() + set(PACKAGE_SUFFIX ${SCM_VERSION}) +endif() + +set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${RAMSES_VERSION}-${PACKAGE_SUFFIX}") + +include(CPack) diff --git a/cmake/ramses/createTarget.cmake b/cmake/ramses/createTarget.cmake new file mode 100644 index 000000000..b49020ad1 --- /dev/null +++ b/cmake/ramses/createTarget.cmake @@ -0,0 +1,233 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2023 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + + +function(setSharedTargetProperties) + cmake_parse_arguments( + TARGET # Prefix of parsed args + "" # Options + # Single-value args: + "NAME;LINK_VISIBILITY" + # Multi-value-args + "INCLUDE_PATHS;DEPENDENCIES;PUBLIC_DEFINES;INTERFACE_DEFINES" + ${ARGN} + ) + + if(TARGET_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unparsed setSharedTargetProperties properties: '${TARGET_UNPARSED_ARGUMENTS}'") + endif() + + if (TARGET_INCLUDE_PATHS) + foreach(inc ${TARGET_INCLUDE_PATHS}) + get_filename_component(inc_resolved "${inc}" ABSOLUTE) + target_include_directories(${TARGET_NAME} PUBLIC "${inc_resolved}") + endforeach() + endif() + + if (TARGET_PUBLIC_DEFINES) + target_compile_definitions(${TARGET_NAME} PUBLIC ${TARGET_PUBLIC_DEFINES}) + endif() + + if (TARGET_INTERFACE_DEFINES) + target_compile_definitions(${TARGET_NAME} INTERFACE ${TARGET_INTERFACE_DEFINES}) + endif() + + if(NOT TARGET_LINK_VISIBILITY) + message(FATAL_ERROR "setSharedTargetProperties: must specify linker visibility!") + endif() + + if(TARGET_DEPENDENCIES) + foreach(DEPENDENCY ${TARGET_DEPENDENCIES}) + if(TARGET ${DEPENDENCY}) + if("${TARGET_LINK_VISIBILITY}" STREQUAL "PUBLIC") + target_link_libraries(${TARGET_NAME} PUBLIC ${DEPENDENCY}) + elseif("${TARGET_LINK_VISIBILITY}" STREQUAL "PRIVATE") + target_link_libraries(${TARGET_NAME} PRIVATE ${DEPENDENCY}) + elseif("${TARGET_LINK_VISIBILITY}" STREQUAL "INTERFACE") + target_link_libraries(${TARGET_NAME} INTERFACE ${DEPENDENCY}) + else() + # TODO Also support interface as visibility specifier and use it for header-only dependencies + message(FATAL_ERROR "linkDependencies: unknown visibility specifier ${TARGET_LINK_VISIBILITY}!") + endif() + else() + # TODO this entire else branch should not be needed with modern CMake + # ensure it was already found by outside dependency checker + if (NOT ${DEPENDENCY}_FOUND) + message(FATAL_ERROR "${TARGET_NAME}: Missing dependency ${DEPENDENCY}") + endif() + + # link includes and libs from vars + if (${DEPENDENCY}_INCLUDE_DIRS) + target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${${DEPENDENCY}_INCLUDE_DIRS}) + endif() + + if(${DEPENDENCY}_LIBRARIES) + target_link_libraries(${TARGET_NAME} PUBLIC ${${DEPENDENCY}_LIBRARIES}) + endif() + endif() + endforeach() + endif() + + folderizeTarget(${TARGET_NAME}) +endfunction() + +function(createModule) + cmake_parse_arguments( + MODULE # Prefix of parsed args + "" # Options + # Single-value-args + "NAME;TYPE;ENABLE_INSTALL" + # Multi-value-args + "SRC_FILES;INCLUDE_PATHS;DEPENDENCIES;RESOURCE_FOLDERS;PUBLIC_DEFINES;INTERFACE_DEFINES" + ${ARGN} + ) + + # Check for configuration errors + if(MODULE_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unparsed createModule properties: '${MODULE_UNPARSED_ARGUMENTS}'") + endif() + + # resolve file wildcards + file(GLOB GLOBBED_MODULE_SRC_FILES LIST_DIRECTORIES false ${MODULE_SRC_FILES}) + + # check, if all dependencies can be resolved + # TODO This can be for sure modernized with newer CMake functionality for dependency resolving + set(MSG "") + set(MODULE_BUILD_ENABLED TRUE) + foreach(DEPENDENCY ${MODULE_DEPENDENCIES}) + if(NOT TARGET ${DEPENDENCY}) + list(FIND GLOBAL_WILL_NOT_FIND_DEPENDENCY ${DEPENDENCY} SKIP_FIND) + + if(NOT ${DEPENDENCY}_FOUND AND SKIP_FIND EQUAL -1) + find_package(${DEPENDENCY} QUIET) + endif() + if(NOT ${DEPENDENCY}_FOUND) + set(GLOBAL_WILL_NOT_FIND_DEPENDENCY "${DEPENDENCY};${GLOBAL_WILL_NOT_FIND_DEPENDENCY}" CACHE INTERNAL "") + + list(APPEND MSG "missing ${DEPENDENCY}") + if(GLOBAL_MODULE_DEPENDENCY_CHECK) + set(MODULE_BUILD_ENABLED FALSE) + endif() + endif() + endif() + endforeach() + + if(NOT MODULE_BUILD_ENABLED) + message(STATUS "- ${MODULE_NAME} [${MSG}]") + set(GLOBAL_WILL_NOT_FIND_DEPENDENCY "${MODULE_NAME};${GLOBAL_WILL_NOT_FIND_DEPENDENCY}" CACHE INTERNAL "") + return() + endif() + + if(NOT "${MSG}" STREQUAL "") + message(" build enabled, but") + foreach(M ${MSG}) + message(" - ${M}") + endforeach() + message(FATAL_ERROR "aborting configuration") + endif() + + message(STATUS "+ ${MODULE_NAME} (${MODULE_TYPE})") + + # TODO create more than one components to make it possible to install libs, tools and test content separately + set(INSTALL_COMPONENT "ramses-sdk-${PROJECT_VERSION}") + + set(LINK_VISIBILITY PRIVATE) + if("${MODULE_TYPE}" STREQUAL "INTERFACE_LIB") + set(LINK_VISIBILITY INTERFACE) + elseif("${MODULE_TYPE}" STREQUAL "BINARY") + add_executable(${MODULE_NAME} ${GLOBBED_MODULE_SRC_FILES}) + set_target_properties(${MODULE_NAME} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") + + # TODO Replace this by a simple script which only copies resources, never installs them + if(MODULE_RESOURCE_FOLDERS) + copyResourcesForTarget( + TARGET ${MODULE_NAME} + FOLDERS ${MODULE_RESOURCE_FOLDERS} + INSTALL ${MODULE_ENABLE_INSTALL} + INSTALL_COMPONENT ${MODULE_INSTALL_COMPONENT}) + endif() + elseif("${MODULE_TYPE}" STREQUAL "STATIC_LIBRARY") + add_library(${MODULE_NAME} STATIC ${GLOBBED_MODULE_SRC_FILES}) + # TODO this looks wrong, should not link all static libs dependencies public! Make settable and fix + set(LINK_VISIBILITY PUBLIC) + elseif("${MODULE_TYPE}" STREQUAL "OBJECT") + add_library(${MODULE_NAME} OBJECT ${GLOBBED_MODULE_SRC_FILES}) + elseif("${MODULE_TYPE}" STREQUAL "SHARED_LIBRARY") + add_library(${MODULE_NAME} SHARED ${GLOBBED_MODULE_SRC_FILES}) + + set_target_properties(${MODULE_NAME} PROPERTIES RAMSES_VERSION "${ramses-sdk_VERSION}") + set_target_properties(${MODULE_NAME} PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}) + get_target_property(MYMODULE_SOVERSION ${MODULE_NAME} SOVERSION) + message(STATUS " setting shared library '${MODULE_NAME}' property RAMSES_VERSION to '${ramses-sdk_VERSION}'") + message(STATUS " setting shared library '${MODULE_NAME}' property SOVERSION to '${MYMODULE_SOVERSION}'") + + elseif("${MODULE_TYPE}" STREQUAL "INTERFACE_LIB") + if(GLOBBED_MODULE_SRC_FILES) + message(FATAL_ERROR "Can't have interface library with source files attached!") + endif() + add_library(${MODULE_NAME} INTERFACE) + else() + message(FATAL_ERROR "Invalid module type '${MODULE_TYPE}'!") + endif() + + setSharedTargetProperties( + NAME ${MODULE_NAME} + INCLUDE_PATHS ${MODULE_INCLUDE_PATHS} + DEPENDENCIES ${MODULE_DEPENDENCIES} + LINK_VISIBILITY ${LINK_VISIBILITY} + PUBLIC_DEFINES ${MODULE_PUBLIC_DEFINES} + INTERFACE_DEFINES ${MODULE_INTERFACE_DEFINES} + ) + + if(MODULE_ENABLE_INSTALL AND ramses-sdk_ENABLE_INSTALL) + if("${MODULE_TYPE}" STREQUAL "INTERFACE_LIB" OR "${MODULE_TYPE}" STREQUAL "OBJECT") + message(FATAL_ERROR "Can't install interface libraries or objects!") + endif() + + # Special case for MSVC which expects DLLs in the executable path + # TODO maybe there is a better solution with modern CMake? + if("${MODULE_TYPE}" STREQUAL "BINARY" OR MSVC) + set(INSTALL_DEST ${RAMSES_INSTALL_RUNTIME_PATH}) + else() + set(INSTALL_DEST ${RAMSES_INSTALL_LIBRARY_PATH}) + endif() + + install(TARGETS ${MODULE_NAME} DESTINATION ${INSTALL_DEST} COMPONENT "${MODULE_INSTALL_COMPONENT}") + if (MSVC) + install(FILES $ DESTINATION ${RAMSES_INSTALL_RUNTIME_PATH} CONFIGURATIONS Debug RelWithDebInfo) + endif() + endif() +endfunction() + + +function(createModuleWithRenderer) + cmake_parse_arguments( + MODULE # Prefix of parsed args + "" # Options + # Single-value-args + "NAME;TYPE;ENABLE_INSTALL" + # Multi-value-args + "SRC_FILES;INCLUDE_PATHS;DEPENDENCIES;RESOURCE_FOLDERS" + ${ARGN} + ) + if(MODULE_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unparsed createModuleWithRenderer properties: '${MODULE_UNPARSED_ARGUMENTS}'") + endif() + + set(MODULE_DEPENDENCIES "ramses-renderer-impl;ramses-build-options-base;${MODULE_DEPENDENCIES}") + + createModule(NAME ${MODULE_NAME} + TYPE ${MODULE_TYPE} + ENABLE_INSTALL ${MODULE_ENABLE_INSTALL} + SRC_FILES ${MODULE_SRC_FILES} + INCLUDE_PATHS ${MODULE_INCLUDE_PATHS} + DEPENDENCIES ${MODULE_DEPENDENCIES} + RESOURCE_FOLDERS ${MODULE_RESOURCE_FOLDERS} + ) + +endfunction() diff --git a/cmake/ramses/externaltools.cmake b/cmake/ramses/externaltools.cmake index e33f5280f..5c299aba9 100644 --- a/cmake/ramses/externaltools.cmake +++ b/cmake/ramses/externaltools.cmake @@ -11,7 +11,7 @@ # ensure python3 can be found (when python2 was searched before) unset(PYTHONINTERP_FOUND CACHE) unset(PYTHON_EXECUTABLE CACHE) -FIND_PACKAGE(PythonInterp 3.6) +find_package(PythonInterp 3.6) if (PYTHONINTERP_FOUND AND PYTHON_EXECUTABLE) message(STATUS "+ python3") @@ -20,10 +20,6 @@ else() message(STATUS "- python3") endif() - -### enable ccache when configured and available -OPTION(ramses-sdk_USE_CCACHE "Enable ccache for build" OFF) - if(ramses-sdk_USE_CCACHE) find_program(CCACHE_EXECUTABLE ccache) if(CCACHE_EXECUTABLE) diff --git a/cmake/ramses/folderize.cmake b/cmake/ramses/folderize.cmake new file mode 100644 index 000000000..836104626 --- /dev/null +++ b/cmake/ramses/folderize.cmake @@ -0,0 +1,45 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2023 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +# Converts e.g. "/some/path/" to "some-path" +function(cmakePathToFolderName OUT) + # first get path relative to ramses root dir + string(REGEX REPLACE "${PROJECT_SOURCE_DIR}/" "" relative_path "${CMAKE_CURRENT_SOURCE_DIR}") + string(REGEX REPLACE "/[^/]*$" "" folder_path "${relative_path}") + if (ramses-sdk_FOLDER_PREFIX) + # use user provided prefix if given + set(folder_path "${ramses-sdk_FOLDER_PREFIX}/${folder_path}") + elseif (NOT CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) + # otherwise adjust path for ramses used as subdirectory + string(REGEX REPLACE "${CMAKE_SOURCE_DIR}/" "" folder_prefix_path "${PROJECT_SOURCE_DIR}") + set(folder_path "${folder_prefix_path}/${folder_path}") + endif() + set(${OUT} ${folder_path} PARENT_SCOPE) +endfunction() + +function(folderizeTarget tgt) + # skip interface libs because VS generator ignores INTERFACE_FOLDER property + get_target_property(tgt_type ${tgt} TYPE) + if (tgt_type STREQUAL INTERFACE_LIBRARY) + return() + endif() + + cmakePathToFolderName(folderName) + set_property(TARGET ${tgt} PROPERTY FOLDER "${folderName}") + + # sort sources in groups + get_target_property(tgt_content ${tgt} SOURCES) + if (tgt_content) + foreach(file_iter ${tgt_content}) + string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/" "" tmp1 "${file_iter}") + string(REGEX REPLACE "/[^/]*$" "" tmp2 "${tmp1}") + string(REPLACE "/" "\\" module_internal_path "${tmp2}") + source_group(${module_internal_path} FILES ${file_iter}) + endforeach() + endif() +endfunction() diff --git a/cmake/ramses/getGitInformation.cmake b/cmake/ramses/getGitInformation.cmake index d75c521c8..694fab6eb 100644 --- a/cmake/ramses/getGitInformation.cmake +++ b/cmake/ramses/getGitInformation.cmake @@ -7,29 +7,29 @@ # ------------------------------------------------------------------------- # use predefined values in dependant repositories -IF (EXISTS "${PROJECT_SOURCE_DIR}/originalGitInformation.txt") - FILE(READ "${PROJECT_SOURCE_DIR}/originalGitInformation.txt" GIT_INFO_FILE_CONTENT) - STRING(REGEX REPLACE "\n" ";" GIT_INFO_FILE_CONTENT "${GIT_INFO_FILE_CONTENT}") - LIST(GET GIT_INFO_FILE_CONTENT 0 GIT_COMMIT_COUNT) - LIST(GET GIT_INFO_FILE_CONTENT 1 GIT_COMMIT_HASH) - MESSAGE(STATUS "Get git info from originalGitInformation.txt: GIT_COMMIT_HASH=${GIT_COMMIT_HASH} GIT_COMMIT_COUNT=${GIT_COMMIT_COUNT}") +if (EXISTS "${PROJECT_SOURCE_DIR}/originalGitInformation.txt") + file(READ "${PROJECT_SOURCE_DIR}/originalGitInformation.txt" GIT_INFO_FILE_CONTENT) + string(REGEX REPLACE "\n" ";" GIT_INFO_FILE_CONTENT "${GIT_INFO_FILE_CONTENT}") + list(GET GIT_INFO_FILE_CONTENT 0 GIT_COMMIT_COUNT) + list(GET GIT_INFO_FILE_CONTENT 1 GIT_COMMIT_HASH) + message(STATUS "Get git info from originalGitInformation.txt: GIT_COMMIT_HASH=${GIT_COMMIT_HASH} GIT_COMMIT_COUNT=${GIT_COMMIT_COUNT}") -ELSE() - FIND_PACKAGE(Git QUIET) +else() + find_package(Git QUIET) - IF(GIT_FOUND) + if(GIT_FOUND) # try get hash and commit count - EXEC_PROGRAM(${GIT_EXECUTABLE} ${PROJECT_SOURCE_DIR} + exec_program(${GIT_EXECUTABLE} ${PROJECT_SOURCE_DIR} ARGS rev-list HEAD --count OUTPUT_VARIABLE GIT_COMMIT_COUNT_TMP RETURN_VALUE GIT_COUNT_RETURN_VALUE) - EXEC_PROGRAM(${GIT_EXECUTABLE} ${PROJECT_SOURCE_DIR} + exec_program(${GIT_EXECUTABLE} ${PROJECT_SOURCE_DIR} ARGS rev-parse --short HEAD OUTPUT_VARIABLE GIT_COMMIT_HASH_TMP RETURN_VALUE GIT_HASH_RETURN_VALUE) # optionally get branch - EXEC_PROGRAM(${GIT_EXECUTABLE} ${PROJECT_SOURCE_DIR} + exec_program(${GIT_EXECUTABLE} ${PROJECT_SOURCE_DIR} ARGS rev-parse --abbrev-ref HEAD OUTPUT_VARIABLE GIT_COMMIT_BRANCH_TMP RETURN_VALUE GIT_BRANCH_RETURN_VALUE) @@ -43,9 +43,8 @@ ELSE() message(STATUS "Get git info from repo: GIT_COMMIT_HASH=${GIT_COMMIT_HASH} GIT_COMMIT_COUNT=${GIT_COMMIT_COUNT} GIT_COMMIT_BRANCH=${GIT_COMMIT_BRANCH}") endif() - ENDIF() - -ENDIF() + endif() +endif() if (NOT GIT_COMMIT_HASH OR NOT GIT_COMMIT_COUNT) message(STATUS "Could not determin git info. Use default values.") diff --git a/cmake/ramses/makeTestFromTarget.cmake b/cmake/ramses/makeTestFromTarget.cmake new file mode 100644 index 000000000..d833fab96 --- /dev/null +++ b/cmake/ramses/makeTestFromTarget.cmake @@ -0,0 +1,102 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2023 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +function(makeTestFromTarget) + cmake_parse_arguments( + TEST # Prefix of parsed args + "SKIPPABLE" # Options + "TARGET;SUFFIX" # Single-value args + "EXTRA_ARGS" # Multi-value-args + ${ARGN} + ) + + if (TEST_SUFFIX STREQUAL "") + message(FATAL_ERROR "makeTestFromTarget: SUFFIX not specified for '${TEST_TARGET}'") + endif() + + set(TEST_NAME "${TEST_TARGET}_${TEST_SUFFIX}") + + if (NOT TARGET ${TEST_TARGET}) + if(TEST_SKIPPABLE) + message(STATUS "Skipping test '${TEST_NAME}' because it's executable ${TEST_TARGET} is missing") + return() + else() + message(FATAL_ERROR "makeTestFromTarget: Test target '${TEST_TARGET}' not found") + endif() + endif() + + # TODO: tests should not have to be installed, this line could be deleted after tests have been refactored to + # not have the requirement to be installed + if(ramses-sdk_ENABLE_INSTALL) + install(TARGETS ${TEST_TARGET} DESTINATION ${RAMSES_INSTALL_RUNTIME_PATH} COMPONENT ramses-tests) + endif() + + add_test( + NAME ${TEST_NAME} + COMMAND ${TEST_TARGET} --gtest_output=xml:${TEST_NAME}.xml ${TEST_EXTRA_ARGS} + WORKING_DIRECTORY ${RAMSES_INSTALL_RUNTIME_PATH} + ) + + # attach environment variable for clang coverage + set_tests_properties(${TEST_NAME} PROPERTIES + ENVIRONMENT LLVM_PROFILE_FILE=${TEST_NAME}_%p.profraw) +endfunction() + + +function(makeTestPerWindowTypeFromTarget) + cmake_parse_arguments( + PTEST # Prefix of parsed args + "" # Options (Unused) + "TARGET;SUFFIX" # Single-value args + "WINDOW_TYPE_FILTER;EXTRA_ARGS" # Multi-value-args + ${ARGN} + ) + + if(ramses-sdk_ENABLE_WINDOW_TYPE_WINDOWS) + list(APPEND TEST_PLATFORMS "windows gles30") + list(APPEND TEST_PLATFORMS "windows gl42") + list(APPEND TEST_PLATFORMS "windows gl45") + endif() + + if(ramses-sdk_ENABLE_WINDOW_TYPE_X11) + list(APPEND TEST_PLATFORMS "x11 gles30") + endif() + + if(ramses-sdk_ENABLE_WINDOW_TYPE_ANDROID) + list(APPEND TEST_PLATFORMS "android gles30") + endif() + + if(ramses-sdk_ENABLE_WINDOW_TYPE_WAYLAND_IVI) + list(APPEND TEST_PLATFORMS "wayland-ivi gles30") + endif() + + if(ramses-sdk_ENABLE_WINDOW_TYPE_WAYLAND_WL_SHELL) + list(APPEND TEST_PLATFORMS "wayland-wl-shell gles30") + endif() + + foreach(TEST_PLATFORM IN LISTS TEST_PLATFORMS) + string(REPLACE " " ";" TEST_PLATFORM_ ${TEST_PLATFORM}) + list(GET TEST_PLATFORM_ 0 TEST_PLATFORM_WINDOW) + list(GET TEST_PLATFORM_ 1 TEST_PLATFORM_DEVICE) + + #if filter exists and window types is not in filter -> continue + if(PTEST_WINDOW_TYPE_FILTER) + if(NOT TEST_PLATFORM_WINDOW IN_LIST PTEST_WINDOW_TYPE_FILTER) + continue() + endif() + endif() + + makeTestFromTarget( + TARGET ${PTEST_TARGET} + SUFFIX ${TEST_PLATFORM_WINDOW}-${TEST_PLATFORM_DEVICE}_${PTEST_SUFFIX} + EXTRA_ARGS ${PTEST_EXTRA_ARGS} --window-type ${TEST_PLATFORM_WINDOW} --device-type ${TEST_PLATFORM_DEVICE} + ${PTEST_UNPARSED_ARGUMENTS} + ) + + endforeach() +endfunction() diff --git a/cmake/ramses/platformConfig.cmake b/cmake/ramses/platformConfig.cmake index 9752ad4dc..c1fd4732a 100644 --- a/cmake/ramses/platformConfig.cmake +++ b/cmake/ramses/platformConfig.cmake @@ -7,7 +7,7 @@ # ------------------------------------------------------------------------- # check selected c++ version -set(ramses-sdk_ALLOWED_CPP_VERSIONS 14 17 20) +set(ramses-sdk_ALLOWED_CPP_VERSIONS 17 20) if (NOT ramses-sdk_CPP_VERSION IN_LIST ramses-sdk_ALLOWED_CPP_VERSIONS) message(FATAL_ERROR "ramses-sdk_CPP_VERSION=${ramses-sdk_CPP_VERSION} must be in ${ramses-sdk_ALLOWED_CPP_VERSIONS}") endif() @@ -24,38 +24,38 @@ set(CMAKE_CXX_EXTENSIONS OFF) add_library(ramses-build-options-base INTERFACE) # helper function to add and remove flags -FUNCTION(ADD_FLAGS VAR) - SET(TMP "${${VAR}}") - FOREACH(flags ${ARGN}) - SET(TMP "${TMP} ${flags}") - ENDFOREACH() - SET(${VAR} ${TMP} PARENT_SCOPE) -ENDFUNCTION() - -FUNCTION(REMOVE_FROM_FLAGS flags toRemoveList outVar) +function(addFlags VAR) + set(TMP "${${VAR}}") + foreach(flags ${ARGN}) + set(TMP "${TMP} ${flags}") + endforeach() + set(${VAR} ${TMP} PARENT_SCOPE) +endfunction() + +function(removeFlags flags toRemoveList outVar) string(REGEX REPLACE " +" ";" flags_LIST "${flags}") # to list list(REMOVE_ITEM flags_LIST ${toRemoveList}) # filter list string(REPLACE ";" " " flags_filtered "${flags_LIST}") # to string set(${outVar} "${flags_filtered}" PARENT_SCOPE) -ENDFUNCTION() +endfunction() # variables to fill -SET(RAMSES_C_CXX_FLAGS) -SET(RAMSES_C_FLAGS) -SET(RAMSES_CXX_FLAGS) -SET(RAMSES_DEBUG_FLAGS) -SET(RAMSES_DEBUG_INFO_FLAGS) -SET(RAMSES_RELEASE_FLAGS) +set(RAMSES_C_CXX_FLAGS) +set(RAMSES_C_FLAGS) +set(RAMSES_CXX_FLAGS) +set(RAMSES_DEBUG_FLAGS) +set(RAMSES_DEBUG_INFO_FLAGS) +set(RAMSES_RELEASE_FLAGS) # gcc OR clang (they share a lot) -IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") - ADD_FLAGS(RAMSES_C_CXX_FLAGS "-fPIC -pthread -fvisibility=hidden") - ADD_FLAGS(RAMSES_CXX_FLAGS "-std=c++${ramses-sdk_CPP_VERSION}") - ADD_FLAGS(RAMSES_C_FLAGS "-std=c11") - ADD_FLAGS(RAMSES_DEBUG_FLAGS "-ggdb -D_DEBUG -fno-omit-frame-pointer") - ADD_FLAGS(RAMSES_RELEASE_FLAGS "-O2 -DNDEBUG -fstack-protector-strong -D_FORTIFY_SOURCE=2") - ADD_FLAGS(RAMSES_DEBUG_INFO_FLAGS "-ggdb -fno-omit-frame-pointer") +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + addFlags(RAMSES_C_CXX_FLAGS "-fPIC -pthread -fvisibility=hidden") + addFlags(RAMSES_CXX_FLAGS "-std=c++${ramses-sdk_CPP_VERSION}") + addFlags(RAMSES_C_FLAGS "-std=c11") + addFlags(RAMSES_DEBUG_FLAGS "-ggdb -D_DEBUG -fno-omit-frame-pointer") + addFlags(RAMSES_RELEASE_FLAGS "-O2 -DNDEBUG -fstack-protector-strong -D_FORTIFY_SOURCE=2") + addFlags(RAMSES_DEBUG_INFO_FLAGS "-ggdb -fno-omit-frame-pointer") target_compile_options(ramses-build-options-base INTERFACE -Wall -Wextra -Wcast-align -Wshadow -Wformat -Wformat-security -Wvla -Wmissing-include-dirs @@ -67,17 +67,17 @@ IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" STREQ if (ramses-sdk_USE_LINKER_OVERWRITE) message(STATUS "+ Use linker '${ramses-sdk_USE_LINKER_OVERWRITE}'") - SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=${ramses-sdk_USE_LINKER_OVERWRITE}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=${ramses-sdk_USE_LINKER_OVERWRITE}") endif() -ENDIF() +endif() # gcc specific -IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") # optimize for debuggability - ADD_FLAGS(RAMSES_DEBUG_FLAGS "-Og") + addFlags(RAMSES_DEBUG_FLAGS "-Og") # remap GOT readonly after resolving - ADD_FLAGS(RAMSES_C_CXX_FLAGS "-Wl,-z,relro,-z,now") + addFlags(RAMSES_C_CXX_FLAGS "-Wl,-z,relro,-z,now") if (ramses-sdk_BUILD_WITH_LTO) target_compile_options(ramses-build-options-base INTERFACE -flto -Wodr -Wlto-type-mismatch) @@ -91,14 +91,14 @@ IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") target_compile_options(ramses-build-options-base INTERFACE -Wno-implicit-fallthrough) # disable unfixed warnings from gcc 8 - IF(NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 8.0) + if(NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 8.0) target_compile_options(ramses-build-options-base INTERFACE -Wno-cast-function-type) - ENDIF() + endif() # disable stringop overflow from gcc 11 - needed for lodepnd - IF(NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11.0) + if(NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11.0) target_compile_options(ramses-build-options-base INTERFACE -Wno-stringop-overflow) - ENDIF() + endif() # handle enable coverage @@ -107,16 +107,16 @@ IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") target_compile_options(ramses-build-options-base INTERFACE --coverage) target_link_options(ramses-build-options-base INTERFACE --coverage) endif() -ENDIF() +endif() # clang specific -IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") target_compile_options(ramses-build-options-base INTERFACE - -Winconsistent-missing-override -Wmove + -Wimplicit-fallthrough -Winconsistent-missing-override -Wmove -Winconsistent-missing-destructor-override) # do not optimize debug build at all (-Og is wrong on clang) - ADD_FLAGS(RAMSES_DEBUG_FLAGS "-O0") + addFlags(RAMSES_DEBUG_FLAGS "-O0") # handle enable coverage if (ramses-sdk_ENABLE_COVERAGE) @@ -136,78 +136,63 @@ IF("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") if (ramses-sdk_ENABLE_SANITIZER STREQUAL "ubsan") message(STATUS "+ Enable undefined behavior sanitizer (ubsan)") set(sanitizer_options "${ubsan_options}") - elseif(ramses-sdk_ENABLE_SANITIZER STREQUAL "tsan") message(STATUS "+ Enable thread sanitizer (tsan)") set(sanitizer_options "${tsan_options}") - elseif(ramses-sdk_ENABLE_SANITIZER STREQUAL "asan") message(STATUS "+ Enable address sanitizer (asan)") set(sanitizer_options "${asan_options}") - elseif(ramses-sdk_ENABLE_SANITIZER STREQUAL "asan+ubsan") message(STATUS "+ Enable address and undefined behavior sanitizers (asan+ubsan)") set(sanitizer_options "${asan_options} ${ubsan_options}") - else() message(FATAL_ERROR "Unknown value for ramses-sdk_ENABLE_SANITIZER '${ramses-sdk_ENABLE_SANITIZER}'") endif() - ADD_FLAGS(RAMSES_C_CXX_FLAGS "-fno-omit-frame-pointer ${sanitizer_options}") + addFlags(RAMSES_C_CXX_FLAGS "-fno-omit-frame-pointer ${sanitizer_options}") endif() -ENDIF() - -# flags for integrity -IF(${CMAKE_SYSTEM_NAME} MATCHES "Integrity") - target_compile_options(ramses-build-options-base INTERFACE --diag_suppress=381,111,2008,620,82,1974,1932,1721,1704,540,68,991,177,174 --pending_instantiations=200 --exceptions) - ADD_FLAGS(CMAKE_EXE_LINKER_FLAGS "--c++${ramses-sdk_CPP_VERSION}") - ADD_FLAGS(RAMSES_RELEASE_FLAGS "-DNDEBUG") - - if (ramses-sdk_WARNINGS_AS_ERRORS) - target_compile_options(ramses-build-options-base INTERFACE --quit_after_warnings) - endif() - - # integrity is an unknown system to the eglplatform.h header - # so we manually define the correct choice for integrity here - ADD_DEFINITIONS("-D__WINSCW__" "-DGTEST_HAS_TYPED_TEST=1" "-DGTEST_HAS_TYPED_TEST_P=1") -ENDIF() +endif() # flags for windows -IF(${CMAKE_SYSTEM_NAME} MATCHES "Windows") - REMOVE_FROM_FLAGS("${CMAKE_CXX_FLAGS}" "/W1;/W2;/W3;/W4" CMAKE_CXX_FLAGS) +if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + removeFlags("${CMAKE_CXX_FLAGS}" "/W1;/W2;/W3;/W4" CMAKE_CXX_FLAGS) - ADD_FLAGS(RAMSES_C_CXX_FLAGS "/MP /DNOMINMAX") - ADD_FLAGS(RAMSES_CXX_FLAGS "/std:c++${ramses-sdk_CPP_VERSION} /bigobj") + addFlags(RAMSES_C_CXX_FLAGS "/MP /DNOMINMAX") + addFlags(RAMSES_CXX_FLAGS "/std:c++${ramses-sdk_CPP_VERSION} /bigobj") target_compile_options(ramses-build-options-base INTERFACE /W4 /wd4503 /wd4265 /wd4201 /wd4127 /wd4996 /wd4702) - ADD_FLAGS(RAMSES_RELEASE_FLAGS "/MD /O2 /Ob2 /DNDEBUG") - ADD_FLAGS(RAMSES_DEBUG_FLAGS "/MDd /Zi /Od /D_DEBUG") + addFlags(RAMSES_RELEASE_FLAGS "/MD /O2 /Ob2 /DNDEBUG") + addFlags(RAMSES_DEBUG_FLAGS "/MDd /Zi /Od /D_DEBUG") target_compile_options(ramses-build-options-base INTERFACE $<$:/RTC1>) - ADD_FLAGS(RAMSES_DEBUG_INFO_FLAGS "/Zi") + addFlags(RAMSES_DEBUG_INFO_FLAGS "/Zi") if (ramses-sdk_WARNINGS_AS_ERRORS) target_compile_options(ramses-build-options-base INTERFACE /WX) endif() - ADD_DEFINITIONS("-D_WIN32_WINNT=0x0600" "-DWINVER=0x0600") # enable 'modern' windows APIs -ENDIF() + add_definitions("-D_WIN32_WINNT=0x0600" "-DWINVER=0x0600") # enable 'modern' windows APIs -IF(${CMAKE_SYSTEM_NAME} MATCHES "Android") - SET(ENV{PKG_CONFIG_PATH} "") - SET(ENV{PKG_CONFIG_LIBDIR} "${CMAKE_SYSROOT}/usr/lib/pkgconfig:${CMAKE_SYSROOT}/usr/share/pkgconfig") - SET(ENV{PKG_CONFIG_SYSROOT_DIR} ${CMAKE_SYSROOT}) -ENDIF() + if (${MSVC_TOOLSET_VERSION} LESS_EQUAL "141") # disable warning unreachable code for VS 2017 due to false positives + addFlags(RAMSES_C_CXX_FLAGS "/wd4702") + endif() +endif() + +if(${CMAKE_SYSTEM_NAME} MATCHES "Android") + set(ENV{PKG_CONFIG_PATH} "") + set(ENV{PKG_CONFIG_LIBDIR} "${CMAKE_SYSROOT}/usr/lib/pkgconfig:${CMAKE_SYSROOT}/usr/share/pkgconfig") + set(ENV{PKG_CONFIG_SYSROOT_DIR} ${CMAKE_SYSROOT}) +endif() -IF((${CMAKE_SYSTEM_NAME} MATCHES "Darwin") OR (${CMAKE_SYSTEM_NAME} MATCHES "iOS")) - ADD_FLAGS(RAMSES_C_FLAGS "-x objective-c") - SET(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++14") - SET(CMAKE_XCODE_GENERATE_SCHEME ON) -ENDIF() +if((${CMAKE_SYSTEM_NAME} MATCHES "Darwin") OR (${CMAKE_SYSTEM_NAME} MATCHES "iOS")) + addFlags(RAMSES_C_FLAGS "-x objective-c") + set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++14") + set(CMAKE_XCODE_GENERATE_SCHEME ON) +endif() # distribute to the correct cmake variables -SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${RAMSES_CXX_FLAGS} ${RAMSES_C_CXX_FLAGS} ") -SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${RAMSES_C_FLAGS} ${RAMSES_C_CXX_FLAGS} ") - -SET(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${RAMSES_DEBUG_FLAGS}") -SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${RAMSES_DEBUG_FLAGS}") -SET(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${RAMSES_RELEASE_FLAGS}") -SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} ${RAMSES_RELEASE_FLAGS}") -SET(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} ${RAMSES_RELEASE_FLAGS} ${RAMSES_DEBUG_INFO_FLAGS}") -SET(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} ${RAMSES_RELEASE_FLAGS} ${RAMSES_DEBUG_INFO_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${RAMSES_CXX_FLAGS} ${RAMSES_C_CXX_FLAGS} ") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${RAMSES_C_FLAGS} ${RAMSES_C_CXX_FLAGS} ") + +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${RAMSES_DEBUG_FLAGS}") +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${RAMSES_DEBUG_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${RAMSES_RELEASE_FLAGS}") +set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} ${RAMSES_RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} ${RAMSES_RELEASE_FLAGS} ${RAMSES_DEBUG_INFO_FLAGS}") +set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} ${RAMSES_RELEASE_FLAGS} ${RAMSES_DEBUG_INFO_FLAGS}") diff --git a/cmake/ramses/platformDetection.cmake b/cmake/ramses/platformDetection.cmake index 8c6a4b7d5..6e8abce97 100644 --- a/cmake/ramses/platformDetection.cmake +++ b/cmake/ramses/platformDetection.cmake @@ -10,29 +10,26 @@ # exclude unsupported platforms if (${CMAKE_SYSTEM_NAME} MATCHES "Windows" AND MSVC) - IF (NOT MSVC_VERSION GREATER 1909) # VS2017 is 1910 - 1919 - MESSAGE(FATAL_ERROR "Visual Studio 2017 or later is required to build ramses") - ENDIF() - + if (NOT MSVC_VERSION GREATER 1909) # VS2017 is 1910 - 1919 + message(FATAL_ERROR "Visual Studio 2017 or later is required to build ramses") + endif() elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7) message(FATAL_ERROR "GCC 7 or new required to build ramses") - elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 6) message(FATAL_ERROR "Clang 6 or new required to build ramses") endif() -# guess TARGET_OS -IF ((CMAKE_SYSTEM_NAME STREQUAL "Windows") OR (CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_SYSTEM_NAME STREQUAL "Darwin") OR (CMAKE_SYSTEM_NAME STREQUAL "iOS") OR (CMAKE_SYSTEM_NAME STREQUAL "Integrity") OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) - SET(TARGET_OS "${CMAKE_SYSTEM_NAME}") -ELSE() - MESSAGE(FATAL_ERROR "Unknown CMAKE_SYSTEM_NAME '${CMAKE_SYSTEM_NAME}'") -ENDIF() +if ((CMAKE_SYSTEM_NAME STREQUAL "Windows") OR (CMAKE_SYSTEM_NAME STREQUAL "Linux") OR (CMAKE_SYSTEM_NAME STREQUAL "Darwin") OR (CMAKE_SYSTEM_NAME STREQUAL "iOS") OR (CMAKE_SYSTEM_NAME STREQUAL "Integrity") OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) + # We support these systems +else() + message(FATAL_ERROR "Unknown CMAKE_SYSTEM_NAME '${CMAKE_SYSTEM_NAME}'") +endif() # guess target bitness -IF (${CMAKE_SIZEOF_VOID_P} EQUAL 4) +if (${CMAKE_SIZEOF_VOID_P} EQUAL 4) SET(TARGET_BITNESS 32) ELSEIF(${CMAKE_SIZEOF_VOID_P} EQUAL 8) SET(TARGET_BITNESS 64) -ELSE() - MESSAGE(FATAL_ERROR "Unknown sizeof(void*) '${CMAKE_SIZEOF_VOID_P}'") -ENDIF() +else() + message(FATAL_ERROR "Unknown sizeof(void*) '${CMAKE_SIZEOF_VOID_P}'") +endif() diff --git a/cmake/ramses/platformTargets.cmake b/cmake/ramses/platformTargets.cmake index 4dfc5a657..45afcd7de 100644 --- a/cmake/ramses/platformTargets.cmake +++ b/cmake/ramses/platformTargets.cmake @@ -14,13 +14,13 @@ add_library(ramses-common-base INTERFACE) # add platform specific libraries -IF (("${CMAKE_SYSTEM_NAME}" STREQUAL "iOS") OR ("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")) +if (("${CMAKE_SYSTEM_NAME}" STREQUAL "iOS") OR ("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")) set(CMAKE_THREAD_LIBS_INIT "-lpthread") set(CMAKE_HAVE_THREADS_LIBRARY 1) set(CMAKE_USE_WIN32_THREADS_INIT 0) set(CMAKE_USE_PTHREADS_INIT 1) set(THREADS_PREFER_PTHREAD_FLAG ON) -ELSEIF (NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "Integrity") +else () set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) target_link_libraries(ramses-common-base INTERFACE Threads::Threads) diff --git a/cmake/ramses/rendererModulePerConfig.cmake b/cmake/ramses/rendererModulePerConfig.cmake deleted file mode 100644 index d21b1e2e9..000000000 --- a/cmake/ramses/rendererModulePerConfig.cmake +++ /dev/null @@ -1,83 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (C) 2018 BMW Car IT GmbH -# ------------------------------------------------------------------------- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -# ------------------------------------------------------------------------- - -SET(RENDERER_CONFIG_LIST - "x11-egl-es-3-0" - - "wayland-ivi-egl-es-3-0" - "wayland-shell-egl-es-3-0" - - "windows-wgl-es-3-0" - "windows-wgl-4-2-core" - "windows-wgl-4-5" - - "android-egl-es-3-0" - ) - -#helper macro -FUNCTION(RENDERER_MODULE_PER_CONFIG MODULE_PREFIX_NAME RENDERER_SPECIFIC_DEPENDENCIES) - - SET(ADDITIONAL_MODULE_SETTINGS ${ARGN}) - - # always allow disabling when dependency missing (similar to ACME_PROJECT option AUTO) - SET(ACME_ENABLE_DEPENDENCY_CHECK ON) - - FOREACH(PLATFORM_NAME ${RENDERER_CONFIG_LIST}) - - if (TARGET platform-${PLATFORM_NAME}) - SET(MYMODULE_NAME "${MODULE_PREFIX_NAME}-${PLATFORM_NAME}") - - # build acme module for this configuration - ACME_MODULE(NAME ${MYMODULE_NAME} - DEPENDENCIES ${RENDERER_SPECIFIC_DEPENDENCIES} - ramses-build-options-base - ${ADDITIONAL_MODULE_SETTINGS}) - - IF("${ACME_TYPE}" STREQUAL "SHARED_LIBRARY" AND TARGET ${MYMODULE_NAME}) - SET_TARGET_PROPERTIES(${MYMODULE_NAME} PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}) - GET_TARGET_PROPERTY(MYMODULE_SOVERSION ${MYMODULE_NAME} SOVERSION) - ACME_INFO(" setting shared library version to ${MYMODULE_SOVERSION}") - ENDIF() - endif() - - ENDFOREACH() - -ENDFUNCTION() - -FUNCTION(RENDERER_MODULE_PER_CONFIG_STATIC MODULE_PREFIX_NAME) - - SET(ADDITIONAL_MODULE_SETTINGS ${ARGN}) - - RENDERER_MODULE_PER_CONFIG(${MODULE_PREFIX_NAME} - "ramses-renderer-lib;platform-\${PLATFORM_NAME}" - ${ADDITIONAL_MODULE_SETTINGS}) - -ENDFUNCTION() - -FUNCTION(RENDERER_MODULE_PER_CONFIG_DYNAMIC MODULE_PREFIX_NAME) - - SET(ADDITIONAL_MODULE_SETTINGS ${ARGN}) - - RENDERER_MODULE_PER_CONFIG(${MODULE_PREFIX_NAME} - "ramses-shared-lib-\${PLATFORM_NAME}" - ${ADDITIONAL_MODULE_SETTINGS}) - -ENDFUNCTION() - -FUNCTION(MODULE_WITH_SHARED_LIBRARY) - FOREACH(PLATFORM_NAME ${RENDERER_CONFIG_LIST}) - SET(RAMSES_PLATFORM_SHLIB_NAME ramses-shared-lib-${PLATFORM_NAME}) - IF (TARGET ${RAMSES_PLATFORM_SHLIB_NAME}) - ACME_MODULE( - DEPENDENCIES ${RAMSES_PLATFORM_SHLIB_NAME} - ramses-build-options-base - ${ARGN}) - BREAK() - ENDIF() - ENDFOREACH() -ENDFUNCTION() diff --git a/cmake/ramses/resourceCopy.cmake b/cmake/ramses/resourceCopy.cmake new file mode 100644 index 000000000..2de68e4a2 --- /dev/null +++ b/cmake/ramses/resourceCopy.cmake @@ -0,0 +1,80 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2023 BMW AG +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +function(copyResourcesForTarget) + cmake_parse_arguments( + RES # Prefix of parsed args + "" # Options + "TARGET;INSTALL;INSTALL_COMPONENT" # Single-value args + "FOLDERS" # Multi-value-args + ${ARGN} + ) + + if (NOT TARGET ${RES_TARGET}) + message(FATAL_ERROR "Target ${RES_TARGET} does not exist") + endif() + + # install whole folders if requested + # TODO: don't install resources (fix tests and remove installation here) + if (RES_INSTALL AND ramses-sdk_ENABLE_INSTALL) + foreach(res_folder ${RES_FOLDERS}) + install(DIRECTORY ${res_folder}/ DESTINATION ${RAMSES_INSTALL_RESOURCES_PATH} COMPONENT "${RES_INSTALL_COMPONENT}") + endforeach() + endif() + + # create copy target for directories + foreach(res_folder ${RES_FOLDERS}) + get_filename_component(dir "${res_folder}" ABSOLUTE) + + if (NOT IS_DIRECTORY "${dir}") + message(FATAL_ERROR "${RES_TARGET} has invalid RESOURCE_FOLDERS entry ${res_folder}") + endif() + + # generate dir target name + string(MD5 dir_hash "${dir}") + set(target_name "rescopy-${dir_hash}") + + # collect files + set(output_dir_base "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res") + file(GLOB_RECURSE dir_files_rel RELATIVE "${dir}" "${dir}/*") + set(dir_files_src) + set(dir_files_dst) + foreach (file ${dir_files_rel}) + list(APPEND dir_files_src "${dir}/${file}") + list(APPEND dir_files_dst "${output_dir_base}/${file}") + endforeach() + + # add files to target sources + target_sources(${RES_TARGET} PRIVATE ${dir_files_src}) + + # check if already copy target fir dir + get_property(dir_copy_target DIRECTORY "${PROJECT_SOURCE_DIR}" PROPERTY ACME_DIR_COPY_${dir_hash}) + if (dir_copy_target) + add_dependencies(${RES_TARGET} ${dir_copy_target}) + else() + # no copy target yet, create one + add_custom_command( + OUTPUT ${dir_files_dst} + COMMAND ${CMAKE_COMMAND} -E copy_directory "${dir}" "${output_dir_base}" + DEPENDS "${dir_files_src}" + COMMENT "Copying ${dir} -> ${output_dir_base}" + ) + + add_custom_target(${target_name} DEPENDS ${dir_files_dst}) + set_property(TARGET ${target_name} PROPERTY FOLDER "CMakePredefinedTargets/rescopy") + + add_dependencies(${RES_TARGET} ${target_name}) + + # store target name + set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY ACME_DIR_COPY_${dir_hash} ${target_name}) + endif() + + # TODO check uniqueness (?) + + endforeach() +endfunction() diff --git a/cmake/ramses/setCmakePolicies.cmake b/cmake/ramses/setCmakePolicies.cmake index ad2124f0f..70eee667d 100644 --- a/cmake/ramses/setCmakePolicies.cmake +++ b/cmake/ramses/setCmakePolicies.cmake @@ -6,6 +6,8 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. # ------------------------------------------------------------------------- +cmake_policy(SET CMP0048 NEW) + # Only interpret if() arguments as variables or keywords when unquoted. if (POLICY CMP0054) cmake_policy(SET CMP0054 NEW) @@ -20,3 +22,7 @@ endif() if (POLICY CMP0076) cmake_policy(SET CMP0076 NEW) endif() + +if (POLICY CMP0022) + cmake_policy(SET CMP0022 NEW) +endif() diff --git a/cmake/ramses/testConfig.cmake b/cmake/ramses/testConfig.cmake index bd873b7c6..b1a9aa818 100644 --- a/cmake/ramses/testConfig.cmake +++ b/cmake/ramses/testConfig.cmake @@ -9,7 +9,7 @@ # configure valgrind memcheck set(MEMORYCHECK_COMMAND_OPTIONS - "--log-fd=1 --error-exitcode=1 --leak-check=full --show-leak-kinds=definite,possible --errors-for-leak-kinds=definite,possible --undef-value-errors=yes --track-origins=no --child-silent-after-fork=yes --trace-children=yes --num-callers=50 --fullpath-after=${PROJECT_SOURCE_DIR}/ --gen-suppressions=all" + "--log-fd=1 --error-exitcode=1 --leak-check=full --show-leak-kinds=definite,possible --errors-for-leak-kinds=definite,possible --undef-value-errors=yes --track-origins=no --child-silent-after-fork=yes --trace-children=yes --num-callers=50 --fullpath-after=${PROJECT_SOURCE_DIR}/ --gen-suppressions=all --suppressions=${PROJECT_SOURCE_DIR}/scripts/ci/config/valgrind/suppressions" CACHE INTERNAL "") # enable ctest diff --git a/cmake/templates/build-config.h.in b/cmake/templates/build-config.h.in new file mode 100644 index 000000000..f527bf3b4 --- /dev/null +++ b/cmake/templates/build-config.h.in @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------- +// Copyright (C) 2023 BMW AG +// ------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// ------------------------------------------------------------------------- + +// this file is auto-generated - do not modify it + +#ifndef RAMSES_BUILDCONFIG_H +#define RAMSES_BUILDCONFIG_H + +namespace ramses_sdk +{ +@EXPORTED_RAMSES_CONFIG_SYMBOLS@ +} + +#endif diff --git a/cmake/templates/ramses-shared-lib-client-onlyTemplate.cmake.in b/cmake/templates/ramses-shared-lib-client-onlyTemplate.cmake.in deleted file mode 100644 index 55dc51d0a..000000000 --- a/cmake/templates/ramses-shared-lib-client-onlyTemplate.cmake.in +++ /dev/null @@ -1,73 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (C) 2018 BMW Car IT GmbH -# ------------------------------------------------------------------------- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -# ------------------------------------------------------------------------- - -# Use like this with find_package: -# find_package(ramses-shared-lib-client-only [REQUIRED]) - -INCLUDE(FindPackageHandleStandardArgs) -SET(ramses-shared-lib-client-only_VERSION @RAMSES_VERSION@) - -@PACKAGE_INIT@ - -#find include dir -FIND_PATH(ramses-shared-lib-client-only_INCLUDE_DIRS ramses-client.h - HINTS @PACKAGE_PROJECT_INSTALL_HEADER@ -) - -IF (CMAKE_SYSTEM_NAME MATCHES "Windows") - - # no shlib versioning support on windows - FIND_LIBRARY(ramses-shared-lib-client-only_LIBRARIES - NAMES "ramses-shared-lib-client-only" - HINTS @PACKAGE_PROJECT_INSTALL_BINARY@ - ) - -ELSEIF (CMAKE_SYSTEM_NAME MATCHES "Linux") - - # require exact shlib version - FIND_LIBRARY(ramses-shared-lib-client-only_LIBRARIES - NAMES "libramses-shared-lib-client-only.so.@RAMSES_VERSION_MAJOR@.@RAMSES_VERSION_MINOR@" - HINTS @PACKAGE_PROJECT_INSTALL_SHARED_LIB@ - ) - -ELSE() - MESSAGE(FATAL_ERROR "Can't detect system type from within CMake. Expect further issues!") -ENDIF() - - -FIND_PACKAGE_HANDLE_STANDARD_ARGS(ramses-shared-lib-client-only - FOUND_VAR ramses-shared-lib-client-only_FOUND - REQUIRED_VARS ramses-shared-lib-client-only_LIBRARIES ramses-shared-lib-client-only_INCLUDE_DIRS - VERSION_VAR ramses-shared-lib-client-only_VERSION) - -set(ramses-sdk_VERSION "@ramses-sdk_VERSION@" CACHE STRING "Ramses version" FORCE) - -message(STATUS "Found ramses-shared-lib-client-only libs: ${ramses-shared-lib-client-only_LIBRARIES}") -message(STATUS "Found ramses-shared-lib-client-only includes: ${ramses-shared-lib-client-only_INCLUDE_DIRS}") - -MARK_AS_ADVANCED( - ramses-shared-lib-client-only_INCLUDE_DIRS - ramses-shared-lib-client-only_LIBRARIES - ramses-shared-lib-client-only_FOUND - ) - -if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.13") - if (NOT TARGET ramses-ramses-shared-lib-client-only) - add_library(ramses-ramses-shared-lib-client-only SHARED IMPORTED GLOBAL) - set_target_properties(ramses-ramses-shared-lib-client-only PROPERTIES IMPORTED_LOCATION ${ramses-shared-lib-client-only_LIBRARIES}) - if (MSVC) - set_target_properties(ramses-ramses-shared-lib-client-only PROPERTIES IMPORTED_IMPLIB ${ramses-shared-lib-client-only_LIBRARIES}) - endif() - target_include_directories(ramses-ramses-shared-lib-client-only INTERFACE ${ramses-shared-lib-client-only_INCLUDE_DIRS}) - - if (NOT TARGET ramses::client-only) - add_library(ramses::client-only ALIAS ramses-ramses-shared-lib-client-only) - message(STATUS "Created ramses-shared-lib-client-only import target ramses::client-only") - endif() - endif() -endif() diff --git a/cmake/templates/ramses-shared-lib-headlessTemplate.cmake.in b/cmake/templates/ramses-shared-lib-headlessTemplate.cmake.in new file mode 100644 index 000000000..10da7f5ea --- /dev/null +++ b/cmake/templates/ramses-shared-lib-headlessTemplate.cmake.in @@ -0,0 +1,88 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2018 BMW Car IT GmbH +# ------------------------------------------------------------------------- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# ------------------------------------------------------------------------- + +# Use like this with find_package: +# find_package(ramses-shared-lib-headless [REQUIRED]) + +INCLUDE(FindPackageHandleStandardArgs) +INCLUDE(CMakeFindDependencyMacro) +SET(ramses-shared-lib-headless_VERSION @RAMSES_VERSION@) + +@PACKAGE_INIT@ + + +#find include dir +FIND_PATH(ramses-shared-lib-headless_INCLUDE_DIRS ramses-client.h + HINTS @PACKAGE_RAMSES_INSTALL_HEADERS_PATH@ +) + +if(IS_DIRECTORY "${ramses-shared-lib-headless_INCLUDE_DIRS}/glm") + add_library(glm::glm INTERFACE IMPORTED) + set_target_properties(glm::glm PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${ramses-shared-lib-headless_INCLUDE_DIRS}) +else() + find_dependency(glm REQUIRED) + + if(NOT TARGET glm::glm) + add_library(glm::glm ALIAS glm) + endif() +endif() + + +IF (CMAKE_SYSTEM_NAME MATCHES "Windows") + + # no shlib versioning support on windows + FIND_LIBRARY(ramses-shared-lib-headless_LIBRARIES + NAMES "ramses-shared-lib-headless" + HINTS @PACKAGE_RAMSES_INSTALL_RUNTIME_PATH@ + ) + +ELSEIF (CMAKE_SYSTEM_NAME MATCHES "Linux") + + # require exact shlib version + FIND_LIBRARY(ramses-shared-lib-headless_LIBRARIES + NAMES "libramses-shared-lib-headless.so.@RAMSES_VERSION_MAJOR@.@RAMSES_VERSION_MINOR@" + HINTS @PACKAGE_RAMSES_INSTALL_LIBRARY_PATH@ + ) + +ELSE() + MESSAGE(FATAL_ERROR "Can't detect system type from within CMake. Expect further issues!") +ENDIF() + + +FIND_PACKAGE_HANDLE_STANDARD_ARGS(ramses-shared-lib-headless + FOUND_VAR ramses-shared-lib-headless_FOUND + REQUIRED_VARS ramses-shared-lib-headless_LIBRARIES ramses-shared-lib-headless_INCLUDE_DIRS + VERSION_VAR ramses-shared-lib-headless_VERSION) + +set(ramses-sdk_VERSION "@ramses-sdk_VERSION@" CACHE STRING "Ramses version" FORCE) + +message(STATUS "Found ramses-shared-lib-headless libs: ${ramses-shared-lib-headless_LIBRARIES}") +message(STATUS "Found ramses-shared-lib-headless includes: ${ramses-shared-lib-headless_INCLUDE_DIRS}") + +MARK_AS_ADVANCED( + ramses-shared-lib-headless_INCLUDE_DIRS + ramses-shared-lib-headless_LIBRARIES + ramses-shared-lib-headless_FOUND + ) + +if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.13") + if (NOT TARGET ramses-ramses-shared-lib-headless) + add_library(ramses-ramses-shared-lib-headless SHARED IMPORTED GLOBAL) + set_target_properties(ramses-ramses-shared-lib-headless PROPERTIES IMPORTED_LOCATION ${ramses-shared-lib-headless_LIBRARIES}) + if (MSVC) + set_target_properties(ramses-ramses-shared-lib-headless PROPERTIES IMPORTED_IMPLIB ${ramses-shared-lib-headless_LIBRARIES}) + endif() + target_include_directories(ramses-ramses-shared-lib-headless INTERFACE ${ramses-shared-lib-headless_INCLUDE_DIRS}) + target_link_libraries(ramses-ramses-shared-lib-headless INTERFACE glm::glm) + + if (NOT TARGET ramses::headless) + add_library(ramses::headless ALIAS ramses-ramses-shared-lib-headless) + message(STATUS "Created ramses-shared-lib-headless import target ramses::headless") + endif() + endif() +endif() diff --git a/cmake/templates/ramses-shared-libTemplate.cmake.in b/cmake/templates/ramses-shared-libTemplate.cmake.in index b5591b48b..84da2700e 100644 --- a/cmake/templates/ramses-shared-libTemplate.cmake.in +++ b/cmake/templates/ramses-shared-libTemplate.cmake.in @@ -7,12 +7,10 @@ # ------------------------------------------------------------------------- # Use like this with find_package: -# find_package(ramses-shared-lib [REQUIRED] COMPONENTS [GL_VERSION]) -# must be one of: WINDOWS, LINUX-X11, LINUX-WAYLAND, ANDROID -# GL_VERSION can be one of: GL4.2, GL4.5, GLES3.0 -# GL_VERSION is overridden for some systems +# find_package(ramses-shared-lib ) INCLUDE(FindPackageHandleStandardArgs) +INCLUDE(CMakeFindDependencyMacro) SET(ramses-shared-lib_VERSION @RAMSES_VERSION@) @PACKAGE_INIT@ @@ -21,83 +19,62 @@ FIND_PACKAGE(PkgConfig QUIET) # Process COMPONENT parameters -LIST(GET ramses-shared-lib_FIND_COMPONENTS 0 RENDERER_PLATFORM) LIST(LENGTH ramses-shared-lib_FIND_COMPONENTS COMPONENTS_LENGTH) -IF(COMPONENTS_LENGTH GREATER 1) - LIST(GET ramses-shared-lib_FIND_COMPONENTS 1 GL_VERSION) -ELSE() - SET(GL_VERSION NOTFOUND) -ENDIF() +if(COMPONENTS_LENGTH GREATER 0) + MESSAGE(FATAL_ERROR "Unused components passed: ${ramses-shared-lib_FIND_COMPONENTS}") +endif() # find include dir -IF("${RENDERER_PLATFORM}" STREQUAL "ANDROID") +IF("${CMAKE_SYSTEM_NAME}" STREQUAL "Android") FIND_PATH(ramses-shared-lib_INCLUDE_DIRS ramses-client.h - HINTS @PACKAGE_PROJECT_INSTALL_HEADER@ + HINTS @PACKAGE_RAMSES_INSTALL_HEADERS_PATH@ CMAKE_FIND_ROOT_PATH_BOTH ) ELSE() FIND_PATH(ramses-shared-lib_INCLUDE_DIRS ramses-client.h - HINTS @PACKAGE_PROJECT_INSTALL_HEADER@ + HINTS @PACKAGE_RAMSES_INSTALL_HEADERS_PATH@ ) ENDIF() -# find library -IF("${RENDERER_PLATFORM}" STREQUAL "WINDOWS") - - IF(NOT GL_VERSION) - MESSAGE("Using OpenGL 4.2 as no OpenGL version was specified") - SET(GL_VERSION "GL4.2") - ENDIF() - - IF("${GL_VERSION}" STREQUAL "GL4.2") - SET(RAMSES_GL_LIB ramses-shared-lib-windows-wgl-4-2-core) - ELSEIF("${GL_VERSION}" STREQUAL "GL4.5") - SET(RAMSES_GL_LIB ramses-shared-lib-windows-wgl-4-5) - ELSEIF("${GL_VERSION}" STREQUAL "GLES3.0") - SET(RAMSES_GL_LIB ramses-shared-lib-windows-wgl-es-3-0) - ELSE() - MESSAGE(FATAL_ERROR "OpenGL version ${GL_VERSION} not supported on platform ${RENDERER_PLATFORM}. Use one of [GL4.2, GL4.5, GLES3.0]") - ENDIF() - - # no shlib versioning support on windows - FIND_LIBRARY(ramses-shared-lib_LIBRARIES - NAMES "${RAMSES_GL_LIB}" - HINTS @PACKAGE_PROJECT_INSTALL_BINARY@ - ) +if(IS_DIRECTORY "${ramses-shared-lib_INCLUDE_DIRS}/glm") + add_library(glm::glm INTERFACE IMPORTED) + set_target_properties(glm::glm PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${ramses-shared-lib_INCLUDE_DIRS}) +else() + find_dependency(glm REQUIRED) -ELSEIF("${RENDERER_PLATFORM}" STREQUAL "LINUX-X11") + if(NOT TARGET glm::glm) + add_library(glm::glm ALIAS glm) + endif() +endif() - MESSAGE("Using OpenGL ES 3.0. Ignoring GL_VERSION") +# find library +IF(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") - # require exact shlib version + # no shlib versioning support on windows FIND_LIBRARY(ramses-shared-lib_LIBRARIES - NAMES "libramses-shared-lib-x11-egl-es-3-0.so.@RAMSES_VERSION_MAJOR@.@RAMSES_VERSION_MINOR@" - HINTS @PACKAGE_PROJECT_INSTALL_SHARED_LIB@ + NAMES "ramses-shared-lib" + HINTS @PACKAGE_RAMSES_INSTALL_RUNTIME_PATH@ ) -ELSEIF("${RENDERER_PLATFORM}" STREQUAL "LINUX-WAYLAND") - - MESSAGE("Using OpenGL ES 3.0. Ignoring GL_VERSION") +ELSEIF(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") # require exact shlib version FIND_LIBRARY(ramses-shared-lib_LIBRARIES - NAMES "libramses-shared-lib-wayland-ivi-egl-es-3-0.so.@RAMSES_VERSION_MAJOR@.@RAMSES_VERSION_MINOR@" - HINTS @PACKAGE_PROJECT_INSTALL_SHARED_LIB@ + NAMES "libramses-shared-lib.so.@RAMSES_VERSION_MAJOR@.@RAMSES_VERSION_MINOR@" + HINTS @PACKAGE_RAMSES_INSTALL_LIBRARY_PATH@ ) -ELSEIF("${RENDERER_PLATFORM}" STREQUAL "ANDROID") - - MESSAGE("Using OpenGL ES 3.0. Ignoring GL_VERSION") +ELSEIF(${CMAKE_SYSTEM_NAME} STREQUAL "Android") # no shlib versioning support on Android FIND_LIBRARY(ramses-shared-lib_LIBRARIES - NAMES "libramses-shared-lib-android-egl-es-3-0.so" - HINTS @PACKAGE_PROJECT_INSTALL_SHARED_LIB@ + NAMES "libramses-shared-lib" + HINTS @PACKAGE_RAMSES_INSTALL_LIBRARY_PATH@ CMAKE_FIND_ROOT_PATH_BOTH ) ELSE() - MESSAGE(FATAL_ERROR "please use component syntax, choose one of WINDOWS, LINUX-X11, LINUX-WAYLAND, ANDROID") + MESSAGE(FATAL_ERROR "Unsupported system: ${CMAKE_SYSTEM_NAME}, cant find shared lib") ENDIF() @@ -125,6 +102,7 @@ if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.13") set_target_properties(ramses-ramses-shared-lib PROPERTIES IMPORTED_IMPLIB ${ramses-shared-lib_LIBRARIES}) endif() target_include_directories(ramses-ramses-shared-lib INTERFACE ${ramses-shared-lib_INCLUDE_DIRS}) + target_link_libraries(ramses-ramses-shared-lib INTERFACE glm::glm) if (NOT TARGET ramses::ramses) add_library(ramses::ramses ALIAS ramses-ramses-shared-lib) diff --git a/cmake/templates/ramses-version.in b/cmake/templates/ramses-version.in deleted file mode 100644 index 5f6d66068..000000000 --- a/cmake/templates/ramses-version.in +++ /dev/null @@ -1,6 +0,0 @@ -RAMSES_COMMIT_COUNT=@GIT_COMMIT_COUNT@ -RAMSES_GIT_HASH=@GIT_COMMIT_HASH@ -RAMSES_VERSION=@RAMSES_VERSION@ -RAMSES_BRANCH=@GIT_COMMIT_BRANCH@ -RAMSES_BUILD_TIME=@RAMSES_BUILD_TIME@ -BUILD_ENV_VERSION_INFO_FULL=@BUILD_ENV_VERSION_INFO_FULL@ diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index 57bb27a6d..fbb4973da 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -6,10 +6,4 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. # ------------------------------------------------------------------------- -IF(ramses-sdk_BUILD_DEMOS) - ADD_SUBDIRECTORY(ramses-text-layout-demo) - ADD_SUBDIRECTORY(ramses-text-shadow-demo) - ADD_SUBDIRECTORY(ramses-dcsm-list) - ADD_SUBDIRECTORY(android) - ADD_SUBDIRECTORY(ramses-dcsm-scene-references-demo) -ENDIF() +add_subdirectory(android) diff --git a/demo/android/DemoJNIInterface/CMakeLists.txt b/demo/android/DemoJNIInterface/CMakeLists.txt index 119fe581b..1b7fbe1e2 100644 --- a/demo/android/DemoJNIInterface/CMakeLists.txt +++ b/demo/android/DemoJNIInterface/CMakeLists.txt @@ -6,21 +6,15 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. # ------------------------------------------------------------------------- -MODULE_WITH_SHARED_LIBRARY( - - #========================================================================== - # general module information - #========================================================================== +createModule( NAME DemoRamsesJNIInterface - TYPE SHARED_LIBRARY ENABLE_INSTALL ON - - #========================================================================== - # files of this module - #========================================================================== - FILES_PRIVATE_HEADER include/*.h - FILES_SOURCE src/*.cpp + TYPE SHARED_LIBRARY + INCLUDE_PATHS include + SRC_FILES include/*.h + src/*.cpp DEPENDENCIES AndroidSDK + ramses-shared-lib ) diff --git a/demo/android/DemoJNIInterface/src/JNIInterface.cpp b/demo/android/DemoJNIInterface/src/JNIInterface.cpp index f8ab1adbf..1c4f5564b 100644 --- a/demo/android/DemoJNIInterface/src/JNIInterface.cpp +++ b/demo/android/DemoJNIInterface/src/JNIInterface.cpp @@ -111,28 +111,21 @@ extern "C" JNIEXPORT void JNICALL Java_com_bmwgroup_ramses_RamsesNode_setTranslationNative(JNIEnv* /*env*/, jobject /*instance*/, jlong handle, jfloat x, jfloat y, jfloat z) { - reinterpret_cast(handle)->setTranslation(x, y, z); + reinterpret_cast(handle)->setTranslation({x, y, z}); } extern "C" JNIEXPORT void JNICALL Java_com_bmwgroup_ramses_RamsesNode_translateNative(JNIEnv* /*env*/, jobject /*instance*/, jlong handle, jfloat x, jfloat y, jfloat z) { - reinterpret_cast(handle)->translate(x, y, z); + reinterpret_cast(handle)->translate({x, y, z}); } extern "C" JNIEXPORT void JNICALL Java_com_bmwgroup_ramses_RamsesNode_setRotationNative(JNIEnv* /*env*/, jobject /*instance*/, jlong handle, jfloat x, jfloat y, jfloat z) { - reinterpret_cast(handle)->setRotation(x, y, z); -} - -extern "C" -JNIEXPORT void JNICALL -Java_com_bmwgroup_ramses_RamsesNode_rotateNative(JNIEnv* /*env*/, jobject /*instance*/, jlong handle, - jfloat x, jfloat y, jfloat z) { - reinterpret_cast(handle)->rotate(x, y, z); + reinterpret_cast(handle)->setRotation({x, y, z}, ramses::ERotationType::Euler_XYZ); } extern "C" diff --git a/demo/android/DemoJNIInterface/src/RendererBundle.cpp b/demo/android/DemoJNIInterface/src/RendererBundle.cpp index 53b0b6ecf..c81c70a36 100644 --- a/demo/android/DemoJNIInterface/src/RendererBundle.cpp +++ b/demo/android/DemoJNIInterface/src/RendererBundle.cpp @@ -17,14 +17,19 @@ #include "ramses-framework-api/RamsesFramework.h" -RendererBundle::RendererBundle(ANativeWindow* nativeWindow, int width, int height, - const char* interfaceSelectionIP, const char* daemonIP) +RendererBundle::RendererBundle( + ANativeWindow* nativeWindow, + int width, + int height, + const char* interfaceSelectionIP, + const char* daemonIP) : m_nativeWindow(nativeWindow) , m_cancelDispatchLoop(false) { + ramses::RamsesFrameworkConfig frameworkConfig{ramses::EFeatureLevel_Latest}; //workaround to ensure that the socket connection is always initiated outbound from Android, inbound connections might be blocked - const char* argv[] = {"", "--guid", "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"}; - ramses::RamsesFrameworkConfig frameworkConfig(3, argv); + frameworkConfig.setParticipantGuid(0xffffffffffffffffu); + frameworkConfig.setInterfaceSelectionIPForTCPCommunication(interfaceSelectionIP); frameworkConfig.setDaemonIPForTCPCommunication(daemonIP); m_framework.reset(new ramses::RamsesFramework(frameworkConfig)); @@ -38,7 +43,7 @@ RendererBundle::RendererBundle(ANativeWindow* nativeWindow, int width, int heigh m_autoShowHandler.reset(new SceneStateAutoShowEventHandler(*m_renderer, displayId)); - m_renderer->setMaximumFramerate(60); + m_renderer->setFramerateLimit(displayId, 60); m_renderer->flush(); } diff --git a/demo/android/DemoJNIInterface/src/UniformInputWrapper.cpp b/demo/android/DemoJNIInterface/src/UniformInputWrapper.cpp index 7980e3563..5dd076c91 100644 --- a/demo/android/DemoJNIInterface/src/UniformInputWrapper.cpp +++ b/demo/android/DemoJNIInterface/src/UniformInputWrapper.cpp @@ -19,7 +19,7 @@ UniformInputWrapper::UniformInputWrapper(const char* inputName, ramses::Appearan void UniformInputWrapper::setInputValueFloat(float x) { - m_appearance.setInputValueFloat(m_input, x); + m_appearance.setInputValue(m_input, x); } diff --git a/demo/android/DemoRamsesAndroidModule/build.gradle b/demo/android/DemoRamsesAndroidModule/build.gradle index 15eb0ef36..5f13d3a29 100644 --- a/demo/android/DemoRamsesAndroidModule/build.gradle +++ b/demo/android/DemoRamsesAndroidModule/build.gradle @@ -26,7 +26,7 @@ android { externalNativeBuild { cmake { targets "DemoRamsesJNIInterface" - arguments "-Dramses-sdk_ENABLE_DLT=0" + arguments "-Dramses-sdk_ENABLE_DLT=0", "-Dramses-sdk_ENABLE_FLATBUFFERS_GENERATION=0", "-Dramses-sdk_ENABLE_WINDOW_TYPE_ANDROID=ON" } } } diff --git a/demo/android/README.md b/demo/android/README.md index fd7579245..ff5435ad8 100644 --- a/demo/android/README.md +++ b/demo/android/README.md @@ -17,11 +17,11 @@ sudo ifconfig lo:1 10.0.2.2 netmask 255.255.255.0 up in a shell on the host system (This temporary assigns a second IP to the localhost interface used to communicate to the Android emulator. The step is only necessary with the emulator to simulate a normal network connection) - Go to the bin directory of the RAMSES linux build and execute in two separate shells ```shell -./ramses-daemon -myip 10.0.2.2 +./ramses-daemon --ip 10.0.2.2 ``` and ```shell -./ramses-test-client -i 10.0.2.2 -myip 10.0.2.2 +./ramses-test-client --daemon-ip 10.0.2.2 --ip 10.0.2.2 ``` to start a RAMSES daemon and a RAMSES client on the host system diff --git a/demo/android/ramses-renderer-android-app-native-activity/app/CMakeLists.txt b/demo/android/ramses-renderer-android-app-native-activity/app/CMakeLists.txt index 87de6b7cc..e1a6ca0ed 100644 --- a/demo/android/ramses-renderer-android-app-native-activity/app/CMakeLists.txt +++ b/demo/android/ramses-renderer-android-app-native-activity/app/CMakeLists.txt @@ -19,7 +19,7 @@ if((TARGET DemoRamsesJNIInterface) AND ANDROID_NDK) android native_app_glue log - ramses-shared-lib-android-egl-es-3-0 + ramses-shared-lib DemoRamsesJNIInterface ) endif() diff --git a/demo/android/ramses-renderer-android-app-native-activity/app/build.gradle b/demo/android/ramses-renderer-android-app-native-activity/app/build.gradle index 71da79c65..8ed32128d 100644 --- a/demo/android/ramses-renderer-android-app-native-activity/app/build.gradle +++ b/demo/android/ramses-renderer-android-app-native-activity/app/build.gradle @@ -26,6 +26,7 @@ android { externalNativeBuild { cmake { targets "exampleNativeLib" + arguments "-Dramses-sdk_ENABLE_FLATBUFFERS_GENERATION=0", "-Dramses-sdk_ENABLE_WINDOW_TYPE_ANDROID=ON" } } } diff --git a/demo/android/ramses-renderer-android-app-native-activity/repositories.gradle b/demo/android/ramses-renderer-android-app-native-activity/repositories.gradle index cc1977101..a19293781 100644 --- a/demo/android/ramses-renderer-android-app-native-activity/repositories.gradle +++ b/demo/android/ramses-renderer-android-app-native-activity/repositories.gradle @@ -8,5 +8,5 @@ ext.globalRepositoryDefinition = { google() - jcenter() + mavenCentral() } diff --git a/demo/ramses-dcsm-list/CMakeLists.txt b/demo/ramses-dcsm-list/CMakeLists.txt deleted file mode 100644 index 8a704f159..000000000 --- a/demo/ramses-dcsm-list/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (C) 2019 BMW AG -# ------------------------------------------------------------------------- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -# ------------------------------------------------------------------------- - -MODULE_WITH_SHARED_LIBRARY( - - #========================================================================== - # general module information - #========================================================================== - NAME ramses-dcsm-list - TYPE BINARY - ENABLE_INSTALL ON - - #========================================================================== - # files of this module - #========================================================================== - FILES_PRIVATE_HEADER include/*.h - FILES_SOURCE src/*.cpp - RESOURCE_FOLDER res -) diff --git a/demo/ramses-dcsm-list/res/ramses-dcsm-list-line.png b/demo/ramses-dcsm-list/res/ramses-dcsm-list-line.png deleted file mode 100644 index 5a6ffa4f226302a7ee946515d52dd26d3efa2b4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11748 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 a7Lv$9;`w*#N$34$Yh?xX7v*v$A^!uTa1ty4 diff --git a/demo/ramses-dcsm-list/src/main.cpp b/demo/ramses-dcsm-list/src/main.cpp deleted file mode 100644 index d933fe34d..000000000 --- a/demo/ramses-dcsm-list/src/main.cpp +++ /dev/null @@ -1,294 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2019 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "ramses-client.h" -#include "ramses-utils.h" - -#include "ramses-framework-api/DcsmProvider.h" -#include "ramses-framework-api/IDcsmProviderEventHandler.h" -#include "ramses-framework-api/EDcsmOfferingMode.h" - -#include -#include -#include - -class DCSMListDemo : ramses::IDcsmProviderEventHandler -{ -public: - DCSMListDemo(int argc, char* argv[]) - : m_framework(argc, argv) - , m_ramses(*m_framework.createClient("ramses-dcsm-list")) - , m_dcsm(*m_framework.createDcsmProvider()) - , m_alphaInput() - { - for (int i = 1; i < argc; ++i) - { - std::string arg = argv[i]; - if (arg == "-offer") - { - if (argc <= i + 4) - { - std::cerr << "'-offer' argument error: too few arguments, use: -offer sceneId contentId categoryId viewportProviderId" << std::endl; - continue; - } - - int sceneid = atoi(argv[++i]); - int content = atoi(argv[++i]); - int category = atoi(argv[++i]); - int viewportProvider = atoi(argv[++i]); - - if (sceneid > 0) - m_sceneToOffer = ramses::sceneId_t(sceneid); - else - std::cerr << "'-offer' argument error: invalid scene, using default " << sceneid << std::endl; - - if (content > 0) - m_contentID = ramses::ContentID(content); - else - std::cerr << "'-offer' argument error: invalid content, using default " << m_contentID.getValue() << std::endl; - - if (category > 0) - m_categoryID = ramses::Category(category); - else - std::cerr << "'-offer' argument error: invalid category, using default " << m_categoryID.getValue() << std::endl; - - if (viewportProvider > 0) - m_viewportProviderID.getReference() = viewportProvider; - else - std::cerr << "'-offer' argument error: invalid viewportProvider, using default " << m_viewportProviderID.getValue() << std::endl; - } - } - - m_framework.connect(); - } - - ~DCSMListDemo() override - { - // shutdown: stop distribution, free resources, unregister - if (m_scene) - { - m_scene->unpublish(); - m_ramses.destroy(*m_scene); - } - - m_framework.disconnect(); - } - - void createScene() - { - // create a scene for distributing content - m_scene = m_ramses.createScene(m_sceneToOffer, ramses::SceneConfig(), "dcsm list demo scene"); - - m_camera = m_scene->createOrthographicCamera("my camera"); - m_camera->setFrustum(0, 735, 0, 50, 0.1f, 100.0f); - m_camera->setViewport(0u, 0u, 735u, 50u); - - m_cameraViewportSize = m_scene->createDataVector2i(); - m_cameraViewportSize->setValue(735, 50); - m_camera->bindViewportSize(*m_cameraViewportSize); - m_scene->createDataProvider(*m_cameraViewportSize, m_viewportProviderID); - - ramses::RenderPass* renderPass = m_scene->createRenderPass("my render pass"); - renderPass->setClearFlags(ramses::EClearFlags_Color); - renderPass->setClearColor(0.0f, 0.0f, 0.0f, 1.0f); - renderPass->setCamera(*m_camera); - ramses::RenderGroup* renderGroup = m_scene->createRenderGroup(); - renderPass->addRenderGroup(*renderGroup); - const float vertexPositionsArray[] = { 0.0f, 0.0f, -1.f, 735.0f, 0.0f, -1.f, 0.0f, 50.0f, -1.f, 735.0f, 50.0f, -1.f }; - const auto vertexPositions = m_scene->createArrayResource(ramses::EDataType::Vector3F, 4, vertexPositionsArray); - - const float textureCoordsArray[] = { 0.f, 1.f, 1.f, 1.f, 0.f, 0.f, 1.f, 0.f }; - const auto textureCoords = m_scene->createArrayResource(ramses::EDataType::Vector2F, 4, textureCoordsArray); - - const uint16_t indicesArray[] = { 0, 1, 2, 2, 1, 3 }; - const auto indices = m_scene->createArrayResource(ramses::EDataType::UInt16, 6, indicesArray); - - ramses::Texture2D* texture = ramses::RamsesUtils::CreateTextureResourceFromPng("res/ramses-dcsm-list-line.png", *m_scene); - ramses::TextureSampler* sampler = m_scene->createTextureSampler( - ramses::ETextureAddressMode_Repeat, - ramses::ETextureAddressMode_Repeat, - ramses::ETextureSamplingMethod_Linear, - ramses::ETextureSamplingMethod_Linear, - *texture); - - ramses::EffectDescription effectDesc; - effectDesc.setVertexShaderFromFile("res/ramses-dcsm-list.vert"); - effectDesc.setFragmentShaderFromFile("res/ramses-dcsm-list.frag"); - effectDesc.setUniformSemantic("mvpMatrix", ramses::EEffectUniformSemantic::ModelViewProjectionMatrix); - - const ramses::Effect* effectTex = m_scene->createEffect(effectDesc, ramses::ResourceCacheFlag_DoNotCache, "glsl shader"); - m_appearance = m_scene->createAppearance(*effectTex, "triangle appearance"); - - // set vertex positions directly in geometry - ramses::GeometryBinding* geometry = m_scene->createGeometryBinding(*effectTex, "triangle geometry"); - geometry->setIndices(*indices); - ramses::AttributeInput positionsInput; - ramses::AttributeInput texcoordsInput; - effectTex->findAttributeInput("a_position", positionsInput); - effectTex->findAttributeInput("a_texcoord", texcoordsInput); - geometry->setInputBuffer(positionsInput, *vertexPositions); - geometry->setInputBuffer(texcoordsInput, *textureCoords); - - ramses::UniformInput textureInput; - effectTex->findUniformInput("textureSampler", textureInput); - - effectTex->findUniformInput("transparency", m_alphaInput); - m_appearance->setInputTexture(textureInput, *sampler); - m_appearance->setInputValueFloat(m_alphaInput, 1.0f); - m_appearance->setBlendingFactors(ramses::EBlendFactor_SrcAlpha, ramses::EBlendFactor_OneMinusSrcAlpha, ramses::EBlendFactor_One, ramses::EBlendFactor_One); - m_appearance->setBlendingOperations(ramses::EBlendOperation_Add, ramses::EBlendOperation_Add); - - for (int i = 0; i < 10; ++i) - { - ramses::MeshNode* meshNode = m_scene->createMeshNode("textured triangle mesh node"); - meshNode->setAppearance(*m_appearance); - meshNode->setGeometryBinding(*geometry); - meshNode->translate(0.0f, 50.0f*i, 0.0f); - renderGroup->addMeshNode(*meshNode, 1); - } - - m_scene->flush(); - m_scene->publish(); - } - - void runLoop() - { - m_sizeReceived = false; - m_released = false; - - m_dcsm.offerContent(m_contentID, m_categoryID, m_sceneToOffer, ramses::EDcsmOfferingMode::LocalAndRemote); - while (!m_sizeReceived) - { - m_dcsm.dispatchEvents(*this); - std::this_thread::sleep_for(std::chrono::milliseconds(16u)); - } - - bool currShowing = false; - ramses::SizeInfo currSize = m_newSize; - m_camera->setFrustum(0, static_cast(currSize.width), 0, static_cast(currSize.height), 0.1f, 100.0f); - m_cameraViewportSize->setValue(currSize.width, currSize.height); - while (!m_released) - { - std::this_thread::sleep_for(std::chrono::milliseconds(16u)); - m_dcsm.dispatchEvents(*this); - - const auto now = uint64_t(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()); - if (currSize != m_newSize) - { - const uint32_t listElementHeight = 50; - const auto sizeStart = !m_sizeAnim.startTime ? now : m_sizeAnim.startTime; - const auto sizeEnd = !m_sizeAnim.finishTime ? now : m_sizeAnim.finishTime; - if (sizeStart <= now) - { - const auto currFraction = sizeEnd == sizeStart ? 1.0f : float(now - sizeStart) / (sizeEnd - sizeStart); - - auto resultSize = m_newSize; - if (currFraction >= 0.999f) - currSize = m_newSize; - else - { - resultSize.width = static_cast(currSize.width * (1.f - currFraction) + m_newSize.width * currFraction); - resultSize.height = static_cast(currSize.height * (1.f - currFraction) + m_newSize.height * currFraction); - } - resultSize.height = (resultSize.height / listElementHeight) * listElementHeight; // cut height to a multiple of listElementHeight - m_camera->setFrustum(0, static_cast(resultSize.width), 0, static_cast(resultSize.height), 0.1f, 100.0f); - m_cameraViewportSize->setValue(resultSize.width, resultSize.height); - - m_scene->flush(); - } - } - if (currShowing != m_showing) - { - const auto showStart = !m_showHideAnim.startTime ? now : m_showHideAnim.startTime; - const auto showEnd = !m_showHideAnim.finishTime ? now : m_showHideAnim.finishTime; - if (showStart <= now) - { - const auto currFraction = showEnd == showStart ? 1.0f : float(now - showStart) / (showEnd - showStart); - m_appearance->setInputValueFloat(m_alphaInput, m_showing ? std::min(currFraction, 1.f) : 1 - std::min(currFraction, 1.f)); - if (currFraction >= 0.999f) - currShowing = m_showing; - - m_scene->flush(); - } - } - } - } - - virtual void contentHide(ramses::ContentID /*contentID*/, ramses::AnimationInformation animInfo) override - { - m_showing = false; - m_showHideAnim = animInfo; - } - - virtual void contentShow(ramses::ContentID /*contentID*/, ramses::AnimationInformation animInfo) override - { - m_showing = true; - m_showHideAnim = animInfo; - } - - virtual void stopOfferAccepted(ramses::ContentID /*contentID*/, ramses::AnimationInformation /*animInfo*/) override - { - } - - virtual void contentSizeChange(ramses::ContentID /*contentID*/, const ramses::CategoryInfoUpdate& categoryInfo, ramses::AnimationInformation animInfo) override - { - if (categoryInfo.hasCategoryRectUpdate()) - { - m_newSize = ramses::SizeInfo{ categoryInfo.getCategoryRect().width, categoryInfo.getCategoryRect().height }; - m_sizeAnim = animInfo; - m_sizeReceived = true; - } - } - - virtual void contentReadyRequested(ramses::ContentID contentID) override - { - m_dcsm.markContentReady(contentID); - } - - virtual void contentRelease(ramses::ContentID /*contentID*/, ramses::AnimationInformation /*animInfo*/) override - { - m_released = true; - m_showing = false; - } - -private: - bool m_released = false; - bool m_showing = false; - bool m_sizeReceived = false; - - ramses::sceneId_t m_sceneToOffer{123}; - ramses::ContentID m_contentID = ramses::ContentID(101); - ramses::Category m_categoryID = ramses::Category(1); - // Viewport size can be linked to data provider on consumer/renderer side. - // The viewport size represents the content size in screen space - // and can be used for various layouting effects on consumer/renderer. - ramses::dataProviderId_t m_viewportProviderID{667}; - - ramses::SizeInfo m_newSize{ 0, 0 }; - ramses::AnimationInformation m_sizeAnim = ramses::AnimationInformation{ 0, 0 }; - ramses::AnimationInformation m_showHideAnim = ramses::AnimationInformation{ 0, 0 }; - - ramses::RamsesFramework m_framework; - ramses::RamsesClient& m_ramses; - ramses::DcsmProvider& m_dcsm; - - ramses::OrthographicCamera* m_camera = nullptr; - ramses::DataVector2i* m_cameraViewportSize = nullptr; - ramses::Scene* m_scene = nullptr; - ramses::Appearance* m_appearance = nullptr; - - ramses::UniformInput m_alphaInput; -}; - -int main(int argc, char* argv[]) -{ - DCSMListDemo demo(argc, argv); - demo.createScene(); - demo.runLoop(); - return 0; -} diff --git a/demo/ramses-dcsm-scene-references-demo/res/ramses-demo-scene-references-roboto-regular.ttf b/demo/ramses-dcsm-scene-references-demo/res/ramses-demo-scene-references-roboto-regular.ttf deleted file mode 100644 index 2c97eeadffe1a34bd67d3ff1c3887fd53e22c2ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 171676 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 ZcAgz$Pp~K2ZnnEU$(~HkLK9l*e*jr~ZsGs{ diff --git a/demo/ramses-dcsm-scene-references-demo/src/OverlayClient.cpp b/demo/ramses-dcsm-scene-references-demo/src/OverlayClient.cpp deleted file mode 100644 index fa1c1a3bf..000000000 --- a/demo/ramses-dcsm-scene-references-demo/src/OverlayClient.cpp +++ /dev/null @@ -1,105 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - - -#include "OverlayClient.h" - -#include "ramses-client.h" -#include "ramses-text-api/FontRegistry.h" -#include "ramses-text-api/TextCache.h" - -#include - -constexpr ramses::sceneId_t OverlayClient::Overlay1SceneId; -constexpr ramses::sceneId_t OverlayClient::Overlay2SceneId; - -constexpr ramses::dataConsumerId_t OverlayClient::Overlay1ViewportOffsetId; -constexpr ramses::dataConsumerId_t OverlayClient::Overlay2ViewportOffsetId; - -namespace -{ - ramses::Scene* createOverlayScene(ramses::RamsesClient& client, ramses::sceneId_t sceneId, const std::u32string& overlayText, ramses::dataConsumerId_t vpOffsetId) - { - ramses::Scene* scene = client.createScene(sceneId); - - // create font registry to hold font memory and text cache to cache text meshes - ramses::FontRegistry fontRegistry; - ramses::TextCache textCache(*scene, fontRegistry, 1024u, 1024u); - - // Create orthographic camera, so that text pixels will match screen pixels - constexpr uint32_t vpWidth{ 480 }; - constexpr uint32_t vpHeight{ 80 }; - ramses::OrthographicCamera* camera = scene->createOrthographicCamera(); - camera->setFrustum(0.0f, static_cast(vpWidth), 0.0f, static_cast(vpHeight), 0.1f, 1.f); - camera->setViewport(0, 0, vpWidth, vpHeight); - - // create render pass - ramses::RenderPass* renderPass = scene->createRenderPass(); - renderPass->setClearFlags(ramses::EClearFlags_None); - renderPass->setCamera(*camera); - ramses::RenderGroup* renderGroup = scene->createRenderGroup(); - renderPass->addRenderGroup(*renderGroup); - - // create appearance to be used for text - ramses::EffectDescription effectDesc; - effectDesc.setAttributeSemantic("a_position", ramses::EEffectAttributeSemantic::TextPositions); - effectDesc.setAttributeSemantic("a_texcoord", ramses::EEffectAttributeSemantic::TextTextureCoordinates); - effectDesc.setUniformSemantic("u_texture", ramses::EEffectUniformSemantic::TextTexture); - effectDesc.setUniformSemantic("mvpMatrix", ramses::EEffectUniformSemantic::ModelViewProjectionMatrix); - effectDesc.setVertexShaderFromFile("res/ramses-demo-scene-references-text-effect.vert"); - effectDesc.setFragmentShaderFromFile("res/ramses-demo-scene-references-text-effect.frag"); - ramses::Effect* textEffect = scene->createEffect(effectDesc, ramses::ResourceCacheFlag_DoNotCache, "simpleTextShader"); - - // create font instance - ramses::FontId font = fontRegistry.createFreetype2Font("res/ramses-demo-scene-references-roboto-regular.ttf"); - ramses::FontInstanceId fontInstance = fontRegistry.createFreetype2FontInstance(font, 40); - - // load rasterized glyphs for each character - const ramses::GlyphMetricsVector positionedGlyphs = textCache.getPositionedGlyphs(overlayText, fontInstance); - - // create RAMSES meshes/texture page to hold the glyphs and text geometry - const ramses::TextLineId textId = textCache.createTextLine(positionedGlyphs, *textEffect); - ramses::TextLine* textLine = textCache.getTextLine(textId); - - textLine->meshNode->getAppearance()->setBlendingOperations(ramses::EBlendOperation_Add, ramses::EBlendOperation_Add); - textLine->meshNode->getAppearance()->setBlendingFactors(ramses::EBlendFactor_SrcAlpha, ramses::EBlendFactor_OneMinusSrcAlpha, ramses::EBlendFactor_SrcAlpha, ramses::EBlendFactor_OneMinusSrcAlpha); - textLine->meshNode->getAppearance()->setDepthFunction(ramses::EDepthFunc_Disabled); - - // add the text meshes to the render pass to show them - textLine->meshNode->setTranslation(20.0f, 20.0f, -0.5f); - renderGroup->addMeshNode(*textLine->meshNode); - - const auto vpOffsetData = scene->createDataVector2i("vpOffset"); - vpOffsetData->setValue(0, 0); - camera->bindViewportOffset(*vpOffsetData); - scene->createDataConsumer(*vpOffsetData, vpOffsetId); - - // apply changes - scene->flush(); - scene->publish(); - - return scene; - } -} - -OverlayClient::OverlayClient(ramses::RamsesFramework& framework) - : m_framework(framework) - , m_client(*m_framework.createClient("OverlayClient")) -{ -} - -OverlayClient::~OverlayClient() -{ - m_framework.destroyClient(m_client); -} - -void OverlayClient::createOverlayScenes() -{ - m_overlay1Scene = createOverlayScene(m_client, Overlay1SceneId, U"First Overlay", Overlay1ViewportOffsetId); - m_overlay2Scene = createOverlayScene(m_client, Overlay2SceneId, U"Second Overlay", Overlay2ViewportOffsetId); -} diff --git a/demo/ramses-dcsm-scene-references-demo/src/OverlayClient.h b/demo/ramses-dcsm-scene-references-demo/src/OverlayClient.h deleted file mode 100644 index 3c1a93381..000000000 --- a/demo/ramses-dcsm-scene-references-demo/src/OverlayClient.h +++ /dev/null @@ -1,47 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_OVERLAYCLIENT_H -#define RAMSES_OVERLAYCLIENT_H - -#include "ramses-framework-api/RamsesFrameworkTypes.h" - -namespace ramses -{ - class RamsesFramework; - class RamsesClient; - class Scene; -} - -/* This class contains a RamsesClient which provides overlays - * Creates and publishes two small text scenes with data consumers which allow to control their - * viewport offset. - */ -class OverlayClient -{ -public: - explicit OverlayClient(ramses::RamsesFramework& framework); - ~OverlayClient(); - - void createOverlayScenes(); - - static constexpr ramses::sceneId_t Overlay1SceneId{ 10001 }; - static constexpr ramses::sceneId_t Overlay2SceneId{ 10002 }; - - static constexpr ramses::dataConsumerId_t Overlay1ViewportOffsetId{ 100011 }; - static constexpr ramses::dataConsumerId_t Overlay2ViewportOffsetId{ 100021 }; - -private: - ramses::RamsesFramework& m_framework; - ramses::RamsesClient& m_client; - - ramses::Scene* m_overlay1Scene = nullptr; - ramses::Scene* m_overlay2Scene = nullptr; -}; - -#endif diff --git a/demo/ramses-dcsm-scene-references-demo/src/main.cpp b/demo/ramses-dcsm-scene-references-demo/src/main.cpp deleted file mode 100644 index 9baff9ce0..000000000 --- a/demo/ramses-dcsm-scene-references-demo/src/main.cpp +++ /dev/null @@ -1,329 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2020 BMW AG -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "OverlayClient.h" - -#include "ramses-framework-api/RamsesFramework.h" -#include "ramses-framework-api/DcsmApiTypes.h" -#include "ramses-framework-api/DcsmProvider.h" - -#include "ramses-renderer-api/RamsesRenderer.h" -#include "ramses-renderer-api/DisplayConfig.h" -#include "ramses-renderer-api/RendererSceneControl.h" -#include "ramses-renderer-api/IRendererEventHandler.h" -#include "ramses-renderer-api/IRendererSceneControlEventHandler.h" - -#include "ramses-renderer-api/IDcsmContentControlEventHandler.h" -#include "ramses-renderer-api/DcsmContentControl.h" - -#include "ramses-client-api/Scene.h" -#include "ramses-client-api/SceneReference.h" -#include "ramses-client-api/IClientEventHandler.h" -#include "ramses-client-api/RamsesClient.h" -#include "ramses-client-api/DataVector2i.h" - -#include -#include -#include -#include - -/* - * A class which contains a RamsesRenderer and a DcsmContentControl - * Automatically tries to bring all contents of a given master category to a Rendered state - * and shows them on the created display. -*/ -class RendererSide : public ramses::DcsmContentControlEventHandlerEmpty -{ -public: - - RendererSide(ramses::RamsesFramework& framework, ramses::Category masterCategory, int argc, char* argv[]) - : m_category(masterCategory) - { - /* - * create renderer, display and DcsmContentControl - */ - ramses::RendererConfig rendererConfig(argc, argv); - m_renderer = framework.createRenderer(rendererConfig); - ramses::DisplayConfig displayConfig(argc, argv); - - int32_t x = 0; - int32_t y = 0; - uint32_t w = 0; - uint32_t h = 0; - displayConfig.getWindowRectangle(x, y, w, h); - const ramses::displayId_t displayId = m_renderer->createDisplay(displayConfig); - m_renderer->flush(); - - /* - * Add Content Category to DcsmContentControl which defines that we are interested in the specified master Category and want to - * show it on the created display. This will make the DcsmContentControl automatically assign contents - * of that category to itself. - */ - m_contentControl = m_renderer->createDcsmContentControl(); - ramses::CategoryInfoUpdate categoryInfo{{ w, h }, { 0, 0, w, h }}; - m_contentControl->addContentCategory(m_category, displayId, categoryInfo); - } - - void startLoop() - { - new std::thread{ [this]() { - - while (true) - { - m_renderer->doOneLoop(); - m_contentControl->update(0, *this); // passing always 0 as no dcsm timing (timeout or animations) in use here - - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - } - } }; - } - - // ramses::IDcsmContentControlEventHandler implementations - virtual void contentAvailable(ramses::ContentID contentID, ramses::Category categoryID) override - { - /* - * When content of the category is available, request content to be ready automatically. - */ - if (categoryID == m_category) - m_contentControl->requestContentReady(contentID, 0); - } - virtual void contentReady(ramses::ContentID contentID, ramses::DcsmContentControlEventResult result) override - { - /* - * After content is ready (Ready at Renderer and Ready in DCSM), show it atomically - */ - if (result == ramses::DcsmContentControlEventResult::OK) - m_contentControl->showContent(contentID, { 0, 0 }); - } - -private: - ramses::Category m_category; - ramses::DcsmContentControl* m_contentControl; - ramses::RamsesRenderer* m_renderer; -}; - -/* - * This class contains a master RamsesClient and a DCSM provider - * Creates and publishes a master scene, which contains only scene references to the two overlay - * scenes, a scene reference to the optional external scene and two data providers, which are - * being connected to the viewport data consumers of the referenced overlay scenes. Offers this - * master scene as a content in the given master category. Executes an animation on the scene - * references states and data providers to influence positioning of the overlay scenes. -*/ -class MasterClient : public ramses::IClientEventHandler, ramses::IDcsmProviderEventHandler -{ -public: - explicit MasterClient(ramses::RamsesFramework& framework) - : m_masterClient(framework.createClient("MasterClient")) - , m_provider(framework.createDcsmProvider()) - {} - - void setupDcsmAndSceneReferencing(ramses::Category masterCategory) - { - constexpr ramses::sceneId_t masterSceneId{ 10000 }; - constexpr ramses::dataProviderId_t o1VPOffsetProviderId{ 100011 }; - constexpr ramses::dataProviderId_t o2VPOffsetProviderId{ 100021 }; - constexpr ramses::ContentID masterContent{ 100 }; - - /* - * Create a visually empty master scene and create in it three scene references to the two overlay scenes and - * one external scene and set their requested state to Rendered. Also create the data providers, which are later - * supposed to be linked to the overlay scenes camera viewport data consumers. - */ - m_masterScene = m_masterClient->createScene(masterSceneId); - - m_overlay1VPOffset = m_masterScene->createDataVector2i("vp1offset"); - m_overlay2VPOffset = m_masterScene->createDataVector2i("vp2offset"); - m_masterScene->createDataProvider(*m_overlay1VPOffset, o1VPOffsetProviderId); - m_masterScene->createDataProvider(*m_overlay2VPOffset, o2VPOffsetProviderId); - - const ramses::sceneId_t remoteClientSceneId{ 123 }; - auto mapSceneRef = m_masterScene->createSceneReference(remoteClientSceneId, "remote client scene"); - mapSceneRef->requestState(ramses::RendererSceneState::Rendered); - mapSceneRef->setRenderOrder(0); - - m_o1SceneRef = m_masterScene->createSceneReference(OverlayClient::Overlay1SceneId, "overlay 1 scene"); - m_o1SceneRef->requestState(ramses::RendererSceneState::Rendered); - m_o1SceneRef->setRenderOrder(1); - - m_o2SceneRef = m_masterScene->createSceneReference(OverlayClient::Overlay2SceneId, "overlay 2 scene"); - m_o2SceneRef->requestState(ramses::RendererSceneState::Rendered); - m_o2SceneRef->setRenderOrder(1); - - /* - * After publishing and flushing the scene, the scene will be offered as a DCSM Content with the given category - * by the local DCSM Provider. - */ - m_masterScene->flush(); - m_masterScene->publish(); - m_provider->offerContent(masterContent, masterCategory, masterSceneId, ramses::EDcsmOfferingMode::LocalAndRemote); - - /* - * The category of this offer will be recognized by the DcsmContentControl on the RendererSide class and it will - * assign itself as a consumer for this new content and it (and therefor the master scene) will automatically be - * brought to an Available state. Since the scene references within the master scene were set to Rendered, the - * referenced scenes will be brought by the renderer to Available as well alongside with the master scene. - * see RendererSide::RendererSide - */ - - /* - * The callback of the scene references reaching Ready state will be called. The new state allows the - * data linking between the master scenes' data providers and the overlay scenes' data consumers. - * Since the requested scene reference state is Rendered, it will also automatically bring the referenced scenes - * to a Ready state. - */ - waitForSceneRefState(OverlayClient::Overlay1SceneId, ramses::RendererSceneState::Ready); - waitForSceneRefState(OverlayClient::Overlay2SceneId, ramses::RendererSceneState::Ready); - - m_masterScene->linkData(nullptr, o1VPOffsetProviderId, m_o1SceneRef, OverlayClient::Overlay1ViewportOffsetId); - m_masterScene->linkData(nullptr, o2VPOffsetProviderId, m_o2SceneRef, OverlayClient::Overlay2ViewportOffsetId); - m_masterScene->flush(); - - /* - * As soon as the scene references are reported ready on the master client event handler and we established the data links, - * let the DCSM provider mark the master content ready for DCSM: it is guaranteed now that a rendered frame is - * visually consistent because both the master scene and the two mandatory overlay scenes are ready for rendering. - * The optional external scene will not be waited for. - */ - m_provider->markContentReady(masterContent); - - /* - * This mark ready by the DCSM provider will be triggering the contentReady callback on the DcsmContentControl's - * event handler, which is implemented to request the master scene to be Rendered. This will be done for the - * master scene and the two overlay scenes atomically, because all of the were in a Ready state and the scene - * references requested state is Rendered. - * See RendererSide::contentReady - */ - } - - void runAnimationLoop() - { - /* - * control referenced scenes' viewport offset and scene state from master scene and flush master scene - */ - constexpr int32_t overlayRightOffset = 960; - constexpr int32_t overlayLeftOffset = 0; - constexpr int32_t overlayUpOffset = 420; - constexpr int32_t overlayDownOffset = 0; - - int loopCounter = 0; - while (true) - { - constexpr int32_t distance = overlayUpOffset - overlayDownOffset; - const int32_t y1 = overlayUpOffset - loopCounter % distance; - const int32_t y2 = overlayDownOffset + loopCounter % distance; - m_overlay1VPOffset->setValue(overlayLeftOffset, (loopCounter % (distance * 2) >= distance) ? y1 : y2); - m_overlay2VPOffset->setValue(overlayRightOffset, (loopCounter % (distance * 2) >= distance) ? y2 : y1); - if (loopCounter % 400 == 200) - m_o1SceneRef->requestState(ramses::RendererSceneState::Ready); - if (loopCounter % 400 == 300) - m_o1SceneRef->requestState(ramses::RendererSceneState::Rendered); - if (loopCounter % 500 == 300) - m_o2SceneRef->requestState(ramses::RendererSceneState::Ready); - if (loopCounter % 500 == 400) - m_o2SceneRef->requestState(ramses::RendererSceneState::Rendered); - - m_masterScene->flush(); - m_masterClient->dispatchEvents(*this); - m_provider->dispatchEvents(*this); - loopCounter++; - - std::this_thread::sleep_for(std::chrono::milliseconds(15)); - } - } - - void waitForSceneRefState(ramses::sceneId_t sceneId, ramses::RendererSceneState state) - { - while (m_sceneRefState.count(sceneId) == 0 || m_sceneRefState.find(sceneId)->second != state) - { - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - m_masterClient->dispatchEvents(*this); - m_provider->dispatchEvents(*this); - } - } - - // ramses::IClientEventHandler implementations - virtual void sceneFileLoadFailed(const char*) override {} - virtual void sceneFileLoadSucceeded(const char*, ramses::Scene*) override {} - virtual void sceneReferenceFlushed(ramses::SceneReference&, ramses::sceneVersionTag_t) override {} - virtual void dataLinked(ramses::sceneId_t, ramses::dataProviderId_t, ramses::sceneId_t, ramses::dataConsumerId_t, bool) override {} - virtual void dataUnlinked(ramses::sceneId_t, ramses::dataConsumerId_t, bool) override {} - - virtual void sceneReferenceStateChanged(ramses::SceneReference& sceneRef, ramses::RendererSceneState state) override - { - m_sceneRefState[sceneRef.getReferencedSceneId()] = state; - } - - - // ramses::IDcsmProviderEventHandler implementations - virtual void contentHide(ramses::ContentID, ramses::AnimationInformation) override {} - virtual void contentShow(ramses::ContentID, ramses::AnimationInformation) override {} - virtual void stopOfferAccepted(ramses::ContentID, ramses::AnimationInformation) override {} - virtual void contentSizeChange(ramses::ContentID, const ramses::CategoryInfoUpdate&, ramses::AnimationInformation) override {} - virtual void contentReadyRequested(ramses::ContentID) override {} - virtual void contentRelease(ramses::ContentID, ramses::AnimationInformation) override {} - - -private: - ramses::RamsesClient* m_masterClient; - ramses::DcsmProvider* m_provider; - ramses::Scene* m_masterScene; - - ramses::DataVector2i* m_overlay1VPOffset; - ramses::DataVector2i* m_overlay2VPOffset; - - ramses::SceneReference* m_o1SceneRef; - ramses::SceneReference* m_o2SceneRef; - - std::unordered_map m_sceneRefState; -}; - -/* This demo consists of the following components - * - * - A RamsesRenderer and a DcsmContentControl (RendererSide) - * - An overlay RamsesClient (OverlayClient) - * - An external client application (optionally started by user as external process, provides scene with id 123) - * - A master RamsesClient and DCSM provider (MasterClient) - * - * The RamsesRenderer and the DcsmContentControl will be set up by the RendererSide class - * in a way that it listens to DCSM offer events and automatically brings offers of contents for a certain - * DCSM Category to a rendered state. There is no other logic there. - * - * The OverlayClient creates and publishes two simple overlay scenes and creates a data consumer each which are - * connected to their respective camera viewport. - * - * The MasterClient at last has all the logic to assemble those components. It will be set up by the - * MasterClient's setupDcsmAndSceneReferencing function. Refer to comments in this function to learn about - * this process. - * - * After set up, the MasterClients animation loop will then animate data provider values to move around the - * overlay scenes on the display as well as scene reference states to show and hide the overlays. - * - * IMPORTANT NOTE: For simplicity and readability the demo code does not check return values from API calls - * or callbacks. This should not be the case for real applications. -*/ - -int main(int argc, char* argv[]) -{ - ramses::RamsesFrameworkConfig frameworkConfig(argc, argv); - frameworkConfig.setRequestedRamsesShellType(ramses::ERamsesShellType_Console); - ramses::RamsesFramework framework(frameworkConfig); - - constexpr ramses::Category masterCategory{ 1 }; - - RendererSide rendererSide(framework, masterCategory, argc, argv); - OverlayClient overlayClient(framework); - MasterClient masterClient(framework); - - framework.connect(); - - rendererSide.startLoop(); - overlayClient.createOverlayScenes(); - masterClient.setupDcsmAndSceneReferencing(masterCategory); - masterClient.runAnimationLoop(); -} diff --git a/demo/ramses-text-layout-demo/CMakeLists.txt b/demo/ramses-text-layout-demo/CMakeLists.txt deleted file mode 100644 index 079f15786..000000000 --- a/demo/ramses-text-layout-demo/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (C) 2018 BMW Car IT GmbH -# ------------------------------------------------------------------------- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -# ------------------------------------------------------------------------- - -MODULE_WITH_SHARED_LIBRARY( - - #========================================================================== - # general module information - #========================================================================== - NAME ramses-text-layout-demo - TYPE BINARY - ENABLE_INSTALL ON - - #========================================================================== - # files of this module - #========================================================================== - FILES_SOURCE src/*.cpp - src/*.h - RESOURCE_FOLDER res -) diff --git a/demo/ramses-text-layout-demo/res/ramses-layout-demo-colored-quad.vert b/demo/ramses-text-layout-demo/res/ramses-layout-demo-colored-quad.vert deleted file mode 100644 index d7870c539..000000000 --- a/demo/ramses-text-layout-demo/res/ramses-layout-demo-colored-quad.vert +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2018 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#version 100 - -uniform highp mat4 mvpMatrix; - -attribute vec3 a_position; -uniform highp vec4 u_boxSize; - -void main() -{ - gl_Position = mvpMatrix * vec4(u_boxSize.xy + (a_position.xy * u_boxSize.zw), 0.0, 1.0); -} diff --git a/demo/ramses-text-layout-demo/res/ramses-layout-demo-red-text.vert b/demo/ramses-text-layout-demo/res/ramses-layout-demo-red-text.vert deleted file mode 100644 index da78ec58d..000000000 --- a/demo/ramses-text-layout-demo/res/ramses-layout-demo-red-text.vert +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2018 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#version 100 - -precision highp float; - -uniform highp mat4 mvpMatrix; - -attribute vec2 a_position; -attribute vec2 a_texcoord; - -varying vec2 v_texcoord; - -void main() -{ - v_texcoord = a_texcoord; - gl_Position = mvpMatrix * vec4(a_position, 0.0, 1.0); -} diff --git a/demo/ramses-text-layout-demo/res/ramses-text-layout-demo-Roboto-Regular.ttf b/demo/ramses-text-layout-demo/res/ramses-text-layout-demo-Roboto-Regular.ttf deleted file mode 100644 index 2c97eeadffe1a34bd67d3ff1c3887fd53e22c2ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 171676 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 ZcAgz$Pp~K2ZnnEU$(~HkLK9l*e*jr~ZsGs{ diff --git a/demo/ramses-text-layout-demo/res/ramses-text-layout-demo-WenQuanYiMicroHei.ttf b/demo/ramses-text-layout-demo/res/ramses-text-layout-demo-WenQuanYiMicroHei.ttf deleted file mode 100644 index 05c29aaea99fa76724c5191dc6b2e1943aaf62bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4505016 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

7^fsLn@q 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%0D3(S39nHmjRbddFVvOa$uAGW;Lx4otx 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%0Di1j1t@^8K|)-U5@ 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 A z(V9NXy{-LNA2s?B(Inaz<3F&`R~MJ7n>hcZmSh_=c8^as0WT6xeAuIfrk8wh&bLMV z_6r_dp6h>+>nr6ADAreDwMi;jc+yU)y?N`AGrTvILZ-@nkP_y#7LUQ$#;l&aYvAc0i+|gnP&HtM$|pzyvgqFIh)<*e4DlZ z_{YkXOYXd9{`^Y&w3F?Vr+wSfam!dx&1KDu)pJb$9p-x6t~&y?wrv17T-R<1)GM~C-zoRUeXRM9(;+jB+&o8 zbMX|9crA}=2cEHkCG5R{a4-gIcC$v08jj0JhvAyEzMWQFz}p}5I6gmQEar+-8~Rq- z>hx-RJf7Cti|LG+ZqKx3Q^~BEOvloxWFidTy`?cY^rKO_b%rIo;>mI;&7!Sv?CJfd zgIRUY_uR8#&mMN&-fes9Oe+yj<3ifBp*d#R>R^Rfsa@`AX==BLh{Z@IT0+Am>>jAt z)l#xYo}c<;b@y{@r1KN;f$paE4sG~R=mkVm4Z)e4vye|fQ0Wz<K2IKTIhQ|^)cuApYF_tDM8p`JVc+j(R1s_)(qeRTL!J%4$0H9M;3kIUJ1 z=SH?9?)>x5Q}3?3x@Ygk;7OG~)2I4Mf1NFO%@gdkCl7u8C@!Pf_vmAjt{!&kn42cs zH{CRsmG@7&|FM;Sdd$9Q@*VXTS0}I?s~zpd;J71eZ}m?&|7vx%uv=OM9eMJF^N2B2i50AW))se z+QQrlc2@eZ7We!U%9C~SAq&>FXce;&q%eYST?FV)G{-Z>3#;?Cwl@VSV*c65sCI#3hV z-iefDfOsA5&7=?VgAC;`P>2h|j=)Jn=$dTA|*x%g7BuZub)=^%+Z(BEvPPXzQw-Ee(U^*jXU&E zfAcVO4iY%p7XI0Au^@pX{@Fb#r+e6m(^p*h#*_=E%nhDf{T$mw-SFFY*L`?o`_IEW zpQqKw6YfZIcyO+l3R$wzwfTiC1l~72Llj6{%`5MdZOz~D;6ztawndPXR>G+s;UnPs z-NtSYPWA`vPsuMq(cXOXxqKl-GyTGi?o3Pi%+Wq;Q^AGucgS|+x{Rpnz}ddKxLmE# z9pL@pbxEC2jJs*O?;XB(vF$u+UrLSnUf(h_#%<^Np3U?6!2LYb2lyzV(aCq@_(ePW zXZheppUginl;Duiv@iT-=stK1DbCA1e|U~}0+ARP3|Ag(1Fla}nw%B2Qd-1#;qEd7 zv=6TL(l{d6A&VEYsAV|^vYM9GD+l{iV{oJM8|SBE zm7+3ativu%z4ncTTlRhLe=c?Ay#09TinpEerR?TUuKB04^)mKU{kzVvC!Sy*I>VUZ z2=apvap$=xI&d zd*snF`Cik+TO>`*C6r3K;WWtkhC8Qsd-LPlGursM?;TFhE%1QU@EVz8rDd~^2JZ6O zu|o#>MZT>N(T>+&u?Ao<`_>-yH|yF&h^6kM8QOosIP72g+DP-d#9kT3ZnsliG*VyN zSg%_hdWTySyayP+@U2L^3~YRkYo7&zF3rO{GqZY;v9YiddXVs;WpkGR< zJG#CoOj~H%dTHh#ZtX%09nYnYT=x| z1dmz&Phb71KAvpd>4W?G9`I5@XwV$Tx;OU+expeXSO+HGKbe z{*3kzvLtub{2F@*q2>b}qF;yk8Jo7$w1ah3V=o$BJA942cp3VP8hOtZ?-}>IiD&yE zU&N?5ZU#2|1a{`|RhhsK*%bqaFCE3EIJl*jaK)dKuhp|d_x{c63&|3|1uiUDuwGmI ztj6xwsUHV^=WC1Rj{60g{CK+6dynsHHiI(U{#&=mKGf`UH*NB@)n5kA^y=&X>}L6_ z#=mC?oeQnyn{GpRXIE0#r4C{vH-qI%Wt&nJS` z!i{A%+IdTP)p-lQKV;wESiNN9pA_6mn(Vacrp|Um{I(kDWHuE;c!rgYMLSB#LOEZE zm+boVk$FY^;suwS_S9oLcRsf2sp@g#FHwGUCa_NNxrDnV&<0|NS{cFPmi;l2z7UkL zi1()b5O7ca8RJ(D0EQU^6MyJv(ip}L6@2vws%*Kgt*4(o%9>7j{5>ZS4^W?RQu3a>*LpkS)ug<&p z=AL!^Z70^1&ERsys2NeSUG>KgpIez2{7PkS@U1JIO5-ClC*3jgx>*xv&it4Sa{hLo zQ~3GPnRD-0H*x0IXFW1EIOWJzlvoG1a6V;5W*_`TL;a-hOgbMYc>H#In)I!vyt_K^ z(s97;CCCA62E-j|q;XMPy@(Chagt|d06c~41+uZ}UNzcc=USH}C=qNAL%yN$e89Pl zO0jxF&S;eB-lfmPcM?cG@b5SUkEu)awAHO&&**nO{b0MdZdKm05l5~hV(~&UJ~N#% zQt383VU&_~1W|aUjMAAw+V*Uv-Ad|KD)`!(=?flM5MA(C@{rLV@k-fztddg}nfIB> z4V70yhEpyUaX)S{nM!}48QF}E+lgaQ)kqXlnvScc>}(McNo_ej@3d5}dngs%6f;62 zjywfi7DzcE`O{6nuq)?EuFW?vw0?~bUMBgpk!u$|eTjG$;7#yu*2n@)=u4vhChl;P z?G~thsjb6i@OX~TPu8@-gOja7a`y*^)lYk}X!pfc#1CKmru&q)A@!lACW2BAyf(NT zqBsuM*J#0Z!8;b$M?rHf@6tQIGcyVBakL}*dxKY(^tCs^>smAJntj~W+`g6L=DdB) zuWHH@zbN$CRvV!y*7jB4Z%u~}UZSyE`TT9TEkT0}uCZ7!)svqreusg<=h zTp0ckMU2CAqC>(%`a9V|aM>{phicNezuL$a=*t;5V(5&^jVETj`%L(e85=HtcH%#9 z9ynKj*OA?6k{)t|OF2{5`f$vB@EdFOOu{e6z0lE`*30rsz()&Y$E^AF`2lE~FOfyN z1)S8QIn4W7Rzq__&+zDBP1bAMZ~uzjnUHmjR~Pp^i2kkt7x3U*7d-GP+Gsogs{9`x| zY}}zb26i~~UQh?TMWgu#ceV3FF*v&Ex@w0a5DS@p`YR=z9EC!{`1`m5Ff=rAK1&R~ za3UKD5I#h*_^Yl>jVXy-PiIciqY0-F)TjODE9~5)Q|jn;$|@WAKJE=~pS`SUT2H#a zhN47+dZnPW)3vSDL|q{3q*;2DF$-?tZnG1%jgy5}P1n23T(_zoYRAzB6@J@vHk=7- zGiOFp$x$=wb4ONF>XNv&D49w+cB_#pCNHfsBFSWTcZtOyoLm|yy8J#8+UU1O`2JYa zy2Iv@<^Xdev5y@;j}AC})c7i=d-LzJRhn$?9pW?IJg@zUgbRBX@GV?BUfty@&)eSo z4pEt)8tW>r4TE@A)7rymO!BfONHUl9xSmG2v|YX=vJhO1n`6FO%a4vk7{r*!|L8`S z)BRiG3>t25=QeO3JBtLnDiK>LS5^8{v;74o-^Oo}dLJxi;r zbL&*b-f^yCo1O3dTA5p2O`lMz`Ubmpp7Y9l_O&C6cIBB|Zt;=_tb3+STRdP|bt_&uDB9(tN0Wq>OS`YqD>8xm30)stdf(WsNKtgvmCGPqnbKYhkMSH(8OCb4nO zHg@W*w|@B8+c&)Q^y;TK%w943)qA$S=lqQgdhb1_!j8FzEq(gw)hnDC_;KioBXIkL zdMcY$6V4OW^q;(Y!s^br>468Xz2LHIAAH8?ebnjw@$FCE{$nS{Do!1{YESxsN8efS z{G-n=c;>+;orj-!W&v_ZKks}>KkFKgED^jl#C5Eu)kLd4{z~}nOkNV1y3b;{z~YgQ zji1KnUG86sHkzPS;fs8nN6{LE_I*O&6huRc>}o>ShL?-9Xu97WN8#ZB8Xyfl?&KVV zbMkzNoWK=sI;t^=A+CpD$BT8vLH>7`TuA+wUESW6NDrHNwe!peJDp2zR~9g5-Hi{e zUsn0!&M9BndE;Zbu&x?uJ8WhQ!_abIvx=DmC}TzJtdY)Wwqa!|B{OQ6hLRV*G^-M@ zo$u{gIcMYc*v2IbmTY8aF1hVn&EHdAb3Pa~!uj?6t5z}g_sviJl#Sjo;^)tNWye!z zzVYOU&2^uo^rV(Gv{=l@X0tdXkWO_oX#E6J34}%fYK@jmEo()~`00dkSPc2bm5w`> zFW)fFx$owM%a`Bm%)P^T#96hF-7MB6>8*YI5Y~)G7lIdiTCNA@{BZxoe)Ph5y5!MU z`s%TW*S|ErU-<&*BKgz%!E*$+e|7|T3bp_w_L1mxEvyNb*Gitk`?F&t;I8DD=8b`m zd4IgGE_l;Z*k61qv<@~N>5He!SlrQw5Tld8!lH(^f`R3pNil^1@}JX@Lu}5HJ}Grb z=Khw1C4viS@0?-<8+_jQ{v;Rt$@ZD-au!^9rxJDEt@O@Uwy|N(pi9P`y<*5}I*py{ z=wL=ywa9<5ahofJ&C0oYGMh;KT=`!0Vs^A;ElRbv=^cvN+NzezJ$LozFZEL{zW1*3 zc>OMC@7)hh|L2g=7f+Z_J>H42n6B$iHyox)(fMTgayIa1Mn+2;&TGpCRNKx-<9@KX z7T0oGJoNq-u@sl4_}s3&z2BVJo*mY>Mxmz`c)q7M_StBVi5{HG>!#=X=1P`-#jR^# zqm{(dsff>hvG?Qgi>5@qcRB4fk<6*(ZJdUitQxqmC$JWTzPr|_=>z&E&H}zrbl;Nw z;J(tPl`hn4n~zoKCfngn7d^l`6bs>BMz^zr@rFVSM=#Xny+tsoyv7L)q~46 zB>Zv@F7v9@>|en@iGI;}1>WbT71Ub`9$S1-dQlUX1g>#m zufU?rb@pa5nd;G+r9<|mvswm>A{#N0 zRWuXFt!!`M1hZjTM(DyLtBf8^b3U=1WPr<~FY!IYT5MYGdv>w+jMJ&m8(R9}o@6W! z!QoYTi8Pd!z49HhGqHxHJ_C=72lweYb|0M9kEj1MJ?oVx+ZXbwX_>pCy?Tmnyfp5= zhUduZ>&u~3WX_hidLa`p`cy^F_?n!$EflB*0^{yhU7p$3zeesY3RzRUx_!1xeymqF z`J=osH9h0ORrDQx5i!>MAcAwh$l36v8$8a-&3`r;vh7A5Cx2%EyOf|)?))7#7OU9z zx9|iDNhW^QtZr_s4jnyMY}$dwCAEVi&^MW^Sk-4S5HP5j|E}v)aKU6(9NTeDyqZyr zTx(~X1=^js(q$Sr1zELoDOCqu_Fwm*^!mz(bw6ec*RzGrv+Soh;5xPX^7`t_%2ekZ z_P+D%MQptDLs&VZDxbT5g^X%@PE*aKo;SU}qOpXLh^P3^&LeY!;EA6;{`jYVzUrz4 zS2;&dzyFa(?w`*7d6m<7^UXK&A5sRGefYLiLbrr-znSF0lRbD1En1U2IPqD`r(HOV zwn!HTJb$(T+2|O))3~AilmLkiG{|DlLQNk1owdW4= zex{c4XUg$$72O4G1!I+tw4zswCexhKM^8s$x7|8>>J5``yXQmYy%mwc_sqU$>aAmE z+;h-*(~d^FS;BBSdIMcfr_Su|;D*W_Y!MDQoZ6qW)pzGAi>v7j!%ZDshF-Vo%o)?a za_gYD(T?%FJN%M=M2cr zgQ3Tf>1+>c46x*H5!XIv|7Lb_@8@F|sxu>Vp3kxgfuqLQY9Mm?nqSukCPXDKp!JLS ztI-3nr_1Mv);yj`Tlt&rOv(8($RBHyuY%92H|IhRT5=d%LtYo~GQlVIUF4&~GPSYp zUX&NyDrAB7WE-4MNtzz#d#6Y=D&hXR_*i!Bqq;Wv^&r|)E%ou(UC^t%Hnqha!eUjK~_}y~Fz;-x=*|$3K=)HE@hymWKu5)noSY+qFL?IOnSo z#FZS*S0}-4y&RF8HwSL}zvin$FL@ z`Czt^vlX>k4qBNEB_&8`m5j||idn@0^0b5a6_0!V|Kg)cdD@pR7`xyy=XbZ?)EjV4 zxOUoci;kPj($$ZYXI^qXN?+TDV?I|~yJ+sWeu|%*lI7fU8f$vmh01wJLMDUunx3L} z4)ZP1dcj`Ki?mYQ8NHCdr*l6x-rIZ%)n{Em)9KMTWPi>215f17lAu`w^*vAZ?YfcP zBs#&f8PMzD1BuLD^J^tADWe8(vgN_f&n~(IYklgrZuV$zWz<_L_QIi4CwoUUv?2D8 zVqt_feI?IkeGB&=yK5Okz?Ls2h9^{l#Faa`^qZuu*r#xBHtVl5vY)dd z8{qrB}o7u3CpbJ;u@vC{x=aowRvg+f?+bgQyUr~L1 zx$<`P@u%57XS(u3_L?)LI*R}OHY+(=&!+y;n*H^SH&zdsH}A=LH#^U;SsRu=_0nsr z{h)c@S_DwtAD7+h+|7=0u3>weDqDW9bKvoZAHEvtwS$))Su_*Q$#2VLov_)g^L=wz zAYQoce`%iNN=CeloI}2kC!y!oyhHquxRm?O2#+SRoW`(YRl{3}?|@e9Hi8r1k>%q; zPu%$k)g}26BvcIxffxzgO=}9N$y}hNW7fY zue@1Pp3Wcaqr87hr)~~h@>|j*yu58>GwOu@Z5WG(OZZzP{<)pI>RsQn;a=bTd!LQI zKJ>5eOz$0@Cu%J??P25RyrgL{!F_W>cCLrVrpLTCYT(u|AKa&ZTSFy$KGypjl9OwG zeIl}fTA$J6^YKZV5O{BT)T@WjJMfONdN?`5HwNHK;(iFd!+lbx!M4mHAIVAZh5iT5 zw>HAAq#N|E#5sZc1CIp05r9;OX3dpuc~~4`<8*klX~XC=*B!;THnF1_910EW!bxoG zkU`|c5}Lhmj!U(-&8?;bty?t$ky58idY zdiOMT0sc4Oe9ieTD>yH)YoB1(IlIAR23~pRo9%-mw=A4^U*vS>CFkkk>np$Q&v5Fy za@Z=xmZ=jqSW$^6S;UpkA08}qHP|AF})#C!yT|Nnkn}H6n12Sc$e+JeebnA)Ady zy5i37-k&>n?nl*QzkbIZKgD0wPsQ(D?Iw1~15T%rvJgjX1NHlb#4FWIp%)9o$${g4*~p@o*qE+@7Gad zw3IntAUOEW#-F;SC7fc-!FMr=T7U^~3i`@}nn^ zyg-G_I4}v8gfjZoNq-?PC_ARNCwa1Zo@|rz^sPHkx655l_xi)*KqSv03UvF);k!?z zcXof-?Nv>EWTQ6nT0`_cp3&YAmn+Si(cfBGV3U2k_Exthubr&Nha0z%wHmTcLnj9C zbpTz@i{YRQ%5Hd2T&dLbm)H|%-ItVS}3k2ZJfv&2MrCKIJiI43x2?sIS+j8hy3f5 z8zxMduCc)Q_{<^WrzB!Zx+hl`=*gH$CK_|}IL$c=9zFDiUxu~Nr>?Z%yd!S@VLhJkLpd4_%D!9i6XoYVcHI)*my)#HbsEa3*vtp|GC3`nGLbCg=yJXpR%`aQB_M{x84R02CrXScUp_twziJWB;0<4-wgOiF%-tR+@BFvtP}b8>jF!9Q^=z} z72}Wce29Dpm=dZX_aP2mQ6MF@{ozUiCTQcAc6;mjA%PHW(edPc!^hMac`etfBfF-S zblSVKPBh={v}ahP%ogM$@nmmK{f0Ux}4fBxn9&YRa&=1+5elfTXmnTc$+C+lRR`6!DAi=`Y~7C6aT zz53pVmffW-+qrb-L%&(J?A{-*`+L-BiuIq5-QS~b15P<0xLiN*0;jfti9nQWX8)yCmsj}7(g|CnUtOF;?u=;Ppf zRDN4p!VOoKSpNo<_v)b>1YF0I^z^taFZvH&xaWbIc5|RJZgcIeBY5*WUb`CXH1uaT zyjif3&^qrY)t@GKelEfD*ZJx%gd&kSr{hj8F$Ww+`*R+gaEJx1Ur$9~YI!%+K+aw_My|j(Ni# zZK3`hMH;VfE~UOG>TmvqSI@kjjyjAA^FP(Ar)Dm<|Az;6*8!h{m%c-ANQOIri*JU$!OtWPhEEGK z4P6U{hdfdMy^G;L6e)jXCM|4FHkr14+if}$SV0_Pu!zl}O`*d7rh-TRdnR4~5S??S zkL4s-n^R^gKJVJju;?AL=3cq1H0!nJTc>T>#1@yFpG;hLC0qH=%l`JneanDP!b6Vm z(*kPfM~j#xrebhdu{q5HfjeG47ob!zX-14b`^YZ&Y z?ne357lqw^NgGG>YxkvIzjXf^vi~aK9&eE4f8>?t@)Gc=$*OSrn|FxPWBQ z_Un#R7v`4hRT781uLpkFhZD%;G{%PY3vGv@&#+qnK}CmZDUJ}{R-{_?323peCAP9~ zttU^}AZR&QEU=FJA!PpVsB|$s3}M~c6E~bzlSOr>tv+uXnPR){qjJHeS@UXtXC_nkFkC13-~Q z5=qvnM3d0K6jQAf3qd;*v*Sj=K%QO1saT0T=C@7Dt0CmqHLX||6!KiP&>1a6wG7j7 z}kV}19pz$cq{FJ=7XoklKbQES`QrE&^R=cA z|D-PfZ_V5%L2vPSJgoe|8wd{mPqHuvd{WfhSrV66476m1N?UvIh1jzq8)XmzZU>AU zL$$;YS6_J`UQnpKB%kD&+Pqkf%J8gz) zC5=j#l~K)XDU#1*5C@5TK}j4TOIdozjAv2^g0ALLdL$Q4D29`8@+p>KXgpxT`drksR{Q`c6=j-k9(O)80MtCG|+?Z`^6;)o5h z0a$t*e&5{BBP9!&W!lo^9I}(~&9CF+jPNnjm3`}DV{Wiq$R35lZK?k=(c^+GOCcIg6$BeLy z+~6>OuJ8dv#!yt8SRrIz)0tLo{_($q=~65i4D_Vw5OVuTf?sB;jEfy2yLfiy1 zF~cx-i+WqJR6_oJNmDXLW#=BkMGYN#I08;LN*sCo;=m<@Bd@%NyMxEp;Eu{?TwF&& zEnH6pe-U$&g*_RCr5xk8ZOdS9H?Z5YA8GS*EGe3}F>TNoy1sEBoe>^P*u|wFFf(yM z!T`BdLhf(As}WSb+3?6Mv!4IX@g3`DZQe4^`QXaVS=T#Y1Pj%uE z&Cu+Or6(gXoCHqmh`lbPS~J;LB?Es)U93=!Crn)p?pbyJq+9QQ^s4ip+W+YO`R_jU z)GY^A{durcK3%!rdfsBcu$;7$Z)b@ld<|Vr7m|F!;)VfV$eD#!R#F1hk`r}u#H!kq ze0R5|rVKLx{Ui%IYK4A6cSF#9a&()3+@)z8Ffu|O&w=CwQ5rCA3>)c?$e}-shD}>K zf|%IhvGN>$6tpvZOo<`>5}{YDGm?)eVJ8q(5zU^-XZ2*vO5mWfmhVcjh-JikkO|%~ z>aAQVr{^lyZwfEDezEo8mtL_SFs*x>UtRaN*_Zy_x^(g4#r4J3R;vwxIZkVr)f?z6 zbS9lL955^%ceGNzh(VXgd11A6<*=G_)wiCWb;Wh_pPn^-$YX0(teF4z6)W!l%~}>b z`<|5u9bKCeJtm2w_bU}+-%KhM+ zcMBP?ld7Tq(7G>#RO|%{nd$18MALk2?4>o(xEH#wSP7Cxr~2sqV4pqU>i4k7e7q&g zc@B7#upxKslWkKQTpts>ekZILiKFoiw|Ka?eAlCo^jtJeb)VJfQicDP?@(P{pP)xy zEaBdo-HQqE^@(IZ$HzYS0eo<(htH4f(>J_&q`fQfSJ))sf^Tv@OSu^cQJtg)aG zCnNg~&uEMsk3ufL@UpB=j2*PoGO^zn(|zsduaV{F^iGF8oEhB; zJ-0rPdz&?En!Wj!b@j-XP$NnT zbIpew4_8b45KVDf+s0+ICr<<%^CR%N-K90LCb3?MXHz}osgm>T(g!qKtj1>#uV2(t z_?cH;`mm(FmiCqB{S@W%b0j?7Yl4n41Ltg=(A%ZVR0E?%z%ym>wn-1*H2V_t3FNr` z74N~j9{q7UY1?-RDs&3y4#Ezb3(Mtb=nuoO8;n)SyomV$L->i56S#CETV>4YjGvL> zjGxC@1}IIL%qPedG|uUR=~d2d967=Ud+_$PBU$gY;~%@^Y-jfPOP*d^Y(C>@Hug@Z zvf|En?qrjlSBh1O{Mu=lsWJG(br`ViEShrkN<6Qp?QH18$`zMg7CcxTsLcIkV&emm zJ=3Spxb1=4Zu`le$Pba>fAPlOzrW_e_p2>VyMfE^!X43+hF#liBA-d?P{0ETm@~jE z)`vKwj&-o-3c}GI(0e4b=PUdf%{5;mgmdK5y2-;i&1nj{Gp+fM1-I_iJ6oMI{u9LNeB4{@ojB$9Qy-=E z-Sg+kTPE)3ZKhGy4kaK9U?HF{$H+QJkKy#v!smBCW?hz-x+EWO$U0iWHP8yzNn1FT zcseHWT$}p14V3HB3Gha`J?CxG-aU%yk@rMcp(OZ+xWP}@yPS)h7^?5v4IK@aJXvM< zIhvnaCcTqWmAiTB8f~shN)M$*k*%}auzi$H8&N&2ca$nx&@!_|!ig!CmNm23SG0@i zQZ}3E=~j}}R&0^<2I|wW01)Thme%vCQ4Lr)#GY4@Mxq!`g_8M#RZZ!7)Q&`z4m0aS z^%PEStM_WonS(UTXTPNJVu zpTBrr*n3ar$rtkNpvyxJ<57G3JSRIQ;4#_~16-LzK_YmT>Bm5BJS)92ws79fE_u2|d8XX(PJDRAl;e@8bjqAfpl| z;JIf3sxFQfkUs%rI1K#I{XQ9zP-MTO-<;J+f)$n&|H;~|SiC-*??9|ccN>d#bZGFe z1?qC0c8A>oXqG;mofX_hmO*L-P+cKqRl3tEC{;m)75mV zqtacU?#BtZf3ZxpsJEM0lr8DSf@&4DQckl9S}}*Advw}2e?KCW050TM%;RmG)*u(I z1a6Di+BhD*O3FVl4dxz$W-X_$ z3b7%~`?_!{seEu~SnD@lRz8R)oV}M`eJMNX;U$;ew}een9$oUuCtscY@Vry+d-#k~ zt55vxwuuj)f7?mri^^<$A3I$>9Xn$(5_Ov$taqW?L&U%FTsBzOy z6*5LV&Q1J|w576mSuJBP*25#0qhj4`5qrsD$QAN)W)Vq8H?0U8T-@{OMT)rbZXhw> z4L;olIQ<5V9@5}llp~a;{tU1#npPE~YEt|(|KtzlZA50UC~_3C)`XU+c14s}wSzv` z;QV5~m(3z+T&@5Wq?0G~H910(XpgF$`fd;}U zis%k;GzdxNSD1SmD+QeWdyTb0zv{|~SDti7WQy~H(9u7<{mQ3SUeZ00oiN;adjA|D5(qk0Hi=*Fggt>1qtgS4fuXHBe@1#=k@I;-{YYceL&R-2 z{mr9SPp841;;t=5_~-2iS!U;L6V>(;{onQ&;b<$j>`9);zWQ*!b&=OsX-`TS>Dqu_@0|=8X+-9%4EI(8>!yUq z1xQOf>R;-46TXf52q%!CB>NGsf4Pc&Z-wT_+nU~cLc{rVPHKOJb@u+(xog$mS}c zRm^FD=wr=0HE^0!j&sS&{wT|n4j_0Or>D)BJH#>d>B#)qi$#Lco3(u+$^f0keeiPM zeDAp12QRpA>IR#kCCf3(F%TBuzlQFCT`B0}z@LzL zz{e)8Lz7m|;Z#?K`XqSL7OXBgHj0A>d<&-aR<l9t z_xX9eOw_e`sIM;mj)Yfya9`U8IwjoO1Khrp+K`r!!to*I4tAP&wtX3mhGvkjYe@&2 zAq;(Eq_OjM_O=`bmwK+`)hGD+Rd)4z%mvp)IDc^WviV#eL@W3@O49vx(0_*_l0xJf zr0f&?HY;*&Hw@u{ZPnK|4AM}MZOzp+StsV`$w zEtPXhX{gC5r#&4jMvb~b8)q(`ck_^WcZ@sRxogduIyIZ=aBvH0rYnqKS1o~X>`X~{ zHjzQZK2n=xvx&5hNZ1S#JSPmKr_i$L%%OiE|8v%a6^LCsJ(t%CIc$6;@>czidfvYO zmXqyM&KMtjr26GP9ztuM^IFeNA|Ar!p5P(7XZYqSNA%nL1DacJ9Z-1=mpteDUirA7 zh0V=W-rFB1+T}co^Vvq84LZ~UPbcP`_&lfIRlkT~ZSG4Fp>leV^s^Z}+qbtux)yJn zyamX#c7<%4`o#GWtvw>TlFpiHp8Ig*J`w)syx58nT!V&T%Su!X_FT)pHg6uy2R%LjK_cfnmy@NdEuf zLv`kPf0b=;8(O@R@AlQj*SCPTK=b8w5xzN`@QwRcT%RP3S?ExF%z0Cm7dAS{1?=-l zxzMUS_X;;=0JB)hav| zOUEjA1ui@Pzb2w@@E+>D=hK-bL})hfSgj$j87IJEh7xkbtzM-f34nu^A{VOTiD6<=gpet6w9qFl}yDQq?a)A zrrz0x1RlklQ^1~lLCG?N)0evSPM21Mj3rv^yZ=+*5NnxMeI&H9*Ta?M>jw)2r&;DW zoQ^v@%i z4I$6*j-;iHvaQ49`KsSaILVe-G>?MM@KzpOn z%f{`asIOA49oF_5w@)5F+4lgICMxG`r?5tO9D(2gzWc>!?SeOmwcZSiL-v*QSd2mV z?wiXJF8YM>2X1tGBg@z9`8L1i)g$#SQP29_9$fMa0bk?#pJ-g14(xR`JjR3bIcVJe zU21)$Q`=X;rzw{uo>loXp*NTVLptei*~|CTdEW?*b_8uU`{6!abJM4?9okcC z}KBpZsd!%qZ96KrdHBn_$@y;-i8#)@|vG9@|qvco+u085&0BiiR$Z~Vdp$uIr%Ym zu5%ydgjlXTQGJv?!Pu=E-iw}pYwV0;pE~`Xtxru~zM;~-f}QRBeDzbq-}~-lNBM%? zu+YuU`@eM7u{po|arI4h-5jU9(D~+FSB?Hzbl{L7kDmSVjiWVX?8|2_-Tqi**x8wj z&l(^4?BDH*0X1@NJf0Q_?Yo;$CgFT-*}yRNNJ~C=hOfnsIS*X!!Brt&Ih}=850LO2 z<}yd+o4-XfwPuo6UdlK@6WbPhaH2^IC)yWW9GLiline{Uob=l!&98Xx$n_}J>8cJ1 zCtm?yF9$#$KKqw=sBg^8{;~PsEkBd>9Hwg=bZsA6??MLwE?`Z5+v|^9i(+j1Ka%hy zaKY()>r^Vg6%;LLn&6PNTe-bQPiyBuuA74rPUEtveDi_B#wC4mVh+5!4upQ->Q>^^ zd8m26uWjO$Kcc!;9{Pf|TO^$7;yh#HnKb|2z7p{nJ~nC_cYAcuQ_7_*Y00Q5Bfj?-YM=GD6CE+9=1iqnTU_anrfxpP|vTcE<{2g9BaUma`JDbXTIy3PpPTNAi z%HJu=3%Nlu@g$Cm|CMl0UWxL};N84kq6?GrDbmLpw+-WOb=N-QP${`qLYD$Q(r%nV zk@i6vw_r_paRy}ji*@%FL4CGA(Q*=Ub|i1*U%ozMs1ICbHm-hxKm8|f$LqI{@f$dP zVfW4M9S=O-Cr^2ce8RNPdkxPZ(tyB8@IMi(MBzTy0HP$flyU!Y2>=;tL{&hVe58} z#?8B6dqyuHC3SlyUW})eyX}|~*OR7-Q;rEOXR^4_kzs}v$+1|bqi!e;MUTu}H zW60`e*kv_5vu1rS2k*t|hu;brb9L0WCA1uSz9NIF$eFu1@D%C$*amW=##~SQaSX~l z(O7Mq2#&|XnJ+eh<edRE$5%%%e$F{i_Jkms+1 zoiuaWWpiJfyY%?M!81mmw{G-hr_VhPS9Ra~3{aZem~*#fq7tNrD( z7F=fEf7AVoE_mgMYoA+oR{E@Kule~|i=Mcr`jabXFMja)OPybxaq=azN4DHD?Rs{_ zx=o9}v-#4IA6eJ$sDIZQc~Z`x?03vLocV;0hNcd9HVcre-O;PXVAxbt&V zJEqRho4L$r-0?EaI_0C0&vKlOhw@$hfPCiLmnrc;;QR|z zoJ`-oR5`Zy;|uMJkB!XUm#hm+cn8p_2Tx*OL%x$~syiO&7Oo!pf-8TCez{x`a&wI< zuP1^R?;?HIzU9=wX-!DI7VGqU*>{r9Chc)GZzueCes>XD z10F8*`p>95)=vuZpcGmGOPMs(7;?PF5eL7z)j2~ z7Mi%TieF=esAvRONF{ZENace-cOLJK<)>^@&Oc-RdCHWY!G8^Yx$?WTa%FXYaQ+on z3_WkKdd8@VHlLlK%&Ios!&W$7cNQuU6Gu^X>|g87KPu*TodX8@+8#&4esmH!6wHP5 zW+rDP+p;WNG$Tl)5E^{NhzIYP)}vqhKTh`QuO9zE!}uQOI_1B?lE ztzi4Qi0eV^&U)3+^ps&`obK$RClOiiWT5LKfIGlC$pF{7I6cSLUhCDU%*?My!!oiT4-gL*B|1$ zXjhcC-}K;8e-&$U9kdT`ZTQw-9(n?YlinftJzszOeVsc#|lO-%l*^WG`BkXY-)GW(u6!#jLz57KqF!5bA z0F7x4aTOYuY%A}r-B5m@_nzb%5?`)-^yZJ?SVz#-y?XdK1-xawgwy!Ty#D{95mN8) zbu{Eqg7kXGA(21!oPdTsV?A&kvY9NvTIwf>M$2;{BJ_YOFSyl-GNMw50-RB}K!LO# z-}eBlo%+$Z0?Pe-ff=7t?DgWr6=ZL0?VP-@K4!$kOff+b_^C9M?vK{=MswIR1m zQjc4Rfzcf^L!_dyG|R_hM#@kV={QcM*tS`$wwg$NgUmGLTn=Z7%IQ*p)}Uvb2)jVq zCZBlgitD3d9X7ux*Ojnm!3WlXFC6CcL&TyJCWkz2KrtNr8(C9uhr-Z$7$E+XQ^8EISkZ0D9Y~*Q%1YrJkDURc zRG&~%)Wg-lWL?U}aRDbvfif1w%Q!K!wKW@r)3}W;)oGk0{DUMPu>)mtC$5U1! zx5x9!1-pjFvs~E=kv{5eDma!M=wnA!P9+mYIh&smHtgJf7WMVw;w*a16#9I z)rXh$Je%$odZMmPknHYmJ#1k7jw%KZF;hHt?iYs(e+kB1He}U-wQ?gz+6a* z`vMOl-Lvr0}mmxV1feaL-ZKLE+*svF)oU015Jt@QhL@06?ryLV%YE@ z$V<*6UctirWf5c;Limav3I;C$pJ^88c&c1}s@qF@;i(@%R+z_Rj!g z3Ng1L7B5QkS$_%})NmBT`7CJ9_z_{l2cFD^h4O~e6+_al*0#8XkheCc%j^ngf zZLj8FH^LE6Kz60bp-6p4Bn)6Tr9-hH5n0#JH)K|e)q~>PKxZM*#9=BKhU$q#sQ*M}XM)lw5of5N zSMwE|e{54=0^%Gm4&@=6S=z~EwY-+Gg5heuK2s`HtrT(-$8tHHcx9!#KFc#0Btkhm z9kn&R74edK8XMy21el$Xs&*H+#a!;n7_mZJZ?g+Fq5{iw_FL%1a(vMKxZVc+AdP(j z#P}UHJ{6})zHp3;hDVoB;TAL~0m8PSaNBxF2aL2PrFW=|&R5b2rCZ4rGXH74U2V-x zvqsrYrkJ$Ue0`y<4P7^51wGM|$K7a3CST1#iHc)8ud^rb{uRntM%2zGKeYAYlh0;T zxhG=_pG-Zu$oY2llZ`)}J^REhM_v7bGn=j7s0@s3TjTug;fEh~mLMHHokKREYiGeP z>Tz;^6mnFn?$~f{pH?g+7gQ8l%nDBtbSCC@7}DlJH=<*7C?ula{l_k84hxk6)WoV* zXW;h3WU5>-!jZagPkSP0#huPBMq&r?5}i&F7bCV7bC%PUVJX`&SZgHgWR-x`ibI*W zEzydrIMbK35_-tYWuvyE6^pT=Woarxh`UN^JP#46=HqIn$lY84W+TX=+G;~N40hof zE+duD=_k=_9p;p1md1M;#(J4M+IpO0CoNfCg5+-F4i5?}2yPY2RQ0bz`4a?Mfx`II|weA41-!gx1Zebt zJ%cM=?O--hG1R)W-jj(0yGc>hD>gR3aSug%3WuI>+##=qa=7~y>5gpOK&oY&bkLG2 zB3}?u-fk6~Kr70rDP+qk!F9yC0ky+uxS%^7nQYw5gyU_9BQGNQrC`~mNL!ZTho-`h zd{t-}?5&HCDfBDI6iPk|;r$CB152phzoJ3$gNU^uVjfyNNPt8ctrRR1AIcCLGz2md zgjG)l7=Gj4jT&mr;>2?;p{hjpY8<$2~w2Kneudd9qMJ}qm{+xQ_(%Y4vYLRu3PoFaHlxe#acJgi4HmaA!E}wkL zDI+g>>drggy71iFKAtx1^lPRK8ez}A=%KIAzB4xOwqxflKIWMlvE0C0?*@Nq3uPj6 z_APS|nKkl~NwXqTw=N63)!Q|00hkTp-^Q90eAc~1Xd}GSl%;dxLMAmt^eQBBBB(l1HO`D!JeQ0yXxjxf zSw0jg#u5pfz-?n?rdmv`mXx?vF6$Xco~#*WpeRIuAo7IkK-gxCShn!hFQp{md?|c6VHM5g)=EodklFSpx~6+deOASR8CtY#Tjm z%Q2iT!lpt5=}}q>R=xuX!mut>#AtU`k-4rD`9t%z)!r3K=uWmhi=)S#PHPV4T+C4o zT4&vV(?Oil~6x*(HJDT)>({V~4J)(PSASxOHXHuR6ewB^r5iwb4 zh)3~zLg_Sx2zSR>r7owGVToW0d@+XbH>ay;A)$xCkd!gvl$`ErUT1AAmN!k9G?lix zuCSSd4u~`ciAWWggonjw&)J!hfn06jOskenr4xBQXQpwEpVYUM;RGB^QnVP&Vpk-r zbmKfosyE~7yQjgp{U3JZ{)4JcIB7_1>@cyhJ|xySntL!uLb2vUK`uNw6y1RehM^Q; z^k!4I8>h_ypVC25Q7da&t?kU}!421Nu|!xn7LVF-tD1EJB_%ft3=CZDyABCxd-p_^qnZwScw0gcd4e~jK zr-y;FBR4G0sCTC!+HvTj+HC-w#DB_l2srFO(jL%U!m;o$iX$E@*;%v;o$Vpb#B7_k zY2~dF)`bnfPNrH=ozC`hrkL&OGK(1mIK@y=~2` zotK7VCYi#9pO%3TueUQv6@_p}5VB6w=?XoyzFaO_PCA!yVr_*s&`3C&*I1F2n5`vs zSVxeEpnuI&5e1q|WRC!IjHC*^*f5ZMA%oL~YBq^$su4Sy${HyhMoBV}%p!0VI3@kK z6jFgv;>%wPJOHYI>Ich^crplMcpuXx57$usA?=7X$RQ*g`*4hX03K!Qos$$C0cAXh zaE=`b$H79SGt^q?QjmlgQoaMerL>Wa<@IE>Jzwq4Cliz@6Hr!$QV1M?@4{3x^g`Ow zl&*}XS?Q#j5)k01iffoOSbD$@3c*hX8%*g2OvQOhRmb9jpV2@)_0Txmlkp6!ig=Hi zLV}Q}Ru@AukpMo(KWid%S4|cX&8s2Xg_=SFZLCyNPXS`}0ni&KrDhGwK)0xVOs9$~ zvjC#^C?~B8gT2?*tAzr59a4l3uqqsq%qzuEw!2L! zR@=;chLTdKis?l8dRN8XZF3u~mu;?(g#2spePKWT8iH@S!N(sse8>4U<$u_F9@ZW9 zgo?0|2BQ4@KS+K;d&cm&Q4OePhF4D#?MgUecOlVh`Hv{S;U@&gn5jQ;+?{g*{=+kf zQK}a~Q+(HlW%7!iSK@~{gB|%wG1-OWU?Br{@gXk}47^HSLC!CyGYwY6V;xD~2QR~Z z!S|?NFRi0yfDhU`;o}qV{jYj(Hx`1I-=33jgU2)27~k)yo+gsSq8^DqQO`lcgA+c` z?ndITA$rQoPe8kG6MWO}Kg7P?=AQ+AfIF?;R;e!9_u%h{Hg*j4>e3pXpnZz{-wOsN zO;qvTmfMlV{@8IJPM*r&OXAEEd_?pf_A1wU?{T^8j-6)A>$C93@iFFIzZl`l!^yzV z=5ez8oZf4I!yL6=|D_3kq5aSo{zpC|+0W;<3A+Z$ky&P9R@DX%EPQ#Tg8re9xL5Jz!`68GNBvvr<7%1=BsCI9QBu-K25_ zD^@5Xnj%v*(n^}8E1ik@M66VT5nwxEyBZdDztRqEqAS;p_Q57R zo9vHH{lqCz*&h%i5h6ZVh_nR3-vUZFABWc{Pxp`qqm|Bl#SCJ-;lMQuhmr})WTs=< zPRy>Rp@p%I_B;rax2yGPL6Xa%gM-}v3*6KHK`+lxH!!y-*Pv4Wc5fy)J(c~Y7*K-S z1aF21(ksvLdvE~34}^Vit_O(v54t{88}+AYo@AcWUSD~G=4ty}s%P6hJ~*clst4yA z?~rgoJLpgIX9V8|YVi8Q*PrN5^XDF1@*~-wg&th)Ig0xCt@6Q5>d(Ol#{)*^)zACW z>>Sq53mV?xPqo512j-PhMS-&v58tybpx@~I-+lG)`U{+nXqLSG@bxR^ZRH<*aGP);;JdJDeC26= zP(viN^GhCF_W#8_1YdoO2bX+Vc-|=@cF7AquKHjt6dhK)kQTvd4kfI)(_e-?D z@gpBR>(UbN&HeLoJ^SaakB@3{p1XLDpw)Wy^L9o3&7b(-Jg!9G!^<6@VGKoIyDo31 zychz1r&9eMzD0S+b8+sGW=Xay<)fUp`y@P#`UO3T`9Wl}R~~*fcmBOIG{nF1eC?no z$VJ58qOnL^aGoT_uw{s^F23GG+j-N26VC&$lDOc0aaqqVTsYKrZ3?22?bnC);yGy=9I&gvD+dts*)8X|gDd5znfuWs-gws0bc*+A1IXuRl z<^s4XVD|A$%=oW(pU>j!6zw$cM}0iTbztblJ-_Z7kGIBJCdjs^&w{t}wbu-4=5-No z&ycQ`$Nxd)A4|BHb8oJiLE#?0IBg32dhr`XJA5nxPLf-em+=-7KG}my8WHf7{x!kp zRlsfk8urm_9vY{tU)IwE>3}u#qJ$F-a@i+&#D@}|apx!x0LJ^@5-!FIya{?*i5bM# zaX43|HT)u!_p)Y;{QoF>7XUe`DsQ~2db+1U7dj!2_JMRvGHFDlAtY%YAqB+HqG+oq zrJ$C8N%?LBv<(O)A{HV{@d#Ajm3N1{I+H}(*V4L+^s1|ktJ1i-+ddY?RdM#ybvrZN zHQ(>td+T=fWb*C*KcQ!uQ@8HpoO91T_uO;O!QH`2uxq>$d1ENM=f@*9c8_zV3u$nc zOMh`zMlKX{kwjxqe6UX>Llo>%l)IY$!#NvHz~D$4xS{@Ub&&2u2g%}}I4~wl;h#VJ z>bp;$eaow7zxez=;^!y+X?M-O_A^Dghrd_-_|V)JyY7o~&-y53_hg@+`RCm6+;hq= zK7Qssr_a9a)pP#b`aXW1@A~)5x$chJ4${xWy-q5nlw69`7maiyZl?xASOV$MoAFYn zn9XLei?m?vDwXnTCS%l-mNka|N~LH@M|Rx2=9l52n)fTkV)V-Y=L`PRnQR)~gD#9W zj$6nXKrQh!>HCcKGk?x#_B^AFqO>NBSQ2*Y1zi7{IYS1+U%KSNMP!~iLj>vLyTyT` zo3*O0tCcb(Sl|66nwX= zlIuEF9#<%Nzl5LRR&XzPsY(43{k3WCj#vag@VOA2$>{@cAvir1<#X`yk>$4ryh`I{ zb3xQY#e@2dpgeT>dqQv~GkE#^!JMLcSi7R0K~bLj@Pg;}v;D{ZJ5Ey1KS?+{2a@Aj zD&avtKR;z$0sL-)AAVGp57h%7tcL|W4?Wwab!mNvT_pP{a83O|PJnkwIJ55s_3VE- z1TV4rfw7rGk0j`=V^2ujF}jTqe+?bnLOnaURXz*R=-36aKCwn~d~7`~;eC9+@8>7L zEvlWO%**<<5dOwPa5nBED;b`E6;}SOSZn`<=E;clw=u*KdGMXRdN`geD!)JQl_9!u zywcd>M|X*O_*{_q9ytkL91hmhUOkLHGS(--89h1O_HGN+AL`FK%pf^llk_A`x4lPL|zt}r6J}32w>F9jSPac(Z1#^@7G<1aZ zZ1`t#+@2iA(EhhlJNw@zpM_{$StjcfeLHwAm8RBYzj$31%?7#mqJD*NB4c(EJm2G0 zit;Dh<>O;Lt-L|P`{wjcQ^JG#E$Yv@_ei+VmHC{a2z|1jA)3nAlH9gLJk09Z6YLK- zUJ1^4e%(2Xd0W?pB$~Reod8e4~FUs(c$22A$SPi zyM7RYOa6d1|2u-?9FO!>lt+B#*VCQyGsxEf=fg~)fda@fGR=?Ex!D=yfXJM*mcaHz z|HuhA^l!cuO=J^pMloTvH}XoRt2fk0p<8KXEWhM8bv07&<}+#rCgZA__mj$4w;3Jv z5Er~brc-iq@|1eXOFE^zRxpcBTlZ|mY_xLl=u8aMtquZK+78a?;fH`9L-0N4BSwdV zc4)jk9`409B6@NS~^wr5@M6UBym^wjve&1cS5Ez~Z<^JYjp z=pJZgI{uaM^oWm4IoINDZ!PItdk)?hsz=fnxncs@Rz*J>y!(~t=dOPt;{fnU{KocQ`)IEmAO4q6yT#tT+;tLNPTC#_ZzWnoSUo(e zeSG*Q{kYimC2B*;3gX3$)W)uje<9r6o#K6(A}e)PIuPj{VfD{ zdShO9m4pYn8N=O?QzbkYuSNB5|Ez#pj8DNAPu|Di2W0tRysSJ$agwy*{Lbp%`P%8> z+rw~+_4EByf8YG8^!iVhCRu-#!J*B9W<$SDC;KKFGnbp2 zuNQFQ>(JQNciGr}EbvP4GBDrxxOYFop8ZI)AClXMnaK1JtZO>rf_U*S;ttq_kx%gb z`_g4}%*!uD()mtkKR(GiH(7h>T}2pC|rf zo{0Db57Hw1%yRj(nM;)tB{-aQa6bW#El$D7x+M*lU`Gq^APwX-=3 z2eVqX0zcu1UrNAF*h(aEv^18|@>p5ZhU*&HqHmOxlId3zIMDj8QcCy*wUDmnlW86P z%4sc=#L<4c;2|?2T`Ke4OgW>ZkiV!f8F5`t$+-nbE9njl4Y<|&0HQWo4kAGn4cj#A zqV9CtnWW}uGa0nEw+hAv$$?0mO9&&Bi(cv`av$<=U{mZ{&#-UZ zAo~@XW3Wst48hB6j&bSq8Ud$1u{p#0v?16>aXBI9OJ#`l7@8pK6UA$gYnP1IKz-u! zfZ=3|FWV+uiSje!8+tM-C)PmLr>%!V<->9XI;F$~uZNEzEPrMFu&*ALM zV9uk2>~Dy!TTWbuIh}%a=i$jy25*hOZRYBcyDs{H7Fn?~xoV?Y{>Lkx(V)o){;aaO z%=)nTL$aMtm~&I`DnG5k-mJn{L19Pcx^u@Xs?6FE*1gtiSTUQE#3xxV-n%FMHy{ z-|Ooy%|BNDiBFvb{!pk-A^m;dOQG^sPloLKBf;VG;9v|Ud<+)R-Y?kosc^T$>K0Mp zzPV`G%rkHwhjxI>q4SJ+xc{IP9IFtGbc~umsuWsqkB0l-n3`2;deI$oy==By?q)LZ zoXqWDF+XYHn#@L%?bOz+~Hzx1jvcF7O=a@w9kH(CWx)f>MnP?o5_pvq{Vx zI`?8$8amaq@P8mU!xt+LzeM0mIAh>1Ib+bL9yhxw)$F>_%<*(I)h(-fHywxTN6qab z78Pkv-@L;S z@k_g}M-%1f8OPrmjncNddtv;;X@8yn(%Y3&KlDiV=Z|!sdh}8C@<-Iy2|SRVm4;6Y z>QBZi*HNbhS#y_P$jEA0hj>aYNKs;2W*f0lwl>pp(uh@gq{47yDyxjv!W&Ol+ zc`8r!^ySZG^YFkyYFE}z<6>t`1i{&g7pkWmH-q~3l_i|;4_@c(%6zp{j~&nOc0=bC z1oxP49HYyjcguRjc?Ifuj@Jfien!%U$;#c$6L6%{*f>tTKo65+AfA9f&{eVxF<*IK z_q`zDF8T!8@G}m2D%-8ZQ^D9qW=S~JV<%Vdx&B9y%Wu%a^dUMhnLv4i`{+q$7Qn}W zUlW|DA=)rd-sJoXE9M#rr+ur$tIX+2V{@rbd~7mru*rOwbMVo+h3c1MFp%e*>0oDs~EsX zdgCKF#(e}DRj6Nl9CCd7APYtPCU2WgS5993$Q)7Lfp2<|`nst|<%hp3>k;d#z|$`! z+yuWG0si)VS;7n9c`na5h@J+`H(t-yFUay_V@U_&rHr+r|H2oI^VrsL2`9NA%Fm2% z0tIAwCXaY|@^}((vWoye#3|4_p`{48#py}qVIMqS!r7fI$h9k|e4kunYtEi0rr^^- ze;_aT+&L2HwFdL`L%Zw$2jz$M|CZi*^3|cf;vL@CP1r>+IO#GD<;dZs_y4Z1PjU>8 zzh0InT%bJXyJKu^VKQFqFL?b&f;Et6Q0&cxBRv|3dCSU+**p9l*;j@?KEC~$?32my z$LYD{Gzlj@MPK(~Kh?)~#7E#S-Y0n$#O0VA+dgogz=h7wa0wSUt3hvMf2vgfNR8?b z&41A@r!!@J6y-I(pX2?c%!s0XlJk%k9Cv$<$nqpV^Fcq4WkPU<2j2gEJy}TW5&FaH zm-#Bm&cW6Z4&MU_D*Mm$PY#!7Uk2`fR^n$TSMST76}#+?6+ed5#>nM6l~5Z2Z@+c& zO@mul9-|s?GOY>IH?Q#jHyfhACsI8n*580umT#&gYIh}2+4bPrXp2<5#w;{@oiq>a} zwoAP^iuLE^vTZ?Y-lr`)36A?U)CTYrNk@i|Nhu495XPwB#`!F|B97ZhBHbiQyC{=Q zimQb<@t_M$i;zy35=q15hAwzPJBCKQ40kl~99Nne2`_=525usa%(l9xXH!Wp=a?nC ztW~T+($7S5xIdcFyMO1p(M^N@^68^=bGDmQ?Xgx1S4wf$$W0@9TRWCgjGE{AM!Tt` zb3RNqxQCv~;T}%LGo2!C?5G;jfffrzY6m%)`#(+z7h`3w@d+H@j?ZRtb>H$pB55 zj-c3`F1pLVXsI&u48#qXy?Evs>L4f>i7i{8ES?h`GF81$;*NjI5` z{`Iz|Kl}m3{^Y%1^Suu!H+=NYU;gz2fAZDG78JVK_bjrlG4qdi;`;x+mv(RVuWb+9 zrpJ@OrsHP}3(@U#HR;gJxRRB1ik(s2|LITd->G*N{`gb7s+NpVWFzh@P*&pJuY;p5 z&#yO%qe{wuefF`;K)K`ZtaBZk<^b@=Wzm-FCH`#STh66*yChufJ2^k^{)&Lp8p3oS zPPeT+nMU{~dj56FT+Q+P#9X$NtFnI5T~jlMHY!uf=fM|{$y6TJk|qf|$xCv5l4qeb zza+mT<6a?DzunWB_h82%>LHmT>)HHy2`BtO7Vz=P+*}6J$$&R5lh>7Gd0{i&YEnN5 ztD=4Z=Xlkq`KEGj`N+Tuk=zcIDZxIccMx&?$I5^30iZL8fE7(5_FAiNG7r zH%Yb0`a^5no_`Qc5mtN38#?~o;WHqLr4VE4`;(#TJc3pk3vL3ezn2)U&~%JIvpNW%LX z%ffp10Sa=X9LlTJKw9UPyFaphZI~hxcu?Q$Ins`9I|HWDO~-mrLAJU z-ijto&DM;RlGB<>PRZe3bhDW+7PGlgZ{TN+e_)+*9~Wr@Z|r8&e;Z#Ndkk_7QDrh& z*V4UoMomH5mP@dt;{rO;dgVP6u>kdaS+zNj(pUq$4ej4cI6b^i;?(Nt8VAFAR3U&v z-n}h^Qzko!@30qKBjDUt1U}_7*!_Tn)80$Kai(fWxX^KqK|{lyueXMh&B+aDEbsFW zU)(`*;)!?v0p+Q6*?-1Id|t{NZp0Hz4* z#~$ssvYrCuRwlsD;lD|E-`tS-@<muJ`ZWRlOz z?JD)({Z^zR@TPN}o<@XwIv)@9$0Rw*;WEb&rwi)maOrD6?N(`h-F)YiehNHL=Ci*3 zQ~5OH%FP51$rZ9`VZ0+RO({=%Va$PDY_$*JpZE~?KXUq%@+3QQ0UnNG)FD1WJ>)GC z>W`!sUwW676FCW<>ooPhCm(p>mY9@@+9Ti?5-C9n2X=zN2#-30i(% z!il$lJ6`@sI|OI@JPzOaObE{OV?JkgE(*azYvmK43&Bmo#ZK%vLbxU#DiLjXf8^OQ z@lYku(GYPqF6$|BUgtb5b39S`dSF+ed?XrPz?yDMk}eJ0QPwBe&GNDyq5;NB^~{Vv z{xS(C8HaxI@yc^|s{aF24~JJ;ga}qY^%FeAdF4pB9!_I6m$pMf$?`7MzvGqEZb%jo zuM7Oc-Wc#4@giF%DH{xIEInCCa~|z-ybXiuvVI2N_9H5PV*Q+6(qDngdW?(1dA=W} z3yJ=Wj8H$R6;?mtfyut@bEeRd@Q?<5AxlE^)mVAL^EIevXy+`dO~j1la?1$XmU*bS z-hjFu2DRV`484J4O7zQSbknIFDJuw1MS`=eHba!vhL4`TLud7P#&dA>jfKhO!_B@v zo0BsUqxfRR8|&dya%2KH5cNo&fsJIlzzNOgYH$9#q?PdUBrnh|WxxpGg!*a|?S?@k zWR-hl%J^8HMz+v9L={n2f$z7s!EPNIH|ai1W^DRsUmKL`*`u|E&y(Hnl=b9zpEw`O zoFGIC#(#9G9qKF9Z_)hR28j@YS0+uakBO+(ki?-eqdey)=`TTiBlo+|H3GSFJ?gK< zpP0F353r`*uSFI=0GpCVdV%{jQ77Sx>mC11eVD@2(6hJQ-q$zIKQ{8wyyJxOte&Hj z1ed%+V`uBe<|TdgaGZcnJikeZZ%mAZ;A~DF0Y&gL$nL!Xhw#am%i>*LiD;tlp>vep zWfSIBNFfUG3F5cmrpx@<3u(W-a0v?*r_7jji87mIl9;DrFL;?UBRWrw^85W16mBRw ze=!A2u$7y)D>wU_lm*?hkS2Xo^dk>u2J{+|J~^*ubPA4M{Cbj&hK>m z|Bn4BSMqdyGLq179+fLaQ>lj8)@z8+6)Chl%1@lPOPTnbSaHF3_OhX;_ZksK=q&Jm9r{&wYV}Jm1E@Fn|t-6wK zxn;gTFK|8-atAA$>kaN-TjF<#H`Hzi!d zUWminI0rqqjt7{-#2aj0xZnYjYqZd@xgA>nlJN~si+X4;TItF6V=HC-w636jF)nBv ztRB(@T(Sc}tHM5|{1A<97_(Tk5gIdPgb{FR1N_RzAamK6Si34@n-XLDx+qULvZy@A z(Pl^&Ru7Gh)kARP(z;&2Y3?ySir2I4&mnlVSO4Zu$$F?f_?y?i?n@F*G9rKv?O|G( z@C7kpYw3+`=*!yI2kYjVfN1z)d5PL>?v4gc`vYiaA*#^!IDj~adPhWoH;CJHhPp&!k8g=93 z3+JrS5Oe%1-&1diM9)pHcn4oi89xY{Bp?5lH-zxQc!0MBB~zJ$~K%LnjH9}#exf2@9P8#(eL2{%C# z%ng3jLMgRje5t zuUi8BSI4^O(A;z}Zp5hX%~_Iz0e{7}J{f9*;fdGd{!I3X;#L=eHD-4y1h;zfZp#8$ zkD&9>Z|5R7RsLo!0oeo%rH@|;XE`yihjuQc9uodAo=>tEO>T1a{@{7vw6 z{7$INGD33)G#l!J>uBvos!8f925`XkD|}4D0dEn!OtO)|L%AfF4JQk{nUOh&2Jsq1 zs2E~4oOd%X1Zk4SmBheWL&@PNHX?URK(KpS9nU4VN1iTXac|8fAAc5aRm4+8*6c+~ zOKPfKF5j^{RrjmuwX4eIirS329n(XuZ23`|zx+q-0sk+4)j0T4;n%N1CX$w;xV{PF zHsaSu4FCU(IM4b2WBzHQUsV%`|7HDsJn6um(;Y*Uvzu@BQfU=I%^KYXOyNnxZGm5L4DM7?BHMn@ZFv0&N8&6))yGZ{g$n9Vz)<>MC#+|#`9 zi2q%1Sf5T*1YDHIx!-PCp5_<$l6lJo^X0H;gZ7bl#@Diuw~2SS&ep4I7r}Afz|Txe zk+WghK0mSq5&fWDDm?Tph0O`V-r{c%_Lc>$rAS3kb|{Y@*L}pzU0CS&zra6-U%$Ww z3_H=NC)#RbEZJx#TCK#GVrMLG-0-Xn(yo}rR*eFa(&)5 znRL@|3_Y$#lPte^4RBd6f$lgB6q%+_v^ZaIH4sQ z9<13Mf?(F{5~Bsp`8@1a?;)I%1QYl(0LR=W2xRG71zd+*_LwffC2}B3<$BEV{Ur7( ztR7m|YeYLfZ#I2cz^S~|J7=Iw5hwBn_Q;=;aGHOTZchYy0N1UcFZ236SnDMHWqGVl z(1JyOgq?zy9|_h}zHbBXaT)F{4b{Wu_me*-JV^Y|y1gN8C0BbFekXFpYqdyn)xlrI zBbP4MrnEBxS%g*g5WP)p3Y?dTH+fs@di!FMCBQlBn~){LcoXlC+;)Oz&>wCS{WF-) zG;(w}OU{p7_CfOVqqo^RMXXm9j(hl#50QTyZV@@6KC#Ci+1WSO2%jN+>&TDCdgH0m z9-nH5@6!g^zE~62{c~SEL|-$YFYG~q9>(#k2V-}kO|kyiZ0-Y2J0z?~2W9=VuLQ0b zPU6xhkNR2c@zpS}EXT!kSn|>f$q@4E5V&CV2XyED9s$nd`_Gc~h;w&Ni*-L_Z;_S< z5y@>-{Em4jo-+4AC(k=bY!O>ILa~*#l0TMdjTXn+S$J?4b9oEK@JzPc)Zm2Ia1rPk zUA8mb8Y1{L9QE_X*A6X7o-r?qK&sehvVgikeJyTJSeYn7s5zNLqMR(~RnN$G zQ!X7{P{i)4)l)6Ei2eFr~}pd>JGJHaD7j<6fQ8N72gBF17>TFAn0V;Mnk z(O^G}wS3w6i?Zh_LkN-{KV6~hPfM5Kj0Yz_bd4cR*BDe5J#t=@j z$FRO6uR8ROh^pLj*&ojTI{1Y41Z=Jz`4Hi5&trr-#_NP@;^|!^gN}Vbz%8cJ0{-L* zDo-`C@(%WIj5f#KPw>NU7I5NGk8E30)&{J@496e)WF0;O33>#2S2uHM+WqR^n%;ON8t9#i^v!OE{W<>6(X5Y*Fk||dQYoa$86$^Cte%a0ALR<; z(P~X;6;f_GAAe7$_04;JxOVLi@A;hndf>yLQXY1}UXce%bok}PGoN&+f!jHEJF8R=5G=z-XbYmZ!^jF5Wn?6Wz z!YHeUWGjQa$NI__Sp8JbO$6W0IrQjfBd3Dq*Z<)eR^CB>Y??=v(@|dJ@S~VUXq$55RY=cx{I67(9D^pu zjK0E-IV#$g>*^+yhYuf~vU;Q*v5E6@0Jm8Eke7VShZ(=2T}$@qWvowsN3B!lXGRAh z4+M`Iq79-O!z-uTaIi1P174FM|iVE3JnN*Xw@Js|P3kTn6j(JG`Or_vOHIMcQUZ>{ag4tM9qi8&xS}da!@c;O@;|OpQ z%OlxXZDLfZi9QLvVFO#wh9Ob$6USVz$tDj^_A{~WE=26s^8;%y-+i)Ph@K#pXlT32 zH>wK}*L7%-gKUG*w99&6j2B~t@zf|fSyP&{Qwcswri45{bv3C(TH4rH3>hjt&*^H8 zpLW!uqt@V=qq}~=^(%VWHXTbZ;X0n=38v>A{x;!m>pQ6X;1$9ho3ndM1h1ST;KUQ$ zR{te}AG}Y%J(M@e4z`KWW)mof9|ipApnM7Y_7mbbV(`Hp9eQ2_fg%w-w(hCbZl2vD zNVVJ10=8>rcT_K0N+gZ|xvo;7VdUvQSE=H^ift-6G=)AptS#^#MODE(T|!-8eD6TF z1H3XwefA)K@iTzfjx?4MGEKY+HiebwAxqP>1b0DV12O0W6gHRTOIf;Xp#r8!MqzJ9 zq-o@s>X{Hoj0Yl|EDQsMbW&{|chpug2E^C{9lE>xlR17`d*Y>25l$Ab}Eg*lNU&sHJh} z;C>22L)c&2i`5Z9x$fFXNs#qzzoAzUuUbu6r8YL)xDZo-9&gv}y6fT>^072QCg`t4 z_#EN97c+@$3BWND%MLCU6B*;7sU$dwbUTW64B{c6ab)4(g6P~igF!kF3SGr~>DY}| z+GY^Jmd%Sr(%IY0?7dkRn+CtBAVMk5EgG10EhRmXikiLy^lP_imbodRERMeM!cO&f zi8HRxJ9#T(o4C_j$Lfcu+nH*sFjh@xy^`Lp`IP~?R9dMdl=Q0`l>EkFYc67wnl^3+ z!Bw$fS3XwESMJSp+ArUfS5x}Wx7?ZbGD!EN`|X-jRdNXjS)y#Kt!b`i*=?YM@pA#^ zYv5=1D4Uz$Y6j1NpV_(iCMIXSJi&3B=K9IygAc`T2mUUJ@H}=5i^Sj|Q&@GQpio$#Pz5Q5<|pBi={mDiF{!KLUtfOQKe7p=BqBgeE+Ts zKK8^Dw|?i=@2*|@-CMt-sOPNqyxYEd=bkxVc+;29{OX-webdF4@9F%ud(Hc9NIsIh zW~u*&=*M2V*+2ME8oLg;l!~V!DMa~5GauMUsQ)5bHf|a%Me-gj5N9h`4VZE^YazZ3 zrf_e|Nh7v2f?Ib-Gl+|Bc#N|>H8D|uSudjETRb(ME*6tsvZ(0YQ6#vm_)P=$DEL&4 zA~bT|uPXUYQ^}VLZTialRe(&&!2|RhmiYxDOcC9{)+At9!Y^I7UdAt6&!clcL{8`3 zjGc*ZtiSba#j%W7*)R*mObMw0Qu=7oEaY%)+VLyqDAoa`rP_X~Yw^Fh+idzZsDloC zYTFZL(S)ODQTGvXE|D#2btEWL6NseSpyUfq!(cfW@RjoubdvZOn~!{DI}d%wbPM&n zhgYC=-cszgrXV(&tfq5O3zy$vwaGhJEh)_jR~h5q;>gZqjC-t? zrjo~PyClN+BgiyTjHr64+k(3(Tu!?=C6z+BUwlPJ<2ZArdkYS@fPA0d0KY%zX2P{u z=m8KOeaJ>^;TibAyoe7i%E8}Q$wX(32_u>*)3U3gH$^91H8ekh4UVUJAaI7UMwxzh z_1^b565sHUc2{jAga z)6Mn1#|x#Jl}t@k3^V1W3QbaZ{7fv9N5bs9QS9PaxYixTFD7?v=wA-qaXRp0l1B$% z`CxFeQDw;Hbb#5kB;13XG8p{(C_i+7Sws#%cZQsv4{m^$Tr8(60#Aeaez6bi<$xVvAsY zQ5*C@qOFNSSuIz(1y)N1ZOLzbB$gb{dS0wSF7U{XR`v5rwG-igd3(vpT)c(Y+Ed#j zPJ`R`3FB%Zp8);mDQ!QYx=MSZ7<}^+@$C4hMaK|UEagY2QN-HDhyG%0NzBbQ#ukk1 zEP+4K)^y{GL$r-id#dYoP2Q54oX~@B-LaTC0kcL#jaUO7NpB=rfiO_A-6UHsGm!!1 zipjO{qw$-db#Tf>xHl~w#E^cCOfY*9`e=RZQ{xZqA9&@&3-pQKG;xvo^@)Y*G3BAo zwF(r<7-H_5WS%W_X9A;MuXqG6HFbtRC%jMmLn4IbhKQ1*?+^fDe)viQz$({9^Lz!HeQ| zqles!nDlxk^^nH(B>buF+i~M(AN}&cj}MHG@87SU-F;5|Hw4Zf559Gq@w;F5mpyO% zhtG~j9(pJq={}&m=)XdFapFm(MFLyQp#pNjWud#q*;+q9EQS8io;n+MF+ceJf@6xL zuuScO%ggfyDM$JOU=Y|+mdu+?_h~8n4lUWpah1spG!u5kYo^BBLQ2Ct$){ndQqs|8 z$8w@2AFL>l;8D`^xSKHw6A8bRaWl43jOsdU38f~C1I7A)?%SjIamVn(`dGp|w?NC@ zTs{XlW2!SdpzBMGmO(M#I@ zJD8^d50h+|PkJ+O1`ZC*Q@)zPUmPgK@zsoZrNLAi2Eb^mgUuT0H)SQ!MK%`xP05wZ zu!6)Q|&0YLv;^uz4Y{7{v;$KV>^uqhn;Gxhr* zY{CrQnAETy1HSPW3?A52E%X0(J*s%kK#8;?x>HbXSPrSBL0D zEBi_5wRFj1M1>K%SxiBm9YZ-qJ+whRIDX1TbyoL}NSZZk;b;vQSIR#bnOU zX-YBMZO49m#~s~O%EkvCc)zLNHNss3I8BnBedF^q=3!WV7@TYwcn>xXo#0+z z9SA^%|GxQ&%ZEBK*%L)4#KNUoFS2>1-RNBx~ zn7Y!fK@Dr@PNCKA0(}9GGFuYI!=8ZW9Q2>TN7%S1LM(pbun&;!rVyHQujiwJGSfd2 z%)%fM5Dx7zaCTgQCEAST$EzM02GoL5ooEN&#*2fIPEB_m6Q@LVC9QY!5bsT;k-_M3 zjR`IkotDz9R4RV5j9ioj7cvB~fedWP>d=L%CFDIUs4n`MU}q>=d>QL!Z*7HiAPOua zMNzH-vfo01oIOu*2dWc!FIwu@1J#o%EuCWAI&Q@-@sn=NU(!K>$f)C2S@-ZOw#+5` zlFdS6{P9!9A8#cz0#3SCh4Jg1G%pU_E#Mm9Y`$!Ij>>QRR{?hcXR=}B7J>))SClg+ zuZjOYerj@cCB7nc$=c<2C0A`IKbO4n6z#{Xox$yi`j^ zQ|(q%OJxSWfD}x&|Jly-?|N5sk^k?=^W+;ag2av)NNKHWgqJGD=@7m~Zb<_#?xmv~ zU!&m148b@4lEy#0L*ONk@>QmrE@bfS0?zpta)R^ko_7g&3FR%qy9T60OB7sNg|hyc%K z6}W;2*%YBR`Nj-Z2>uVyCA8(4A|=HCr(&(Hq2wK<=zQr*NTXBfbO8C{yVUxk;B=HQ5 z8C;Z%SFp=uWA>QL2AtL^oF`zWF*vR7j5qk)+Z)*KJ@|Ao+0V=G{ZCQeU^vone;m>@ z8Q%-5JFBONdK~iOe$tG%h|#<-m5}vUInPu1@-?Eo4w^B0z6-9#J`Eg>pHrX@Sy|EfE&x35}e2;K=J~A0VZzQ8687F&q13stlD=ZL8=FL8fCyZoC z{p!dkSC3U;C@cHA(rS;v3}V2FIEpZ_wlX$h2jBb}?QOg5q>g3SDImXO7I~yo1~zCV zwO&OQvO?9VJ5F6~f>D`h^hc|H8OwLcG2FauAi=XiNO?JMLViRSFuBy*r}j94X~D2R zu(fWX_{;WqkWC8%nbjK|Tl+kcP4A}UOfKU2IqFa3SGK#oe%(5C-AR|j_yzV|e$jI7 zpkK=3&P&lVI-|3vE{jA@;_GUU&XvIK?_w`mBwrhvF`)le8Y;?@ENAm<y)6X zQ;+5V%aIbMG*Zm|1ySV)+p+l8DY~(Ocf1r&8%D#bV-C<{pnu@~1}1~)CzN)nP^cCP zbvjDuy2$Zb#+Y?x3ws$m4Ld6|*Tp`TUvgzVr42Pc|C??TPuWOM;dBy~X%sD0^P=98R!HyJ_Gu&iZz5tp?)W-(g-9rR8U z{H{;ocRHUBWT52u>0}zsgz4a3ynZ}q$I9b+FygkDua?pt&ksh9N6IIAsxuL%JFssm zny%}9i++td8qS80cM@`q0UgG}cTL`edq^K9S@l9Jx$4o@l0ROS(y(7+xFA`T=W-0^ zU|AY7gVUWurV9~{cD%@NaXOp(MqCTwBFF=LPNZ)wm}bnu%ny7P@Hd8!)P|?R3o(7X z?2%`ts`F!1{*7{vryQqTRkA=R{as>*f^?N}jLNFRPO=&1jQZ9=c^%AFzT? zfzTimV*g~pTWD3}CYD>~mdNc9Nd7b63$+;TPl10a+heGMFuI=s=O~z(M9$GDB^oD} zH1ZUtQzIy0ODR&Eia#uI>B%1|+|C!m1^IaxQ5VrNlzcL2!xJ^x%^KK!;B2f|$fs=> zy!Cpj_`~C$AAVnYHi|L$8l)?G0T|l&%sQ{Nz@|yug7>QVZ)2ZLiI0>f?;LK7h zu#*nRT4wEX71LE5gdL$tSD~|aY$vj2+i&Yxtz;_|7{135 z*#zw4?5lzES@1LRALc54iv6i5jdab_l!i@#+)0Z>>giEz$;);tIJxwjcGsylYyq#_g;F!I>F*v@O47QdUbL4f!+BfIPN_Jx+3lG7@Q!`Ew&J)10Rw?IeP0^K5rLh%ab71HyPUyhg*l==Sc5$r6XSK)Mi=8Pzdfk>9oK=V4O zT>bx0X5d`6Wz`QNbFQs*PgQ^1J^y_5*6xG%DQn>4c-DRX();e)zFo;|_m{Fi+ts7$ zKXxxYU;Rk;@q3jU{VnR8d;R72-iu?21zY{)?9bM?HPJ#kr-q3j%doMheAjQ{6izLJ zi*t6>&vtCEDbDus<0tSdmOt4Oj=vJ{?U0?W5(Qp|Dk@>OqTt~y>{E2`u^Hmyz^5)) zvnhYrm611#?Dg2a^mrPFd3>irFd>O)r6NhZY#P{HAul+OFJ?SW{LOgWjHSn04&&@R zc)T5a8?RD05UXBSw0ak2GW;1m0aA0ub55+Pq>ty@PSomH!6$A)-;ZdT*2IT&e)ewo4^7Q7yC!?L2m;v={VnL@=LO5@vC~2AZBT<}8L5nqCkc1Ps~K z5c?Yfwib8wY$c!T)@V!SH55qyqMz|?_>SndlF5}*xmvRu!G(_Gn3;9FZp-bKQ`Lr& z8Y?5M>ZodE;KyR+2FRWQis)>QT4Faomb1OjG(sD(Q|6~}-v&LQct$6PdIiX(Pbr%9 z$n{EfuxTt(4_gX2EIW!*%2CIt8u;!dQ{_<>V>GKaFbu8Jh&HfON-AS9Wc+q5zXe|O z+-ll&t%O&!D@rz{7hGu9uH(}4QEcN|4%8`{w-$?c1HID4jK%%P(`i_MbGsVm^WmQp zC+_)f9~^sof&ktmIA*tpeIo2lefvT98$nkL@F6_U$ac}=&fdO|kb}bywq=|{!DiBn zh;mHdm;+`UQ)?c!3%O{*8+Y7jq^o%YqZ27lflWe9D^8~nd>e1Yi&@>urMnF>iB^@Q zQp)<(j$Y1Y^im_^B$9@rb(8V5uG>0rTqVyH&z|H;L~_cwmggWg(IJ!{vG-nb#EFtB+Yd2gWc)j zhaR6Eey}6pV*IfCOi;V%9ezZ++=jsVGaQ@|1m)M<|A$D#?`Vd^Tl#!<(ob=GVr?jafOD-IRkIq_Z{pB<1-U@!uk%1@Z#m(Tt=5 zw<;nKze^sS6a8uD^_vG?>PA+m-@WUHANs_353aubs&3@t{!`ykE9tDOWmI##36F+W z(zOb$Og`gfw1MHBckevlpZBi|4!+?te^lO~skMTZ)lH}CLfJHROk=~!mkrhO>bZu8 z?h3rvJvt3uF#NE9_tPUjOiM$+4`Kcs4#Amxsj&Gh`7MXF*kO0Vi^M;}4~5Ey z>v>SX``X1FFDnH1s2;{2@qIrC)e|1qS55+lRA&9}s~_*y)^>g^Xlfb`tl6vl)zWn}FsJz76%&YEy z9@6#&Epq9()9}_8Lhmpdb6(j$Hw2e;fyVcQ+78jI9LT0AG}!-&P(7jghd+OkdcHM% z`SK0Z!-uU%0QVvlVnSC4%CNA=6E z&Ti$S6VHUq+4YS+UXgX}y0Q;0X}$Be1eZD-(XK)G7(q|Li@`Xijt?B!->z6|J{E$9 z@$u0RoUIYNAPJ`MdU(zHh$t`n$=8VC4~ObuI*e%7JPCYF2p;Cu)l=cS7oX6daQP33 z@>AEon+2Tu8IlFVH-+HU5Pw1AnvO4a1-kp^QP0=;ntl;$1@VnX|(Nkp*F&E%MftFA?GosKWt;V+zEXh+KF{D8Q+Oj@`Pvc z9oU0U!+%$8KNHr#r!VO&S$^U>yIvCSXv_~xt{KB0knn!J;QiCVXI`}v>+R%REpo|k zSR1)8-M1f|?j5Y3?~`!yL6dFoJRsqs?Hv!EfZH(5gY|FFJcRrIwov(KxY~J^ifX!%Le9$OV+q`&v9-}`sZ_H~7*g}TQ$`-#tUp$S=kqTkN)cTumz~IwBVqnV7Qp_ zt9~h6as0NDC}YG)7+12msOFnm*2-wn4?1&)lz&?O2JiZJp7qfi-+205f8$$jy6Mp8 z*S=-#%C((Ws^wJL$q+G&rc(wnG&i3@!p4DCHk~RMxItu?mTUWNDrM)hZZf5mY!_!< zm9CMobes{IR?ae`|CTNq9j9u;Y=;m@R=J3ivSzuOuX^85TUsuQ2ohb~9`JG7v7qQi zzM{jfTg}ue0WHEZ=#7MKCKJhy+LLiB1$>J99){K?X&RCdo-N>0=j0W#eBZh>3=52? zCmUXWhBXPlWN>dFvtaRj`s1hK)P2^niOZI1!8pRQVug5*+GqU|vhgwj&rhBWIMYcI z&`G|0uK#)eH&?agy9&B{I!jlwj&NYp)*cDa-EWtl90@!)Qnr;jdE9-o}J;NP@dUmGEM4?|)L zzF7vmit)}|wdEI)Uu%)2=Zj}Ktie58ERpXBxhimXp=_V_EK(O4h6c>)xq9+tgWKbG zBv(JNc=)GRTzBDr&0KZf<){8wi(Gy>dp>oX7lhgg(+Iti{R;DXR=}yAYN-A5Lh$g| z-yrJ|<9OsTcK!=ZL-r?3k8@@DOlXd+dgRf|BbQ#G_1+1$@p}0V#l7j_@Ei&6!=d*Y z3Gc`G>q2lhRL|KG-alUg-C!!;L-P^1>(iZvhXnjxSlbWK*~EF5&mK#z@>XpB#uXcu z9Gw;)IPsa54hur?FyG7%!6mOj4|}bEYb-}Xz&ArrV*SR`d9sc0JLyneVIDXu1h0nh z6ySC096K{qK3vbML-26BuM%+TYmYaAzP?i8iFjbOGZy)y7P(NIJjiDoX>RVHr?eu` z^X|OhFUd9A{zut5*}7oK`Ts2EDbX#AyH`-VIGHdar9mg=;hc;-5$KeNpKyskvB5|b zK6JC9@d%w2&YQ~?!2vIo^GcRm&uLZ1a#}8ul5{P%Gpg3@BEq#5ly;>HUkVHEV=)hx zbt`7mDw?=U-gFy|qu9}+R>Z~EwqMrWCJxdJ928;|hio$^-;REkAU{72YOQkDp85*y zycM+TjN%+Ak9la}JZk~nqd&2)3s`572$@H&!WiH>H+w#%mxvECj|q4UVH8W|Em!6Z zR8t!AHk9g_9Nc)UItEe1g^U87cQ$aoX|!E9)!C(@)@>?H)gDc#PS-##=JBCL?j=FD_c9SITQe2YYc5vYrfoS!zNqO?nF@|S=43KX!)dx> zHZHoSom>;cV`C>81!Ip7WB-O9m7SkeanJYa2sCC42s(}$#c@#|rATjKUO7QtXcaf~ za7`I!&q)j#&aH3|VfV)2CB+osqLT><6O;pM?W0YmSC_061D~e}v&23ho{-kh|+_Va1Gp`}ylA~LQ zMQkB(hhNU5^#;6_@>8_qk-6dci~*8;6{I1Wt;5IGIZ*IRRHg zBe2`RN<&tiDDGZrfcoEE7O1=p6FKSjZIh=1#7@pG3`^Esi(I;+YEc1Ay zi~z(IoYWf=Wn~P!$Ne^wEOQCw4&6lq8Dy=*XuRHXywX8nNww@v&;`d#8kOV+QWK-} zK|UX+G~TrBSjMtDV@eSt<$Pa8972oaBxT`D#$Ak_#;N20*9d;_*91R!ki&6Ko=17e zHuo^Wj~o+lox!O*G?Gwx6Mg|Ut7kuh9}wkfTn6zRWy8e1t6&dnqh02se)Kr4VMl(- z%M)&~R-wFmh|2E|_W2fk=2`z;^bq|I>UU7SOy!SG5d7%4XxBmg>^{~pXqqU$SHNw^ zDPA7!;yk}B;5vh2T>L)6@ElqGn!!)TKZp8RKcD>gGci1gM~E`DbuiSss!R7&^G zBZ@qBC4C?JtcA=OfZt%^y90i~5K{%yWiSU0Hq)h&mPq+&uk$>+{;1S5bm={>zHaSD zKl+ii{^t=4rs9#GEq&k;(5&NHxn|;8VI=BY)o^_{nVFg%-;6L7hH+_EQ+D0&-|#TH z2|gzLctjVrR;;)pqnUNN}A>PNfoKC1uV2L;?=en=z}J%UR<@lakTya_za zlI2PN2Hdmy$`^s>3eA@?bi-u4oD}74#-BK2cAq3TjauM=_!jkWJZwExzzGkG|0`IZ zaF1!dfD;{=t%fK#GcNP2Q~e=)ZvM6?Ka6_rj+c_Fk50TicJUo6ewbW!3TN(<0 zFNi*@9cmx*`$zO1?gtQj1J0385z_5d@RL3l{(~36caZ$8gtPlZ5 zeTPd6d|&uZH%43H1-&gXj1X|Gg5`l?MX&{8pzfvMyxuXCY`Q}6faK>Fwsk+8&R42e zI_jz2zd7A+AAZ=$uXmLDGxg&aDKpghzxVWqe?9Tkme*YRU$+0-;a!)$LphUTkodoi z+poFe(#~-7mUsC3zUY7Hi~fCI^t&r>U8%p>{|Mc_Ui`L@>`c@ z&RhAhl}lq+Wvma3|L(f2v70~HU8{VlbGL^aW zZEzd(tkvbyPdVk1SFhNST(#}IwUd{8aYaM6&*tTZ&r-{hPc24A@DcF~;FxW!ybW9U zcY5<|{p^b%x0pO2{-SwM@6{6_9f12Yoz3JJBO@~sfl8|K8o28&fJ^A21~MhFj6sdpQ7w^&e<7OWVB zSP9`7aM#Mpnx=2z-)J!pov-W_UEOLaSy!>TRXD0SMpMz12rf|+{C2rMN@L)5Ym9m4 z^)!Z^uMlIVwTa8FZ3I8GOu$XRnGD=}F&ncd;3Wpf+~0c%!RaZ((M5=(^Y`)llB-8P zqWn!OYi~%dI(oxzPF?Zd70Y=w zVYQXsVtsbGz+nt%rin#pR~SKNA$-j;=wRWnm-CSaVm3LEBCHK27Yls3TY2|J%eN5XdAOeL|a}lMe`?Ky>l@G4=zq#7~&#zp4 z-j?%XXE37X5OD#X;3ZJC?ZD67s~E1~B6v&APy3Fpl$07S2(=WuJJ#~5;6u=qXvc6z z*aS^i3Y?PM4#O7+IPn0Z=}zp@P#&DX>dCY6khi>^t-lxWDfJJ}6mZTP;DJN`MRHa0 z0H-l{Z0n0XTFQPB`~`w<{ST@K{x>Fath=!WthD@ARRko1hvQ<3`oVNJ(%<4^m%|^#F$>42Q=a>Vp$4 zpd}&>;Ugh9Xq{X%{Jj6l?iS<~22Ieb@N<{$e;u34-a~6s+5wfO)W`P1VjQQ5@~bA# z!1$J+o<|}(W)AMzKly_CH1I*=@aQ~bJ-Cu%qrZMJzQ3E=?zVSK)2=J=@$$a%7&l=e zIdllR6{Ob{p*DhN@!^+=cM9ken-`+(K!4ykCmg~uiMlk_gE0N*8Fsh82g4J1i{7bV zKa)cAK5qBUiSq?;@vOxZlM3Njcr=z7xQ@JNDFpwV*@G7f|4!~$#+Le}(=lbdsO8d# z2a(Hq7Lp-aI6hE{nwRO+%8ur_x@+fcN5SQb(c)+hQTsQ^oT@4({Phr6}l8H+_7<8hoWjBxA;=}voLz6)TQ57T}~uJ^7`GI#Ye zrV$tAsIpc0%2jLLw&F*fwz~fDZg;C6+p2EpOe%3@!jGvN)ab-b!M6veMO^Gp_ujke znpM|4@Uia85OrgR+WnZn>(*Ou^*7&k+im{2{&l{xR4Hqv8IY=9*gVzTlS9AR~OznHcar5-}J!^IY$zGaP(W zI9Yg5!l*MHXGfoRPq@uCa8^E3#08fKMfFSIK5V6+q|H$?=Z_VY46Zq<&a{+@pEC7E zo4Tf8Mf{w~?f}4!gm`wnTZ6s37{dP@2a@y2za2pZXV025 zFTD&>`ew9oQ(psMGHm&`{Zv#;4J2g zDKncl=&n&ZnM#;BJDV+JlV-|*Dw^{VJW59(`C7Mu%LiGyGuEbGqkhA(NVp()M}?hr znYh0~d2ANhDd2YEf|1=WV%OGTC zEvCEsq*9Wta%d6M6WW{&!WB49W(gME1V_ctT3FmD!a56$mx;j?siwzq~)?0=uCX?cOOviPN}7@Zuzvo zbl{nZiyn&Z=)7;B@>Kg5dpu>x|AlgZ{XR&)qd0Y>OU-HAJxHhhb~cmE=Caw8na-qA zS-+egcuv*=5Uazu#DP*|OjIFRXb>=iR^g>}P-Tn_K7yD3d%5d2#px z;&dtFx$K7w<8o)1tU1u%aL*whcjIX(FM1M!VU^q7;;uK*_Pl|_L_7Gz)5j6VGv4&m z2>sY%AC1~~nf)UT8H(KrR`ah?Gk}v_0qvmuCkej)R|4(=Zp9;5n^6*Z3EoTaNDMqf zuj%Q=J@k|X@45MjhVhrInliY|zf8%yu zcVUz^T(g^*XcUVBS%1v(5wcbHSU)Ya3tp|182->()*pw(kO<#)xF|y89O|RovP{8C}(DZr8#cvbOD;6xEpZ ziEI|wyK>we%utsEPAim~mf-LO3~V(-gCdQA<8;GIqI?!O%`=-}i^jI&7XnT;L#|V? z-TjW|1>9q_EClWDX!gNTKkv_u=OkR%xDH{D2i{}2LqBasW0XI9JN0vuPvsenHP}vz z5gxac;#b1TG7v-1t*J)f*%al8LYOXyE?qJZn@!U>Ky$oJvZ$IwIEZ{Asph>-3HHQf zqHF7>trwzRDT&LNxcd{eJEh!5YAcDZeq_=oY^W?RB_>^CHQf05vjOk}Uf z#B*4IPm9cyIkF(BgfV6wQbf)^Tfx+uP9`YLx@+n&iU6s!yJmMZ1{NuLUexIngU>)3 z=Pym)FxVYlwT*}b7VaGTqZ4It2kwr61zh3+G4D*y2S+~H<8$nVAxqgk2bmAKLlJDC2ZE38f zOe2vK3w%mzD-18#;-H`WZnT;ks88g);7%v~MRd)0qSNwlBGWV*zL8gOC05atbRGu% zMqKNZR11L%OTJrFvz?sVt*UthV0GN0S9D+`;j+vD?Q;Qd9lel9&tzNy@N{zZric@A_MttwQo#NV{KENn9W-z84%L+m>e>;kHM9m{J_`8IvB#Md?2329d5z%hpUJv% zSP!hAuC2!jj`@7o0Uq z`gK3|uUP@LVcGlr|L`w=tMZJxTltbQbG7gL_Un{=%AybXzxQwM*sn**;<+a|lCVWZFQ3hp?M~{ZzzH z40a$Pu2&Yhbfg_W+bY2hH=b2pE$d}6(Nee3RB&}uYmc@4w1GL-dpI;UzD_>& z7cpjot)GBksWtBik^5`0}=;wXT#b@&*bGThNR%+4Bk{rh3xz)u2prX1vi zB{dtK?=MFy!)%tMD7g6nfqtT4X=%o-a$dJgEt5v^7=Zs7Nm#o1HC5*?$I>%!TQ;eCju}3w*pxb-*8mGiRKo|Gx>BMkLYZ^p!|Mjx24)xyL7%-p*(pc!*KrhWm$h7IegPt zQ?DktlzCJ?<3Y|-m77E5T`Ak*N9Kg!_mM2z`lA=*{AKf9<9aS%n}hg>(9_K@+ztGn zJeEsdj`vt#^COvBBpW2JXXM+VcG+4^@MQeSwGvMHHO56F!uj`X0Vkb+&#k>w{^&ij zJk8N8>VHcg9!M7{(0KQy`rr=WCfO{xZm}2G6LL%g9Oa*Uwy!)N7y8fT{+_>y@+9BN zBpb@GyTHD}bUJ}ICNp^X@=&P$3hC&3F`^K?Y}8XF`tD_CGel+4AKLdweC~yG74QPu zg)YhKKk`8Vr+QesoKMQkCV~DG0k;YNhoh{Y_$}*a{pWbzxQXDv8yG3 zfC9sVCPUdofg<){D-$4QC?pwhmdUO-2~oCIK;gLy)ZM7*>Mjr5_HUp2;I;01T}*6E z{_mW7=W66Yd1)QuqpNf8J@;()+;h)Goi?MC&EU;1ThBUra>UAJqur>;Q|@s}cG1b@ zh8+`i{{5=Sb0b<{Zy3l-7t0>(@Eq{atoYUNYw8u=L>(BeS4XjeX!LIPdJ;c-uJddD;w!YZ!^V(b zD&W-+&C^my-0_RItAE87+n@Gz-*!tNNz-3O9-23@E_&7N{`wpJtvAMxFe|2QY~jDP z>vJZ`B)!0YW7(MR$S22~sWM-PzexV_#HV{MY=9Gu;GYCEQuziRzwf2^PURJlK4U*; z)02dUq*l_}2H#}`9{BJZC7kxdtZ!BzrVhUVQmgnb$F;eBcEEU8z6JlT4$iGej=O1I z+plWqm&R$6+zUC_vO@JM z2ibjyYA^RVLY_8S67DcB5`NZO8xtWo_q$%`gMdDx@UN=xp~(LPody0)BA(jT!XR8DKJDT2)NqU?6kHQ`t__r?k$1hOd8fAL_BQOFL-+-J&*33F9{b0zXNh>_OA=lJUo)J%N7#?3 zgx|aWbmV+bpmqKY?)N#{BOCX>d-}KZ3}{4iW8Pbh=p1A`;2G-M4&n~*M=w=l$_tql zV_JEMgcFYmp65Al_^E``oU3Gecfo52>5wAU#v$3+xQO5z7pwNtM|l2I1XntN#)-I1 z!1JquITO4oaA*Hbc-sy&Pgy?aFMsol=2;n;&mimw#!Fc6 z;%z$AhqAS8@%4eOBLAOlJbC_r2R2yfT8wqY=Lo*+iwY0?O0`-!17B7J2BYqahU)E>Jv3w>B| zOPyhy0Y9BKX`H?U1;MtT{)7J^$}0YcC$xfpBG3;1y{=Pm)N$B&l*NSAFX8iMZ8TiSWCFIR)kYb%}i%~BbMR!#SJ`3>sNSV+Cz#Z>lfh|^kmY=5k)SIUT~1s zT0m}ZU!P+kgl7Ywb>jq;PZ~xFx|xSi(LRd#2eGwDoQFv7-^|207ww~X24low7oz#& zCPXOoF98&R8H>{w8~}tjBWm-C5{NOp^3`9%q}+X`gM-&ha|A&loCah|MR!DZjbb61 z9Q2ce(V;%i_N%e(5ncWC^^^;h?vX;iHQYU4mr3Emelp=Kss%E z$WO3KJry6j++G8<=3_42pDE_^23`)zLMIdgJ+hAUX8SsvgNq$0`vl~pg1-bGt)D<(-L>2A&ml=z`fS+HM{E(;PPvB0_ zm1CQy|L>gd<0bLh#~04tG4orA1$e1rU*yp2U*~L)>!ah|sf9X1*^?r(hIME$!X`f> z{EFs#0$%F|Zxm^tv>YBL&Ip3FBc02aX9Kh^kGUJ!!wIQMRC=$Cky?b1$I zNg)YxJkoy_290n0PiEw-nE4OUdj7ndI449 zHN^Y{-sSaq5`AGOh*GegCll44K9avfc9_>>*C6$#Xy2e0V1t7_D&@*Djo0j=_95Cg z_#>-*IVKPIOVqcJzm7V*2-;(=g2uZ78dG~^%b{_;rf95#_Ru*i?xKEIT&ds0be~; z!9(kt;PnOHjNsE2=uuiP3-P%?V_g~nzwr%es@q4mq}ftVn@AQCzl@=rO5|Sh53DKQ zr4}s_xWLG)& z+v)A~(^hoQH?$t#DpT3i0naH9nYibRcN!{w*|MTWcRXq0k6kR9cpfj3^Wp`R&`#G< z;wSp<((mBU&HlUEq5hM%KX%ie%WuE^C4Y@}#W!xcY4M($9@7SH_TSmO`Tf>w4YZ!3 zbA|HP1wA_h+aUA~)=Nm>vPS7aF2GE{}{o+rK5F-)Ab@mks5 zM0<I9&Y52Ojr9a9et5@G_AfubeQPk2dq={Ki+a5S^K=(a{+v0F36< zrjJrNSNi$9mqT3cp4zBSx%{7+5RA&49r`aj>l{hwM-&vwi>09 zg@W&yWWsONk-A-VasNCTNg-Es$aRPKPjd^BMQ!c9R3=?3rPApUp#=$6b_|A9O#>B#g7)^Ar}gM{r+vfiefmzMt!3vAqWA6}2ApCztjE1JS`V=oz?rGRcOZ+r9^uH?Y_5!aE>`8=BFNek zhYSc5K)NYWukm*zJ4&e+vX8%|;N!L6_Q~UMt!G>_zDhroE05cBq#ihXeo{ML8=i33 zAfo%agV!I7;0G@=_SLWhs6wvgPdSxDy;D7E@K(W#_V4pNJ&#oQqMg)G16=phNl^yX zPb4bMhE*BV^sJxv%YF$DDI?mJscPN5b}b9_S?kdpRGaP7kvSIpvz*BqIo$2XYpj}q zIs%pMbS9M=$fZ-2Tn_*D`$|OvY=I)3g|w4O>xpzW%XBB7iTzYzTbB*U^^p7?_!b@S zNjTXbj?D^N@Z2~F7xs;G$g~D|%0r%{&ryM#ZF{R#tT*ui+lWn&;6$9HlgC4FmOr7p zR>I?9xB<9DcWQ-iur1gZJO(@9#*M_Hop;0|GyhH+q%-i_NgvsyE0+By_0ch;`p7i+ zJLf7m?QL`LB#uLS-m&6a|5W2*-M0KQglkz7eJ7&vxij8cuMaVV-D zDV;uTOhm(D+2BB2r)-8uJBQ)@veNBY}-tzPAr) zhFWBB7+pAhc;_mC1hq`Hcci;yq{^t}9<5R(XG;UCxdtAUFdfKDp4SvIwt?g4E8%bP z9P(-p1qjZA$^=LJ+yb3)aRVn@r{UJ z?NOLbMD6e;+I{oQQQDt!;0`m+RR*JZgnx5sU(ee{0fsP3&w8azynxbLbE3USZYdf4 z-f*S4r&!A7(>U}TjyReH?(Qj-GTCTC*Ntuuk2lv64!yV$Hym6QF3~F*-3DsVruA|^ zcGs4Z#c^pqU#PXBLcf7`a`Q-xkHk>lI+aARk^&OZ(uV1lAY`7+Z*=9Me>&p#fWMeF zdoG*}>&3JrzYVn9vy9eV(J}*?QC@xf)^|Y5mZ#5R2M~9g87|*Wr?Usn{Gr2Q4R?(x zk9flLvA7O8ITAHIqbU!Ki-3E&H(D@iJvjQP<&3CjKvxW;T7A6JVQZ;)DFbcY?fU6* zr5kDj2Y{fLX1i4_(u3$GzmG_^oqe%wyx;7o`(h;D82{d-wC3te&p<3XPwF^~i}w*Krf-FjAgn=*tGE&0W!^uXv#poCk&m1qQ+U=qjvov!z*dop{nimyL7}*A{i)v z9yj_4~s&k5zD~gGKvYixs?KoNNA{g44MGbggK=8Y}yL^CHiN z;iM0EJj-X&e3$)^;E^cP$c(4>j2oOeZbDSuQYHo!`MhqtcckD}n)2RA-m~!fAS(OR zY^^_58Og^}uHVx;+>4slIOwwQ^k-_{V6;dy$gy5diJVEiZHJv7k7WmFpCP{6aqD8x za?hV6oZ6S^OqW_>r3w-*>_-NZod+!F-zARn82_o=0?E$+EZXDurM2o8Xg<{|&)ef9yt7Cq#c*cV5RW ztC$V}KV-Rf0PydCcMzH{(}(o#Zk8h&71M|4$#fR{w*6w&ucWgW&&qiUF738huQkE` zkmRCN=bvpj5s~yKKFJ3Burh#?&Es_u_}#;0dodo!qma|C8zr3R#^cg|5aQc_|8=?(Nc8kr zSBB{62>ulERfe}G=^)p43+qFrA81{wgnw5zjgzEUwl9hK3jS2NPhZ}DP|F?hb4VA@ zUJv?5!yqrS;V}h;`bJzk-WjKx1pfe}I zE4%NNaN?CW374StmV=HJ?S;%|k$3fx1{uh~@8k2Mt;`e39+Ivpk^S7Vo93q0lKK_8 z6{`;0^MdR*i`Yg*%^}{Emhe2+sKR)}IJf*-jgxq&6*dg>4B1s7tKd0WW8@EA-I-VLUeZm7Sl}0q^SX#~0u)|s1D#9bG^yWB zKOs1cit&qm0Pu)$b}f={rTe7KI7-5^SVzu35_D7fM}iJUfM?Sys=c&@s}X@g9HwrY z8*I-md@9G2YdMSJzw0iJK@Xqxb>cc28+eXnrc85K{v6>7$qeax)pzIL6t1}~kI?yC zWnoif)<^Y7V*avsCl1d(PKTzFJ}={=QQ?iM-w*6ZBmEGbv6~fffgS&sSqG!oJ3;Z!367@TKGr^eX4rVB0qaJ`~Z{B{`Jq1g(KTan22Z zoTxlUzWs`b0$=k(?2;265TK;%fZ?6oF*7Fe?LgX{_r7`^)rZ z>8TO)PI(dPYh^zzcS$4 zU+x%r^!jsKA8Y;8)Yv7z^wXC|Pw~sYjQzmfAL$?IuEdgtQA7$|Ww;bgDwl{GwP!sp=?z&8!LfD($NByV3U1--poF^b z&Yh9dzCc<;jfwaMG84$s#!mGe2^VA99q^OjGuTWad#nFLwkLQu=_3K(Shp2Yw+TFy z87aq_6*4d28y`||vLWEhHSf?IK4_e4dsX{1bOHKx;VFe^O1iunnwO^9liq~RBFxC= z3jAK;`y%NqcNO6wzE?CY)7e~QaRYwhd5dI0jDLAuXG=d>&|&#UB_5&==e7#?dU%bD zpWs$tGm(#Sm4xeoeit@y{cRFXc|Io1OZtZ^B%F9FB!lZ-m2fdHv=_FkV^G40KJ4qs zaZU}vnLYx(r_Q6~4}%V3zFjfZFUdaW&+ztj3y4ZvUXrwL#B~n&CMco&NtzdCnuxCpc!l;(7STcQg!eOvhj>W&fr+^H)ezh! zS(133kZ|6M63;#R+Xml3%nNWqH!q0q5-)O|^3$-)4SS@5piemqcspf(39p9j4)K>2 z@XH?T(s*v4!C2``d(oaR91)qdMn~*z=ibP_<8=FCUHCP$76$fpf26r9-+_Elo}-}o z+G`{}qIpOTy}B->wax_nuM23d*7`Y)`zReub7$Ko@!ub!l}q_5g2z_RB{*!AiM4?J zL>}T;u824*at58Jifa@TWOf9dXi)}s*x5k%|6hQ63Q&e^!tg~!hGS})zV zM4i{1pUr6lo?me@nIg)C;<6MB)NoFG(C?1=AdYJlx}VijM$Wc+{9GA#S>2jz7#d2{ z;LXF@;DA;g4UzI=hH2fkLDE9#4CsKBGkI+SIV9Z^S_73+M=?3pNkV6A`frJc*0M~z zA^2=_i-eP|a!5W`o(}x&Wvp2uUItykPqg0`@d}ppU7q}o(33iH!mw)z`N0&=?x8-G z-I;FWS<;h`MKPx31&Jq1@dTyE;wwL+;Ph?@WQf`$58^@-A25IC>w8e_1^-R9Cq7zD zczAB}SI?UfnY~dDzEco-N%kGqORRqcywj7oh#m}Qdc-^aBH``(&u`fpzbmoG{rut0 zv;RKl&BXl5!oGv%OxDqWX(j2)JR9UN{aKDX95ar)D!QxJq4o^#Tu*DY?g;u8={?y$%f67A&UNZLr1!DDT}DG6S=8_I;!B0zOe}VO7yVSE zWy)@S3HIKwByt=ipI8T>%Q^!)b|J=%ys^Zh%EIQz>EF?@MwE4@#?E{uzO!nlf|GB9 zacqXC3hy@HOR?_+&xmm^|FmpRv|-*8>*GGJ;6!KOAvmhU{)jlly_WkHYX@BA4x*3r zqu=Gvgk0nT9_)apgFhd^xkDax49WlfBywnix>Ta(NyrOs#(UzSnkLQ2Xl`k)yTv$# zMrIfP;Ji5~`a}6#q>9857NyYO25hvA~uDe#b9;+UDRZ_8k-C4SP;7>@_O9koB?`yq1PKauT)?#8@# zuzl{pu2PM&Ky+f?r~SF#5!Ii0T7AZSi+#ze?$CEaI(O3p3Ql8!4yC?P*EY~aM2k@S zHG4ztS;NtBnSt?f>}6>+(g(>uBQk($y%YE|;a}+3JAaa}k#&$`Apb@2 z*RoR@;8+(ygXb;`!I=ij8_yRAf48zb3*4VaUXDagKUNpxGO=gCJa=KXTj?{J`n|RCk78pGw zQca#QemZK|iG1dU%CZwDy+61G-$uj9Ee9i4CwSX{%T`djjx?;u+L0Nm=%pSWY(~=7 zggDiLZt@CQ-6}YSn=2Zq z6w}FcvWPUVQ~|&G_*XXVI5{_4%Hd_HR4T0@`OL!0MmfDyC|TKDu2!jl2g!HgHC&OQ zH4n+|1ud_{pBHP}HhJT5(Rus-fxX>&tt0ZuDKF@P=V_gb($9-`^wTm+om$}-2>bT# zatSB?V!7Np+`wPN?+hnC#lAV%p9sII1pg8{;|94Wz5`rZhPPF}Bp={M!S5Y!NjTXV z){B%~8S)+U{r9zeIewRHKi`>nN%PD%WzA}`$7~lwAKQ)!;VP4ixGbAO7e&OKeB{qW zV*H|f3S15DqsKMsjvR$IBV${l+9WC*717eom3?^!=|+QwXBJXLyinEOoX}FecCSBB zYjyeuZ7msZb$!AOof1!CWZlMk;dY12F zM@4Z{c#XvM+5}`-f;pUA3h_{Ps9JQ(UQZR-`zS=?G&^P?Q?w8R?RFj4E1}3prrU4^ zqY0qLnwp@Y!@B2gTD!dk>XvmFoh3rYZ2Kv}ck*y#d!`??MEgMZkX;M4rxO(H_6sGo`(-@yyH?)!?&&|*vD4wT(Q#f0Tr?Wc;xhFexi1&F3kQyMGa^oy!?UV= z!&p~6F5wiXV*Ikc2`D1_C3~diu7rC`_i%o z&10%P#k)as(SA4Vs+t#%N6>I{osVVgRJ7k5#0IHftByz2G!ZrS6)$ZAO%?tswHG|I zWwnG`@WG0ti@Su6akhjL{z?dcJ_HwZ16Ls89OhLaZ*Mlxjqp4i;KA8?&pU*u89AQp z>*8+o)W}(pb8%-y+>NFz^$B=4n{Gg`!=P%2j>Y{I93e^|6eTM9O`k#PyHRyh-SQJ< zU9bUzV!GdFdC2Nb7s}pXzS|v1SNn1A!S$=i$1W672-d-r>xLGI_252&bi}SJRs;EzxiF?vK<8CT>l^J&0p>opsVa5}lJ;FRmANd*y&%-e!DR6YagCSD7nPd4oV8p5yEbkn1RhrT6wL+a2~ziq%z zGR1fV&#Vaa3E^iRrjvI>UY-u&;qg#=T9=nBLJhZRjeL*LrDbc=t41KyC23qR+0*zJFZD^pKL-19Q!%;NlIfyzE zsy&TK$Q#Sy#?Lj(0eHmPtFy?4IpA*GKi+?C8zw6}lDF0zO5;)baa6zRtYtKOox)GP zr&^n?>q7Vi53vo~FhRlPo`k56Hy3`W@KF3z;z4eFr-DlxDb{!sydI`AjYshW@E{tc z;4EXJ{kmW;E$;CHzu?8anNWK*e$;Ud)}Ca5;p=XpaW62eodkAHpGew6C) z?ZKW|t=HOb68#CA;veP{_ba3Nm3w&6uezsC_oa&DgKwOR_U#*Cq*qjp0A+i{*%47t(b}%hZPUFWczw?8o7khn9ZCneEeZfmdv{35p5U$R;pO%&W z7`OxRy05D~r0u3rA}9X`Avoh9p9=Ra!m`eD5bafbV3hvvgs&!PC-d<{`%YcK&u@7- zz9g~8b>hvZx6b)-+vKM{x&KYgK-Ge8Y0pMZm@;L~S-e(q+_YN*?J*jOH!FPkt?()m z?P8Q2zs85SSpzz!@Mbc!W8>YfXpDmiZDZjZ;1*aL?pET?^w{ySmQ>L!dFftLuX&a? zFqA9UJ(iQs;C)0lTekd^hX;kq*=A#3U(rmI6NM%dZvhP^3XYBXi+(weucuMYQtRJm z#gkelv!CuE^L^c>Su&_&1_xxc6EaQ)IK-GvnK6wAg}*1DqVnVv4kEd#OT3)N zJu^F+uT`Q$2q`$Js*$OcO*5H-Q($APmfxBh=&$v9xTjf(<}r3BgR9&+u5IU|c@4KB z@aVGZC!CsN7_Dw=UaR)DCQ=AzRP*^TrBNxx% zejwF?o{k%K5xRvs%9;A0c_8tJClE{ox9IY-RnHZhX_V)hK4TKS5!p=jU-1(;=~fTtw&XX-%B@D@#v$27cndTK-H*JQ8T8ur;Lg;O^&lK7f(C3VWrCj zT>DGmF>)=E)m=OWnn6`zGlNG&bKtOymu&9K>6rpb(ID!S)r$q(SuWuTfTEtuwbE-i zwE?SE;n}Ctnp-v!>0CDBc4rJcpk1ugN-@NXbh;0@V*hD4tGF8(S~F0cKhKiT5}(|D zGVXDo!c#g%PKiaKKBuu*R(bK8o&Z7VW>mzjmo#gi+`>UU{P)_EFPl8Ka>5NaUR9fV z%ihR-Epfrw%YJyz@{^|cYyCTB|8-&Qw_fy}`DA4?`w?5d^G*{dF%Jr?sEjX$DiD%kR zIN`Lh*FCnm_IUI^yT(5g`|@XMQ|HZ#{y;nG^Vio_5O-_4bLZvGJN>-#u9!RL?EJ^B zI=OJhyjd49?~+bOj006-#)E$sa(xAUZ-AZ~z{Y^CveRW~a0TuO(yrlTDl{l)qxgYh zjp0k8yt}Bg0$n1dI49zcgOxPilky5Z!^ysUw4~`7c>E}ZI*7N&`m`ZDE$0;~aPvL9 zpN_+pcB;IE2>Fv)$ z!Q|prHd!gSS<}>=ayDNuT^F}X@eU1+0gJhG0!fScRN6#1GvRk@x{s#|a67S(#Ic^1 zLQO;5hELjsBA!yf!`?RY_IRP1MA8aVwGML)(&pemOdky>6=4yt_de_<3T znUZo}<6aIbJ2lcDEmf<21~+P5RAt6ltv}>vOuCYc)pbVtV=27a>x>M=T5IJ@wABimr%ABB2MdH5{_e0mm z?}HEd@JEK{e_TgoaMzO&H!}A0OXu7H86$j{AN;`Cc%258KU`0jPm=JAWDM89$)wYz zZ~O&K4!3|MapD+gc&-${ndhSPY;Uw1^|uin8XW1*LJWiyWHm~N%u zxmsedr|6ZD>+G1dZqz=(*CSfbkmHy=j-6<=Qhq95^z1zGxtJ&M1Jh^orIJ2#_cym( z5uck_wD*?>fAjR;fIjp0PCf0FnTP0T!1BRywLlJ@uk&5NUjpBe5?@x(Gicw5-Hnn# z&esy_9J+H(u^%PWBY8`ghuQaRhHyKYu3ntoU9Rw1DVceaEFe zsG#Mu!5UIOJg&|cY3@q*mG)1BeQ?qL{{YVdvgRWj^vJ9iB)^sGV^(*hh_Bc-Qy;L+ zv(y}eJ*$Aft>7%z0Uz%CtArOo6R}RLZ#FMb@Er0C$wn}rY(5?pIRe^-?0~yewikDm zBn^=h@!k+VU^$ceRjjLuS<<@lS-OBP+o15sankLs5RIsPp6DR)e1P3hu~=%qHyD3n z{<`K{UY}*pG3A&f?Wn>rYQ%zb72H zY&=ZDX-pg+5OeSrh2Xr#1aFIn?f~dvo2z1tv}V|u2DDIdTEg$t;cNcVFdop3Zb5*i z;kdDw*C`G2Qhp-EX+vv3bYNK^&c&J0g`x3tj-{BFiUU(k5fRUNg79>J8j3!JI!~+n zGc^9~O83p*_0o}%IWOwsJ2a*WeP<)<4JA8ye23vXVc`%zge0my6rUm<8^mZ#w5PS1 z-0^d4`3lJHh6ZnI*^4!bZk4(tBv%xDR6KAa^)KeW_7{W@?P*-401o{`YBPX`Xth3w z1!Yk)8TZws9M0eS(l3Yg)w=i&&EKr^pNhd!AIu-3Z*{jrtT%9NgY|+g2lpH}chCgh zzM2EmYVGERbk(au;U(bi|%7MxJ|7AkJYmp z#zcD?_J6nZbNio#+A}T0+E=O}cr}DOS3{ra< znW7QTP2_A-?+VWR=q{o5odHeBt}`CeY}iK~B-;yFhp$n2j^KNt{S_Xj(F$(gfWJ(3 zdj;#O2DnZ7d=)4V&=fVBP0-Gb--Sw)6fc@1&siah^j;FV!~F09l9rWEjBRKPt}+J# zsX-Jk@*=uK&+`H{9bcM8KjcmaFRF=m)x_U;?+;-SCzh>w*tS}E#8Z15Jj{S|)jYPL zxUyICGSO4_nQs_uO1a6y0PtsV5Ot*;m%QB;I-KA4$dfhx3+!GJ#F0eAde1 zB4#e>;@ETu@r{%ozYEWAp|nrdE%pxdRf?8gbW8+6-^Qz&eoimyez%vfTeIa@GUMua zUR|#ZdlDHZxW!Z5Pv}QrarAj?C6&q%BSnez^{EujATvx}rVBpIFY_hv}Sl z*}-6sK&LWXoE@n3AwFWh5WKxL@NGzMa(nOE2K*!=f{)masoJgtXFghkeRr_V7?0vp zfuAbHDn1f;9&2bX?6dNnRs{N&^d^r-*!kzcm5fK>r@UO$tYQhJ?A!+0eYv3zHP%(| zlt+z~`w&=G!LqBdmWhXnYRIqHE8#RI<@12n-weSo4)kDKi~IK|_Q7&I?86WqOZ`zC5slG=AtE$QtA&G=2exO`}tlkWWo;p4aN5N43`h zN53A!V==oL7Yd9wMR~7`291ghW#1HGvBpFkQX9LK_rBk zriuHxVH=Y$z}d6=U6^xsLre>uYvf{cjp#Bm(_L~sf~TuzuaC6>_;$3rwJni|eW zBYw6i?bXtF$GxwXF5>ryjTif-QUo7`;-ja_e+(a6FtS>55Z4X*P!-TXfX>Z;(Bl>BEvUpNMz`)=}`f${VyGlSNv0m>b~l2=-tu z?ll!@y;iL!__n~Op!R&8M-a5{mH1uo6}Ol0A1OH9UjrTqe?!8{z*C@p_h2UqJi-3b z#a+k}!xuB0+&bcO7yHx@o^8L7@SFC}ZCR=C&wu8&_+RF1m?^&F;5!1B=;QnHI~1pN z)OX^m90@nTi=i>?IYz?Exc|w1vm9&SgA~!8?US6_cV&AEaHi=tXd38O9Hj9ais(0l zzXK~L=SJ=8H2Tg5z+abeqMa3@9W(*su}~9;bqe(hntofti4I}>-;!`|)EWf3rU*F8 zu$fWKS8Y3;F=vfqD~aH2EMcSkG6)4rpHMnyEldN!;-;(}R)i}V5Eg2$C; z-v*xo+=uz#I}Yy7h1c^{^&Rs0>;1o?;3Yw)GFC?P%y_7dN`2kh_l&1cJHF1J8JN3W zr)~LSg^TnZ`q*VO;4-n+F2P@bw+}3VEgc1ap(62+PsZ&ftq=|3eogcn#`B_r%l753 z5ie0M_(N|bp4GB3?%@X41iVl&7pG&FMBpcCQ)GM)wI415-&_d)1Gd6cv-8r>i6dcUE9lN zJ*}sd@q02EYxuTkPc2)Bm3mzqCR;Wh3oPVO9kp1+VNtO+(Hrk`_t|a?$F^QNiMMbO zJ2Z1zH;#8rWQqBB*C+Pfyp9s1R@Kbvc0OOqSScJ47R>>}F6e3P)yH37Qv1nK?dN^% z#JA3yb>8QqSCsvec6{+^ZQ0YYmj+S)5LJ^iIGe;{jYYkXF^UGb3;biT9Mt)wDrwvx zuUJD+TDEWG_h7$8Hq@ZK#HL^Kng_O<w z?*6wg{=!LzV@xIH2iW6{zoYML{C%iz=IM>-RY-v36Y_B-zqQ4mEUWezDL0T6)M&?y zQFZZD{*Hrjv3$~qFuy(WJJh#D7T?4yAf*TXB9^pJQY?_#Ge+mp{yr)%Jk z*}%_p5dE%&M00zRu}Z!D+WMRu@$U@bS98Yr16iVY2E*leST;aY7yZ`f+g?UY4BvWz z`zHU2*GPT`9v;Iig$FW_8IhD z=bJ4O?v7fYU`^D<4;8bWL>aD=ng*SWHjqcQY1%JM{haA|Zt_v>bi*HVuKJy!%^aEO{O@*s zkN;DrJKJg{OdJ3fb53hQ^N^_E545h#7pGcQpQ)vEe^5*1GpK^oTosxqnewt}q{jC8 zcnMjL8d_?wFyQw$rLl2JprG-MH{N(}zSm9+`u$bQ(;|I6rM?*URCzO(O=QxAl&z<8 zb|RB8tzxQxD>8Y`T?w${oz345|^2{pnCMnRWr(jaRcn?P-0AEiGo zWp<;46K{oNr(=VJ7ddBE@z!!^Ac;RGcfvS&=wFJj4Vsrm|l0#&tq>!JImMMH(jAZ|7L9dBhmc{EOGo?PD>X-|rJkrU)Z3Ko8|m*h zDn_+AQFgNB9ygn=_4-KWjz+X0zl35@TEAwa|2g|VjamZ_VefG3FHk>Z&)fg_t>P_l z7IZt!Z})9-9wz3^@Kt|c4HDQJp&F z`RVXaG4FQH=i+xpX}XmELh7Y4(S@tMgSGIunBbzONH!H6de4Xsrqa=@o6Dp4bvIr# zNA5@|zt2N1d)_tkTGbj!#`KYHkM@6YylLpVIo+=qCBq%*L*sHfA8U2(A~IJTKT*Ri zJkZKyADrfR3y&EiSG2P0bKD--FrrCljw)BcLQOe7H-9cgcsf=oJmi}bex`}4KTN*1 zQHOU0`>Z<6d5yM0!sU2q52Wyj_oFd>+9y^Se)}NSV%Iqd`U1W4Qh-jdnA3vyU3KVx zy0-w$&)kMfi$v=fokAZ*Z$4muekg)zGo<>T2>nl6_9-mM;=fI;nN+mL@5QTb1MelY zetIZ6+M7%pT92NrS=nmMZ0@fW3Vl||$Rxa!4FMjo{Z?(ri?-H~rW5=tj!T42$#8J_ z(7`e#5c?YLiB?g^J(FsMA6Uh`hiJEr8t4wyB{%oGg69M;cHKq3Yzcdl2E{X(M?F)5f(7tv8>j z!ILgE=lqgx59Hid+wU1HQAzsVf`&7C+iPka9-^PPg&fbk={N=+YR~w2J5%tHKj#mn zl6foBjmw|({GH+I@WbsA-h#tC$mg@=JdTZ=82R+ST0@Z`30sZu3jJ@?=zBA;$h2tn z(f4FiHJe7Csddwf;2u2+UaNS)bLu?g5+2i*L&l|5->ac|d~dH?)JnE**5cuA2E4Yj=3Hb8Y2o8QOvV1wr&qReFzx93i8EJ77 z4jWG|`jXc|{}a?;dNibkHqmn}eZH5@M3X~9coim%WY>OJDl?6wH*9v2%@*EQ=&tFp z+)zIVlLLb~uGWV;w%hJ*E}jgq%&5*JP2m+B`lQn?VmpPoL2iV?!SON7LQIUv7G8(Tx`@I_lB+^GE*uNp0nf zqn=F8)|Nec^2yJ>_oTmHyZPkWZq4zFcB!Ooe{$l~C;s-gm6u+6>#ZyO|2ckmk@m4q zo$%R5e zVx8URTy}zS&ZJ4^XYTXAr%k`lY-+7}mS&tW*0@ZI+<4PXH|~$z{`%{Swdu~CnWxI` z)fxX2{&V_cmH|P~-q6&W}c z&G_9ZX*tO^Wy}HN+xdGM-`+64Vtj~oavaLNN6tsse!!QVL43C2Xa$#de760V@X)9D zL;l{&q~rK~nDa5yDdQ%gBEh7CG(NPNF@0<^v0Mf31|0@ooXDxq zk`@xe6$E%?xqhysRik)m%1sw>N5Jq~dxuh5GULHo`vbb} zNBeR6icIX;jP2ruE7x}QLUBM#XL7puy_hz##X;6tbT(C|;oXW> zb^|=H69(67r?vPUufM*J#!AOxw8%59uJZ-#+dA;VyJ7*5ZKndp&}0{jI`N z6nJ)B+0g!`{S%Pq=h9j?Cld3Q&Pe=d&YN>Srt}T>z4u4-9paKk_)b`}5S-#3l{zhU zzNOkrzS%z9&^O5zTeunxbZjM$6-* zcsJvh>+=`qsp)F5o+c8q49#ZRPVR!AQk&m4c{R8R|7DB8-?QWz3f^?&_b5gf z=GmtdKDk!JBUr-@2`_NnbI2;acNvml;kN-U*Szjm>4MiJJcu7FUK98?*V_lOB6#Ns zHO|KIJg&wWj9;{0qS{Lyl)O({iMqdwac+MQ<8R;o5YGbN4bxVB2V5=TvX7k{!=MpM zxReX`&H#&qmxNuBe2(k@g+I*yk1BZBp0#g#g|Kb=dgFH_{T@-@xf^dpV;Wf+-`#Ya*P08;_TP6AGVd@7_6%#q> z7&APM;5BMGgFhZQHSFcifY++g4|%-u-;?m~u$MvdCgf5082tNj&E5NS&GVX%@VtG$ z=U;w~0)J)C8@Z2u>tsRe&*`3yQrAUDn&W1oI?4~=T)lvY=qxWbujA&C3?4(zL~mLW z%MO>C?jJ5)@5cWt<2raY-F13Rl+{Kt8C(y<#m16h^wONDjw>*_CeOfaI{FbgKO$;Pt~pN2LVgv^kQ{vwf>jWzIk3tp z=JF~0Kap(p2i<{uzvEh&gqtq=d9&ZDr2E_fcO+7=i>}w|p&~oTVcUc5P|C|_y-0N- z6=NBeMOr73j3QB>Xy@=|ebMTR_1CI>2``Z?p-6nK2aodA`m?Fl)`F+Q&qlN(leV+a zV0}3)=l2ib#lK>{Cr3q^`f=ulfWlC7e+H2QJ&!wDhEeT9!6w_nFRfPIpc+g{zW2go zXt1BO2M4e10u38%abD<^a+0|ZTBnhp_af4X8X7+u4QK9emy!eN49lgI0Z6Wd%RU4y z_e%;c`M=Emijvn{VBaJjOSUiI3|P$JbMHS^**@5xC_69pL=azBehl@PXy46Nu?rfH z=TH*#;LqZl|E|VLdmYeE${ltyvc1$_qJ39J!A~PRiN()+(*Ny#|ID|}Xcns>Wt-z3 zU4J`Z9^A*4pJMIWpXBI2;l+2b%|TD;AxR4gAE6S z{$H>>3;CFODo)h(3btFc3W&JJA>`luK%UMy865PQc2D%zD@I;(-Pq|PuQl133hti9 z5{S1V9p;jKyIU3{zvs`t1g_DFf*^|!ov-R*%4vF#E3v^DS>lw9vPfp~Vu ziOe~$hs*;^H$)2QHQyt_1#>_p6~=&$0~5gGk`Y0U3WiwX187*b^G?ogZgmQ_S**S6 zI5jAIXJ1cqb~w>AxUbjyzYwpfwd52WyfJE9X0cTJ(_; zi5lS&51;99ZY}FFcm!irM&1;UN9S`{lOWFI| z$=tqDbLG8C>|npo!G(AHjTAdP@ZP7T=?9n0{JxQWAE-Z+O>8PSBf8VnKk{ZFTE?*f z;;+33la?r8T5P)W0R5rNT}0*&hy9*$@Oq9DUXOUMS{+n|jwZ$2K6uxmWt;7NC9f$r zTn&D-tU2gsbj^tlqP9AcV^Ef6@5B4Kj$X=Ij;rHo+JCi) zko?3vNO?r?;Yr#X_^fgc^GBWcEn&N)y$i>1S*H*N+&O_y*obF4I8yQQt^Apa0Whxi z7Zy-YFF^ag51$tmzP#8&co!Zp-VmQ}_&3#F+El@}Wkk;25C59$Lh!Im-lE_QHn<}= z7g9QN)9;v0o@!qV+xU)~6ueP>ZzMRRN8(}MnsQr@ghx6XQUoEQ18$9i#1zFFAIH7v z6SQWwG#@HI+E7z+cmVuVOT-4ek%AXZyHu7TpU3^_oEFh6EuY2jZmsx#ks;r-T)gy~ zh2Q9D{e7u^e+Z?&`Vxj?dxbcPJA9DSh$&)CDZhtlD|N^XG<9BUyTR}I0>L47biQ4I zO&$-g<7iY!n+BOXS5!G5e>&d6L||J>_0T3CGoIhPKAO(B;ghJ$f@$!-5iI9?5vqiM zkL$aSuy0N}D3&c{GO=`TAF|F$h1#I+`KFHpt)iB9pest6Rq%Rx%e74FH=1S*e@pwN zSFmuuwE#MnOFjOeITY(dnU>tp(1`beG*y00(<^#9RV?LmX=Di7X&XuYI$lTc3OTsn z{^2g(@^y-ZgqO_y8AUhkyoJ}3?R@;814tb>JPooVZNc?KPNwrYQ}Qi5t;c1Q#_v8Ox0HI(Wi-ptgW2?LjWmNjH^s zj8d^LRzazntlynUIhmZJ8&-8B^6r}7|Ng~KeEz0+H{JZXqd$N1ao;=r%MZM(y@gX~ zB(T`YJTjAP2fUVw{=qk_a`ZLapSE;8W8}?z$?i@Y!7rxck9qagW&R)MK61*j{u8%f zF=5rQS7>AAt~zdlwot6A^l>CF)X&6Zyj%J{fxe`51#Q729?`wrO{+qF2=jo34AZq;~A2FHX`zoh||vE{n~dJ60Y*_Bh28<(D8 zm2B_UpJ<5_=brZPc^Bj#GIto){D9X;+9?s!lB<3d)39-E;~iH@+ER{4*yds9%k9Y~ z34KlaBHnR@gpXS5J>L(tuk%j4<8lQL_{IaC_O5>rwyytHeXtKM^~GiCI}Ll}jYlYW zm_Oqk1qBamix}&r3NGiyJR9%0M8S)aUw=cRQ9k^rxovz=wGU|FF)c2Z@Qd~z+Hz0) zVbZDZ3}ZjNYxoDanfKqrVl5iRy0NNoW$XJ6MBm_(L$j_@^kAAx{s%uxxEYemc*jKw z-q1(KXB9l~R|Gvia{%0yqOM=*oCHE|Bmd=LA51@c8Gi z`$qiTIbWRh5`Rba!O@WRbrw?|-)H~l0`QuH6Z9nK3(iE2Ah?!;^>w9A<*Fu6N5!5o z&C@0v8O04exB+~HuP*DeU6R9otk!u#Jk)n6Ukc0!m&xv0~L7DIu-16{o?2xTurPI@y$7 zD#t9UoniXjI-(ZUoME`l_aAZGv9;IVJouK6yV|o0XPf2|enBg5o-^n2IdlB4-?#Am zgTGYH&OY?capRu((!x(?oBoP>dsYh0L@uAn8jhDrB`qDl74>2XWRiB8Wvl|ah4yW< zmnHb(@+Tq@|4E!|T_e^2b$>WU2D^ESw3{D^d>r1?l*qgYxJVn5h)r($1RPD?nH@Gh zxG6wdl3Y%zAtEzzIg<{1IOOWWCe~Hvr20Qaw(jqKu=(4~ZR|aGyK2%ulncD)|O}sqhIjv`OB^Mve`@_huULBLC?BQWI5UJX@DE(yl#N#Ox!Ht1u2JUj~W1w zTFKi|-qx*eSQqkndA&A5--Xso?Hks1y)S&x`liSbYD*l2xO`wU)pwS`I}70K4{;xm zonw%BG8MaTvVvH>IE>(A)P57?_aG8T)S|j){68{?F zN36;Jo`1c+C$DK^@6?WsI{u?t^l|NLC5xB0H1*6(zA%_7l%`J26!Zda+d)=DE|=6r zxW|AKo5r=>ngZMLA5{Ipfhav+N;U5eIQ<+gyTz8wP;a@pyOz(ylBS^-(k0{& z_Icv}Q5_qS==1BNiOzkA&S;At|3CP<(SF;0bg4h}vddbRww|LM?a$L*;h&c1{Y#^+ z-}@W?S?$K({3|cFaor@X5m9(w_~y}YS~J8Xwn;e2r0_XN=G*tYPBiW~w-J}vcWl`a z(4YB9`XrT|1f@}^?+8B${b0>7F5pAlM6RduZ@^E_sqaXiNP1{Emy_)SKZbE_z}Xvc z<@mEjfsakzN?SX5-tZgN|AYD&<+rS_`#)KEzE5+FcOb^XxRfoH{5)U6)p^ER?6pFE zk%@B=F-He*Vcp{$XECna{sefvjJ&uj2m0t7F_HqdRz6!>2jBct5qeGcc)X!97g=%G zfgo-yl~ACJ5F-p!Py6KY(*FgSUc=rZR7cft!eI2$>{fL z^P^YPHuBG}_HA9QUwEm1%a_M%_a2g*USP3+Hkl%KsOAuLT!McwX|9N8hO+PL+sP z=J03Ao(dmN_I8H)US00u9lFG)z9W54U{dXQj%%?y4b4&Uy7%h_xSHd-r39zG8IOuH ziao$u_}dIG;vN;-W--6zo`fqr-cQ~i{}%XIidLAtlyk-H4@AtVjLq zf#B_H|4E;A%;fJF!MIq?<4ZrSa2a7+<2|F`fowt}sc|6!i*a4Pe;Um(v8eL$L0k1( zXIP0vI~N@H_N<$y{(&V`j6EypAjf{1#2fgO!cN6IPE~N(Uee80aJd(e>zGyJlX+=O z|MqR{@pNF`l5UkC-hjQ(Pg$oOu0G@Sm-wcuek4AI2l%E*+R1&1{LWN02BIBucQ;LD zN>3p;=QQw{b>uL9^N?;SoftU}H)au+Wm7qZucVPJod(Akv4iQcDKjQcIJ5Y@;iHD=5v#Q^6aQ6805xe&mXQ=YFo;^F-H68alysbLr1ajw)*;Pq#fvva zZ=!WTD^~Vm4buIcU9v3k3zUJDTk8N5K-Dz2Z=zfcvK1@^qZL{+qVJ^`G6$gJM}(TraCh6J9O7ABx@ZfD_l}nN?Uu9 zYA^c`eeZJx*}!h4cH5!aGk(@vo2KRP7je^!dml@S{9Jr<1eEQ^q8; z$&oQ*KQyX|22|MMQ2HAYQLyZ^DN|=m!{3~iLZ87FgTX1|oS^aVZW^v_+SC*quC0E~ z-*LBgN_Gumn!oXfCnr}Nm3`{!J8$`Uto&3aI@SMXCiPTi!BcaL2fpU7+H%tLzMj_q z{Ks_vo!kF%(%b)N?fb^`==ax6KIxzT)K2h!`uv~&{C3|-%^&wKKl4m&(_im$YY~5f z|K_FI;o8b3kF%Jg(EbtvC@zr^gd7$H1)lW8LGvgg;1()_$`1?`=Lr0+x<)=#7XqVri za}hr4k~(gZf;aF}$1$V(ka#8%oMSG`ubjg*Gh#%pi)gbLMiLt<1@^Xw!3xF2I)3D^)#lUP6@Fr|iVyM+?)hcNub`{ge z!%QQl;<}dYBnMF?{X>(ThKDMSmT(-W2ZvHs%FA>q#Dah3dMhXtKZxSN8eVku46~4_Jg_w1KLQ_7NkBc^_GcahNUbWTNhX-kk#vlgjr!C7;JR)S+ zBbo0wnn)X7JE6bi9%BN3_65`?*%@Ebwg~Gn6MOuxCgx8+=cB)#vvKNQ`E%l-iqbLd zYY$?~DIg}F`qUQksV7HtXoYJcpT}+MnJ)PCYacjK3qyU#;uh`;;B{Tt|ykmrfGlM6p- z=hLxY&VKTYyAun(vS$Cx{lA>Mea;cAC!}nJ&!g50AB+4&arP|sSVu`7khz7@R>V6# zCE?^(S#@7zQy|w=-<$oN?LDM#gmiEJuH*@6TQ)(XsQuTGieH!qH$khY&k!+ZJr&p_ zp{L+k;m^mwN1r9zgIGd1m-8sp4555Z?1kx`W_Yy`AyW7Ju=u!y#JjN%J%=V%`c?eg z_x3v4)&5%TkJ{C?cJ;mu%`5j^x1#B$;b(#$ZGqvx=)aiP7RTb+3~h$)zv;g@^4kyO zl)69D={t*f9sP3c0qutu`2K~%7kvMwt4_Y}IxN=#DMs()H`x2=i zKPlJA3}V?*E5qw&n$jLwv==l@D!8;i63=)ESAKLQ8d{&j)Y=JLyF;Ii#{-MZx8Vjr;MO_|)%)_5f#~rYHOw`Yz5f z)?+mDpI$o>)pN5-4f;&*_!zP_`o1HG}MmMzzATfThxE5mQUwoE%-yUky!ey;u5rkh`Q&Hvj~%l&s3{JpmI!voPx zI?nv_1ytViBYuyWa#A@PuQjyd-&!e}EbhMKk}I!VTDx=U(&&==?(?nt{ktCZzjM`> z9$mU<(V}HnEt}u``F|0aW_@!`%hS9bMDInrS6%bY%!f{X3RSL8IQw_A4nJiLw`t^q z1T6{Ri&3Z8fqhv_e4|a%BU4XIELuK1raJqN(+^L~e`4yQzns=F4f=s=G_b9~{)gTW zN#oV`vz1Lj9ESnm;G=D0BYcfKwm}}- zyUKeVZp z58&m}q>U_#gq<*wc#AfdPp9-W3Y9m@nGDV;oO=5|au}XA40~nszs+ZU^P6YP;1Mg` zFRfg;(tX`DO=3@MA-&m(?2FVKV9OrHRaK|aVkQAaM?(~bc-@!+32zn-^<#D6B< z&!OK_IHLcLC{8@mge!0g1bqKN*UqHjC|W~K2AMD^ zRL*nGJTnLXPRPBov(jE3MkI$m5WJ-PsqoW>D!3P~5We~$3OT(l`Jg#J6X_W_bpSO)~`Ax8R^}9bLU3=tKD-z7t9skr!|Xd zE$7!xePf--qAuAkobUQ$!Ixskfn&{giw4i66UKiw8q60gL~_qN9`#~b;RS|G5QUWS z(}s5`@t%|(G@1qesUIj5^mIlGWb>#{7K!R^Jc=t`5&DUg+6}9O_)OBpdo^yZIh@f7 zTB58~YWZfe(5(M62f~f>w4q|Kvp^Lk3r)PH5N|sR2fZm1zm1q?=n1P-i6>&Q;Z!_U zN~Q3hJA@2zJr=RC+{E#WlpcvEli`2)2M5ImuEeiL&pGF!i{3#-+K3$tPYa)Q>x;za ztp$=%;x>Pl0iRp3vXJ`DM0tZZ!|iXyTOMZk>PYYYLgYsazo5f&c+xrihl}o82oGe! z7)T$&Piyjff=AHyxdvvQ_e+vk9NCR$eu5z0{;W+6AnfFF_m`|CVsA6NF0tOW+NFBdn@d$ zrgLGdj@gJ3lIP=<38vas5Y3sh=C{ua+xf9Na?@jDb?=AXyb!4bRy1HjWSKnYm9LH8(Wu;yPgGrS6J)v*G^0{pP}Y&9&OC#uZy1nfv>1&DFjY zI^Lc+%bgsGH-*%9zKKkg3 z&OZI@lM{*otlMg@fL5qY8unYJEW6sx^2MMPT(i{tGWKK$D5V9 zQ!Z!H(F_!hSGzbWZ)uTw*m9_@s}@64w1}+Bxc4u&*1WfE)7Z7zCsDNZs3)In{*r1a z*x9W1jQq#g_J86GG*lfO#VaXMY>n|)lrN^5rY&wWl|spnQHm^3^^Ug01GhDg4gR|M z0my}&vu>k5LNnhz2@s+V-Jmr@>?L?T>uZtR>2;0=P~HqQdA2kcFpTYJRdxLPXPfb0Z5VIXM-dx3 z=G|$&A*-W;8xnOB`C>AeNv2VxsMF03VK0C*{sKjpBF%a;VxXRsJCa9kXBK~W{NQ+j zv|(e!4rwPjF0h8y-Y}h1Fq~|jux;!!^c|3J%G(ohhXUKYw-nqIcD01SD6BfHu96SO z+vCATj!JmTZxV9zcL`4+PDTCp4xT;o8nPGGOzwT+g23IqsYD z&p!jTTWA)O!<2K%b6EtMtmC;5P0-T)gZulr{bzJVdUhtS4lI087wrz7+h67PTeK52 z{fmUtSp7QmXVu>GiwaEZKS_8Nz83R_Pqk#srf)>jdp_pwVuVEkd7Cf#( z;38ya{bq9b$<_Z5#~&HTLGk3iKTPWrAB$lbu1MMBOZu47xQ#hdoR*0=sJ`^k9tv(YM8yv&M<5LB^ONCM; z#o=bY-6`d*qBAyJ$vN2yO5dRbnB%5uWvw$9K#a18QnYnPi)e-5aB8%asa7gOnV|~l zrLe0OYEs-qykF<>UdT0MlPcVyw$w)@T?k&JdPBPqnSrKcd~mKqL@n`_G^orxruHRj zKZus7y$-)b!3!Q9+yZ~r2QN}BGQy90wjMvypcm`eu15$ykn^?AwD1hv>4W?558UB{ zXIt&}KkS3^_+`J(_~3p#SUuGExqg#qKY%q~!c$~3Ku@OIfTze+t7_3aCPv*df~GhaUg zZ{vgLf5&mMACe1xgJjn=G_KMH)xXD29`C>>d~lukwthR>ckOwNzC}2d4yZZq^89q# z%ay#jKxSHdJhD@=J#suNzCH|s3z|RgtrZhw5^I8IN0bap9^Inw(3(T|A(P0R^7fxs(N;UJLC>4u@ zp~&PJT+N;1jxbD)I>$)vJ9unq7G6d$;rp5zb97p!>> zc=BPLZ~Kayor}l<)ApIDhWI}=OQ57W4FZYSV`OI@t>|EYtuz&DMfbo9*Q#%g z;_ZW>&?sJfPHG9ahT|Pn36GBz%=Bns7>U&=PiTXl5iL(PyiPVeoy8O*&777`ro+1J z5Gft}a^WrFmy38EIdFO070=js?9)aRIF7ezM^T2eW}8N9uDO=Z>=^XXfOSUDK)tmm zXo>wPX*TTgVZQdP1N0se?DGF1IO>m*Php2GL^}?F#GMXFqn#v?i;KIpgF>|9VG2P4 z+bP61qnn&)9JC->M;n_Czq5=3mr$Yrb?xpZYGUvBGHW578XL(bH{5nkOt_N@S$J}25!WBy9Pc^vmUrXezpkjWPJwS zgsClAa{HnUk)Hk!Py55-XBH);e91LQtZh4{`Do~qyPeM~xRl?WCfObOmh6Yl-)PMM z9KEkRxV^wX(8ABY19%4>;YJ3mzZwtwce4Fo6r9#<*k&mMPx;`?YpEZ-3QqGvdx2lQ z>nCIe@O|)oBV8Ne;|VPU9qBXH!P0sUD#w&)+4xAfzcQf@X@h~~BnekO;p*!NPv5Yw zeVOE2j%kI2p9-6u6>|Se&|+IcwdZ~#y@Q$dz`|ZV5b4?Xv)G>(|5<-Rj!Uk)gb#1g zT&})H{ydF~*SGzK5AL(|jEUs9wuYF<8>@a*>cAH07Nc(#3C;gNI_JleEP!Ku!l z>Q}vA*YcluJcAgm8h^RP(`}j$?i>FAs3_Yfh2KWwLH*0A3NGJ{qw(NQe2ab)*7?md zReLGJqWzZ13QlyBG6OwYL)3Wb4cRx@TgB@a;qNN=5WxjcU)({ROMiiRI&dklbPaAM zTrX}?-+{V|wHwgy_;B&OOIK2B!l-cZxTH>aazp-DsWm25Pm|~^>?PCVGm90TiS4gd za60dnbAax`TTc!-7i0Vm zZ-1n(rtLZTkxNcrv10jk=dSo@=u$U^^1c}jW_kq2DMMPOQFn7A8MBbKbCGz$Nm*4b zHk>b3rJVTe*Opg_#1hVag5bIHtb(V+{#U?X{Fo0ebOtG1`jQ!qO9UHK(n*z^_&cV(JKoW&lMxGgd%K$xGtapk*iSsYYPeI-!0)bq( z6G0{N+2{+x5rKQh5r2I83|-w7!98%)aqG>uD9)5OOkKylYqOlG8_9Sst_M+sKb6a9 z|5GTK(IKPUnbWdv%`Wu6xxsb6)qL$g-faK=X8d}Re!0b9zM0FHP@U2;n?|r!N6;vu z8Nqz1mK-W3<0D1AGU>pH!KLof$IV-`Tkn27INkloVcK`ylkRPvbnm^v@3v_5HBUdV zU;OAvBTU0I>jCt5;C!Ot-Z7$l->)To$yUKPkveMt74em7K#FzpYHB^u;-9qHf)fK* z$@XG@K|T)Kc=e{Bu&IQfZQ;80s=eIXihc(%GVb@u@kJe94Br;%Eq!Fco2UH%?+-6q z9nm6zKyrmH&6_p3}3eZOT1oo#|e9)>)tW5Nep*uRXv&rv(?Bd6qku<*HDy9Xr=d28T$l zIyKWTS5=RuN{3Ou7^|z44d42M`pAD%aLGH#Yrp+3v2TU71oe@7RC~e$85m&QUhCiM zsdXOf;g?nWq}-FTjo5A}xEv4lJ30LLg$gd$L$UsE@MMYB9bOB?I48Vym-F-lvVDcb zlVCl;ICto*A?%N{jq~-3y1;8?dy%t(_2}_4FrKa=Jat4C@m+j}bdJ}g0eCxDqc3BO zzv9nSK4$4(=zGuI@wV>)Cx4@hza8%TFCRak#}3CkXjb?|HK$S?DagC9g4ittw3)LI zIY&w7uGy&9+q%8ZsaA9kbIP<}`^@<$p+~8_+7a9kVJsBj)kZYbhqOl>2hP5p&f*~g zGaGknZY6;#Sy9}njA?qUSaOH8XtmHRBSu|MkG3W81$(r;sJps5f}=? zhb;rOjSX!$7R@^~Jkvg;A>E}~jl}In31uk@4r1hWgzrY{n2w-#TNL~W4}xlv{=DuF zl8?er-@Hw>{R?TA6lmvpi|M@cylEGSjI&?|&z?vj_T=p;8=J(KHu^5JFUK+a$mn<= zrbWtbA`=?b3ON`5!clIt<}^!82QAQ+5QIqLcy}b8L|ye^cO-*{MBJz(Lm78*ZMutE z)B-q-P3GexG)4kMNuK9@(M?+CvV) zqtt%y0|eg%t10Cz)uFfW?2Sq|$xXS{{-xt3oNPAhW&h2DXYb<*?)lR!&HJ8Fa2@Bi zkV|R_Pt^0v3BSSkA7nfj6_1DDY;X3|-v(zs@4|wHe!cdj$K3vN7X2MA9;cA-kXc)ok{+^>JW1sebQ#@paop4X4xHl7l}cisKA z_Kas2qGbn--(fsVw^hIOjfdM0UQF$GzoOuBpCIRZje^TP>Pzr^J^he+!*ttScw4_b zUy6*PJ$g}e(20*kVZ;^DFCu)1E;23|uM>T%cR`d5BpC(IvOZiq2gViN|8G7x&w1D1 zx&7N{O)r%AWR7HoX)0uT@Jt_`T#I)7%N2aWICr&TxkNkIb}^oQcq394lTw$kgFi?n zn08{ms}Z&0_H<5@AsY4bTGsEaeIiDvbXCP_34S$=lj*Z;@IU~o$Eh22@#Y8R&fbFk z$%%n0aB6xTY#7Q7@$GYI}$+Ysg7HRFXXHl|T8!Vo@nT>a_JVU=oBSSx* zBSQEdnL3V5!qtVjZ?l|azMR2jczZlBWHlbpmO6j?+c!RN%bl0r(R z=YFB>qRsAJ_ruyI?XtU`7~Asfsb{+%I{VgJuZi7oYXTVquf?^tzkL6v)8BYv{n@u_ zN8Pwod;ecvyY(BtYhHfs@7*`f9t)q_{LP0Vr{8nht2cf0I&F8Uls6JN7(XLTrD+n$ zyq;^mdD^dcMW8Cj)+VmDmFG{ipg4%bt|)`-$Ug4pE=^ z_`MO43x=DxL&x&>(zmIfogQ4|Mv<=1AS~36!jmC>?}wLzH>+E^dpOy!Nblgq?+Khb zpyRA@eN&q@zVFm8pK(ZJ*{*fkd&hq{|Kz2=U^`B}H*m<&|5D->vX{d8k4VNrvZ_=2 z!8E~Nx|nSbtDSGYo_!3j0yYH~JSV#u?uVD6;3eh@k4Ky*NV_cTuNdb= zzV=p&4r?L73XhQWJ%pdAq~JMfFX+$@?lYY33oxFY9o*hqM_cPLAn_vDkt!+dAm>RNNiHz5@R zwwfD%#xmX251$^o3Y}v5ksN^!JD;0C59L>gd90qS#wF!KjBAHCE?T<`qMwk9{_|ve z@;A6|G2R{jt>E&_pq+P7zeM$kV=dnEHaOEy(&@db{TUq>D7om_b^O1MogUXcTrxH( zcv|%KAXkGI+sS_H{EHS?a-<%J^bB+y8d!3N{GNe3A2#8nIl@lOmwl1!Gp%+huR%l_m9dYY}ZddJxk7xlsVD(u`2U{QD;5Ph@-79~KNWqS|T zrUwGupVdV_(kCcFL*V^AN;4X~_hxSl1>T#oZJ00ymJPuNb(TY85Ikks+wp7P7|K3f zJl6+jJi-R=pC#cr;9=ce^(~@$-!CMbWSHkD)|(x-_~6XP;$NwKu_58g9}#QCYD2;W zKS0~!wbY&@P|=F!puuT>1iR9VaO~(q;&H z7F%cUL{E%U!1pbc?TJ2YOLoJ$KyE)nv*RcG6_6t&c%OK?P-6d@Hp@k)c9G3=p`-G z&U#(K$!_PVJ@G!$^}<5>^aW_L)Rpg0AH+ADy?mVrE8*!@AKM>NeaN*|@Ok?a5>EW# zzQvrkKcd?684hegg?d%;az5HWPP(Z~M0*5U)ZVByC#aj}YeHgdJ_ZfnH4xi+9w3pY(y-OaK3PUwgLEf*0%F>w~i$ z+=zC4fMFSW)a^_=kF-ECMa)n2UiY+s7sm2l#lN#hZ8D_rb@3p(6Lc<57whxL5> zgVZn0n&C=6*Rsuhd;6ELI)eY`UA1TWh_!RV*&54{^HFNQ29Yh*FONsq=;uB9P&}FE zCGf0=6sz`(-+`sWIQ{ftJlnoPaHS(;*D^$heZdLiBtM<)BkNB1g+CB}w~Tkk=lkcj%?EjZBSLM%Pp2vYXF0hqR|k_gL=M=I^y%Xx~kx zZ+hjG+YLm^M$@*gM^jV))-6W0udTb({qmVwn{&^3h)Q+h4F+n{%o-&ZrJR%QXegb@ z8QIv(SCWw_?QZ$hzu!JQb0irYZbmJ-7Z5hB(R@7{weYOi9lct#tx*Y;$G+J%cX@iq zjOfXvj*GQsEHY}OJMD;GkD~HxC7?R4Q$mIX#RMszStyo^V11HK8-N zf74Ci7IQ(`o|j=8R@%D*+sDTjhL(H>>ncea)gB=GZ6SAC)a;?xP2pJ2^)D>$OdYhaRvDpT=9wXWk9Q`9gLc*VCC9~pMdsFtYY zbJbcQTd0?fYOU#HhVVS^a5#=AUncKHkv8JwM(ZdaZ{@XN6bG-Tk=W~6$!b1|7k|_F zLMoAnMGDBI$aki*(TIi18hAf5j>^2zq>HzlbGZ!euj^J)>RuYYfLfmlt#xiDY&g!r zVLRTyOAS9GQJ4HEkqs5Px8-mjoNa-yS-x{{$_Ya*#NInem*`W4C*QJtYro`!vyN@s zFrmFTD?-0}z6iLtYlSz7F_FDjb}JHIpZDS7wQd{xKdSEub`G(2FQ!L$*dCL`0{mu) z=L+NvU&p@RbM0dbyKr9AQ@ZejfyDzl8Yr5;UI;&B|DgOH;q%!e2VSpgU#7WJRsi0X zY}rAX!yx)sISe%3if5N$r>6Muv;7k7`}{m&UXY$d!khf?g0~I<&o#dB_}cF`d~jZG zw*G|hu-1wB%6SQ1@7bi<%RQlhZ^KSf&6nvQWM;dr;BwC|=;mNmlkFv3toPfNNI2a& z|ETA?Ob*)*Z1LKsgdZUAZ-fUY@uWe21F;NVKh|zma2dN5^w}Qv!L62Tzv!)tv>q@W zXq@niJh>qLvJ4CQZ$BXMI1JYjTfur639mlNho61EJvzaC@<{YlIykuU;Py1H$>ELQ zf}#(vi(>q{y>*fHXDxZ_dg+7IOWzy(ofOta_NDtNBGvQ!?~V>Ec@Ku+$uG;J;M?Xu zseXL^1LWoR5`F>b!Sb>No*-`HmEUaPF-T6gt(_p(L_gL;!A}QNQT_5fWWRs*!5L5K z1)2eQS-$qL0i-dJ@YXp#c#irN{NLIs;iNZgtAszWd8%qJ^-Rd!7LPyV2eLkGD-nL> zM;W-6#p4us_PkHxQSF5g?T2>@S*c3+L>h(re(J-|{qp#iZT@~>-S~k84=(wxw5L30NxNyPkCgDs zwtSf8p!%Tw0qd2Z=@zeVq7loygn!xBH}g}-%Z?ceF3(db&k1=-&^ko0)+oC2cW8ef z>2hwM&-$Q|7%!c}TEx?S{6C;S_KmltkKyfS`uh14$?J>TK2JPt`KjJKxDSzYwHf*< zX_BY!2wmCg$t=y0WS`|p@M*{IRNr3i7SC==YuzQe0bj-Z_J4wBGQ@r7 z$D{Zxg?eO{cGgG+6E#H>rtem)uLsGd7!&Df&(`GJz~Xat8o(jg%aKmz1Cvk@U(`YO z?%Nrt8+B9QR-8|;qs8|Waj-BCr_3RS;HBY-=N6tr24|SEeW^q~Zi0!sc5v8GOC~gL zzC5|$a|-$cr_$a*1$oi4j+ojpcXrq+n7F%z*CmWZ#VX{oaE24f&C3_xz4)@B=jPqC zXI|%Xoj19Ee*LCU+f8$xo3o7l!y57~`ud&p^+LE8=;YTE~TEhCk3YNJ** zX&HJx5*r##6zsecD`ksWEuO)RtF)W3qiH*>p$b|&JbPE|+=UAkOuPS4_r&@&hjm?i z&e(rgHg1}CqIOy|fd`}v9Zkza$>DNmVx*X^gf=_hb%MbrN(SNQ{7#&B!?C0LdRe_t zNu^K;AYY_n&%+uXVJ|kULN#6~cE-m8^;CUmXgD%j0W17|3+W(k*8GuJB;h{WwfS}* zT-lt-U48e_w`i4lyL?W_3H0gO7B4nMI$LI6gl+kn_sc#gmWRF(gQPsqkRCu}{?i{o zKLc&5AC_w&@0%-R?9qP?-^-GIlp>J1u5I&3ektyS&nVZ~qR+&>?Zr{wd~AY?b?L>q zJ~-PGVOv+#!4G-w1UfchlAAt>+4^9cm#i8 zk#MK)O$itD!`uY@24VZuoLT?H+}571;I!6YoMd;A>+>0~glLq6AN`fE73nRN=Z!7? zt$t#pXYk_x99uG5$4V|~$g~pkE__|#lkp_c&%m_`F4sC?-(S35!Rc&E_76Y4wWpEs zABq28ReNbe2Vn1fafvLiE7$|=s55td=xd)NxsiDAEgnCu1H$fYr3AOp01Z*h2f=qn?Ji^{50s(Baz->x@~Mx z5VW2B(yf6X1n|mU{#ajstcx$6M0`2OuOdC*aX20w4_ZUkUh2cc<6DE(+($3c-Mw_b za%UzKSoDC7INqN75XTEFoUD8Nwb*ya(DycwSP-1%l_R*2F9%d&IK@tx&VmkWFY>i# zIpy|UOpUO!LgFHs@MP8d>^g~0@z}|K3QW`HwnHsO)Cuoo8 z=_@`s%l&G20K)okdpgJE@d|laeSyO7$?y5pp0rnull4H1bI%SSrM#ZPmzRS6qza{ij`KXRTP{nJKG1lodrx9G~?!nK#=bJ$rw) zcx>?{zI%a6p0F>3^e7q%nln$=d+~;Y^myZ|zVRX=5uc zl@{xn80*fzDm?O>qwh7c=li|&{W8?c+Zw(-(z~rXjy#Yh-;MNazd!Syg@0f2DV_(7 zi{(ey+kubx@bS3Bdit`r-zWYuuLOVB{9Lt{aj8LAW?x*Lbc$ns0>8@9&9N?n=1dRI z=fNs3*M1%o(F8ILDPgz;T!OZk9i-=VU*AH0xc&CKB%Julc%(cW>T6$NzUBxI%>=)w z=1L0hQB!_DW%n<{O}I}6?hM?ERqOGT*qgx@ttai{%0eAf;FV~km zR(|7?D^9-kvQzIm^ytbP$31P`_1MuHuUmbc)^Wj6)=|sc$E>N#tYz8-CqFhbxIT0C z(%ygVedy35g3VKofA*BqkM2F;lp~I~`sj;JKP`QG@3U7wk-7QFS3maoTW(o!`w2&! zcFWRP4^Qn~`p_vi2iu=bKa_s5G4}B3S3KPNr)N*NdEF65cRbtiso%bObNhdyblsW< z=$HGo|CilKNPls8&qu$aEjnt+sm=1LI}d&2&}CWeGsO)@9iQ#laOX#J_nrLeqnEDg zJMPBgF1_?8$KC%(wpq^IfBvI)PTlcp&%f>HIquEPOTO}|`_j~-jyh`9hre=}-UC*% zj7h&#*zJw~P9h-Tye0`dCHKBnC70|=(z!71mHnIi4*4cUS_6bmZvUdfMeB)LC)b{= z;7Pv@IaevT^ljM|bnV(fy=-}j`##9F4SqmA6h9xK(ogWcW<%ZN3AKO?+y9^8+RmBHQgy}_6HGvT*m9hA0biW*x|*d#Icf!01k z%7j=q22YUfh0kMp>qaT0@JRnp#IqfYPK}9WRNyZxRB$;DF;+2;B5XC!L;5P>J9}C_ zuUzA#RDDU}qBVop2I-@hC%_>;axL-pw_?ozT<&{2)tG1wSuZBP{ssk4$$T=<8B){I zC!!r_D#o+Rzn4*R0KHu8!^7hg^IAVD;X;>yr-UBCu?rM@!rHoajfCs)*@Z0srH=K; z9w9hRcr!Qy-XErW(=+f!$Wom5o{bu!mjtfFyN=ffK91Gnfxu&dzQA*Vmjn9)p9^3f z2siQNE?k;kLKpmGOiI{%HvBl^CA%K(5YeiMpCGD5zo+49Hdl`lYl3fO5bJ}eDkA@5g&(pf*Jb7%`N~3tBu8#*IR=nm!qlvhg zkEaqbGh;;~X09A7WFjcYTF)om{z)Fy@6zs2+QN;HuGu*)9>asNoylNl+bE8)-Qi>^ zZ|as)i$_qzS{qHcad$WpEjB8sO;E^^-GJQ;kC1KLhbRX2QLv3{PspBH1TVom#C-x! z-_7y7h(inbu8L|;dZ*eCdUl`iuwIMyd(Tqsv%(JSM$`-YNzVtR_C^cO-Un5CiRa}! zwcqtO1($hL0{;tdC^(&i!#?bShdBX%R;&dt9d{6XLcbeVDLj)=mnyj27mD^TKc(P?n1kr|rJ#b#eS?70tqUnDX@C0- zlNDV0?xH>2lu@$flNEJO!WbVB<6H&$;AZB}eC>7OCEY8b3N~-hr``zTD--$i^r@UzM%=%m%=`(mBKOS#&!#`9_7iE{>X+_=(K-pt&~L295`!aYroH?*z_-hlcz>poSrmwA$6Uj2y1C||=l0H~tCsWBs31d(wMlCkmdw zAnb%1=PIu~`Cxe(CucTxkxU>$2k%DME)mxdV^Vpw2ics}p5I1r_B{kGC`(zo@j<}%?NseEe)_1K z8Fd#)w0E#uP<&%u5P4Y>=*ILBblV08MYZ?YQo{-%xmDJYB+H^})-naSpEV!F_z( zKj?#7t?{UwBI2WuZWD4=#C#z+$a5QF?FX%t{Zez=da|z%=DirJs^KE+4(T$}dUZ<& zCdk$nSYpOcdu5&jxA$@!z99RK`SskF17o%12xrJGUm@Z5@3we*Rgy0s{Zzh*!S7d) z9ptg@`xiAP>2ruNZB7wAHlM9zqo8bztWhKLC5XPtzTquP&lZ#1u%E}YnpoF{#>V)B zjI8!z6!e{9i|3T#qWF<(+2Y+T+o1dLy!biQo@|NYpQ_D6V-@`S1#=scuJCieVmvCx zN1bO2xH``!xZoc!AamtfA0D4=rBhCcU*?1fxp^TW;gl03+A}ZK{6w`+gLh)>DWjG` zQo(8bYF-6P!O7=QI@jmdA*|m+bL-m0ajd>qRNs{rJ+`;j zRPrTSbUIY_t`Bcq@HP*^vlxfZ_{@I8s}tycYzPm4i=$tD%Hp3`jEiGm2vR@ z_Mb864yo42RvzzHqvQZ5N3WWo;f;Q49h0<;o^|5u-vI*m*i?oA^IZZPdZHyQ} z%Z&z6N5{#>%_Iu)VJsAr%8(olo=0o&-lOICGw|D3jtGw1=yeIF^SnIKw78n}=`q!w zvPr_lT1)UJ2(I#+X)jWw`^&{=2u~k&Pm=F)kIHb7NfR!C z%aV2u@xBPjLd^MT{^n=-Gs-Qsm`=RcEOU z)U0x0v}6rwfuS1eE>v#M~5rn6iQKb;u-eB_skT^StanM zL^KwUW)pf6H7YC6k5pLaT*2u$o%2&|UN>r0f@~@d3hU902T%D8r>TdZg8${pX;jR! zBW2rGAwFcL^aJHWFqKADogOqClknHqA61M;eaB!}?o8hKZUo z@p!xWmeUcaWv!%T>WPA7+Xc6rG3;TmA%*w+QVHlsp5-lpGw_)t4}I(-pnllL$1g|y zFdeU$O-8)#19($}EMA~}Hk&<^hn|TS8SvZ!NM)FfG)8n$*^T=;5)~2d~oR zKNXp#J*@4$%00h%p;p!srdxM2?tSe;2jT^;{9o4Mjus1k+gU`8ewz*jakM1HT0jhMIIKfg8 zK-DXHY5g4TJ1ggvQ@Qm*{WgXv?uUPVvHU;XpEy+>TuqD zXz$t~ZC$e?_+ReB|MS4FwOe@nwQ*g}tBvR7*$DeoVkQ*#LS^&gB!wBPni1<%3etA6{oOZfWnYdh`=XCf>2&j}puh891z_&(edOGIeh zCz~IkK6YP#IeXmOCTUJ_Equ2A0MUbbW4MX$aLUbCMf&Ad;&ujAA(P2`QO_hJIVgW7 znTo~qJRPd&c*V$xyOm}VZ{=Ayqrv^e?5OD$P1K-HX3@-!*OCdGwTx8a;0MW~kB5Dm ziL`)|UuP;lh5P(5H%G}T+V%W(F1Rp1&}W$zhPUVq^; z%%4q9&roVSKSA^MkEw_=e?(dHLZ+tAl7G|x`(8ZZ+S$Pi@K#z9jKwQ+C~bzy5zyUHZq1KSp2Jf+wAVt(xs`TC(gO% zv^m$#S$^UL7c`rH48HHaPdERlJ@}#d=bm-exo0L4y^9jP3FD-TFLuA0?{3zDvzjPG z>i+b`i&waH9N7J?S-@+!hT&H52lJEmj#;Aj?&C?1ce6K0Hj(8ci5Pf`ryH0T?_qg@ z-;69FJ#ePf#!FHC@misgd`^oskashGa@co7zw-D^a z(><@Z1Cq7o#RedU9{^ zuE0;Yt3TP!Zk}IocPw# z6T!Xk)(*wfyu#&u7FM6NbUWO)j_|>U6*j^S91XvG;!YI1QF?5cxkP`2d%83r47B5N^2v{KquZR(K{n}N5D?zE1769ZoBniE1hko zGr@$@Oa+G`op{17+u2NNWj!6nfw^TRav2BJ@Xd_lcE(~xQLp2H$6yrim5w?!82yZC zXG}b#h+*?On-cr)9kd3AcR+&`ocHm277@HOF5yMoU*a6%mlxDV;T4>#56Sjv;K@y% zT{;Oe{q!CkO1}oeDqja`EhM~cOFq6?- zXD%Ppn;-iBC9c8gF*q-=Oe|-|^mxQH+h_mZ%WoQd=U7T9&heU)hz6j|#pBcHj5X4` z`QGtQ=$~0oMveOozyHyaWxDv>guW{Ig4}yP1??LwlDi$i2RL?Uiq;%gmU7zl&hQuh zL~vLl+Ls{S1y6GURycm^(z6K^prao$3x|g&orzX0{6N~M-~ugY0z^+kP^n|8QsHD% zS}v`ZbRFC4p-6DJF&Yb18=cWuD4QHQfW2O9JfOw!N6jsmTDjaT25nR_?hHmF*cg@* z>0nMPL|m|}+!=Ax;e0Ni%VANnE&NXDR>pN~8@KD+Y8{ViXsA`L z22ZwsiQs#@eG1tQ=E?TPL3k$cR@KrGJckV$`~|gl&XfJ-ym4;-GWWZN;NT&}y&Sw< zN;c&N@=G{wq_pQ?tKwyb;rYj|*{CHC7JIe@xyGDnOre~a2<1$lIt#BvDr3X1{LT}v z{KBW1S<)&A-7>)GbfTC@;oX$ZAf84EMc;~sVtD=5Zg#d8(|SIMy?zo7N&rPFB2n1* z&Ug$?jETo)QV~0vMH~xR>lrPR)#G@AMUTQG$(u$rn@yCf(R$fUYbCn?qt;%=H_WVQ z;_2+XnK$zI&vYZGlWwBo1)d7A9V?&HQ>knwl`AC**=jbE&!+GkV$4cdW+q~H>L{CS zN1|5V=tS`m#pfc$Qi;Qe5qtYI;v2_gx2+&}$=jO~yhQB@0(qG!`6J-qzl6iPVEnX? zU_8$G%r``qRllsmqW$2K`U?({Mcy}me z$Fs3$%)yg{$&zlDvL(dqu=m4bGI=Le)B>&(*AVKo3{;;n;<1Qprc3$E5d2a2$TfWC zj#SfO>{sy=Z7P{Jb0i=+6G?qd+>kX=Nj)3sOxJ1_+zQ;vweT8GCXUrBn@c0Y8nde@ zJl>p6mGNkS6~R*lsfv#3`-Y|Cf80pfls1HQc`xr2g>GWMe#`hwydQiU;$<@sEjtM- zyBWBJIU%$8K|J-FUjp!!_*M;7(6U`N5_VVa8bnJvGGWh9pb@^ILL!ntL*a|he(?BD z1n=22cdqtd%|+}Vp5NHJsCS)RBv*@N9Ry?r6{ew0cf8B7?^&dU^Bgb8Rrld3L*d{)Q zaay*QdF;4!XpCo&z5YHXNrYN+p2W9qpb( z_I1`I+IYVmDC66#b)^uL!K%ipmOknh5ev~en|3H4LII(CC0?_VBW}^agRp_7-AvgT zCvCSyZ37Q&gcAokvsuKlvzk+|3&mn^$ax#2P#w+;S2O%4)0}+PIO2qLntQ(&Go|&2 z<9Pju4g>DR11VmXC0g|(nhdxXd!@J^+qD7g;E<;cPDvV)4{xG9FvG6F>ytE0i(JKi zIE=u9qod^rQ#h95p$k2-FH$#ucFYdZOSnk?$S)bOUL*>KV_j> zAb0olBejE|(iAeW+oHTHc5N*cAGNC4s#U2OqjnKz25HwBjizF$Vxm!jUKFT?6@2Bm zVH!mj8$JzHtqeO>)Z+*?6tqePkJ4xG9DE{|$g~?2z_wBEDeYErc)YFmxb(xqCuR!)?v>1vsL8WE!c z4+hU7+70$dnijzugXoiF&@7hv5(uo zgW!WV%Jw?&WLUlm6Z)k)a}Lje*T+8Z43)-NqG1Q=eS%y z&aEMj9_>i(M8BBnLL@?-_uXsh=G`A$&W2EaBHGdJvCR4*s%}_#FyvM?u!7> zpAk)bwB3prBg5%_;imB2k(Fz{(tJ7ap83}-xnpui|6phn?nijCP+;0Hjkm!XGMr*9 zX&|IK)Qr|_^3h59Vu0`TKgZhtcHe}ih#ZT?adao)tOIXNxC4R1?6UIqbZS;G2u858`VE^2u`k;x-AVIag?ITQ8#aThqSw4%y4K6@vG* z^o!&i_ywN6r(}DgwU0(G!vEkokS#D<<1E}Q;dIW;diU}pjQ>dqr}1YPKgn6R?+FPf z9%ML%ekj4oQb?N8I$&V@kbK1Lym^rx`SiN)aoL{W%mXq0_18-HC&qg^K8-u9D?f9R z+ZCAF{nN$0s912_2i}BSroab-bhyBM6J@z?(n%k!3x_DU?Au|xD0B#Mx9icj5$=PZ zM%8F20(fFT#MdvwJsyri;NcSQO&%;Nq6A<&oG-c=Lt*w1h0OTX#1Jhr@<;2&M|f16 zQ&|pLWM)lz_s`m<-}|Y1RzBR=5IpoD?WgXnhnjzW@u7!LfAS%%cjmwT*D0ggpT3W4 zm*4t71JmQ$R~k=iQ{tP0^P8`GKkZA>2_vV+Ynp*7FlHoihlo zWP;?E<*UfDQe>Gx>@$sAKfvKj1jHR*c+#iLl&MHKJmVK!aD+BJwphbHI5?fo&^UJA zjZ3=VGeYzMZL~;FrJ@F;7RXkH5dTc!EDV)2%Z3|k*7BO6#T-3aiW+!g)lF7QNV%w) zMF%-ddAGCO8Xa{;>j>fx6`ew{Y!z|lZN*|yqmrmQ$Y#LX2p(1L)ly@TZj#1($QF>oQ|jQ3T@(@g`ydA zO~_ALH>(Y)9mX6(e z;gfg#DYT`TTD>|rqxm!UO|5^M*8lnEKeqK1?F-Gz+&_$IU(@mzxL3OG{lWLO8*Z8Q zbiB_!)y?hO_{s&^TJ4~%L>m%TQ7=Y@wXxP1BL3qEl8?B%l`PIaf2&zY0G;m+(0H_Yw6 zLBwK69&>>?fm0AseLIhpnen}&i>={AO`ak}mU)~+?qe`+;w+?usD5gQp7qmGSvS{A zwIw3Cd@>ia6Ujt$4B0k_#6e_7f>k#V$`))lFl;xGyM)k-)sEMP4IMdO2ErY&M9#_~ zFkTu(yxzjDGM>%n?0m6^$9fwzw-Y%|qe#vPkj&o8`bG9<5l%q=H<^XV@*+v0CGlH^ zDB7k|G^3qPHIXdz&zP#_ruNil*X7fxrEeVwu4>*CS~Tj0PH5g6e8~NU`@_hd$K4;c zU68)gy<^?u!4uwDgxzI#-+0e$%ewz%(v44N)~v}qea+;jyRW%zarZsllOewB3sXK2 z*%f#p@O4yn$m}Y{JKo9}#gn)1my>NLz$|toEo~P=1UYx0ZwZr!~3zSqCHI?%#=1Mi^wr?mVWzUOjD;%%gfDi{L$cC0g)3%v2k;g>%Pum|To#lRgr=AZiD>{q@t!f?-rPhl-!_#pcO z+dUtV-~ta?;$CQ*4^N4BKM1+Tz7yY4{rd2zx9F2CJR=zA?#nw{?QQaxY5b@G^%sRF zA^a0Dp1#KvTo-W@G2bn&gbRNLcuEU-JTramdHlOUnS;hz=6xskJMeoSo*a!w;3=JU zFnoabmFqV6;9uwQ6TD~J6x43qrsKndd4u{tt9{>Lzgyti=AD7i94x}M^-DBX`jqjI zpI%gbbPfENXJX3FNS29yJTBi|g$eK%z5NySk>`1HdsTOj_zYjblcP;ftG<)IzQd{( zmpErpblUPK)t>hG5{}$sc&UmOEEke*_o?<0zo56O^+0nL^ZE~(Eq%&(mW_9JJRN=@ z(mU|G_XOrYt7HGZ_V&P<@xPz;H~piqxk6VH*r7~CmhCaZ0!<=+2IdA%A0kJT7^TE0whJm3Zh( z+~OFj&Tv>XrU&_OCF1Ama|y2AP^0~6DVz}ciMuEGRJNzR9roQ0)>p)6Jvfc0KslLP zwgbNGGg;JR2rPI*r@7G_Sck+MoW%;4gm1^mst*_SU1lE+F%}=3`uHZnMIQst1pWRZ- zZYkVVjeJ%v7DN^5v2u2ii+=<*p!nI@|k3_62v>rM#eOZ%4m87Cukwlj+lmSM}iLC6+nts z&ULb`oh!JuWe(#jj&9|Wb|Qm&7Pg&98AdK|R;^Szhs}#_Cvo~F5bh~$5Y{@ILq|1-h@9nb^76SCNf#;2!vEhN;mCNCL^P5 z#-#bvNI95PNPk0K4Wh(=PMXm+|G6ng1}}7faftSUd);#y&LY3J`B`^+M!WQG_eQNc zkb#W15*+t__qT-~Xy1Ca z@aNUVPivF3qwPi@nvWqf$AMNwI@@wM-3~Of_(>uQn&+Da|JiQP={@SeL5mnpGG>!} zJBU#oBx5;}vHdvVjdX2k(x+Qst)-4$qU5iq|B~AS3$N1!KB66ek7YBw=0mcdBI_OW z%Q=oQt(S1s|EB+$&_DScHp%ghE`qD^68;_Jb1?p%9h*KDSa^r8Z=TD*B<{oGHQ{n- zymZ6bhfC-qY{#Zo4}#0~dp@_-ToUc+KP74ci0! z4@o%LIJSvotx&fOdsQEPtTlATd=P&2O;&3Y;6>muDF(L-s~hk?DDmW&$G}6B0sH|8 zzYuiTO|^CRU(_C0@PZzQ^y~wg39?%xIhioe`|za> zwmGkdvp8ou6?J{C4qS(P>yN`ei<)m8GAjucqsb7{AJ~t>9AmFawR5=G^u!O54Kq!c zD5iR_uRdr9xG_S@eoQZr$?;LaxHFKSu5rkC>lNV?jNQad$c-s6^KbC zEG><^ZKMi1hK3Mex_Rt78MU@rBx>TJWV;rPW@3&VPb89fQoNdoT173A7;^sM$F;_% zaivIiaEegK;bri0#>r@rwx9oRX3*m$>wq(V0xwBMi{U)Uz*~VOGj!<3zH#!ckrPjR zE{CtdEgDg&FNJeJ@z%~w;1IT&%#|sEE5QzC3RO9xQ0+T^g!Bh^QZkr}Oa_E>#h*xG z`bR8kk0y~gQmDkOT#or-vf7QJg9J}h+WvhDd=lrp;VD+wZr(J7*{*CVL?vMWN2mCAe%W5s|q~f_M zPRJwqVGYk@7b+2_lxPmS)p*>Bz2oPFB7I(q#gQXFf6jkBf5E<;TVLyWZP)DQUzzvX z)?=Ul-2B(Jw%_;9@rnE~=!$dl1U?ZjY_bPbIo4;ZvVgPSy&1bo)Igm_pRRw2^^w+? zEZJ4Ab}c{ayO%$+*{0(&r<=afcMq{#+K@8&-)nhoAdQu}E&Yjgf1vhCx5b_#13&R} z>VI%4eLAQsTx>_Faw&Y;xe}LHA8dT*M1oJSQ+ev!Ipu9|VOMxPSq+bu@r$@5+UIF) z?Qyz;qe~85*yPV19$z7B(b~_$=65{teDjUKOw5nQ%eV-RdEYH@k>6Y<|9CH~561do zf+NN$+aqK6cx09YMEDGsI}m#iAr?4~__qwScz<*DzBYAww-yg(3qgBm2pL(mW`Z6k z68X!yV7{3T<~Wzx4n?#P7eDR8HXbda{~Q}J4D8bBA4Qw4YE+9w8c!B$5Y|NYWsjXW z9x)Cci|8Ml6wN@GHJBY4Y1#!O`(+wKp?I?zL`;~nq}%P`Vcg(#Gc*(=kvgNJqX?Xj zXc5W3CGa^QBRk8W$ucLQoj5)=|C@^qmXo;fEp~sI=(XcpOs@{mi_V9=@e$QN9Dv2~ z)6GY(7M)I+8i8GyBri^I-VR;O)KbArA!uU!4)#|;gXvWaj%roP#~zD1^=!F43bQ+u zOXpKaY_d~GYo>S`MS;o`H+D)6Uv(*!Tm9?9N__SPn(LhVmkXQ`Sccce|H;dnYdYU9EmuHnQArLi)0pmnWaH0!~b;R05+&2liHMM|ha)@f*EJBtJd zb2OXv&6#WjW(z)kllTb!lF!6(6(>RPKF`Ox5%*(W4=<0b9C%XuLEAAWe=qsNNN?ZI z54mgc2TuJXbTDyDaPbFE{Th%2aR*nY8c8_kgEnasVtC<>HrW1Il!}80EJ%d~{qPPt z-Ba+%9f;@|R=kK^QogfOGe*q1Tgkbltd>tVhYP`aq!a0C#b{iM*NmDwY~f;(78!2O z9Vm#)w{3-3q&(~lmqtptAuV>m2q&|4)H0iusF^8N?OJiDXj|AQNV~_ri~qc&#@~5*{ECi`wf_vC z&3tn=Xf@}m51!WE18c^1Nrzp6ecy#>AmU_QK78!EQSLIT&*M*SPyHCAo74~DBrpyN z&iftFz8@vD`(=uMaF1E3Wd-}BSJsnd@AEqZ7t& z6JEj&33;6p=L357v;7tDK)V*jeZ4wl=y06E9fgQ6+T-SiFCZ)iOXnIqTAzP0J5($L zQ3NpO#H(gz3{lgZ8*fXGmG}o&I!d|nSgm9_CJ5%n$1EW=b=;fFxcPdsR1cNYwOrOJ z*u@NAng^f6-ih=DBbglD^(>KB$`9{Vgq-YS`N8;;eD_z}D?Ac${OM#r_%`8mAI0a1 zS!lS76ogU8w?_`(vTm)BXsbAOCL4Ef{cFrFv`5D5wk&RO4>_Z5pph{Q93jNxIk(t` zt5!vvXL4Q=$%an`_QNw!wC7whAsf4}!mF{fpD)J#GIqoY&h6!#yqHmX{4Crxx)(9D zT|3%O59oeA(%Npd^kN%!VybW6JF#Ab3plHiaPlj_cQGb$KS9#jH;0{jeeF43weRbs z4}?wOXTM3{dC9MjKKy&&J*oBu^0NdVRjoXtotQK0)!@%3v{!uJK16WDOlZwWLOvsq z&{@bgghvCn795@w3C*528COK0T5uJnysLVplChw3z^d40I#`8rb&f^aM&QdD`8bY; z8%f7O3V)J*!nus;>WK13Tq`rInWMoH9+n)&649^?qZ7v)Sx7>{9db~xlW8bpih?Gp zj^9D@LDdd@G?86V8={0%E~N1J9O(W4U5o3ykP)8NUtJLcmr_kB;idGf1J7 zK_Y0UlX$xdJW|W-T|HZ_U}Exa$^39()sULeXN7_UMwPE8i>N z#G4|~PV(kFU;6^Zk%Ub2`SpbD*A~QDkcZ#WD_S?eOQE+radv=T;lP8y|3vtc@Qyls zYYMy|I{=Dbiy|Gii&B)#)d!45Ktalya+zZhRVh?Cdq zTQwY8S_8qwWLzoUNky)57)kD0B_ z$K&r|=9$gr)9;42MSJq=3)IhcXi3Z$CEdYn01QZp6b^4@EzL)oYs?%mD%*wc%VIl zb9`?<&WBTMz#r#}X9VCC4~`5}FIGb13G}nML$s&$$rFyYK1Q;1-J5}aeESRid^7;B z`Z$eQe!sp^FfIgH8)Y>3A5@9pNQY@rgO)X3|+=ghk;$!|v#u z02^7HC6`Jq>`KnXZH7H5`}&kFj}q@jyH!2DlvV@t%e)!uQC)31+PVzdm}S$!k% z%jz2f?bQ}-z7iaNooK4^KssnY(|CMZgN&&9{Rr8?=Uee?cWPg4>GKMye@|92TITKd zc{0fFpDNRiu_M?x`MmbF(>QpG!z(SBLh=FQr;4?n9S$8=96i6|qoCPNM2*SQJUw(C zqg-Z-5ydiMW#mi5%94Gl4n8Oth65n2OrOcE-?{Zb`D@H0edMM~uG@Ljwbwp(|AJ*J zmMvRQzJGLV_l^z_GLPePb&x)NewF??Dfo3Lx|&)MJqMNjVinHWb2x{K+kJqrllbGLvip6sn&PBQA1 z_X{}rGdj^z?<>^4_X`3}GOfno&lB7V+MhbjUyN;^XwT^q^g-~>m|yRB5z{xN#FA=S z9y#ZO%5>;W&krDdV3N(c4-yl!|C2yp+?HkUCd~PG3D~l|UlefO57FlDsGmwK&_2Lh ztIs4jXqIAKz!BmLQ<)Dp9n$tJR0u$%0cik8!|LiDONywG*fF&WyWy>{1Emo%nH;6R zPlfLR2&zZKlcr=zxtyVqPnW8|r$d}X4QGet!C@1-`{7EnK%Ynhm3@Y?Qeq@EfY9V% z#2g+j42^~yJK66JWl*>W$-~2LqiAKaPKcXGhHHl{lo<)x6)A)uU>dg$SP{#}rwn9` z=5>@_Qw-D2DOx^jW}#E&j~?0ax!BdL2F6*ez7cE)sg>XgHpHG8u0$FScH(&L#-hgK zQaKl1bK^6YES+-q6?3N~+)vMT-@Nu;r~SsA7hbaXi3^t;x9qYrKX|zN%uM&~!XHoh zY54i@_18U-12})?aYwkTX1Kq*>JO*9DW7t$c8AgZ#l^*D5yNdpO?W^t|@$dTmZqKUNeTOgpiu=x|mS6V`_i^WnOLIrAIP$Dd zxc>;B`^OdT9>%-e{;&|=K9XpZs725fqX&EUy( zm~!p>>0R;Jo88sRq;qyW^Mt#9+2FEkuU&f6FXXvSV(C(O$c;#MN1oX~SS%t4T*7t$ zf?x$?;0}ke87nExVM%F(3&&k_k+bE0jwm1TfV9i~nEPAx|0K=*NnP!I=Z=f6?&b13 z%jF|qyKOocN;X(ej4R2sq)EM#;Myla&PC1L_C$F zA`5vGqCsd_$`rCCoQ5^@B$PpE(N&p+-HWDYe?588=axMAU-*emvYx5dQOL?^xcY#m zWe4;sPFy6BQ(RI?xU8m#{i4#4Q&r;ew4wB6;yLV8Srw&XW>P~9r4gy@A1JDE6I)9S z9I|cT6pb7Q6K16>m@rH0O*wc>e~X$LE!*KV?3G-{>GLXzr;>D6wGus~1M+o;MGcf=^ezY}>Xs@Er3eIgh_QHvnh+%HgZG z_;97=6TnZq$A^=gH$A#tlwcvl3m~z*1qcrW|{4T1uC9lb|QcG zb4uVD=I>Z{)7-dB6Sxk@$F)}m+WS0@8py4+Bb;JPe5~6J`0a(i!^cXu9R$`=j1~C? zrwBOVj=lF;cr*c>P4GI&ATB$$264n?6Ps;s&=#z<#h>f%e>%WqRf1n|*j&EaCaB0)7(awUKhjs+a#Mbk2{7=J(E- z6guaJ3L0P?#>6=M9B~?NAcnFw8+cE(Wk+oIW&j=-*H+Jlsp0(wq-6w_E(P!yjQ1n*c#B8f*u3o_wU{C=f!jOe0t>L z5$lhfl1YL7*?g%b?n{_J@X#SlC%-A|9iAI}BDC>bc&m`?BH}q?)-)Vun}ymc{QuC# zh=gLZn(UhYXb*+IE#3RHbZ=8%|Epg~xBt}n8voyR_wC3w_is9enF|eK?J?=;?~h9{wqp)}(dpWqeO>Ve3eI%*Vs{c=cUv z{rGs>L}6rFcWSROdZ6~gUhaKE&;-$zFXPvR{l2mtKci-!p@)Y_b zyt4N*y6D=pkfcd3b}$8v71?vpA_v)b=#S%MOG`E=h;{hy-!jF2KgHBYR)Kg?L*PgO z=W7c1<{Jo3Bk*|@#i1$-$VNlc+d7Gdg|KlP=DLOsXjk?IF4=fV%;=H zMxUGxSI_tRBD@^Kq@6Bihs4Tm03vkwy^P*m^gU`TUuU%L2~?cw=MG zzh*qf*TcVOMZ-IE;-#(dqHuRlg5DwSPoVc#TejMo*8}hH?UO`o-C_aHG28=J)$}mo z@F{dgc;fiLy!U~F!8e;l`{L+DgxkcDU8&(?l^yf%;aG;X2rM)Fiu@@)NB^!Aju+s4 z)8V9#=v#k1TKI}>I3WP9dvdBX_Uy_OZ%^Z5^6sTFjmxJ+pFimq8OS?uo!>vht@SLm zAM;+H4gz<#5M2Sjq zZ3l~S;07Ko?NA&|+w84fMO0p8yPxebUu0QQGzocfQ^PqsoM;Y(EtH#1B^~%eLkXu3 zC5$tLgptB-0c~d5Nx1igno*UrZY`QTFlb6OHzzBohM4evE2%g$lSFxjX2CGL&|A#N z7O8GsE}bqUlf_ItnMmc*xP3rLCbfj>*lyg_@~Ge0kZUNCT987DqV(f4=q7SXq8QI4 zYqC~V1{IHHeA$n+`Gi;lS~Et+obUGrV-2;IPUs2pLx5J-&kn3*KyI#80`OYPujcpR zz>g)GTLW8%Y7|lyD_^-q>=s>EJAr%%sz!)tCreyJfjN9NHJDH(_Nm8@O9UL(FdSEu zt?ZF5o8H}%?v}Q@AD+@JKj{ANOu;3mIiFsal#nFc2Rcz>NlPO!?YVkKK6jV-H>Y z*sXtl&Aw~-eUHt*P3pg3-N&xE;kFx=Uwqe%&F?f#xuoIjlxu0x_X1WE&gd1SiVVlz zj}W~56!11UU_UvYS zc%C$kb~`}--7}7yJcIV}z&BnINYsF&hW<7QXrG&-BH5TN4^FOe7MlgekS6=bqxtR% z*p3dbQN#gxs63q#FGTrkO0$fjx`n31)46m!Zl%nUE!pthD~Kj!!8*w-LUX9hrzA^B zl!ecuwrox>AyU_esB}R>Bs+!VQU%AWOKPrYYSqGk20+do3>Wp3jJ#4MpUl{URzKW- zO;0BK?E-?;{Rt_FqFKnb$o=*;a^77fV!u}oyttI16=DT$5{tbQG zv(#VzmOT<%^BJEP$*zJv;qU0VO~6S81lkAgnIiucdWO@{T94NicBTnqqIW|t&1~xj zde?5ZMYxP9czXI}SX%y^1G<$eC^9&wQ_(;4TQSg{$tphn4fO!L6}yfF?!zM4hWV~- z_Xo$!x93*TPm19oPxBfGb#|FcGnn1BQ!~U!KhTbm5nhmbCb*+Hsm~EtrJvQ%drzPT)ILqyJ-$sS3b8j_XgS#rs)j3pmL{y=C{U!%F$M zVEsJLxWvZ=wXakD8ef}jUfvmBYv5wn@u2Un9$*2H-zR-swC2#Y=A&$_Z}Q(0@MCvu z^xseT5cW)GZ0*%PoX#bH23{tb3a%013HTE<;NgkZ+Mw}rxL=Es<`IaKZGJ%XKZcKb zZWQn=^P|9zPyUu>w$Gz~YR}eFtQ+$`*Gp$+?BTYb`068+e??1*)+qOnJ{QXFxNdB%j<6DLgth~ICEwkiYRh-(jAo7osliASpP|R?=d4dRtH_J&Ocx1+JIfv1IZ_7 zAq)9zVr}QOtu2?GK5snh+3xN7Job|3XV986KaQ{YT4)Q_{}>s(+KYiuoSMmxmk}j^ zM);&Z9^zH3Ij8ltr~3UU9eu=;Latmd;40p$lN=%*fX6pgz;(dkV*s9F{i^~_<6$(r zYYM@sH~d6G!=pCxj?#VxU+6@vE}c^%>L8jy8K4OgJ`qjs-(Nv)U>5a6sudgv$~t`! zbEG6ISuhEX9-)HBAf2EwP{C*jMLF4B^fty*r#dP7rY1qlFM703aF&BY5ZuqqbRcNi z7&XC*Nq;=kyi-(UN3s($uoFS5u!Uf$V7Ei!LuEsjCQM`Si7;CXqz4V-T==kL4$`@$ z!NCZw1Y`S#O4E{XOb^GW1_xuw{W!&>$tWp>D|GU1$~8=yK}0j%s#A0g%*{>Fm&nW2 znmU{Xm}W8Pn+)0IY}cPo_>g5B!dN;dAnkOF|^E!*Yws6b|>JO01GhAe9QmMyvdtz?EPNV%$Iap*AXCMr0-W%O682C8Gx6znL@iiH|O5>7C|K67h^ znQ~Pr*Qc2}3Z$XzPz6qlUXY47DVnx4P14PNoH0^>uE9Z=S#&v@9!VNiwId0--cN*Yd~DRua+~? zz)2_~rEspJKcX4{3zpyX_a~2h z@YuP9pM2!Oi=>m}@K8yYLg`cnMdom5yQCB{YDuMy6s$pk5y$e>`gtK^OoP6QGrc63AbGNC}yf<83giBlV~VpFfwQ7NzwW$gMb-A%EpL(=<` z;UryhgVP>;sAbXT48)QaPIe?RxMtEFK!JTaRApP1gbI4;WZbePW;+vYaQkEv zG>p7nNT9}aF`Fr65}2OAujSzvkxl}GF+zjDUmWuCX#4{-?rCE|1bmt&go_5K6j3W^ z;Q>0-=?tOB*GS5iw3uR|TnsMq8I1DKeeO;7wX&r4TUK=T^UqhT3Z&qWdr4`~ z{iXD-drV}#D@(5Pmi)B?CrXRX3foOJ_Q*`<9ruT~NNV%Ds9$rIQ;$u(*nM~8uD`qQ z-FM-K_CE2AZ#=Pgee=8Fqdp|Bv48`zlVG%2*YMmarKHd zM0V_uJt$?v2HX5>jRQO0pghXg!-|N}Fq{A{Vnq-`@xLjC701rYRk@1G4v?LkZ&u|5 zRITl*a;`Fxw{iYdLkxpdksNB)Bb9!|$`;K`deCYnDQFTx{ij?mJeVmOY8nSu2Ewf7 zQy+LWK|a*JV~AIJ5JP3O#LrNxkiXpw&fG5FJNPR7$sq+PDGq*5=*V@ z<}!h&O7WCSDhkp*Ga_*5*M~$G#ZQ^3-tyKl50&Rmiy!EnyXTO|uzTHAbDx-X>4rte zY&d3tQ`@j$@rFC4Z(VvU{xuA`@2AV~U|<5{I7D4P0EgbZa3VcYGAsFtnKrB0ni;q2**-g+9jFv+CW9z1IET4SLR~F>>WNp}Mh2Ix)U#NVJ{)+H;RoY8`V2{x(r~}l zFiS=i7kv%UM=rX9Nqv}ZaZ!YDzS1HzYgiuw(vQ<7;h3tM-v0*N)VyloShR$3ZniQYRcvB^+_NS(l zj0(;$4zrJL*4?T}B3;5o8ep+B`wU1mOik~TLYPaXgt;jAKApqG*!380I7{~%!^RK= zBjl`XRdFagHv|a6z$2JmB%3+V&*p$b+;$Z!M)fC1*A!SDH?J+Ob3&S}*#$(v4ZFm2LILxtl6|rE zB&HLvD<^bC4z@aC3a=c?;4I$7_UTG&Ey*U})!fbv@f%F}y0%A-d_E;y2ptI0_3%N2 zI|m+}MCJ>cOs2oYM+QwJ=!DZPzl^2u@e=TnvD4O{iw#_PTAwy0peTs0obR45? zpi)4%(5!eLWVd6>t%M_jsQR2lp`VhjIcYFx6+K%S#42>PP;dw7(5!>gjIucZ_Ep_J z(5}qR3F*kOr5nGQ{1KKGSBHGGFu7ASrC47c1g24mK3v~E(r?Rosw)DRm63FyRDPu_~nArzPl1042?Q8>nFhw=+#tv4ll`a}5xgfYiyq)t3MPR}M zmLl0wqk!;Q-mSPwK`!axEYAE^VC^SiUD>ey$nLk(%+@k8Id|A3>)suxfsqHzno)Cd znkLuqOS2%QKnEeJ6-;(Njpx|$M$~R+d-gTi+vAQ%%Ci^>lpo3vy4469m_7c0aNA|o zG^=jh!SPPH>K0J5f#-0>i7tCJ@;7!JM#B_v+k+#o&x>Q1Jile_DMZI>VWSDU7csCs z#}Rzb2LwD1eqb@SUTqNlZ;PgRd-7LmWK;Ft!}>vQtRLzhHYjohAlcBr2QM%<_-YTc z!S;ZQSo<84BgAj$81Kcm{&=7d(4O(z)*lfZHq27o4gUGqt&ILCkBsk#PtFJ}KcRch zi6nU#B}CXMbK1J|zn+HEPu|xV$WYQVBz-(OnlrUy?AIOV&v3U(cezhWr>}{)r#vW~ zh?}Htb^mbm4EYXsk@Rc#c=xq4-L)i@q{$CO+~f*rCbQLkG9w&uHcG#8Pl$e`(C?PQ z)0+Pyhj&MsOCxLK!y5IM5BKm1hlj&St8q_y&X_Xg>d$m!CC`&Sx=cC4Jz z{MVE6{hylrxs}V^n@&4TTEBedy@#wk!HGT+Z7!dD_^l5tb|0HQUAp(?71tylnC#qn zGsm33b5n+0a?4`+DwGS z%eFyLeln7f`fL^l#J;5MCNottm&c5=PQ^~8rFz~`RXI~aR6B2IIUSdm4Mua6Mr}E$ zhg6mGrqrKm^vj4iq)Y=GNwBf zrBMli>D^fCnWSvE@Ur-5VUZ&q%lXi&4}`hl>8Tx|oTE0COgJSYK2?H8f1qGRYKEK1 zrOkAnt};TsPaRQtT!AzU&j;cQo3tn=kj?5+Las`N1^EDh8h5Lft7fW(fsI-%rsAt&`wwI8`}xRK2j3b-e(f`j2XJ(V^sTzQa_hn$4% z6w_)ooERKV>#AF;NKhqu0ZwEp1NSkTk&UJ)4d4!_f}4kzpU>gOgg%@&FSu$J{(r8R z&bV4mkuo5Rq??T;ve|TQnCiRZMpL)akDKbUnM53F3BC)uQ4?`3tM*G6PCjR%&I~SL z&*v51o?z@Zw?Bg%{Dhl<9AtW#Xh~<)YA(YU?}H>pW&4GRMsx}Gp#_>X6$&yJ4dIwCl*#^q8rhXOEO02S4-B{n z2%{=^BU~FW2hBzfhNzu!LV3AVaBB*#=>xr>zF|_XjHR<0;;2c`nGVOcZdTZp7D}c7 zrCFyRH;%$v_yO@ceGSu)iup{Yq+~KF74a0+fZGmZR_jmOL%IpFH$ZmGo8h zdrW`s5A+W`kM_4yd%r$9={+`{-Ss+T>npb;=mDyKd9G0dT)g4~drxxL74N@URU4Uy>H0!T%@*f;+&cdLVCs6_Y~|#)UY+^c4^sDCe_-j=SI3`?pDt^g-1C}eyeIwW zmS^Q#9&#p0KR@H~r_X)-ai{j>?@f4fXZYa*D=)a7I#tXa2M|Rl+-ET z13h@U`%m{TUC1?){TQ8(yWLg*r`Q_uaJpk>0A6Klxr(hBFz&B$p#4kUp0TjOytRBe z*nf@nzx1Gf*qZUReC|M?|01_6D`@#R>-S!1XtcleMp~{YgO&fpPBG7U-X? zd*!6@`+1r2O@nwAwk7A)m%VY4jmp>SHrD>uvHib-ycO84n|!#!d3f_*+WM#Qv-REk zsWv#th&{0fxs1TMkg^gzXRIDK$?|^`Z z-Moj|(+pXAf-^gwAjr38`yaq5!Pm#0^v1OH$d?X1_qX$qg^uq&K6*&UXJhjdOI}&_ zzSn2r)K+5Q){+z%?VB@8$%tp!Jb9lxwu*Q8>$mxpgXZtcNW|P*^pr)-KCJiNwX8ib z;Ex%xDlxX$7Gx^<@Bp7sAi;*r$A+?j76#=j-dp3^sLztPL- z;qCqL__@{uN1WFi=f*1n<6%CG=zp^h&j$J2&s(H%2Hv~+?Z9{#pL5#aIg5-RM2-xD zclEwN*y{oJvhT=l0Zw^t9JTOqwr|ze zKkOy1{rZS#zZi4-aqKbB(k+KgoHKXcg^7h`KJ;EFbjEKK8ZY56_}nG_djv00<__t} z0Nh}*hONVTBj5vQ`k8~||H6ROu{ct{F{p_&)erVq4X9vUT+7sH~ zpngwg(dP!?O4{Y^E$y#5sjh>KZ4tHXvcKIWT;^YpI8CGOdH|6N~H>ev zK#DZHB3DP|biJvB>#=N8X&8o8sFk!Lye4cn*(lVK%jRP`ijKlIEK&4^e)#)A%ON_a zj2R*b4Qt@2$vnA~?bUljfWEH z)kgl-^#Xr<9e}^Riy2%cIBdz{Xo=a`AA~K*Zzm(`nwgWRj59Sw#j25M>s>R4pbcD* z7@0g2A(Cxu_*CFZ4vfT2WD4i=sg#v2C{E59tiyGX%&csva1^w(G=MEzjkXM62J5;} zbPBGWNu?8mg&`$_$~FxYqn1(wh#opfRdF@RAo`>@WtHf9-+46dedp4!0q5&Z@Occb z2zVazXS!tXwFG}|s(@2JHtUD!q33WO_kU*r3GQVyiY)S=kK;4SP_Wp_#aszHO z?5!I#*BAHEoHlrRQ^S4F*vCMPpq0eaFX26+eO|O@dV`)4v_$e1?O*Doe*E@?OD10l z0?BukXiqf8`hRgkTYC*W8W_)uSQ-Dl8|iGws&&nwd7IBnAy>+K_n2pPc{)z>){D<= z?eU^7T6ZSL#GKa!#>B?@3allJ6G2Kw)evY5@zsHaJn3KEy)nFb-I5c`UH+m;0zL&{7UjH@!`CIzsVyfCzSo4=oRIclafjaLDmrdNOkFfpYrw+(DMT5k<7|s=a7G#&D&T_7IQ{MVp@8e44Xi2b zSGLwKu~;Nx(AwzbY^}qHCa}mEW!j9r9I_i*rz|Eg2Ccbh>44ejh>s|RNFk0eh@G;n zOgb@wE3sWIF7>yqY&w!z_w1%k?#(|udgt+%{My(XUO}F{MQh2%K4y&DAdSa^_jtBU zY1AT|g9kaUeN40`J_zvC?jwA7F$nLu+=rW-H$^|l5oYfto`wGMWJM2rUi^eZHsD39 z^%02U&0-R;C6T}Z2gyGaZ$MNFGK=ykaE1cE&z>daY>GP$*-i-(OCGi>kcl`H;nom$ zjN{iz&TZuS7063F9>edT78|D|QYf$9?j9tvvH7w*1dBwlIYkCJpkf*Toy!PWGh&^ zF86mHYR3^POWi&AQ|bNpJGcwfsg>mNjgF#>UGcDS&F}g&%l&3!pRQpO%XVkFO+z|T z{(wC2AJ=}5MB`-Rr!~O%cR>vV> z23{)Qr2DEZ+JfW4+jE)__=L>#Y{C-fe~xFsFBa_!=*JGw&_@JZL;C=Y_FOc!e=(j5 z1MmR<_T&Uy$Q_Tbdp!S{Y=BDZz1vmMUdMQX{U8g($3qZ4U%*RVKfC@(c=L4-;eqLn z9mviKtPAxY#ODQp{sZtW-ntN;D}-k;-}6O#4YpSRKQDWFaqReE`H1mrZF<+Ug=m~X z{40L{!rvC}g{6ca+LKc;{>Pz9W`vY~qfJ<1%!CA0Y`5i?}VMw zw>C=u_pI~-cjmM1VH@4Eq}AHp`Nq;Mvh;E3g#66W(5FBBf~3^IkO@1Z=AA0?@}!cT zOwtZt9+jH0b>|G`WHtKRUi>`!G=4@tdt7HZb-Z+r`?uRR>*>2^pET>WbTV&e`<1Ms zMH-nj>}114PMU0{FE5K+|RX)n{X+kN=&&ZsQ|$j4VD| z^uz6;mJB`1htqpo>wczyQ%)rFmp6RA)jxE64dcnhOys9QYEzWE?K<5_5MGEkPNmcV z=n@vl6cZoDw|FVDkI-2qLr$7Jq4pZnR3hoQXXJVy4cdl zRQk<<6!x9_a#8{!Hqxipie{>gLbxeMmolcC)Etzpu}sQPNWu#$TFOw2g4VSG)Emy1 zq+BJNCRe0jWb;zm$~JHatq*sM;S$$DT+53Vg?hH;)(5jzstAfmCF{ik6eC+BqDf!o z?D1j?+z#~NJzfmXmyJ9735Pz$#C~Uy?6|8IKKa7pRL9!*!q5EWok!jG!9=`F&N(%yR~>Q(a!8kX zQyR485X?e1iOfT6Wk(XOrkc84a&g{0rJ=I!u&pR+yx}e#l;g9n&r5N^(3%pI*#OXa1{CZ!i6iXg>ztgZK}_WeaDFuAaHwhih&9 zd$BEb)Byea^wR_XYTWhpd|htOwc29^$GjI3Dbe*JlX5*N_8r+!jCRGGr_eLl_wsCH z?;IWN+=H&^L=@#c(+wMvGNaXYx(Alq=f7qtt*bAWxgFYCZ%zSOT3gAx}O` zhazK?Q%i>;;pN7Xr()>nvE}&0k z!ZCUJ=L?JVV)ODvS$%pT+VEHW$4_@nbeFGke|Y`*i^Ec<9qsJ=%0E^~4^KQ~{mwHV zS$yT<^RBq%;>GZm$HDbPF=uXv(HekKz}JXcvG(~fe2wT7gKOYBCWmc!2m!sTqJ2=# zC;avW-hR)`)IYa5SPYff_t3x{hyz0#F=+>m@sbRfsf=(8vfhxfG@jm-W-$)Bw^C{T zE}XJ)>>`J4_<8|5>;pCe!kMB}qx;H}V>NL2ZA*H@Nf#`bWkWiG+Ibu`P~5({VN3By z+E>4AGJbmxjh`q_;GvzLju3E;&z77yT)@fytoU|qR~7a&?3qssT({HRVd9xw`x@-~ zliycc`j*wv{<0?AM{oq)VkKAjPlDKtDp$llz&4g4Bf7d5#j3ELU zdmPy}(3(_N1YQ`=e3DqsGd3sK`3iOb?5Geck4d%Yv9~Y@TLzKwMi*IF%2#ql=db-k z*&(-@mj`7#kF$+^iL`7xNT*F}$&?i@sudS;_M%)$A+5uyj~)59?0OF2nU5fU$~3K_ z8p-0gK{it}%>gJ;JM9ivk!75bkba9Q)M{%|z6=Jwxt%Rm=@85}3jdP$glgm-5$Zzh z1@;r!02DDikwpn7O_R9ef_;VrO%@^s6Yrcr1ziz5nkpkuM9J*X^s<7QoleQBXc7)@ zC6bYvQmNxslnS!6G9#gIwV8)1arBtx=1LZlEU`J$SJJ^P*>uXux&<>~B??kQsx%zj zy2W!YjW%5v$Eb-v^mZEU=^!{;zmKr_M3FT*h1I)eN$F933U3_hVcBj8oP=fb!XX8! z^9xDK{-*n&PmnvMB)lw~W*U^r{;$rau}dWVyf>MO-1XG=F8fq!lHR%Vj}QLx7U_8T`Fz!^6{fPkBbWP2^{Wq^d1mw5?zfZMj=p=VJk@!_ z{lsUb`=y^FWc{Q2ar4f3yI0@+2A_Mo{TE|%r&z%PHuobr<+4m8Oe2^}g(JY>qbB*8 zN3&3Pv#H0rvhDoM4F&h+)WmNoLD zvC=M5L;Rp=yA5+lF5v{QUc_lNRZ^tP3K19 ztnYn@p>v%`c3Jm}k2SUUipP+-_l`Z!^L_c0zQ^D!vQ$6 zBPuUZd*Va(Uf=Ez_J!=XF9{r=C8}_p?Zb5re|BqIKNKTjU0_E(Kye zh-hNY{v7DL&hFhm7#(u2#k2Q{chUEJ9cYeSJK8ROE-iQ7CiEKdiY7~+HN!dY=wX83m>8d!Z*WHkWTi1tB@H&nr@?uiwY={RGbCt8!KhP19FIUkw6a@uUWH3p7BlR zGs8<)ANkmkU-rAxWk{_}1 zu)qGou4L5GwFk7vsQ`VA;rm^u_|FI)Vt9?c!hA{SUhxjt_q1=r&h(78treZj5G!$u zly7;pXlgdH7N338iQ8f5DsHRvT(jD{n37G|JYWoRjhAcN*V%Icg zB^%dn4VtBDqcWiNH!xvAM+~Rj-q=z0xsGBHgbN*W<~a z6pQdaa7HmBAcF+QLFPs{L1&c0^j8$-`5Mb|&$Vr5qLnRx$nsJgYdS{6h1Xl|Z!P&^ zh=#3BF?{8L!RY)9Vi_5Epor5xIAV2>d}BT&d4t%a)@b>#uD%}QvcCL}0G#O)&bJ%C zDd6ObGCJ7!jTSD@5A97cy65dHURY?9xk?w zRnwF}yE%)EuYmC}9JBFt?F7yeF|slQzqP)LmH$RV7kCcB|0>`<9ytEXLq1&CQ5CFU z0MA?&dG?5~i^`sDDR^D<^%v0>$p*Iem9J7?G-@^`TAQlxXUEEa4#3%1ML&NMaPpah zb9hg{bHVr};@rA`=h;3W#>)HIdA)#h+V%YYB~Kr9`)_B=pT4{^)^j);7vp z&Y`}~A!OtWZ_#($=b*m*d3x*AMNfbd*R$`)24Q#>@4^TeoW>Wxe=nqsXix7!U%M_2 zjNuaY-5C6oe=nY)wF%H$rNuKU?~Bi*{I1`g)lb(pCb`PMg|KI=SV`!D z<8gN0k4^J^1#Y`cV&^X)Q&_F3aV_6)iPXT=VWX)@D{Xt^8Yzq9Bb<5Biur<7vWoqh zJBT#JwtOC>Gls9V<-~{?-Z?yM7Ldwg=}t*TOwKk2BvnTFDg1~uEv(6+(W5CgmRPdy zF8S4SZk*NCx#XGd&~_y>OGU;VQ7_eaDbX5WPJH)d8r!WSjW&)7qkK|NM*5Y5ndC&} zpgo8eesAoM{}Ig`XgufD0+O~@4E3QX8nbFmI7tvh#ZT3t7aUaZG$dj%;Df;deL{V6Obl%ka zE>;FL`*61Q>^(wWR&N*I3EJmlDF2q8VfI0W#)fE2@Q$Dm=mGmrGMTg|qPh#)tH7?4 zXDPrT8V|qWFK&`f{`#gjosT-3E|k6^o#k$QtoKU~{>1Gg4=9|1$uJjA(SGI?JvtWG z^5@*C>wcYeN8P=}2j<@)H5a&}PZ#${sv$oFhv>`DP1l+73?JKUUcT?oWJIhyK+^(_ zh@Tk#xa@cV5)>zDxy+?p7berhGrtkf6rnp9eESN9ZBKWQPT}o8$X4jrqCNG~!gs9l zB>}%=^vaIsh;Eqq{^rKuQ~!JiTD{vUbFIv=gcVUZ%1s{tPtY1htHE1K2n$)zSnKX-;Oo9%v>nIa|Q=N0k27ekQb-oqp zxAP^vl+yI9tAVTagI4H}Me4M&d*<7n8#-4y?zQ)=x&?I_|K{cm>$@YGM!u{X9%vS$ zup#inI2mg}c;a%Bb;w-ARLY1zgDKAdi**%kPhkn`9Ge?|b@{^l@uG zv2APr7|$tFNQ;ta!wa4MhqS}c$EC^~waI-wgIfrwzk+d2wIDaZ@!_T}oqZkMD+izqQu zbgUX;%;3zL)9;#wnm0<4rl1t6-S3!E^sWO>hEdYv9n(B8Yb@J_<2`ScT{rRE1J5m6 za?>)YQ@S}-Uh(}YvMQhQ%v#j`nua^QZf&tF(U|JVJU;I&DoqdYJoGKY)4s`AACE3! z5iz<9+On~7%O7jw8IrE-}@?jnTtnYMYDAjey_mI zR|z!?F6<7*Yo6Ts3j2=BE6;w8mA@>W@%fqewGCdcXkTda$2{HHuD4_5S4I1=^0+48 z)IP}nUlec^bzlQ|AnUV!doedY2SJNpU}N&%&7fU-jvzXvc8pGi+z@i+6~8}EA94EK zC*U{3r`kq7)r*PIQTOx8wP*bkXEPQJ)s)QHN!SCPZQ;u=f$P2WJgiwEcUW%00iVXS#lf|(h7=( zN`R0u*iA~f{o1fIoRjRTn^B}jg`T8%f}70ZrtE>NtB8yPibJ~nMIA-_QAR(PS6JyT z9ETq?tRh}PVLc1Q+N$nQt!VYZ<}8eU3~OIxeh9bPXRw<;nbl5~C1Uaz^5HDSj-uBh zMl}u&CXG&X)gJbpiZN7Me#5qjW8bqC+8h**w~2AJ%gEh+`=po) zs2H=@CEzqJCY!fD2e|O@%RAf0B}TMEJmcFr9KQ85{~hi2L9hRgpe~8+^YnerEw=P` zXZK#%^sdp_@zS&S|1jfU&xYr?toZ%4`vThu4sYnD(Myoq!)cU!qsh?dUM=Yhp%Vm6 zQf?PjH1Lvm!pwK(5Tauk=}$?_dAY0V+$Fz*igeG$Zok@5xV+PeR{{c?9`4w(8g8zCTvp zDB!+bE#_wnxGx9zn0CF(#`J=CN1M%4e%^m3Xv1^~A7+D?N4w1h&SP-O8wu*g^#VTD z&INz4_B385gTy+o6L7Mtg7%h0aKxvn{Y$WOqu`8FLJBy7Q9cf{9u;9`Za3Rh0rh+J zP9f1LX?ixQcOX^hV09)bhNkLfUNTgqYxT=H1=SL(b0wn=Np;ikcsRaiz*hDiu8t}lub(4V&k@C z7EwJ9GD2;mrC7P;r)Ao3vxZQ>WO_T!RttDLO~%Tr1iZDsek@z{XU4W~li8eqC??_K zA5|vczrs?x(SLu7_G4S2O&ENvt@MG>Pn)dW!eT~j-bS0gdQ!Ak zkPBS%bzH36Gk!nivVaqRavEhkU4FuEFKBuzxGb3$a{&dUGFY?x3gpY+VFcWq$XRB_Az+2Y0F3D z`28&P2aeVG`(ayBzWFq&EH<8fDTx|mUgbrJU99FGV(;!k9xI)QVS7*bKl^RQ1lX8u zVc&EtZI2Aub|f(}L?1bG^%iM<*X#+O{@_LDyDQ!P<1cjjS4+=07s+2(Bfsq&C-tt5 z#eKePrkzSUEf@NWZoMdLAM5H&B=0%f%$#uCRK=7os7rC}uDdjMxbA+f4Yy*Q9;PV+ zM`F&1mvdY^L~xcj!DKG5a&)VftVsVDM@Ufwnb7cI4-Kp;QH;(xQ&f&`zY#*B1EqqR zFCG{gi;cc0q*GRJq|)&Ia#^?0ufwZG9Y~}ehIF@RQe@rxyqQWDGpXom9~*tm;!rA; zO4l>P3W}1U#KAz?N@6D!7mgY9AI+8auByInj+GyzsRrpp=p3&xd=y z4b#Qt&j>ilq!v$k`smZ_9lq>&*LG$RlJ+=g~R^R*9-W&;R|+ZIQKlqZTXCxes$S5CsEkuJQNqBfHW#6YENzpNiiokI+O?W07Sm~6 z&!RlwP-<-0CSthRI*Nm-@TCk>D@iG*-_eyk(q|F5m03@wi~86W&hJ5YCMwBv7Ipbk z6-9zC6Nu*k4|k28*YPs+_2HpTWFJ!XH`uVq+l76KXa&^rL}GI)xj}|1OB7;<_fZdT z7CcOx-Jd;YT&;aqC6Yj_xDVy(2jUj?m51bbDiO}1ni=v&B_o3yK`gh=>CZ`)lteN} zEsSeOkPbW0R~k8O?40F?iX|iwW&3o^luA`2j63)=HJ?iAxLhr5Bv9N!OQmZ0Og1OI z>(3Ched_3uxWDy=#Lau>IN?$I;}bHECKkN%flc3@|JiewjphH&*w&v3dQv_4&3R&r zx9>)DQ>T04UpX%NZf92+R%2p8`AzlsxxZ2VC3wk<-I-W|=gw26&pi=Z`J3A zDFGjA7ij+mey@P*-kvgz4gKFla1k5$BG&Sx&^fGz-wc%G<3foYPm?FioP)$zW{^7{3!OCP>WxjKFNr=EID?)KNXqNfrAW>(V-t$_3I z+GwbP8qOILMKn`b4_e2TOp2B7Aylz-56B&4SVI;9lh+A(DE8NxuJGj83ichzP$rkQ zE@pBJnp^be$x4!mT|41fBOk)^z1w9LY&G_bFB64MS}x#4FAr15vb)A@-_?5zy`%SN zF=k;i@xC^-?DU`w02_gx+4Xz&j4xxwGjE9ZjPa?;caHo1Sh*zNCCGitJ0H^-+xW+W z`eAnMruPJ#)`Z!2{Jq=G_xm@3YqjZhzkS<2(WWm8xRBwvzZS8CHhZv55AodB@*QHG zymjaL{&vxq?|*T>Z*#;SldvIKWX6kYFB3R#+rKF<9oyF?L=6Jt(mh(_>$=5@=lFKt zW>~s`_P)=_>uGQD`~}jpEj{kh=WSw4C9*pqec(HrI8GkSzAa6ax38rSBXOr`Y$b{{Igkj2OTFr^4} z$@C($B8OdF;~0Uj(WH);4WTx3mRvq#hG1xp*E+8qOiUP5ZW zG;A26rW5%q`Ocenfa6EwO6aUbj1u}g7nBR3Q6+SCQ{Y#~M}hU51%6331%0q30`B`% zy#3}&1)TVY(GG{NCd`I#Pm?~nsADyBQW*7Sk3b!uxvXmM)nsKL2EbGcFPBQ{#yj~q zi7LVi?Bf6IR14wN|HJNQMnURX<0-Dl$_*QuubEm=GZJPAR}e@AL+dyDQTV6dlH;1@ z4C4xi1|4XD2?hgf7}?B-EYR<6S}s8s!N%A>B8~A^BL6&ab@Z6aZ@d5gkGem<=*Ih( ze)RXB`2BBg`%$=8RsXT-?`rv~@|}0$FLlFJs|tm}pRT$pe5r4sbKYaT$vF~q4vqv3 zLVJUEC`KCa7t8SE#rg>O!|jmr4Fb+(2KH4bHh{mOY53_n^i@E-BETDEM6AR+E+zSz zxcTV|LPt4E=1rRaMSj`<&k*kiWODg>|Cx5(r4k(XI18DDb)AgrT_+7Ip-YH^ z#!rl=HD*iBs=bc=tz~QT`?s;SEPZ%=tiE5FHr9>^SIvA;D_E*+$Q3iMB5|~Uve|VD zwWxErO~i=W)=1U{2jaRS6j2_@dcQe?;r*8UsBb}N8C?BR%%hHioiU2I-q+Afcw#Db z30BKD>#7=7Pyz#KDj95AFkH6u1?l@()h4t6k2Ev3!pJEWc$&2~;v8zq1(SO(8N9ipzDvXu%3$u+l&z7(8 z`_kHI;S&Px&yDM&&F>H#`!>`Mc%MbWKZM=U3lKTLcsX}ND$bt69f?@SY;up~=xm%b zU|jEAB_nvBvaFFm?jLhPiaam+J06>dbE;w_klgN8(zU8xP0}&+Y6Xe$CMp~CRgx*2 zN|xj91Y8W7ZCEM9^THW9X$=f0D07r5s%d9Xuaxi*N(qhObr~1UWHXsuO4qBY1WKTU zG7U4HRJE*Y52RArj9a0gxt=g8nTnA~*Nwa$E|FPlI>}Oj3iQc+nq4c@D&bZ#Gouf& zt{cDSt%RTt&n{tfv#Dj5w6D>}{k0J7H%%YYkJ`4TkhJ(Y1Z%|FBdWu1;h_6*=zdAi zAvN<%F^P|ei}*=qVP8gf}`31s(zoyerY`;Y7lsOlFMzD zFaO=OiDRU@PWo{3V;7%v{z(@f{oz;FzBKio&s#G-eDUAo(wwu7ci%dCLZu?jdzGqm z<9~9aJ>dwI@!La3(fIWAlzSGJ8w(|_guLwet@zPdtoAeWZq^24%OXkM#5%qHTr1If{>LY6+bX#RP{Sf zRl@B?wc;-f952)gM!{6lHbMa|B4kM`h2x}#tD%Oalq6Y2^*@4k;FUh^1RscXnBl|Q z_VqUXPQZO%d$UE@q^AV`d27S?Z%azRDUXoZHJdYN-?it9Yz?q}7?Uqsx%}L;$?q#8 zY$49ee-Ut^rv}BecE6vrHEDGGc=rTRU39v^IaA7| z6H2y}ry!@IeBQ~OE#Kq(Q~njo=;!q;et>(5`LlK5yiq-h)}?#}TNmP+DvKS0SA2g0 z5k5S(obyd$Nv7t!d(P<0DcpMzG_ay2J3BkJvh}#Fpm&hs~|%Sk{xjD1CaBwA;Pyd(zQMe);89<-gr8 z&FgWi^3SfD{lID0jlani#wM27%Yv2L;qJfMeeHMGeP22T40^pg`t3!3gUrmHHUHH4 zX9GVv?7)CcP#qMsdmrM298NU7;OMCH);WKeQ(|~i@JuZbtG%4jHRWJu>HbYd*Ar0j z|GW^mmd~4;fo(QFOH(<~_oLX^#I_>;BC86~$bch83Wg~eqiU&EMX8u7!b_E+HCP;U z2f}GPoWfm=DakIN23o(38pKKwcVwk;?C2M(q!rhl)7?=%;-&|lS@MT&_hXkx*LCBt zcz1KkSj-I8Pu=!RN}oc2}s*i+7Z{J3u}{qUL6 zR`=FDE1#0G4@ochE5PtYcwzL(WL8)2Ym9cGU6@P}e!wd%x>LSP;GgVmCjT~4#Bn^i z#q7$>OfE9N>k76H>(!j;g1(<0f+s*(Xmq~Q&n=uWiQhd*)#s?9(BTs0u#JafS}v>) z$sN=fXgx(n9WPopmo{?eNbjdA4i+n#i?r@P<1_Cm=?v-p*SK%EPjy`)OZd*yA)CTUIsYuCm2KTPybBG%c8I7!=OaJH8%ToFj zICInSd2={lG)j3rolF+2%&?}Yv1z`(S(Z^%)JDx9nW?W20eM}`87O9ii+TIAnOx3F znF*>}Pyn7=awS%E1^#s}Cb#iRgnur((LUqx3)epzKPTGL8Z$e+5=DCvqoJo5j|rW{ z^+q}GuQAIJ_4Z*mJ|N(vXIk_UD=+fLM7(-IXV;T+NuMCQn6z2U^W{(%;!JSdHCT0c zf&)gu|=pRMt#4V51kLJT7$`kg6umiMshfK^E7U?ymgnq$3WQ#(a4Ay&bqCG>sjTnW1dx>=s`!bD8*okTotgX>Sz!E3_*}r(@}ZD@!Ji z{NbxO4fzRaM(&1tUitW@V^8?Z(iNX*9z7OWF||J2s3SrIK~<&8WpH0JeviD~eCKhw z!~Mb4=U)Hx%jq-DP(FUkh2hhDC%1{cC-5NxPYwDJ^AtYmlZc;s_6X)rHWHKPZFvXq z>)U9T**4{ii58LfLGsCtd9})*0orzs5fLLX%{D+WUCMll;v`QKv?~T z6)EDTAo$C;$|{?P7gWn812tb5G7ww}R_aft^rDd~2>NO1%Gk!Ggd)HxF4OYe zyDE>6Bq@J{eV6y-yNL5#B%V<{`LZ$02 z+5Z%99phr-wSU?64!)-1%m8EJs-VFVC-j8OTf!}^I`mUnRT*ECiJexzm41Yp5!mDC^jvd^aay9ynT6|-@eTrE1x6awEmpN$WHL~0H6mjhdaFQ zSYJyjpOXGEuY2y4#DZOi{^{H2tUi4L+R%L1^OZB{`N|nIn*Ybxw*WR>UH|8sv}xz2 z#3B$MNDyq}1Bf6Hh0-stHad~0AW=~wAOul}q7YOtf^(7h2r6xWq2X1+JG^F{Q@h_D zcK+t9bDQ1fwz^I2Hs`v{X{F7#|Ia!1es8|1?Drp7!1KM&bId$?-x07UPxL!dZ~%$0YwlR@sP^$FX*> zVgyYPor$>&A?OnmWLp%YBYeGv2Hzp-MBm}sXg*j+i!H6?>CsIih;;HK(&Y}WWA#EhbP9XzRWR;G4zvg z)sCR6C&o$mD%cg<7%w!J+S8wMy;HMuTqXArS9syJo)~NCqlxM;?&aF=ffb4ID!-(- zYn1!8>R;k)2Po8>2W*8n;#q-vWIv&oDd*}=^jjQnU z?|?7v^BcSR8iXZlo-w|=W5*-w&fe_4%)e!Wwy=ACsn4FDX{_rvWcuT?HvV*B^|-my zXOyg8_slcK$D23XXM`fyK5C~~%x)7m1v+qM{v~l!pg%!Rmn%H8t~c>4{q^uLFkP5@ zFV<$Xs$-?i%Hz5DDnU|yH>39W!zZy(_+O?zwaAWtVkC&pYq2+UK$}Zn=1CbV;o4+4*<+pV+RweC-V5 ze=ojl)r`^CYG*%J8_m6cUi4YVbG$hT1j1pU2KZKbOs>#H7JRZ?6Zp)sra1&Y<~r6j zuiVdae7}%wedUK&wI}JCZH>wk;w-dEu4x^uxoh#0x7ccX-krE)Vl|{9(O=!OYi#{{ z-IM+?VKMquX9q>+;-=gx-rroP`t_GRPdfgEm1q65=CLtGa2o*Zah7SXG5dSy|&hPo7RbA;Z%D**RJc1 zt}n?cUxDu+J%&sCEr+g9@(cWUdRLdL_QV7KYM!!d^kYm5u_8G#Ox7d15_(a{5$Hc& z>*6!pN}?#-h1UP`wAk4WS>X2M>YlPQfVVWn{)XBT$Bg>o)V6)_*Kq2tR6C= zAH1R0^6SN6`Nk`P?_3T#lMt7iPPMN54y?HOLk>1GEK_ZF4T;rzN?5(qA zpI`pWkOz(TW;NYqTz$>B*)P3i{6ZU(-=GafiT4(+*M=MKX@|4{`D*iDCCv%LW`vBU zFf!~V?0A$E&Im2Q6cNTI#yCBA)x?pb?rFSn?Z}CSZ(P;6WzLCfCpJ%VfPUy3(@9Z28rYZ2 z?^gTNv^UR|_2NbJ>Xv;)P5qGhCiPNeJF#AobTWv_LpMYbD*#&vucnW-EwSBgy9c^~ z^DbfMNIPoFSre{SjvEhO)F~_ua+RMCZwENP=(jix;Rk|oDnh>pPSDDR$$Pis$c&+g zf_P`Tt*zM|i3ah2t|u7swfJ$*pfATesy3f__O!%TDD=XZsQtirM0?n{MEk`K?K9&v zZTs@&#yoAWF(>hhV`JjRaby3Y85iHU>UVZL1wf%2CFS&Y%4qjtcWOjqVRzPp_4j4m z9!!IWJ4_B^i zzXzC->&JRt>JMmihLav=IO}OMpTq$Ec6aY-!u}+Cv0~B2)uroo+t^oJwTBvecGP_T z^6$xZl^pr&9~Lav{o@Mbw1 zsc=5iMR1$}%ku2IIu}tJ7ve0Kd{?^Zy*jmPopkQ7GgY*|l?#V5SR0E&!)iyo2X7Q3 zoH@V0@5~&&d<&dUP@wcv+JxwsxHo|#!lo3`oZ&4hBVZq9T=vZIl4WXsDa1wRN;92N z91&{aZf_)v6QeM$rUw}m;rWR7#RfrQh^%xIU{OxB;rvt^%8!BL1RIAg!h6IId&7^3 zwH?_1znRi0*ZW27#_6fC_V0dNeTz0{Zu;XljXS1m7rmN3Ts_jr+rPP>+IaB%%Cwe$ z1M_{}m`^{Y$3k=Dd|yh*z0OtLJNqI@`#j-_J^l2Yho7Dqid8-Bs`HNu&OB7Nc$C-c z=*_2lo-X~>$@`7{cWKK`ety@&^7^kVw8!ib2Hj;psqiLcb%D=JS$(>c5z=oUWyIHr zG^{)>dFQJVPW$$P?dg2tGzkxZ$E|x7cu}|=f(si!@MgJ$lTXpwPML(0AA`p#$K#Z6 zl37-Gse~73p4&gNgy7H}CT;ga6yN3Wd&65R){l4`s|`LT4wr_5;P7HfhB5)Wm;Ogq zw_l;%dR)8J*r`?IhiK;-JO8acU_6$;)86IwX8j&VxslNE7-KZ*^LazwKzB5QtPDXT zg{R8Hx<8)_eRL_GH6hSE@W{ z&-BDfkgK-|F->-16VR13p^v4mmUt_X_)&fq@~vP$CnSS|*GBn#B-F0z6zydXR7*JF z)Vh|MgognadggB=m!BXojfs3%3FYH)CVM5Ed|sj*wnesmU&2WrFnrTv)Q++bh=xS_ z*mt^|Nkf*u8F`<$efDiEI=c7My_4o%_G4G=(S0Xh*7sii4~(JM$LdVxWnH2_3%w?L zB%IE5n9c-FlHC#>#9S<6*t9{yqu8G?9cNb)eg*cECnfF6SOKB;l3&O=WVhp8LD!<= zRe#uAvv7Q=YcZmXowiqMTCX?<{IK}>5&D&SJ%=0k!{!Hgdmf?U) zIyYlglAp^y3O+9KZpnRo@-rS6>1pe_{9D#h@#kW^^`?GReFz^?@>5w}!R7qE>^~7> z{iiB#?mGnhA8M>}Ex4V^J+IIz?|GHSO16O5(|RVA#-izJUR)E^Bn1i!Oru1_Jv(*yWX}4as&n?<@pe&Uu1Yf zqu?(y_QE{d;J6Dq+CXVKXd7Deu*6u#@s)5 z;o5TTEu$41Ry_XY^Xt7f9B~){FHTBRfhM0v4w{}ccm=|?EW`f15`ve= zFz@l;s!7yn&c<7{IG(&o`RQ7f)`@=^tw?aug*#bc6llp&0f85JCUWG@M5jkSB$_8` zWuEpH_!GBKOg|sVYnDMr;5)O(N99XC(!p?ZZ%6cNky**V6IpZn(w}hP2=#p+XX(3- z?=ks_s#9cRCn4#%tv+%zXZB69T<|UNXXWu9Q6I^VxeoE6;9cg<f5L<^#hkPm-R zeM>od zv2MuI12`N-blD9djoKK|jCv9dccVBz3V4=01@wd8GUCLcYiW-Q5vWMHYxCJ7@ZYrs zjLwMBmg>^+R8p4~@wkn&BM=XSA~1^s-nciA2?PRhHv&dG%A=`Rssq35cmI1pd7|tL zZSk#-2kts=#mv#e`}NE*{`bbe-S$S$mrUuBl6cw^kKuRRp9*!hCgLIdOV?6560HPq zCW#+!w>KDcC)_>_krOTc7A=LBJ)>HA9&b)GUw_jX*MHrZx9hBRXWjI7uVtU(hkq=G-M@x>{#R`&egC zv*}l`(v33-N>?<)dRFRX@QWOqxrZcLs$?t2nfyT3iAY_}^mN=jdy3)whsSn&0r!lr zTt42rQow~haY8>?p-=LiaMf4dfb@&_9&cy+xCw@{mMAp~ZH+alhOFNSbG@Iojo{e7S}L_fN*j~mUW0&^$= z61Q%}ja%5}#KcMiUnt@oSq%>X8u$aHPb*w3p#M1)7HM}+_Y)!xyhV>!k~2L`T5HPV zO@onAiA1Q=(VX(-QXzV*C6tTd3t|A`iSke^)Y8*|G(f3XI2I3PLx`W$`q=YlJAd-U z<*CwtojhH0JDPd|DWh4_OUgZdOg0)t^jHMf5+lG}B$x<9(pgVH4~1jVPMNw-q5 z2>ve`iKHE^tZOJxcFpSUMp$Mn$F2P5O#Xq$4e(g6+oC@zj~}^yABU!v23{H@%*guW)<=Q^~Hu<-NFqx5a(G5JmE8EHSY%W;q{De#Ij`FjZ` zonh%e`8x?$cZm*q(7I`}{#N#3j*Z8hd{5RP+fDGY*c<#t!9%id*p0A`vA?V+d;BC` zAIV4@NV{FwoNT>2voZb_RO4p3k^Hr6GtxV%?AwKN@S}Iz-qc5of2m^8(WSOeblbQ= z*m>Ml`fL1aF}G8&r`dK*{z|q(Hf;fy&SaZ}M|(XIF6Q{IDsQec$sA|$ml95A?m{L) z>$)m4)A{Q!xNkq!(ux=Ob>+OxwrFmZyB^{?@9;OW)hz2z(HmIf*ZCXCHnElhCqJ`{ zLCl55T)Fc~u2U`Bv(_qqOcsPUqv&!_yZ`XX3x7#Fu07x-iPl;BO|b#2N$l z43DauUxJ3-Q1GI)+O>w-+O<~JkueN{wkeVURP$s0ruKev>@BiA(#Zw9JCj&-IWB1{ z^4Xv>2|G^0O*#^^`a=m1;k}>Tcm@u!A_Gb*YOlK8_JMBmXSt5z?-OD!Kah3q!=3!K zPP&hH-e>Q+79IOt)wW4zkJ|34Yka`={H4U9=o(Dwu5qjKk^S1XxEwi zzHGY~UP&jRz{UTl_5|G!&MPyk=^JTuUJp~I@i(F8x0mxbvTeDZ#34q_&RHWzoObHqDek!btb>deXG3b!ak~}Dl`w0wPKEf zH(r)-+Pho(_>O{0dAe@{;bdQu{>*)i{|&7JI}Pj2_%>yaX!j*qpXf;7K){<6oX(id z7$Rr#MFp?Jeb9MCFQxCg7UwRv`Ha8UOsVl#)aJI@x^>&65K5L~pZHap2W*0i;h~;v zn+<#5b{pg@`R&PEqTd?Z;yB=SUZRPUUl{T-&Jdsalcf&h0AT~<0>-|b+`p&v<6|!m zKe5QthHzJ$F*?RO9__i$p6_|iaaaDUCBrrw_iI02vhh04X20>H&BmLX@fP=en>&M! zSTxbBx%@`9v(;$wb>?Dt?k8jf!adj+6ubM{((!0KKh!S0qvnzPj0FpBopee6ht8fe?b7JkmreTGW&TN}pDD{A z9fsi*MjUzsxest)fUReH6wiJLIwh?;Y2rro&?{+0+94z z`t-!Q!#-d*(F4<&p=-8{uQ(@#(F@)Lq+XN-MH&P9#cVQi?;U5I<%J+a#OSgSB z_(RSOl99`YdxwKRR=XA*^>v=EUvXJ&#iHcJwr}gU(f{MBJ8>R3?j`+6|8Kdhu6SCE zWshv5U~dq-a(|F;x*xiR`PKBrif=Qy#I;;vhOAG11m@iXz%2GsVi(iM^;LX_wDERZ z>M%XX(z_(w^!u1r+)fFv?!B7h9T%M#+<6~vp~ov0o%mJh`-u5iiP(ip$NXjD+hecc z@mlwRrrh4aV&~iq?mClo6e?h_{IA2adCM`PnlhR>0f{72f`C#R(!dBpxdF3Nbc+IV53(V#8C&q?j}lr}jS!uIp`fxz3k zEA9&TjE`Dw+Su9s%4Y3*MvaE#_x~_jt}1;Hr~TnzcS|ajz$P$Nx<+kbS4I~YAFnm) zjQeJ4L9Nk#$Ix@DMvi@F0e&^jrC*;exZFGCtaH3mhTd6q?#LJn#huwda^Ex;EAOXNTNBF=BP3!o@uCX+L$10VjoL%Tn`1AU z5a7DR7hDi%LG8=qg-Z6iOcaBljyDob_@`e8du=4?nl zCEJe{wg055W9nF;Bc8Cpn+k0mf=p#9FToCDILSWi9AKr%zOA;QGYMNw zwnMb0=5~Cs?X=$I6}8X&i)^b2!qzRcDaMzey0)*e?|!E<0jWTzsDtpP>}Z+$&b-kN z9*3`#IlXl3FyiZ>{c#l>M;aFAjCw?nOf@=mC80q-^yD=nJNXXlsa!LTpBjJt`A@Ww zJ+0cj?^2||4}M^O-`){Pr`rP^&8Ze7sf`69;b<(7Z_SsMMo*^FC3<(W`J=ggMi)#l zO^0jNqq%WM$Y|?M_q~l;`rxu<#@ox5+dJM{wyfoL^CLeeEjR<9vQj%mWhvy zn6~uJJ5x8@^LYNre3O>(wfLI7*{I(ecl+G_xbAVn(`CJf8YM+ zF&;x^v>HRvIdI0S+8JLMgKJUxzr~yMu_Yf- zeHGc2_0T;tPtCL319NXnZUGsaHGHJ7=6W^0rZrjk;~S@s)G!m*@A5vY&m`if^8n`o@;S zzpZ}b$<JINzN66)l#{EXB*9I@BH`nGv z21#E$)#7#IX$jn!>dcqNGrnxxGw~VvOF*$KW_Q8f%_dpCI==2CNu2@m3$+e zP`QunRA+LHq-#@-@;B*we~7=4Y%j=VXYxTw4@5s!eer;VldUW01ojusz{uit9`MV4 zis(MMTGmQpRxVrRFZ+oo2y>Nbq%eWTZ*cTPYnp#6ZlUjlxFHWnNYt&493 zABdn^=v9W(IIVJErG)!H&qCgd^02}1>npbTy_3pbbgqD%b~Y%QUi`k2PryQ;XA-NP zQwdaLXJo-!MVIa9l%Z6Hyl|8>_R1z)R2Iqd3sPb zGx^W5My2B|kK6vv7fT(ddGbcdf$mw`9e4KBm;C9vkjLkVxKI8mtxa~k(=#GvKbpTb z|EZ&Y#(pw?xJ!E}XjB<*WsG-?QMYUNXjkN)*M1+ne$Z1T_1EFoimR^<1_xX^V%f40 zZ+&{xlwTTePr15m`Q=T<^V-YL{p-5xj9Ze)`xcIS_Kv;AU$rxK+mNdnnKn7? zH?3;|ZOLmzvdy|CD< zJ08|KE|qY9A6(r@a8PHox17quf*8WtL~jOtiepozL^{tw_7VJ>e&ZG_e#`F{5*d#N z;FS49`K%vRij%E5n$y@9=ngL;Ed6QZs^~~j4jwJo1S%H@wPUrtFA4nKGM-*3NNTW_q3Yaj%^$~Hb^4{R!N$BJ$Y z2W;0M`e7EL+lz0?%EM)x_0fikv+$_w=oiAC8jo{o^Y1T(u;QDUo`?R9BRPxRm!{`- z@zMp7S2@$c4x`nsH>W$w{d(N*)nX@Ocz`1q&*kvcaMlRLvv_>i5i!#A4sqJ>Yb|&q zzO_}0wB}p*FQ+SlUkM~o3WZWZZ+ogIg?BOHMsp$()pV^%i+bS-i^e1L2hzJXMG~Ez zNaBL?FT93<&sR7#=G$mknm)A-)OrT`LyU3Ke!v`plbhohKBnLw0%D1 z9Vj~Gq{#sZ8qFSX zm9NudG&d8~sh?O5m#j1G(RePLDXof5t(DFCGkXzVz80zI$&v$k!1|8#%-h1&y53rX#@golCSI8~?cV3T^Vj(NB+F zxIh2r!3h0&bhLfq%+*gf*Zp$$OCRgMtdGyU_t;Wx(A?i@+Q2fSciSbpHarktI@#!| z8@=?By6(k&m#owig7+B7G>~E`5zTiZf3(NQww8FKlP%`lLn~JwUrYllQ}nj z`OLsd$N6l|X?e9u%zM5f9-B}60{XtC_lmNEScAcc!oStF03Q9Nw&k`JwpF$VZ4a|` z4%2&p8R1n08$|_vku`lvD+e`b0}9$@57b$>*D)H)bcS|`#@BtYhD6y^U*s_>>vP=) ztrlJXvH!Df*{a|Ewtm&(nRA|+^Zd#;XZ}JPKJPXkF72%Ei^r1kX9f<|?jJl{yVO{u zy=qJubN`w#gUcQrxa{XsHiwKqBykVq{tL&1w0>pUqr=ZPzCC#$rTW&A+Q9rg^{>g= z`Nki#y|^nh-27MRRcos}pMJx;cIA`HZp#m_|I7H`hs!?IZurKV?tV{>(kd0UFS}^y z1$T@eWsJPesG8;-Z~->5-igyzTx}nAo%XGB&k8;D$S7a+@4*H}7yU7I&YU^MKj+Px zC+!C_caN|=Zs&EPy8?Xn2028xC9cEy%T2sqkk!Q!h|`Eb0`(tMO3TLL1*=B;C+vEL zj{T3T_TLDf6?GpQ->rM>P@U^3_cG&OpDwT0&f2FNU8GV%fnW-G!?M8^%@zs!wE!*> z^il4PrvFmtl$^YK^6)8x2VZ#CC7T0R2DN)P3^wkF`_j;Jv2ei1Y5r_9k6#|V|3MeOkEPi7@dg>q}`~135%lehS z`JQLxyjR?h%~?~sa_y+9$LhB&Th`CBtj@jCRb!l3*{^P8jdt#M_cC{tlJ(;CRdbidCZ%7p{S0cW)1)q1}><;IgLUH-$k1zyGr@nEbMpE&ItSd+i|wE z$#y!TabI)QZgLL%!=#PlW?<~pF55eD$9juwH;!@SEIt+3@Z2Wtcf|N;5A96eB;mAw zDa^BSEAOq7ux!x>#mkGf*w)(CWU0e=+xrRHGWX1LxDNF%a4KZdjj|5yQN(yf|KzU~ zdu+43u;*r}@@8Cwu-R@PIJE!6y;qiP2TxMIJ$k|GO17--v%wE4_RVqD0)qOJ4ziQMRO?zaB_Dkb)ZMyMtBMhDQ4P#oW zU&`38&3_9JW%dr=RxAtlgQ>erH^e$)C&`7nfeSdiELdTQ7ckfxckojkAX@ytZuiLjBg+ zb(J^7f08hckNAl(X7#knYqWcf@An(1ElvF7hG{pa>?0yUZ!T8uZt)q-Ego+)kZlg8 zyhbPzK*+4FMe%Y&G=uQjXx0_id-Oz0D3|r=?XgI_^f=vfOx$wI0}p_H$nJ*xl(w=n zdA(d)UD1eGTbz0_Es$?t@uQ4AFMIal-uH~}@OPqN^&RIYzhz1e_Ms!uCVWw{j=47$ z@M~3HQMq3jWual5t}<`2{#4_>MzvM!xB9wjD`w##XYy(VFXAoc7H9G*?t{)3t^VDB ztW)p_i1F4*ILV`Ci=UyE;OzGw58p@vdv*`MZ|Xz-$r-pscMGz^E`Vi5J9ttb;|Hk? zI!rBVZa&(Tl376Bee4~O!$GJjL|(w%j|f**RWe&8y>JURz?oylQ9fA?yawT)%FU{M zFO^OwT>0(pSYoE*G2ER)PF=6-VORL0J;8^YntG1KT8ux3wJOIVt?u`8T8$qYtD|B2 zkWivSduo(1**F5obpdVh=Zk~URbg#aV0rrKpmEXbj|{lrBg6Idxud%AD?i8S{-Wc7 zzUKrb-9LRWJ7oRRbuMJ`ySE=6TU{_~Waaep*G(Tg>yuo*V|vqqr7KpE z_kk6&kkhVw#e-urL#B_do;K;O7q6Kywt7~{gdsC)^4shS<`{=o8ZWH0Umezlr~?F` zAGAX9&MH@@)3N|g>zJkWlKbIl5>D&LG%WSOR0$_JY^8@O3U1Q8kdN@Za{DSa;SeNz zMODs$h|#;@4TIE`-;4X#B|NNnNXX$DRbHNz?fg0MgB`hgtF-IJ}^+(S`hla5STQ$bESse$2m5wL2iO zERUzmC?F0S=1?GbQid^U3+EF=Fa$a|O3AfCe>-w{pHt8K9Nu(3nn6-Sq?2r>%%%=M zWg)|pH0s~oZCW4mv*hYkZkP7=;HrieWDk4&U8BR-{=!T=h8k+o=EUoa`E$CTwZG8S z)umOrjqS$OzU8nE-U2&lN9k>2hnf)Z;i@XBxj4e&L0Y$nBBH;`#FzMx=8Ypx2dgd%{6K3gis84^K8U`Mh*=m7&Wt9d?`UiMsWu-yL%y8%x|3iifm-FX8E# z+E>7r4m{a0Yu4FMpFLW;&S?7CmG5Y~9D|HourH7P^|_mFSbY8T@HOFUjPE~w*0Qsv zAJBeu?tsO&FTVfgWz#45u2~#je#YXPZ&*BO`uJrx-+%o@_;q~x^aIyiga00ye*LhU zr=M}7X-*QE7Pd+bMZ|5Cl$KNa=qZK0>w2B4aY z8vfMTppP7XdqwRwf9V^#ZTvf2hWwy~b7SmjxP8(WIfkD@eRFm=XYOo#v_obD%EF+_ zfymcW@c4t>L01R~8dm|vM(lt}8asNtH6sgK#STx#5$wr1GI}@>IoXj;2X(tYgH56* ziYtfyAd>XNJg&4iZNwvZ?gaM{G04_*A{EDV48v_ikkGnW^R(IhUGU&%<4t(a!CPLA zJWw6DXOi_I)vJ%ks$;CstB7uZRi;iScnm`aTnIk~KjbG|NJBYe46zMNZgNZDv z(*H^UsAW}{&sawyYj4kc9J)Uk_u*8*ZbNFyKvOIp0oi&IMl{?KGs12pJaD(AVj*1M zFGnar03@AgZH*c6Tzj}RUha&Ak^L0uh6BNzKjGIg3q7hgW6zAI&V0@J4kKV>Lv5PB z!yfHvGGI&lV=%Un&NAq2ZT6+SsZ5G!g>2_ItyeZml$ABll?a|-I8KEChgVtBoTSGr zPI&+~&wz+dW3-pd!uta_pY@>q5a7HYJ3O4=hli0c**J=-(cg#=Gx zm(2arS$&$`kE-uxIQEbDG5)DNaq`5c+P97OwN1uSd)ndaX?6JUvHM;5 zCSR+j2XNdE3JoA*iB9j|82%j1Fk)Yre{|hC`$KA`e!0+Kf$?a6xa)MFg4VcApS`k@_HrVW=fW;FG5 z8E!Xj90m}p*&0NQmnY}Zx^j-Dd|c}?%C#;Ucf>p&Ja7h$@AzstZt_)!Sf}kI_+dyF zo+rg#@O)0}Vt7o#NspCN`2!AuA2{7op5<2VRe~QQ%_i=wlYDa9Tr%4;PHZds(_63n ztYVejfyg#Gt_4!W;|m3xfOoMg_GtJ7?3|e>Z7%nDD2+Kxn3x_aKiO6i$C+#g?ht#m zpvRkv>1mh8@aE$lWIlHpKCQJKNyhDXAk1h=BfYV^yV>Y!>GHLw+caN^H-ZPOb6~j+ zUr_h8AnkS_T%K`j9=o1FvbnsMysPxX_#hu9WIYx*PZqcsgd%Y8-Jb zY=ay?lmH@^fkjz9Drxv4YUf0wgl}M4TvAclcr(FsubuY3YMuQs+}mxLtY&JOQB!nAZP`> zM4UdvhLOL+3u((zmP9VrNQH$-vSyN&>xY!|iF{W{7Egx7^YM<3SS*`~`~1kx)9%&c zT7;B_Bhr(0U{{EXw~lDW?#biPj5edqCfI}_=k>0|hqT^f_5-ya_%5q(Hh#;pcEBOkL5_KNgH&6oLR*_&Q*auSqNO8$ zS8&r8%VRoFuHb>PePpv9{pCzs^~<^qr{=qyw(6le3b3rAdgy;2Y|aYj{zH~}+|My6 z8PFzLRpS+N6#blLfwQgB2wo_n<*-j|413(=B#EHf4-q_v8CN(n zB?=w{ZVAq^V@s!k7p=vnkqUlm*&(xhMcKi2$2i*n$&0!PN1ll9Q}0KUFst?m%$5>s?qi0#*%rAsB&{=`+h#%$LT+S_^LfJ3rEWkziJ+~-0 zpKY1&O~8~IXVjz(@b$|Uc!J=K&-3^TYZjJrfY;za0jI?8D1Qv{q-c#3g>gdunDe5& z4bSWF99kQqU^yQ0lcF6#_j@l^aKaDZN1p4`zSQ0OnBNp_#aIspO+HN0ZbHaUxyB7c zi`pT6evD{)V|0jZ((^jTbmU`89me&Zc~qx}zIaSR_8p4J^5U)!$+E9e`6N8f3fC5X zJMa$$j{tXAvm?CrmiDF1zHc9o6&RQ8_yw-F|B7&@=1jU3<8&>`z2>k@dKPo2+*eN} zHbZDJy+)<1W?DM2TeTO2Od(ogemHuCg8RTHBu{v)kNOqd^ii^$*<RzWZG4Ny zt7IbKmie48it{6k33r!4`I3jyg}(P?RUhVlTa1bK8C*Ut=;`1~)QSaHvhRaC2yVfN zq>m#HbKfjY>oH<(OVWa%wY13-G>;U~nq0qv90+3H4EVmEavx|*wv!g+WjmKDxDI>} zUU^*)7S6yT!VVNX+7MFZ(}MO7Fx?Q=6^^3R&VjG?nKRJ`*5~L2ea0j0OfjAYkEI_E7tiytty{@F2j7qICmNf9v6GnMrie-H85oix4&(JDlh4E@3x|G%J(_Y zKHDtG5tj04l92-c$4&Yo`fjH2Gz$E)OhOKi6zD4so3|96wzK6u2zg#lhjwQ6j-m2w zUW6yYN*Hbil}qaZ+c7%M8{ojhgy}#W`I_QUEt)wQ$&{or-5t?D8c$Q$BUyVSk6^{F zAkt94zm<>oz(`1G;e5KJ&G2UpzcZFdhti2)Bo{Mcot;`D*MdX6L@cl8a(Mls%jJ#f zxi+}hB4q!$@W^3%yGf50df9iE!bbx1rRZqSwF)lzY9BBMnlI3!lpEP$3@^xTlF`J^ zC|Th=X3``1mRT+x!pcKl73eo$wbc(?V(FLZW&aeGKZSm!4%ACFMwtXk+ipK6Ru% z5whpt7xeWY_Do!d<_)}NS-x6i35^%KQ{WGL z5=HxvBREID0C^WhOe;J!@DZ2s*?_zZROFMUz0fI&a4>M;us~edMg$8$^tS=W{P{PVj8 z4Y_Z|s3a~}{JApuP?z@iyOMX8Ja+ksXWn|`z(2M=a-jX$SY+Pn)%Iy$w1p$Cj4N0g z`6A=&GS0f}@RP4U)YwtKrt#e;?^}4wefJ^2714$HawDq;0^W3!Pfvw2UcZ(~@RKw+ z5K?rLAl&YU1!7t2DC<(aqH+`VMcBVWB6Il^;6Yx~4*{>-^MU!*N>9h0=eA7x52Fs# z|6Z1ts;#)W7s|F!c}3?Ll#fy$`&8?4tRcZK`y)l=$=+c8JxFr2@*tv>;Kc~bIzC|! z?(-5<@g(I^WSeI1Z;Ifg`x3;fjoelvw?(`o^(EKY2ak$nym7p<#C$8lP40An23i5G0e!nTC{-?FWEcg0sD`r^Et}F`aYyMp%GqJj@rn<`vC>`pq%_ z{^LvVb=(fdSy6W46-R|^$1n8p)`85Y=tYf(`RLdK)Q(yoYJZFKDc7RSH>l5eT!r}!bSy+5uLc4o8l5qb7R??gKY0JXQtt!lNesW z5$#j4WvrUC3O=c9jWb$N`Q(b%-vdVKOL0q-WHZJ@e9g2f=AEokbtJ7)EAUso-50)T z8NpQ_h(Z^uvyDxs5j+#9j@!uj=h4w*Vr zgq*UuHI~loIg;0bbIJkZ(zq1j2&d`%Rr4F3oTdkjc!%L>H)0{p-vpN%&WyBHN0{Qq z#Sd~?rt(kY`DZ(VqM{K$p0Vv|@xT;|Hlv-RU^4l^ZFaFXb|6nPrN=U&C~XKU(h; z;g2Z4!r3rQ!GmJ2AULA&G6k19{`e-sw1vjwg70>H%5bzM*R5y|aR`?bXhtL+&lc2IKk4re&7wH1cq)nQkIfn}H z<917VrdcuPEeQn=iMi2iE1X+aNjRMu!EOitMrqE9x2XMaq5W-E-eTFb2|X6^ipmIu8+a>@3?SOeAm))Nv=wm`zEo8#Yf9>KBG zi6gcEbcY=e@#PrAeDmpOabO}Y5pq^O>~&;RhcwiNnuc1jt!KFri*$Z)g7+U&R)RNH zY}oD?9cf&E1#`N30-7yF(eP2XCl>Uj$~%pAZz|qvv)iJnsB>IoNnnY0qplmhF4x8w zf+8kI9{HZz_3qP z?(ei*_ok9|tS;*q>8ot)=5@5{47R0-=HPGrnZ&19cV%N7tdnr!o5yKgm^W&lpB}PR z576o3!5xuN*>Np81!&&SA?1u*f7_!aPm!3s`$r2=11P;j!F6?`|mow6O0ai9^h1S_1oO?gam zgKgAfyf0Pqgw~bYVVPCAgV+23))-?VIn8|wIbCnvcPF@S^Z{9ey{zmz2w9-!wi9hh zxU`#g30nj+S8&k|<6=8Jp$eWV%&o{qVH%V&4&FpddGvc=4%4RDzTeavCw{==#8+9K z_6drQcKnZmM*vs+viC<49x~r|WE?rSA5`W2!uArf_?UIRVw}Wxl}BJFLh}Bc`&MVH zqOJO`skY2JHH?cLu;%1ggI2oO0SzkQeyeWUHCe#}(pSRm?6m6p7|;1t9u0O|vK`aE z2@L~pD9d$8r`ZB7j^7~56TR>nlUzWqqmQUo@&ehGv?;{;ykN>9vJqGY9tu;P9nklX zIu%@pXpC{y_;-R5rKxdo|2tO@oLa=MGLCO;;&V9hhA!QPoy&G0S$Y&>gMkX880MkH&o+8I3_p4IRor*^&)?hZ?tEuyi`V0UpDY}VM#^zY-C?*>`3_yv zQxt-h@y0?r@;W1(TGZ3rWHjSlVNK6zF6;NWyYKgswq|IvCX_=sTDDaSMVeDaD=xMK z5TGAuLUwjMPag0Ga2uvI4mlD(HJH~cD)?T|;?9C@@C%zttjVqcght8Rye5LTcYh|~ zq?auA<@T%vo-Xjr-oGliX`3-!o%oIg&T>eMXGZ}Z#dwsAsJ~5?5BFX##<}GykXyC& zr`bHZZ5sFoW1WUEg`8_hC%%WAR6AR*DV?}UhjcsMtY}5(Sf=^LekxxQ;xF*a>N3(ksKs zo>px&&QfEOK1Z=G4F$d{;-MYz2&yqz*QHs(CI5(ab~+@S_=<6|9a5=?uR>ONva?c^ zr*p7uMP+gz=?!%bMz%5Yp_ua#$Uf{!_w$&@R!Pv@1PyJv&N3#3i!o*XruvX$$}K`G zl}A8BVq}b`5c)w|vd&3ahj5-IdZGTY;!7-W)}um>Hky5q-P}~b_g+(v5nWeccHXORkH|5Hv6oJh-F0GTdkfyVz1!`Qcr93Fm*+DUp;JG!-xjdth? zUZg{|7i^9qJ=<`uEFa@>Voi@l`iRJ9!2Gr8a~i+Or$x34uY>5f{->&6qFJ<) zeG26(X*DqGA(pW!zT3W5)geAr@Eu77CpuI1)QLS3PG>`G6A;B<%`TU4%0ZK(zD10I zngjV|qtwo!FpVjP6?QpyPm}8``#AC^9_jhAt$=Bp?LW$JlV;T(kYp(OF6iJO))RdQ zp4bNaLd{vw>#JOTVi6qWbI(xwghe?XvNE(J{G#R!E`?W+>yR#w zQ5~M2GkKnbi#Fm*EdUf86F==vtI}ZNLYk z_t#8uE#5fWzQ#7t_fYj?uEp7aV_9$O1>c<9&sBHaYrmtn>!K6-w}C&!Gbi?!OHSqX zLb$j4n)4CY;?zCbxwbR>m)5KjUTDU7kz6}+sTv!}a&$biJotO+BU#YNEFUC(AW^wa@qXEv1a~o<;^CNgkEN*< zqFB|3kkyRmt&s2x7dj325py_J&}k%hm5kZ`4OKoM>@ZP&t4Z@hW?;ScfjeM3bg1%D zmWcA(3j0HO?jYL%XUg$NeJ0^Qw!kgp*;*>$0oH$zdGcK73<;<4u)VmaAH_l+`IrP1 z(_z#qr?x@@NVvt;fPbt^!f7p8{~UdX=B4Pc&;Dd9sMOL9;SDQ=m%+^O5Pe$Y=C)U4 zd9ja;xfUJyTZy~+!kRNGoG-kjzRdu=JR!gmA~-Afrr>3X1kdcjqz0r{T9`WR|;(MUQ zS-oy4$$Fo7kd1>8h*$ zm2FS&@2`A%GR1oeyPahB->9FXBV>DIduBNQoK;_ARNvVSY@ zS>$NF2@i10DCT$cDsI0(;Oc!$`IoDj!4@V1j1=&X67dci_ zt|-12W8G@*XVuw|EN|kF_=5RM$gNF3kaH7q5pxqbYB23~8c)11o*e~v(cb20!G>qjkXuriWN{lu02h}&}A;@IbZ_aJsk#L$D%MMX~-*yELiaBh)np&r? z+&-NvH#)JX zr}kO@3B2yRQNiyjyTX}qEzbSaevNJTMYC&uhJU|lzu9&{)$Ymt@gC)C=h;S&)rHI_ zzaQ2>v~iSW7U*|kFTcSHICeS}mCsBkt-RCz-n;lF)|2Eo+oAi-_4l(JEzqh7C!F$n zit%SIQ1c}oO}G{{eyj9z{jzEway5U@+im-)Zo71CMcs)E&Z)kRjPlo`PvVcJf-Q8k zMz%qFoh-%w?Sv%(-)OD~4*nwu_NhHZaIE`Ia2U#)Ya(@xNsH-*qVky2&Lc(b)4E&b zi`fp%mGLR&dSq--JJc^qg6~cKR{IIT1H9Fxbx}5qr0I65m3g0Oj{IUwyZgHdO5Db9 zfeY}ynDd5S38yh}c`>HOtrC7C<8Hlc@l#`MU$fs)Q#Ynvj<4>(zTS`QPt@!h|G9N6 zVvHLMSzquw`V)Jn4bZkc4x$gNt4ULP-j?t%_URekk33D|-e9hYkd?sA_R9#qeY7m! zhj$wW5uEcb&M12rF`VMAG({ZHTi4TMP63>4k_N;N<+h+0&0%AQ4T45AUCH<`mhLs1 zfoPPg62FJxhDc>qfBa3q+22eJh+EQ7)rH_tTvDgV1@ZDQ-Qsgx+JZm=y!w|i5~&tv z$<*#wp5C)Zt9tR9uXX<~DW-O#%bkki70LE?pU>rX2)u;Z)utPa90VMsT)$1dV-bhNT^rU4rg6d_i!WHWIDA z>P?8 z%Uf=H_TW1ucXvNq@0i^48vWSU+fREF8R3lZokpfs8+r@U1S~ImO?&i*JLYd~I(bwJ z#5FyHYwjMSnd~OKEt6_$LeL6ME`nNC3nsLXFCacgJWV+xmuP9xf-QKTF5(O?x$2yA zqaUVMMH`~$o^zG)*E`qWnO?BpI7_=I@Y!U?i6wz^&RPCJ!^-6i%g;F{V0^M<{gQ_H z`6}e*$|6Hn&{v))kH#Vq1jis{mX-@316l!&yau=<>WyG|Ko{g6O_RKBU~Q^o46UNG6e-WO=)xOTo?ZYzJ+!(wCJkKBw}y!xieyk)GvDj&vf;F{cPj;9Ijp z@$#SLE@C|6*B${PyKU(-A|Ou2OCsHA@x$X)cgyy} zdWS#5wCm_DT9jneeprm~3R>apOA+w>1vvSyRC#Rr@eBAM-(%bYH|;YKe-1qDhh>I4 zJ*G`X{i1!>qW!1YZIkxt(6(7qr_eW+o1*V61$Z57P)i+G?XlTYd#isY_dz6A#h9Uq z(N_tL4Va~PG>losX^FQ%Zs5E?OKf-B?&0@Am?v0*g1^LXNJVg3(Ql5)!VSr>6cfOi z8qcI0nS%;kc?u|H;j*qMMV^Qs&23NP8Z#o_!xRtT2=p|ids-a+ybJ#uv8Q{Q9D#hq z;j@?HqF(vQpuOGbaJi6b)33Q)E(1ci+0g075&H5c9Ps0QK5p1&Jsx|mxvMQ_5BVS_ z=_X|p(@u{5?ZgG|OeC#`^bq36+VGbi_9I@*pFzx6SD2oDr3YYJ{7rr>TJ*ojK!^GX zWWEa+sVw~h9fWyb5rZ!L5v{>7)9*xU%5-z|hk#dRX7g9rFibl^vEJfa{Y&(1!)KDu zrkDMIY|7(@$S$jYX?VA*Y~NJvJ;eFgke5)}4II;9zV|Sg^Ak@5FG0)EuG4`Z`kSKh zE(DBHn^Bg0XBpNQm+El^p9|r&YkGkT+fiIPw?CB0X!ck>VHbY{@=f#%wf0v8)&&!( zj&OSdm(SC>KdjkG{B-u}vm*t5BJK_bbfdlW_LCi@Yg65N%6?tSK0oygu0Z@efFxpZ zJggju#e2LCFP_qEw`bEvD&3Qdgwo|fhfm0sNn#%&=1hO$-ZYYy_r5!6tv(XIOJK-~ zx4j1lrvh(;L;glQM0h(`__hI-6TV5D37nesHhjw8qF5*1ThzZu_4bXzpUw^c`akOZ z^HksVvhfmq)YZTe2RP(Uqfol;)4I?*C7<&gs5op}LuMtLCj-;y%B>t$_lZ}+fz ziV;6{<`so?VS7vB9xIA9|K$Im2T+<#<8>iD>c#(nqbUg2Ov;2y?(B53m6qXKKkdxX zl${iok=C9_N3^!Ke9D1LN!gIs-q{n2=ek;A9i5t6^Mvy;+>p@2<>j5NKDd3_gZ3bz zX*I8wKt@$0+)Nu8qbt#tXp6&aLFREsAQFh>eaMB9PJ|;gF0G|ESlWVddGJ4!nHfoe zO&=IVktzAZ37g|VA2NxibUzY3N20j;k;$cV*$^VYJP zuUtL|kXsM6H$_`po3v0Uk0+HQ(at7MhtU$$q6s~f=}c$SnREvK;UBqZ2vNtOrg&N_ zw})e4!{ZO6kvf;;8O@7*Ok(_dAPdyIax4Rn(7X;^WPw}CAD&==v#lrEIgVYMr5)gh zZm|NNz!JmBu3#B->>7e^fOkWc&k#P30&{qo^I{37v-T#U|6^BidDE6u>mte@F!#3v zkMTN+buoQ<1ZNq29P@&Y*g7vE_jtYsqL%SQ3hPTZ!PPjU)V}Cf-ESt}gP0eMvvSv9 z9_?RL9meUN+o^A=so>nVz#ZM}X84TWiI%n&e`}?!%KpNntp99R?XJNeeqQtTgvYsD zxc5xx`N=pl*ywt@!nrYe`A<~4;A@jM_aC$Lmnh))$SesbJ0nGQdG=}=b^UiO<#Pn5 zTi@_cTF1>e5aZqgNrPVwwqwvQc5W0wY!iDXLGZ)?2mw~U>_KJ2!5(LhM((|`DmEuB z6uI|2_EeU#hdA`ERv2z3W=-z_JPed{!`tQGmwYi zUr=Em+K(3ai*6#Twk&dH{~LYb$Hcfyc|tgRT(wiX=lUB;C%&c^+Opv4`R6Qcg=kD- zj$4cA9dq0>o913b@65jf=V`0V3KC8Z)G@9KW0JB!ls_=d(ywJadj?zJymtH8B2%(5 z2$>Qi{v({jjz2x)LgtoB+B+e?qnJ1E4dnWDSlZ&TO5SAN$Da8XtoK&z=gxpHa}aZycKR2eFyX*x9BW-R1}Ntvc|LgOCa>ivFUweL+^WVoE=h#bn(pmr4Ys7i zTBoBq97{E8kyt*KL8_aS@uco?xVqXMLATdf{(?`NleVwTuPb*s#^o!1Z?6gE_kO-7 zzj}_nHj}^J{x`QVKNyZT*=+&%Ai9v2+ZXJL`r|Fl@sOSkCt9^;FCHK%=?Vm~mGBv_ zoO5$S!@Px~=l!5|;ms#MjLln;n^znB`uN%~T z60gF>$68B#e`LWo+XuAPu*p6z7yS~y1c=6X?VNjVu#^ugo_EsCe>Kjy$-ju(fK|e9 zYNx5t&e114h4LwaOB&MwHs@=hF_j~L_?>0^o)pb@18jHMzVy8aDYtj0rJV%ZotPKO zgQK~;ANFW7@@*}uUoz>lNj2oDe@y79mjpan4Nm(ke4D1HYpDCeyb`7T4%Nub& zf5RUnTe3JbO^KBXn^ZJdahtXxfX~m+c_7 zps+?#ckWt5sa)$_vU~)#s+EorhoagE@c4g0?NcoUZzlOp zw1hi1aS2zpgp@VEvy^B2H0&;-B~`wmg6I2NOL-oDBg?p=c{Q2%bUB|-Sjsd1?D>8X z9;p2c+lRFT&qCX&@hh7eF&~VC*qSgkC z68bsVJCbk%&+8!DWaM7AX{2R0Oxs^g#2Wvru}NF! zyK&k0s)IF)tE$|i#=Cd-%z0(E@#+OJk2f5R2UB?at6Xyh^I4-Q6m89U@j^?mt<7im zIcn1hd&Yye#@wm6<4U2h^S4YGJa71fF<0NeEO~X=vc;jRue|(6p($l+`i?!(&C8ixxy_IWFP7i6)MZeFT&WI4R;hhOT0b@Cx_6nq6%Il{K??gy!S z5j>>Er#-Irt2wi!>o#!Y;+5Df}^Z86W~i)ET~!!IrPW!gN-@=ECxT3fM~pG51i0UAfG z2m7n&roM$n0=+}Zj+!)B;e7tL=YUG2JY*WAmO;y=PNDPs6y=eS9*&>O_KAm@OdfJo z9>$&rUYw6rTTDBf7ZveEu-7AOLSd;jV#KOE?|TF;4qk78Gj9vIukJ1pU&ITx>aHP{ z@)mnS-K7$CB5*DG-F1Pbe2m%={ciGE;8_z#E~mOjB=nZz(_K}T@+_MK?hfye@KgJU z-oe*$j$IM*U)-@Bie1_$!~*u!1L7PS7paS%do1kOEK;BLbL3^QO8vN1>c6fqCxr%CUyl?kRPrE@U5YqS* zI)qGG7HP@W30W>%autvZL}=lO&0^9bNaIF>f(?i?Bf?D}$<2f$HzApHCR=BcU@K2; zx98yUF^UhzjW<3O#|0hr=^HZBcfQ{_Rkyl&65s#(2PVtu>Qhx`t5c^=)zSTQ)|*2x zRhk};kSR+s#=>h5E3c}@B4xkl@|NO10>v*$kgR?q#4eqt}R zhIDpgTz-e>#6FLt>)Rwe4>;$cZ-11=C3kW^RO6~O^~%F;K=wg%LH~pcFRFYcG@{z` zczeNxNjws8v`0=8D8}uD&4WHymJ+<{2NF)aCv{^w{3TGZAa48ZX}}>a3Dy$vb7K17#X>^$0-; zEB9Yu`eMxDMU}1>B%E?&SRP33;h!?~<-S3WEwEh3F@9fumw4$=UoF1v_qabH$M|g9 zl`Z2Sc!^|8;8OR12+lr^fN%Y()(7hk%S-2ej2j&KuIwWtWJ&0Xy7NM~wv)V6r=#`i zEn#ED#L5^BAO_9$vj*4H-gg07*63a#@$)FY;8+)4t0z6F4_xW#Tc=e3JG*vT6cd;YtOb# z$f~*nL+v#k-@0#d`)YzlNm8liuGu5+771U0I@kxJW_(rO+;Bg|{PC{wo|XMeuQY@2 z$@d0`W{5GKEWby5;PV!#Lwhy2u-|W{{%MvPJs{(v{ieW=Cp`5g{d#E26v%yZyq(XG zm5Y+@vwXMc;^u$T`d}FoeW)DuNn_nIB->LSfaZU0_@aah+QOc&oJ80Cw}!t&HfYP4 zG#ctf&8-y3FkTtZ=d9)Cx3uTjf*9w%g<8MN>ppk@ny%A$lwHEhURb9Qf%(_gz6GwW z!MbrJzVn4>0et=QqR`TlOz^dL4KlF|Q;b>65&HG$yGKHZ*GC8^%~#Tw`x7|(&XMCI zI`Vo-Tt)sBzQgS&(W~p-ZDs`;=lFZ%WZou)np={xQy zT1(D!VNc68lH>t%*-=hlE*rWSc1at!sEbtMDv$U${K9xW}XqBgl&{V*O0Xykx%LBj1_yloS*$@@v?3);wdlQEyA33>8XIMNtT+K9*`T8*Vs1R&N{R@=18V z0uRX!_-VwGir_^#n!#D@Nukcr+p&+B!fDRb!90pAp;|0eYrtKi*(f7EV_Fg6ePw++eS=tBx7P@jDwz(UyW4EvT3&2M@NxB88d7b z<*TW9^?1t2#FFWPVaHKc+Vw`_{$M&0Gx8t-WXlV?9l3VgobUQ-sEmKpI_A)Q`pWM~ z4geMHXzk;-wt8nW{3{Ah?Je+Iz`rcv8&GGx9=&Pm>fTO3)}tc36<@XQ<1_!f^pu5X z;#T4pSBIDUZYiW6eTp~)=}VGjZ-$1uLOIR7Y>#?_y`k_m0N-Pxediu{1(4WTj4RLj zK;OZ?G5)}I(swp7F2LnCm+$CEy}I<_g$pr;CQkBOJNGOJaDsn1;H*SVmH8Z^sK}I) z=OnS23wK1h^74#usD1W1#-yyMe0$0-XN_bo;U%+{U#=jKkwGR-)ilyZ2wd|b!{dqI zCu)x2B9>*>(-R>xW91EO@zCDM4Adj$Kx3%CF*HE*dKTmHqG_Ce-S}>@YO55zD1Y=G zoS9-(oVsG~)qsOU3s>`=*24YJ^C9)3y3KUXF|_Dbo5;#0QKX;gbK;a-I2E~vc=<&Y zwdv%PQM?MpVNT3GxcCNnMMwxC&NY#DkN0f~pV;_I@bl&^t9$Oed$lol%at#OZ_1pW zTt9n5!}j;yZ5*IX#Jj^zp-kz&I77@PEW~GABVW$@iRk%buY~`z*FU@0pR@fC>J--< z6c0-p*+w;$OvIgR1{CkTh0z2And{hh#vkC!h@CVBaVDNE-A< zKcQ$aGqeD2XW7TnfF8x=EI1D5^2I&Qwww%W|7W!NY~+&hf9n=$uax@Q7;Rs=aaHN= z8#iA1p5mG{d)7R<{n7HBt25oJOT`LeE|F{HJ zi)^B(Hi`=O5f@Y->l@drHT2@iRR#8p)4j zbE%jcPp423*GaSeGV5+Q)94R={C@LO-!zVo`JA$fQBmIrx{zkoc`(s8qkNmhy&dD04X_k1OWTLLc-pIM*{`$HSvK;PNT zfmVjoc_j7?yEhQr4c5>Cw+F7>F1PQNeU#ZhgWcP`p5YrM{4#D|jITcQP~-W~ta%SE zU4xT0^nyMrY=_au2JB)m-Y?1TSir@;TcwS01!E!~uoTUq5B9m#Sjpy=Xnwmf8sPb& z#6#z14BzlZ#uJQHz~N_XfTyp`ncgyW2~W@0>2C7w?gS8FoRljj@SNDvzKHSjIB6uH z(RWn8a-16w-O%t`e7CTGadLaA*2MVVk256fedDWlex~tk=;X||m;F%DNX!{@-S%HC zxIE06_vd}@Ab3|z;v%1=PJ5#R)nT@$NzsSkGw^KSsDPrKzz-fAzMuvF9TOKLzyF^2 zYVXy?dnb-ATD5FPeEIH8Cxy)C_-sSS0q{M|J{D1)=R#+|CF)91v2wNrqAN|>Yj6bZ^BQXwd`WrH~YPzv(vv= zd^L?CMzTo!W#4c^2YtWmSxGC0zYjk4olkJ0FVl7z%kDk!S6+GjjCp@q`el-9`5BFc z{hf_R$zG~?3fUrDLbebwVqD^_C+H208hsyDa7oLpuMk|}Ne6T7gEa`|iWSS{ev??e zr{ew`d5~~H@O=B6ct;$b$C;t2)5U^KsjI>#iRQfVJlsYv=9m3E455)J)I8j03fXZt ziBg``(LyviW+Gp2sIBIY`02Rs`f;b6r?ocSz!fLkOAVVauwpJ^U4^b6x{=1#vxw$` zb@ic%Gdn&XJsowqP7Wnd&53F`ox^!e$M_I*d4Yc5^|sHM5;tatrqJK%a>M`TTZC^~ zf7y%Si{_qo`s1^nXnfTF@?HMLo)^x09!fe7G}6YKHu?uXY0L?SBmXge_QjO6_{o3f zU4-q%4?9mgCH}}YTQ0fe=JprI(~%#Kr`t|-Zf=DAnf?#2Gu~`;8!I0Vk2`CQ<6y9U zNdJN;5cn5>m%t@6BDh)?%7LG<@2>=>N-*4B;DIb{g1+J% z`YdDX;U1{yCvV=ij=t4*Ss?`su{4GW(bQQqG6=TChFahWNXHGMwU{0uBgp{-=UdUwO2D zxuq`?W6<<^+s~;zY8enb8TBCNZws9pdI!8T*je()$ote>&n4`U%piC5RGh8MK64sf zW{Toz!r*ig6+vg=s2*38Qt5Eb&W01VUF=T{XH$()&yS^t3Zr(mU^wAyKGUe<@q|G? zMa4kJig>P+{}%O9mRZW%WBEcpQA=cosT7<)GT;p1&4o%PT^jPTS=&#Hj37;X#4^hx zu&LmA4s)<+-`NMQLRWsM(V|H3BkUv4r;?_@Tf&h2|h#+&1zeI}2y@^^wCtZ40dJQ6>wDUaX9cud0I!{gkg z%N*k;ia{3-Xn5G)+kGv;kEL5B-ZCDN=`ztr;Mtbf@bFqF;8f*A@txs>8T9;v2Cp^g z>;`*TqO%$&&U|ZHdqD^0%Z`T?JR{;df(|=$Ibl4a-%Wc|dl~Q9{+CH|k{0|Gcy?Z= zwP!p#VL>#$6kvCl4m;8W-*KMSUgJyG{uVg-CqvY}D~R8U`GQ|!oZCJq+gqSB_uG9J zjdRoMWB5Sokw(vMNDIRq_&){WyI8xs;F+uTj7Q8%-A%Uwc_#ev74Ff{4<^PIO*7w) zK8l!Ih0Z5vRv2qQJNgdu;OXBHoW9xGhnPbb>oDGD&P`m=aUfd4Eh}38(zthWUFdzG zn?j;8_-A3mUB}|r{Ew@D&msUqL+$tp3?J6g=AN4`y$iz!>(_zn zSTmzja1PCP<>jBaLr)b#TfRj;!^}tpmr$G$cibwMl0%7jIh8=9sDh{Y`}rq7j@Q4l zWurnbf{UNViI1Fkn$g*LZqFr`EWZBq+d5|zOZRkK{>atmUh=)z@8`{Waq&I(yjJv| zpx?i39O^Z`wj{S?{#jR-Lp*I<>i^7`?mH6NB(r$%OoS2(f)dS^{CB1s3TSLyf$mB){dJ@>3rFtt3PhT z&ga8b-!>9Lo~=j@YiIN@BH1k8yglDQu;D@Wg$e))dG4gf#Ox?&!5MYQhDPJ*XtzsVgK7SBae|I^|1=x zN&wf~Sk^6?gE8MN7-q~HN7ONy0ugYXH)K=UEdD9D{s6KHaBZ*cx6T?wu!>`rWe=oA z9V_2vjpyjwRs>HTpmcnp=z1uT?I{W+F-goFwkC?$18h3>#h}!WMQzj!?g<{wmQ{417B)+Qpl4?&W>Pu=mOnx)* zPtfbFa@;fyrZ2&f#}w!h!TJ13)b47re@yp-akTGTdpmP07?bQn+)Gk@XMk%^^gVz+ zj$E4#<|t)kzc+~|EwZETF^Ms$IUN3pHV2j|8WZmJ|BSG0!fGyc*lo(m5u8*j`jrB{ zV-4J|!P$Qna{kocBwXkn`WE`~Oo!H<^+>c=cL68y{ZLhFFYExz=Wawq)Od=`@$9lS zcvge2RB*B<&==8fANw_Oz8XLGyr9}keHZOj{Zg_^HNr1!+}h8`_C#xzb0HUdFVyhl zo8vhGsp0XLfyX5{-NM3q+QI&d@UUJBxmy>^m*6~Kp+~#Er1hJpb%UyiOepPti z%5P;|(D5Vo<2vxx66Iwtl$E)z!EOROh$%A>D4SuNV9+B0@|=pIfP7RnpMz`IDa@fa zK=_@2FkS>XWA+*I47J-ho<|$Gdj0wjMf%4x#*4S#aqGv7Tjy_@KKqwH`OKFZ_c|%N zp7*Sjkw)RIj1$KTjxqcy$1Nw7PFP3hjwDiEI%&ikwi!+}2KgsL1%+HHBouw$_?D+# z`|MJ2h!+f4&sC$p3N4i^aodWp{5I zvA^by&#L{>4BzbfU&y}msw`4@b5_p2$;=r>&oTE5B<^4EB$1B5cN(; zixgbg7uM%JACqvh7iHFy2)&bB**7HG3%w$}WjoyanruJGUi5xV!iD}oA4K~@fv-kz z&7Y{aT6>Gv(HvXmKtHKH>!;MileG34eE0nlPUB(wQn{J+Fqkjl5qiku-+jJ@hv~Ct zMT_1}>bLSQs=d6ovg`7e_SA2V=yvE+E&UQbzeI5M1D5yRdvj=sXA-g~=U~8Q;B}UF zgyup2FAU*yCkO*T`B^S!u)^bC^sFrYm==%V)P5e{-|vud&}n$tN#D8R<-1Tmkqnni zGmOoCCRMTuzL_iMOyq!v9n;CA!iA%Dzf-VNNz2ILRVFLlaP36f$XGna0y*H0=Uq2~ z-=pPhB2loBo{+}bea_A$)3tmWS>{7Njux^5hBXv6x{ff1N zDiJF@nlE}VSR=$*N#l7Pjgxv&9cSDL>Y`e^L?!JTJfvk})DrB5>t2c<<(KxGw zN60c&Iac^tmUsQ51wXZC`iQlLZr--GCp^&6?-N|+J)k`z9dE!{K1I~$;l&@!4mTBG z;b7P3ceaL9>=7j!$^OxkiFm(F1PBO5uTZr{LLg5A1+!%4{c-`>NBOoaV(bPURjeB^ zE1x?$l*ie8$n(9nd~Vz`YKRx46F8N(8)bWlWQu=Az45f?MFz&x(Ndvk74sIZvXvlN zE}m%2Icd{M6_MKEHb%{K#?ImyWWT?d#S<#AY}P=6j3Z=8#_B3eD-|~uvcG8*ZJ`x<<~IXm-jtqwJ&X3hBXM^A>J}igiKLYR0*eg5QGan zM@->ftq-$-6C{D>Xc;_5~uY2HlxZv|%rNMbj+()#6 zkurXgXO=^@J<*M?SM7rweUhKfGS<(jQL6pQG)ar^2$`d*tZJ-6_pk%i#!5P7H~qPu zhg2W3Zy}>S!G5E_aeuYH?hE4h#J?KHdEt}e?n6xLmvuwX`!M$EJRYKv^yiQ(*|G7g z@W=$c(Lm=4b>f2C{F<&#%CQptf<}lP>HAxTSD{y$Euish?faUx zK-wlTepUCF=*H{s{x=^(Pv)g)zw172{M3GS_nna%QHWw-m~S&~aES0In?<~@1u+zi zKUil~zj8c%E%=o#Y|!_m!e+5t_g$~(rupTll^57(!mo{g=O)>n#`y!{i?rQeS%?pq zUxD5*oyDC+z~|s@?ye}_z}{9!rj+yFVn$?XlTp9x%aJ5%yP%E0Dt zxA1~wL(q`txb9CaeJGmmZ0?ch?1yMI)*>4fj5SX=n0p^5cvnTklcn}_n^luZ!E3;e zD zu~fd8Gvbb4lBI^tapY9WA1pNuJA>C;;|V7jZ)Dq?u>$?H$K0`+>rzBNnzr!^Pwf{y z%gxw$@U>7b8l%Wr9K`>KYFqe~P9-yDHE-gHBfM~LQ=M5LP@Qq}$B4nH3eXEJAQszrd9ol)0geyJ^zHQm5wP*Z2r?iYm*ecc?g7+Sj z?en;c+{bc6a=Lu$x99wN**O=UtYn_`TH4R;^d0yaBumxk?KqE^8hSI%B+h`}q48Vn zQ>Te+!V?TiCN$=p47X#cLCMhMdyqqd(<*XB0xv}Pv$_vL{BC<38`GSVF_W=U%yx=) z!*=abK4)GO85~dT3U@U=;onxaa*hpuA?3eFcaYsgJW?L547PhmbM5ZYe0v4_2CIql zyZoHt4Vj62%CW7i5v$kDLb@DY?&nL+)>N^8r|fEQd;1%0>2%UG3l_F=mR|+O?JB?L zMs%+;yzDdbY&QFo0Au8n_tUEv|w10J!~cLnsMd6k=M z-;TG)_5?2k@Tt)qfgecx660q+?hgDX)i2@4EBnJE!-}58CVkXhX~N^sctkDL7XGrY zBJRJ^tQilD$3-zuLH`vS&kr4#7+KVJ(E_YgbTiZVd5I# z%JWN8lubkX$@lV`7Sr(#F#cmpJB{^>-?5m7WHRgv@DaUmLFb2)_05DWjT6h8wS4 z5&lym+4xfXH;*pnpQqfh!%)J@#}0?DG0vRwaLo9U|7!e<5edzlT3~>t*;an!oq#2k$-M+G}sW^1^W=@xTXe|L6nv-u~84zNK-&yh|T=ptIBe z^KF-WbI&zy|Nb@Yr`_DW_O^TeHY4$lyWX+=sta#2-su1FfqQOx&iMGkg*UzP z!ocrf*%9_k-8~jOZ^~5QZ;~#ue@8dVwKbO7*P60__!VM@LP zfh&F=d4~q)aqfT&PO<^IPVANKNd|=<%Cg-10S!-`WK+y{_iY+nv=_GI9}-S}`fZx7 zO#3=M;CacotC-jBU~emQh}IO*Z?Fc5wM@K+^ow`E(-)_2iawgn*gUAlnG`;e z==b0o6kO6@=)-pGP8bjARw)=i=ysz9&o<%D1ay8s+JAu0Eq291i#}jt{p)$>8KEWT znF1fpgLPZfAM9$*kKT(@^KpsKq zXEZ$Zz}DbgZX$^fq_t!Nr|nmhWwPdzQ=f*S3sT z>E}UMX^d$g_*B<9A>-=4IDLm{B5h`{S0OmtSRqs1AGI-+nzZd=Uq{ZF*FVvISBGlf zvOaAC^%VWIId8j6!L_psoxJ_Nm$oUIS`C}ONi?$zCFg7f*gp!ND|B!02hp?&v0 z;#>77&&^YF(DA5&b{ zqNPqekSd;6ltoBrA(PAC@wOSpZSCnsA(f5{_;$s>gVN!{o<01bO#$Y8J^N?1W^ctfTKNabkc0zdJ(O1LEPU!HLwcRuJ2>(3t`SJT6emL^b z_Q8;_a^uu%{Hu+@mH2h{6<4fW*?436yh34m;^nM$ zo#lTgl{S`sA*X1^dPr49C-IH*=7g*sj%s|aHSLwE&q#b@9+2$emLReTBz{x)d;*WU zQ%}5Mc@gmSf!!7I0y!1$8wgG8+A48nbU#tOJwP!V_FH=~C*-W?ey);j+($k^8x!j@ z^^NxjaK@u(QE1L>>l+oE_GQZUwA8$0ecL{TWRj|?qZiCy*L9{x=i`5(o{or^%Ou&S zDxs=>?uTVOx~{2PCTL4E0q%I`)~`@MU0W1QG}*5NbQk-oJo34#+*$64lR ze*pOBH9RaUQW}Do5YbWhWk0Rj%YC$vtqlh>INM)AM>&UXHHT`G_kI6I`;L+orz z;51gnlXVXb)qh+jQ%6^UlV^_yx@Y>SdoMMvGfC6cp5Ew zDD_0lohnVLK3sk)1JaKCTStR4O{Bj4T7zrrt$J@k$fA<%?hmQMazRBuqMEj?e8@YSGCS>&&TU$^w}WzQf-*>3qx zAorbnv65j|2+vFEkhbn^8B*V&eS`YWlaJGPs-66uBJU~S3+nc#%jM7Lem2jaKZ|Za zbXWF6aJ5fRwfTe%ZPNYUWP8#{9@nOR>bWKMRBF;?Q|ge zYt*#9`R_fa$T!(kjox#4ysF;I*7lOEhsuxIW3d^S8$<|*{ABZuP zfTx7_>t-U`dVUDcK&Z3*>qF2sTE6)u5XIlv^pE!TU>Hu;0h53#**VZa(pXQM&atrMp~ra_Z67zb+7_O}0K*$*!jb~^*#gZ-h6*cCu> z*U9gYEW*~~F4OaX%UyHVQ)(W()(AQ6dQyES19_);ATq1zj#%f=KaCgBA+{T9;HfiB zXfBFh>wc$jX?6oUvTrN6zDJ09up}BD@KKIwt%Td)yUTUddwG6qwDv{KE@76i4rjIE z67+Q;4GdoZTrWgF4OO>Vr2q6 zh*OZgYTAux_cLl-+Whc3%xe-(^WZgk!*1Z|+)&}!IQaBUsEKKIe3$S0pNtsvEbIst zhE|5M@B3)wgpr;Nfz?Odj^zhF?;^$Mi75SR@DN$y^iWyP>D~)ehgQa4* zh-@U3&$7&P0fktuNS^!n%9W?im~rZ&=YKZ$`uz3z{1qRbdv5;vxhoen?y-_-6xhn* zKfm9yaM)sk&l&K62TE|&erfH)jZO_bY5q6Or9z_!SU>VTWX%boGyf{qXf_+*Fee zeGB-ZH!HY>ynfJS5ph| zZN3|N6rA(~-pyuja|D7ch+F|SR5-KO;~&*S0w?mMit*Z8$>uY%h`e|TP<`(U@RD}ckt zYg|s3iTzEQa_lj_s`^JGwCoWRdaCX!kUnbtY{kl?@t(pC%XU zmMVA)e~zqBa7jPGx5H@#C%Gg1OpimC93Ot<1P!i@zpqQdi#nb?;GP_3fS>hm%bzrO zF_;(LG<*SSE@&#(Ng+onKb+Q8?pKU|3pE$*rOzbVADYnM+BkQ;Rl%)}4C%LM?`Ac) z9gGL|)6(Ep6Rv7+5FfSnhu@{zlW!;KiM_$k6}**?f7Rf;{)_SK!VW{ut0?7!`Lc_) z7y*40eylq`(%?d0x&7hhx)b1G`X70t)}Gf3(eIv5w7`|T?YZus!0%UZnU}Pi{Fi9u zIR$T#!`-$9*Z894pSI}5?hkA2d0v9=6@9*%JoGmGen+h1VqS;ertpxR#(FLA?AF(T zyaw0#uIXLnmkJM!6LcKn!uHKM2F~~oHF+b~dP&pmTKjUK^ROL*3Qo2OG!pH%q8B-STjW;=_(5G(SSN)p z9dxz!nhxxGLBZu-O5j=FuizQbPRZi>=M|jjpy1V}&eJ-kbYo{iYoBe}K2=kIY+t?! z-})`pKCAI8y7M~n`+_y5^Wcf>Q^CWKx?ODAMfGkz;VSZXAS+Xmb2EZL z$@X4ztlmx9Set!Bw>9l(U<+90dJr9vc%)rt8g+VGX-qO6M13n8iyAjgTPNoy=I#AK z;VSBSu&EhGA-Y2U1Rpl;(%Lg01iY(DgKPbIk12S|9JI5Nmhot3W4744N&Js$cxs9d z(alZYU#S9I9BSZi<7c!d4idO(Bo zI2qo#9(su#Xj$?tD`+h~+l%kuvquz9ohH54HGNxK>{UcR;tg3Gb5bm(id-Xc&tMkz zozv-kK)zovs~tfVRIo)6gmy$>=TXcBH`plIFI>l!k%1zf1t^RRqq_76Uf+x3nt(Bo zL;3IA5UvkYyzzoJPzdKr5of#*&1d~lw_NG>hD(EkHPoo_EB%!Lqcl1+UN4m*xGrFr zVM@o%px@`^UgpUVC^|*7_wofc6_7qV|1fOZ>C(Cezc=2k_3>tD$d9 z{BGgh=C9++d$#`MbqhbdY&+&iaquF454%j{mj>$q#XV~@E;`Kx{EHGF*+q}w)rSeb zf32EFhGZeWYIkp2`;zOH{UE-)@0?3ROP({)fNcfwm)1Z2o_K2*+%8U(G5<7jJ?U-H zX~;F>w{-FT2oBgqI1M2%gwDiGlYwadHMiVu9<6(HG#<&-a=CD3bfhg&Y?Sh5C6kM$ zy`csQ85SD-nW2j74h*?ftLBO^knM{hW;(=U58{?(?AwKX+sAMRLk{elu*GNzoqM~4 zm(adMy0UR6!MiXc{A$NrT_vow;;taQpm8}#F=BswOK<^#dGbCW=0lmOCm|Nd8FcE8 z+4I|x19u9Zrs6DCDi#$SLU2M|-Ax+R@<0U7@MfK4*eW;^yF3x9qW)7Z zl`YksV$RK$3ppoO9yapvc*e||cuO>m(#)A;BHv%lQ6j64qE3D_SI8FA`HbsYrqm0` z=Vwl4T0TLf0slq33H;ynI)Wo1O2CPhB1Z|Fk4K535>H^SfdTEYiZQ%Nd#sqC{bwYc z?AqGsgE$w$Zam)6eSM@a^m_D5IcHmF+c--v53Bq&XDHHMZQ}6sCB(8w( z^&;x<7=ToB4`Uak(kNPnOCgMPmwQoliLl*J^_wq$cA5 zoq5e4ys0y7X7dM+sWd;McE^5BA!S^>QxY(XC4g&oFX)`zc7?GhwkBqL#NU6FF(d!` z{6hcFcNu3T{r_Vu&RMud6@I@tUdWbhyC45&vqwXPJf)Z8(OdrA$`x|rFY4dp**IO^ zO+~Wf6^9;CO_>=ZmQLVVTrc70{dlIH&a~ff_xi^_{fOW7%e%jB%<=#Bll)JN`|+-< zW0p()Hyb16ayZ@?F6!bOJB4I&s?~nIYb=e^I%Yc)k9+#Osg&n??Jv1kUZ)R*?E|eYL3uN^p0aGA$mK z@%Z`k=FY|ghx0?A96fYsfPnPJ3@18(zAeW#QeL@Kv5ZpDFX!{AYQ-+6%0)XHM|JbK zJ!+uLR0?k{XwD6Rr(DtpK}Tp z-nh$}u4&t*Wmy>B%_cVoV4fgEnZOi@eSddY2E%TP%Rp-1Gymo#n zS54xhRnER53y6gP=dN@^^%OcoQH-a=EYM6H#LQn}NPg2Fvjml9&1j3Ili_~9A5T6F zzZN%!(iwj!Jb?Su#(*7f6w`jWJ=v(|-HZ|HFOqbT;o;J_g+F9ry-bnp?>{8zN$WDd_bTY7Vv+=Rqe=K3 zaL%z_|3$SY|1bkMAc&s+S%Yt3|BUH>q z!+t)U&h-yj;Vka!)Xb~_!N=p;wPYrojH0$-rkFK-t6a;u^p}1tvPLeMFs*?ByuOZ? z&PIym(x{agL`}GXbh>Ie>9S!a6NTaco*b}|zn3oNtP1Ey>rjR0O1DU{KEWH4bhSFx zM^o`teJ`yFEw;&*BuXlpS13@n~hU|sGB_RvE1;13BrJO8Nl zTP2yK@jxbBIO{9cvoD zNANymZ2^CCJfs_vZmgq z?>n&DmE)IjKOqlVjYVmL1bj;~j?+T_4OeJ**mlVFYZP3rXOuzTLWc~-L-S%e)bfUd z@iW|ezlO(cj$g|grv1K}*Os-aJ&hmuw_rDke7`qpaK^Le4j~eT#PXf`xy_NBJYEyuuIcDjaJX$_yZ0PRN+Us z@FQ|Z2Qy}2Xap}$!W*btxuA#&{W(g5FBgn#co>7vkS&G2q%)av)*g>VoK&(@8uSNI z1J*B>{jB3#G!rN1T1B(uIu>jY`LN9UeOU40Q7o2X9z^R>)Ks+o7rdx38}ooCOaD;< z(Bz6L@TSOtWs1Y49c@?<^R-wc(a2@m%SVg+Vkq4)rXM4dAMw*BUZfsDg&2&_#mLQa z*>TE#al8=59>*#AZqbJqlXc8ursTLL*-Vl>-e(cT;j;wii^2zm4ELQy@cnP2X`ns9 z#r&}HVQ+q#2KUHE+<}uzoWJ5*+@ALS>=Q}+_er<|ILDqO{8Jhpw)y)Q|2{<67!SqV zbUe-YF^wPolf;AFBDc?@J^L9FehQg=Z5Bh3sdk2&eT(lGiO1_!3<3W z<{1b%;^(cT3kKZ+LA_$)i%}UURwB7@G28IQ2f}4HTy5Cl^jIbw3a7>k6mO)TusdqR z;p;=Ns-+>nUNBN^Zlj95wOhi=`c`B(W|!?)DVK9f_ijj*q%E-q@T1h1Sgp?zdz4(%nUp47Zq83{*ZxPsDBDPr1UPt@xzv*+jA zDseAv4ExDs5uu}GtenVYouZo>vtgYI#X@N;K_P-P#gN=^xls&$jQYrcIW*)C7QLZd ze|yX*_>R*+p`xKuF*)kt;Vx|cjX{(4iW$c*jXPoN_$zte!}Ky)C!ZI3=%N)q^W#KHH-{DA|)l{1@Rf3knI{FOKsJd8*`H> z%(mcX@ywCQ)XbyAI#IJuZwI(-g)zq_d*g)r8yWQzAFO8vXiw2UoEdH(^lUqgXOXgQ z!}JlME0+`gXaz;Bi;)~2u_@*n>2N6P4?(uFBWbH_BZ)Vca#EN(=?brha_+%-qlx#- zJj$KiyPkFZ<3tikclKWd-K+2>81BGk0+lO5rlW@{$FX}1UepJHAHZaWvp2P3Z z;1%-Kg--AJnuHhFwu4895gCB(`G|y*AHjHz05fv)E|qZNsY7dou&q!1UE!C!7X9uD ze15g2NIKl5`ju;qgcnr562FAssqjnxOW>#1r6k>G{RBOGpW^X2)C#E@mY$`uk#5%jf&we;QVff9OoSpekJJkbo9RXsy!#1 z)xPv@GlV$&S&`6^D@^gdNj81=Z{+usyomk}2K|UN3;kE%p~8;`m()C58kgYb?o(Re z;E$x!%MwofEYaGqi{re+Rf#79n+`nG9&5&bDmeM6sQ~|$vVs#I)S7hQs}fFYSdPc@ zJ?6vh1P{GFbT97p<)Tl8=v=8;k(;dM1)&h0iNHonoGcjh2pa7Fj488W?dg!oXrD5p z9Tq?Ee&|ksJbj`l)a{pS^#9|`q3~z^`{#4(qvO9B|MXD%{I!18^%t~#bZoTkb>okO zhZ~9D$H>^T!r$mRdTCeW!l_rEnaeINopZ@t^JC`R$Zvk-&-UMYgK?Ab&G4oEmODR# zUwBP168?`n{4amp-+5pAow*DB-QWA3aos$sH%B@E`xWSb3y+OyNpPkO=?45KUhkxC z=bJR!^%mJ)=qhMW+|$-;l7%eAjKte7BoT~Xy{9T={X-H?`C$dZFX6Xo`16EE$O2Wk zSF*!?!uszJujo_!dR@pvzXjOk>1t&@8}Ba8BTm61#XI%%%vh5Hc)M;6&I9?VWb(>} z1Z?FuMJnEC-hvBf&nRT9V!b)hBSEeONDs1FVt-ZDu(Mvguht`}T?b&u>Hw(Wu zkXN#`HMSvtM%~xw&KKkKg&5BD$aWPGlb(%t70-jWxBze9T@0yYR}lw1Fc&DOu+&%* zxQ_zH@l3!bQ(5=wBP9!dVI^E1!Lz3^55AY-wPE)*>bqeNS9u0wNU%4J%2=kobhPBP znPc_f$B5;J{6fwsz~RprNWypE@E42eM6ooAgNjl^F4j?jLE;8MMapV2S=B%pW5FIsh)Izr8`Kv zK8{A`zV^NFOSr9c*h#vjdS@+CSR=jWo61zVhhat!<}_{XY2l)9P>gm#i=z z3xCPD)VOq+{|*1U(K9~(<3`^2x3p;foG4#r=RZhJgq?zplKem(5MPAkz-P&8?GKXN zZg~vw&T4t2R|@&StF6hMy2NG zvJfrHaB(7GyH>6U0dpe#emH7E1{p_sh_SECIHa=?n_^NkZWrY6Vue<%8H_iXRJs-K`kH-9GZ@>Lr zpTFn!4{QUCNRQdKd*%kBt6FzR|5%TA5M85Np3rEQYw~T!5eZ)oJbk3c+pc*%uWMi* z-RXG}e7ie(m!j!OSZU~$WTDk^oq=rAnu23mA|U-q`}6-7xsLn@+n$|9&9_R`YIs8a zW$eSz+g@8AU9;`z`?vqDJ$-awQ{?D)H2C@LtzYx)ON|G^yNt!glBNE4{THKmHEzT^ zn5g4fpx=yhKkYB@4-E_$SNx%IoiX$t|FtXd|4(jTzVxZnmvk$!S-b{Fu%oAMmTes115y>yC{|XghWuokY!^ zA9w~zTePhjwScb|#!XJBh&We`bj+SQ9seOa_|c8VkNk7g&m(uf?|tuCHF5Wy z8{fC_(~%R$|FX&Wi9cKYJa*^%-+%3D-!I;|>HV7`4JVm0MhZB7treYOsXsq}Vqj*e zp0)DXBnsN#NW5tD(+W6h4Wu%;Oe$w(Gx>D1(D;<`EmUdzAM1rw=KY!3IqSMi{=7(A z+AX_&$gdAn9d}^RDZ8VCMgnis;l5^V2-zFOU=J;Q>fT%n4`UP8#zN5#ldY+p5MOcd zA){mBAB)~;5+BKrs1m=VfANy!7xAUsgm(q|LgJ(H+hGH5k?lzy7%uq;DN=S0amK*T zseAk+hc4|e>Grm!^CiJ|jOU#M@6zMrF6sP%R~Vkt@Uss<5{r0iRl)^+*kp0{SlJZX zCw0?4sqd^egccYkVh2xe3Vk)yzNn4qVd34nD&OT6{ReSip$D-2TYIQ~vh+MZiYaZP z`5i0(zT&B0h4zPDw`j&PvTMS3Q?%{6QH@#phUw13isau&O-PPSo7|Ui8B>_m@Kp51SaTcVX!J6aTp2 z4xYQv=>qU!7ZsPQ+#r`Q_=^N$|9Q^M_urLei5!2Xryxct#e z*ZQd79QtWA*N)b3*u*OfbF6ET@Mw@|HbJX$1vtBrkYLrDh{f;b!Tnr)3 zoRKZTzbr&kW0|(}*g#u9LY_$7s3JAVDH(XhG47}92%C(Ka(@o!lwn`_&7mavn?}8f zABy&IzgR;<^XBuCI;$-ncD=C?FP!iqiSe8l8y_kl`NNE1nM@n068UZInSt>N<+en! z;dlk_4Ez0YKVC!G;N)Oz)Eg)d45+?y=zBV{j8e_H@>w_fq}TWB9I<~Q*P7^n_X=qQ-Xi(w%G_cjy4dfE|qKLV!x40WillS zwTY3Haz=6}hNK&>T=6h8@U#LxSmcAO{an0`v#ua!lLy=-_}=G8-nMk1IknFPXMX#h zB=`}w)qsokkb%8lBKY3(WqZP3rrbxu3|+on;gRxxXdA%^8{;QGyh3ZlKE#oMC)hU@ zfyW_Rv=^=_?vlSqEz*h*a;$sEqL!QTkQKAOQFBE=?|0FS1hIe5m_!Z;z(uwM405kcm zcC2BD-@?;~;LlkLW^~Nzn6bdXpKrr&fo8N%r%Lf>hJ<()3^Uc%pF-ub!Q@yn?GLu+ z6TXx5V+o^_H0p7`oIpxeDdCseY8_Pp5jpi);d%`r`gP(&a1D}hH7pokC{o7vu4^&L1JskF}Hmv zn<=O5u@vn@W0Bmr7s3%gxnrR)&Rpv z4tY%!a^7{VgwqTPjxOV08EZ>(8(hg=WG;hYUTAghTwIi zT0!mEYQ!8JjF^o|7;_^3GMwYzGv0U-pL@tp^6`K}73{U}lO_v=j!VdF!7nr&z}CBA z!h^5)nXo+)2V(~_Wpg-PfHT24vN_hh1oE}&EYCrFP3Ru!%a@r(fxeJUHA(*Wp9Fa4 zuHVw9UGD)!M4Y_{84-DvR6%1QV(b$u5qbin7JgAa9lC?r^IXrqLcNK<{ZR*^&hCU@3L_cI>3uLQ%I~f}ANAv+)doCh4 zSubvH1wO-`hY3#BPL7FUBj9JR3wUSmlk{oVHT)UbI`*-IO;b4tG+vi%Da8>r8=J$J zc<)i+J&CfhgopPM0#79;`zF7m#Q}^wnNBK3OzRN$+w)h( z0}9Ex(fC0NVZ&AISjar)Ea<(#QsW8qnQgepYYMf2)+XIQWt(>a_T(Y)0Nk{h&6o(1 z+2B}Ue@>>3@)U#p#WDJg5E48bBNVQB;r@|y33=DBR(7%M_rpUvT6QBbe>7bx<#T1z zu_1G#?HPBx=7vLF8tR@!?y7-ARUG`&IeVqOzgl%H6REd$4yS4rAHvx`2#2L4fC$57 zy+)}qj4ft1oE}WVL=0GNdNgf&P8F*iWQo>amOsItEuSTNz>ZbGhb-)vh|kXE*evYW z6cw2j&J?Gns>p1h*to+1Yu~hIUM-uc;4t2+9Q7(jIb%6FbHKOS5@W+oc%%`J471Od za?=QFA=A=@Nej_jGx)a~r8*8sy36_nDimZmIom2Azad|8K@^xj1Sy9GlSBS!#z_sv zb57X{=4RnMAQP<1JyFT)55ecY0OyFSd)H3Q~CzXJmU}Q?PFn zG}`izgiqd=VJ(sKC7A^t-hV~At{^z}7=#Bj#S1P}5hu`8UUl^RyD2l+7nv%~dQO^- zXzo;F>Pa2ph>-|89tF1|R-->#a!SdvlS*NA7)J2g2n{r9kxT;_sh(G=x2LKFCtq!^ z)$1-&d&ld=T(Xi%n#k7}_Ok_~#5jiM8i@uX-_8Ie*NBt)(QYCR`Jy~ZwohAOfpC6< zwUp;3Vl}W^!cSiJ0|lq{*r)YCw&PI^F8!Gv_Ma8pg5S<|_YiiPX#YcrhiJN$>>@!> zv3)`4g-~eG43lJ@zEh<>*1b6CJJL4p_-+fF=D@NoYCvm!)4maScI>A1J9<^$Qtk+X zcky_y%lYvl?vrZrjDnfuElfG(ZAQn;d+S zWcC`VjF-rGBaIj~NQHKkTlYqYPkuR7sZ|E6MVQIKWW7?eV0lMqfRvA(rL_mX+cZcJ z7jNSR`MLYM}MM$qkuW*V>q72PkSX^OGT26+(^YK&@})Y zpIAkBugQ8iX5`b5jgUW*OlNDQVK-y7m&l@dq6V6u!al85^sSnaEBS*%xf-e+S4sm} z!J7>D3BL8sCfapfu4vi{zwDpD-!&P&4_bqmAF#39DIY=Nf9LVxY>ryT1Fm5_!8+Q+ zBj76nJpM4oZ^c*dy1|$qdSu?2nfD})8yq?An!mB;%dv>EG1(hn9k4;QaKFgohs_9FgA$~xBKL4+`p;aH-- zQZmE+j)AFQjyC4PHli;TgRUO=0$!#1%i>M*zIvSbUqaJYaM;E^_6_?$Q3d}o%Ma0b z#iKtm?h1t#oM=*BKhf-2*iAY&pNn^|INMI0mH)qz3r9n9f(PFzz=10P$`Rt{ut5K| zUpm$P(qMR9$*x$%qig^F5Hs?X&%NWx*xH!U?vKUd#G0YET4Cs8r!E}3tx#|iGF6!g)Kb14zkm` z=;ir4j~(02^^?c8WAHeJBL9H^Pd!KW@WII~i>D!4Yu>-=$zu(P(&w$^y*^v=({y|b|+Qf~|+ zvp3`VmS@_zp@KgOlZxobcpL5cv3)X3e`II~*_p?hW!uK4-o&QL^@p%Fr*H`|SMWc% z_Ufw|`)|6lW=-h24J8-MsE>%#Oo}zpX>nlF+woU@sZ+O8;qJ`DRada14Gs?Y5&0}7wK^k+ zEsiwAALB!Vpnx+tXvD_)jrgFEr*$xC)&?>I#>hY=@72fFrp#h7?!@A_MmK20hlgEi zK3uQo;H3>2wG0+ags-A;)X2HB4X6*E)u6m4}|nI;k{+BRhuvE<{z5wFqkH1*@$f71S( ze*%Z^sN+WsvhRiVguc0OL*{8E2Q9V+_WLaDJ2hG(q@L0CkNg_irz4Ll^p;Rk@csDS zG1Ay59AT7jxp9#E(LuPN``@9n1HFimN14If{?>86l^m*}3^p=((uu2*nd>@5$i zB>4Vd-Jr8kntvc13qTYKml)boq(x?#9E z9-KP`c&Mtp8YlLOXpiyWJ=#pRUPl0<-jOQ9gU-$Nd691;+N=8Hv^UMMJebqLtG+!Q%nAgF;Kco5b z-PvBusD)p|cO`F|e<$(hCNAh$6a5g;C!WHcqVpI2L6Zlx7x90MUqt_8!1qJCoH)cU zF`m6IYINg%#eAuPyu_ae{UL93`xf#5t})zV`GK7D&73q}83z!2-xtUa(T(pA_km*2 z-*0L7VNZac;F>RN0{5 zG*+bphpP%sIzTebc=kN0;6w}H5pbAr zBt!9@WmMo7^?9Yu72{-{shGTwY0!=`$y;bdG^KfQ`#sq4Df)Oc{@%YGGoBjhl8{lV zR;=3NzFGk9g~m#FV9S`E2ZO!-If$ctC3*$5|9Lyr{dqfP_tZ6~weuX(ppoM9?w=`K zG9D_&`hk zC*jv*z!G;-1wB+p!Xb5H|1XW9F>&-TfI2 z7q7k42WtDiQG>Id3tFqXsABDMgE2iF_z1$6Asfu{diWO#56z8ewECaWh}-Ynq_x*< z0Kqb$t_X)DU0_jc(5k(I5#)f1Uh>Ie$oD^=Gtyy4S|db_)dLZc^d!v=P;g58V@uC zD+cC9=NrNX^SblQG}SlFndFH3?*6obQ*H(N7B;V^U4wIb0oQWpIo2QGQTgG*zXJYs zFCW88&0pHmV&~}+PCh}IeL7^E0|E3ICem zsz#?;!^E7`yAr~0RsHTBQ21N?oda7HT>2408rOYPgR||YaiZ$V*A<-XqZN$vNOR9f z>yher!&_8)DW{?qCvi^Ef&JojKR<@fd~PFnT!n?<_QbOy`xdMp>itErewcwwY{!bE z@C$mPN4)P1ZYp?@Z3xp-)e;r{ucH6vvzhF!}B@NLaKuUuQIAD=`!;sMb@^jlMS zXz!!&A8J!@;(boeEvoAG5l#7yiQvKdri@a*nP5EYbs1v&F8Zym)$j|RL5AW{uOQ)c z?#R69{uIHx&eYnoKPdav{izy_e?2^q7M-LsJJNgb82*WF-?cny`jqKR{d#lo4oh z5lFArFWSrT-lC0{_AhJ)N#fO71?`e^$rHWA9?5-L!zb2l=11@QBs_t=@rbaU{TcY} zzpzFbkKhOT?;D|>NgCw1GP3XN5PEV4Vxzdk zG;bzfiJ|BwPN@)m?ws8|XBzJJi@N~x3>*lGOQ950rNHSlI^&&@q?2cU+Uk_);`(8` zmp1XxUO9yeGr7E(9IN4h#Z1CYrttzPxdGw-Gk3KG*-*_}89XU46fC?wn00o_5tA8b5q2^7ZkhZFpJE ztPQ1b)jNmevk?m^j!7GjImE20@20ESyqPo7m2|a~P7T!R`2wDt%N2{&aKUNha4kP; z8`W?u5*xStCs%#SK5^}~58DrY{70u;wCJicjk7-ma~rJTUURMM{vRSW>xJ3zHR3tz z#R})n5G>>c*PDTki|?>pb>Az$(_F_HzWFN(mo0Uib#{xdwP!zMQyS7s}b6}sB z=B{iN^Z~62PNfA%a$kh8muO z`um+TbLZYG8(#UKKzcOy%$c*#b|#M%9tAH@UWv8;a{QbdNjyiX_H-T+{R&;^S|s5j zHd1VYc6e$VC=$C@eNpy7IWTBi&(YU121^c)Jf+Tpi}hB-+1zQWZ#f5&G##nnA+Z)K z;YUa~)jrFT{WbiIsqw1zlxKk+F$Q&CglsQG@vH-xkGdDeBZKiG`T{%9tAFrVl8 zxjTh(2_Bs1FyH06!vX6RH@>WoZ$Xt20j8Qqz37Rq|35Es+5A5rJwgD$xlZB#wP$X* z@4L|#?!4uc)9yU&jOec_!p`ZX-<|4A|9Hu*mn=Ez^5w_ruarLbk3;S~RJ&@=$RXOC zL#lqQT~KwX_W9C_&JXl|YxDIVY3JzQy;r~OxN~DOwL1MeZG+zQ5X!N5^+Kh;l%^AC z-I`BJz!FNmdK%ZsLb(5nI)weD{;X?gq}rW2=k15@dE%aPuDwt{O1t2UlW%`sdoH#8 zG5xV?zxsuS$xF56+NQ{qlb*Ts(l1V#^2PbTc(?A#8?U_a#>j1TC*62u-KFzOD|M7( zMH!kiyoq2JIh4oD-Excd@XuKb06qBy`E)*%e+qN$z-Vy!XHvT#6EvL zq{jHkhh<2A3eaQdbPr)$HA(hOb{T`+sO*j5i=jh1$(}mCe1*faf_j?!TnHywTBbWs zakQQE^ju(J+Q}ZFYeEOt8fzzIIp(T~k`~HedZ@OEq#YxZcxYaj!dS`fOdimBdWPs& zKg9VFMyA@c-;j8a^(cCHziRtmZFp#nisw|ay@%RkKb_8zl7Bbq6SJO+2f3y>*aP3jJb8_h}U%?-64&f=tOfHko^!SHTINx49(i?W>w4vdAw-yTyxq3$e z#y{?KARHX&35Sy?)Dwus{8_)>k2AG>L)mVvI^#m&(j=;Q;U#LWRZ23A5MR3BxsaDZ zf;>)wvtMgpLwwm`%#9R0|7(J8HRgN-XJ52)CgGu9B_4{Ez@Nvgs|?QgX{Fy&ODp~2 zSIC@j@i*I322|mSF)r?V%Tm<`=@|9F@J-)S@Cym1NqWUM zYu;0Eq8GX>#!V}T+;3+m;ia#&gU2CCrfX=M-Gdy#*gy=P0;b z&)E1PwYTA6Inx`^W%!FkC$+@+>NssGuqM8jq@@~(ythJA0TB>H-Xu&7mdBS?jQ^%xxHkwh;v=BZ z5nSZMnLjFkQ?bp;EUqTFO%mE0jEQ1?VnPsXI-_ZJbf9I6P?l$sN)od?>P@=aRVoT*8%@ zzP@bISxD*$x;vA?gR>wL@=Zc6Ny>#cBg=rEw-EV&6aP6!rV-)Hu;EuIR`9%i-}hO6 z48LN+x&2PVuShw9Zvu~zvk+Y2!4A@XOTP)CN3`Gfumv7vIQP2^nY*I1iQYcgH|YY$ zg5yHa-F^GiZ{!OI7ruebtW|L7pQ!RRWQJcE*ILl@nEO6ze^O_YFE(XQ7xI&2U2mp7}YY>dJZ+S!>L4NoGwk3s-ef- zS}2=!4rLK4Q1Zo}%jL8@se87B!>}1Gqz&mFy_nJR&XkZhQ>cr*n51JrvIs|F=+cgU^hoF-+}OS?$w8XZ})4`+|W7d5X2Y zMc>p}2I2$fd4kWXW}dJ^#BB+0wBYA{w{03jKli&<%ux@rw72jf-y`8s&c`v{!ecxx zt6qurtS23#7CaU{cNptgL}$dbqb+0o5#d?7=^T1-WZtVG!BeU09gr?`0xcA;Ej;Zg zS>Q}lfn{K8uXv3&W?}IP59dgNce{-FJ<)2#KVoSgC7Fqua2EWOD=MAn7*_42@9Zp> z0qrMKd*nHz|#Vc#p9y68@H!QN|+58IUf1Wq}gxw zW(yucKjYumX@OgO&6)uTzXoqY#oYm|-0)k(JH=Dv_y4lsikp2m zF15f}WLo z>QSrd8x5ZipNYGVlNElsu0$~m@L3kPg{QlreTq(t-Zh?Ofph!DGl>@@3DsWc9U!=K zV2xPv3>Apr zgY+I9CFuqTrr%clQm*$f{0!J9g%l-8YzCQ$D-n!>~&6Nx9I8I(5e z%a)KDitD;qBIYCFxDbi+`IW~$qRq?YkTrU9B5TY!aV;-UC=_%b{^iO9d$YNGq_5aL z5bVumbCKQx3gjBFQgS4iNT50yCK!FC9{kIcVUqQkW!U*Ui%gWB+c`r`le+Fq)e3(nb z@orBt9Zh?EsL+w~cMoXcK-S~YaG^PyjrMdWGWt-sm>66@C7H85JzA`%)W`qX|Neg| zFLx%;9rpwh!E7-c3WmMuBpv|r`@Og+ok?qs!E`E~(cc^y(t2nTT8)2IPX81c$vn@h z>mL)pG5ZvGYZ|xjPIoMJoDE&R2)fLxSd-=RSYqcv%sw#-wP`kHHS$R- z2Ek!jDIKDj?hGc<`9b{U4C*~vP{)fIxN_jnp#DQI{y0bz94G25_w@~G>GV)9s^lb5 zjt~>>2>+*qo0xhx9`tb)wD4eRkjmF5V}Zf+5Q;FTk};=0n#|&*m=Y?2rnKtx5Uy-y zhVkl81s;!aBoYa|Cl-s9e6j9iCYDQPQn|F@D=jgl<0E2^QRd{o0Ex@S+(9=mkJUC$DRXc-8Pz z3tZ68cDCV<5`NO?nN`oxT(V#2yAsge8AOT0_sOYrtRPCV?9*b<$ zzA3>i<7)UxIi9sVE?b=JHf7Ub##I}g4L?$NNLN57&*|J7Aql24#yKqDt}f=}m3IL+l)#~OY} z?dz?%GV?DtmEc4*dWv+L z@hn;8IlXd0Iz&Ixn`{B%Ay!!6o9MPG9&R+RdmCG%Gv-honmMjWE{0P0)#-xPpX1s* zo{_=aNYa%q#r-`9UQS=go$~deOi>Rm1{YA>6&KXBA_8C*SERCP?MtqCG>A35yA_>M z&YhSYp>B)M0%u#>`v}45SIK`V7jD0FV~yi+%q50HhHPRq<}EjtePiTx+(*;J7F;YF zT7j|kA$42OvOKU)Q)a(g z(w2VNw}|!~umnZF1-{!a;bb!m-?fDKW}Sms=u~$AC{74EnQvRI?G>FkGiuF+d3}HH zOqMUcvd{zm)6Ba}?u>_K@MUnec_A-4t*-E7X)Y(=>K!uTPl9}ufHxREO01hDuy@S4 za6O^~<4J=L3F4=OACPdG<1rt$28kZa9M7ho4bRE;#2;(FMFp4o-~I}(8(I0l{M^?y zhCdmmpY5gLSqmO+-}yh(zG1Hg9yiCkr%A!(T&I)Qdq^u3-<00scEoQiaDg9*E%=XK zxIOI)rU<|EmHRF21)UfXe5>KRg?=JG5d81@x}|-=)Y}fu+hh!2-!0m!vxkI7$eqSp zD=SqGml#167S$Rsc<|Pf)6St2EEQ4o;(d|#rfAnYFsr@Ne^Mx1Of7xXZJaE}%_n$dWzwjGpUU+pW;r>nO zQs>rFmz;e3)SsSz{tf4!Kk7Jtdlox4DW@-{XT!dXFYJy#amnG2-!b=lYxGyPZ`bC$vf<_@mOOE1?M)9adH9Zz zf8TXo^3F9+yq#E{y!YPg)?EAW+}s^ky8a%I1`?@EAebv+l8?LC8aDR4nciFyWx!L! z%xcHe$myYPxOXnzA37dqE|)njfJNaDB|mSB-6bBr7(8=twm6P38EQk7mFE0+jLK!Y zGd*NJ=Wj(F<&ZCwD z_4BSN?LY6E6-&<3A1Rdn_QDGlkA(H^o>Z@P$p66z=pp5)?0a`#OFnS-HKbYfn9^IAIyJMg7+g0Fu6`uhizJEdP|INlc8vEUNcp86`-=e?kX4BaxsdiDd+wNU#!W0j}< z2`jS9@ftVDgxjSJU$_>TCgQVojhAKEb+<)^|KV@QHwgK%47r87{@?r?X4IJ z>i?4cqyW#+S*V5r!~aQeL_Bdelx&7-H5jj(EQQ@T#3~-GcW?!Bv*@Lz0OmyDNaVAj za6r>g)0}+2eMf8=)qf{_5~u(4X7paXk4sOe81OyFSMMiq)xsQNkKu-VhADl2Bx&ZxriYsZAylj!(e1^a4l zG-KNPUv{l=;)Ryq>eo1@m45&FkE9_G8g_o{>{%1gzMR|?(_g4qqvxhoKT+Xa{Ychk* zK%|sH0lDtvKq8eKbmPtJbS_@%FFC@YLM%Rz4@aCS;-AD_Q+xLHtWW+JS+}SI1?s$z$l@?&HRxI}A9_kp#bR7Vmo0GjDgF$aefCWRpyF6)M;oPtH^E zT7D(vlSa@~F*H&PIEMoH-bB`m7apR%k|U}QHqq(3(uwClJ0x_Zq)*Zx{N z-FbR;@c$T>Dc8SS0dsgneV)R(Y zn`gBIZ>?eN97oRw=o^cUkk6!dN$bFE^SmPtuv3-BBQ#o$q2rE|Cw-FExc_+NmB}9* z`}~8mOW!+w{rcauIlo-`)j`g>(yvDPKaJn+h>Ua8_?s11}+K{OWf3}S`4$qI3hcUj$}C z$4%qo+1We}+|=1W+3*1WcC3($p~vj^9b0?&G-(t4Hn>UuA8l~6-#^&kz`yzYar!m! zylrc5_WOGq+@$k&Hn@q$SjSL4U(%1+x;++bd2}3lqH4T-miD|pAn{-pWyd4oJg*u@ z|9?&GaUN0l&Kr@(i9F z^2;OHc-$4&d-B*wbdK)tsl+zTIMLQJ9`JDs&S8wv50k&YqT#4xxqZe6(IN3MW&_x8Sq%e2_zqp1O_Caj>BR}~xR^ZHD z#hut(%{p%w&Q$(_js4*Nf)`S@Z`j5bS31&bgPXef3me?z9q&nT91SCmUV|EORGD`H z`#jp`?l zIq%=1|3$z3+@HUB#c>C9PPq77eWU(P3rbvlS}JzU;w$xEt#8xn=Fise)i+$8y?e`T z`sUiBkG}TDb$Z`fXPtX_<;?$!XC`lqb?0(BeAUJqlV?Bw1o$g9xXH_(mBTy1-A~cm z9$WjglHpHnaFd>&*x+ct4Ly#*Z|eV#slAHZDadvLzYjq=P75NuB7lF-anVWu+4?8s zNENBP*QfQ4^yAS7uNL<8I8&t*74$7da@dvr#7sKnk4JS!0WUkEWbRO3-$fIs=ILYpQ$~dcdgLY=r=uH`r*;j9=`K}XTEf_^PA4# zc&4fRwCRsuvhkbvcbLgP)gJI2=KwCF{|?O7)o+mXt|P|(M#ywmpu7($>uwtyx+LM) zX|v#m&PezRHn`ayyM>na=GdP91h}!+qIA0h*~u7PQaW7!JsTdg-{)*_@JP~Gw87DS zi+ddW2A-vB|Kqq=a|Pd>{i@<=R^fWK9GC3lyEZub-i6E>?^9vEAV#8a8U48H_mzJm zaT$9w<-XZiPZn_Sb@R6k>=u5T<2KfqMfIdCCNFcDDg;T9*sPTbE!}8ug>m@^l&mqPwF%}k1PGAUOPhnU*n&VTR+V{-M$08 z*$PMFf&X!B{HRp_QRzY5)%~e6R%e5~q zI3`4I9xJ^i`PD4l{CD7bH{_UmlKSm2$>XKF-*yDFBNpBeYE{3X#`wOj(V$~=@mK3! zESq;eX?(Zt8NJ={O8Cv-i24rlZznmpsaGX`-iwoZw9^8|I*^j{4uY##n8eN1TAY1a zh7)`u7SdJ=ogb?+HWFz4)|}k(HC5?`>4=&63$FQ9&6xhlF1hkb6dT7q>gr%PkxQVU zEoynjh{COU;!}lxyFVI9r()58 zQZg9{Bd*2*8NCn=S7-3(RM2$@9p=LdVJIE&>Ct4&lk@mOS~!#qqr_``JS|Z?weoJe zji;vVY_q|o&#PIwDGZ(-8gkTaP;x^I74q3^pR1Vmh6rxyTeYvx+u&z=R(4d59^`oE z%$;*Lmg$X&Ym4n01#gcN9P=KbySHQY_c+ICxSM~T18e$X<(H(6WQcO1Ig7A4f;s+N zu2%twu}A8rGQE?q+6=l}IQMji*z1x-J|X3+sIMEVCK# zrWFz#(}y|EmhwKN9CPLH&9KxlcnCit?|ASwMA$KKHMX`fxrOW(D@ztXf%dI8jgO~w zyg32dw_P%R`~rn)VX{tXtmMbGP- zY;e$-gNBXa&x4+ot6E1rnzrPG;Mrw;)b*i(a0yME1dFwj{1b z`bMt#yvX~uUvkXHeC#a8IpTx?9T1pA+e4HH)EqwhOIi(%7Pzpa?UEl!Gxc$j9KAd&Wy+3*4M$Pf?>&x^< zcC|mLovIIf_X%v3J?T6@o6RJ3k1v(Pp=lg>?56v_C285`Wh^Bo%;`C!BO`n!(5EdKT>jb(ZbJ#8E>rkFbSv<(+%%6)zu zy*6z*V{32H^pp*5j$yS8Zua}64Gub`u03IcoA@8M!GS+t2Rdu>rx?^Kz2(?M&^^m- z-6MWP_VL#q3Ve zS+wkbJ!pf254$Md)b2vAC%mN6$*ht?%JPlXe>`A=n>1mSNA`Uo;%ug71$Z$Oax8qu zvov?g=v9u_&%!eXsvjkz`upT}w_qPQ?Y`Z!vSnpy)#&Jg7wI-y=WS{{R)2r5>WgA} zRMC77!5x*jm6O1EhXAilAL%$6wMLMkisCjdEW%FlamMMUHCP-4qo4Snc%}EiK+e@S z;*Y1D9;Y|u$pzw(Qm&NktsERFmWG_wIH!kyy92oPT^z>aNj`715F7{xBXL*0)Q!z1 zx}7mdHoh9;;dcV|Qv9ge{oDwo54bpMy)#B95eOT%r?Vbus`b1V+JpPk#{N&rz`p~l+^Dx;v`=z-;Ay_p)}GFp zrYQDOOVs0hU*b3MVD+R7k7)n#7(F0p*zTAdK;or=Dx2#{Zmv{`{ME4PyKZ4J@e{c*sQ2hqs=VsaPpTPQ% zs+rL|ue6gmy-BR)DSkFzVdF!X`2aamzp$utB>j1Yo8!8ud|VBjW0uh`@!;H%vHl_W z9LG5!P-D@z7~69M@ErsvOXhK97!LV1U}ii94!KZERQLUgY%k;secplyV3DujGxfWT z;EK*Cd0alvjD1&){Fp{ezkr{`vhhYFe$wYO#wGfOPZ2b2`%t!z*lDUyO1Nn61COTK#!R%PXzgp= ztH3Wk1?e&PE$%L(4?)vbSc&Z0#@B5>lJL1gFFY%^eq6%17G7bD5V&X@%0_TT9-f48 zbu%2i5G0_pppdjWgdQ;aMIJc(e6T8_(*I+jpV9%F5;wG|f>N z^e_5_XUUeq4;`oNVEPh<-ff*r-zgc>T_x7XJo|5|e$D+ya4wBy-+VT;S8zcu?5WxA zhlE{X91R@(75+TN&Gn2=}MaF zo;6=H_89A zBwUOO@P<|!Jya)!`OvYT3{Lc*eKWj(h5thCfJ^?IA@Q5?I^EX(Y^F0P{P0G~+%k*9o9;uk(8ZH#M!Pr{YlB|RYt7kHqbo8PdFD`dz;;BWk^ zY%ll&J!!V{;VIN_X+S(*xudQ$({b#nM=pqCyeO{&&$ri|xjt1JswKym31J zM)&8KkAl8-!{?A5v!8GC5q;!&lpXR~dP573|1JrSGM2dqZ(f5-SkM_PX40&$6<6!7*^ZZH^mm2p5O0x)l8Poev&5V}irPD`; zm7WVeDSm7?TDBK@tL!MZU&6&WFuo06BASR|%NR1o7#a~l87}ZEer))ngbO?vn;7#3 z_A^*}xfyh>;&XKe$+HLntiJR9%Zxd69KpI*Hj&rfvFy$h9h8E`Eq@%#rqvg1va80r z0pr1u!wFL-XgUr;Ct`nboi+0OTIb}_-;94g{D%HgSUV=Fw`h-gs0kb%{1>*22XB)!ZUeyjNuDF-2ElF#in zd5XTFV;hdN!OQewkD)I@hat}mM~u@RcnbD-uIw|1`JzRSM8Br~8gW?Y3&tz;#jq>U zK7sWgKrDF7aX~{-FXdP{UePb{=T=*L!MBLQf0%5qWJ6N1)fXkMTCb z6Il2Y!~5ZBS}zst8x6Zu{BJtm)?UyFKJWY{!Bzex;7VSbZm{(mGVt#-{Hfq`(1173 zmhFYS%<&p}CE$?zM!Oyg{$N~9$Jyu{02szcmN*Py}eFnxa^#j~txR4*{ z6Yx#;_LwU)yk062XCzH29w+!K+SeQYJYd(Su5}WB#15~A6>+};kD{mfWC^EQ2Ap3s z!jdg~7kFYuzm4!3_^SeMw^QB`D#Q(LNXVi0I(tT-r&P;_M0g zHSEI{LSd(;lyFw(hJ!h6pp?a(+MqMgulp)c>K9?4--~NLLtee;iebw>JRGd(84kMB z@l+y-MZIj{z+fs>=_zDmbjYqy3Z<(r>d7sd~TsPJ~m&vsB*zS7yY8_o4mn0+=aWzf~&xN zvuwJ|Ia5&dEyl3(MN4}j8|DSQY^i7xGGN(^txYJ#+_7D?51{@D=>)g$3M#m)e<9&> z6(V@h(F=686^Qx4;>XpkLJpC;kXH)4vmQ426gNoxsxyo=P~~ zeWWn^tTzl=vvh1 z`L+8I;t%GeOE%BC=i>!m4q>NJ+A`BBc&FZkBN;G{7+%{rktnObMSa8bebG;g&(eR^ z?7#3!3!LME=%fB-3tY$97x&7)XAQz8Mya$7&iRpEYSD_K;_<( zyxML&B!Rb3{^dGo2(MEfXF|vhrWYeD7 z_EV_uc37P3yGVA}asfeY<4cV9gI4LJEJ*sbVS)$gZ%Q`|4xXWQp3c-YpcJ?aNpV;8?Y zC-5}Ys`i9GhIvst(`k*(S$Z$G00`*cZ)tCd6Ju+!3H*(B+S(&di1F5c-2xYO%rb78 zuHY0`Ko7k@2Dw0+B!7b7v+`3rp_NLGtiM96SI># zoNB(@ajWA&$0LsKII@n7ju!TJ{D#_O%WG47e2LCG;V>&5b;ZBu*3LMbPd81+|3g&} zOwGh|YvSmu3y-u-ruvX*GXu{Phyzg5$A#kV_*9GkoGSe%Pv;B=I!`klsQdSiU6eff z=&vWwy7h$Vhu57_yJ_#ysl?td?7cO0cjD^gHMj2l`OkM8cJ#6b?!MvctM$vjme{mx zwd;bBG#*6J`n^ zPQE@swNaPgMEi2bHF#?*E!I_8UL-S`Q%|Xrbvx;NGtR5Y=r#fMUnWeUYD4tTTvSS> zoB!eqo{~?UR7LaFV`~xZ4yAfAJfEE{?u>DT6Iz|6XZjA+b zU|%)n#JzeV6dLkHGBH%#(Fa^1zZM@3RP+u9`jer!D;bX_2eeQim#&T_VnuIH++VGS zlBJ=rmQCm=6QL#CxoF7mkK#Nvy^0pWAHs1R=j^(BG@sVv)4du>Sp|#1uwICTs8*HF z7Yg}&`cS|h2m}KGZ`|kiB2?$Y`4Avbt5C)t>`r)--ehke>4T~yBP#Y8anYSM=T_vm zC_b?LHd*7zPV9@XeEKEF-yM$Nbs>0)&i}X_^OuJV`4?#3D!yx+!}51Q{^Z}Ij-zlo z$WQXd{Y=QsQCT$&`OE}|Mi(8Z>c|ah@vPI~t~h;ao*7JKLqdiEsY25Luel*Ur81E0e{N^ z&k&xD-w=Ggk+)NO#@}@W!9{$)9gp;Ame-lg+Aa>#aMEe|o3x+)#){HRjSo-BcVYsS z1w03XGG;FS{%Sm>5_T3#NvzD!%P`u&aK`wPJ2(>R&Gu*Uzh0-0?pUCFa$;y`2z@6; zQ&7Z{3$wje<1R*6NFh{K%V3pgu;g(ChX(Otjn_F) ziaWC-eJ+0~7QhevPOO1Bb56%_%=nWVrHZRlnM4V1OeL~e{L7h7WRjlD$Pm5t0#}(5 zJSQ0?X-}(Z3h{8)QN(j(&M~ZMXYq!oaW~rfVhwm{?9`B_IDA~*+DD^BD?BSCcEnT;ZV}SC|Hc`@sBQO$<-h3>bfMM@j=4oN7P$)ktYDl8m4} zTN0Nds07ZSs?8P|je+J2&hGOZdpEKS{6)3ylF*YBZn*tl`53!u(z{ za;m*|lRS1mLqlL&iV9tCe~RH~E!#&RKeXSy#@3$JklB`Ye~IDOsrKQj0JY!ym$LSh zzYw11%HR>;AvnC4g`P0OfxKYb3XsHfspO!il} zJW~O8HJzde{{g;g+H{ryqLQ>4i*$s&h#UFNK`>a)`?RQw?#BdB4GDFgQ+UWGiRD$i zWP_4}rQWF4uX*x_Hr+iVLlsHbG71A0W8p$Lf(m)nu%UpnSNEWnVLBLgC*$3*3@Qp` zGlgghn~CXQCYnpfGPso$M_7sJflMKz=d<}>1{J5ZXr`d&;>b2)LU!ceiC2&s{MT|C zI?FT!UYT&i?}st3Dww?L^2+wn(PdSa@Vx3W%JX+8cVdPCJaPCI_Ji%K34i?x3;wK$ zXXhOX56zL_H}`y-WoOlyJkhf!Z<8sl%fe1@&o*4vK8^P>luVm2t5V~NQ+uHUyS{1Z zm;LfKWZ=*lvtL@LPF^NrOMY-`UT2 zp!t%{2z;{YcOPa|3{T*UmEhYkUhuhat%MUj2?IagN_bepmAvRBD~!v)AB5lOCH#B- z+z&h(KaXH6`1Ri1!>5o)uee@_n@E=n%4!-N*dPvGAslF+Q6- z_dTe-i$KmwFWOg0IMJ0h`{~%E+LPU=@ioj>?W5qIl5?jqUne|S!ruvvv*?!^-=04c ze8;T{j~rhI&qtL$3O*4(kabz`kZ-g2PDCyI#Xgwm1U;RwOYD;x>#KyPV8VALE$vxu zo$v%T?w!wD;7J}Y>$Q463$Z}p*>@4)r(R^gQTX_r(0A<8?hpB~nlm)$ku*si<}p`V zzG2-H@0qDF)7X-xY_|ViwGUT?n5Q(h+Rh&`1)!MwAiX62#q@45{g;%3u>YM|iHqz6 zzTWT)n+YuNB!wN4d@zEG$8X zKYI*3d@ zc7=P@Z&Hs%|2to>z~gLh^GW*jtD>uD^Z~ukNjT{ek9%h~w>SKuum_C0Q{*HD9%}zx z@+X~_eKb6OcIZUUlCDXMw1rbb0q%$78l!%MK5aN4`w{zvijV3YI_ih@Oz?M4TG2)G z3x$Vf84|zrquU=Ldbj;S!Gl(PZGBC`;|6a#mwO=ZEf-0@PV}%1H7#H(G3`|P8Nt^k zW86{XVoay-8yyCIqF?!4%X?y~eb+f-r`~!)jjTntR8iY;}qPc4k54Olv*q<(5^lH z{{&>+yt2gWN>t5vI%jg+h_bv16M4x=%ulPLC9msq?WOg*b~(=~J?~smT3-rhefdlx zokU*vCS9)RCAy+l69Vxuy5SkFFgEL_N%epNJaj25+ROp<=r{T>7BiB|wuxqXje~^6$_||n8$)(|Nh2Q4m z)!T+-<4L7k?nX0*i3l4P<7zre;vxLohz~p7JQDb+7p5}=er5^29oayQd&60(-=KgC zJk9X5a$HH^mpWbJ&aPH)IbOjZ^%fd54H9iy1g zsz)6oSP%E+u@=&!7xh8_3%EhQAN6zlB3hs#`x#uU(07^R?KsosSAyUl+aK$7?Z$`c z*L5FDS%j*Y&n&*%^KOBY((Ti>~r?H z6x`->wtPjw<-A7VY5JlCUL@TkIP!;O5>9Jt?CU54!94zYOMBKykM88c|p2lgdQ1H6b^k2rB$-RV!BxmQdkn#4rEpQ%}fNy(K!DXBf@U0k~#ABfc zYYwX{@I0T{MURj!qL*rK;;-G7pIT_;}GCdbb^1gCxTAD}BX!C&!UT#If;P#$S=6ZiU5idy2m-XF7=mJvHMj zt%X3&+aF-KDSzYrB<4pRTB-VF8`+A;Xpv9Yh!@*_JO+Q9Y)9~7`(zs)=u!T-vVQ43 zRM11#YQaxFf#Xpp$0Nd~=wVyly9@1W8=s@E>aSD36_wq(_gy2uiNUw!AnTQ@A8>e_ zYeSCHj>7AtFXC4>=hWvQZhQ`VIw#_tNq$uT)i?R!D=I)W5u317nCf_K#w^sHnkxVP zYyrbVuU?IWvxA3BA2~^`%#Bs5w?U zr&Qq_DZTA{*7eMP9S&q}EB!QaaeLgtV}>`tLyz%skW~kAQj4sD$N`h&vnY1pyh@V= zk7-NpZ5LVKET{Z=HasMA&K-me@0)IE&pJpiCEIjg<}9Lp12jwN0k0bg9n9~u!A;#X zb6(1|LBDMOP1Rnm8w&h+L_dX}eX)RVMdreA6=%BsL3Ap6r*Y*tSA}iiT&}rpC+9U; zL@>-P3>z(*2k$W8gok5~=r?QT;1o-Yc;)Ue<{ZkOh`87AZAHHkSJ~E^jr#lN|GK&P=>rBe&UkCXC!~5^b^{t>G{@N`2xy#EdDj zl-Pu6uG$%ulRuLoSIy6sa=Dx{hKG&zMtXX@T24=gQrMTA(d?Q8vTG&dHSGAXUi;&+IM01IU_O0Oen>cRK zR|;;)9qX^P!J#9YiZ)$Swk34A{u_M`PZvF^(YBys3gPWJg z@m<09c1S$8KyI7d57*SL@_+PuylV~VDg2yQh&*S@Q4(&GO^ti~LzedJgF2omqerf- zb&Qn3CGSLT*YT*rBV{Ato6-tSx&plxcF@>hfwSI8ds-;rqEqiTOlpzO0iJO>kFYorNxu8-1&6 z++^qIw{Zf&E%r(46M)m{avMA$bd2n?Qmby$gzcZeg4G?<)GGO?7Q4YcKs& z=SCZy#7`dAt`7)q;irT*G?(GA#l((xEdBEQO6c82co6(Wc_Xz?I}UTq#@_lRI0FcK z!YSq1ND@V$!9A*jwW=8lwTZL@L5r?fk)7t6AS;>CF%+!M`lgq!)S})r{Wktmt^3}y zXCCoP_1A8H=tBK}s_#L0>918gUk(@gw1VE984Qm)^4auYbTF=WYsGvqpcO}=Sziu! zn9|8?yjQ~&r+#eVXHxWtIm%<^=rm0-;}4|#2xM5v#d)~W+jUDHp8E6K>y~Q2*Wbgj zqKCiowic}jqyk<%NEXke@J(Ke4CK<;e5QbdNRdD|Ma#UssYK5(&WnYxJmU0by3^_I z432db{Q0CG+xKdn_ayi`tkuw5@U4$9PygX5U@7ZFMEF4=`?W2^YN;zJ@@wt4mdT9f zQLuql?9AJAl;*g=-!{qCUc>~<$3#rn3*CYrjHy1@zhrN+eG|lcZ?_UI&TW@${=;p< z^Jaw%xJp8Bx)PD{SDW&r4oI3;wC^R&)Q3Lb+E z-r`yD%!Y}Fg%&P+2|M01246)61|5|A;`kuw%HATsqjNb~>VMr6WVJTm%5XvNrgqiO zt*Aq|)tIw4zcLxCmyACq=C1;O(>~Rne5e}tMl)ZQc`miDaj*TVrM>d$m}M`uz}a>M zJq^}eg=5CP*ckrBNe{X(Pq*oTTmu#Q)atU}&l8>P@J%*3$bBq9=lc3;e1P%Nnj`b4 zZ4TPkHnHAN%x7NEZ@?vZvCGU$NC&}dIRzDjA+6z4Ek?C0Yr1(KMZ7lpi`*!H=7&_6uX*48Mqz;5^{wxWZ!nO~`StvmDQw-;cq=I!w2c z41V(4eBr8BZVhtJ?K{UZ$Ot)wk$} zeYQxZd2lkeJMRw-;>nX@t~=0^^H31VRXj`;_cD64J3pZ1aYcDR&kmsUrqUg;dPwHDpcQMH3 zi=^vo;d#q+&fXW!w&1+FN}JJSOwz3iP|0@1m zx6j$!n}-!1xxTR*D-xDD3hi02?NMxoPc-I`K|b#cIvHNO0Tx)}Zh=SRF-NO@p!~to z_KbGc=s)Hh5u!T!mm+`tq5FRFv!8R!JpgX4{LTfa%J|-Cui(#1_Z;HbGy29nSIEG_ zb}9T^`;R59R9lPfERSe|-CN}4HEtCvGyp_$XoyyadieTH|S&#@q(owBQl^2L4@rfEzLAO4$eTT>0^wuKG6i2@%Ud z>kk!N=GB59Z9lTWnI=JNOJ2bVKV&4^|3twlhXAeYmuowJO}(^U$27%9ed4#y%jkSa z>j$JzEK|9cDsdUIAinar1$^^Ss*kAHZ<26pOiP<%E*N|b0#B4^<(ad{1l?^v=e}c@ zC-XRHZNXE!bqmqEwL{WH^L>m(_A^=aPh$k%nJ2ZIIZNI8J^2mARpU4JW<);;m;3(o z4RSX~QnhDY*o>W4i)^Dtt|!{7yPY(jIZXJz(p4(Lzt=#ay+ zq;vV9j=CR(3^}Jw-ZX|Jj)kZ%pLfSSD_Z}2jbr}LLqz9g@4d(UPQ#g*XWjRa3|<9% zW8X=Yl5Yt?PwVGMZmkCq_h?QFeNujiZn#?VWYPonXFL~huRTZhPy0FSFSj)kF7fLw z#9!7;oB!?@Qr}!7zTwtOTiaXg70FZ+M9qjqHQvx4W6fsBoz@-G zj@jol8hSvyGJSJx%O*d1>1y=}p3s^GbX@38>lj_8Tg_$uk>%d8e+-=EA^P5YqM|7X z`<65zUaVE{sNk#Uw{1YdnO4ur%w%W(=&}9}ph28B+hPsbC2$JqZbKHu<0sko5&Y?J z1Ygmww5AggCxLIQONG5Gm*sC!N1x|d-HklWxE~_k zub+wghjhvzinRxv{K72}+np9|HgdCR^roH*s|@lG1l|fRI~z78Q=4Nw9p*~921c;l zOr^{7nx_<2e|oaMTEFXjEu!ts<#s=E@(0?N7vt5;58u%BMcR!cA6T=sZrsYyi)HH$ zagTN3!5tXElO6c0G=$sMC0}VMkoIbArS6IwPq^^!4G*l=Ch5bQANY^!H=mZHdAwSt ze7(W0i|&@!r0nS2pIV!QWt<+u4{RHxpOC50lWm(-dpbv|d}K?lf>WF^W2HHFqVx2C z3m@F|b=98inDDdBY^hRknmeg}>+!3kht7AyURsVQ(|?LR8N|vE=@HHTv7T|c)XQ+y z&j{D1GfySUGh>`w?_0Z!S}VJc8va?(w5?C}E&Mj{kSD-7qPdDD${hjc80OCXQ^Cpq z(-?ScEucw`fzF`;-cnK4FY!Y1$m4Fg$kIM#;q{FYPB|ar$*m(iYFq?op0<<#ukGAv z{L1o_{FCy$M)g7bL*Mx`F}B+L=`=#y8j9(o5Z%={*l(^quWTGLm(H#vIB|vBM-Ydf zL+#cTTb{kev0y_8Ips6&(rwZCzX_o?P#$dqT#+Y zYE951o5tZ-7ym52t^9W^9gE=BaV`~#C*$!b-s{1+ET3QNji&oFM_BI}8qj+}nx`8# z(PCOa3#Y?+tlF2*&gd;9ay@!1ohZh0TE({xzxJNx&YwR$^3HekhcjM(8h4qK@j`6C zucgwxy**x3)anUp9+aqXBz>VkA`o&`rvt%csB$r0APOcTp;#b+yVHSaFcd7MeZgQV zACCsIp=2tU@kemIIi4GgrmDSh=gWAkD3^$a3;B4~b=T4Bzj03NaNVz+tG8)?X%2+K zG0cQx#dNH9)R8WxOMSs4>RN@Ic!Efe7y5$z1;4K+<k)#6n|B}EtKLIx-NVv_uKfg zf*W}*`=OjU&n7(Ns~O&fuOuGoSNe9$7VoyeEpv$0$8B&)PmMdbPr=C#0nYfPAKH2d zekEPMuxbNz-HhRfJEmd1Wf5u!#2k+4*dm~(O(E68U^L!YBe-ts4zCm~y+8>hIH%uYfagtvMd zy4`I!hC1>VkcREle1#{C8Zy|EPsFuC2F38`j|nZC4uB6n?Pb!rk zjQ2XjeT77^XZRnhkKXub{DH^f?N|Sg_S|9myZY9nwW<31+N6zI&t|PsAAVr<2`4-@ zUw`P#!yh|(o;xw?t}B*ZeD3|%-+%wCTP|L9?zwkef7he4uD|$Vz;3=vUpkU;CXzbd zp2;Tleq5`}gp;U~mJI5rN zcIJy+GVVm3(#0$Hd}EvMY*!4WscwPO;Sa@8Cm@rFdsCTEI_s}aR7d;z5|M~E7VXDY zlolPyThq_~d)v$X)}jqBYjgFVY1ivbORl){ieqlRX@>r|cBlS5ZHfNI4DAtjG8{*N z3%uv%k9oWaugB~4_`<$qC=d#GK!7JvD(XHx9nn%+!I{;3pO&WDcg3_5QTQU6il^g= zs6QKshOu84_aI3)2tH#O?gY~Lv@f9Uz{#kFxB#oOUvu0gBtk2(b@+#UhN1T)hW|6b zsW6I7$r*GD7~vHkavVJjOgF?WgpS=v<8N0WtNHQ2+;G2!s~jVR&*V3p_a%-#TKngP z_jmQq)BpSj{R92JQ?%g!#+O|dKU1sFZhGJxecm_pbF?k1wIAqbYESFm(Eh3)wpyRA z{Z7AM`?h|W+uMhGC_R08w)DG^?y9=a;9F*$dF&DQEkAeG5%*2Gc=@bHk2r1Cx$};@ z^XBeZXP&9IEMNZUvimQ36#u#Cex$}Cwj`0yz?XMiOqrpIMYPAxv99YdS`(?CkLE^> z!}C-T8=G1r{8Y4;G3Omri*ET)wWnMhF^4J<8Ef*0y)<9pm?mSUHRf>cDe=#dcp}Js zIj`fjl_gIff0bv!4I%s}^DZ^V@wnGsD8C8tIuPP@(+kvh!#WA4vwR|dr;Ji$a_;&l zjn9J`J2a6RB;jA^fMrC*G^}YatogH?09u+!c9%x$h+Na>27M>--{N&O7$H6&}jB z)qG@IOu?hzG35=wk98yy57h&pemS35Yvwm$F}IL;lF^=OxbmEB>%WLQ^_NQgLDV6z z$b4-?!3|wIoZ6E{$nnZNN#g&C1zs@on}$siPJ5WFn_?c2$#WHIM=_V&GoK~-EK?lULhX&mwSN7opJ@kOtp8)zE z|7^}bn8{=Y6WPp=?$8E@P!(mcqPOIUN2oloGgRt!c4vIvY{nZ)q~lSPd5QMLOK$Yj`>!r+SPRna4q@gwL$0sT{?JKMH>pqZh`FL?{_ zwWl8g@5cCOZ38rHdeYW@F8uo*ddKIfXYY2L))jJ~$M3IjEO6mZOS%rLaLhY5B)*rv zaUHLplI1Eoc+A2_HNdm0{@CYGmV%JrQFHAGhR^xtZuLU35v{(;mtyA!zHTE!>X(HlO_lN+``ByueS^NKN0y_; z0eYLN%IGEeD_vd3z8UR_U(gHLzkAzTst>t#xoTad4?6>x4T(s`eZ)ol>_Qf38+*d^ zGl<)!3}ZqjtoOAoI(@b4Sf(~MT}uR z-SPzmmundt*vFB!GJcB1Y`<%v(Y9C|7ULHBwqu@bPkANl!FFVhVmOT!v%$`N>HBLm4y@?E_ zl|cG`C^9?MIzP3{mk@d9;t~Njpy(>r23$XI{^7};JMVb?<=C#Co+j<>*PWZA(O}FM zL-E*5whw>dl6)?c;XnD(zEmO>Pek(Z-mq586pKYK%Dnjodr_e(jS7jepk5666aJuU z`n}KIt2b%a|LHr?JM#BFi@z$a+>f}2H|66lpR-@@%jp5!If#_}!+li_y+5l5v)Vu) ziqcD|7>;%p^}%#sx-<|>#X>p1GnnqvgZ^SFfYMimU=dE5cr1A-*YRsRP9n*z{tN#` zdL(=?B7n!eu2ssP*3=o!zPNVBR{FJVm->d~xel`&o8C(~3td{5lenUc3-TBKejRol z6&{Ye%{z!9`jz2i*Ex!XA`YxQMZ)Keo`Lf%!1K&6k2>I4vVHIa$HE6g;v1SXbklDg zNuo*NBDl)kap!zQ;v$-O41%UsqYrAICOiV(aI$PqJY*jva_B}fPlLB?hpgM~dg~m3 z_iG!ln?0Vt6!;tAog^>F4&kq;vYxGd#OgD|-bOt#T4_6iR+e4US1h<#c0vw~hQBP6 zb<@vedy+N#KEdzCKPWs&;RE>0tb6UV3LX>rlEB~8qwq){D&u!vwU_fV(Z1fGlX7VG zFR+kaMNR@;xk?uR~vi|7yEmwT}ruDR<_KxJ>(wS=McE!Dc(uJ}lzc`*ncJ&h=<=ueZs&CX_hF>OcYLy||xK4p7~RcJ3|X3|6b!qz06#y-!u_FoUq zO8LA*x$oLLZ+0x09|B#Z{jwi<-dD&XXXZ(gPXtGM$b#Dwzoib)IC93g$qoyK9b%3B z*Z_S%>*x`xbC1)C$2h1?;u$!xc%9=0N5YYDG$Ja@o;+npmkNWcu9_8q6hO>t2yIdrsZ#NqEnEs%<@# z9wXt&nb?zZMO(D*XqR24->)wWX_{6O33%(h_4+OP+LObsAFnIj*V5vAsB|Cw6TRVr z`_H=Xy4~;WtJSK0H|rMzZHpV1WlGM^Hrsmg-9zG4+GTGYa^xvTR>dcs9>4sQ87I6w zukD0h?Va=I;cwTx^QH!`2_AdYoGphebD=exmTu+aq|tqI16s@j+CE0ZkNB*mns0O+Qw6$p69i z#2B(27Cfxu!X|Sks`g|na?F?`ntslhYp@Nien;U6RXs)cg>AK-Ovx+r$mYvBkhwEl zoevXquG^&G#5ec?0q-!PK-(YNvg(r=kEBfU^L!ZtIfpg66@u;mSf->wr&E%b;yfMaO= z5(`|d;ozOIzgqBE;zRvn3*4$J-3lJJ`nUSCB%J0OJyd6I6;?`Z{!^}53R$i_!H*9h z%ddbx`^e|6dg*${f-6Jd_x8aG$NX=H#P4N(bZrB`^d0Fu$40@wjmEgCek-p}2*1D1 zh#z9Ufq0fbgKUxVdr0X_#uWg~tgj6l>9+>NMr2ek@NbkKS@NLmf0O+OP&Ye8@(^py z8^({#?uKF6{xsmhnE=GK*Qa5OO)q?Yc)`^n`q8OnMpKo2@cjqF_w!C4_9w_m(>4({ z=;$9Vu|9+L_MC$`UjRjJJ@F)JRr~Zlmsc;~%5E&5@elMw^n?bJ^d;gb*qi9nlh_nR zavcwc)9Gl`>CF=FB3vI9yl8%mMoLv;M4>L5XL_$E2%;AE!GOKX=~4N>G}=RiBU`b$lRT(?^)<1o#pnDp64y?8NRdI z)}Ht*;Lq9MWJ{a_N<80_c*L3l_(1e~+#Ayh&U0JE2ZK&EPZM}LZm_gxxeIu+6;G7- zEb#9%Vy_`OM+ z))#~#5Y|$xQ+E`~Uu1(w1}Qo|!qXIWu$S4B$Ms zs4O2~d9{f?=qYmb{YBOrKA2<%i|-BdBD$eI?`JLzo{3DZb!;3aNSbxcnKHr@0@Nj ze;bVpoStYQ=4u!z1%8pMlDCsGRtk8SuQhPE%+E)3i&4EipPxl;X}^)^!{qIlb-b|f zcf0A6ke%Uj@_nMip*gZ%u^z+ged%8kuJQ5Zz@=zEJJ|rMj=I>GC(c^)cVym^0M-vJ zGAHxJ$Z@}ZBGpZAF+5{TnMi;CW6)^xPX&Ak;B=pTRr>pXQpR>^igRk6aevRsBNMiX z`h~6L^&iO5R-6szG?8b+1Mn}>Io{R%(b7lG(<{&}cHSNmGEUUtwc7o<-^q7`9238B zvi;{0ZjM7tZtCn7c!=sgww(1HZ!!FS@Ponkb(hOLyDxLQ5Di&*-scmhzfW?;^pnH4 zR?B)tUa>Um8)YYhj|NIH{)w@a%WR!F&zF4SxG0m9q&L$QPS?%%S>SMxOxu|{Zh>=I zgS_A#-^VR*t`m$#`&UYMl#e;QJ*gKZJkQ4LH~oETC!1^VcJBYTrMyKa>ULP*sa!iL zbDH1*?N@*|l!YqKHc-770|ow^ZiUfK+27-kW(z$5udmJHF}0Johv^(;GZN*+y1Agg zIbY4upOxPMY?A)0ot&3R=q^@4&<4wa3}Cy3!Lj9>)YPXEpRTs3=d^v5cLLy zTr>HtKR~Uj?;)N=#aPR5ZEKM42wQ)gt(y^bMR_0B<-;Ek7UHVJ!)eFxq(@lb7CT0n zHAHz4Gr)6j5CtVXLNrah!0_kl&9jp`b3DkCi(`*TJf_WI?MdWpbX3?P2B%C%vR(_1 zDU*qWC#gL=&x?f)CU2NbZ2OihPd=ZN>wV1@IF~!(P1!-v88!q`FXZmjXIg&!T)ruT zhkS?edhZ5CCt#IutA66C#KUEV$;RHC?;+L?xqiyrCOH}AHg7W3LZ|Urd(FNx8p<37 zxpx>%_Wa#ax6bs0mEUgCg!`SKEk7r18IQ%91@H6r_bhn0jIln{n)nG1yN8T0lsI<$ z;cTYZ+>m&qi-aV~$E1egWg9RTnrf8Gg#3An`d<636;73HJ$^WW1!)xsW}0 zU5uABmm+y(b;$e@xj7cmx{P*B@lK_z-y&nXnq)nuU&Q(+^A2#j$}xbl4@f+mpVTe~ zor06_7^4&8Gi4$XaMKTGZI$P_iJ!4tA9k5*Pz1N|b5~A|L@W`>JR<5IlK9CNlDd2F zrc>aIeg`oIqqPt+LBEEaY|FEHG&i%zSY577dHPl}-%p7=$#8RHiDWhjHUvH$IB~ANC^YVDEJ-bF*@bLC9Jo0=7(Z|B~ zEgLN5Io%+mShu(OguML>PyNHPyy@RFnXaEF;d$f7wwzp<`pWQZ%dK89S1*PrScJW%)e%Z>yJZq7UgKE59|@9+FRj6CKRB zoGT@b!mQ7XMhDM71s9VoOB@QV8IlZ99c8Ikspsp)3S2@Dcs(K}4R{$ZB0{HF+hhz* zjw=8grPSt_(vQ5cc|77d}jbCi-O^j=evb-r{j2@d4 z5>CE?NkfO+Gol<5Y!8c%{c;bB_V(jcFUbaCPGK2Dd&GVSub1zK;IvsBG8S`a>@Gug z>Vn7m<0p>U(LJT?$S^Ia({?IbQSaqw>4+D&J6hPMy|XPC&NPNnjTzPJ%SLdL4O!6D zl-JM=T&LHnY7Q+CkL&SxA{7b5b=)JTrJ}Z2HsN;Su!9z`r4#lL8d<3DyUXuqu|9Nv zH0Hl__Q{63%5dzjpf@hNq(ch5*+~a_g6&pzf9AbK-cENLvYpwKU?{E@rxHGYHkyrS zs=pccE#96?-d2$5&=%5`*3+*U(hR6+L5T#kOxGNvwMlU=A1KA3+fLZt7Z?*-RhyKaLQ5 zo9P=7oay+sL*4L@F8i@|g|$h{uNi*DhP(u{wi-dsxo;A6WELLWt0y--)PA^VZ*W9S zCJWM8SEJ1x$|i8nX*%Xkx8rhxly{nM3YVW{vJI3zSMqt=d4b9e$k6J74sYZZL;F(n-7ueDVUyI04K+f^NZP1SS%wdkWGWfg20_hRYvgmjieW zpL#ftF5Y0!?Zvk*i$hDTxDgc}`&51P+U(o6&AMpqMU!m(?HjVmj!==(7J=BR;V&9O zp<>**YR}k03=}rocC70*Zs zXY|Cn>@$ovrY$A>*7BE~p}beZnf%Fm!D-R{eEh>DJiq)f3C}M-RKoMio9in=9%TD3 zrSix&Y|07U080^x6A;6X6+U*FicUz0fJ7O6h%0<42Sdd`Twza2^A$2@EgPklQZ zwP*kE(MQ)lHgEPtcP|`1`z>StLbc>u#v8-bRbMGTw%>bvT3XlBI?_`GT)Inxor}jJ zsc6ABzfzRA{^Zl@5=kp7eCmPkY^oglyUM?wmLHzezT;TO*4trAmUc}k8s(UQzbKOh zkK-AG`;6ePs{HzE=v*PjAp9CVP{d$T{iwY(L&zIaRIf*lN>f z7=asyKVN>MI`pe4*l}&;@YxIF4S{guRHw#P~%$2xPq+g+Y8 zz6rWbfIiGBS+uFJ>to}$nsUXvB@XD!*{8uZSiX~A#~=yM*DYvvK5tUDk}i@)=EJ#; z)}4EL__~3ohwuN(>EY|#1P5nLUEyQLHx)NdJC9T*Dl@_3ucHk1k$ukR7o=l{fAHwi zk^TD834V4=v4S*?ZZK8JE$BAdoX)C_+jw1M!~7T5Pye6U>cvK{H8!=!lhY>dx$f9Q zs>`O1dZ<^4+FuP#Gk#$F)G&;F6IG1BU%~L~FISDfJv#T!h2K8($SwQ+bJyab71z%Q zJ+pnq%pbgOD;nnN@u!t*Rw`X>o0q|hK9!$!dx%V@fuD)41MQie?!xZy$IzJO4HE;&>`M&yBe4fqh2eu$1`ybtH`j zdD}%eyR9PrLk$^8HEQU(5kOYmB>o$2Y-&=2P1$Dl%XsN44M_HU`)8f!+HVO2VgbVw zK>i5ap4=Q@cPAqrCm1aF)mO+?r%eZuh!%eA(`hniKp*7-<{e*xTp7Yz=5B|k{?pCA zGG_ghzT*}P9uaRkDYo8Uen|bhSvc@W$LOPv!ySrfp0F>QNSYw~r`g`N-c`mMRmNLY zpU--1*0Rc~Yi?b-bXH-#;XK*yXwRV6LDlm`Yb=-u7vR_z0|dFOvc5s~;A}!xF{3-3 zTdrm$PCX3K-9U4WQC?STux|!CK}-&9sPy8 zAbkefznVtsTLsq}`w>6XqCV6SRTmk5R0kUM>YvsSXbqrl=T}kb-+IEhb*B#JH{7gT=@8s*#i3j{5MT!40<8mRCM3C}Oz{|s=~8Q}fS0Pic|CeL`ieNGRr zc}K!odz_2*4mSRv3>p8MK|3wBis5oDdV%iY4vhW%*iCo_yjELM1FL7+G0Z?reQ;qW zaw%?Xxt!^Nrc74+I2V7jg7p@4Ae+3x6~nXy^G1;)reYvZ5n~Dji2xlHDHMicBpHpb zAq={DU!R`O_ULK*d51P%4PLk7vnJ!ViE7zTUOe`g@xE=_9TSa*1`RfTSg^!+NgdPi z5t0h*vaQ)^yq!v^BX;ULjVr%GSkyg(2KDLl&ATEqZjVfudi$V9ihP$|y5)Ie5Z3xk7wQ4XMmqYaLX9@O_3AH53gx9^1R)Gb5Hjx zOCaU!05IA53ibo;&=?AKcN8I!L+J!^k18l%5-EVC5WC8qC-OCaq9o!cPmz4f@hNk# zu*YYQUfi$e8=nt&e&v$K`mLY6e#$Mgr<4s}w(E_z`t?;0uAlMPb9HYFUwq4$B|ZCP zJ{quO$>JsbmP}bUI=#4ZW#t{O%&1&HW=eX>nDxUef4y=_<@(v`>t8vP+Gahz@yemY zhF!QQvgx8858qZ@`pB%}9^*z0`+VEbi%J_WdVJ{T)kCkjYR%@WMxszSw7HBG3TJ=0 zcH1@8Yezk@Y24<=OYh$}ZsQ}PM%9ETJzm)S6+>^n{b90OcI-1IsqkEKa_wBS$*-0u z*N^CZ@e{~jxl8S#%ozUXiyu6L96DG!am)Kszlu&T3kSAHxK%b69Ui5$D~l!%8ay32 zX?LlUlr=+sIq3b<$hPCO?>p8No*sS>s~y(<$@UeT9=;u`xuU$aeU5czY)tnt%65kG zD#4Xr(za8slioPpk@NpXU@RvKe=W1{w~>v92RM?vKzKhe;QT+_>ofl@#OU4+&nsZ) zF^w1r<>0f)vhv=iNc*Xd(P-87D6amrSmz zy7*#;<10+N#kM;aU45rIQ@Qift)&gf{oF^@l!q=oZ_pcjJcakz1mDT)h+`e^dc)@WWNEm_Cdc`E$KQ}p0eIF>2s%VFXXzHTozU&LviTsACz=;k!7wjWC-4F{HR zADwUidFRN!XXr!0ogGnIQ^FQW*lJGK8*AGRV)o*6P>)XAW^-;M`+f*3N?%ewdj>q6 zXMlH{0e;v=NaH_1cx-6 za(xc$2dyu_!@>D4+}no(zVPzTC^3`h8GqJo07bYn!^47qs~M8Q_0A101W9`F$36%r%zoG+KFv@~6mi z4OUQ2)puuA{op=7xZ;Y+&#+5zCfo7n)7P=P<@9j<<1@fNIxT$aF3;<)`1*`~>9qa+ z@U-tZ^bhjk&Z-wa>;I4O;c<_c{eV@DVho8nVpy-G9Xh_|O;cz-xu)qfpJ0!nk(UnS z*h5LT$JX4T6|`mJlzTAQ*=WaBhuY$^rNka$pd*blI04NFL=CMWft!Vbi3WRXur;1- zOj4s%TewRrj5%D`_fph8s*>A@88doRDHM57ufi~r#kdnmf5eTQ7R_kF1@(cX!-bRc zM!V6LOuAE^K%l+NR{TZA(agt9^4}5XbrdsL%rAF|{-@YkDG5Ik6YwO~VbTsS;==q? ze#`w9c!KstI9#5uq4KF*`MrxQ<=MXYp(4UVZ!tbmd6xHx7BnhtdKr@o@SaH^&`G2#eWts4#O+3htVV#pQdLG4W6!2X8iJoDT&Ifb!6m;PA9-L#T zm&KRx_HQ0+>-KCe9Xt2v$%qw?Ugod@+4 zST=n}{Sy+G<(+Db^OCOY9o}#KPkFdxdv}=geBM6^uQumA-SjnS$}jPs*$Lk7{cj8W z)Nig&=(p3Mzn@pHq~C#L9-QdJ>_=*a1m~PIO?gE{y+<9p<_Q z;R!JO*HOKMP4LmA8>7SC2Lzn@l{DoV=bdwWxvFTV<6%L!smJ#G=;XxR6W)=ynC=`N zmPb3{Su)qwT4=5;&urt-lDzUEK`t5xx508XzH|0WSd$t3G+M+W5B1sGFnqa^ng~?cfNtm+hg>w{E`Ke!21MT4Q8Q z;o`XAZfQ;?ok>?PfDN(sFH(-y%H`z)j;c$I4I8rmwVh@BH|+_WUPLrV`9IqwH#(nl5O+m-KX z!V;lP+-^yEs4?dQ)K;Cwrd@|}b_>35s%u!$>sUXz7yrt%#cd4{wHSAQ8d_shGUZHxiJ6Sr3?{alyt3H&`esRE z@=pV%yw*$?aISBXC$+Cz%1eF5`1nUzp5okflZTsMl;z1+0NuGh?9TC)>XoviTm8*U z4t^Y7jI`H!P7kYn7I=cy%k1@u%PepP5tyG+?6yT)Swm|ZTT6HDkFteB8< zW~;WK^%h%5G8NCY>1a~akuYsMm**XtrEY7Vb2f(JH6@K?EI5sK7Wv<09q*(~LY}to zTkz{9Es$e%rECw4V#4b1HYM`^60c7r|2$m(6Kizd*vto<~f0a-oD1 zEpqTX1Uv~F#Avhw9=3(YG%lwpo{sy|GS-;O3usF60>6gPQ2QzNHbwG#6e|}vdj)F) zIl^dl#%I4_&q!v{IsMuGFM*HxXVI%2ra$^9E06k*_IVriZ~wO_Pwi?T+spk6iZqpU z)lEIencdeUemZ+A>38%(3m)!+p?^4Ixkr{KJqeg}t*(%8l8v1HZxL|PkyuVgbkpwe zTqLNsZZzSq8)Kaoor2Epa^m=f7bL!MIBFhCYiOW4dKT;ZY=PxEzR^ z64>4ui+Hv!Fz_`EjH3Kvr9J~ki=}glh#su^Y|*4G(At_!hxJq>9#3V|;s7l=1R{~J zI}&lbvN2n%v&HVt>UI~xNf1)f5k&g`Op{G_8;wTPrzQ$p5(&yY(i}?YdMeNqO@|Xr zh(HNPgU*E0t=Te(IG*T63o?`-8xzu#q>>pu6^E>Z7~jF)LlcNTd)^at=mysrTq$7X zq02H#g0JGRu?ypwxthaPbXayQiqt~JVf^e*OQtq0<4#5m767dZpUM8) z7k_U2be(#m@jBd}C$?-E5yHKrA!9+oeI3s`3cMXnh4iCO!IF-R!xk7H8|j(qWre@k zd2;DaM`TR)P2-jO)QGXT(`)~tGhBFXIOdN<{uw(MdpY*KSi!+%M&|YEYTH3$(}y3b z_b~m5K(At?C-v~mosN3UR2ZE2!EElH-2|^@a*SSv0S{UDF@<`Gs7I3{IUfUmagG#@ zW*iwkun!3*d4)v-ZDzJh_#PF^^;6s9F1Ol%o4rFCcBzs!ceaInA}3S0ts;P^w{*6# z*h&5%eUe7J&>d3au2?#fayLeTn$z%wvW;#-38@(eyhY9D4Isoms^c#4c+jQ?w79Ay zqE06=HOFFIN;KOZYcN`Y!L3CajA$UP@xBMqM`r8zealDsT4=(?m0DPQ&;%NeF9(Zr z$6SNub{hhJYqWADQKg{pOjLn~TMEZ_j7sWhe%RKxxXdRNGSFF@-|dZSZne!qjo8U_ zqS>#uss3a`)+5E)t7e)~kyNHL9!<6fn+uUYD2*F<0wEoz8E~FKvp4zT5#0tF1+z_1 z%U1p=j75_Oyw$=@Q9VqyJiz3^S5kH!! z8go$G>SV(GpreIe`|4$RLEjSvR9>FR3;}LpoZ zKF=Pk6`+;B>W-iD?`V~PYi4^6VI;?Wqcs8^0oI{>cYV2HC`bF)u$WtOI@KElxF!gs!2g*;?-yPakSw0f91s_NT8SM_iU%(un z)kH18Gue+l%wMSs348(bovj$QEqvg(K)-$W5xjbcte@%td=F+5P6s_@_+uuX+Mnd% zA)CPb!Xs+{FFV3^P>z5$XcO@|MYs;Z`$N{%6|xSpFX-bT8gI+UzJQY4Z;0X@#=jF6 z5mbL$yu;-NV=iy+PPC5IL3Cs^NxnhlWqYYztRK9*-ejqp`Ouu-ThU&86^q=m3B+UZ zXsq$b$9Nku^eQYA5y|PXP0vPG7Pi@!thDu;C1IpfK7$-RRWj#&VA^2h`V{5{VoKsW zs29hFU3?MPLMJm2%@+wrwboQ?9Eo*3ahHYzK3T8dXVbG`n?KvYK22Y=`m{*QUhHv4 znu2MkidzP4#r#tkibVWDT;$?E*%((7et)PXjT17>;b2oLi{Zd8<^ zPiYG7u)FUY#*laM*R*~2*PCfC9u1~aEt{Bf`K=1NE4@Eu54_KwZ zD`q?+yAJqKW@k>kDe%#`z!WQwdcaGxj+H06#|fUqEF3c^)(@;OMPYa0>s!36a@;Ot z@&t=c#0EGW#0qe!6dzJg`fYk#4t_q5v52pHv^#J`sNJ2+#!}f>5`J&Mo>H3)PiK?b zW;pFW_ID%w?JrE845f>l9gWerihyFTSILhFr@Cg3rao0Hl^ zI^(D}>xgw-$@@U?lBq8oQd)5~X}f4w9_{p31uovd2+kCq$6Az;aPsdYy!Imj=en1p z$14I({mb>+?B5Sq-F)35hVg0>)p6vn;++`e$kOjxHjY7`9)muC7Z`o$lv$}VM4Z^W z20n_6f}=5rPkv$ZVC_Z^gnMkjo ziO@&;Pp+L?aognYEdS?nLNimp07A(pT19U&@I$8wrCsX z;pZrBjMF~c%Sti!({cW04$Z~b(W_Eg0x2Ya&QXM0uFVK*O{r+4Go!Y-J)PSf?yS%5OJyVRY$S!S z5?_p{hAR^kL!l37hyVLhdMKWBrsJVRO2u7`nOMet)bC7$^l)QiBo_09qk2O)pvNOI zuNI2MLxBQ^uF<6nDMX^EZVfk4Mj{%V4IjJzFG2UM23i9x&7nrnm1H=^WcysmW7*zw znB;#?=n2_=$bHR&1SPFt@QGamFt!lQX)G$=-Mj1k&N(GTyGy#-I;X*(z881m#vBt7 zsd)+FUT1;gbCm_k?PULZ$+`YWn0Ge$Fa6rUufvsbH9=xcAOwecvB4w1;uoM6BZ6nB8G3H6lIKV?E|CzT?q8e;9IU zPoqLpA2V(-`g}be*+(0*mK%TEUc7qjQg!wA^~UUFMoV?^>MNG0tE!*PduHEydghp8 zM$Rre^zRCK>zEf>;op&P@{6SXsof{w++Q>8mEh^#vxWWfWBpRTqko&;ftG~3DvNfo zbt^Y~Tdpk>uN*>V`Y<$xjw+UtC4sAi`@Ois+mDXSk7kRHg|uh`j(!=BOy|ooFT%)9 z#;L!mjvjcSZQ$sF*{WOZHO%V5&JCXjfgh+_OC%k0cTxDRe6go6htnh5sY(hiYmOp2eVdY>egO zmfk>Y5OA)Kd<^=Q1QVvPn!we&X^$rq3AV*;exGJ*z_r=UY~Q;W# z9lpQZxY?L_^>uxg^_i#+T(%(apWj^Acfr_+#vt4Ddq-Y=`AFlFs~yVXk;c#7?(BS9 z9X;|VH{G=0@;TSt1Tu>;!zvT|iTpx#xozRRuM=>xM~yUgANgxD#_mHCuu^X7N5;ZO zQWJu6|1jjNI`;gG?c?sa_#fPx#jTs zE-V^TIN$dQKGOVrAMIt4&*q%AtH1I=*FSyNXv3Kd(Oh4?le|E@!`D)Cy1hrhbG|vZ zPqhNR3~kyDJXQM3h5vwupTg;p3%X7kHQL58f5oikr^@}BawV`Z`JBaUZh7)jMaez} zc|v;`EduDTY>QrFsgu))+v42KM*d_xBJnD+e)go)+)c(6z_$&WnuN_ z<=VnauPd2G{ZwB7bPOvSGWpBAJ=5Z2ZiI-Pc-KWmk2@YheDoNL7cdJa_wK>{G)tAJ zvRqjYY_!J%L&_FTa83_5$Cw%gYf4%~^NBtP<>^z}2gbEjTy4gtA~eOPZ68ucrjq1b z3}Y9T55{FR_~!bHr)=@2yJ0QlT1k#NT=s&ktDOC9LoU1G>dPLxZ0_7EuGn_f-QG)j zU3SI50~4>hd1dw+wmXcIfB1vi*QhtvU7_Ck```b{K4QtNxwqfm*;*JniO>^lpqU@L zNA;>6vdxW#7oLKV_Nx9?$C&Ie`;eX4r7vce?6eQfjAkp10BaIG-Hsa$(Ors@P#^`B^sF%^Hb2ysyOQnzOg&DEo-A zOnE|Cg;=Ghm1mU~lzj?DW*R9g@Zl?^#5r2`q2Cy7#ivx*Ybr-_V}4FI%BnavM@ODZ z#qV+m6Hd&~BK@FJe9rWoo|zvmWch6&9QK5vshMJ00m34zkx=6itf?BU5mh%5E_jiu z$|h&J-jR$Ivze`z4ZnPr>tSny0XAoFWA#3f4eMAy)CvPl*>(ge`PeVr?&@q++ge)S zP=+&^Y`Qs{X>C=*>{raLb!Kd?mKMX;(xNstHX4D(#+V)19MojgXpR}pQQZH59q8s} zdqivWApw}y93@XS+N?Ee0c;INoNhH42?PVp$kU)k!(O8W_%o{8`$f`+uLS)F!k2CH zH~Rg41Gg9nAl_A50cyfW#Re*>1L9b-W~BWJ_TITa8^y^+o;wKhIocY@eG^aV8{;nq zUW-KlBQH^*&W*N~q^&LM#MW)V9%yf}YuS)3nRVkfc4YR7hILOvFqGAa3%01?aVXI! z)=F`)34U}$j3Dx)xI!TW6$CPBw5_d;xJS zVa>w&UpyX*>Dh?cB)`#QPr=$F^9Gv5#yE}dU+&*e<7nz(YA%CQ{yU5l1VQe7XrZ*r zY~PBq|Kc9Q_XV7fedIqS3BIGiQl5<~98c`>^FUOJ|2kn zAV%e-JUmqXp;Pd%I4F*1&l*wwLF7c*;#h+`XFlUWr4ZRW`+wY_*qE<@eL{>b17Hsj z{Xir)Ka|S?2H*S)vNa`>@QB0a{+60b+I(5I;BJd%^`s-(-mD_o7}hod3DsvbCX6&r z-6V`CqhYjb8fg1uN!78Jm4%uzZdA$CgY%WoHD!F3Jttc7@$&`hlWz8v?^l5a$2u4s zJE{0`pzSoj^5mW9W$V~QVvwTqxfEGKpF&H&!bP4=J!lUkJDf?I7VK=p^w5JFd>ZLz zwjEI!ar5KwB-&13uy$V zM0sQ>p>1rWq}3IEUbxr5f+rJ5X4n>2wQP|i_C?HrcDNzIEgkMQTYI)RLHSP#KMNvH zU97Pa(K{#GgTcb$jy8KFiFg3SvIxJ3$xA(Ghdo?!+X+tl6cKg?H(W@1(y027Ri{-~aUE$Qt7kG9G>g%O+6$dLv69+Lw<1Y+ zn!#5QoNo6KaMC@LukBB8dP~A1to&*Q$6FE}%Hg3~h$Os*!DD1QPLy{c)Nq|oh}zZ0Bp(~)>A{W?C@0%z;5C#YX#wJN<;Ym@IlFF^}D!&!pA zU^0>4CPs_;e^_v7WS{DPNVrbK2vpINcqL{Ib zfIYii^Ikp^Gqx3ly)|WGuRI;2x*VG?wcxYxJhflKX}k?!zx#PcssBp2&^@jj`!A7j z(gCwCWi|hyw`h(g>SlFt{irUL@6h-m-a&k+ISows8C!4KLvtjYVwyJ^@c2W7LZhmxfq7@PzlH#;Q+2)+Sg9@b*(#p85?v+xuOD zTX;x1132*zqjZy{e2yND&7Vj(*?UnQaZH;f+@x!W;g99P(azm>G8<)uGacg^zh|hW zJZ}$|^Uqi>KF{YvI($kUysbpo+7)5#0hEV;E z-E8b(Jn|Q9a6E0w`p!PeE~V$#^Cr9_p6P^ZFEk9ajahI#>4-ZQC4OTViqp4IQ}90U zB5opc2|P5sOY32*ef6*5-LezQSSw$ZZDDx1+-=b;c$podjQ-GtdnDZK6QkWB)|%bu zw`ilqcGdmuly_o;ONZ@}<$p@|8l8w0D3$MMigW7T8y0_5`J0LV6wl2u5AWeFyGHpQ zwE^Kmgu z^%?t=AIS2g)6#D24oSF)pUGYgD2=akna_;!a=twgSz2dc&TY=IY44Y2UX(*d44MG+QeCRLo)km`{ z=X(vJlw)?0v8M2;a+ms@&T9{^J^Yk9DVucJZp+48w)fl%7FB5duB&hjaAtcw^71ne zuT}r#bQ^CTKCF&9{M3o6a@YN?0XJ1RA9s2txQicg_IvS>5x$3~buJI-4SFb)z|5{K zju;H3y*(LHlMR7zxIOH+^M)IqUO63KMtJ^{RmQdNp0)3+`Tta#Z!m1e>e=dx>W^)& zY7?`!Y^Sfz4{oTcsJfwI)6=D!p5DG=czV+}hrhe|nsnh)Nqiv`J!yFGt3mba2pmbO z?o%<-*ClJ`jV7OAy3}C}lQd zgZ$AqrXN@Vq=`QZXT%n->Q)=PDW}U`-1dc{wrEDo=0zrj)((GR5?dL1HcH96lbvne zcob0so@7C&BWd#)O3W53Y)#-)pV5M3g8_fS2pf?|JQ>F-NE!jldbTae`an9)$0X)A z?>23D2(cG&qS5ZVsLvL-81r`DPH>B#Nd6|ur{FDMjt(D7;%~_LKlGNMT>xuhSeLCW zdFJ=HU#+(+FUDpniTh8?v6DmC*a^5;xWqjZ)tca z>UhDSJF9kmbBa>ALBsRi3ouIDEuV`qP4s7>#A%KV7!|s;jrC{mjosjMu7~=6Yd5&R zh~1UV+BWUQ*^t?@uZ&G-5^ZC+*fWQ;twghZq^~b6zDybaz6QSJ(KE{WdfO@WFupKd zn!5XThcd3G?4u8|LTy{X-eLL&E7gp80)U zHwnb-N^vyi*IkK#$5Wh5B@x8moK4}xgRd~&+L~xdv>M(n#g%aBsw?ZaH6#ENHY!4L zc=*TEmOa?H5%M{$Xf4)1%J$9yeCol;SNEu_obZTvCUOBBSxfI6eqFr7U;U6)$ zDGy|~n0;cn9Gl*ibqGIz!?_;-I>fpXm_r3HSEp4Tk>B5(qtkD5e$Ic}S`un;!sg7j z(Hg4Fg+Sx*7m6)JD?bLUr7AYX5%{9n;h}RkEpZ(4#lN`fVgKB5(We9-Z;!u8ZLeWI z4pyvb-3B%sJfXb{YEK0jFw1uZoq!rsVe*82^AlB6#y*nPt4n&1>D|v}r(i@RSsHY) zoox@=?2L&#x>s#U=y5e*q-iNi4H~I7mw_I6GYzzKV6-5`YLn_UnsK24<>s|{oJl8c z6!1B5b$lX}(tH7@?usCo#~%tt0|9r3AIr+2!j`a>(1P~%&VbXNjN!;iEE@O5qMk&t zvk42O@u)3;fW`(|g2vemqon~?6Br4jy`|mNmc$V*+;JaPJ!)gTL3QI`Q!++6%IWv&97&C;F^r48O}8_Qbsn zLBGqbJ41m)G@6Vw#XvEyL(zkgP^_c%>#)M1yMumzARN>}S~#9@>;7Ok=nwhBIvkFM zwnp8EWtv;*Xj>!Bdc|?zIIX&q@km1hjvcq#oCvl@XsGLqjA8bO;ufG!HJIsw!px5q zG=^=*Dqeq)16g=+w37v&%*^`=@q)i5SM|8I6evEot?7%jt_`dSm~ElhFYc?tp4B`> zW7eVhE{Fo)5y(P>>|D({YJbgoq4%^m8)I<28+=mHQw$EfQsUS*dqi%HB1v$5&mYwy z@iTZ0vqLrczO!f#>Rlzv(-=)~ULGSp!!r%-%+Q#$<#%@}m0LB~yPBsyR4Qj`{5cAjVV53m^QJr0K+9B2Mr&65Hi13sPnqM8`%p9$ywTdrNqf5X~v0`!PFH0X?zWhJ}_ z`PS4)aA@&#N2Wjv1`TK0aIt@h5Imw_G?WM>{EVMOCk^`q_0;|&M@7F{(M|?G%vO*j zoYrw!jL`8LSov>?@_Y>rcn-4lC|N$p%0sS?&!+P97NaMXPgA|MQGy@)nSgWre%PCf z^V&o7f59=Kq-?uKc@Jw&VKUm*_NQ+;r98{owD|`lU-hVkmG=Wriq@g`kE8M@ay}sS zi`vQhRg2M&mFMjVOZdYA9yIX~2CQm6B;ce641ev?Jp3FF>gDD4PZH&+Un$CoA>daD zIF}!S!>U@^c^4}mbyn@X;caE?VDeAuqfWd8TIl>4*3q|6pDlf(I?y*>$DwLbhsI)# zA=9<SW%!dOT(tj60e_U$yU&T0LtEnnx^4> z^zllp>#+BEKhb~QpZ#OS`zb~*z_|?8ne?J|Gy3kU%cBeR6Jvvkr+T2kBX#a5_-f%3 z$t+@^fFBgWo}DQNoPVIpF<3hb-3iWQc^^DQQw9+yJsEp;FFHc-m?&S%NEuyzJ))_Y zE$cnfX~WV$3GLf4|HwRPPMMi=d%R;tvtw{Pb_$b4eDm;LxzA@=XIN{Gb;e$O?y)0_ zpL5^L9lAFh4x=KDXsDx>~3q3$tmG+uh{!NsaijdUg*+RNV`Tzc!B zH?6;EU)6I<-!~3a1$wURap6ey#`l*kNd8Ri|6Ho$`vsFy#(2cC=u zBeC0_(?|x12W(DRyNzh4KP!0SXF3O3*J37Mp*6QB zALTc{;{=aM1DI@aJmB%+E|eD5;@?`wpMq(H)%e(elV-O>(_{xbQhy^C;h zY1KEfccP|Da%ipDE-sVM;Vr|c4vFhFcD5*jHO@KNpDN1OSsH4pMW2`rCpm`PAKRSw z4%ZW+)rLHHu8t9TaH4tAv8<$ApR6eMNjN{NA|uR`rX&-}VRN{CR4J>ZtiNhvQ2heEivm*59)Dx!P#0@$p~R8~+**n|{rn5}X15(*?G_8%JkX zJ~v~=8zTo_{qqW=t>Vh-mvC7oI{;au)0&oXh3Fb5`@`+Y9?WnVKczj{`olbVikh;| zWhMbRVC5qW59E^Lsl8XgH3ldCFD5=qdFA{MV2$F%JpNzD%C9IX-*de46^bRGHy~dj zCdX1IYVR@M!ZVUFHZ~r)kf74WN7y@%N#52yheVsGJkufG)|w##&ga8uQ_Up8BiBbb zjnD^P{>WZYUfOhy|43TmA^fPf22{XU3hNp#wEz_&>` zyYhOCKj%C}yQ)j_`oZY|e8)c`IJJV|S4`pMVgcF@C?XB)5$_Mjh=}1bzslBZH zt|@u=Nhag8?$7DB?@ft^;E-)$r=QNl13Sj)B+nxAwL;B=*JGBz+V4fILId%wKHS7~ z#M#2&-S}Tkt{TZC@h#29?&Z|Z`eg#n`-}GQdg~vvlxOX)e>AUOUf^N6`O=I$-Sn~k zqCE%4M>)6{5)2_F7({4_ajf%hj!VWc8`&9 zBkJATh74^Po>GS0R1{F7X*%oF5{aclNmY5;_?=q1>V=-moHJ$={^!XDJAY;${h+b! zNn_ii^wYR@-h+1|5&hj$o*Z7VdDW86*X-k$7%#3ewk)HcCuiKUWbutR-gwWHRTOW` zWm4L=%|(J2oVJi{u5$P|%UoT~cbQ|re zeURX_59GBMGJX(t-C4HhGWJuWGI{#Pay*fQ;@u_UU8!eWPLCWUI6T4_+JHD8$k#U% zH`ar&F2N)N@Yd^U7Xk3m#d1zd5h;OZ*HE z&4O8Z-Y?LBX0jGM5tQfcJVbCwA0O*4WR16H+cg3Y=}ekra9=X7yqs(D@-*YLv=jAm z_z|=Hoc_SW=W5;h1vsCpNI0hh>?V20xCeN;z|Z>&JIL`@_Y`p6-;n7iA8u#822UYF zoCo3=N*IKZ-p8|U_~hIz*^RDOih6iIWc}pf;;Tqm&@}{nTOj99li6K2|DR@0g``;Y zp`EloI2&gl={%d+?h>9sVmVEnK6v)gLwCm?P&Z-flu>Wn>~gNS?xCuU4u_vL7=9R2myxoEjd-y$k_fakh8r6FNP!u}^6&u)1-#DK^up46 z)JiqbnQ_c)c%`&7v?#dY=DI~MJp7@tchU7dSNFJZlzQ`rD?+>9`UZjoW1Z0gS27+C z1h7h~7)_WX>49d|3A@!0Ye<8(QkJ>wZ-IAjp?Mtk^Cgo@3@&*{ydh3fwl^`?k@Ca& zp57{O@j3#K2d2N|K{8y@e>?mdtAp_gZHh6z`k@!!V^V;(Sb2^ga!Z~Y^d$!uYZV;- zk!_+p9|It_N8nkZJR%4zeIPhI!#p^hi6i{`^5CS8-2MOq%3}s4@KAZ+5%rq>5b0xr z&fRjqO4iHe82#e@goIy?T&&-6EJ1&RuXW((&2M)=_O}e^16uGt>!^d(ePq9=J8yim z(1LtuoyMIbN2skdDhXQW^+Ajs1{6P*(Xl(X9jgwsk>rTu95rsNz$S?|)RAe7M*;=jK$^15>CGOm zj?>XDw-GTsZb-APMpKPmHQn0WtVUWL{?3-p7S-)BT5(TyxIoK>vrR@Qs4A+ix^3-k zaj&~En{Ided|E2w*R+5)=~atufd*G`BJ0HF4K{FCUpbvY|D*6+k$2MMdl+MT6Y=HH zxdcCS9?=Ht{k;5QJbLbOQ;9UsktEimoGwiG`|qxNztbS#?on%oMaHi$@w z-oOPxbS$l35sSzeBd^7x?Wq6sm^_)NcG5+z#9Y$6?w|RAB zX*Q}R+C5%Z+!G1m#*bo3_7~1HB)#oTzDz@drg@_QcdXdi(xtd#k%kCm0J4RD)p&>6 zL-nmtd#w26lNH9lR*WBa{P-Qm3vTI*U{gNh4f!x;$JAE4(wQ{eMvD*oBW?B!Zar{E z-TtVzAscLkxpAk{9vouy#+@$g5aNVvP)$kO3z_A#)YlQoBpkM1z;z2eO+I;Dm}p#w zRSZ_1?@Jo;@h&06yZARq<5$$2~W_P8odLb z>#00A+FG|bS03yCRL2R&LhR=Z!7a*^F$%3hZUJVv%f}!-hwT@_nnS|-Ua1xhat~&g zne~Ry_wCnfkp2I}@Pc2hzSK59d&l^B;eCT__4BTs=ez8Cs%B*F-@5hE`>}1%c7t#3 z?87})^tj;K@O$b1y$m=O4u5#Vrm8u=``Mb>nRiyqG5)y5_|&!O-uu?vyW(f+TIYc^ z_g2{AwzB`bKv?NNPtbqb`Z3DqN`yI6e7JBp5MxbU6!h>V2jMi!*~f3a_1V{pUtW9b zmK}?B%uy$O@|0`Qj=YI3jn3-qcRAG!94 z=l(GN;-j$}@3+74{mZ65xNu^xxpP-7y#B%I%btFB?!beOUBCFhona$nH2K0v_9t{T z0bOm?VEo!iR}=1-TZ?*e9H-Ug^O3GPO|--noEiHq#YK0x!&X# zqDekfF1aE#ZnM;5=#0=M?A)<;q`vNYo^b6l$2_jD&|{(ZYr2=;4NCxBHGMu_UPCO# zZ}QsYWA=dT%2}tBXKO=bIiTCC1%C1mGqjel>$y|PvpIlh|0+?Q>cyI}?AJ;GC;x@v zIoczK2m6Aeo$w8LM)X`p&~nJ@2t@Uf)wmb(iXpFp&b5i(wBFvEt+~_LIht+p`+&{s zKL2lnMZpX@d=iU9FfYRpid+W9kJhezW$jvHntej&t9|K=S{z&6T7yx{er1RJ@9jt7 z`G!B*6voXsjp{9(bL_8lUTbfRXUni=N@CWEb7uwh|9=<{$Q!L8WtdHegn++KTKL7* zR0JPm7I@Mz+gYVAyL-^MuF7VtFk9uCjSu1-7B`2zl1YrmQMT7R6fqJMk$I;$?+ANy zev#GS(BM4A-p^hx>`jq4EDK9CUNs9BIgq%Q-*7ozEoJ73|5R+iqY1uax^`15lcZe_Q zJ#+4~f-j1lPFL8CkPK(G5!uUZ2wMrIQhoq6ccj>vZgw@LQZY|+*yl@Wjwrk{B%urW z^>i#9YmMvS1~vmpx{Yrnk%Tnt*S!8vEF6d;^rBt$MnXOf5g#sBC=!hZW1(QUtf3g`vqkQsF51pcFg(Yk2j2kYa)kpueH#Q+paU+&46=%TjX=ri7gt>LM z>a-=(uy36yzQ`nPmQ{Zr7xF;9HS;6LBEc@v5`=;^Haq;@CdXo|fbjdOgW?SI^U6Ub ztsGar1I@MdV)@F5SLJ1+ofSCx2yRnWlfrzTjRyY;7D&sO#jkx-ZM_}+928>2_87{_ z>Ez^By*-%1c`5Q&E$PR?lWbJCooX(}R-EvvS!^$5^e<&$2#!JJZd$~lV`?~44G}7u zdG^^o2Nh23Kfj{)KwI{cF{M-HY_MHE=#>Kph7G^ynhH6|GgFCYz5h$2LgCy`USr@<(0l_mGK{XslZ#gf zy)%8yPIaT#c=M)TKXm={=haE-;`OsG3>lw4PqU;j<};B9r~6W8}XZ>I5YefpKdhn0+|`o=laKpiSdY}Id z#fEi<5OQ94G5^7ssXBXioBXKc*CQQtQQzL?1-I-l`xr;q=ka-qVI(r>i4wi@mJ zmZs*0U~^kbqn2uDqIJCxi;dH8JI8qCdbE!G)LaJUiZQ(g(UQZMlHP;Fvhkqu!4(6^ zJrdUq=FLv17bT(>CB4y$-eU?T*q%E4x~;Hy!TPJKimJwr?K@enFS#Z@ukJ9uYaF|KbbxC-Ncnw?0$azW7}TAL?mc5_;p`8hRj>-wkDix z^w7+Nc{@fErhxE@;SV)7G-v#<9lo|^1Xm_7egr@hoz4#J?N4h1y57{n>;=J-w5PD^ zS+WcCRQNGIzBab&8D=lcc?ZV>|6s2xuRO;?aE8C;(l(BVmxtd&aEx0k1b!ONUZt}b zFI9Y^R9>bj&Z*VsEl?^eH2&V5UDp&HLTqbU^|^%|&S}YrazLFr_TiBai?OhQe41+J zi&V4usD#G=2LyFtw}SPB%R?A&HhlntaV3k1Otugc@Vv z&SJG89FD1;q#AFEL?g*Y#7ed~;{mtV9d}1V=~mpzf+P(MI_5iwvkZp(s^6hCgu65x>{SP=Kk?62#~d@M(HL%cQtl98hDi&a^!cMB4l2uXm;!nkp}RYU1dc-b|gv`%~}tYnye`Uzk@rJ4`lZrI8wlv4$ zX)PH zgDU}Sn#Y^)d$MVJsxyei{uHWLl89Z6rc*&fLBi8`(A^rvd08i}14yLXVUQ5KkE8?z z4FSy?3@4CeobCWfW4Moi5B^|WErwqYdctqFN3g!+IPt2`FN#k}fbNKdK~~{&1l>^% za2;!1#|eHUD&Zo&LBPK);Y1gdC%lNCiAgxgw#2hLH`kw3^gDv@#ath|&TA%h2ptja zsV-*iIa}a~!arc`tY-6#Y7;KoNtp_<#{VNry*g_TAn2FF0*@1XJ1hyx|B&F|>%-vd zi;g9Nt5>tEkG=YhDJ{ntXCyJKfR_ocf}Dmz4CnnYtg~-b#F8@R0z%nZ2GZpaw>QYi9a3j;K)2 zxo^UjifQWH&S%R#1x1GMSLy|GjAuSHivMoBJnh3n##6R_BgzriGwZ$yk59M{Yeb2V z&mRxBLA?_JEffre{9fCrf}r0Qjv<1vpx+~#N8Nu{WbLT%+{ecaed6&^nCPQm^^MXxDIpr)_-5TNBdc_Kj2f1fD@m9&(!Vx|F)E8wxEv54c9$^{|8ilS0D6e zYTb@yd<85=-DxhqUb9q&ENqhI~dPXes{^W%J`>au7%9QT7l7ZjEASUZfPOq0!=rBHH8h%~T_tUG-W<3!^%&unT- zd666$xehfo(HV2wgPjqVyFfEinI@z$>GT#i!_2p~`(yDyJ8r+QDQ=hHvAI3jh>GO! z_CPkGMbrdtAMwIqbf)Y+x(l~0<`3%8AXb%wzJ??^=nn?`zK|Y>c|CedBpOHLZPN`7|w3M>(i6b2y)Cb*(SQcS2^B99IsDV z=*W1uBVWe+h)GM4|90d<(6{W!M?_`HB_Vjl<$;X>b$3c!ety=7_=~e#GX$LL3F=Od zA}q&qdLqV*-9O9mkl`0&50}GzOS|Jg28p)F2oY2s(~ZpKQO#LsBW>)m16J8FLe z(^p2%O@HoQeoGar7f%Hq@|EMz6y#eldFPnJD0#@6cW1ICR%0%TFmfY~pcFJ^JmX1>jxj3M4w3ERxLEylKNjzB`ve-2WpX-n)7BEL(k4$a_B<%-pt^~7guQG_1A7Z{ zCFCAHr}>QEcdJ8c#j_aUJEYMY7Mp7AzoQnK$I1^v{lDNe{u_8z`^f+f=dlUEL;f4u zh2F{jB}|@R?As~e6}T@V=~#zzihb<8$E}`lHO~C~SH2T~UWoQ$lsZIGQ3qKPvItqn zdkgw26NYGbLN&5ya(pMfK^t=8EXCpgUjp({;vfo1T#S~;c!=_3T_l|A6YI+^bN)rN zk5XSSiz8Z@^%9)*aeIKlO+A+IIe2I?T5)4&63T#v<8HUqaULEhNe;s&nw$rwM;KhwA?&-B+ zjq$GW^GmO}=8nIp_ZlylADb_#VSTkF5Q}tV)R+g?Tts6Yo3BB|27BD+#!0*;quuK* zZg6`1_H<_e=gFeYEzv+&!_?Yu#EgjJ-qNw79{M01oc_@Dx30cw#n^|dR$qAG)$fL0 zx^Kg-zNLeQ-gn>Np&?^c&rpvI51015uUJh70{@zBbX|DmBa?^SK3VT~k@3{tYaYGf zTHEZhzE9n~qqMAS^tj2l_v<&gsQ9VM%J{f(hX#zAbi<^{qkCTT;G^TDY{Zz%Fx@)x zDY1>os*lS!^Whvkzk;w&T!xvg-91{Cr!!zA7Ytsl3OL1zGTTuNfV1rA7?Ls_;~8*8 z*)tk9;Hv;?wF600ULi|U1TJ14cB3Xn<>@Wi2WC&WKGs041f1f_s7sWEuzR{;1#R4WlAp#H8D@?8a=kyF95O%#-zX#ajIxtyb7{ z;qoJPs$nCY3$&zsUF}8~HzMO|IVBh1+25GvjR(A_@QwP3NtS1*N8zQ$lU8>+713is zJ)t9_(dLOH@?iuW4TKzV9$KCuhg8?CmNtLNABdP{#OzE*V;L)Aq*3@WrF&z6gwM(* zyeQO+cMTPM`6VfNk=>wbo(Olm>J2Xy@+py{61ea91wgbc>+6` zU(2-=d7VN}&603o`}tb#dffpp=t9rN^%7ph@0({!IK?r+a}6v5j@v55g{Mx!rQN9e zej+rR&|++DhvU-Ez@hvaMClkjwBq%<;zrVp1uQL^j^P1iAvp8z364ElSi-~3_WRrtPQIVOqj}P{ zZ86w;GX%eq;Q=<$uc)y|)fF{QL!X3)A>R^Da{-Ux9M2&;4r@SW`1t4d!xMt-mw5JV zmiP$|WL3=T5(y_;OYN~DMdP6yTWUYEA3W?F5AC$YybS&>{br+IdjoIQ0YA+YcW^OU z+1@eEC;sVx^LfD{_8Si$CpgdnrySQP@lP|f7}m@7E$9v|@N2cr3*0A6A6ByxeQ$L1 z!Ev#*Z?1E|9b?KA){OQQat?Ao#{DnAcYBJ)L_CzX+oN8l9jD=N=wr~L2jdyy=$GN= z`!UHAlt-9lYs2QY`C{3g)j~o3EA*m+76xd+*Nn~UP`nLa0Iw4shM#DJI(YWJhkDwQCla3BF5BC@$mX>PXGUT?f~OB&TGTJmpV5=^;-2dzo}ziBZ;^1C z7s&t{Px?uTU+99y6N~WEycj)s`>jul_F)$H!T9NoWvyZ}WKZ*$E9IOyFS0&1W5fb3 zc!=X7?TA0T9s+2Azn$i|s}I+V&gYo{q!d18{&+N%4p!~Ej;>b2m-g#!e~TV*1#`&d zaiQd7ISLJVeYx%uoa^8r5VV(0^S z4be)3i}=wn&A^ig!8oQ+mj@+B)MUiMdp536I!AR(QES48su@*79ZfCY8gOOs=2mx3 zqaP2-_u`$J)_5k18mC4sjbZ~4R8Hb`8AuQo`+?3!fkCw-4=v%UH&gZP} zgEJiPR>p@6zM&XD$p!1TS(oiaT!YVnq8k!FTN5$PmmF}@9{)fORgM$wDIOv5Z@1%^ z1wLo}Hrjq4hf6xh{T#s=kMOz%hd*}UXY$C$Dg8(C2brJ9;f?1OwTG^6gksYw8ee48 zegU7Bv$qms9v$acTrMc*ZZdmykFp#K|2RtD(0w9#-o$-M92uiRJpEl%A?xT5?VP|Nk?JLTKIJ~e}oq7lmlGGRPa>kgR6)lIZUeOYhBrIhG67{@>P z4y(hL^`&t(mc{>OtTz0Z$*j?mM3t28)^G;^ftHNc+7{?QK!f;BXSySZ*wS${+rz^J zEl7?cl<&pQp&Hi-IGr`HoNivvmA)nL7&xE6T5>!MZ%eq4Qx4x}^E<~A0j<|izs z^RtW?;13bpfuHO!@d{c(1|Agn6BvJj;-8J*X7C3b{j$6_!hm{};{`mx?(jx2&UIQh z;wQ&-;{!$qniph=x0m%_X*^-dVdwpBv1LrsLBL;@{gNLPN1ohYjv9ZO>H>)NHcy+t zAEohcfwW*=_V|N%)8+ek$Fugy*z(fpr|SyhZ(nnj&T#2`yc7$GP(PIKhqIa^j{ct| zczx~+(jf)9Ql1##hhC?CB%IbcN$cFyMDQ(S(rtRMdGPTz;adz&{iX@ezE)~ay^Hob z@B_~w?CKo&iB3tP-xelKMeFIne`uS;L+b#(pq7Y@>~z3mj&&fZJZhX+&zmHi_yYZI zH;cv-VdGEZP7bsd`2AJcp5}%2yx%Q9cfiAp&TKpfr#RqD4mf>w%yPgPeK?*c@hvva zIQmVIj@|M+^}FR~0?yYF{Lb?>_c+JH>K1T3bx!$Xbl`AV-gG!H5+F|U6Qbw@DGLTu(hNy;hfIa@3i&~T5Q2C zp0y`jX(nsn9`O7t4tRpmn!%g)JK&guR(tSkZe`l*WE)d|7oSld4thNCseqGB_#xri z^(zIw@P@H)_i3y|r#SW9_+1+7p2E5bTg2%~Ct`ALEyRzzu>wW(BYH@F*$sNgxiQ)P_s*Zhv{0*16!UIxT5eU6T~ySe&xkjcVC39D$RlD zmnArFpV=V$CjS<5&J1Q_wfpAnNu~h7dCxJTJ?VbN^ZZPSW-jI&0GxOd5UkI1iJy3b z@H4-iu!->y9>K!{Jq_;(IIT^T>^{e{*WO>ycsTC?1I~f4Q)PH)uf=5T@U;ZT%Gq#g z4;{G^)&t{r>LNyKPKWhu4X~pp{B^j$S)6%11=gC*8ZSof*8)~EVd4Pf<%}9YcA7Gc z=|Tz5LdFdR{9VN(E{IyPpF)}#m9$e)3v^KB$kSXCD|u9hX9tmsjPMXM^(8Gydb2{a zn(2v^8oK|HG)?t~;x8LxUN*@dm8U zSh+tQ=m}`Ca-ZjGxzW~4>%FUT>T36&6W06rZc|lmXTQ~FKR@Jo-^?U3W+rcT`;sV~ z*IJ^X)P2V4$N&guAd&H9%<^=`n@N@XI$QLXY$oH)f+aa$5&sZRvvt{G`(w0@QoeD2 z*0#MwXRb4OdwV|;WU&MAiNNz?v5tg?>5wfyV*FX)H`*WRHlJ+yp=i(f1bFy)p5!;e z6E5(Ttouyu3p&cvbWq~aF@7m8Mdynxw!XnR#$B>K;gRFt^;XgN3C`qV3%O9XJaRq9 z=urPn2fU>~&&H1(a0mbIY;eFMh4wq^1)S!acIfIKIN(ek`FQGZ8pFoJ+Xv-14@)@h z-wWgazJPPO$@y-1$^n;jgKhmU2i(CYPyEdRchJA#p8`&L#xc%%Xb(v@2mYNy9B?*Y z+Hp913_ebp7o)RztE0VRy>@yWaK=}(lmGL!e zPnma)^(6jfd?)M1@%<3`CQXUEi80Zh@Wc!JYu+NqN_IoE$3Cpb(VpG;;B?)7z5|Z^ zlU57QQz6e2^FGOei`P|Reec-o_>RN2Z)N8>vVIJ;xR;KlXK>1-78L;ZiG z%!uj=DhvE=K=FATxBZ;He-t>0{}~^WV(VwK(@YOAIu@NnI{1f99U0wepUC)g-z$X2 zbkdjeGwV0qEZWD>FN1HslC?)PM)Ea+;f!a&@AP?Y*PJBW54~+lQk1_gS7835z=etjFz#aUwd9DNQ zXutg@0#1C%+BZPsIqW!%htVeu3*wMR+Jmrt`g&+u!~;P+L7&Ue{z0vF*RM)Sr+rxS znl^iLPI*8%p?Y);-Hn@!?_oWOKj0TGBmUet-!V2hci_BSzzez&UXX(~`qS3-Nhl7^ z$Hr)7E*E&>=-Z?<syuuYro@$)qe+G~37 zSO*@qU*mW-hvj(AFZ(gQDY5Q-Q0ABVHNgP`XF-pBh8KXI5^EkU*p~fHo8!<$o1MDZ z0dI_me#l45kUz|Mw)tOjTm%OXHsp)emgg}LZ?bt8oi|FK5%*f1I*{*sSbNj9rMzFz zmB%cP{D8N1XZ*`T)FEPJIwn#8`#@vFnW@IE^(yd?VKG zM$w+n0c*YUOti1qc{Y8EI8Mk>qQD1cRQ6HKA0J3ubY2Fz=Y2FR6z~Y66Xd7?Rv4VT zl;DaBHMPSy=Q7x9SBg5^J=_sc96o5M8#@(x#ZL{FbcfZ5FPRM|d}$Q+)q10PCg4iz zy={q-cGLzoa;^EO)m5G;Z&Nd_cGQ}2WqMmuGzYRT8S=L&+K1TrL@_2AQzr=Em@%6( z#n_^x)AmD~0S>AQIGuNZP6R={Fi=avnSJ5#eKwr*e3<$r{K&V3Rb}m?(6Oxy59^mA zR00pRN52i974=K|;+TyG^>QuQKE(Yh-mi=|B+x!g>)h}u!*h;oFV<`yAjnB$>r9!p zpb>p1LVH?{OU7l0_d+x#4zGh%5Oc8mW^<5nAHu`#CN*2Z=<24DyIhp?6H;(Y^{#_A%@M9f(RN$|4;9+}p8Yk|A zUM%4hzlBV3JPrSraG{eJ?L2$#cfd1+@yNY>k<8+~f$JUZQ;g2E2A3es^uPRHlS^Fmat5d(Bau!=F((zbdK9=UR#LeDNv&X*c zZ3jF_HjKA#ux%KPk?HvLhlmD52}xt-=WM|aw+K#h1lrJ!vPdpS-$>qR_(b9nIRTs( zwbHvX3B_Z4N)QQNUZkw?>>7hsG-}k7_>5>0FYuA{nMwAOj31#rFs>!Cf8lcq zK6T0m^Sj97!Ww~+#cL{fpj*Oi{7h~d{vqMS8#F%#HzBnWkHg;4rvff)4sXBfO#z<^ zzHad7TJ7VjM<_K<>k2q|+?$`$?Es07&yV%P^IjCZ$BFO7aVPCq_7Y+Rd!s~go@mlN zde{Y=W2%Mfw9^4Cy!Ppd=fS%~LXWu_%AIwHIKT*^NKQjlegQ@@j(|9s$?SUD0hiv^qzC$aU*Nd!uo0u2D6RbTT$e;eHfX5jfu#Wug@m-)lw8agQ zC-@q(KDOEMM(UgCf68=r@FT%Z);DY4I7RdkV11jQMOz#CPXFF9ro+TT>0eJG8of-) z1^SNguznjC9EG3j!QdCeH>@p!)7+9Yo&9l_&SsoT)RK5-#Y)7^8)W6MA}$U^w1AfwzeyOXxzp z&cup~6syEa$^t>mpO=~|(ePfmzo*k3)GZ&q5}S6XbCFVC9GCNasZiudCas!OdM@P( z<+Hfv6T+QzJU9|DlU5uLATwR)(sNzzG^*Kz%nXewg)v2Ox9N0c1n!#4*QCU|?HCz` zARlDr33}bA5UC;~xiG4bJ50~w6n{<3^$ML8(O?PVp$|NPI zTmkCZkE;!!f!egjOCDm_4*SC=#}8L3N20U~U6nqGDmNermR^>rs2niTg?#WqBTEjZ zlPNc9tM<6cdmkC9sTb$oR%f0#^f}ip`NzJk-heBZ)mL0wb?F&Ps>bL4wYEcDWv#jU z?z>%ok0(`cd3m{4L%OUHj|LN()f4U1;w9xhyxv*bRgR~B+fmo28>Kn1fO^1!B}=5N zSl*8nG|LTFAlL0qVFknS7WD^esnwhNXH{AG_}ePBT|WI9S9AV}f8X%7YYd|o$sXuM zl)~PSetUM_#`t;#*8LGWr%bONs+{{%9kGZlN7Su~a_&pIP3yG$uBYL@7_Cdd!!hW* zFmkSVMQ>y+QGOC^ImlH=Eup`;kT%Q`i;7DOU%TTf6o47?BQHse-XYbw@3jja7meG|)mjBi zjC_#onaUg`M0XddzVK+A8Pb30E;2hmoam5pls?0fjxJhFS*CsM0Y1fdU%=k zQo6K0YOSZ|^K$ob$>e<`>G|_ZTaV~2(=45oyyT`^ZW?;is><=duE6YF5dM#5O5k1;+X z-^O1Ar@brVufyPIoU?$tS$mQL2YpP8hr#>NXU|v(zoamJZPuQp-Il&6h&?EywXo@X zzJtCYacsoU&SD)L{Jndr#8*V8y_X9(*-+-AiFw=b{_8VewlAX7;ex!CvslNN{T-mR zci@=)zfqLlJeO3AD~31fL0_%5?lk{L`o~l51Z@hm6h4g@!#wH>c8KyO*5W-gy18*O ztk7uXeB2hkjM*WP+etEo98}Vm!|lh0NZ7a_eW}Qy5aj?B1q3{0(4y3yi(KhjQR}?? zZ%dY2i(S`!UaC%Tt+38&E;% z*B$xqu%EXwh92?xtlieCF$;`$Ho6x1{np39z&j=PMtZDO3)IKdGe}im9adj+ef+Je zQ)*?PcvB@xA>C;fE91HLF})lnWO7|j?)AZVcG5z;&- zeKQmc8pgCq#@I>IeA85~s~zviMT15-5cY=ys0^d~f?Cw4N1)$ZHJ=ZoLkY4d>QTgv zs9`BS{Y=`>GTl+63mW?bKlIa$+g=oKZc8QqH2$gxUeNhuMLGUre*3zF7wg7L1zh%< zrhRM0-U!okmoQxNGodpsmfs0*o@U?im~$k&NCwP{Bz$^V6Y*^N1z@jza*x_ov!g1d z&3d%*^XK&GlMgVt68B*QPD6ZG&Y#26Ka)5`doi}z0xtJ%tS^r_OSTtsEAU?^;8c^t zDW4Y*96KG-rJI>Qasp~?QJwk<V7ldDTmw`hfOhjNc{KO~F1@`*@G8a=u6+|g%( z|I13tCRUarn;$nRDY2jbp2%ytQ3+o7>2wRz{*R;8mq{6SM!(F9K_#A#jtok!^P20d z&wOgLUp*&xX~|gcx|BM=+mhdq-tGEM?ydC0!F6fZ!SuuDnQ3)hGID0HJnluI?Y`bq zP4zC#sxn-+Ce<5Hw#Mt$n(V{DZD^cXcV2S0>(T$z2hywsD<%#&dBn77myS4l#Kf~+ zSvTTj^X#*(s2*`C`#tf;dm2YH)~X*_J4URiu2y%8{|+B9@sf!nE@8htzDNw!ACSux z#y^oz*zkFSS_~zcP);UP?jwJxHIj^>a6}}7YE8J880)KKI+1E)o~lq)%!yb*%2J!H zd_!5L+@jox^>a9p{Qlu|)xJ7A*!dxLRs=_V9Dr$$Yq$FEE?Tj2ERBUR4e!h=pdKb( z8%v`0prz4I$+g#9bIn6%J$g-be)C5AF)Hh)`F&frs)wyh=%;i+q}2+MR~mMzz$AI4 zUKCi0GOsk*(%$9sAqBnE7w#e_Hm>@}Ee#oJhq~5^g!J;L7SR@7YQ*E+X`{mky2s_F zUpn7&yJx=os`~1WF73Via&@saC4WoFm51;@?%d+5=foz>yQ*@|w2LRrpL6lF(@(D) zvUuFQ`Re2OgC`C-ee_KA-mC7LAG=`Q)sN1<5n+nL8gRdU7~8Y^m9g0ER^iq0tCa}q z>MElLmsXA*I=Ygw1OHznxm&}VN~gKi&b;X^&GnZ2;r|0dhYk0|DZ_o?LTicpncVer zLY@_#kot`J^r1N)heGP*)|~uzO2+QP|CF2-jfCRhXOyT5;l5``_xXa}XdoO`vw2hV z`oQ3+2%cpGgQsAypr)WV3;OPP(%hu=7(C44m!xx9Jkp#g;S@`)KGh55Nc_v#Wi#T*NL z)(~7NVQ-N{;d{Cfa~}=KR9IbF$F8ml?}5Ynq$E-DK~b8Len#VVlt>(vFf)Zih*T&Z z3Hu@uzwb!Oh4(E{roB8L@9ya6a<#c4-JP91d9$mtv%?ebX)6nUo{ELNxIz{5Xl5#j z5@Ps;x54oyIR0hlxF+TkBOmPmth6?3?~fk3xBA*DcvGN3@n@a`mY8b+&hj)4^d1EV z1eG~*Ujn!DCkl9|Z;y%SH2##~Ooxq$-Luy59>hGz&s|s@m>tjvj@p2csOVZXa6n~M z>EMYI$ekOEee>YSFgUzK=*SaQs-{;qbaEvMyyC;jR00^rNQTTncCpo#q#rIxUsw(S z^L*QQ_pq8q_H8nDapjA7Ypv%a_2S?C`nP``lKW)XAKKejy2CBYe7|!qiFb4?b9HvQ z-%J`_mEJ>Yz~J{?ng@Yer`rX%`kU`8VCm%iGoJ()RY-T|4$4)jo^qe(;gl%3)Ed7jrXGGqJqF8*y%@<$CWx|tFjksAusszLKxV+ zP?<>PM6vX6gztt{p^Pnidyj!36uhp$=r4q-;?|;Y*J#57x2=F{^u)nD=X@})Ie?PL zEJTPs&v0)0F#xu|MWl5UT9eLVMG@T>M`7ec0X<=aLWyVqI}JmP>AnEU0ffR~GZ50T z)o3x)(sq-cDhh={^k{cdZ|U`QB>+K@(og~gFjB$RGGFqDl62w4gJ8gbArD1b@WOO7 zj<8`grgug1T?sFCEZFSmeqYcCw3O>c5XG``DR>%bE2E<#w-)K5U}Q`!$6sK5T933P zU9o)9757dH@`~4WRU-yN)kkYo0&+GLY0XSG8uTS$Dx?erPG~>}|EMg$y zt3Z@uD7Fa*YA-TKBovFqv9asSWK>_mkGivdZvX^5uJS!4 zrR?C+Doq5~07e1@jhX47MMa_W{~K5%7S}X2(%#b(iRatm(eCc3&mW4qwEs&uA%V5W zf~KLf@g91_?+u3n|NpJJM@d63L~!SX`;n(2#u4CA;i0tuqR3Rie(_TgF3x45U?mQP zP*Ei9Y5iPpFZCViOwlFjQau`pCjBjuz>#d)6$trLJ|pZZ59wLRY0!e$Agw7Gw~S;8 z@n3Hw;!Sx&ILqj8x3qvCW0CHJ8Nf5y5CaXmj^cb#@^dzP&U9KesWj#NWYXat zgeKrgrt89p0U;6!-H20B{Gpg!2yyZ_to@0I$W@ECb8E3qKq6+iq~^X>E*qN zgHK|IJs$p&D4l7L{-LYnTqW97IqBGI@Nk=2WTxn-2&KVsQ+DFHNMxWALJ*ruV5k_1 zC;Enz4X?At*Qv*(KDPQoAOA4DZf@9DzRv3(o~^s6S5;5HR~>4-U#3p8npz$YfA{f4 zmUYo5bFDwC^UAD02R&V%d%H@@kMyMI_3^ZoO?k{HN_G08fq<3q`?{?V_9-2{)=u5Z z_&m$3_RKQ@KWq{mbuEUb(dUPeplHAZ&g+;d-mC7)2A=1WOS$VPSBU=j(S-;1>NJIdmXJ4 zTaU8zYwkL>PLaN^!#6euE`9uap!7mrnK)_Stb+?mKh~AtLbkqSn{YO@jpZV2yHVhy zb&RuE9wN?&<*~Rn&MD)_HEcl)!3SGLr<4pA}ZwwvNk?o*^ zyegDftvpjjH5@pvY$hi;~h64bcF1ilR+_SJ?u^)31K zu*;W5I-=%BrJIg&%jZgz3+lk+nmdqG90QDx2@DgZ=YtZn-=VZHC-2W*R zbeO$oP~VN}unT5RxfOPO|+jt`IO z$MY0@#fG!7pEcRdi<8=ZH{+*CnLY(F~>RJ&Ux5pIxBIGxX!am zTe^O9{^LGtVzOqoHXAolGJWq&>C+dA@lotOgmL_h^>wzu$!S1(0Xr&;;VgpVeU1oX zglW{kK7qZ^aVq3_6y%vA81PzY=fw)$L!Fk5QOMp5_qc~cgyDEV9kc)PTa5hvqKF4F z?d)nv`va~P92v%sn5mGO)?KZkKt2%-svVZ!6;s<$EKU3^mWie#nO-9qgR|xCPUsmu zZgoce8Gpo+4CuNU4d>!+e*~qkJ0jtj73}Kj%4hHk<|`A)B*P)pM8_646U-#N$z&jH z+WZ+I`W`H0{I*2U!oYa~qx(~^N{;&0cS8oQ#ree2!z22ZE6Rmu>yXZdw*ok-)#)79 z=sUG+HFVzbq7v8{@*VAJ+ze&fkBZeXVpXA37fRdNFVN7_6WCn}Z8y58mc!t(Nn_L@ z%24-sb&T7ky6HgxHJ&7AH`tO#$fUhT_jd&hBay&sV93Cr|Ck}a>C^LVW+0f*U5L<| z8k}3x@9z4RdTi>Z%I6Hg7Y=nf2sq{)@}#s;fftD z!bWW%XHi>+Jr(o;*Or&%cXYnqO*c^MqOoRKAJEL2hq72?->eT_A4`rY0sL$Zhpq|& z{~lkr-JbA6zMldfZj&vA`$O-_zI1KA4QtcaU3}a`er{bm%I= zb?_tyoY9HX@#a*d-X0PNLU61+Q zM&oLDYZ&bAp`W-hvY8L+_Ik5Eo|;PQ{bY&6C2Sed3UAA1BwXkD@dCa}!UZpLc*6%0 zF3wdB!b);3lXGL|Dtztg9u@6pL$5r+Y2{L;zpZ1<56(GTnf_lo8VFh?I6n}+rhT#> zakkV5udAO7#0u>v%Rm>o8nYlV0{AYtj?0tc1_n_roRZH4-@&_8roI zSPK^W6ytKN^^Ri&4G9-pQ{KLQw1fw_95WhL?AbsPP}fu32iHeCv5FGT=Cb#iqt=!2 z2j{1Z?ORgcCgw3_&(GXGT3PA~W2JLTHm3cX2rBE0Q+qZyj;Hy6>{rMUhp#_V!f9?o zZ{q&Ol@53d(T~^7pl^xs>wL~sCBoJz>`iy#zZmw<14O>zNM(#d@48RH{_+x5uL1i* z+nXT+jUuzsKC>sGV4vx~+?nCYRklmR>@z$e{!Yu70ac?5drPDs;ve@k*NOR$tr9#v zs4D(oI^SZXTXcU$5BMXIj)>JpzhgmP)ED&mwK(35XpMXQiGb?!#l7xyFwvGY1Fao; zr#osGL4^7J{_d3fmG$l^e!o8+i{R03Ez;5zjU)^!)!EsZZ|~^nu<#PGza^E<`dZSO zzdK{JW;5+tSF$vi4C%c+0mBT3!zJB8f3HHRrscts@aOHGR;(Y-J&Gc~c^K$eL8}Pq zKUwk!;{~3(?V!Doec}Pspm|oViBZ-;YsGmj^AlgW#KUv~U-Qf#WqWas!gYnLl|sD3 z^rfn(Q_GSG`0IhjPYcY+x2#+#!?m;5J6qqv#J`FXv(VI1kWx>05HVS++h*-)$Nt;W~6VWN-U_ z73d8gm(JE8^VkCfMa`3o@M7P57OP$Y9~aRmUb~OtQY?`Mf#U!|)DSRH;}0jzr9($e zriT=U4`u-YRAe5CKo)KHi?$9F+!i&>>{J4_btvsr6S<&QL%BS^R_@j;WT9jN5i98r zrGkDd>`M9aohsg0!E-vTtyV`wZOw+GUOd$hQB%E&X{Zr^!Z;UBqM&We{# zJ%P}2yfm3Wd?21zLjJVhaD^kjv@h&Vn?66@(2C=f5*ajMRG~!)T))pu!1-?T;<-Ke z&YBtvCgQr`>oN=jyE{BHY9vF54^vu91o<{Wyr&cn2cq;Q1KCEd;~4#A{SX7Tk;xhE zg~2vTKBDzy{Lbr^Ncp=Sco<(Fx@s8Kch~K59Z3$rr(7LZr`IhP@u(-w{?3S@#b=nLzujb7~?M<{diJv!oMsV_I*!U@5ljWjux!nJ28r6Qx z5o<$rSbHR5K@27$-e0)TA2VEL4KGBEJw+G?X_VYbk3v_Szm7_h! z0}5jPZ%TNP?UnUY3VMX`Wdml0KV1G>AphgB|L3o(Tqe4yTX1iVtqazj!`rIU@GKSA!C_9Kuzbm5%lZG zb!%RCo0Ukbs49(9pZ{l+L0bZHPZ}W{XK!&K$crcT0?-gXGZjdMdj6*h0VHc&hFZM|&q=U-^bgfoD07 zL&HcsY&?AY<`@T@$>YvDj*=mVtnB=mqkU_EK0A+-a9d|?>4zr--36OZdO-7R+$P$S zf1G6Jzs$3$`>w=el-*+E*Jkbb_mbY~ZF+#sA&6QAZ&N*zoo_Acol&BX(`G$2^<$-G zwvI1@wlkn7SsZ@%i=Qgm(#PIVwkxI8AM4;B(le3{*@8d4vk*HFuqbF$TwRjS>IZy zu2NrdHQ#pqF<&X1=KeG~e3o?&BFoHuTE$-jqL7phkc zI^KFQf6CVvj#+$JksQ$a;C`ah2DVULanA2gO`&B2k5J8{BwPQVvG-sG33xxb*$xj| zu0NA+w*D0xP9QEm%KwCYz>6WwKA?};;8VyNY-X^eciMG$U(B=9_L&!<|0Vcr>GpQ@ zYDJm2N(ZMNZdE=}l;An|`>aD(ovKV5q0?CTxdqcjRG(1RdZMw$q>Okrkcy)A{ru~F zpVO!szr{Wqk1?5^)ikE}d#A4?*;yZm4Vv~`%|&va%%|e?Y9!NZ)0gWv=o4TYEZ`F% zzhRQ!?JrOG6qPPo>C;13eb|k0kiWuoO=JIk0m%~l4=zg^K6UhwuyrWv-~HGDFVNnz z9a(5cj|uUC!hT=G6tWRor6T`6N%-ltqJDmYoEu_Ig*6wkBRM%%8c*o_+H~r&ptBaxtd$$ z_qe@d^O3cwNfsRN#$Pz*!}<~YY|B5LYcP1@T$&B-bR~Y~8&j3Ae(^<)pUF7tF7(Lu zj2F1Q-ek{%#>4TiwW-D0Qw%yoG)WByO*mydCO8)~;qhtQ$71xVX#S9BX^txHr|x$I zrP<)m@%YY#WiKM0bBa=_T*T^aF26GKIK7XeQ;{RPL27|v8!zIprI}| zogdR7)E9m<#?6vrC=UQXlr)2^0PdplW1IhJRH*d0$wSv`JAXdrp*!w)=#2HXk1kmM z)Xfhbe&*&Y%QV;Xlf93gh|eCpKLE4<~?P#UM{E3HR5_mzG)_H_@+Ox_Mou9U!>3XAfhMxWoymrtT@(AwCDJ@ zJSy5#{4zzkFPl0Ezj?aE6GAK!Yt;NC%~`C*X`+wwan?_El6>`92Zzq+oiSX8(yu-%y1bRh%Ea!Shsr~*2f;(tsa9crD_#Aj>Ef{UZn!YRhr8Tu}oJZE; z>)-w_vb_Tj>NFSp62Tk1cAsNBg7*o6_Jwmk!UNxR_e)2OpUF!7%hdj89;4Uh`pt{A z7xVhF7$^COycP=CXV0z^9ph)by&G1vAKe`KVbfn_`yyVI^(}}#Ob&U!n|0ZK7T&vr zFVQg8J*y8d3)AP|EwW{+JdY5s95-R|H8alEYS&-(jT5FHTeBTrYUqa2>ccwcLCV{} zx;9{Z{dkbU#XSEe`s2C<{KR$3!E@y}Xnb;SaFF?x$XBSby2_sRRO5_q_@1 zFIOn8vmANID}`-ctE{6q?|{*iyD&ifxE+yNWO7YBX(+a91BW7K%jGVcIAn6w7^F0K z#)#BnJj>3vYse4BKZ8fo(-nho&l15PZfVJA|L;y(K_pEx1{TUa%@V4dm zsI%stvvTa5ed-ktl>Yti#~pXYDc>CRL`nI{BLnJ<$2~Ic+*1e7zi-aEtIj#r+UZ&} z^{iV*O|=HDy6@(jkKKIrs$*8=>TdO`!~GKy`R7j=e!)u}(*p0DvS9knH~%(feV=|G zm~hIHV{7uC+;--KRnygXr=PUy_^LS*R*k?t`mPf96J7Zf{fnzgPk;B?Z)b4c)0!^1 zQ@#GVZFSa=-G5TG3-IXQ(Ta4J^+{@}& z)?4eW(a){grruv-1Xd)LF3A1e^@`t@{No=Ng#&A>gRj2&YX0vpyzqjxHoC$eU4f&c zPkwOOYp)%6*7xFp@*liY{*R4Vc&5WdoKNt{dk%g`57Wr?+R zFklh?QD)BnDMP1Zy!-6jubXho%11w&)Uu85=@TNMMQ>Z*%c?)wXieR?LVxdB>pSXo z*5~T03qo(dqrPBGrJvmDuNbB9ROkPq&RT4}w%B?m5{XnzzA7?rY>j!?TJx~yxUUqJuzAO^=36t4wZhu6y}mN-$2#e8c<$0>wiN7HdYFWZ zyQ>Ya73tYgeS^w`&+&WYacsX=+1K}a^+V^q!gx1WwpZGsS$o1`3vY%K-?3O4x8|uc zL_cBh3yXnp{ykVD`!CYZo8Ipa-wTb{PyaK!yyGmu1$UdLN|}&Ib-SlN}D{^*wQ$b3AH^JnKn)VNM>sK)~gJ-k#(;qK&B0x=q=s>{j+E zhm;>8cN_<9lUeAUY1=^v!dFeU860;6SSp(1@C|9wbjZ!K_x__V+h^>MLl&=_Je(Ph z$?TR24go2F&^`q$6{mqLnO(eTwC!v|ExCKikRj^zqei`#`_apx^VIdDPP3N%;~(nv zpXM|CKR4G-ow{JE>-2Mn+%{y?{iALl?7C>`;M;~w{lnOcRL|YD`1UtOjq1vMpT50e z^l8?O-QDUpKFjaq|5;XBP2V0qamdQSqw(z_u1ls4xqZmg-hBzL*A=BJ_5K)MOp1ql zQ{hB5lE!IX)KEhqS2W_eV%w}B*jU42c9u6eA+>st{F zSZBMY>;6E{`V-CyKf`|QH9azUfsQLO6BZbn5gLl$A>)k8440}!&7|)2#qcG+OJ`r| z#Fu*TrFK0M4AYkaVfvC6ze9#EWO#mFJ>?6ReKF@7eh^Z*xhAFu=GdKo z`pZ!Kouu2VB zlTd?U3)MkQ)wlY#=WpA1lY1skTlGkO>HKlmS61Zzp6_I9uWq za>Df=Wg^J-ToxE_^V|U;BT^PH51f{Y_O!PJ{*BOX4!usb*IEd^e+JD%o}Y3(LUiY{ zqf0zt=pD4@aCt6F?V|)b7X=u-;~o$p3m`l(fHYT!6Rh0B}&^a5GaB)|Q z@Pp2U`OYlsWb~naXFt)bwDc!e$hWwf*J$GXsS$FC5jWz8!V;LOootD%2=bV;UE3Vt0{1#DY{W0vj^iDmGsWCp3 zb*yRK9sDNiQj^|cHhw>(8*k^>aY?cTygkvmV(%jQbnkVN2295I+Hb26k6P z!~vqcfOB4#adGLx?9%AmH2r(8-JWzC@giIMEe|^Im_%bv-CYA5aMmx!Pmx#IZ@eH& zhp{FMPIFF^Z16c#R8qEg;E^%l2->rK6>nenkZezJ3X(CRk!SnU1V^TU&gmem998+E z45afBp`uC!bPt;LI=CZ>3J?QNMtI7$bg*iUyPmin%eE6pN8m;e8z2#K`qF?CH&XEK6Uz;^X9dLt#0~#``|xWe-5Z)OU_c2 zBwTykg7+GsARLTvARY*nm{uy^X@yZ;QSEZ|SRpl#wf=(g9hNs5E>D$X?|}zf^=vW_ zKv|oX(o21z5G4p_U0ryg6&Z_hD@k{wTIg<6Fdj8hdPx-3OC!nNmT1Updc!5Yme$T- zS4)4rkKDK89BahZRPfA+5^mctCVLc-l{`@3dk;leB|Js3C(dIO@s)6ES4rB`=tujx|dIz*FDkCq<>^6CdFl}r^8q(P;VN2@p}w0E?W*z&)L zTC$Rxye;4P;_?^n7FnNKYgbxRF1*m) zY~|I2EC1;CzUMIlQ_g$%*2TFdH&W5p_5Sn5o9?>Gdg3pC`Afd}!w)~SX5PQ*s%>*U z$DMiBIS-saf9mZEZ@*{Z_lJLT&JCM?a_*UDJ}`gbln1D83qRvcBBJcP5yx*2+C^wx z_HAr>1CcHQi-;UanArPsk>x=MS4Ytdv<54ABkPNYP8(v2Z`cau~*;U}HX zVz`-esD0{m*_<9h2NF44r4iW9EfM<&*0Q_(M4Ss-mtflsycizGo z*ULU&C+aXt$5^?&5x4-y=?oha%%=>P5Rlv!fweUU&k2zGp(||Mk$LHKYVQW4& z5x1`Ep!Usu0-j*D6tr$SK=3AL9MLbe$2fVvJJ?(4q9VZhzqVU;iuHlt#rth76$Yq&XohoT|N~Y=lq%Zi*TymjkU@2XK1DF@av)%yga6Zs;H zXE>cU*>M2QOOk#Kzm#+p>$!*RDd<~Yw9a+dc{4c0U)efr8qfH}Zg1lslloh4w{i?# zWV)WUx8W5e1>jX?_rZ6D%}4v#`pbTahZ+8ZwZy|xcBoD^d@rqb%iclCq>prbCV58U zs=t={sQZl=EAbw~MPtETOB*hExbE#QoI^zucHtVELBBaLd81da8Jy>Y*>YQP0DdxN z_$xstTTa-#pR)H*qHnEs^P*!;oAShrS2(4_ch0BrYPGw+=jp9}P9I9l(Xj%o7c z@U4Mk>Rxt_BpoL1l9A3cNmp&Dp;^_vF1{B-U(7$JH;Axju=z{3`{8`IYl~>VOsw0| zLx-#>iW0cVUOA`k;pMn7Mp%eyNlXOUgh$$_vfNLop-Z&WvA0RNLe8K z%qMH|5gyaF0TNH$n+!j=lJ$ED>Pce0+EUE-k-kr-|CKU3>9!%0QR;(i!)x>%k}d|P zeN&ovqwWoYOWs(4^NaO_>w90z|D*4N$_V_p*%;Z5zE7t=~2C-RBAXQr{e?VsnJ1U~`mw zLzKl`V&+>qd6tiVcU-iW^i4sU9P53R&HuX(=icw@t*#=eCY-><*AVpOb6f8hxNc-~ ziw_*L>(V?~{xKVVm>){3yyHH{_n-@RZ^9d^2W*LApGi8_LBl)7L3JS5{%(IA!A)db zvi6d`yDu(kA7$gl*bV}2>tSYZ8Q+O&m-A4^p4YqlrUHq>=`5?%17~{pX)4~oK3g3# z@!ZKc{9y(b*#Ia9Lr1vuk4lHHRnygxWz2m(+Co#lK@}E92GS)?d|AtRJYa{Knc`)^fc2 zgN)Cwrt@vCWIpD~Xpwjz>Wu{gUT;2}^7&}%q^7liFXlxkGwwAKD4HDfqAW}RS2s($ zAA8Jv%$gt7tv5EV@%mS^w%_{97VD)Kmwu+s!kO@6k6C}-h?8NTFPd@Z{6;XJj>qFx zd515d87MQShj7EBCmzpr$4$Q(^ZB~7My55DL_vn~RHnQo!PiF0IOD;J4S!-QcM@AS z13Z!;86>I1`dlH_lfJ|BHtBz?^B@7|egW}6(~p@E0#10C9^vo<;10n@ga_*{@Yu0< z-X3s)2VR!oBU{hHQ$);^NsWo91NfZF24wQ7n~Qi%%1q{`1ScsFxQfQKJ0jrZV=~=# zcpvpq2bo9I@`w18>Pyi%ON#A9rYbWKK`;vJ#7 zI{PYSzoF;Sc4cCfOd#m*APVkN!{43ky1{x?J;VBwT5Hv>w?;pvUh>kU`xkFgFKl%` zo6>x0Cg0+U=S^3TZyt-vX{lTcNW&>JOLp{8h-;)Yt zlNSC>APS&%C0&{tdgf)k$dSvSPPL`)y%G(l-^^j}09um&rvAzq+?xr>0^A0FGYx8t%RLyG_;LJ>}A^2yC^V*Q03_eun`5``wzuZR3r4TQ{I~RltNC}i-~95{hAXzOFN>?8op;Pi z?>c9QTJvcB?Z?z*gU{ZEES;{s-ZRug`L(V;=TCQipe^TsG(Ys<88g&B zTc@jkKBH>P@-bD`(A%qK+;fHc+o}_9*T&Bnf6p1Nxe)xnTGp~Vm#P=2m*-!)&fIYC zAvIkRti98E@|kDU%NJdipZ(Zlu5SN%!Tg4J{1&V1^^;9+L|t5Raj5O#hd)mw@}9)H z;5J+re?MciG_N!L>4(efvReBptiy8bIqN+S61|Tp#Tn%7xt}R#&*+=+0Y2+zv-ZPg zh;+ozG^=kwZtoeIMv^$mwN zA+E^aQa5rQu7}QGaH)R};{P4viQ}#`_bJ;5z8M~vY|mq#emS5;~*(4C)=eP?6hbKA}A0cwxN`W7I6xdD=Q0wj+1 z$97!xI@AlliDV9;Pb!#qE~}7>|3xf}{wW*DGpQMiPnFF&;H?(SYa9*V?cCBASV z5e(&fbQD0=@n{qM$Mbl8c=g#Qt~qIfYn)Z#y2$$ImSuN6`|MrIZuw{6%Bknfu9~uN zbY&=CSv{i6S~ej(;gefEx0THdyBbF%4P)Kx+18cQ@0)YooH=usOjt74f8BK>mMj=? zU3mC)3&gqx%YI4wwS(VS);)a^TMyeu=DaEGZGEla1-a&2hW4N0;1Q?aeq#Uj^~EwF zd3gtRO%9&Y!3PXaJ@#kte|-|qc*I0whyk6!Wbqd>!K2U_=Q6%QyoK@L?9CVDuTaKK zd4S%17V)RT9Bz=f1g~+v-TA(N$834r;wl=Ggx6nH1P|IgTZc%PL%t+03;ecUENRE# zbq`R#u>IWUPr#=5aCV?87l6mPO<`{=A;3R!=wu4ijvj=>R=oRx>d>(J^v}dEF#i8- zTe8o6t(E`a1N9F+>me&JQ9WLL``u5~>mGEy`|j#LyzL(RpLm#^DXS%)_~-n!53YQ~ zP(v=*s$O?MU9aM40Ly1(N@8EZtz>Iz`)Mx}(Y&7>Z-C}!{6TQXzM*cd1J3y@On4-p zk&a^fvVF7JS~GdT{c;fpV7W&pvmDRqD6hs}q{f zXW5N$>%dEYQlr<;vo786{4GyEeR2G^x2%b~TED`gsju8=opRd9`!26}e56YynU(hH zDe{90c9QHK))YD(Lk&?v*(|Bg+jav>W?Q(3a<& z&oekYG|3a({{6hD{~+Q5SgR38~VP5 zT4-3iEWB%qG4;+6b0Ap*e8+0wso0SqY$8S==TQger6@im z@P4+z{Mr|{-+qaJbG`@PrA{mAw?Ce8(LU9$-(~@qwkrK{5gv)Z5&KZby!2u`jXMOM ze*NwkCEz;V$XbuHq}t}bsiiY&bOm(+W_*PIe3QF)42|1UyN`2Ednn2yYZf) zaq<4KZfS}SRixPom53Ef+1k!%CH+*L$q>HNXb~+gMSXDIBVIyu7<}vzW1<*7 z-euG9*5$Fvl<(`9WBv6v_D)-AlCz%0|TUn zt&HloI$WBSz$0E!RP8cK(?=quMtRiqhw#*vZuBArIIQ~9R!r@pO59eP+ODQh`xrm% zojxrUP0tVss zApS%{Q9X{AS@7UoPcR(p!J|oUt9YqP~g|40_8E z5i`(*fd^gmu#S31{%A<79n+8LS83HqYT24{`2g>1_%~Zq$T2vglzkSqWhDK^7ktOo zot~|>e@XnwbQEbP)V{X&X>o=}wokt<`X>AieVT3;@Pyqj*DYy8x7m0kKX3S6(Rji( z|MRzy9dbdsRQ6kcrNCqK%_-YM=P?@^^A9RRr(QwTGn@NXeQjDt%??H@p(77G#Hdjs zzQ<)8_(Y7>O>p?PG(XG(Hw1#<@fcpeBi?>Mfu`oyt!TT2WPd89?LsMS=Y?o~GwY}i z@lcl#1?0Zi7o)$#)tXH5fEOxoa{tBg*vae`flCW{6XhtQ?T^JGF>lN;!6(5;!fc5c zn%5OGym5S*^~F$vUwo=Z;)#}U$e-)zN~v0^s|!`d_>hIPkMZ@a#d+-_lJ=&A8S{GK5wE+jr*-sn^vxd z0#><|rgtj*?d?dP2?e4ZYT)Q5@C1`3?;>6sPoj`sSe@ph_$^-Xv%K9%qzQ(6nS>vO zbu%Ra3gdPs61nbl+Lujf-u9MYOD5B*b(+QeEo|61Vx42f{8v9&z&Rg7$E6?b$A3k9 zR`+euUfPmv%$t)kC-K+aUxbJFi`nevrHt=f0uRY3%gaeK=}b2Y_)65*IN({YElsC$ zf9U&s`qede5FcND?2H=w=e=hTw2cNg8%6>$=rXJA=?tKb65ghHgkGA1o zqP>(yfrr4{PS|*u|5SgufcNWnjje<9VtTKE6=QfL{W$(j5doL|)~zn0-?fOnJ&JlG zOV`q_y;q-5wNRV2zwY1f>DN~NQrfSS9W!48t0=}xvGk}d*LAlGc(Qn0O}hkKj*IJo zH8}^KEYXR>*CRS4=vU;M*3TDknm1zN$faQVvi|F`z3>6~yzA{4ezTC+bWIe|mDVjK^Is|s528=kVCMjuN?M4q&J%r; zKfq#toM+P4Nw}D^pxqGx4-~hrztqv5@!o-ti~QvN{myl?PkYi>gL)bz^@2Yd{Igl` z&!`qDzh_JD^HE+D&pYG$+=cK=5Ru@w7|EHSAEY$0BQg2^N1sOgw*zdpx6|!v&Fe|7 zAxTSG5j+;wk@qLn77H1M{Kry~yld+axv#08L+d5? zHT`^v`ilgfD0q?SLcu?u6C8P}b0Pa=gZXU?x$X)(R_C5{l_bT?J~`Vjsk%5T7N%GX;OIqG>pNwx);a zkuU!2GxizjB?qrHok2_P)ID|g5hl_=3{E`4bcWDLhJ+XK?Do3_oZBVn;ln2p9@1|T z54*F!dyzd01}A@$#n9?61iT_!L7%SqlzrB~`_`5=O|WM4<<9v)e?(jQXwKTJoH*`l z`iFd{FX$9!dE3xX$kF(g=rd@Kz5W7%%Q;c~SLVx-7RQ~8?~C^R{Cc1#zb^r>r=<}q~vWdIGv|3I@Hf%t^X>7g+jp&ilRk5dxz4sr8~dh~ zHF#>_Q-l>wU0c&W1AB$d(~ou6yf*z+^wZxCohR@GfX~6R^}2u)&C)E6Gl|i%Tfl>$ zaSO$3IF0LU-XuHf7*~3K`{zDQ z-CY9SZ#@n?Dd3V-DSH+{BHP5zCvU z+L3NN78Lg9d)iVlHIO&**)A=u#Z}YYh0S;>6V9Y^*_7a0JC~S6aGILseCaJs_+*s* zi)_T90&Zdr5GMhg>7WCH1w0Kp^Y&50S*Y@I|ZH?@UZp= zu#VuNY5}M5Gk&a#Gx#3_ob((UX9GqEI$*S{Us^|ohceFq2R1PtL5G7=X}+9piihQB zfgi-pOJ0`(Q6(%a3mg@nfEJj*5kJCHZbQo`k;F3w$qiMdbP=A_tr%Xxckjd{p0Mh* zw5%Eqrpr9}}!(tQc5r8|f! z*Xam0(E%;(^QuUFZ`J)cF3p=I;ob}aKv}a*PxfZJpLi?ug_bF3=U#&c6u>2gmuG?Xl35U7qI8kUAATUexnj80II$f9u^AuM8q zafo{qwKI-NbQD8_Ojv{f0tMKqbRkZmjFzM06fQG0BWkIrx3 z_oTo1{=cuGi{d=@Is1E__iXppupQE(33O?q;;)n|c%3!AI%?{uVB6r1j;SMCcYWI@ z`0Qd(*JNYd#@v>-N?p^KW_;-DuQqL#m+&^i%hY#20&T~4Ai}j{@%8B>DEh=*@_YyJ zSLpd|FQV|Rzm@Ph!r6y2eR1n=B%Jz?>bnDH`cyuHGkqn*(;yqTk7gv3+ix_f{MyvT z%XYtv#)}O^h9NaUp@<*ZVI^j2!vU$;VSqH{%%DkRT!Gpe_c=9Gux` z4wNl*-v)WUYw}BFMhvhscqS%|;$ZkcRV19oSCg*?5P;KIv2c#(cFdMcJQRP`)aico z{C)dNIMp@l3M?4uo+sf!{B$}(#UE))+F3U7S51C`dZpbD6He~~59=>QyXhvJ>TU}8 z*>uzAR;%1Xc{@I-_SyCZd7gAh<)3!hO+0y951)Y#qw2q5!f8C_`utaUp7g3p?N9qp zcpkN3;yDug03V_IjQ8#LH-9>Dp^!+QaZ~%hQZJs8^Rwd^$4$Pdrg5k9R3CHi7am@) zc@Fz)YU?j0&OFL!!fEe<@;1(C(Ki`YodkO?+-HM6a0e7d>WbR>ZJ7JQt~Q*3y`UQl ziPL^)u(~Y|l?1y`R!^y2Rejy2b=>GX#f9^NUnJK3kC?d5#GOu)ubZ!U3@5OIz8r1y z<0N0)=EyItJ*(%r?or3I!mGkdw%oMN8wqbvo7{ZlimoRb_lylo8`^bVw#lHc(JHpo z#n~A5Uccd@>pqh>wU9XR(hX;zlus5CFMh7UDZi#puKznKb+(da`M~in8ELrcy@3* z2&bNS_s4Dz(JD-Mfz+J##0&3_J#I5Eee;D-KE=eMJyPk6q*s?m675~tR@f#_@MCo+*H3lH#N1;Y|f8&o3&QA-znmZ6I@1o4!+YT!1bz4<slf+DJlk6ozV+u4PW_eR zKgYz6RPblq?0L3R4SY9ypTvInGBTYQJsM|w zj~kg@CqBB4)>p|qVJO)z46%5Qf?wSkMp&2JImvpRwucLy@!I|X9yul{BtJa}awE2G4UtTVaB`Vla%h$Vw@pc;fVk`KZ@N%kAthA>5H2!Ipy%uiq z&K0m_HibXt+dYemehJrk7xMV1VWFJFcZW;;%tT=}w_M2K|0|1FZAF{W-HJN->G%Gb z>f*kd)DhWLi|X{QvrXMCNjN>vem$PV^KVw+O^W9(tnR{(K3JSmLFI7Bd47v-ewg^Y+LAs`{ z?)<%p$F3VtotS*yi4f%TiQxGx*R6#An8MBX*>f11S{NaC&6d6*bu`WyArsZm*d^2pL!q`MKELTOVRM9H= zT6SWbS|v-ZvRK4yO>DSp#s#+FABk;{FJsn?wwZOKZAVKu+hh&43M+*z!P^Vs(KfcI z!r3O-_RRbRXOAnppJ&>89QKw@9As9{!|%+TgEe%rGy0kowK2&toj#_QJZ#h-^bRx{ z;w)CV@&C>$o$da~vyzwXd|UGMdmZ3!9=(0nc@K=7y849&MsHYq^0dln$M1XAOAlQ8 z{m6g)tbf@f-(P$C1wZhAb;fVUUTNCB)NVAAs@>aO)o**tY@7d^cK`kd-}9a$5A%QK z|Mb!`j=K8MSDmr(-3R3FJNTuqyXva5$`@U9!rOAG^|-yhxDsvjIT%!t8+~C9jgB_w zHmqP=#fXYF{64i6&d!L|n0wRDPtdtpK11`WL?r>w6eCdTpiP-W_s!vy%f7VUamXeN zgQT+f&iZElAP@~xW*h7OnGt%;0I$}}+Mylf>qF_E?d;~bgbfDMWceUF+$Y}4qRgj>Ho(0B>|u6o|+ z>&;ij^iRhFj~joU5wtgX!p}<_fi*A`Fr zKRjCZCz9>8YJ07aob|_BBbC(-{T=Q9pBleZltFi)I9^ZJ{iUf&-AnA6XYtInCzk!W z+(p|B~35xgPe+?MkhG zD2JQ>dh`gu|NR(?cJy%8o*nH_TD`t^i~rh7{`FtIZQdvR%P;x-=e?UHWY^SKY3;h? z*FL`Xm3OXv<>Se(tzGwKZ@Yh1vgdu?Kb`)rokssZc2u;L>pnbc9&uN^Y7^d2IO3u~ z3u764r_Wy)Z!Hwy4^{nzIg;9Q{U&{kG#Kl_wwfNTzKJy$G-a2 z7mnTfe;jk+(WG&4ZzAhWRI-`OL@ry(Wefd0>+F%ZchjS@(A$pr$up08{mECSx3oW* z_(9^CFFN$(tI+1O&oD*j2yT4}+I;d>^aad*pNjUy?85l;b$FwbiKEP2hTvxH`UFN{ z4{b#rk%u`e<}zlsy6%}C&VS)$e>`_Jzmpj=Y#bYw5B}4-ecB`G4vm{$!<#SR zO?<5mMzx7za@;GdE_>w`ef+FkuF*Plb!oKS$l+7$m1e$F?X0cTbNS>%slGB_S@f6v zMVt%t8Y^=P^-3A7JXI_B@&y^a_KKZ_}lDXE#Ih;M?t59&-?WKXFp`U9RG4()0ISM&X2- zcuw;d@g1)-DV|?GhVNm2;ryGA|I?`plD^ETA3(nFUW8|=xY}lZzBUjSrETWCoc68b z@_ZTVfA6+=O>epDh{Pki|Ae!$FED+Ed^9S@K@=C2CMX}~$+mp=V)ZBpH|u_PZlgR! zo)7ASzUCG@Z`O3^$#m*7n^d@|a}Mu5M};@2Y#e^;(K|IU!`Z%cR)_Iw4k!GpW7c6iZ8e)^}Y@s5t8(_23GJMXC7PaJ*bNmy}4 z8D}=ff;vZ=Q~?+b=SJ<##dGr#;QBp=H;Bp3f^bDVBTkp-&Uup3qIa?Tpvk$ zB@ZsooIf|V+{7Pzv<4?4=UbJvir2#b`!)KpSg#e^d3+tQUCC29XwAm7JKazQw)c_5#3(gX24M&XGh#>FziYiJ3q1CcOO&2KclVxY92OCsq0Vk#Zca>MqRM%D((ya-2M*h@P zy@loJ>eNzWV!YX`lu*A&Co|Qkx4U>%Ky$5H9nUq(T*Hn0|E5mS->F}leVlDCq<(j! zwS%G=&(I%970};JKgH=e*1dvRYdcWZ0r-3Em~YAq13i?-1)609>+xzmm4of5z7dEW3(6c4e2d^SnHZ@3lVleZa@nfgWkxW4ZAK#=d!x;fGV&hc&VGEF@6ys=Aq9zNBhD%|V| zZ9CkA12xaH9V4FWKSbf%j!@4VKlS>z4un%bH}Ty1HVO~wk<#tc`XZ+ryaSo~Q;Yh+ zZM#jn6g~AP>IZXF@6r!Gh-%<)@pW&#*gS9Z$?>2caQ)m`mw4Fcr19qM1MyQn*@wh~ zDA)7pd3aeRoYzMFv8!|lzIo5@i}}k0y>FJ^i}3Em`w}o{Pmb!)CuT$qusQ-QfYCw_ z_?Q=og*o0!(rW$*hp)}@GTB2hrp1OYygMO$4W_4GL3=Q7Y|%Y+i1EQ2_a3Qw^rwpYl)F5xJw+<3wSK)=td&OE#ats-!0TvQ>HJ(a-NZ+Snw3hflr859 zOB0oLy|cJDzO*=Y(zee2|M=nhwtDHMm%Z)7AFQ8yZmE9Sx!%Xl{hQM+eC-L@`njiF zxOSR<)yvKmmhV` zs}H{5oPD-zIppPAQYSpQ1QlwhYw+L7{#@DXCb7!l=P)T<@e1<=G@Xn=X6)cTe=Amw zrO!~GufkV5jM_T5XTf6!`qJHysQzUD!JMxTEo6g3eUL~;(l7uKd4-5l1- zGU-mUnX9KK`nwit*v77VIAzwz=9A<3Yy&-Nsen(U=Tp`CT4`<0#|6edns2;|Q~kM4 zc8;zL7N@F>Mzzsx)w{EE?Mh>A-0$Ra=~8+k)A0JuTn?|`TJ#qS)qJgnw;`2ldHhl< zX zgF$k+v(QO4S7(z`wQ8}PZ4?S?%ieguf}Q-pj4X<2k zm#g?wE7z(%jtZ2^rE;lOsAAy+bwT5MhjilJjbs})K-DFj<_)k73AcJq&l|ho@Oz=U z_%oj9tOIL?}HA`V9(Rk(t2#yt>1ee zn8{}9AZ7$pCHj3#^uJ32-yA$oTg=BVZairESdRPozMO}tYuVXv1SniHw+TPrgb7S3|oor^Ti`gA^-^;jE((n8Ixw*N1KY=ry zbNH5TJ5@;V8)KiJ$l;x_3HClw2s$Hh+)3VX`zBKd9ZcJ{WS?kiGHV>;9Gq2`L?uT~ZtYb)(!XKk)Mva&i}O^&0rFoE5bv%jSZey+KeXy>xy-Tr)q5*=W#*yY$V=+y0W;?KaFpO*rXnm)i4s>>j{hK{&jkuN$ld zY?j*e`o}4BDnlIXyM5AxmocJdu(BAo9jPC^Zyj^=jR%d{c07n)>*H+4y&~<1Ii+b= zd$oyb>yxR)Co}kt*#ceSta^!Z6I;^RuAiyxPIT5f$qFXzxD7JzwN~f+>PP|e`kJ2^ zY4U&bW2K!_oze8JW%Kul=7s)Z$uCy0#M;UgiUn`Jh>l+$$*xUgG9y{^^>W!?G8>>c zOhe~s7JaPCma%du4sIKs)Q~dOwgKwML^`!IhE!(f9j!$3w;U{w3C^Z4-x7XmzJPV3uFB@w)e6vzuHbtq>2~|%Qe4C|9bt^WxV8Wc5bfF zD0zuybELgmYvb7SlwTaFnDRB?*Hx*`?|T~M|31tzO*mw_OE}!waJ68_5a7eyXd9T{?6pKweO96>)N#^k_UXsf5%6Y`=_pe8Z|H*UZDQ_%YFfN7%qpMk`w@=bE@Ok!{ohyTCYj8`OJ4>h9aAxe-q1 zs5>-|y3^R+oga~KItOfg$2g8m*;O;7Y|@&BsQY&Jm0L+k`MGU^dGxU2wUKPU<-$MNAKquO-LU6+)b0P59%h^ z?F)V}T}`}R_4`Prh84isWWrC3w&_ilICRw6iFLw}MrXW|nZOd>LyCynworyVr zxtMI&-~6d|z1(akm*_tjGdj&mtDRizHY&|ds!=awn$2#v*>0|7lT+E%MJ)PGHJTZ} z+Fwqh|K*Fg$xs|=1i#e_<$ARMzY;$e=tL!6WI;cv!uWdzeY`_-4C_*dLLbR5AkT)& z0#DCW033sZpF3j42E5RgUJg%RH6XWO(j3~_xHo(neudShk#?=vEmTMFO4ru>cqyA1 zZ)IBxSwGkDGx>INeq_GAYMkz5)$5P%TBL7h;n%tJuGxC2Tdbwp?S65#oy{+F;rGlg zXZ==k0?x#^KfhA-@;I1PoL`%ppRZQe7UoKY*4*4ktJy7fTgC>Orq5w4yr1?-LZ7c` z`ZW5wtg!$Mp6}B9iJ#wQ_hFjmJRP3r@LMhko|mzf!m;Oto@(MD|HJs2Q<$V@`*V7{+&TMosmTR zw?QSKk!I|K-B3MDsdzB52Jy61IFuwPSA*(@%7t%C@2{RW^AE|7(Hs*$)kB!igH=48 zFrM4it8jA{k>kJl0TpiIpMn;^U!bKA8si#h7c-XK@;?;b=>*T`kuJ)6gYoqTo;TtA z{0)1{^O1hiv13^i=kRYxRnnU}hmM>@??R{YVXumO5!7ceH|DRb(7m`9k@gt%P-<#C z2LAs+=NZaly$JZ*2Vp^CZeqNXPAB_27mepk-ZTB(ofuo$Io?U8*OrsrWO`T8{`Ta` zx`Ll?ug$0DRtj^S^x9HtBA3tLE^^gdD6U}lpN@TudpWp<{)+k0!WZgt%{HzcmOw3Y4dp zD30j|lJ+S9r@;M&oES83;`%JVxj$x;am1(|Xt*BxJ} z%#Szw-O+TXvuiFtTU#7&k7jl)ry7M)t5sO4AeM5g?oW9oOtL4^{t^TtSM=~onHsjP zns9{rJ{HU9Mc~W+LbuwP^K;c2T%PR7 zx7hIfJ@78lmh~^v->B}xxE>AS%CVmzX?#h-X>WXr`orCKnY6zc4X1BJAWx4_I5dyi z5Mv8gMEA$rVQDUZs@W-2z9IRNW9ncD+6--iMk8I^mtLp|xAN%qIC{T+=wl;`?PP7O zHIiAGCr7d7&oy$53XYT%%H>va7H^!I&o;&tEFPORKYe)w%Y1X6!PVA700_;yGnVS7^^k0emFl7K8IJX=fmgFUNrAA zdUgMkWV@exTl%M_?kOC;HS}bK>WokmUPHLCIhv#6PK>n&3OCPl_)Q0@=gs(i6UGLd zV*pFyDIgxif5(Lb;oK(hE$?45;TZMh`5ewkplu(>Q$B-uqz&<{jspgsr|>z7zk7Qu zyhZVR0;2`;6UI;d6VKoDD-#~(Q|@s}xzK}5et52h=Sleo;vxH|a@{x(Puc3#9XC)o zp+S9C%oqmcSM@`7(WSoMHTB;O>p#z9RLk%^tS@mf z-mUZ~-5>q<`jJ;8yhCp-yfA(7Cr=nTEHOIy+mrtmH+h~mvg@R$n{z+hzGyFwy`ja5F?juel1p3=qFC#?TM z?QY_~_43&Bb(5c&Sh%Tw&i^fFwIJOfe!S1^`e^vgxmY|cIwyI{t0?@ISE%Q??oGM2 z9TJ4o+$tz9@BP5uFGjf@r2DenJFpr&S3uie_ae_rB#v7S+WsbLn@|41drM;Un055V z2&yx1>HLL>^9IwNTW5o`j86og%Zv$I|5?IG3!0RN>wimmrjf+NLp~IbA-9<^=NA%w zCHm@B98MN58UyrozlOKV2#Ad^A8Y?J%9xK9d>N6DnnMgb|Rs{J8`ZeWKh2I1#3G!po<@{4mll;^3 z$S36-eV|R@@FA%0O~HqF3Qj4lB-HApxRxLBZ_3-UhgvR2D0;=OPAj8|Ljj&wV8${1^uEfTF{b`Or29?5$5c;r+Y!75_zl_w8?c|LOgw|2%~3obj;L zRnJa%)yBlc;%aR!)A9;)8918BMhiOz74)lCva~j3{$dj@KkrQx$6J}Y-|^?G=#K3= zI$3q1iOYpqbg~7kbK`G0m08P=?)7fIKf){{#i&^Zsoi%g) zTbj zqTP&DqDtv|<$E2(Yb^MBIp4~n704tmY0?f7yv;+7gMWE!Gug;sdN?uCU(L1F=BnOG zd#yb>-zv9CEw9v>_2-M7*-Uo4i&vTYiaMn{(9m>tfZZVvjpahnQW;XpR_wFw5^0&fSZGVHqs`4@PFuPwEB#N zB^tvpOWu2*z3~5ZejwOG<|B9`#cp||wzf#!rLak$Qko*5k^>u%b z>I?PT-+c)51@%a~An7g56L(*_yKz?Y#8*KBsL%BA&K)|T5$-e6cHFv6v_Z$`+hK^P zBe7t**-Jw|Og*)33{`hDDv&-IZ2sHzae%8fzgX#Fu&$QM$pUuTa0oqLNp~m8MZfCL zV-wF$m+Q$1+6?cIX?23%yi9$hUcxcDJ`S-r8kN;TeI=7EHgmIue6c!OtJZ4OChqxm z78X#rrw4PZXAaz8J`C@@r@rx5rf=W~<|J-anQNfy4mvbN+mO!78Uq6ojXm-g0%lfK zXp|dGbV?qM`<53MR~P0Muvwf=V%l23nO%Hn2#3CKFL~B4G^eKURNpUR?bO57F*H`A zko9NV{zBVd!V8MAHt$V&*|t|eFDT%sNwI=SRk4A4&N--Gt=_@3N`Cl_wb^ocqM0t% zaiw(J{Ou-JC?5PVZCo^Mk2cD(-{E{Cz5qk%`{d z$Txra1{_wfcqhoLTcLB|{kUcWdm{#+LuuE9Zt${KfsTbvXgVNuN_k(j^(jUU8a;j2 z=RY>`+|}>XUw^H&)bwggOWsjlp}D+V?YFT-k18wRfaXdQAF*v0yu>`d2-Wdt7me2e z&m)yz`>uD^+5WH3^b$Mk^{KJhwLUdsgKYa1?|a|d-*@GQEDQ4wQux)nfUGLofG#mKGrMD-qGHe`O~|z zUxm|87_V?oo@@+V?5Q4R`dHz{wPo5Yk_{s=H~ige;0?kUq)-_|+-!z1R32)McKb(g z^$6qH=5+^`R{W`Cx!?9z#*5W@spPeGp5rZalj9xQC&Gz{uAhC@z1Mor_~=JJ`epAW z{zKlY{LfzJU+CTHf8IO&8}}w(>i^Hxy`J}%z0uz{%2`|noWsHIHr5g<6|aacUCDY^ z;J8SwRjFXaTYk-J&OZAcfB0+v?=QXP@yD;Z)Vunx|8V7%SNhMn=9+6vpP_ZCDZJ0* z;`!Le&<-3P4?S|P%?BTtJbE)9IpI4$deZB-3-9TDyN;tK~m)3CAPwUa54YT4-iE`CKNwHsuv?458c2rg4W2r*ta#7T#iQ zJX_DLC8PA(Gb}6n~iP;U%%^gC%WzN%woULpXzoern;jyt`)tTKjFU` z&K}i&9_{j#)Jw4j@)YXNt;4)siG6e5KIv$*56wMDd&h9PflhUtkT@GYBi2^nSD9%Uk zR&A=f#j5wM*q&M%y!y-Wo5z3 z=K1pGO}at>I2Sda3bQaO-p3!O!>T)LMdyywAQPcYiuLwO0D0 zcNS7z^q;r3aQ`LO;%HcofpZ(1FXuOfc zEZV1FY=llmC%n!B%me**`agR7$8Y`dxBVYYU;MbY&YScS-W#4w z7mKOoCzr-(U6%Szmo(x-|3TNX+P$s++#a+s0q^UOL(RrWzO?1{r<(1R zeydXLE?WyZJ$*4$#M$A+(g@dq~m?h&!sU8rdv2@lff(GXuen- zX&b*a^&s|!@c%e3W!525AG;2D8A(iyl-9w=gZ8b*h-KQ_Iu-g%8%>3mJ3ITG1a{yQ&JbUZs(+Nm91$oHG zLD)Ao_bJSe;|`in&Q7G;S8Xrv^rN9&HCNWO4*<9uT>WO`5LqVQ;%#G$5~hM zYqhbJVj+{ywW_UZd#c=Nd+Fu5TBqC2!QSlnJwj_#DSF2~)rUuZ0KS=gJ^Q0HR@_SK zGM{@Y#`kqo?@Yo!z;iUuzCYDUUv%U4cP36au7G}Won0>^PM!Rc_RXXT!Tq>8*7D36 z&*#35G3lav-nwqx>0dte%=DIjIMaXI?k}GCR|Ql#E8%OwiEIg`!B&7Xm7%3E;RYjk%sq;J@D=m zEH81+8(!A%U@NGH8qPGt)BAv6RR>lRtn7#rZ=p%B1CyF=ypMq6W^9Fmu#EEq;*w7aP z-b9Rdk6`)Kioswv$P3m@!D|avNpZXC^w@_j?yd@^c~=O}mRn=55v(e8qOsG1^tiq> z_CkxTE5`D;#jB)jPdxFHvKl#g*X=KLOf2lEOzdCbV2h>dP8&ptz+^S9s8jOLv_JmLv(>L zw=RsDKJC^8<~em?W|!f)b%A+KT@cL z)&=pA(vuG_Q>kZGVz%(#2%nt!38DRDq#r44`qzS4A4y{i2CMCEu+K8{8;W!KF^ZEk z!suFY9s5X{cdua9NBVb@7a9dq)7J}TwNmrmFPQj9({Co;!}LUoN;06oI?nR~9{U~| zdr6R99Lrfud@ZGbJCvTNR%5lrU=v(|w&(mZ%k z2D9z1dGHtvW_={Z+y2KOJ=O)q+rDV9$b4Z2H7T3{Uu|YP)rot)z?fSX1as=b%s*3{ z?)if7+`52uf?@Lo;kk8zd2U@`%&iNIxpjfD7fPK~^n784vHb+Aia*Ud#MoGXsrdr) zhUkLfx#tU9cDF7FcE6-&Z9(Ow=hlUrsjj2*g_+MHlxKvgUPI!vwg7(^V<+>Rx*&OR z>VlNrsSCn$>jKw{Qx}-$)&)*a<_l3>_*BdmjBSx3U`+j>l-t?~+Z{sDH{H6x80$hY z(1njseTgoFV~*AZ=DBs@ohCiEE-){m3q}zpXWn6WU=NHrJU(!o=Lu$gB#pUsfq8CS zV9c!xjHQC~v@RHIh%PYpxbOyb!IXW7E->cS1&%Y+g^U`*ucSKbH6^`4UEnw)x-dfw z(cmGvz&y7ua9-THz&y7uFwdw>|C=mKN#5{*4uTTpE@ zL>ElG^xV3@ao#9#4y+^16sWGFy1;9MGaM)SzRAm=E-=rn3(Rxt0_Vl83(Rxt0`uIu zV0c4x!SF;Ex{1=d(%2H#c78*po;g-*p^J5-QmTk>jOoz4LkaBzM7f>V^e+V~#(C5K zAy`$gADcK|Kygk#vIict*rb;i-oF_hMq2QoU zl*Hu473Z_}fQ=51v-exUY&)nj^q|Kky?VT^d;cO>&4En`Mm`d?t&Z~s!3xaleZthW zX?J?gq~~bc-dhA4Y$L<#T@b-k8}+tCFrAm91hZw=yaNSu)OBxMFh`%*{y)Rh+x~rn z1#7A^hzGMI>=F!yKa75yp4{;V)Bkqw0sDnubcT}FMm6s{g2`Un4xi$D_=hIWZwO}B zR5cG?AliWL3UZuZGkHNuu!ZXct2okouV7UN?{dNHnz4@aT)}D%-l==Qju6b|MaMZI zm@6+M!?7O5CR27>FDlL+biiQsc-ec83TDfo>a%yBV785PoZAGmbCjvEx0kZg*wg?myIoG9@_%;7gF;E`+n6@wNzbk$Y2Mv}*>$9^ z8eZS%6?unZbwTd420YcLvPTX17+3w7*)S4{adk8OeS(urYg z3(kCsE!1}xWv@-eDYj4#_S#h0#TLSRYK(1x+9;OK9@|14<4#t!-@R1VVhhNNtPSWm z*%rW)@-fCdwgrkat>QF&No*m+botm8!Zy~o#W2@m^XKbMn)QRQ=x*+wXd4VoS88jB?f&&Y5!GVRkzMYO&u?3Z0s0(5XDm}4pg6Pd_4Y7UStne^#)(V8*B1agO1I^M!bN?-O3t!F!Wn z6$e({1MjpwU@zE1oZ)LyP7Y=G3R`N>$8WE$;=4~1MP4Y z)pZXmF$QzAk(w{)^q3dU7c^%2lA13trs|~^&KKgmaJ~@dh4Y0NPt6x%Jee;9eSXI& zRO*L+9km7O|M2bqlh7T9hQ5s6zMt4a7!LmiWqUfI(`zRGZfr>~yYH-d-x5rG zq#b)voYP;RI3H&JiF{G^ku>j4;aMN)Uy0ZMv&qXR1haml=DjCKuWK}2^UzjkpRf!X zJ4Y}(U(i@#57^;?*}XQ++ea`vU(lErV6lAmekE8zd?dx|p^Z%$?3$`7dk@~Y!E6~c zc8_3ozM$i53ub*J!|T&3N2+&?V0ONs}(t||| zw2|hahZ@YbyDkIR2CIsXqopT^D+%=ROVMQuT2 zQCrYh)D|=rwFQO6Y(ZmDTTo?>*@DKRwxHvT+5+=v)*8139cR=QG%soknisVN&I@`a z(u>-H=0$Bmmmz8kiWjp5m0rvij4t%Q9J2*uTl7L-^goOerr+B6w#Lx24QAJoG=>@- zu!Tj$HrfAY6DNKS*n;NWCD@=Y+-i8A6wHnfn%4-@)8j4kE)&d_fyWibUM-ll1&y7u z2kiNR4eA2NxvyY@y1-a6z+(CA{aP@49!ce;w;v~BE zR@J(|>23dAkX}F3Ax>}m;|A;E95v4+Q1{S<8^soad4!oMO!hx&P=M&SPRu=DU|wwQ zY~~An^k&%CBNC^bmto9D5 zdx-Pdd%&FY1uma^z986tOPn-UqCGQJcK3XN>$B&aFG!ru`9dF~3Tz=dch>1e<_nS+ z=0)eun&+G^^tYPyobv@KgLA$hWf+gNkxI`!U*PoY+&NfBTB2O^uZ-CO_uDrbG-?aq zMCfGSU@=?Jn4UXho=g27^YQPQINu=ox3-{ppB0SeUf|K380nRWr}mw>KWLt|g(y$k zLKM@s5XH1DL@{j(QB2!H6w|g4!3Jz0ifLN_W?F!2Cl#{=RWAd!5REfx3(Py0>N;i% zI?kvqXkOG7G*8{fp07(AcSaz>W~i?zL&&gkaW3(%49V#qt^3Yg4?~ zUYjbr>@S3SZ5q2z(z8C2j&qw})<J@kT|U^Xx_boSzDMldD+|Wt{2SKiRQ6?OZ(j1 z$25=q+pr88J5S=Y=TtSu{w?lT%l?nXayBorFRQWrgE%|JPt{l|z+(B7bE=fjg($Cw zu>>|7-tmM^ppBqF=(k^yIITUadg(nNn6(9!m)@rZE3C_y^q9A=iL)t~wFMpLRf5@l zXU)T?i+tiedveE9^DycTV4N4z&w9rQW^F<9o+FrTcU^|33TADAd8WU}IaSKbl&VjY z-u6YqYuUS;4;@XVe)wO+7ATzC2V+Ta_n*fxCnk41Td;)~PwsePZ(PpL%$PWHl;ZTG zk`EV;$FUanLOIUc43BkToAB%%aFx$#9>-cpFV3q4<&Io{)`tS}JgVbn%) z_utvJa`!)MBTkQbuah`!`{}&AG=gznps+{}>Hs?l%dUACtqo?|U61fL?Zo8Fb|ugS#gn`Ld5qJ%rrI}g z9z=1@@+8>qKLx z3s#Z((%1_vCc2<8j69|c)<@FVSb(uzDooC7Q~R(kXbj^W;;aPckyP2`%yuO>Q>C%{ zB|WPPI?fvVoFs2~(d|7jzk(B0Q@LiYI5bsW0*Vf=W-$Y?JM=WUiD3(&F2s4EF2s4EF2u1=7vfl` z3vn#eg*X=KLL3WqA%-bkh-2TAda1|jOz8siRQoAih{qY~g62J)>RRc7#!R13x}Y)U zg}R_I=7qYz7`H8Bp)SOEp)P2i=@Y$B7h*i63z}!rQ@YSlx&RN>)N^#~N9a&pFxU`X zU<_+p=+jYMfMpHU1?Iu7%@|uFdz~$C)hPn{PLS2YsFO+=R z{RJJTQx_yJPF-NktqX#=b-|Qq5=y!pxeng{{&j26chs)VdJmxpjeg zZe3u^tqY7fbwSGD)&;{Gq6>`uz?82X_(B+;ahK=M0J;BJeZu?|cX^&5w4WS~$=taV z_(+N;bLUdvBkeG8{+i;P{*uILeI(7p?*q1QtKt2(;axA7)k@8KzhKr!(!4hdX3M9s z^8z0Cca6OyNH30I=40|{Z9((kWf;u*NE+k0Gt~?GND7m=bLb;!>~V>+F7tR*c9}bu z0v}0Z4+zitNIK3>3ufoe%$ujWmOGx*w(KM6IIj|(ojYsZYXfZAjJKM1W(3oD;kh%F zo$FfjcEr1B*l}tGxe@YDkT~7Cz&xie2+yet+(z#Cg7DnB zz-4gi0`uIu!08R%{f8#9E@0I@(pT8F;DHa-1%nOI1;!SFzBzBk2iAoTn?4QeGvnr< zE-(*iJ9Ob4hUeA==DBr&d2U@`%&iNIy+rb2*Hn32Va%-yjO`zky<^I*d2U@Wydk>4 zm|GW2*@x%?V{TpGINiGN7Gn#|ppBF+^stKqPXcUdBQ78J7v{Njf%D?l1n<7f_4f zxpjeg*93KR%{bBHb&p1cz#vW~=cHbfUpy~y4F zQgE-0HId_V>%z%Y*F$uH<8RO$_(3t5H>MoDQn0JAc&$ge==avYj^Kz76 zjy~8sP%uaPsWTWlFD9Sr3!pHr>d9O-G3eD zHsRUzYv%oc>RR3L%jHqyNFopGAJr0#ffoH{Q@3eUEYPVWH09CaPL`>%N6 z9nW}u#_s-`nd}bqT9a0I=BR7>-y^2~?cM|S3&HGpB+X-6!2Uvt=L-*-IKO7%WLpUD z@@O900^QeMG5+7D4UbgCz6T?`1Fm^&3zQ!FNSepC5SCA4YzyI89*warxG}bc@b16n zu`N(u*q_!I+k!KnvGYiZ7dwxn$}YAL=2K($1#MJ`)@SVQKlARPx{jSk(s8maP#bai zG>>f|yx*^RXGmV`djWL$*cQSz(mb|>u-!EeBRR$-s?S+dKE;cjM^e1lc_j2n+-tj4 zYyr9;>sf4DEz|$l7Tg%yLL+z&hUNvjAhy7|fRPA&nr(q}VZ!8J^8#HETi~(dgN7IA zg7kOI3v@v+%?orv%BQhF7aUli3l1#Q1@2=y&OjHWuV^gLg?K(gU66fe#S3*I-bSG= zI54&aYNJ>_V|V|#KHo`ot#pCs&Z@3scmFjn)P;1kZIv!Crpp)Tf};$9E;#CpZJ`m^ zg34#83$pL5($jbU!}-Dv%!G_>g?AK;kGBIW0t52}J`<}jHD8Eh{~>YOyAm|APwuFi zJ`Ke&a{_y2TgQpt4QB7l-fVcUGQ3Yk;?%tNMKH~KlVJA#g2ux60+&H!r|lumaJ~@d zh4Y0t7S0!9n3^xdvHzC5*!v5r>}tNiJdI5Wk8B~BFYGY&spbos_fo2BHDAz}=@V+c zpfToMAnDom(|HN!3(Qk>9lQIlc?U|Ij`maY1)Ud@Pc>hNrDyLiV7|cEBUEa71AwGu z!!b6a-yTb7e-Bv978Fm-ojI+ZiSu}hQ_Y&C{fGuF*Y-Ua%*#?;$815z`Ibl< zX)J0B8jIQj=LIWrNH3f_$NGetJ2R%sFfMtqW3EasW(z94*!_inrd&Ag{@;o4VR!#E zwq)`_en0E4#=a$(oiEVN-K2Q~Kq_|MS@ZC_@&D|6;U?k@x%;nq?+Mc5`GV$MDwy>F zHFl0*_Pn#k3VXl~7tHQEYu-MB+4+LTya0>kGj`uu@nU!XRoQ!AjO0^e@cvDGS|3Tr z*%r*+UogBOcmH*qyha_)7c`I8s0mv(K9c60ZqpP0*W_i$-G9y7Pk8pcfi45DQHS#d z#f#l{R_Vp=J3mae;JEvLhv|PmA~g0M42?x?L1R%{c)-N@a}(zmq-=J+pm`|EfGyl; zc>iX2pNQ0n=0$Bm^P;w(v8XL*ENTlHi`s(5qPC#1s4XZgW(ykoPbqs{{ApG8m@Q~5 zY708fs4Xz>+f>)F_h9Haqqd-VQCrZws4Z|_zH8Fs{hDy@ta(ve&}E3)g5t$&L8TY9 z1-kW*GZ;_@sb@QG8r$Og)3Lk%8bi;PF{d1k0m4I#8f-uphTQ$vao#1oL0!Q79(MUj z!R+{;d5s`FJ>D|!GQn*5cwAxZ)q)M`0%PzVO&MI+^938!1?KH5*q|;jmJG01K4W+P zRX$^P|5e#zcmFkZucSAq3nnkIyZ_8X3&0lMFFbo5NyqtS!R$Jc=A9Q{N*9LQ{pa%C zZ_>*N&$f|HZ-2pTyX$)4ef)?n47vNS(u>{we}r=3xchH>F1~l|o-fEf{a>0qcHtu_ zA5XCV5X|~6h&7CzbI(NqW4!n}#Md!|%7qtbAMQuUH8MOtCMQuT2QCrYh)D|=rwFQO6 zY(ZmDTTtbT&7CzCojdC|@jG;{i*F_K*uuPXsjg#lXB}tM7Bnwv3!1kjVkexJ*P8UA zwxD@YThLh478Dk<1(jZG?)(tt!g2TiGYB7c_g`bbFl7thv#qi32xfgGv?7c@L`P3NiFNE*e)^Xx@u4{R>F!NyLu!Ro@W_=_b zXU+0t|3vf7x0u}h*Ss?#80TfPN$*I(Y#V9b0fO0f*JW5ISXK7g6fd^drqYxBg|gjK z>pzEbL8nzse7Nr>^D@*Q`YlE`SPJ{MPE5|J()~1EM^Zewzd(N6yqQbPm^e`?*us}2 zJ!=b^$NnwuwaHq;ZHBj(;ax90TPK?Le!=X%v*x`yQa+8H7w{S;pBj5fkX{_iSxnCN zYu^4roSj&lsQ_bJP&_%ON_D*u<;mTD+;@(*k(^V-eP;)TH4F6jRbiH8@a%j+^DxSr^z1s4=A9YAI4`E3$=!cvKkK2NnmBE{>oPo5 zFl!4cJvpaJeQ8S7r%7-7qTwyu^GFXJL#2KgJuC?4_8|{$@*y)m^w$r^rdI{CcgGcP z`e%aKGdV32=L;!Cx%;XGbFuRVVd9Wmtp7oJ5289!h+gxEiU$DIB zg2sL>nCL=}Rz9VT9!5QwIIS+Idg=YMV785PoLDh3yu!MK88?}C0JTw%*X*VOUC?nh zglBa@^DrYcaavu_yjMps&Wq`1JzleO_Q4*n*-Zua+H@KAl{l;7BkA^g>;T^AHExCoH#)Gt`ARFVuxN7V1JA3w0rmg}M;OY&+QTR^?Oa0`pXz zDP4$Tp)SPI8=?!(Ft!ltLOf2lE-)|Dg*Y$N1)UeumwKTt#Cf4EXrAd4N*Ch1P!}}M zq&Gwt)`=}dZPGKg;MN6$4bcV0eq#FHl>Mgd!~bVpV4hPK_#5v-bb)zJT@W6|12a|) z=z?Gu3ufOHX2uS|oVp;GQx^nt>VjZSU0}?u3xY*-fwTa+V9HJ!VahkC3ye8+LE?1k z!i;Zh!L17%XG78(&;{YSb%A+qU102#NE_+$IdwtG;MN6>)2#~z3w1%}SZylx%pb)T zFm6it2dSQCene;=R>Wg!9Gl)HnAKm!oBpX_)x<^*yEL-WoJc+1gx>76Q=qYw6u5X{b-xF}C2=1%nOI z1;&1A`onVIBe5jLxKy1+b)2M%3etSXrGk$CK2%&iNIxpjdt zw=OW|)&+wN(FMk$x?6$37B|?~J*1fibr(FqRGCWFJY#>DC3q8=?!0 zxpl#meTXhF=GFy{)2$0A*-%~JINiFyJhv_|&#eobms6z9tdGQ+&pfv-FwdaM$ z7*aFO60E2(PH!4}au~z;?mxx(4wDyHKWykTg4G?oRwO;m!|x`}vhXx^(H?lHQNwFE zcqa%}abTPF5a)*BSZ__Rd_2zHl3b*xWn@>)Ud6zmd)0cYZL@?D3y@Fu2jdU3f7tB%Dy?q25=x5xQdYB~-^d%MN z_Fow+yyK7Z!WPEF7Q*oNQUBZVJ3{+dDMNftYuBrZmiE|glIgQzw ziPOc~A(&lPBt@It@iW7_O|ZJG)9UoD70mAKY2Mofvunv3d*dE>r9EKB3uf=5>o_L` zv+ono*!tmd_ErURl)Z-iAd+(c(^i;j{9u~~*r>VU3x)F?d-=n(j{i9&TcwP72 zEtt)x=3U~%Oke7~DuU^}oGh4aBVC5W1as7NZ=+zg-8By#+tiuOr%G@8FAY`;){Ed{ z<1P!HJ12gy)eIPi!Ih{=&n5Z{qwa#VNKxya}_Opm{qaFLoX2 zYQx)Zcx(%GoI~iwcx(%F=7TZKV_OKzpfR=u;x$a18e>~Zlth4Ajb#@H6>*jtX}Q*41S-41tCU5hQyJuT+xIN27$ zyF83Bk8J^%oJZ0;wuKN=b=_lI2-`^W*cQUO|C-0PK=nCm>P+(zL0?*o)~DD)c#q%c z!j79kk+EOunQeH_x_9cQ2mVhb7zbRnM4P#44&6fe|;csqo; z;J`v%h~-o1f^G-YR$vSCeg@{Lx(;`G1-c-% zpz~?_wcU4C>4`1Se4!l97k1Rd7WnRe;P>yiz}P}KUnqxj0>;#QA&xy^?5n%GfwU;j zk07?m9q`W4r~h3ry7v%CPt6w?dxzm+77N~IgjaX)T7uDiiNG)3!92tPtqjYj<3x@O z=Hi_#m@R|mh4Y1Ydf|K_j%^s8o|-Sjcxt}D7>y2-+zx8K5XYSJ1+Eu0U(mcF)wP;0 zXw38pHDAye^TPRp#+Y}GlszABqh3LEof{>%21d7pvoSz1&u{*LC1Ng#90`K z6RPI01s&&mB5kCxs4ZwLY73kfte7Ibs4Zw-)E0CZqPC!UQCm>yi7kXa8RkcrFJO(v zlr0Q@FTy8xJeAP?(}rWye-eyzhTg-kc+)==%)$ixW`0k-gU!R)@X=1mJ`eWW_^ z`hPUMTLiQ71I$RjTXW#{|m@PsjOA zi;0h?`wt^wFS+K z+Jfex4hC#NV^LesSkx9Y7PSS9MQuT2QCm=0%oa2jwFOo7m@Q~5Y708fs4Xz>n^e~^ zThMVvZ9(&*wxD@YTj0F>mq{;b3z`?T1zm=yEht{h7F2pMTQIuNe>7$bZ09}7WB=)q zex$JJKMOXX3w^_z{!zphj4t#slY=e%n_vUFAiTa{gSv3D;oU0OfG!B{qk`FWBpy4M zhdI-LEokg-0-n|d=Ka+kuov4n*A>+G&b&hf8`K5H(m|YB7Yx?hCD@=YF!ocy26TbT z-g_)aPw9eSpBHRU7dXy41RKTmB+hqPY(N*7R}C+~@8Vr-%PQ0_;Xcc~LIZL9X$n8$hffJv_)JV!r^&7E}__KDP|&Zig2i%QSV zod?&E`frKZ!uw6%d;{e%Hg{H7%oa3eZNb`|#vYM87NssVHWSG|`Y~(>-ipH(G%sQc z^zI9s*CBR_*rx4lJ?l85wxF@7Eodxi3mS{sg2tk@ps<)NXlzN!P>MecE$BF-wxF>KBJHQK=-ip}0tV8H+Jfdq=gzteQCrZws4b}UVsqzDn4Ypl zY$2RGn>E`0iw*h=Q#L0S+iO!i*96u}$_*p*UlEZJM`3(z8Ai{2TD#b%1x9 zVAe;{ylVxsK9c5PJQ(niH1@`T$Nq)JN_)VL7tHRpY2Kt@)<@FV`XJ7(@vRgV+iTO< z&xB`vBvtmuyc)sO{zCuxRM)Y+HXSFF!|?2xZN`{)iN$2EP4iwA z!8k8RoAgc=%=$>0cbH(-N77~3D46w;G!GqH^0}z$+N3A@3&A~weui?P?>oyr$5w<- z_V*(+c7H)(v2&^#WB)chr;2eKaUMu<#_lg@-sesJ>CE|x$v-LfWFKB3cy|b9Z9((c zzg-UYoi&gB+o0Thr&;2xN}P7zS@YPxr8wD6GNuz(wh~`-e1sh-W_0U3(VV>;_O`~nB8mBalThD zYYUopd4TbJLG#XyV4N4z&tmr%H17!ESzFL$m=Mg`g653`>G6C)r6=c9(I@1L$y=$^ z^acRo*n8LH9SyttuP}ATlgBuX{Y>JN^GFXJNpU`mnnIucreOB&zvl5c23wFj;OCjV zWDTz?yt-hThnWcXu>sx-3=ef+Fk3zyCy!(3n*%&fkH;}L#^cyB&R}rab(}noxp_Q} zxiQ-gjW*ieH#$7M=p9eG^$%URN^D^&IH&Y_ zgb%y>udvveZH@iJ~YAypIWHeI(6$ zr(jlpHFj~pg66$Cf^lA+W70cCFxy6&2k*<& zr_}{r2B?_S^{grb$C(V$TQrzTFTBe$pbMCN7*41cY|ClNW9;s~!jvws25Bs!3q}zp zA3{$U(1kcJ)CICxy>jH8& zR2P`%)CIoh9lPVH^WxM6;W>3dFsCjE=F|njoVp;GQx^nt>H=eKT@cKz3#QJ7=mKL- zU644Px-jJKzpCqA!}Px?%;`GeSUlwJzvj7hf%AEa#5t%7j5&2d%HY-o=DBsjV4@3Q zpZ{CxnZe!vF#N9(J~^{!YynneG|1*rVKR4~f-hPzb)qqzJBL0JW4|=E@R-DD_u4d% z=gw5mJa>ML$;*Ej-W|fTK9c5rTrle+Y2LdcWzbkP;5AGcH1^6pU?&M?eI(6;=WOcC z&Yd;3cYv{vq%fH~Q`r}yd6BvER4{i|WtY4EQ^6fijm=7WwvBXpJa?w{jGnLU0}?u3yisSfibr(Fy__;gALII#+U~XODINiE%Bh|Hgz94bB zb%A-#`GWAAx-i3eIVEBXs!zn;6=wW?;W>3d(n|)mpymtA8=?ysQ6u?a+j_6D1-C93 zY=|x}_EU+oA!p7}N6?xNnLh2-1?IVR;cCNk>jLxKy1+cQE->cS1;*UEz?fSX7<20a zV{TnA*brS{>=#m>by?q4Z8Ss|Ouh8ny1;R|bpam#P+j0S@zdzupe``atqaUMH{fYq z;JlnF*q|;j?+C%{+?mIB=DBr&d2U@Wydk=P*+wKEuQ&Y`r*Verg29I90%LAnpi&H3 zN19=tTNhqpco;!U8(1Gn<#XDt3(Rxt0%LAnV9c!xjJb7zF}E%-=GFy+4bcV0+`3@u zY=|x}=GFy{)2$1~Q(X_y1&*^J?O<(z+kts*U0|MD7dS7c4DW+Obb)#M3eWmTs!#OX zx?p(v?*G^l&RAbbrHJq2UOG~gSW7F>-o?)kExW4GT6{n};yl9X&klxvX z*)nL}34&D|acI_CKpXxkPtj^RKj5ziMNss0Wk@{5Uku>j_RM+Y}lEzG* zPR-H$R^Y)3^ja#!R+0CjjbOZXL!dm9%p#R zGhX)aj;8~ASkkj=swyw)j;C(F$EmJkcmGvghj%;&(qkURJX0^W{WR}Y5lrWW*QlL+ zuy>g79CaPL`>)fp{o3Y5r5C&Ve+89#$2Y|m!ZSw~nEv+#Ld6!GSnNEK;>FG*q0hk< z<|xkCc_htaTcCU8D`p)6K0A1yGd#A1@b16nu`Psmc{Go0AuNN&*cOP_h^5E2;KtY% z!n^-EPPT>c?!U&^7M%Hvokvo<*xi3s_SktOjj=6+cmH*qUDLmNc=un& z*$|%fku;BOA-vzOd29A|MB(aah_GxwKyOt zr>G4gqH(|kzR(~9ha{~`4Mgn>A~PM0u@eOf)YzF@CJQxrMxzW)&u4~?GBjdLCy{S` z>s!^({>#no!J-aIHrG4$% zwzdDM>fgRQzkyn)zY_r3dS^nbFL192Ccd!M+fuCf0&?e-|7p=CNd?v-&L!z@(I3(A ziZ4(L(DGXnulfS>ac!^o0*75 z1^i8~ZE24IlYF7aYQ6x>^M#e<4w^6YyqYfn^E2Lxo!pIgi;sKP!|C2~)6eA_h^0H? z8Ioi%6mzgYsnWB^F1N?f@0uZ!G3Pyyl7u+FZz6DOx9NN1&xXJEX#CWFxT?C z!g^lK7l287xqiWNNMJveM!jN+WLx>?ud<&{R-Tv8P78u{T2QRj!lK9AlwwNmjD9Gu z)q=fyu)aLQd944JtHG}=GyjBb7i+xqL_BN9j>_N$$(N7)2 z;pF9Kptjd)LE7u;7t9f?_fMu4>RIiTX>2QxPH0#AFIZRatk{>mkF|cm_MFAskYaYV z|H@k?FXsz(#-_$QKkldmZcrIVNl4 zm3I`eT)&{$6G}|vtAcg)&WhbjUOpqq*t>dX#a7A7HJ%#tR$}?Pv-4*EndZ8ych;C! zlb6p(%Db#!`~8fhyz_`H^1HM5WyRinT6?Dw%WI^(V~OSO3p$1;mY93n_PTm!X|Jny z_OBzef9kZ5oM|%q`_#uy3xai8P^{I$Ydq$CDQ2ey<+WO{`Ln)!%6Y98l-FuOd94-{ zYqg+Qs|CecEhyG%L9tc~f^}L@tkr^yz0-nXtK2JD3mUW40(hTKbKPk{W42mQUaJM= zS=)SGK_}?Tm%P1J3(9M?pkrvYAiPcs(q5~D*(XykO77hF!tBn3cD4VmS72T3zhYnE zxbt^s_l4P7irLlvD{n^L&=+<(@596fzQDe`tIYE*e?H*Z0iI=bfiD0vjdFd|7l6HN z9qffUCTst-Jv(K1%%LwJra2?qQ(thbtNoXlQ+tXyZ{Q0Udsq7}^EtH{cV55driGh`<=TIZc}>RnZU7i~b{6*b@|`2)o!?;S%g4OE(})dxf#cgqZ0HM^v%`oj z@;;KZ*VX<@dxP5llt+2KF#Es(_8;qi&7J+sHG41Fy}Ul^5mId@LA|LHwas`@uy_~}jy*tb8C&`t~6?5C5NW1SWh z3$>84W{Y`wGtbIvwcxq`?4^myhqhXH5L-^THmRyMfjDg;RW4?)f*`)7^X5h72P+qGA z^d+5Y`rKK0trm0)hY#0%kMdeANPAuGyxn)vix05>*j}k;q#v`NPiA%^wmtT^ajg5+ zCcN%j+hrbebBfu0Yg66|+skJpn?LKzQO>)WSUw{u?|NeS&bIQdY{sD2C54AGl49qq zgPlw)-`Q5)gjhZ!DRxMSiL;zw-M2QyzD8a?BgxpiZ*7Y0A#XEte2uxQVDf!oW;@UJ z=DUgIdS{J!EwP+C0|O7wx%xd#d8QWMXZd${Uq z<>h@n_`Wdn_V|6F)G5r4Oug9ivqnjM++S)uEuXag_tb=Twf};3wf~CU$Cx=^u=|S^ z^XVyOSNpF#oNxD+@6IoG-eJzWjWM$ply?)ctOezndk@B^n0bq1`Ma}XINw@rGQa;Q zb{ctE3yN(lF?V|IqL__@?LiBIb+!MBJxE^Gf{eZU+l7klB`?=6Xv{kb1}%X18)cT0i8+%CqOPk3D1P%Tv9*Gl^v_DDMPfSqnObBZy@!DDM%aJ>+K6 zUcINvdnESUv%0qDRV}bS+%#%6#?}MV0-j^0lOB^APpqr(mhl|3--E%nws_2wQp~RQ zUwL?r*<7-{?{(fW&fCSY?FjmU=UA<&SKhVc3J^nB-;`i*xhvFr=VyOCJ- z1?62`#^>YSqu6DI*JJ0cgIU}5jI&xRo;@_?v4sa;Q0$2X!`CRE^z4V1KwD2psQ5yMi7)h6>{;SA7AP@~SWNSoMV-yMTSkcV9K8_1XPC-xB zo<&};2dC8+l;?XwekY9f1u?5HD9}h@OQT+0pcYzRupf{5f@34T04(+eJHd_m z0(hY>%)jZp*cZSHeSy5{3mCUQABa_7=&|YxJ!a3J-WM1%^aWy}F93^ufmrMdKK2n` z0CqR~Iq(I>R9{GcGr*sz^KYfO9`OalTw=_eFDNhe1@JB^y#4Z=26hIqoG*Ax+YG5M zp)YU@u`hrZ`+{R#ji=`e^M5xG>zeyJk&x*AW#9waZHk!0*$ji+LJ$;(>Mn8=-L z?LT;*O>@@O{%g$Z7&B`@d9N!N_Pg?sJEye;rhP%~9PYEO_Fs9(op;;&jlDkC+8%P} z)E<06cr|zCdlC1A`5$&#fX;s}%}Jau%pHq;fmrAZ^F1kMoG*|U`@-$c3w?pS*cZTy zeF0eP3&3Ju0A_O(_yVxl7l6gS;Mj;S0DFM#Wi7}W#lC=fnZ~|=n6WSXhHK%yY%jl4 z)%KRkeuvh9ffxD$`x5%X9DNCWfxOrkzzcnWywDfW-iR;Qs5!R$+=RB?>RQ+^j*a+& z^Oi$jn7_mKw7G-tqoFT=7yH7M&by7gp)Y{f_`)7M1Hikc8G~Z6FMt>O0x+ASffh98 zMq>GQ!YCH|g2x>31z@o+_}E8$0oY#lW#|it8T-O^*TVbA%UX~RPomv&l!&h4o<>I;sI_yVxl7kqqj1IbtrcFZqmw^#d>V1@K~DKwn~C05A3h@M2#8FZKoJ zb+!Lzr%}&srF?9tjX%e7s8j6cQSE<&$sLSy7Fz5c#$2)*@Pxg{V;-Mcl=_99Hz#is zYsa7GJR7m`ZXvdVF{Ph!9|`s0%DYO&m+samW={g!)2WuH=bcL|jiKdP+s@k-V8^Tz z^U!fj?qGE7$$g}5&g2e8kKM)gk}ou4kXjz)JwDB~+(%OES&qGlG1D5=bqDW?5CiW* z!P>rTCbo4D6Fl2BV{K{%c3u^ygXg?pT{TEDXJdv9yW|_D3 zf2n+HJIiB!Gqu>&{woi6Fsv?*?{}Z$JUek2?`Fo_K}>nq6U*;Em3Jkv{Ju`HOV;6? zvkrDLvHVU|V@`$zM0(twaSV%m zkEHEE3#p&`+P&1){_k;LSlMY5Ji~MD3~jwL%}H1LFPQkkR&PtO;tR}o;0y2fJ^dNB zmuvr(SA2n5z_a5mqJ^dMPE~o=GiLtoth_6m@hMh(f$!E7E4~n5)fcw-*p*j&fqO-< zL&p1AeSx+A!mGZ(zR1|CF9aC0uw=h8wd<$&0tY=$R6?SC6{?t2A)7q6TtgWCU=cLRC34#9JRk=lR7E?tLrF0ovLp}d+e;C{bi zHDBnlnlE&iS{SF9IJKkS)j`}TVh zI;dYr9Kkv*DAsD>`#ok-t>(_?zw%lwJlAIC%JPsXPp*gUb^3dp)qe~Up8|efoHuR-Yhf*(Ao4`(ulFXo}g@J1cLQyqqtblz6j$cixAHP}?bueHJ(PD8v0T5P*b_=jt9RCzSCg0X1?62C%t$~%u(uKoAEtQ_p^*?U>cC(lUAJC<0! zv#n!zVu`u8ZLh0$miF3u=RH4=dNKQYr-kpcpHF7Cn*%Kf)@ebpRtu(ki)l_DXhC_) z99z~xdTLB&f9kwe3(9M?puAQKinUr$?Cdhn(1K#NRs$_4)@nhqRtti4T2QRjf{eY> zf?}-}G-j&>@ct~#b*BZ5*=j*~trnEmY5{%uD{rsWg7R7|=onfp2(QzEwAX3DePQ+o zofe?;<c_D{wr_RXaUavQ-Nvd!^8%@F!O#czl&JT zot5`SVnbg*%-0m2KKp^aY#q!_d(^^a@e}ZlCN}g1U>izI^##Ye+J6~CSNkt>*46$i zw#N2`zJT^_BR2E}J2{yaZX%Xz|25_{8Ds7Y47|mHi7#M{rsp>p#<$zsJB?UgBW-UZ zv7s+u42Kb0ppmogmoYM9*p#y zLwkw6kbZ}r$K0gvb;xVAfWExad94<746PP)46PQF*J?r9>vCuRzA)QDE!5oE{NMI_ zdP?N)3nA8hYZG2q`+t_lJTt}YzO^ZD#pg18AHlb_=QwZTyqk&TGm`SICzj7h%Da+S z_Fu&=DLkB!6gy`f%x1^dynbs_p7qwTd`432kP;Jjwgv0HwJG*B@^bDhWADDTDYl2a z!S{uk&nLex)ZeoW-U(^0yV`$^c`ak++*x^-XY7$1{OqW_3mOdLd%m}KI(t;wwJZ=z0P~0^Kic1U%s^|?B`GWE;ZZP!4_u15*lJ@+lYot6=r(;NWi2T05v4uk zX3`$-sakuux3cPr++!`}exRlve^WkH@t9ZI?nNJ2Z=ZJa#Q(*D)OccD6~l9^*8U%r zc&X~&D(Ot09`l?aCZ1!p_CKj$vU+UdE$yT^Xr7u$B>0`herPZ;0`$hI;dp|GJPY}zs{~Gh71;bv! z`q_!ZV!oePuKky_?P@%g_j>a37?iiQ!O)izy*)cOjO{b2@$A-5YCO9=I6Zj0y|%`4 z)!uj5Znv>JXLs?7r`pdatItemSIZ+-Q;Co(3l@7*a3c?0Wa;|{?4|>Tp};~g7RKlF!+M(ub zlb3x#$M9rg*%y>|P-$5YB?kE4kUE$o*}-V&lO;y zFRZ2~f&NaIo>zSV7@i%EabER>p7(AacY5CTyy^=*R(+wzsxS0d^@Sd*zR+XU7kUiO zq~wVmCcXen)_Fs87 zk(afgyla|yR&25Ga7I$>{BHV1w-zP`TP^-y^mPdg3NVS(9{KL+>l~{hKD*f#KzO(XTUqH;* z7l56|_HvD<#*BRdyx13j#lGO!h%W%k&yTDH8GGytn9r{EU&b(vec=OXuDjm}qwU4M z0AA<|>`UkibBr(a1@dBF059|f@9qWyx14Odqn9c@&#F=uJ#|k@ak3zd+-eS;WQ^BzTnu1F93^u!JZ7G zz5w28>A(7d&6M$CUjXmMW{=_d2VU$8z+ztjc3z2z^OMGmeF41K7l6gS;Mj;S0E>OW z$3EfO0{RmB0(j46Kl2$$)?pg^ z0(b|Jmuq>1H>mx8$iAH)mqtB*F13K#|8!p`y}_K!pO#RmUqC&&U~&he$L{fd?6P`i zVBhI6Cn@Hj_P=Rwc~0IY<88CPq^y53Pm0$2NPT-Zkhd7{t|GQIz%E^fcP_CshPE%K z5Ze}D$E*|6Mq>Vj-!;(0lsgz*dvYJC8@t@W=&`%lUdk8R_HHkj@*bV$dQkh{tdZQo z=y~Q2KEAYmE$>3X+P-Wime#1{*)!W?26H{A{cm}eC6~T5?ePvqt^K#>m(?#kOfA%( zo7Sw>;2#n?Tkv=xw)`)|^6y0z-tylN%kMak^qBuI#a#X)Vq5s`P(vmjJG$n*|D`t zcOts6Pxm=y^{xG!P5(Es+@8$Kbjp}{jifKr-y|<%;Qd9K>*=o$%VXD=zpxJPCy3>_ zR^Ce;v->>3_{>#o?BPC}+R57IC0N_3jmLRQ{k+`y4e~bi?cIrI_4e{j&7aaMpQaY- z&!6P`ZzatMwGd*|!gl+cmSqQVd!M`$;Wq ze|R(NorOm&Y`6O-=%v^{9Sc)Vi*3_NJT z=9>4Blm{&&>``4`poO|d%7Yf-IfE9q+g+c%?Ovi5(q7uvwnr_P_AqBFznVt9$J`-n zl5+_y+wLk_D4acli7!A4iWOhryTc*+5L%US>iUiF0yzW zn36B_*!|;Umwcg{mqERA+urSrneJ@YbvQH4wd4zm`JRw`K{4=bH~AW+JKHrTcoz!R z_GL4%tpTs*3;nztPF}8Ck^MQX`9jBIzEJo4#x&}c(*|0wKCYZ(KcDP5BB7EyBVPI` z{>89`Cl}6=VDQqcdP*J?q>pjv2otrny`YN6I5*qYf+IgDDUKeza{ z-z{r4EgbCQ4zcC06U+I6@Rt9ISbpDc=LU=UxD<2w4~XS_L3z806t^A$y=*z?2-uD))AA>zdP3txH)G=62<=7I>NYc+c|ASc07o@#AztXgK zbn3fr;t#YSSf>TWS}oZAu$W&>F*_|Nuhqh1o%cu1Yqg-fRtw5&wV+t5 z1;tt|DAsC0u~rL;wOSCY(}H5H7G&(578Lt9*F0-MW42lV?=xwxJ1uC;Rtw5&wV=Eo zD*F#QL0|rlx7TVxd94<746PP~*J(l86D_>WHS4}G+uLaYI{)3&$Jw`){)=8`jxFym z^NieCc*}oHY~Twse>b)Khr|ZHz?i>7Z0HM?HJgThmDs=+$on5ndw2$b_tV6Nz5wi% zg@^GeZ%b*f$DWt5fiEEDGl&g+0obDproQ0V^xuf(_ej#0>E9C@_yWd0{ZxtBt(K3XHpbY!y*urxY*?*H_mkS}1JnZYh;rtdz0I|NJUzq) zxpTuC)cz;Tv~9j{eTo_93uv#)o&AhAvo}G;)7-gj?*&GjI6 zu9)wMLGD~J@WOn7{k)Lv(x~@~utBfPNl0-d@g~hwqVQ zZy0Eyf)r9KXF=LQ>Sp~afqd3L!)n4<_C~3vGL< zg@Snoy^z@6UY=)-*=j+tRtt)?T2QRjf?}-}1nab**!>*C4%@e7e8`<=KS3d>A^i?#d9RK3mhJRq zW2?3Q{!Vyi>M@?VqGS2orR~|1V1OylT*a|`MpEp@3J>0_*bflP-`W&=e#Ur4QtX+; z@)=37$CUPPMiMOkzJRe$_mP*+NHTVQUr4{#8)HyxN?txAX?wp}FkJ`V71LiKmd{8U z^A|FYXCz?YL6^2)_^nNOFC{O3YeQeYJFV?>Q^tZZgx?o9FBZesc1gc4U@yhr7iQib zzc1|1-`Y~8vuVM0smH57f13S#GJC3{rX_1D#Jbvl;dQnDM|sSrr<@$wM`>(v4$jdrW-Zjk_6k9Aj>?6g_Uk5vlSl%nj+gNxz{hX?pjl}0O=x0~^ zFFf8;O=CwtW$az;zhZmYUanuzn09jZzU1E(2432|`JU?j@_V*5=5^%dd#cKNUBQqq zDDUD1LtlKKb+!M>vwiCQ%z2`Y!Ol{g&%Hj^%6mjzE?Qcb|Nt?yq_3r|8*VO+W!IX_2lI-q+K*w^{4Li2OA7? zeWJH_7O}iW-~qG$h870v4j+07vBA3IcT=>r{~Wu=>}ot8m3onW1Axcdo&CbA{wHhy zkE4}?2Uju-#Lid zD6PFSh~+g(yN~;<{rk>?F+7>PMVyfa?HyFw+vnqx_PW}C_=060&arb4?(Tyi{N4c?XZTH{uJAq!x-VP+MvDj`@OP zBfbDE_66(Rs4svQ`U2{_M|=Ui&=<%HeSui$3&cWSAQt)pvCtQYg}wkR_61_GFZkF; zd;wVK3yk>@-z({For^ETwg0*&rc2}vd?Bv=AMj#dKtIo5%%Lv;dp0rV3-ASwA@&9E zVqb8qtMPnv+L`lz>$LEY{d_Y2vTMOq>ztM*8 zcjY_VgMMx-yq#Ul!wQBolC;O%IgR~GEw8Qd)UmfUo&#(z+skJpZ4bF~(t^K7BJa~_ zuG<>VLCotsW~!;j_^^K9A$LyLejkIZEo(f}_<(6&&ScEIM#?*ZW5`-i-Vx;GGm`R< zJE!*G8N%yoJRjvd^Y=R~_*rfKI|+^R1z@o+5DR@_ZcqI&Um!2`h1;AL<_qM-z5rhA z3&3Ju02ccKu-F%X#l8S6_65gAd;wT~e(b<|Bs_01_ShGQ#lC=;u`m2un(Maqf6!j+ z3*d$M0{asB!W?}$qtSxw!8l(aFZ2br7y1HtBfely)TSTMmYoVs3$ZUaHsTAw;0sOd ze|np`VIe(Z+-ruu0AB11J6sDkYtGBLv-ER0_66`_UjP>S0Cm-XA*_4A<8 z&wY-`+74>}@3%AjBh#qo&!ZO7og=TiosqZ2@-7Ix!C&$K(zM=0*A`_mR3ellw?Lb{E@AzF>D0bnNoCNSx3d>}Ou1wEMW1rnYOw=34F}_3a%`S%x= zOhY|yg}nUU?H`*tfolGph=hK)I zVywkqQ|yrOnEd+-5$_`wQvsJ1gdU zVv2XvwVzmb@GfV2dHrNh$lrHHOqrMI>Ez`#Qr>aIg1MgJ9d%tJ<>h^y`y%b}?=Ni6 z-_-WldCF>ezCbOMJ4b8(>if^0C`Ah)*8VLLv{3FN zt=ZYYbop`TK?^(b-(MK;poLQJG2lT9bqpF4T1dPl_ff^5g&2bt%HJXxvvN_&Xd(4;Upt@FLapO> zUs%~mEtH?r7v7oXr1}Ea0J?Kbd||7%rC9L=_8-3Re&5rdq0agH7qq?Q;tPDI3N61y zv|zs%g)z0g;tRx-SA2nEP^|bufE8Z|u<8rgD;l%-0{4ny#TWX1R$rhNgjao`Ux(@o z0aksX>!H9@|0ho@j_(Cv-;tRo?6 zyZ?#j`NE3ryg|Ou$DEV5i9A13;_uW;zR>e-AdfZRI2V(5RWk;SdFeX5nlGR|<<)$l z$7;UNV~38nC;39hlY9Y~tb^nWJr?E*m>0?xvUS4-?ujUIKFV``-e|J`%y;1gl=6BbMy}Iyl4p;2#b+8xYm^?!$?udzNXl{8ztd@J_m)7m?gSY9LL;T?6&7j#~pSYjey5MKMY&M|hh*Zr+? z@5Rgf^}L@tkuFdJ?4EWW~T+^wOX*}p=se$&TF-xyjBa!Yqg+Q zs|CecEhyG%L9tc~inUr0tkZ&GtrleLofZ^pwV*LuEr9p=G}oOLG-j&><+WN+UaJN4 ztpCkoGz)xG&5;*=fPGHM=wQarW(vP6S*24`TeCFnD%= zxBL}i17Db}`JVnXv4Jm;HzPLmg`Lj(FtLFzkoPWPLtg;Tvbw+*fW4;h)E9ugY#r={ zIp!lb`|}UaD)8)-;q4850hl?XV~e~aBfP0SksZtLkrcD1h+_j^z}TmsFYRFrvPM&z zapw(v!TU13jo8o^?5C!Mn}}_q?`h0yGRE8)7pa z*eS#YXIQp(%sMd-9mnE)0WrJWS=J%W7ceh_dgpT1X3Q{OnAuJ@Ews6_wio6L z4RQ(g<-!KTe7@D&+e|FjXI<_r>mc9SB)E{;zp1o+`>sDXkU+ z>$IR)s|C}jopmlxF*_|NuhoK`;EeZD=e1f;UaJM=wOUZD)q-NJ78Gl>pjfK~#ab;0 z)@ea8^LKxa<>!Emz0-nXZSJfwTP=WhL7MAM3mUW4g7R7|D6iE5`tk~IuhoL`+T2;k za5(2BKg*QYYC+oTa_4VLy_miD0Q--7N%p6dsZM5gA~y7K<5>5tO?ch6wr_b%8?kBO zGmJU-zJT`leIb1xdC1Qm|KPl%oOd&M`HZCPT~92Zk(763GX}*jDLkAn6gy`f>||oO zu1$FpV)=}u*dZn6u6BIgw>IH*-`ZsC-M2Qy_OQMDtxaR@Dwupr+yn%|x4w>FJ= zEqOV21_quz?Qky9?+aL4eqX5H;n0`odV8mn7wp05am4Z&NyqRcV)=}u{mlD%@O@$C z?eY6UsoB}{=+ukZkw!`Vx4Jfd=lF*Ge5CdtHE8&~0B!C60K1PdnJ>(qo??0xL#=De z`GT~!jPvdOqJ{n5m%~y|S-((f|Iyy^yAzgrVzYlQJB?{(zmyQs>$*s*elb@r6BwyEmB++7a$;PeE> z%v#Vf96>B=LEC#o=?n4&@NB1SSZHhiQ`Nuq;%ik4tgZfH>Lcp4rN$G_F~Ow9Q?V0M z)V9WR4ZP<#_67Da*Zu~1XZGdO-j`#Yw=2&HFz?}H`4Q&u*@z#cH8Cc>!VA;alb6S!ysZs}zWBP!-*@iEVE@9`D41*c z`_4V@;PLj_8qc+ReK#CUEfl|C?f;)Aw7avNemYkBub9QQ{+IfNwZC=Vccz$K?Z5I? z$;)S?|Lwf=1nF!4m3Jd~`HZBztBGabQ|z+B!?Rzp^VY#mC6;|bdC=Ex^AkKb6?1LWI-dAk@h-_?|UrjwGlm+zD+ zubh#1=Tb3qhf&@I>tNLv`hHel=&|YxJyw08!^9VW$=JmgdaU|FAM>LeU%tbNn2$?y z-PQhUdv7K$-~UwJ>qE@<(zN=5_668kV4*KCX6Oqewf{2L)6f@2YX6mYk?;4CFRbA?jeed%Z0HNXLSNt* zVqX9+_65h<8qYP$Z>@e|f2W22V?Q6O{THmO@l;H6=S90i;=X_y&v{NFL8f^E4xqwe&B#zwhino%p|4nC>MnpOG}?9R>5; zIpxc??#VS5)@=TK&T{T7bIsg2^%LU*=Dg|a$jiC2@-A*L?aP_O@){}c1Y-G&q&(!# z={&mE=S+FXol`&I3&Lw_JdrQVf4|d$YZ>*^alQa7_61_0FO1awOM6|7r$0MTuO0gW z+lzg{)MGvr`vS1o7l6gS04(+eV6iU%i+#bd5nlk7pC8l$yc=VWeSui)3y2x}!mqi$ z-pl=$wIFLW;tSxtw%PB>3w>dv_Fs9=ZnPkKqW$~MgLw&kA+G(WFW5<&Z4EVPr$Rd; z+39NVOoi?o8}S8T@P(Q?gZFmdo1bUQp)Y_J`@#kD`WfM@P77=vQ5 zFMt>O0@fBv`S0%FF#a7vo%_&pNVXv7!5i+uroiG2Zm zIit~ntZnx@VPqXfd;#qp#F+V8oABD&|FwJ4sOQh67E0~^+Mn6aS$8dWFi=}9nB2kW zF^g@_hg}aZp?2J>{&W4p-w@lvx^j&$r@SvuPCb?Sg}%KT$Xg6}R}tG9V3)4L zJC_*8u%>;nmG|*&3t}F#PRv8cF}Z_*@kx7fAE_I=+`;IvyVzc?U(lGh7fgANPjfAI zFcf>1V{alauMyS{yemQsybBvl`?8r>UL)BPavuqOQQqO?<#kuyf#dD*4o0o<|4JHJ zQ2TFZn9^_G!sA>%P?jVI?!c(ulpn2f#F zcm~)WwwK?VYRp{?hM4xGGA+EDSg!ro_O2zC*GPGnhuD6Pc|n6|Urr~M*GSttj#x0) zUG2Z}>>=;#m-{L0wYC3i`%({s+JC!8YFe--ik*=j=kaQtk6_(>B*mbGTKoSckC}Rt zwUBCnj_`Ad@}Px!AL$RB_i^Vz3$^xNdDpX_`5dl1XrYclF=(OI_bUc1#2Bb;@uk^+D?i5-`{oLo6@Y>q{HTQ*;okoFva_$Um zy)(^8SNkuR_`+6iOR?e$tk;Gwyx;fqXV_lWg7S(l@SQ5O{1(wdt^HTt^^BRdpuFM> z{AR0I@r3{@z7Sy57qGUp^S;hn5MEpRk9;Ah z{r5B1NbSF1k}u%yxMDS50G6D0GE)1myt(fc`+cyiZT*z<&UG!^LM-1~R^AQ7^4(qK zT}3S4jaTf_b$He)a~_OAc{N|?v6?URSj`tYO!9>uyPtgt#xD5+crq_t{eq04tNjP> zC9Z|{u)SPkqA_c}pzVQo1$lY>lvneGetetB%WI^(35gkL#7ql<0@G;bUp=&SK zFXY<)HG6i4wf|qRpO4l43)X2tu|^B1{a^d*)M8h^puAQKf8u?4hVxo2D6iFmVyzZ5 zW~&9oS}iEnYC*A93yK}ebPTYO% zzs78}puAQK%4@ZNzNEJ*{qMn0UaJKiL#qYhby|@2IxV;_1hxOr`JJhcUG2YMUG2YO zmY4UwFjD)kycu~zU)bqd_;A?|yXrG%r1l@r4)81+4159DYl!7~9p$}j9qffUCTstd zcQmn~FQB~*C8qjGyv zugMs5XJFth7EF9$r1l@<`>E3-blW zymTF2m@kkQ<_pA*StsV9<5-+8Af|k4>&71E3#@++^99DdoqgHN_Y_zM+v#KZ0%L~x z!bt7EtgZZ>ZHxhH`&MsnGy9TrXXUl|g6xSnUtm8EWXzn$OM5wY&b9w|j}+AYdk#NR z`!86h1;tt|{EdxxtoC1dZSL&v3`T1ImDg%Pd94-{Yqg+Qs|CecEhyG%L9tc~f^}L@ z?0&B64(?kSd#44(S}kbIRtwz#Gy)Jk5?+Zcge>yRa)&2|CeQQ(9-cb$CNPp!qP5pN6w=7o=$EVn0;UPa(?EH1G(}-m)C~sro?d-;Y^R3l8b6$kk)&48?AY*1N$k@Bu zf5rBam+Kca<{bsYcW3bIy`pL1eZ;aBH0E{0vKEwQw^e;@c^8$xgCYHk1>RFlYl~b0 zeR-;5XOfq-pzWPNENel>a0Ice1?4@W^aZ(@w8wj@_m{up{QIdFtENlm>?=S2UHkcD z)!+NF#xulNGyvKiXj6ZA^2`8qa;@Z;@!sH%=L7MSoQ_wy|!TcC8qDC=|v5Oz5qLeSY9J- z&(z|3BKv}l!FGmY*%y>|P-$-?b{JwaBRdEfPIZIm#B02 z!Xd7OFA&RrqY7FEZx6BIv*S_Di+usS>I?eJ0k8T(k6q68@^b~x4)Cfk^t|c|Jyw08 z$Eq*%SYC%<>?6M5V;}JaVAU78_V_ztT3>%`n(Gl?K+Nh3h>7(BFZKoSwuWm9EcOLp zCz6-%smdC$_CNWo_65Ay7aS8`SaO}f7ru>JXnbM*uxkO|hC(d%1@K0E!IV1Y3*?2q zF#m@0ZuL3YUH&eQKLf}MeSy5t7l>WPn2UVg`ZI^vdFx=O5*zpedB+x>_yV!e7l6gS zKrHkHj6L=RVxccEX6Os^|8gyazQCAE9N)kf$P0aeywDejg}y*6^aYL~^ab)lUjQbx z|M1dJrcuxTMYI5o{Eq#6GEdoVU*joQSL3M|a_3s(Iro_NrS0X7$5u@yx14Oi+uroiG2aQ*cZTyeF41K z7n~=)04=~5ZV)Zt=g`)hTnihDPC9JF7o4{o`@%2#p8h;z4t)W<*cX1zd9g2m7yANu zu`d8yEc+JY!?Oce>S0En6WR| zZXWdo#Eg9byx14Oi+uroiG2aQ*cZTyeF41K7o0cZ3qRgz0Xl!FYoX=~*eimK_=59B ze8Fa8)EB^uec>$U#l8Su>MASnLbHVqb7<#20{7U+C6Be4)qg zZnOZ8Ld@6~Uf^1IFL{IaNOPa-Y3vK&#l8Su>>i&>)cz-4Ivq{swo6P4pCz`% z>gL;+mRHz$I@R(3v;A*8dzu>W7GgUB-VMYS1Kw4{wg%Xx>+sGcwiNJAA+{~Rj#($> zq2rj`!RW>=_mR3elRFqab{E@AzL4fx=2L2Ul=s*)*HXWrnC}VsJD!SxcLm$a>nD3j z?jwPxec4Q2UL)llPb`>gxsRlMK|c>{`XcS|4#raXre@C!(}FRalYWkT?zhtZTluGi z)*8%RY9gMy&wZ^j_d#&*dusv*V za}d+cgFc@b!@B=vnro^3@7AdPeP_m0-sOzBDd@`u>`Sn=)6>Zd_F(<{3&9wkL|*<@ zDtp(q+~{8FYX5DXtd_@~(&Xfv`VQZJc3LS~sC7Pqb@!1JgBEHn&&NIHAEcPweI(@- zE%000hn$yA$5{)t_Fs8L3%uK)yrKo3wG}H`2(Y4s0IOQq;_pi|X3+xgG%HrL(D$=y zf%VS9>uUdH?A?7N#i|y%ezM-V*8YQ+PElD4^>2}A%xm)=EyK=VkZXqw-TW)zZUjXK3L-MX_#-P}x z#8Uln+upgv@)(p?^M#&Q^MxL(`9glezN-YXby`rY)xvU$nX+bUaTEKWwV=G3JEJ|@|JIkMC7$HYeP3EF zD6iFmVyzYwYqg+Qs|CecEhyG%L9k8>inUshIqS5b*j=3KoI7jG+Y6@i`CVzQJ1uC; zH<6e3v|_Cm6l=ABzSvXW+H19-yyMwk`aKv)Lpp|53(9M?AnkSa3pP)-GtFhZt$J4b zsQrAh^2mgCf9qVZuHIR(FEi#6&q#NA%*UsgUA?pNOf$%xnJ;`e@n-+(JUj0>mcKhI z@14Z*w>IVNAePU7ioLqb@AIj=^N8izfA5PO zic@>`ME1VqHB#QO#PS(Q$AEX#HD3^3SMMzCb@k3`sTZ?<=(O+=`}t({w~jux>7!ts z78Gl>@OvK9+#=9|@>(tYrt|*Td94$D*4 zi58Fxx-ZN=(P;rXPrJ9T{THmO{a5TO++zb@nAr))wD4(S`Mb07X2kNHZ9D^h-Ff!E zdtaD&KbPM{Z0HN%y^&ac=4i~<6rTD5crRNAdtr{r+JB9CG_j#CAm)Y=Q+>g)uJ&JI zcD4U9XI<^TV)nfAIUD){+PjU|&==B4qObkenAeau^ab!13nspRHJYB^U>IMDyf8hD zSY9J-ZzHjxFJKIZ5nE*LEWEDvU)meg{-<---qefP`>BPRJAcf6KAHWpqmTEtLM+Y~ z!0U2n&lhHPsy8jzlgHL)m)*^ib1HdZzA*b)=hlng(f$bgG^aVU`FXzr)`@b*0N1DC9)5352 z+HFgH>~d$pIxQ$>ry<|B`5lZs?`=Prg9chqUaJN3apV1v^K2{wEhw+mg7R7|DAsC0 zu~rL;wOUZD)q-NJ76j|GpjfK~8GEM%#qQ#q<=k0gwz)HSc9IFSpfTIrS$VA%l-Ftj zefbe@uhoL`+T2;k&}u<>trn!cE_e3t3$qtd3pIECE#H6Nm-@)>3nA8hYZD&7FVt^s zQ;%s*Xj=G;uTT9w+sd<4%JzHu-N+Al?sc>CzSDX3za7iDv+}Mdmd{AayOLP;U&Ssd zJe-jfJ7*p2WMa8~L3tBm`HZC4AtmOnc6{;og_)1N`_?97@4mGuwukL)W{xj?>Atmr zXD1TV!n?`K_mMQ_wZ!rnNqLuNjJ5yDyP(0)my^6bd|wFnS@*3?$6#-Md{5*vlD20X zn|o=W%(b`2?+f>rTF&23y_h|NTCln{e&_gI`}t&cgrgfgUWoCYYO0Ic=4}a&_f)Ok zneUNSJ?3|$n57pr#(+FY zvBkpcF`RF$W-8#Bs%@TAz4I0yyT;sDV(#oPoNrT1XhC?qr<%t8rIyF~g*0|(R>sbI zs#fnDV0+n@yhhsI9R<^Mu-#=^cptH>1#Ry-Vp$8ozb0HJ!eY6P^sco3R!>YQYdk}Y zHJ*#61+1&^So^N8d zpZPA2#AJH=nHiuWO_+Ezk7!@_i)Cr=3Vl3$|X4Z3$x9iJSez9#h_r z7Od~f)&@ggPW1N9B9`wXAto^UZ*MQx{_7Z?LM*SJ@(vzvk2Rj>l;{7ir(UccO)YH7 zKVN;O{d}_aD+y&SPr>}B6aN|XE3NyQi5>ln^DpL1+e@df3Xf7rOU z6km9Y^K8VX1v~EzMz<$gS{`=7c{25fAsm>{Kx~`hroMU znrr^OwzNiD1WP+(VY);fePJ~*Cev3YEbR}EnSbw#_T?h>C1dCdVxB=>UL)l_o4o7` zI)*2cmwiEb2bK2r`52@<-q|j`kQ~9ZVA+Rr(sNUFZl|MxFLaprLXUloF_#$AePPvh zg6(N@2b-_t3q7y;0x&!S9^<@SzUL$MQOC>SKy8fQRRwuaWpdk9~su%+G%5Cu{$cXDIJ+X|BZ=6hnKlFM#)YwwK4C z{j9!#nCJ^)p4F_8@=hF|Yu5fJpY8iueW7b_#1|e(Ews;m%fd%}!Lbov0CunUzx4&{ zooV57j8}B2&0AA<|?6Ja zEc6A&41Hn#->wDoaeuxKeE~6-h~<32pT*?8wqX0kPr!?P0oWPj4SfMv=nEV}>iI*|LizdJ_Oa!w7RfyCo!-ZSrT=fWHTx3A-1($(MpEoP@^bEM zxtN{bKc9H>eXlNqNYf(|qprxmMmIN_+4H;W2kUr)a_Rg}Kd^a~fZm?@n_P=L^7MUmzCx!rZOWu@PSY7Wx9l9{U2Z*cT8p z_Jvn=`i@&q(eI^SAq+Hh1utLtg-Ijo8o^e!+RMFM!wj0-gcj znL7-|pjhk+;KjZGEcOLpu`d9NeZjF2UjP>Sf{%T~7l7^M*oVG=n6WRs-nC%s#Wh0i zjOQ(4+J8HiwEzsf*cZUNxbWZ^vJQNwTD@D_JAu5c1?9!Qfc9cv@Uf5h!mFu;))!vp zT6lu@Bg960!FjCxuUhc;Nb?`@Jssu?bMRtcc#-q$31Vx|yhkE0_66`_UjP>S0@3n#l4!h8X9Jze5DWG(!PF(z~HVqZXC zVqZXC&KTckBfbFMlgZ0xB=iOC#lGM?t^Hp(!tP-FS{n8IxzxfgdyjNZI&;mBOk-Pn zM?&QehGYIy{EG#-gVAI6cpp*wZ!y>2QbI_L? zh%FLR-c`gnzBR=zU59rrv85p9Da5t~*fHzGJainBI~d*A2etocKGW)O9pny1H=l$1 zNQoz7m-|S{dt91pxsRk++8GOSA4xIruHg7K^=mY!{croSnY_G4I=;9{cq>x zz@{%UcHY6TT28)y`d}K_+?>QY_2<8w=6U7o3FRG(5R)3u-JZ7yk9RPZ%D1+gJmxo2 zOsW0vc`NKoekZr&ynCH@GqD})zqWThvHWgLdANg7$Dr6H>+sH52eYT3dDN19Yr_~c z=7djjxUcv)=2)ob2n#F``@=`PtdWsp5h(#QhBGU?H$ncMaIrM81@~3-)laQM!jOE zcjxRVKmQ-T|Mn!5S_rZ3K9cad`$+cOHhuk3irL*qQXaHW?;~C7yx(&kv`}mRl?N@< zzeS?F_CAtg&_d!Zxtg}$HF7x>*-c-0sBHLAW4V9-MHg|46C3%U+F(p-x#fG2ZZeSzZx z2Hxe|OPhE%PkGQn^1ThdUvzxXLb%VU1)J+&&WbNk3)?Jkp-l{?~kA}fx%t-L(ViR23%Ciy~-)qDY%=L;*Z@jd-nwwLb4mopyP8`S=nGv$iyO4GtE zV@NdvE$^yke12|Z%$hIsymQG*_s-jxHDBnlW7g?Q%@;bJe(zMz=zCCL|n$vR9ov%O#s4(b=0b&!0a zZ?EPHU3)6%HdvM*FEyxo32S$SSUJ1q#-X+g183&1v|n36lAkII{KPBwA> zrMF{~m1j7w)q?U`Ehw+mf?}-}6l=AhSgQraS}iEnYC*703yQT`kg<1KQ0y+wXMU%u zF>f!JuEWu3t|fO?%=bj61#Rz&X8jbqu))xmXM20C7L-?WXY@tKU~g3h&p_oJINn}Y zzwp0OFIMcWlyhoM@HYGTWaZHbojrCO>*}3_xBO-Dmgs9Yddv+eW>@>Kyk+w8_k}kl z-t6C;_aS2WyR-7_eYDLTgS{Zf@cM-j{Uf?`i7 zF_9w**3~;Jb}xB5cn*@WclFMSt&*2(JT>O6#PWA%=gs~z&2?ArtTC@9FXs!&yR2aQ z-Ak2s9c#AzIxYN? z{d_X}dq-O>2-ayqu~rLqp0}9yrI?)-l-FwE8s~k=d94_}8~DO(&G+=DiRJIk z%9{}z`od1FoG%b-^M!X?Ta%g1@L0Y;UYIZ7@6>m>v!4yg3-bl?!hC_)r5tpjfK~#ab;0)@ebp`#EPjs682brv=4YEojVE3*cRl=DN$BHD;rQ zSz=74rUH8tWB+cT6XmtJGy3uhZ?DyY@{VWB{M^wow7IkLS}jO>UGDtB)Qj1RsfC(5 z|8g1|{&srztxd4*Tbp8E_5Rn||KtdhnT^=?^k;|-zAvD?@~$M7{a3L|3J*DyV&|-bolI=-ePM=vPKf0*lEyrw#N6e6B3SpWO|h?$ zw?n@n@bBAyp|poF$h>sFTUcZ6A}^njz_XoaTEO$F{+?}(c`bSQjHJBFGp64c*iX}p zuhHQ9!tA-;-s!~h8fkmS5zA*J9Yg!AO?e08_VoJ#+T-_yy61PMUd)a(O6p_z9?iei z{w_zcb_H8YKb`o$$n^`UE^3>vt77*tCi8{a(-SYLz@8oNCzkUC<>7p5_0Ig(_KV)0 zoeE7ub|UqDW-TbswB}f@Ur-F^+d2k~xmbAEM~a=l4us&~e>DCKP|yq!KTirGj8 z{S=J%RMXhM)bd#SZ}ra9f{c&%RIT1Q!1l7eyhhsI9R)i=*3aH6nilMMBT9S7&7?ivQ?)&T zdn>EYOTAdTnp!Z8u+BTV-DK5zU`ol)u>fO@XZpU6XPof5+W(*Nm?x!}tnp006Q<|w z@&0cswf{fvJUew5FF(h21Ux*)w&mLYHRa(swylg$F+9g&9-d<{hUeHe(*nkzG4ULW zd3cV+7@kSt*jeLg_fPteUgbPfzwvG* zmVH5aHxkRfpgdE%tz8|rVwV*j=0vgc*1=9CmVH5a#}*!ZL9r(m4Esnh-r25oZHhfW zUiJkUJMV1Qx;Dk`CNKMf#{5XZ4p>;U{2#pUNOR3vo?6$YF_*|ozR>bsTQK;7@-AvH z^u_nt^bBIz7nEmT<9z+HFX$M|jT~E~FDUPz(%wGb@6sOcY!_chj$m4_CzW%OFThK# z@cn0R!Uw+4Vd4uthVyOxoiOl)LsLxgg`QV^0T`ZRk9OWJ_H#$y&*kb1J+Jygk5ymj zvFZywcDnZ^`9hD`6VlcvJ^OpC`a+Lk9g;6}nD_!PStId<9{U9QlI#0rKE)T5XK&F5 zzMz=z3GoHRz^lHX7I=&AJt4l(^Qtc>&)XAUz}XnSus|)e&wl&y zs4qA+;tRm;^);xqJn#iO!HxO?c%d)Mzv;Z#7r+aBfxPMq7`Hzkh*e+cvFZyw7Wx8X z9?P*W^6r|p7y1Htu`dvdeZj{*;tRm;W?u%rz?kX_>AkQ&Q|I4uExeb!oG;*ciipk%bSI z)A)dCUywW3=dtqa^zUnw&q&Hc?p$mCm1j?0=WSYmF9?shb8XLkVg82)*nfO%d9Og{ zznA7D&KKs6#lAo+^o6;dfW~}*yx13RcV6fV0BqzAKV7ihTj|GL3x!F=Jo&jWpMBzQCAE>`T@H`T}0)3*?2qFh^fP zUm!2`1@J;&ATRUkWW*O78}S8Tu`j&C_w?u4Ue2ARz2(>! zT(x|snx2h#rYbM?1@K~D0A}mx&yHNnv(3j2EcOLpu`dAISYoO#0E>OWu@PSY7W;zF z^@uM3+snQTeE~6JU)Y}3sP%>91$j>-kML(_)&el_VqZXCVqZXCVqX9+_66`_UjQ%m z1?P?U!lj)SpmTdFj{1UQBfbDE_Jto!G2{10bMRtc*y6m{7r={s0le53fW^K5%p&`m zXPvyt_XDul7l0jGc+i6KVqb9Hh%W$pfH8-@;A0>01z>lRm$e{$nZ~|gDj)R)#9U&` ztc69N7w}?V05A3hj4$>D@M2#8FZKoSVqb8c*8YFne#@2Km_|K+KDDsR-XopkXFS^< z_6+#0gvuR^4wE|=J$8@J7i#~vT3hL;W6We51;{BNOp5nY) zi0ue?HxOG4c$OD=due=aOnVYI7V*v{wiNJAA+{~Rj#($>q2rj`!RY!a_mR3elRFqa zb{E@AT4=^DwLHq(kmg$MU?}E$Lh2V31Mdp9m)0-klMT_ln6$CM)+l?`C58ovOBXJ+b^AQ+ei!w%_X*6tftPMZ9y? z!A>TY->GWM39(%Juh=2uF?k0g96M_~x7)WiteuRVHJ;n;TU(D=hUVkT?^LBPtnu7# z-%6GDcWJJ92V=WEuN3n=!5YunPxJ*mJ1=>A`Mt3A6Yr=iPy2E@d3kRt?>J(?T(kCn zdwGAYJloiVzDRq#gJD`={`GTd)GL2ZEz~;-ulD`7H=)!*h*1macLZ;BZ3&NB*jC;n z**Vho^ruoxY9aBC=y=dVt^I$w^QO*&7ScTgXrb2rYs_6f_H>6v*WsgS zuBnAZ)54Z^ji`lmS7fJP;6V$9@jjCF1zM=J|JoO5A>3zO?Z5Kyy~cjGFu%2Fd(cAK zOZ(cj?X=*&u<{{l!JeDk573tFuA+qy6JLN9gjaom`xd_N0pHV~pt)1-?@i zUiF23jjAsMSoMXjpW+M90_M|Bltl~47r>LbuD-zWDX;p%2H$_mtG)nC$5(tI7(?-e zV9uZg^95=_+k+O&ho}W(^mFjH3tOJ8uI+b>p~WsGmg*N;>|A20 z_P@nWA(rkDx7ac3#H{&3$CG@a$Ly5oV+h7B`9e3JgWCV5pOP;q?+4OcOTM6(?+M8l z6a%m33yOhvA;%D`?Q}D-tu~(}U%(z5)GxGSI9%qs<<)$lo6lUo;CnvB+>u7TV!e=l zq@UaVuzmYI36NQ97XH-7o&5I*e+DRTnOMG?_lm@u{k!w* zG-vZw&q&I9C$aoJS9zv+=jAh!Vs^fCET54SJ9{1M1vw_q5Xw7>Sgv1C>jSOFl1(-AmpMo*`uHtarBDIlxxQ%Qc=F^HyRxU+|c-Z>70ry>rTQaYoYit|l+% z3(7OE_W8`;mX&uNv0VG_eOWo!+dGw5UL)llODunP)-m86bv+{qkM+*=j3n)`-nr(^ zKbv|n`+BE^pRk`#W?y#H6fxLGf^}L@%-F`Ox$|p1raj#PEhrCKu={Du7kLK^#i*E;(0SKeN$1?52t zb`P>&+i9x>;ZX}|K9M_1d(=YX3$s7ywBXvB*{(FDd^`8wK(D}9```Eiv9CC<*0s4W z%*?TEPk)**2fjewj9AtJo&g_n-iL{0El59E`=6chPW9@(I3&8FpZ{Q1pc1B6DZq zvGzZ;r*rM?4Ql_>*~*@mw%b2QE!5o7&uTM!(i*C{b2*Ozi}M9y_jn)Ew>F&d-eA!t zv-hN!alSxam@mw{r-R(N?Ms+1kardPlJc^|d!_dUF>S22r+3-==^hL71;z~X1!8vM z9*t?I#4#++7Z6jvwV|J~MsdD?`RsCM8T%l2uJdWxz*xS(m|?y!124=M$P4p@*)Mr} zo0~P#@rC&U=OxS+*j|_~puL(tYnDgujZM<7JUpttem>-+Iyr1!Vq#1av7Bpt71;tt|DAsC0u~rL;wOSCY(}H4l zx^xW%V;^Xtna@rO8dG!U%DX7db^IO)a~;1&nt`WUs4;zwk{Cxp)9e-aSwukL)W?rN1?JAhA!wG4w<-4KV zwV*tlZ&P1(`n)KH^KIBq-czl)v+!8^pT-W&%Gh~NHGSvqvAyhPUL%cphhu5|bRFzT zYg%|8vHYz~+q;ff)&el_URN;W1j@VEu{1tl=!@?&{*9_|pG{3=wx?_EtYbKW?PV=! zdygpXAzzU8cu#eAzIU|hid@@DEu^1gA8kqdZ`I%Xvi3j3SmU|d=Cb3l_J4P|kCaX; zlhx<=p8kyYakssT?Rj{P?Y3u!W2wg3c*iE*8lD}yOZ~g_lQo{ZOAWaF+ss?WbF7X* zF+9g&-UaMut}9nw?n|ymSM0cTczBM*?cq6A#-7HT$G{rTI(Egr$o6vWzs$?@6U6cw zDet2N!(IW;P9&y<_Y=#t|Gy$KkhTB2%kNpx zF_@A}C*gcf4{rJbp0~#u&#JwP(#TeirWUprzj(2)!7~%eTAmQ&o$YkD3%(#c*8XoV zzHqk3{LU1Uced;I1?8=J|IJ5vMmp7b{_J4wfBn9oyc@~OzMwqwKljS)3yN9J;aK(s z#m-v?vw!Ej>KzP?xkO&}1?9cAV0iW`&-R&bNiLhG&3d;tL&b#20YZ z`GWT``9jazLo7dYPIoOl#x=By*p7hrZesbqsq(5XpgqN^FZ5XTg&wmQwyx<})nnBc zdaU|Fk5ymjF!2RovJR~M-(BvhDpq}=j~V;I4;BUN-_yTyLFOV1d0k=B4+3dqZlDF_Z7e*TkrYGjT>7~t7<1=(MpEoS#>{6V89Q_5dPY)gFL_xDiXnGS>xbOg zv6MC2uJ|;0SqsX$j#xe;0W*gA33BH&hW&mnQJ(ELpRoOF&B=%_q<@irpK9w1!0z*yjV~leuxHNO+1}6>z>9rhhqq_5=FgR^h1A1|_jX~q z@dfYw#NA>PcixT7hXziOW&US{RPBKGd!8wh)oN(khde?-9T)S znDdseO1#9jXLuc-#=JD*vAuJNEon?*7QiFxQaCU-EpvCDm=Zm#7HMvvXa z_L488wUx2UeI(^QKFyij!BEWigxtYU47@AY-ll$yIPvOl4`gAro9gOUDDGjbE*@eamP{?@kg zjTDo=bN+|fUc_4=mfyR*(ED;<>dW%Y#PT~;ZSQ(w`Q4iGa0jD~L9t8L;n~y0_WM$K zU#Gm2iRE{yicN^+T71P08IQ?37)$n@5%VcL-odcHZQai`?_eyIcd9Zk|36<}0&eG3 zR-L4fLZLLBpdio|+S3WJ%z;j65eOoX4!~7GLZRg{X+(uf2e=?=l_?a2swe?ETn05F zIrp41*8muVcjJ#k`KXPTfypJ2twNTGsaCxqUdZ(UwmswxdHZbN|sQ3FB zb1meUYoXr#XP#@J-u-9HwUE}cyZcW(XrZ3LpteH`^$Z4MH=^xv9*JYR7U~%c=gH2g zXrcZV3CDaLVn*)FJl8_K-_JbPLdCc)u7x^A%yTVBU*miY^Zb2{_UFEKE{$Ca_3l6M zpaoeUac;$YVftQZp?>b#daL$H^#yPUBVTYWF!l{-8P8PriWi8!-eYt6i@-t)%qzZt z_s*{6Uo~Fw1@sB?iZ1|To@=4DkFnwlXdh$67ZR-c!d4pt<`rMSc_hY)FZA`SzJT}6 z#H+s0k3;o^1amE@FLd?9-G9Y=P4fPHt@btfg7c`aara-=ceIar)fYBdzc8=*f@9pi z;tNR|iZ3L6=2{S6fEKvC;tROTL*?<^|C%pMe*s$X`wJysnEtHx3FQlOHJ5OV@`WC& z`GRATBZwAc=7}$S$+V!~71r}sfNch5d%USR808B+Zzp(q525Uf!MhyTT!LM?22V=0 zoIvkjv@xY#$QOF7<_kSm^Mwwhe4)qgKzR$O%ag4?ZHy>ia2{i6zTo{r`2zFw2@dBA zjCpylM_o31U2HaZmbr0o-?^DflrMNp8r$&}#GFgoC%NXVzNYUA>+89GSWnNDyzOy+ zLGlIXiB4sYbatnOpRj&@w4$9B2F{-+r3JJ)=Hc-`Iqr(4WT8ngSY zbLK5uU)Fr#3Cf#%$9NwA7Vj6BCp$VBySfi&>{Y;GZ^$uURCxYOm9b~8fyuwK^5VWT z^G*O3_ZJvDti<%3l(6o%&KbK6ym;?S?dyK)oUv8#;*KZBydGHWk&HL_p7wS3Tjw0} z3h-i&#Jraj?11f&m?tw0W8|3CWxB!2^JmoQJR6@3UgQhhh9gT%&ljk??zhgVyzaNo zw`naV|JG?iG%EA&YlhwnWyL%qtkVKxw}2P1;$z}P}@5#FxF~;W42mw-j}tnJ1ua` zRtwB)wZObq3tpF-t-MwX%xks4ZD_SXyiN;LUZ(}An`q(F>*SA*EAkf8mj6~e?*2D= zbqsg^8(#qSb;JxUm@iEB+MNCZuz2rGX*8&SY zGxp-bV_)#{p0x%hc^I^?#nwNLc_Of(FL+FGMy!qO3x?tDzk1@$ZOpN(BE}o|g7-D< z{@3S7)GuS{apMhr!OOb=*w7c`Q_;fJz~b&d$GkEyKf z)V)9ZIXFHA*w7ce4Mzc+NA67Z#NB`GPws0gZ*cct_J@<-(OT@iXr26#deq$6_G~QC{TE%)7j4A7huU z!AtW6)a6X@w)5T>*hy=|JYqJM=L;T_-nG%b*xTOa&eWfIzJQo%zA%xQE?RgO>N&_4 zP+poZ088@)V3IZ1JR9T-lQ&s;TY$}R?gCz#FQ5%+z5rgDFL-&8JL}#5`W$KU3!N71 zImF~*tz)MJ!a6N5)@Wh!Qi;~TclL2*UYk41oD&VHlKR}4d94o&eSiR7C2_B1?S1ym}r4x zwpw6bs|Dt@TJXC3td-YlfqAVKxDBlqnAd87%4>6HfA2hb3bau7K~J*zrw$hH7gDTy z*G9bVU7NjMm`ICdPJhwHuD)wy-V|8ukz_9-S~$UY*8+<@67${;EcQsuy9`+Dkr=zA z@O+QNnB-G3$LhN_#>86;i~9?Vjex}-iLt{=%q80g5!StHW9*yY#U6>;-o0yM>_+eg z?-#s3@qVGcYjd8=Jki2C!HYB79P@R+B6nti`Tkb#wfVa!#!e|QcUiwMCM}Wi(U`6U z!f;M?cgYtRyA!<7f@3yDIHxN2+6MJ>Y#(?rMjZ1a1@m`p-kFhTW^E6Q+aUXV!;(3OyZ_SHNuM19-aPUJF7KdHp63fx9?q#=6X%g| z_y2Lw!Y=vT$46J;;obif!yV6EqJ@r!JDyj?d8EA$x0sK#IsG?Q$6ay;qv!cLwkz)b z@BK03oou`%@D>uDuVcH)-GAo!I#%1jn6G0w&)2aWlYW)4+g0wPb4*{ya-OeaIVJ;P zG4Z_z+;-gY+*Q8!fW`=SJa@@?q^>`4_rH!2)f2z(teDRgkLkYf9@Hi7{&RV6EcNVp zFDO`Fm$?S>x;#S17I!@B7;$;>FJ|>M?s(SzWZvComxnu^Tgv+X7_CLQ`@gMx{t%mg z$0~|5+bM=K+uMpS5D$0%x5mA;z1qX0Rs9!nW_w%l1?H_<{kO@QbC<2D_cxyY9cQ+; z6<=W9JHU(emU&kIi#3O_mlU3_{fu3(26h&(@CD|Ha%Jqc$=dJhCSyky%;ySWIJ2#7 z_vc8AeG9zs1!_CaY;P;Rz}P3j3t!-vA1>HB>ucvdQ2QEZwsnlQ5@vH^ya*nAVO1HU zy~^G=etE$TbiC&@nAgQ)o(n9-h}$4<7Nw<$)x(D7#Yg3JV&)72MxUiAgXeC;^Ic-0qrUiF2ZSAC(! zsxS0d^@Scg2YnW2C%K;07kXaxg&vD>h;x+`lYGH>zW&)5kuUVvr%=yW`#I)E8_Z)K zs(p>S|GPvBeatt47xiS`D^twtS$%=q?wIWJZS7CyAifhuJ(zhmC&(B2dRAXxo|QMl z7Y>FN+O=P@@R`0~*bHBA>^5s#>kCr5nZDq>RpT|j;CIz=_rLW8=cT>?Ug`_LQeOa; z`U0@j7l5U{04((d$8ui)mivOWeTFYMmihu>roJ$^```M4$6PF9jJ(9FXo~eG>VtyOEc-O|fy})9Rq#5977z{`E%dgIC8wl@qd_bf&*hA%jFyYU)d&|Cka1#t(9IrIhR?FBaUh1VD__XX#3y!%jY!6@9OXD!Z7pz}q_=3mGeL;5oy)Ud>E}xF{ zX{xNAQnvMHXu<2^yxbSOF3&AI_Y4{be4lFdZf;-h3*Ls@7reZKN_p%H#%p~+9YJG!iw|qe8G9SFF2O_f@8Ta zIF|c@W4SLlmivNXGkn3Z+!w6vGkn3Z+!s7%?h9vVU(fIbkD2>|^KxHsUhWHCmvb?; zu^zkEI8XN1Htw-Ua$j)X{eZ$yVdIGcmIWFHy1qe#+6fnZI%1sZA{rkNxPTi{eoknd#THPwJvlXspstk zZ$9B&4s0&LE?t9nKCs1vcP6my33k#NF^`yy(HV@c?Q|ZgYdf7s>akCtJk1xhuc<%j zE)Vk#)xM_lNQ~K>7~K7@~X1r^GEhKe$JFvL>&%Dck#q&DGE?I-OZ4FFTLF==4&c`uF zz~b&dV~5X2Up>O#Ek?*ET#tEXRv7RuR- zy$`baC%ss-kYe3=B;s}Fkz{_zoc>FV*_}sXo@=4r<&pih@cz_zu7!H{pLwo@dY6ZJ zu7%nL##{^a%n@U*g&cD&)Vu%8b1l@n|BSg7(t391k%-sb{in8f=aCq@(dL+*Ir4XA z9MiQ>&m%eSL)zEfc_fbM?`w37cG+`N=D8LG!|#nS?;_MCFs_Shp^g#rTnqK?Kl5A* z+MoNVE;m{mTnk#y{e}^*yZbMWAX}|I4o#ilP`GtsIRLppnZ-xulmBKc5KNP z9OJs2GkXq>i!UU7R(t_k;CjZq4lPi5eD}ZR3)7#67X1D~+5b&{#2l+zJ+liQZf1%}F-n4;ZUb+VF zd|-M9qm5bfg&wQ&bt3$VF_SMvogk9jh`Ex(L$w>-n=AbnR@$BX+5k}o*+C~b9f z_h0+@IQe|!_opd$b`D{k78tt~b;LXx-2FcVSZINHE5M>J zR~>y~Hy1{GmAz5f)?d+Xj@fE~u~rL=wOU}T)dFL!76|LKz*wsV>a$J@jI~=`^E$@liL%UxniDYg%r&qlL`>|%nRf!PxWB;IVI`*Lq=a?% zof*3gyanuosO{Z-XU0~+i#whi^Lk+M-r0CF?*4PkE5M6e8>4up%5Csx)HPoqUU%P_%IoesKSF0%a`*p%Hvhh6=)F*nm`8+lT41cz zg042B$?Y1m(*pBaEl691_c`OWT3}wQ1?II{V64>wW33h#Yqh{ws|ChdEfCgefw5K# z)b>sbjI~Ao@d?nU~n7czp<8i z?o8!%TA=beEtoGPcmG}Id$f+--G9QmyZ?-R9d#V|!i>BB%$t;czRT7XUjufT7Cs1U z;0yS!fwz@DS?YMN1r~bdn3BOHzTo9OYYps)5%b_JU3sE=i#hZKk15WG^4J#)>+b$j zJ-fUA)Mwq@f5!HryrD06c{czX`ht8aTDTfm-2La6R|baM*)iwM7mR#i#@&B!-yd0d zXM-1G#O3*(x;{tZevxNG11%7*yZcY&4etJHUhpWJ?Z2~5{z$x5kNp00o-a6- z=L^7^d||J&M`B73i!ZdfGnLon&bG(%d)|4zfbw>t4Ut1Jug#qqOY;Skm*xw=&O~{7 z-&y;TW1h4|U5=QI<@th_NAKDwSMs*!`2w&{puBk3M)mA+XCDWd>9g_$#7y%Ad}lzL zJJZE+Yqh|@>(r0uhjy#q16KO*3T}l%bg!-J^7S%^2dB8{OMTvd^C|Aae2Q` z^J>DncWsRMd*_AW`-O?LSbX7&C@;QSn0ZrRkvmIUr7kBJ?^a!`QYpurq+gnQi8cfW;n(vBOJD-^&r!y=!Cao8T?*I|Lu2?p+)83%+x{ zzH8%{ONbeJB z9krN|J98Uk-(%Q3zh6MV#JnE7Ul`o|pNliwdmpQjkha4$Gv%*fAwA)#y~&C z_X6OYs@|8~Zu5w-+byQtcgFjXha2xfS|YTdcc%Ou5A%F~EBBr8uIOEg0s%QKjD;!0q#U#P~h|-{0<%_m-{&F7Lt;v&YT`7FuAQ><3YoUDhv* zMO~tvgyEd3w*BUoH@*`wLkrCNJ7975pRs+wVvHF3NWpv@9MfG?++WZ!@^@|2*SPzy z_4M{R<~%>As+i{s%zG}%3(U)VfR%S1u+ReYWbRmhh8DOD;t}ZUeeE0^A5_Zo+>FY@ zIaN6qi*qF6*K)`6(a?gd{kS_WAIRNgoxO3#GsSSnQ`a0{Cb{loTMh#Bix#*1|sSi+O`$hf9$#~n{Of8sn@GRhd?j;EaYNU%>KCdP={j=TTu_(|bsUNG~80q;2t=54omhPyoJJi}d{&2g7!&<1fMSwA+%d89#k8%lZmtqoKj&TO~7 za5S{AQ2atZ5MMY<`viNW6eC}74`<(%ZK7<2>!4ebRsMg^owQU>Md-8zb_C9;?34$NVVDi!-b~e)l#l zRA1<0R$pMudDRyf^Lkca;JP?geWB-7Utpfi3G#)WSABtbRv!6+t^a%77dA>4xVDDA zpgl9o7Yv)>3y%4_q1G2<2RG9foR|8-%KsYgdejs70@eWMrM>`O^#%7mTOWW`U+A&w z3q6+l0%E4V04((d$8ui)mivOWeTFYMmihu>vM-dif8__Jh2KVB4}HO7E&@Zo;Nt*Z z?hDS#eZjGFXV1ah7tn^>7d&R}3x;)fJmr~&+~xUi0f$=nk$gT{`G%sn<5|6nFyzj4 zkHnbg&Q%Ndu$cd%MU?va=$3!bcvr^`OU-nFet?W8XMXuKOx&(H$%t_Bu+B*v~p z%+NZ=oCk*7c~IVkYhY&si#-y@Jf-k|sC=d` zc+5q#Z|Doo%YDIlxi5HKa$j&>?hDS#eZhITFBq@8`)?Wb%447fJ&$Dfr&oUoYo!)MKAOdFl&t z24j_D%5K5hzGcv#o3yX#3`QUG_29)A`FwHS%Tmnd5}ilt+qVV07$cg4oa0X*u-kEuBLOk5@tnb?H zW--67G4b9x=lL0od3o37F|`(>=^e(q7WG_6%6mJocvi_gaYY&P+6KlhS%bH24eShH zxck4#F-O2~_kVyLK07APV5Duw9nX1r*Vgql&S1>TyS5&?5#`~zDc1$Rzc4SkGxNTx zeT_31^RiwsrmftFJD#)o|Q*^hBFv-jF@*SVkUh(KDy~o=Eb~@ zx=?wz<5|B4>0a8h>HmQi=E}Ra|B=r}(|w9U3n>OI%!wA}?3n@apoMMaU7Pro#QdDb zgcg){OviIA%$4Uz2aGp1o@-&Qyldm~TnlsM`3&=13$+c5xfYbSXmw$1oAq;!odGPK zBQejlFemT6c6Bjb3u!%}1#SDyEe~4IwtGFP?a+dj*JG}QIhpUi@27e~3ySeLd{p}y zT9_;E+PJ*eq3y9pa?E+I1!;rd{a68pY1hYn0^3S5KW>lU0X7{iWX9ge8IIqyy^=$kL14aJ2t2NY@Ygp zj{)I={U@v1NMV^n=1!KyE~7C7b-+80{zak!kcpuXTd8l&n9Xdm;cFKn{; z$2`}9`hsIz7uP~M&v5r&`a0>e;tS9Mm*-m0x#U`)^18eKhiR*)zX&ZX$sWnh{Q912 z`g}zxU+6H(7kcbg>u1du{2a0D#KadQQyt_BJ+I~qj@kZpDo=ETcO7El%r@7vJ+tlm zV(=~pFWSd3F9n9P-UHtG!!``CnlJSAtocHZ9WlE+$`?8wAiSnDc7Bz?j$b*=Re?b8~-=w*ZSV;`Y^i!DG_+QNGZZw|;hc z-QEBDXsf4Ei>4mBs~=4tt7xYM!a6N5)@tGI7W0W3lX7RTKl55G{HO7bGhV9&=Iulq zVoo!!)dFL!78q-_z}T564|jQ18Edt`SgQrXIxR3J8I$F?u?|q%J1sEQYJp>Z2r+T@ zf7N+X`$P*Iv(*A)tri$-wcvG;6BbgQ%tC8lXn}dH7Pt-f1uw>(d94!k?~$ml$5((C`2zFA`K^8NZkc%(0K?sXugg?-&l|^Q0gEwW-pRmXkHr0QWQpnd z0`YL)x$cpS=jGwPbIqL((OOLYAGA<&%e&Q=O%d70-6%yTW&+*xWTJW-GETnjaKW}a)I=A_J9u>PuTV9d2p^JB(b3pwUmkeHY& z%yTW&+?lb%P?yjGVbDU&of&g2)ZCfc4lUH&nK9Rbj*-u4j_F#cxwG^BR{I)v|1}48 z9>;VoXgznCW|`+&Q0xF7;$;7g_=8a8(a$-)3rc6XhG|; z-|9l;K?^l^HeZd0+)AX zV9(^^b^vD1R?7)N@eJkbJSofa6o74^V8^7qaY>8V5u z%xksq9jlA1(86oAz`Rxq%xks4SgQrbS}icvYJss<3yif|Agt2@V|SoGV;!KjcUoYq z&7C=Bn>#!2Ioj8q7C2_B1?II{U|y>QugmkTyjBa$Yqh{_Xtlt+Rtr>KmpdP-wU}&$ z7HaPNJ^6ezIm1x-H=?Z+%ik|JFMq!yh8wQ+Zvekj`d5tYvY(ws$sE5V(jn|bIJBdgyruSye|3s1@E)` z{Q|HXQC_@jN-_TJNCK2T%g{=!1!&I6wO52^ox?D6ihx$<4(Nrogm-`_68yS4%E zYVaaoVBVF(Huzn#iN~BTJl}^i@4_`O-`~pjS0T4#p6_oLB%j;Wwc)4|)3rc6oKw}d z-`w(W_g~v?_2jnWoa#b6M;c)JP+pu<q_V?Nb zyj3f2Yw?9I8_%@d{=V~o_YUyF7npYiu<&2TUQ&4O3yfW`26h&(@CD|Ha;49<7GGfO z$b$J?Aq;1>we9W;jD5>^`df8gPwH!&+1?hu!2N=|JX^~cam)`R=9YuknGbo)2kIEL zzt=X1xd>i7Q)S-E3+B&}xSr23Oxx#}*Tv>p`#YY4@*W0W_yV`#ekc#VFyL({(=#>e%{%)NZCPI4|`D|Bd$I=YRHIRNu>zVt4^YEQ8tuI*HXZV6+pG0|)FWB0M zn5i$U{Lr-U+s4!1;ND7};V~D14SWH-+!vge`+{TVB4&({`-x+zFQ5&%FE}sv1;g4q zo-220t5^RGTBz^*{#iaBt*FA{?th9Qch>io+pWKdhupcmcV4|!V`|ncHzquvzaw|9@12=fd+IHU~5f8a@eeXia6*jUvMn<1z@Q!tc*2go-cs6 zYISMyg_RE&?|RfTo~hc}0bcG4&btCJqn~|kbS(D;$1VUbp0_eD_XX#jTzIYp=HWK+Hvq!_XI;m-+&DsW150=J^76xi5GdQeOaX z1I8hqsnR&KzMzgEtMuETg?8UwDhOhC6oQ8d&ZN&dYtlvD_C7o8b$N-D!1c zeZks3!xtRehc*m-!DG5F)bmK*pOQh9XR5^)^f{`m{rWF9t_a)4FSNkCR}}03tkGzfeZik2t-JtQXno=7+V1>0(u!d- ze8I8Y7oM##XZV8iR;^E3UpUuzxi2{H9pJ?t$=3kq<-Xup?hB6PzTntdC~xo_X~nVJ z7aTisc0FhKg7NZv!N*~SFId0i&yl>IuYEnk7d&R}3(kAFwQpP5Be{1wFZTs+ zpZqQQO6MTHD{O1g0*&oB_XRI6_XR7D@BZ68@c*l=UioooLC+j*+qfY2!~an}AFVu4 zQ96&*VRQzg$0R?Hw)x$EjrnzpIntPP9;xT8fVUZSxyg7(81Fh@3w=H5E|14#-sRv$ z`xv_v*nCpn`M?$v>`Y+W6YQilVoFP_Uy}0Z3`W-mI*-)#8J)rCu}`2p^#vUVYCD}r zV%|Nqujvd1V>Tz~cRU$$-pf#4j33RV!QKD1E?dBhF=F0nz>>bEyF6SMujl%vF4T6M z!Klwvzoo67{s>yApZ|l-zo{u@veDwD*z&&vi)W+6TmA~Lc-FPwV*ZzwxcryE;$AuP zK4w^buJ}dc-D14oZ(=g{`hC%(b9*1NKoHZ0N z7v2jk)X!a8GP{ZvQjC0I&dMTQ^##mZS=YpCuC_T{eZj|odBqo?1=sTHj8}XCF_~9< z0T}a&FQDCw6<N~SZ&7dnjcg&wQaqpAxrA5qg`Rg`@M7G#U)DEuq4JO~)H#2rwt6a~+SKD_`Fu1zT~W%Ny&i;hT41cz z!Yvl_@fwqI=RRi5ogEY1NL_x+c&!$=yjBa2J=l1y7C2_B1;$z}FxF~;u~rL=wOSyo z(*k36V2lCNcvZ_a1^vqKmU9Ad^DA;S+sCBtAC0u|36^rGy47g#9NkU zX_m9a-u`PA^H7bs{1?FDjwkbW1B-k?vKcu){_n>7Jz(*inR&n7l*hbZ1QzXP?0JRf zIRRr&E#>vtqXNS|oOurd7Wo2W_bAws^%-H~{{$BAof-Qou*esv?c={HF}Wi z3mo%(1v{>e-{das>+x>^i)Y3h^H+k0J)mLWZ7-O=Utr$Tfz9D;p4a6^R^HXOx97XSB442L_Q*a(=4Z`MZr55xJOnp^%K`Fu3_ilMHB6oVG( zeP`l93pLOE7mN7~jR`I2_fYvhm3gj(nmfxg5~+)17{YTc)bEU9o@=4zSIl!Q)HX2Y zTBx}*W3GjoJ2U25sJSy^u7#RAGv-=Q%(Xxmw4iOj+2$r=u7!Hvnc5C5)cekieH>#O zTHu(j1sy-vg7b7&9a^aE!R89#<6RmjuG=*3+dR7 zT?_LicP1XRP;+N04_eSQ{wnKh^M%PiXrWpACm+)~PVR2?PqF0#zy`kHyybre7Fsr6 zm{@+l{25@O1=BKke*$di3z9X97TybN;0xgWMpK@z0nU3Bu%RzF_WZ*0^@w>}OL;x^ z=)eZP;4vQzZ0HM)-Lqiq3x+*L4N zlYAVmwYm9bU;|%3%vT3*;0w-sVZq23z>}=c+A#Emk63xKdj{6GL3VA1#a!Yx913ia zeZkAyBMxW0&GI-@_btw|^7hE8EZCCVd+(d6wKGtU>mOY?<^JP8tBnlFIY<_oqr1TW1OfL+@33G>be7P&KHX9A1d znX!}Bh~(pAmA3`Fxj{_lodzuFYkJp4c@65hzNrh97rFEBInv}6ofa$?pS(cp*yYZI zby{HTR@5Wrk?hYz3o`Q(EikXu!cE5eN#nIzU|y>Q=CxX2tknWztri$-wZK@b1;$z} z5Y}mdu{+S83z#d^_D&0oeFD6}bEJv&XQu_{NvjhraLiT<%xks4yk|Fa)7v*^<+WO1 zUaJM}m-`}StnbWgwLs-{x%0QQ7L%WV7V198p3zPoXXr*NE5-1BVWGThBOcx_$X!P4 zi}y>k(c~}Iz$=+2E64z2Uiy9kF@LV`+?$#A zG+^p7PGv-ZUviQ10$3+2A^Dq~~tVvoe- zy}w|-M{?fD+SlW^0E;~m$9zrjut#Fv#lYfS8}pt~uzS$h{bxIrZcL@;T12*Za<><5v{3f3XqgRQ3G( zc554Bx0gEFm`@(0Jiou7cb~1i$QPKm7re+9{?^JnO8$=f3;OQW=MnR+M$FKjVau;m z-bC0ok0M{-nDfBmy)$DMu7RBmEanRHPAR-y)-Q}jU80_Zb$9<6yVGKp_s-g;7^8Zx ztskTDKGY?~h-1pm*~THhE6n@ze%fca`>$sMwzgx7yZ`0>g5QOnIM4f2G0zuhjK|sE=xp1FyZ>blBBo;x2QTtOZo@HX!#uRW|tI8d4=7}O?%xfDMdnvHE`_I@#YhdR9i@X2KJ9Q1-(Q9D7CZ%o19nZAw z(85*a?mzVl?*7*?qI%-)zhXXDoF_XHnVauHJ>%{_$9yBOIOoH>R~D=rTipF`+h_9( zcRcGDae4ADZ2XeG#@+uq?##PeQx|W$mDk_VV80Pi_8*(~A}kvv?_n zGuzvXFAxuR|I2s6tjZi0eaRC6(Sq#Tq;1>F9*KFYz`_@njCX(IT@NgLfqCx$7V9nZ zt^gLkz}QO)&-X}-U9bjr7O?OI=AB%4?hA|^Sumd~gyGC~z1PN=>}RddVvj^^$C>Tz z{bqT~x5M!wKv z-$cwfKPY1&S~yZ;k}veU>I;s!mJczWv_;yjYh%xQC$Kon$h_(ceaz|$Jyw08$Id}{ zagLMAJ9Uk^9K8k>;}GjD#Ux+o+D^XEW1m8KvG#M!k2aXEsfTJ`lP~l!b6;>?^@W}% zS+b4auzfbq$QOEE^##Yc4YEpF%oulXgU77Cz&yid_`<=^Lc8{_*FKry3x>_`1;;!m zXnjFyH`5oKm->R=^Tyr(?ImCEHNbhPFMyZ&0lpchW4SK?%YDJxKEoFrlQqc78~6fZ%A5j5zA(7^-}-{bTm*063xm7=>TL(ePn?(g zf@9|*=Fk@$OML-t$bG?ixi1(7U)WamLVu+*Q@;Vg>an$a{&D$yv?8-vv>+;y{z@_A z&RfeKiFn=J|4&%VZ);3^Z{F6jM`GSyloxxX4;k;DjdugE*dsB|bLXw)xfS!SY}&xs zJg~@}8M|-|%yZ|h>I*0@^#w0) zhA+GaT4>k)UpFn}zF^o4UvTVpYg_$J825#@+MJe^$Gj$HL(gz5_XWpJDLnQC$Brr(`+{LJe8I8Y7p(0we8I8Y7d)oSCL4#L zFTBdM@NQs3U+|c@FE}sv1?T0y;C0D;!FjnaI4}1F=jFa&yw(@g5oC`fv%%Wk`ocwu z=J~>kVKaQevD_Da&gS$@C@-|&Yk>1|U-(JmT@PODk(ihJg7c*Hwl9t~XGx-sRvgQH z!Li&I9Ls&dv6E3=d_NMGcVxjhUohSbUvMn<1?#gJzTjBy3m)^ss7ri@r1$3*?duu7 z;4v4$8~TFta$j&>?h9U*+!vge`-1awUvOUT3&!KS|N8q2tF}j4d7MZ==gN{iNAllC z|Cr5{2Wg+s8H^62GZ;N~D`GAp=3a|=lE$R-NIh=_yv^W=7s$Mo86>>xfGza(9Nhia z-?wMpFnUb<*v3}# zg{EKV3p!*AqIqzjCFUF7N5}ikK9=A`FG;0oyPXm_pHJwM|x_CX; zH+7-%a0X+sJUjghZM953*Fyb#x6Z%mzbcB~I!`hD{=%ZXGxOYpc*}kcaIw5=lVT+1 ze``$q{zA^1TK((Yf0_Tn`?~S2HLTu$=kneTUOZc4-ethznK5IRtijv126hIpc&5rR zN5JAPK4XW^jye85uz2Q3JpBGb+IIZ@!eaS8RqB`Vjffe~R5_;jmCV1z@~+K!H)~(x z_ZJo=ckad(zrV0pzW0@RFGb8~1J^}XYbh@s+wnQz#TYT~RPd6%9{V%uMR~{C*VE^9 zS{{CXp}yz-w6=QsMQEXZ{zo?dboPc8QVd$CcmIh8Ey#BZu2RHcI9nb19L6ZyYU>;wV-$L zcXcsc3u!%}1#SDyEe~2KXE0W&?a)HK`_GtbLC?wf^B}6{*tJl;LlQA%Ct9>n&tP!O z*P&lxkK~y1Tno|$-2G>sYoTIX7uP}^Bj&jlq^~iTnCDv1{@iDM#yr=8_UHb#KcNL# z<1y}kp{<_&KD1CjcTN7LqSY6`A&h)s&dOr!8`fXq3#h;Q!aHqFe-T)G{{r)hFF*^v zj=kP^#TO8hdBqoiF|YUn+Q8T)Ru?_@iuXYV+g`^F| z7m_|Jz5p$7J&P|u3sfGouoS=7=J~=@vKN5rQF%$ z5!Pveu~rMK7V`-jlXB-iruc`=_sy8o>b#@rvBqn)z~!}CU|y>Q#x8C8lCkrFg`OE} zwZK@b1;$z}5Y}mdu~rMz_D&0owYf9L{1Do=1%2(jk@hv^&fOSwTHx|pEim@%bZq5` zsg&1hfq6A|_PTHzS}icI)q?Y^yzc(OpKC3q@`TpvSnnwOv3x$79-=6I>paDlB@?z> zDDHR?55IL@^99-aNX)}E=JMyki)X6LTLu>Sg6xarjQD>U?*qW%nJV+%3M|g%F>e7_ z>iLqy{fr+ZE?eSaZ%sT;CM}jZ$~y~Kj1lur1{V1Ox51xL>mJwh1>)hi&b2>1U!d~vTjzCN|C!ce^6gFwe<+`i zCi@M&S5rsAIxR4E3wV()$Q+lL-_)3$7MRy+LFT{k{@Qr07MS-|lowiHUaJMhS}icv zYJss<3yhtBx`Y-OJFL{xwLn;>1;$z}P}@5#FxF~;W42mw-rs0ncUs_>trnQqYJqvJ z7Q8P1Xyvt9U|y>QZbPdD;&ob}@>(rSKBKi*{qs%>rme|X(aF6rb_2aSw)|aS17864 zbzq?d^M#3Ia-xMV02}xMc=Ei-)~cZ|yx({q1UB#m@ZJV2wBT!p^Ii)q+Q8V03y*!l zdCyt{dt$^qc#Exn9P>nAp#{b^m6+@cR^Iq~z=poy*zLduzTj;if4P+BZJ_ZR?*$h3 z+Ndt$8-NXcVM+UXd^NCvFQB|D0~`2)^X3aiz5w2Z4d!*x+HM@54J^ip+i(i7p)Yv9 z90hEieZkAy^P^H8_qCO`=T5_x?C!tjQKp5-??MYTceXv(1kS=KJ$y+-Ro@7ib&i}KQZ0ldpmUgTvQ^U|hI7(2gd17l|b z+s=6n$~$R|m`BXU@_fN#(z~{XPOQlV7v)wxGNiBbpOk?#yjS^97W*9x(^Gr>d&= z5Y}mdu|^9PZM3TYqR*X~*XGWu{?SBsD~T4E*J^=ztri%&w3(ZXwOU}T)dFL!78q-_ zKv<^*#_m8p7oa_Ad#44)+T59Awz;$OWX2|1;Fzy(<{0x@EikXug4gAFR$i+G=C!#q zx1rSn^VT=@r1HAld7su|@>FP{<`Ex}&qov41qgbm#Y-`~U#NHgiMRX>@M4doJFd~> zT#bo$ZOZfK+RT%^)L@Ua%XqTu5T1CdVX;SI-rIr29*KFE0So_S?2^Ls{RLy&*1*mH z7WdkiHv$%WB*qReF_&y_MOgQKfw6Cb7keaXJKitU+?g@il_Ed(+?iv_t_B#7!^5<% z@qR(SpTT(?Qyc(ew9DGynDbs57~Zup@1h3tx;)y-I|o>d5%W$37JDRa!_mNEkHoxn zQ66$xx8)Lq| z)tIgm#?A&Ww7{6}Z0vjeFz^9`kw0@XP;} z@)3(vodJybI+kO;jvbJ@Jl=NZo!Zoev7^_(d`(K*jys-B+x`0) zaL4mNx%*H3iM#)GjHoWS`>&YK73ZC%eTF-p2Sf|q*pALY z8*#_8juE#Z#y06|-2Ja(#Js!BE)RD+cbE0wb{?z04=vQs->ma!^;oSV&TOX`&TQ{4 zzCb+O{oh@D;awKG*k~0|=zQDY901IDW-W9-N z&0*{%h39KOV;8J}odqm>fq9}_t7rHEV@DRu=L%ssv#o7+UtsK8;Ds+x+i_-lcku$ z>0n6Dj&m=>0REhIc~2k8@CA9|keZ)+PkX7z=hSAC(!&Ov!` zc9LUOU+8(&7kaGvLWhwrI7VYczR+WzLOo;cr+Uutg+omX?*T8)sd9NTr>w7Ij2v^` zD^two1illd9b3nq1YXQdF0cAR-!HOC4#u5%cWdh6ZMX6|Ul2zSEgU?<7uK5=4zqZ> zi9x3m!A|g~8o_^|k}#C(g@#!Lf5^ z&olgv=k9V&)z){kA@>E3nfroa@P#_B|3GJEx%*!}U-=jLe6;!&(}JkTJSe zi0{p-dnCqgZ?y2+#`~tmMDASoNX+xxS^M%X>&ss?-rpMU2E>d#67xKF-d%EM=3Uve zfw6gDksmX5;To9d&N4pOLon}@!t>mjv7-v+dnCe;J8Rp0kHpxW;Kd$^+K$}0?vWVV z2VQ7_V}7Jy$5>xG@2|A4kvmr{aLhM=7keb;y`o^Q1?G9~Ty?^AIS;&Kp5gbMs}`8& zxwDS@K5B!`;Em&hN?qJfh=<&{%z4ceL<`beO+9SAUHL;r^L)Xv+!uhQzOb^aG4p%@ zyxbQ)XuLFE0I%_d75TTKXI()yF6X}BF>_yVEcXS+&O)Ds7JRL8EcXS+a$hiPhA%jl z`hvGT_XS|NFL=z{7k)?kEYBAZbFs`Z*SgOa=e@jOyKK=y?hA(DnQH3`j^(~^mCemBqr5nS;cJKU za$opm_zoY+8^#t?i8m&yiL<=3<#+ z>TP~PAZC%A=qKJ&)w~ zr+-U6AFVu4Q96&*VRQzg$8NQHEXn%?$KGi%M;epvFZ8^624maC?NUcsgQPA;81Fj7 zTR z;OQ8(ywiXsecj#t=knH5U7EJz3`V`j|GQfI>5rg=`nm38M$`YRD1Pfa#pw5)JvSlV z@>i@L^-i{|+YcW^{l>t zXR5@jzR-_R^@RkhzR=Z^eBl@xKhH_4FF23-dR%+~T5!yH)fYBVO#K%d$JG~H3*5fq z3rQP_FC=|dd;waZdRAY67N|UEVad*HmwaLRGH5~b1wR}ATT=6p|E4_U3mr!JLXXva z!7nlE(y8TS{;oc}#-*;GcA z>Y-)*rfK1HMF(1Fuz?m@jJ2ScI>KoBc#TQ9v)7Tz8{Fkl-lFk-%y>0-?s+?@Pg-89 z1&-Njfw5K#jI~-|tknWztriIDw7}RMXrJC+X!?4fg%6d{PD(@lM*8?q7 z%;v;E3oXW4Xt7oc-o8_;ye+fmV5bG{7uG^sUaJKvue-lsZ%3v_t&=~tzldkMzhU!l zqt^Q57@g{d^UysFiysyzdj`=IW!ybuw+Y9FJ z+L-rrU~@R1;dS|umG`)Ud0#W{cwouc; zwOa7H{GFB8YJqvJ7Pt-f#u$Ybh}UU>$|EgYWg0SHnC$DcV4C#bPM_QzWeq&TvE>86 z;_g53mj4-8XxV&WV$Y_QKLadsXXgD0u%RzV)+`!&FR+0xpe_<4@df9-3Rtwk*9XU* zUwEzs#I(P$LaNY|GM!tZ0${R2nqoFT+#LAQ1GqAo5 zvTK_)wqx087#3QfdhQX2GiHawBWgO*>m}C zr|0>CV|l&+>{gT&dFRzq)@Y(nR%hf3;I+B)qVeQOknpZU%)y=+JkeTdo3?>@mjlzh zti>)}gLgi#XamPQ6IkTVjGeSb%p+!FdA=~QwhwaWW{mQD!TY7lovEIAzA(8=`#R4T z5cBnD!)DH1z)SN5@Sff9ye@CB^0okrF|r&IywiXseNFG$D31bf{p|80cOE`Rn!IA2 z{IPkY^(eXckOo}3bwXG&Jy;5m|)*X7RIQzQSK`hgbO z@>(r0uhjx$tri$-wZK@b1;$z}FxF~;uucn%aqir-eV~OF`vm$ra%YbDp@Q+aORF0# zv@ux=6>}bIp<*_d=)JSgTd&KUmB(6Wd94<>4fjPGVtxNDn=fA8dSIbhDzD3(e^+ZU z`3Y#D-tWK4;>iw9(2W)^#qfTi=FY_H-nG5MVxFZj@vg1r&dmE5%8Nac^t))`c;kt) zi-u}G&OG_=0meL8Sq+Ol5@SDCc<#-NJq=jo3yeJ`Fzk^SI~G{%kr+Fql;?XS!t(bE z-uCf+@M4cdZO8kCnmaQ#1~2wVT;BT&M(-CUCu?7i{r$p1d6&pB{r!TBJHKB5?_$J^ zcWul|-!Dvl+{*L!3+X%?r|%cgFaCa^=FU`J{(iyB!~2B;<-PO!v=)=&poRMRTjlfN z-TxHBIn@K@jwkVO_h0TibQeDQfbGEv~-GArZPx~6@RCSEDT3tA%tgpt4`wPsI zT?R1E7nt|l2J^Z+z{-=i6UK`%V&21nMV`oQI0o1}o+B~upiQ5^V=Tz(2B3avH z$09oj#5%WAbqK4m3p4!Oom1KIHr_6lXuY?*g3%BK03!dbq(IpYhXSO zY1?tfb7%S91L`x}@!VPN{xkL|lo#L6K=s5OPaQv>E6$T0iD=GEeqk)`oa)#k?z;_Aw?gz;j<nAI10tolNaRbS{a83?P(W{d;Xg?zzzzW&)5kuUUE^@Xmy8NP6+_BHuJ zAM=e?&(;^5SAC)9RbSw`cpIuO^t|c|%(FQ`zR>fkFEG!_o8b!w%MiG>hMHWjeS*6@ ztuGih!xtROeL-p`bNVKfH}nPPrM}?zyl41=^HN^`FZBgrvi@LQS*&XZu+$fTrM>_x z^#x$5FF2O_0W+B^V-+QovRi&<{J<*w7@*iovRj@=ecvmxGv`*W-`xk$FpjId7e9Kf9|us=JGsu z*7DpJh=<&{YQge_l}~qCu)W&KZtauY7aYrd0a)q_D=RjqQ(pit_l4_?Cx6?#4J}YT zmvdimUhWHy<-Xup?hB6PzTnu&sAqgvn6G1w<-TCp3}0|8^#$*<+!ug-5_JhJP+i8k zFTB^Z@Y~=G@&&|P1QuHGad2LmFMyZ&f_+QF#&PNk;HCKjc&RUdm->S9X83}vsA$_z zlcIjnLhcKO&F}@sZb!_4FZg#zu3T06nb#xd<-YJ5)4~nlg%*4ba9-{U&dYtlF^Q4* zf@8TaIF|c@W4SLlmivNXGkn1@aa61W>dzBYQy+!tiWKhqaHru=Pn zffiPjF3$F8LY1?M_ zf?+d!!Li&Iep+MZ`NE3xa$k6|@nnTee8G9SFE}sv1;=t}1q)=nIbJ zzF^o4UvMn<1#9~ZUvMn<1&^8g!WlNFb6@b7i)Bvx9?<8D^KxHsUhWHCmva#_)?=E3 zGx7!J-LLeu?~$07`-1TXcmEH_nVkh~b#wQBPCma>J|C?duPB|t*tW63=nO`W-D-98 zyZ_4j1&eu_#-#fTJx_K~GT)cv*|lTOH=eXuc-H}207mtsyF5Mba`5Jom=eRvn@_Ow zfh{K3nZULu*hy=|JYqIRXE3@p(0Qb;&*%(Bk9`8=sW0d_P=C^SB<9JRTv^}W1>RP) zkNTSKFZ8^ZffwV)yk|EU*JTT^7$fG%tg$gl`kKxoab0Zg(p?_Ms63p(sLvK(t*u^> zXFFC_{rtr`|E32Nt-tS#=X`|G?>l>L!q_(uGwx)Yk52zrW8(K07R&dkGH(iAJXic# ztIO@iyB63&QkS;_i~HfslV=3dm$iM2U9tvm+Zxyzz~XK^#~cBRyZ?+GK079Ue?i~1 zd4CeG{=ReH_WJwI33emOi}z_9bE(069KND`O~3EljcxsX=RtYSdnsZ@8)$6t`_6g$ z&H-<35Yy)1_*7s?Uys{o)LdT7>!=I09lyV@qr7YTWo`BJZ=i*G7UW`^f3l(!Eu>iY zTO`C=b}iK3@f7bAeSKDAcE3f!JlBHWzqreu3+S$DH2t{oTnh{3T^sXU3-zoL^IQwH z4UD-Kl(*QG=UT|IGi-e7c^!`FTF~=BySkXJg|wc~LcPmFF`)%*yVsN2-u)H{W3Gh- zc~<1_%s8fNL9t`3p3ak@vy`q{PhR_&AO3q3}@FlY539<(55 zFi?N@h4toTBL6<e_f~RM9Ti)eO z`)uEcn3t}>lTvM;8f{=+%@_LeYQE58HDBm3$`^X<4%B48lo&V9^Q3(qs&gN=6`+7|PH z%WJj3yjBa0N#7xN_PQ|EYJssc!PEOCsy)V9Eil$sjI~-YOjRJeTp1~A!w%Gi z`eksJr>SSB1?QcpF*_}A%vKAGwOU}T)q=NA-X2JKtrnQqYJvNu)dKTcEl_#g{e@q# zo|I=a)|Yj!CQn&K3-?rXat6v#N4DYyWcuz-ZFTxNBSA% zO}=Bi4*-ij67yt7C-b51!AY&2(Jl}^i_RKY~Cqzu_A((dpu*eq}JFLX? zoRqNcx6T>64ZO$~sO{ZvoinxyUff^cnAZb~e8G5=?`dCmzje+ruK+Ld1?Ih^UwW33hl>$JdFs|9L%rv=7ZEpW_M3(otp_I0NPj@fE~d94?`JHw0N9s{?XWMg4-fZZ7W@CB2;JoEDUzkV_&&n6T zTS0k|mpxAyqlrE7qIYeSXMmUH3*cSew1Hz@3M_Ij#?pKNG0y~VkS_o`X^ogi%*OJ3 z!DG_9wyy16?o8vC=L?AWA=G6H=NS{3>7s>q0oyvjY);U-Hm-~F(tN?kXmH=Te+Ca%bkXT3}wQ1;!+XtplM2##${f)@p&VRtt=^ zS|F^`0%Lc~ZhNN%>X%Ln9JAGe^Dff9?zF%$+uWIXtrnQq=FVQ1pSAK@EikXu0=J>f zotf8afy(Q0XIpJ2Pk}Gg-1)^e|71rjS~%3kAjP_OZN$U-h5D}Tl@?Q4EL!*?Vh-Lf zOsu@+DX`chX-|zNCm8QqV6jKy^4<5V(W}ax_oxld~7krHH zenH=1?W*q(z?0R^`ZM0Oaa}}Z1I+94C@b$AU@=C_I~7>$k+=;<1B*Qp^JHRsdHjB1 zV&&oeLOruBb3|tQanOR?UBi10UA>2Q|5FU-RQ0~>cIy-3;qHHZ*Y+xl`5-M3cmMS+ zt&aio_FDb*{m5MwbGz|mS13H$kxJX-&M)4%Fz;&cLJP)Qex33r!WLeAm&h^a3(xZf z#(aM(cizx1jQReyzH4LbloHeD3S&{1K|QsGGXH#kt8KrzZ9DESXxqJ>)OMUxmHW;K zwhwiQF`~NQ?!S&7kHh_}F7Gy;j*-7>Dm65Ye(r0PIE9?SeT1U8 z%{_)n&X?Fdrl5u`eu%7P>hBU(kB?ycZy5jGxD}xrF!5 zZTlQ^U#MfmJokkqdB@+)37ki&{mHzT*HIU5yOr0S!I1qM-TklY=CRh#KcOhjY*$P_ z#rFc>nW~<#o3nL{v0JQ9sxLf5d3st~N_+;e@CD{A0}Ee}IVa`aUwO+P02aQ$yte|2 z^_F=Hz+%l|OrATKXT;ji*fZC_WJl*-gLRX6Cjg5*5@Uyzm_CmP!!yF1GTU5OjYWF_UQtA86RD%dfo_FoKt09^@WZ{zR+WyFQ_j#Mtw%U&|^2E zE^$tk>N3L@4%NO!zM#I)$CRC?wSC|V<9X$+3cJTvZH&klxP6XQU+BxLzQ8=26XXj$ zFXnaB)7x(4&G3bTp@nwsmn>Yga4&0Hip}r^3tpGp7aWs$YP|4nE-&>3v?2Eei#fv= z9Q#&N-g9(juKY)*g(vI0UHO`#c-L07Kp5V&)jbkpU$?R(UqHSi^Gaggt}&52S1mBl z-?gc)-DSRZrt$vHcppUlV~@nVw*iYi67yc$w41RP7oO*&j6G`&?1>Q*@7kDmBCyZ` zW3npQn9t)rI$?O%R`*DZNzWNC_DGJ|7$JAAdnD=?ylc}j+S>N#_y)v;7Ss!5jQ(2t zdR1choCc3$UKtpkBROU~bqD7mcV^y&4aV(@XAz+V=A8mw?2(vPpGEi{iFkO|rv2%j zK|H)`tMl6Og_S?+v|yTC`D5*q+!q|neF4}lsDEg|d|_p3bNchZ2EG7Z?hDr$FU=Rg z%YDIlxi2`D`+{S+FF2O_f@3G3eW3;0r>;1b`+{LJe8I8X5HqwuW0d=X_scl<1&^8g z!tZHccliRBcLnMa&ygH+Ug`^|OX>^WFL}NIUhWIthSV3Fm-~X3hkT)G!F@s85N#W3 za?!NlXD}+~$@1xF#jqK^;8^Ml{v2uLH&9+^fn(;r@M_aS?h9UC?hDS#eZjF8qb`v< z``Y1H?hB6PzTnu2h#6Yon7J<)Z-y^8*7^eV8O~r7U$D^`>B=s1dN1lS^aYRE_<}J; zEAmuScvpiLdn7K;eL!xxMf&ynN|#zYWmF7qM9mj4Tw#%p-X`++S2`zebl3YR%8J4=hXHQ|kcZ3d=2JX$&0 zc<%?cknr9DY@T_`uTh?|2W{Y(vJx1!gLx=V_H{F{&A_%NyoavAyU%Ri_+ZE$`^(iF3~G(Nw0Q9$c^&ii0E_2fGUuc&G805YyMQeqCiC75 z49`EU_M>I_%JYi)>Us?2*luz04**h7HDGgZb8osEs}0+zIW z{0(49+s9vQVp6}1r@&&2s4nAcfyMJy=lzrR_4w_;k}(=z1}y5yyh~Ed=F)gugK=HN zudJ`<`mr64fFEHdiS4su7!H{pE1{hV!PXz(1N!8<_>c$)Vu%Gc4(pg771gn z1s$WUT};7tk+^RbTKq##r$MJac5M z_yV**SoH;HfwAHX{TNkWNU-V)T|LPcj-m0heI@yV^Qf<@FQ9#nIj{P{CW>kEg?z!a z!0ju(khG!rLO-_E7x3Ph>REjOTA=cvg(W?+UGs(ME8z>86U^0p|MaI#3y0WTNioV7 zI^Obr@D>qM=9TPAUSxCnuYu{=_>yOMdCOy9n}OM$aw<=B#82K2Y$4&j1(=?_)|kwb zQ4(LMZD8zTU^^1tGuFVQUoEDdsc*}B=o-A5FLXT07kaGb3ywX>`qRdU@`WB-L0xpr zn|e~dz`Q4GUsJxon3q@c1;(6L^99D7SMvqOXdK2hU+8%?U+8%?U+8$0FLXTQ3w2J& zGcMU9NiEP{bLg-0te+pHDCN!`kFZV)jI~;Ly2X^$ROWQ#dZ7j8wOV+B@g8KnRtwB) zwZObq3yfXd%v;75ofa5twZJj2L)-N(kLtvECum=H zTHu(i78q-_z*wsVughsxUaJM>wOZgdv|1ourv)kxTBv(6sheowzR*Jbe2aWOn%+av z$-&lNDYpC{z;w;g-_Ir9^1lF!cWqCzn1^Z1<9P2&SUbg z>*UX%9`fEp=HFMfj-3_=>$JdFtA(dpOsPSl1?II{kU1xH`77i70qPlAU|y>Q=CxX2 ztknWztri%Q|F%90EiiT*u+RcytriIDw7^)a1!{Yz1;&<9m(T*oY_;G#d6trBfn&B> zU|y>Q=CxYzy8MHc*J^=ztroZqtrm#aX@Sb?v|zq4`Lj+7TWtP)THz=poyyk7wpYmTog zj%_bI_65geUuWeFeZjHEMNB-O;h4t*3oS5q?}D)}Sj_Qvfen4Zv9ALg_=5M@_zNW_ z`2w&Bu+Re4W&A;4LtnVw=H}ag4SWGHUkhyL3(k9S!4A}Q8a!EhF}99*UH;I@lib0u z7$X|n@rl5au^n$J82N&ix95AMJnm~NZ_n+9>AeTvA6{s){hD?1N9s}Yi1Xz0(L`p~ ztbD<-JYN8|A2B0$mU$&H-=Q(-T^sFF!AtXniOgo12 z?o3#x1;$z}{FKF<*O;9anAd7S)(e@p%I>wmyjBa$Yqh{ws|ChdEil$?^m}zL+uJ-^%@Cyo+MY z_qQ6;wLloosp|R9o7z8~YoqdTPF4Gp``XIGIn}G=PWpmAPu~59T8ls0sajZf_jU7f&+M9w3+sMt-LdN) zy6&WPr>$Fe<8~c%`uA_z zuf_xGaWX{co1fkCXb}ue4yD)J^KIbwB)}QfsOG(bj^;m%1N*%KF2PKYab>4Lg=LEdF3| z!%e$>aMRKcZrZisrXTFwwf?3bp1xsW{r>N5-EjI3ZrX73hSTr<+5gYpy9Y_Go%dll zKz9!=Y4T!rc6M<|UlM(1X9iRx61sC|2ed>AmSqbRDG8D+3$jERmLEa%rQ)uNlOUC{(|Ne6^G7PZJB_pX zeFx~-*-NTamE^yqXS%2705~}3JKy;pzwbK-RW+>2!Hapy;#}^^e7oK5Os4WJ1I!>v z3UaW#18b2;$I`3PQj*yH#lccLSe(4}F?R3b^*cw)#lgA6s6lAV5n3nvx$Vtb$8M6! zs1x7SuA{W{n5$T5O;OOz7l{?e*VVSwbDddARi`;KcsxiEh|X0O(7g|&0P)ghkxSn% z{h;*2(vL|$DgCtc$Eghm%VRV{P-1!fJ@V@hukOE}El-TIPp}gV;?2+T#o@dD2i*?N zAIu+KEf3J>!@Kv6aQ+;PC&C?|bNB8aoSmaj53kP7ukPJ{C~sq1P7RZ#!&y5JI&e@xPLCo2|LF|gT;$h8l zgSqlv>6GLR_*9x7JYdrNTv8^fb9ez>1@97z=5JIaqSXRDLSX zKggeY+pN?5z|CX1Ee4-S*?QggMI6*wR7cymSwEBVFx&>a#xpX*nbmUSM~u2@80!f17?>dJPM*D2S4UmJHFb=7Glo=J`(UM~W0;46-auxUI)Y+CvER zT^Y4ZVr{~$)oR5yd1Ez&$=9i#>Sojieh~3$w~?)Yw;^u>hzmQhJ=YI?N6BG5H{k$yY@L<Jj(DDN>2!fpTO1HC$?UtoX8^ZDLE@w^7wmfQ>?(=n*cf1an+woGaQnzSWkuwQH z)#RBLdw%5mL7GN`wAk#{tZSNP*y4kmah&lsD@*1Nwe0bxO>1UJ())$(s23hI=}Xf4 zq(kVh4}wF#S8}8ukp7_br=>qD{dx3$Vza~LmFV3C81&+DK9=4;80V^BR$^H3UJ7oV zU!EM_pWb672O!0$H6{Y$k9!W$xd|O<)IECJY=+R#159)n8?ZEL--$FmOj|SXy###gdbZ_xNm)mM zrtfN2_^?%a*>#7A7rX&q226b>DZOLHrp?R5c4Bwo`Ei{1A&=sMUTZ%}Q{ONX-#1)0 z!7=>Ev8nq-8^_xu!LMv2sbiT2_gQK{Jq2EvSb=MrVQTX}*9=3C2hVc~Ka8RrDu0XP zNnn^+;KIBlxe|bf1)sk2HwB-+USSf!Ogp^0 z7iAD`M+frxsrXc+V#s_J#~Z$OJeDy%<~=X^N~yS;%WKyQ3Zr(+>)f+4%bRJTom;^i zGlALk|fS9vK+;E(l4rO!}q~9mXfac)?!+xt@+ww#(M0Jjdq7bn_PHh zzX()7^po`3t@2;`dsF#~@@A1AP31(#WC6N-U5v@2x2AHETY~l#^bY6+3wRn^hir1( z7g&(C_@)2hrk-dle=OwL<4j!jjhk!4nB<3|^y@cu3Hu&=@(ZFI*8kK8=b)xGz5UBn zo@+PfPvvj@!{_CY4%F5hI{SUncSs+VUYFeGTI&^|s}zjgWDAn!7P3K%F9{1}(nRc7 zxp(}0gx@%aB@B6xKUrs4rbwm6jn?%-&kfuNQqjjx$2X3<&DPB06}@MxvIryJ(R|M- zYu@JYDTbU&Jn|V-In-If%R%Rv=hz|a#n6wfAczA$1c%HL9PJ8j7i^nxcU4D`o*Gf! zv*ucxCzx+3`stBR`qihR|L=Q&E~MXu|B=6ZtGp6(;m?Wl-#?Y-s3#HgDyQ;CZ&EpI zpK%_Nzw!W7yz#XTg#`p!zzBN4Ao^}dNV0GL4;0V&ok~#)Mcr+gCrchTcwg|AyMZh* zZ?~NrkSawIhq3F&%??a&BvA}HV&=@tuFG+O)DEsbN0Dk z5#?W*%4tqj#CKo#kEr}+PWh;Rlv}XFL9dd|hlLo3{d8jK-efzCn&~r=C;e{8CrL7T zs>gQ^PT;HvD+>HLzdt{|dv9r6LA$`?G0x%f%+F7ST_xBYlTAa{G}Ut=OLZM1 z)}V1~=!Dc?f#Ry14^166KHp?oqFT|~OHJ2O)!4L}SWp}v#OEaQ1%JLRu*A22l1Pkl zl8facfL!=nf^WYxm5=;S^95QH{QSk6^Bs&8%D?{iMLAFoQ4h@vA@kq)BcdD#AYP+A zN^+NE?n8JEKLE-5BN)Zy`4PGI9L0@>OZ5yfzMOkz{(`p{cR&TM)dFm5H z;mbW=0nKr8Er}!6XBmq^3_v*ZVpiL3#+@wB;>4@tBCKk-{dN?nIqb7lm1j-M%nTN8 zUgW&XuZvj#6=3rf>@SuLz8r;doW{hW`e{~Y-FH>1Mkw-RA~Usfi_0{8_P zqhcDI!LfoF3QA5^>SWr=m?QHIH?17B(P1)k)A4*e3&S`Mwz*HV@q9CD;6arR}NlCM0UnK)~j{3$TM2G>RP~(;Xh=dYA>@LawL`s|Mtf9CaEaE+| z-}W}&;?b`PURwy?hj@)>{7Lwyf%J#}@6h-ZBz8v^ME+ISwj~j_OBa4+C6~dLuk5%o3^iBnc-1Av24+*0;>9)C zPN~0P3DZzbF?c<_2Iv>t$wEKZ<)nc6N@Bm&4b$wbHnm(&j(U%HiR2voW$1rcyO4AL zHt~{>i?nAa^#7Z{H&9+o<>YVV3;oX7m*Hce963VbeDWPe`vO)r%3(E#@)UMKMfK$H z8&D2w6)#yTaDD(k{k_r|Xe&Gi0(k+Pfbo{_(n0MJ0w!KNB>45tfmb}u3UVb@xoNSm zke#7Xq(Lfny<%|33)Ahq<~F8z$6C0}9VQjv5gTGNtps2*5O_kdT#rk8z-zFo(|wn>`X7tGcO7t99%)q0Q3P4 zhcSZ&1${m@(JcorWeW>F@aUiY(o{~idPL_x2IaNI+>5dNmv5b4k#7H$ zuTlBi%bW7OKPUXHPd(n9e7kU$irS50yXq#i=|-`7ODfzciH>q8_yK z9rP3RpfWahymnSI@6ioP~1Ewba#3C!B|?0VPJ72@3~&Cbav%(Q<4M}*h7UeBt2Wv}2SM&%}0 zE#UH{ZXCOLn&vw9qRupR(+Et zQ}`=yLvjIG|Cy=J9{N^``37mKEq?XS-zqO?9NrQ0>eoO&K|7*%{yZl5Z~f(|oaT?1 zC%H=Xyz}c*ImtVZ>iP0d3z~}afo+g{%n?7+BuDyPm>E9-T7qf!q7Rbq{2|4>9$tx1 z*VTOpM~FaCGD=4PmiH)xK_Ks`2#bw^@DvghCG`27OficjVg|EZH}134aZDsT12oOW7ytxyJeLR6uqs7*oVua8>@{8ti-E~?Y(-gOnn zG;+_`4(rU|Ny4K<@YFb0N#+aP`siO3eIp!#?2|~$Izq|!8uJs-m=qsa-l2e~ke#Ez zBLPXhJB0Lvmkby}m~W7%hvF=nkI<+$!t(S!G0$wxR>mrBTwYyQ!aT{+unnqRCRc-} zM2c0rp{C`k*JeI9#iZT2vN81Ht!LXT+in#**LW5(09ojHm~X-~aW{2RI=MAu+_Vj= zu=>dHX>xlmBw~Ie1p|#oT?TPOb}Z?k1lr>}#LV~^cm&eXEq8Mt06wKo-s&UJnDd9P zFbE{F!%l=V1xnD!A5B_;(Xii@5AJ^m;lF((nc~g&Ru!=zxG{~=yzIg@%gcr3YI>%* z0cH(AyJDv^H)n32c&QuaK?!H6mlMxQOqbPx64hY{=gaR}0Ka`vArjzm$6Yvnm?TCC z8x9Cr76By-ycCnr%FHITZPu)|K+6z|k`Q-sc#~I+R~D4cfO!!X%*0fLa`JhfMPM4R zmu(g}!!4Yo3yZ!dW|1)uZ19Q=gwF(LjCSarYV!k0Ik_?NPvC}sfDxsja-FBqycq&3 zq<~o4FgqHOqHjwJNO$IBTeZvsmszv$I$zlN(B)<7XK@Bj$e0UWiD0kCL1D`Eupu;5 zLLkXdBwps2vm`5pOdRn`7~go^lw&TB`0(SuGnJDao06>j#%QPiD=(Z+^WzsthUSZ@ zJjeOq6~qMoNh%-hUhvQ~pC)Z}9~7~`Lz70r{2&Ws3Hx0L0g@E(c7$kMER8b+(_|q! z6UC|&?7* zD#76P*9ZwBGT|wk>v)!48}qmynnY`=B*}_6LKr*I@eg8sz#^lnpJW&5Rbbco0gb_p zojK}5(x2xjKYIsU35x<&>oopC7k}gb5T$z>~8Y*$xlR3$}_xMd3t z?p!xZ{6f{{`FhQ(X6RdLBl%jw4$J4Fjen7vh;j$x6$^U-7CU;BQaP-miFXm*BTAuLn`)~6Wn zeENI;n0)t(%lr7B%IJNNJk=_KGyA;BFeP2Z)a?)max6>guuZd#qsCeUOR7&d8j}#o z07_gJO1bkP!dc9a4m18;hfkY`z#pe=ncFlAH63Ei@qp%d47a_H6(G5 zYMKFNa%>Ago9=NH{NH%YVKBOZHakoMthvE^y+y2YOE`cao&uT*Uj6^D&g$q^-gWH9lO=YRl5p{u*bv{EEIsG8Cv4Cq=d4v%tC_+V3B*&|{h zl-uB~jQsKukInbKVhQ?$3m3QstRC>(zcigsJeLy={N?{jl>Y@Phb1)HmH}qe0j%{g zOKtCk{jBsyM=2vbuDxuT{>~9cKgD}aLWmqRdLwOY?=8A{HEq*hgH!69}O* zDTGl$rS-^zVDK>TuY>%DAlRmsoj8WA>zbkYR^d5tC13`7cCNC-O~km69VX;ebyv`N zANM9(1(Ng5+51MEXzxptk6jRk@{NBW&fohKAwmw2@ruNvVAF}oB|5((6DBx6&0bI-npL`wVd*tPx6UT^GL?7P%@5D9# z>`h&C4S49y|523x*{M9n`9c@J`Cm}^oA8?OqP#y*kC4&l(5oJ@ihdUIO|Xj4UBW7z zvQH>=2r~Kzp#S|5-WId;;Npxp{fdNT{J;%gh^1(!dfr?_qXDzZY0Fq^}f>!>j z%nRO!+j5Kei@n-xVi#V9%_7H09Jjb$72UQ>bXa3fY5=X?2qe5tvp7YRx3P`H5D<8f z`SYf(vvLR^5X*wOLyJgIx{7ON%0)T249Sp*1N)3`!@wwG0G5%BV8O<)907v|5U#!) zTPa|nTPXOTC54$Md;D8cK{tmmMdM$2WE;SAff^SLr{{DcZnG+3l2FqlSBF~f zK^0`TglP~ujwc6MS+`c?8;Pn_of-OmnuV#4h{2OWUMG3G)@>_dYKvFow{zGyFxjhJ zn3h@BBR&a}alW#6(&>g{>qUtqO2odR!gHIo-DHNDMi#FsBNE4Kau6{b;+PXLDSJ|} z*fF7qXDOgi3qrscJYDZFR6J6AUL#>{4Z+X=$}=;o8~Ks=G=a_VA>fEV1&aPY=?_T% z=yPlDE@=|MN(<5_6f&f^;pxP#`@8cwsQa;3g^)hJrt%iNYosL3Q~*$A1bneioz{!o71Pn5T`BjOycsmg30B-) zo$XdgBuwQP{g(iK7;?nc*X91(LW<+qvSJa+nNMr?cZ|5)mosRKtQwp^5P6%iwE z*kBy9)R#4QF%WpX_kABShvu=s`yRh4=KY@`atc262@gR=);>3zaGp=SIK$sfm3QN^ zBM3Lnc-SuQO0kj?+00~fSg{p136RT>@@x^-J*?KGU7U{OD4G|giNEzC(1G_aBtPBAUm+Xcf+T}n{%433pb_zw zMYi5M|FbCnr>PvdMgCYmVGgj`Uql=Bk_)c20%ik}^PLQn_GL0wmhXW}F4D6f9xcc8 zJf%QPFYxprbmh2$X9>qU%lRf`Y9)ud3NtqIWK#||Vco5LRaZ)w|C{)D8@0@DD#j!&?O0{ zkt0Tw;&dd?I4iTlPvg*tV#|DLDg}2vc*IDVb-Fx=TsJ_l4Ge)uP~srVVN(={+=gY4 zUBhZm(p*``c4O%B&5-!i(?*|Oo zo1=MpbxEYTS}qd6OulObf%nu#zYT{q)lfQZ?gfq;Y+}A*n7}Ezpea(GUpI)VSYbQXAf~_|L7A)*GBOAzV3XsW7!4bX>3REy_k-(bi%o`e> zRaKTE*$J}9w{o9#ky|6u0XtVKEC_2d&i}uPL5()feq96kaKeLw)w}-@qlgKOA{tNe zO^8KGMLi*4ma?bGaToj{R0!;7k=g=7`si}`s!V)>lvY$hf5~}+j@Uq*V8uri+05J^ z@XuhDB0CThRYWqpy53Yw_4uZSjL3^V*w$NkHLebn)kT_?Lp9{}P|)W>%LedE>}9d8 z#g-Qtdf{rw2aRnj%)Bn^3Lvj7Zioda%q?LnSFz7C-uaXgtjg;!9Bj{vp)~w8eUMxn z;|H)mr}#lkvf@Uk5U%s5Aw$l-{*yv#0<*y}@=0nDr~2x;1LE!HY zwdPQ4OpD{(@#4TSD?_(@K#PvQit;GWY_CKfFZ`eur`a5Yl(d$R82@ zg~<6l`|=kBh5ji)fAR~)xW!DA0!b6)Bb}l9s-L3rT%mGcYZ>ysBItu7LCXtRT^MC# z#0j(X?hQQk;1ZM+W#oyE)+@>c9mmf-?MK9mrj7_};P2)1gFJ@>IqHp8r?69_JVfxv zr=qM;Rg~p<2$APpH;Eo5ffA?mN8S?s+qn-LD!?SLO)K_*U_J?v6xIOmsTOf-d&n2k z)WC;p7<);+>b|uoFw4}7a`DMcjyy)@BlWOOJ4)mZv#d*N!Sm$nflfpr@Lfb?n&yJA zDRNnq{}WUBjUP6Z|Kn3R(OdZPQ%=_(zo{pJ-v)f`OCpZ@=#Ne1HqM7$##{?U{qX8< z#xtPuw?B&Vvv&${^pA*R?hU5JL@B%vQ67od80Pb@UWjtkc+&^ig2=P@yHx(Q@0-d! z5kG{j@Xmd4KJ4wAdkb1T{%chJ_8+8j_>9yykR5Yy%q9B_=AQrJ>|-#Lz#JI;z1AeA zf&G1xxQn^9&zqvPUsnipoSmO6D0V}J`602^gK@eZ`1#W$hb$a4wzf@5nE~byArc4_ zJ}n>&xrE8&89?lZ4T3poj9hM{oj4rX7RcsB+89#tlU+jZq=G3XDa~f>Z8r0s7vKc& z9&fRR%QR9Sh!#@UEHg75?rb7Y%XJv{jb$d54Jm6QcWVvopH~5ILS$1mNZpAYgg>Sn zKf*kTmY9#=pFd0_6Y^5f419(EQ4S1o8WZ9-$k-_kENG1LZ|3pn-~aO8q4QzSlPyE` z+a1_!pO$_UamJs9kA&J7+#3zR&I{{*zWpFPhogUkLAXh^7E_(<+VY)HNgRj1J_w2Z!)Ae z40t=Wk$(iIG^~}ZO|rO|_t}833dJBI=J*Ka#PSNG)HzsYXTs5RY|k+*BR4FLd5l}- zrn_djX(U*NX9czs*hs4KdEo=dSDdnO+TwZ}6k+P6u+?%hho<>H&b!fX`U6B}QBHmn z^wry=5ApL<4tW^~`IWu(si%(R+@>-;_M|GzyI^R-IrG5V=>FQHh_1re?Z!;yRI1r|Vrk z#u0oZ&g+!Of8Ne?|D7;0dZn&}YO&A39e3rR3>P76$uM^w9>+8t!@W?WZ?&rKromT={jXtAb|6YcATGFH({1 za4CI1a!0rz$FTj*K`4`jzMNRX*g1`#=VJi1yf}>nU1Qt*XvO zYG`Wcs|@%Ik_!-zin=P%|1ZmOd$}s9LkJ9YT3s+{&xRu0S`a(2Dt60$cJbR zv5o@>1tRudji7Ld#WR^>WKNR$F|64D5Z!9>vnakG_^BdWZt=*Qcp!b2ivfS`^U@t z(w(odcSMMAQo6-@P0uP~=IQ{x`6l5##BP?(5-c?7lL1Ok%T$kLIUw8W@ww5X$V~ z@fX~3ky&T;P0z{#Bi=v+I7LA@kn;+wxYk94?0_osatArfb&}MBwZn24Ma4b~A;~h* zy+ZzuHt6S`%Q7m5{G0UXzC6MCsn9DQ6#V@o)GySNqr9N=$K2RrZ{v&M3q^Vo&($h!w7uLTzJj_BX#|JR%LPI~3@|BGnRr*D;uetfB= z@;Cp?R6g3^S|fJ(8xc( zcLc`(OI1XI0xicvxc!`x$`Eft|H**e+n)r8j9wTequs}wer_k7$(v2Vq#HSMr%=Xx5Ht=m>*4%qlsy4uL=_Q>mxe}!oL&YrhOK8i7U{96U>KXo%E?zs2l zWXHN=(gdTbK7R1|ae1L_x-#Z#dZV zBsY?#3|Bg!!9-bkb{FLaHdgR7j`fsKs9mLQW_j(aF`4E>I*1CCgV9b0G<7y?0& z?F&<5ltB-#4&tYFn@1kG<_=cLu)d%u&#vcdCC4h!WL74dHS=9ej1{s}pb+7DH&_sA zrfr*G`N)tDTaL_9vU0XE3gaMDqYerHIdVC1jR>2zy=@(;Q`(pWdX24i2EM0aBgzsG3z00Oss5$P{HyW*qZwOU1RHyhUX6R zw+rXm#}X#5vDSbfLY68{hm^3P?|7(fwpi$2qdjB?QTtPPXvflbigm(2ffkK3W8#i| zPQ>X83(y~4PAky&iHOi2Fw=#teq)e}Wt52J0}{YO^#*J86t6%?6yChMYv*lI!Q{q1 z9>6x*lCKanCf7bgIz&>-o1}!1Qq~CVc)H`ed8yY$6bxISw?hJD5Bof48@UH>^b|x( zd9v!W9Ke<50^G`sD-Se~5Glr3z_`F@ zOe*4FF%E}zAWOi4KEULFyNhP%oCJxJx$f9}vx#`9ThF9EYS!L7%yC=8@Yg2qu{%;Q z_l7dZvSS$DA<`Eyi&cT;L$)HP>0J64?EfDGqV*Gq`tR`+p=!vuM{Y1R9F$P5xKqUM`7To zh+fy(OmmQP=VzVY6;vaL>Spv{%gV?6m}z>Pxbr*aojjXpL`q$JRti?jC#}&Y4&2~W(Ssmu%r3D9+Jvp; zUBv8}H+>nRV9j%ng|h%(3XcT~^QngIFbX}Q)askBy1(tBZUvmS$3na+;@fq)UgJ-n zl5Qf|JL>XRMSs8droVgJ>qDaad#3V4*zTai<6oolN~LnFYYIW<4Dw{$U4%HEP=y~F z*SbT!lPWm2)-Mg^i+*iNSR9X|-M z1Pp5u&D=D7X4ON_a`cz9I=y!~WytuH8f5RGS~)BTW}&5`^QvvLu3;*WmCSNO-wnK7 z;^BO3Hjp7BYZo-brkRHs{8z$Uu#gAamSMrN=sV@GIOH=SH~tOu+j%ZS_c+4gz32NP z-8sCV(4L61ECH&(F~`mpkOkwMA1$1oX^1()l7arDzqt$;(&i~3Ault4Ddn2y@&r0+ zvnVt`MZGR#NdZ>L1Zid=u9s$WEUnAnRAZl%)GxED%K~P|W?KcWoM!+b`qYGp-+HN! zeXCrBwnsurPa7mj9-w1_H+=L9cF3pxkl+DGv0LTffvFq_ohTJ_D!yF0n3qk7gVL z{D8nD7fJk37Nm3^fvv?&NQDehSrOcK7feSE*U4mpvOD+h9I+nh#chSPTO6@Yu*#Mk zBYNi=jO37+LFh$7i$Rku^uduUqX8#oQJDr+3Lpd?5(9jn=!?`cjI@lwk)}B$AtZM+ z6V(6@$+;2oB2u!9=UBj97TA!jP-{O6D=6p$BwL)}xF99U$Qn6> zSI)L@gA#<4uvRSqN&#I=Z$vw=f)Gg*NNm9JjGW#JGaq1G6)?qfq>QggXFFt1u?AFe z0RLfwi9JaQR3N#&n{r){#2kZyb^W9d=FoWMkb8>P(S~ZoOBVW{Qvbg+$|Yzh!84Fv zv-^t=YiE>ScX%LuM*dYCc;M(V`^KxxdzF3oRrd0$?A334j5!};FMo`E_OqWo#QU?C z;4ail*Dr2e|D)pi*PQ$Bx_rr;C3|`K{POYI#lN?|Ry&tIMb}ar=43(G&Y2?r<&U%I zW&JJ&VR}$x4FYKx2y7+Z> zD}2yPVoJ^XnYZXAa3ljjIrH+I_d+p1Lh@e9$78hhA+0SP4@nHH(#v%0l&*W@?D7rR z^S3@P?kd$w@yRFI#S#Yf1?=kO>-P?l91+{PL)fq5HQZAb z?x+{|1y+Z68J5%dYa|O*SU7~}`P3I`)7&MVWmuLdDiyalJ0B|r<|J-$kf1VN`9a|a zh-PJKjx|%RxD`X7eePC(9sxd9fk!tX(j?MGwEF=694Jc#VAMg97S~>p_Hzfj7!k7YE4`$;J2}(Un1i^>PC2HzRt;u-h9^_Z-}eR=>o~ zUlN}fUuW0s8b?XM$Lqsc62s>~vcr#~;K#Z7 zHqYQPR57&xFqrrnvV7z-Y2TKyCAZq}{LVLhByU_2ce{0;k#M5+X}q7;c?z-t^hok2 z`GM512pqAe7qwj?dJ$otVA6%4lF3IJ%^-)K(+CDBeiTxchnNEaH*0)>E`B&1ye*LR&@=T+PYcoARcuvNfCl7wK^LW7>l@Qv2F zqg|IbTXP-27lwCci%*OA13Qh)|ONOMhu%gB6%<&lP{rxs~b%Ir3 zn)q;@f3v(5mVkymfQXxKa~12p?b;kaeX9XTfvv6RbD}fir{S}DPiG?0{{5|V?vTbU z7K&kfo|(9=6{GVqTK~qy>o1ZSM1FzFDgIaKYV=hCjT!AbBAvcQ*y_pc&&$^dFMvLI z{;5hNn{Nqb$n1BCZOH8HDBB^5=mq_dJqu)4%3%3P1nFRdIN4DR*TZH$T8zC4i0$D7Mzz2Z7haDUz53wNNqj8{R*)4g=GrB`VGKp;J* zmv4MhhR!>A4aTU@!0(q|W%s8VScy0emyVae57N0tnPpeRa#IZD} z4XRu8Q;EO}m~p2k%Ei&XEe5Spp|))0c-PU{M3?B9BWF0DbmGP0P`iI}cBsi_Sd~Go zL>qv{t98d5jPwiV&QBg5YUdaCum|WWEX%NN)(Cys>lLNFP`xhdMFpgvXlJ_P*RTc? zoxFVgA^twZ|BF}I-Gk$M$FgVH$R0$mu!DjH{bX4P*S5AD*mjF=VDm&*b?AiKb)LR) zb-Azc9%`aHP>l<^!zHUytV0VUsx=A`$&67$(tbG8qgeaNwj zVF#tAmg^>ohKlZe`@E~e^UL|+ocVoED^c87_K-k6xHWVLL@ieMM`FDPq(59<Ctv=(x^Tx8|BSV|z~ z6&&nXF_cowngi^r0XG9N76g>fWn159J+@g3qO8}}x>9c3SZkcs%4xLN6}p8)6*(E!E zA5(O(48~zi#w44W;i`G@ysPh`6S3P?%>`PvX;vXN%%UtNSz0p{_4z`kin~!526v=7 zWy$xq;>L_Y1j{RoH`??PJAu4nZpgv-JDOCIg3+rL3G{gd@tza34U3;%JHNblafqS0 zLT_Ied6}?q*(FBj{xL>N#_C!m)OHQ6p%bGu(c!2~nwO9EKjyVCQ9$qkpO$<6-2yAoR z2V+nv9c3Do(os2tP7tpEqm4HUsrv&m+FmFox?0Z?rBi%h{6GLsM-~O+-dBh-Z&(%I zua3uq(%vgkMlPm9a*6~OZ2k~>8X92NdDGWeNFQaZ4b%H(jcuCIG^91!vDzb{CeC5d zJBUXX&8Djx+qX7Y8D(zT3?8b6fDoqc-XEHi9wk{WWamu-@6DXp?zO!u#EO~zv=+{f zm)3-l)r>tIPmaWUM86;( zu*4JP3}D#YL1d42UheaiN2{Tk?jmGs;X9ySd`O667!L21gxA6lC1W&YZc0gO^iHwV z58R>1-m`RGEi|l|T+)tf&w25q4AO(RQNSXHA7aNEJ0yv5h!L5&={jx8E(45Q=T4Ta zGCb;o*>$w;-O^{3Q^G6Js|vj7sMsmKi1}KsSY%`=0uGs8B+$&8XPX0UPV@Z#5npa{ z0C0fduzVG=UHsCcOL7GPJUW0wtf6vx&m`=JPkZ98%c3ZYvXuR&E_OA0p%{l~p4QJc zi+0|9t2~`EAy3&PPc?SKq%W~ip2~gM)Y!(jV?hWTtzi1PY`0*rW*x$bLj*luA){6? z>k2f?x(eI6RGWgYss@N7WKx>C*ws88be1P|lt{!Bg;LIlM<7=iIB6DOMefK%IE*5V zH}f9H$*#VTpoUM*@F~p=z?8DqtSJ>`4qZId2!UYhwKX%Kpx1P*Z185bnXWydYcKB6 zwQ}Fkk{HtDh!!_^w2Cdm)r+yy>I!K#I$g^;*;`l(UC4U{+bpny_p)<#dNo&OAvR#w zv8dRpHme$-s;#q?xzKqOH980K2#+gZk={%`8-w^(=iRIjeSpnMUJIO&6JQB3OEYo> zGdax(G|G8`dwb7RIcFARydqiOeM(9dSYF;?dr_=QfZuH}mD*^^#RBBUS#%x+p+JlL zX{eZ~=tF=BOZAM$={Yho($4|9SvU;(Xs`u{K9fwJZ+7ce!Q>|9p}B57+sr%g%7~k2 zckpZ&*i0v{$=JpLn)XnX$ku4XuGjK9Pf|d}*nSw=62Yl-{Gsm*^LNX!l9iM^-8%;c zRpB=1$nxayjjQ=NyO?2a@l60sP|2Ed9q-#|qn$106wsTGK$U$GvludRZ$`=o?07kE z5gHG=D2a+ZY}RngS2Q^b7O&fw_J=N!H~382@D*-I_aFP#0$r8Z?C|akm<6UvP0ckSA(u$3PWWkAx{VLxjN zz(IJ)(B#rWoi|ujgiKd}0T@lnoV-|~-`Jv#E!3_;B4FNhJW*A{M!uIzvaC>6I}5$) zLw6|9W*6g8gJvIqOxrh>=s>A9%EOD-XUBJ?Ep`*Oyfwrb6XWWc)iwNEl?K?g@F%*C*1d?HxX~ryHXJlAS^qB`PqOw>9vugTn z+X19PdHjU(V%WNcz8 z2)ew+Zs~Hlz_Qc&hAzMd8r}d(kE;sl6xiWU_Nq;{##q$Y1>M7QV45P@Q-%1X#78?b zFdwn;l9H0S9pub6w?la{0NQ@)i;asGRrpFc^9l1|6Uh-`lmJ^o2t2;P@! z$fN$ohJ1<5yIr6ZgCX}d>q8`yVCH6Y56CW@tE@$vBt~O}w`|d_Mv&b;Ah!U6`a+#A z$z*QAeC!8<&O@$igrrLf0~n%C`xzvfSTcAFAGd|>OZ zs&V_S-}o?c=tYlMa#&%+X+uls5m9K%2rCx>}b{pO;)X&5);UABi>?n@CNHl9A>QgW`pQP1i&uB|0y0{ z;$nx-Ebv>6Ax)<4wiZXw-}kXIjs{pUf-D3C>Zd*|X`q2@gQ9B9H!>`YB2Ji%@eLt2 za>xzgt79`C1^YF;#1vXOtRUJw)U**%B|F?JElq<$Zn-^go;q@f2h;=(wlM;^F^>$? zW1#={SjTjTN+=?!AHWh;nP!CdxV7B0~FBh;#DLIpg!0Xq!gix#rtLw)JSpyhtkT3VSDR*UShJ{AE5uQ;6_+@wjA3k2rzxcHdHfTRskV`3>k($TDNWo(F20(OvC

W}9fC?smLPl4G4RY@*Wf01B`ANX$jb^G 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%0DBS(_!0j3qQJf*<Z6wP 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 z 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 z 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 z%0Ar=Dl94+luMF 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 z�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

loirMC6D4K=Q?2?EPGPt31 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΅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

NW+hcx*N)y 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¨ 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%0A*NBMcIv9urL}_>n#d5BGs^^MaHXe$$r3hm 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%0DX`?L?5P)&`0TG^l|zGeUd&! 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

&_I7OXePI0G%Q_{g4+#wv&p&Z&_9M<6+ z-Vq$pksR4k9N?&q=ID;$n2zPxj^ntF=Rn7IkP|qe6FD)blvCO%N)kD22Mk#k<-{|;xu)dInA9GPD`hi z)7oj{v~}7!?VS!zN2in1+3DhRb-Fp-ogPk4r)>ErZu`Z@ib0nR{YkTcjB;tX|$ zIm4Y1&PZpJGuj#BjCIC2zxhGMrV_=+1cW3b+$R%ogL0jXP2|v+2ibW z_Bs2V1I|I`kaO5M;v993Imew7&PnH#bJ{uMoORAQ=ba1AMdy-p*}39eb*?$rog2@ogdCm=a=)_`Q!X`{<%rqq;4`dxtqdG>85g1yJ_6CZaO!;o59WKW^yyTS=_8{ zHaEMQ!_DdDa&x0&PK5-#adF6}Zd>vAsd z3a;o%uIwr1=57nOrQ6DF z?Y42-y6xQdZU?uc+sWq?XGdxy6fEa?gn?GyUE?`ZgID|+uZH$4tJ-!%iZnnare6W z-2LtW_n>>oJ?tKFkGjX)U?gjUvd&#}*UU9Fw*WByw4fm#d z%f0R1aqqhK-23hW_o4g9ee6DQpSsW7=k5#lrTfZ#?Y?o}y6@cg?g#gy`^o+6esRCL z-`wx+5BI10%l+;CasRsiyd+*yFPWFzOW~#TQhBMpG+tUSotNIr;AQkOd6~T|URE!g zm)*ec8uYy<6i}T{W z1TWF6UUjdASJSKI)%NOmb-j9CeXoJn&}-y1_L_K2y=Go>uZ7prYvr}} z+IVffc3yk0gV)jPQ3p_J(*vy=qd~bob&|Bm! z_Lg`{y=C5VZ-uwgTjj0x)_7~Zb>4b!gSXM!@SQeIpLbBiI-=flXmE*c`TiEnzFz8n%ILVLR9!c7PpWC)gQw zfn8xY*d6wOJz+1{8}@;HVL#X(4uAvUAUGHffkWXiI2?|EBjG4G8jgWu;W#)RPJk2P zBsdvPfm7i$I33P_GvO>a8_t1q;XF7WE`ST+BDfeXflJ{sxE!v4E8!}*8m@tB;X1e; zZh#x%Cb$`Hfm`7=xE=0*JK-+48}5O7;Xb$@9)JhoA$S-bfk)vncpRR9C*di08lHh? z;W>C7UVs07?-JHG3CKJT{Vsl2znkCP@8S3Kd-=WnK7L=n zpWojf;1Bc%`Gfr-{!o9IKinVTkMu|Rqx~`dSbv;9-k;!4^e6d~{VD!bf0{qtpW)B+ zXZf@JIsROKotNk_pT7R9t-rwMF^f&pN{Vo1h zf1AJE-{J4{clo>hJ^o&QpTFNf;2-o4`G@@@{!#y!f80OepY%`pr~NbjS^u1W-oM~q z^e_3B{VV=e|C)c@zv18XZ~3?VJN{k&o`2te;6L;q`H%f4{!{;%|J;A!zw}@Eul+av zTmPN^-v8i#^gsEZ{V)Dk|C|5a|Kb1ifBC=tKmK3;A4-CfqGTvJN`X?MR46q{gVLgO zC_T!6GNMc3QFL#0q@R0fqr|xCL{te?MpaN%R1H-}HBe1d3)Mz-P+e3H)kh6bL(~X0Momyt)C@I8El^9; z3bjUUP+QavwMQLLN7M;*MqN->)D3kmFVIW$3cW^e&|CBly+J^0piEFUC>N9uDg+gSxF9}A2oi%zLFJ%IP&KF)R1azd zHG^6~?VwIjH>elX4;lmwgGNE)ph?g)XcjaNS_CbFRzd5aP0%)I7qkyL1RaA;LFb@L z&^725bPsw2J%e6B@1Rf6H|Q7i4+aDSgF(UIU`Q}D7#0i|jnXH<%a94;BOqgGIsOU`envSQab~Rs<`9Rl(|D zO|Ujt7pxC91RH}*!RBB~ur=5gY!7w>JA+-p?qE-_H`o{K4-NzegG0gL;7D*ZI2Ifa zP6Q`|Q^D!rOmH?h7n~0+1Q&x#!R6pea5cCVTn}ypH-lTj?ch#uH@Fwv4;};$gGa&R z;7RZ_cosYlUIZ_LSHbJxP4G5&7rYNX1RsM>!RO#h@HO}rd=GvEKZ9Sv@8D1HH~1GO z36qA&!sKC!FlCr3OdX~P(}wB7^kIfDW0)z-9A*i#hS|dGVU93om@CX3<_YtL`NI5R zfv{j$C@dTn35$ls!s20xuw;mZcu0g~NQHFBglx!#d?i2hdsicVXv@v*eC28_6z%m1Hysfpm1hdaWZ;jVCZxF_5j?hE&a2f~Blq402cBs>}(3y+5< z!js{t@N{@4JR6=1&xaSni{Yj4a(E@Y8eR*phd08T;jQp?cqhCY-V5)C55kAxqwsO~ zBzzh^3!jHC!k6Kz@OAhmd>g(C--jQ`C zNuy*@@+d`=GD;Ptj?zSFqjXXFC_|Jn$`oaevP4;5&nckrmmI z6SJoL0 zx<%ci9#PMzSJXS|6ZMVyMg5}z(ZFa>G&mX(4UL9H!=n+=$Y@kFIvNv=jmAaeqY2T( zXi_veni5TorbW}E8PUvWRx~@B6U~k0Mf0Nt(ZXm^v^ZK4Esd5%%cB+1%4k)zI$9I0 zjn+l$qYcr>ycIyw`bjm|~qqYKf+=u&h!x)NQDu0_|Q8_~_^R&+bM6WxvOMfalz(ZlFb^f-DF zJ&m44&!ZR7%ji|~I(iemjowA?qYu%?=u`AL`VxJOzD3`oAJNa~SM)pj6a9_;#gfF5 z#*)R7$5O;n#!|&n$I`^o#?r;o$1=n+#xlh+$Fjt-##tOv>$BM*?#)`#?$4bOX#;_P3BVuHXiqSD9#>Ti99}{9?Op3`dB?e+@OpEC; zBWA{|m>qLsZp@3pm>)y2AQr}=SS(g5RytNDRyI~HRz6lCRxuVAi;pG55@VHOm19+6 zRsSOh9suhG0sw%{wr$(?*68Bo!i$qX+cw+Iwr$(CZQJ&Hn2Kqbjv1JVA|#kyhLu^w1YtQXcB>x1>h`eFUC z0oXun5H=Vaf(^xnVZ*Tz*hp*?HX0j)#bXIrA~qHqhmFT3U=y)P*ko)9HWizOO~+mVb*k)`C zwiVlkZO3+CJF#8ZZfp;>7u$#J#|~fzu|wEl>eR zU>C7V*k$Yrb``sZUB_-U>~th*k|ku_7(eveaC)aKe1ofZ|o2D7yE}N!IR?2@Z@+3JSCnAPmQO+ z)8gsy^mqn5Bc2J*jAy~K;@R-*cn&-#o(s>7=fU&h`SARB0lXky2rrBm!HeR>@Zxw0 zyd+)LZs9iW;4bdr zJ|5s99^o;(CEf~ejkm$u;_dMEcn7>A-U;uFcfq^j-SF;s54}puAB+#dhvLKV;rIxABt8lsjgP_O@dP{(AB&H}$Kw<5iTEUZGCl>LiciC*<1_G? z_$+)jJ_nzR&%@{A3-E>bB78Bv1Ye3T!P@!=K|X@R#^2{5Adt ze~Z7v-{T+fkN7A2GyVntihsku<3I49_%Hl7{s;ey|09wRNr_}cav}whl1N3QCejdT ziF8DIA_I|;$V6l&vJhE`Y(#b<2a%J=MdT*(5P6AwM1G1Fq5@Hos6d_5Cln31R!XFAy|SVctRjVLLy`W5(=Rb8le*gVG@Y22%B&Sm+%Om z2#An~h#1k5XhpOp+7NAtc0_xk1JRM_M06&)5M7CGM0cVG(Ua&!^d|ZceTjZVe_{YJ zkQhV^CWa70iDATWVgxag7)6XG#t`vD0+C3JCB_lsi3!9+ViGZ#m_ke?rV-PL8N^It z7BQQcL(C=S5%Y-!#6n^bv6xswEG3o^%ZU}lN@5kUnpi`uCDsw^i4DX?ViU2M*g|Y2 zwh`Nj9mGyz7qOezL+mB?5&MY)#6jW^ahNzl93_qs$B7ffN#Yc7nm9w8CC(A&i3`L< z;u3M0xI$bdt`XOX8^lfG7IB-nL)<0q5%-A)#6#i{@tAl*JSCnH&xserOX3yrns`IJ zCEgM5i4Vj_;uG zIx;<(fy_u|A~Ta&$gE^GGCP@r%t_`VbCY?>>_~PZJCj|=u4Ff|JK2NmN%kUplYPj(WIwV$Ie;8U z4k8DWL&%}zFmgCKf*eVXB1e;B$ape=OeDvW&W%w267|0iQG(XA-9s- z$nE3~awoZq+)eHw_mca_{p11iAbE&9OdcVRlE=v7lF!KJ zm7dB#Wu!7unW-#PRw^5noytMwq;gTYsXSC(Dj$`fDnJ#a3Q>irB2-bT7*(7qL6xLR zQKhLeR9UJVRi3IqRir9Wm8mLJRjL|QovK0Aq-s&MsXA0$svcFJYCtul8c~g@CR9_Z z8P%L>LB&xRg;NAYQWOO!nqnxH;wYXHD3OvVnSzu;sgy?PltGykqAbd$9Ll9U%BKP< zq#`OtwWL~6t*JItTdEz^p6Wn#q&iWZsV-DksvFgv>Ou9SdQrWpK2%?-AJv~4Kn|HJlnjjig3Vqp2}eJe5EtQe&xc)OczFHIbS`O{S($Q>kgxbZQ1QlbS`% zrshy{sd?0VY5}#7T0||TmQYKnWz=$N1+|h|MXjdRP;04m)OuHu|+Iz%0&j!;LbW7Ki#1a*=+MV+S3P-m%g)OqRxb&H+nTdPF^@o={JzXVi1*1@)48MZKopP;aSs z)O+d!^^y8SeWt!pU#V}@cj^cAlln#drv6ZWsed2|ND7jH>vlo337qlAP>k3@`3!I04N9wfx@5&C<=;!;-Ca52}*&|pbRJr z%7OBr0;mWofy$r?s0ylq>YxUw32K4bpbn@D>Vf*80cZ#sfyST-XbPHv=AZ?L0~o*o z0Z2dr0BFDf7I1(E0uX@&WB`EzRG23R-iR#1KNUi zpgrgSI)YB1Gw1@kf^MKY=mC0yUZ6MV1Nwq~pg$M@27*CgFc<=cf?;4d7y(9tQD8I} z1L8pfNCacSI4~Ye029F^Fd0k%Q^7Pa9n1hT!7MNv%mH)3JTMOTjX* z9IOB;ZehKCmAg00+S#a2Ol` zN5L_09Gn0r!6|SWoB?OSIdC3a02jd}a2Z?ySHU%K9ozsn!7Xqb+yQsNJ#Zg901v?< z@EAM+Pr)C3!RnDMrWsU&^hT`bZ$BiotMr>=cfzM z1?fU`VY&!ilrBaWr%TW!=~8rQx(r>GE=QNAE6^3`N_1tq3SE`1Mpvh6&^75=bZxp0 zU6-y$*QXoM4e3U7W4a05lx{{hr(4i*G)ChzL6bB^1Dd88nx#3Krv+N1C0eE-tC=CXHx|wrPiUX^-~lfDY-1j?pdYR&;B+4c(S*N4KXt&>iVcbZ5E?-IeY}cc**M zJ?UO_Z@LfNm+nXRrw7mj=|S{hdI&v~9!3wRN6;hbQS@kf3>{A=(24X|dK^8ToUT(X;6}^jvx#J)d4cFQgaIi|HlwQhFJ^oL)h%q*u|a={59P zdL6x<-av1pH_@BvE%a7;8@-+0LGPq@(YxtA^j>-&y`MfnAEXb_hv_5qQTiBtoIXLH zq)*YO=`-|M`W$_pzCd53FVUCjEA&&$n32pVW;8Q~iDwd+L}n~Ajv3EPU?wt? zn90l(W-2p{na<2$W-_yw*~}bfE;Emr&n#dTGK-kS%o1iPvy55JtYB6$tC-cy8fGoC zj#b~3w|-OL_lFSC!?&m3S5GKZMM%n{}&bBsC8oM28e zrDdfyMm7_hna#pxWwWu_*&J+6HW!#_CO25dvN5!;w;!Zu}_vCY{QY#fWRI7_f3OR<2ZS%zg_B!9JD4594rPb2!`Tt+NOlxEnjOQ&vk7b>JC+^Cj%O#Z6WK}Z zWOfQWm7T^;XJ@c8*;(vtb`Cq2oyX2+7qAQ2MeJgB3A>bC#x7@9uq)YB>}qxmyOv$Y zu4gx}8`(|lW_Am^mEFc}XLqnW*L+oMp2z!)0#vW%+uqWA5 z>}mE4dzL-No@Xzx7uiefW%detmA%GZXK%1K*<0*w_6~cOy~o~XAFvPEN9<$v3Hy|N z#y)3XurJwH>}&Q7`<8vjzGpwMAK6drXZ8#GmHozkXMeCi*)?Bv*uAm;p%er zxcXcJt|8ZmYs@v_nsUv!=3EOdj>9;dBRG&^Ay`f~la{@eg=AUB8`%njj&a>Kac+z4(YH;Nn0jp5?C1TK*q%Z=m4a}&6U+$3%? zH-($ZP2;9>Gq{=DEN(V8hnvgI94@KyP0e09DCUz4xJ*XHZ+b@_UH zeZB$TkZ;5{=9}hnI7~hg_#kc0$@NM~ae0#nF-;wXccjmkBUHNW&cfJSTlkdg% z=KJt{`F?zVegHp^AH)ylhwwxBVf=7@1V54=#gFF4@bP>CpU98p$MNI&3H(HU5LH-bbm_NcF<&W{l`4jv}{uFL;ex}n18}Q<)87- z`4{|4{uTe4f5X4!-|_GH5Bx{|6aSh2!hhwz@!$C${7?QD|C|5A|Kg9kgtS6BA-#}6$S7nIG7DLRtU@*+yO2Z3DdZAz3weaRLOvnCP(Uas6cP#x zMTDY4F`>9nLMSPe5=skYgt9_8p}bH*s3=qtDhpMFszNoPx==%?Dbx~b3w4CLLOr3r z&_HM?G!hyMO@yXGGoiWALWmPE0T&2?6es}*w7>|gzzMt{2%;bfvH%4|Pz6oU1w$|e zBv^tiID#v9f-eL@C`3X`XeqQ3S_^H2wn96hz0g7ED0C7!3tfb+LN}qi&_n1c^b&dt zeT2S3KcT-cKo}?t5(W!HgrUMPVYo0t7%7YrMhjzvcp*VZ6vhhUgz>@zVWKcem@G^Y zrV7)9>B0%tA;rf^HRE!+|A3ipKj!UN%<@JM(pJQ1D>&xGf~ z3*n{kN_Z{25#9>#g!jS+;iK?L_$+)8z6#%j@4^q^r|?VoE&LJw3jf3;Vp1`gm|RRD zrW8|&sl_y6S}~oNUd$k76f=pL#Vlf0F`JlO%pv9!bBVddJYrrkpO{}PAQlu0iG{@? zVo|Y}SX?Y2mJ~~grNuI0S+Sg0UaTNi6f239#VTS|v6@(2tRdDEYl*eRI$~Y1o>*UO zAT|^miH*f3VpFl1*j#KO#)+7Si-bsuln6vxWJFfvL|zm`QItekgrXv7XvXABQYkn6kCa{#WrGFv7Ojn>>zd&JBgjeE@D@)o7i3KA@&q|iM_=> zVqdYJ*k2qV4ipE8gT*1@P;r?#KW5sdecyWR_QJf@B7N>|) z#cASnafUckoF&c{=ZJH~dE$I=fw)jyBrX=0h)cy~;&O3?xKdmtt`^sbYsGcqdU1oe zQQRbM7Pp97#ckqtafi55+$HW7_lSGNed2!cfOt?mBpw!zh)2a^;&JhWcv3tio)*uD zXT@{kdGUgHQM@Ex7O#j`#cSeq@rHO)yd~Zi?}&HBd*XfZf%s5-Bt90Oh)>05;&btZ z_)>f&z82qzZ^d`wd+~$#QT!x+7QcvJ#c$$w@rU?R{3ZSt|A>FZe^L@Dsgz7gE~SuC zN~xsOQW`0(luk-7WsovTnWW587AdQgP0B9ika9}7q});-DX)}I$}bg=3QC2f!cq~b zs8mcUE|rieNR8A@{Rgfx5m88m26{)IJO{y-{kZMY`q}oy)sjgH{sxLK= z8cL0%#!?fhsnkqrF13*2Buv63LLwzf0un7T5-V{MFA0(;Ns=ry|-PHHc8kUC18q|QMr$=dP=>d-clc_ zuhdWKFAb0eN`s`q(hzB=G)x*UjgUr4qomQ&7%5&#kP@Y_(l}|nG(nmuO_C-{Q>3ZV zG-r|OJ}6B z(mCn8bV0f(U6L+KSEQ@bHR-x^L%J#5l5R_Pq`T5R>Av(pdMG`T9!pQ8r_wX&x%5JM zDZP?jOK+sN(mUzB^g;S4eUd&)U!F@6 zJYAk4&y;7$v*kJRTzQ^6UtSY49yj|WQ@054RyX8IdUU{FqUp^ooln=>=Thod|kdF-;{63x8*zXUHP7TUw$Azlpo2Dltr{9XPb|CE2pzvVyjU-=(Q0+YgIFgZ*CQ^Hg* zHB1B3!gMe_%m6dOOfWOd0<*$wFgwfvbHZFOH_QX`!hA44EC36_La;C_0*k_8usAFM zOTtpHG%N$l!g8=YtN<&*O0Y7l0;|GmusW;(Yr zuoY|#+rYN49c&Lfz>csJ>I4tv0!uovtN`@p`iAM6hYz=3cO91MrRp>P-+ z4oAR|a197;|lmy%n_ zqvTccDfyKGNswvf# z8cI#2mQq`(qtsREDfN{GN<*cQ(pYJtG*y}@&6O5PoPsI1LMWs{DL|nWMqw3B;T1s< z6-kj5s3?l6Xo{{Fim4#QQf$RhT*Xs-B~U^oQesL=rIpfJX`{4N+9~ao4oXL*lhRq~ zqI6ZdDczMGN>8Pi(p%}H^i}#P{gnaAKxL3JSQ(-WRfZ|Ul@ZEFWt1{n8KcB22}+_e zRvD*^S0*SEl}XBEWr{LYnWjuvW+*e2S;}l>jxtx7r_5ItC<~QE%3@`SvQ$~7ELT=2 zE0tBsYGsYGR#~U4S2idcl}*ZKWs9;^*`{n)b|^cQUCM4{kFr#CzVsmY2}P^Ryn7fS1u?Ql}pNH<%)7uxu#rKZYVdETgq+aj&fJIr`%T_ zC=ZoK%46k;@>F@IJXc;QFO^ryYvqmdR(YqqS3W2ol~2lN<%{xF`KEkVekebcU&?Rg zkMdXfrzTO8s>#&kY6>-_no3Qrrcu+X>D2UU1~sFaNzJTgQM0Pq)a+^wHK&?O&8_B9 z^Q!sO{AvNUpjt>RtQJv=s>RgeY6-QZT1qXgmQl;9<<##FtC`f3BUq1s4otTs`bs?F5qY6~?^#Z+7+R8pl>pwcR%vMQ(Ys-TLh zq{=E(6;)L=RaXtwRFP__w(6*^>Z!gOsG%CEF}0=IN^PySQQNBR)b?rzwWHce?W}fD zyQHu}1I!GO?4pE1y!_?vG2z8`7N*%3^QRCGFHBlX_ zj#J006V!?7Bz3YnMV+cnQ>Uvl)S2omb+$T3ovY4M=c^0Uh3X=8vARTEsxDKPt1Hx% z>MC`$x<*~Au2a{m8`O>JCUvvAMct}yQ@5)-)Sc=sb+@`l-K*|X_p1lggX$smuzEy2 zsvc91t0&Zx>M8ZKdPY5~o>R}O7u1XDCH1m;MZKzCQ?IKx)SK!p^|pFPy{q0+@2d~g zhw3BsvHC=PsyMQlN`bK@LzEj_;AJmWPC-t-XMg6LNQ@^V})Sv1v^|$&* z{j2`dl4wb_WLk19g_crFrKQ%=Xlb={T6!&mmQl;3W!AE2S+#6hb}fgNQ_H30*79h1 zwR~EBt$T6wL4R#B^@Ro1F#Rkdnbb*+Y0 zQ>&%b*6L_=wR&28t%251Yos;SnrKb6W?FNtg%+n_8mZyf)>3Pwwbt5bZMAkt%ufA>!tP9`e=Q%ep-KRfHqJYqz%@FXhXGO+Hh@zHc}g9!@mhkGsEyUe zY2&pC+C*)VHd&jZP1UAp)3q7eOl_7nTbrZJ)#hpQwFTNjZIQNETcR!1mTAki71~N| zm9|=2qpj7}Y3sEO+D2`YwprVvZPm7E+qE6qPHmUATic`U)%I!owFBBg?T~g@JE9%c zj%mlW6WU4bly+J>qn*{xY3H>I+C}Y>c3HckUDd8>*R>nkP3@L;Tf3v()$VEcwFlZm z?UD9ad!jwno@vju7urkhmG)YDqrKJMY45cU+DGk^_F4O)ebv5c-?bmwPwkiXTl=H^ z)&A*8^rU(+J-MDjPpPNUQ|oE;w0b%{y`DkOsAtkM>sj=ydNw_~os9osdNsYeUPG^` z*V1e2b@aM=J-xo(KyRow(i`hd^rm_AWuJqAuyO z4s}IWbxqfGLpOD#Te__~x~qG-uLpXlM|wuvP5dON+n-a+rEchWoSUG%Pc zH@&;wL+`2g(tGQD^uBsOy}v#{AE*z~2kS%hq53d=xIRK3sgKe}>tpnIJwZ>@$Lizs z@%jXPqCQEVtWVLW>eKY;`V4)hK1-ji&(Y`V^Yr=p0)3&rNMEck(UVqrOSstZ&h`>f7|~`VM`kzDwV&@6q?_`}F<#0sWwUNI$F}(U0oK z^yB&o{iJ?MKdqn9&+6y&^ZEt-qJBxgtY6Wu>euw^`VIZ2eoMct-_h^t_w@Vv1O1`? zNPnz9(Vyzi^ym5u{iXg&f33gK-|Fx5_xcC@qy9<%tbftJ>fiM5`Val5{!9O@|Iz>I z|BNI?QX`p>+(==hG*TI4m_6f_DMg^eOcQKOhq+$dp`G)ftzjWR}AqnuIRs9;nyDjAiHDn?bKno-@TVbnBg z8MTc%MqQ(xQQv4_G&C9+jg2NoQ=^&D+-PCM8JK|^gh3jV0Sww;4A$Tb-VhAYkPO*? zhGM9OX6S}tmgrWjL=X~uM8hB4EaWz06_7;}wz#(ZOevCvp#EH;)HOO0j5a$|+D(pY7z zHr5zxjdjL)V}r5L*ko)rwisKDZN_$Ehq2SxW$ZTg7<-L<#(v{~anLwq95#*^M~!2~ zapQz>(l}+DHqIDljdR9%~@z8i= zJT{&fPmO2BbK`~a(s*UOHr^O-jd#X-l|bDFu#+-4p#ubI!x zZx%2MnuW~5W)ZWfSXg)F@ zn@`NA<}>rT`NDi@zA|5%Z_KymJM+Ey!Te}`GC!MN%&+D*^Sk-O{AvC&f17{Izve%b z1SLhuP;!(4r9`PvYLo`0Md?s_lmTT#nNVhw1!YCqPLNbC#K`PRajtpcXge+tu z2f4^YJ_=BXA{0X{Q7hCMwLxuBJJcR^Kpjyh)ERX_T~RmG9rZvxQ7_aR^+A16Khz%$ zKm*YrG#Cv*L(woa9F0ID(I_+;jY07!0VSfbXdD`kCZLIE5}J&rps8pYnvQ0mnP?W8 zjpm@aXdar67NCV_5n7CvprvRTT8>trm1q@Ojn<&GXdPOQHlU4Y6WWZnpsi>d+KzUh zooE-@jrO3uXdl{-4xoeR5IT&Gprhy*I*v}DljsyWjn1I6=o~taE})C(61t48psVN_ zx{hw3o9Gt0jqaek=pMR{9-xQl5qgZCpr_~=dX8S8m*^FGjozTQ=pA~GKA?~26Z(w4 zps(l~`i_2}pXe9*jsBp&=%1CuN@^vul3OXPlvXM$wUx$7Yo)W&TN$j3RwgU6mBq?x zWwWwdIjo#kE-SZ{$I5Hvv+`R7tb$e{tFTqXDryz8id!YDl2$3Jv{l9`Yn8LgTNSK| zRwb*lRmG}mRkNyFHLRLeEvvRw$Es`9v+7$7tcF%2tFhI@YHBsJnp-WbI196Ii?B$G zvVcWfjKx}<#an_UT9PGO&{8bb(k$IFEYm`kW!aWvxt3@7R$zrzWW}tORx7Ku)y8UT zwX@n=9juO4C#$p7#p-Hxv$|V7te#dctGCt1>TC70`db66fz}{vurti#q3>!@|iI&Ph?PFkm|)7Ba5taZ*hZ(Xo1T9>TL))nijb#6n3dTzb2URtlL*VY^Bt@X}&Z+);nTA!@X))(um z_09Tj{jh#ozpUTZAM3C6&rV_|wUgP&?G$!OJC&W*R|`}_3Z|BL%WgP*luDswVT<^?G|>NjoG+O*rZL_ zz@}}+W^K;qZNV08$(C(sE4FHDwr(4?X(QXRZQHS3+p~Q;utPhtV|GismEGEIW4E>2 z+3oEPc1OFD-P!J9ceT6O-R&NBPrH}h+wNocwfouq?E&^cdyqZY9%2u*huOpJ5%x%X zls(!WW5?SGcA`Dj9%qlYC)gA1N%mxWiaphyW>2?g*fZ@}_H28OJ=dOR&$k!Y3++Yr zVta|b)Lv#Uw^!IJ?N#<_dyT!;UT3ejH`p8PP4;Gci@nv}W^cE5*gNfA_HKKRz1QAn z@3#-w2kk@lVf%=E)IMe(w@=t7?Njz?`;2|oK4+h|FW49DOZH{^ihb3-W?#2&*f;H4 z_HFx)eb>Hc-?tyw5A8?xWBZBy)P80^w_n&V?N|0|`;Gn9erLb8KiD7bPxfc~i~ZI9 zW`DPT*gx%G_HX--{n!5IByo~D$(-a)3MZwL%1P~{and^Job*lxC!>?e$?RltvO3wE z>`o3Rr<2Rc?c{OtI{BRZP64N&Q^+ao6mg0=#hl_!38$n}$|>!XamqU7obpZur=nBI zsq9p7syfx2>P`)(rc=wQ?bLDVI`y3TP6MZ*)5vM;G;x|b&79^=3n$LO9NZxs(xDvS z&<^9U4(ITW;E0ao$PRQAM|CtucMQjLkYhQv<2bJ4IldD(p%XbVr=`=%Y3;Oe+B)r= z_D%<~wLuI^CS^P7kN2)641Y^l|z+{ha>J0B4{x$QkSmafUj>oZ-$0XQVUA z8SRX5;++I1(HZNEbH+OpoQcjPXR>&$cJI}4nJ&LU^A zv&32IEOVASE1Z?iDrdE`##!sEbJjZ>oQ=*VXS1`#+3IX_wmUnVoz5<2x3kCD>+Ey( zI|rPD&LQWpbHq979CMC4C!CYcDd)6v#yRVpbIv;#oQuvS=dyFfx$0bVt~)oJo6arg zwsXh1>)dnhI}e-=+*xJlh)ZgMw;o6=3?rgqb~Y29>gdN+fc(aq#$cC)xy-E3}l zH;0?k&E@8H^SF84d~SZXfLqWlxBs>$r8@dTxEUf!ok+_xP9GzZhv=xJJ22E4t9sQL)~HSaCd||(jDcF zcE`B!Zi1WWj&;Ym)j3RMt76D+1=u9b+@_O-5u^ucbB`{-Q(_c_qqGs z1MWfhkbBrY;vRL6xyRiT?n(EQd)htYo^{W;=iLkLMfZ|>*}dXkb+5VC-5c&r_m+Fx zz2n|>@45Hg2kt}nk^9(v;y!hsxzF7f?o0QT``UfuzIETZ@7)jXNB5Ka+5O^vb-%ga z-5>5x_m}(I{p0?1|9MHgq+T*FxtGFA>80{gduhD1UOF$mm%+>EW%4q6S-h-XHZQxE z!^`RA@^X85yu4mMFTYp7E9e#S3VTJoqFyntxL3j}>6P+Idu6<`UOBJ4SHY|3Rq`r( zRlKTRHLto?!>j4l@@ji^yt-aJufEs7Yv?ud8hcH=rd~6zx!1yr^Dqzh2#@qA4|uf4 zc&x{HyeD{~Cwa05J;hT!&C@-@Gd<*4p6xlF>v^8<1zzYyUd(IhwenhfZM?Q#JFmUh z!RzRC@;ZB6ysln1ue;a7>*@9KdV77mzFt4Czc;`e=ne7)dqcdT-Y{>tH^Lj~jq*l& zW4w4T!AtbUdgHwD-UM%=H_4mqP4T9B)4b{43~#14%bV@Z@#cE-y!qY&Z=tuyTkI|I zmU_#)<=zT!rMJpk?XB_Fdh5LP-Ue@@x5?Y=ZSl5x+q~`G4sWNo%iHbk@%DQAy#3w* z@1S?cJM10tj(W$uznS0MZ{f%Jn2-B}Px_P(eA;Jx z*5`cQ7ktr|eA$P-;;X*q>%QTeKJqQ!_8s5#J>T~OKlCF%=C|})`K|pnep|nt-`?-w zcl0~?o&7F;SHGLz-S6S|^n3Ze{XTwQzn|aVAK(x42l<2jA^uQ*m_OVf;g9r3`J??Y ze!QRHC;DUkasGIJfe*b`f z&_CoK_K)~S{bT-d|Ac?iKjokH&-iEkbN+e%f`8G!~(0}AV_MiAq{b&Aj|Aqh3f91dS-}rC+cm8|-ga6V0Ag498pAZ?H?NFQVfG6tD~%t4kQYmhC-9^?pe z2DyUVL7pIQkT1v|6bK3ig@VFCk)UW$EGQn72ucQ}g3>{mplnbsC?8Y^Dh8E;%0ZQ& zYEUhx9@GeG2DO6PL7kv(P%o$-Gzc07je^EOlb~tPENC9I2;u@Pzyl&611bOk9WVhK zZ~-3(ffz`E9Kb*c)IbaLzzEC$1y*1OPT&S!;0Hkv22l_TS_Z9x)gR#N5V05h7CI?f3sll{hdN3oH8O#c12XlhC!MtF8upn3%ED9C}OM<1r zvS4|zB3K!$3RVYeg0;cAV12M5*cfaIHV0dRt--cnd$1$e8SDyn2YZ6O!MM%{1HcS_$4>N=r!%Si3FiV&<%ob)3bA&m= zTw(4oPnb8%7v>KOgayMwVd1bySTrma77t5=CBsr->99;#HY^vG4=aQf!%AW0uu51p ztQJ-eYlJn!T4C+5PFOdr7uF9Ogbl++VdJn#*feYwHV<2baUmAsArX=x6@risnUD>+ zkPn4W45d&GVW@;^sD*lHgl33BE3`u=bVD!n!ypX9D2# zPGRS;OV~B+7IqJNggwJvVeha{*f;DK_74Yy1H(b#;BZJdG#nNV4@ZO}!%^Yra7-8< zCWMLM*l=7pKAaFv3@3$?!ztm^a9TJ$oDt3pXN9xFIpN%JUN}Eo5H1WCg^R-_;nHwf zxIA1Dt_)X&tHU+n+HhUCKHLy)3^#?F!!6;~a9g-N+!5{!cZIvdJ>lMPU${R!5FQK< zg@?l<;nDC|csx82o(xZgr^7Sh+3;L=KD-cK3@?S3!z8KR6)rYLiiCCVCQi?T;KqMT8# zD0h@6${Xd2@<#=tf>EKUa8x8J8WoF*MgO8qMA{ysCHB*svFgd>PHQthEb!aanvMg8a0cWM=heb2#fHDh{%YFKtxAO#711i zM?xeKb*6x<@^to>8x;cho298}*C&M+2gP(V%E>G$a}t4U2|HBchSfsAzOFCW?;| zqQq!yG%gw+O^7B&lcLGdlxS)+Et($9h-OB!qS?`$Xl^twnjbBQ7DkJr#nF;zX|ya_ z9<7L0MysOL(VA#&v@Tj7ZHP8To1)FpmS}6VE!rOKh;~N1qTSJ+Xm7MH+8-T=4n~Kf z!_krGXml((9-W9zMyI0F(V6IMbS^p{U5GA5m!iwjmFQ}8ExI1vh;Bx=qTA7(=x%f` zx*t7=9!8I%$I+AMY4j|59=(WOMz5mR(VOUP^e%cIeTY6rpQ6vvm*{KsE&3k)h<--D zqTkV<=x_8dmL!%mmMoS$mLirimMWGymL`@qmM)e)mLZlgmMNAwmL--omMxY&mLrxk zmMfM!mM4}smM@k+Rv=a|Rw!0DRwPz5RxDOLRw7n1Rw`CHRwh<9RxVaPRv}g~Rw-6F zRwY(7RxMUNRwGt3Rx4IJRwq_BRxegR)*#j})+p9E)+E+6)-2XM)*==c!(w=hh>a%17?Wah495Nw1P_390|5X)XWQO7+qS*Vwqo;=T%7wuXS1_y+qP}n z-g`Z;o>(ueH`WL1i}l0$V*{{(*dS~$HUt}r4a0_G7=~j6Mq(63V+_V(9L8e;CSnpM zV+y8X8m40aGcXggFo@ZhgSnW8`B;F3ScD-ghK;~RVxzFpSOPW%8;gy@#$yw(iP$7; zGByR9icQ0&V>7Us*eq-|HV2!FC1Ufi`Pc$%A+`uxj4i>IV#~1Q*a~bVwhCK~t-;n} z>#+6M25cj?3EPZq!M0-CuD4!LDN0u?U>#yN%t!?qc__``82QA@&G+j6K1g zV$ZPW*bD3>_6mEAy}{mM@38mS2kayE3HywF!M?igM`;Gm<{$l^|BzRIh z8J-+Zfv3b%;i>U7cv?Iio*vJDXT&q%nei-mRy-S?9nXR1#B<@f@jQ55JRhDPFMt=s z3*m+FB6u8L6fcGs$4lTP@ltqcybN9zFNc@ME8rFJN_b_w3SJejhF8aH;5G4Dcx}86 zUKg*2*T);+4e>^JW4sC86mNz%$6Men@m6?iybazKZ-=+XJK*tnN4yi>8SjF3#k=9% z@g8_jycgaZ?}PWn`{DiZ0r)_C5Iz_mf)B-q;lptZ$8iEDaSEq#24`^&=WziSaS4}k z1y^wm*KvRwxQSah#BJQcUEITcJitRd!Vw5$Ph=o65}An1L>3||k&VbsB916Z6eEfgC5VzlDWWt{hA2yvBgzvMh>AodqB2p1s7h2LsuMMcnnW$4Hc^MD zOVlIk6Ag%lL?fax(S&G9G$Wc5Er^yxE21^ghGs*h>^r7Vl>_p(dx*WnK4L#{fH+7TA`TNrh@-?Y;y7`FI7yr$ zP7`N{v&1>#JaK`zNL(T=6IY0<#5LkNaf7%?+#+rhcZj>hJ>ov`fOtqeA|4Y@h^NFe z;yLkxcuBk>UK4MKx5PW*J@J9~NPHqb6JLn0#5dwQ@q_qD{33o6e~7=tKQalKluSk@ zCsU9q$y8)&G7XuQOh=|CGmsg{Ok`#<3z?P7MrJ2-kU7a*WNtDKnU~B*<|hk~1<68W zVX_DrM;0ZEk;TapWJ$6VS(+?EmLyh=z24q9B5!sk*LN+Cvkz~#>f%mNOBZ8noJ9k+;b^;#3K$BvpzkO_ia_Qst=fR0XOcRf(!hRiUa<)u`%J4XP$pi>gi4q3Tlg zsQOd`sv*^gYD_huno`ZE=2Q!+CDn>*O|_xgQthbrR0k@a>PU5>I#XS!u2eUwJJo~g zN%f+7Q+=quR6nXeHGmpO4Wb59L#UzDFlso3Q8-0VBt=m)#ZWB8Q9LD3A|+8WrBEuR zQ91=EgEA?Lf|N};luLP(PX$y+MHHf9)Cg)MHHsQdB~W9ivD7$fJT-xuNKK+9Q&Xs^ z)HG^3HG`T-&7x*gbEvsgA~lbiPc5JpQj4g?)DmhbwTxO$t)Ny?tEkn~8fq=Ij#^J` zpf*yQsLj+CYAdyk+D`4Dc2c{j-P9gxFSU=_PaU8RQirI+)Dh|^b&NVrouE!qr>N7^ z8R{%`jyg|Wpe|CEsLRw9>MC`Ox=!7oZc?|X+teNEE_IK(Pd%U>Qje&|)D!9{^^AH> zy`WxFuc+758|p3fj(SghpgvNcsL#|F>MQk)`cD0zep0`v-_#%KFZGX3LMNq@(aGr) zbV@oEotjQVr=`=;>FEq~MmiInna)CIrL)o5=^S)UIv1Ur&O_&=^U?X~0(3#T5M7uq zLdVfX>0)$ox&&R4E=8B7%g|-%a&&pR0$q`=L|3M(&{gSbbalE0U6Zaw*QV>xb?JI^ zeYyeNkZwdbrkl`B>1K3ux&_^mZbi4I+t6+4c658X107Fyq&v}_=`M6vx*Oe{?m_pY zd(pk=K6GEYAKjlGKo6t`(SzwB^iX;jJ)Fj9oF-_Jrf8aGXqM(^o)&14mS~w)XqDDz zod&c)o3uqk+NK@ar9IlG13IK58qqO&1U-@-MUSQv=rQzIdK^8ToUT(X;6}^jtcTo=4B87tjmoMf75N3B8nFMlYvV&@1Ux^lEwyy_Q}_uctTA z8|h8-W_k;~mEJ~gr+3gh>0R`0dJnyq-be4J56}ncL-b+#2z``3Mjxk7&?o6r^lADG zeU?5)pQkU-7wJp%W%>$zmA*z_r*F_V>09(|`VM`UzDM7uAJ7l!NAzR*3H_9QMn9)t z&@bs%^lSPJ{g!@5zo$RYAL&o@XZj2MmHtM5r+?5t>0k73`Vak={>LO?k}}Dd++lZna9WMQ&0*_iB14kjm)i^s!TPeI#YwG$<$(MGj*7{Og*MP z(|~EnG-4VvO_-)kGp0Gyf@#ULVp=n8n6^wirajYviDx=8otVx{7p5!Ijp@$xV0tpW znBGhurZ3Zv>CX&c1~P+~!ORe5C^L*1&R`7A5Ddvs49zeM%Ww?O2#m-`jLayE%4m$v z0LEZU#$q62GY;c29^*3s6EYElm>4sH8Oe-dMl%V_7-lRpjv3EPU?wt?n90l(W-2p{ zna<2$W-_yw*~}bfE|bX2W9Bmpn1#$DW-+sbS;{P9mNP4umCPz;HM53U%dBJ8GaHzV z%qC_tvxV8pY-6@FJD8o!E@n5ghuO>QWA-x#n1jqA<}h=FIm#Skjx#5ilgugRG;@YI z%ba7*GZ&bP%q8YBbA`FeTw|^?H<+8uE#@|Jhq=q#W9~B#n1{?G<}vexdCELvo-;3) zm&_~XHS>mf%e-UWGas0b%qQkE^M(1!d}F>dKbW7)FXlJ%hxyC=W0SB+*<@^THU*oK zO~s~W)39mTbZmMy1Dlb}#Aar*uvyt`Y<4yWo0HAO=4SJ-dD(nyezpKxkS)X(W{a?K zY*DrtTbwPymSjt@rP(rUS+*Qoo~^)EWGk_i*(z*Rwi;WVt-;o0Yq7Q2I&59G9$TMn zz&2zXv5na#Y*V%w+njB|wq#qet=TqgTecnBp6$TKvmMz^Y-hF$+m-Fcc4vFAJ=tDt zZ?+HHm+i;)X9utY*+J}Jb_hF^9mWo4F&1YDmSicGW*L@cIhJPyR%9hsW))UtHCAT< zYp^D3v5>V{hjm$x_1S<8*@#7Kj2*#_WJj^1*#veBJC+^Cj%O#Z6WK}ZWOfQWm7T^; zXJ@c8*;(vtb`Cq2O=Rb>^VtRLLUs|mm|emyWtXwb*%j`rzUyPMs^?q&C}``H8RLG}=Pm_5QCWskAP*%RzZ_7r=XJ;R=5 z&#~v(3+zSq5__4w!d_*svDeuf>`nF-dz-z(-evEx_t^*RL-rB-n0>-NWuLLn*%$0f z_7(e@eZ#(G-?8u659~+w6Z@I{!hU7HvESJr>`(R=`AhNw}n3GA=onf=kJz z;!<;IxU^h4Erj5b9uPDTs|&8SAZ+X72*nWMYuSw zC|8Ut&XwRwa;3P^Tp6w`SB@*sRp2UemAJ}W6|O2*jjPVp;A(QUxY}GDt}a)PtIsvy z8gh-e##|GwDc6i^&b8oLa;>=5TpO+}*N$t?b>QN;j$9|MGuMUd%5~$qb3M48TraLS z*N5xN_2c?;1Gs_QAZ{=>gd55YhjRo+aui2%499XD$8!QFauO$V3a4@!r*nWa zIFqwD$l093xtz!OT)>4~#33%mjo?Odqqxyr0yl;m%Z=m4a}&6U+$3%?H-($ZP2;9> zGq{=DEN(V8hnveKa`U+P+yZVPw}@NJE#a1O%edv-3T`F0id)UC;ns5Nxb@ryZX>se z+stj@wsPCJ?c5G-C%22+&F$g#a{IXb+yU+&cZfU89pR30$GGF%3GO6!iaX7n;m&gB zxbxfv?jm=IyUbnTu5#D7>)Z|QCU=Xw&E4Vda`(9V+ym|*_lSGUJ>i~m&$#E@3+^TN zihIqy;ofrZxcA%#?j!e!`^mht zFUA+=OYkN6QhaH?3}2Qn$Cu|T@D=$=d}Y20UzM-MSLbW+HThb6ZN3g)m#@dy=Ns@1 z`9^$Wz6sxyZ^k$0TktLUR(xx|4d0e;$G7J@@bP>{z7yY>@4|QGyYb!m9(+%}7vG!j z!}sO;@%{M${6KyXKbRlF59NpP!+DIyd4eZ-il=#oXL*k2d4U&siI;hWS9y)sdB7XI z$y+?+ZQkKs-s62f;6py*5g+46@FV$A{AfObAH$F3$MNI&3H(HU5A8` z=C|-$`EC4meh0sk-^K6d_wal9ef)m@0Dq7_#2@C5@JIP${BiySf094NpXSf-XZdsd zdHw=_k-x-W=CANq`D^@j{sw=Ozs29?@9=l|d;ER=0soMH#6RYr@K5j zzvkcYZ~1rpd;SCek^jVh=D+Y?`EUGp{s;e)|Hc32|L}kLe?k%=sgO)aE~F4r3aNzD zLK-2hkWNT1WDqh6nS{(j79p#UO~@|f5ONB+gxo?NA+L~6$S)KS3JQgU!a@-tPADoA z6N(EZgpxujp|ns&C@Yi`$_o{Qib5r!vQR~+DpV7y3pIqALM@@TP)DdM)D!9p4TOe5 zBcZX-L})5B6PgPxgqA`pp|#LPXe+c6+6x_oc%h@vN$4ze5xNT9gziEQp{LMG=q>aS z`U?Go{=xuZpfE@nEDRBb3d4lq0w&-BA&>$k&;lc{0w?f-Ac%q_$burMf+pw!5DdW- zECC9(;0UhZ3BC{rp%4j3hzTQvk-{ipw2&Z-5ylGRgz>@zVWKcem@G^YrV7)9>B0rzVZN|HSSTzK77I&+rNS~{xv)Z5DXbD!3u}b6!a8BSutC@;Y!WsL zTZFB`HetK4L)atRhwwtBKXc8e&bcmRMV?Bi0q`iS@+>VneZ! z*jQ{LHWizR&BYdCOR<&MT5Kb>72ApJ#SUV;*iq~xb{4ycUBzx`qQ|u-77W;^O z#eQOcaez2b93&1FhloSPVd8KR6LFCcNs$t1kr7#u6M0b(MNtxEQ4v*96Lk@YhG>eG z2t`|TL|61gUkt=hj6@{H#1Z01ag;b(Oc2M2W5sdecyWR_QJf@B7N>|)#cASnafUck zoF&c{=ZJH~L~))tUtAzA6c>q$#UliJQeO z;#P5+xLw>K?i6>4yTv`?UU8qeUpycl6c34q#UtWT@tAmAJRzPGPl>0+GvZnCoOoWm zAYK$NiI>GI;#KjQcwM|9-V|?%x5Yc+UGbiHUwj}w6d#F?#V6uZ@tOEsd?CIRUx}~9 zH{x6Io%mk-Abu1-iJ!$U;#cvT_+9)V{uFm66Iy<)rdb1*xJ`NvbSWk*Z47r0P-)siss*sx8%#>Pq#b`cea_q0~rf zEH#muO3kF^QVXf2)JkeCwUOFN?WFcn2Pt0aD0PxLOI@U{Qa7o))I;hi^^$r^eWbop zKdHYoKpH3wk_Jmdq@mI#L`k&7NUX$3yd+4XBuTQQNUEesx&$OcG9^ob zk}WxsD|wPH1yU$Q5|U!l2x+7=N*XOCNMoe2(l}|nG(nmuO_C-{Q>3ZVG-8x~4Ixk(2 zE=rfA%hDC;s&q}dF5QrBO1Grj(jDopbWgf3J&+zskEF-a6X~h+OnNT8kX}l!q}S3L z>8xO24Gv(jV!s^iNJACzX@Q$>kJsN;#FBT23RU zmD9=T= zmL1uZJ=vE7Ig}$A$uW6^JW?JdkCqeUG4fb>oIGBhAWxJh$&=+N@>F@6JYAk4&y;7$ zv*kJRTscvmC(oA`$P48~@?v?3yi{H$FPB%yE9F)4YI%*kR$eEsmp8~8CUGi>ukGxmjC-0XJ$Oq*^@?rUid{jOrAD2(aC*@P}Y59zNRz4@6moLZ{ zuBj(k_XC*PMJ$PeX5@?-gl{8WA>KbK#~FXdPAYx#}* zR(>bHmp{lKVl1fRfq*2l; z>6G+J1|_4CNy)5aQL-x8lQrK{3S>8|updMdq?-bx>(uhLKH zuMAKIDua~4$`EC!GE5n+U<$4f3aL;EtuP9!a0;&oil|76tSE}AXo{`?#ZXMeQlMfh zj^Zkw;wym?Dv^Sem@+~csfsvJ{}D<_nb$|>cvaz;6;oKwy#7nF<2 zCFQbmMY*b6Q?4sFl$**e<+gH1xvSh$?kf+Jhsq=6vGPQDsytJkD=(Cn$}8oy@lc-76WNLCXg_=@LrKVQXsA<)7 zYI-$;no-T9W>&MPS=DT6b~T5ZQ_ZF3R`aNN)qHAxwSZbsEuPsGQMH&_TrHuN zR7XwVYaBt)Ny^E2)*$Dr!}=np$10q1IGuskPNQYF)LST3>CTHdGs_jnyV< zQ?;4eTy3GYR9mU7)i!EdwVm2t?V!f19o0^1XSIvkRqdvBS9_>E)n00EwU63Y?Wguv z2dD$pLF!<2h&ogqrVdvz6;}zBR4J8K8I@Hzl~)B-R3%kb6;)L=Rab#(sHSSEP_PB^wx>?<#ZdJFb z+tnTFPIZ^MTiv7XRrjg;)dT85^^kg4J)#~}kEzGi6Y5FzlzLh{qn=gIspr)T>P7XE zdRe`qURAHD*VP;9P4$*~TfL*+Rqv_y)d%WB^^y8meWE^9pQ+E)7wSv(mHJwJqrO$& zsqfVf>PPjH`dR&=epSDz-_;-LPxY7jTm7T{RsU&8w4_=xExDFLOR1&OQfq0nv|2hX zy_P}CsAbYJYgx3cS~e}amP5;_<t+du!8?CL@PHV4q(Bid@S|_cu)$V{hHIFHYlKE>ltyce#%i3#Yl0?fk|t}4rfQm|Yd|wJQ?oRv*_xxd zny2|%poLnbAuXnj&_-&bw9#6EHbxt(jnl?!6SRriByF-bMVqQk)23@Pw3*s0ZMHT? zo2w;i^R)Te0&StTNL#Ee(UxkTqxMPrtbNhGYTvZ)+7Iog_DlP%{n7qv|MVn!QazcTTu-5=)KlrH^)z}~J)NFj z&!A`2GwGT2EP7Two1R_Iq36_d>ACehdR{%Bo?kDZ7t{;sh4mtOoL*EfrWe;s=q2@1 zdTG6kURE!sm)9%k74=GbWxa}CRj;O3*K6oC^;&vuy^da2ucz178|V%7MtWntiQZIi zrZ?AH=q>eDdTYIn-d1m?x7R!9@p?zSlipeHqIcE1>D~1ndQZKV-dpdZ_tpF9{q+I* zKz)!tSRbMf)raZBbxg-~LML@fr*%eWbx!AXK^JvNmvu!~bxqfGpc}fWTRPNj-O*j$ z(|tYALp{=w9@9tYBlS`GXgxt6qmR|c>ErbY`b2$_K3SilPt~XC)AbqpOnsIgnOZ8>?a(#uqQeUO7*4OB3^>zAseS^MH-=uHWx9D5-ZTfb7 zhrUzarSI1F=zH~j`hNX@eo#N8AJ&iPNA+X+as7mTQa`1i*3al?^>g}p{epf`zocK* zujp6xYx;HlhJI7OrQg=?=y&yd`hER@{!o9UKh~eQh%kt*5BxF^>_Mv z{e%8d|D=D`zvy4}Z~AxrhyGLlrT^Cd=zsNpAPGncl7ZwP1xN`}fz%)kNDI<|^dJMs z2r_}pAPdL}vVrU%2gnI>f!rVu$P4m;{Gb3R2nvD1pa_TqML{u89FzbhK`BrglmTTy zIZz%{02M(cP#IJKRY5gS9n=6dK`l@l)B$xtJy0Js01ZJS&=@oUO+hoz9JBx}K`YQ2 zv;l2FJJ23<0P&zB=ma{0E}$#u2D*bDpeN`BdV@ZoFX#vQg8^V57z74`Az&yN28II+ z;D7)mpa2aRzyc2NKma0;fD9C%0uAT@00Wr70ub210WR=>4+0Q^2p|vxBfv;73XBE` zU0kz!31)%WU=ElI62Uw$A1nY1!6L92ECEZwGO!%1 z04u>Nuo|oZYr#6O9&7*`!6vX7Yyn%rHn1J+06W1hup8_Fd%-@i9~=M&!69%M905nc zF>oB504KpIa2lKeXTdpe9$Wwy!6k4RTme_XHEX|058ES@EW`UZ^1k89(({F!6)z;d;wp!Fvb{Tjd8|!V}dc!m}E>g zrWjL=X~uM8hB4EaWz06_7;}w8W1cbJSYRwP78#3;CB{-?nX%khVXQP(8LN#o##&>Y zvEJBVY&13*n~g2TR%4s7-PmF5Gao)IKTr@5jmyIjNRpXj*-MC@gG;SHUjXTC&tD4o!>ShhIrdi9ZZPqdC zn)S^3W&^XK*~n~cHZhx;&CKRz3$vx!%4}`6G25E$%=TsnGv4fIb}~DgUCgd#H?zCh z!|ZAHGJBhS%)Vwnv%fjO9B2+Q2b)98q2@4ixQUs#NtmQbnY786tjU?YDVU-unX;*v zs;QZ}2~5K@P0NI)Z91lFdZup%W@ttxGGpclbEG-S9Bn3;W6ZJUICH!?!JKGLGAEl; z%&F!ybGkXhoN3N7XPa}(xn`m{&zx^AFc+GO%*EysbE&z^TyCx~SDLHL)#e&=t+~!z zZ*DL*nw!kc<`#3Sxy{^e?l5Px6Iq-9rLbv&%AFwFdv$a%*W;v^QrmF zd~UulUz)GX*XA4Zt@+M;Z+yI&q`t?wUSxM ztrS*DE0vYnN@Jz9(pl-P3|2-fla<-ZVr8|mS=p@|R!%FImD|c=<+buz`Kj|(W+!swyIcFt!h?vtAR5HH zdRBd_fz{AzWHq*$SWT^FR&%R`)zWHZwYJ(=ZLM}zd#i&LZ*{afS)HveR#&T=)!pi0 z^|X3fy{$f0U#p+h-x^>Iv<6v&ts&M>YnV0M!Yte(EYhMZ+F~r$;w;`0EYXrI*-|Xk z(k$HqmSLHeWkJif9Lu#l%eMk6v?2>xF>8c1(i&xrwi2u{)>vztHQt(FO|&LildUP% zRBM_w-I`&|v}ReetvS|QE76)~&9@d<3#~=gVrz-D)LLdOw^mpytyR`)YmK$mT4$}d zHdq_2P1a^>i?!9-W^K22SUas<)^2N$wb$Bb?Y9nC2dzWaVe5!>)H-Gzw@z3mty9)% z>x^~QI%l1?E?5_>OV(xUignexW?i>#SU0U()@|#Kb=SIQ-M1cC53NVmW9y0a)Ouz; zw_aE;tyk7->y7o+dS|`2K3E^EPu6Gai}ls|W_`DQSU;^_)^F>N_1F3blfa}f8B7jS zz?3i*Obye(v@jh^4>Q1wFcZuSv%st{8_W)Kz??7_%nkFvyf7ck4-3G8un;T^i@-Qo z6c&TUVF_3gmV%{W8CVvUgXLibSP@o&m0=ZF6;^}QVGURl)`GQR9atCEgY{tp*bp{? zjbRhm6gGp+VGGz2wt}r;8`u`MgY97l7!NzbPOvlV0=vR)usiGld%|9@H|zuZ!hWzn z8~_KxL2xh}0*At3a5%&u4hcv?3eu2)EaV^$1t>xZ%20tS)SwOlG@uDB2%!xf=t2+r zFn}S9Ac8SC0*-{E;Aoft$H1|092^fPz=?1YoD8SHsc;&c4rjoba2A{m=fJrz5zd42 z;R3i2E`p2U61WsDgUjIxxDu{{tKk~B7OsQq;Rd)7Zi1WP7Pu8|gWKT_xD)PzyWt+V z7w&`m;Q@FM9)gGA5qK0HgU8_scoLq1r{NiR7M_FW;RSdRUV@k56?he1gV*5=coW`& zx8WUl7v6*S;RE;(K7x?vE@79nOWCFEGIm+JoL%0oU{|y&*_G`o zc2&EYUEQu>*R*Tdwe31~UAvxL-)>+xv>Vxt?Iw0pyP4hGZeh2yTiLDcHg;RPo!#E< zV8`1X?M`-QyNlh`?q+wld)PhgUUqN0kKNbqXZN=U*aPiB_F#L6J=7j%54SNJw+WlH zDVw$#o3%Ncw*_0YC0n)?TeUS?w}EZgrfu2Kwr$6DZO``Yzz*%mMs~~|VUM&&*`w_Q zdyGBS9%qlYC)gA1N%mxWiaphyW>2?g*fZ@}_H28OJ=adO=h^e^1@=OFk-gYnVlTCq z*~{$}_DXw|z1m)5ueI0N>+KEpMthUJ+1_GrwYS;Z?H%?`dzZc2-ed2z_u2dH1NK4t zkbT%bVjs1S*~je@_DTDcecC=_pS91~=j{vjMf;L{*}h_5wXfON?Hl$@`<8v%zGL6D z@7ee52lhkzk^R_yVn4N?+0X44_DlPf{n~zGzqQ}l@9huvNBfig+5TdGwZGZl?H~3} z`7;T}J87J>PC6&OlflX8WO6b)S)8m+HYdB2!^!F7 za&kL)oV-pxC%;p`Dd-e(3OhxdIH#yn%qi}aa7sF*oYGDir>s-XDeqKpDms;%%1#xh zs#DFW?$mH)b@Y43D! z;+>98C#SR1#p&vFbGkb{oSsfEr?=C`>Fe}!`a1)hfzBXjurtIN>I`#+JD7tzghM)% zLpzMaI-J8hf+ISTBRh(tI+~+9z%d-tu^i~wj^ntF=lD+GgihojC+3WBMmnRM(N2Oh z#u@93bH+OpoQcjPXR>m)k!ocYcIXQ8vmS?nxvmO9Iv z<<1IcrL)Re?W}RuI_sSE&IV_rv&q@)Y;m?a+nnvr4riyc%h~PharQd#vDYq&MtT5fH(j$7BQ=hk-{xDDM#ZezEJ+th95Hg{XNE!|dbYqyQt)@|pucRRT8 zZb!G1+u7~nc6GbC-Q6B;Pq&xb+wJ4_b^E#f-2v`EcaS^S9pVmkhq=RD%*9>8C0)v; zUB+cy&gEUf6{Nu&$wsZbMATff_u@uJ{^fdnLS*UMa7%SH>&rmGjDb6}*aGC9kqq#jEO7 z^QwC_yqaDuueMjmtLxSC>U#~mhF&ADvDd_F>NWG4do8?{UMsJ)*T!q>we#A09lUt2 zqu0sn>~-KIE%Fw7OT4AtGHY9mytUpsZ@ss{+vsibHhWvVt==|oySKyJ>Fx4%dwaaS-ac=?cfdR79r6x) zN4%rnG4Hr{!aM1m@=kkaytCdp@4R=xyXal=E_+wJtKK#5x_867>D}^fdw0CM-aYTW z_rQDTJ@OuVPrRqzGw-?g!h7kx@?Lvyytm#v@4ffI`{;f0K6_uhuiiKByZ6KU>HYG4 zdw;yY-akKypVUw0C-+nMDg9J_YCnyi)=%fB_cQnz{Y-vlKZ~E$&*o?MbND&^Tz+mp zkDu4i=jZne_yzq!eqq0eALkeKi}}U<5`Ia)lwaB}tNk_pT7R9t-rwMF^f&pN{Vo1hf1AJE-{J4{clo>hJ^o&QpTFNf;2-o4`G@@@ z{!#y!f80OepY%`pr~NbjS^u1W-oM~q^e_3B{VV=e|C)c@zv18XZ~3?VJN{k&o`2te z;6L;q`H%f4{!{;%|J;A!zw}@Eul+avTmPN^-v8i#^gsEZ{V)Dk|C|5a|Kb1ifBC=t zKmK3;Uyvk78YByn2PuM-L8>5ikS0hQqzlpq8G?*KrXX{WCCD0N3$h0}f}BCFAa{@_ z$Q$Gf@&^Tif@ju$_C|v@v1C}f1XdSc(+6L`{_CbdrKIj;9 z3OWZ}g04ZgpnK3G=o$11dIx=izCpjBe=r~z7z_#q2Sb9P!LVR>fCYF!1Y|%3bif2` zzy*9D1Y#fsa-alipapsW0wXX3D}aF=IDs2@fgc1x7(@XIV!?=DWH2fi9V7%}g0aE4 zV05h7CI?f3sll{hdN3oH8O#c12XlhCL1Hj3m>(<%76yxg#lezbX|OC<9;^sf z2CIVA!J1%gur631YzQ_6n}W^3mSAhJE!ZCH2zCa$g5ANMU~jN5*dH7S4hDyU!@-f@ zXmBhz9-IhH2B(74!I|J}a4t9>TnH`(mx9Z|mEdY{Ew~=s2yOxgmuGuVg0Z{*f4AqHV&JFO~Ynk^RPwOGHeyL4%>un!**f&utOLhb__d( zox?6+*RWgIJ?s(o410yW!#-i(uwU3e91so+2Ze*fA>q()SU5byLOdiwGNeK}WI{IN zLOv8iF_c0%R6;e>LOleb5t^YD!q5(#&<(xN4}&laqY#C$a6~vV92JfZ6T&g!*l=7p zKAaFv3@3$?!ztm^a9TJ$oDt3pXN9xFIpN$eF`O694;O?B!$slZa7nl{Tox`5SA;9W zRpIJzO}I8(7p@OCgd4+6;pT8lxHa4sZVz{aJHuV!?r=}IH{2KQ4-bR~!$aZW@JM(x zJQf}gPlPAKQ{n0GOn5dt7oHC|<%#k} z`J(($fv8|qC@LHkiQ=N7QL(6aR3a)Fm5NG7WumfCxu|?pA*vWviYiA{qN-7~sCrZ* zsu|UaYDaaVx>3ETe$*gp7&VF-M@^!pQM0Id)FNsbwTfCtZKAeOyQqECA&QSWMxCP0 zQJ1J|)Gg{B^@w^#y`tVxpQvxtFX|r+hz3T3qQTLSXlOJn8XjR09uW~4Q4t+65gTz4 z9|@5dNs$~Wks4`{9)ZY+%*cvhWJgZqMqcDcK@>(&grZn9A{rTuibh8X(U@p#G%gw+ zO^7B&lcLGdlxS)+Et($9h-OB!qS?`$Xl|4k&5Pzo3!;V5qG)lnBw89RinFI(XHrqbSJtS-HYx=52Ai=Ia>qL-bMz(p8hwktM?a#U(XZ%t^e6fo z{X$YVlndoXc~D-I59LP% zP(f4(6-Gr+94d;6q2j0nDv3&=(x?n7i^`$$r~;~pDxu1#3aW~#q3Wmxs)=f$+Nchy zi|V2Jr~zt-8llFh32KU(q2{OsYKdB*)~F3?i`t>~r~`^e9Z@IL8FfKjQ8&~b^*}vQ zFVq|LL48p_)E^B%1JNKf7!5%~(J(X|VF*VAA`yjX#2^-Nh(`hvk%VNVAQfpyM*tbf zL>7X`MhcQ_xg24NXTg&`dN7 z%|>(3T$G6Bq4{V5T8I{*#b^myik6|}Xa!n{R-x5s4O)xVq4j73+K4ux&1ehSingKc zXb0MfcA?#9588|Nq5bFpI*1OT!{`V)ijJY<=ma{6PNCE23_6R>q4Vehx`-~J%jgQa zimsvS=mxrpZlT-g4!Vo(q5J3odWasO$LI-qik_k8=mmO-UZL0M4SI{-q4($m`iMTE z&*%&KioT)m=m+|Vexcv!5BiJ##gfF5#*)R7$5O;n#!|&n$I`^o#?r;o$1=n+#xlh+ z$Fjt-##tOv>$BM+_Vnt)cV#Q-6VkKjxVx?nc zVr66HV&!8MVijYRVwGc6VpU_+V%1|cVl`v6Vzpy+Vs&HnV)bJUVhv-BVvS=>VohVs zV$EYMVl88>Vy$CsVr^sXV(nucV)3z#u}-nhu`aQ$v2L;M{|SNzz`B6|0HE7Cv$NUR zw%ysbZJY7Mx;U5Q&$eybwr#$Lb;G)2J+PiwFRVA#2kVRV!}?2*dA;zwh!Bn z9l#D^hp@xg5$q^-3_FgUz)oVPu+!KX>@0Q;JC9w!E@GFk%h(m{Ds~OKj@`g+Vz;o{ z*d6RHb`QIcJ-{AfkFdws6YMGW4112fz+Pgnu-Did>@D^Vdyjph(1)dU5g{Q{T;A!!6czQeoo)OQ4$K#ptEO=Ht z8=f7{f#<|?;kofVcwRgoo*yrO7sLzUh4CVI0$vm^h8M?6;3e@=cxk*0UKTHhm&YsM z74b@VWxNVr6|aU@$7|p<@mhFoybfL$uZP#i8{iG`MtEbq3EmWMhBwDs;4Se)ycOOW zZ-ckR+u`l;4tPhr6W$r`f_KHc;ob2bcu%|+-W%_O_r?3+{qX_#KztBB7$1TU#fRa; z@e%k)d=x$!AA^s@$Km7g3HU^O5a(;4IGJJTBlOF5xn+;3}@+Iu3CIH*pKMaR+yC5BG6|2Y84_ zcnn{LFUMElEAdtMYJ3g87GH<2$2Z^`@lE(*zlLAOZ{RoaTlj7K z4t^KEhu_B^;1BUf_+$JD{uFl0+$@G*N~qOOzwZ6BUSx zL?xm!QH7{VR3oYrHHex-EuuD2hp0=`BkB_kh=xQXqA}5gXi79AniDOEmP8`aifB!= zA=(n{i1tJWq9f6X=uC7Wx)R-p?nDowC((=OP4pr968(t&!~kL-F^CvU3?YUR!-(O; z2x25LiWp6dA;uEpi1EY(Vj?k#m`qF|rV`VL>BJ0TCNYbcP0S(Y67z`p!~$X=v4~hq zEFqQ>7=aT6K@t=}6AS?emf#4U5D1Zw2$@g_mCy*CfP_JqghkkdL%4)T_yi&XA|xUr zMl2(i6Dx?7#42Jnv4&VntRvPF8;Fg>CSo(Oh1g1LBeoMeh@HePVmGme*h}mq_7ew) zgTx`?FmZ%9N*p7O6DNq1#3|x5afUccoFmQ?7l@0*CE_x1g}6#wBd!xSh?~SM;x=)I zxJ%q4?h_A)hr}b|G4X_WN<1T;6EBFD#4F-8@rHOyyd&NdABc~{C*m{lh4@N*Bfb+q zh@ZqS;y3Y!_)GjFlaNWtWMmwfoJ>KcBvX;8$uwkIG98(o%s^%&Gm-IRW-<$zmCQzF zCv%WF$y{V^G7p)T%tz)Y3y=lLLS$jG2$?_@C5w^8$r5BqvJ_dGEJKzh%aP^D3S>pH z5?Pt7LRKZKk=4l>WKFUbS(~gw)+Ota^~nZgL$VRsm~28eC7Y4W$rfZwGLdXWwkF$< zZOL|Id$I%Bk?cfvCcBVb$!=tKvIp6d>_zq_`;dLfeq?`g06CBxL=Gm0kVDB~|+^^@)7x%d_q1YpOMeW7vxLw75SQcL%t>7k?+Y5sxVcAN}!5T#i-&`392MjiYiT&p~_O_sPa?=sv=d1 zs!Ua(s#4Xc>QoJ?CRK~7P1T|5QuV0%R0FCZ)re|LHKCeP&8X&73#uiRNVTF`Q*EfW zR6D9Y)q(0rb)q^`U8t^9H>x|;gX&53qIy$(sJ>J`sy{V=8b}SI22(?*q0}&HI5mPA zNsXdLQ)8&H)HrH9HG!H)O`;}KQ>dxbG-^6EgPKXrqGnTbsJYZUYCg4qT1YLT7E?>8 zr4&Zt6hV;`MbQ*P0g9zKil+ohq$EnF6iTHuN~a)YP$p$jHsw$*B6m zsO8iOY9+ObT1~B?)>7-J_0$GxBejXzOl_gIQroEQ)DCJVwTs$K?V6fY0qP)i zh&oIip^j3=sN>WL>LhiFI!&FS&Qj;7^V9|EB6W$nOkJU_QrD>K)D7w;b&I-9-J$MM z_o(~S1L`65hLvAxdQH8d-cs+V_tXdKBlU^;OnsrgQs1cW)DP+> z^^5vV{h|I+|L7!iQaTwOM<=IK&?)IubZR;cot92Vr>8T}8R<-PJe`@<9D3(b?%7 zbWS=Kotw@>=cV(}`RM|5LAnrKm@Yym&_(HDbaA=_U6L+Em!`|mW$AKsdAb5!k*-8n zrmN6Z>1uR!x&~d7u0_|T>(F)SdUSod0o{;pL^r0J&`s%PbaT1|-I7kEThXoQHgsFM z9o?SpKzF1&(VgiobXU3?-JR}1_oRE#z3D!5U%DUNpB_LDqzBQ1=^^w`dKf*N9zl2>sadIP2vgX`T~8CzC>T9uh3WNYxH&c27QyhMc=0H(0A#3 z^nLmP{g8e{Kc=71Pw8j$bNU7Sl7238&d`UCxu{zQMKztCUlZ}fNi2mO=& zMgOM%(0}QFOcEw3lZ=UDk~1loluRlnHIs%(%cNt{GZ~nSOeQ9t$;@P7vNGA2>`V?O zCzFfG&E#S7GWnSNOaZ1KQ-~?d6k!sWqD(QSI8%Zt$&_MBGi8{vOgW}JQ-P_-RAMSK zRhX(wHKsaKgQ>~XVrnyWn7T|orasevX~;BU8Z%9prc5)YIn#n^$s{tZnAS`irY+Ns zY0q?EIx?M@&P*4kE7OhX&h%htSd7g$jLUe8&mbmXLMCEj%ra&< zvw~U4tYTI(YnZjnI%Ykyf!WAxVm32dn61nopn6Jz?<~#F)`N{lZ zelvfVzsx_71SAE?KpaR8Qh<~o6-W)zfV3bTNDnfAj35(;2bn<@kQHPD*+CAF6XXK9 zK^~A7VkTpK4<_Mf<~Y*XabsoW}rD}0a}7Y&}paBB_U;zhsAOI0a zKn4m>fd+H{fdNcl0UJ2L1s?DL0s#m?1Y%$rSPoWzm0%TE4c36QU>#TwHh_&_6W9#4 zfURH~*ba7ponRN(4fcS&U?12I4uFH;5I78ufTQ3TI1WyLli(CM4bFhG;2by)E`W>R z61WVmfUDpdxDIZBo8T6>4eo%u;2yXS9)O475qJ!qfT!RYcn)5Gm*5q64c>sa;2n4m zK7fzl6Zj0ifUn>i_zr%6pWqkx4gP??;2)cWP0A)?Ha0t(gU!k2Vso>3*t~2$Ha}Z{EyxyP3$sPo1hyz!j4jTVU`w*4 z*wSnnwk%tYEzee9E3%c?%4`+3DqD@M&emXSvbEUSY#p{PTaT^JHeegFjo8L)6SgVa zjBU=gU|X_@Y%8`k+lFn+wqx6~9oUX+C$=-&h3(3AW4p6G*q&@Jwl~{{?aTIK`?CYs zf$Si5Fgt`D$_`_Pvm@A%>?n3LJBA(0j$_BO6WEFDBz7`8g`LVyW2dt-*qQ7sb~ZbQ zoy*Q+=d%mgh3q1BF}s9a%3>_e5-iD5EX^`3U|E)9c~)RWR$^sVVO3URbr!M)YqAz= zvkvRB9_zD+4cL&4*ciKvUCypxSF)?v)$AH}ExV3g&u(BhvYXh=>=t$_yN%t>?qGMa zyV%|A9(FIgkKNB6U=Ol~*u(4*_9%OdJ=pJZ zdyT!$-e7OCx7ge49riAJkG;=6U>~xN*vIS>_9^>}ea^mMU$U>**X$eiE&Gmr&wgM( zvY*(`>=*Vc`;Gn1{$PKyzu4dGANDW%k4wTO<&tr6Tyic2my%1xrRLIbX}NSYd4xw$-CUM?S(pDVx>+Ho-MJoIPp%i&o9n~%<@#~`xdGfj zZV)$^8^R6chH=BW5!^^_6gQe1!;R&}apSoO+(d2?H<_ElP35L>)43VkOl}r8o14SU z<>qnoxdq%pZV|VbTf!~nFb?Mkj^rqg<`@oeEXQ#?CvYMsaWbcHDyMNe2RVZ?Ig7J7 zhjTfP^Et!?T*yUSj9bPn=T>klxmDb1ZVk7VTgR>EHgFrcP26T~3%8Zq#%9=T2}Zxl`O}?hJR9JI9^pE^rsQOWbAd3U`&e z#$D%ba5uSI+->d-cbB`z-RB-~54lI&W9|v}lzYZK=U#9xxmVn4?hW^rd&j-!K5!qo zPuyqj3-^`##(n30a6h?U+;8p=_m}&}C*hOw$@n-vIiG@0$*1B|^J)0Bd^$copMlTF zXX4}e%zPF;E1!+e&gbBB^11ljd>%e8pO4Sa7vKx>h4{jJ5k7%0$`|8{^CkF_d?~&( zUxqKsm*dOx75IvLCB8CWg|Et2+=oxhI}KwG2eu5$~WVi z^DX$6d?Me9Z_T&i+w$%B_IwAvBj1Vd%y;3t^4<9Ed=I`S-;3|f_u>2U{rLX;0Dd4p zh#$-k;fM0W_~HBrek4DNAI*>9$MWO&@%#jSB0q_r%unH`^3(X~{0x33KZ~Eu&*A6t z^Z5Dv0)8RCh+oVv;g|9lkMjgi@)S?=3=epg=XjnMc#)TQnOAs~*LaW_>KG~elx#?-^y>}xAQyro%}9- zH@}D9%kSg&^9T5Y{2~4@e}q5EALEbnC-{^6DgHEnhCj=n24{{xW}szsg_Z zuk$zfoBS>QHh+h|%irVg^AGrk{3HG`|Ac?aKjWYCFZh@IEB-bAhJVYycT2 z{xkoD|H^;kzwFW z3GqT^A&Zbz$R=bLatJwvTtaRkkC0c$C*&6j2nB^gLSdnZkRTKliV4Mq5<*F#lu%kI zBa{`&3FUJn6Lxo|&aAAZnQWzzS7RCr;g>k}oVS+GGm?TUVrU+AoX~J}2hA>l@CCnD)2y=yb z!hB(YuuxbeEEbjsO9f281wtSNN}vTs00Jv;0xt-HC`f`VD1s_zf-XS85KO@mY{3y+ z!4rG|34ss_kq{G>3Co2Q!b)M4uv%CntQFP?>xB)%Mq!h%S=b_M6}Ac6g&o39VVAI5 z*dy!}_6hri1HwV!kZ@QyA{-Tt3CD#K!b#zja9TJcoE6Rq=Ye}B0LqI3D1QW!b{Ma(K@6SIps#GGO-F}IjU%q!*-^NR(JF&gkLF_1Y5<81s#I9mDvAftq>?!sVdy9RCr)rMOC5Ev^yQitEJn;s$Y}xJleBZV|VN+r;hS4soZrOWZB) z5%-Gw#Qov{@t}A}JS-j&kBY~{Y zQc9FsNv)+eQd_B=)L!Z!b(A_uouw{PSE-xSUFsqAlzK_Mr9M($sh`we8Xygn21$dZ zA<|H3m^54(A&rzqNu#AP(pYJnG+vq@O_U}{lcg!rRB4(tU78`ylx9h@r8&}EX`VD+ zS|BZy7DWUNQshYiIITBN}R+?f+R|kBuk2S|zQP)<|omb<%oigS1iFByEESe(8X8P&y4bDrIwhT!&PZpabJBU~f^<>3Bwd!SNLQt6(sk*E zbW^$|-Inf1ccpvMed&SpP4o%CdL_M<-binychY<5gY;4QBz=~? zNMEII(s${H^i%pJ{g(bnf2DtN5;>`yOpcS2%PHiPaw<8soJLM7r<2pm8RU#|COKZt zEN79k%Gu=Xat=ACoJ-Cv=aKWu`Q-d^0lA=DNG>cFkrU*iaxuBMTtY4>my%1%W#qDQ zIk~)CL9QrQk}Jzq3Kt|`}&Ys+=yx^g|azT7}=C^wQD%T45_ax=NP+(K?C zC(5nl)^Z!Ut=vv-FL#hT%AMrSau>O)+)eH-_mF$az2x378?q@|vMoEZD|@mp zBRP;mIg(@YGI_bYLS8Aal2^-Xv zDc_QB%Xj3v@;&*!{6KyvKawBIPvocaGx@pvLVhW~l3&Yj{wRNvKg(a_ zukttfyZl4`DgTmx%YWp*@;@bsl2l2i#3{*@6iP}Zm6BRXqoh^RDe09AN=7A<60c-d zvM5=VY)W<|hmuptrQ}xfD0!89N`9q)Qcx+R6jq8T2})6=m{MFRp_EigDW#P%N?E0x zQeLT`R8%S{m6a+=Ri&CzU8$keRB9=;l{!jYrJhn>X`nPz8YzvHCQ4JKnbKTop|n&I zl~zh?rH#^7X{WSTIw&2LPD*E`i_%r;rgT?&C_R;4N^hl)(pTxH^j8Kb1C>F_U}cCh zR2ilWS4Jo!l~KxQWsEXb8K;a_CMXk?Ny=npiZWH1rc766C^MB=%4}thGFO?W%vTmD z3zbF6Vr7Z4RKXNnArw-f6k1^vps)(3@QR>_iloSjqNs|d=n7N}#Z)ZCRvg7uJjGX# z5-6b(DKTZ4vRqlAtW;JhtCcm%T4kNGUfG~*R5mG_l`YCvWt*~H*`e%Ib}74+J<48X zpR!*$pd3^VDTkFK%2DN*a$Gr~oK#LJr>E^Ub&!LR4yr(l`G0s<(hI`xuM)t zZYj5wJIY<os1{NSt3}iVwWwN5Ev}YOORA;R(rOvCtXfVj zuU1eis+H8rY8ADrT1~C4)=+Dzwba^b9ks4nPpz*uP#da^)W&KPwW-=nZLYRZTdIj_ zE48)SMs2IMQ`@T@)Q)N=wX@nq?W%TDyQ@9ao@y_(x7tVTtM*g-s{_=5>L7KnIz%0+ z4pWD#Bh-=VD0Q?tMjfk;Q^%_l)QRdOb+S4|ovKb#r>is6nd&TcwmL_htIkvBs|(bH z>LPWqxZ?c% z)KHDon7T||uC7p5s;ku1>Kb*ex=vlMZcsO>o7Bzf7Imw-P2H~UPKXN{dQLsBUQjQpm(KpZ~`c8eXeo#NEpVZIl7xk<9 zP5rL^P=Bhw)ZgkK^{@I*OQI#!l4)^TaxI0HQcI><$wbj~b?X?bCN3E0AS?i*8)w*fjwH{het(VqY>!bD6`f2^O0op)qkTzHwq7BuC zX~VS<+DL7bHd-5_jn&3!ZI(7$o1@Lu=4tb_1=>Pw zk+xV{qAk@h4c7>b)F_SC7!7Ew#%a7JXrd-*vZiRNrfIqcHA6EsOS3gcb2U%%HKYYv zs6|>#Tc$17R%k1=RoZH8jkZ=>r>)mEXdAUn+GcHwwpH7vZP#{aJGEWfZf%dYSKFuU z*A8e0wL{ur?TB_%JEk4iPG~2!Q`%|mjCNK#r=8a>Xcx6h+GXvEc2&EkUDs}CH?>>Z zZS9VBSG%X(*B)pOwMW`x?TPkOd!{|tUT811SK4dsjrLZ1r@hxcXdkst+Gp*H_Er0) zeb;_yKeb=lZ|#rvSNo?Y(Ua=Q^f*1aogX(&P2adKNva zo=wlL=g@QNx%Aw69zCy~PtUIx& z^on{Vy|P|Kuc}wmtLruNntCn0wq8fCtJl-(>kagVdLzBD-b8PzH`ANzE%cUpqTWhx zt+&zJ>h1LQdI!Cu-bwGQchS4*-SqBy551?}OYg1s(fjKC^#1w)eV{%_AFL12hw8)h z;ra-Dq&`X?t&h>i>f`kB`UHKVK1rXfPtm99)AZ^341K0POP{UJ(dX*(^!fS%eWAWc zU#u_Dm+F{~>x53~luql64s=%MbY2&9QI~XCS9Dd^bX|wKp_{s;+q$E>x~Ka((gQuz zBR!@s)0gWj^p*N5eYL(uU#qXv*XtYfjrt~iv%W>&s&CV`>pS$F`YwI9zDM7y@6-3| z2lRvbA^os^L_ew@(~s*X^ppB2{j`2YKdYb9&+8ZTi~1$~vVKLss$bKu>o@e9`Yrvo zen-En-_!5w5A=unBmJ@dM1QJ3)1T`v^q2Z8{k8r^f2+UK-|HXrkNPM5v;IZ@s(;hJ z>p%3L`Y-*r{zw0-|AR?jQkV?J!Q?OnObJuL)G!T93)8{$Fayj8Gr@S68D@c5VK$f@ z=72e2E|?qUfq7v*m>(8^1z{mr7#4vEuqZ4Bi^CGIBrF9>!!oceECh#*adcl-C%dv1NMZyU~kw5_J#dme>eaRgoEHfe7FEEgp1%}xCAbR7{nm~Nk~B&G7vx( za*&4t6rluVs6Z8JP=^p2(1aGWp#xp$K_4O*zz{|-2A9F*a0OfmSHabA4O|P?!S!$h z+z24kOpPY24et&H8_Je1Vc0=LpBscH8evv zpkWxMVHvjJ7_Q+NzJZLu2#v^y8Ox02#tLJlvC3F&tTEOa>x}ir24kbK$=GacF}51p zjP1q_W2dpp*lp}F_8R+){l)>~pmE4JY#cF;8pn*|#tGx3amqMtoH5QC=Zy2l1>>S| z$+&D>F|HcdjO)e?|ypadzrn>K4xFD zpV{9WU=B0~nS;$C=1_B(Ioup!jx#!73Y zv(j4`tc+GBE8fa%WwEka*{tkV4lAdX%gSx#vGQ8^to&91tDsfLDr^<860D+DF{`*$ z!YXN%vPxTJtg==)tGrdgs%TZRDqB^os#Z0tx>dufY1Oi7TXn3uRz0h})xc_KHL@C8 zO{}I?Gpo7P!fI(HTCJ?sRvW9W)y`^fb+9^GovhAQ7ptq)&FXITuzFg(tlm~1tFP71 z>TeCO23mux!PXFKs5Q(QZjG=;TBEGd));H7HO?AuO|T|fldQ?s6lE3iT$r8oI%%D$&y9dTG6~UR!Uhx7IuB zz4gKRXnnFiTVJfN);H_B^~3sU{jz>rf2_aOKRbz?)J|r{*~#q`c1k;yo!U-gr?u1B z>Fo@5Mmv)oZ)di%*jeptc6K|5ozu=`=eG0MdF_05e!GBO&@N;bwu{&ac2T>SUED5V zm$XaSrR_3yS-YHF-mYL*v@6+_?J9OvyP93yu3^`-YuUB!I(A*Vo?YK=U^lcI*^TWc zc2m2V-P~?rx3m-OR(5NN7y6nQTAwij6K#KXOFih*c0tZ_GEjCJ=LCOPq$~-GwoUS zYe*wqT34WXrZo4wuM zVehne*}LsM_Fj9Rz281yAG8nIhwUTwQTv#E+&*ESv`^Wm?KAdS`<#8=zF=RpFWHyv zEB00Untk2AVc)cG*|+UG_Fem)ecygyKeQj&kL@S+Q~R0y+Ev>9J9(VEPCh5UQ@|6{o6G&8hCxaB4cWoZ3zur>;}asqZv!8aj=f#!eHb zsng79?zC`PI*Cpzr?u0@Y3sCe+B+Sbj!q}1v(v@t>U49uJ3XA9PA{jo)5q!S^mF<< z1Dt`*AZM^M#2M-gbA~%3oRQ8bXS6fM8S9L5#yb<7iOwWvvNOe*>P&N{J2RY_&MarP zGsl_h%yZ^D3!H_{B4@F)#98WK4(<>R=}->sFb;57hjVyGa70IPWJhsSM{{%sI)-C9 zmSa1P<2s(>JID!~(21Owv&>oUtZ-I3tDM!&8fUGu&ROqla5g%doXyS_XREW#+3xIc zb~?M9-Oe6oud~nD?;LOrI)|LY&JpLRbIdvJoN!J$r<~Ky8Rx8X&N=T~a4tHRoXgG? z=c;qfx$fL>ZaTM|+s+;5u5-`1?>ulGI***k&J*XU^UQhfyl`GRubkJ;8|SU_&Ux>A za6USpoX^e|=d1J0`R@F1emcLL-_9TBuk+7M;wE*Ixp8iCH-($hP35L`)3|BfbZ&Y# zgPYOKZe_QMTh*=RR(ET-HQicnZMTkF*RAK)cN@43-9~O>w~5=- zZRR$2TevOVM7NdO+HK>ub=$e^-41R?x0Bo1?c#QIySd%n9&S&!m)qOz z%cL|qtDVKH`7r3m;xx6d5qAR(wtGKGGxw;Eo!!=#Y zwOz+`UC;Gh2L+)YshVypmoi zue4XjE9;f>%6k>Oie4qJvRB2c>Q(cqdo{e8UM;V-SI4XC)${6m4ZMb4Bd@X7#B1s` z^O}1typ~>~*UD?{wei||?Y#D02d|^o$?NQO@w$55yzX8Pucz0`>+SXN`g;Am{@ws@ zpf|`H><#gTdc(Zo-Ux4`H_99Bjq%2MBfPo9)f< z=6dtI`Q8F=p|{9e>@D$@dYFfMghzUmM|+G1Jl5kp-V;30lRVi|Jk`@Y-GiRtnV#j@ zp5wWm=lLG;0x$F;FXk=tmU}C_mEJ0EwYSDw>#g(FdmFrs-X?Fex5eA)ZS%H!JG`CV zE^oKD$J^`e^Y(iOyo25$@342oJL(z(t?dl$Tm-X-s{cg4Hv zUGuJcH@utPE$_B>$Ghv@^X_{OyocT+@3Hs9d+I&&o_jC6m)%H^ddmp@y z-Y4&~_r?3_ee=G1KfIsbFYmYa$NTI3^ON{V{bYWepWIL3r}R_#sr@v5T0fni-p}A? z^fUSKer7+5pViOiXZLgXIsIIIZaJ-`H>BH}#wO z&HWaBOFz+X<+t|R_-*}metW-z-_h^nclNvZUHxu;cfW_<)9>Z?_WSsK{eFIbe}F&G zALI}AhxkMNVg7J`gg??B<&XBq_+$NX{&;_aKhdA$PxhzyQ~hcFbbp3F)1T$f_UHI> z{dxX;e}TWyU*s?Lm-tJ4%*TDgCwvKNu3%=+}zU(W$>TAC4L*MXC-|}tW z@m=5ZeINOOANr9W^OyO{{T2R7f0e)5U*oU!*ZJ%H4gN-dlfT*D;&1i0`P=;+{!V|F zzuVvA@Adcj`~3s{LI03{*gxVQ^^f_-{S*F4|CE2)KjWYE&-v&53;sp_l7HF1;$QWz z`Pcm${!Rauf7`#~-}UeL_x%U{L;sQg*ni?b^`H6A{TKdA|CRsRf8)RP-}&$T5B^90 zlmFTO;(ztO`QQB?{!jmx|J(oL|MmZ&Bq%9LhT>3ilmewhsZeT^2Bk&mPVx{CeyBehfCi#LXfPUrhN59;I2wUQ zqETox8iU57acDf6fF`0zXfm3DrlM(RI+}rIqFHD*nuF$|d1yXbfEJ=fXfaxXmLd${ zh(IKw5RDiF5Q{j(BLRs>LNZd2iZrAnhzw*R3)#p)F7l9%5DHL;A{0Z*&~mf_twgKP zYP1HeMeERdv;l2Io6u&o1#Ly!&~~%~?L@oKZnOvOMf=cxbO0ShhtOek1RX`k&~bDE zokXY5X>u5Emp5 zQUoc3R6*(>O^`N77o-m|1Q~-&L41%o$P#1?vIW_L96`(f|wafC$Kd3h00dK)?oEzz0Gg z22vmgN}vW>pa(E80yD4zJ8%Lw@B%+TK@fyN6vTpM!SY~5urgQ`tPa)$YlC&c`d~w_ zG1wGr4z>hagKfd~U`Mbs*cI#!_5^!_eZl_VKyWZP6dVqY1V@8o!SUcka56X*oDR+e zXM=OW`QSouF}M_54z2`OgKNR{;6`vWxE0(E?gV#(d%^wSLGUnm6g&=|1W$u!!Smon z@G^K6ybj(3Z-aNi``|m3LA$_!lq%fuzA=bY#Anot-{t}o3L%zE^Hrm2s?(I!p>oruxr>Y>>lI4m3qv5gecz7Z_8J-GHhiAgG z;kod9cpB3oABRuEr{S~kdH5oH z8NLc%hi}5S;k)pC_#yllehNQ_U&629xA1%TBm5cu3V(-x!oT6aC`ptwN*2XM$)gle z$|zNoI!Y6zjnYNwqYP2TC{q+4Wsb5$S)*)G_9#b`Gs+d^j`Bo#qkK{Rs6bRODijrt zibM%f(WqEdJSq{Dj7mkNqcTz1s9aP&st{F-Dn*r}DpA#_T2wu%5!H-pMYW?kQQfFs zR6lAEHH;cXjiV+})2Lb0JZcfOj1r?(QR}Ep)HZ4twU0VP9ivWB=cr56HR=|1k9tHs zqh3+(s87^4>KFBo21Em+LDAr7NHjDW77dR^L?fe7(dcMQG&UL+jgKZo6QfDdW+5f|~15Q&i# z$&nJNkrwF@jEu;PtjLa>$c?`Rz<6$HPPB=U9>*h5N(V$ zMVq57(bi~Nv_0Ap?TmIsyQ4kP-e_O6KROT{j1EPIqa)GL=vZ_-IuV_WPDQ7qGtt@T zTy#FV5M7KeMVF&1(bec$bUnHe-HdKUx1&4J-RNF)KY9>7j2=agqbJeR=vnkUdJ(;h zUPZ5?H__YZUGzTs5Pgh3MW3TD(bwo(^ga3!{fvG^zoS3V-{@Z~Ni1nBSu8G=JeDGs zGL|ZqI+iAuHkK}yK9(VtF_tM7AIlue63ZIP7Rw&X5z86N70VsV6U!UR7t0?j5Gxof z6e}Dn5=)2`jTMU(kClj(jFpO&j+Kd(jg^a)k5!0Oj8%$Nj#Y_Oja7?PkJX6PjMa+O zj@60Pjn#|Qk2Q!jj5Ufijx~ujjWvrkkF|)kj3vfe#ahSO#M;K%#oEU@#5%@0#X85j z#Qt+_72s7JTen4m1t=|bcW-koQcobkrMN?Y0D+J|1b26L*W>PrySux)Tm7Hh*-88U z_kGPi>zs3E&7PS(YtKHsrbIKMInjb>Nwgwb6K#mLL_4BA(Sb-OIuf0T&O`>$h3HCj zBQlBZL=Pg1=t*P~IYcj_H<3%^5&1*`(TC_u^dtHc1BijdAYw2vgcwQ;BZd^r7 zVl**^7)y*J#uF2WiNqvgGBJgiN=zfB6EldJ#4KVqF^8B-%p>L#3y6ipB4RPIgjh-} zBbE~@h?T@DVl_b!Bta20!4NFL5j-IfA|Vknp%5ye5jtTICSega;Set25k9eoSWB!U z))O0ujl?EmGqHu(N^B#x6FZ2V#4chtv4_}8>?8IQ2Z)2jA>uG`gg8nZBaRa%h?B%A z;xuuFI7^%(&J!1ii^L`3GI52tN?aqZ6E}#P#4X}Bafi4|+#~K24~U1vBjPdfgm_9k zBYq=(C;lM*B>p1)CjKFxBcCTrkR{1dWN9*nEJKzh%aP^D3S=x5)FU zhFnXoBiEA~$c^MCax=Mw+)8dEx05@_o#ZZZH@S!0OYS50lLyFygpuah^(o8&F>HhG7C-KQdDUwhAKmqrOHv|sR~pqRgtPhRi<8` zs!%UdFHu#gm#J5%SE<*i*QqzCH>tO%x2boicd7TN_o)x452=r+kEu_nPpQwSn{ zFR8Dnuc>dSZ>cycKn1A~6{aFoJe5EtQc2Wz)c4d6)Q{9p)X&r}R5F!9Rimm?sZ<(O zgQ`i@qH0rhsJc`=sy@|#YDoP`HKH0*O{k_+Gpae&f@(>%qFPgJsJ2u)sy)?#N~bzf zov6-K2Gxb?N_C?$sqRz{DvRn#Wm7p+FRC|{OXX4dQ~}k8>Pz*b`cng_fz%*sFg1i4 zN)4liQzNL6)F^5+HHI2Xjibg>6R3&QBx*7>g_=rDqoz|csF~C(YBn{8noG^2=2Hu( zh14QyF|~wRN-d+7Q!A*I)GBHo%cCTcUah1yDOqqb8!sGZazqpnjosGHO+>Na(Ux=Y=o?o$t_ zhtwnLG4+IcN?A^EM1YV zL|3L?psUa?(l60f>6htO=vV32=-25t=r`%N=(p*2=y&P&==bRl=nv_S=#S}7=uhd- z=+Egd=r8H7=&$K-=x^ybIzR{M5FMr?bUd9vC(=pucl7u45A=`pPxR09FLW}ULRX`! z)2VbCU4yPk*P?6Fb?CZuJ-R;KfNn_tN;je#(@p56bThg+-GXjOx1w9qZRoaiJGwpH zfljA8(w*qebOzmp?n-x~GwJSh4?2tPNoUhJbT7I$olED@`E&u@hwe-Fqx;hX=z;Vg zdN4hN9!d|RhtnhIk@P5fG(CnMOOK<+(-Y{4^dx#RJ%yf1Pot;PGw7N0EP6IQhn`E% zqvz8L=!NtmdNI9(UP>>cm(wfgmGmllHBHbYP0=*X&@9c-JT1^7EzvTq&?>FbI&IJ< zZP7OE&@S!KKD~xsORuBX(;MiG^d@>Uy@lRNZ=<)kJBgUlk_S2G<}9XOP{09(--KA^dx0tt?cbIpX_n7yY510>`kC=~{ zPnb`c&zR4dFPJZxub8iyZB(d>IZQ96H&$n32pVW;8Q~8Ow}g#xoO`iOeKsGBbsl%1mRXGc%Z(%q(U$ zGl!YW%wy&=3z&t>B4#nOgjvcgW0o^3n3c>bW;H`FBttPY!!RtvF+3wMA|o*}qcAF? zF*;)~CSx%+<1jAcF+Q_~S<9?r)-xNJjm#!yGqZ)+%4}n{Gdq}_%r0g(vxnKs>|^#b z2bhD*A?7f1ggMF_V~#T?n3K#Y<}`DLIm?`5&NCO7i_9hFGINEw%3NcvGdGx<%q`|N zbBDRh++*%D515C{Bjz#lgn7z5V}4_PXZ~RRWd36QX8vKHW1nYBuqD}2Y-u)zEyI>& z%dzFz3T!M}k*&m5W?x{durIPNu~pfZ*;m+C+1J?D**DlX*|*rY*>~7?+4tD@*$>zc z*^k(d*-zL{+0WR|*)P~H*{|5I*>Biy**G@92H6lBW+QAoo4_WrN$hv*_v{bskL*wE z&+IR3GMmCyW2>{NY#LjGt;yD6YqNFOx@i|xr~vpH-pwl|y0=CS!~0o#Y| z%l2dYvjf>ze9JA@s|4r7P2BiNDbD0VbEh8@d}W5=@-*oo{Ub}~DKoytyQr?WHI znd~ffHamx%%g$rxvkTaT>>_qCyM$fJE@PLoE7+CnDt0wXup~>dG|R9o%dtEwup%q5 zGOMsEtFbz3uqJD#;t&hF#09W7o4A*p2Kab~C$$-O6rbx3fFgo$M}lH@k=3 z%kE?Mvj^CN>>>6rdxSm89%GNQC)kthDfTpbhCR!kW6!e}*o*8X_A+~gy~?8Iu`-FYUK4X7le`o(-|78DS|7QQ;p5vb9N^m8)Qe0^+ zhAYFB<;rp8xe8n?SCOm4Rpwscs&FrIFL715m$_HCSGm`?*SR;iH@UaCx4Cz?ce(et z_qh+a54n%HkGW5{Pr1*y&$%zSFS)O{ueooyZ@D-wzy-Mw7v>^dJeR;Fa!K5G-1poM z+>hK(+|S%ETr!u!RpY92sazUYgR9Ba;%aksxVl_Du0Gd*Ysme|HR2j`O}M69Gp;$; zf@{gO;#zZUxVBt7u07X*OXoUrow&|i2G@n_%5~#1x$ayKE{p5QWpg=PFRnM2%jI$T zTmjdI>&x}y`f~%gf!rW&FgJu7$_?X&b0fHs+$e4|H-;O_jpN316S#@oByKV{g`3Jv znxS8B6ZZe~n8@P?!CT=sgh1<$)8D;H&U2@-OjK`Iq@u_*ePY_}BS2_&52t__z6Y z_;>mD`1kn__z(Gy_>cKd_)q!I_|N$-_%Hdd_^Nfluc<@}2n3d{xScAf671Of8&4W|KR`R|Kk7V{}G-O zo)=08C52K#X(2`^Ba{`&3FU=!Vkia!cW4_!Y@LykRntQstc(?novWiDbx~b3w4CLLOr3r&_HM?{3MXz6NU>TgptB1VYDzt7%Pku#tRdK ziNYjdvM@!MDohim3p0e7!YpC7Fh`gx%oFAd3xtKjB4M$xL|7^;6P61rgq6Z7VYNUA zq(BL@zzD3s3A`W(q96&fpa`m<3A$hireF!S;0UhZ3BIsKSSzd()(abijlw2jv#>?j zDr^(B3p<3J!Y*OAut(S{>=X722ZV#dA>puaL^vuO6OIcfgp}#gD{~#ZSaf#m~gg#V^D!#jnJ##c#xK#W*n_2E~vV z79(Q3m>?#KN#b|n_u>!YkK#|_&*Cp)vX~-P6RV4)9TZ?VPwqiT6z1Tra7dwib#Li-d*hTCrb`vwj?qUxy zOYA9Ti#cL1vA38j=85@Yf!IgvEA|univz@g;vjLbI7A#O4ikrqBgB#7C~>qnMjR`S z6UU1a#EIf0ak4l?oGMNer;9Vhnc^&Qwm3(eE6x+=iwnes;v#XexI|nkE)$oFE5w!J zDsi<)h@?n~w8)68$celth@vQovZ#ovsEN90h^AJ0vx21Qaccu5F_oWY{52cT!kEKtfPo>YK&!sP2aX zAO)q66qX`Vyp$j%N=edp()ZF2(vQ+l($CT_QnHjHRgDNE`pWlK3yFR8bbE9FV~Qi0S*>MQk=`bz_(fzlvpurx#(Dh-o{OCzL_(kN-P zG)5XLjg!Vp6QqgKBx$lVMVcy2lcq~Eq?ytzX|^;+nk&td=1U8th0-Evv9v^5DlLEfWCTX*@McOKDleSAcq@B_(X}7dT+AHmo_Dct(gVG`CuyjN^Djk!KODCk0(kbb* zbVfQWos-T>7o>~QCF!zsMY<|oldelQq?^($>9%x7x+~q2?n@7(htebIvGhcGDm{~a zlYW=}kp7hZlKz(dk)M;FmrKYcd-;tXxs9Bv+PSkgLcq$}h=P z<(K7G_VzBoJ-NQzKyE1iDmRiF%T45_ax=NP+(K?Cw~|}SZRECcJGs5wK~9%D%AMrSa)#VR z?kabaGv)4b4>?QjDQC+$axb~JoGa(a`Er5WNA4^4ll#j9m&+^UmGUZiwM@vQOv$v&$gIrCye!C~EXlI0$f~T#x@^d%Y{|Cl$gb?kzPv_W zE3cE+%Nyj4@+Nt+yhYwBZxjJC6!W2X(dJ}qm)(3Ddm+4N~}^*siag^UQnti zFDfr7Rh5^OSCm(k*Ob?lH%VFO)Bp zuavKqZrvB1*iHpd>0u%6H25$`8tq%1_GA$}dW?lA=^osw=5Vno>il zsnk+xD|M8*N!(m_dAIx3x% z&Ps;TMd_+^Q!8WHZIZ7|3x00*mDfvo)(nsm5^i%pP1C)WvAZ4&JL>a0K zQ-&)el#$9PWwbIz8LNy_#w!z)iOM8pvNA=Ps!UU+D>Iat$}DBJGDn%K%v0to3zUV* zB4x3%L|LjVQqA04ODY{}PreZ0!;wY}- zDZa8sS*xs5)+-y7jmjovv$93ms%%rXD?5~($}VNMvPapg>{IqD2b6=#A?2`gL^-M) zQ;sVql#|LS<+O4}Ijfvg&MOy`i^?VCvT{Yas$5g9D>syz$}Q!#a!0wV+*9r=50r<> zBjvI3M0u(_Q+`u^SN>4`RQ^)_R{l|+Q=eB$s3p}>YH2k_Eu)rI%cnxH1CN$Pj%_v#PokLpkA&+0E~vYMh+Q>&|~ zYMNR@t*O>hYpZqCx@tYOzS=--sQ#)pQX8vH)TU}PwYl0tZK<|WTdQrL7Kn zIz%0+4pWD#Bh-=VD0Q?tMjfk;Q^%_l)QRdOb+S4|ovKb#r>is6nd&TcwmL_htIkvB zs|(bH>LPWqxoAQE7XZ!iEMqR6}Q`f5-)Q##Ub+fuf-KuU=x2rqUo$4-ix4K8&tL{_xs|VDB>LK;8 zdPF^{9#fC2C)AVbDfP5^Mm?*ZQ_rgx)QjpR^|E?Jy{cYQud6rIo9Zp~wt7dstKL)Z zs}IzN>Lc~B`b2%IK2v{Fe^>ud|5X1{|5pFep3|P!N@yjuQd(&(Mk}M0)yiq*wF+9S zR#B^@Rn}h6s%S52FKJb^m$g^4SGCu)*R?maH?_C4x3zb)ceVGl_q7kS54Df9kF`&< zPqojq&$TbKFSW0JTowUwchSo*vs&&&cweDIEElcaEWotQFFRizhtL16=T7lL_>#OzC`fCHU zf!ZK#ur@>+stwbIYa_Ig+9++bHbxt(jnl?!6SRriByF-bMVqQk)23@Pw3*s0ZMHT? zo2$*!=4%VIh1w!*v9?58sx8x&Yb&&s+A3|eMrfo)X|%>@tj1})CTOB2X|kqhs-|hW zW@x5nX}0EQuI6dJwnkg4t<%17qpAoCGE0yMZ2n9)2?ebw42&3?Y4GD zyQ|&P?rRUUhuS0UvGzoJsy)+w(|*_f(Eil^(*D-|(Vx?w*GuRn^-_9iJw`91m(|PZ z<@E}BtX@&Cq*vBo(5vV#>M!Y4^_TTm^jG!Q^w;$_^f&dl^tbhQ^mq05^!N1-^bhrq z^pEvV^iTEA^w0G#^e^?V^sn`A^l$YzJ)j5mkRH|}dc2;XC+bQ1cl!7G5BiV#Px{aL zFM6_`qF2+a>#2I0UPG^`*V1e2b@aM=J-xo(KyRr3syEUb>rM2gdNaMb-a>Dwx6)hd zZS=N!JH5T$K~L8^>Yen?dWPOb@2YpxGxhFz4?Roosb}jsdM~}Vo~!5S`FergNAIim z)BEcK^nv;yeXu@6AF2=2hwCHsk@_fov_3{3tB=#i>l5^e`Xqg_K1H9ZPt&LCGxVAI zEPb{l^fq`X+s|zD3`vZ_~HyJM^9UE`7JYN8hXO z)A#EK^n>~#{jh#SKdK+okLxG&llm$Bw0=fEtDn=)>lgHk`X&9cenr2kU(>JaH}sqO zE&aBBN58Az)9>pK^oRN*{jvT;f2u#zf75^0|Iq)`|I+`~|1q93o;OMuC5=)>X(Prc zW0W<@8Rd-%MyyfMsAN<&UNEW{FB&fyRgIU8SBzJU*NoSVH;gxpw~V)qcZ_$9_l)@)To2aJQpA>*)d#5igkGmaZ4jFZMGlv&Ckrw%`eO^&9BU_&2P+a%{Vh)2F;KeHX~-dnP4WGN#=Lv z_vR1gkLFM2&*m>?vYBF5Gpn1aW|~>UtZCLVYnyeys%%)~D zv$@&AY-zSKTbpgnwq`rCz1hJ`H#?f0%+6+p*~RQ?b~7{0?q&}&%j{`pn>l7Lv$vUR z=9&3sf!W9GYxXnyn*+>&<{)#hIm8@l4l{?FBg~QJD08$q#vE&oGsl|~%!%eCbFw+b zoN7)pr<*g(ndU5WwmHX~YtA$0n+wc^<|1>kxx`#*E;E;#E6kPVDs#0-n50RWw8@yP z$(g(6yN{#$0QzGuN9N%#G$IbF;a{+-hz!x0^f6 zo#rlcx4FmMYwk1mn+ME;<{|U2dBi+w9y5=dC(M)PDf6^>#yo4DGtZkB%!}qF^Rjux zylP%EubVf_o8~R^wt2_AYu+>On-9!~<|Fg5`NVu`J~Mwae>eXy|1|$H|2F@zp0l2} zN?0YWQdVgz#wuf#waQuLtqNAGRne+sRkmKRs#q^tFIiQsm#tT=~hU=j zT1nP-*7w#A){oXt*3Z^2RVc3vT9p(th!b`tG?C1YH0mxHL@C8 zO{}I?Gpo7P!fI)?vRYehthQD=tG(61O1C;%ovhAQhSkODYIUWm`E` zFRQneYvoz_R)N*W>TC70`db66fz}{vur*S+b>As-;=FWmu+VS+?a^uH{+2wZ>X&t+Uo!8?24iCTp{`#oB6Z zv$k71tew^_Yqzz>+H38z_FD(6gVrJIuyw>bY8|tVTPLiO)+y_>b;detowLqc7p#lc zCF`$Y{rx@+CD?pqJ6ht?zOvGv4yYCW@lvwpY!u>Q3Evi`RI zv7fV_w@cV1?NWAWJH{?!m$l2;{spA?APr# z>^JSV?6>WA?04<=?Dy>t><{ga?2qkF>`(2_?9c5l>@V%F?62)_>~HNjJ75RxkR7%o zcD$WnC)!E&clP)85B87tPxjCDFLtt>Vpp@P+o^V%UBj+v*RpHdb?mx!J-fc$z;0;& zYB#bQ+fD4Ib~C%V-NJ5Zx3XK?ZS1yoJG;Hz!A`e3+MVpqc81->?rL|lGwtqn4?D~5 zX=mFxb}zfPoonaW`F4Tb$L?$Qv-{fv?1AJ>hub6Uk@hHiv^~ZiYmc+X z+Y{`G_9T0=h)K58GckJ~5gllCe5w0*`tYoD{v z+ZXJM_9gqWeZ{_NU$d{f_nh~g51bF3kDQO4Pn=Jk&z#SlFPtx(ubi))Z=7$PI49r) zosbiDB2K)M;3PUp&UeoD&JWIy&QH$I&M!`~lj2l!synGpnp4B6>C|#+J9V78PCci- z)4*xy{OUAv8aqv#rcN`bxzoaF>9lfMJ8hh{PCKW))4@r1Iy#-4&Q6BY#p&vFb26Ro zP7f!`>FH!UIZiL9x0CDSIr&b3)5q!S^mF<<1Dt`*AZM^M#2M-gbA~%3oRQ8bXS6fM z8S9L5#yb<7iOwWvvNOe*>P&N{J2RY_&MarPGsl_h%yZ^D3!H_{B4@F)#98VrbCx?R zoR!WhXSG8(q(eEh!#J$NIlLn{q9Zx7qd2OgIl5ywreis_<2bJ4Ili;TS?jEG);k-V zjm{=#v$Mt7>TGkiJ3E}6&Ms%Sv&Y%%>~r=z2b_b>A?L7j#5w95bB;SFoRiKe=d^Ri zIqRHr&N~;Ji_RtIvUA0`>RfZKJ2#x0&MoJ*bH};s+;i?b51fb2Bj>U6#ChsGbAEGv zcm8nxbpCSwcK&gnbDwuhxFy|EZfQ5hE#sDT%em#<3T~`h(XHfGc3*I-xG%adxmDel z-B;XK-PhdL-8bAf-M8Gg-FMt~-S^!0-4EOk-H+Ul-A~+4-Ot?5-7nlP-LKrQ-EZ7) z-8eVk2HlVwb|Y@Qo8Tt8N$z*<_wEnwkM2+I&+ad7vYX;obE~_lZkk)et?AZsYrA#a zx^6wUzT3cU=>F<9avQr%+@@|bx4GNGZRxghTf1%Cwr)GOz1zV}cRRYB+|F)>+r{nb zc5^e`?rsk^%kAlAyE$$zx3`-KZ|y93;T?jU!tJH#F84s(aQBixbh zD0j3w#vSX9bH}?A+==cacd|Rho$5|=r@J%UneHriwmZk2>&|oMy9?Zf?jm=wyTo1U zE_0W=E8LatDtEO@xTH(Dw9B}x%elNOxS}h$va7hNtGT*sxTb5lw(GdA>$$$W#$D^K zbJx2Y+>P!gceA_2-Rf?0x4S#so$fAox4XyP>+W;+y9eBZ?jiTEd&E8J9&?YoC)|_n zDfhH{#y#tvbI-dM+>7od_p*D%z3N_bue&$go9-?5wtL6D>)vzkyARxl?j!fH`^0_f zK68I_e|P_I|8)Oy|91cJp7WmfN_Zu`QeJ5<#w+8M^~!nWy$W8eSJA8FRrX%+s(3GY zFL_nHm%UfKSH0J~*S$BqH@&yKx4n0~cfI$#_q`9i5514PkG)U4Prc8)&%H0aFTJn4 zuf1=)Z@oA#;03*q7xp4vyqDl5dP&}Q-uK=Q-jCi--p}4IUb2_sRr9KQsa~2_!>j4l z@@ji^yt-aJufEs7Yv}#zHS!vJO}wUFGq1VV!fWZZ@>+XsytZCDuf5m7OZPf@oxILo zhS$aG>UHxnz3yHQFU#xcWqUbZFR!*aa*UV+!g>+ALN`g;Ssf!-i*us6gT>J9UT zdn3G&-Y9RhH^v+5jq}EP6TFGuByX}e#hdC)^QL< zvA4uq>Miq@dn>$^-YRdkM|h-1d9=rPtjBr0CwQVKd9tT?s;7CnXLzP(dA8?xuIG8a zx5iuRt@GA<8@!F)CU3L1#oOv_^R|0Cyq(@IZ@0I{+w1M~_In4sgWe(Uuy@2e>K*fr zdnde;-YM_2cg8#Go%7Cn7rcw!CGWC##k=ZV^R9b0yqn%F@3wcxyX)Qa?t2fshu$Oa zvG>G#>OJ#*^M3dK@c#7v^8WVz@t^ab_e=OC{Zf8uKgKWPm-Wl}<^2kNtY6WuSQ`Y-ua{g?e${8#B4K zU&F8I*Ya!ob^N-1J-@!+z;EdP>NoNm`%V0&elx$h-@1X>nelNebpX=xO`F?@l$M5U+^ZWY){DJ-;f3QEq zALyPut`xE?${v?00KgFNwPxGhyGyIwUEPu8?$DixZ^XK~u z{DuA^f3d&BU+OROm-{RHmHsM!wNLn@Px-XZ_^i+Qyf65oFZr^s_^Pk@x^MWVZ~3#y_I`y2d?{w9C3zs29`Z}YeNJN%vgE`PVb$KUJk^Y{A){Db}>|FD0= zKk6UzkNYS5lm03Hw137w>!0(_`xpF+{w4pif5pG*U-Pf~H~gFaE&sNE$G_{}^Y8l) z{D=M{|FQqXf9gLgQ;?k*hyI6ca`kdKeKKYdV2 zb1|K;U3#ZZ8QJ;eGm0)wjBVEy-*czR3diG4m?Z$Tf52#S{-}ftY`}ew3&5r50rMjU%F}3nDvpQ$Q zWJXPyTG0DU=zT5R`%FAUwVj%bqk%(LVsfFAKKX5|A3fAxDUD5G{(Qmi<%0J{~fvtdH-I=Hu+BjvHAZ2 zF->qY`B77jXv>{UV3lx~`vnccN?LE+zuP53?1>-=JqkSNu|yO!WHy zaFvd7l}gFTPcK~~9XXAAm=rDpe5G2UM_G_>V_rQAhCb6#5grs#<`b7yUGPvvl*qdKf4h>TEm91-ET zxN_-TGBX2lalug0RU}Y+6)e8`_i4EJUZnUczW6Gk_$smZDyiryzL+f@_a_`zz4%!X zS0G&6ZlJieKyhn<;?{!2tp$sF94u-jE?C^pU~zlF;(i8;TMibt94u}*RNQi?xaClB z%c0`22o-aLiaA2X9HC;4P%%fSm?K=w5iaHk7juM*Il{#p|Bg$zm?K=w5iaHk7jwiH z_bR^l7xBfvh%f#{eDN<5ihGq%+?Rynz9ba)C84*`6ft4GGD|#A;E8+^o6>$aPins!CMO=ZnBCbGOkw1aBB7XvLNx)we8-YME zR}xl;a6DGJa8Vq^W5o-{7d^+y7mknMw&Ee)p6w@FxI&0`MmQe**9)0Dl7TCjfr}@FxI&0`MmQe**9)0DmIz zCjx&W@FxO)BJd{ye@}KK1XaNP@PKfV*gJko*;% zKEg@Bodn!P>#F#7(1#@GLlX2M3Hp!(eMo{nB!TbA;CnLoo(%lS;CnLgCj);n@F&CA zCquuJfj=4elYu`O_>+M@8TgZdKLz+xfFI6M;S}Ic0sa)=PXYcE;7LE*2K?24zZ&pY1O95j4_R3lva)bB@E@|XFl3kE zYQPWKS{O7n44N9Q4*ZaHhN}aAk=9lR{_4PA9r&w*|JA|&>cC$e{I3rFR|o!7@IMv& zPX+%|fj<@ePX+!|@IMv!Q-MDf_)~#D75GzuKNa{>fj<@a(||t>_|t$Nve$4L@TUQP z8t|t9e;V+o0e>3srvZN&@TUQP8t|uK{s?5p5y*}sahN|6hy9PlVg5)Q=8wc-{zx3= zkHlgA2<)?wILsf3!~Bsr%pZxv{E;}!ABhA00QNr;!2U-9z#qW;siEk5fmGa2WTWo| zQbW;pkWGte-ZeKJ)foC>(>!{hi0KNm1czmbiOJF-!~k&XI|Y}9XLqkba`{l#$~NX2oFEb!yFM;Q2V+#?M9 zIPMV!ejN7*13!*?gn=K&J;K0`;~rrcPaOAwR2=unM#mjl;Ky-~F!1BJM;Q2V+#?M9 zIPMV!ejN7*13!*?gn=K&eIOObJ+i=$;~rrce;oG+!}#O4M;OK*$34O@{y6RthVjR7 zk1&isj{86=j(cQb{BhhP4F2P|M;QFaagQ+gkK-O;@E^xL!r(uSdxXJ%9QO!=|2Xag zscC_jmU!Xzj~cu>anz%yz>cFHVPMBmk1%FW3rAUkP@_XO>J_rlQAIZD6|&J$MK;8dX zh$Mt#dS+%9mbLOSI^|?{#=i`KW1*CC8T~q;vm+veC(#8Ej6-R6c3wfRjNHr|V8dw- z>MslyPJ0lB!Ag#+*eM5{_j{t+Wqx{Yf81U&PEX?!lfp3Ac&iA;CCA6~%E&{n!izAb z$tk5$3vzSdCQKOK7SL~^%OV(uw*_ROO}s523=@dA1%$DmMX7OoAQ;9pDB&$zt4nTr zkNkpMn6n_{^zngEBv_$u1}bo%vRryzUfD+QVzf_zAm$99(ay-rN9D=<3_PlVPzash zGkSF^TQ~yUGIKkZL$emW$Sa3#Wn^~kmftNlzZ)t!;_JK$T{8O=UB~93xz2_Mm-(*Vx65e8>*#*HvuC;^;t2Lm{xMiyMc88yOq7y~$? zMi_<>XVeJeg%`jXHNxnGlAD{;H!Gt{ewpZ1L9g;rn_K8Je%LvuZ#KT_hz12;74+&H zEvE$oIAKK=n#Kt$!Z^eNIAKLNx^W;I-MEm&DK zSd9TJT3oK#?r{S?gn@i!!z$*fQf?gG)V~j4N z>!bLs!X;7+3Rejpek^Xm02Vi7p}D_+DW8?oHM3KC;b)j~aG8@E zo0*N;L#kESNNGfdzL!NQgLqhj!PIi744RwK1-`JM2Jt`zLwKTsp@cGdnLRVJ(sS`I zL)FV64eXj*Q0Q8;tuVBOcKnRq1(|)&{+-7vgT}Ges;O!!S-!5PHp#o`0RzY4ReA%-gD?hVW zR)75Kq@;@I#aV9W{X&6`l7qr=kG>UxoFWAINeJ?j5I$`LL-_O%48cAHm7O3| zc96w^0F@ntaUeit2Vp$EVHlY(=nPbL&~qFJP}xBkk1|wt5XON3m7QQ1bSMlu6b2m% zgARp3hr*ykVbGy4=ujARD2yvRs6F7vl^ukEA6Iq|27X-GK{)}=%EFN1gdxQVLy8lI z6eo-;JLrAzKM@CF7*}}EeH@5kT;V|&2Vxjkco4>c7{(PIgz-icP6GZUyb*+Pbtf3c z)g5GE{Bd;$Vc-X`41-vPK`g@{mSGUfFosX9Y~wUk)h#-Zpz=cDjp&_dBtv~62=xVI@eT*|1%&Ym zg!%%)cm|-pfN%gAnu}1Y;12_5hWUiUuhJJ-`JT^h4-D7YBim>N)t2vUixF8_TFO9NCQl9*N+egE&AQKoYb zY@;g;G$De%CUwa~`E}=xIsIa4qDd?2ENtEp*t{dKc}HOLj=<&}fz3OToD@?tkQhM= zEx+(YQqT+g3}=-H97-bSP!fF(2e$|uIwEk$h`=Eu0*8nQ93mps5@T>egPX0E8f6Q? z$ij{u!F8@+1a{O2?5GjgQ6s65@+i5=?pT04&c`i5HXRRJV>}dV;&BNTtv(!C@d2C! z#0OGix}bj&m6cJd@K3aj_y~@w_y~@w_y~@w_y~@w_y`W`_z0f5_y~@w_y`vG_z1Ke z!BZC>!TU;l1WjG9!l^5qvUoVg$Hxatb;>H}2sgu}yJe*3=9lT5nT|>)o#8GJLuM5p zpH!h|L0(~wT$qW&J3tDnIUZJVJY+NRuu9`$mBvFh6A#%;Jgm|LTtf~f1n?jv1n?jv zzy_ZXz|JS&UA8cu3i*+ZZj#7GwC zRjN=NUf#$8Kdw?C4C9EaR0sn!}#OV1Hv%=`1F7<_>bca9lC%Y#~Z@HkK+wt z;K!iM@2m?P>CWL_>m&*_aeq1g?82E9y3}N8Mzm2!myrkxeQ@g&-j~9C=P2Ovfw{H-5?D9w;;K%1Cgt0#XoVXy2{Rx161wg+7_}GMsI(WPSpko2hG4#y?_YZU| z06G=`9SeYt1wh9F`0x`7;KL8Hc-#XaXa}Et(0v$3eD*;Y#u1-=5XSQ!2!S6VJnsQ~ z^g++@ya({nCltU(A7sG~(9rZ`gAj)K2OSN7j>6X<^c?0NbTj}u8UP&)fQ|-0M+2av0npI^=x6|RGypmp038j0 zj-oRP9&gam0O)7{bTj}u8UP&)fQ|-0M+2av0nkx&|A1^0m;_78M4#P?yJ2*N%Ab@)&a_6byvhxdR7_K6_u6G7M~P#GD& z2m3@2;vSv2@O{V!gOCpfVI2oy9S0#FMCT%SAN+xQFbL}y>h2+^yCaL&aS+xq)Zx*6 ztY1OM2ho8Bw+rhy2>D^3@RJt0Bl&Ly)hAaJ4%W!qskMVH|L^8)3Z9g&>~|K|ULTd^QC6YzXq% z5UzHILb%$EEM6ZW$Ztb1pCOpf5X@%?<}(EI87i7j$ZtcC--aN+4MBbzg8ViF`7L~H zLDL5QLw*~A{5AynZ3yz)5ahQZ$ZtcC--aN+4MBbzg8ViF`E3aD+YscpA;@n-kl%(N zzYRft8-n~c1o>?U^4k#Pw;{-HLy+HwAioVkej9@PHU#->2=dtwKZ_cw%rALoq-ga0^hL?2tgf1EcW4E#86L>TyS-iR>pNq~h%l@_oHrs2>ksFR2*dirc_YFw{zc=2 z^G5V>3H--;i!k_)^%i09AL}i`@rC;@vcQk^7GdDWc_YHWkM$N|;Kz9*!Z80>Z_&ps z@E_|f!r(vFTZF-XthWdQKh|4>fgkHF!oZL93Sr>KdWA6XW8Fd@)qo%C7Q(=fbqnDr ze_9gWFA)ZQtY-)VKh`sZfgkG`!oZL93}N8MdWJsc0YBC=gn=LH8N%Q{)-!~GAL|*y zz>oC|Vc^GlhA{ABJwq7yv7UwD-{>L>^N;lmVVHlcX9&amV?9F{)<4!Wgkk+-Jwq7Q zKh`sZVg2KJO&I=-F0$Z1)-!~`f2?N+ga2615C;FTo*@kW7tIgWGjt#Lv7R9e{8-Pz zsaVgD1%9k&2m?RXGlYR3>lwnpkM#^;;KzD~@c;32Z&|LRSQG%qWW@86=lqA%+$5^M zv<}XyeWkP=d1Sz@|GQsvYuEqzc5B!F`4-L5y+_yo`F3m9|2cPS*ZN=IGv|{CB_R*7D!|np?|%_iJt~|J|>-wfuL#=GOAx{hC|5 z{_lQG-0yzPJ-Ytye$B02|98LU)~^4%Uvq2M|4n1Mwd?<;G2PnrfA@24?fSp_Ik$f2 zfB1ht=KgzMyB}6NzWn>ywEE?r*T2QX|HJ>o|HJ>o|MUF6KmYO%{}2D~2l(&*&;P^! z!~eto!~eto!~eto!~eto!~eto!~eto!~eto!~gri?fdue|9;r3cK-Z7{J$UBdi-7g z!~eto!~eto!~eto!~eto!~eto!~eto`#JE-k5C0GU5C89n*?wL={}2Do9=}UX-t+(P|9%?&{yYBPkJ8oh`G5F- z_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+ z_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_<#6+_o|HJ>o|NDoEpMUSahyREF zhyREF=k>Gk@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1tynZ$w{vZAy{vZAy{vZAy z{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZCI z*Wbp&|HJ>o|HJ>o|HJ>o|HJ>o|HJ>o|HJ?DKCihs{Nw+5pV!30|HJ>o|HJ>o|HJ>o z|HJ>o|HJ>o|HJ>o|HJ>o|HJ>o|HJ>o|HJ>o|HJ>o|HJ>o|HJ>o|HJ>o|MUF+HwWn7 z>*OB){J+P)_uqT`d;h)1KmYFW@AY?&fBxL#-|PP#|NOhhzxV%p{Coeu$G_L#J^sD^ z?(xt6d;EL}wfyJ*t>r)eZ!Q1%e{1>A|GN}X z{`3FV@}K{=mjC+yTg!j`-&+3b|8Fh-`G0Hq&;MJ?fBxSoit?ZTx0e6>zqS15|E=Xe z|8Fh-`G0Hq&;MJ?fBxTE{`3D%jg;G>p|M`Dw`Op7b%YXjgTK@C@*7BeKx0e6>zf(cwKmTtn|M`Dw z`Op7b%YXjgTK@C@*7BeKx0e6>zqS15|DA#=|M`Dw`Op7b%YXjgTK@C@*7BeKx0e6> zzqS15|E=Xe|L@dS`Op7b%YXjgTK@C@*7BeKx0e6>zqS15|E=Xe|8Fh-`G2R}%76af zTK@C@*7BeKx0e6>zqS15|E=Xe|8Fh-`G0Hq&;L8sSN`+=*7BeKx0e6>zqS15|E=Xe z|8Fh-`G0Hq&;MJ?fBxSo#`2&4x0e6>zqS15|E=Xe|8Fh-`G0Hq&;MJ?fBxTE{`3DX zotFRnzqS15|E=Xe|8Fh-`G0Hq&;MJ?fBxTE{`3FV@}K{A$+rCG|E=Xe|8Fh-`G0Hq z&;MJ?fBxTE{`3FV@}K{=mjC>}Gr8qI|8Fh-`G0Hq&;MJ?fBxTE{`3FV@}K{=mjC>} zwfyJ*ok1@D`G0Hq&;MJ?fBxTE{`3FV@}K{=mjC>}wfyJ*t>yFo&IbPT|L%$6`10?f zPxZ?;|KrYhe))GF6pz1rd!XFMU%vStw|@ENf86@zoBweq%**G$`5$*&KL5@Cxa0Eq zZ~n&}e}8}c_;3El9hc94^FQwR`}@QEk2_)C`SaiWk2~)C`EUNm9e4ixH~-^~JAeNF z`2XYokN-dZ|M+j-$Xzht`t#qskvsnW{xWam*7EuPJ_<@4XXkvlG*|K^R{ zaryi=Z{&{4=f8O)cU(UI%^OMPjohPr{#zw{$K~_iD&adWpZ`_~-*Nf;|MB0vk^8vk zAOFo8x$8mX^WVIYJ1(F9=8fEO`TRF;(777nD4lJ{{Q%I8S{NyKL3CGw~RU6 z&0gO1<-cXjcbq@`|MA~4=KHwcKmULHw~YBd?)vlpZVn>H^N;_QG2c~O{NcZ4%y-=N z=l_rYmNDPQoj?D7{I`txKJNVa|KtB%#{Bp1F@NhWV0!s?Uku~#{LSCGkH710{?@JE z`P+fx*3Vz_w{HE;-~6pxzw(=u5ANX(n)?LLWpa14>-SOwY`CGSs=YRJuc>iwx z-*x=zcm8+Xy82!JyN>(S`v1EITkZVy|K@ew)pX8Z{~!2oUe|qGKK}#%1OEg6@49O_ z^7(H*SPJ|P{15tntF!Op&Y%B*|K^X~$KT%{f&YR3LH{54ANYUQvb}$g{y*?P@IUDP z1OM;xV?Xcr&;Pru*0_BB2maq>r5<-njlh@c*vqGyb0cf&X{enQ{625Bv}M z|G@vi|DgZBi=+JauD||2=>N^POXl0%qkR6GZ+FM#^WS{CJMR4XZ@%3fcmDc+^X-!P zcK7J{$A9zf?znvZn{Ri=<@4WsyE`tQ|AGHO{~!2o9^PH_>H6#c1OEg6gZ@A8Kkz^3 z{{#Q;{x9#k>i+}(1OJ2mKkz^BKkz^BKkz^BKkz^BKj{C1{y*^l-yT8#?qi16|KE=V zTnGLK{eR$p;D6Bn2mOEGzxj#D{KR|o`scs-iFe%l5C4PyKkz^BKkz^3{{#O6|AYQN z@IUZB@IUZB@IUDP1OEg6gZ@A8-%iGN5wGW;{y*?P@IUDP1OJ2mKkz^BKkz^3{{#O6 z|LvHZ?3jFy-hcTY^#6hXf&YR3LH{54AN2o$|AGHO{~!1t_#gNm^#6hXf&YR3LH{54 zANU{i|AGI3|AGHO{~!1t_#gNm^#6hXf&YR3LH{54AN2o$|AGH@mQI2Hf&YR3LH{54 zANU{mAN2o$|AGI3|AGH^f#Bck<}T=SeEyklo9@Cnaq{~N#a3IBUsKL0=dfBgUS{~!N9 z_y0fs--+cfhyUgg-?i)h|N8%r|K=6n$DP0a|KtD1fAfv+&pChofBgUS{~!N9_y0fs zfBgUW|LOnDUrrzYKmOkdz3)8uZ+`RrIr;qm`2X?$s8|Nrsd zJnCc~^*#Fi^WVJcJMQ<-|BwIXTc^8w*gFsYfA0T({QvlGUiSTYzkmLJ{Qvm>@&Duh z$Nx|Nf0xz2JpOHk0efBgUS z{~!N9{(t)akN+S4KmGs5fAiJv)(U?A{5N0yj(h#_|Kq=T>vt;#&wu`Z{Qvm>@!x#* z`}3ZE+%=#5j?3r2`RsRGKL0=do6nxiXTL|UfBu`#e#gE3_-{V@9rymD|2Logj(h*% zzxnLReD-_v{>Ojw+3&b~{+rK!$K~_?_B-zS^WS{-JMQ}P-+cCDKKnh&=fC;vcU(UI&1b*k^7a3p z`~M&R&2PUy=luC^etR;%{T{vk`Tz0XJoo#!eEys7e#hnW-~9JG?)>@x@&Duh$N!K2 zAOAo8fBgUW|MCCh|HuE2{~!N9{(t=c`2X?$s$|L*JY_x>08ANBvy{r|}SsQ-`rkNW?}|ET|u{Ezzo$p8Dj0doF+ z|93jhdmh~FNZvUAqtE{%|D*mt@;~bTqx=6+{~!4u`5*Zo`5*Zo`5*azH&J={{EzPc zNB&3tNB&3sf8>AUf8>AUf7Jg+{zv{t{zv}bO=Vs_|0Dk+|0Dk+|D*mt@;~xF@;~xF z>i;AEBmX1+BmX1+;c-v901cI)^4 zWB0aOzrR21-gfKfkKNmD{oa4<-gaxxd;Z(KE!n;89_91j?rnGc{rzqCwp%}c?A~_k z_x@-1wp%}c?A~^3uRr~NAUf7Jg+{zv{t{eR?t|Bie8>HqCUcgH>d_5XIGyW?Je`hUC8-EsN(Bqh|HS`$ z0pHK@{|2g^KmQZ|6aN$c6aN$c6aN$c6aN$clm0*H{}ca{{y*_Q>Hib|lm0*PKk5Gy z|C9bd@jvPR?V6YDns-mb^H2Y8*StIK`s@D_|C9bd@jvPR6aSO`Kk+~5{}cZc{}ca{ zzxV9QcQ2>w&;O+Vw=3U$-0z?N$^HMt|HS`)`||y}AD{T2_@CVWzx(pN$Mye-|H=LT z#Q&uKzdQB$dHMWL`v1iL#Q&uKPyA2(Px}AF|HS{K|4;h=#D5zA-lvs({@VcXj(h&| zKk+~5{}cZc|C9bd@jvlD>Hm}a|B3&J|H=J-I|<&WxA!0ZC;lh?C;lh?+iCFrcV2(| zxAWkAnRx%f8u}Qf8u}Qf71Ua{wMw?{eR+r;(yZr-@QNoURRm_S^uB; zpY{JXh`eu?@Aa4YpY{Kl|5^W^`JeUwng2GHy#Jr~`p^8&{J&o^@%}yh&-}k%#$jAO z|Fix-^FQ-H>;G+BdEefjKX(I5{~!7M&-~B&|IGia|Ihr-{LlLT%>S(a&-~B)&-~B& z|IGi)|IGi)|E&Md{LlQ)`v1)TtpCsa&-(w&|IGia|Ihr-{LlLT%>T^)?EZh|f98MI z|7ZSZ{%8JY{%8JY{eR|v=6}}zXZ~mYXZ~mYXa3t<^z!TX&wrbX-tqVTYje?CzrR0h zE_&@1ptVSmuA`f98MY|J`HM&&lV1=6~jY*8kgY z^!^#)^~e9L|Ihr-{LlQ){LlQ){LlQ){LlQ){LlQ){LlQi-|77`=Dq*h@ATI1{m*`< zw|4#cpZTBppZTBppZTBppZTBppZTBppZTBppZTBppZTBl|C#@p|C#@p|C#@p|C#@p z|C#@p|C#@p|C#?;|DXAv`JeUwng5ypng5ypng5ypng5ypng5ypng5ypng5ypS^uB; zpZTBppZTBppZTBN|Ihr-{LlLT%>T^)%>T^)%>S(a&-~B)&-~B)&-~B)&-~B)&-~B) z&-(w&|IGi)|IGi)|IGi)|IGi)|IGi)|IGi)|IGi)|IGia|Ihr-{LlQ){LlQ){LlQ) z{LlQ){LlQ){LlQ){LlQ){LlQ){LlQ){LlQ){LlQ){LlQ){LlQ){LlQq`-A_zZx#L* z{ulli{ulj!;eX+O;eX+O;eX+O;eX+O@%ew@f8l@Of8oDBh5yC<|HA*m|HA*m|HA*m|HA*m|HA*m|HA*m z|HA*m|HA*m|Dyjd{4e_d!vCWGFZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3 zFZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3 zFZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3 zFZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3 zFZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3FZ?h3 zFZ?h3FZ{n>3-afG<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW)&E!i zSN(tGf8~GG|5yH3{#X82{#X82{#X82{#X82{#X82{#X82{#X5f<$vXW<$vXW<$vXW z<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW z<$vXW<$vXW<$vXW<$vXW<$vXW<$vY>{YsVh{N#V-f8~GWf8~GWf8~GWf8~GWf8~GW zf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GW zf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf8~GWf93!E z=G*uC=YQpY<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW z<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW z<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW z<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW<$vXW z<$vXW<$vXW<^TQau)o(=7_}}>7_}}>7`2XL!yT<>< z|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u z|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hl8u|Hgm&GvDbCZ~jH&f8&4S zf8&4Sf8&4Sf8&4Sf8&4Sf8&4Sf8&4Sf8&4Sf8&4Sf8&4Sf8&4Sf8&4Sf8&4Sf8&4S zf8&4Sf8&4Sf8+mu^UfRp8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s z8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>sJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZX zJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZX zJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZX zJO8`>zw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KI zzw^KIzw^KIzw7@y|2zLX|2zM?{=f6T^S|@I>;F6dJO4ZXJO4ZXJO4ZXJO4ZXJO8`> zzw7@y|2zLX|2zLX|2zLX|2zLX|2zLX|2zM?{=fTsqw~M>zw^KIzw^KIzw^KIzw7@y z|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX z|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|GWOb^S|@I^S|@I z^S|@I^S|@I^S|@I^S|@I^S|@I^S|@I^S|@I^S|@I^S|@I^S|@I^S|@I^S|@I^S|@I z^S|@I^S|@I^S|r=JO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZX zJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO2m&2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E z2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E z2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E z2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E z2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E z2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E z2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E z2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E z2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E z2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E2mc5E z2mc5E2mc5E2mc5E2mc5E2mdGkC;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^ zC;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^ zC;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^ zC;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^ zC;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^ zC;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^ zC;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^ zC;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^ zC;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^ zC;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^C;un^ zC;un^C;un^C;xpP@Lh)aK5tz7U;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPE zU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPE zU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPE zU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPE zU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPE zU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPE zU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPEU;JPE zU;JPEU;JPEU;JPE_kHbmS^Hc6wD`aHzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoS zzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoS zzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoSzxcoS zzxcoSzxcoSzxcoP|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t z|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t z|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t z|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t z|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|C|4t|NHrW=K;R`DmMQ&|2O|P|2O|P z|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O~lf9J#h&Hv5+&Hv5+ z&Hv5+&Hv5+&Hv5+&Hv5+&Hv5+&Hv5+&Hv5+&Hv5+&Hv5+&Hv5+&Hv5+&Hv5+&Hv5+ z&Hv5+&Hv5+&Hv5+&Ht_c-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF z-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF z-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF-~8YF z-~8YF-~8YFKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%Z zKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%Z zKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%Z zKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%Z zKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%ZKm0%X|D*pu{6G9Z{6G9Z{6G9Z{6G9Z z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{J&rP z@wZ;-@c;gSq<;H<9R45vAO7DB$Nk*;LZY@BaTi{;mJI$G`o*?(uK^?LGeO|8}+a1b({@+^u^Z(ZJpZ~X(|NOtT{OA9zzuP;?fBxTE{`3FV z@}K{=mjC>}wfyJ*t>r)eZ!Q1%e{1>A|GTZE{OA9zzqS15|E=Xe|8Fh-`G0Hq&;PrPru^ss zt>r)eZ!Q1%e{1>A|69v{{@+^u^Z(ZJpZ~X(|NOt(cglbM-&+3j|JL%K|F@R^{J*vQ z=l`wcKmTtn|M`Dw`Op8mZK(X`|E=Xe|8Fh-`G0Hq&;MJ?fBpZhzuTnBfBxTE{`3FV@}K{= zmjC>}wfyJ*t>r)eZ!Q1%e{1>A|GPb_{OA9zzqS15|E=Xe|8Fh-`G0Hq&;Pp}t^DWzt>r)e zZ!Q1%e{1>A|69v{{@+^u^Z(ZJpZ~X(|NOt(;L3mg-&+3j|JL%K|F@R^{J*vQ=l`wc zKmTtn|M`Dw`TYO!|HuCy|9|}d@&CvFAOC;+|MCCF{~!N<{QvR)$NwMyfBgUP|HuCy z|9|}d@&CvFAOC;+|MCCF{~!N<{QvR)$NwMyfBgUP|HuCy|9|}d@&CvFAOC;+|MCCF z{~!N<{QvR)$NwMyfBgUP|HuCy|9|}d@&CvFAOC;+|MCCF{~!N<{QvR)$NwMyfBgUP z|HuCy|9|}d@&CvFAOC;+|MCCF{~!N<{QvR)$NwMyfBgUP|HuCy|9|}d@&CvFAOC;+ z|MCCF{~!N<{QvR)$NwMyfBgUP|HuCy|9|}d@&CvFAOC;+|MCCF{~!N<{QvR)$NwMy zfBgUP|HuCy|9|}d@&CvFAOC;+|MCCF{~!N<{QvR)$NwMyfBgUP|HuCy|9|}d@&CvF zAOC;+|MCCF{~!N<{QvR)$NwMyfBgUP|HuCy|9|}d@&CvFAOC;+|MCCF{~!N<{QvR) z$NwMyfBgUP|HuCy|9|}d@&CvFAOC;+|MCCF{~!N<{QvR)$NwMyfBgUP|HuCy|9|}d z@&CvFAOC;+|MCCF{~!N<{QvR)$NwMyfBgUP|HuCy|9|}d@&CvFAOC;+|MCCF{~!N< z{QvR)$NwMyfBgUP|HuCy|9|}d@&CvFAOC;+|MCCF{~!N<{QvR)$NwMyfBgUP|HuCy z|9|}d@&CvFAOC;+|MCCF{~!N<{QvR)$NwMyfBgUP|HuCy|9|}d@&CvFAOC;+|MCCF z{~!N<{QvR)$NwMyfBX;p5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O z5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O z5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O z5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O z5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O z5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O z5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O z5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O z5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O z5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O5Bv}O z5Bv}O5B&f5|MCCh|HuE2{~!N9{(t=c`2X?$s$|KtD1|BwG4 z|3ChJ{Qvm>@&Duh$N!K2AOAo8fBgUW|MCCh|HuE2{~!N9{(t=c`2X?$s$|KtD1|BwG4|3ChJ{Qvm>@&Duh$N!K2AOAo8fBgUW|MCCh|HuE2{~!N9 z{(t=c`2X?$s$|KtD1|BwG4|3ChJ{Qvm>@&Duh$N!K2AOAo8 zfBgUW|MCCh|HuE2{~!N9{(t=c`2X?$s$|KtD1|BwG4|3ChJ z{Qvm>@&Duh$N!K2AOAo8fBgUW|MCCh|HuE2{~!N9{(t=c`2X?$s$|KtD1|BwG4|3ChJ{Qvm>@&Duh$N!K2AOAo8fBgUW|MCCh|HuE2{~!N9{(t=c z`2X?$s$|KtD1|BwG4|3ChJ{Qvm>@&Duh$N!K2AOAo8fBgUW z|MCCh|HuE2{~!N9{(t=c`2X?$s$|KtD1|BwG4|3ChJ{Qvm> z@&Duh$N!K2AOAo8fBgUW|MCCh|HuE2{~!N9{(t=c`2X?$s$ z|KtD1|BwG4|3ChJ{Qvm>@&Duh$N!K2AOAo8fBcX9kNl7PkNl7PkNl7PkNl7PkNl7P zkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7P zkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7P zkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7P zkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7P zkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7P zkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7P zkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7P zkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7P zkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7PkNl7P zkNl7PkNl7PkNl7PkNl7PkNl7PkNi*kPyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2( zPyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2( zPyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2( zPyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyA2(PyD}MF#m5}_x*a{ zH~;j0A-np`KfPa6u72}R?-zTk-~7}2Mbxk6f8u}Qf8ziB!eBr5=HJ{e?p4d@f8u}Q zf8u}Qf8u}Qf8ziB0?YURs8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s z8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s z8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s z8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s z8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s z8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s?Pq*%+rQ8M8vh&r8~+>s8~+>s z8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s z8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s z8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s z8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s8~+>s z8~+>sJO8`>zw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KI zzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KI zzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KI zzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KI zzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KI zzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw^KIzw7@y|2zLX|2zLX|2zLX|2zLX|2zLX z|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX z|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX z|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX z|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX{|EmE z{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE z{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE z{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE z{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE z{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE z{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE z{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE z{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE z{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE z{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE{|EmE|0n+^|0n+^|0n+^ z|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^ z|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^ z|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^ z|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^ z|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^ z|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^ z|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^ z|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^ z|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^ z|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^|0n+^{}=xk{}=xk{}=xk{}=xk{}=xk z{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk z{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk z{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk z{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk z{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk z{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk z{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk z{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk z{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk z{}=xk{}=xk{}=xk{}=xk{}=xk{}=xk|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P z|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P z|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P z|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P z|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P z|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P z|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P z|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P z|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P z|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P|2O|P z|2O|P|2O|P|2O|P|2O|P{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU z{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU z{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU z{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU z{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU z{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU{}2BU|L+DTfBQNe z{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy z{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy z{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy z{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy{vZAy z{vZAy{vZAy{vZCI=l|XQ=kN7*kAM3N-Q(Z>fA{#e|KC0S?f-X=fBUc9A|69v{ z{@+^u^Z(ZJpZ~X(|NOtT{OA9z}wfyJ*t>r)e@AgjepZ~X( z|NOtT{OA9zzqS15|E=Xe|L=BI@}K{=mjC>}wfyJ*t>r)eZ!Q1%e{1>A|69v{{@+^u^Z#z6 zCI9(>yIj$|a6-7Z)<6%#lZ090Jp60LAyHQqR3k57k4}ohO z{%`C5{QtK8&;M`h|NQ^9{?GsK`7ZsR|KHaC`TuSGpa0+1|M~xI{h$Be*8lncZT+AB z-`4;6|2-R~|MUOb`al1_t^f1?+xkEMzpelC|J(XM|G%yO^Z(oWKmWhy%JhH!e_Q|O z|F`vj{(oEl=l{3$fBt`4|L6a=^?&|;L@!w*Jrm zZ|nd3|F-_m|8ML6{QsV3)BpMZZT+AB-`4;6|84!B|KHaC`TuSGpa0+1|M~xI{h$Be zvvB%9|G%yO^Z(oWKmWh2|MUOb`al1_t^f1?+xkEMzpelC|9g&3|L6a=^?&|;L@!wm$y@{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6 z{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6 z{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6 z{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6 z{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6 z{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6 z{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6 z{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6 z{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6 z{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#O6{{#Oc z|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+ z|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+ z|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+ z|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+ z|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+ z|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+ z|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+ z|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+ z|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+ z|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dk+|0Dks{}cZc{}cZc z{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc z{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc z{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc z{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc z{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc z{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc z{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc z{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc z{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc z{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZc{}cZ+|1!FMQT|LzaI!zufBfAAd=*}uQP z@36-H-5-31Cid_C;5)pqfA;L@!w*Jrm zZ|nd3|DKi5|M~xI{h$Be*8lncZT+AB-`4;6|84!B|KHaC`TuSGpa0);7WzN`zpelC z|J(XM|G%yO^Z(oWKmWh2|MUOb`al1_t^f1?dqzY5=l{3$fBt`4|L6a=^?&|Poxw*JrmZ|nd3|F-_m|8ML6{QtK8&;M`h|NQ^9{?GsK z*%1Ao|KHaC`TuSGpa0+1|M~xI{h$Be*8lncZT+AB-`4;6|2;L@!w*JrmZ|nd3|F-_m|8ML6{QsVX(f|4X zZT+AB-`4;6|84!B|KHaC`TuSGpa0+1|M~xI{h$Beb2R!t|G%yO^Z(oWKmWh2|MUOb z`al1_t^f1?+xkEMzpelC|9b{U|L6a=^?&|SJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZX zJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZX zJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZX zJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZX zJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO4ZXJO95w{NLXn|2zLX|2zLX|2zLX|2zLX z|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX z|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX z|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX z|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX z|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX|2zLX{|EmE{|Env-~WUE!|(sW|Ka!l;Q#RZ zfAD|!{Xh6W{Qe*OAAbK2{tx~S{tx~S{tx~S{tx~S{tx~S{tx~S{tx~S{tx~S{tx~S z{tx~S{tx~S{tx~S{tx~S{tx~S{tx~S{tx~S{tx~S{ty2D^Dfii|KR`N|KR`N|KR`N z|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N z|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N z|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N z|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N z|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N z|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N z|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N z|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|KR`N|K$JV|K$JV|K$JV|K$JV|K$JV|K$JV z|K$JV|K$JV|K$JV|K$JV|K$JV|K$JV|K$JV|K$JV|K$JV|K$JV|K$JV|K$JV|K$JV z|K$JV|K$JV|K$JV|K$JV|K$JV|K$JV|K$JV|MdHR@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw z@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw z@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw z@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw z@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw z@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw z@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw z@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw z@_+Jw@_+Jw@_+Jw@_+Jw@_+Jw@_+Gv@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u z@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u z@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u z@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u z@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u z@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u z@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u z@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u z@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u z@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u@qh7u z@qh7u@qh7u@qh7u@qhDw^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy z^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy z^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy z^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy z^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy z^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy z^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy z^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy z^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy z^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy^MCVy z^MCVy^Z)Sw@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t z@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t z@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t z@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t z@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t z@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t z@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t z@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t z@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t z@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;1t@c;7v z^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx z^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx z^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx z^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx z^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx^8fPx 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 z!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm z!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm z!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm z!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm z!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm z!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vDhm!vD(u z%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$ z%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$ z%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$ z%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$ z%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$ z%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$ z%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$ z%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$ z%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$ z%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kys$%Kygy#{b6u#{b6u z#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u z#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u z#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u z#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u z#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u z#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u z#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u z#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u z#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u z#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{b6u#{bU$&i~H;&i~H;&i~H;&i~H; z&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H; z&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H; z&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H; z&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H; z&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H; z&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H; z&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H; z&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H; z&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&i~H; z&i~H;&i~H;&i~H;&i~H;&i~H;&i~H;&j0ryo`28TdOyScJ^$xDpho>Y|K~lhMg2Yh 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 z{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF z{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF z{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF{NMcF z{NMcF{NMcF{NMcF{NMcF{NMaP{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z{6G9Z z{6G9Z{6G9Z{6GA^{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa z{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa z{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa z{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa z{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa{J;Fa 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 zmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(Ch zmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(Ch zmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(Ch zmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(Ch zmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(Ch zmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(Ch zmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChmH(ChjsK1R zjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1R zjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1R zjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1R zjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1R zjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1R zjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1R zjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1R zjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1R zjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1R zjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1RjsK1Ro&TNxo&TNxo&TNx zo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNx zo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNx zo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNx zo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNx zo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNx zo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNx zo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNx zo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNx zo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNx zo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxo&TNxga3p7ga3p7ga3p7ga3p7ga3p7 zga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7 zga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7 zga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7 zga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7 zga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7 zga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7 zga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7 zga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7 zga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7ga3p7 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≶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 diff --git a/demo/ramses-text-layout-demo/src/TextLayoutDemo.cpp b/demo/ramses-text-layout-demo/src/TextLayoutDemo.cpp deleted file mode 100644 index 6f4417cd4..000000000 --- a/demo/ramses-text-layout-demo/src/TextLayoutDemo.cpp +++ /dev/null @@ -1,244 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#include "TextLayoutDemo.h" -#include "ramses-text-api/LayoutUtils.h" -#include "ramses-text-api/GlyphMetrics.h" -#include -#include -#include -#include -#include - -namespace -{ - const uint16_t QuadIndices[] = { 0, 1, 2, 2, 1, 3 }; - const float QuadVertices[] = { 0.0f, 0.0f, 0.f, 1.0f, 0.0f, 0.f, 0.0f, 1.0f, 0.f, 1.0f, 1.0f, 0.f }; -} - -TextLayoutDemo::TextLayoutDemo(int argc, char* argv[]) - : m_framework(argc, argv) - , m_client(*m_framework.createClient("ExampleFontMetrics")) - , m_scene(*m_client.createScene(ramses::sceneId_t(123u))) - , m_textCache(m_scene, m_fontRegistry, 1024u, 1024u) - , m_textEffect(createTextEffect()) - , m_colorQuadEffect(createColorQuadEffect()) - , m_renderPass(*m_scene.createRenderPass()) - , m_renderGroup(*m_scene.createRenderGroup()) - , m_quadIndices(*m_scene.createArrayResource(ramses::EDataType::UInt16, 6, QuadIndices)) - , m_quadVertices(*m_scene.createArrayResource(ramses::EDataType::Vector3F, 4, QuadVertices)) - , m_windowWidth(1280) - , m_windowHeight(480) -{ - m_colorQuadEffect.findAttributeInput("a_position", m_colorQuadEffectPositionsInput); - m_colorQuadEffect.findUniformInput("u_boxSize", m_colorQuadEffectBoxSizeInput); - m_colorQuadEffect.findUniformInput("u_color", m_colorQuadEffectColorInput); - - m_renderPass.setClearFlags(ramses::EClearFlags_None); - m_renderPass.setCamera(*createOrthographicCamera()); - m_renderPass.addRenderGroup(m_renderGroup); - - const auto fontLatin = m_fontRegistry.createFreetype2Font("res/ramses-text-layout-demo-Roboto-Regular.ttf"); - const auto fontChinese = m_fontRegistry.createFreetype2Font("res/ramses-text-layout-demo-WenQuanYiMicroHei.ttf"); - const auto fontInstLatin24 = m_fontRegistry.createFreetype2FontInstance(fontLatin, 24u); - const auto fontInstLatin48 = m_fontRegistry.createFreetype2FontInstance(fontLatin, 48u); - const auto fontInstChinese24 = m_fontRegistry.createFreetype2FontInstance(fontChinese, 24u); - const auto fontInstChinese48 = m_fontRegistry.createFreetype2FontInstance(fontChinese, 48u); - - const float boxY = 440.0f; - const float boxWidth = 400.0f; - const float boxHeight = 400.0f; - const float leftBoxX = 50.0f; - const float rightBoxX = 550.0f; - - std::u32string layoutedText = U"This text will be aligned to fit in a box, and also uses multiple fonts: "; - const auto chineseTextOffset = layoutedText.size(); - layoutedText.append({ 0x6211, 0x5011, 0x5728, 0x5929, 0x4e0a, 0x7684, 0x7236 }); // U"我們在天上的父"; - const auto restOffset = layoutedText.size(); - layoutedText.append(U"."); - const ramses::FontInstanceOffsets fonts24{ { fontInstLatin24, 0u }, { fontInstChinese24, chineseTextOffset }, { fontInstLatin24, restOffset } }; - const ramses::FontInstanceOffsets fonts48{ { fontInstLatin48, 0u }, { fontInstChinese48, chineseTextOffset }, { fontInstLatin48, restOffset } }; - - layoutTextInABox(layoutedText, fonts24, leftBoxX, boxY, boxWidth, boxHeight); - layoutTextInABox(layoutedText, fonts48, rightBoxX, boxY, boxWidth, boxHeight); - - m_scene.flush(); - - m_framework.connect(); - m_scene.publish(); - std::this_thread::sleep_for(std::chrono::seconds(500)); -} - -TextLayoutDemo::~TextLayoutDemo() -{ - m_scene.unpublish(); - m_client.destroy(m_scene); - m_framework.disconnect(); -} - -ramses::GlyphMetricsVector::const_iterator TextLayoutDemo::FindFittingSubstring(ramses::GlyphMetricsVector::const_iterator first, ramses::GlyphMetricsVector::const_iterator last, uint32_t maxWidth) -{ - if (first >= last) - return last; - - int32_t xmax = std::numeric_limits::min(); - int32_t totalAdvance = 0; - - for (; first != last; ++first) - { - // glyphs with no bitmap data always fit, take only their advance - if (first->width > 0u && first->height > 0u) - { - xmax = std::max(xmax, totalAdvance + first->posX + first->width); - if (xmax > int(maxWidth)) - break; - } - - totalAdvance += first->advance; - } - - return first; -} - -void TextLayoutDemo::addColoredBox(ramses::Node& parent, float x, float y, float width, float height, float r, float g, float b, int32_t renderOrder) -{ - ramses::GeometryBinding* geometry = m_scene.createGeometryBinding(m_colorQuadEffect, "quad geometry"); - geometry->setIndices(m_quadIndices); - geometry->setInputBuffer(m_colorQuadEffectPositionsInput, m_quadVertices); - - ramses::Appearance* quadAppearance = m_scene.createAppearance(m_colorQuadEffect, "quad appearance"); - quadAppearance->setDepthWrite(ramses::EDepthWrite_Disabled);//text must be able to overwrite existing quad pixels - quadAppearance->setBlendingOperations(ramses::EBlendOperation_Add, ramses::EBlendOperation_Add); - quadAppearance->setBlendingFactors(ramses::EBlendFactor_SrcAlpha, ramses::EBlendFactor_OneMinusSrcAlpha, ramses::EBlendFactor_One, ramses::EBlendFactor_One); - - quadAppearance->setInputValueVector4f(m_colorQuadEffectColorInput, r, g, b, 0.5f); - quadAppearance->setInputValueVector4f(m_colorQuadEffectBoxSizeInput, x, y, width, height); - - ramses::MeshNode* meshNode = m_scene.createMeshNode("quad mesh node"); - meshNode->setAppearance(*quadAppearance); - meshNode->setGeometryBinding(*geometry); - - parent.addChild(*meshNode); - m_renderGroup.addMeshNode(*meshNode, renderOrder); -} - -void TextLayoutDemo::layoutTextInABox(const std::u32string& string, const ramses::FontInstanceOffsets& fonts, float boxX, float boxY, float boxWidth, float boxHeight) -{ - const int ascender = std::accumulate(fonts.cbegin(), fonts.cend(), -1000, [this](int val, const ramses::FontInstanceOffset& o) { return std::max(val, m_fontRegistry.getFontInstance(o.fontInstance)->getAscender()); }); - const int descender = std::accumulate(fonts.cbegin(), fonts.cend(), 1000, [this](int val, const ramses::FontInstanceOffset& o) { return std::min(val, m_fontRegistry.getFontInstance(o.fontInstance)->getDescender()); }); - const int fontHeight = std::accumulate(fonts.cbegin(), fonts.cend(), -1000, [this](int val, const ramses::FontInstanceOffset& o) { return std::max(val, m_fontRegistry.getFontInstance(o.fontInstance)->getHeight()); }); - - ramses::Node& boxTopLeftOrigin = *m_scene.createNode(); - boxTopLeftOrigin.setTranslation(boxX, boxY, 0.0f); - - addColoredBox(boxTopLeftOrigin, 0.0f, -boxHeight, boxWidth, boxHeight, 0.2f, 0.2f, 0.2f, 1); - - int lineVerticalOffset = -ascender; - - const auto glyphs = m_textCache.getPositionedGlyphs(string, fonts); - auto itLineBegin = glyphs.cbegin(); - while (itLineBegin != glyphs.cend()) - { - auto isSpace = [](ramses::GlyphMetricsVector::const_iterator it) - { - constexpr uint32_t SpaceCharcode = 4u; - return it->key.identifier.getValue() == SpaceCharcode; - }; - - // remove spaces from beginning - while (itLineBegin != glyphs.cend() && isSpace(itLineBegin)) - ++itLineBegin; - - auto itLineEnd = FindFittingSubstring(itLineBegin, glyphs.cend(), static_cast(boxWidth)); - - // check if we had to cut the line - if (itLineEnd != glyphs.cend()) - { - // check if we cut word in the middle (line ends with non-space char and next char is also non-space) - if (!isSpace(itLineEnd - 1) && !isSpace(itLineEnd)) - { - // find last fitting whole word - auto newLineEnd = itLineEnd; - while (newLineEnd > itLineBegin && !isSpace(newLineEnd)) - --newLineEnd; - - // set new line end if there is any word fitting, otherwise keep hard cut - if (newLineEnd > itLineBegin) - itLineEnd = newLineEnd; - } - - // remove spaces from end - while (itLineEnd - 1 > itLineBegin && isSpace(itLineEnd - 1)) - --itLineEnd; - } - - if (itLineBegin == itLineEnd) - break; - - const ramses::GlyphMetricsVector lineGlyphs{ itLineBegin, itLineEnd }; - const auto lineId = m_textCache.createTextLine(lineGlyphs, m_textEffect); - const auto lineData = m_textCache.getTextLine(lineId); - assert(lineData != nullptr); - const auto lineMesh = lineData->meshNode; - assert(lineMesh != nullptr); - - ramses::Node& lineOffset = *m_scene.createNode(); - lineOffset.setTranslation(0.f, float(lineVerticalOffset), -0.5f); - - lineMesh->getAppearance()->setBlendingOperations(ramses::EBlendOperation_Add, ramses::EBlendOperation_Add); - lineMesh->getAppearance()->setBlendingFactors(ramses::EBlendFactor_SrcAlpha, ramses::EBlendFactor_OneMinusSrcAlpha, ramses::EBlendFactor_One, ramses::EBlendFactor_One); - - const auto bbox = ramses::LayoutUtils::GetBoundingBoxForString(lineGlyphs.cbegin(), lineGlyphs.cend()); - const float highlightBoxOffset = static_cast(bbox.offsetX); - const float highlightBoxWidth = static_cast(bbox.width); - /// Add ascender/descender highlight boxes - addColoredBox(lineOffset, highlightBoxOffset, 0.f, highlightBoxWidth, float(ascender), 0.0, 0.0f, 0.5, 2); - addColoredBox(lineOffset, highlightBoxOffset, float(descender), highlightBoxWidth, float(-descender), 0.0, 0.5f, 0.0, 2); - - boxTopLeftOrigin.addChild(lineOffset); - lineOffset.addChild(*lineMesh); - m_renderGroup.addMeshNode(*lineMesh, 3); - - lineVerticalOffset -= fontHeight; - - itLineBegin = itLineEnd; - } -} - -ramses::Effect& TextLayoutDemo::createTextEffect() -{ - ramses::EffectDescription effectDesc; - effectDesc.setVertexShaderFromFile("res/ramses-layout-demo-red-text.vert"); - effectDesc.setFragmentShaderFromFile("res/ramses-layout-demo-red-text.frag"); - effectDesc.setUniformSemantic("mvpMatrix", ramses::EEffectUniformSemantic::ModelViewProjectionMatrix); - effectDesc.setAttributeSemantic("a_position", ramses::EEffectAttributeSemantic::TextPositions); - effectDesc.setAttributeSemantic("a_texcoord", ramses::EEffectAttributeSemantic::TextTextureCoordinates); - effectDesc.setUniformSemantic("u_texture", ramses::EEffectUniformSemantic::TextTexture); - - return *m_scene.createEffect(effectDesc, ramses::ResourceCacheFlag_DoNotCache, "text effect"); -} - -ramses::Effect& TextLayoutDemo::createColorQuadEffect() -{ - ramses::EffectDescription effectDesc; - effectDesc.setVertexShaderFromFile("res/ramses-layout-demo-colored-quad.vert"); - effectDesc.setFragmentShaderFromFile("res/ramses-layout-demo-colored-quad.frag"); - effectDesc.setUniformSemantic("mvpMatrix", ramses::EEffectUniformSemantic::ModelViewProjectionMatrix); - - return *m_scene.createEffect(effectDesc, ramses::ResourceCacheFlag_DoNotCache, "quad effect"); -} - -ramses::Camera* TextLayoutDemo::createOrthographicCamera() -{ - /// Create orthographic camera, so that text pixels will match screen pixels - ramses::OrthographicCamera* camera = m_scene.createOrthographicCamera(); - camera->setFrustum(0.0f, static_cast(m_windowWidth), 0.0f, static_cast(m_windowHeight), 0.1f, 1.f); - camera->setViewport(0, 0, m_windowWidth, m_windowHeight); - - return camera; -} diff --git a/demo/ramses-text-layout-demo/src/TextLayoutDemo.h b/demo/ramses-text-layout-demo/src/TextLayoutDemo.h deleted file mode 100644 index a6c2a5be4..000000000 --- a/demo/ramses-text-layout-demo/src/TextLayoutDemo.h +++ /dev/null @@ -1,65 +0,0 @@ -// ------------------------------------------------------------------------- -// Copyright (C) 2016 BMW Car IT GmbH -// ------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// ------------------------------------------------------------------------- - -#ifndef RAMSES_TEXT_TEXTLAYOUTDEMO_H -#define RAMSES_TEXT_TEXTLAYOUTDEMO_H - -#include "ramses-client.h" -#include "ramses-text-api/FontRegistry.h" -#include "ramses-text-api/FontInstanceOffsets.h" -#include "ramses-text-api/TextCache.h" -#include "ramses-text-api/GlyphMetrics.h" - -/// Text sample application visualizing font metrics in RAMSES text API. -/** Creates a font, text style, text node and inserts a single text object into the node. - * Shows the string "ExampleBasicText" on the screen. */ -class TextLayoutDemo -{ -public: - TextLayoutDemo(int argc, char* argv[]); - ~TextLayoutDemo(); - -private: - - ramses::Effect& createTextEffect(); - ramses::Effect& createColorQuadEffect(); - ramses::Camera* createOrthographicCamera(); - - void addColoredBox(ramses::Node& parent, float x, float y, float width, float height, float r, float g, float b, int32_t renderOrder); - - void layoutTextInABox( - const std::u32string& string, - const ramses::FontInstanceOffsets& fonts, - float boxX, float boxY, float boxWidth, float boxHeight); - - static ramses::GlyphMetricsVector::const_iterator FindFittingSubstring(ramses::GlyphMetricsVector::const_iterator first, ramses::GlyphMetricsVector::const_iterator last, uint32_t maxWidth); - - ramses::RamsesFramework m_framework; - ramses::RamsesClient& m_client; - ramses::Scene& m_scene; - - ramses::FontRegistry m_fontRegistry; - ramses::TextCache m_textCache; - ramses::Effect& m_textEffect; - - ramses::Effect& m_colorQuadEffect; - ramses::AttributeInput m_colorQuadEffectPositionsInput; - ramses::UniformInput m_colorQuadEffectBoxSizeInput; - ramses::UniformInput m_colorQuadEffectColorInput; - - ramses::RenderPass& m_renderPass; - ramses::RenderGroup& m_renderGroup; - - ramses::ArrayResource& m_quadIndices; - ramses::ArrayResource& m_quadVertices; - - const uint32_t m_windowWidth; - const uint32_t m_windowHeight; -}; - -#endif diff --git a/demo/ramses-text-shadow-demo/CMakeLists.txt b/demo/ramses-text-shadow-demo/CMakeLists.txt deleted file mode 100644 index 0c4b5b31a..000000000 --- a/demo/ramses-text-shadow-demo/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (C) 2018 BMW Car IT GmbH -# ------------------------------------------------------------------------- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -# ------------------------------------------------------------------------- - -MODULE_WITH_SHARED_LIBRARY( - - #========================================================================== - # general module information - #========================================================================== - NAME ramses-text-demo-shadow - TYPE BINARY - ENABLE_INSTALL ON - - #========================================================================== - # files of this module - #========================================================================== - FILES_PRIVATE_HEADER src/*.h - FILES_SOURCE src/*.cpp - RESOURCE_FOLDER res -) diff --git a/demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-Roboto-Bold.ttf b/demo/ramses-text-shadow-demo/res/ramses-text-shadow-demo-Roboto-Bold.ttf deleted file mode 100644 index d3f01ad245b628f386ac95786f53167038720eb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 170760 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�!3r53e9=KAt@g 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