Skia 编译及踩坑实践

本文要点

•了解并入门 Skia、OpenGL 和 Vulkan

•了解 Skia 在后端渲染上的坑点

前言

Skia 是什么

Skia 是一个开源 2D 图形库,提供可跨各种硬件和软件平台工作的通用 API。 它充当 Google Chrome 和 ChromeOS、Android、Flutter 和许多其他产品的图形引擎。也是国内大厂自渲染首选图形库。

OpenGL 是什么

OpenGL 是一种跨平台的图形 API,用于为 3D 图形处理硬件指定标准的软件接口。OpenGL ES 是 OpenGL 规范的一种形式,适用于嵌入式设备。

Android 支持多版 OpenGL ES 情况:

| Android 版本 | Vulkan 版本 | | Android 7.0 | OpenGL ES 3.2 | | Android 5.0 | OpenGL ES 3.1 | | Android 4.3 | OpenGL ES 3.0 | | Android 2.2 | OpenGL ES 2.0 | | Android 1.0 | OpenGL ES 1.0 和 OpenGL ES 1.1 |

Vulkan 是什么

Vulkan 是一个跨平台的 2D 和 3D 图形 API ,用于高性能 3D 图形的低开销、跨平台 API。

Android 支持 Vulkan 情况:

| Android 版本 | Vulkan 版本 | | Android 13 | Vulkan 1.3 | | Android 9 | Vulkan 1.1 | | Android 7 | Vulkan 1.0 |

三者关系

在 Skia 图形库中,分为前端和后端,前端通常指的是图形库提供的接口和功能,用于创建和操作图形对象、设定图形属性、以及定义图形场景;后端指的是图形库的渲染引擎,负责将前端定义的图形场景渲染到屏幕上,后端通常涉及图形硬件的交互。Skia 的常用后端包括:

1.Skia 自身: Skia 提供了一套最基本的后端,用于在屏幕上呈现图形。它主要通过像素操作实现图形渲染。这个后端在桌面应用程序和一些移动应用程序中被广泛使用。

2.OpenGL: Skia 支持使用 OpenGL 作为渲染后端。这对于需要更高性能和复杂图形效果的应用程序是很有用的,特别是在游戏和图形密集型应用中。

3.Vulkan: Skia 也可以使用 Vulkan 作为后端。Vulkan 提供更直接的硬件访问,使得在支持 Vulkan 的设备上实现更高效的图形渲染。

4.Metal: 在 macOS 和 iOS 平台上,Skia 可以使用 Metal 作为图形后端。Metal 是苹果公司推出的图形和计算API,用于替代 OpenGL。

5.PDF: Skia 还支持将图形渲染为 PDF 文档。这对于需要生成可打印文档或在应用程序中导出图形的场景很有用。

实践

第一步:获取源码

不废话,直接上终端,这里默认大家了解 GN 和 Ninja 编译,不熟悉可以先看看:xingyun.jd.com/shendeng/ar...

bash 复制代码
git clone https://skia.googlesource.com/skia.git
// 拉取 skia 所需依赖
cd skia
python3 tools/git-sync-deps
bin/fetch-ninja

坑 1:如拉取 Skia 依赖库失败,可自行设置翻墙或将公司网络 DNS 设为 8.8.8.8

第二步:编译集成

使用 Skia 的方式有两种。

动态库方式

编译出动态库(libskia.so),命令如下:

bash 复制代码
# 生成配置
bin/gn gen out/arm64 --args='ndk="/Users/hexianting/Library/Android/sdk/ndk/23.1.7779620" target_cpu="arm64" target_os = "android" ndk_api=24'
# 开始编译
ninja -C out/arm64

将动态库(out/arm64目录下)和 Skia 的 include 目录(对外头文件)复制到宿主工程,并在宿主的 CMakeLists 配置中补上:

bash 复制代码
# 宿主需要依赖 skia 的头文件
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ./skia/include)
# 申请 skia 动态库
add_library( skia
        SHARED
        IMPORTED )
set_target_properties( skia
        PROPERTIES IMPORTED_LOCATION
        ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libskia.so )
