blob: 72a8164ce65c37869afb16e19f9e9e775778af0b [file] [log] [blame]
Gurchetan Singh73c141e2021-01-21 14:51:19 -08001/*
2 * Copyright 2021 The Chromium OS Authors. All rights reserved.
3 * Use of this source code is governed by a BSD-style license that can be
4 * found in the LICENSE file.
5 */
6
7#include <errno.h>
8#include <string.h>
9#include <sys/mman.h>
10#include <xf86drm.h>
11
Yiwei Zhangb7a64442021-09-30 05:13:10 +000012#include "drv_helpers.h"
Gurchetan Singh73c141e2021-01-21 14:51:19 -080013#include "drv_priv.h"
14#include "external/virtgpu_cross_domain_protocol.h"
15#include "external/virtgpu_drm.h"
Gurchetan Singh73c141e2021-01-21 14:51:19 -080016#include "util.h"
17#include "virtgpu.h"
18
19#define CAPSET_CROSS_DOMAIN 5
20#define CAPSET_CROSS_FAKE 30
21
22static const uint32_t scanout_render_formats[] = { DRM_FORMAT_ABGR2101010, DRM_FORMAT_ABGR8888,
23 DRM_FORMAT_ARGB2101010, DRM_FORMAT_ARGB8888,
24 DRM_FORMAT_RGB565, DRM_FORMAT_XBGR2101010,
25 DRM_FORMAT_XBGR8888, DRM_FORMAT_XRGB2101010,
26 DRM_FORMAT_XRGB8888 };
27
28static const uint32_t render_formats[] = { DRM_FORMAT_ABGR16161616F };
29
30static const uint32_t texture_only_formats[] = { DRM_FORMAT_R8, DRM_FORMAT_NV12, DRM_FORMAT_P010,
31 DRM_FORMAT_YVU420, DRM_FORMAT_YVU420_ANDROID };
32
33extern struct virtgpu_param params[];
34
35struct cross_domain_private {
36 uint32_t ring_handle;
37 void *ring_addr;
38 struct drv_array *metadata_cache;
Yiwei Zhange12d3ae2021-09-27 19:58:56 +000039 pthread_mutex_t metadata_cache_lock;
Gurchetan Singh73c141e2021-01-21 14:51:19 -080040};
41
42static void cross_domain_release_private(struct driver *drv)
43{
44 int ret;
45 struct cross_domain_private *priv = drv->priv;
46 struct drm_gem_close gem_close = { 0 };
47
48 if (priv->ring_addr != MAP_FAILED)
49 munmap(priv->ring_addr, PAGE_SIZE);
50
51 if (priv->ring_handle) {
52 gem_close.handle = priv->ring_handle;
53
54 ret = drmIoctl(drv->fd, DRM_IOCTL_GEM_CLOSE, &gem_close);
55 if (ret) {
Yiwei Zhang04954732022-07-13 23:34:33 +000056 drv_loge("DRM_IOCTL_GEM_CLOSE failed (handle=%x) error %d\n",
57 priv->ring_handle, ret);
Gurchetan Singh73c141e2021-01-21 14:51:19 -080058 }
59 }
60
Yiwei Zhange12d3ae2021-09-27 19:58:56 +000061 if (priv->metadata_cache)
62 drv_array_destroy(priv->metadata_cache);
63
64 pthread_mutex_destroy(&priv->metadata_cache_lock);
65
Gurchetan Singh73c141e2021-01-21 14:51:19 -080066 free(priv);
67}
68
69static void add_combinations(struct driver *drv)
70{
71 struct format_metadata metadata;
72
73 // Linear metadata always supported.
74 metadata.tiling = 0;
75 metadata.priority = 1;
76 metadata.modifier = DRM_FORMAT_MOD_LINEAR;
77
78 drv_add_combinations(drv, scanout_render_formats, ARRAY_SIZE(scanout_render_formats),
79 &metadata, BO_USE_RENDER_MASK | BO_USE_SCANOUT);
80
81 drv_add_combinations(drv, render_formats, ARRAY_SIZE(render_formats), &metadata,
82 BO_USE_RENDER_MASK);
83
84 drv_add_combinations(drv, texture_only_formats, ARRAY_SIZE(texture_only_formats), &metadata,
85 BO_USE_TEXTURE_MASK);
86
87 /* Android CTS tests require this. */
88 drv_add_combination(drv, DRM_FORMAT_BGR888, &metadata, BO_USE_SW_MASK);
89
90 drv_modify_combination(drv, DRM_FORMAT_YVU420, &metadata, BO_USE_HW_VIDEO_ENCODER);
91 drv_modify_combination(drv, DRM_FORMAT_NV12, &metadata,
Yiwei Zhang7648f062022-07-13 23:15:22 +000092 BO_USE_CAMERA_READ | BO_USE_CAMERA_WRITE | BO_USE_HW_VIDEO_DECODER |
93 BO_USE_SCANOUT | BO_USE_HW_VIDEO_ENCODER);
Gurchetan Singh73c141e2021-01-21 14:51:19 -080094
95 /*
96 * R8 format is used for Android's HAL_PIXEL_FORMAT_BLOB and is used for JPEG snapshots
Jason Macnakd0cce892022-07-19 14:48:39 -070097 * from camera, input/output from hardware decoder/encoder and sensors, and
Chia-I Wu52be91e2022-07-11 13:41:44 -070098 * AHBs used as SSBOs/UBOs.
Gurchetan Singh73c141e2021-01-21 14:51:19 -080099 */
100 drv_modify_combination(drv, DRM_FORMAT_R8, &metadata,
Yiwei Zhang7648f062022-07-13 23:15:22 +0000101 BO_USE_CAMERA_READ | BO_USE_CAMERA_WRITE | BO_USE_HW_VIDEO_DECODER |
Jason Macnakd0cce892022-07-19 14:48:39 -0700102 BO_USE_HW_VIDEO_ENCODER | BO_USE_SENSOR_DIRECT_DATA |
103 BO_USE_GPU_DATA_BUFFER);
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800104
105 drv_modify_linear_combinations(drv);
106}
107
108static int cross_domain_submit_cmd(struct driver *drv, uint32_t *cmd, uint32_t cmd_size, bool wait)
109{
110 int ret;
111 struct drm_virtgpu_3d_wait wait_3d = { 0 };
112 struct drm_virtgpu_execbuffer exec = { 0 };
113 struct cross_domain_private *priv = drv->priv;
114
115 exec.command = (uint64_t)&cmd[0];
116 exec.size = cmd_size;
117 if (wait) {
Gurchetan Singh4e767d32021-08-25 10:24:50 -0700118 exec.flags = VIRTGPU_EXECBUF_RING_IDX;
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800119 exec.bo_handles = (uint64_t)&priv->ring_handle;
120 exec.num_bo_handles = 1;
121 }
122
123 ret = drmIoctl(drv->fd, DRM_IOCTL_VIRTGPU_EXECBUFFER, &exec);
124 if (ret < 0) {
Yiwei Zhang04954732022-07-13 23:34:33 +0000125 drv_loge("DRM_IOCTL_VIRTGPU_EXECBUFFER failed with %s\n", strerror(errno));
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800126 return -EINVAL;
127 }
128
129 ret = -EAGAIN;
130 while (ret == -EAGAIN) {
131 wait_3d.handle = priv->ring_handle;
132 ret = drmIoctl(drv->fd, DRM_IOCTL_VIRTGPU_WAIT, &wait_3d);
133 }
134
135 if (ret < 0) {
Yiwei Zhang04954732022-07-13 23:34:33 +0000136 drv_loge("DRM_IOCTL_VIRTGPU_WAIT failed with %s\n", strerror(errno));
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800137 return ret;
138 }
139
140 return 0;
141}
142
143static bool metadata_equal(struct bo_metadata *current, struct bo_metadata *cached)
144{
145 if ((current->width == cached->width) && (current->height == cached->height) &&
146 (current->format == cached->format) && (current->use_flags == cached->use_flags))
147 return true;
148 return false;
149}
150
151static int cross_domain_metadata_query(struct driver *drv, struct bo_metadata *metadata)
152{
153 int ret = 0;
154 struct bo_metadata *cached_data = NULL;
155 struct cross_domain_private *priv = drv->priv;
156 struct CrossDomainGetImageRequirements cmd_get_reqs;
157 uint32_t *addr = (uint32_t *)priv->ring_addr;
158 uint32_t plane, remaining_size;
159
160 memset(&cmd_get_reqs, 0, sizeof(cmd_get_reqs));
Yiwei Zhange12d3ae2021-09-27 19:58:56 +0000161 pthread_mutex_lock(&priv->metadata_cache_lock);
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800162 for (uint32_t i = 0; i < drv_array_size(priv->metadata_cache); i++) {
163 cached_data = (struct bo_metadata *)drv_array_at_idx(priv->metadata_cache, i);
164 if (!metadata_equal(metadata, cached_data))
165 continue;
166
167 memcpy(metadata, cached_data, sizeof(*cached_data));
168 goto out_unlock;
169 }
170
171 cmd_get_reqs.hdr.cmd = CROSS_DOMAIN_CMD_GET_IMAGE_REQUIREMENTS;
172 cmd_get_reqs.hdr.cmd_size = sizeof(struct CrossDomainGetImageRequirements);
173
174 cmd_get_reqs.width = metadata->width;
175 cmd_get_reqs.height = metadata->height;
176 cmd_get_reqs.drm_format =
177 (metadata->format == DRM_FORMAT_YVU420_ANDROID) ? DRM_FORMAT_YVU420 : metadata->format;
178 cmd_get_reqs.flags = metadata->use_flags;
179
180 /*
181 * It is possible to avoid blocking other bo_create() calls by unlocking before
182 * cross_domain_submit_cmd() and re-locking afterwards. However, that would require
183 * another scan of the metadata cache before drv_array_append in case two bo_create() calls
184 * do the same metadata query. Until cross_domain functionality is more widely tested,
185 * leave this optimization out for now.
186 */
187 ret = cross_domain_submit_cmd(drv, (uint32_t *)&cmd_get_reqs, cmd_get_reqs.hdr.cmd_size,
188 true);
189 if (ret < 0)
190 goto out_unlock;
191
192 memcpy(&metadata->strides, &addr[0], 4 * sizeof(uint32_t));
193 memcpy(&metadata->offsets, &addr[4], 4 * sizeof(uint32_t));
194 memcpy(&metadata->format_modifier, &addr[8], sizeof(uint64_t));
195 memcpy(&metadata->total_size, &addr[10], sizeof(uint64_t));
Gurchetan Singh4e767d32021-08-25 10:24:50 -0700196 memcpy(&metadata->blob_id, &addr[12], sizeof(uint32_t));
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800197
Gurchetan Singh4e767d32021-08-25 10:24:50 -0700198 metadata->map_info = addr[13];
199 metadata->memory_idx = addr[14];
200 metadata->physical_device_idx = addr[15];
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800201
Rob Clark8513e1f2022-07-18 14:34:20 -0700202 /* Detect buffers, which have no particular stride alignment requirement: */
203 if ((metadata->height == 1) && (metadata->format == DRM_FORMAT_R8)) {
204 metadata->strides[0] = metadata->width;
205 }
206
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800207 remaining_size = metadata->total_size;
208 for (plane = 0; plane < metadata->num_planes; plane++) {
209 if (plane != 0) {
210 metadata->sizes[plane - 1] = metadata->offsets[plane];
211 remaining_size -= metadata->offsets[plane];
212 }
213 }
214
215 metadata->sizes[plane - 1] = remaining_size;
216 drv_array_append(priv->metadata_cache, metadata);
217
218out_unlock:
Yiwei Zhange12d3ae2021-09-27 19:58:56 +0000219 pthread_mutex_unlock(&priv->metadata_cache_lock);
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800220 return ret;
221}
222
Rob Clarkbb62d422022-07-20 11:27:23 -0700223/* Fill out metadata for guest buffers, used only for CPU access: */
224void cross_domain_get_emulated_metadata(struct bo_metadata *metadata)
225{
226 uint32_t offset = 0;
227
228 for (size_t i = 0; i < metadata->num_planes; i++) {
Yiwei Zhangccfca972022-08-10 18:22:34 +0000229 metadata->strides[i] = drv_stride_from_format(metadata->format, metadata->width, i);
230 metadata->sizes[i] = drv_size_from_format(metadata->format, metadata->strides[i],
231 metadata->height, i);
Rob Clarkbb62d422022-07-20 11:27:23 -0700232 metadata->offsets[i] = offset;
233 offset += metadata->sizes[i];
234 }
235
236 metadata->total_size = offset;
237}
238
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800239static int cross_domain_init(struct driver *drv)
240{
241 int ret;
242 struct cross_domain_private *priv;
243 struct drm_virtgpu_map map = { 0 };
244 struct drm_virtgpu_get_caps args = { 0 };
245 struct drm_virtgpu_context_init init = { 0 };
246 struct drm_virtgpu_resource_create_blob drm_rc_blob = { 0 };
247 struct drm_virtgpu_context_set_param ctx_set_params[2] = { { 0 } };
248
249 struct CrossDomainInit cmd_init;
250 struct CrossDomainCapabilities cross_domain_caps;
251
252 memset(&cmd_init, 0, sizeof(cmd_init));
253 if (!params[param_context_init].value)
254 return -ENOTSUP;
255
256 if ((params[param_supported_capset_ids].value & (1 << CAPSET_CROSS_DOMAIN)) == 0)
257 return -ENOTSUP;
258
Gurchetan Singhb2917b22021-04-28 16:24:49 -0700259 if (!params[param_resource_blob].value)
260 return -ENOTSUP;
261
262 /// Need zero copy memory
263 if (!params[param_host_visible].value && !params[param_create_guest_handle].value)
264 return -ENOTSUP;
265
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800266 priv = calloc(1, sizeof(*priv));
Yiwei Zhangafdf87d2021-09-28 04:06:06 +0000267 if (!priv)
268 return -ENOMEM;
269
Yiwei Zhange12d3ae2021-09-27 19:58:56 +0000270 ret = pthread_mutex_init(&priv->metadata_cache_lock, NULL);
Jason Macnakaf840f02021-10-04 16:07:48 -0700271 if (ret) {
Yiwei Zhange12d3ae2021-09-27 19:58:56 +0000272 free(priv);
273 return ret;
274 }
275
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800276 priv->metadata_cache = drv_array_init(sizeof(struct bo_metadata));
Yiwei Zhangafdf87d2021-09-28 04:06:06 +0000277 if (!priv->metadata_cache) {
278 ret = -ENOMEM;
279 goto free_private;
280 }
281
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800282 priv->ring_addr = MAP_FAILED;
283 drv->priv = priv;
284
285 args.cap_set_id = CAPSET_CROSS_DOMAIN;
286 args.size = sizeof(struct CrossDomainCapabilities);
287 args.addr = (unsigned long long)&cross_domain_caps;
288
289 ret = drmIoctl(drv->fd, DRM_IOCTL_VIRTGPU_GET_CAPS, &args);
290 if (ret) {
Yiwei Zhang04954732022-07-13 23:34:33 +0000291 drv_loge("DRM_IOCTL_VIRTGPU_GET_CAPS failed with %s\n", strerror(errno));
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800292 goto free_private;
293 }
294
295 // When 3D features are avilable, but the host does not support external memory, fall back
296 // to the virgl minigbm backend. This typically means the guest side minigbm resource will
297 // be backed by a host OpenGL texture.
298 if (!cross_domain_caps.supports_external_gpu_memory && params[param_3d].value) {
299 ret = -ENOTSUP;
300 goto free_private;
301 }
302
303 // Intialize the cross domain context. Create one fence context to wait for metadata
304 // queries.
305 ctx_set_params[0].param = VIRTGPU_CONTEXT_PARAM_CAPSET_ID;
306 ctx_set_params[0].value = CAPSET_CROSS_DOMAIN;
Gurchetan Singh4e767d32021-08-25 10:24:50 -0700307 ctx_set_params[1].param = VIRTGPU_CONTEXT_PARAM_NUM_RINGS;
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800308 ctx_set_params[1].value = 1;
309
310 init.ctx_set_params = (unsigned long long)&ctx_set_params[0];
311 init.num_params = 2;
312 ret = drmIoctl(drv->fd, DRM_IOCTL_VIRTGPU_CONTEXT_INIT, &init);
313 if (ret) {
Yiwei Zhang04954732022-07-13 23:34:33 +0000314 drv_loge("DRM_IOCTL_VIRTGPU_CONTEXT_INIT failed with %s\n", strerror(errno));
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800315 goto free_private;
316 }
317
318 // Create a shared ring buffer to read metadata queries.
319 drm_rc_blob.size = PAGE_SIZE;
320 drm_rc_blob.blob_mem = VIRTGPU_BLOB_MEM_GUEST;
321 drm_rc_blob.blob_flags = VIRTGPU_BLOB_FLAG_USE_MAPPABLE;
322
323 ret = drmIoctl(drv->fd, DRM_IOCTL_VIRTGPU_RESOURCE_CREATE_BLOB, &drm_rc_blob);
324 if (ret < 0) {
Yiwei Zhang04954732022-07-13 23:34:33 +0000325 drv_loge("DRM_VIRTGPU_RESOURCE_CREATE_BLOB failed with %s\n", strerror(errno));
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800326 goto free_private;
327 }
328
329 priv->ring_handle = drm_rc_blob.bo_handle;
330
331 // Map shared ring buffer.
332 map.handle = priv->ring_handle;
333 ret = drmIoctl(drv->fd, DRM_IOCTL_VIRTGPU_MAP, &map);
334 if (ret < 0) {
Yiwei Zhang04954732022-07-13 23:34:33 +0000335 drv_loge("DRM_IOCTL_VIRTGPU_MAP failed with %s\n", strerror(errno));
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800336 goto free_private;
337 }
338
339 priv->ring_addr =
340 mmap(0, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, drv->fd, map.offset);
341
342 if (priv->ring_addr == MAP_FAILED) {
Yiwei Zhang04954732022-07-13 23:34:33 +0000343 drv_loge("mmap failed with %s\n", strerror(errno));
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800344 goto free_private;
345 }
346
347 // Notify host about ring buffer
348 cmd_init.hdr.cmd = CROSS_DOMAIN_CMD_INIT;
349 cmd_init.hdr.cmd_size = sizeof(struct CrossDomainInit);
350 cmd_init.ring_id = drm_rc_blob.res_handle;
351 ret = cross_domain_submit_cmd(drv, (uint32_t *)&cmd_init, cmd_init.hdr.cmd_size, false);
352 if (ret < 0)
353 goto free_private;
354
355 // minigbm bookkeeping
356 add_combinations(drv);
357 return 0;
358
359free_private:
360 cross_domain_release_private(drv);
361 return ret;
362}
363
364static void cross_domain_close(struct driver *drv)
365{
366 cross_domain_release_private(drv);
367}
368
369static int cross_domain_bo_create(struct bo *bo, uint32_t width, uint32_t height, uint32_t format,
370 uint64_t use_flags)
371{
372 int ret;
373 uint32_t blob_flags = VIRTGPU_BLOB_FLAG_USE_SHAREABLE;
374 struct drm_virtgpu_resource_create_blob drm_rc_blob = { 0 };
375
Yiwei Zhang8f16db92022-09-14 20:52:26 +0000376 if (use_flags & (BO_USE_SW_MASK | BO_USE_GPU_DATA_BUFFER))
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800377 blob_flags |= VIRTGPU_BLOB_FLAG_USE_MAPPABLE;
378
Rob Clarkbb62d422022-07-20 11:27:23 -0700379 if (!(use_flags & BO_USE_HW_MASK)) {
380 cross_domain_get_emulated_metadata(&bo->meta);
Gurchetan Singhb2917b22021-04-28 16:24:49 -0700381 drm_rc_blob.blob_mem = VIRTGPU_BLOB_MEM_GUEST;
Rob Clarkbb62d422022-07-20 11:27:23 -0700382 } else {
383 ret = cross_domain_metadata_query(bo->drv, &bo->meta);
384 if (ret < 0) {
385 drv_loge("Metadata query failed");
386 return ret;
387 }
388
389 if (params[param_cross_device].value)
390 blob_flags |= VIRTGPU_BLOB_FLAG_USE_CROSS_DEVICE;
391
392 /// It may be possible to have host3d blobs and handles from guest memory at the
393 /// same time. But for the immediate use cases, we will either have one or the
394 /// other. For now, just prefer guest memory since adding that feature is more
395 /// involved (requires --udmabuf flag to crosvm), so developers would likely test
396 /// that.
397 if (params[param_create_guest_handle].value) {
398 drm_rc_blob.blob_mem = VIRTGPU_BLOB_MEM_GUEST;
399 blob_flags |= VIRTGPU_BLOB_FLAG_CREATE_GUEST_HANDLE;
400 } else if (params[param_host_visible].value) {
401 drm_rc_blob.blob_mem = VIRTGPU_BLOB_MEM_HOST3D;
402 }
403 drm_rc_blob.blob_id = (uint64_t)bo->meta.blob_id;
Gurchetan Singhb2917b22021-04-28 16:24:49 -0700404 }
405
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800406 drm_rc_blob.size = bo->meta.total_size;
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800407 drm_rc_blob.blob_flags = blob_flags;
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800408
409 ret = drmIoctl(bo->drv->fd, DRM_IOCTL_VIRTGPU_RESOURCE_CREATE_BLOB, &drm_rc_blob);
410 if (ret < 0) {
Yiwei Zhang04954732022-07-13 23:34:33 +0000411 drv_loge("DRM_VIRTGPU_RESOURCE_CREATE_BLOB failed with %s\n", strerror(errno));
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800412 return -errno;
413 }
414
415 for (uint32_t plane = 0; plane < bo->meta.num_planes; plane++)
416 bo->handles[plane].u32 = drm_rc_blob.bo_handle;
417
418 return 0;
419}
420
421static void *cross_domain_bo_map(struct bo *bo, struct vma *vma, size_t plane, uint32_t map_flags)
422{
423 int ret;
424 struct drm_virtgpu_map gem_map = { 0 };
425
426 gem_map.handle = bo->handles[0].u32;
427 ret = drmIoctl(bo->drv->fd, DRM_IOCTL_VIRTGPU_MAP, &gem_map);
428 if (ret) {
Yiwei Zhang04954732022-07-13 23:34:33 +0000429 drv_loge("DRM_IOCTL_VIRTGPU_MAP failed with %s\n", strerror(errno));
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800430 return MAP_FAILED;
431 }
432
433 vma->length = bo->meta.total_size;
434 return mmap(0, bo->meta.total_size, drv_get_prot(map_flags), MAP_SHARED, bo->drv->fd,
435 gem_map.offset);
436}
437
438const struct backend virtgpu_cross_domain = {
439 .name = "virtgpu_cross_domain",
440 .init = cross_domain_init,
441 .close = cross_domain_close,
442 .bo_create = cross_domain_bo_create,
443 .bo_import = drv_prime_bo_import,
444 .bo_destroy = drv_gem_bo_destroy,
445 .bo_map = cross_domain_bo_map,
446 .bo_unmap = drv_bo_munmap,
Yiwei Zhangb8ad7b82021-10-01 17:55:14 +0000447 .resolve_format_and_use_flags = drv_resolve_format_and_use_flags_helper,
Gurchetan Singh73c141e2021-01-21 14:51:19 -0800448};