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...

作者:京东零售 何贤挺

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

相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing6 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试