From 91f23c7067af248846420854a0dc78c26ea6e300 Mon Sep 17 00:00:00 2001 From: Niklas Haas Date: Wed, 14 Sep 2016 20:54:18 +0200 Subject: vo_gpu: vulkan: initial implementation This time based on ra/vo_gpu. 2017 is the year of the vulkan desktop! Current problems / limitations / improvement opportunities: 1. The swapchain/flipping code violates the vulkan spec, by assuming that the presentation queue will be bounded (in cases where rendering is significantly faster than vsync). But apparently, there's simply no better way to do this right now, to the point where even the stupid cube.c examples from LunarG etc. do it wrong. (cf. https://github.com/KhronosGroup/Vulkan-Docs/issues/370) 2. The memory allocator could be improved. (This is a universal constant) 3. Could explore using push descriptors instead of descriptor sets, especially since we expect to switch descriptors semi-often for some passes (like interpolation). Probably won't make a difference, but the synchronization overhead might be a factor. Who knows. 4. Parallelism across frames / async transfer is not well-defined, we either need to use a better semaphore / command buffer strategy or a resource pooling layer to safely handle cross-frame parallelism. (That said, I gave resource pooling a try and was not happy with the result at all - so I'm still exploring the semaphore strategy) 5. We aggressively use pipeline barriers where events would offer a much more fine-grained synchronization mechanism. As a result of this, we might be suffering from GPU bubbles due to too-short dependencies on objects. (That said, I'm also exploring the use of semaphores as a an ordering tactic which would allow cross-frame time slicing in theory) Some minor changes to the vo_gpu and infrastructure, but nothing consequential. NOTE: For safety, all use of asynchronous commands / multiple command pools is currently disabled completely. There are some left-over relics of this in the code (e.g. the distinction between dev_poll and pool_poll), but that is kept in place mostly because this will be re-extended in the future (vulkan rev 2). The queue count is also currently capped to 1, because of the lack of cross-frame semaphores means we need the implicit synchronization from the same-queue semantics to guarantee a correct result. --- video/out/gpu/context.c | 8 + video/out/gpu/ra.h | 9 +- video/out/vo_gpu.c | 12 +- video/out/vulkan/common.h | 51 ++ video/out/vulkan/context.c | 501 ++++++++++++ video/out/vulkan/context.h | 10 + video/out/vulkan/context_xlib.c | 116 +++ video/out/vulkan/formats.c | 55 ++ video/out/vulkan/formats.h | 16 + video/out/vulkan/malloc.c | 424 +++++++++++ video/out/vulkan/malloc.h | 35 + video/out/vulkan/ra_vk.c | 1590 +++++++++++++++++++++++++++++++++++++++ video/out/vulkan/ra_vk.h | 31 + video/out/vulkan/utils.c | 726 ++++++++++++++++++ video/out/vulkan/utils.h | 153 ++++ 15 files changed, 3727 insertions(+), 10 deletions(-) create mode 100644 video/out/vulkan/common.h create mode 100644 video/out/vulkan/context.c create mode 100644 video/out/vulkan/context.h create mode 100644 video/out/vulkan/context_xlib.c create mode 100644 video/out/vulkan/formats.c create mode 100644 video/out/vulkan/formats.h create mode 100644 video/out/vulkan/malloc.c create mode 100644 video/out/vulkan/malloc.h create mode 100644 video/out/vulkan/ra_vk.c create mode 100644 video/out/vulkan/ra_vk.h create mode 100644 video/out/vulkan/utils.c create mode 100644 video/out/vulkan/utils.h (limited to 'video') diff --git a/video/out/gpu/context.c b/video/out/gpu/context.c index c5721c73b4..25e2a754bf 100644 --- a/video/out/gpu/context.c +++ b/video/out/gpu/context.c @@ -44,6 +44,7 @@ extern const struct ra_ctx_fns ra_ctx_dxgl; extern const struct ra_ctx_fns ra_ctx_rpi; extern const struct ra_ctx_fns ra_ctx_mali; extern const struct ra_ctx_fns ra_ctx_vdpauglx; +extern const struct ra_ctx_fns ra_ctx_vulkan_xlib; static const struct ra_ctx_fns *contexts[] = { // OpenGL contexts: @@ -83,6 +84,13 @@ static const struct ra_ctx_fns *contexts[] = { #if HAVE_VDPAU_GL_X11 &ra_ctx_vdpauglx, #endif + +// Vulkan contexts: +#if HAVE_VULKAN +#if HAVE_X11 + &ra_ctx_vulkan_xlib, +#endif +#endif }; static bool get_help(struct mp_log *log, struct bstr param) diff --git a/video/out/gpu/ra.h b/video/out/gpu/ra.h index 10245b250e..7a2fa0e11c 100644 --- a/video/out/gpu/ra.h +++ b/video/out/gpu/ra.h @@ -146,6 +146,7 @@ enum ra_buf_type { RA_BUF_TYPE_TEX_UPLOAD, // texture upload buffer (pixel buffer object) RA_BUF_TYPE_SHADER_STORAGE, // shader buffer (SSBO), for RA_VARTYPE_BUF_RW RA_BUF_TYPE_UNIFORM, // uniform buffer (UBO), for RA_VARTYPE_BUF_RO + RA_BUF_TYPE_VERTEX, // not publicly usable (RA-internal usage) }; struct ra_buf_params { @@ -369,10 +370,10 @@ struct ra_fns { void (*buf_destroy)(struct ra *ra, struct ra_buf *buf); - // Update the contents of a buffer, starting at a given offset and up to a - // given size, with the contents of *data. This is an extremely common - // operation. Calling this while the buffer is considered "in use" is an - // error. (See: buf_poll) + // Update the contents of a buffer, starting at a given offset (*must* be a + // multiple of 4) and up to a given size, with the contents of *data. This + // is an extremely common operation. Calling this while the buffer is + // considered "in use" is an error. (See: buf_poll) void (*buf_update)(struct ra *ra, struct ra_buf *buf, ptrdiff_t offset, const void *data, size_t size); diff --git a/video/out/vo_gpu.c b/video/out/vo_gpu.c index bd245de05b..a26912e0d8 100644 --- a/video/out/vo_gpu.c +++ b/video/out/vo_gpu.c @@ -60,6 +60,7 @@ struct gpu_priv { static void resize(struct gpu_priv *p) { struct vo *vo = p->vo; + struct ra_swapchain *sw = p->ctx->swapchain; MP_VERBOSE(vo, "Resize: %dx%d\n", vo->dwidth, vo->dheight); @@ -69,6 +70,11 @@ static void resize(struct gpu_priv *p) gl_video_resize(p->renderer, &src, &dst, &osd); + int fb_depth = sw->fns->color_depth ? sw->fns->color_depth(sw) : 0; + if (fb_depth) + MP_VERBOSE(p, "Reported display depth: %d\n", fb_depth); + gl_video_set_fb_depth(p->renderer, fb_depth); + vo->want_redraw = true; } @@ -289,7 +295,6 @@ static int preinit(struct vo *vo) goto err_out; assert(p->ctx->ra); assert(p->ctx->swapchain); - struct ra_swapchain *sw = p->ctx->swapchain; p->renderer = gl_video_init(p->ctx->ra, vo->log, vo->global); gl_video_set_osd_source(p->renderer, vo->osd); @@ -305,11 +310,6 @@ static int preinit(struct vo *vo) vo->hwdec_devs, vo->opts->gl_hwdec_interop); gl_video_set_hwdec(p->renderer, p->hwdec); - int fb_depth = sw->fns->color_depth ? sw->fns->color_depth(sw) : 0; - if (fb_depth) - MP_VERBOSE(p, "Reported display depth: %d\n", fb_depth); - gl_video_set_fb_depth(p->renderer, fb_depth); - return 0; err_out: diff --git a/video/out/vulkan/common.h b/video/out/vulkan/common.h new file mode 100644 index 0000000000..4c0e783f0e --- /dev/null +++ b/video/out/vulkan/common.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "config.h" + +#include "common/common.h" +#include "common/msg.h" + +// We need to define all platforms we want to support. Since we have +// our own mechanism for checking this, we re-define the right symbols +#if HAVE_X11 +#define VK_USE_PLATFORM_XLIB_KHR +#endif + +#include + +// Vulkan allows the optional use of a custom allocator. We don't need one but +// mark this parameter with a better name in case we ever decide to change this +// in the future. (And to make the code more readable) +#define MPVK_ALLOCATOR NULL + +// A lot of things depend on streaming resources across frames. Depending on +// how many frames we render ahead of time, we need to pick enough to avoid +// any conflicts, so make all of these tunable relative to this constant in +// order to centralize them. +#define MPVK_MAX_STREAMING_DEPTH 8 + +// Shared struct used to hold vulkan context information +struct mpvk_ctx { + struct mp_log *log; + VkInstance inst; + VkPhysicalDevice physd; + VkDebugReportCallbackEXT dbg; + VkDevice dev; + + // Surface, must be initialized fter the context itself + VkSurfaceKHR surf; + VkSurfaceFormatKHR surf_format; // picked at surface initialization time + + struct vk_malloc *alloc; // memory allocator for this device + struct vk_cmdpool *pool; // primary command pool for this device + struct vk_cmd *last_cmd; // most recently submitted command + + // Cached capabilities + VkPhysicalDeviceLimits limits; +}; diff --git a/video/out/vulkan/context.c b/video/out/vulkan/context.c new file mode 100644 index 0000000000..bd456d214c --- /dev/null +++ b/video/out/vulkan/context.c @@ -0,0 +1,501 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include "options/m_config.h" +#include "context.h" +#include "ra_vk.h" +#include "utils.h" + +enum { + SWAP_AUTO = 0, + SWAP_FIFO, + SWAP_FIFO_RELAXED, + SWAP_MAILBOX, + SWAP_IMMEDIATE, + SWAP_COUNT, +}; + +struct vulkan_opts { + struct mpvk_device_opts dev_opts; // logical device options + char *device; // force a specific GPU + int swap_mode; +}; + +static int vk_validate_dev(struct mp_log *log, const struct m_option *opt, + struct bstr name, struct bstr param) +{ + int ret = M_OPT_INVALID; + VkResult res; + + // Create a dummy instance to validate/list the devices + VkInstanceCreateInfo info = { + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + }; + + VkInstance inst; + VkPhysicalDevice *devices = NULL; + uint32_t num = 0; + + res = vkCreateInstance(&info, MPVK_ALLOCATOR, &inst); + if (res != VK_SUCCESS) + goto error; + + res = vkEnumeratePhysicalDevices(inst, &num, NULL); + if (res != VK_SUCCESS) + goto error; + + devices = talloc_array(NULL, VkPhysicalDevice, num); + vkEnumeratePhysicalDevices(inst, &num, devices); + if (res != VK_SUCCESS) + goto error; + + bool help = bstr_equals0(param, "help"); + if (help) { + mp_info(log, "Available vulkan devices:\n"); + ret = M_OPT_EXIT; + } + + for (int i = 0; i < num; i++) { + VkPhysicalDeviceProperties prop; + vkGetPhysicalDeviceProperties(devices[i], &prop); + + if (help) { + mp_info(log, " '%s' (GPU %d, ID %x:%x)\n", prop.deviceName, i, + (unsigned)prop.vendorID, (unsigned)prop.deviceID); + } else if (bstr_equals0(param, prop.deviceName)) { + ret = 0; + break; + } + } + + if (!help) + mp_err(log, "No device with name '%.*s'!\n", BSTR_P(param)); + +error: + talloc_free(devices); + return ret; +} + +#define OPT_BASE_STRUCT struct vulkan_opts +const struct m_sub_options vulkan_conf = { + .opts = (const struct m_option[]) { + OPT_STRING_VALIDATE("vulkan-device", device, 0, vk_validate_dev), + OPT_CHOICE("vulkan-swap-mode", swap_mode, 0, + ({"auto", SWAP_AUTO}, + {"fifo", SWAP_FIFO}, + {"fifo-relaxed", SWAP_FIFO_RELAXED}, + {"mailbox", SWAP_MAILBOX}, + {"immediate", SWAP_IMMEDIATE})), + OPT_INTRANGE("vulkan-queue-count", dev_opts.queue_count, 0, 1, + MPVK_MAX_QUEUES, OPTDEF_INT(1)), + {0} + }, + .size = sizeof(struct vulkan_opts) +}; + +struct priv { + struct mpvk_ctx *vk; + struct vulkan_opts *opts; + // Swapchain metadata: + int w, h; // current size + VkSwapchainCreateInfoKHR protoInfo; // partially filled-in prototype + VkSwapchainKHR swapchain; + VkSwapchainKHR old_swapchain; + int frames_in_flight; + // state of the images: + struct ra_tex **images; // ra_tex wrappers for the vkimages + int num_images; // size of images + VkSemaphore *acquired; // pool of semaphores used to synchronize images + int num_acquired; // size of this pool + int idx_acquired; // index of next free semaphore within this pool + int last_imgidx; // the image index last acquired (for submit) +}; + +static bool update_swapchain_info(struct priv *p, + VkSwapchainCreateInfoKHR *info) +{ + struct mpvk_ctx *vk = p->vk; + + // Query the supported capabilities and update this struct as needed + VkSurfaceCapabilitiesKHR caps; + VK(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vk->physd, vk->surf, &caps)); + + // Sorted by preference + static const VkCompositeAlphaFlagBitsKHR alphaModes[] = { + VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, + }; + + for (int i = 0; i < MP_ARRAY_SIZE(alphaModes); i++) { + if (caps.supportedCompositeAlpha & alphaModes[i]) { + info->compositeAlpha = alphaModes[i]; + break; + } + } + + if (!info->compositeAlpha) { + MP_ERR(vk, "Failed picking alpha compositing mode (caps: 0x%x)\n", + caps.supportedCompositeAlpha); + goto error; + } + + static const VkSurfaceTransformFlagBitsKHR rotModes[] = { + VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR, + VK_SURFACE_TRANSFORM_INHERIT_BIT_KHR, + }; + + for (int i = 0; i < MP_ARRAY_SIZE(rotModes); i++) { + if (caps.supportedTransforms & rotModes[i]) { + info->preTransform = rotModes[i]; + break; + } + } + + if (!info->preTransform) { + MP_ERR(vk, "Failed picking surface transform mode (caps: 0x%x)\n", + caps.supportedTransforms); + goto error; + } + + // Image count as required + MP_VERBOSE(vk, "Requested image count: %d (min %d max %d)\n", + (int)info->minImageCount, (int)caps.minImageCount, + (int)caps.maxImageCount); + + info->minImageCount = MPMAX(info->minImageCount, caps.minImageCount); + if (caps.maxImageCount) + info->minImageCount = MPMIN(info->minImageCount, caps.maxImageCount); + + // Check the extent against the allowed parameters + if (caps.currentExtent.width != info->imageExtent.width && + caps.currentExtent.width != 0xFFFFFFFF) + { + MP_WARN(vk, "Requested width %d does not match current width %d\n", + (int)info->imageExtent.width, (int)caps.currentExtent.width); + info->imageExtent.width = caps.currentExtent.width; + } + + if (caps.currentExtent.height != info->imageExtent.height && + caps.currentExtent.height != 0xFFFFFFFF) + { + MP_WARN(vk, "Requested height %d does not match current height %d\n", + (int)info->imageExtent.height, (int)caps.currentExtent.height); + info->imageExtent.height = caps.currentExtent.height; + } + + if (caps.minImageExtent.width > info->imageExtent.width || + caps.minImageExtent.height > info->imageExtent.height) + { + MP_ERR(vk, "Requested size %dx%d smaller than device minimum %d%d\n", + (int)info->imageExtent.width, (int)info->imageExtent.height, + (int)caps.minImageExtent.width, (int)caps.minImageExtent.height); + goto error; + } + + if (caps.maxImageExtent.width < info->imageExtent.width || + caps.maxImageExtent.height < info->imageExtent.height) + { + MP_ERR(vk, "Requested size %dx%d larger than device maximum %d%d\n", + (int)info->imageExtent.width, (int)info->imageExtent.height, + (int)caps.maxImageExtent.width, (int)caps.maxImageExtent.height); + goto error; + } + + // We just request whatever usage we can, and let the ra_vk decide what + // ra_tex_params that translates to. This makes the images as flexible + // as possible. + info->imageUsage = caps.supportedUsageFlags; + return true; + +error: + return false; +} + +void ra_vk_ctx_uninit(struct ra_ctx *ctx) +{ + if (ctx->ra) { + struct priv *p = ctx->swapchain->priv; + struct mpvk_ctx *vk = p->vk; + + mpvk_pool_wait_idle(vk, vk->pool); + + for (int i = 0; i < p->num_images; i++) + ra_tex_free(ctx->ra, &p->images[i]); + for (int i = 0; i < p->num_acquired; i++) + vkDestroySemaphore(vk->dev, p->acquired[i], MPVK_ALLOCATOR); + + vkDestroySwapchainKHR(vk->dev, p->swapchain, MPVK_ALLOCATOR); + + talloc_free(p->images); + talloc_free(p->acquired); + ctx->ra->fns->destroy(ctx->ra); + ctx->ra = NULL; + } + + talloc_free(ctx->swapchain); + ctx->swapchain = NULL; +} + +static const struct ra_swapchain_fns vulkan_swapchain; + +bool ra_vk_ctx_init(struct ra_ctx *ctx, struct mpvk_ctx *vk, + VkPresentModeKHR preferred_mode) +{ + struct ra_swapchain *sw = ctx->swapchain = talloc_zero(NULL, struct ra_swapchain); + sw->ctx = ctx; + sw->fns = &vulkan_swapchain; + + struct priv *p = sw->priv = talloc_zero(sw, struct priv); + p->vk = vk; + p->opts = mp_get_config_group(p, ctx->global, &vulkan_conf); + + if (!mpvk_find_phys_device(vk, p->opts->device, ctx->opts.allow_sw)) + goto error; + if (!mpvk_pick_surface_format(vk)) + goto error; + if (!mpvk_device_init(vk, p->opts->dev_opts)) + goto error; + + ctx->ra = ra_create_vk(vk, ctx->log); + if (!ctx->ra) + goto error; + + static const VkPresentModeKHR present_modes[SWAP_COUNT] = { + [SWAP_FIFO] = VK_PRESENT_MODE_FIFO_KHR, + [SWAP_FIFO_RELAXED] = VK_PRESENT_MODE_FIFO_RELAXED_KHR, + [SWAP_MAILBOX] = VK_PRESENT_MODE_MAILBOX_KHR, + [SWAP_IMMEDIATE] = VK_PRESENT_MODE_IMMEDIATE_KHR, + }; + + p->protoInfo = (VkSwapchainCreateInfoKHR) { + .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, + .surface = vk->surf, + .imageFormat = vk->surf_format.format, + .imageColorSpace = vk->surf_format.colorSpace, + .imageArrayLayers = 1, // non-stereoscopic + .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, + .minImageCount = ctx->opts.swapchain_depth + 1, // +1 for FB + .presentMode = p->opts->swap_mode ? present_modes[p->opts->swap_mode] + : preferred_mode, + .clipped = true, + }; + + // Make sure the swapchain present mode is supported + int num_modes; + VK(vkGetPhysicalDeviceSurfacePresentModesKHR(vk->physd, vk->surf, + &num_modes, NULL)); + VkPresentModeKHR *modes = talloc_array(NULL, VkPresentModeKHR, num_modes); + VK(vkGetPhysicalDeviceSurfacePresentModesKHR(vk->physd, vk->surf, + &num_modes, modes)); + bool supported = false; + for (int i = 0; i < num_modes; i++) + supported |= (modes[i] == p->protoInfo.presentMode); + talloc_free(modes); + + if (!supported) { + MP_ERR(ctx, "Requested swap mode unsupported by this device!\n"); + goto error; + } + + return true; + +error: + ra_vk_ctx_uninit(ctx); + return false; +} + +static void destroy_swapchain(struct mpvk_ctx *vk, struct priv *p) +{ + assert(p->old_swapchain); + vkDestroySwapchainKHR(vk->dev, p->old_swapchain, MPVK_ALLOCATOR); + p->old_swapchain = NULL; +} + +bool ra_vk_ctx_resize(struct ra_swapchain *sw, int w, int h) +{ + struct priv *p = sw->priv; + if (w == p->w && h == p->h) + return true; + + struct ra *ra = sw->ctx->ra; + struct mpvk_ctx *vk = p->vk; + VkImage *vkimages = NULL; + + // It's invalid to trigger another swapchain recreation while there's + // more than one swapchain already active, so we need to flush any pending + // asynchronous swapchain release operations that may be ongoing. + while (p->old_swapchain) + mpvk_dev_poll_cmds(vk, 100000); // 100μs + + VkSwapchainCreateInfoKHR sinfo = p->protoInfo; + sinfo.imageExtent = (VkExtent2D){ w, h }; + sinfo.oldSwapchain = p->swapchain; + + if (!update_swapchain_info(p, &sinfo)) + goto error; + + VK(vkCreateSwapchainKHR(vk->dev, &sinfo, MPVK_ALLOCATOR, &p->swapchain)); + p->w = w; + p->h = h; + + // Freeing the old swapchain while it's still in use is an error, so do + // it asynchronously once the device is idle. + if (sinfo.oldSwapchain) { + p->old_swapchain = sinfo.oldSwapchain; + vk_dev_callback(vk, (vk_cb) destroy_swapchain, vk, p); + } + + // Get the new swapchain images + int num; + VK(vkGetSwapchainImagesKHR(vk->dev, p->swapchain, &num, NULL)); + vkimages = talloc_array(NULL, VkImage, num); + VK(vkGetSwapchainImagesKHR(vk->dev, p->swapchain, &num, vkimages)); + + // If needed, allocate some more semaphores + while (num > p->num_acquired) { + VkSemaphore sem; + static const VkSemaphoreCreateInfo seminfo = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + }; + VK(vkCreateSemaphore(vk->dev, &seminfo, MPVK_ALLOCATOR, &sem)); + MP_TARRAY_APPEND(NULL, p->acquired, p->num_acquired, sem); + } + + // Recreate the ra_tex wrappers + for (int i = 0; i < p->num_images; i++) + ra_tex_free(ra, &p->images[i]); + + p->num_images = num; + MP_TARRAY_GROW(NULL, p->images, p->num_images); + for (int i = 0; i < num; i++) { + p->images[i] = ra_vk_wrap_swapchain_img(ra, vkimages[i], sinfo); + if (!p->images[i]) + goto error; + } + + talloc_free(vkimages); + return true; + +error: + talloc_free(vkimages); + vkDestroySwapchainKHR(vk->dev, p->swapchain, MPVK_ALLOCATOR); + p->swapchain = NULL; + return false; +} + +static int color_depth(struct ra_swapchain *sw) +{ + struct priv *p = sw->priv; + int bits = 0; + + if (!p->num_images) + return bits; + + // The channel with the most bits is probably the most authoritative about + // the actual color information (consider e.g. a2bgr10). Slight downside + // in that it results in rounding r/b for e.g. rgb565, but we don't pick + // surfaces with fewer than 8 bits anyway. + const struct ra_format *fmt = p->images[0]->params.format; + for (int i = 0; i < fmt->num_components; i++) { + int depth = fmt->component_depth[i]; + bits = MPMAX(bits, depth ? depth : fmt->component_size[i]); + } + + return bits; +} + +static bool start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo) +{ + struct priv *p = sw->priv; + struct mpvk_ctx *vk = p->vk; + if (!p->swapchain) + goto error; + + uint32_t imgidx = 0; + MP_TRACE(vk, "vkAcquireNextImageKHR\n"); + VkResult res = vkAcquireNextImageKHR(vk->dev, p->swapchain, UINT64_MAX, + p->acquired[p->idx_acquired], NULL, + &imgidx); + if (res == VK_ERROR_OUT_OF_DATE_KHR) + goto error; // just return in this case + VK_ASSERT(res, "Failed acquiring swapchain image"); + + p->last_imgidx = imgidx; + *out_fbo = (struct ra_fbo) { + .tex = p->images[imgidx], + .flip = false, + }; + return true; + +error: + return false; +} + +static bool submit_frame(struct ra_swapchain *sw, const struct vo_frame *frame) +{ + struct priv *p = sw->priv; + struct ra *ra = sw->ctx->ra; + struct mpvk_ctx *vk = p->vk; + if (!p->swapchain) + goto error; + + VkSemaphore acquired = p->acquired[p->idx_acquired++]; + p->idx_acquired %= p->num_acquired; + + VkSemaphore done; + if (!ra_vk_submit(ra, p->images[p->last_imgidx], acquired, &done, + &p->frames_in_flight)) + goto error; + + // For some reason, nvidia absolutely shits itself when presenting from a + // full queue - so advance all of the cmdpool indices first and then do the + // present on an "empty" queue + vk_cmd_cycle_queues(vk); + struct vk_cmdpool *pool = vk->pool; + VkQueue queue = pool->queues[pool->qindex]; + + VkPresentInfoKHR pinfo = { + .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + .waitSemaphoreCount = 1, + .pWaitSemaphores = &done, + .swapchainCount = 1, + .pSwapchains = &p->swapchain, + .pImageIndices = &p->last_imgidx, + }; + + VK(vkQueuePresentKHR(queue, &pinfo)); + return true; + +error: + return false; +} + +static void swap_buffers(struct ra_swapchain *sw) +{ + struct priv *p = sw->priv; + + while (p->frames_in_flight >= sw->ctx->opts.swapchain_depth) + mpvk_dev_poll_cmds(p->vk, 100000); // 100μs +} + +static const struct ra_swapchain_fns vulkan_swapchain = { + // .screenshot is not currently supported + .color_depth = color_depth, + .start_frame = start_frame, + .submit_frame = submit_frame, + .swap_buffers = swap_buffers, +}; diff --git a/video/out/vulkan/context.h b/video/out/vulkan/context.h new file mode 100644 index 0000000000..3f630bc10e --- /dev/null +++ b/video/out/vulkan/context.h @@ -0,0 +1,10 @@ +#pragma once + +#include "video/out/gpu/context.h" +#include "common.h" + +// Helpers for ra_ctx based on ra_vk. These initialize ctx->ra and ctx->swchain. +void ra_vk_ctx_uninit(struct ra_ctx *ctx); +bool ra_vk_ctx_init(struct ra_ctx *ctx, struct mpvk_ctx *vk, + VkPresentModeKHR preferred_mode); +bool ra_vk_ctx_resize(struct ra_swapchain *sw, int w, int h); diff --git a/video/out/vulkan/context_xlib.c b/video/out/vulkan/context_xlib.c new file mode 100644 index 0000000000..2611fbb706 --- /dev/null +++ b/video/out/vulkan/context_xlib.c @@ -0,0 +1,116 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include "video/out/gpu/context.h" +#include "video/out/x11_common.h" + +#include "common.h" +#include "context.h" +#include "utils.h" + +struct priv { + struct mpvk_ctx vk; +}; + +static void xlib_uninit(struct ra_ctx *ctx) +{ + struct priv *p = ctx->priv; + + ra_vk_ctx_uninit(ctx); + mpvk_uninit(&p->vk); + vo_x11_uninit(ctx->vo); +} + +static bool xlib_init(struct ra_ctx *ctx) +{ + struct priv *p = ctx->priv = talloc_zero(ctx, struct priv); + struct mpvk_ctx *vk = &p->vk; + int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR; + + if (!vo_x11_init(ctx->vo)) + goto error; + + if (!vo_x11_create_vo_window(ctx->vo, NULL, "mpvk")) + goto error; + + if (!mpvk_instance_init(vk, ctx->log, ctx->opts.debug)) + goto error; + + VkXlibSurfaceCreateInfoKHR xinfo = { + .sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR, + .dpy = ctx->vo->x11->display, + .window = ctx->vo->x11->window, + }; + + VkResult res = vkCreateXlibSurfaceKHR(vk->inst, &xinfo, MPVK_ALLOCATOR, + &vk->surf); + if (res != VK_SUCCESS) { + MP_MSG(ctx, msgl, "Failed creating Xlib surface: %s\n", vk_err(res)); + goto error; + } + + if (!ra_vk_ctx_init(ctx, vk, VK_PRESENT_MODE_FIFO_KHR)) + goto error; + + return true; + +error: + xlib_uninit(ctx); + return false; +} + +static bool resize(struct ra_ctx *ctx) +{ + return ra_vk_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight); +} + +static bool xlib_reconfig(struct ra_ctx *ctx) +{ + vo_x11_config_vo_window(ctx->vo); + return resize(ctx); +} + +static int xlib_control(struct ra_ctx *ctx, int *events, int request, void *arg) +{ + int ret = vo_x11_control(ctx->vo, events, request, arg); + if (*events & VO_EVENT_RESIZE) { + if (!resize(ctx)) + return VO_ERROR; + } + return ret; +} + +static void xlib_wakeup(struct ra_ctx *ctx) +{ + vo_x11_wakeup(ctx->vo); +} + +static void xlib_wait_events(struct ra_ctx *ctx, int64_t until_time_us) +{ + vo_x11_wait_events(ctx->vo, until_time_us); +} + +const struct ra_ctx_fns ra_ctx_vulkan_xlib = { + .type = "vulkan", + .name = "x11", + .reconfig = xlib_reconfig, + .control = xlib_control, + .wakeup = xlib_wakeup, + .wait_events = xlib_wait_events, + .init = xlib_init, + .uninit = xlib_uninit, +}; diff --git a/video/out/vulkan/formats.c b/video/out/vulkan/formats.c new file mode 100644 index 0000000000..b44bead99c --- /dev/null +++ b/video/out/vulkan/formats.c @@ -0,0 +1,55 @@ +#include "formats.h" + +const struct vk_format vk_formats[] = { + // Regular, byte-aligned integer formats + {"r8", VK_FORMAT_R8_UNORM, 1, 1, {8 }, RA_CTYPE_UNORM }, + {"rg8", VK_FORMAT_R8G8_UNORM, 2, 2, {8, 8 }, RA_CTYPE_UNORM }, + {"rgb8", VK_FORMAT_R8G8B8_UNORM, 3, 3, {8, 8, 8 }, RA_CTYPE_UNORM }, + {"rgba8", VK_FORMAT_R8G8B8A8_UNORM, 4, 4, {8, 8, 8, 8 }, RA_CTYPE_UNORM }, + {"r16", VK_FORMAT_R16_UNORM, 1, 2, {16 }, RA_CTYPE_UNORM }, + {"rg16", VK_FORMAT_R16G16_UNORM, 2, 4, {16, 16 }, RA_CTYPE_UNORM }, + {"rgb16", VK_FORMAT_R16G16B16_UNORM, 3, 6, {16, 16, 16 }, RA_CTYPE_UNORM }, + {"rgba16", VK_FORMAT_R16G16B16A16_UNORM, 4, 8, {16, 16, 16, 16}, RA_CTYPE_UNORM }, + + // Special, integer-only formats + {"r32ui", VK_FORMAT_R32_UINT, 1, 4, {32 }, RA_CTYPE_UINT }, + {"rg32ui", VK_FORMAT_R32G32_UINT, 2, 8, {32, 32 }, RA_CTYPE_UINT }, + {"rgb32ui", VK_FORMAT_R32G32B32_UINT, 3, 12, {32, 32, 32 }, RA_CTYPE_UINT }, + {"rgba32ui", VK_FORMAT_R32G32B32A32_UINT, 4, 16, {32, 32, 32, 32}, RA_CTYPE_UINT }, + {"r64ui", VK_FORMAT_R64_UINT, 1, 8, {64 }, RA_CTYPE_UINT }, + {"rg64ui", VK_FORMAT_R64G64_UINT, 2, 16, {64, 64 }, RA_CTYPE_UINT }, + {"rgb64ui", VK_FORMAT_R64G64B64_UINT, 3, 24, {64, 64, 64 }, RA_CTYPE_UINT }, + {"rgba64ui", VK_FORMAT_R64G64B64A64_UINT, 4, 32, {64, 64, 64, 64}, RA_CTYPE_UINT }, + + // Packed integer formats + {"rg4", VK_FORMAT_R4G4_UNORM_PACK8, 2, 1, {4, 4 }, RA_CTYPE_UNORM }, + {"rgba4", VK_FORMAT_R4G4B4A4_UNORM_PACK16, 4, 2, {4, 4, 4, 4 }, RA_CTYPE_UNORM }, + {"rgb565", VK_FORMAT_R5G6B5_UNORM_PACK16, 3, 2, {5, 6, 5 }, RA_CTYPE_UNORM }, + {"rgb565a1", VK_FORMAT_R5G5B5A1_UNORM_PACK16, 4, 2, {5, 5, 5, 1 }, RA_CTYPE_UNORM }, + + // Float formats (native formats, hf = half float, df = double float) + {"r16hf", VK_FORMAT_R16_SFLOAT, 1, 2, {16 }, RA_CTYPE_FLOAT }, + {"rg16hf", VK_FORMAT_R16G16_SFLOAT, 2, 4, {16, 16 }, RA_CTYPE_FLOAT }, + {"rgb16hf", VK_FORMAT_R16G16B16_SFLOAT, 3, 6, {16, 16, 16 }, RA_CTYPE_FLOAT }, + {"rgba16hf", VK_FORMAT_R16G16B16A16_SFLOAT, 4, 8, {16, 16, 16, 16}, RA_CTYPE_FLOAT }, + {"r32f", VK_FORMAT_R32_SFLOAT, 1, 4, {32 }, RA_CTYPE_FLOAT }, + {"rg32f", VK_FORMAT_R32G32_SFLOAT, 2, 8, {32, 32 }, RA_CTYPE_FLOAT }, + {"rgb32f", VK_FORMAT_R32G32B32_SFLOAT, 3, 12, {32, 32, 32 }, RA_CTYPE_FLOAT }, + {"rgba32f", VK_FORMAT_R32G32B32A32_SFLOAT, 4, 16, {32, 32, 32, 32}, RA_CTYPE_FLOAT }, + {"r64df", VK_FORMAT_R64_SFLOAT, 1, 8, {64 }, RA_CTYPE_FLOAT }, + {"rg64df", VK_FORMAT_R64G64_SFLOAT, 2, 16, {64, 64 }, RA_CTYPE_FLOAT }, + {"rgb64df", VK_FORMAT_R64G64B64_SFLOAT, 3, 24, {64, 64, 64 }, RA_CTYPE_FLOAT }, + {"rgba64df", VK_FORMAT_R64G64B64A64_SFLOAT, 4, 32, {64, 64, 64, 64}, RA_CTYPE_FLOAT }, + + // "Swapped" component order images + {"bgr8", VK_FORMAT_B8G8R8_UNORM, 3, 3, {8, 8, 8 }, RA_CTYPE_UNORM, true }, + {"bgra8", VK_FORMAT_B8G8R8A8_UNORM, 4, 4, {8, 8, 8, 8 }, RA_CTYPE_UNORM, true }, + {"bgra4", VK_FORMAT_B4G4R4A4_UNORM_PACK16, 4, 2, {4, 4, 4, 4 }, RA_CTYPE_UNORM, true }, + {"bgr565", VK_FORMAT_B5G6R5_UNORM_PACK16, 3, 2, {5, 6, 5 }, RA_CTYPE_UNORM, true }, + {"bgr565a1", VK_FORMAT_B5G5R5A1_UNORM_PACK16, 4, 2, {5, 5, 5, 1 }, RA_CTYPE_UNORM, true }, + {"a1rgb5", VK_FORMAT_A1R5G5B5_UNORM_PACK16, 4, 2, {1, 5, 5, 5 }, RA_CTYPE_UNORM, true }, + {"a2rgb10", VK_FORMAT_A2R10G10B10_UNORM_PACK32, 4, 4, {2, 10, 10, 10}, RA_CTYPE_UNORM, true }, + {"a2bgr10", VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4, 4, {2, 10, 10, 10}, RA_CTYPE_UNORM, true }, + {"abgr8", VK_FORMAT_A8B8G8R8_UNORM_PACK32, 4, 4, {8, 8, 8, 8 }, RA_CTYPE_UNORM, true }, + {0} +}; diff --git a/video/out/vulkan/formats.h b/video/out/vulkan/formats.h new file mode 100644 index 0000000000..22782a6958 --- /dev/null +++ b/video/out/vulkan/formats.h @@ -0,0 +1,16 @@ +#pragma once + +#include "video/out/gpu/ra.h" +#include "common.h" + +struct vk_format { + const char *name; + VkFormat iformat; // vulkan format enum + int components; // how many components are there + int bytes; // how many bytes is a texel + int bits[4]; // how many bits per component + enum ra_ctype ctype; // format representation type + bool fucked_order; // used for formats which are not simply rgba +}; + +extern const struct vk_format vk_formats[]; diff --git a/video/out/vulkan/malloc.c b/video/out/vulkan/malloc.c new file mode 100644 index 0000000000..31fcd36ddb --- /dev/null +++ b/video/out/vulkan/malloc.c @@ -0,0 +1,424 @@ +#include "malloc.h" +#include "utils.h" +#include "osdep/timer.h" + +// Controls the multiplication factor for new slab allocations. The new slab +// will always be allocated such that the size of the slab is this factor times +// the previous slab. Higher values make it grow faster. +#define MPVK_HEAP_SLAB_GROWTH_RATE 4 + +// Controls the minimum slab size, to reduce the frequency at which very small +// slabs would need to get allocated when allocating the first few buffers. +// (Default: 1 MB) +#define MPVK_HEAP_MINIMUM_SLAB_SIZE (1 << 20) + +// Controls the maximum slab size, to reduce the effect of unbounded slab +// growth exhausting memory. If the application needs a single allocation +// that's bigger than this value, it will be allocated directly from the +// device. (Default: 512 MB) +#define MPVK_HEAP_MAXIMUM_SLAB_SIZE (1 << 29) + +// Controls the minimum free region size, to reduce thrashing the free space +// map with lots of small buffers during uninit. (Default: 1 KB) +#define MPVK_HEAP_MINIMUM_REGION_SIZE (1 << 10) + +// Represents a region of available memory +struct vk_region { + size_t start; // first offset in region + size_t end; // first offset *not* in region +}; + +static inline size_t region_len(struct vk_region r) +{ + return r.end - r.start; +} + +// A single slab represents a contiguous region of allocated memory. Actual +// allocations are served as slices of this. Slabs are organized into linked +// lists, which represent individual heaps. +struct vk_slab { + VkDeviceMemory mem; // underlying device allocation + size_t size; // total size of `slab` + size_t used; // number of bytes actually in use (for GC accounting) + bool dedicated; // slab is allocated specifically for one object + // free space map: a sorted list of memory regions that are available + struct vk_region *regions; + int num_regions; + // optional, depends on the memory type: + VkBuffer buffer; // buffer spanning the entire slab + void *data; // mapped memory corresponding to `mem` +}; + +// Represents a single memory heap. We keep track of a vk_heap for each +// combination of buffer type and memory selection parameters. This shouldn't +// actually be that many in practice, because some combinations simply never +// occur, and others will generally be the same for the same objects. +struct vk_heap { + VkBufferUsageFlagBits usage; // the buffer usage type (or 0) + VkMemoryPropertyFlagBits flags; // the memory type flags (or 0) + uint32_t typeBits; // the memory type index requirements (or 0) + struct vk_slab **slabs; // array of slabs sorted by size + int num_slabs; +}; + +// The overall state of the allocator, which keeps track of a vk_heap for each +// memory type. +struct vk_malloc { + VkPhysicalDeviceMemoryProperties props; + struct vk_heap *heaps; + int num_heaps; +}; + +static void slab_free(struct mpvk_ctx *vk, struct vk_slab *slab) +{ + if (!slab) + return; + + assert(slab->used == 0); + + int64_t start = mp_time_us(); + vkDestroyBuffer(vk->dev, slab->buffer, MPVK_ALLOCATOR); + // also implicitly unmaps the memory if needed + vkFreeMemory(vk->dev, slab->mem, MPVK_ALLOCATOR); + int64_t stop = mp_time_us(); + + MP_VERBOSE(vk, "Freeing slab of size %zu took %lld μs.\n", + slab->size, (long long)(stop - start)); + + talloc_free(slab); +} + +static bool find_best_memtype(struct mpvk_ctx *vk, uint32_t typeBits, + VkMemoryPropertyFlagBits flags, + VkMemoryType *out_type, int *out_index) +{ + struct vk_malloc *ma = vk->alloc; + + // The vulkan spec requires memory types to be sorted in the "optimal" + // order, so the first matching type we find will be the best/fastest one. + for (int i = 0; i < ma->props.memoryTypeCount; i++) { + // The memory type flags must include our properties + if ((ma->props.memoryTypes[i].propertyFlags & flags) != flags) + continue; + // The memory type must be supported by the requirements (bitfield) + if (typeBits && !(typeBits & (1 << i))) + continue; + *out_type = ma->props.memoryTypes[i]; + *out_index = i; + return true; + } + + MP_ERR(vk, "Found no memory type matching property flags 0x%x and type " + "bits 0x%x!\n", flags, (unsigned)typeBits); + return false; +} + +static struct vk_slab *slab_alloc(struct mpvk_ctx *vk, struct vk_heap *heap, + size_t size) +{ + struct vk_slab *slab = talloc_ptrtype(NULL, slab); + *slab = (struct vk_slab) { + .size = size, + }; + + MP_TARRAY_APPEND(slab, slab->regions, slab->num_regions, (struct vk_region) { + .start = 0, + .end = slab->size, + }); + + VkMemoryAllocateInfo minfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = slab->size, + }; + + uint32_t typeBits = heap->typeBits ? heap->typeBits : UINT32_MAX; + if (heap->usage) { + VkBufferCreateInfo binfo = { + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .size = slab->size, + .usage = heap->usage, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + }; + + VK(vkCreateBuffer(vk->dev, &binfo, MPVK_ALLOCATOR, &slab->buffer)); + + VkMemoryRequirements reqs; + vkGetBufferMemoryRequirements(vk->dev, slab->buffer, &reqs); + minfo.allocationSize = reqs.size; // this can be larger than slab->size + typeBits &= reqs.memoryTypeBits; // this can restrict the types + } + + VkMemoryType type; + int index; + if (!find_best_memtype(vk, typeBits, heap->flags, &type, &index)) + goto error; + + MP_VERBOSE(vk, "Allocating %zu memory of type 0x%x (id %d) in heap %d.\n", + slab->size, type.propertyFlags, index, (int)type.heapIndex); + + minfo.memoryTypeIndex = index; + VK(vkAllocateMemory(vk->dev, &minfo, MPVK_ALLOCATOR, &slab->mem)); + + if (heap->flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) + VK(vkMapMemory(vk->dev, slab->mem, 0, VK_WHOLE_SIZE, 0, &slab->data)); + + if (slab->buffer) + VK(vkBindBufferMemory(vk->dev, slab->buffer, slab->mem, 0)); + + return slab; + +error: + slab_free(vk, slab); + return NULL; +} + +static void insert_region(struct vk_slab *slab, struct vk_region region) +{ + if (region.start == region.end) + return; + + bool big_enough = region_len(region) >= MPVK_HEAP_MINIMUM_REGION_SIZE; + + // Find the index of the first region that comes after this + for (int i = 0; i < slab->num_regions; i++) { + struct vk_region *r = &slab->regions[i]; + + // Check for a few special cases which can be coalesced + if (r->end == region.start) { + // The new region is at the tail of this region. In addition to + // modifying this region, we also need to coalesce all the following + // regions for as long as possible + r->end = region.end; + + struct vk_region *next = &slab->regions[i+1]; + while (i+1 < slab->num_regions && r->end == next->start) { + r->end = next->end; + MP_TARRAY_REMOVE_AT(slab->regions, slab->num_regions, i+1); + } + return; + } + + if (r->start == region.end) { + // The new region is at the head of this region. We don't need to + // do anything special here - because if this could be further + // coalesced backwards, the previous loop iteration would already + // have caught it. + r->start = region.start; + return; + } + + if (r->start > region.start) { + // The new region comes somewhere before this region, so insert + // it into this index in the array. + if (big_enough) { + MP_TARRAY_INSERT_AT(slab, slab->regions, slab->num_regions, + i, region); + } + return; + } + } + + // If we've reached the end of this loop, then all of the regions + // come before the new region, and are disconnected - so append it + if (big_enough) + MP_TARRAY_APPEND(slab, slab->regions, slab->num_regions, region); +} + +static void heap_uninit(struct mpvk_ctx *vk, struct vk_heap *heap) +{ + for (int i = 0; i < heap->num_slabs; i++) + slab_free(vk, heap->slabs[i]); + + talloc_free(heap->slabs); + *heap = (struct vk_heap){0}; +} + +void vk_malloc_init(struct mpvk_ctx *vk) +{ + assert(vk->physd); + vk->alloc = talloc_zero(NULL, struct vk_malloc); + vkGetPhysicalDeviceMemoryProperties(vk->physd, &vk->alloc->props); +} + +void vk_malloc_uninit(struct mpvk_ctx *vk) +{ + struct vk_malloc *ma = vk->alloc; + if (!ma) + return; + + for (int i = 0; i < ma->num_heaps; i++) + heap_uninit(vk, &ma->heaps[i]); + + talloc_free(ma); + vk->alloc = NULL; +} + +void vk_free_memslice(struct mpvk_ctx *vk, struct vk_memslice slice) +{ + struct vk_slab *slab = slice.priv; + if (!slab) + return; + + assert(slab->used >= slice.size); + slab->used -= slice.size; + + MP_DBG(vk, "Freeing slice %zu + %zu from slab with size %zu\n", + slice.offset, slice.size, slab->size); + + if (slab->dedicated) { + // If the slab was purpose-allocated for this memslice, we can just + // free it here + slab_free(vk, slab); + } else { + // Return the allocation to the free space map + insert_region(slab, (struct vk_region) { + .start = slice.offset, + .end = slice.offset + slice.size, + }); + } +} + +// reqs: can be NULL +static struct vk_heap *find_heap(struct mpvk_ctx *vk, + VkBufferUsageFlagBits usage, + VkMemoryPropertyFlagBits flags, + VkMemoryRequirements *reqs) +{ + struct vk_malloc *ma = vk->alloc; + int typeBits = reqs ? reqs->memoryTypeBits : 0; + + for (int i = 0; i < ma->num_heaps; i++) { + if (ma->heaps[i].usage != usage) + continue; + if (ma->heaps[i].flags != flags) + continue; + if (ma->heaps[i].typeBits != typeBits) + continue; + return &ma->heaps[i]; + } + + // Not found => add it + MP_TARRAY_GROW(ma, ma->heaps, ma->num_heaps + 1); + struct vk_heap *heap = &ma->heaps[ma->num_heaps++]; + *heap = (struct vk_heap) { + .usage = usage, + .flags = flags, + .typeBits = typeBits, + }; + return heap; +} + +static inline bool region_fits(struct vk_region r, size_t size, size_t align) +{ + return MP_ALIGN_UP(r.start, align) + size <= r.end; +} + +// Finds the best-fitting region in a heap. If the heap is too small or too +// fragmented, a new slab will be allocated under the hood. +static bool heap_get_region(struct mpvk_ctx *vk, struct vk_heap *heap, + size_t size, size_t align, + struct vk_slab **out_slab, int *out_index) +{ + struct vk_slab *slab = NULL; + + // If the allocation is very big, serve it directly instead of bothering + // with the heap + if (size > MPVK_HEAP_MAXIMUM_SLAB_SIZE) { + slab = slab_alloc(vk, heap, size); + *out_slab = slab; + *out_index = 0; + return !!slab; + } + + for (int i = 0; i < heap->num_slabs; i++) { + slab = heap->slabs[i]; + if (slab->size < size) + continue; + + // Attempt a best fit search + int best = -1; + for (int n = 0; n < slab->num_regions; n++) { + struct vk_region r = slab->regions[n]; + if (!region_fits(r, size, align)) + continue; + if (best >= 0 && region_len(r) > region_len(slab->regions[best])) + continue; + best = n; + } + + if (best >= 0) { + *out_slab = slab; + *out_index = best; + return true; + } + } + + // Otherwise, allocate a new vk_slab and append it to the list. + size_t cur_size = MPMAX(size, slab ? slab->size : 0); + size_t slab_size = MPVK_HEAP_SLAB_GROWTH_RATE * cur_size; + slab_size = MPMAX(MPVK_HEAP_MINIMUM_SLAB_SIZE, slab_size); + slab_size = MPMIN(MPVK_HEAP_MAXIMUM_SLAB_SIZE, slab_size); + assert(slab_size >= size); + slab = slab_alloc(vk, heap, slab_size); + if (!slab) + return false; + MP_TARRAY_APPEND(NULL, heap->slabs, heap->num_slabs, slab); + + // Return the only region there is in a newly allocated slab + assert(slab->num_regions == 1); + *out_slab = slab; + *out_index = 0; + return true; +} + +static bool slice_heap(struct mpvk_ctx *vk, struct vk_heap *heap, size_t size, + size_t alignment, struct vk_memslice *out) +{ + struct vk_slab *slab; + int index; + alignment = MP_ALIGN_UP(alignment, vk->limits.bufferImageGranularity); + if (!heap_get_region(vk, heap, size, alignment, &slab, &index)) + return false; + + struct vk_region reg = slab->regions[index]; + MP_TARRAY_REMOVE_AT(slab->regions, slab->num_regions, index); + *out = (struct vk_memslice) { + .vkmem = slab->mem, + .offset = MP_ALIGN_UP(reg.start, alignment), + .size = size, + .priv = slab, + }; + + MP_DBG(vk, "Sub-allocating slice %zu + %zu from slab with size %zu\n", + out->offset, out->size, slab->size); + + size_t out_end = out->offset + out->size; + insert_region(slab, (struct vk_region) { reg.start, out->offset }); + insert_region(slab, (struct vk_region) { out_end, reg.end }); + + slab->used += size; + return true; +} + +bool vk_malloc_generic(struct mpvk_ctx *vk, VkMemoryRequirements reqs, + VkMemoryPropertyFlagBits flags, struct vk_memslice *out) +{ + struct vk_heap *heap = find_heap(vk, 0, flags, &reqs); + return slice_heap(vk, heap, reqs.size, reqs.alignment, out); +} + +bool vk_malloc_buffer(struct mpvk_ctx *vk, VkBufferUsageFlagBits bufFlags, + VkMemoryPropertyFlagBits memFlags, VkDeviceSize size, + VkDeviceSize alignment, struct vk_bufslice *out) +{ + struct vk_heap *heap = find_heap(vk, bufFlags, memFlags, NULL); + if (!slice_heap(vk, heap, size, alignment, &out->mem)) + return false; + + struct vk_slab *slab = out->mem.priv; + out->buf = slab->buffer; + if (slab->data) + out->data = (void *)((uintptr_t)slab->data + (ptrdiff_t)out->mem.offset); + + return true; +} diff --git a/video/out/vulkan/malloc.h b/video/out/vulkan/malloc.h new file mode 100644 index 0000000000..65c1036929 --- /dev/null +++ b/video/out/vulkan/malloc.h @@ -0,0 +1,35 @@ +#pragma once + +#include "common.h" + +void vk_malloc_init(struct mpvk_ctx *vk); +void vk_malloc_uninit(struct mpvk_ctx *vk); + +// Represents a single "slice" of generic (non-buffer) memory, plus some +// metadata for accounting. This struct is essentially read-only. +struct vk_memslice { + VkDeviceMemory vkmem; + size_t offset; + size_t size; + void *priv; +}; + +void vk_free_memslice(struct mpvk_ctx *vk, struct vk_memslice slice); +bool vk_malloc_generic(struct mpvk_ctx *vk, VkMemoryRequirements reqs, + VkMemoryPropertyFlagBits flags, struct vk_memslice *out); + +// Represents a single "slice" of a larger buffer +struct vk_bufslice { + struct vk_memslice mem; // must be freed by the user when done + VkBuffer buf; // the buffer this memory was sliced from + // For persistently mapped buffers, this points to the first usable byte of + // this slice. + void *data; +}; + +// Allocate a buffer slice. This is more efficient than vk_malloc_generic for +// when the user needs lots of buffers, since it doesn't require +// creating/destroying lots of (little) VkBuffers. +bool vk_malloc_buffer(struct mpvk_ctx *vk, VkBufferUsageFlagBits bufFlags, + VkMemoryPropertyFlagBits memFlags, VkDeviceSize size, + VkDeviceSize alignment, struct vk_bufslice *out); diff --git a/video/out/vulkan/ra_vk.c b/video/out/vulkan/ra_vk.c new file mode 100644 index 0000000000..ce0cbc66e9 --- /dev/null +++ b/video/out/vulkan/ra_vk.c @@ -0,0 +1,1590 @@ +#include "ra_vk.h" +#include "malloc.h" +#include "video/out/opengl/utils.h" + +static struct ra_fns ra_fns_vk; + +// For ra.priv +struct ra_vk { + struct mpvk_ctx *vk; + struct ra_tex *clear_tex; // stupid hack for clear() + struct vk_cmd *cmd; // currently recording cmd +}; + +struct mpvk_ctx *ra_vk_get(struct ra *ra) +{ + if (ra->fns != &ra_fns_vk) + return NULL; + + struct ra_vk *p = ra->priv; + return p->vk; +} + +// Returns a command buffer, or NULL on error +static struct vk_cmd *vk_require_cmd(struct ra *ra) +{ + struct ra_vk *p = ra->priv; + struct mpvk_ctx *vk = ra_vk_get(ra); + + if (!p->cmd) + p->cmd = vk_cmd_begin(vk, vk->pool); + + return p->cmd; +} + +// Note: This technically follows the flush() API, but we don't need +// to expose that (and in fact, it's a bad idea) since we control flushing +// behavior with ra_vk_present_frame already. +static bool vk_flush(struct ra *ra, VkSemaphore *done) +{ + struct ra_vk *p = ra->priv; + struct mpvk_ctx *vk = ra_vk_get(ra); + + if (p->cmd) { + if (!vk_cmd_submit(vk, p->cmd, done)) + return false; + p->cmd = NULL; + } + + return true; +} + +// The callback's *priv will always be set to `ra` +static void vk_callback(struct ra *ra, vk_cb callback, void *arg) +{ + struct ra_vk *p = ra->priv; + struct mpvk_ctx *vk = ra_vk_get(ra); + + if (p->cmd) { + vk_cmd_callback(p->cmd, callback, ra, arg); + } else { + vk_dev_callback(vk, callback, ra, arg); + } +} + +#define MAKE_LAZY_DESTRUCTOR(fun, argtype) \ + static void fun##_lazy(struct ra *ra, argtype *arg) { \ + vk_callback(ra, (vk_cb) fun, arg); \ + } + +static void vk_destroy_ra(struct ra *ra) +{ + struct ra_vk *p = ra->priv; + struct mpvk_ctx *vk = ra_vk_get(ra); + + vk_flush(ra, NULL); + mpvk_dev_wait_idle(vk); + ra_tex_free(ra, &p->clear_tex); + + talloc_free(ra); +} + +static bool vk_setup_formats(struct ra *ra) +{ + struct mpvk_ctx *vk = ra_vk_get(ra); + + for (const struct vk_format *vk_fmt = vk_formats; vk_fmt->name; vk_fmt++) { + VkFormatProperties prop; + vkGetPhysicalDeviceFormatProperties(vk->physd, vk_fmt->iformat, &prop); + + // As a bare minimum, we need to sample from an allocated image + VkFormatFeatureFlags flags = prop.optimalTilingFeatures; + if (!(flags & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)) + continue; + + VkFormatFeatureFlags linear_bits, render_bits; + linear_bits = VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; + render_bits = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | + VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT; + + struct ra_format *fmt = talloc_zero(ra, struct ra_format); + *fmt = (struct ra_format) { + .name = vk_fmt->name, + .priv = (void *)vk_fmt, + .ctype = vk_fmt->ctype, + .ordered = !vk_fmt->fucked_order, + .num_components = vk_fmt->components, + .pixel_size = vk_fmt->bytes, + .linear_filter = !!(flags & linear_bits), + .renderable = !!(flags & render_bits), + }; + + for (int i = 0; i < 4; i++) + fmt->component_size[i] = fmt->component_depth[i] = vk_fmt->bits[i]; + + MP_TARRAY_APPEND(ra, ra->formats, ra->num_formats, fmt); + } + + // Populate some other capabilities related to formats while we're at it + VkImageType imgType[3] = { + VK_IMAGE_TYPE_1D, + VK_IMAGE_TYPE_2D, + VK_IMAGE_TYPE_3D + }; + + // R8_UNORM is supported on literally every single vulkan implementation + const VkFormat testfmt = VK_FORMAT_R8_UNORM; + + for (int d = 0; d < 3; d++) { + VkImageFormatProperties iprop; + VkResult res = vkGetPhysicalDeviceImageFormatProperties(vk->physd, + testfmt, imgType[d], VK_IMAGE_TILING_OPTIMAL, + VK_IMAGE_USAGE_SAMPLED_BIT, 0, &iprop); + + switch (imgType[d]) { + case VK_IMAGE_TYPE_1D: + if (res == VK_SUCCESS) + ra->caps |= RA_CAP_TEX_1D; + break; + case VK_IMAGE_TYPE_2D: + // 2D formats must be supported by RA, so ensure this is the case + VK_ASSERT(res, "Querying 2D format limits"); + ra->max_texture_wh = MPMIN(iprop.maxExtent.width, iprop.maxExtent.height); + break; + case VK_IMAGE_TYPE_3D: + if (res == VK_SUCCESS) + ra->caps |= RA_CAP_TEX_3D; + break; + } + } + + // RA_CAP_BLIT implies both blitting between images as well as blitting + // directly to the swapchain image, so check for all three operations + bool blittable = true; + VkFormatProperties prop; + vkGetPhysicalDeviceFormatProperties(vk->physd, testfmt, &prop); + if (!(prop.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT)) + blittable = false; + if (!(prop.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT)) + blittable = false; + + vkGetPhysicalDeviceFormatProperties(vk->physd, vk->surf_format.format, &prop); + if (!(prop.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT)) + blittable = false; + + if (blittable) + ra->caps |= RA_CAP_BLIT; + + return true; + +error: + return false; +} + +static struct ra_fns ra_fns_vk; + +struct ra *ra_create_vk(struct mpvk_ctx *vk, struct mp_log *log) +{ + assert(vk->dev); + assert(vk->alloc); + + struct ra *ra = talloc_zero(NULL, struct ra); + ra->log = log; + ra->fns = &ra_fns_vk; + + struct ra_vk *p = ra->priv = talloc_zero(ra, struct ra_vk); + p->vk = vk; + + // There's no way to query the supported GLSL version from VK_NV_glsl_shader + // (thanks nvidia), so just pick the GL version that modern nvidia devices + // support.. + ra->glsl_version = 450; + ra->glsl_vulkan = true; + ra->max_shmem = vk->limits.maxComputeSharedMemorySize; + ra->caps = RA_CAP_NESTED_ARRAY; + + if (vk->pool->props.queueFlags & VK_QUEUE_COMPUTE_BIT) + ra->caps |= RA_CAP_COMPUTE; + + if (!vk_setup_formats(ra)) + goto error; + + // UBO support is required + ra->caps |= RA_CAP_BUF_RO; + + // Try creating a shader storage buffer + struct ra_buf_params ssbo_params = { + .type = RA_BUF_TYPE_SHADER_STORAGE, + .size = 16, + }; + + struct ra_buf *ssbo = ra_buf_create(ra, &ssbo_params); + if (ssbo) { + ra->caps |= RA_CAP_BUF_RW; + ra_buf_free(ra, &ssbo); + } + + // To support clear() by region, we need to allocate a dummy 1x1 image that + // will be used as the source of blit operations + struct ra_tex_params clear_params = { + .dimensions = 1, // no point in using a 2D image if height = 1 + .w = 1, + .h = 1, + .d = 1, + .format = ra_find_float16_format(ra, 4), + .blit_src = 1, + .host_mutable = 1, + }; + + p->clear_tex = ra_tex_create(ra, &clear_params); + if (!p->clear_tex) { + MP_ERR(ra, "Failed creating 1x1 dummy texture for clear()!\n"); + goto error; + } + + return ra; + +error: + vk_destroy_ra(ra); + return NULL; +} + +// Boilerplate wrapper around vkCreateRenderPass to ensure passes remain +// compatible +static VkResult vk_create_render_pass(VkDevice dev, const struct ra_format *fmt, + bool load_fbo, VkRenderPass *out) +{ + struct vk_format *vk_fmt = fmt->priv; + assert(fmt->renderable); + + VkRenderPassCreateInfo rinfo = { + .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, + .attachmentCount = 1, + .pAttachments = &(VkAttachmentDescription) { + .format = vk_fmt->iformat, + .samples = VK_SAMPLE_COUNT_1_BIT, + .loadOp = load_fbo ? VK_ATTACHMENT_LOAD_OP_LOAD + : VK_ATTACHMENT_LOAD_OP_DONT_CARE, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + .finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + }, + .subpassCount = 1, + .pSubpasses = &(VkSubpassDescription) { + .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, + .colorAttachmentCount = 1, + .pColorAttachments = &(VkAttachmentReference) { + .attachment = 0, + .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + }, + }, + }; + + return vkCreateRenderPass(dev, &rinfo, MPVK_ALLOCATOR, out); +} + +// For ra_tex.priv +struct ra_tex_vk { + bool external_img; + VkImageType type; + VkImage img; + struct vk_memslice mem; + // for sampling + VkImageView view; + VkSampler sampler; + // for rendering + VkFramebuffer framebuffer; + VkRenderPass dummyPass; + // for uploading + struct ra_buf_pool pbo; + // "current" metadata, can change during the course of execution + VkImageLayout current_layout; + VkPipelineStageFlagBits current_stage; + VkAccessFlagBits current_access; +}; + +// Small helper to ease image barrier creation. if `discard` is set, the contents +// of the image will be undefined after the barrier +static void tex_barrier(struct vk_cmd *cmd, struct ra_tex_vk *tex_vk, + VkPipelineStageFlagBits newStage, + VkAccessFlagBits newAccess, VkImageLayout newLayout, + bool discard) +{ + VkImageMemoryBarrier imgBarrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .oldLayout = tex_vk->current_layout, + .newLayout = newLayout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .srcAccessMask = tex_vk->current_access, + .dstAccessMask = newAccess, + .image = tex_vk->img, + .subresourceRange = vk_range, + }; + + if (discard) { + imgBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imgBarrier.srcAccessMask = 0; + } + + if (imgBarrier.oldLayout != imgBarrier.newLayout || + imgBarrier.srcAccessMask != imgBarrier.dstAccessMask) + { + vkCmdPipelineBarrier(cmd->buf, tex_vk->current_stage, newStage, 0, + 0, NULL, 0, NULL, 1, &imgBarrier); + } + + tex_vk->current_stage = newStage; + tex_vk->current_layout = newLayout; + tex_vk->current_access = newAccess; +} + +static void vk_tex_destroy(struct ra *ra, struct ra_tex *tex) +{ + if (!tex) + return; + + struct mpvk_ctx *vk = ra_vk_get(ra); + struct ra_tex_vk *tex_vk = tex->priv; + + ra_buf_pool_uninit(ra, &tex_vk->pbo); + vkDestroyFramebuffer(vk->dev, tex_vk->framebuffer, MPVK_ALLOCATOR); + vkDestroyRenderPass(vk->dev, tex_vk->dummyPass, MPVK_ALLOCATOR); + vkDestroySampler(vk->dev, tex_vk->sampler, MPVK_ALLOCATOR); + vkDestroyImageView(vk->dev, tex_vk->view, MPVK_ALLOCATOR); + if (!tex_vk->external_img) { + vkDestroyImage(vk->dev, tex_vk->img, MPVK_ALLOCATOR); + vk_free_memslice(vk, tex_vk->mem); + } + + talloc_free(tex); +} + +MAKE_LAZY_DESTRUCTOR(vk_tex_destroy, struct ra_tex); + +// Initializes non-VkImage values like the image view, samplers, etc. +static bool vk_init_image(struct ra *ra, struct ra_tex *tex) +{ + struct mpvk_ctx *vk = ra_vk_get(ra); + + struct ra_tex_params *params = &tex->params; + struct ra_tex_vk *tex_vk = tex->priv; + assert(tex_vk->img); + + tex_vk->current_layout = VK_IMAGE_LAYOUT_UNDEFINED; + tex_vk->current_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + tex_vk->current_access = 0; + + if (params->render_src || params->render_dst) { + static const VkImageViewType viewType[] = { + [VK_IMAGE_TYPE_1D] = VK_IMAGE_VIEW_TYPE_1D, + [VK_IMAGE_TYPE_2D] = VK_IMAGE_VIEW_TYPE_2D, + [VK_IMAGE_TYPE_3D] = VK_IMAGE_VIEW_TYPE_3D, + }; + + const struct vk_format *fmt = params->format->priv; + VkImageViewCreateInfo vinfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = tex_vk->img, + .viewType = viewType[tex_vk->type], + .format = fmt->iformat, + .subresourceRange = vk_range, + }; + + VK(vkCreateImageView(vk->dev, &vinfo, MPVK_ALLOCATOR, &tex_vk->view)); + } + + if (params->render_src) { + assert(params->format->linear_filter || !params->src_linear); + VkFilter filter = params->src_linear + ? VK_FILTER_LINEAR + : VK_FILTER_NEAREST; + VkSamplerAddressMode wrap = params->src_repeat + ? VK_SAMPLER_ADDRESS_MODE_REPEAT + : VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + VkSamplerCreateInfo sinfo = { + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = filter, + .minFilter = filter, + .addressModeU = wrap, +