Administrator
发布于 2026-09-03 / 8,018 阅读
0
0

鸿蒙图形栈与 Surface:XComponent、EGL 自绘与硬件合成

鸿蒙百篇系列第 66 篇 · 高级

第 61 篇我们钻进了 RenderNode 层的渲染管线。这一篇把镜头拉到另一条赛道:XComponent + EGL 自绘。它们的"渲染权"在 Native 一侧,能直接持有 NativeWindow 上屏。游戏引擎、视频编辑、相机预览、自定义图表动效都用得上。本文从鸿蒙图形栈的分层开始,到一个能跑通的 EGL 三角形 Demo 结束。

一、鸿蒙图形栈的分层

鸿蒙图形栈按"渲染逻辑到硬件"分四层,每层各司其职:

层级

责任

暴露给开发者

应用层 (ArkUI)

组件树声明、动画、事件分发

@Component

渲染层 (Render Service)

场景图、绘制指令编译、资源管理

RenderNode C API

驱动层 (Graphics HAL)

硬件抽象、跨平台后端切换

EGL/OpenGL ES、Vulkan

硬件层 (GPU/Display)

帧合成、垂直同步、显示输出

不直接暴露

应用普通 UI 走"应用层 → 渲染层 → 驱动层",自绘时绕过应用层,直接进驱动层。XComponent 就是打开这条直通通道的"门票"。

二、XComponent 的两种类型

XComponent 不是一个普通组件。它有 type 字段:

Type

渲染主体

适合场景

SURFACE

Native 直接绘制,画面独占

视频播放器、相机的预览流、纯自绘游戏

TEXTURE

把 Native 内容与 ArkUI 内容合成

AR 叠加、自定义 HUD

代码上看起来差不多:

import { XComponent, XComponentContext } from '@ohos.arkui.advanced';

// SURFACE 类型
XComponent({
  type: 'surface',
  controller: this.xComponentController,
  // onLoad 是 Native 侧拿到 XComponent 句柄的回调
  onLoad: () => {}
})

// TEXTURE 类型
XComponent({
  type: 'texture',
  controller: this.xComponentController,
  onLoad: () => {}
})

XComponentContext 是 Native 拿到 component 句柄后的上下文,包含 NativeWindow、nativeRenderContext,是后续 EGL 初始化的入口。

三、NativeWindow:拿到 surface 的钥匙

EGL 要 EGLSurface + EGLContext,Native 一侧通过 NativeWindow 把 XComponent 的 surface 转给 EGL:

#include <ace/xcomponent/native_interface_xcomponent.h>
#include <EGL/egl.h>
#include <GLES3/gl3.h>

struct XCompContext {
    EGLSurface eglSurface;
    EGLContext eglContext;
    EGLDisplay eglDisplay;
    NativeWindow nativeWindow;
};

bool InitEGL(XCompContext &ctx, OHNativeWindow *window) {
    ctx.eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (ctx.eglDisplay == EGL_NO_DISPLAY) return false;

    EGLint major, minor;
    if (!eglInitialize(ctx.eglDisplay, &major, &minor)) return false;

    const EGLint configAttribs[] = {
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
        EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
        EGL_RED_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE, 8,
        EGL_ALPHA_SIZE, 8,
        EGL_DEPTH_SIZE, 24,
        EGL_STENCIL_SIZE, 8,
        EGL_NONE
    };

    EGLConfig config;
    EGLint numConfigs;
    if (!eglChooseConfig(ctx.eglDisplay, configAttribs, &config, 1, &numConfigs)) return false;

    EGLint nativeVisualId;
    eglGetConfigAttrib(ctx.eglDisplay, config, EGL_NATIVE_VISUAL_ID, &nativeVisualId);
    OH_NativeWindow_SetNativeWindowColorSpace(window, OH_NativeWindow_GetColorSpaceFromVisualId(nativeVisualId));

    const EGLint contextAttribs[] = {
        EGL_CONTEXT_CLIENT_VERSION, 3,
        EGL_NONE
    };

    ctx.eglContext = eglCreateContext(ctx.eglDisplay, config, EGL_NO_CONTEXT, contextAttribs);
    if (ctx.eglContext == EGL_NO_CONTEXT) return false;

    ctx.eglSurface = eglCreateWindowSurface(ctx.eglDisplay, config, window, nullptr);
    if (ctx.eglSurface == EGL_NO_SURFACE) return false;

    return eglMakeCurrent(ctx.eglDisplay, ctx.eglSurface, ctx.eglSurface, ctx.eglContext) == EGL_TRUE;
}

每一个 NativeWindow 都对应一个 EGL Surface。把 XComponent 的 surface_id 喂给 OH_NativeWindow_CreateFromSurfaceId 即可创建 nativeWindow:

OHNativeWindow *window = OH_NativeWindow_CreateFromSurfaceId(surfaceId, OH_SURFACE_TYPE_TEXTURE);

接着持有这个窗口去做 EGL 初始化,后面就能直接调 OpenGL ES API 画东西了。

四、画一个三角形

完成初始化后,每帧渲染流程是:清屏 → 画 → 提交:

void DrawTriangle() {
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    const char *vertexShader = R"(
        #version 300 es
        in vec2 aPos;
        in vec3 aColor;
        out vec3 vColor;
        void main() {
            gl_Position = vec4(aPos, 0.0, 1.0);
            vColor = aColor;
        }
    )";

    const char *fragmentShader = R"(
        #version 300 es
        precision mediump float;
        in vec3 vColor;
        out vec4 FragColor;
        void main() {
            FragColor = vec4(vColor, 1.0);
        }
    )";

    // 编译、链接、绑定、着色器绘制过程略
    // 提交 Surface
    eglSwapBuffers(eglDisplay, eglSurface);
}

完整示例工程可以参考华为官方的 NativeWindow & EGL 指导,我把它简化成最关键的几步:着色器编译、VAO 绑定、uniform 变量设置、glDrawArrays 调用、eglSwapBuffers。

五、ArkTS 侧绑定 XComponent

class XCompController extends XComponentController {
  // 被 Native 通过 OH_COMPONENT 接口回调过来,绑定 surfaceId
  private surfaceId: string = '';

  getSurfaceId(): string {
    return this.surfaceId;
  }
}

@Entry
@Component
struct EGLCover {
  private controller: XCompController = new XCompController();

  build() {
    Column() {
      XComponent({
        id: 'egl',
        type: 'surface',
        controller: this.controller
      })
        .width('100%')
        .height(400)
        .onLoad(() => {
          // 注册 Native 回调
          // Native 会拿到 controller.xcomponent_
        })
    }
  }
}

Native 侧收到回调后通过 OH_XComponent_GetXComponentContext(component) 拿到上下文,调用 OH_NativeWindow_CreateFromSurfaceId(context->surfaceId, ...)。

六、Native XComponent 与 ArkTS XComponent 的边界

这两个很容易混:

类型

回调给谁

代码位置

xcomponent(ArkTS XComponent)

ArkTS 侧 @State、事件

纯 ArkTS

xcomponent_native(Native XComponent)

Native C++ 侧

C++

完整声明:

XComponent({
  type: 'surface',
  controller: this.controller,
  xcomponent: {  // 用于 ArkTS 侧接回调
    onLoad: (context) => {
      // context.surfaceId
    }
  },
  xcomponent_native: {  // Native 侧接回调
    onSurfaceCreated: (context) => { /* C++ 回调 */ },
    onSurfaceDestroyed: () => { /* ... */ }
  }
})

ArkTS 的 onLoad 用于直接调 canvas.fillRect,不能拿 EGL Surface;真正走 EGL 自绘要写到 xcomponent_native 的回调里。

七、合成类型:TEXTURE 的特殊价值

TEXTURE 类型适合做"叠加"。比如直播场景:把摄像头(OPENGL 推流)当背景,画面上加一层 UI 控件(点赞、礼物)。TEXTURE 渲染的内容会跟 XComponent 兄弟节点一起合成:

XComponent({
  type: 'texture',  // ← 关键
  controller: this.controller
})

Native 侧把每一帧画到 GLES texture,再用 OH_NativeWindow_SetSurfacePixelMapRotation 或其相关接口合上去。具体合成逻辑见 Render Service 的文档,正常情况下框架自动处理。

八、性能与坑

  1. 帧率控制。自绘默认是 vsync 60FPS 锁定,要解锁 90/120Hz 要申请高刷权限

  2. EGL Context 不能跨线程共享。多线程渲染需要各自创建 Context 并显式 eglMakeCurrent

  3. NativeWindow 释放对顺序敏感。先 destroy 资源,再 OH_NativeWindow_DestroyNativeWindow,否则会出现 leak 警告

  4. deprecated 的 API 别用。GSNativeWindow 已被 OH_NativeWindow 系列取代

  5. OpenGL ES 版本选用 ES3.0+。ES2.0 缺少 uniform block、compute 等特性,画图表明显吃亏

  6. 重要资源清理放 onSurfaceDestroyed。否则 framebuffer、texture leak 等到应用崩溃才发现

九、几个工程模式

场景

推荐

普通 UI 图表

用 ArkUI Canvas 即可,自绘 CANVAS 性能足够

高频波形/音柱

XComponent + ES2.0 + VBO,最少性能开销

相机/AR

XComponent TEXTURE + 外部纹理,camera 与 GLES 共享 buffer

视频解码后帧渲染

XComponent SURFACE + 视频输出回调,性能最高

完整游戏引擎

XComponent SURFACE + 自管理 game loop,v-sync 节流

十、和前几篇的衔接

  • 第 61 篇讲 RenderNode。本篇是 RenderNode 的"放大版"——XComponent 直接拥有整个 EGL Surface

  • 第 14 篇讲音视频,那里的视频渲染底层就是 XComponent + Surface

  • 第 64 篇的 NDK 是 XComponent EGL 自绘的工程基础,没看过可以先翻一遍

十一、小结

XComponent 是 ArkUI 体系里的"Native 渲染通行证"。一旦你的需求超出"声明式 UI"的范畴——做极致流畅的自绘、做视频编辑器、做实时滤镜——它就是必经之路。掌握 NativeWindow + EGL 初始化、shader 编译、draw/swap 节奏之后,就可以放心把 OpenGL ES 生态的东西搬过来。

下一篇转向电源管理,讨论 RunningLock、Thermal、后台保活的取舍。


评论