/************************************************************************** * * Copyright (C) 2022 Kylin Software Co., Ltd. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * **************************************************************************/ /** * @file * The video implementation of the vrend renderer. * * It is based on the general virgl video submodule and handles data transfer * and synchronization between host and guest. * * The relationship between vaSurface and video buffer objects: * * GUEST (Mesa) | HOST (Virglrenderer) * | * +------------+ | +------------+ * | vaSurface | | | vaSurface | <------+ * +------------+ | +------------+ | * | | | * +---------------------------+ | +-------------------------+ | * | virgl_video_buffer | | | vrend_video_buffer | | * | +-----------------------+ | | | +-------------------+ | | * | | vl_video_buffer | | | | | vrend_resource(s) | | | * | | +-------------------+ | |<--+-->| +-------------------+ | | * | | | virgl_resource(s) | | | | | +--------------------+ | | * | | +-------------------+ | | | | | virgl_video_buffer |-+--+ * | +-----------------------+ | | | +--------------------+ | * +---------------------------+ | +-------------------------+ * * The relationship between vaContext and video codec objects: * * GUEST (Mesa) | HOST (Virglrenderer) * | * +------------+ | +------------+ * | vaContext | | | vaContext | <-------+ * +------------+ | +------------+ | * | | | * +------------------------+ | +--------------------------+ | * | virgl_video_codec | <--+--> | vrend_video_codec | | * +------------------------+ | | +--------------------+ | | * | | | virgl_video_codec | -+--+ * | | +--------------------+ | * | +--------------------------+ * * @author Feng Jiang */ #include #include "virgl_video.h" #include "virgl_video_hw.h" #include "vrend_debug.h" #include "vrend_winsys.h" #include "vrend_renderer.h" #include "vrend_video.h" struct vrend_context; struct vrend_video_context { struct vrend_context *ctx; struct list_head codecs; struct list_head buffers; }; struct vrend_video_codec { struct virgl_video_codec *codec; uint32_t handle; struct vrend_resource *feed_res; /* encoding feedback */ struct vrend_resource *dest_res; /* encoding coded buffer */ struct vrend_video_context *ctx; struct list_head head; }; struct vrend_video_plane { uint32_t res_handle; GLuint texture; /* texture for temporary use */ GLuint framebuffer; /* framebuffer for temporary use */ EGLImageKHR egl_image; /* egl image for temporary use */ }; struct vrend_video_buffer { struct virgl_video_buffer *buffer; uint32_t handle; struct vrend_video_context *ctx; struct list_head head; uint32_t num_planes; struct vrend_video_plane planes[3]; }; static struct vrend_video_codec *vrend_video_codec( struct virgl_video_codec *codec) { return virgl_video_codec_opaque_data(codec); } static struct vrend_video_buffer *vrend_video_buffer( struct virgl_video_buffer *buffer) { return virgl_video_buffer_opaque_data(buffer); } static struct vrend_video_codec *get_video_codec( struct vrend_video_context *ctx, uint32_t cdc_handle) { struct vrend_video_codec *cdc; LIST_FOR_EACH_ENTRY(cdc, &ctx->codecs, head) { if (cdc->handle == cdc_handle) return cdc; } return NULL; } static struct vrend_video_buffer *get_video_buffer( struct vrend_video_context *ctx, uint32_t buf_handle) { struct vrend_video_buffer *buf; LIST_FOR_EACH_ENTRY(buf, &ctx->buffers, head) { if (buf->handle == buf_handle) return buf; } return NULL; } static int sync_dmabuf_to_video_buffer(struct vrend_video_buffer *buf, const struct virgl_video_dma_buf *dmabuf) { if (!(dmabuf->flags & VIRGL_VIDEO_DMABUF_READ_ONLY)) { vrend_printf("%s: dmabuf is not readable\n", __func__); return -1; } for (unsigned i = 0; i < dmabuf->num_planes && i < buf->num_planes; i++) { struct vrend_video_plane *plane = &buf->planes[i]; struct vrend_resource *res; res = vrend_renderer_ctx_res_lookup(buf->ctx->ctx, plane->res_handle); if (!res) { vrend_printf("%s: res %d not found\n", __func__, plane->res_handle); continue; } /* dmabuf -> eglimage */ if (EGL_NO_IMAGE_KHR == plane->egl_image) { EGLint img_attrs[16] = { EGL_LINUX_DRM_FOURCC_EXT, dmabuf->planes[i].drm_format, EGL_WIDTH, dmabuf->width / (i + 1), EGL_HEIGHT, dmabuf->height / (i + 1), EGL_DMA_BUF_PLANE0_FD_EXT, dmabuf->planes[i].fd, EGL_DMA_BUF_PLANE0_OFFSET_EXT, dmabuf->planes[i].offset, EGL_DMA_BUF_PLANE0_PITCH_EXT, dmabuf->planes[i].pitch, EGL_NONE }; plane->egl_image = eglCreateImageKHR(eglGetCurrentDisplay(), EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, img_attrs); } if (EGL_NO_IMAGE_KHR == plane->egl_image) { vrend_printf("%s: create egl image failed\n", __func__); continue; } /* eglimage -> texture */ glBindTexture(GL_TEXTURE_2D, plane->texture); glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)(plane->egl_image)); /* texture -> framebuffer */ glBindFramebuffer(GL_READ_FRAMEBUFFER, plane->framebuffer); glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, plane->texture, 0); /* framebuffer -> vrend_video_buffer.planes[i] */ glBindTexture(GL_TEXTURE_2D, res->id); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, res->base.width0, res->base.height0); } glBindTexture(GL_TEXTURE_2D, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0); return 0; } static int sync_video_buffer_to_dmabuf(struct vrend_video_buffer *buf, const struct virgl_video_dma_buf *dmabuf) { if (!(dmabuf->flags & VIRGL_VIDEO_DMABUF_WRITE_ONLY)) { vrend_printf("%s: dmabuf is not writable\n", __func__); return -1; } for (unsigned i = 0; i < dmabuf->num_planes && i < buf->num_planes; i++) { struct vrend_video_plane *plane = &buf->planes[i]; struct vrend_resource *res; res = vrend_renderer_ctx_res_lookup(buf->ctx->ctx, plane->res_handle); if (!res) { vrend_printf("%s: res %d not found\n", __func__, plane->res_handle); continue; } /* dmabuf -> eglimage */ if (EGL_NO_IMAGE_KHR == plane->egl_image) { EGLint img_attrs[16] = { EGL_LINUX_DRM_FOURCC_EXT, dmabuf->planes[i].drm_format, EGL_WIDTH, dmabuf->width / (i + 1), EGL_HEIGHT, dmabuf->height / (i + 1), EGL_DMA_BUF_PLANE0_FD_EXT, dmabuf->planes[i].fd, EGL_DMA_BUF_PLANE0_OFFSET_EXT, dmabuf->planes[i].offset, EGL_DMA_BUF_PLANE0_PITCH_EXT, dmabuf->planes[i].pitch, EGL_NONE }; plane->egl_image = eglCreateImageKHR(eglGetCurrentDisplay(), EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, img_attrs); } if (EGL_NO_IMAGE_KHR == plane->egl_image) { vrend_printf("%s: create egl image failed\n", __func__); continue; } /* eglimage -> texture */ glBindTexture(GL_TEXTURE_2D, plane->texture); glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)(plane->egl_image)); /* vrend_video_buffer.planes[i] -> framebuffer */ glBindFramebuffer(GL_READ_FRAMEBUFFER, plane->framebuffer); glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, res->id, 0); /* framebuffer -> texture */ glBindTexture(GL_TEXTURE_2D, plane->texture); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, res->base.width0, res->base.height0); } glBindTexture(GL_TEXTURE_2D, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0); return 0; } static void vrend_video_decode_completed( struct virgl_video_codec *codec, const struct virgl_video_dma_buf *dmabuf) { struct vrend_video_buffer *buf = vrend_video_buffer(dmabuf->buf); (void)codec; sync_dmabuf_to_video_buffer(buf, dmabuf); } static void vrend_video_enocde_upload_picture( struct virgl_video_codec *codec, const struct virgl_video_dma_buf *dmabuf) { struct vrend_video_buffer *buf = vrend_video_buffer(dmabuf->buf); (void)codec; sync_video_buffer_to_dmabuf(buf, dmabuf); } static void vrend_video_encode_completed( struct virgl_video_codec *codec, const struct virgl_video_dma_buf *src_buf, const struct virgl_video_dma_buf *ref_buf, unsigned num_coded_bufs, const void * const *coded_bufs, const unsigned *coded_sizes) { void *buf; unsigned i, size, data_size; struct virgl_video_encode_feedback feedback; struct vrend_video_codec *cdc = vrend_video_codec(codec); (void)src_buf; (void)ref_buf; if (!cdc->dest_res || !cdc->feed_res) return; memset(&feedback, 0, sizeof(feedback)); /* sync coded data to guest */ if (has_bit(cdc->dest_res->storage_bits, VREND_STORAGE_GL_BUFFER)) { glBindBufferARB(cdc->dest_res->target, cdc->dest_res->id); buf = glMapBufferRange(cdc->dest_res->target, 0, cdc->dest_res->base.width0, GL_MAP_WRITE_BIT); for (i = 0, data_size = 0; i < num_coded_bufs && data_size < cdc->dest_res->base.width0; i++) { size = MIN(cdc->dest_res->base.width0 - data_size, coded_sizes[i]); memcpy((uint8_t *)buf + data_size, coded_bufs[i], size); vrend_write_to_iovec(cdc->dest_res->iov, cdc->dest_res->num_iovs, data_size, coded_bufs[i], size); data_size += size; } glUnmapBuffer(cdc->dest_res->target); glBindBufferARB(cdc->dest_res->target, 0); feedback.stat = VIRGL_VIDEO_ENCODE_STAT_SUCCESS; feedback.coded_size = data_size; } else { vrend_printf("unexcepted coded res type\n"); feedback.stat = VIRGL_VIDEO_ENCODE_STAT_FAILURE; feedback.coded_size = 0; } /* send feedback */ vrend_write_to_iovec(cdc->feed_res->iov, cdc->feed_res->num_iovs, 0, (char *)(&feedback), MIN(cdc->feed_res->base.width0, sizeof(feedback))); cdc->dest_res = NULL; cdc->feed_res = NULL; } static struct virgl_video_callbacks video_callbacks = { .decode_completed = vrend_video_decode_completed, .encode_upload_picture = vrend_video_enocde_upload_picture, .encode_completed = vrend_video_encode_completed, }; int vrend_video_init(int drm_fd) { if (drm_fd < 0) return -1; return virgl_video_init(drm_fd, &video_callbacks, 0); } void vrend_video_fini(void) { virgl_video_destroy(); } int vrend_video_fill_caps(union virgl_caps *caps) { return virgl_video_fill_caps(caps); } int vrend_video_create_codec(struct vrend_video_context *ctx, uint32_t handle, uint32_t profile, uint32_t entrypoint, uint32_t chroma_format, uint32_t level, uint32_t width, uint32_t height, uint32_t max_ref, uint32_t flags) { struct vrend_video_codec *cdc = get_video_codec(ctx, handle); struct virgl_video_create_codec_args args; if (cdc) return 0; if (profile <= PIPE_VIDEO_PROFILE_UNKNOWN || profile >= PIPE_VIDEO_PROFILE_MAX) return -1; if (entrypoint <= PIPE_VIDEO_ENTRYPOINT_UNKNOWN || entrypoint > PIPE_VIDEO_ENTRYPOINT_ENCODE) return -1; if (chroma_format >= PIPE_VIDEO_CHROMA_FORMAT_NONE) return -1; if (!width || !height) return -1; cdc = (struct vrend_video_codec *)calloc(1, sizeof(*cdc)); if (!cdc) return -1; args.profile = profile; args.entrypoint = entrypoint; args.chroma_format = chroma_format; args.level = level; args.width = width; args.height = height; args.max_references = max_ref; args.flags = flags; args.opaque = cdc; cdc->codec = virgl_video_create_codec(&args); if (!cdc->codec) { free(cdc); return -1; } cdc->handle = handle; cdc->ctx = ctx; list_add(&cdc->head, &ctx->codecs); return 0; } static void destroy_video_codec(struct vrend_video_codec *cdc) { if (cdc) { list_del(&cdc->head); virgl_video_destroy_codec(cdc->codec); free(cdc); } } void vrend_video_destroy_codec(struct vrend_video_context *ctx, uint32_t handle) { struct vrend_video_codec *cdc = get_video_codec(ctx, handle); destroy_video_codec(cdc); } int vrend_video_create_buffer(struct vrend_video_context *ctx, uint32_t handle, uint32_t format, uint32_t width, uint32_t height, uint32_t *res_handles, unsigned int num_res) { unsigned i; struct vrend_video_plane *plane; struct vrend_video_buffer *buf = get_video_buffer(ctx, handle); struct virgl_video_create_buffer_args args; if (buf) return 0; if (format <= PIPE_FORMAT_NONE || format >= PIPE_FORMAT_COUNT) return -1; if (!width || !height || !res_handles || !num_res) return -1; buf = (struct vrend_video_buffer *)calloc(1, sizeof(*buf)); if (!buf) return -1; args.format = format; args.width = width; args.height = height; args.interlaced = 0; args.opaque = buf; buf->buffer = virgl_video_create_buffer(&args); if (!buf->buffer) { free(buf); return -1; } for (i = 0; i < ARRAY_SIZE(buf->planes); i++) buf->planes[i].egl_image = EGL_NO_IMAGE_KHR; for (i = 0, buf->num_planes = 0; i < num_res && buf->num_planes < ARRAY_SIZE(buf->planes); i++) { if (!res_handles[i]) continue; plane = &buf->planes[buf->num_planes++]; plane->res_handle = res_handles[i]; glGenFramebuffers(1, &plane->framebuffer); glGenTextures(1, &plane->texture); glBindTexture(GL_TEXTURE_2D, plane->texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glBindTexture(GL_TEXTURE_2D, 0); } buf->handle = handle; buf->ctx = ctx; list_add(&buf->head, &ctx->buffers); return 0; } static void destroy_video_buffer(struct vrend_video_buffer *buf) { unsigned i; struct vrend_video_plane *plane; if (!buf) return; list_del(&buf->head); for (i = 0; i < buf->num_planes; i++) { plane = &buf->planes[i]; glDeleteTextures(1, &plane->texture); glDeleteFramebuffers(1, &plane->framebuffer); if (plane->egl_image == EGL_NO_IMAGE_KHR) eglDestroyImageKHR(eglGetCurrentDisplay(), plane->egl_image); } virgl_video_destroy_buffer(buf->buffer); free(buf); } void vrend_video_destroy_buffer(struct vrend_video_context *ctx, uint32_t handle) { struct vrend_video_buffer *buf = get_video_buffer(ctx, handle); destroy_video_buffer(buf); } struct vrend_video_context *vrend_video_create_context(struct vrend_context *ctx) { struct vrend_video_context *vctx; vctx = (struct vrend_video_context *)calloc(1, sizeof(*vctx)); if (vctx) { vctx->ctx = ctx; list_inithead(&vctx->codecs); list_inithead(&vctx->buffers); } return vctx; } void vrend_video_destroy_context(struct vrend_video_context *ctx) { struct vrend_video_codec *vcdc, *vcdc_tmp; struct vrend_video_buffer *vbuf, *vbuf_tmp; LIST_FOR_EACH_ENTRY_SAFE(vcdc, vcdc_tmp, &ctx->codecs, head) destroy_video_codec(vcdc); LIST_FOR_EACH_ENTRY_SAFE(vbuf, vbuf_tmp, &ctx->buffers, head) destroy_video_buffer(vbuf); free(ctx); } int vrend_video_begin_frame(struct vrend_video_context *ctx, uint32_t cdc_handle, uint32_t tgt_handle) { struct vrend_video_codec *cdc = get_video_codec(ctx, cdc_handle); struct vrend_video_buffer *tgt = get_video_buffer(ctx, tgt_handle); if (!cdc || !tgt) return -1; return virgl_video_begin_frame(cdc->codec, tgt->buffer); } static void modify_h264_picture_desc(struct vrend_video_codec *cdc, struct vrend_video_buffer *tgt, struct virgl_h264_picture_desc *desc) { unsigned i; struct vrend_video_buffer *vbuf; (void)tgt; for (i = 0; i < ARRAY_SIZE(desc->buffer_id); i++) { vbuf = get_video_buffer(cdc->ctx, desc->buffer_id[i]); desc->buffer_id[i] = virgl_video_buffer_id(vbuf ? vbuf->buffer : NULL); } } static void modify_h265_picture_desc(struct vrend_video_codec *cdc, struct vrend_video_buffer *tgt, struct virgl_h265_picture_desc *desc) { unsigned i; struct vrend_video_buffer *vbuf; (void)tgt; for (i = 0; i < ARRAY_SIZE(desc->ref); i++) { vbuf = get_video_buffer(cdc->ctx, desc->ref[i]); desc->ref[i] = virgl_video_buffer_id(vbuf ? vbuf->buffer : NULL); } } static void modify_picture_desc(struct vrend_video_codec *cdc, struct vrend_video_buffer *tgt, union virgl_picture_desc *desc) { switch(virgl_video_codec_profile(cdc->codec)) { case PIPE_VIDEO_PROFILE_MPEG4_AVC_BASELINE: case PIPE_VIDEO_PROFILE_MPEG4_AVC_CONSTRAINED_BASELINE: case PIPE_VIDEO_PROFILE_MPEG4_AVC_MAIN: case PIPE_VIDEO_PROFILE_MPEG4_AVC_EXTENDED: case PIPE_VIDEO_PROFILE_MPEG4_AVC_HIGH: case PIPE_VIDEO_PROFILE_MPEG4_AVC_HIGH10: case PIPE_VIDEO_PROFILE_MPEG4_AVC_HIGH422: case PIPE_VIDEO_PROFILE_MPEG4_AVC_HIGH444: modify_h264_picture_desc(cdc, tgt, &desc->h264); break; case PIPE_VIDEO_PROFILE_HEVC_MAIN: case PIPE_VIDEO_PROFILE_HEVC_MAIN_10: case PIPE_VIDEO_PROFILE_HEVC_MAIN_STILL: case PIPE_VIDEO_PROFILE_HEVC_MAIN_12: case PIPE_VIDEO_PROFILE_HEVC_MAIN_444: modify_h265_picture_desc(cdc, tgt, &desc->h265); break; default: break; } } int vrend_video_decode_bitstream(struct vrend_video_context *ctx, uint32_t cdc_handle, uint32_t tgt_handle, uint32_t desc_handle, unsigned num_buffers, const uint32_t *buffer_handles, const uint32_t *buffer_sizes) { int err = -1; unsigned i, num_bs, *bs_sizes = NULL; void **bs_buffers = NULL; struct vrend_resource *res; struct vrend_video_codec *cdc = get_video_codec(ctx, cdc_handle); struct vrend_video_buffer *tgt = get_video_buffer(ctx, tgt_handle); union virgl_picture_desc desc; if (!cdc || !tgt) return -1; bs_buffers = calloc(num_buffers, sizeof(void *)); if (!bs_buffers) { vrend_printf("%s: alloc bs_buffers failed\n", __func__); return -1; } bs_sizes = calloc(num_buffers, sizeof(unsigned)); if (!bs_sizes) { vrend_printf("%s: alloc bs_sizes failed\n", __func__); goto err; } for (i = 0, num_bs = 0; i < num_buffers; i++) { res = vrend_renderer_ctx_res_lookup(ctx->ctx, buffer_handles[i]); if (!res || !res->ptr) { vrend_printf("%s: bs res %d invalid or not found", __func__, buffer_handles[i]); continue; } vrend_read_from_iovec(res->iov, res->num_iovs, 0, res->ptr, buffer_sizes[i]); bs_buffers[num_bs] = res->ptr; bs_sizes[num_bs] = buffer_sizes[i]; num_bs++; } res = vrend_renderer_ctx_res_lookup(ctx->ctx, desc_handle); if (!res) { vrend_printf("%s: desc res %d not found\n", __func__, desc_handle); goto err; } memset(&desc, 0, sizeof(desc)); vrend_read_from_iovec(res->iov, res->num_iovs, 0, (char *)(&desc), MIN(res->base.width0, sizeof(desc))); modify_picture_desc(cdc, tgt, &desc); err = virgl_video_decode_bitstream(cdc->codec, tgt->buffer, &desc, num_bs, (const void * const *)bs_buffers, bs_sizes); err: free(bs_buffers); free(bs_sizes); return err; } int vrend_video_encode_bitstream(struct vrend_video_context *ctx, uint32_t cdc_handle, uint32_t src_handle, uint32_t dest_handle, uint32_t desc_handle, uint32_t feed_handle) { union virgl_picture_desc desc; struct vrend_resource *dest_res, *desc_res, *feed_res; struct vrend_video_codec *cdc = get_video_codec(ctx, cdc_handle); struct vrend_video_buffer *src = get_video_buffer(ctx, src_handle); if (!cdc || !src) return -1; /* Feedback resource */ feed_res = vrend_renderer_ctx_res_lookup(ctx->ctx, feed_handle); if (!feed_res) { vrend_printf("%s: feedback res %d not found\n", __func__, feed_handle); return -1; } /* Picture descriptor resource */ desc_res = vrend_renderer_ctx_res_lookup(ctx->ctx, desc_handle); if (!desc_res) { vrend_printf("%s: desc res %d not found\n", __func__, desc_handle); return -1; } memset(&desc, 0, sizeof(desc)); vrend_read_from_iovec(desc_res->iov, desc_res->num_iovs, 0, (char *)(&desc), MIN(desc_res->base.width0, sizeof(desc))); /* Destination buffer resource. */ dest_res = vrend_renderer_ctx_res_lookup(ctx->ctx, dest_handle); if (!dest_res) { vrend_printf("%s: dest res %d not found\n", __func__, dest_handle); return -1; } cdc->feed_res = feed_res; cdc->dest_res = dest_res; return virgl_video_encode_bitstream(cdc->codec, src->buffer, &desc); } int vrend_video_end_frame(struct vrend_video_context *ctx, uint32_t cdc_handle, uint32_t tgt_handle) { struct vrend_video_codec *cdc = get_video_codec(ctx, cdc_handle); struct vrend_video_buffer *tgt = get_video_buffer(ctx, tgt_handle); if (!cdc || !tgt) return -1; return virgl_video_end_frame(cdc->codec, tgt->buffer); }