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 的常用后端包括:
实践
第一步:获取源码
不废话,直接上终端,这里默认大家了解 GN 和 Ninja 编译,不熟悉可以先看看:http://xingyun.jd.com/shendeng/article/detail/3477
git clone https://skia.googlesource.com/skia.git
// 拉取 skia 所需依赖
cd skia
python3 tools/git-sync-deps
bin/fetch-ninja
坑 1:如拉取 Skia 依赖库失败,可自行设置FQ或将公司网络 DNS 设为 8.8.8.8
第二步:编译集成
使用 Skia 的方式有两种。
动态库方式
编译出动态库(libskia.so),命令如下:
# 生成配置
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 配置中补上:
# 宿主需要依赖 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,命令如下:
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 画图:
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 后:
// 将 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 * 4;
// 实际像素对象
SkBitmap bitmap;
// 生成一份位图描述属性
SkImageInfo image_info , buffer, SkAlphaType::kPremul_SkAlphaType);
bitmap.setInfo(image_info, bpr);
bitmap.setPixels(buffer);
// 构造一个 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 后端绘制,编译配置调整:
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 中,需补充:
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 绘制改为如下:
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 更贴近驱动编程,事无巨细的将决策交给开发者,带来的就是繁重的配置及调教。
提供关键初始配置如下:
// 初始 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);
初始过程,核心流程可以总结为:
一顿操作跑起来,内心暗呼自己牛逼,好景不长,实测下来,又踩坑了。
坑 4:Vulkan 性能不及预期,一直怀疑是代码写的有问题,不断啃 Vulkan 官方文档,结果还是一样。既然无法证明我的代码是否有问题,那就去证明 Vulkan 有问题。
以同一台手机测试 Vulkan 和 OpenGL ES 在各个方面的性能数据,如下:
在多台手机验证后,OpenGL 在电量、温度和帧率都略优于 Vulkan,此刻的我释然了。
第五步:转战 OpenGL ES
OpenGL 比较人性化,开箱即用,就像一辆跑车,已经将骨架都搭好,我们只需往里面换个轮子,换个色漆,换个尾灯,随便配置,都可以是法拉利。Vulkan 更像是连骨架都没给你搭,只是给你张图纸,你若牛逼,造辆兰博基尼,反之,可能造了一台野马。
Skia 来开启 OpenGL 后端绘制,编译配置调整:
skia_use_vulkan = false
skia_gl_standard = "gles"
宿主配置 CMakeLists.txt 中,需补充:
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 绘制改为如下:
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,简单太多,配置如下:
// 建立与 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);
初始的核心流程,总结如下:
编写完后,很轻松跑起来,再不用关心 Vulkan 里的各种概念。
结语
实际项目比这个复杂,但思路差不多,大家感兴趣,可以上手试试。最后记录下过程的心得:
参考资料
https://geek-docs.com/vulkan/vulkan-tutorial/vulkan-tutorial-index.html
https://skia.org/docs/user/build/#android
作者:京东零售 何贤挺
来源:京东云开发者社区 转载请注明来源
Skia 编译及踩坑实践的更多相关文章
- 【react native】rn踩坑实践——从输入框“们”开始
因为团队技术栈变更为react native,所以开始写起了rn的代码,虽然rn与react份数同源,但是由于有很多native有关的交互和变动,实际使用还是碰到蛮多问题的,于是便有了这个系列,本来第 ...
- Kubernetes-Service介绍(三)-Ingress(含最新版安装踩坑实践)
前言 本篇是Kubernetes第十篇,大家一定要把环境搭建起来,看是解决不了问题的,必须实战. Kubernetes系列文章: Kubernetes介绍 Kubernetes环境搭建 Kuberne ...
- dubbo 2.7应用级服务发现踩坑小记
本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star. 背景 本文记录最近一位读者反馈的dubbo 2.7.x中应用级服务发现的问题,关于dubbo应 ...
- 【踩坑速记】二次依赖?android studio编译运行各种踩坑解决方案,杜绝弯路,总有你想要的~
这篇博客,只是把自己在开发中经常遇到的打包编译问题以及解决方案给大家稍微分享一下,不求吸睛,但求有用. 1.大家都知道我们常常会遇到dex超出方法数的问题,所以很多人都会采用android.suppo ...
- 踩坑 —— Eclipse MAVEN编译
一.踩坑 1.昨天download了Netty和SOFARPC工程的源码,Eclipse编译的时候报错了,信息如下: Plugin execution not covered by lifecycle ...
- TiDB 深度实践之旅--真实“踩坑”经历
美团点评 TiDB 深度实践之旅(9000 字长文 / 真实“踩坑”经历) 4 PingCAP · 154 天前 · 3956 次点击 这是一个创建于 154 天前的主题,其中的信息可能已经有所发 ...
- Ubuntu16.04编译安装tensorflow,2018最新血泪踩坑之后的全面总结!绝对成功!【转】
本文转载自:https://blog.csdn.net/pzh11001/article/details/79683133 大家好,我是 (深度学习硬件DIY总群)(719577294)群主: ...
- QT踩坑记录1-Q_OBJECT编译问题
QT踩坑记录1-Q_OBJECT编译问题 QTC++Bugs 错误输出 Q_OBJECT 宏错误的地方会编译出现这样的错误, 无法找到.... 由于自己不想再看到这个错误, 此处 复制自 参考连接1, ...
- openssl1.0在mac下的编译安装(踩坑精华)
之前做了一次brew版本升级,然后用pip3安装的一个python命令就无法执行了(涉及到openssl库),执行就会报一个错误. ImportError: dlopen(/usr/local/Cel ...
- DevOps落地实践点滴和踩坑记录-(2) -聊聊平台建设
很久没有写文章记录了,上一篇文章像流水账一样,把所见所闻一个个记录下来.这次专门聊聊DevOps平台的建设吧,有些新的体会和思考,希望给正在做这个事情的同学们一些启发吧. DevOps落地实践点滴和踩 ...
随机推荐
- KAFKA EAGLE 监控MRS kafka之操作实践
本文分享自华为云社区<KAFKA EAGLE 监控MRS kafka之操作实践>,作者: 啊喔YeYe . 1.Kafka Eagle简介 Kafka eagle 是一款分布式.高可用的k ...
- 鲲鹏基础软件开发赛道openLooKeng赛题火热报名中,数十万大奖等您来收割
随着云计算.物联网.移动计算.智慧城市.人工智能等领域的发展,各类应用对大数据处理的需求也发生着变化.以实时分析.离线分析.交互式分析等为代表的计算引擎逐渐为各大企业行业发展所看重.作为鲲鹏产业生态的 ...
- [BitSail] Connector开发详解系列三:SourceReader
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 Source Connector 本文将主要介绍负责数据读取的组件SourceReader: SourceRead ...
- 【辅助工具】Postman使用
Postman使用 批量处理 https://www.bbsmax.com/A/A7zglyjoJ4/ pm.test("测试结果成功", function () { pm ...
- POJ 1742 Coins(多重背包的可行性问题)
Description People in Silverland use coins.They have coins of value A1,A2,A3...An Silverland dollar. ...
- 即学即会 Serverless | 如何解决 Serverless 应用开发部署的难题?
本文节选自<Serverless 开发速查手册>,关注Serverless 公众回复 手册 即可获得下载链接 作者 | 江昱(阿里云 Serverless 产品经理) 破局:工具链体系匮乏 ...
- Nacos注册中心搭建
1.Nacos服务端搭建(需要有java环境),下载地址:https://github.com/alibaba/Nacos/releases 下载对应操作系统的包解压. 1.1.解压:tar -zxv ...
- vue3常用 Composition API
1.拉开序幕的setup 理解:Vue3.0中一个新的配置项,值为一个函数. setup是所有Composition API(组合API)" 表演的舞台 ". 组件中所用到的:数据 ...
- C++跨DLL内存所有权问题探幽(三)导致堆问题的可能性
0xC0000374: 堆已损坏. (参数: 0x00007FFA1E9787F0). _Mem 是 nullptr 这里提供一个可能性,不一定是内存所属地址冲突的问题,除了MT和 MD编译,还有可能 ...
- Redis 集群模式搭建
本文为博主原创,未经允许不得转载: 目录: 1. 哨兵模式与集群模式对比 2. Redis 集群架构搭建 3. 集群原理分析 4. 集群元数据维护方式对比 5. redis 分布式寻址 6. 集群选举 ...