# 依赖 skia
target_link_libraries(${CMAKE_PROJECT_NAME}
        skia)

源码方式

源码集成会严重拖慢编译速度,但对于定制 skia 和断点调试比较方便。

由于官方源码采用 GN 配置来构建,一般宿主都是用 cmake,所以需要将 GN 转 cmake,命令如下:

rust 复制代码
bin/gn gen cmake --args='ndk="/Users/hexianting/Library/Android/sdk/ndk/23.1.7779620" target_cpu = "arm64" target_os = "android" ndk_api = 24' --ide=json --json-ide-script=../../gn/gn_to_cmake.py

会在 cmake 目录下,生成关键两个文件,CMakeLists.txt 和 CMakeLists.ext,前者只是壳,后者是 skia 各个模块真实配置。

同样,也需要在宿主CMakeLists 配置中补上依赖关系,跟动态库方式一样。

第三步:用 CPU 画出一个三角形

先看效果图:

在 Android 宿主工程搭建一个普通工程,创建一个 SurfaceView ,用于 skia 画图:

java 复制代码
public class SkiaSurfaceView extends SurfaceView {
    private static final String TAG = "SkiaSurfaceView";
    private Surface renderSurface;

    private final SurfaceHolder.Callback surfaceCallback =
            new SurfaceHolder.Callback() {
                @Override
                public void surfaceCreated(@NonNull SurfaceHolder holder) {
                    renderSurface = holder.getSurface();
                    // 获取 Surface 后,传给 C++ 的 skia 使用
                    nativeSurfaceCreated(renderSurface);
                }

                @Override
                public void surfaceChanged(
                        @NonNull SurfaceHolder holder, int format, int width, int height) {
                    nativeSurfaceChanged();
                }

                @Override
                public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
                    nativeSurfaceDestroyed();
                }
            };
    public SkiaSurfaceView(@NonNull Context context) {
        super(context);
        init();
    }
    public SkiaSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        // 系统 SurfaceView 闪现黑屏 bug
        getHolder().setFormat(PixelFormat.TRANSPARENT);
        setZOrderOnTop(true);
        getHolder().addCallback(surfaceCallback);
    }
    private native void nativeSurfaceCreated(Surface surface);
    private native void nativeSurfaceChanged();
    private native void nativeSurfaceDestroyed();
}

C++ 获取到 Surface 后:

scss 复制代码
    // 将 Surface 对象转换为 ANativeWindow 对象
    auto nativeWindow = ANativeWindow_fromSurface(env, surface);
    // 设置 ANativeWindow 宽度、高度和像素格式
    ANativeWindow_setBuffersGeometry(
            nativeWindow, 400, 400, WINDOW_FORMAT_RGBA_8888);
    ANativeWindow_Buffer *buffer = new ANativeWindow_Buffer();
    // 锁定 ANativeWindow 的缓冲区,准备开始修改缓冲区的像素数据
    ANativeWindow_lock(nativeWindow, buffer, 0);
    int bpr = buffer->stride * 4;
    // 实际像素对象
    SkBitmap bitmap;
    // 生成一份位图描述属性
    SkImageInfo image_info = SkImageInfo::MakeS32(
            buffer->width, buffer->height, SkAlphaType::kPremul_SkAlphaType);
    bitmap.setInfo(image_info, bpr);
    bitmap.setPixels(buffer->bits);
    // 构造一个 canvas 对象,将 canvas 画布和 bitmap 关联上
    SkCanvas surfaceCanvas{bitmap};
    // 创建一个红色的画刷
    SkPaint paint;
    paint.setColor(SK_ColorRED);
    paint.setStyle(SkPaint::kFill_Style);
    // 创建一个绘制路径
    SkPath path;
    path.moveTo(100.0f, 0.0f);
    path.lineTo(0.0f, 100);
    path.lineTo(200, 100);
    path.close();
    // 使用画刷绘制路径
    surfaceCanvas.drawPath(path, paint);
    // 解锁并提交缓冲
    ANativeWindow_unlockAndPost(nativeWindow);

以上就可以将一个三角形画出在自己的 Surface 上。完成上述操作后,一切都很美好,直到使用到了更多 skia 特性,出现了本不应该出现的问题。

坑 2:在宿主链接 libskia.so 阶段,出现各种 ld: error: undefined symbol: SkCanvas::drawXXX,在宿主运行阶段,使用 SkData、SkImage、 SkFont 等,出现各种指针异常导致的闪退,不要怀疑自己,果断换分支,Main 是开发分支,不是稳定分支,就算是稳定分支,也不代表真的稳定!!!经过无数次的验证,最终我们选取 flutter-3.2-candidate.4 分支作为我们的基础版本。吐槽下 Skia 团队,同样是谷歌,Chromium 和 Flutter 的 main 分支就很稳定。

第四步: 改用 GPU 画(Vulkan)

Android 早期只有软件绘制,从 Android 3.0 开始系统支持硬件加速, Android 系统一直在追求高性能的硬件加速。从最终实测效果上看,GPU 绘制对绘制提升不小,具体数据跟业务有关,较敏感,暂不贴出。

下面将借助 Skia 来开启 Vulkan 后端绘制,编译配置调整:

ini 复制代码
skia_use_vulkan = true
# 一定要大于 24,Android 7.0 才支持 vulkan
ndk_api = 24

坑 3:build.gradle 中 externalNativeBuild 配置参数 -DANDROID_PLATFORM,会影响 CMake 中 C++ 库查找,不同 Android 系统内置的 so 存在增删,比如 Vulkan 库位置在 toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/24 目录下,低于 24 会找不到 Vulkan ,因为 Vulkan 是 Android 7.0 开始支持。

宿主配置 CMakeLists.txt 中,需补充:

scss 复制代码
set_target_properties(silkjni PROPERTIES COMPILE_DEFINITIONS "NDEBUG;SKIA_DLL;SK_ENABLE_SKSL;SK_ENABLE_PRECOMPILE;SK_ASSUME_GL_ES=1;SK_USE_PERFETTO;SK_GAMMA_APPLY_TO_A8;SK_GAMMA_EXPONENT=1.4;SK_GAMMA_CONTRAST=0.0;SK_USE_VMA;SKIA_IMPLEMENTATION=1;SK_GL;SK_VULKAN;SK_ENABLE_DUMP_GPU;SK_SUPPORT_GPU=1;VK_USE_PLATFORM_ANDROID_KHR;")

将上述第三步中 CPU 绘制改为如下:

scss 复制代码
 if (!vulkanInited) {
    // 初始化 Vulkan 上下文
    if (!initVulkanContext()) {
      return;
    }
  }
  // 使用 Skia SkSurface 中的 SkCanvas
  SkCanvas *canvas = skSurface->getCanvas();
  // 创建一个红色的画刷
  SkPaint paint;
  paint.setColor(SK_ColorRED);
  paint.setStyle(SkPaint::kFill_Style);
  // 创建一个绘制路径
  SkPath path;
  path.moveTo(100.0f, 0.0f);
  path.lineTo(0.0f, 100);
  path.lineTo(200, 100);
  path.close();
  // 使用画刷绘制路径
  canvas.drawPath(path, paint);
  // 转换指令及提交到 GPU
  skSurface->flushAndSubmit();
  // 保存 Vulkan 流水线数据
  if (!cacheInited) {
    grDirectContext->storeVkPipelineCacheData();
    cacheInited = true;
  }

当然少不了对 Vulkan 的初始,代码量多的令人发指,在 Skia 上没有做到开箱即用,其中任何配置出错,都可能绘制失败,这其实跟 Vulkan 设计理念有关,为了高性能,Vulkan 更贴近驱动编程,事无巨细的将决策交给开发者,带来的就是繁重的配置及调教。

提供关键初始配置如下:

ini 复制代码
 // 初始 Skia 封装的后端渲染 vulkan 上下文
  if (!grVkBackendContext) {
    grVkBackendContext = new GrVkBackendContext();
  }
  // 创建 vulkan 应用信息
  VkApplicationInfo appCreateInfo = {
      .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
      .pNext = nullptr,
      .pApplicationName = "silk_vulkan",
      .applicationVersion = VK_MAKE_VERSION(1, 0, 0),
      .pEngineName = "silk_vulkan_en",
      .engineVersion = VK_MAKE_VERSION(1, 0, 0),
      .apiVersion = VK_MAKE_VERSION(1, 0, 0),
  };
  // 创建 Vulkan 实例
  uint32_t instanceExtCount = 2;
  uint32_t deviceExtCount = 1;
  const char *instanceExt[instanceExtCount];
  const char *deviceExt[deviceExtCount];
  // 扩展实例支持 android surface,以下都为必选参数
  instanceExt[0] = "VK_KHR_surface";
  instanceExt[1] = "VK_KHR_android_surface";
  // 逻辑设备要支持交换链
  deviceExt[0] = "VK_KHR_swapchain";
  // 调用 Vulkan 函数创建
  VkInstanceCreateInfo instanceCreateInfo{
      .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
      .pNext = nullptr,
      .pApplicationInfo = &appCreateInfo,
      .enabledLayerCount = 0,
      .ppEnabledLayerNames = nullptr,
      .enabledExtensionCount = instanceExtCount,
      .ppEnabledExtensionNames = instanceExt,
  };
  VkResult result = vkCreateInstance(
      &instanceCreateInfo, nullptr, &grVkBackendContext->fInstance);
  if (result != VK_SUCCESS) {
    return false;
  }
  // 获取支持的物理设备列表,同一函数调两次,vulkan 用法套路,先取数量再取实际值
  uint32_t gpuCount = 0;
  result = vkEnumeratePhysicalDevices(
      grVkBackendContext->fInstance, &gpuCount, nullptr);
  if (result != VK_SUCCESS) {
    return false;
  }
  VkPhysicalDevice vkGpus[gpuCount];
  result = vkEnumeratePhysicalDevices(
      grVkBackendContext->fInstance, &gpuCount, vkGpus);
  if (result != VK_SUCCESS) {
    return false;
  }
  // 取本机第一个 GPU 物理设备
  grVkBackendContext->fPhysicalDevice = vkGpus[0];
  VkPhysicalDeviceProperties gpuProperties;
  vkGetPhysicalDeviceProperties(
      grVkBackendContext->fPhysicalDevice, &gpuProperties);
  // 获取物理设备支持的队列族类型,比如用于图形的,用于计算的
  uint32_t queueFamilyCount;
  vkGetPhysicalDeviceQueueFamilyProperties(
      grVkBackendContext->fPhysicalDevice, &queueFamilyCount, nullptr);
  if (queueFamilyCount <= 0) {
    return false;
  }
  VkQueueFamilyProperties queueFamilyProperties[queueFamilyCount];
  vkGetPhysicalDeviceQueueFamilyProperties(
      grVkBackendContext->fPhysicalDevice,
      &queueFamilyCount,
      queueFamilyProperties);
  // 我们只关心图形队列族,只需找到图形队列族用于绘制
  uint32_t queueFamilyIndex;
  for (queueFamilyIndex = 0; queueFamilyIndex < queueFamilyCount;
       queueFamilyIndex++) {
    if (queueFamilyProperties[queueFamilyIndex].queueFlags &
        VK_QUEUE_GRAPHICS_BIT) {
      break;
    }
  }
  if (queueFamilyIndex >= queueFamilyCount) {
    return false;
  }
  grVkBackendContext->fGraphicsQueueIndex = queueFamilyIndex;
  // 队列优先级 0-1 ,高优先级
  float priorities[] = {
      1.0f,
  };
  VkDeviceQueueCreateInfo queueCreateInfo{
      .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
      .pNext = nullptr,
      .flags = 0,
      .queueFamilyIndex = queueFamilyIndex,
      .queueCount = 1,
      .pQueuePriorities = priorities,
  };
  VkDeviceCreateInfo deviceCreateInfo{
      .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
      .pNext = nullptr,
      .queueCreateInfoCount = 1,
      .pQueueCreateInfos = &queueCreateInfo,
      .enabledLayerCount = 0,
      .ppEnabledLayerNames = nullptr,
      .enabledExtensionCount = deviceExtCount,
      .ppEnabledExtensionNames = deviceExt,
      .pEnabledFeatures = nullptr,
  };
  // 创建与物理设备对应的逻辑设备
  result = vkCreateDevice(
      grVkBackendContext->fPhysicalDevice,
      &deviceCreateInfo,
      nullptr,
      &grVkBackendContext->fDevice);
  if (result != VK_SUCCESS) {
    return false;
  }
  // 初始逻辑设备的队列
  vkGetDeviceQueue(
      grVkBackendContext->fDevice,
      queueFamilyIndex,
      0,
      &grVkBackendContext->fQueue);
  // 加载 Vulkan 函数指针(skia 必备,会通过这个来回调各种 vulkan api)
  // function<void (*(const char *, VkInstance_T *, VkDevice_T *))()>
  // void (*)()
  GrVkGetProc getProc =
      [](const char *name, VkInstance_T *instance, VkDevice_T *device) {
        if (device != VK_NULL_HANDLE) {
          return vkGetDeviceProcAddr(device, name);
        }
        return vkGetInstanceProcAddr(instance, name);
      };
  grVkBackendContext->fGetProc = getProc;

  GrVkExtensions *grVkExtensions = new GrVkExtensions();
  grVkExtensions->init(
      grVkBackendContext->fGetProc,
      grVkBackendContext->fInstance,
      grVkBackendContext->fPhysicalDevice,
      instanceExtCount,
      instanceExt,
      deviceExtCount,
      deviceExt);
  grVkBackendContext->fVkExtensions = grVkExtensions;
  // 启用任务拆分,尽可能的利用多线程优化渲染性能
  GrContextOptions options;
  persistentCacheVulkan = new PersistentCacheVulkan();
  options.fReduceOpsTaskSplitting = GrContextOptions::Enable::kYes;
  options.fDisableCoverageCountingPaths = true;
  options.fDisableDistanceFieldPaths = true;
  options.fMaxCachedVulkanSecondaryCommandBuffers = 100;
  options.fReducedShaderVariations = true;
  options.fPersistentCache = persistentCacheVulkan;
  // 生成 Skia 所需的 gpu 上下文
  grDirectContext = GrDirectContext::MakeVulkan(*grVkBackendContext, options);
  if (!grDirectContext) {
    return false;
  }
  // 创建 vulkan surface 和 android 关联
  VkAndroidSurfaceCreateInfoKHR androidSurfaceCreateInfo{
      .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
      .pNext = nullptr,
      .flags = 0,
      .window = nativeWindow};
  result = vkCreateAndroidSurfaceKHR(
      grVkBackendContext->fInstance,
      &androidSurfaceCreateInfo,
      nullptr,
      &vkSurfaceKHR);
  if (result != VK_SUCCESS) {
    return false;
  }
  // 获取 vulkan 所能支持的 surface 能力及属性
  VkSurfaceCapabilitiesKHR surfaceCapabilities;
  result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(
      grVkBackendContext->fPhysicalDevice, vkSurfaceKHR, &surfaceCapabilities);
  if (result != VK_SUCCESS) {
    return false;
  }
  // 获取 vulkan 所能支持的 format 列表
  uint32_t formatCount = 0;
  vkGetPhysicalDeviceSurfaceFormatsKHR(
      grVkBackendContext->fPhysicalDevice, vkSurfaceKHR, &formatCount, nullptr);
  VkSurfaceFormatKHR formats[formatCount];
  vkGetPhysicalDeviceSurfaceFormatsKHR(
      grVkBackendContext->fPhysicalDevice, vkSurfaceKHR, &formatCount, formats);
  // 找到支持 RGBA 的格式
  uint32_t chosenFormat;
  for (chosenFormat = 0; chosenFormat < formatCount; chosenFormat++) {
    if (formats[chosenFormat].format == VK_FORMAT_R8G8B8A8_UNORM)
      break;
  }
  if (chosenFormat >= formatCount) {
    return false;
  }
  // 需要支持透明从窗口系统继承,而不是自己设置,交换链需要用到该属性
  if (surfaceCapabilities.supportedCompositeAlpha !=
      VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR) {
    return false;
  }
  // 创建交换链(类似Android中为了解决jank问题,引入的三缓冲机制)
  VkSwapchainCreateInfoKHR swapchainCreateInfo{
      .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
      .pNext = nullptr,
      .surface = vkSurfaceKHR,
      .minImageCount = surfaceCapabilities.minImageCount,
      .imageFormat = formats[chosenFormat].format,
      .imageColorSpace = formats[chosenFormat].colorSpace,
      .imageExtent = surfaceCapabilities.currentExtent,
      .imageArrayLayers = 1,
      .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
      .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
      .queueFamilyIndexCount = 1,
      .pQueueFamilyIndices = &queueFamilyIndex,
      .preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,
      .compositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,
      .presentMode = VK_PRESENT_MODE_MAILBOX_KHR,
      .clipped = VK_TRUE,
      .oldSwapchain = VK_NULL_HANDLE,
  };
  result = vkCreateSwapchainKHR(
      grVkBackendContext->fDevice,
      &swapchainCreateInfo,
      nullptr,
      &vkSwapchainKHR);
  if (result != VK_SUCCESS) {
    return false;
  }
  // 获取交换链所有的图像列表
  uint32_t swapchainLength;
  result = vkGetSwapchainImagesKHR(
      grVkBackendContext->fDevice, vkSwapchainKHR, &swapchainLength, nullptr);
  if (result != VK_SUCCESS) {
    return false;
  }
  VkImage displayImages[swapchainLength];
  result = vkGetSwapchainImagesKHR(
      grVkBackendContext->fDevice,
      vkSwapchainKHR,
      &swapchainLength,
      displayImages);
  if (result != VK_SUCCESS) {
    return false;
  }
  // 组装 Skia 的 image 数据(参考了flutter配置)
  GrVkImageInfo grVkImageInfo;
  grVkImageInfo.fImage = displayImages[0];
  grVkImageInfo.fImageTiling = VK_IMAGE_TILING_OPTIMAL;
  grVkImageInfo.fImageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  grVkImageInfo.fFormat = VK_FORMAT_R8G8B8A8_UNORM;
  grVkImageInfo.fImageUsageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
      VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
      VK_IMAGE_USAGE_SAMPLED_BIT;
  grVkImageInfo.fSampleCount = 1;
  grVkImageInfo.fLevelCount = 1;
  // 组装 Skia 的后端渲染 target
  GrBackendRenderTarget grBackendRenderTarget(mWidth, mHeight, grVkImageInfo);
  SkSurfaceProps skSurfaceProps(0, kUnknown_SkPixelGeometry);
  // 生成 Skia 所需的 sksurface
  skSurface = SkSurface::MakeFromBackendRenderTarget(
      grDirectContext.get(),
      grBackendRenderTarget,
      kTopLeft_GrSurfaceOrigin,
      kRGBA_8888_SkColorType,
      SkColorSpace::MakeSRGB(),
      &skSurfaceProps);
  if (!skSurface) {
    return false;
  }
  // 获取交换链中下一次可展示的image索引
  uint32_t nextIndex;
  result = vkAcquireNextImageKHR(
      grVkBackendContext->fDevice,
      vkSwapchainKHR,
      UINT64_MAX,
      VK_NULL_HANDLE,
      VK_NULL_HANDLE,
      &nextIndex);
  if (result != VK_SUCCESS) {
    return false;
  }
  VkPipelineStageFlags waitStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
  VkSubmitInfo submit_info = {
      .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
      .pNext = nullptr,
      .waitSemaphoreCount = 0,
      .pWaitSemaphores = nullptr,
      .pWaitDstStageMask = &waitStageMask,
      .commandBufferCount = 0,
      .pCommandBuffers = nullptr,
      .signalSemaphoreCount = 0,
      .pSignalSemaphores = nullptr};
  result = vkQueueSubmit(grVkBackendContext->fQueue, 0, &submit_info, nullptr);
  if (result != VK_SUCCESS) {
    return false;
  }
  // 将 image 提交并显示
  VkPresentInfoKHR presentInfo{
      .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
      .pNext = nullptr,
      .waitSemaphoreCount = 0,
      .pWaitSemaphores = nullptr,
      .swapchainCount = 1,
      .pSwapchains = &vkSwapchainKHR,
      .pImageIndices = &nextIndex,
      .pResults = &result,
  };
  vkQueuePresentKHR(grVkBackendContext->fQueue, &presentInfo);

