Skip to content

Instantly share code, notes, and snippets.

@mac-l1
Last active February 1, 2017 20:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mac-l1/ef4b4d54322be6c60755af2dcd1a97c9 to your computer and use it in GitHub Desktop.
Save mac-l1/ef4b4d54322be6c60755af2dcd1a97c9 to your computer and use it in GitHub Desktop.
sample test code for simple ffmpeg player using rockchips drm rga api to directly convert nv12 frames from vpu buf to rgb X11 window buffers; needs rockchip drm rga kernel driver and user lib, probably also armsoc (for dri2); ran on rk3288/firefly 3.14 kernel and ubuntu 16.04
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libswscale/swscale.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <linux/stddef.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include <libkms.h>
#include <drm_fourcc.h>
#include "rockchip_drm.h"
#include "rockchip_drmif.h"
#include "rockchip_rga.h"
#include <X11/Xlib.h>
#include <X11/Xmd.h>
#include <X11/extensions/dri2proto.h>
#include <X11/extensions/dri2.h>
#include <X11/Xlibint.h>
#include <X11/extensions/Xext.h>
#include <X11/extensions/extutil.h>
#include <X11/Xutil.h>
#include <sys/shm.h>
#include <X11/extensions/XShm.h>
#include "list.h"
#include "vpu_simple_mem_pool.h"
#define DEMO_DBG(fmt, ...) do { printf("DBG: %s:%d " fmt "\n", __func__, __LINE__, ##__VA_ARGS__); } while (0)
#define DEMO_ERROR(fmt, ...) do { printf("ERROR: %s:%d " fmt "\n", __func__, __LINE__, ##__VA_ARGS__); } while (0)
#define DEMO_INFO(fmt, ...) do { printf("INFO: %s:%d " fmt "\n", __func__, __LINE__, ##__VA_ARGS__); } while (0)
struct dma_fd_list {
struct list_head node;
int width;
int height;
int dma_fd;
uint8_t *vaddr;
uint32_t format;
int virtual_width;
int virtual_height;
};
struct rk_dma_buffer {
int fd;
int handle;
uint8_t *vir_addr;
int size;
};
struct cmd_context {
Display *display;
Window win;
struct rk_dma_buffer x11_dma_buffers[2];
DRI2Buffer *dri2_bufs;
int nbufs;
int window_width;
int window_height;
int choose_screen;
struct rga_context *rga_ctx;
struct rockchip_device *rk_dev;
int authenticated_fd;
int frame_width;
int frame_height;
int frame_size;
int frame_fd;
uint8_t *frame_vaddr;
uint32_t frame_format;
struct vpu_simple_mem_pool *vpu_mem_pool;
char *input_file;
int displayed;
int record_frame;
AVFormatContext *format_ctx;
AVCodecContext *codec_ctx;
AVCodec *codec;
AVFrame *frame;
int video_stream_index;
int fps;
double cl;
double last_cl;
int cycle_num;
};
static void init_x11_context(struct cmd_context *cmd_ctx) {
//cmd_ctx->display = XOpenDisplay(getenv("DISPLAY"));
cmd_ctx->display = XOpenDisplay(":0.0");
cmd_ctx->win = XCreateSimpleWindow(cmd_ctx->display,
RootWindow(cmd_ctx->display, DefaultScreen(cmd_ctx->display)),
0, 0,
DisplayWidth(cmd_ctx->display, 0),
DisplayHeight(cmd_ctx->display, 0),
0,
BlackPixel(cmd_ctx->display, DefaultScreen(cmd_ctx->display)),
BlackPixel(cmd_ctx->display, DefaultScreen(cmd_ctx->display)));
XMapWindow(cmd_ctx->display, cmd_ctx->win);
XResizeWindow(cmd_ctx->display, cmd_ctx->win, 800, 600);
XSync(cmd_ctx->display, False);
XFlush(cmd_ctx->display);
usleep(100000);
}
static int dri2_connect(Display *dpy, int fd, int driverType, char **driver)
{
int eventBase, errorBase, major, minor;
char *device;
drm_magic_t magic;
Window root;
if (!DRI2InitDisplay(dpy, NULL)) {
DEMO_ERROR("DRI2InitDisplay failed");
return -1;
}
if (!DRI2QueryExtension(dpy, &eventBase, &errorBase)) {
DEMO_ERROR("DRI2QueryExtension failed");
return -1;
}
DEMO_DBG("DRI2QueryExtension: eventBase=%d, errorBase=%d", eventBase, errorBase);
if (!DRI2QueryVersion(dpy, &major, &minor)) {
DEMO_ERROR("DRI2QueryVersion failed");
return -1;
}
DEMO_DBG("DRI2QueryVersion: major=%d, minor=%d", major, minor);
root = RootWindow(dpy, DefaultScreen(dpy));
if (!DRI2Connect(dpy, root, driverType, driver, &device)) {
DEMO_ERROR("DRI2Connect failed");
return -1;
}
DEMO_DBG("DRI2Connect: driver=%s, device=%s", *driver, device);
if (drmGetMagic(fd, &magic)) {
DEMO_ERROR("drmGetMagic failed");
return -1;
}
if (!DRI2Authenticate(dpy, root, magic)) {
DEMO_ERROR("DRI2Authenticate failed");
return -1;
}
return fd;
}
static int init_authenticated_fd(struct cmd_context *cmd_ctx)
{
char *driver;
cmd_ctx->authenticated_fd = open("/dev/dri/card0", O_RDWR);
if (cmd_ctx->authenticated_fd < 0) {
DEMO_ERROR("failed to open");
return -1;
}
dri2_connect(cmd_ctx->display, cmd_ctx->authenticated_fd, DRI2DriverDRI, &driver);
cmd_ctx->rga_ctx = rga_init(cmd_ctx->authenticated_fd);
if (!cmd_ctx->rga_ctx) {
DEMO_ERROR("rga init failed");
return -1;
}
cmd_ctx->rk_dev = rockchip_device_create(cmd_ctx->authenticated_fd);
if (!cmd_ctx->rk_dev) {
DEMO_ERROR("rockchip_device_create failed");
return -1;
}
DEMO_DBG("driver name:%s", driver);
}
static int get_x11_dma_buffer(struct cmd_context *cmd_ctx)
{
int i;
unsigned attachments[] = {
0,
1,
};
DRI2CreateDrawable(cmd_ctx->display, cmd_ctx->win);
cmd_ctx->dri2_bufs = DRI2GetBuffers(cmd_ctx->display, cmd_ctx->win,
&cmd_ctx->window_width,
&cmd_ctx->window_height,
attachments, 2, &cmd_ctx->nbufs);
DEMO_DBG("display width %d height %d nbufs %d", cmd_ctx->window_width,
cmd_ctx->window_height, cmd_ctx->nbufs);
for (i = 0; i < cmd_ctx->nbufs; i++) {
int err;
struct drm_gem_open req;
DEMO_DBG("dri2_bufs[%d] name %u attachment %u flags %u cpp %u pitch %u",
i, cmd_ctx->dri2_bufs[i].names[0],
cmd_ctx->dri2_bufs[i].attachment,
cmd_ctx->dri2_bufs[i].flags,
cmd_ctx->dri2_bufs[i].cpp,
cmd_ctx->dri2_bufs[i].pitch[0]);
req.name = cmd_ctx->dri2_bufs[i].names[0];
int ret = drmIoctl(cmd_ctx->authenticated_fd, DRM_IOCTL_GEM_OPEN, &req);
cmd_ctx->x11_dma_buffers[i].handle = req.handle;
cmd_ctx->x11_dma_buffers[i].size = req.size;
DEMO_DBG("ret %d dri2_bufs[%d] handle is %d size %lu", ret, i, req.handle, req.size);
drmPrimeHandleToFD(cmd_ctx->authenticated_fd,
cmd_ctx->x11_dma_buffers[i].handle, 0, &cmd_ctx->x11_dma_buffers[i].fd);
struct drm_mode_map_dumb dmmd;
memset(&dmmd, 0, sizeof(dmmd));
dmmd.handle = req.handle;
err = drmIoctl(cmd_ctx->authenticated_fd, DRM_IOCTL_MODE_MAP_DUMB, &dmmd);
if (err) {
DEMO_ERROR("drm mode map failed");
return err;
}
cmd_ctx->x11_dma_buffers[i].vir_addr = mmap(0, cmd_ctx->x11_dma_buffers[i].size,
PROT_READ | PROT_WRITE,
MAP_SHARED, cmd_ctx->authenticated_fd,
dmmd.offset);
DEMO_DBG("x11_dma_buffers[%d].vir_addr %p", i, cmd_ctx->x11_dma_buffers[i].vir_addr);
if (cmd_ctx->x11_dma_buffers[i].vir_addr == MAP_FAILED) {
DEMO_ERROR("drm map failed");
return err;
}
}
}
static int rga_convert_copy(struct rga_context *rga_ctx,
int src_fd,
int src_width,
int src_height,
uint32_t src_stride,
uint32_t src_fmt,
int dst_fd,
int dst_width,
int dst_height,
uint32_t dst_stride,
uint32_t dst_fmt,
enum e_rga_buf_type type,
int virtual_width,
int virtual_height)
{
struct rga_image src_img = { 0 }, dst_img = { 0 };
dst_img.bo[0] = dst_fd;
src_img.bo[0] = src_fd;
src_img.width = src_width;
src_img.height = src_height;
src_img.stride = src_stride;
src_img.buf_type = type;
src_img.color_mode = src_fmt;
dst_img.width = dst_width;
dst_img.height = dst_height;
dst_img.stride = dst_stride;
dst_img.buf_type = type;
dst_img.color_mode = dst_fmt;
//DEMO_DBG("src fd %d stride %d dst stride %d fd %d", src_fd, src_img.stride, dst_img.stride, dst_fd);
rga_multiple_transform(rga_ctx, &src_img, &dst_img,
0, 0, src_img.width, src_img.height,
0, 0, dst_img.width, dst_img.height,
0, 0, 0);
rga_exec(rga_ctx);
return 0;
}
static int display_one_frame(struct cmd_context *cmd_ctx, struct dma_fd_list *node)
{
int ret;
unsigned long count;
//cmd_ctx->choose_screen++;
ret = rga_convert_copy(cmd_ctx->rga_ctx, node->dma_fd, node->width,
node->height, node->width, node->format,
cmd_ctx->x11_dma_buffers
[cmd_ctx->choose_screen % cmd_ctx->nbufs].fd,
cmd_ctx->window_width, cmd_ctx->window_height,
cmd_ctx->window_width * 4,
DRM_FORMAT_ABGR8888,
//DRM_FORMAT_ARGB8888,
RGA_IMGBUF_GEM, node->virtual_width,node->virtual_height);
DRI2SwapBuffers(cmd_ctx->display, cmd_ctx->win, 0, 0, 0, &count);
cmd_ctx->choose_screen++;
//DEMO_DBG("DRI2SwapBuffers: count = %lu", count);
return ret;
}
static int drm_alloc(int drm_fd, size_t size, int *dma_fd, uint8_t **dma_vaddr)
{
int ret = 0;
int map_fd, handle;
if (size < 4096) size = 4096;
struct drm_mode_create_dumb dmcd;
memset(&dmcd, 0, sizeof(dmcd));
dmcd.bpp = 4096;
dmcd.width = ((size + 4095) & (~4095)) >> 12;
dmcd.height = 8;
dmcd.size = dmcd.width * dmcd.bpp;
ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_CREATE_DUMB, &dmcd);
handle = dmcd.handle;
size = dmcd.size;
if (ret) {
DEMO_DBG("drm alloc failed\n");
return ret;
}
ret = drmPrimeHandleToFD(drm_fd, handle, 0, &map_fd);
if (ret) {
DEMO_DBG("prime handle to dma fd failed\n");
struct drm_mode_destroy_dumb dmdb;
dmdb.handle = handle;
drmIoctl(drm_fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dmdb);
return ret;
}
struct drm_mode_map_dumb dmmd;
memset(&dmmd, 0, sizeof(dmmd));
dmmd.handle = handle;
ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_MAP_DUMB, &dmmd);
if (ret) {
DEMO_DBG("drm mode map failed\n");
struct drm_mode_destroy_dumb dmdb;
dmdb.handle = handle;
drmIoctl(drm_fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dmdb);
return ret;
}
#if 0
*dma_vaddr = mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED,
drm_fd, dmmd.offset);
if (dma_vaddr == MAP_FAILED) {
DEMO_ERROR("drm map failed\n");
struct drm_mode_destroy_dumb dmdb;
dmdb.handle = handle;
drmIoctl(drm_fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dmdb);
return ret;
}
#endif
*dma_fd = map_fd;
return size;
}
static int read_yuv(const char* filename,uint8_t* pdata,int width,int height) {
FILE *fp = fopen(filename, "rb");
if ((pdata == NULL) || (fp == NULL)) return 0;
printf("read yuv-frame(%dx%d) data from %s\n", width,height,filename );
fread(pdata, width*height*3/2, 1, fp);
fclose(fp);
return 1;
}
static int write_truecolor_tga(char* filename, int* data, int width, int height) {
FILE *fp = fopen(filename, "wb");
if (fp == NULL) return 0;
printf("write frame(tga) data to %s\n", filename );
char header[ 18 ] = { 0 }; // char = byte
header[ 2 ] = 2; // truecolor
header[ 12 ] = width & 0xFF;
header[ 13 ] = (width >> 8) & 0xFF;
header[ 14 ] = height & 0xFF;
header[ 15 ] = (height >> 8) & 0xFF;
header[ 16 ] = 24; // bits per pixel
fwrite((const char*)&header, 1, sizeof(header), fp);
int x,y;
for (y = height -1; y >= 0; y--)
for (x = 0; x < width; x++) {
char b = (data[x+(y*width)] & 0x0000FF);
char g = (data[x+(y*width)] & 0x00FF00) >> 8;
char r = (data[x+(y*width)] & 0xFF0000) >> 16;
putc((int)(b & 0xFF),fp);
putc((int)(g & 0xFF),fp);
putc((int)(r & 0xFF),fp);
}
static const char footer[ 26 ] = "\0\0\0\0\0\0\0\0TRUEVISION-XFILE.";
fwrite((const char*)&footer, 1, sizeof(footer), fp);
fclose(fp);
return 1;
}
static void get_fps(struct cmd_context *cmd_ctx) {
struct timeval tv;
gettimeofday(&tv, NULL);
cmd_ctx->cl = tv.tv_sec + (double)tv.tv_usec / 1000000;
if (!cmd_ctx->last_cl)
cmd_ctx->last_cl = cmd_ctx->cl;
if (cmd_ctx->cl - cmd_ctx->last_cl > 1.0f) {
cmd_ctx->last_cl = cmd_ctx->cl;
printf(">>>>>fps=%d\n", cmd_ctx->fps);
cmd_ctx->fps = 1;
} else {
cmd_ctx->fps++;
}
}
static void save_frame(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;
char szFilename[32];
int y;
if((iFrame % 100) != 1) return;
sprintf(szFilename, "frame%d.yuv", iFrame);
pFile = fopen(szFilename, "wb");
if (!pFile)
return;
printf("write frame to %s, data[0]=%p, data[1]=%p, data[2]=%p, data[3]=%p,\n", szFilename,pFrame->data[0],pFrame->data[1],pFrame->data[2],pFrame->data[3] );
fwrite(pFrame->data[0], 1, pFrame->linesize[0] * height, pFile);
fwrite(pFrame->data[1], 1, pFrame->linesize[1] * height / 2, pFile);
fwrite(pFrame->data[2], 1, pFrame->linesize[2] * height / 2, pFile);
fclose(pFile);
}
static void init_ffmpeg_context(struct cmd_context *cmd_ctx, void* mem_pool) {
int i;
av_register_all();
if (avformat_open_input(&cmd_ctx->format_ctx, cmd_ctx->input_file, NULL, NULL) != 0) {
printf("avformat_open_input failed\n");
exit(-1);
}
if (avformat_find_stream_info(cmd_ctx->format_ctx, NULL) < 0) {
printf("avformat_find_stream_info failed\n");
exit(-1);
}
av_dump_format(cmd_ctx->format_ctx, -1, cmd_ctx->input_file, 0);
cmd_ctx->video_stream_index = -1;
for (i = 0; i < cmd_ctx->format_ctx->nb_streams; i++) {
if (cmd_ctx->format_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
cmd_ctx->video_stream_index = i;
break;
}
}
if (cmd_ctx->video_stream_index == -1) {
printf("can not find video stream info\n");
exit(-1);
}
cmd_ctx->codec_ctx = cmd_ctx->format_ctx->streams[cmd_ctx->video_stream_index]->codec;
cmd_ctx->codec = avcodec_find_decoder(cmd_ctx->codec_ctx->codec_id);
if (cmd_ctx->codec == NULL) {
printf("can not find codec %d\n", cmd_ctx->codec_ctx->codec_id);
exit(-1);
}
cmd_ctx->codec_ctx->opaque = mem_pool;
DEMO_DBG("codec ctx opaque %p", cmd_ctx->codec_ctx->opaque);
if (avcodec_open2(cmd_ctx->codec_ctx, cmd_ctx->codec, NULL) < 0) {
printf("avcodec_open2 failed\n");
exit(-1);
}
cmd_ctx->frame = av_frame_alloc();
if (!cmd_ctx->frame) {
printf("av_frame_alloc failed\n");
exit(-1);
}
}
void deinit_ffmpeg_context(struct cmd_context *cmd_ctx) {
av_frame_free(&cmd_ctx->frame);
avcodec_close(cmd_ctx->codec_ctx);
avformat_close_input(&cmd_ctx->format_ctx);
}
int decode_one_frame(struct cmd_context *cmd_ctx) {
AVPacket packet;
int get_frame = 0;
if (av_read_frame(cmd_ctx->format_ctx, &packet) >= 0) {
//printf("read some packet index %d video_stream_index %d\n", packet.stream_index, cmd_ctx->video_stream_index);
if (packet.stream_index == cmd_ctx->video_stream_index) {
avcodec_decode_video2(cmd_ctx->codec_ctx, cmd_ctx->frame, &get_frame, &packet);
get_fps(cmd_ctx);
}
//printf("after\n");
av_free_packet(&packet);
} else {
get_frame = -1;
}
return get_frame;
}
#if 0
int open_vpu_mem_pool(struct cmd_context *cmd_ctx) {
int i;
cmd_ctx->vpu_mem_pool = open_vpu_simple_mem_pool(
cmd_ctx->authenticated_fd);
if (cmd_ctx->vpu_mem_pool == NULL) {
DEMO_ERROR("open vpu mem pool failed");
return -1;
}
for (i = 0; i < 8; i++) {
int fd;
uint8_t *vaddr;
int size = drm_alloc(cmd_ctx->authenticated_fd,
1920 * 1088 * 4, &fd,&vaddr);
cmd_ctx->vpu_mem_pool->commit_hdl(cmd_ctx->vpu_mem_pool, fd, size);
close(fd);
}
return 0;
}
#endif
static void init_cmd_context(struct cmd_context *cmd_ctx) {
memset(cmd_ctx, 0, sizeof(*cmd_ctx));
cmd_ctx->input_file = "sintel_trailer-1080p.mp4";
cmd_ctx->displayed = 1;
cmd_ctx->record_frame = 1;
cmd_ctx->vpu_mem_pool = NULL;
cmd_ctx->format_ctx = NULL;
cmd_ctx->codec_ctx = NULL;
cmd_ctx->codec = NULL;
cmd_ctx->frame = NULL;
cmd_ctx->video_stream_index = -1;
cmd_ctx->fps = 0;
cmd_ctx->cl = 0;
cmd_ctx->last_cl = 0;
}
static void parse_options(int argc, const char *argv[], struct cmd_context *cmd_ctx) {
int opt;
while ((opt = getopt(argc, argv, "i:r:d:")) != -1) {
switch (opt) {
case 'i':
cmd_ctx->input_file = optarg;
break;
case 'r':
cmd_ctx->record_frame = atoi(optarg);
break;
case 'd':
cmd_ctx->displayed = atoi(optarg);
break;
}
}
printf("parameters:\n");
printf(" input filename: %s\n", cmd_ctx->input_file);
printf(" display: %d\n", cmd_ctx->displayed);
printf(" record/save output frame: %d\n", cmd_ctx->record_frame);
}
int main(int argc, char **argv)
{
int ret = 0;
int count = 0;
struct cmd_context cmd_ctx;
av_log_set_flags(AV_LOG_DEBUG);
init_cmd_context(&cmd_ctx);
parse_options(argc, argv, &cmd_ctx);
init_x11_context(&cmd_ctx);
init_authenticated_fd(&cmd_ctx);
get_x11_dma_buffer(&cmd_ctx);
//open_vpu_mem_pool(&cmd_ctx);
/*
cmd_ctx.frame_width = 1920;
cmd_ctx.frame_height = 1088;
cmd_ctx.frame_size = drm_alloc(cmd_ctx.authenticated_fd,
cmd_ctx.frame_width *
cmd_ctx.frame_height * 4,
&cmd_ctx.frame_fd, &cmd_ctx.frame_vaddr);
read_yuv("rgb1920x1088.nv12",cmd_ctx.frame_vaddr,cmd_ctx.frame_width,cmd_ctx.frame_height);
cmd_ctx.frame_format = DRM_FORMAT_NV12; // nv12 = yuv420sp
//read_yuv("frame601.yuv",cmd_ctx.frame_vaddr,cmd_ctx.frame_width,cmd_ctx.frame_height);
//cmd_ctx.frame_format = DRM_FORMAT_YVU420; // yuv420p
struct dma_fd_list frame_dma_node;
frame_dma_node.width = (cmd_ctx.frame_width + 15) & (~15);
frame_dma_node.height = (cmd_ctx.frame_height + 15) & (~15);
frame_dma_node.dma_fd = cmd_ctx.frame_fd;
frame_dma_node.format = cmd_ctx.frame_format;
frame_dma_node.vaddr = cmd_ctx.frame_vaddr;
DEMO_DBG("width %d height %d fd %d vaddr %p", frame_dma_node.width, frame_dma_node.height, frame_dma_node.dma_fd, frame_dma_node.vaddr);
display_one_frame(&cmd_ctx, &frame_dma_node);
getchar();
*/
init_ffmpeg_context(&cmd_ctx, cmd_ctx.vpu_mem_pool);
cmd_ctx.frame = avcodec_alloc_frame();
if (cmd_ctx.frame == NULL) {
printf("avcodec_alloc_frame failed\n");
exit(-1);
}
struct timeval start_time, end_time;
do {
int get_frame;
gettimeofday(&start_time, NULL);
while ((get_frame = decode_one_frame(&cmd_ctx)) != -1) {
if (get_frame == 1) {
struct dma_fd_list dma_node;
dma_node.width = (cmd_ctx.codec_ctx->width + 15) & (~15);
dma_node.height = (cmd_ctx.codec_ctx->height + 15) & (~15);
dma_node.virtual_width = (cmd_ctx.codec_ctx->width);
dma_node.virtual_height = (cmd_ctx.codec_ctx->height);
dma_node.dma_fd = cmd_ctx.frame->data[3];
#if 0
int size = dma_node.width * dma_node.height * 4;
dma_node.vaddr = mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, dma_node.dma_fd, 0);
if (dma_node.vaddr == MAP_FAILED) {
DEMO_ERROR("drm map failed, dma_fd=%d\n",dma_node.dma_fd);
}
#endif
dma_node.format = DRM_FORMAT_NV12; // yuv420p
if (cmd_ctx.record_frame) {
//save_frame(cmd_ctx.frame, dma_node.width, dma_node.height, count);
count++;
}
if (cmd_ctx.displayed) {
display_one_frame(&cmd_ctx, &dma_node);
}
}
}
} while (0);
deinit_ffmpeg_context(&cmd_ctx);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment