From: David Steele Date: Wed, 28 Jun 2017 12:40:23 +0000 (+0100) Subject: Adding support for multiple images in AnimatedImageVisual X-Git-Tag: dali_1.2.49~1^2 X-Git-Url: http://review.tizen.org/git/?p=platform%2Fcore%2Fuifw%2Fdali-toolkit.git;a=commitdiff_plain;h=46322a558e537267a6d3c48630c45afca91b5e27 Adding support for multiple images in AnimatedImageVisual As well as supporting animated GIF, the animated image visual also supports animating between a number of images. This can be done using a property map containing the following: { "url":[ "url1", "url2", "url3", "url4", "url5" ], "batchSize" : 3, "cacheSize" : 4, "frameDelay" : 100 } The cache size is the total number of images that will be loaded at any one time; the batch size is the maximum number of images it will attempt to load before displaying. If the cache size is smaller than the batch size, it is increased to match. If either are larger than the number of urls, they are reduced to match. As soon as the visual is created, it will start loading up to batchSize urls into the cache. After each frame has completed, it will start loading the next batch, etc, until the cache is full. After the next frame, it will load only the next URL into the cache. If at any time, the next image is not ready, it will wait for the loading to complete before displaying and triggering the next batch/frame load, but will not reset the timer. Change-Id: Iccd59768aec814ab3b73fb7b817e45b95299b2c2 Signed-off-by: David Steele --- diff --git a/automated-tests/resources/application-icon-20.png b/automated-tests/resources/application-icon-20.png new file mode 100644 index 0000000..aecb4a6 Binary files /dev/null and b/automated-tests/resources/application-icon-20.png differ diff --git a/automated-tests/resources/application-icon-21.png b/automated-tests/resources/application-icon-21.png new file mode 100644 index 0000000..f5b1418 Binary files /dev/null and b/automated-tests/resources/application-icon-21.png differ diff --git a/automated-tests/resources/application-icon-22.png b/automated-tests/resources/application-icon-22.png new file mode 100644 index 0000000..4221262 Binary files /dev/null and b/automated-tests/resources/application-icon-22.png differ diff --git a/automated-tests/resources/application-icon-23.png b/automated-tests/resources/application-icon-23.png new file mode 100644 index 0000000..4e7507b Binary files /dev/null and b/automated-tests/resources/application-icon-23.png differ diff --git a/automated-tests/resources/application-icon-24.png b/automated-tests/resources/application-icon-24.png new file mode 100644 index 0000000..680257c Binary files /dev/null and b/automated-tests/resources/application-icon-24.png differ diff --git a/automated-tests/resources/application-icon-25.png b/automated-tests/resources/application-icon-25.png new file mode 100644 index 0000000..a404573 Binary files /dev/null and b/automated-tests/resources/application-icon-25.png differ diff --git a/automated-tests/resources/application-icon-26.png b/automated-tests/resources/application-icon-26.png new file mode 100644 index 0000000..10138e5 Binary files /dev/null and b/automated-tests/resources/application-icon-26.png differ diff --git a/automated-tests/resources/application-icon-27.png b/automated-tests/resources/application-icon-27.png new file mode 100644 index 0000000..5dbb3b3 Binary files /dev/null and b/automated-tests/resources/application-icon-27.png differ diff --git a/automated-tests/resources/application-icon-28.png b/automated-tests/resources/application-icon-28.png new file mode 100644 index 0000000..1d71a31 Binary files /dev/null and b/automated-tests/resources/application-icon-28.png differ diff --git a/automated-tests/resources/application-icon-29.png b/automated-tests/resources/application-icon-29.png new file mode 100644 index 0000000..040c36b Binary files /dev/null and b/automated-tests/resources/application-icon-29.png differ diff --git a/automated-tests/resources/application-icon-30.png b/automated-tests/resources/application-icon-30.png new file mode 100644 index 0000000..445590d Binary files /dev/null and b/automated-tests/resources/application-icon-30.png differ diff --git a/automated-tests/src/dali-toolkit-internal/utc-Dali-TextureManager.cpp b/automated-tests/src/dali-toolkit-internal/utc-Dali-TextureManager.cpp index c38a70c..2d06a44 100644 --- a/automated-tests/src/dali-toolkit-internal/utc-Dali-TextureManager.cpp +++ b/automated-tests/src/dali-toolkit-internal/utc-Dali-TextureManager.cpp @@ -34,7 +34,7 @@ public: { } - void UploadComplete( bool loadSuccess, TextureSet textureSet, bool useAtlasing, const Vector4& atlasRect ) + void UploadComplete( bool loadSuccess, int32_t textureId, TextureSet textureSet, bool useAtlasing, const Vector4& atlasRect ) { mLoaded = loadSuccess; mObserverCalled = true; diff --git a/automated-tests/src/dali-toolkit/CMakeLists.txt b/automated-tests/src/dali-toolkit/CMakeLists.txt index 39b37de..d3b5bba 100755 --- a/automated-tests/src/dali-toolkit/CMakeLists.txt +++ b/automated-tests/src/dali-toolkit/CMakeLists.txt @@ -8,6 +8,7 @@ SET(CAPI_LIB "dali-toolkit") # List of test case sources (Only these get parsed for test cases) SET(TC_SOURCES utc-Dali-Alignment.cpp + utc-Dali-AnimatedImageVisual.cpp utc-Dali-BloomView.cpp utc-Dali-BubbleEmitter.cpp utc-Dali-Builder.cpp diff --git a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/test-gl-abstraction.cpp b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/test-gl-abstraction.cpp index 6e9b9f0..c4bc01d 100644 --- a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/test-gl-abstraction.cpp +++ b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/test-gl-abstraction.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Samsung Electronics Co., Ltd. + * Copyright (c) 2017 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,7 @@ void TestGlAbstraction::Initialize() mLastBlendFuncSrcAlpha = 0; mLastBlendFuncDstAlpha = 0; mLastAutoTextureIdUsed = 0; + mNumGeneratedTextures = 0; mLastShaderIdUsed = 0; mLastProgramIdUsed = 0; mLastUniformIdUsed = 0; @@ -108,7 +109,7 @@ bool BlendEnabled(const Dali::TraceCallStack& callStack) { std::stringstream out; out << GL_BLEND; - bool blendEnabled = callStack.FindMethodAndParams("Enable", out.str()); + bool blendEnabled = callStack.FindMethodAndParams( "Enable", out.str() ); return blendEnabled; } @@ -116,6 +117,6 @@ bool BlendDisabled(const Dali::TraceCallStack& callStack) { std::stringstream out; out << GL_BLEND; - bool blendEnabled = callStack.FindMethodAndParams("Disable", out.str()); + bool blendEnabled = callStack.FindMethodAndParams( "Disable", out.str() ); return blendEnabled; } diff --git a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/test-gl-abstraction.h b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/test-gl-abstraction.h index 5e71779..8408b10 100644 --- a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/test-gl-abstraction.h +++ b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/test-gl-abstraction.h @@ -2,7 +2,7 @@ #define TEST_GL_ABSTRACTION_H /* - * Copyright (c) 2016 Samsung Electronics Co., Ltd. + * Copyright (c) 2017 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -426,6 +426,7 @@ public: paramName<<"texture["< mNextTextureIds; std::vector mDeletedTextureIds; std::vector mBoundTextures; diff --git a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-event-thread-callback.cpp b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-event-thread-callback.cpp index 0f037d3..5b8a324 100644 --- a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-event-thread-callback.cpp +++ b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-event-thread-callback.cpp @@ -99,10 +99,8 @@ CallbackBase* EventThreadCallback::GetCallback() namespace Test { -bool WaitForEventThreadTrigger( int triggerCount ) +bool WaitForEventThreadTrigger( int triggerCount, int timeoutInSeconds ) { - const int TEST_TIMEOUT(30); - struct timespec startTime; struct timespec now; clock_gettime( CLOCK_REALTIME, &startTime ); @@ -129,7 +127,7 @@ bool WaitForEventThreadTrigger( int triggerCount ) } } clock_gettime( CLOCK_REALTIME, &now ); - if( now.tv_sec - startTime.tv_sec > TEST_TIMEOUT ) + if( now.tv_sec - startTime.tv_sec > timeoutInSeconds ) { // Ensure we break out of the loop if elapsed time has passed break; diff --git a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-event-thread-callback.h b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-event-thread-callback.h index 1ae27d2..d718e10 100644 --- a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-event-thread-callback.h +++ b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-event-thread-callback.h @@ -66,7 +66,7 @@ namespace Test * * Will wait for a maximum of 30s before failing the test and returning. */ -bool WaitForEventThreadTrigger( int triggerCount ); +bool WaitForEventThreadTrigger( int triggerCount, int timeoutInSeconds=30 ); } diff --git a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-timer.cpp b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-timer.cpp index f46dacd..2259719 100644 --- a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-timer.cpp +++ b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-timer.cpp @@ -234,3 +234,18 @@ void Timer::MockEmitSignal() } // namespace Dali + +namespace Test +{ + +int GetTimerCount() +{ + return Dali::Internal::Adaptor::gTimerCount; +} + +void EmitGlobalTimerSignal() +{ + Dali::Internal::Adaptor::gTickSignal.Emit(); +} + +} diff --git a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-timer.h b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-timer.h index e0c886c..0110eaa 100644 --- a/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-timer.h +++ b/automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-timer.h @@ -62,4 +62,10 @@ private: } // namespace Dali +namespace Test +{ +int GetTimerCount(); +void EmitGlobalTimerSignal(); +} + #endif // __DALI_TOOLKIT_TOOLKIT_TIMER_H__ diff --git a/automated-tests/src/dali-toolkit/utc-Dali-AnimatedImageVisual.cpp b/automated-tests/src/dali-toolkit/utc-Dali-AnimatedImageVisual.cpp new file mode 100644 index 0000000..97f04ac --- /dev/null +++ b/automated-tests/src/dali-toolkit/utc-Dali-AnimatedImageVisual.cpp @@ -0,0 +1,553 @@ + +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "dummy-control.h" + +using namespace Dali; +using namespace Dali::Toolkit; + +void dali_animated_image_visual_startup(void) +{ + test_return_value = TET_UNDEF; +} + +void dali_animated_image_visual_cleanup(void) +{ + test_return_value = TET_PASS; +} + +namespace +{ +const char* TEST_IMAGE_FILE_NAME = TEST_RESOURCE_DIR "/application-icon-%02d.png"; +const char* TEST_GIF_FILE_NAME = TEST_RESOURCE_DIR "/anim.gif"; +} + + +void CopyUrlsIntoArray( Property::Array& urls, int startIndex=0 ) +{ + for( int i=20+startIndex;i<=30;++i) + { + char* url; + if(asprintf(&url, TEST_IMAGE_FILE_NAME, i) > 0) + { + Property::Value value(url); + urls.Add(value); + free(url); + } + } +} + +int UtcDaliAnimatedImageVisualGetPropertyMap01(void) +{ + ToolkitTestApplication application; + tet_infoline( "UtcDaliAnimatedImageVisualGetPropertyMap" ); + + // request AnimatedImageVisual with a property map + VisualFactory factory = VisualFactory::Get(); + Visual::Base animatedImageVisual = factory.CreateVisual( + Property::Map() + .Add( Visual::Property::TYPE, DevelVisual::ANIMATED_IMAGE ) + .Add( ImageVisual::Property::URL, TEST_GIF_FILE_NAME ) + .Add( ImageVisual::Property::PIXEL_AREA, Vector4() ) + .Add( ImageVisual::Property::WRAP_MODE_U, WrapMode::REPEAT ) + .Add( ImageVisual::Property::WRAP_MODE_V, WrapMode::DEFAULT )); + + Property::Map resultMap; + animatedImageVisual.CreatePropertyMap( resultMap ); + // check the property values from the returned map from a visual + Property::Value* value = resultMap.Find( Visual::Property::TYPE, Property::INTEGER ); + DALI_TEST_CHECK( value ); + DALI_TEST_CHECK( value->Get() == DevelVisual::ANIMATED_IMAGE ); + + value = resultMap.Find( ImageVisual::Property::URL, Property::STRING ); + DALI_TEST_CHECK( value ); + DALI_TEST_CHECK( value->Get() == TEST_GIF_FILE_NAME ); + + // request AnimatedImageVisual with an URL + Visual::Base animatedImageVisual2 = factory.CreateVisual( TEST_GIF_FILE_NAME, ImageDimensions() ); + resultMap.Clear(); + animatedImageVisual2.CreatePropertyMap( resultMap ); + // check the property values from the returned map from a visual + value = resultMap.Find( Visual::Property::TYPE, Property::INTEGER ); + DALI_TEST_CHECK( value ); + DALI_TEST_CHECK( value->Get() == DevelVisual::ANIMATED_IMAGE ); + + value = resultMap.Find( ImageVisual::Property::URL, Property::STRING ); + DALI_TEST_CHECK( value ); + DALI_TEST_CHECK( value->Get() == TEST_GIF_FILE_NAME ); + + END_TEST; +} + + +int UtcDaliAnimatedImageVisualGetPropertyMap02(void) +{ + ToolkitTestApplication application; + tet_infoline( "UtcDaliAnimatedImageVisualGetPropertyMap for multi image" ); + + // request AnimatedImageVisual with a property map + VisualFactory factory = VisualFactory::Get(); + Property::Array urls; + CopyUrlsIntoArray( urls ); + + Visual::Base animatedImageVisual = factory.CreateVisual( + Property::Map() + .Add( Visual::Property::TYPE, DevelVisual::ANIMATED_IMAGE ) + .Add( "url", urls ) + .Add( "batchSize", 4 ) + .Add( "cacheSize", 8 ) + .Add( "frameDelay", 200 ) + .Add( "pixelArea", Vector4() ) + .Add( "wrapModeU", WrapMode::REPEAT ) + .Add( "wrapModeV", WrapMode::DEFAULT )); + + Property::Map resultMap; + animatedImageVisual.CreatePropertyMap( resultMap ); + // check the property values from the returned map from a visual + Property::Value* value = resultMap.Find( Visual::Property::TYPE, Property::INTEGER ); + DALI_TEST_CHECK( value ); + DALI_TEST_CHECK( value->Get() == DevelVisual::ANIMATED_IMAGE ); + + value = resultMap.Find( DevelImageVisual::Property::URL, "url" ); + DALI_TEST_CHECK( value ); + Property::Array* resultUrls = value->GetArray(); + DALI_TEST_CHECK( resultUrls ); + DALI_TEST_EQUALS( resultUrls->Count(), urls.Count(), TEST_LOCATION ); + + value = resultMap.Find( DevelImageVisual::Property::BATCH_SIZE, "batchSize" ); + DALI_TEST_CHECK( value ); + DALI_TEST_EQUALS( value->Get(), 4, TEST_LOCATION ); + + value = resultMap.Find( DevelImageVisual::Property::CACHE_SIZE, "cacheSize" ); + DALI_TEST_CHECK( value ); + DALI_TEST_EQUALS( value->Get(), 8, TEST_LOCATION ); + + value = resultMap.Find( DevelImageVisual::Property::FRAME_DELAY, "frameDelay" ); + DALI_TEST_CHECK( value ); + DALI_TEST_EQUALS( value->Get(), 200, TEST_LOCATION ); + + END_TEST; +} + + + + +int UtcDaliAnimatedImageVisualMultiImage01(void) +{ + ToolkitTestApplication application; + TestGlAbstraction& gl = application.GetGlAbstraction(); + + Property::Array urls; + CopyUrlsIntoArray( urls ); + + { + Property::Map propertyMap; + propertyMap.Insert(Visual::Property::TYPE, Visual::IMAGE ); + propertyMap.Insert( DevelImageVisual::Property::URL, Property::Value(urls) ); + propertyMap.Insert( DevelImageVisual::Property::BATCH_SIZE, 4); + propertyMap.Insert( DevelImageVisual::Property::CACHE_SIZE, 8); + propertyMap.Insert( DevelImageVisual::Property::FRAME_DELAY, 100); + + VisualFactory factory = VisualFactory::Get(); + Visual::Base visual = factory.CreateVisual( propertyMap ); + + // Expect that a batch of 4 textures has been requested. These will be serially loaded + // below. + + DummyControl dummyControl = DummyControl::New(true); + Impl::DummyControl& dummyImpl = static_cast(dummyControl.GetImplementation()); + dummyImpl.RegisterVisual( DummyControl::Property::TEST_VISUAL, visual ); + + dummyControl.SetResizePolicy( ResizePolicy::FILL_TO_PARENT, Dimension::ALL_DIMENSIONS ); + Stage::GetCurrent().Add( dummyControl ); + application.SendNotification(); + application.Render(16); + + tet_infoline( "Ready the visual after the visual is on stage" ); + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 4 ), true, TEST_LOCATION ); + + tet_infoline( "Test that a timer has been started" ); + DALI_TEST_EQUALS( Test::GetTimerCount(), 1, TEST_LOCATION ); + + TraceCallStack& textureTrace = gl.GetTextureTrace(); + textureTrace.Enable(true); + + application.SendNotification(); + application.Render(16); + + DALI_TEST_EQUALS( gl.GetLastGenTextureId(), 4, TEST_LOCATION ); + DALI_TEST_EQUALS( textureTrace.FindMethod("BindTexture"), true, TEST_LOCATION ); + + tet_infoline( "Test that after 1 tick, and file loads completed, that we have 7 textures" ); + Test::EmitGlobalTimerSignal(); + + // Expect the second batch has been requested + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 4 ), true, TEST_LOCATION ); + + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 7, TEST_LOCATION ); + + + tet_infoline( "Test that after 2 ticks that we have 6 textures" ); + + Test::EmitGlobalTimerSignal(); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 6, TEST_LOCATION ); + + tet_infoline("And that at least 2 textures were requested"); + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 2 ), true, TEST_LOCATION ); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 8, TEST_LOCATION ); + + + tet_infoline( "Test that after 3rd tick that we have 7 textures and 1 request" ); + Test::EmitGlobalTimerSignal(); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 7, TEST_LOCATION ); + + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 1 ), true, TEST_LOCATION ); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 8, TEST_LOCATION ); + + dummyControl.Unparent(); + } + tet_infoline("Test that removing the visual from stage deletes all textures"); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 0, TEST_LOCATION ); + + END_TEST; +} + +int UtcDaliAnimatedImageVisualMultiImage02(void) +{ + ToolkitTestApplication application; + TestGlAbstraction& gl = application.GetGlAbstraction(); + + tet_infoline( "Test that the animated visual still works with zero sized cache" ); + + { + Property::Array urls; + CopyUrlsIntoArray( urls ); + + Property::Map propertyMap; + propertyMap.Insert(Visual::Property::TYPE, Visual::IMAGE ); + propertyMap.Insert( DevelImageVisual::Property::URL, Property::Value(urls) ); + propertyMap.Insert( DevelImageVisual::Property::BATCH_SIZE, 0); + propertyMap.Insert( DevelImageVisual::Property::CACHE_SIZE, 0); + propertyMap.Insert( DevelImageVisual::Property::FRAME_DELAY, 100); + + VisualFactory factory = VisualFactory::Get(); + Visual::Base visual = factory.CreateVisual( propertyMap ); // TexMgr::Request load tId:0 + + // Expect that each image is loaded each tick + + DummyControl dummyControl = DummyControl::New(true); + Impl::DummyControl& dummyImpl = static_cast(dummyControl.GetImplementation()); + dummyImpl.RegisterVisual( DummyControl::Property::TEST_VISUAL, visual ); + + dummyControl.SetResizePolicy( ResizePolicy::FILL_TO_PARENT, Dimension::ALL_DIMENSIONS ); + Stage::GetCurrent().Add( dummyControl ); + application.SendNotification(); + application.Render(16); + + tet_infoline( "Ready the visual after the visual is on stage" ); + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 1 ), true, TEST_LOCATION ); + application.SendNotification(); + application.Render(16);//glGenTextures 1 + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 1, TEST_LOCATION ); + + tet_infoline( "Test that each tick, a new image is requested" ); + Test::EmitGlobalTimerSignal(); // TexMgr::Remove tId:0 + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 1, 10 ), true, TEST_LOCATION ); + application.SendNotification(); + application.Render(16);//glGenTextures 2 + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 1, TEST_LOCATION ); + + tet_infoline( "Test that each tick, a new image is requested" ); + Test::EmitGlobalTimerSignal(); // Internal::~TextureSet() + application.SendNotification(); + application.Render(16);//glDeleteTextures 2 + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 1, 10 ), true, TEST_LOCATION ); + application.SendNotification(); + application.Render(16);//glGenTextures 3 + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 1, TEST_LOCATION ); + + tet_infoline( "Test that each tick, a new image is requested" ); + Test::EmitGlobalTimerSignal(); + application.SendNotification(); + application.Render(16);//glDeleteTextures 3 + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 1, 10 ), true, TEST_LOCATION ); + application.SendNotification(); + application.Render(16);//Gen4 + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 1, TEST_LOCATION ); + dummyControl.Unparent(); + } + tet_infoline("Test that removing the visual from stage deletes all textures"); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 0, TEST_LOCATION ); + + END_TEST; +} + +int UtcDaliAnimatedImageVisualMultiImage03(void) +{ + ToolkitTestApplication application; + TestGlAbstraction& gl = application.GetGlAbstraction(); + + { + Property::Array urls1, urls2; + CopyUrlsIntoArray( urls1 ); + CopyUrlsIntoArray( urls2 ); + + Property::Map animatedImageMap1; + animatedImageMap1.Insert(Visual::Property::TYPE, Visual::IMAGE ); + animatedImageMap1.Insert( DevelImageVisual::Property::URL, Property::Value(urls1) ); + animatedImageMap1.Insert( DevelImageVisual::Property::BATCH_SIZE, 3); + animatedImageMap1.Insert( DevelImageVisual::Property::CACHE_SIZE, 3); + animatedImageMap1.Insert( DevelImageVisual::Property::FRAME_DELAY, 100); + + Property::Map animatedImageMap2; + animatedImageMap2.Insert(Visual::Property::TYPE, Visual::IMAGE ); + animatedImageMap2.Insert( DevelImageVisual::Property::URL, Property::Value(urls2) ); + animatedImageMap2.Insert( DevelImageVisual::Property::BATCH_SIZE, 2); + animatedImageMap2.Insert( DevelImageVisual::Property::CACHE_SIZE, 2); + animatedImageMap2.Insert( DevelImageVisual::Property::FRAME_DELAY, 100); + + VisualFactory factory = VisualFactory::Get(); + Visual::Base animatedImageVisual1 = factory.CreateVisual( animatedImageMap1 ); + + tet_infoline( "Create two image views with the same URLs, offset by 1 frame."); + + DummyControl dummyControl1 = DummyControl::New(true); + Impl::DummyControl& dummyImpl1 = static_cast(dummyControl1.GetImplementation()); + dummyImpl1.RegisterVisual( DummyControl::Property::TEST_VISUAL, animatedImageVisual1 ); + dummyControl1.SetResizePolicy( ResizePolicy::FILL_TO_PARENT, Dimension::ALL_DIMENSIONS ); + Stage::GetCurrent().Add( dummyControl1 ); + + application.SendNotification(); + application.Render(16); + + tet_infoline( "Ready the requested image after the first visual is on stage" ); + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 3 ), true, TEST_LOCATION ); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 3, TEST_LOCATION ); + + Visual::Base animatedImageVisual2 = factory.CreateVisual( animatedImageMap2 ); + DummyControl dummyControl2 = DummyControl::New(true); + Impl::DummyControl& dummyImpl2 = static_cast(dummyControl2.GetImplementation()); + dummyImpl2.RegisterVisual( DummyControl::Property::TEST_VISUAL, animatedImageVisual2 ); + dummyControl2.SetResizePolicy( ResizePolicy::FILL_TO_PARENT, Dimension::ALL_DIMENSIONS ); + Stage::GetCurrent().Add( dummyControl2 ); + application.SendNotification(); + application.Render(16); + + tet_infoline( "The texture cache should be holding the requested images; check that the renderer has a texture" ); + TextureSet ts = dummyControl2.GetRendererAt(0).GetTextures(); + Texture t1 = ts.GetTexture( 0 ); + DALI_TEST_EQUALS( ts.GetTextureCount(), 1, TEST_LOCATION ); + + tet_infoline( "Test that on the first tick, 1 new image is requested" ); + Test::EmitGlobalTimerSignal(); // Both visuals should tick + + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 1 ), true, TEST_LOCATION ); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 3, TEST_LOCATION ); + + ts = dummyControl2.GetRendererAt(0).GetTextures(); + Texture t2 = ts.GetTexture( 0 ); + DALI_TEST_CHECK( t1 != t2 ); + + dummyControl1.Unparent(); + dummyControl2.Unparent(); + } + tet_infoline("Test that removing the visual from stage deletes all textures"); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 0, TEST_LOCATION ); + + END_TEST; +} + + +int UtcDaliAnimatedImageVisualMultiImage04(void) +{ + ToolkitTestApplication application; + TestGlAbstraction& gl = application.GetGlAbstraction(); + TraceCallStack& textureTrace = gl.GetTextureTrace(); + textureTrace.Enable(true); + + tet_infoline( "Test that if the cache size is the same as the number of urls, that once the cache is full, no new images are loaded" ); + + Property::Array urls; + CopyUrlsIntoArray( urls ); + + { + Property::Map propertyMap; + propertyMap.Insert(Visual::Property::TYPE, Visual::IMAGE ); + propertyMap.Insert( DevelImageVisual::Property::URL, Property::Value(urls) ); + propertyMap.Insert( DevelImageVisual::Property::BATCH_SIZE, 6); + propertyMap.Insert( DevelImageVisual::Property::CACHE_SIZE, 11); + propertyMap.Insert( DevelImageVisual::Property::FRAME_DELAY, 100); + + VisualFactory factory = VisualFactory::Get(); + Visual::Base visual = factory.CreateVisual( propertyMap ); + + tet_infoline( "Expect that a batch of 7 textures has been requested." ); + + DummyControl dummyControl = DummyControl::New(true); + Impl::DummyControl& dummyImpl = static_cast(dummyControl.GetImplementation()); + dummyImpl.RegisterVisual( DummyControl::Property::TEST_VISUAL, visual ); + + dummyControl.SetResizePolicy( ResizePolicy::FILL_TO_PARENT, Dimension::ALL_DIMENSIONS ); + Stage::GetCurrent().Add( dummyControl ); + application.SendNotification(); + application.Render(16); + + tet_infoline( "Wait for the first batch to complete" ); + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 6 ), true, TEST_LOCATION ); + + tet_infoline( "Test that a timer has been started" ); + DALI_TEST_EQUALS( Test::GetTimerCount(), 1, TEST_LOCATION ); + + application.SendNotification(); + application.Render(16); + + DALI_TEST_EQUALS( gl.GetLastGenTextureId(), 6, TEST_LOCATION ); + tet_infoline( "Test that after 1 tick, and 5 file loads completed, that we have 11 textures" ); + Test::EmitGlobalTimerSignal(); + application.SendNotification(); + application.Render(16); + + // Expect the second batch has been requested + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 5 ), true, TEST_LOCATION ); + + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 11, TEST_LOCATION ); + + tet_infoline( "Test that after 2 ticks that we have 11 textures and no requests" ); + + Test::EmitGlobalTimerSignal(); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 1, 5 ), false, TEST_LOCATION ); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 11, TEST_LOCATION ); + + tet_infoline( "Test that after 3rd tick that we have 11 textures and no requests" ); + Test::EmitGlobalTimerSignal(); + application.SendNotification(); + application.Render(16); + + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 1, 5 ), false, TEST_LOCATION ); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 11, TEST_LOCATION ); + + dummyControl.Unparent(); + } + + tet_infoline("Test that removing the visual from stage deletes all textures"); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 0, TEST_LOCATION ); + + END_TEST; +} + + +int UtcDaliAnimatedImageVisualMultiImage05(void) +{ + ToolkitTestApplication application; + TestGlAbstraction& gl = application.GetGlAbstraction(); + + tet_infoline( "Test that if the cache size is the same as the number of urls, that removing a partially loaded visual removes all textures" ); + + Property::Array urls; + CopyUrlsIntoArray( urls ); + + { + Property::Map propertyMap; + propertyMap.Insert(Visual::Property::TYPE, Visual::IMAGE ); + propertyMap.Insert( DevelImageVisual::Property::URL, Property::Value(urls) ); + propertyMap.Insert( DevelImageVisual::Property::BATCH_SIZE, 4); + propertyMap.Insert( DevelImageVisual::Property::CACHE_SIZE, 11); + propertyMap.Insert( DevelImageVisual::Property::FRAME_DELAY, 100); + + VisualFactory factory = VisualFactory::Get(); + Visual::Base visual = factory.CreateVisual( propertyMap ); + + tet_infoline( "Expect that a batch of 4 textures has been requested." ); + + DummyControl dummyControl = DummyControl::New(true); + Impl::DummyControl& dummyImpl = static_cast(dummyControl.GetImplementation()); + dummyImpl.RegisterVisual( DummyControl::Property::TEST_VISUAL, visual ); + + dummyControl.SetResizePolicy( ResizePolicy::FILL_TO_PARENT, Dimension::ALL_DIMENSIONS ); + Stage::GetCurrent().Add( dummyControl ); + application.SendNotification(); + application.Render(16); + + tet_infoline( "Wait for the first batch to complete" ); + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 4 ), true, TEST_LOCATION ); + + tet_infoline( "Test that a timer has been started" ); + DALI_TEST_EQUALS( Test::GetTimerCount(), 1, TEST_LOCATION ); + + application.SendNotification(); + application.Render(16); + + tet_infoline( "Test that a timer has been started" ); + Test::EmitGlobalTimerSignal(); + application.SendNotification(); + application.Render(16); + + dummyControl.Unparent(); + } + + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 0, TEST_LOCATION ); + + tet_infoline( "Test that pending batch of image loads are cancelled instead of uploaded"); + DALI_TEST_EQUALS( Test::WaitForEventThreadTrigger( 4 ), true, TEST_LOCATION ); + application.SendNotification(); + application.Render(16); + DALI_TEST_EQUALS( gl.GetNumGeneratedTextures(), 0, TEST_LOCATION ); + + END_TEST; +} diff --git a/automated-tests/src/dali-toolkit/utc-Dali-Visual.cpp b/automated-tests/src/dali-toolkit/utc-Dali-Visual.cpp index f8e61b8..3c774c7 100644 --- a/automated-tests/src/dali-toolkit/utc-Dali-Visual.cpp +++ b/automated-tests/src/dali-toolkit/utc-Dali-Visual.cpp @@ -39,10 +39,10 @@ using namespace Dali::Toolkit; namespace { +const char* TEST_GIF_FILE_NAME = TEST_RESOURCE_DIR "/anim.gif"; const char* TEST_IMAGE_FILE_NAME = TEST_RESOURCE_DIR "/gallery-small-1.jpg"; const char* TEST_NPATCH_FILE_NAME = "gallery_image_01.9.jpg"; const char* TEST_SVG_FILE_NAME = TEST_RESOURCE_DIR "/svg1.svg"; -const char* TEST_GIF_FILE_NAME = TEST_RESOURCE_DIR "/anim.gif"; const char* TEST_OBJ_FILE_NAME = TEST_RESOURCE_DIR "/Cube.obj"; const char* TEST_MTL_FILE_NAME = TEST_RESOURCE_DIR "/ToyRobot-Metal.mtl"; const char* TEST_RESOURCE_LOCATION = TEST_RESOURCE_DIR "/"; @@ -1081,44 +1081,6 @@ int UtcDaliVisualGetPropertyMap10(void) END_TEST; } -int UtcDaliVisualGetPropertyMap11(void) -{ - ToolkitTestApplication application; - tet_infoline( "UtcDaliVisualGetPropertyMap11: AnimatedImageVisual" ); - - // request AnimatedImageVisual with a property map - VisualFactory factory = VisualFactory::Get(); - Visual::Base animatedImageVisual = factory.CreateVisual( Property::Map() - .Add( Visual::Property::TYPE, DevelVisual::ANIMATED_IMAGE ) - .Add( ImageVisual::Property::URL, TEST_GIF_FILE_NAME ) ); - - Property::Map resultMap; - animatedImageVisual.CreatePropertyMap( resultMap ); - // check the property values from the returned map from a visual - Property::Value* value = resultMap.Find( Visual::Property::TYPE, Property::INTEGER ); - DALI_TEST_CHECK( value ); - DALI_TEST_CHECK( value->Get() == DevelVisual::ANIMATED_IMAGE ); - - value = resultMap.Find( ImageVisual::Property::URL, Property::STRING ); - DALI_TEST_CHECK( value ); - DALI_TEST_CHECK( value->Get() == TEST_GIF_FILE_NAME ); - - // request AnimatedImageVisual with an URL - Visual::Base animatedImageVisual2 = factory.CreateVisual( TEST_GIF_FILE_NAME, ImageDimensions() ); - resultMap.Clear(); - animatedImageVisual2.CreatePropertyMap( resultMap ); - // check the property values from the returned map from a visual - value = resultMap.Find( Visual::Property::TYPE, Property::INTEGER ); - DALI_TEST_CHECK( value ); - DALI_TEST_CHECK( value->Get() == DevelVisual::ANIMATED_IMAGE ); - - value = resultMap.Find( ImageVisual::Property::URL, Property::STRING ); - DALI_TEST_CHECK( value ); - DALI_TEST_CHECK( value->Get() == TEST_GIF_FILE_NAME ); - - END_TEST; -} - int UtcDaliVisualAnimateBorderVisual01(void) { ToolkitTestApplication application; diff --git a/dali-toolkit/devel-api/visuals/image-visual-properties-devel.h b/dali-toolkit/devel-api/visuals/image-visual-properties-devel.h index 0471ae2..501e61c 100644 --- a/dali-toolkit/devel-api/visuals/image-visual-properties-devel.h +++ b/dali-toolkit/devel-api/visuals/image-visual-properties-devel.h @@ -76,6 +76,31 @@ enum Type */ ALPHA_MASK_URL = WRAP_MODE_V + 3, + + /** + * @brief Defines the batch size for pre-loading images in the AnimatedImageVisual + * @details Name "batchSize", type Property::INTEGER, number of images to pre-load + * before starting to play. Default value: 1 + */ + BATCH_SIZE = WRAP_MODE_V + 4, + + /** + * @brief Defines the cache size for loading images in the AnimatedImageVisual + * @details Name "cacheSize", type Property::INTEGER, number of images to keep + * cached ahead during playback. Default value: 1 + * + * @note, cacheSize should be >= batchSize. + * If it isn't, then the cache will automatically be changed to batchSize. + * @note, because of the defaults, it is expected that the application developer + * tune the batch and cache sizes to their particular use case. + */ + CACHE_SIZE = WRAP_MODE_V + 5, + + /** + * @brief The number of milliseconds between each frame in the AnimatedImageVisual + * @details Name "frameDelay", type Property::INTEGER, The number of milliseconds between each frame. Note, this is only used with the URLS property above. + */ + FRAME_DELAY = WRAP_MODE_V + 6, }; } //namespace Property diff --git a/dali-toolkit/internal/controls/image-view/image-view-impl.cpp b/dali-toolkit/internal/controls/image-view/image-view-impl.cpp index 4183b89..7934109 100644 --- a/dali-toolkit/internal/controls/image-view/image-view-impl.cpp +++ b/dali-toolkit/internal/controls/image-view/image-view-impl.cpp @@ -64,7 +64,8 @@ DALI_TYPE_REGISTRATION_END() using namespace Dali; ImageView::ImageView() -: Control( ControlBehaviour( CONTROL_BEHAVIOUR_DEFAULT ) ) +: Control( ControlBehaviour( CONTROL_BEHAVIOUR_DEFAULT ) ), + mRelayoutRequired(true) { } @@ -227,6 +228,11 @@ void ImageView::OnRelayout( const Vector2& size, RelayoutContainer& container ) void ImageView::OnResourceReady( Toolkit::Control control ) { + if( mRelayoutRequired) + { + mRelayoutRequired = false; + RelayoutRequest(); + } } /////////////////////////////////////////////////////////// diff --git a/dali-toolkit/internal/controls/image-view/image-view-impl.h b/dali-toolkit/internal/controls/image-view/image-view-impl.h index f335ce7..dc449e9 100644 --- a/dali-toolkit/internal/controls/image-view/image-view-impl.h +++ b/dali-toolkit/internal/controls/image-view/image-view-impl.h @@ -166,6 +166,7 @@ private: std::string mUrl; ///< the url for the image if the image came from a URL, empty otherwise Image mImage; ///< the Image if the image came from a Image, null otherwise Property::Map mPropertyMap; ///< the Property::Map if the image came from a Property::Map, empty otherwise + bool mRelayoutRequired; ///< True if relayout is required, e.g. due to unreadiness }; } // namespace Internal diff --git a/dali-toolkit/internal/file.list b/dali-toolkit/internal/file.list index 67b6c9e..9b6809f 100644 --- a/dali-toolkit/internal/file.list +++ b/dali-toolkit/internal/file.list @@ -12,6 +12,9 @@ toolkit_src_files = \ $(toolkit_src_dir)/builder/tree-node-manipulator.cpp \ $(toolkit_src_dir)/builder/replacement.cpp \ $(toolkit_src_dir)/visuals/animated-image/animated-image-visual.cpp \ + $(toolkit_src_dir)/visuals/animated-image/image-cache.cpp \ + $(toolkit_src_dir)/visuals/animated-image/fixed-image-cache.cpp \ + $(toolkit_src_dir)/visuals/animated-image/rolling-image-cache.cpp \ $(toolkit_src_dir)/visuals/border/border-visual.cpp \ $(toolkit_src_dir)/visuals/color/color-visual.cpp \ $(toolkit_src_dir)/visuals/gradient/gradient-visual.cpp \ diff --git a/dali-toolkit/internal/visuals/animated-image/animated-image-visual.cpp b/dali-toolkit/internal/visuals/animated-image/animated-image-visual.cpp index 0ba7833..be736e1 100644 --- a/dali-toolkit/internal/visuals/animated-image/animated-image-visual.cpp +++ b/dali-toolkit/internal/visuals/animated-image/animated-image-visual.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 Samsung Electronics Co., Ltd. + * Copyright (c) 2017 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,14 +20,19 @@ // EXTERNAL INCLUDES #include +#include +#include // INTERNAL INCLUDES #include +#include #include #include #include #include #include +#include +#include #include #include @@ -52,8 +57,42 @@ DALI_ENUM_TO_STRING_TABLE_END( WRAP_MODE ) const Vector4 FULL_TEXTURE_RECT(0.f, 0.f, 1.f, 1.f); +#if defined(DEBUG_ENABLED) +Debug::Filter* gAnimImgLogFilter = Debug::Filter::New(Debug::NoLogging, false, "LOG_ANIMATED_IMAGE"); +#endif } + +/** + * Multi-image Flow of execution + * + * | DoSetProperties() + * | LoadFirstBatch() + * | cache->LoadBatch() + * | + * | DoSetOnStage() + * | CreateRenderer() (Doesn't become ready until first frame loads) + * | + * | FrameReady(textureSet) + * | start first frame: + * | actor.AddRenderer + * | start timer + * | mRenderer.SetTextures(textureSet) + * | + * | Timer ticks + * | DisplayNextFrame() + * | if front frame is ready, + * | mRenderer.SetTextures( front frame's texture ) + * | else + * | mWaitingForTexture=true + * | cache->LoadBatch() + * | + * | FrameReady(textureSet) + * | mRenderer.SetTextures(textureSet) + * V + * Time + */ + AnimatedImageVisualPtr AnimatedImageVisual::New( VisualFactoryCache& factoryCache, const VisualUrl& imageUrl, const Property::Map& properties ) { AnimatedImageVisual* visual = new AnimatedImageVisual( factoryCache ); @@ -63,6 +102,25 @@ AnimatedImageVisualPtr AnimatedImageVisual::New( VisualFactoryCache& factoryCach return visual; } +AnimatedImageVisualPtr AnimatedImageVisual::New( VisualFactoryCache& factoryCache, const Property::Array& imageUrls, const Property::Map& properties ) +{ + AnimatedImageVisual* visual = new AnimatedImageVisual( factoryCache ); + visual->mImageUrls = new ImageCache::UrlList(); + visual->mImageUrls->reserve( imageUrls.Count() ); + + for( unsigned int i=0; i < imageUrls.Count(); ++i) + { + ImageCache::UrlStore urlStore; + urlStore.mTextureId = TextureManager::INVALID_TEXTURE_ID; + urlStore.mUrl = imageUrls[i].Get(); + visual->mImageUrls->push_back( urlStore ); + } + + visual->SetProperties( properties ); + // starts loading immediately + return visual; +} + AnimatedImageVisualPtr AnimatedImageVisual::New( VisualFactoryCache& factoryCache, const VisualUrl& imageUrl ) { AnimatedImageVisual* visual = new AnimatedImageVisual( factoryCache ); @@ -74,23 +132,40 @@ AnimatedImageVisualPtr AnimatedImageVisual::New( VisualFactoryCache& factoryCach AnimatedImageVisual::AnimatedImageVisual( VisualFactoryCache& factoryCache ) : Visual::Base( factoryCache ), mFrameDelayTimer(), + mPlacementActor(), mPixelArea( FULL_TEXTURE_RECT ), mImageUrl(), - mImageSize(), mCurrentFrameIndex( 0 ), + mImageUrls( NULL ), + mImageCache( NULL ), + mCacheSize( 1 ), + mBatchSize( 1 ), + mFrameDelay( 100 ), + mUrlIndex( 0 ), + mImageSize(), mWrapModeU( WrapMode::DEFAULT ), - mWrapModeV( WrapMode::DEFAULT ) + mWrapModeV( WrapMode::DEFAULT ), + mStartFirstFrame(false) {} AnimatedImageVisual::~AnimatedImageVisual() { + delete mImageCache; + delete mImageUrls; } void AnimatedImageVisual::GetNaturalSize( Vector2& naturalSize ) { if( mImageSize.GetWidth() == 0 && mImageSize.GetHeight() == 0) { - mImageSize = Dali::GetGifImageSize( mImageUrl.GetUrl() ); + if( mImageUrl.IsValid() ) + { + mImageSize = Dali::GetGifImageSize( mImageUrl.GetUrl() ); + } + else if( mImageUrls && mImageUrls->size() > 0 ) + { + mImageSize = Dali::GetClosestImageSize( (*mImageUrls)[0].mUrl ); + } } naturalSize.width = mImageSize.GetWidth(); @@ -107,10 +182,24 @@ void AnimatedImageVisual::DoCreatePropertyMap( Property::Map& map ) const { map.Insert( Toolkit::ImageVisual::Property::URL, mImageUrl.GetUrl() ); } + if( mImageUrls != NULL && ! mImageUrls->empty() ) + { + Property::Array urls; + for( unsigned int i=0; isize(); ++i) + { + urls.Add( (*mImageUrls)[i].mUrl ); + } + Property::Value value( const_cast(urls) ); + map.Insert( Toolkit::ImageVisual::Property::URL, value ); + } map.Insert( Toolkit::ImageVisual::Property::PIXEL_AREA, mPixelArea ); map.Insert( Toolkit::ImageVisual::Property::WRAP_MODE_U, mWrapModeU ); map.Insert( Toolkit::ImageVisual::Property::WRAP_MODE_V, mWrapModeV ); + + map.Insert( Toolkit::DevelImageVisual::Property::BATCH_SIZE, static_cast(mBatchSize) ); + map.Insert( Toolkit::DevelImageVisual::Property::CACHE_SIZE, static_cast(mCacheSize) ); + map.Insert( Toolkit::DevelImageVisual::Property::FRAME_DELAY, static_cast(mFrameDelay) ); } void AnimatedImageVisual::DoCreateInstancePropertyMap( Property::Map& map ) const @@ -120,84 +209,126 @@ void AnimatedImageVisual::DoCreateInstancePropertyMap( Property::Map& map ) cons void AnimatedImageVisual::DoSetProperties( const Property::Map& propertyMap ) { - // url already passed in from constructor + // url[s] already passed in from constructor - Property::Value* pixelAreaValue = propertyMap.Find( Toolkit::ImageVisual::Property::PIXEL_AREA, PIXEL_AREA_UNIFORM_NAME ); - if( pixelAreaValue ) + for( Property::Map::SizeType iter = 0; iter < propertyMap.Count(); ++iter ) { - pixelAreaValue->Get( mPixelArea ); - } - - Property::Value* wrapModeValueU = propertyMap.Find( Toolkit::ImageVisual::Property::WRAP_MODE_U, IMAGE_WRAP_MODE_U ); - if( wrapModeValueU ) - { - int value; - Scripting::GetEnumerationProperty( *wrapModeValueU, WRAP_MODE_TABLE, WRAP_MODE_TABLE_COUNT, value ); - mWrapModeU = Dali::WrapMode::Type( value ); + KeyValuePair keyValue = propertyMap.GetKeyValue( iter ); + if( keyValue.first.type == Property::Key::INDEX ) + { + DoSetProperty( keyValue.first.indexKey, keyValue.second ); + } + else + { + if( keyValue.first == PIXEL_AREA_UNIFORM_NAME ) + { + DoSetProperty( Toolkit::ImageVisual::Property::PIXEL_AREA, keyValue.second ); + } + else if( keyValue.first == IMAGE_WRAP_MODE_U ) + { + DoSetProperty( Toolkit::ImageVisual::Property::WRAP_MODE_U, keyValue.second ); + } + else if( keyValue.first == IMAGE_WRAP_MODE_V ) + { + DoSetProperty( Toolkit::ImageVisual::Property::WRAP_MODE_V, keyValue.second ); + } + else if( keyValue.first == BATCH_SIZE_NAME ) + { + DoSetProperty( Toolkit::DevelImageVisual::Property::BATCH_SIZE, keyValue.second ); + } + else if( keyValue.first == CACHE_SIZE_NAME ) + { + DoSetProperty( Toolkit::DevelImageVisual::Property::CACHE_SIZE, keyValue.second ); + } + else if( keyValue.first == FRAME_DELAY_NAME ) + { + DoSetProperty( Toolkit::DevelImageVisual::Property::FRAME_DELAY, keyValue.second ); + } + } } - Property::Value* wrapModeValueV = propertyMap.Find( Toolkit::ImageVisual::Property::WRAP_MODE_V, IMAGE_WRAP_MODE_V ); - if( wrapModeValueV ) + if( mImageUrls && mImageUrls->size() > 0 ) { - int value; - Scripting::GetEnumerationProperty( *wrapModeValueV, WRAP_MODE_TABLE, WRAP_MODE_TABLE_COUNT, value ); - mWrapModeV = Dali::WrapMode::Type( value ); + LoadFirstBatch(); } } -void AnimatedImageVisual::DoSetOnStage( Actor& actor ) +void AnimatedImageVisual::DoSetProperty( Property::Index index, + const Property::Value& value ) { - Texture texture = PrepareAnimatedImage(); - if( texture ) // if the image loading is successful + switch(index) { - bool defaultWrapMode = mWrapModeU <= WrapMode::CLAMP_TO_EDGE && mWrapModeV <= WrapMode::CLAMP_TO_EDGE; - Shader shader = ImageVisual::GetImageShader( mFactoryCache, true, defaultWrapMode ); - - Geometry geometry = mFactoryCache.GetGeometry( VisualFactoryCache::QUAD_GEOMETRY ); - - TextureSet textureSet = TextureSet::New(); - textureSet.SetTexture( 0u, texture ); - - mImpl->mRenderer = Renderer::New( geometry, shader ); - mImpl->mRenderer.SetTextures( textureSet ); - - // Register transform properties - mImpl->mTransform.RegisterUniforms( mImpl->mRenderer, Direction::LEFT_TO_RIGHT ); + case Toolkit::ImageVisual::Property::PIXEL_AREA: + { + value.Get( mPixelArea ); + break; + } + case Toolkit::ImageVisual::Property::WRAP_MODE_U: + { + int wrapMode; + Scripting::GetEnumerationProperty( value, WRAP_MODE_TABLE, WRAP_MODE_TABLE_COUNT, wrapMode ); + mWrapModeU = Dali::WrapMode::Type( wrapMode ); + break; + } + case Toolkit::ImageVisual::Property::WRAP_MODE_V: + { + int wrapMode; + Scripting::GetEnumerationProperty( value, WRAP_MODE_TABLE, WRAP_MODE_TABLE_COUNT, wrapMode ); + mWrapModeV = Dali::WrapMode::Type( wrapMode ); + break; + } - if( !defaultWrapMode ) // custom wrap mode + case Toolkit::DevelImageVisual::Property::BATCH_SIZE: { - Vector2 wrapMode(mWrapModeU-WrapMode::CLAMP_TO_EDGE, mWrapModeV-WrapMode::CLAMP_TO_EDGE); - wrapMode.Clamp( Vector2::ZERO, Vector2( 2.f, 2.f ) ); - mImpl->mRenderer.RegisterProperty( WRAP_MODE_UNIFORM_NAME, wrapMode ); + int batchSize; + if( value.Get( batchSize ) ) + { + mBatchSize = batchSize; + } + break; } - if( mPixelArea != FULL_TEXTURE_RECT ) + case Toolkit::DevelImageVisual::Property::CACHE_SIZE: { - mImpl->mRenderer.RegisterProperty( PIXEL_AREA_UNIFORM_NAME, mPixelArea ); + int cacheSize; + if( value.Get( cacheSize ) ) + { + mCacheSize = cacheSize; + } + break; } - mCurrentFrameIndex = 0; - mImpl->mRenderer.RegisterProperty( ATLAS_RECT_UNIFORM_NAME, mTextureRectContainer[mCurrentFrameIndex] ); - if( mFrameDelayContainer.Count() > 1 ) + case Toolkit::DevelImageVisual::Property::FRAME_DELAY: { - mFrameDelayTimer = Timer::New( mFrameDelayContainer[0] ); - mFrameDelayTimer.TickSignal().Connect( this, &AnimatedImageVisual::DisplayNextFrame ); - mFrameDelayTimer.Start(); + int frameDelay; + if( value.Get( frameDelay ) ) + { + mFrameDelay = frameDelay; + } + break; } + } +} - actor.AddRenderer( mImpl->mRenderer ); +void AnimatedImageVisual::DoSetOnStage( Actor& actor ) +{ + mPlacementActor = actor; + TextureSet textureSet = PrepareTextureSet(); + CreateRenderer(); // Always create a renderer when on stage - // Animated Image loaded and ready to display - ResourceReady(); + if( textureSet ) // if the image loading is successful + { + StartFirstFrame( textureSet ); + } + else + { + mStartFirstFrame = true; } } void AnimatedImageVisual::DoSetOffStage( Actor& actor ) { - if( !mImpl->mRenderer ) - { - return; - } + DALI_ASSERT_DEBUG( (bool)mImpl->mRenderer && "There should always be a renderer whilst on stage"); if( mFrameDelayTimer ) { @@ -210,6 +341,7 @@ void AnimatedImageVisual::DoSetOffStage( Actor& actor ) actor.RemoveRenderer( mImpl->mRenderer ); mImpl->mRenderer.Reset(); + mPlacementActor.Reset(); } void AnimatedImageVisual::OnSetTransform() @@ -220,8 +352,117 @@ void AnimatedImageVisual::OnSetTransform() } } -Texture AnimatedImageVisual::PrepareAnimatedImage() +void AnimatedImageVisual::CreateRenderer() { + bool defaultWrapMode = mWrapModeU <= WrapMode::CLAMP_TO_EDGE && mWrapModeV <= WrapMode::CLAMP_TO_EDGE; + bool atlasing = (mTextureRectContainer.Count() > 0) ; + Shader shader = ImageVisual::GetImageShader( mFactoryCache, atlasing, defaultWrapMode ); + + Geometry geometry = mFactoryCache.GetGeometry( VisualFactoryCache::QUAD_GEOMETRY ); + + mImpl->mRenderer = Renderer::New( geometry, shader ); + + // Register transform properties + mImpl->mTransform.RegisterUniforms( mImpl->mRenderer, Direction::LEFT_TO_RIGHT ); + + if( !defaultWrapMode ) // custom wrap mode + { + Vector2 wrapMode(mWrapModeU-WrapMode::CLAMP_TO_EDGE, mWrapModeV-WrapMode::CLAMP_TO_EDGE); + wrapMode.Clamp( Vector2::ZERO, Vector2( 2.f, 2.f ) ); + mImpl->mRenderer.RegisterProperty( WRAP_MODE_UNIFORM_NAME, wrapMode ); + } + + if( mPixelArea != FULL_TEXTURE_RECT ) + { + mImpl->mRenderer.RegisterProperty( PIXEL_AREA_UNIFORM_NAME, mPixelArea ); + } + + mCurrentFrameIndex = 0; + + if( mTextureRectContainer.Count() > 0 ) + { + mImpl->mRenderer.RegisterProperty( ATLAS_RECT_UNIFORM_NAME, mTextureRectContainer[mCurrentFrameIndex] ); + } +} + +void AnimatedImageVisual::LoadFirstBatch() +{ + DALI_LOG_INFO(gAnimImgLogFilter,Debug::Concise,"AnimatedImageVisual::LoadFirstBatch()\n"); + + // Ensure the batch size and cache size are no bigger than the number of URLs, + // and that the cache is at least as big as the batch size. + uint16_t numUrls = mImageUrls->size(); + uint16_t batchSize = std::min( mBatchSize, numUrls ); + uint16_t cacheSize = std::min( std::max( batchSize, mCacheSize ), numUrls ); + + mUrlIndex = 0; + TextureManager& textureManager = mFactoryCache.GetTextureManager(); + + if( batchSize > 0 && cacheSize > 0 ) + { + if( cacheSize < numUrls ) + { + mImageCache = new RollingImageCache( textureManager, *mImageUrls, *this, cacheSize, batchSize ); + } + else + { + mImageCache = new FixedImageCache( textureManager, *mImageUrls, *this, batchSize ); + } + } + else + { + mImageCache = new RollingImageCache( textureManager, *mImageUrls, *this, 1, 1 ); + } +} + +void AnimatedImageVisual::StartFirstFrame( TextureSet& textureSet ) +{ + DALI_LOG_INFO(gAnimImgLogFilter,Debug::Concise,"AnimatedImageVisual::StartFirstFrame()\n"); + + mStartFirstFrame = false; + mImpl->mRenderer.SetTextures( textureSet ); + Actor actor = mPlacementActor.GetHandle(); + if( actor ) + { + actor.AddRenderer( mImpl->mRenderer ); + mPlacementActor.Reset(); + } + + int frameDelay = mFrameDelay; + if( mFrameDelayContainer.Count() > 1 ) + { + frameDelay = mFrameDelayContainer[0]; + } + mFrameDelayTimer = Timer::New( frameDelay ); + mFrameDelayTimer.TickSignal().Connect( this, &AnimatedImageVisual::DisplayNextFrame ); + mFrameDelayTimer.Start(); + + DALI_LOG_INFO(gAnimImgLogFilter,Debug::Concise,"ResourceReady()\n"); + ResourceReady(); +} + +TextureSet AnimatedImageVisual::PrepareTextureSet() +{ + TextureSet textureSet; + if( mImageUrl.IsValid() ) + { + textureSet = PrepareAnimatedGifImage(); + } + else + { + textureSet = mImageCache->FirstFrame(); + if( textureSet ) + { + SetImageSize( textureSet ); + } + } + return textureSet; +} + +TextureSet AnimatedImageVisual::PrepareAnimatedGifImage() +{ + TextureSet textureSet; + // load from image file std::vector pixelDataList; @@ -232,22 +473,70 @@ Texture AnimatedImageVisual::PrepareAnimatedImage() mImageSize.SetWidth( pixelDataList[0].GetWidth() ); mImageSize.SetHeight( pixelDataList[0].GetHeight() ); - return Toolkit::ImageAtlas::PackToAtlas( pixelDataList, mTextureRectContainer ); + Texture texture = Toolkit::ImageAtlas::PackToAtlas( pixelDataList, mTextureRectContainer ); + textureSet = TextureSet::New(); + textureSet.SetTexture( 0, texture ); } } - return Texture(); + return textureSet; +} + +void AnimatedImageVisual::SetImageSize( TextureSet& textureSet ) +{ + if( textureSet ) + { + Texture texture = textureSet.GetTexture( 0 ); + if( texture ) + { + mImageSize.SetWidth( texture.GetWidth() ); + mImageSize.SetHeight( texture.GetHeight() ); + } + } +} + +void AnimatedImageVisual::FrameReady( TextureSet textureSet ) +{ + SetImageSize( textureSet ); + + if( mStartFirstFrame ) + { + StartFirstFrame( textureSet ); + } + else + { + mImpl->mRenderer.SetTextures( textureSet ); + } } bool AnimatedImageVisual::DisplayNextFrame() { - mCurrentFrameIndex = (mCurrentFrameIndex+1) % mFrameDelayContainer.Count(); - mImpl->mRenderer.RegisterProperty( ATLAS_RECT_UNIFORM_NAME, mTextureRectContainer[mCurrentFrameIndex] ); - if( mFrameDelayTimer.GetInterval() != mFrameDelayContainer[mCurrentFrameIndex] ) + DALI_LOG_INFO(gAnimImgLogFilter,Debug::Concise,"AnimatedImageVisual::DisplayNextFrame() start\n"); + + if( mFrameDelayContainer.Count() > 0 ) + { + // Wrap the frame index + ++mCurrentFrameIndex; + mCurrentFrameIndex %= mFrameDelayContainer.Count(); + + mImpl->mRenderer.RegisterProperty( ATLAS_RECT_UNIFORM_NAME, mTextureRectContainer[mCurrentFrameIndex] ); + + if( mFrameDelayTimer.GetInterval() != mFrameDelayContainer[mCurrentFrameIndex] ) + { + mFrameDelayTimer.SetInterval( mFrameDelayContainer[mCurrentFrameIndex] ); + } + } + else if( mImageCache ) { - mFrameDelayTimer.SetInterval( mFrameDelayContainer[mCurrentFrameIndex] ); + TextureSet textureSet = mImageCache->NextFrame(); + if( textureSet ) + { + SetImageSize( textureSet ); + mImpl->mRenderer.SetTextures( textureSet ); + } } + // Keep timer ticking return true; } diff --git a/dali-toolkit/internal/visuals/animated-image/animated-image-visual.h b/dali-toolkit/internal/visuals/animated-image/animated-image-visual.h index 4e11cce..2796c20 100644 --- a/dali-toolkit/internal/visuals/animated-image/animated-image-visual.h +++ b/dali-toolkit/internal/visuals/animated-image/animated-image-visual.h @@ -19,14 +19,15 @@ */ // EXTERNAL INCLUDES -#include #include +#include #include #include // INTERNAL INCLUDES #include #include +#include namespace Dali { @@ -43,29 +44,41 @@ typedef IntrusivePtr< AnimatedImageVisual > AnimatedImageVisualPtr; /** * The visual which renders an animated image * - * The following property is essential + * One of the following properties is mandatory * * | %Property Name | Type | * |------------------- |-------------------| * | url | STRING | + * | urls | ARRAY of STRING | + * + * The remaining properties are optional * | pixelArea | VECTOR4 | * | wrapModeU | INTEGER OR STRING | * | wrapModeV | INTEGER OR STRING | + * | cacheSize | INTEGER | + * | batchSize | INTEGER | + * | frameDelay | INTEGER | * - * where pixelArea is a rectangular area. + * pixelArea is a rectangular area. * In its Vector4 value, the first two elements indicate the top-left position of the area, * and the last two elements are the area width and height respectively. * If not specified, the default value is [0.0, 0.0, 1.0, 1.0], i.e. the entire area of the image. * - * where wrapModeU and wrapModeV separately decide how the texture should be sampled when the u and v coordinate exceeds the range of 0.0 to 1.0. + * wrapModeU and wrapModeV separately decide how the texture should be sampled when the u and v coordinate exceeds the range of 0.0 to 1.0. * Its value should be one of the following wrap mode: * "DEFAULT" * "CLAMP_TO_EDGE" * "REPEAT" * "MIRRORED_REPEAT" + * + * cacheSize is used with multiple images - it determines how many images are kept pre-loaded + * batchSize is used with multiple images - it determines how many images to load on each frame + * frameDelay is used with multiple images - it is the number of milliseconds between each frame */ -class AnimatedImageVisual : public Visual::Base, public ConnectionTracker +class AnimatedImageVisual : public Visual::Base, + public ConnectionTracker, + public ImageCache::FrameReadyObserver { public: @@ -74,13 +87,23 @@ public: * @brief Create the animated image Visual using the image URL. * * @param[in] factoryCache A pointer pointing to the VisualFactoryCache object - * @param[in] imageUrl The URL to svg resource to use + * @param[in] imageUrl The URL to gif resource to use * @param[in] properties A Property::Map containing settings for this visual * @return A smart-pointer to the newly allocated visual. */ static AnimatedImageVisualPtr New( VisualFactoryCache& factoryCache, const VisualUrl& imageUrl, const Property::Map& properties ); /** + * @brief Create the animated image Visual using image URLs. + * + * @param[in] factoryCache A pointer pointing to the VisualFactoryCache object + * @param[in] imageUrls A Property::Array containing the URLs to the image resources + * @param[in] properties A Property::Map containing settings for this visual + * @return A smart-pointer to the newly allocated visual. + */ + static AnimatedImageVisualPtr New( VisualFactoryCache& factoryCache, const Property::Array& imageUrls, const Property::Map& properties ); + + /** * @brief Create the animated image visual using the image URL. * * @param[in] factoryCache A pointer pointing to the VisualFactoryCache object @@ -125,6 +148,13 @@ protected: virtual void DoSetProperties( const Property::Map& propertyMap ); /** + * Helper method to set individual values by index key. + * @param[in] index The index key of the value + * @param[in] value The value + */ + void DoSetProperty( Property::Index index, const Property::Value& value ); + + /** * @copydoc Visual::Base::DoSetOnStage */ virtual void DoSetOnStage( Actor& actor ); @@ -140,18 +170,50 @@ protected: virtual void OnSetTransform(); private: + /** + * Creates the renderer for the animated image + */ + void CreateRenderer(); /** - * Load the animated image and pack the frames into atlas. - * @return That atlas texture. + * Starts the Load of the first batch of URLs */ - Texture PrepareAnimatedImage(); + void LoadFirstBatch(); /** - * Display the next frame. It is called when the mFrameDelayTimes ticks. + * Adds the texture set to the renderer, and the renderer to the + * placement actor, and starts the frame timer + */ + void StartFirstFrame( TextureSet& textureSet ); + + /** + * Prepares the texture set for displaying + */ + TextureSet PrepareTextureSet(); + + /** + * Load the gif image and pack the frames into atlas. + * @return The atlas texture. + */ + TextureSet PrepareAnimatedGifImage(); + + /** + * Set the image size from the texture set + */ + void SetImageSize( TextureSet& textureSet ); + + /** + * Called when the next frame is ready. + */ + void FrameReady( TextureSet textureSet ); + + /** + * Display the next frame. It is called when the mFrameDelayTimer ticks. + * Returns true to ensure the timer continues running. */ bool DisplayNextFrame(); + // Undefined AnimatedImageVisual( const AnimatedImageVisual& animatedImageVisual ); @@ -161,15 +223,29 @@ private: private: Timer mFrameDelayTimer; + WeakHandle mPlacementActor; + + // Variables for GIF player Dali::Vector mTextureRectContainer; Dali::Vector mFrameDelayContainer; Vector4 mPixelArea; VisualUrl mImageUrl; + uint32_t mCurrentFrameIndex; // Frame index into textureRects + // Variables for Multi-Image player + ImageCache::UrlList* mImageUrls; + ImageCache* mImageCache; + uint16_t mCacheSize; + uint16_t mBatchSize; + uint16_t mFrameDelay; + uint16_t mUrlIndex; + + // Shared variables ImageDimensions mImageSize; - uint32_t mCurrentFrameIndex; + Dali::WrapMode::Type mWrapModeU:3; Dali::WrapMode::Type mWrapModeV:3; + bool mStartFirstFrame:1; }; } // namespace Internal diff --git a/dali-toolkit/internal/visuals/animated-image/fixed-image-cache.cpp b/dali-toolkit/internal/visuals/animated-image/fixed-image-cache.cpp new file mode 100644 index 0000000..d60834f --- /dev/null +++ b/dali-toolkit/internal/visuals/animated-image/fixed-image-cache.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// CLASS HEADER +#include + +namespace Dali +{ +namespace Toolkit +{ +namespace Internal +{ + +FixedImageCache::FixedImageCache( + TextureManager& textureManager, UrlList& urlList, ImageCache::FrameReadyObserver& observer, + unsigned int batchSize ) +: ImageCache( textureManager, urlList, observer, batchSize ), + mFront(0u) +{ + mReadyFlags.reserve( mImageUrls.size() ); + LoadBatch(); +} + +FixedImageCache::~FixedImageCache() +{ + for( std::size_t i = 0; i < mImageUrls.size() ; ++i ) + { + mTextureManager.Remove( mImageUrls[i].mTextureId ); + } +} + +TextureSet FixedImageCache::FirstFrame() +{ + TextureSet textureSet = GetFrontTextureSet(); + + if( ! textureSet ) + { + mWaitingForReadyFrame = true; + } + + return textureSet; +} + +TextureSet FixedImageCache::NextFrame() +{ + TextureSet textureSet; + ++mFront; + mFront %= mImageUrls.size(); + + if( IsFrontReady() == true ) + { + textureSet = GetFrontTextureSet(); + } + else + { + mWaitingForReadyFrame = true; + } + + LoadBatch(); + + return textureSet; +} + +bool FixedImageCache::IsFrontReady() const +{ + return ( mReadyFlags.size() > 0 && mReadyFlags[mFront] == true ); +} + +void FixedImageCache::LoadBatch() +{ + // Try and load up to mBatchSize images, until the cache is filled. + // Once the cache is filled, mUrlIndex exceeds mImageUrls size and + // no more images are loaded. + bool frontFrameReady = IsFrontReady();; + + for( unsigned int i=0; i< mBatchSize && mUrlIndex < mImageUrls.size(); ++i ) + { + std::string& url = mImageUrls[ mUrlIndex ].mUrl; + + mReadyFlags.push_back(false); + + // Note, if the image is already loaded, then UploadComplete will get called + // from within this method. This means it won't yet have a texture id, so we + // need to account for this inside the UploadComplete method using mRequestingLoad. + mRequestingLoad = true; + + mImageUrls[ mUrlIndex ].mTextureId = + mTextureManager.RequestLoad( url, ImageDimensions(), FittingMode::SCALE_TO_FILL, + SamplingMode::BOX_THEN_LINEAR, TextureManager::NO_ATLAS, + this ); + mRequestingLoad = false; + ++mUrlIndex; + } + + CheckFrontFrame( frontFrameReady ); +} + +void FixedImageCache::SetImageFrameReady( TextureManager::TextureId textureId ) +{ + for( std::size_t i = 0; i < mImageUrls.size() ; ++i ) + { + if( mImageUrls[i].mTextureId == textureId ) + { + mReadyFlags[i] = true; + break; + } + } +} + +TextureSet FixedImageCache::GetFrontTextureSet() const +{ + return mTextureManager.GetTextureSet( mImageUrls[mFront].mTextureId ); +} + +void FixedImageCache::CheckFrontFrame( bool wasReady ) +{ + if( mWaitingForReadyFrame && wasReady == false && IsFrontReady() ) + { + mWaitingForReadyFrame = false; + mObserver.FrameReady( GetFrontTextureSet() ); + } +} + +void FixedImageCache::UploadComplete( + bool loadSuccess, + int32_t textureId, + TextureSet textureSet, + bool useAtlasing, + const Vector4& atlasRect ) +{ + bool frontFrameReady = IsFrontReady(); + + if( ! mRequestingLoad ) + { + SetImageFrameReady( textureId ); + + CheckFrontFrame( frontFrameReady ); + } + else + { + // UploadComplete has been called from within RequestLoad. TextureManager must + // therefore already have the texture cached, so make the texture ready. + // (Use the last texture, as the texture id hasn't been assigned yet) + mReadyFlags.back() = true; + } +} + +} //namespace Internal +} //namespace Toolkit +} //namespace Dali diff --git a/dali-toolkit/internal/visuals/animated-image/fixed-image-cache.h b/dali-toolkit/internal/visuals/animated-image/fixed-image-cache.h new file mode 100644 index 0000000..b30a11a --- /dev/null +++ b/dali-toolkit/internal/visuals/animated-image/fixed-image-cache.h @@ -0,0 +1,109 @@ +#ifndef DALI_TOOLKIT_INTERNAL_FIXED_IMAGE_CACHE_H +#define DALI_TOOLKIT_INTERNAL_FIXED_IMAGE_CACHE_H + +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// EXTERNAL INCLUDES +#include +#include + +namespace Dali +{ +namespace Toolkit +{ +namespace Internal +{ + +class FixedImageCache : public ImageCache +{ +public: + /** + * Constructor. + * @param[in] textureManager The texture manager + * @param[in] urlList List of urls to cache + * @param[in] observer FrameReady observer + * @param[in] batchSize The size of a batch to load + * + * This will start loading textures immediately, according to the + * batch and cache sizes. The cache is as large as the number of urls. + */ + FixedImageCache( TextureManager& textureManager, + UrlList& urlList, + ImageCache::FrameReadyObserver& observer, + unsigned int batchSize ); + + virtual ~FixedImageCache(); + + /** + * Get the first frame. If it's not ready, this will trigger the + * sending of FrameReady() when the image becomes ready. + */ + virtual TextureSet FirstFrame(); + + /** + * Get the next frame. If it's not ready, this will trigger the + * sending of FrameReady() when the image becomes ready. + * This will trigger the loading of the next batch. + */ + virtual TextureSet NextFrame(); + +private: + /** + * @return true if the front frame is ready + */ + bool IsFrontReady() const; + + /** + * Load the next batch of images + */ + void LoadBatch(); + + /** + * Find the matching image frame, and set it to ready + */ + void SetImageFrameReady( TextureManager::TextureId textureId ); + + /** + * Get the texture set of the front frame. + * @return the texture set + */ + TextureSet GetFrontTextureSet() const; + + /** + * Check if the front frame has become ready - if so, inform observer + * @param[in] wasReady Readiness before call. + */ + void CheckFrontFrame( bool wasReady ); + +protected: + virtual void UploadComplete( + bool loadSuccess, + int32_t textureId, + TextureSet textureSet, + bool useAtlasing, + const Vector4& atlasRect ); + +protected: + std::vector mReadyFlags; + unsigned int mFront; +}; + +} //namespace Internal +} //namespace Toolkit +} //namespace Dali + +#endif // DALI_TOOLKIT_INTERNAL_FIXED_IMAGE_CACHE_H diff --git a/dali-toolkit/internal/visuals/animated-image/image-cache.cpp b/dali-toolkit/internal/visuals/animated-image/image-cache.cpp new file mode 100644 index 0000000..1d0db6f --- /dev/null +++ b/dali-toolkit/internal/visuals/animated-image/image-cache.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "image-cache.h" + +namespace Dali +{ +namespace Toolkit +{ +namespace Internal +{ + +ImageCache::ImageCache( TextureManager& textureManager, + UrlList& urlList, + ImageCache::FrameReadyObserver& observer, + unsigned int batchSize ) +: mTextureManager( textureManager ), + mObserver( observer ), + mImageUrls( urlList ), + mBatchSize( batchSize ), + mUrlIndex(0u), + mWaitingForReadyFrame(false), + mRequestingLoad(false) +{ +} + +ImageCache::~ImageCache() +{ +} + +} //namespace Internal +} //namespace Toolkit +} //namespace Dali diff --git a/dali-toolkit/internal/visuals/animated-image/image-cache.h b/dali-toolkit/internal/visuals/animated-image/image-cache.h new file mode 100644 index 0000000..5061d76 --- /dev/null +++ b/dali-toolkit/internal/visuals/animated-image/image-cache.h @@ -0,0 +1,103 @@ +#ifndef DALI_TOOLKIT_INTERNAL_IMAGE_CACHE_H +#define DALI_TOOLKIT_INTERNAL_IMAGE_CACHE_H + +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// EXTERNAL INCLUDES +#include +#include + +namespace Dali +{ +namespace Toolkit +{ +namespace Internal +{ + +class ImageCache : public TextureUploadObserver +{ +public: + /** + * Observer class to inform when the next image is ready + */ + class FrameReadyObserver + { + public: + /** + * Informs observer when the next texture set is ready to display + * @param[in] textureSet The ready texture set + */ + virtual void FrameReady( TextureSet textureSet ) = 0; + }; + + struct UrlStore + { + TextureManager::TextureId mTextureId; + std::string mUrl; + }; + + /** + * List of URLs + */ + typedef std::vector UrlList; + +public: + /** + * Constructor. + * @param[in] textureManager The texture manager + * @param[in] urlList List of urls to cache + * @param[in] observer FrameReady observer + * @param[in] batchSize The size of a batch to load + * + * This will start loading textures immediately, according to the + * batch and cache sizes. The cache is as large as the number of urls. + */ + ImageCache( TextureManager& textureManager, + UrlList& urlList, + ImageCache::FrameReadyObserver& observer, + unsigned int batchSize ); + + virtual ~ImageCache(); + + /** + * Get the first frame. If it's not ready, this will trigger the + * sending of FrameReady() when the image becomes ready. + */ + virtual TextureSet FirstFrame() = 0; + + /** + * Get the next frame. If it's not ready, this will trigger the + * sending of FrameReady() when the image becomes ready. + * This will trigger the loading of the next batch. + */ + virtual TextureSet NextFrame() = 0; + +protected: + TextureManager& mTextureManager; + FrameReadyObserver& mObserver; + std::vector& mImageUrls; + unsigned int mBatchSize; + unsigned int mUrlIndex; + bool mWaitingForReadyFrame:1; + bool mRequestingLoad:1; +}; + +} //namespace Internal +} //namespace Toolkit +} //namespace Dali + +#endif // DALI_TOOLKIT_INTERNAL_IMAGE_CACHE_H diff --git a/dali-toolkit/internal/visuals/animated-image/rolling-image-cache.cpp b/dali-toolkit/internal/visuals/animated-image/rolling-image-cache.cpp new file mode 100644 index 0000000..8abe059 --- /dev/null +++ b/dali-toolkit/internal/visuals/animated-image/rolling-image-cache.cpp @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// CLASS HEADER +#include + +// INTERNAL HEADERS +#include + +// EXTERNAL HEADERS + +namespace +{ +#if defined(DEBUG_ENABLED) +Debug::Filter* gAnimImgLogFilter = Debug::Filter::New(Debug::NoLogging, false, "LOG_ANIMATED_IMAGE"); + +#define LOG_CACHE \ + { \ + std::ostringstream oss; \ + oss<<"Size:"< +#include +#include + +namespace Dali +{ +namespace Toolkit +{ +namespace Internal +{ + +/** + * Class to manage a rolling cache of images, where the cache size + * is smaller than the total number of images. + */ +class RollingImageCache : public ImageCache +{ +public: + /** + * Constructor. + * @param[in] textureManager The texture manager + * @param[in] urlList List of urls to cache + * @param[in] observer FrameReady observer + * @param[in] cacheSize The size of the cache + * @param[in] batchSize The size of a batch to load + * + * This will start loading textures immediately, according to the + * batch and cache sizes. + */ + RollingImageCache( TextureManager& textureManager, + UrlList& urlList, + ImageCache::FrameReadyObserver& observer, + uint16_t cacheSize, + uint16_t batchSize ); + + /** + * Destructor + */ + virtual ~RollingImageCache(); + + /** + * Get the first frame. If it's not ready, this will trigger the + * sending of FrameReady() when the image becomes ready. + */ + virtual TextureSet FirstFrame(); + + /** + * Get the next frame. If it's not ready, this will trigger the + * sending of FrameReady() when the image becomes ready. + * This will trigger the loading of the next batch. + */ + virtual TextureSet NextFrame(); + +private: + /** + * @return true if the front frame is ready + */ + bool IsFrontReady() const; + + /** + * Load the next batch of images + */ + void LoadBatch(); + + /** + * Find the matching image frame, and set it to ready + */ + void SetImageFrameReady( TextureManager::TextureId textureId ); + + /** + * Get the texture set of the front frame. + * @return the texture set + */ + TextureSet GetFrontTextureSet() const; + + /** + * Get the texture id of the given index + */ + TextureManager::TextureId GetCachedTextureId( int index ) const; + + /** + * Check if the front frame has become ready - if so, inform observer + * @param[in] wasReady Readiness before call. + */ + void CheckFrontFrame( bool wasReady ); + +protected: + virtual void UploadComplete( + bool loadSuccess, + int32_t textureId, + TextureSet textureSet, + bool useAtlasing, + const Vector4& atlasRect ); + +private: + /** + * Secondary class to hold readiness and index into url + */ + struct ImageFrame + { + unsigned int mUrlIndex; + bool mReady; + }; + + CircularQueue mQueue; +}; + +} // namespace Internal +} // namespace Toolkit +} // namespace Dali + +#endif diff --git a/dali-toolkit/internal/visuals/image/image-visual.cpp b/dali-toolkit/internal/visuals/image/image-visual.cpp index d050442..7ce2f23 100644 --- a/dali-toolkit/internal/visuals/image/image-visual.cpp +++ b/dali-toolkit/internal/visuals/image/image-visual.cpp @@ -962,7 +962,7 @@ void ImageVisual::UploadCompleted() } // From Texture Manager -void ImageVisual::UploadComplete( bool loadingSuccess, TextureSet textureSet, bool usingAtlas, const Vector4& atlasRectangle ) +void ImageVisual::UploadComplete( bool loadingSuccess, int32_t textureId, TextureSet textureSet, bool usingAtlas, const Vector4& atlasRectangle ) { Actor actor = mPlacementActor.GetHandle(); if( actor ) diff --git a/dali-toolkit/internal/visuals/image/image-visual.h b/dali-toolkit/internal/visuals/image/image-visual.h index 31dc79f..ce03e6f 100644 --- a/dali-toolkit/internal/visuals/image/image-visual.h +++ b/dali-toolkit/internal/visuals/image/image-visual.h @@ -239,7 +239,7 @@ public: * To avoid rendering garbage pixels, renderer should be added to actor after the resources are ready. * This callback is the place to add the renderer as it would be called once the loading is finished. */ - virtual void UploadComplete( bool success, TextureSet textureSet, bool usingAtlas, const Vector4& atlasRectangle ); + virtual void UploadComplete( bool success, int32_t textureId, TextureSet textureSet, bool usingAtlas, const Vector4& atlasRectangle ); private: diff --git a/dali-toolkit/internal/visuals/texture-manager.cpp b/dali-toolkit/internal/visuals/texture-manager.cpp index 7334b48..b125a9e 100644 --- a/dali-toolkit/internal/visuals/texture-manager.cpp +++ b/dali-toolkit/internal/visuals/texture-manager.cpp @@ -169,9 +169,8 @@ TextureManager::TextureId TextureManager::RequestLoadInternal( { // The Texture has already loaded. The other observers have already been notified. // We need to send a "late" loaded notification for this observer. - observer->UploadComplete( true, - textureInfo.textureSet, textureInfo.useAtlas, - textureInfo.atlasRect ); + observer->UploadComplete( true, textureInfo.textureId, textureInfo.textureSet, + textureInfo.useAtlas, textureInfo.atlasRect ); } break; } @@ -201,7 +200,6 @@ void TextureManager::Remove( const TextureManager::TextureId textureId ) { TextureInfo& textureInfo( mTextureInfoContainer[ textureInfoIndex ] ); - DALI_LOG_INFO( gTextureManagerLogFilter, Debug::Concise, "TextureManager::Remove(%d) cacheIdx:%d loadState:%s\n", textureId, textureInfoIndex, textureInfo.loadState == TextureManager::NOT_STARTED ? "NOT_STARTED" : @@ -483,8 +481,8 @@ void TextureManager::NotifyObservers( TextureInfo& textureInfo, bool success ) { TextureId textureId = textureInfo.textureId; - // If there is an observer: Notify the load is complete, whether successful or not: - // And erase it from the list + // If there is an observer: Notify the load is complete, whether successful or not, + // and erase it from the list unsigned int observerCount = textureInfo.observerList.Count(); TextureInfo* info = &textureInfo; @@ -492,26 +490,29 @@ void TextureManager::NotifyObservers( TextureInfo& textureInfo, bool success ) { TextureUploadObserver* observer = info->observerList[0]; - // During UploadComplete() a Control ResourceReady() signal is emitted - // During that signal the app may add remove /add Textures (e.g. via ImageViews). - // At this point no more observers can be added to the observerList, because textureInfo.loadState = UPLOADED - // However it is possible for observers to be removed, hence we check the observer list count every iteration - - // Also the reference to the textureInfo struct can become invalidated, because new load requests can modify - // the mTextureInfoContainer list (e.g. if more requests are pushed_back it can cause the list to be resized - // invalidating the reference to the TextureInfo ). - observer->UploadComplete( success, info->textureSet, info->useAtlas, info->atlasRect ); + // During UploadComplete() a Control ResourceReady() signal is emitted. + // During that signal the app may add remove /add Textures (e.g. via + // ImageViews). At this point no more observers can be added to the + // observerList, because textureInfo.loadState = UPLOADED. However it is + // possible for observers to be removed, hence we check the observer list + // count every iteration. + + // The reference to the textureInfo struct can also become invalidated, + // because new load requests can modify the mTextureInfoContainer list + // (e.g. if more requests are pushed back it can cause the list to be + // resized invalidating the reference to the TextureInfo ). + observer->UploadComplete( success, info->textureId, info->textureSet, info->useAtlas, info->atlasRect ); observer->DestructionSignal().Disconnect( this, &TextureManager::ObserverDestroyed ); - // regrab the textureInfo from the container as it may have been invalidated, if textures have been removed - // or added during the ResourceReady() signal emission (from UploadComplete() ) - int textureInfoIndex = GetCacheIndexFromId( textureId ); + // Get the textureInfo from the container again as it may have been + // invalidated, + int textureInfoIndex = GetCacheIndexFromId( textureId ); if( textureInfoIndex == INVALID_CACHE_INDEX) { - // texture has been removed - return; + return; // texture has been removed - can stop. } + info = &mTextureInfoContainer[ textureInfoIndex ]; observerCount = info->observerList.Count(); if ( observerCount > 0 ) @@ -524,15 +525,12 @@ void TextureManager::NotifyObservers( TextureInfo& textureInfo, bool success ) info->observerList.Erase( j ); observerCount--; break; - } + } } } - } - } - TextureManager::TextureId TextureManager::GenerateUniqueTextureId() { return mCurrentTextureId++; diff --git a/dali-toolkit/internal/visuals/texture-upload-observer.h b/dali-toolkit/internal/visuals/texture-upload-observer.h index c300274..e5c09fe 100644 --- a/dali-toolkit/internal/visuals/texture-upload-observer.h +++ b/dali-toolkit/internal/visuals/texture-upload-observer.h @@ -57,11 +57,12 @@ public: * This should be overridden by the deriving class. * * @param[in] loadSuccess True if the texture load was successful (i.e. the resource is available). If false, then the resource failed to load. In future, this will automatically upload a "broken" image. + * @param[in] textureId The textureId of the loaded texture in the TextureManager * @param[in] textureSet The TextureSet containing the Texture * @param[in] useAtlasing True if atlasing was used (note: this may be different to what was requested) * @param[in] atlasRect If using atlasing, this is the rectangle within the atlas to use. */ - virtual void UploadComplete( bool loadSuccess, TextureSet textureSet, bool useAtlasing, const Vector4& atlasRect ) = 0; + virtual void UploadComplete( bool loadSuccess, int32_t textureId, TextureSet textureSet, bool useAtlasing, const Vector4& atlasRect ) = 0; /** * @brief Returns the destruction signal. diff --git a/dali-toolkit/internal/visuals/visual-factory-impl.cpp b/dali-toolkit/internal/visuals/visual-factory-impl.cpp index 8a5d245..469ead5 100644 --- a/dali-toolkit/internal/visuals/visual-factory-impl.cpp +++ b/dali-toolkit/internal/visuals/visual-factory-impl.cpp @@ -119,31 +119,42 @@ Toolkit::Visual::Base VisualFactory::CreateVisual( const Property::Map& property { Property::Value* imageURLValue = propertyMap.Find( Toolkit::ImageVisual::Property::URL, IMAGE_URL_NAME ); std::string imageUrl; - if( imageURLValue && imageURLValue->Get( imageUrl ) ) + if( imageURLValue ) { - VisualUrl visualUrl( imageUrl ); - - switch( visualUrl.GetType() ) + if( imageURLValue->Get( imageUrl ) ) { - case VisualUrl::N_PATCH: - { - visualPtr = NPatchVisual::New( *( mFactoryCache.Get() ), visualUrl, propertyMap ); - break; - } - case VisualUrl::SVG: - { - visualPtr = SvgVisual::New( *( mFactoryCache.Get() ), visualUrl, propertyMap ); - break; - } - case VisualUrl::GIF: + VisualUrl visualUrl( imageUrl ); + + switch( visualUrl.GetType() ) { - visualPtr = AnimatedImageVisual::New( *( mFactoryCache.Get() ), visualUrl, propertyMap ); - break; + case VisualUrl::N_PATCH: + { + visualPtr = NPatchVisual::New( *( mFactoryCache.Get() ), visualUrl, propertyMap ); + break; + } + case VisualUrl::SVG: + { + visualPtr = SvgVisual::New( *( mFactoryCache.Get() ), visualUrl, propertyMap ); + break; + } + case VisualUrl::GIF: + { + visualPtr = AnimatedImageVisual::New( *( mFactoryCache.Get() ), visualUrl, propertyMap ); + break; + } + case VisualUrl::REGULAR_IMAGE: + { + visualPtr = ImageVisual::New( *( mFactoryCache.Get() ), visualUrl, propertyMap ); + break; + } } - case VisualUrl::REGULAR_IMAGE: + } + else + { + Property::Array* array = imageURLValue->GetArray(); + if( array ) { - visualPtr = ImageVisual::New( *( mFactoryCache.Get() ), visualUrl, propertyMap ); - break; + visualPtr = AnimatedImageVisual::New( *( mFactoryCache.Get() ), *array, propertyMap ); } } } @@ -200,9 +211,20 @@ Toolkit::Visual::Base VisualFactory::CreateVisual( const Property::Map& property { Property::Value* imageURLValue = propertyMap.Find( Toolkit::ImageVisual::Property::URL, IMAGE_URL_NAME ); std::string imageUrl; - if( imageURLValue && imageURLValue->Get( imageUrl ) ) + if( imageURLValue ) { - visualPtr = AnimatedImageVisual::New( *( mFactoryCache.Get() ), imageUrl, propertyMap ); + if( imageURLValue->Get( imageUrl ) ) + { + visualPtr = AnimatedImageVisual::New( *( mFactoryCache.Get() ), imageUrl, propertyMap ); + } + else + { + Property::Array* array = imageURLValue->GetArray(); + if( array ) + { + visualPtr = AnimatedImageVisual::New( *( mFactoryCache.Get() ), *array, propertyMap ); + } + } } break; } diff --git a/dali-toolkit/internal/visuals/visual-string-constants.cpp b/dali-toolkit/internal/visuals/visual-string-constants.cpp index 1fc3272..81af2a0 100644 --- a/dali-toolkit/internal/visuals/visual-string-constants.cpp +++ b/dali-toolkit/internal/visuals/visual-string-constants.cpp @@ -80,6 +80,10 @@ const char * const IMAGE_WRAP_MODE_U("wrapModeU"); const char * const IMAGE_WRAP_MODE_V("wrapModeV"); const char * const IMAGE_BORDER( "border" ); const char * const PIXEL_ALIGNED_UNIFORM_NAME( "uPixelAligned" ); +const char * const ANIMATED_IMAGE_URLS_NAME("urls"); +const char * const BATCH_SIZE_NAME("batchSize"); +const char * const CACHE_SIZE_NAME("cacheSize"); +const char * const FRAME_DELAY_NAME("frameDelay"); // Text visual const char * const TEXT_PROPERTY( "text" ); diff --git a/dali-toolkit/internal/visuals/visual-string-constants.h b/dali-toolkit/internal/visuals/visual-string-constants.h index d57733c..3bbb95e 100644 --- a/dali-toolkit/internal/visuals/visual-string-constants.h +++ b/dali-toolkit/internal/visuals/visual-string-constants.h @@ -67,7 +67,10 @@ extern const char * const IMAGE_WRAP_MODE_U; extern const char * const IMAGE_WRAP_MODE_V; extern const char * const IMAGE_BORDER; extern const char * const PIXEL_ALIGNED_UNIFORM_NAME; - +extern const char * const ANIMATED_IMAGE_URLS_NAME; +extern const char * const BATCH_SIZE_NAME; +extern const char * const CACHE_SIZE_NAME; +extern const char * const FRAME_DELAY_NAME; // Text visual extern const char * const TEXT_PROPERTY; diff --git a/dali-toolkit/public-api/visuals/image-visual-properties.h b/dali-toolkit/public-api/visuals/image-visual-properties.h index edd94cc..44d5692 100644 --- a/dali-toolkit/public-api/visuals/image-visual-properties.h +++ b/dali-toolkit/public-api/visuals/image-visual-properties.h @@ -2,7 +2,7 @@ #define DALI_TOOLKIT_IMAGE_VISUAL_PROPERTIES_H /* - * Copyright (c) 2016 Samsung Electronics Co., Ltd. + * Copyright (c) 2017 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,8 @@ enum { /** * @brief The URL of the image. - * @details Name "url", type Property::STRING. + * @details Name "url", type Property::STRING or Property::ARRAY of Property::STRING + * @note The array form is used for generating animated image visuals. * @SINCE_1_1.45 * @note Mandatory. */