初始过程,核心流程可以总结为:

•创建 Vulkan 实例

•获取 Vulkan 物理设备(图形,非计算)

•创建与 Vulkan 物理设备对应的逻辑设备

•初始逻辑设备的队列族及队列

•设置 Vulkan 回调函数指针(供 Skia 使用)

•生成 Vulkan 上下文(供 Skia 使用)

•创建并关联 Vulkan Surface 和 Android Surface

•创建 Vulkan 交换链(类似 Android 中为了解决 jank 问题,引入的三缓冲机制)

•创建后端渲染 target 及 SkSurface(供 Skia 使用)

一顿操作跑起来,内心暗呼自己牛逼,好景不长,实测下来,又踩坑了。

坑 4:Vulkan 性能不及预期,一直怀疑是代码写的有问题,不断啃 Vulkan 官方文档,结果还是一样。既然无法证明我的代码是否有问题,那就去证明 Vulkan 有问题。

以同一台手机测试 Vulkan 和 OpenGL ES 在各个方面的性能数据,如下:

在多台手机验证后,OpenGL 在电量、温度和帧率都略优于 Vulkan,此刻的我释然了。

第五步:转战 OpenGL ES

OpenGL 比较人性化,开箱即用,就像一辆跑车,已经将骨架都搭好,我们只需往里面换个轮子,换个色漆,换个尾灯,随便配置,都可以是法拉利。Vulkan 更像是连骨架都没给你搭,只是给你张图纸,你若牛逼,造辆兰博基尼,反之,可能造了一台野马。

