3.8.8引擎源码解读-鸿蒙原生端启动流程(游戏画面如何显示在原生窗口)

引擎版本:Cocos Creator 3.8.8
目标平台:HarmonyOS Next(纯血鸿蒙)

Cocos游戏在鸿蒙原生端的启动流程包含三个核心阶段:

  1. 创建原生窗口并建立OpenGL ES图形上下文
  2. 初始化JS引擎并绑定必要的Native接口
  3. 加载并执行游戏入口代码

本文将结合Cocos引擎源代码重点分析第一阶段的技术实现细节。

完整的启动全流程见末尾。

一、环境与前提

HarmonyOS Next(纯血鸿蒙系统)不兼容安卓,是鸿蒙生态的新一代操作系统。在Cocos Creator 3.8.8中选择鸿蒙构建目标时,注意以下几项:

  • 渲染后端:当前的鸿蒙6.0系统图形接口支持Vulkan 1.4OpenGL ES 3.2,但当前Cocos Creator只使用OpenGL ES作为渲染后端。
  • JavaScript引擎:可选择JSVM、V8和Ark引擎。但鸿蒙系统和iOS一样出于安全考虑,只对系统内置的 JSVM 引擎开放 JIT(即时编译)权限,第三方引擎(如 V8)虽可运行但没有 JIT ,因此理论上 JSVM 具有最佳性能。
  • 开发工具:鸿蒙官方的开发工具是DevEco Studio,但它的模拟器目前不支持GPU加速等特性,因此Cocos游戏还不能运行在模拟器上,只能真机。

二、XComponent:原生渲染窗口

在鸿蒙原生端,Cocos引擎使用 XComponent 这个 ArkUI 组件来承载游戏画面。根据鸿蒙官方文档,XComponent 有以下特点:

  • XComponent 是 ArkUI 提供的一个自定义渲染组件,专门用于:

    • 通过 EGL/OpenGL ES 进行自定义绘制
    • 写入媒体数据(比如游戏画面渲染、相机预览流等)
  • XComponent 内部持有一个 Surface,并通过 NativeWindow 等接口:

    • 向图形系统申请 Buffer
    • 提交 Buffer 到图形队列
    • 最终把自绘制内容显示到这个 Surface 上

2.1 ArkUI层声明

在Cocos鸿蒙工程的pages/index.ets文件中,通过以下代码创建XComponent:

XComponent({ 
    id: 'xcomponentId', 
    type: 'surface', 
    libraryname: 'cocos' 
})
  • type: 'surface':代表需要独立的绘制表面,自主控制渲染
  • libraryname: 'cocos':指定底层对应的C++动态库

2.2 Native层回调处理

XComponent创建完成后,系统通过NAPI机制调用预先注册的C++回调函数。在OpenHarmonyPlatform.cpp中的关键代码如下:

void onSurfaceCreatedCB(OH_NativeXComponent* component, void* window) {
    cc::ISystemWindowInfo info;
    info.externalHandle = window;
    // 后续处理窗口创建完成逻辑
}

其中的window参数就是鸿蒙系统的原生窗口句柄,在底层对应NativeLayer*结构体。这个句柄是后续EGL创建渲染表面的关键输入。

三、EGL:图形系统桥梁

EGL(Embedded Systems Graphics Library)是 Khronos 制定的一套标准接口,作为连接OpenGL ES与底层窗口系统的桥梁,主要负责:

  • 管理OpenGL ES上下文的创建与销毁
  • 协调渲染内容与操作系统原生窗口的显示同步
  • 管理渲染表面的创建与配置(颜色格式、深度缓冲、模板缓冲、MSAA 等)
  • 在不同 surface/context 之间进行切换(例如多窗口)

简单理解:

OpenGL ES 负责“画画”,EGL 负责“找画布 + 搭画架 + 把画挂到屏幕上”。

3.1 EGL初始化流程

在Cocos引擎源码文件GLES3GPUContext.cpp中,EGL的初始化过程如下:

1. 获取并初始化Display

eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(eglDisplay, &eglMajorVersion, &eglMinorVersion);
eglBindAPI(EGL_OPENGL_ES_API);

这一步完成了:

  • 连接到底层图形系统(获取 EGLDisplay
  • 初始化 EGL
  • 声明我们要使用的是 OpenGL ES API

2. 选择合适的 EGLConfig

配置参数指定了渲染表面的特性要求:

// GLES3GPUContext.cpp
EGLint defaultAttribs[] = {
    EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
    EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR,
    EGL_BLUE_SIZE, 8,
    EGL_GREEN_SIZE, 8,
    EGL_RED_SIZE, 8,
    EGL_ALPHA_SIZE, 8,
    EGL_DEPTH_SIZE, 24,
    EGL_STENCIL_SIZE, 8,
    EGL_SAMPLE_BUFFERS, 0,
    EGL_SAMPLES, 0,
    EGL_NONE
};
eglChooseConfig(eglDisplay, defaultAttribs, ...);

这一步指定了我们希望的像素格式和缓冲区配置,比如:

  • 支持窗口 Surface(EGL_WINDOW_BIT)和离屏 Pbuffer Surface(EGL_PBUFFER_BIT
  • 使用 RGBA8888
  • 深度 24 位、模板 8 位
  • 此处关闭 MSAA(SAMPLE_BUFFERS = 0

EGL 会根据这些条件,从驱动支持的配置里挑出最合适的一项 eglConfig 供后续使用。

3. 创建OpenGL ES上下文

// GLES3GPUContext.cpp
eglDefaultContext = eglCreateContext(eglDisplay, eglConfig, nullptr, eglAttributes);

这一步创建的就是全局的 eglDefaultContext,也就是整个 Cocos 渲染管线所依赖的 OpenGL ES 上下文。只有在某个线程上调用了 eglMakeCurrent,把这个 Context 绑定到某个 Surface 之后,该线程才能执行 GL 绘图指令。

四、双Surface设计原理

Cocos引擎在鸿蒙原生端采用了双Surface架构,这是保证渲染稳定性的重要设计。

1. 创建Pbuffer Surface(离屏表面):上下文保活

在引擎初始化阶段,首先创建1x1像素的Pbuffer Surface:

// GLES3GPUContext.cpp
EGLint pbufferAttribs[] = {
    EGL_WIDTH, 1,
    EGL_HEIGHT, 1,
    EGL_NONE
};
eglDefaultSurface = eglCreatePbufferSurface(eglDisplay, eglConfig, pbufferAttribs);

设计目的:保活。移动端应用生命周期中,原生窗口可能因各种原因(如切后台、分屏)而被销毁。如果OpenGL ES上下文直接绑定到窗口Surface,窗口销毁会导致上下文连带失效,所有的纹理、Shader等GPU资源都需要重新创建,不仅慢,还容易导致黑屏或闪退。有了这个Pbuffer Surface,当原生窗口不可用时,EGL 就把 Context 切换到这个“备胎”上, 只要 Context 还在,游戏资源就在。

2. 创建Window Surface:真正的显示表面

拿到XComponentNativeWindow句柄后,创建用于真正显示用的Window Surface:

// GLES3Swapchain.cpp
_gpuSwapchain->eglSurface = eglCreateWindowSurface(
    context->eglDisplay, 
    context->eglConfig, 
    window,  // 这就是 XComponent 回调里的 NativeWindow / NativeLayer*
    nullptr
);

3. 将 Context + Surface 绑定到当前线程

EGL 要求在某个线程上调用 eglMakeCurrent,将 “绘制 Surface / 读取 Surface / Context” 三者绑定到当前线程,后续在这个线程执行的所有 GL 命令才会作用在绑定的 Context 和 Surface 上。

初始化时:先绑定到默认 Pbuffer Surface

// GLES3GPUContext.cpp:L282-285
void GLES3GPUContext::bindContext(bool bound) {
    if (bound) {
        makeCurrent(eglDefaultSurface, eglDefaultSurface, eglDefaultContext);
        resetStates();
    } else {
        ...
    }
}

正式渲染时:再切换到窗口 Surface

// GLES3GPUContext.cpp:L331-333
void GLES3GPUContext::present(const GLES3GPUSwapchain *swapchain) {
    ...
    makeCurrent(swapchain, swapchain);
    eglSwapBuffers(eglDisplay, swapchain->eglSurface);
}

这里的 makeCurrent(swapchain, swapchain) 会内部取出 swapchain->eglSurface 作为 draw/read surface,并通过 eglMakeCurrent 绑定到同一个 eglDefaultContext 上。这样,后续的绘制输出就会落到 XComponent 对应的窗口上。

4. 绘制 + 交换缓冲区

最后就是常规的渲染循环了:

  • 绘制过程本身通过各种 gl* 函数完成(OpenGL ES 系列API)
  • 每帧结束后调用 eglSwapBuffers(eglDisplay, eglSurface),将当前前台缓冲和后台缓冲交换,把这一帧画面提交给系统显示

五、小结:从 XComponent 到游戏画面的完整链路

把上面的过程串起来,可以得到一条完整的链路:

  1. ArkUI 层在 pages/index.ets 中创建 XComponent
  2. XComponent 内部创建 Surface 和 NativeWindow
  3. Surface 创建完成后,通过回调 onSurfaceCreatedCB(OH_NativeXComponent* component, void* window) 把 NativeWindow 句柄传给 Cocos
  4. Cocos 渲染后端使用 EGL:
    • 先全局初始化:eglGetDisplayeglInitializeeglChooseConfigeglCreateContext
    • 创建默认 Pbuffer Surface:eglCreatePbufferSurface,用于 Context 保活
    • 使用 window 句柄创建真正显示的窗口 Surface:eglCreateWindowSurface
  5. 在渲染线程上:
    • 初始化阶段:eglMakeCurrent(eglDefaultSurface, eglDefaultSurface, eglDefaultContext)
    • 正式渲染时:eglMakeCurrent(windowSurface, windowSurface, eglDefaultContext)
    • 然后:glDraw*eglSwapBuffers
  6. 最终:渲染结果通过 XComponent 持有的 Surface/NativeWindow,进入鸿蒙系统的图形栈,被显示到屏幕上。

总结一下:

  • XComponent:是 ArkUI 提供的“画布容器”,为原生渲染提供 Surface / NativeWindow。
  • EGL:负责把 OpenGL ES 与这个 Surface 关联起来,打通从“画图”到“显示”的通路。
  • OpenGL ES:在绑定好的 Context + Surface 上完成实际的图形绘制。

最后,我整理了一份Cocos游戏在鸿蒙原生端的完整启动全流程,如下图所示。

六 联系作者

作者的公众号和博客会不定期分享一些游戏开发技巧和上线实战经验,欢迎关注,共同进步!

同时创建了一个游戏开发交流群,供朋友们技术交流、学习合作、问题求助等,感兴趣的朋友可以关注我的公众号,并留言加群

2赞

学习了 :+1: