K230 GPU 应用实战 - VGLite 绘制酷炫图形#

概念介绍#

矢量图基础#

矢量图形是用点、直线或者多边形等基于数学方程的几何图元表示的图像,矢量图形与使用像素表示图像的位图不同,可以无限放大而不失真。SVG是一种典型的矢量图格式,其本身是XML文本文件,描述各种图元的位置,通过浏览器打开即可看到渲染后的效果,如果你完全不了解 K230 GPU 使用的 VGLite API,可以把他看成是一个弱化版的 SVG。

K230 GPU 支持多种二维图元

  • 直线

  • 二次贝塞尔曲线

  • 三次贝塞尔曲线

  • 圆曲线(当然也可以用三次贝塞尔曲线拟合)

注意:这些图形都是线条,GPU 不能直接绘制线条,只能绘制由这些线条围成的闭合图形。

GPU 基础#

在 K230 SDK 的小核 Linux 上,主要通过调用 VGLite API 来与 GPU 交互。VGLite 内部会维护一个 GPU 的命令队列,当需要完成绘制,或者队列满时会提交到 GPU 硬件进行渲染。命令队列的长度默认为65536,可以调用 vg_lite_set_command_buffer_size 函数进行修改。

注意:VGLite API 不支持在多线程上下文中使用,如果你的应用程序使用了多线程,请确保只有一个线程会使用VGLite API

K230 GPU 是一个 memory-to-memory 设备,本身不具备显示输出能力,如果需要显示可以与 DRM 配合使用。

使用 VGLite API#

开发环境准备#

VGLite API 主要包含两个部分,头文件和库文件,其中头文件的位置在

<K230 SDK>/src/little/buildroot-ext/package/vg_lite/inc/vg_lite.h

将 K230 SDK 完整编译后,库文件会放在

<K230 SDK>/output/k230_evb_defconfig/little/buildroot-ext/target/usr/lib/libvg_lite.so

make#

将代码的源文件放到 src 目录下,创建一个 Makefile 并将如下内容粘贴进去,设置K230SDK环境变量为存放 K230 SDK 的路径(或者将第一行的 /path/to/k230_sdk改为存放K230 SDK的路径),即可使用make命令进行构建,构建完成后将在 Makefile 同级目录下生成可执行文件,将其拷贝到小核 linux 上运行即可,也可以使用 make install 将其拷贝到 K230 SDK,再到 K230 SDK 目录下构建镜像,再烧录到 SD 卡或 eMMC 启动。

K230SDK ?= /path/to/k230_sdk
BIN := test-vglite

CC := "$(K230SDK)/toolchain/Xuantie-900-gcc-linux-5.10.4-glibc-x86_64-V2.6.0/bin/riscv64-unknown-linux-gnu-gcc"
CXX := "$(K230SDK)/toolchain/Xuantie-900-gcc-linux-5.10.4-glibc-x86_64-V2.6.0/bin/riscv64-unknown-linux-gnu-g++"

CFLAGS += -I"$(K230SDK)/src/little/buildroot-ext/package/vg_lite/inc" -I"$(K230SDK)/output/k230_evb_defconfig/little/buildroot-ext/host/riscv64-buildroot-linux-gnu/sysroot/usr/include"
CFLAGS += -L"$(K230SDK)/output/k230_evb_defconfig/little/buildroot-ext/target/usr/lib"
CFLAGS += -lvg_lite -lvg_lite_util -ldrm
CFLAGS += -Wall -g

CXXFLAGS := $(CFLAGS)