Skia 来开启 OpenGL 后端绘制,编译配置调整:

ini 复制代码
skia_use_vulkan = false
skia_gl_standard = "gles"

宿主配置 CMakeLists.txt 中,需补充:

scss 复制代码
set_target_properties(silkjni PROPERTIES COMPILE_DEFINITIONS "SKIA_DLL;SK_ENABLE_SKSL;SK_ENABLE_PRECOMPILE;SK_ASSUME_GL_ES=1;SK_GAMMA_APPLY_TO_A8;SK_GAMMA_EXPONENT=1.4;SK_GAMMA_CONTRAST=0.0;SKIA_IMPLEMENTATION=1;SK_GL;SK_SUPPORT_GPU=1;")

将上述第三步中 CPU 绘制改为如下:

scss 复制代码
  if (!glInited) {
    if (!initGLContext()) {
      return;
    }
    glInited = true;
  }
  // 使用 Skia SkSurface 中的 SkCanvas
  SkCanvas *canvas = skSurface->getCanvas();
  // 创建一个红色的画刷
  SkPaint paint;
  paint.setColor(SK_ColorRED);
  paint.setStyle(SkPaint::kFill_Style);
  // 创建一个绘制路径
  SkPath path;
  path.moveTo(100.0f, 0.0f);
  path.lineTo(0.0f, 100);
  path.lineTo(200, 100);
  path.close();
  // 使用画刷绘制路径
  canvas.drawPath(path, paint);
  // 刷新并提交绘制结果
  skSurface->flushAndSubmit();
  // 将绘制结果呈现到屏幕
  eglSwapBuffers(eglDisplay, eglSurface);

