diff --git a/config.h.meson b/config.h.meson index dffa8d7..d7a16f7 100644 --- a/config.h.meson +++ b/config.h.meson @@ -34,6 +34,11 @@ #mesondefine ENABLE_MINIGBM_ALLOCATION #mesondefine ENABLE_VENUS #mesondefine ENABLE_VENUS_VALIDATE +#mesondefine ENABLE_RENDER_SERVER +#mesondefine ENABLE_RENDER_SERVER_WORKER_PROCESS +#mesondefine ENABLE_RENDER_SERVER_WORKER_THREAD +#mesondefine ENABLE_RENDER_SERVER_WORKER_MINIJAIL +#mesondefine RENDER_SERVER_EXEC_PATH #mesondefine HAVE_FUNC_ATTRIBUTE_VISIBILITY #mesondefine HAVE_EVENTFD_H #mesondefine HAVE_DLFCN_H diff --git a/meson.build b/meson.build index e452517..5d34f1b 100644 --- a/meson.build +++ b/meson.build @@ -246,6 +246,30 @@ if with_venus endif endif +with_render_server = get_option('render-server') +with_render_server_worker = get_option('render-server-worker') +render_server_install_dir = get_option('prefix') / get_option('libexecdir') +if with_render_server + if not with_venus + error('render server makes no sense without venus currently') + endif + + conf_data.set('ENABLE_RENDER_SERVER', 1) + conf_data.set('RENDER_SERVER_EXEC_PATH', + '"' + render_server_install_dir / 'virgl_render_server' + '"') + + if with_render_server_worker == 'process' + conf_data.set('ENABLE_RENDER_SERVER_WORKER_PROCESS', 1) + elif with_render_server_worker == 'thread' + conf_data.set('ENABLE_RENDER_SERVER_WORKER_THREAD', 1) + elif with_render_server_worker == 'minijail' + conf_data.set('ENABLE_RENDER_SERVER_WORKER_MINIJAIL', 1) + minijail_dep = dependency('libminijail') + else + error('unknown render server worker ' + with_render_server_worker) + endif +endif + if cc.compiles('void __attribute__((hidden)) func() {}') conf_data.set('HAVE_FUNC_ATTRIBUTE_VISIBILITY', 1) endif @@ -280,6 +304,10 @@ with_valgrind = get_option('valgrind') subdir('src') subdir('vtest') +if with_render_server +subdir('server') +endif + if with_tests assert(have_egl, 'Tests require EGL, but it is not available') subdir('tests') @@ -301,6 +329,11 @@ lines += 'minigbm_alloc: ' + (with_minigbm_allocation ? 'yes' : 'no' ) lines += '' lines += 'venus: ' + (with_venus ? 'yes' : 'no' ) lines += '' +lines += 'render server: ' + (with_render_server ? 'yes' : 'no') +if with_render_server +lines += 'render server worker: ' + with_render_server_worker +endif +lines += '' lines += 'tests: ' + (with_tests ? 'yes' : 'no' ) lines += 'fuzzer: ' + (with_fuzzer ? 'yes' : 'no' ) lines += 'tracing: ' + with_tracing diff --git a/meson_options.txt b/meson_options.txt index 52b8df4..d3a99bb 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -52,6 +52,21 @@ option( description : 'enable the validation layer for venus' ) +option( + 'render-server', + type : 'boolean', + value : 'false', + description : 'enable support for render server' +) + +option( + 'render-server-worker', + type : 'combo', + value : 'process', + choices : ['process', 'thread', 'minijail'], + description : 'how a context in render server is serviced' +) + option( 'tests', type : 'boolean', diff --git a/server/main.c b/server/main.c new file mode 100644 index 0000000..556609c --- /dev/null +++ b/server/main.c @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#include "render_context.h" +#include "render_server.h" + +/* The main process is the server process. It enters render_server_main and + * never returns except on fatal errors. + * + * The server process supports only one connection currently. It creates a + * render_client to manage the connection. There is a client process at the + * other end of the connection. When the client process requests a new + * context to be created, the server process creates a worker. It also sets + * up a socket pair, with one end owned by the worker and the other end sent + * to and owned by the client process. + * + * A worker can be a subprocess forked from the server process, or a thread + * created by the server process. When a worker is a subprocess, the + * subprocess returns from render_server_main and enters render_context_main. + * + * When a worker is a thread, the thread enters render_context_main directly + * from its start function. In this case, render_context_main must be + * thread-safe. + */ +int +main(int argc, char **argv) +{ + struct render_context_args ctx_args; + + bool ok = render_server_main(argc, argv, &ctx_args); + + /* this is a subprocess */ + if (ok && ctx_args.valid) + ok = render_context_main(&ctx_args); + + return ok ? 0 : -1; +} diff --git a/server/meson.build b/server/meson.build new file mode 100644 index 0000000..d491496 --- /dev/null +++ b/server/meson.build @@ -0,0 +1,29 @@ +# Copyright 2021 Google LLC +# SPDX-License-Identifier: MIT + +virgl_render_server_sources = [ + 'main.c', + 'render_client.c', + 'render_common.c', + 'render_context.c', + 'render_server.c', + 'render_socket.c', + 'render_virgl.c', + 'render_worker.c', +] + +virgl_render_server_depends = [libvirglrenderer_dep] + +if with_render_server_worker == 'thread' + virgl_render_server_depends += [thread_dep] +elif with_render_server_worker == 'minijail' + virgl_render_server_depends += [minijail_dep] +endif + +virgl_render_server = executable( + 'virgl_render_server', + virgl_render_server_sources, + dependencies : virgl_render_server_depends, + install : true, + install_dir : render_server_install_dir, +) diff --git a/server/render_client.c b/server/render_client.c new file mode 100644 index 0000000..8f993d1 --- /dev/null +++ b/server/render_client.c @@ -0,0 +1,345 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#include "render_client.h" + +#include + +#include "render_context.h" +#include "render_server.h" +#include "render_virgl.h" +#include "render_worker.h" + +/* There is a render_context_record for each worker. + * + * When the client process destroys a context, it closes the connection to the + * worker, which leads to worker termination. It also sends a + * RENDER_CLIENT_OP_DESTROY_CONTEXT to us to remove the record. Because we + * are responsible for cleaning up the worker, we don't care if the worker has + * terminated or not. We always kill, reap, and remove the record. + * + * TODO We reap with WNOHANG in render_client_dispatch currently. We should + * use SIGCHLD instead. + */ +struct render_context_record { + uint32_t ctx_id; + struct render_worker *worker; + + struct list_head head; +}; + +static struct render_context_record * +render_client_find_record(struct render_client *client, uint32_t ctx_id) +{ + list_for_each_entry (struct render_context_record, rec, &client->context_records, + head) { + if (rec->ctx_id == ctx_id) + return rec; + } + return NULL; +} + +static void +render_client_kill_one_record(struct render_client *client, + struct render_context_record *rec) +{ + render_worker_kill(rec->worker); + + list_del(&rec->head); + list_addtail(&rec->head, &client->reap_records); +} + +static void +render_client_kill_all_records(struct render_client *client) +{ + list_for_each_entry (struct render_context_record, rec, &client->context_records, head) + render_worker_kill(rec->worker); + + list_splicetail(&client->context_records, &client->reap_records); + list_inithead(&client->context_records); +} + +static void +render_client_reap_all_records(struct render_client *client, bool wait) +{ + struct render_server *srv = client->server; + + list_for_each_entry_safe (struct render_context_record, rec, &client->reap_records, + head) { + if (!render_worker_reap(rec->worker, wait)) + continue; + + render_worker_destroy(rec->worker); + list_del(&rec->head); + free(rec); + + srv->current_worker_count--; + } +} + +static void +init_context_args(struct render_context_args *ctx_args, + uint32_t init_flags, + const struct render_client_op_create_context_request *req, + int ctx_fd) +{ + *ctx_args = (struct render_context_args){ + .valid = true, + .init_flags = init_flags, + .ctx_id = req->ctx_id, + .ctx_fd = ctx_fd, + }; + + static_assert(sizeof(ctx_args->ctx_name) == sizeof(req->ctx_name), ""); + memcpy(ctx_args->ctx_name, req->ctx_name, sizeof(req->ctx_name) - 1); +} + +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + +static int +render_client_worker_thread(void *thread_data) +{ + const struct render_context_args *ctx_args = thread_data; + return render_context_main(ctx_args) ? 0 : -1; +} + +#endif /* ENABLE_RENDER_SERVER_WORKER_THREAD */ + +static bool +render_client_create_context(struct render_client *client, + const struct render_client_op_create_context_request *req, + int *out_remote_fd) +{ + struct render_server *srv = client->server; + + if (srv->current_worker_count >= srv->max_worker_count) { + render_log("too many context workers"); + return false; + } + + struct render_context_record *rec = calloc(1, sizeof(*rec)); + if (!rec) + return false; + + int socket_fds[2]; + if (!render_socket_pair(socket_fds)) { + free(rec); + return false; + } + int ctx_fd = socket_fds[0]; + int remote_fd = socket_fds[1]; + + struct render_context_args ctx_args; + init_context_args(&ctx_args, client->init_flags, req, ctx_fd); + +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + rec->worker = render_worker_create(srv->worker_jail, render_client_worker_thread, + &ctx_args, sizeof(ctx_args)); + if (rec->worker) + ctx_fd = -1; /* ownership transferred */ +#else + rec->worker = render_worker_create(srv->worker_jail, NULL, NULL, 0); +#endif + if (!rec->worker) { + render_log("failed to create a context worker"); + close(ctx_fd); + close(remote_fd); + free(rec); + return false; + } + + if (!render_worker_is_record(rec->worker)) { + /* this is the child process */ + render_worker_destroy(rec->worker); + free(rec); + + srv->state = RENDER_SERVER_STATE_SUBPROCESS; + *srv->context_args = ctx_args; + + /* ctx_fd ownership transferred */ + assert(srv->context_args->ctx_fd == ctx_fd); + + close(remote_fd); + *out_remote_fd = -1; + + return true; + } + + /* this is the parent process */ + rec->ctx_id = req->ctx_id; + list_addtail(&rec->head, &client->context_records); + srv->current_worker_count++; + + if (ctx_fd >= 0) + close(ctx_fd); + *out_remote_fd = remote_fd; + + return true; +} + +static bool +render_client_dispatch_destroy_context(struct render_client *client, + const union render_client_op_request *req) +{ + const uint32_t ctx_id = req->destroy_context.ctx_id; + struct render_context_record *rec = render_client_find_record(client, ctx_id); + if (rec) + render_client_kill_one_record(client, rec); + + return true; +} + +static bool +render_client_dispatch_create_context(struct render_client *client, + const union render_client_op_request *req) +{ + struct render_server *srv = client->server; + + int remote_fd; + bool ok = render_client_create_context(client, &req->create_context, &remote_fd); + if (!ok) + return false; + + if (srv->state == RENDER_SERVER_STATE_SUBPROCESS) { + assert(remote_fd < 0); + return true; + } + + const struct render_client_op_create_context_reply reply = { + .ok = ok, + }; + if (!ok) + return render_socket_send_reply(&client->socket, &reply, sizeof(reply)); + + ok = render_socket_send_reply_with_fds(&client->socket, &reply, sizeof(reply), + &remote_fd, 1); + close(remote_fd); + + return ok; +} + +static bool +render_client_dispatch_reset(struct render_client *client, + UNUSED const union render_client_op_request *req) +{ + render_client_kill_all_records(client); + render_client_reap_all_records(client, true /* wait */); + return true; +} + +static bool +render_client_dispatch_init(struct render_client *client, + const union render_client_op_request *req) +{ + client->init_flags = req->init.flags; + + /* init now to avoid doing it in each worker + * + * TODO this does very little, and might confuse perfetto. It might be + * more interesting to preload Vulkan ICDs, by calling + * vkEnumerateInstanceExtensionProperties. + */ + render_virgl_init(client->init_flags); + + return true; +} + +static bool +render_client_dispatch_nop(UNUSED struct render_client *client, + UNUSED const union render_client_op_request *req) +{ + return true; +} + +struct render_client_dispatch_entry { + size_t expect_size; + bool (*dispatch)(struct render_client *client, + const union render_client_op_request *req); +}; + +static const struct render_client_dispatch_entry + render_client_dispatch_table[RENDER_CLIENT_OP_COUNT] = { +#define RENDER_CLIENT_DISPATCH(NAME, name) \ + [RENDER_CLIENT_OP_## \ + NAME] = { .expect_size = sizeof(struct render_client_op_##name##_request), \ + .dispatch = render_client_dispatch_##name } + RENDER_CLIENT_DISPATCH(NOP, nop), + RENDER_CLIENT_DISPATCH(INIT, init), + RENDER_CLIENT_DISPATCH(RESET, reset), + RENDER_CLIENT_DISPATCH(CREATE_CONTEXT, create_context), + RENDER_CLIENT_DISPATCH(DESTROY_CONTEXT, destroy_context), +#undef RENDER_CLIENT_DISPATCH + }; + +bool +render_client_dispatch(struct render_client *client) +{ + union render_client_op_request req; + size_t req_size; + if (!render_socket_receive_request(&client->socket, &req, sizeof(req), &req_size)) + return false; + + if (req.header.op >= RENDER_CLIENT_OP_COUNT) { + render_log("invalid client op %d", req.header.op); + return false; + } + + const struct render_client_dispatch_entry *entry = + &render_client_dispatch_table[req.header.op]; + if (entry->expect_size != req_size) { + render_log("invalid request size %zu for client op %d", req_size, req.header.op); + return false; + } + + if (!entry->dispatch(client, &req)) + render_log("failed to dispatch client op %d", req.header.op); + + /* TODO this should be triggered by SIGCHLD */ + render_client_reap_all_records(client, false /* wait */); + + return true; +} + +void +render_client_destroy(struct render_client *client) +{ + struct render_server *srv = client->server; + + if (srv->state == RENDER_SERVER_STATE_SUBPROCESS) { + /* destroy all records without killing nor reaping */ + list_splicetail(&client->context_records, &client->reap_records); + list_for_each_entry_safe (struct render_context_record, rec, &client->reap_records, + head) { + render_worker_destroy(rec->worker); + free(rec); + srv->current_worker_count--; + } + } else { + render_client_kill_all_records(client); + render_client_reap_all_records(client, true /* wait */); + + render_virgl_fini(); + } + + render_socket_fini(&client->socket); + free(client); +} + +struct render_client * +render_client_create(struct render_server *srv, int client_fd) +{ + struct render_client *client = calloc(1, sizeof(*client)); + + if (!client) + return NULL; + + client->server = srv; + render_socket_init(&client->socket, client_fd); + + list_inithead(&client->context_records); + list_inithead(&client->reap_records); + + return client; +} diff --git a/server/render_client.h b/server/render_client.h new file mode 100644 index 0000000..9d9fb4e --- /dev/null +++ b/server/render_client.h @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#ifndef RENDER_CLIENT_H +#define RENDER_CLIENT_H + +#include "render_common.h" + +struct render_client { + struct render_server *server; + struct render_socket socket; + + uint32_t init_flags; + + struct list_head context_records; + struct list_head reap_records; +}; + +struct render_client * +render_client_create(struct render_server *srv, int client_fd); + +void +render_client_destroy(struct render_client *client); + +bool +render_client_dispatch(struct render_client *client); + +#endif /* RENDER_CLIENT_H */ diff --git a/server/render_common.c b/server/render_common.c new file mode 100644 index 0000000..3498dff --- /dev/null +++ b/server/render_common.c @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#include "render_common.h" + +#include +#include + +void +render_log(const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + + fprintf(stderr, "render: "); + vfprintf(stderr, fmt, va); + fprintf(stderr, "\n"); + + va_end(va); +} diff --git a/server/render_common.h b/server/render_common.h new file mode 100644 index 0000000..3510fa1 --- /dev/null +++ b/server/render_common.h @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#ifndef RENDER_COMMON_H +#define RENDER_COMMON_H + +#include +#include +#include +#include +#include +#include + +#include "util/compiler.h" +#include "util/list.h" +#include "util/macros.h" +#include "util/u_pointer.h" + +#include "render_protocol.h" +#include "render_socket.h" + +struct render_client; +struct render_context; +struct render_context_args; +struct render_server; +struct render_virgl; +struct render_worker; +struct render_worker_jail; + +void +render_log(const char *fmt, ...); + +#endif /* RENDER_COMMON_H */ diff --git a/server/render_context.c b/server/render_context.c new file mode 100644 index 0000000..19dd5b2 --- /dev/null +++ b/server/render_context.c @@ -0,0 +1,510 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#include "render_context.h" + +#include + +#include "util/u_thread.h" +#include "virgl_util.h" +#include "virglrenderer.h" +#include "vrend_iov.h" + +#include "render_virgl.h" + +struct render_context_resource { + uint32_t res_id; + struct list_head head; +}; + +/* XXX we need a unique res_id to export a blob + * + * virglrenderer.h does not have the right APIs for us. We should use vkr + * (and vrend, if that makes sense) directly. + */ +#define BLOB_RES_ID (~0u) + +static void +render_context_resource_destroy(struct render_context_resource *res) +{ + list_del(&res->head); + + virgl_renderer_resource_unref(res->res_id); + free(res); +} + +static struct render_context_resource * +render_context_resource_import(uint32_t res_id, + enum virgl_resource_fd_type fd_type, + int res_fd, + uint64_t size) +{ + /* TODO pool alloc if resources come and go frequently */ + struct render_context_resource *res = calloc(1, sizeof(*res)); + if (!res) + return NULL; + + res->res_id = res_id; + + uint32_t import_fd_type; + switch (fd_type) { + case VIRGL_RESOURCE_FD_DMABUF: + import_fd_type = VIRGL_RENDERER_BLOB_FD_TYPE_DMABUF; + break; + case VIRGL_RESOURCE_FD_OPAQUE: + import_fd_type = VIRGL_RENDERER_BLOB_FD_TYPE_OPAQUE; + break; + case VIRGL_RESOURCE_FD_SHM: + import_fd_type = VIRGL_RENDERER_BLOB_FD_TYPE_SHM; + break; + default: + import_fd_type = 0; + break; + } + const struct virgl_renderer_resource_import_blob_args import_args = { + .res_handle = res->res_id, + .blob_mem = VIRGL_RENDERER_BLOB_MEM_HOST3D, + .fd_type = import_fd_type, + .fd = res_fd, + .size = size, + }; + + int ret = virgl_renderer_resource_import_blob(&import_args); + if (ret) { + free(res); + return NULL; + } + + return res; +} + +void +render_context_update_timeline(struct render_context *ctx, + uint32_t ring_idx, + uint32_t seqno) +{ + /* this can be called by the context's main thread and sync threads */ + atomic_store(&ctx->shmem_timelines[ring_idx], seqno); + if (ctx->fence_eventfd >= 0) + write_eventfd(ctx->fence_eventfd, 1); +} + +static struct render_context_resource * +render_context_find_resource(struct render_context *ctx, uint32_t res_id) +{ + list_for_each_entry (struct render_context_resource, res, &ctx->resources, head) { + if (res->res_id == res_id) + return res; + } + + return NULL; +} + +static bool +render_context_init_virgl_context(struct render_context *ctx, + const struct render_context_op_init_request *req, + int shmem_fd, + int fence_eventfd) +{ + const int timeline_count = req->shmem_size / sizeof(*ctx->shmem_timelines); + + void *shmem_ptr = mmap(NULL, req->shmem_size, PROT_WRITE, MAP_SHARED, shmem_fd, 0); + if (shmem_ptr == MAP_FAILED) + return false; + + int ret = virgl_renderer_context_create_with_flags(ctx->ctx_id, req->flags, + ctx->name_len, ctx->name); + if (ret) { + munmap(shmem_ptr, req->shmem_size); + return false; + } + + ctx->shmem_fd = shmem_fd; + ctx->shmem_size = req->shmem_size; + ctx->shmem_ptr = shmem_ptr; + ctx->shmem_timelines = shmem_ptr; + + for (int i = 0; i < timeline_count; i++) + atomic_store(&ctx->shmem_timelines[i], 0); + + ctx->timeline_count = timeline_count; + + ctx->fence_eventfd = fence_eventfd; + + return true; +} + +static bool +render_context_export_blob(struct render_context *ctx, + const struct render_context_op_get_blob_request *req, + enum virgl_resource_fd_type *out_fd_type, + uint32_t *out_map_info, + int *out_res_fd) +{ + const uint32_t res_id = BLOB_RES_ID; + const struct virgl_renderer_resource_create_blob_args blob_args = { + .res_handle = res_id, + .ctx_id = ctx->ctx_id, + .blob_mem = VIRGL_RENDERER_BLOB_MEM_HOST3D, + .blob_flags = req->blob_flags, + .blob_id = req->blob_id, + .size = req->blob_size, + }; + int ret = virgl_renderer_resource_create_blob(&blob_args); + if (ret) { + render_log("failed to create blob resource"); + return false; + } + + uint32_t map_info; + virgl_renderer_resource_get_map_info(res_id, &map_info); + + uint32_t fd_type; + int res_fd; + ret = virgl_renderer_resource_export_blob(res_id, &fd_type, &res_fd); + virgl_renderer_resource_unref(res_id); + + if (ret) + return false; + + switch (fd_type) { + case VIRGL_RENDERER_BLOB_FD_TYPE_DMABUF: + *out_fd_type = VIRGL_RESOURCE_FD_DMABUF; + break; + case VIRGL_RENDERER_BLOB_FD_TYPE_OPAQUE: + *out_fd_type = VIRGL_RESOURCE_FD_OPAQUE; + break; + case VIRGL_RENDERER_BLOB_FD_TYPE_SHM: + *out_fd_type = VIRGL_RESOURCE_FD_SHM; + break; + default: + *out_fd_type = 0; + } + + *out_map_info = map_info; + *out_res_fd = res_fd; + + return true; +} + +static bool +render_context_dispatch_submit_fence(struct render_context *ctx, + const union render_context_op_request *req, + UNUSED const int *fds, + UNUSED int fd_count) +{ + /* always merge fences */ + assert(!(req->submit_fence.flags & ~VIRGL_RENDERER_FENCE_FLAG_MERGEABLE)); + const uint32_t flags = VIRGL_RENDERER_FENCE_FLAG_MERGEABLE; + const uint32_t ring_idx = req->submit_fence.ring_index; + const uint32_t seqno = req->submit_fence.seqno; + + assert(ring_idx < (uint32_t)ctx->timeline_count); + int ret = virgl_renderer_context_create_fence(ctx->ctx_id, flags, ring_idx, + uintptr_to_pointer(seqno)); + + return !ret; +} + +static bool +render_context_dispatch_submit_cmd(struct render_context *ctx, + const union render_context_op_request *req, + UNUSED const int *fds, + UNUSED int fd_count) +{ + const int ndw = req->submit_cmd.size / sizeof(uint32_t); + void *cmd = (void *)req->submit_cmd.cmd; + if (req->submit_cmd.size > sizeof(req->submit_cmd.cmd)) { + cmd = malloc(req->submit_cmd.size); + if (!cmd) + return true; + + const size_t inlined = sizeof(req->submit_cmd.cmd); + const size_t remain = req->submit_cmd.size - inlined; + + memcpy(cmd, req->submit_cmd.cmd, inlined); + if (!render_socket_receive_data(&ctx->socket, (char *)cmd + inlined, remain)) { + free(cmd); + return false; + } + } + + int ret = virgl_renderer_submit_cmd(cmd, ctx->ctx_id, ndw); + + if (cmd != req->submit_cmd.cmd) + free(cmd); + + const struct render_context_op_submit_cmd_reply reply = { + .ok = !ret, + }; + if (!render_socket_send_reply(&ctx->socket, &reply, sizeof(reply))) + return false; + + return true; +} + +static bool +render_context_dispatch_get_blob(struct render_context *ctx, + const union render_context_op_request *req, + UNUSED const int *fds, + UNUSED int fd_count) +{ + struct render_context_op_get_blob_reply reply = { + .fd_type = VIRGL_RESOURCE_FD_INVALID, + }; + int res_fd; + bool ok = render_context_export_blob(ctx, &req->get_blob, &reply.fd_type, + &reply.map_info, &res_fd); + if (!ok) + return render_socket_send_reply(&ctx->socket, &reply, sizeof(reply)); + + ok = + render_socket_send_reply_with_fds(&ctx->socket, &reply, sizeof(reply), &res_fd, 1); + close(res_fd); + + return ok; +} + +static bool +render_context_dispatch_detach_resource(struct render_context *ctx, + const union render_context_op_request *req, + UNUSED const int *fds, + UNUSED int fd_count) +{ + const uint32_t res_id = req->detach_resource.res_id; + + struct render_context_resource *res = render_context_find_resource(ctx, res_id); + if (res) + render_context_resource_destroy(res); + + return true; +} + +static bool +render_context_dispatch_attach_resource(struct render_context *ctx, + const union render_context_op_request *req, + const int *fds, + int fd_count) +{ + const uint32_t res_id = req->attach_resource.res_id; + const enum virgl_resource_fd_type fd_type = req->attach_resource.fd_type; + const uint64_t size = req->attach_resource.size; + + if (res_id == BLOB_RES_ID) { + render_log("XXX res_id is %u, which is reserved for blob export", res_id); + return false; + } + if (fd_type == VIRGL_RESOURCE_FD_INVALID || !size || fd_count != 1) { + render_log("failed to attach invalid resource %d", res_id); + return false; + } + + struct render_context_resource *res = + render_context_resource_import(res_id, fd_type, fds[0], size); + if (!res) { + render_log("failed to import resource %d", res_id); + return false; + } + + list_addtail(&res->head, &ctx->resources); + virgl_renderer_ctx_attach_resource(ctx->ctx_id, res_id); + + return true; +} + +static bool +render_context_dispatch_init(struct render_context *ctx, + const union render_context_op_request *req, + const int *fds, + int fd_count) +{ + if (fd_count != 1 && fd_count != 2) + return false; + + const int shmem_fd = fds[0]; + const int fence_eventfd = fd_count == 2 ? fds[1] : -1; + return render_context_init_virgl_context(ctx, &req->init, shmem_fd, fence_eventfd); +} + +static bool +render_context_dispatch_nop(UNUSED struct render_context *ctx, + UNUSED const union render_context_op_request *req, + UNUSED const int *fds, + UNUSED int fd_count) +{ + return true; +} + +struct render_context_dispatch_entry { + size_t expect_size; + int max_fd_count; + bool (*dispatch)(struct render_context *ctx, + const union render_context_op_request *req, + const int *fds, + int fd_count); +}; + +static const struct render_context_dispatch_entry + render_context_dispatch_table[RENDER_CONTEXT_OP_COUNT] = { +#define RENDER_CONTEXT_DISPATCH(NAME, name, max_fd) \ + [RENDER_CONTEXT_OP_## \ + NAME] = { .expect_size = sizeof(struct render_context_op_##name##_request), \ + .max_fd_count = (max_fd), \ + .dispatch = render_context_dispatch_##name } + RENDER_CONTEXT_DISPATCH(NOP, nop, 0), + RENDER_CONTEXT_DISPATCH(INIT, init, 2), + RENDER_CONTEXT_DISPATCH(ATTACH_RESOURCE, attach_resource, 1), + RENDER_CONTEXT_DISPATCH(DETACH_RESOURCE, detach_resource, 0), + RENDER_CONTEXT_DISPATCH(GET_BLOB, get_blob, 0), + RENDER_CONTEXT_DISPATCH(SUBMIT_CMD, submit_cmd, 0), + RENDER_CONTEXT_DISPATCH(SUBMIT_FENCE, submit_fence, 0), +#undef RENDER_CONTEXT_DISPATCH + }; + +static bool +render_context_dispatch(struct render_context *ctx) +{ + union render_context_op_request req; + size_t req_size; + int req_fds[8]; + int req_fd_count; + if (!render_socket_receive_request_with_fds(&ctx->socket, &req, sizeof(req), &req_size, + req_fds, ARRAY_SIZE(req_fds), + &req_fd_count)) + return false; + + assert((unsigned int)req_fd_count <= ARRAY_SIZE(req_fds)); + + if (req.header.op >= RENDER_CONTEXT_OP_COUNT) { + render_log("invalid context op %d", req.header.op); + goto fail; + } + + const struct render_context_dispatch_entry *entry = + &render_context_dispatch_table[req.header.op]; + if (entry->expect_size != req_size || entry->max_fd_count < req_fd_count) { + render_log("invalid request size (%zu) or fd count (%d) for context op %d", + req_size, req_fd_count, req.header.op); + goto fail; + } + + render_virgl_lock_dispatch(); + const bool ok = entry->dispatch(ctx, &req, req_fds, req_fd_count); + render_virgl_unlock_dispatch(); + if (!ok) { + render_log("failed to dispatch context op %d", req.header.op); + goto fail; + } + + return true; + +fail: + for (int i = 0; i < req_fd_count; i++) + close(req_fds[i]); + return false; +} + +static bool +render_context_run(struct render_context *ctx) +{ + while (true) { + if (!render_context_dispatch(ctx)) + return false; + } + + return true; +} + +static void +render_context_fini(struct render_context *ctx) +{ + render_virgl_lock_dispatch(); + + /* destroy the context first to join its sync threads and ring threads */ + virgl_renderer_context_destroy(ctx->ctx_id); + + list_for_each_entry_safe (struct render_context_resource, res, &ctx->resources, head) + render_context_resource_destroy(res); + + render_virgl_unlock_dispatch(); + + render_virgl_remove_context(ctx); + + if (ctx->shmem_ptr) + munmap(ctx->shmem_ptr, ctx->shmem_size); + if (ctx->shmem_fd >= 0) + close(ctx->shmem_fd); + + if (ctx->fence_eventfd >= 0) + close(ctx->fence_eventfd); + + if (ctx->name) + free(ctx->name); + + render_socket_fini(&ctx->socket); +} + +static bool +render_context_init_name(struct render_context *ctx, + uint32_t ctx_id, + const char *ctx_name) +{ + const size_t name_size = strlen(ctx_name) + 16; + ctx->name = malloc(name_size); + if (!ctx->name) + return false; + + ctx->name_len = snprintf(ctx->name, name_size, "virgl-%d-%s", ctx_id, ctx_name); + if (ctx->name_len >= name_size) + ctx->name_len = name_size - 1; + + u_thread_setname(ctx->name); + + return true; +} + +static bool +render_context_init(struct render_context *ctx, const struct render_context_args *args) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->ctx_id = args->ctx_id; + render_socket_init(&ctx->socket, args->ctx_fd); + list_inithead(&ctx->resources); + ctx->shmem_fd = -1; + ctx->fence_eventfd = -1; + + if (!render_context_init_name(ctx, args->ctx_id, args->ctx_name)) + return false; + + render_virgl_add_context(ctx); + + return true; +} + +bool +render_context_main(const struct render_context_args *args) +{ + struct render_context ctx; + + assert(args->valid && args->ctx_id && args->ctx_fd >= 0); + + if (!render_virgl_init(args->init_flags)) { + close(args->ctx_fd); + return false; + } + + if (!render_context_init(&ctx, args)) { + render_virgl_fini(); + close(args->ctx_fd); + return false; + } + + const bool ok = render_context_run(&ctx); + render_context_fini(&ctx); + + render_virgl_fini(); + + return ok; +} diff --git a/server/render_context.h b/server/render_context.h new file mode 100644 index 0000000..ffe0d20 --- /dev/null +++ b/server/render_context.h @@ -0,0 +1,54 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#ifndef RENDER_CONTEXT_H +#define RENDER_CONTEXT_H + +#include "render_common.h" + +#include + +struct render_context { + uint32_t ctx_id; + struct render_socket socket; + struct list_head head; + + char *name; + size_t name_len; + + struct list_head resources; + + int shmem_fd; + size_t shmem_size; + void *shmem_ptr; + atomic_uint *shmem_timelines; + + int timeline_count; + + /* optional */ + int fence_eventfd; +}; + +struct render_context_args { + bool valid; + + uint32_t init_flags; + + uint32_t ctx_id; + char ctx_name[32]; + + /* render_context_main always takes ownership even on errors */ + int ctx_fd; +}; + +bool +render_context_main(const struct render_context_args *args); + +void +render_context_update_timeline(struct render_context *ctx, + uint32_t ring_idx, + uint32_t val); + +#endif /* RENDER_CONTEXT_H */ diff --git a/server/render_protocol.h b/server/render_protocol.h new file mode 100644 index 0000000..822523d --- /dev/null +++ b/server/render_protocol.h @@ -0,0 +1,218 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#ifndef RENDER_PROTOCOL_H +#define RENDER_PROTOCOL_H + +#include + +#include "virgl_resource.h" +#include "virglrenderer.h" +#include "virglrenderer_hw.h" + +/* this covers the command line options and the socket type */ +#define RENDER_SERVER_VERSION 0 + +/* The protocol itself is internal to virglrenderer. There is no backward + * compatibility to be kept. + */ + +/* client ops, which are handled by the server process */ +enum render_client_op { + RENDER_CLIENT_OP_NOP = 0, + RENDER_CLIENT_OP_INIT, + RENDER_CLIENT_OP_RESET, + RENDER_CLIENT_OP_CREATE_CONTEXT, + RENDER_CLIENT_OP_DESTROY_CONTEXT, + + RENDER_CLIENT_OP_COUNT, +}; + +/* context ops, which are handled by workers (subprocesses or threads) created + * by the server process + */ +enum render_context_op { + RENDER_CONTEXT_OP_NOP = 0, + RENDER_CONTEXT_OP_INIT, + RENDER_CONTEXT_OP_ATTACH_RESOURCE, + RENDER_CONTEXT_OP_DETACH_RESOURCE, + RENDER_CONTEXT_OP_GET_BLOB, + RENDER_CONTEXT_OP_SUBMIT_CMD, + RENDER_CONTEXT_OP_SUBMIT_FENCE, + + RENDER_CONTEXT_OP_COUNT, +}; + +struct render_client_op_header { + enum render_client_op op; +}; + +struct render_client_op_nop_request { + struct render_client_op_header header; +}; + +/* Initialize virglrenderer. + * + * This roughly corresponds to virgl_renderer_init. + */ +struct render_client_op_init_request { + struct render_client_op_header header; + uint32_t flags; /* VIRGL_RENDERER_USE_* and others */ +}; + +/* Remove all contexts. + * + * This roughly corresponds to virgl_renderer_reset. + */ +struct render_client_op_reset_request { + struct render_client_op_header header; +}; + +/* Create a context, which will be serviced by a worker. + * + * See also the comment before main() for the process model. + * + * This roughly corresponds to virgl_renderer_context_create_with_flags. + */ +struct render_client_op_create_context_request { + struct render_client_op_header header; + uint32_t ctx_id; + char ctx_name[32]; +}; + +struct render_client_op_create_context_reply { + bool ok; + /* followed by 1 socket fd if ok */ +}; + +/* Destroy a context, including the worker. + * + * This roughly corresponds to virgl_renderer_context_destroy. + */ +struct render_client_op_destroy_context_request { + struct render_client_op_header header; + uint32_t ctx_id; +}; + +union render_client_op_request { + struct render_client_op_header header; + struct render_client_op_nop_request nop; + struct render_client_op_init_request init; + struct render_client_op_reset_request reset; + struct render_client_op_create_context_request create_context; + struct render_client_op_destroy_context_request destroy_context; +}; + +struct render_context_op_header { + enum render_context_op op; +}; + +struct render_context_op_nop_request { + struct render_context_op_header header; +}; + +/* Initialize the context. + * + * The shmem is required and currently holds an array of atomic_uint. Each + * atomic_uint represents the current sequence number of a ring (as defined by + * the virtio-gpu spec). + * + * The eventfd is optional. When given, it will be written to when there are + * changes to any of the sequence numbers. + * + * This roughly corresponds to virgl_renderer_context_create_with_flags. + */ +struct render_context_op_init_request { + struct render_context_op_header header; + uint32_t flags; /* VIRGL_RENDERER_CONTEXT_FLAG_*/ + size_t shmem_size; + /* followed by 1 shmem fd and optionally 1 eventfd */ +}; + +/* Attach a resource to the context. + * + * This roughly corresponds to virgl_renderer_ctx_attach_resource. + */ +struct render_context_op_attach_resource_request { + struct render_context_op_header header; + uint32_t res_id; + enum virgl_resource_fd_type fd_type; + uint64_t size; + /* followed by 1 fd */ +}; + +/* Detach a resource from the context. + * + * This roughly corresponds to virgl_renderer_ctx_detach_resource. + */ +struct render_context_op_detach_resource_request { + struct render_context_op_header header; + uint32_t res_id; +}; + +/* Export a blob from the context. + * + * This roughly corresponds to virgl_renderer_resource_create_blob. + */ +struct render_context_op_get_blob_request { + struct render_context_op_header header; + uint64_t blob_id; + uint64_t blob_size; + uint32_t blob_flags; /* VIRGL_RENDERER_BLOB_FLAG_* */ +}; + +struct render_context_op_get_blob_reply { + enum virgl_resource_fd_type fd_type; + uint32_t map_info; /* VIRGL_RENDERER_MAP_* */ + /* followed by 1 fd if not VIRGL_RESOURCE_FD_INVALID */ +}; + +/* Submit a small command stream to the context. + * + * The size limit depends on the socket type. Currently, SOCK_SEQPACKET is + * used and the size limit is best treated as one page. + * + * This roughly corresponds to virgl_renderer_submit_cmd. + */ +struct render_context_op_submit_cmd_request { + struct render_context_op_header header; + size_t size; + char cmd[256]; + /* if size > sizeof(cmd), followed by (size - sizeof(cmd)) bytes in another + * message; size still must be small + */ +}; + +struct render_context_op_submit_cmd_reply { + bool ok; +}; + +/* Submit a fence to the context. + * + * This submits a fence to the specified ring. When the fence signals, the + * current sequence number of the ring in the shmem is updated. + * + * This roughly corresponds to virgl_renderer_context_create_fence. + */ +struct render_context_op_submit_fence_request { + struct render_context_op_header header; + uint32_t flags; /* VIRGL_RENDERER_FENCE_FLAG_* */ + /* TODO fix virgl_renderer_context_create_fence to use ring_index */ + uint32_t ring_index; + uint32_t seqno; +}; + +union render_context_op_request { + struct render_context_op_header header; + struct render_context_op_nop_request nop; + struct render_context_op_init_request init; + struct render_context_op_attach_resource_request attach_resource; + struct render_context_op_detach_resource_request detach_resource; + struct render_context_op_get_blob_request get_blob; + struct render_context_op_submit_cmd_request submit_cmd; + struct render_context_op_submit_fence_request submit_fence; +}; + +#endif /* RENDER_PROTOCOL_H */ diff --git a/server/render_server.c b/server/render_server.c new file mode 100644 index 0000000..b38df26 --- /dev/null +++ b/server/render_server.c @@ -0,0 +1,163 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#include "render_server.h" + +#include +#include + +#include "render_client.h" +#include "render_worker.h" + +#define RENDER_SERVER_MAX_WORKER_COUNT 256 + +static bool +render_server_run(struct render_server *srv) +{ + while (srv->state == RENDER_SERVER_STATE_RUN) { + /* TODO handle SIGCHLD */ + struct render_client *client = srv->client; + if (!render_client_dispatch(client)) + return false; + } + + return true; +} + +static void +render_server_fini(struct render_server *srv) +{ + if (srv->client) + render_client_destroy(srv->client); + assert(srv->current_worker_count == 0); + + if (srv->worker_jail) + render_worker_jail_destroy(srv->worker_jail); + + if (srv->client_fd >= 0) + close(srv->client_fd); +} + +static bool +render_server_parse_options(struct render_server *srv, int argc, char **argv) +{ + enum { + OPT_SOCKET_FD = 'a', + OPT_WORKER_SECCOMP_BPF, + OPT_WORKER_SECCOMP_MINIJAIL_POLICY, + OPT_WORKER_SECCOMP_MINIJAIL_LOG, + OPT_COUNT, + }; + static const struct option options[] = { + { "socket-fd", required_argument, NULL, OPT_SOCKET_FD }, + { "worker-seccomp-bpf", required_argument, NULL, OPT_WORKER_SECCOMP_BPF }, + { "worker-seccomp-minijail-policy", required_argument, NULL, + OPT_WORKER_SECCOMP_MINIJAIL_POLICY }, + { "worker-seccomp-minijail-log", no_argument, NULL, + OPT_WORKER_SECCOMP_MINIJAIL_LOG }, + { NULL, 0, NULL, 0 } + }; + static_assert(OPT_COUNT <= 'z', ""); + + while (true) { + const int ret = getopt_long(argc, argv, "", options, NULL); + if (ret == -1) + break; + + switch (ret) { + case OPT_SOCKET_FD: + srv->client_fd = atoi(optarg); + break; + case OPT_WORKER_SECCOMP_BPF: + srv->worker_seccomp_bpf = optarg; + break; + case OPT_WORKER_SECCOMP_MINIJAIL_POLICY: + srv->worker_seccomp_minijail_policy = optarg; + break; + case OPT_WORKER_SECCOMP_MINIJAIL_LOG: + srv->worker_seccomp_minijail_log = true; + break; + default: + render_log("unknown option specified"); + return false; + break; + } + } + + if (optind < argc) { + render_log("non-option arguments specified"); + return false; + } + + if (srv->client_fd < 0 || !render_socket_is_seqpacket(srv->client_fd)) { + render_log("no valid client fd specified"); + return false; + } + + return true; +} + +static bool +render_server_init(struct render_server *srv, + int argc, + char **argv, + struct render_context_args *ctx_args) +{ + memset(srv, 0, sizeof(*srv)); + srv->state = RENDER_SERVER_STATE_RUN; + srv->context_args = ctx_args; + srv->client_fd = -1; + srv->max_worker_count = RENDER_SERVER_MAX_WORKER_COUNT; + + if (!render_server_parse_options(srv, argc, argv)) + return false; + + enum render_worker_jail_seccomp_filter seccomp_filter = + RENDER_WORKER_JAIL_SECCOMP_NONE; + const char *seccomp_path = NULL; + if (srv->worker_seccomp_minijail_log && srv->worker_seccomp_minijail_policy) { + seccomp_filter = RENDER_WORKER_JAIL_SECCOMP_MINIJAIL_POLICY_LOG; + seccomp_path = srv->worker_seccomp_minijail_policy; + } else if (srv->worker_seccomp_bpf) { + seccomp_filter = RENDER_WORKER_JAIL_SECCOMP_BPF; + seccomp_path = srv->worker_seccomp_bpf; + } else if (srv->worker_seccomp_minijail_policy) { + seccomp_filter = RENDER_WORKER_JAIL_SECCOMP_MINIJAIL_POLICY; + seccomp_path = srv->worker_seccomp_minijail_policy; + } + + srv->worker_jail = render_worker_jail_create(seccomp_filter, seccomp_path); + if (!srv->worker_jail) { + render_log("failed to create worker jail"); + goto fail; + } + + srv->client = render_client_create(srv, srv->client_fd); + if (!srv->client) { + render_log("failed to create client"); + goto fail; + } + /* ownership transferred */ + srv->client_fd = -1; + + return true; + +fail: + render_server_fini(srv); + return false; +} + +bool +render_server_main(int argc, char **argv, struct render_context_args *ctx_args) +{ + struct render_server srv; + if (!render_server_init(&srv, argc, argv, ctx_args)) + return false; + + const bool ok = render_server_run(&srv); + render_server_fini(&srv); + + return ok; +} diff --git a/server/render_server.h b/server/render_server.h new file mode 100644 index 0000000..ec74c0a --- /dev/null +++ b/server/render_server.h @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#ifndef RENDER_SERVER_H +#define RENDER_SERVER_H + +#include "render_common.h" + +enum render_server_state { + RENDER_SERVER_STATE_RUN, + RENDER_SERVER_STATE_SUBPROCESS, +}; + +struct render_server { + enum render_server_state state; + + /* only initialized in subprocesses */ + struct render_context_args *context_args; + + /* options */ + int client_fd; + const char *worker_seccomp_bpf; + const char *worker_seccomp_minijail_policy; + bool worker_seccomp_minijail_log; + + struct render_worker_jail *worker_jail; + int max_worker_count; + int current_worker_count; + + /* only one client in the current design */ + struct render_client *client; +}; + +bool +render_server_main(int argc, char **argv, struct render_context_args *ctx_args); + +#endif /* RENDER_SERVER_H */ diff --git a/server/render_socket.c b/server/render_socket.c new file mode 100644 index 0000000..58edcf4 --- /dev/null +++ b/server/render_socket.c @@ -0,0 +1,262 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#include "render_socket.h" + +#include +#include +#include +#include + +#define RENDER_SOCKET_MAX_FD_COUNT 8 + +/* The socket pair between the server process and the client process is set up + * by the client process (or yet another process). Because render_server_run + * does not poll yet, the fd is expected to be blocking. + * + * We also expect the fd to be always valid. If the client process dies, the + * fd becomes invalid and is considered a fatal error. + * + * There is also a socket pair between each context worker and the client + * process. The pair is set up by render_socket_pair here. + * + * The fd is also expected to be blocking. When the client process closes its + * end of the socket pair, the context worker terminates. + */ +bool +render_socket_pair(int out_fds[static 2]) +{ + int ret = socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, out_fds); + if (ret) { + render_log("failed to create socket pair"); + return false; + } + + return true; +} + +bool +render_socket_is_seqpacket(int fd) +{ + int type; + socklen_t len = sizeof(type); + if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &type, &len)) + return false; + return type == SOCK_SEQPACKET; +} + +void +render_socket_init(struct render_socket *socket, int fd) +{ + assert(fd >= 0); + *socket = (struct render_socket){ + .fd = fd, + }; +} + +void +render_socket_fini(struct render_socket *socket) +{ + close(socket->fd); +} + +static const int * +get_received_fds(const struct msghdr *msg, int *out_count) +{ + const struct cmsghdr *cmsg = CMSG_FIRSTHDR(msg); + if (unlikely(!cmsg || cmsg->cmsg_level != SOL_SOCKET || + cmsg->cmsg_type != SCM_RIGHTS || cmsg->cmsg_len < CMSG_LEN(0))) { + *out_count = 0; + return NULL; + } + + *out_count = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int); + return (const int *)CMSG_DATA(cmsg); +} + +static bool +render_socket_recvmsg(struct render_socket *socket, struct msghdr *msg, size_t *out_size) +{ + do { + const ssize_t s = recvmsg(socket->fd, msg, MSG_CMSG_CLOEXEC); + if (unlikely(s <= 0)) { + if (!s) + return false; + + if (errno == EAGAIN || errno == EINTR) + continue; + + render_log("failed to receive message: %s", strerror(errno)); + return false; + } + + if (unlikely(msg->msg_flags & (MSG_TRUNC | MSG_CTRUNC))) { + render_log("failed to receive message: truncated"); + + int fd_count; + const int *fds = get_received_fds(msg, &fd_count); + for (int i = 0; i < fd_count; i++) + close(fds[i]); + + return false; + } + + *out_size = s; + return true; + } while (true); +} + +static bool +render_socket_receive_request_internal(struct render_socket *socket, + void *data, + size_t max_size, + size_t *out_size, + int *fds, + int max_fd_count, + int *out_fd_count) +{ + assert(data && max_size); + struct msghdr msg = { + .msg_iov = + &(struct iovec){ + .iov_base = data, + .iov_len = max_size, + }, + .msg_iovlen = 1, + }; + + char cmsg_buf[CMSG_SPACE(sizeof(*fds) * RENDER_SOCKET_MAX_FD_COUNT)]; + if (max_fd_count) { + assert(fds && max_fd_count <= RENDER_SOCKET_MAX_FD_COUNT); + msg.msg_control = cmsg_buf; + msg.msg_controllen = CMSG_SPACE(sizeof(*fds) * max_fd_count); + + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + memset(cmsg, 0, sizeof(*cmsg)); + } + + if (!render_socket_recvmsg(socket, &msg, out_size)) + return false; + + if (max_fd_count) { + int received_fd_count; + const int *received_fds = get_received_fds(&msg, &received_fd_count); + assert(received_fd_count <= max_fd_count); + + memcpy(fds, received_fds, sizeof(*fds) * received_fd_count); + *out_fd_count = received_fd_count; + } else if (out_fd_count) { + *out_fd_count = 0; + } + + return true; +} + +bool +render_socket_receive_request(struct render_socket *socket, + void *data, + size_t max_size, + size_t *out_size) +{ + return render_socket_receive_request_internal(socket, data, max_size, out_size, NULL, + 0, NULL); +} + +bool +render_socket_receive_request_with_fds(struct render_socket *socket, + void *data, + size_t max_size, + size_t *out_size, + int *fds, + int max_fd_count, + int *out_fd_count) +{ + return render_socket_receive_request_internal(socket, data, max_size, out_size, fds, + max_fd_count, out_fd_count); +} + +bool +render_socket_receive_data(struct render_socket *socket, void *data, size_t size) +{ + size_t received_size; + if (!render_socket_receive_request(socket, data, size, &received_size)) + return false; + + if (size != received_size) { + render_log("failed to receive data: expected %zu but received %zu", size, + received_size); + return false; + } + + return true; +} + +static bool +render_socket_sendmsg(struct render_socket *socket, const struct msghdr *msg) +{ + do { + const ssize_t s = sendmsg(socket->fd, msg, MSG_NOSIGNAL); + if (unlikely(s < 0)) { + if (errno == EAGAIN || errno == EINTR) + continue; + + render_log("failed to send message: %s", strerror(errno)); + return false; + } + + /* no partial send since the socket type is SOCK_SEQPACKET */ + assert(msg->msg_iovlen == 1 && msg->msg_iov[0].iov_len == (size_t)s); + return true; + } while (true); +} + +static inline bool +render_socket_send_reply_internal(struct render_socket *socket, + const void *data, + size_t size, + const int *fds, + int fd_count) +{ + assert(data && size); + struct msghdr msg = { + .msg_iov = + &(struct iovec){ + .iov_base = (void *)data, + .iov_len = size, + }, + .msg_iovlen = 1, + }; + + char cmsg_buf[CMSG_SPACE(sizeof(*fds) * RENDER_SOCKET_MAX_FD_COUNT)]; + if (fd_count) { + assert(fds && fd_count <= RENDER_SOCKET_MAX_FD_COUNT); + msg.msg_control = cmsg_buf; + msg.msg_controllen = CMSG_SPACE(sizeof(*fds) * fd_count); + + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(*fds) * fd_count); + memcpy(CMSG_DATA(cmsg), fds, sizeof(*fds) * fd_count); + } + + return render_socket_sendmsg(socket, &msg); +} + +bool +render_socket_send_reply(struct render_socket *socket, const void *data, size_t size) +{ + return render_socket_send_reply_internal(socket, data, size, NULL, 0); +} + +bool +render_socket_send_reply_with_fds(struct render_socket *socket, + const void *data, + size_t size, + const int *fds, + int fd_count) +{ + return render_socket_send_reply_internal(socket, data, size, fds, fd_count); +} diff --git a/server/render_socket.h b/server/render_socket.h new file mode 100644 index 0000000..ead1f8c --- /dev/null +++ b/server/render_socket.h @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#ifndef RENDER_SOCKET_H +#define RENDER_SOCKET_H + +#include "render_common.h" + +struct render_socket { + int fd; +}; + +bool +render_socket_pair(int out_fds[static 2]); + +bool +render_socket_is_seqpacket(int fd); + +void +render_socket_init(struct render_socket *socket, int fd); + +void +render_socket_fini(struct render_socket *socket); + +bool +render_socket_receive_request(struct render_socket *socket, + void *data, + size_t max_size, + size_t *out_size); + +bool +render_socket_receive_request_with_fds(struct render_socket *socket, + void *data, + size_t max_size, + size_t *out_size, + int *fds, + int max_fd_count, + int *out_fd_count); + +bool +render_socket_receive_data(struct render_socket *socket, void *data, size_t size); + +bool +render_socket_send_reply(struct render_socket *socket, const void *data, size_t size); + +bool +render_socket_send_reply_with_fds(struct render_socket *socket, + const void *data, + size_t size, + const int *fds, + int fd_count); + +#endif /* RENDER_SOCKET_H */ diff --git a/server/render_virgl.c b/server/render_virgl.c new file mode 100644 index 0000000..b83caba --- /dev/null +++ b/server/render_virgl.c @@ -0,0 +1,154 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#include "render_virgl.h" + +#include "virglrenderer.h" + +#include "render_context.h" + +struct render_virgl render_virgl_internal = { +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + .struct_mutex = _MTX_INITIALIZER_NP, + .dispatch_mutex = _MTX_INITIALIZER_NP, +#endif + .init_count = 0, +}; + +static struct render_virgl * +render_virgl_lock_struct(void) +{ +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + mtx_lock(&render_virgl_internal.struct_mutex); +#endif + return &render_virgl_internal; +} + +static void +render_virgl_unlock_struct(void) +{ +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + mtx_unlock(&render_virgl_internal.struct_mutex); +#endif +} + +static struct render_context * +render_virgl_lookup_context(uint32_t ctx_id) +{ + const struct render_virgl *virgl = render_virgl_lock_struct(); + struct render_context *ctx = NULL; + +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + list_for_each_entry (struct render_context, iter, &virgl->contexts, head) { + if (iter->ctx_id == ctx_id) { + ctx = iter; + break; + } + } +#else + assert(list_is_singular(&virgl->contexts)); + ctx = list_first_entry(&virgl->contexts, struct render_context, head); + assert(ctx->ctx_id == ctx_id); + (void)ctx_id; +#endif + + render_virgl_unlock_struct(); + + return ctx; +} + +static void +render_virgl_cb_write_context_fence(UNUSED void *cookie, + uint32_t ctx_id, + uint64_t queue_id, + void *fence_cookie) +{ + struct render_context *ctx = render_virgl_lookup_context(ctx_id); + assert(ctx); + + const uint32_t ring_idx = queue_id; + const uint32_t seqno = (uint32_t)pointer_to_uintptr(fence_cookie); + render_context_update_timeline(ctx, ring_idx, seqno); +} + +static const struct virgl_renderer_callbacks render_virgl_cbs = { + .version = VIRGL_RENDERER_CALLBACKS_VERSION, + .write_context_fence = render_virgl_cb_write_context_fence, +}; + +void +render_virgl_add_context(struct render_context *ctx) +{ + struct render_virgl *virgl = render_virgl_lock_struct(); + list_addtail(&ctx->head, &virgl->contexts); + render_virgl_unlock_struct(); +} + +void +render_virgl_remove_context(struct render_context *ctx) +{ + render_virgl_lock_struct(); + list_del(&ctx->head); + render_virgl_unlock_struct(); +} + +void +render_virgl_fini(void) +{ + struct render_virgl *virgl = render_virgl_lock_struct(); + + if (virgl->init_count) { + virgl->init_count--; + if (!virgl->init_count) { + render_virgl_lock_dispatch(); + virgl_renderer_cleanup(virgl); + render_virgl_unlock_dispatch(); + } + } + + render_virgl_unlock_struct(); +} + +bool +render_virgl_init(uint32_t init_flags) +{ + /* we only care if virgl and/or venus are enabled */ + init_flags &= VIRGL_RENDERER_VENUS | VIRGL_RENDERER_NO_VIRGL; + + /* always use sync thread and async fence cb for low latency */ + init_flags |= VIRGL_RENDERER_THREAD_SYNC | VIRGL_RENDERER_ASYNC_FENCE_CB | + VIRGL_RENDERER_USE_EXTERNAL_BLOB; + + struct render_virgl *virgl = render_virgl_lock_struct(); + + if (virgl->init_count) { + if (virgl->init_flags != init_flags) { + render_log("failed to re-initialize with flags 0x%x", init_flags); + goto fail; + } + } else { + render_virgl_lock_dispatch(); + int ret = virgl_renderer_init(virgl, init_flags, + (struct virgl_renderer_callbacks *)&render_virgl_cbs); + render_virgl_unlock_dispatch(); + if (ret) { + render_log("failed to initialize virglrenderer"); + goto fail; + } + + list_inithead(&virgl->contexts); + virgl->init_flags = init_flags; + } + + virgl->init_count++; + + render_virgl_unlock_struct(); + + return true; + +fail: + render_virgl_unlock_struct(); + return false; +} diff --git a/server/render_virgl.h b/server/render_virgl.h new file mode 100644 index 0000000..cb362f4 --- /dev/null +++ b/server/render_virgl.h @@ -0,0 +1,70 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#ifndef RENDER_VIRGL_H +#define RENDER_VIRGL_H + +#include "render_common.h" + +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD +#include "c11/threads.h" +#endif + +/* Workers call into virglrenderer. When they are processes, not much care is + * required. We just want to be careful that the server process might have + * initialized viglrenderer before workers are forked. + * + * But when workers are threads, we need to grab a lock to protect + * virglrenderer. + * + * TODO skip virglrenderer.h and go straight to vkr_renderer.h. That allows + * us to remove this file. + */ +struct render_virgl { +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + /* this protects the struct */ + mtx_t struct_mutex; + /* this protects virglrenderer */ + mtx_t dispatch_mutex; +#endif + + /* for nested initialization */ + int init_count; + uint32_t init_flags; + + struct list_head contexts; +}; + +extern struct render_virgl render_virgl_internal; + +bool +render_virgl_init(uint32_t init_flags); + +void +render_virgl_fini(void); + +void +render_virgl_add_context(struct render_context *ctx); + +void +render_virgl_remove_context(struct render_context *ctx); + +static inline void +render_virgl_lock_dispatch(void) +{ +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + mtx_lock(&render_virgl_internal.dispatch_mutex); +#endif +} + +static inline void +render_virgl_unlock_dispatch(void) +{ +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + mtx_unlock(&render_virgl_internal.dispatch_mutex); +#endif +} + +#endif /* RENDER_VIRGL_H */ diff --git a/server/render_worker.c b/server/render_worker.c new file mode 100644 index 0000000..0f74bd2 --- /dev/null +++ b/server/render_worker.c @@ -0,0 +1,254 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#include "render_worker.h" + +/* One and only one of ENABLE_RENDER_SERVER_WORKER_* must be set. + * + * With ENABLE_RENDER_SERVER_WORKER_PROCESS, each worker is a subprocess + * forked from the server process. + * + * With ENABLE_RENDER_SERVER_WORKER_THREAD, each worker is a thread of the + * server process. + * + * With ENABLE_RENDER_SERVER_WORKER_MINIJAIL, each worker is a subprocess + * forked from the server process, jailed with minijail. + */ +#if (ENABLE_RENDER_SERVER_WORKER_PROCESS + ENABLE_RENDER_SERVER_WORKER_THREAD + \ + ENABLE_RENDER_SERVER_WORKER_MINIJAIL) != 1 +#error "no worker defined" +#endif + +#include +#include +#include +#include +#include +#include + +struct render_worker { +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + thrd_t thread; +#else + pid_t pid; +#endif + bool reaped; + + char thread_data[]; +}; + +#ifdef ENABLE_RENDER_SERVER_WORKER_MINIJAIL + +#include +#include +#include +#include +#include +#include + +static bool +load_bpf_program(struct sock_fprog *prog, const char *path) +{ + int fd = -1; + void *data = NULL; + + fd = open(path, O_RDONLY); + if (fd < 0) + goto fail; + + const off_t size = lseek(fd, 0, SEEK_END); + if (size <= 0 || size % sizeof(struct sock_filter)) + goto fail; + lseek(fd, 0, SEEK_SET); + + data = malloc(size); + if (!data) + goto fail; + + off_t cur = 0; + while (cur < size) { + const ssize_t r = read(fd, (char *)data + cur, size - cur); + if (r <= 0) + goto fail; + cur += r; + } + + close(fd); + + prog->len = size / sizeof(struct sock_filter); + prog->filter = data; + + return true; + +fail: + if (data) + free(data); + if (fd >= 0) + close(fd); + return false; +} + +static struct render_worker_jail * +create_minijail(enum render_worker_jail_seccomp_filter seccomp_filter, + const char *seccomp_path) +{ + struct minijail *j = minijail_new(); + + /* TODO namespaces and many more */ + minijail_no_new_privs(j); + + if (seccomp_filter != RENDER_WORKER_JAIL_SECCOMP_NONE) { + if (seccomp_filter == RENDER_WORKER_JAIL_SECCOMP_BPF) { + struct sock_fprog prog; + if (!load_bpf_program(&prog, seccomp_path)) { + minijail_destroy(j); + return NULL; + } + + minijail_set_seccomp_filters(j, &prog); + free(prog.filter); + } else { + if (seccomp_filter == RENDER_WORKER_JAIL_SECCOMP_POLICY_LOG) + minijail_log_seccomp_filter_failures(j); + minijail_parse_seccomp_filters(j, seccomp_path); + } + + minijail_use_seccomp_filter(j); + } + + return (struct render_worker_jail *)j; +} + +static pid_t +fork_minijail(const struct render_worker_jail *jail) +{ + struct minijail *j = minijail_new(); + if (!j) + return -1; + + /* is this faster? */ + if (minijail_copy_jail((const struct minijail *)jail, j)) { + minijail_destroy(j); + return -1; + } + + pid_t pid = minijail_fork(j); + minijail_destroy(j); + + return pid; +} + +#endif /* ENABLE_RENDER_SERVER_WORKER_MINIJAIL */ + +struct render_worker_jail * +render_worker_jail_create(enum render_worker_jail_seccomp_filter seccomp_filter, + const char *seccomp_path) +{ +#if defined(ENABLE_RENDER_SERVER_WORKER_MINIJAIL) + return create_minijail(seccomp_filter, seccomp_path); +#else + /* TODO RENDER_WORKER_JAIL_SECCOMP_BPF */ + if (seccomp_filter != RENDER_WORKER_JAIL_SECCOMP_NONE) + return NULL; + (void)seccomp_path; + return (void *)1; +#endif +} + +void +render_worker_jail_destroy(struct render_worker_jail *jail) +{ +#if defined(ENABLE_RENDER_SERVER_WORKER_MINIJAIL) + minijail_destroy((struct minijail *)jail); +#else + (void)jail; +#endif +} + +struct render_worker * +render_worker_create(struct render_worker_jail *jail, + int (*thread_func)(void *thread_data), + void *thread_data, + size_t thread_data_size) +{ + struct render_worker *worker = calloc(1, sizeof(*worker) + thread_data_size); + if (!worker) + return NULL; + + memcpy(worker->thread_data, thread_data, thread_data_size); + + bool ok; +#if defined(ENABLE_RENDER_SERVER_WORKER_PROCESS) + worker->pid = fork(); + ok = worker->pid >= 0; + (void)jail; + (void)thread_func; +#elif defined(ENABLE_RENDER_SERVER_WORKER_THREAD) + ok = thrd_create(&worker->thread, thread_func, worker->thread_data) == thrd_success; + (void)jail; +#elif defined(ENABLE_RENDER_SERVER_WORKER_MINIJAIL) + worker->pid = fork_minijail(jail); + ok = worker->pid >= 0; + (void)thread_func; +#endif + if (!ok) { + free(worker); + return NULL; + } + + return worker; +} + +bool +render_worker_is_record(const struct render_worker *worker) +{ + /* return false if called from the worker itself */ +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + return !thrd_equal(worker->thread, thrd_current()); +#else + return worker->pid > 0; +#endif +} + +void +render_worker_kill(struct render_worker *worker) +{ + assert(render_worker_is_record(worker)); + +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + /* we trust the thread to clean up and exit in finite time */ +#else + kill(worker->pid, SIGKILL); +#endif +} + +bool +render_worker_reap(struct render_worker *worker, bool wait) +{ + assert(render_worker_is_record(worker)); + + if (worker->reaped) + return true; + + bool ok; +#ifdef ENABLE_RENDER_SERVER_WORKER_THREAD + (void)wait; + ok = thrd_join(worker->thread, NULL) == thrd_success; +#else + const int options = WEXITED | (wait ? 0 : WNOHANG); + siginfo_t siginfo = { 0 }; + const int ret = waitid(P_PID, worker->pid, &siginfo, options); + ok = !ret && siginfo.si_pid == worker->pid; +#endif + + worker->reaped = ok; + return ok; +} + +void +render_worker_destroy(struct render_worker *worker) +{ + free(worker); +} diff --git a/server/render_worker.h b/server/render_worker.h new file mode 100644 index 0000000..158dc7d --- /dev/null +++ b/server/render_worker.h @@ -0,0 +1,46 @@ +/* + * Copyright 2021 Google LLC + * SPDX-License-Identifier: MIT + */ + +#ifndef RENDER_WORKER_H +#define RENDER_WORKER_H + +#include "render_common.h" + +enum render_worker_jail_seccomp_filter { + /* seccomp_path is ignored and seccomp is disabled */ + RENDER_WORKER_JAIL_SECCOMP_NONE, + /* seccomp_path is a file containing a BPF program */ + RENDER_WORKER_JAIL_SECCOMP_BPF, + /* seccomp_path is a file containing a minijail policy */ + RENDER_WORKER_JAIL_SECCOMP_MINIJAIL_POLICY, + RENDER_WORKER_JAIL_SECCOMP_MINIJAIL_POLICY_LOG, +}; + +struct render_worker_jail * +render_worker_jail_create(enum render_worker_jail_seccomp_filter seccomp_filter, + const char *seccomp_path); + +void +render_worker_jail_destroy(struct render_worker_jail *jail); + +struct render_worker * +render_worker_create(struct render_worker_jail *jail, + int (*thread_func)(void *thread_data), + void *thread_data, + size_t thread_data_size); + +bool +render_worker_is_record(const struct render_worker *worker); + +void +render_worker_kill(struct render_worker *worker); + +bool +render_worker_reap(struct render_worker *worker, bool wait); + +void +render_worker_destroy(struct render_worker *worker); + +#endif /* RENDER_WORKER_H */