SRCDIR := ./src
OBJDIR := ./objs
SRCS := $(wildcard $(SRCDIR)/*.c) $(wildcard $(SRCDIR)/*.cpp)
OBJS := $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(filter %.c, $(SRCS))) \
        $(patsubst $(SRCDIR)/%.cpp,$(OBJDIR)/%.o,$(filter %.cpp, $(SRCS)))
DEPS := $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.d,$(filter %.c, $(SRCS))) \
        $(patsubst $(SRCDIR)/%.cpp,$(OBJDIR)/%.d,$(filter %.cpp, $(SRCS)))

all: $(BIN)

$(OBJDIR):
	mkdir -p $(OBJDIR)

$(BIN): $(OBJS)
	$(CXX) $(CXXFLAGS) $(OBJS) -o $@

$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR)
	$(CC) $(CFLAGS) -MMD -c $< -o $@

$(OBJDIR)/%.o: $(SRCDIR)/%.cpp | $(OBJDIR)
	$(CXX) $(CXXFLAGS) -MMD -c $< -o $@

-include $(DEPS)

clean:
	rm -rf $(OBJDIR) $(BIN)

install:
	cp $(BIN) "$(K230SDK)/output/k230_evb_defconfig/little/buildroot-ext/target/usr/bin"

uninstall:
	rm "$(K230SDK)/output/k230_evb_defconfig/little/buildroot-ext/target/usr/bin/$(BIN)"

.PHONY: all clean install

CMake#

将代码的源文件放到 src 目录下,创建一个 CMakeLists.txt 文件并粘贴如下内容进去,将第三行的/path/to/k230_sdk修改为存放 K230 SDK 的目录,即可使用 cmake 构建。

cmake_minimum_required(VERSION 3.0)
project(test-vglite)
set(K230SDK /path/to/k230_sdk)

set(CMAKE_C_COMPILER "${K230SDK}/toolchain/Xuantie-900-gcc-linux-5.10.4-glibc-x86_64-V2.6.0/bin/riscv64-unknown-linux-gnu-gcc")
set(CMAKE_CXX_COMPILER "${K230SDK}/toolchain/Xuantie-900-gcc-linux-5.10.4-glibc-x86_64-V2.6.0/bin/riscv64-unknown-linux-gnu-g++")
set(CMAKE_C_FLAGS "-Wall")
set(CMAKE_CXX_FLAGS "-Wall")

include_directories(
    "${K230SDK}/output/k230_evb_defconfig/little/buildroot-ext/host/riscv64-buildroot-linux-gnu/sysroot/usr/include"
    "${K230SDK}/src/little/buildroot-ext/package/vg_lite/inc"
)

link_directories("${K230SDK}/output/k230_evb_defconfig/little/buildroot-ext/target/usr/lib")
link_libraries(vg_lite vg_lite_util drm)

file(GLOB SOURCES "src/*.c" "src/*.cpp")

add_executable(${PROJECT_NAME} ${SOURCES})

install(TARGETS ${PROJECT_NAME} DESTINATION "${K230SDK}/output/k230_evb_defconfig/little/buildroot-ext/target/usr/bin")

显示#

K230 EVB 有一个1080x1920的显示屏,在小核linux上可以用 DRM 进行显示,让 GPU 驱动加载 DRM dumb buffer 可以减少内存拷贝,实现高效渲染。GPU+DRM 的相关代码可以参考 vglite_drm 这个 demo,读者可以将 drm.c 添加到自己的程序中。

需要注意截至 K230 SDK v0.8,linux 上的 DRM 驱动仍然不能独立工作,需要依赖大核对 SoC 视频输出模块的配置,可以通过在大核上执行 sample_vo.elf 3 来完成。

其次 DRM 的颜色格式枚举与 vg_lite_buffer_format_t 并不完全一致,例如 VGLITE_BGRA8888 表示的是红色在低8位,alpha在高8位的32位颜色,对应 DRM 中的 DRM_FORMAT_ARGB8888

vglite_drm demo 运行结果

如图是 vglite_drm 运行后屏幕正确显示的颜色:R(255)G(128)B(16)

一般来说为了实现同步显示会需要两张 buffer 进行 ping-pong 交替显示,但是为了简化演示代码这里就只用了一张,读者可以自行实现垂直同步的双重缓冲区进行连续渲染。

绘图#

一些准备#

首先需要初始化 VGLite,调用 vg_lite_init 来完成,它有两个参数 tessellation_widthtessellation_height,用于渲染窗口的大小,越大的话效率越高,如果为0则表示不使用矢量绘制功能,只能 BLIT,通常会设置为最大 buffer 的大小。

渲染需要 buffer,可以从 DRM dumb 导入,像下面这样

vg_lite_buffer_t buffer;
int buf_fd;
memset(&buffer, 0, sizeof(buffer));
buf_fd = drm_get_dmabuf_fd(0);
if (buf_fd < 0) {
    perror("get fd");
    return buf_fd;
}
memset(&buffer, 0, sizeof(buffer));
buffer.width = width;
buffer.height = height;
buffer.format = VG_LITE_ARGB8888;
buffer.stride = buffer.width * 4;
buffer.memory = buffer.memory = drm_get_map(0);
if (vg_lite_map(&buffer, VG_LITE_MAP_DMABUF, buf_fd)) {
    perror("import dma-buf");
    return -1;
}

也可以从 GPU 驱动分配离屏 buffer,像下面这样

vg_lite_buffer_t buffer;
memset(&buffer, 0, sizeof(buffer));
buffer.width = width;
buffer.height = height;
buffer.format = VG_LITE_ARGB8888;
if (vg_lite_allocate(&buffer)) {
    return -1;
}

显然分配离屏 buffer 更简单,只需要配置分辨率和像素格式即可,而从 DRM dumb 导入还需要自己计算 stride(一行像素的字节数量),当然从 DRM dumb 导入的好处就是可以直接用于显示。

多边形#

有 buffer 就可以开始绘制了。多边形由多条直线组成,以三角形为例,首先需要确定三角形三个顶点的坐标,例如(0,0) (0,1) (1,0),整个过程如下

  1. 将画笔移动到(0,0)

  2. 画一条线到(0,1)

  3. 画一条线到(1,0)

  4. 画一条线到(0,0)

  5. 闭合图形

查阅K230 GPU API参考可以看到移动的操作码是2,直线的操作码是4,闭合路径的操作码是0,使用第一种数据格式,可以构造 path_data 数组如下

uint8_t path_data[] = {
    2, 0, 0, // 移动到 (0,0)
    4, 0, 1, // 直线到 (0,1)
    4, 1, 0, // 直线到 (1,0)
    4, 0, 0, // 直线到 (0,0)
0};

只有 path_data 还是不够的,渲染需要的参数是 path,path 中除了 path_data 外还包含数据格式等信息,数据的格式可以指定为以下几种:

  1. 8bit 有符号整数

  2. 16bit 有符号整数

  3. 32bit 有符号整数

  4. 32bit 浮点数

从上到下性能依次递减,不过即使是8bit有符号整数也不意味着只能覆盖-128到127的像素范围,因为还需要矩阵变换来计算最终坐标。

现在构造 path,并绘制到 buffer

vg_lite_path_t path = {
    .bounding_box = {0., 1., 1., 0.}, // 图形的包围盒
    .quality = VG_LITE_HIGH, // 渲染质量
    .format = VG_LITE_S8, // 考虑到坐标很简单,所以 8bit 足够
    .uploaded = 0, // 路径没有被上传过 GPU,所以用0
    .path_length = sizeof(path_data), // 路径数据长度,以字节为单位
    .path = path_data, // 路径数据就放在这了
    .path_changed = 1, // 用来表示路径被更新过
    .pdata_internal = 0 // 表示路径数据不是由驱动分配的
};

有了上面那些变量后就可以执行渲染了,步骤如下

  1. 将 buffer 清空,即用一个单色填充,可以使用 vg_lite_clear 来完成

  2. 一个变换矩阵,关于矩阵可以参考仿射变换有关的内容,这里直接使用一个缩放矩阵,将图像放大100倍,这样最终长度为1的直线在图像中会使用100个像素

  3. 调用 vg_lite_draw 来将 path “渲染”到 buffer

  4. 最后,使用 vg_lite_finish 提交渲染

为了方便错误处理,使用 CHECK_ERROR 宏来包裹返回 vg_lite_error_t 的函数

vg_lite_matrix_t matrix;
CHECK_ERROR(vg_lite_clear(&buffer, NULL, 0xffff0000)); // 使用蓝色填充整个 buffer
vg_lite_identity(&matrix); // 初始化为单位矩阵
vg_lite_translate(buffer.width / 2., buffer.height / 2., &matrix); // 移动到 buffer 中间位置
vg_lite_scale(100., 100., &matrix); // x y 方向都放d大100倍
CHECK_ERROR(vg_lite_draw(
    &buffer, &path,
    VG_LITE_FILL_NON_ZERO, //  填充规则,像素只要被覆盖就会被绘制
    &matrix,
    VG_LITE_BLEND_NONE, // 颜色混合规则,None 表示忽略透明度直接覆盖
    0xff0000ff // RGBA 颜色,这个值表示不透明的红色
));
CHECK_ERROR(vg_lite_finish()); // 提交到 GPU

完整的参考代码可以在 vglite_drm 中查看,下面是绘制出来的效果。

绘制的简单三角形

可以很容易地发现,坐标系是以右为x正方向,以下为y正方向,这也是 SVG 所使用的坐标系。

需要注意的是,我刚才说到 vg_lite_draw 时“渲染”是打了引号的,因为并没有真的渲染,只是写入渲染指令,最终渲染需要调用 vg_lite_finish,这样对性能是有好处的,实际使用中可以多次调用 vg_lite_draw,再最后实际显示前再执行 vg_lite_finish,因为 vg_lite_finish是系统调用,存在一定开销,而 vg_lite_draw 不是,可以执行得很快。

当渲染完成后,可以将结果显示到屏幕或者保存为图片,需要注意保存图片时是用 CPU 来读取数据的,所以需要确保 vg_lite_buffer_t::memory 可读,如果读者使用上面 DRM 的代码来创建 vg_lite_buffer_t,那么没有映射 DRM dumb 的话是没法读取的。

曲线#

K230 GPU 支持三种曲线,分别是

  1. 二次贝塞尔曲线

  2. 三次贝塞尔曲线

  3. 椭圆曲线

当然椭圆曲线可以用三次贝塞尔曲线拟合,本质上可以看成是同样的曲线类型,与绘制多边形的情况一样,只需要修改操作码和数据即可。

下面我们尝试将刚才画的三角形的底边改为二次贝塞尔曲线,并将中点放在(1,1)处,绘制一个近似圆角的图案,将上面 path_data 改为

uint8_t path_data[] = {
    2, 0, 0,
    4, 0, 1,
    6, 1, 1, 1, 0, // 二次贝塞尔曲线,控制点(1,1),画到(1,0)
    4, 0, 0,
0};

当然,为了更好地观察这条曲线,我们将缩放倍数开得更大一些,比如500倍,同时将位移也改小一些,使得图案近似在屏幕中央

vg_lite_translate(buffer.width / 2., buffer.height / 2., &matrix);
vg_lite_scale(500., 500., &matrix);

最后绘制的图案就像下面这样了

近似圆角的扇形

位图填充#

当不满足于单色的填充时,可以使用位图图来填充,位图文件会被渲染到目标位置,当然位图也得是 vg_lite_buffer_t 才行,如果需要从本地的 JPEG/PNG 等文件中加载,那么建议使用离屏 buffer 来存储像素内容,用 vg_lite_blit 或者 vg_lite_draw_pattern 来渲染。

渐变#

对于 VGLite 的实现来说,渐变本身是一种特殊的位图填充,linear_grad 相关函数会分配一个 1x256 的 buffer 进行 BLIT,当然使用者可以不关心上面的细节,拿来用就好,参考 linearGrad demo,具体过程可以分为以下几个调用

  1. vg_lite_init_grad 初始化一个渐变

  2. vg_lite_set_grad 设置颜色和止点,最多支持16个止点

  3. vg_lite_update_grad 更新渐变

  4. vg_lite_get_grad_matrix 获得渐变的变换矩阵指针

  5. 对变换矩阵进行调整,例如旋转和缩放,默认长度为256像素从左到右,如果需要其他方向的渐变需要用这个矩阵来操作

  6. vg_lite_draw_gradient 绘制渐变