From 8d1310981009027c13eaa1c46600955e60508c7c Mon Sep 17 00:00:00 2001 From: Eunki Hong Date: Sun, 3 Sep 2023 15:05:44 +0900 Subject: [PATCH] [Scene3D] Cache image load result so models can share it. Let we cache the PixelData and Texture so various models can use it. We cache the model resources, and let we use them same resources. But if some seperated models using same images, we cannot cache about it. There are relative usecase occured + Unity support this kind of cache system, let we also cache images + textures locally. --- First, we can cache url --> PixelData. This caching might occured on multi threading system. So we have to lock the mutex when we try to access this kind of pixeldata access. Second, we can cache PixelData --> Texture. This caching should be occured only for main thread. --- We support Garbage Collect system to avoid full-iterating cached resources. During GC, we should remove Texture first, and then PixelData. (Since PixelData can be the key of Texture.) Currenly, we will call GC only of ModelCache reference count become 0. Change-Id: I5e89f214593503fa9e8b2290c3859f2674ff7048 Signed-off-by: Eunki Hong --- automated-tests/resources/AnimatedCube2.gltf | 458 +++++++++++++++++ automated-tests/resources/AnimatedCube2.metadata | 17 + automated-tests/resources/AnimatedCube3.gltf | 458 +++++++++++++++++ .../utc-Dali-ModelCacheManager.cpp | 7 +- .../src/dali-scene3d/utc-Dali-Model.cpp | 130 +++++ .../internal/common/image-resource-loader.cpp | 561 +++++++++++++++++++++ .../internal/common/image-resource-loader.h | 111 ++++ .../internal/common/model-cache-manager.cpp | 4 + dali-scene3d/internal/file.list | 1 + .../model-components/model-primitive-impl.cpp | 13 +- .../public-api/loader/environment-definition.cpp | 8 +- .../public-api/loader/environment-map-data.cpp | 14 +- .../public-api/loader/environment-map-loader.cpp | 49 +- .../public-api/loader/material-definition.cpp | 18 +- 14 files changed, 1789 insertions(+), 60 deletions(-) create mode 100644 automated-tests/resources/AnimatedCube2.gltf create mode 100644 automated-tests/resources/AnimatedCube2.metadata create mode 100644 automated-tests/resources/AnimatedCube3.gltf create mode 100644 dali-scene3d/internal/common/image-resource-loader.cpp create mode 100644 dali-scene3d/internal/common/image-resource-loader.h diff --git a/automated-tests/resources/AnimatedCube2.gltf b/automated-tests/resources/AnimatedCube2.gltf new file mode 100644 index 0000000..807c07e --- /dev/null +++ b/automated-tests/resources/AnimatedCube2.gltf @@ -0,0 +1,458 @@ +{ + "extensionsUsed" : [ + "KHR_materials_specular", + "KHR_materials_ior" + ], + + "accessors" : [ + { + "bufferView" : 0, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 3, + "max" : [ + 2.000000 + ], + "min" : [ + 0.000000 + ], + "type" : "SCALAR" + }, + { + "bufferView" : 1, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 3, + "max" : [ + 0.000000, + 1.000000, + 0.000000, + 1.000000 + ], + "min" : [ + 0.000000, + -8.742278e-008, + 0.000000, + -1.000000 + ], + "type" : "VEC4" + }, + { + "bufferView" : 2, + "byteOffset" : 0, + "componentType" : 5123, + "count" : 36, + "max" : [ + 35 + ], + "min" : [ + 0 + ], + "type" : "SCALAR" + }, + { + "bufferView" : 3, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 36, + "max" : [ + 1.000000, + 1.000000, + 1.000001 + ], + "min" : [ + -1.000000, + -1.000000, + -1.000000 + ], + "type" : "VEC3" + }, + { + "bufferView" : 4, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 36, + "max" : [ + 1.000000, + 1.000000, + 1.000000 + ], + "min" : [ + -1.000000, + -1.000000, + -1.000000 + ], + "type" : "VEC3" + }, + { + "bufferView" : 5, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 36, + "max" : [ + 1.000000, + -0.000000, + -0.000000, + 1.000000 + ], + "min" : [ + 0.000000, + -0.000000, + -1.000000, + -1.000000 + ], + "type" : "VEC4" + }, + { + "bufferView" : 6, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 36, + "max" : [ + 1.000000, + 1.000000 + ], + "min" : [ + -1.000000, + -1.000000 + ], + "type" : "VEC2" + } + ], + "animations" : [ + { + "channels" : [ + { + "sampler" : 0, + "target" : { + "node" : 0, + "path" : "rotation" + } + } + ], + "name" : "animation_AnimatedCube", + "samplers" : [ + { + "input" : 0, + "interpolation" : "LINEAR", + "output" : 1 + } + ] + } + ], + "asset" : { + "generator" : "VKTS glTF 2.0 exporter", + "version" : "2.0" + }, + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 12, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 48, + "byteOffset" : 12 + }, + { + "buffer" : 0, + "byteLength" : 72, + "byteOffset" : 60, + "target" : 34963 + }, + { + "buffer" : 0, + "byteLength" : 432, + "byteOffset" : 132, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 432, + "byteOffset" : 564, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 576, + "byteOffset" : 996, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 1572, + "target" : 34962 + } + ], + "buffers" : [ + { + "byteLength" : 1860, + "uri" : "AnimatedCube.bin" + } + ], + "images" : [ + { + "uri" : "AnimatedCube_BaseColor.png" + }, + { + "uri" : "AnimatedCube_MetallicRoughness.png" + } + ], + "materials" : [ + { + "extensions" : { + "KHR_materials_specular" : { + "specularColorFactor" : [ + 0, + 0, + 1 + ], + "specularFactor" : 0.5, + "specularTexture": { + "index": 0 + }, + "specularColorTexture": { + "index": 0 + } + }, + "KHR_materials_ior" : { + "ior" : 1.0 + } + }, + "name" : "AnimatedCube", + "pbrMetallicRoughness" : { + "baseColorTexture" : { + "index" : 0 + }, + "baseColorFactor": [ 1.000, 0.766, 0.336, 1.0 ], + "metallicFactor": 1.0, + "roughnessFactor": 0.0 + }, + "normalTexture": { + "scale": 1, + "index": 0 + }, + "occlusionTexture": { + "index": 0 + }, + "emissiveTexture": { + "index": 0 + }, + "emissiveFactor": [ 0.2, 0.1, 0.0 ], + "doubleSided": false, + "alphaMode": "MASK", + "alphaCutoff": 0.5 + }, + { + "name" : "AnimatedCube2", + "pbrMetallicRoughness" : { + "baseColorTexture" : { + "index" : 0 + }, + "metallicRoughnessTexture" : { + "index" : 1 + }, + "baseColorFactor": [ 1.000, 0.766, 0.336, 1.0 ], + "metallicFactor": 1.0, + "roughnessFactor": 0.0 + }, + "normalTexture": { + "scale": 1, + "index": 0 + }, + "occlusionTexture": { + "index": 0 + }, + "emissiveTexture": { + "index": 0 + }, + "emissiveFactor": [ 0.2, 0.1, 0.0 ], + "doubleSided": false, + "alphaMode": "OPAQUE" + } + ], + "meshes" : [ + { + "name" : "AnimatedCube", + "primitives" : [ + { + "attributes" : { + "NORMAL" : 4, + "POSITION" : 3, + "TANGENT" : 5, + "TEXCOORD_0" : 6, + "COLOR_0" : 3 + }, + "indices" : 2, + "material" : 0, + "mode" : 4 + } + ] + }, + { + "name" : "AnimatedCube2", + "primitives" : [ + { + "attributes" : { + "NORMAL" : 4, + "POSITION" : 3, + "TANGENT" : 5, + "TEXCOORD_0" : 6, + "COLOR_0" : 3 + }, + "indices" : 2, + "material" : 1, + "mode" : 4 + } + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "AnimatedCube", + "rotation" : [ + 0.000000, + -1.000000, + 0.000000, + 0.000000 + ] + }, + { + "mesh" : 1, + "name" : "AnimatedCube" + }, + { + + "camera" : 0, + "scale" : [ 0.5, 0.5, 3.0 ] + }, + { + "camera" : 1, + "translation" : [ 0.5, 0.5, 3.0 ], + "children": [ + 4, 5, 6, 7 + ] + }, + { + "camera" : 2, + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ] + }, + { + "camera" : 3, + "translation" : [ 0.0, 0.0, 0.0 ] + }, + { + "camera" : 4, + "translation" : [ 0.0, 0.0, 0.0 ] + }, + { + "camera" : 5, + "translation" : [ 0.0, 0.0, 0.0 ] + } + ], + "scene" : 0, + "scenes" : [ + { + "nodes" : [ + 0, 1, 2, 3 + ] + } + ], + "textures" : [ + { + "sampler" : 0, + "source" : 0 + }, + { + "sampler" : 1, + "source" : 1 + } + ], + "cameras" : [ + { + "type": "perspective", + "perspective": { + "aspectRatio": 1.0, + "yfov": 0.7, + "zfar": 100.0, + "znear": 0.01 + } + }, + { + "type": "orthographic", + "orthographic": { + "xmag": 1.0, + "ymag": 1.0, + "zfar": 100.0, + "znear": 0.01 + } + }, + { + "type": "orthographic", + "orthographic": { + "xmag": 1.0, + "ymag": 1.0, + "zfar": 100.0, + "znear": 0.01 + } + }, + { + "type": "perspective", + "perspective": { + "aspectRatio": 1.0, + "yfov": 0.7, + "znear": 0.01 + } + }, + { + "type": "perspective", + "perspective": { + "aspectRatio": 1.0, + "zfar": 100.0, + "znear": 0.01 + } + }, + { + "type": "orthographic", + "orthographic": { + "xmag": 1.0, + "ymag": 1.0, + "znear": 0.01 + } + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9987, + "wrapS": 33071, + "wrapT": 10497 + }, + { + "magFilter": 9728, + "minFilter": 9986, + "wrapS": 33071, + "wrapT": 33648 + } + ] +} \ No newline at end of file diff --git a/automated-tests/resources/AnimatedCube2.metadata b/automated-tests/resources/AnimatedCube2.metadata new file mode 100644 index 0000000..70b5b7f --- /dev/null +++ b/automated-tests/resources/AnimatedCube2.metadata @@ -0,0 +1,17 @@ + { + "images": [ + { + "uri": "AnimatedCube_BaseColor.png", + "minWidth": 256, + "minHeight": 256, + "samplingMode": "BOX_THEN_NEAREST" + }, + { + "uri": "AnimatedCube_MetallicRoughness.png", + "minWidth": 256, + "minHeight": 256, + "samplingMode": "NEAREST" + } + ] +} + diff --git a/automated-tests/resources/AnimatedCube3.gltf b/automated-tests/resources/AnimatedCube3.gltf new file mode 100644 index 0000000..807c07e --- /dev/null +++ b/automated-tests/resources/AnimatedCube3.gltf @@ -0,0 +1,458 @@ +{ + "extensionsUsed" : [ + "KHR_materials_specular", + "KHR_materials_ior" + ], + + "accessors" : [ + { + "bufferView" : 0, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 3, + "max" : [ + 2.000000 + ], + "min" : [ + 0.000000 + ], + "type" : "SCALAR" + }, + { + "bufferView" : 1, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 3, + "max" : [ + 0.000000, + 1.000000, + 0.000000, + 1.000000 + ], + "min" : [ + 0.000000, + -8.742278e-008, + 0.000000, + -1.000000 + ], + "type" : "VEC4" + }, + { + "bufferView" : 2, + "byteOffset" : 0, + "componentType" : 5123, + "count" : 36, + "max" : [ + 35 + ], + "min" : [ + 0 + ], + "type" : "SCALAR" + }, + { + "bufferView" : 3, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 36, + "max" : [ + 1.000000, + 1.000000, + 1.000001 + ], + "min" : [ + -1.000000, + -1.000000, + -1.000000 + ], + "type" : "VEC3" + }, + { + "bufferView" : 4, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 36, + "max" : [ + 1.000000, + 1.000000, + 1.000000 + ], + "min" : [ + -1.000000, + -1.000000, + -1.000000 + ], + "type" : "VEC3" + }, + { + "bufferView" : 5, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 36, + "max" : [ + 1.000000, + -0.000000, + -0.000000, + 1.000000 + ], + "min" : [ + 0.000000, + -0.000000, + -1.000000, + -1.000000 + ], + "type" : "VEC4" + }, + { + "bufferView" : 6, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 36, + "max" : [ + 1.000000, + 1.000000 + ], + "min" : [ + -1.000000, + -1.000000 + ], + "type" : "VEC2" + } + ], + "animations" : [ + { + "channels" : [ + { + "sampler" : 0, + "target" : { + "node" : 0, + "path" : "rotation" + } + } + ], + "name" : "animation_AnimatedCube", + "samplers" : [ + { + "input" : 0, + "interpolation" : "LINEAR", + "output" : 1 + } + ] + } + ], + "asset" : { + "generator" : "VKTS glTF 2.0 exporter", + "version" : "2.0" + }, + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 12, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 48, + "byteOffset" : 12 + }, + { + "buffer" : 0, + "byteLength" : 72, + "byteOffset" : 60, + "target" : 34963 + }, + { + "buffer" : 0, + "byteLength" : 432, + "byteOffset" : 132, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 432, + "byteOffset" : 564, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 576, + "byteOffset" : 996, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 1572, + "target" : 34962 + } + ], + "buffers" : [ + { + "byteLength" : 1860, + "uri" : "AnimatedCube.bin" + } + ], + "images" : [ + { + "uri" : "AnimatedCube_BaseColor.png" + }, + { + "uri" : "AnimatedCube_MetallicRoughness.png" + } + ], + "materials" : [ + { + "extensions" : { + "KHR_materials_specular" : { + "specularColorFactor" : [ + 0, + 0, + 1 + ], + "specularFactor" : 0.5, + "specularTexture": { + "index": 0 + }, + "specularColorTexture": { + "index": 0 + } + }, + "KHR_materials_ior" : { + "ior" : 1.0 + } + }, + "name" : "AnimatedCube", + "pbrMetallicRoughness" : { + "baseColorTexture" : { + "index" : 0 + }, + "baseColorFactor": [ 1.000, 0.766, 0.336, 1.0 ], + "metallicFactor": 1.0, + "roughnessFactor": 0.0 + }, + "normalTexture": { + "scale": 1, + "index": 0 + }, + "occlusionTexture": { + "index": 0 + }, + "emissiveTexture": { + "index": 0 + }, + "emissiveFactor": [ 0.2, 0.1, 0.0 ], + "doubleSided": false, + "alphaMode": "MASK", + "alphaCutoff": 0.5 + }, + { + "name" : "AnimatedCube2", + "pbrMetallicRoughness" : { + "baseColorTexture" : { + "index" : 0 + }, + "metallicRoughnessTexture" : { + "index" : 1 + }, + "baseColorFactor": [ 1.000, 0.766, 0.336, 1.0 ], + "metallicFactor": 1.0, + "roughnessFactor": 0.0 + }, + "normalTexture": { + "scale": 1, + "index": 0 + }, + "occlusionTexture": { + "index": 0 + }, + "emissiveTexture": { + "index": 0 + }, + "emissiveFactor": [ 0.2, 0.1, 0.0 ], + "doubleSided": false, + "alphaMode": "OPAQUE" + } + ], + "meshes" : [ + { + "name" : "AnimatedCube", + "primitives" : [ + { + "attributes" : { + "NORMAL" : 4, + "POSITION" : 3, + "TANGENT" : 5, + "TEXCOORD_0" : 6, + "COLOR_0" : 3 + }, + "indices" : 2, + "material" : 0, + "mode" : 4 + } + ] + }, + { + "name" : "AnimatedCube2", + "primitives" : [ + { + "attributes" : { + "NORMAL" : 4, + "POSITION" : 3, + "TANGENT" : 5, + "TEXCOORD_0" : 6, + "COLOR_0" : 3 + }, + "indices" : 2, + "material" : 1, + "mode" : 4 + } + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "AnimatedCube", + "rotation" : [ + 0.000000, + -1.000000, + 0.000000, + 0.000000 + ] + }, + { + "mesh" : 1, + "name" : "AnimatedCube" + }, + { + + "camera" : 0, + "scale" : [ 0.5, 0.5, 3.0 ] + }, + { + "camera" : 1, + "translation" : [ 0.5, 0.5, 3.0 ], + "children": [ + 4, 5, 6, 7 + ] + }, + { + "camera" : 2, + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ] + }, + { + "camera" : 3, + "translation" : [ 0.0, 0.0, 0.0 ] + }, + { + "camera" : 4, + "translation" : [ 0.0, 0.0, 0.0 ] + }, + { + "camera" : 5, + "translation" : [ 0.0, 0.0, 0.0 ] + } + ], + "scene" : 0, + "scenes" : [ + { + "nodes" : [ + 0, 1, 2, 3 + ] + } + ], + "textures" : [ + { + "sampler" : 0, + "source" : 0 + }, + { + "sampler" : 1, + "source" : 1 + } + ], + "cameras" : [ + { + "type": "perspective", + "perspective": { + "aspectRatio": 1.0, + "yfov": 0.7, + "zfar": 100.0, + "znear": 0.01 + } + }, + { + "type": "orthographic", + "orthographic": { + "xmag": 1.0, + "ymag": 1.0, + "zfar": 100.0, + "znear": 0.01 + } + }, + { + "type": "orthographic", + "orthographic": { + "xmag": 1.0, + "ymag": 1.0, + "zfar": 100.0, + "znear": 0.01 + } + }, + { + "type": "perspective", + "perspective": { + "aspectRatio": 1.0, + "yfov": 0.7, + "znear": 0.01 + } + }, + { + "type": "perspective", + "perspective": { + "aspectRatio": 1.0, + "zfar": 100.0, + "znear": 0.01 + } + }, + { + "type": "orthographic", + "orthographic": { + "xmag": 1.0, + "ymag": 1.0, + "znear": 0.01 + } + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9987, + "wrapS": 33071, + "wrapT": 10497 + }, + { + "magFilter": 9728, + "minFilter": 9986, + "wrapS": 33071, + "wrapT": 33648 + } + ] +} \ No newline at end of file diff --git a/automated-tests/src/dali-scene3d-internal/utc-Dali-ModelCacheManager.cpp b/automated-tests/src/dali-scene3d-internal/utc-Dali-ModelCacheManager.cpp index cf2f9ac..a6b322c 100644 --- a/automated-tests/src/dali-scene3d-internal/utc-Dali-ModelCacheManager.cpp +++ b/automated-tests/src/dali-scene3d-internal/utc-Dali-ModelCacheManager.cpp @@ -18,13 +18,14 @@ // Enable debug log for test coverage #define DEBUG_ENABLED 1 +#include #include #include #include #include #include -#include #include +#include #include using namespace Dali; @@ -177,5 +178,9 @@ int UtcDaliModelCacheManagerLoadModel(void) // All reference count should be decreased. DALI_TEST_EQUALS(cacheManager.GetModelCacheRefCount(TEST_GLTF_FILE_NAME), 0u, TEST_LOCATION); + // Collect garbages hardly. + Dali::Scene3D::Internal::ImageResourceLoader::RequestGarbageCollect(true); + Test::EmitGlobalTimerSignal(); + END_TEST; } diff --git a/automated-tests/src/dali-scene3d/utc-Dali-Model.cpp b/automated-tests/src/dali-scene3d/utc-Dali-Model.cpp index 1b898a0..2e78e66 100644 --- a/automated-tests/src/dali-scene3d/utc-Dali-Model.cpp +++ b/automated-tests/src/dali-scene3d/utc-Dali-Model.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -57,6 +58,8 @@ const bool DEFAULT_MODEL_CHILDREN_FOCUSABLE = false; * Take from https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/AnimatedCube */ const char* TEST_GLTF_FILE_NAME = TEST_RESOURCE_DIR "/AnimatedCube.gltf"; +const char* TEST_GLTF_FILE_NAME_SAME_FILE = TEST_RESOURCE_DIR "/AnimatedCube2.gltf"; +const char* TEST_GLTF_FILE_NAME_DIFF_META_FILE = TEST_RESOURCE_DIR "/AnimatedCube3.gltf"; const char* TEST_GLTF_ANIMATION_TEST_FILE_NAME = TEST_RESOURCE_DIR "/animationTest.gltf"; const char* TEST_GLTF_EXTRAS_FILE_NAME = TEST_RESOURCE_DIR "/AnimatedMorphCubeAnimateNonZeroFrame.gltf"; const char* TEST_GLTF_MULTIPLE_PRIMITIVE_FILE_NAME = TEST_RESOURCE_DIR "/simpleMultiplePrimitiveTest.gltf"; @@ -175,6 +178,133 @@ int UtcDaliModelNewP2(void) END_TEST; } +int UtcDaliModelNewSameModelUrlCached(void) +{ + ToolkitTestApplication application; + tet_infoline(" UtcDaliModelNew with same model"); + + // Set up trace debug + TestGlAbstraction& gl = application.GetGlAbstraction(); + TraceCallStack& textureTrace = gl.GetTextureTrace(); + textureTrace.Enable(true); + + Scene3D::Model model = Scene3D::Model::New(TEST_GLTF_FILE_NAME); + DALI_TEST_CHECK(model); + Scene3D::Model model2 = Scene3D::Model::New(TEST_GLTF_FILE_NAME); + DALI_TEST_CHECK(model2); + + application.GetScene().Add(model); + + DALI_TEST_EQUALS(Test::WaitForEventThreadTrigger(1), true, TEST_LOCATION); + + application.SendNotification(); + application.Render(); + + tet_printf("Test if there is at least 1 texture.\n"); + int expectTextureCount = textureTrace.CountMethod("GenTextures"); + DALI_TEST_GREATER(expectTextureCount, 0, TEST_LOCATION); + + application.GetScene().Add(model2); + + DALI_TEST_EQUALS(Test::WaitForEventThreadTrigger(1), true, TEST_LOCATION); + + application.SendNotification(); + application.Render(); + + tet_printf("Test if we reuse cached texture or not.\n"); + int currentTextureCount = textureTrace.CountMethod("GenTextures"); + DALI_TEST_EQUALS(currentTextureCount, expectTextureCount, TEST_LOCATION); + + application.SendNotification(); + application.Render(); + + END_TEST; +} + +int UtcDaliModelNewSameResourceUrlCached01(void) +{ + ToolkitTestApplication application; + tet_infoline(" UtcDaliModelNew with difference url but same model"); + + // Set up trace debug + TestGlAbstraction& gl = application.GetGlAbstraction(); + TraceCallStack& textureTrace = gl.GetTextureTrace(); + textureTrace.Enable(true); + textureTrace.EnableLogging(true); + + Scene3D::Model model = Scene3D::Model::New(TEST_GLTF_FILE_NAME); + DALI_TEST_CHECK(model); + Scene3D::Model model2 = Scene3D::Model::New(TEST_GLTF_FILE_NAME_SAME_FILE); // Difference model that use same Images. + DALI_TEST_CHECK(model2); + Scene3D::Model model3 = Scene3D::Model::New(TEST_GLTF_FILE_NAME_DIFF_META_FILE); // Difference model that use same Images, but difference metadata. + DALI_TEST_CHECK(model3); + + application.GetScene().Add(model); + + DALI_TEST_EQUALS(Test::WaitForEventThreadTrigger(1), true, TEST_LOCATION); + + application.SendNotification(); + application.Render(); + + tet_printf("Test if there is at least 1 texture.\n"); + int expectTextureCount = textureTrace.CountMethod("GenTextures"); + DALI_TEST_GREATER(expectTextureCount, 0, TEST_LOCATION); + + application.GetScene().Add(model2); + + DALI_TEST_EQUALS(Test::WaitForEventThreadTrigger(1), true, TEST_LOCATION); + + application.SendNotification(); + application.Render(); + + tet_printf("Test if we reuse cached texture or not.\n"); + int currentTextureCount = textureTrace.CountMethod("GenTextures"); + DALI_TEST_EQUALS(currentTextureCount, expectTextureCount, TEST_LOCATION); + + application.GetScene().Add(model3); + + DALI_TEST_EQUALS(Test::WaitForEventThreadTrigger(1), true, TEST_LOCATION); + + application.SendNotification(); + application.Render(); + + tet_printf("Test if we don't reuse cached texture, due to the metadata difference.\n"); + currentTextureCount = textureTrace.CountMethod("GenTextures"); + DALI_TEST_GREATER(currentTextureCount, expectTextureCount, TEST_LOCATION); + + application.SendNotification(); + application.Render(); + + END_TEST; +} + +int UtcDaliModelNewSameResourceUrlCached02(void) +{ + /// Make we don't use mutiple thread loading for this UTC. + EnvironmentVariable::SetTestEnvironmentVariable("DALI_ASYNC_MANAGER_THREAD_POOL_SIZE", "1"); + EnvironmentVariable::SetTestEnvironmentVariable("DALI_ASYNC_MANAGER_LOW_PRIORITY_THREAD_POOL_SIZE", "1"); + + ToolkitTestApplication application; + tet_infoline(" UtcDaliModelNew with difference url but same model"); + + Scene3D::Model model = Scene3D::Model::New(TEST_GLTF_FILE_NAME); + DALI_TEST_CHECK(model); + Scene3D::Model model2 = Scene3D::Model::New(TEST_GLTF_FILE_NAME_SAME_FILE); + DALI_TEST_CHECK(model2); + + application.GetScene().Add(model); + application.GetScene().Add(model2); + + application.SendNotification(); + application.Render(); + + DALI_TEST_EQUALS(Test::WaitForEventThreadTrigger(2), true, TEST_LOCATION); + application.SendNotification(); + application.Render(); + + END_TEST; +} + // Positive test case for a method int UtcDaliModelDownCast(void) { diff --git a/dali-scene3d/internal/common/image-resource-loader.cpp b/dali-scene3d/internal/common/image-resource-loader.cpp new file mode 100644 index 0000000..e580cd3 --- /dev/null +++ b/dali-scene3d/internal/common/image-resource-loader.cpp @@ -0,0 +1,561 @@ +/* + * Copyright (c) 2023 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 + +// EXTERNAL INCLUDES +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include ///< for std::function +#include +#include +#include ///< for std::pair + +// INTERNAL INCLUDES + +namespace +{ +constexpr uint32_t MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL = 5u; +constexpr uint32_t GC_PERIOD_MILLISECONDS = 1000u; + +#ifdef DEBUG_ENABLED +Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, false, "LOG_IMAGE_RESOURCE_LOADER"); +#endif + +struct ImageInformation +{ + ImageInformation(const std::string url, + const Dali::ImageDimensions dimensions, + Dali::FittingMode::Type fittingMode, + Dali::SamplingMode::Type samplingMode, + bool orientationCorrection) + : mUrl(url), + mDimensions(dimensions), + mFittingMode(fittingMode), + mSamplingMode(samplingMode), + mOrientationCorrection(orientationCorrection) + { + } + + bool operator==(const ImageInformation& rhs) const + { + // Check url and orientation correction is enough. + return (mUrl == rhs.mUrl) && (mOrientationCorrection == rhs.mOrientationCorrection); + } + + std::string mUrl; + Dali::ImageDimensions mDimensions; + Dali::FittingMode::Type mFittingMode; + Dali::SamplingMode::Type mSamplingMode; + bool mOrientationCorrection; +}; + +// Hash functor list +std::size_t GenerateHash(const ImageInformation& info) +{ + std::vector hashTarget; + const uint16_t width = info.mDimensions.GetWidth(); + const uint16_t height = info.mDimensions.GetHeight(); + + // If either the width or height has been specified, include the resizing options in the hash + if(width != 0 || height != 0) + { + // We are appending 5 bytes to the URL to form the hash input. + hashTarget.resize(5u); + std::uint8_t* hashTargetPtr = &(hashTarget[0u]); + + // Pack the width and height (4 bytes total). + *hashTargetPtr++ = info.mDimensions.GetWidth() & 0xff; + *hashTargetPtr++ = (info.mDimensions.GetWidth() >> 8u) & 0xff; + *hashTargetPtr++ = info.mDimensions.GetHeight() & 0xff; + *hashTargetPtr++ = (info.mDimensions.GetHeight() >> 8u) & 0xff; + + // Bit-pack the FittingMode, SamplingMode and atlasing. + // FittingMode=2bits, SamplingMode=3bits, orientationCorrection=1bit + *hashTargetPtr = (info.mFittingMode << 4u) | (info.mSamplingMode << 1) | (info.mOrientationCorrection ? 1 : 0); + } + else + { + // We are not including sizing information, but we still need an extra byte for orientationCorrection. + hashTarget.resize(1u); + hashTarget[0u] = info.mOrientationCorrection ? 't' : 'f'; + } + + return Dali::CalculateHash(info.mUrl) ^ Dali::CalculateHash(hashTarget); +} + +std::size_t GenerateHash(const Dali::PixelData& pixelData, bool mipmapRequired) +{ + return reinterpret_cast(static_cast(pixelData.GetObjectPtr())) ^ (static_cast(mipmapRequired) << (sizeof(std::size_t) * 4)); +} + +std::size_t GenerateHash(const std::vector>& pixelDataList, bool mipmapRequired) +{ + std::size_t result = 0x12345678u + pixelDataList.size(); + for(const auto& mipmapPixelDataList : pixelDataList) + { + result += (result << 5) + mipmapPixelDataList.size(); + for(const auto& pixelData : mipmapPixelDataList) + { + result += (result << 5) + GenerateHash(pixelData, false); + } + } + + return result ^ (static_cast(mipmapRequired) << (sizeof(std::size_t) * 4)); +} + +// Item Creation functor list + +Dali::PixelData CreatePixelDataFromImageInfo(const ImageInformation& info, bool /* Not used */) +{ + return Dali::Toolkit::SyncImageLoader::Load(info.mUrl, info.mDimensions, info.mFittingMode, info.mSamplingMode, info.mOrientationCorrection); +} + +Dali::Texture CreateTextureFromPixelData(const Dali::PixelData& pixelData, bool mipmapRequired) +{ + Dali::Texture texture; + if(pixelData) + { + texture = Dali::Texture::New(Dali::TextureType::TEXTURE_2D, pixelData.GetPixelFormat(), pixelData.GetWidth(), pixelData.GetHeight()); + texture.Upload(pixelData, 0, 0, 0, 0, pixelData.GetWidth(), pixelData.GetHeight()); + if(mipmapRequired) + { + texture.GenerateMipmaps(); + } + } + return texture; +} + +Dali::Texture CreateCubeTextureFromPixelDataList(const std::vector>& pixelDataList, bool mipmapRequired) +{ + Dali::Texture texture; + if(!pixelDataList.empty() && !pixelDataList[0].empty()) + { + texture = Dali::Texture::New(Dali::TextureType::TEXTURE_CUBE, pixelDataList[0][0].GetPixelFormat(), pixelDataList[0][0].GetWidth(), pixelDataList[0][0].GetHeight()); + for(size_t iSide = 0u, iEndSize = pixelDataList.size(); iSide < iEndSize; ++iSide) + { + auto& side = pixelDataList[iSide]; + for(size_t iMipLevel = 0u, iEndMipLevel = pixelDataList[0].size(); iMipLevel < iEndMipLevel; ++iMipLevel) + { + texture.Upload(side[iMipLevel], Dali::CubeMapLayer::POSITIVE_X + iSide, iMipLevel, 0u, 0u, side[iMipLevel].GetWidth(), side[iMipLevel].GetHeight()); + } + } + if(mipmapRequired) + { + texture.GenerateMipmaps(); + } + } + + return texture; +} +class CacheImpl : public Dali::ConnectionTracker +{ +public: + /** + * @brief Constructor + */ + CacheImpl() + : mPixelDataCache{}, + mTextureCache{}, + mCubeTextureCache{}, + mTimer{}, + mLatestCollectedPixelDataIter{mPixelDataCache.begin()}, + mLatestCollectedTextureIter{mTextureCache.begin()}, + mLatestCollectedCubeTextureIter{mCubeTextureCache.begin()}, + mPixelDataContainerUpdated{false}, + mTextureContainerUpdated{false}, + mCubeTextureContainerUpdated{false}, + mDataMutex{}, + mDestroyed{false}, + mFullCollectRequested{false} + { + } + + /** + * @brief Destructor + */ + ~CacheImpl() + { + { + mDataMutex.lock(); + + mDestroyed = true; + mPixelDataContainerUpdated = false; + mTextureContainerUpdated = false; + mCubeTextureContainerUpdated = false; + mLatestCollectedPixelDataIter = decltype(mLatestCollectedPixelDataIter)(); // Invalidate iterator + mLatestCollectedTextureIter = decltype(mLatestCollectedTextureIter)(); // Invalidate iterator + mLatestCollectedCubeTextureIter = decltype(mLatestCollectedCubeTextureIter){}; // Invalidate iterator + + mPixelDataCache.clear(); + mTextureCache.clear(); + mCubeTextureCache.clear(); + + mDataMutex.unlock(); + } + + if(mTimer) + { + if(Dali::Adaptor::IsAvailable()) + { + mTimer.Stop(); + } + } + } + +private: // Unified API for this class + // Let compare with hash first. And then, check detail keys after. + using PixelDataCacheContainer = std::map>>; + using TextureCacheContainer = std::map>>; + using CubeTextureCacheContainer = std::map>, Dali::Texture>>>; + + /** + * @brief Try to get cached item, or create new handle if there is no item. + * + * @tparam needMutex Whether we need to lock the mutex during this operation, or not. + * @param[in] cacheContainer The container of key / item pair. + * @param[in] hashValue The hash value of key. + * @param[in] key The key of cache item. + * @param[in] keyFlag The additional flags when we need to create new item. + * @param[out] containerUpdate True whether container changed or not. + * @return Item that has been cached. Or newly created. + */ + template + ItemType GetOrCreateCachedItem(ContainerType& cacheContainer, std::size_t hashValue, const KeyType& key, bool keyFlag, bool& containerUpdated) + { + if constexpr(needMutex) + { + mDataMutex.lock(); + } + ItemType returnItem; + + if(DALI_LIKELY(!mDestroyed)) + { + bool found = false; + + auto iter = cacheContainer.lower_bound(hashValue); + if((iter == cacheContainer.end()) || (hashValue != iter->first)) + { + containerUpdated = true; + + returnItem = ItemCreationFunction(key, keyFlag); + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n"); + cacheContainer.insert(iter, {hashValue, {{key, returnItem}}}); + } + else + { + auto& cachePairList = iter->second; + for(auto jter = cachePairList.begin(), jterEnd = cachePairList.end(); jter != jterEnd; ++jter) + { + if(jter->first == key) + { + // We found that input pixelData already cached. + returnItem = jter->second; + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Get cached item\n"); + found = true; + break; + } + } + + // If we fail to found same list, just append. + if(!found) + { + containerUpdated = true; + + returnItem = ItemCreationFunction(key, keyFlag); + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Create new item\n"); + cachePairList.emplace_back(key, returnItem); + } + } + } + + if constexpr(needMutex) + { + mDataMutex.unlock(); + } + + return returnItem; + } + /** + * @brief Try to collect garbages, which reference counts are 1. + * + * @tparam needMutex Whether we need to lock the mutex during this operation, or not. + * @param[in] cacheContainer The container of key / item pair. + * @param[in] fullCollect True if we need to collect whole container. + * @param[in, out] containerUpdated True if container information changed. lastIterator will be begin of container when we start collect garbages. + * @param[in, out] lastIterator The last iterator of container. + * @oaram[in, out] checkedCount The number of iteration checked total. + * @return True if we iterate whole container, so we don't need to check anymore. False otherwise + */ + template + bool CollectGarbages(ContainerType& cacheContainer, bool fullCollect, bool& containerUpdated, Iterator& lastIterator, uint32_t& checkedCount) + { + if constexpr(needMutex) + { + mDataMutex.lock(); + } + + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "Collect Garbages : %zu\n", cacheContainer.size()); + // Container changed. We should re-collect garbage from begin again. + if(fullCollect || containerUpdated) + { + lastIterator = cacheContainer.begin(); + containerUpdated = false; + } + + for(; lastIterator != cacheContainer.end() && (fullCollect || ++checkedCount <= MAXIMUM_COLLECTING_ITEM_COUNTS_PER_GC_CALL);) + { + auto& cachePairList = lastIterator->second; + + for(auto jter = cachePairList.begin(); jter != cachePairList.end();) + { + auto& item = jter->second; + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "item : %p, ref count : %u\n", item.GetObjectPtr(), (item ? item.GetBaseObject().ReferenceCount() : 0u)); + if(!item || (item.GetBaseObject().ReferenceCount() == 1u)) + { + // This item is garbage! just remove it. + jter = cachePairList.erase(jter); + } + else + { + ++jter; + } + } + + if(cachePairList.empty()) + { + lastIterator = cacheContainer.erase(lastIterator); + } + else + { + ++lastIterator; + } + } + + if constexpr(needMutex) + { + mDataMutex.unlock(); + } + + return (lastIterator != cacheContainer.end()); + } + +public: // Called by main thread. + /** + * @brief Try to get cached texture, or newly create if there is no texture that already cached. + * + * @param[in] pixelData The pixelData of image. + * @param[in] mipmapRequired True if result texture need to generate mipmap. + * @return Texture that has been cached. Or empty handle if we fail to found cached item. + */ + Dali::Texture GetOrCreateCachedTexture(const Dali::PixelData& pixelData, bool mipmapRequired) + { + auto hashValue = GenerateHash(pixelData, mipmapRequired); + return GetOrCreateCachedItem(mTextureCache, hashValue, pixelData, mipmapRequired, mTextureContainerUpdated); + } + + /** + * @brief Try to get cached cube texture, or newly create if there is no cube texture that already cached. + * + * @param[in] pixelDataList The pixelData list of image. + * @param[in] mipmapRequired True if result texture need to generate mipmap. + * @return Texture that has been cached. Or empty handle if we fail to found cached item. + */ + Dali::Texture GetOrCreateCachedCubeTexture(const std::vector>& pixelDataList, bool mipmapRequired) + { + auto hashValue = GenerateHash(pixelDataList, mipmapRequired); + return GetOrCreateCachedItem>, Dali::Texture, CreateCubeTextureFromPixelDataList>(mCubeTextureCache, hashValue, pixelDataList, mipmapRequired, mCubeTextureContainerUpdated); + } + + /** + * @brief Request incremental gargabe collect. + * + * @param[in] fullCollect True if we will collect whole items, or incrementally. + */ + void RequestGarbageCollect(bool fullCollect) + { + if(DALI_LIKELY(!mDestroyed && Dali::Adaptor::IsAvailable())) + { + if(!mTimer) + { + mTimer = Dali::Timer::New(GC_PERIOD_MILLISECONDS); + mTimer.TickSignal().Connect(this, &CacheImpl::OnTick); + } + + mFullCollectRequested |= fullCollect; + + if(!mTimer.IsRunning()) + { + // Restart container interating. + if(!mPixelDataContainerUpdated) + { + mDataMutex.lock(); + mPixelDataContainerUpdated = true; + mDataMutex.unlock(); + } + mTextureContainerUpdated = true; + mTimer.Start(); + } + } + } + +public: // Can be called by worker thread + /** + * @brief Try to get cached pixel data, or newly create if there is no pixel data that already cached. + * + * @param[in] info The informations of image to load. + * @return Texture that has been cached. Or empty handle if we fail to found cached item. + */ + Dali::PixelData GetOrCreateCachedPixelData(const ImageInformation& info) + { + auto hashValue = GenerateHash(info); + return GetOrCreateCachedItem(mPixelDataCache, hashValue, info, false, mPixelDataContainerUpdated); + } + +private: // Called by main thread + bool OnTick() + { + // Clear full GC flag + const bool fullCollect = mFullCollectRequested; + mFullCollectRequested = false; + + return IncrementalGarbageCollect(fullCollect); + } + + /** + * @brief Remove unused cache item incrementally. + * + * @param[in] fullCollect True if we will collect whole items, or incrementally. + * @return True if there still exist what we need to check clean. False when whole cached items are using now. + */ + bool IncrementalGarbageCollect(bool fullCollect) + { + bool continueTimer = false; + + if(DALI_LIKELY(!mDestroyed)) + { + // Try to collect Texture GC first, due to the reference count of pixelData who become key of textures. + // After all texture GC finished, then check PixelData cache. + uint32_t checkedCount = 0u; + // GC Cube Texture + continueTimer |= CollectGarbages(mCubeTextureCache, fullCollect, mCubeTextureContainerUpdated, mLatestCollectedCubeTextureIter, checkedCount); + + // GC Texture + continueTimer |= CollectGarbages(mTextureCache, fullCollect, mTextureContainerUpdated, mLatestCollectedTextureIter, checkedCount); + + // GC PixelData. We should lock mutex during GC pixelData. + continueTimer |= CollectGarbages(mPixelDataCache, fullCollect, mPixelDataContainerUpdated, mLatestCollectedPixelDataIter, checkedCount); + } + + return continueTimer; + } + +private: + PixelDataCacheContainer mPixelDataCache; + TextureCacheContainer mTextureCache; + CubeTextureCacheContainer mCubeTextureCache; + + Dali::Timer mTimer; + + // Be used when we garbage collection. + PixelDataCacheContainer::iterator mLatestCollectedPixelDataIter; + TextureCacheContainer::iterator mLatestCollectedTextureIter; + CubeTextureCacheContainer::iterator mLatestCollectedCubeTextureIter; + + bool mPixelDataContainerUpdated; + bool mTextureContainerUpdated; + bool mCubeTextureContainerUpdated; + + std::mutex mDataMutex; + + bool mDestroyed : 1; + bool mFullCollectRequested : 1; +}; + +CacheImpl& GetCacheImpl() +{ + static CacheImpl gCacheImpl; + return gCacheImpl; +} + +} // namespace + +namespace Dali::Scene3D::Internal +{ +namespace ImageResourceLoader +{ +// Called by main thread.. +Dali::PixelData GetEmptyPixelDataWhiteRGB() +{ + static Dali::PixelData emptyPixelData = PixelData::New(new uint8_t[3]{0xff, 0xff, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY); + return emptyPixelData; +} + +Dali::Texture GetEmptyTextureWhiteRGB() +{ + static Dali::PixelData emptyPixelData = GetEmptyPixelDataWhiteRGB(); + static Dali::Texture emptyTexture = Dali::Texture(); + if(!emptyTexture) + { + emptyTexture = Texture::New(TextureType::TEXTURE_2D, emptyPixelData.GetPixelFormat(), emptyPixelData.GetWidth(), emptyPixelData.GetHeight()); + emptyTexture.Upload(emptyPixelData, 0, 0, 0, 0, emptyPixelData.GetWidth(), emptyPixelData.GetHeight()); + } + return emptyTexture; +} + +Dali::Texture GetCachedTexture(Dali::PixelData pixelData, bool mipmapRequired) +{ + return GetCacheImpl().GetOrCreateCachedTexture(pixelData, mipmapRequired); +} + +Dali::Texture GetCachedCubeTexture(const std::vector>& pixelDataList, bool mipmapRequired) +{ + return GetCacheImpl().GetOrCreateCachedCubeTexture(pixelDataList, mipmapRequired); +} + +void RequestGarbageCollect(bool fullCollect) +{ + GetCacheImpl().RequestGarbageCollect(fullCollect); +} + +// Can be called by worker thread. +Dali::PixelData GetCachedPixelData(const std::string& url) +{ + return GetCachedPixelData(url, ImageDimensions(), FittingMode::DEFAULT, SamplingMode::BOX_THEN_LINEAR, true); +} + +Dali::PixelData GetCachedPixelData(const std::string& url, + ImageDimensions dimensions, + FittingMode::Type fittingMode, + SamplingMode::Type samplingMode, + bool orientationCorrection) +{ + ImageInformation info(url, dimensions, fittingMode, samplingMode, orientationCorrection); + return GetCacheImpl().GetOrCreateCachedPixelData(info); +} +} // namespace ImageResourceLoader +} // namespace Dali::Scene3D::Internal diff --git a/dali-scene3d/internal/common/image-resource-loader.h b/dali-scene3d/internal/common/image-resource-loader.h new file mode 100644 index 0000000..a7f10d4 --- /dev/null +++ b/dali-scene3d/internal/common/image-resource-loader.h @@ -0,0 +1,111 @@ +#ifndef DALI_SCENE3D_IMAGE_RESOURCE_LOADER_H +#define DALI_SCENE3D_IMAGE_RESOURCE_LOADER_H + +/* + * Copyright (c) 2023 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 +#include +#include +#include + +// INTERNAL INCLUDES + +namespace Dali +{ +namespace Scene3D +{ +namespace Internal +{ +/** + * The namespace to load the images and cache raw PixelData. + * The cached resources will be reused when the same input parameter is loaded multiple times. + * @note This class can be called from worker threads. + */ +namespace ImageResourceLoader +{ +// Called by main thread. + +/** + * @brief Get cached pixelData handle filled as white with RGB888 format. + * @return A PixelData object containing the white RGB888 color. + */ +Dali::PixelData GetEmptyPixelDataWhiteRGB(); + +/** + * @brief Get cached texture handle filled as white with RGB888 format. + * @return A Texture object containing the white RGB888 color. + */ +Dali::Texture GetEmptyTextureWhiteRGB(); + +/** + * @brief Get cached texture handle, or create new texture and upload. + * @param[in] pixelData The PixelData of image to upload + * @param[in] mipmapRequired True if this texture need to generate mipmap + * @return A Texture object containing the pixelData, or an invalid object on failure + */ +Dali::Texture GetCachedTexture(Dali::PixelData pixelData, bool mipmapRequired); + +/** + * @brief Get cached cube texture handle, or create new texture and upload. + * @param[in] pixelDataList The list of PixelData to upload. pixelDataList[FACE_OF_CUBE][MIPMAP_LEVEL]. + * @param[in] mipmapRequired True if this texture need to generate mipmap + * @return A Texture object containing the pixelData, or an invalid object on failure + */ +Dali::Texture GetCachedCubeTexture(const std::vector>& pixelDataList, bool mipmapRequired); + +/** + * @brief Request to remove unused Texture and PixelData. We can choose the collect garbages incrementally or fully. + * + * @param[in] fullCollect True if we want to collect whole garbages. Default is false. + */ +void RequestGarbageCollect(bool fullCollect = false); + +// Can be called by worker thread. + +/** + * @brief Get cached image, or loads an image synchronously. + * @param[in] url The URL of the image file to load + * @return A PixelData object containing the image, or an invalid object on failure + */ +Dali::PixelData GetCachedPixelData(const std::string& url); + +/** + * @brief Get cached image, or loads an image synchronously by specifying the target dimensions and options. + * @param[in] url The URL of the image file to load + * @param[in] dimensions The width and height to fit the loaded image to + * @param[in] fittingMode The method used to fit the shape of the image before loading to the shape defined by the size parameter + * @param[in] samplingMode The filtering method used when sampling pixels from the input image while fitting it to desired size + * @param[in] orientationCorrection Reorient the image to respect any orientation metadata in its header + * @return A PixelData object containing the image, or an invalid object on failure + */ +Dali::PixelData GetCachedPixelData(const std::string& url, + ImageDimensions dimensions, + FittingMode::Type fittingMode, + SamplingMode::Type samplingMode, + bool orientationCorrection); +} // namespace ImageResourceLoader + +} // namespace Internal + +} // namespace Scene3D + +} // namespace Dali + +#endif // DALI_SCENE3D_MODEL_CACHE_MANAGER_H diff --git a/dali-scene3d/internal/common/model-cache-manager.cpp b/dali-scene3d/internal/common/model-cache-manager.cpp index ac9b4c4..bb826fb 100644 --- a/dali-scene3d/internal/common/model-cache-manager.cpp +++ b/dali-scene3d/internal/common/model-cache-manager.cpp @@ -27,6 +27,7 @@ #include // INTERNAL INCLUDES +#include #include #include @@ -89,6 +90,9 @@ public: if(cache.refCount == 0) { mModelCache.erase(modelUri); + + // Request image resource GC + Dali::Scene3D::Internal::ImageResourceLoader::RequestGarbageCollect(); } } diff --git a/dali-scene3d/internal/file.list b/dali-scene3d/internal/file.list index 761ebb8..48025c6 100644 --- a/dali-scene3d/internal/file.list +++ b/dali-scene3d/internal/file.list @@ -6,6 +6,7 @@ set(scene3d_src_files ${scene3d_src_files} ${scene3d_internal_dir}/algorithm/path-finder-spfa.cpp ${scene3d_internal_dir}/algorithm/path-finder-spfa-double-way.cpp ${scene3d_internal_dir}/common/environment-map-load-task.cpp + ${scene3d_internal_dir}/common/image-resource-loader.cpp ${scene3d_internal_dir}/common/model-cache-manager.cpp ${scene3d_internal_dir}/common/model-load-task.cpp ${scene3d_internal_dir}/controls/model/model-impl.cpp diff --git a/dali-scene3d/internal/model-components/model-primitive-impl.cpp b/dali-scene3d/internal/model-components/model-primitive-impl.cpp index edf3e86..06dad44 100644 --- a/dali-scene3d/internal/model-components/model-primitive-impl.cpp +++ b/dali-scene3d/internal/model-components/model-primitive-impl.cpp @@ -25,6 +25,7 @@ #include // INTERNAL INCLUDES +#include #include #include #include @@ -53,14 +54,6 @@ DALI_TYPE_REGISTRATION_END() static constexpr uint32_t INDEX_FOR_LIGHT_CONSTRAINT_TAG = 10; -Texture MakeEmptyTexture() -{ - PixelData pixelData = PixelData::New(new uint8_t[3]{0xff, 0xff, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY); - Texture texture = Texture::New(TextureType::TEXTURE_2D, pixelData.GetPixelFormat(), pixelData.GetWidth(), pixelData.GetHeight()); - texture.Upload(pixelData, 0, 0, 0, 0, pixelData.GetWidth(), pixelData.GetHeight()); - - return texture; -} } // unnamed namespace ModelPrimitivePtr ModelPrimitive::New() @@ -311,7 +304,7 @@ void ModelPrimitive::ApplyMaterialToRenderer(MaterialModifyObserver::ModifyFlag if(!mShadowMapTexture) { - mShadowMapTexture = MakeEmptyTexture(); + mShadowMapTexture = Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyTextureWhiteRGB(); } mTextureSet.SetTexture(textureCount++, mShadowMapTexture); @@ -403,7 +396,7 @@ void ModelPrimitive::UpdateShadowMapTexture() Dali::Texture texture = textures.GetTexture(index); if(index == textureCount - GetImplementation(mMaterial).GetShadowMapTextureOffset()) { - texture = (!!mShadowMapTexture) ? mShadowMapTexture : MakeEmptyTexture(); + texture = (!!mShadowMapTexture) ? mShadowMapTexture : Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyTextureWhiteRGB(); } newTextures.SetTexture(index, texture); diff --git a/dali-scene3d/public-api/loader/environment-definition.cpp b/dali-scene3d/public-api/loader/environment-definition.cpp index 61e5744..877f0ea 100644 --- a/dali-scene3d/public-api/loader/environment-definition.cpp +++ b/dali-scene3d/public-api/loader/environment-definition.cpp @@ -24,6 +24,7 @@ #include // INTERNAL INCLUDES +#include #include #include @@ -67,14 +68,13 @@ EnvironmentDefinition::RawData EnvironmentDefinition::LoadRaw(const std::string& environmentsPath) { RawData raw; - auto loadFn = [&environmentsPath](const std::string& path, EnvironmentMapData& environmentMapData) - { + auto loadFn = [&environmentsPath](const std::string& path, EnvironmentMapData& environmentMapData) { if(path.empty()) { environmentMapData.mPixelData.resize(6); for(auto& face : environmentMapData.mPixelData) { - face.push_back(PixelData::New(new uint8_t[3]{0xff, 0xff, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY)); + face.push_back(Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyPixelDataWhiteRGB()); } environmentMapData.SetEnvironmentMapType(Dali::Scene3D::EnvironmentMapType::CUBEMAP); } @@ -134,7 +134,7 @@ void EnvironmentDefinition::LoadBrdfTexture() if(pixelBuffer) { mBrdfPixelData = Devel::PixelBuffer::Convert(pixelBuffer); - mIsBrdfLoaded = true; + mIsBrdfLoaded = true; } } } diff --git a/dali-scene3d/public-api/loader/environment-map-data.cpp b/dali-scene3d/public-api/loader/environment-map-data.cpp index 7766487..f7472c4 100644 --- a/dali-scene3d/public-api/loader/environment-map-data.cpp +++ b/dali-scene3d/public-api/loader/environment-map-data.cpp @@ -22,6 +22,7 @@ #include // INTERNAL INCLUDES +#include #include namespace Dali::Scene3D::Loader @@ -37,20 +38,11 @@ Texture EnvironmentMapData::GetTexture() { if(mEnvironmentMapType == Scene3D::EnvironmentMapType::CUBEMAP) { - mEnvironmentMapTexture = Texture::New(TextureType::TEXTURE_CUBE, mPixelData[0][0].GetPixelFormat(), mPixelData[0][0].GetWidth(), mPixelData[0][0].GetHeight()); - for(size_t iSide = 0u, iEndSize = mPixelData.size(); iSide < iEndSize; ++iSide) - { - auto& side = mPixelData[iSide]; - for(size_t iMipLevel = 0u, iEndMipLevel = mPixelData[0].size(); iMipLevel < iEndMipLevel; ++iMipLevel) - { - mEnvironmentMapTexture.Upload(side[iMipLevel], CubeMapLayer::POSITIVE_X + iSide, iMipLevel, 0u, 0u, side[iMipLevel].GetWidth(), side[iMipLevel].GetHeight()); - } - } + mEnvironmentMapTexture = Dali::Scene3D::Internal::ImageResourceLoader::GetCachedCubeTexture(mPixelData, mPixelData[0].size() == 1u); } else { - mEnvironmentMapTexture = Texture::New(TextureType::TEXTURE_2D, mPixelData[0][0].GetPixelFormat(), mPixelData[0][0].GetWidth(), mPixelData[0][0].GetHeight()); - mEnvironmentMapTexture.Upload(mPixelData[0][0], 0, 0, 0, 0, mPixelData[0][0].GetWidth(), mPixelData[0][0].GetHeight()); + mEnvironmentMapTexture = Dali::Scene3D::Internal::ImageResourceLoader::GetCachedTexture(mPixelData[0][0], mPixelData[0].size() == 1u); } // If mipmap is not defined explicitly, use GenerateMipmaps. diff --git a/dali-scene3d/public-api/loader/environment-map-loader.cpp b/dali-scene3d/public-api/loader/environment-map-loader.cpp index 43f3653..4a7c820 100644 --- a/dali-scene3d/public-api/loader/environment-map-loader.cpp +++ b/dali-scene3d/public-api/loader/environment-map-loader.cpp @@ -19,12 +19,13 @@ #include // EXTERNAL INCLUDES -#include +#include #include #include #include // INTERNAL INCLUDES +#include #include #include @@ -90,25 +91,29 @@ uint8_t* GetCroppedBuffer(uint8_t* sourceBuffer, uint32_t bytesPerPixel, uint32_ return destBuffer; } -PixelData GetCubeFace(Devel::PixelBuffer pixelBuffer, uint32_t faceIndex, CubeType cubeType, float faceWidth, float faceHeight) +PixelData GetCubeFace(Dali::PixelData cubePixelData, uint32_t faceIndex, CubeType cubeType, float faceWidth, float faceHeight) { - PixelData pixelData; + PixelData cubeFacePixelData; if(cubeType != NONE) { - uint8_t* imageBuffer = pixelBuffer.GetBuffer(); - uint32_t bytesPerPixel = Pixel::GetBytesPerPixel(pixelBuffer.GetPixelFormat()); - uint32_t imageWidth = pixelBuffer.GetWidth(); - uint32_t imageHeight = pixelBuffer.GetHeight(); - - uint32_t xOffset = CUBEMAP_INDEX_X[cubeType][faceIndex] * static_cast(faceWidth); - uint32_t yOffset = CUBEMAP_INDEX_Y[cubeType][faceIndex] * static_cast(faceHeight); - - uint32_t finalFaceWidth = (xOffset + static_cast(faceWidth) < imageWidth) ? static_cast(faceWidth) : imageWidth - xOffset; - uint32_t finalFaceHeight = (yOffset + static_cast(faceHeight) < imageHeight) ? static_cast(faceHeight) : imageHeight - yOffset; - uint8_t* tempImageBuffer = GetCroppedBuffer(imageBuffer, bytesPerPixel, imageWidth, imageHeight, xOffset, yOffset, finalFaceWidth, finalFaceHeight); - pixelData = PixelData::New(tempImageBuffer, finalFaceWidth * finalFaceHeight * bytesPerPixel, finalFaceWidth, finalFaceHeight, pixelBuffer.GetPixelFormat(), PixelData::FREE); + auto imagePixelFormat = cubePixelData.GetPixelFormat(); + if(!Dali::Pixel::IsCompressed(imagePixelFormat)) + { + uint8_t* imageBuffer = Dali::Integration::GetPixelDataBuffer(cubePixelData).buffer; + uint32_t bytesPerPixel = Pixel::GetBytesPerPixel(imagePixelFormat); + uint32_t imageWidth = cubePixelData.GetWidth(); + uint32_t imageHeight = cubePixelData.GetHeight(); + + uint32_t xOffset = CUBEMAP_INDEX_X[cubeType][faceIndex] * static_cast(faceWidth); + uint32_t yOffset = CUBEMAP_INDEX_Y[cubeType][faceIndex] * static_cast(faceHeight); + + uint32_t finalFaceWidth = (xOffset + static_cast(faceWidth) < imageWidth) ? static_cast(faceWidth) : imageWidth - xOffset; + uint32_t finalFaceHeight = (yOffset + static_cast(faceHeight) < imageHeight) ? static_cast(faceHeight) : imageHeight - yOffset; + uint8_t* tempImageBuffer = GetCroppedBuffer(imageBuffer, bytesPerPixel, imageWidth, imageHeight, xOffset, yOffset, finalFaceWidth, finalFaceHeight); + cubeFacePixelData = PixelData::New(tempImageBuffer, finalFaceWidth * finalFaceHeight * bytesPerPixel, finalFaceWidth, finalFaceHeight, imagePixelFormat, PixelData::FREE); + } } - return pixelData; + return cubeFacePixelData; } /** @@ -126,12 +131,12 @@ bool LoadEnvironmentMapData(const std::string& environmentMapUrl, Scene3D::Loade return false; } - Devel::PixelBuffer pixelBuffer = LoadImageFromFile(environmentMapUrl); - if(pixelBuffer) + Dali::PixelData pixelData = Dali::Scene3D::Internal::ImageResourceLoader::GetCachedPixelData(environmentMapUrl); + if(pixelData) { CubeType cubeType = NONE; - uint32_t imageWidth = pixelBuffer.GetWidth(); - uint32_t imageHeight = pixelBuffer.GetHeight(); + uint32_t imageWidth = pixelData.GetWidth(); + uint32_t imageHeight = pixelData.GetHeight(); /** * If the environment map type is not EQUIRECTANGULAR, * The type should be defined internally. @@ -169,7 +174,7 @@ bool LoadEnvironmentMapData(const std::string& environmentMapUrl, Scene3D::Loade } for(uint32_t i = 0; i < 6; ++i) { - environmentMapData.mPixelData[i][0] = GetCubeFace(pixelBuffer, i, cubeType, faceWidth, faceHeight); + environmentMapData.mPixelData[i][0] = GetCubeFace(pixelData, i, cubeType, faceWidth, faceHeight); } environmentMapData.SetEnvironmentMapType(Scene3D::EnvironmentMapType::CUBEMAP); } @@ -177,7 +182,7 @@ bool LoadEnvironmentMapData(const std::string& environmentMapUrl, Scene3D::Loade { environmentMapData.mPixelData.resize(1); environmentMapData.mPixelData[0].resize(1); - environmentMapData.mPixelData[0][0] = Devel::PixelBuffer::Convert(pixelBuffer); + environmentMapData.mPixelData[0][0] = pixelData; environmentMapData.SetEnvironmentMapType(Scene3D::EnvironmentMapType::EQUIRECTANGULAR); } diff --git a/dali-scene3d/public-api/loader/material-definition.cpp b/dali-scene3d/public-api/loader/material-definition.cpp index 4fc6ac3..0d3cb20 100644 --- a/dali-scene3d/public-api/loader/material-definition.cpp +++ b/dali-scene3d/public-api/loader/material-definition.cpp @@ -20,9 +20,11 @@ // EXTERNAL INCLUDES #include -#include #include +// INTERNAL INCLUDES +#include + namespace Dali { using namespace Toolkit; @@ -110,7 +112,7 @@ Dali::PixelData LoadImageResource(const std::string& resourcePath, else { textureDefinition.mDirectoryPath = resourcePath; - pixelData = SyncImageLoader::Load(resourcePath + textureDefinition.mImageUri, textureDefinition.mMinImageDimensions, fittingMode, textureDefinition.mSamplingMode, orientationCorrection); + pixelData = Internal::ImageResourceLoader::GetCachedPixelData(resourcePath + textureDefinition.mImageUri, textureDefinition.mMinImageDimensions, fittingMode, textureDefinition.mSamplingMode, orientationCorrection); } return pixelData; } @@ -331,12 +333,7 @@ TextureSet MaterialDefinition::Load(const EnvironmentDefinition::Vector& environ Texture texture; if(pixels) { - texture = Texture::New(TextureType::TEXTURE_2D, pixels.GetPixelFormat(), pixels.GetWidth(), pixels.GetHeight()); - texture.Upload(tData.mPixels, 0, 0, 0, 0, pixels.GetWidth(), pixels.GetHeight()); - if(tData.mSamplerFlags & SamplerFlags::MIPMAP_MASK) - { - texture.GenerateMipmaps(); - } + texture = Dali::Scene3D::Internal::ImageResourceLoader::GetCachedTexture(pixels, tData.mSamplerFlags & SamplerFlags::MIPMAP_MASK); } textureSet.SetTexture(n, texture); @@ -347,10 +344,7 @@ TextureSet MaterialDefinition::Load(const EnvironmentDefinition::Vector& environ if(mShadowAvailable) { - PixelData shadowMapPixelData = PixelData::New(new uint8_t[3]{0xff, 0xff, 0xff}, 3, 1, 1, Pixel::RGB888, PixelData::DELETE_ARRAY); - Texture shadowMapTexture = Texture::New(TextureType::TEXTURE_2D, shadowMapPixelData.GetPixelFormat(), shadowMapPixelData.GetWidth(), shadowMapPixelData.GetHeight()); - shadowMapTexture.Upload(shadowMapPixelData, 0, 0, 0, 0, shadowMapPixelData.GetWidth(), shadowMapPixelData.GetHeight()); - textureSet.SetTexture(n++, shadowMapTexture); + textureSet.SetTexture(n++, Dali::Scene3D::Internal::ImageResourceLoader::GetEmptyTextureWhiteRGB()); } // Assign textures to slots -- starting with 2D ones, then cubemaps, if any. -- 2.7.4