From 388d44b93453d5fddd4bdaab9010d9d312833dac Mon Sep 17 00:00:00 2001 From: xu Date: Mon, 5 Sep 2022 11:13:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=B8=B2=E6=9F=93=E5=8E=8B?= =?UTF-8?q?=E7=BC=A9=E7=BA=B9=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: xu --- adapter/ohos/build/config.gni | 1 + adapter/ohos/osal/system_properties.cpp | 18 + adapter/preview/osal/system_properties.cpp | 3 + frameworks/base/utils/system_properties.h | 18 + frameworks/core/BUILD.gn | 9 + .../components/image/rosen_render_image.cpp | 5 +- frameworks/core/image/image_cache.cpp | 6 +- frameworks/core/image/image_cache.h | 3 +- frameworks/core/image/image_compressor.cpp | 504 ++++++++++++++++++ frameworks/core/image/image_compressor.h | 98 ++++ frameworks/core/image/image_loader.cpp | 25 +- frameworks/core/image/image_object.cpp | 74 ++- frameworks/core/image/image_provider.cpp | 119 +++-- frameworks/core/image/image_provider.h | 12 +- test/fuzztest/imageloader_fuzzer/BUILD.gn | 1 + 15 files changed, 819 insertions(+), 77 deletions(-) create mode 100644 frameworks/core/image/image_compressor.cpp create mode 100644 frameworks/core/image/image_compressor.h diff --git a/adapter/ohos/build/config.gni b/adapter/ohos/build/config.gni index 0eeea22fbed..3d494111910 100644 --- a/adapter/ohos/build/config.gni +++ b/adapter/ohos/build/config.gni @@ -77,6 +77,7 @@ video_components_support = true image_components_support = true preview_support = false enable_system_clipboard = true +enable_image_compression = true if (defined(preview_support) && preview_support) { defines += [ "PREVIEW" ] diff --git a/adapter/ohos/osal/system_properties.cpp b/adapter/ohos/osal/system_properties.cpp index be65f767b7d..fa862ea5ead 100644 --- a/adapter/ohos/osal/system_properties.cpp +++ b/adapter/ohos/osal/system_properties.cpp @@ -222,6 +222,21 @@ bool SystemProperties::IsScoringEnabled(const std::string& name) return prop == name; } +bool GetAstcEnabled() +{ + return system::GetParameter("persist.astc.enable", "true") == "true"; +} + +int32_t GetAstcMaxErrorProp() +{ + return system::GetIntParameter("persist.astc.max", 50000); +} + +int32_t GetAstcPsnrProp() +{ + return system::GetIntParameter("persist.astc.psnr", 0); +} + bool SystemProperties::traceEnabled_ = IsTraceEnabled(); bool SystemProperties::svgTraceEnable_ = IsSvgTraceEnabled(); bool SystemProperties::accessibilityEnabled_ = IsAccessibilityEnabled(); @@ -250,6 +265,9 @@ bool SystemProperties::debugBoundaryEnabled_ = false; bool SystemProperties::windowAnimationEnabled_ = IsWindowAnimationEnabled(); bool SystemProperties::debugEnabled_ = IsDebugEnabled(); bool SystemProperties::gpuUploadEnabled_ = IsGpuUploadEnabled(); +bool SystemProperties::astcEnabled_ = GetAstcEnabled(); +int32_t SystemProperties::astcMax_ = GetAstcMaxErrorProp(); +int32_t SystemProperties::astcPsnr_ = GetAstcPsnrProp(); DeviceType SystemProperties::GetDeviceType() { diff --git a/adapter/preview/osal/system_properties.cpp b/adapter/preview/osal/system_properties.cpp index 97261d92a5d..19b33e6c410 100644 --- a/adapter/preview/osal/system_properties.cpp +++ b/adapter/preview/osal/system_properties.cpp @@ -86,6 +86,9 @@ bool SystemProperties::windowAnimationEnabled_ = false; bool SystemProperties::debugBoundaryEnabled_ = false; bool SystemProperties::gpuUploadEnabled_ = false; bool SystemProperties::isHookModeEnabled_ = false; +bool SystemProperties::astcEnabled_ = false; +int SystemProperties::astcMax_ = 0; +int SystemProperties::astcPsnr_ = 0; bool SystemProperties::GetDebugBoundaryEnabled() { diff --git a/frameworks/base/utils/system_properties.h b/frameworks/base/utils/system_properties.h index c61559bc615..86278a309c3 100644 --- a/frameworks/base/utils/system_properties.h +++ b/frameworks/base/utils/system_properties.h @@ -292,6 +292,21 @@ public: return windowAnimationEnabled_; } + static bool IsAstcEnabled() + { + return astcEnabled_; + } + + static int32_t GetAstcMaxError() + { + return astcMax_; + } + + static int32_t GetAstcPsnr() + { + return astcPsnr_; + } + private: static bool traceEnabled_; static bool svgTraceEnable_; @@ -321,6 +336,9 @@ private: static bool debugBoundaryEnabled_; static bool gpuUploadEnabled_; static bool isHookModeEnabled_; + static bool astcEnabled_; + static int32_t astcMax_; + static int32_t astcPsnr_; }; } // namespace OHOS::Ace diff --git a/frameworks/core/BUILD.gn b/frameworks/core/BUILD.gn index 03cdeac3683..6c959d4cf11 100644 --- a/frameworks/core/BUILD.gn +++ b/frameworks/core/BUILD.gn @@ -172,6 +172,7 @@ template("ace_core_source_set") { "image/animated_image_player.cpp", "image/flutter_image_cache.cpp", "image/image_cache.cpp", + "image/image_compressor.cpp", "image/image_loader.cpp", "image/image_object.cpp", "image/image_provider.cpp", @@ -345,6 +346,14 @@ template("ace_core_source_set") { "$ace_root/frameworks/core/components_ng/syntax:ace_core_components_syntax_ng_$platform", ] + if (defined(config.enable_image_compression) && + config.enable_image_compression) { + if (product_name != "ohos-sdk") { + deps += [ "//third_party/opencl-headers:libcl" ] + defines += [ "ENABLE_OPENCL" ] + } + } + if (defined(config.enable_rosen_backend) && config.enable_rosen_backend) { sources += [ "animation/native_curve_helper.cpp" ] deps += [ "//foundation/graphic/graphic_2d/rosen/modules/render_service_client:librender_service_client" ] diff --git a/frameworks/core/components/image/rosen_render_image.cpp b/frameworks/core/components/image/rosen_render_image.cpp index 1041338279d..264a5a10bc7 100644 --- a/frameworks/core/components/image/rosen_render_image.cpp +++ b/frameworks/core/components/image/rosen_render_image.cpp @@ -771,7 +771,7 @@ void RosenRenderImage::CanvasDrawImageRect( if (GetBackgroundImageFlag()) { return; } - if (!image_ || !image_->image()) { + if (!image_ || (!image_->image() && !image_->compressData())) { imageDataNotReady_ = true; LOGD("image data is not ready, rawImageSize_: %{public}s, image source: %{private}s", rawImageSize_.ToString().c_str(), sourceInfo_.ToString().c_str()); @@ -784,7 +784,8 @@ void RosenRenderImage::CanvasDrawImageRect( if (GetAdaptiveFrameRectFlag()) { recordingCanvas->translate(imageRenderPosition_.GetX() * -1, imageRenderPosition_.GetY() * -1); Rosen::RsImageInfo rsImageInfo(fitNum, repeatNum, radii_, scale_); - recordingCanvas->DrawImageWithParm(image_->image(), rsImageInfo, paint); + recordingCanvas->DrawImageWithParm(image_->image(), image_->compressData(), rsImageInfo, paint); + image_->setCompress(nullptr, 0, 0); return; } bool isLoading = ((imageLoadingStatus_ == ImageLoadingStatus::LOADING) || diff --git a/frameworks/core/image/image_cache.cpp b/frameworks/core/image/image_cache.cpp index 15171771116..50b9afafe90 100644 --- a/frameworks/core/image/image_cache.cpp +++ b/frameworks/core/image/image_cache.cpp @@ -198,10 +198,11 @@ RefPtr ImageCache::GetCacheImageData(const std::string& key) } } -void ImageCache::WriteCacheFile(const std::string& url, const void* const data, const size_t size) +void ImageCache::WriteCacheFile(const std::string& url, const void* const data, + const size_t size, const std::string suffix) { std::vector removeVector; - std::string cacheNetworkFilePath = GetImageCacheFilePath(url); + std::string cacheNetworkFilePath = GetImageCacheFilePath(url) + suffix; std::lock_guard lock(cacheFileInfoMutex_); // 1. first check if file has been cached. @@ -221,6 +222,7 @@ void ImageCache::WriteCacheFile(const std::string& url, const void* const data, return; } outFile.write(reinterpret_cast(data), size); + LOGI("write image cache: %{public}s %{private}s", url.c_str(), cacheNetworkFilePath.c_str()); cacheFileSize_ += size; cacheFileInfo_.emplace_back(cacheNetworkFilePath, size, time(nullptr)); diff --git a/frameworks/core/image/image_cache.h b/frameworks/core/image/image_cache.h index cc7f8e44102..1544cb5bdcc 100644 --- a/frameworks/core/image/image_cache.h +++ b/frameworks/core/image/image_cache.h @@ -89,7 +89,8 @@ public: RefPtr GetCacheImgObj(const std::string& key); static void SetCacheFileInfo(); - static void WriteCacheFile(const std::string& url, const void * const data, const size_t size); + static void WriteCacheFile(const std::string& url, const void * const data, + const size_t size, const std::string suffix = std::string()); void SetCapacity(size_t capacity) { diff --git a/frameworks/core/image/image_compressor.cpp b/frameworks/core/image/image_compressor.cpp new file mode 100644 index 00000000000..5656282e876 --- /dev/null +++ b/frameworks/core/image/image_compressor.cpp @@ -0,0 +1,504 @@ +/* + * Copyright (c) 2022 Huawei Device 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 +#ifdef ENABLE_OPENCL +#include +#endif // ENABLE_OPENCL + +#include "base/log/ace_trace.h" +#include "base/log/log.h" +#include "base/thread/background_task_executor.h" +#include "base/utils/system_properties.h" +#include "core/image/image_cache.h" +#include "core/image/image_compressor.h" + +namespace OHOS::Ace { +std::shared_ptr ImageCompressor::instance_ = nullptr; +std::shared_ptr ImageCompressor::GetInstance() +{ + if (instance_ == nullptr) { + instance_.reset(new ImageCompressor()); + instance_->Init(); + } + return instance_; +} + +void ImageCompressor::Init() +{ +#ifdef ENABLE_OPENCL + switch_ = SystemProperties::IsAstcEnabled(); + if (switch_) { + clOk_ = OHOS::InitOpenCL(); + maxErr_ = SystemProperties::GetAstcMaxError(); + psnr_ = SystemProperties::GetAstcPsnr(); + InitPartition(); + } +#endif // ENABLE_OPENCL +} + +bool ImageCompressor::CanCompress() +{ +#ifdef UPLOAD_GPU_DISABLED + return false; +#else + if (switch_ && clOk_) { + return true; + } + return false; +#endif +} + +#ifdef ENABLE_OPENCL +cl_program ImageCompressor::LoadShaderBin(cl_context context, cl_device_id device_id) +{ + ACE_FUNCTION_TRACE(); + std::unique_ptr file(fopen(shader_path_.c_str(), "rb"), fclose); + if (!file) { + LOGE("load cl shader failed"); + return nullptr; + } + auto data = SkData::MakeFromFILE(file.get()); + if (!data) { + return nullptr; + } + cl_int err; + size_t len = data->size(); + auto ptr = (const unsigned char*) data->data(); + cl_program p = clCreateProgramWithBinary(context, 1, &device_id, &len, &ptr, NULL, &err); + if (err) { + return nullptr; + } + LOGD("load cl shader"); + return p; +} + +bool ImageCompressor::CreateKernel() +{ + if (!context_ || !kernel_) { + cl_int err; + cl_platform_id platform_id; + cl_device_id device_id; + clGetPlatformIDs(1, &platform_id, NULL); + clGetDeviceIDs(platform_id, CL_DEVICE_TYPE_GPU, 1, &device_id, NULL); + context_ = clCreateContext(0, 1, &device_id, NULL, NULL, &err); + queue_ = clCreateCommandQueueWithProperties(context_, device_id, 0, &err); + + cl_program program = LoadShaderBin(context_, device_id); + clBuildProgram(program, 1, &device_id, compileOption_.c_str(), NULL, NULL); + ACE_SCOPED_TRACE("clCreateKernel"); + kernel_ = clCreateKernel(program, "astc", &err); + clReleaseProgram(program); + } + if (!context_ || !kernel_ || !queue_) { + ReleaseResource(); + LOGE("build opencl program failed"); + clOk_ = false; + return false; + } + refCount_++; + return true; +} + +bool ImageCompressor::CheckImageQuality(std::string key, uint32_t sumErr, uint32_t maxErr, int32_t width, int32_t height) +{ + bool isOk = true; + float mse = (float)sumErr / (width * height); + float psnr = 10 * log10(255 * 255 / mse); + if (maxErr == 0 || psnr == 0 || maxErr > maxErr_ || (int32_t)psnr < psnr_) { + isOk = false; + std::lock_guard mLock(recordsMutex_); + failedRecords_.insert(key); + } + LOGI("compress quality %{public}s [%{public}u, %{public}.2f] size(%{public}d×%{public}d) %{public}s", + key.c_str(), maxErr, psnr, width, height, isOk ? "ok" : "no"); + return isOk; +} + +void ImageCompressor::ReleaseResource() +{ + ACE_FUNCTION_TRACE(); + clReleaseKernel(kernel_); + kernel_ = NULL; + clReleaseCommandQueue(queue_); + queue_ = NULL; + clReleaseContext(context_); + context_ = NULL; +} +#endif // ENABLE_OPENCL + +sk_sp ImageCompressor::GpuCompress(std::string key, SkPixmap& pixmap, int32_t width, int32_t height) +{ +#ifdef ENABLE_OPENCL + if (width <= 0 || height <= 0 || !clOk_ || IsFailedImage(key) || width > maxSize_ || height > maxSize_) { + return nullptr; + } + if (!CreateKernel()) { + return nullptr; + } + ACE_SCOPED_TRACE("GpuCompress %d×%d", width, height); + + cl_int err; + + // Number of work items in each local work group + int32_t blockX = ceil((width + DIM - 1) / DIM); + int32_t blockY = ceil((height + DIM - 1) / DIM); + int32_t numBlocks = blockX * blockY; + size_t local[] = { DIM, DIM }; + size_t global[2]; + global[0] = (width % local[0] == 0 ? width : (width + local[0] - width % local[0])); + global[1] = (height % local[1] == 0 ? height : (height + local[1] - height % local[1])); + + size_t astc_size = numBlocks * DIM * DIM; + + cl_image_format image_format = { CL_RGBA, CL_UNORM_INT8 }; + cl_image_desc desc = { CL_MEM_OBJECT_IMAGE2D, width, height }; + cl_mem inputImage = clCreateImage(context_, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, + &image_format, &desc, const_cast(pixmap.addr()), &err); + cl_mem astcResult = clCreateBuffer(context_, CL_MEM_ALLOC_HOST_PTR, astc_size, NULL, &err); + cl_mem partInfos = clCreateBuffer(context_, CL_MEM_COPY_HOST_PTR, + sizeof(PartInfo) * parts_.size(), &parts_[0], &err); + + uint32_t* blockErrs = new uint32_t[numBlocks]{0}; + cl_mem clErrs = clCreateBuffer(context_, CL_MEM_USE_HOST_PTR, sizeof(uint32_t) * numBlocks, blockErrs, &err); + err |= clSetKernelArg(kernel_, 0, sizeof(cl_mem), &inputImage); + err |= clSetKernelArg(kernel_, 1, sizeof(cl_mem), &astcResult); + err |= clSetKernelArg(kernel_, 2, sizeof(cl_mem), &partInfos); + err |= clSetKernelArg(kernel_, 3, sizeof(cl_mem), &clErrs); + + err = clEnqueueNDRangeKernel(queue_, kernel_, 2, NULL, global, local, 0, NULL, NULL); + + clFinish(queue_); + + uint32_t max_val = 0, sum_val = 0; + err = clEnqueueReadBuffer(queue_, clErrs, CL_TRUE, 0, sizeof(uint32_t) * numBlocks, blockErrs, 0, NULL, NULL); + for (int32_t i = 0; i < numBlocks; i++) { + sum_val += blockErrs[i]; + max_val = fmax(max_val, blockErrs[i]); + } + + clReleaseMemObject(inputImage); + clReleaseMemObject(partInfos); + clReleaseMemObject(clErrs); + delete[] blockErrs; + + if (!CheckImageQuality(key, sum_val, max_val, width, height)) { + clReleaseMemObject(astcResult); + return nullptr; + } + + auto astc_data = SkData::MakeUninitialized(astc_size); + clEnqueueReadBuffer(queue_, astcResult, CL_TRUE, 0, astc_size, astc_data->writable_data(), 0, NULL, NULL); + clReleaseMemObject(astcResult); + return astc_data; +#else + return nullptr; +#endif // ENABLE_OPENCL +} + + +std::function ImageCompressor::ScheduleReleaseTask() +{ + std::function task = [this]() { +#ifdef ENABLE_OPENCL + if (refCount_ > 0 && clOk_) { + refCount_--; + if (refCount_ <= 0) { + this->ReleaseResource(); + + // save failed records + std::ofstream saveFile(recordsPath_); + if (!saveFile.is_open()) { + return; + } + std::lock_guard mLock(recordsMutex_); + for (auto s : failedRecords_) { + saveFile << s << "\n"; + } + saveFile.close(); + } + } +#endif // ENABLE_OPENCL + }; + return task; +} + +void ImageCompressor::WriteToFile(std::string srcKey, sk_sp compressedData, Size imgSize) +{ + if (!compressedData || srcKey.empty()) { + return; + } +#ifdef ENABLE_OPENCL + BackgroundTaskExecutor::GetInstance().PostTask( + [srcKey, compressedData, imgSize]() { + AstcHeader header; + int32_t xsize = imgSize.Width(); + int32_t ysize = imgSize.Height(); + header.magic[0] = MAGIC_FILE_CONSTANT & 0xFF; + header.magic[1] = (MAGIC_FILE_CONSTANT >> 8) & 0xFF; + header.magic[2] = (MAGIC_FILE_CONSTANT >> 16) & 0xFF; + header.magic[3] = (MAGIC_FILE_CONSTANT >> 24) & 0xFF; + header.blockdimX = DIM; + header.blockdimY = DIM; + header.blockdimZ = 1; + header.xsize[0] = xsize & 0xFF; + header.xsize[1] = (xsize >> 8) & 0xFF; + header.xsize[2] = (xsize >> 16) & 0xFF; + header.ysize[0] = ysize & 0xFF; + header.ysize[1] = (ysize >> 8) & 0xFF; + header.ysize[2] = (ysize >> 16) & 0xFF; + header.zsize[0] = 1; + header.zsize[1] = 0; + header.zsize[2] = 0; + LOGD("astc write file %{public}s size(%{public}d×%{public}d) (%{public}.2f×%{public}.2f)", + srcKey.c_str(), xsize, ysize, imgSize.Width(), imgSize.Height()); + + int32_t fileSize = compressedData->size() + sizeof(header); + sk_sp toWrite = SkData::MakeUninitialized(fileSize); + uint8_t* toWritePtr = (uint8_t*) toWrite->writable_data(); + if (memcpy_s(toWritePtr, fileSize, &header, sizeof(header)) != EOK) { + LOGE("astc write file failed"); + return; + } + if (memcpy_s(toWritePtr + sizeof(header), compressedData->size(), + compressedData->data(), compressedData->size()) != EOK) { + LOGE("astc write file failed"); + return; + } + + ImageCache::WriteCacheFile(srcKey, toWritePtr, fileSize, ".astc"); + }, BgTaskPriority::LOW); +#endif +} + +sk_sp ImageCompressor::StripFileHeader(sk_sp fileData) +{ + if (fileData) { + auto imageData = SkData::MakeSubset(fileData.get(), sizeof(AstcHeader), fileData->size() - sizeof(AstcHeader)); + if (!imageData->isEmpty()) { + return imageData; + } + } + return nullptr; +} + +/** + * @brief Hash function used for procedural partition assignment. + * + * @param seed The hash seed. + * + * @return The hashed value. + */ +static uint32_t Hash52(uint32_t seed) +{ + seed ^= seed >> 15; + + // (2^4 + 1) * (2^7 + 1) * (2^17 - 1) + seed *= 0xEEDE0891; + seed ^= seed >> 5; + seed += seed << 16; + seed ^= seed >> 7; + seed ^= seed >> 3; + seed ^= seed << 6; + seed ^= seed >> 17; + return seed; +} + +/** + * @brief Select texel assignment for a single coordinate. + * + * @param seed The seed - the partition index from the block. + * @param x The texel X coordinate in the block. + * @param y The texel Y coordinate in the block. + * @param z The texel Z coordinate in the block. + * @param partitionCount The total partition count of this encoding. + * @param smallBlock @c true if the blockhas fewer than 32 texels. + * + * @return The assigned partition index for this texel. + */ +static uint8_t SelectPartition(int32_t seed, int32_t x, int32_t y, int32_t z, int32_t partitionCount, bool smallBlock) +{ + // For small blocks bias the coordinates to get better distribution + if (smallBlock) { + x <<= 1; + y <<= 1; + z <<= 1; + } + + seed += (partitionCount - 1) * 1024; + + uint32_t num = Hash52(seed); + + uint8_t seed1 = num & 0xF; + uint8_t seed2 = (num >> 4) & 0xF; + uint8_t seed3 = (num >> 8) & 0xF; + uint8_t seed4 = (num >> 12) & 0xF; + uint8_t seed5 = (num >> 16) & 0xF; + uint8_t seed6 = (num >> 20) & 0xF; + uint8_t seed7 = (num >> 24) & 0xF; + uint8_t seed8 = (num >> 28) & 0xF; + uint8_t seed9 = (num >> 18) & 0xF; + uint8_t seed10 = (num >> 22) & 0xF; + uint8_t seed11 = (num >> 26) & 0xF; + uint8_t seed12 = ((num >> 30) | (num << 2)) & 0xF; + + // Squaring all the seeds in order to bias their distribution towards lower values. + seed1 *= seed1; + seed2 *= seed2; + seed3 *= seed3; + seed4 *= seed4; + seed5 *= seed5; + seed6 *= seed6; + seed7 *= seed7; + seed8 *= seed8; + seed9 *= seed9; + seed10 *= seed10; + seed11 *= seed11; + seed12 *= seed12; + + int32_t sh1, sh2; + if (seed & 1) { + sh1 = (seed & 2 ? 4 : 5); + sh2 = (partitionCount == 3 ? 6 : 5); + } else { + sh1 = (partitionCount == 3 ? 6 : 5); + sh2 = (seed & 2 ? 4 : 5); + } + + int32_t sh3 = (seed & 0x10) ? sh1 : sh2; + + seed1 >>= sh1; + seed2 >>= sh2; + seed3 >>= sh1; + seed4 >>= sh2; + seed5 >>= sh1; + seed6 >>= sh2; + seed7 >>= sh1; + seed8 >>= sh2; + + seed9 >>= sh3; + seed10 >>= sh3; + seed11 >>= sh3; + seed12 >>= sh3; + + int32_t a = seed1 * x + seed2 * y + seed11 * z + (num >> 14); + int32_t b = seed3 * x + seed4 * y + seed12 * z + (num >> 10); + int32_t c = seed5 * x + seed6 * y + seed9 * z + (num >> 6); + int32_t d = seed7 * x + seed8 * y + seed10 * z + (num >> 2); + + // Apply the saw + a &= 0x3F; + b &= 0x3F; + c &= 0x3F; + d &= 0x3F; + + // Remove some of the components if we are to output < 4 partitions_. + if (partitionCount <= 3) { + d = 0; + } + + if (partitionCount <= 2) { + c = 0; + } + + if (partitionCount <= 1) { + b = 0; + } + + uint8_t partition; + if (a >= b && a >= c && a >= d) { + partition = 0; + } else if (b >= c && b >= d) { + partition = 1; + } else if (c >= d) { + partition = 2; + } else { + partition = 3; + } + + return partition; +} + +bool ImageCompressor::InitPartitionInfo(PartInfo *partInfos, int32_t part_index, int32_t part_count) +{ + int32_t texIdx = 0; + int32_t counts[4] = {0}; + for (int32_t y = 0; y < DIM; y++) { + for (int32_t x = 0; x < DIM; x++) { + int32_t part = SelectPartition(part_index, x, y, 0, part_count, true); + partInfos->bitmaps[part] |= 1u << texIdx; + counts[part]++; + texIdx++; + } + } + int32_t realPartCount = 0; + if (counts[0] == 0) { + realPartCount = 0; + } else if (counts[1] == 0) { + realPartCount = 1; + } else if (counts[2] == 0) { + realPartCount = 2; + } else if (counts[3] == 0) { + realPartCount = 3; + } else { + realPartCount = 4; + } + if (realPartCount == part_count) { + return true; + } + return false; +} + +void ImageCompressor::InitPartition() +{ + parts_.clear(); + int32_t arrSize = sizeof(partitions_) / sizeof(partitions_[0]); + for (int32_t i = 0; i < arrSize; i++) { + PartInfo p = {}; + if (InitPartitionInfo(&p, partitions_[i], 2)) { + p.partid = partitions_[i]; + parts_.push_back(p); + LOGD("part id:%d %d %d", p.partid, p.bitmaps[0], p.bitmaps[1]); + } + } + compileOption_ = "-D PARTITION_SERACH_MAX=" + std::to_string(parts_.size()); +} + +#ifdef ENABLE_OPENCL +bool ImageCompressor::IsFailedImage(std::string key) +{ + std::lock_guard mLock(recordsMutex_); + return failedRecords_.find(key) != failedRecords_.end(); +} +#endif + +void ImageCompressor::InitRecords() +{ + recordsPath_ = ImageCache::GetImageCacheFilePath("record") + ".txt"; + std::ifstream openFile(recordsPath_); + if (openFile.fail()) { + openFile.close(); + return; + } + std::string line; + std::lock_guard mLock(recordsMutex_); + while (!openFile.eof()) { + std::getline(openFile, line); + failedRecords_.insert(line); + } + openFile.close(); +} +} // namespace OHOS::Ace diff --git a/frameworks/core/image/image_compressor.h b/frameworks/core/image/image_compressor.h new file mode 100644 index 00000000000..56a5b808309 --- /dev/null +++ b/frameworks/core/image/image_compressor.h @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2022 Huawei Device 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. + */ + +#ifndef FOUNDATION_ACE_FRAMEWORKS_CORE_IMAGE_IMAGE_COMPRESSOR_H +#define FOUNDATION_ACE_FRAMEWORKS_CORE_IMAGE_IMAGE_COMPRESSOR_H + +#include +#include +#include + +#include "base/geometry/size.h" +#ifdef ENABLE_OPENCL +#define USE_OPENCL_WRAPPER +#include "opencl_wrapper.h" +#endif // ENABLE_OPENCL +#include "third_party/skia/include/core/SkData.h" +#include "third_party/skia/include/core/SkGraphics.h" +#include "third_party/skia/include/core/SkPixmap.h" + +namespace OHOS::Ace { +#define MAGIC_FILE_CONSTANT 0x5CA1AB13 +#define DIM 4 +typedef struct { + uint8_t magic[4]; + uint8_t blockdimX; + uint8_t blockdimY; + uint8_t blockdimZ; + uint8_t xsize[3]; + uint8_t ysize[3]; + uint8_t zsize[3]; +} AstcHeader; + +class ImageCompressor { +public: + static std::shared_ptr GetInstance(); + static const int32_t releaseTimeMs = 1000; + + bool CanCompress(); + sk_sp GpuCompress(std::string key, SkPixmap& pixmap, int32_t width, int32_t height); + std::function ScheduleReleaseTask(); + void WriteToFile(std::string key, sk_sp compressdImage, Size size); + static sk_sp StripFileHeader(sk_sp fileData); + +private: + static std::shared_ptr instance_; + bool clOk_; + bool switch_; + void Init(); +#ifdef ENABLE_OPENCL + static const int32_t maxSize_ = 100000; + int32_t maxErr_; + int32_t psnr_; + const std::string shader_path_ = "/system/bin/astc.bin"; + std::atomic refCount_; + cl_context context_; + cl_command_queue queue_; + cl_kernel kernel_; + + cl_program LoadShaderBin(cl_context context, cl_device_id device_id); + bool CreateKernel(); + void ReleaseResource(); + bool CheckImageQuality(std::string key, uint32_t sumErr, uint32_t maxErr, int32_t width, int32_t height); + bool IsFailedImage(std::string key); +#endif + int32_t partitions_[73] = { + 2, 5, 9, 14, 16, 17, 20, 24, 25, 28, 36, 39, 43, 48, 49, 50, 51, 53, 55, 61, 72, 78, 107, 113, 116, 149, 156, + 198, 204, 210, 216, 226, 232, 239, 269, 273, 293, 324, 344, 348, 359, 389, 394, 441, 443, 475, 476, 479, 496, + 511, 567, 593, 594, 600, 601, 666, 684, 703, 726, 730, 732, 756, 796, 799, 828, 958, 959, 988, 993 + }; + struct PartInfo { + int32_t partid; + uint32_t bitmaps[2]; + }; + void InitPartition(); + bool InitPartitionInfo(PartInfo *partInfo, int32_t partIndex, int32_t partCount); + std::vector parts_; + std::string compileOption_; + + mutable std::mutex recordsMutex_; + std::set failedRecords_; + std::string recordsPath_; + void InitRecords(); +}; +} // namespace OHOS::Ace + +#endif // FOUNDATION_ACE_FRAMEWORKS_CORE_IMAGE_IMAGE_COMPRESSOR_H diff --git a/frameworks/core/image/image_loader.cpp b/frameworks/core/image/image_loader.cpp index 759181dab97..1f50fbcfe4c 100644 --- a/frameworks/core/image/image_loader.cpp +++ b/frameworks/core/image/image_loader.cpp @@ -145,6 +145,7 @@ RefPtr ImageLoader::GetImageData( sk_sp FileImageLoader::LoadImageData( const ImageSourceInfo& imageSourceInfo, const WeakPtr context) { + ACE_FUNCTION_TRACE(); auto src = imageSourceInfo.GetSrc(); std::string filePath = RemovePathHead(src); if (imageSourceInfo.GetSrcType() == SrcType::INTERNAL) { @@ -216,6 +217,7 @@ sk_sp DataProviderImageLoader::LoadImageData( sk_sp AssetImageLoader::LoadImageData( const ImageSourceInfo& imageSourceInfo, const WeakPtr context) { + ACE_FUNCTION_TRACE(); auto src = imageSourceInfo.GetSrc(); if (src.empty()) { LOGE("image src is empty"); @@ -243,9 +245,24 @@ sk_sp AssetImageLoader::LoadImageData( LOGE("No asset data!"); return nullptr; } - const uint8_t* data = assetData->GetData(); - const size_t dataSize = assetData->GetSize(); - return SkData::MakeWithCopy(data, dataSize); + std::string assetString = assetManager->GetAssetPath(assetSrc); + std::string filePath = assetString + src; + if (filePath.length() > PATH_MAX) { + LOGE("src path too long"); + return nullptr; + } + char realPath[PATH_MAX] = { 0x00 }; + if (realpath(filePath.c_str(), realPath) == nullptr) { + LOGE("realpath fail! filePath: %{private}s, fail reason: %{public}s", filePath.c_str(), + strerror(errno)); + return nullptr; + } + std::unique_ptr file(fopen(realPath, "rb"), fclose); + if (!file) { + LOGE("asset image open failed"); + return nullptr; + } + return SkData::MakeFromFILE(file.get()); } std::string AssetImageLoader::LoadJsonData(const std::string& src, const WeakPtr context) @@ -444,4 +461,4 @@ sk_sp ResourceImageLoader::LoadImageData( return SkData::MakeWithCopy(mediaRes.c_str(), mediaRes.size()); } -} // namespace OHOS::Ace \ No newline at end of file +} // namespace OHOS::Ace diff --git a/frameworks/core/image/image_object.cpp b/frameworks/core/image/image_object.cpp index dc66b924942..0cf937a7f0f 100644 --- a/frameworks/core/image/image_object.cpp +++ b/frameworks/core/image/image_object.cpp @@ -14,6 +14,7 @@ */ #include "core/image/image_object.h" +#include "core/image/image_compressor.h" #include "base/thread/background_task_executor.h" #include "core/common/container.h" @@ -129,6 +130,7 @@ void StaticImageObject::UploadToGpuForRender(const WeakPtr& contex } auto key = GenerateCacheKey(imageSource, imageSize); + // is already uploaded if (!ImageProvider::TryUploadingImage(key, successCallback, failedCallback)) { LOGI("other thread is uploading same image to gpu : %{public}s", imageSource.ToString().c_str()); return; @@ -147,32 +149,20 @@ void StaticImageObject::UploadToGpuForRender(const WeakPtr& contex cachedFlutterImage = cachedImage->imagePtr; } } + // found cached image obj (can be rendered) if (cachedFlutterImage) { LOGD("get cached image success: %{public}s", key.c_str()); ImageProvider::ProccessUploadResult(taskExecutor, imageSource, imageSize, cachedFlutterImage); return; } - if (!skData) { - LOGD("reload sk data"); - skData = ImageProvider::LoadImageRawData(imageSource, pipelineContext, imageSize); - if (!skData) { - LOGE("reload image data failed. imageSource: %{private}s", imageSource.ToString().c_str()); + auto callback = [successCallback, imageSource, taskExecutor, imageCache, + imageSize, key, id = Container::CurrentId()] + (flutter::SkiaGPUObject image, sk_sp compressData) { + if (!image.get() && !compressData.get()) { ImageProvider::ProccessUploadResult(taskExecutor, imageSource, imageSize, nullptr, - "Image data may be broken or absent, please check if image file or image data is valid."); - return; + "Image data may be broken or absent in upload callback."); } - } - auto rawImage = SkImage::MakeFromEncoded(skData); - if (!rawImage) { - LOGE("static image MakeFromEncoded fail! imageSource: %{private}s", imageSource.ToString().c_str()); - ImageProvider::ProccessUploadResult(taskExecutor, imageSource, imageSize, nullptr, - "Image data may be broken, please check if image file or image data is broken."); - return; - } - auto image = ImageProvider::ResizeSkImage(rawImage, imageSource.GetSrc(), imageSize, forceResize); - auto callback = [successCallback, imageSource, taskExecutor, imageCache, imageSize, key, - id = Container::CurrentId()](flutter::SkiaGPUObject image) { ContainerScope scope(id); #ifdef NG_BUILD auto canvasImage = NG::CanvasImage::Create(); @@ -183,6 +173,9 @@ void StaticImageObject::UploadToGpuForRender(const WeakPtr& contex #else auto canvasImage = flutter::CanvasImage::Create(); canvasImage->set_image(std::move(image)); + int32_t width = static_cast(imageSize.Width() + 0.5); + int32_t height = static_cast(imageSize.Height() + 0.5); + canvasImage->setCompress(std::move(compressData), width, height); #endif if (imageCache) { LOGD("cache image key: %{public}s", key.c_str()); @@ -190,7 +183,48 @@ void StaticImageObject::UploadToGpuForRender(const WeakPtr& contex } ImageProvider::ProccessUploadResult(taskExecutor, imageSource, imageSize, canvasImage); }; - ImageProvider::UploadImageToGPUForRender(image, callback, renderTaskHolder); + // here skdata is origin pic, also have 'target size' + // if have skdata, means origin pic is rendered first time + // if no skdata, means origin pic has shown, and has been cleared + // we try to use small image or compressed image instead of origin pic. + sk_sp stripped; + if (ImageCompressor::GetInstance()->CanCompress() && imageSize.IsValid()) { + // load compressed + auto compressedData = ImageProvider::LoadImageRawDataFromFileCache(pipelineContext, key, ".astc"); + stripped = ImageCompressor::StripFileHeader(compressedData); + } + auto smallData = ImageProvider::LoadImageRawDataFromFileCache(pipelineContext, key); + if (smallData) { + skData = smallData; + } + + if (!skData) { + LOGD("reload sk data"); + skData = ImageProvider::LoadImageRawData(imageSource, pipelineContext); + if (!skData) { + LOGE("reload image data failed. imageSource: %{private}s", imageSource.ToString().c_str()); + ImageProvider::ProccessUploadResult(taskExecutor, imageSource, imageSize, nullptr, + "Image data may be broken or absent, please check if image file or image data is valid."); + return; + } + } + + // make lazy image from file + auto rawImage = SkImage::MakeFromEncoded(skData); + if (!rawImage) { + LOGE("static image MakeFromEncoded fail! imageSource: %{private}s", imageSource.ToString().c_str()); + ImageProvider::ProccessUploadResult(taskExecutor, imageSource, imageSize, nullptr, + "Image data may be broken, please check if image file or image data is broken."); + return; + } + sk_sp image; + if (smallData) { + image = rawImage; + } else { + image = ImageProvider::ResizeSkImage(rawImage, imageSource.GetSrc(), imageSize, forceResize); + } + ImageProvider::UploadImageToGPUForRender(image, stripped, callback, renderTaskHolder, key); + skData = nullptr; }; if (syncMode) { task(); @@ -242,4 +276,4 @@ Size PixelMapImageObject::MeasureForImage(RefPtr image) return image->MeasureForPixmap(); } -} // namespace OHOS::Ace \ No newline at end of file +} // namespace OHOS::Ace diff --git a/frameworks/core/image/image_provider.cpp b/frameworks/core/image/image_provider.cpp index 4ddf30148d7..5be7c21b6fd 100644 --- a/frameworks/core/image/image_provider.cpp +++ b/frameworks/core/image/image_provider.cpp @@ -21,17 +21,18 @@ #include "third_party/skia/include/core/SkGraphics.h" #include "third_party/skia/include/core/SkStream.h" +#include "base/log/ace_trace.h" #include "base/thread/background_task_executor.h" #include "core/common/container.h" #include "core/common/container_scope.h" #include "core/event/ace_event_helper.h" #include "core/image/flutter_image_cache.h" #include "core/image/image_object.h" +#include "image_compressor.h" namespace OHOS::Ace { namespace { -constexpr double RESIZE_MAX_PROPORTION = 0.5 * 0.5; // Cache image when resize exceeds 25% // If a picture is a wide color gamut picture, its area value will be larger than this threshold. constexpr double SRGB_GAMUT_AREA = 0.104149; @@ -244,8 +245,9 @@ RefPtr ImageProvider::GeneratorAceImageObject( } sk_sp ImageProvider::LoadImageRawData( - const ImageSourceInfo& imageInfo, const RefPtr context, const Size& targetSize) + const ImageSourceInfo& imageInfo, const RefPtr context) { + ACE_FUNCTION_TRACE(); auto imageCache = context->GetImageCache(); if (imageCache) { // 1. try get data from cache. @@ -254,22 +256,8 @@ sk_sp ImageProvider::LoadImageRawData( LOGD("sk data from memory cache."); return AceType::DynamicCast(cacheData)->imageData; } - // 2 try get data from file cache. - if (targetSize.IsValid()) { - LOGD("size valid try load from cache."); - std::string cacheFilePath = - ImageCache::GetImageCacheFilePath(ImageObject::GenerateCacheKey(imageInfo, targetSize)); - LOGD("cache file path: %{private}s", cacheFilePath.c_str()); - auto data = imageCache->GetDataFromCacheFile(cacheFilePath); - if (data) { - LOGD("cache file found : %{public}s", cacheFilePath.c_str()); - return AceType::DynamicCast(data)->imageData; - } - } else { - LOGD("target size is not valid, load raw image file."); - } } - // 3. try load raw image file. + // 2. try load raw image file. auto imageLoader = ImageLoader::CreateImageLoader(imageInfo); if (!imageLoader) { LOGE("imageLoader create failed. imageInfo: %{private}s", imageInfo.ToString().c_str()); @@ -283,6 +271,23 @@ sk_sp ImageProvider::LoadImageRawData( return data; } +sk_sp ImageProvider::LoadImageRawDataFromFileCache( + const RefPtr context, + const std::string key, + const std::string suffix) +{ + ACE_FUNCTION_TRACE(); + auto imageCache = context->GetImageCache(); + if (imageCache) { + std::string cacheFilePath = ImageCache::GetImageCacheFilePath(key) + suffix; + auto data = imageCache->GetDataFromCacheFile(cacheFilePath); + if (data) { + return AceType::DynamicCast(data)->imageData; + } + } + return nullptr; +} + #ifndef NG_BUILD void ImageProvider::GetSVGImageDOMAsyncFromSrc(const std::string& src, std::function&)> successCallback, std::function failedCallback, @@ -364,8 +369,10 @@ void ImageProvider::GetSVGImageDOMAsyncFromData(const sk_sp& skData, #endif void ImageProvider::UploadImageToGPUForRender(const sk_sp& image, - const std::function)>&& callback, - const RefPtr& renderTaskHolder) + const sk_sp& data, + const std::function, sk_sp)>&& callback, + const RefPtr& renderTaskHolder, + const std::string src) { if (!renderTaskHolder) { LOGW("renderTaskHolder has been released."); @@ -373,9 +380,14 @@ void ImageProvider::UploadImageToGPUForRender(const sk_sp& image, } #ifdef UPLOAD_GPU_DISABLED // If want to dump draw command or gpu disabled, should use CPU image. - callback({ image, renderTaskHolder->unrefQueue }); + callback({ image, renderTaskHolder->unrefQueue }, nullptr); #else - auto task = [image, callback, renderTaskHolder] () { + if (data && ImageCompressor::GetInstance()->CanCompress()) { + LOGI("use astc cache %{public}s %{public}d * %{public}d", src.c_str(), image->width(), image->height()); + callback({ image, renderTaskHolder->unrefQueue }, data); + return; + } + auto task = [image, callback, renderTaskHolder, src] () { if (!renderTaskHolder) { LOGW("renderTaskHolder has been released."); return; @@ -383,37 +395,52 @@ void ImageProvider::UploadImageToGPUForRender(const sk_sp& image, // weak reference of io manager must be check and used on io thread, because io manager is created on io thread. if (!renderTaskHolder->ioManager) { // Shell is closing. - callback({ image, renderTaskHolder->unrefQueue }); + callback({ image, renderTaskHolder->unrefQueue }, nullptr); return; } ACE_DCHECK(!image->isTextureBacked()); - auto resContext = renderTaskHolder->ioManager->GetResourceContext(); - if (!resContext) { - callback({ image, renderTaskHolder->unrefQueue }); + bool needRaster = ImageCompressor::GetInstance()->CanCompress() + || renderTaskHolder->ioManager->GetResourceContext(); + if (!needRaster) { + callback({ image, renderTaskHolder->unrefQueue }, nullptr); return; - } - auto rasterizedImage = image->makeRasterImage(); - if (!rasterizedImage) { - LOGW("Rasterize image failed. callback."); - callback({ image, renderTaskHolder->unrefQueue }); - return; - } - SkPixmap pixmap; - if (!rasterizedImage->peekPixels(&pixmap)) { - LOGW("Could not peek pixels of image for texture upload."); - callback({ rasterizedImage, renderTaskHolder->unrefQueue }); - return; - } - auto textureImage = + } else { + auto rasterizedImage = image->isLazyGenerated() ? image->makeRasterImage() : image; + if (!rasterizedImage) { + LOGW("Rasterize image failed. callback."); + callback({ image, renderTaskHolder->unrefQueue }, nullptr); + return; + } + SkPixmap pixmap; + if (!rasterizedImage->peekPixels(&pixmap)) { + LOGW("Could not peek pixels of image for texture upload."); + callback({ rasterizedImage, renderTaskHolder->unrefQueue }, nullptr); + return; + } + int32_t width = static_cast(pixmap.width()); + int32_t height = static_cast(pixmap.height()); + sk_sp compressData; + if (ImageCompressor::GetInstance()->CanCompress()) { + compressData = ImageCompressor::GetInstance()->GpuCompress(src, pixmap, width, height); + ImageCompressor::GetInstance()->WriteToFile(src, compressData, {width, height}); + renderTaskHolder->ioTaskRunner->PostDelayedTask(ImageCompressor::GetInstance()->ScheduleReleaseTask(), + fml::TimeDelta::FromMilliseconds(ImageCompressor::releaseTimeMs)); + } + auto resContext = renderTaskHolder->ioManager->GetResourceContext(); + if (!resContext) { + callback({ image, renderTaskHolder->unrefQueue }, compressData); + } else { + auto textureImage = #ifdef NG_BUILD - SkImage::MakeCrossContextFromPixmap(resContext.get(), pixmap, true, true); + SkImage::MakeCrossContextFromPixmap(resContext.get(), pixmap, true, true); #else - SkImage::MakeCrossContextFromPixmap(resContext.get(), pixmap, true, pixmap.colorSpace(), true); + SkImage::MakeCrossContextFromPixmap(resContext.get(), pixmap, true, pixmap.colorSpace(), true); #endif - callback({ textureImage ? textureImage : rasterizedImage, renderTaskHolder->unrefQueue }); - - // Trigger purge cpu bitmap resource, after image upload to gpu. - SkGraphics::PurgeResourceCache(); + callback({ textureImage ? textureImage : rasterizedImage, renderTaskHolder->unrefQueue }, compressData); + } + // Trigger purge cpu bitmap resource, after image upload to gpu. + SkGraphics::PurgeResourceCache(); + } }; renderTaskHolder->ioTaskRunner->PostTask(std::move(task)); #endif @@ -454,6 +481,7 @@ sk_sp ImageProvider::ResizeSkImage( sk_sp ImageProvider::ApplySizeToSkImage( const sk_sp& rawImage, int32_t dstWidth, int32_t dstHeight, const std::string& srcKey) { + ACE_FUNCTION_TRACE(); auto scaledImageInfo = SkImageInfo::Make(dstWidth, dstHeight, rawImage->colorType(), rawImage->alphaType(), rawImage->refColorSpace()); SkBitmap scaledBitmap; @@ -478,6 +506,7 @@ sk_sp ImageProvider::ApplySizeToSkImage( scaledBitmap.setImmutable(); auto scaledImage = SkImage::MakeFromBitmap(scaledBitmap); if (scaledImage) { + const double RESIZE_MAX_PROPORTION = ImageCompressor::GetInstance()->CanCompress() ? 1.0 : 0.25; bool needCacheResizedImageFile = (1.0 * dstWidth * dstHeight) / (rawImage->width() * rawImage->height()) < RESIZE_MAX_PROPORTION; if (needCacheResizedImageFile && !srcKey.empty()) { diff --git a/frameworks/core/image/image_provider.h b/frameworks/core/image/image_provider.h index 93c3fdaacab..84f872e2fc8 100644 --- a/frameworks/core/image/image_provider.h +++ b/frameworks/core/image/image_provider.h @@ -111,8 +111,10 @@ public: // upload image data to gpu context for painting asynchronously. static void UploadImageToGPUForRender( const sk_sp& image, - const std::function)>&& callback, - const RefPtr& renderTaskHolder); + const sk_sp& data, + const std::function, sk_sp)>&& callback, + const RefPtr& renderTaskHolder, + const std::string src); // get out source image data asynchronously. static void FetchImageObject( @@ -156,8 +158,12 @@ public: static sk_sp LoadImageRawData( const ImageSourceInfo& imageInfo, + const RefPtr context); + + static sk_sp LoadImageRawDataFromFileCache( const RefPtr context, - const Size& targetSize = Size()); + const std::string key, + const std::string suffix = ""); static RefPtr QueryImageObjectFromCache( const ImageSourceInfo& imageInfo, const RefPtr& pipelineContext); diff --git a/test/fuzztest/imageloader_fuzzer/BUILD.gn b/test/fuzztest/imageloader_fuzzer/BUILD.gn index c17c33afb3f..be0a6e63217 100644 --- a/test/fuzztest/imageloader_fuzzer/BUILD.gn +++ b/test/fuzztest/imageloader_fuzzer/BUILD.gn @@ -55,6 +55,7 @@ ohos_fuzztest("ImageLoaderFuzzTest") { "$ace_root/frameworks/base:ace_base_ohos", "$ace_root/frameworks/base/resource:ace_resource", "$ace_root/frameworks/core/components/theme:build_theme_code", + "//third_party/opencl-headers:libcl", ] external_deps = [ "c_utils:utils" ] -- Gitee