同样的,OpenGL 也需要一些初始配置,相比 Vulkan,简单太多,配置如下:

ini 复制代码
  // 建立与 OpenGL 图形库连接
  eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
  // 初始 OpenGL 环境,没有版本号需求,置为空
  EGLBoolean result = eglInitialize(eglDisplay, nullptr, nullptr);
  if (!result) {
    // 环境失败
    return false;
  }
  // 判断 OpenGL 是否支持 EGL_WINDOW_BIT 模式
  bool supportWin = false;
  EGLint configsCount = 0;
  eglGetConfigs(eglDisplay, nullptr, 0, &configsCount);
  EGLConfig configsInfo[configsCount];
  eglGetConfigs(eglDisplay, configsInfo, configsCount, &configsCount);
  for (int i = 0; i < configsCount; i++) {
    EGLint value;
    result = eglGetConfigAttrib(
        eglDisplay, configsInfo[i], EGL_SURFACE_TYPE, &value);
    if (result) {
      if (value & EGL_WINDOW_BIT) {
        supportWin = true;
        break;
      }
    }
  }
  if (!supportWin) {
    return false;
  }
  // 描述所需 EGL 属性,要以 EGL_NONE 作为结束标记,顺序是键值对方式
  EGLint configAttribs[] = {
      EGL_RENDERABLE_TYPE,
      // Android 4.3 版本支持 OpenGL3.0
      EGL_OPENGL_ES3_BIT,
      EGL_SURFACE_TYPE,
      // 支持窗口绘制模式
      EGL_WINDOW_BIT,
      EGL_NONE};
  // 存储 EGL 配置
  EGLConfig eglConfig;
  // 实际硬件返回配置大小
  EGLint numConfigs;
  // 获取硬件所能支持的配置信息
  result =
      eglChooseConfig(eglDisplay, configAttribs, &eglConfig, 1, &numConfigs);
  if (!result) {
    // 配置匹配失败
    return false;
  }
  // 创建 EGLContext,调试信息可在以下配置开启,具体调试可问 ChatGPT
  EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
  eglContext =
      eglCreateContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, contextAttribs);
  // 创建 EGLSurface
  eglSurface =
      eglCreateWindowSurface(eglDisplay, eglConfig, nativeWindow, nullptr);
  // 绑定 OpenGL Surface 和 Android Surface
  result = eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
  if (!result) {
    // 绑定失败
    return false;
  }
  // 以下开始构建 Skia 环境
  GrGLFramebufferInfo framebufferInfo;
  framebufferInfo.fFBOID = 0;
  framebufferInfo.fFormat = 0x8058; // 指向 GR_GL_RGBA8
  sk_sp<GrDirectContext> context = GrDirectContext::MakeGL();
  // 构建 Skia 的后端渲染
  GrBackendRenderTarget backendRenderTarget(
      mWidth, mHeight, 1, 8, framebufferInfo);
  sk_sp<SkColorSpace> skColorSpace;
  //  创建 SkSurface
  skSurface = SkSurface::MakeFromBackendRenderTarget(
      context.get(),
      backendRenderTarget,
      kBottomLeft_GrSurfaceOrigin,
      kRGBA_8888_SkColorType,
      skColorSpace,
      nullptr);
  // 设置视口和清除颜色
  glViewport(0, 0, mWidth, mHeight);
  // 白色
  glClearColor(1.0f, 1.0f, 1.0f, 1.0f);

初始的核心流程,总结如下:

•初始默认显示

•配置所需的版本及窗口绘制模式

•初始 OpenGL 上下文

•创建并关联 OpenGL Surface 和 Android Surface

•创建后端渲染 target 及 SkSurface(供 Skia 使用)

编写完后,很轻松跑起来,再不用关心 Vulkan 里的各种概念。

结语

实际项目比这个复杂,但思路差不多,大家感兴趣,可以上手试试。最后记录下过程的心得:

•构建的 Skia 运行有问题,不要一直怀疑自己,可能真是 Skia 的问题

•Vulkan 不是开箱即用,没有点图形基础慎用,都是跑车,搞不好你造的是野马,不是兰博基尼

参考资料

geek-docs.com/vulkan/vulk...

skia.org/docs/user/b...

作者:京东零售 何贤挺

来源:京东云开发者社区 转载请注明来源

相关推荐
熊的猫37 分钟前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人2 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人2 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596933 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai3 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书