From afc50f1e6e9fe258ac6e46220fa5ce675930bb1e Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Fri, 6 Nov 2026 10:15:07 -0800 Subject: [PATCH 1/2] feat(packages): submitted NVK work to /dev/gpuN queues NVK on a Stellux GPU had no queue, so no device with queues could be created. Submissions past the kernel's limits chain through a private timeline whose failed mark reads as device loss, and a replaced shader local memory area outlives the work reading it, as freeing unmaps it at once. --- .../0016-replaced-slm-outlives-its-work.patch | 178 +++++ .../patches/0017-stellux-nvkmd-exec-ctx.patch | 670 ++++++++++++++++++ userland/apps/vktest/src/device.cpp | 127 ++++ userland/apps/vktest/src/memory.cpp | 558 +++++++++++++++ userland/apps/vktest/src/timeline.cpp | 628 ++++++++++++++++ userland/apps/vktest/src/vktest.cpp | 62 +- userland/apps/vktest/src/vktest.h | 42 ++ 7 files changed, 2257 insertions(+), 8 deletions(-) create mode 100644 packages/mesa/patches/0016-replaced-slm-outlives-its-work.patch create mode 100644 packages/mesa/patches/0017-stellux-nvkmd-exec-ctx.patch create mode 100644 userland/apps/vktest/src/device.cpp create mode 100644 userland/apps/vktest/src/memory.cpp create mode 100644 userland/apps/vktest/src/timeline.cpp create mode 100644 userland/apps/vktest/src/vktest.h diff --git a/packages/mesa/patches/0016-replaced-slm-outlives-its-work.patch b/packages/mesa/patches/0016-replaced-slm-outlives-its-work.patch new file mode 100644 index 000000000..22234b366 --- /dev/null +++ b/packages/mesa/patches/0016-replaced-slm-outlives-its-work.patch @@ -0,0 +1,178 @@ +Keeps a queue's replaced shader local memory area until the GPU has passed +the work that may still read it. NVK replaces the area with a larger one when +a shader needs more, and each queue dropped its reference to the old area at +its next state update, while its earlier submissions may still run with it. +A kernel driver that keeps a buffer mapped for the jobs submitted with it +hides this, but where dropping the last reference unmaps the buffer, that +work reads an unmapped area. A 3D or compute queue's push stream keeps the +old area until the GPU passes the point of the stream's next flush, the one +carrying the state push that moves the queue to the new area, or until the +stream is finished, and an area it has no memory to keep leaks instead. A +copy queue runs no shaders, so it drops the area at once. + +--- a/src/nouveau/vulkan/nvk_mem_stream.c ++++ b/src/nouveau/vulkan/nvk_mem_stream.c +@@ -17,6 +17,16 @@ struct nvk_mem_stream_chunk { + uint64_t idle_time_point; + }; + ++struct nvk_mem_stream_held_mem { ++ struct nvkmd_mem *mem; ++ ++ /** Link in nvk_mem_stream::held_mems */ ++ struct list_head link; ++ ++ /** Time point past which no work on the stream's context reads the mem */ ++ uint64_t idle_time_point; ++}; ++ + static VkResult + nvk_mem_stream_chunk_create(struct nvk_device *dev, + struct nvk_mem_stream_chunk **chunk_out) +@@ -75,14 +85,28 @@ nvk_mem_stream_init(struct nvk_device *dev, + stream->chunk_alloc_B = NVK_MEM_STREAM_MAX_ALLOC_SIZE; + + list_inithead(&stream->recycle); ++ list_inithead(&stream->held_mems); + + return VK_SUCCESS; + } + ++static void ++nvk_mem_stream_held_mem_destroy(struct nvk_device *dev, ++ struct nvk_mem_stream_held_mem *held) ++{ ++ list_del(&held->link); ++ nvkmd_mem_unref(held->mem); ++ vk_free(&dev->vk.alloc, held); ++} ++ + void + nvk_mem_stream_finish(struct nvk_device *dev, + struct nvk_mem_stream *stream) + { ++ list_for_each_entry_safe(struct nvk_mem_stream_held_mem, held, ++ &stream->held_mems, link) ++ nvk_mem_stream_held_mem_destroy(dev, held); ++ + list_for_each_entry_safe(struct nvk_mem_stream_chunk, chunk, + &stream->recycle, link) + nvk_mem_stream_chunk_destroy(dev, chunk); +@@ -279,3 +303,46 @@ nvk_mem_stream_sync(struct nvk_device *dev, + return vk_sync_wait(&dev->vk, stream->sync, time_point, + VK_SYNC_WAIT_COMPLETE, UINT64_MAX); + } ++ ++void ++nvk_mem_stream_hold_mem(struct nvk_device *dev, ++ struct nvk_mem_stream *stream, ++ struct nvkmd_mem *mem) ++{ ++ /* Without memory for the entry the reference is never dropped, so the ++ * mem leaks rather than being freed under the work reading it ++ */ ++ struct nvk_mem_stream_held_mem *held = ++ vk_zalloc(&dev->vk.alloc, sizeof(*held), 8, ++ VK_SYSTEM_ALLOCATION_SCOPE_DEVICE); ++ if (held == NULL) ++ return; ++ ++ held->mem = mem; ++ held->idle_time_point = stream->next_time_point; ++ list_addtail(&held->link, &stream->held_mems); ++} ++ ++void ++nvk_mem_stream_drop_held_mems(struct nvk_device *dev, ++ struct nvk_mem_stream *stream) ++{ ++ if (list_is_empty(&stream->held_mems)) ++ return; ++ ++ struct nvk_mem_stream_held_mem *first = ++ list_first_entry(&stream->held_mems, struct nvk_mem_stream_held_mem, ++ link); ++ if (stream->time_point_passed < first->idle_time_point && ++ vk_sync_get_value(&dev->vk, stream->sync, ++ &stream->time_point_passed) != VK_SUCCESS) ++ return; ++ ++ list_for_each_entry_safe(struct nvk_mem_stream_held_mem, held, ++ &stream->held_mems, link) { ++ if (held->idle_time_point > stream->time_point_passed) ++ break; ++ ++ nvk_mem_stream_held_mem_destroy(dev, held); ++ } ++} +--- a/src/nouveau/vulkan/nvk_mem_stream.h ++++ b/src/nouveau/vulkan/nvk_mem_stream.h +@@ -12,6 +12,7 @@ + struct nvk_device; + struct nvk_mem_stream_chunk; + struct nvkmd_ctx; ++struct nvkmd_mem; + struct vk_sync; + struct vk_sync_wait; + +@@ -30,6 +31,9 @@ struct nvk_mem_stream { + + /* list of nvk_mem_stream_chunk */ + struct list_head recycle; ++ ++ /* Mems kept by nvk_mem_stream_hold_mem() */ ++ struct list_head held_mems; + }; + + VkResult nvk_mem_stream_init(struct nvk_device *dev, +@@ -73,4 +77,21 @@ VkResult nvk_mem_stream_sync(struct nvk_device *dev, + struct nvk_mem_stream *stream, + struct nvkmd_ctx *ctx); + ++/** Takes the caller's reference to mem and keeps it until ++ * nvk_mem_stream_drop_held_mems() finds the GPU past the time point of the ++ * stream's next flush, or until the stream is finished, or for good if no ++ * memory is left to record it. Every reader of mem must be work submitted ++ * before that flush on the context it goes to. ++ */ ++void nvk_mem_stream_hold_mem(struct nvk_device *dev, ++ struct nvk_mem_stream *stream, ++ struct nvkmd_mem *mem); ++ ++/** Drops the held mems whose time point is at most the stream's ++ * time_point_passed, which it reads again from the stream's sync first when ++ * the oldest held mem's is above it. A failed read drops none. ++ */ ++void nvk_mem_stream_drop_held_mems(struct nvk_device *dev, ++ struct nvk_mem_stream *stream); ++ + #endif /* NVK_STREAM_H */ +--- a/src/nouveau/vulkan/nvk_queue.c ++++ b/src/nouveau/vulkan/nvk_queue.c +@@ -46,6 +46,8 @@ nvk_queue_state_update(struct nvk_queue *queue, + uint32_t alloc_count, bytes_per_warp, bytes_per_tpc; + bool dirty = false; + ++ nvk_mem_stream_drop_held_mems(dev, &queue->push_stream); ++ + alloc_count = nvk_descriptor_table_alloc_count(&dev->images); + if (qs->images.alloc_count != alloc_count) { + qs->images.alloc_count = alloc_count; +@@ -61,8 +63,15 @@ nvk_queue_state_update(struct nvk_queue *queue, + mem = nvk_slm_area_get_mem_ref(&dev->slm, &bytes_per_warp, &bytes_per_tpc); + if (qs->slm.mem != mem || qs->slm.bytes_per_warp != bytes_per_warp || + qs->slm.bytes_per_tpc != bytes_per_tpc) { +- if (qs->slm.mem) +- nvkmd_mem_unref(qs->slm.mem); ++ if (qs->slm.mem) { ++ /* Shaders of the queue's earlier work may still read the old area, ++ * and the push below runs after that work. Copy queues run none. ++ */ ++ if (queue->engines & (NVKMD_ENGINE_3D | NVKMD_ENGINE_COMPUTE)) ++ nvk_mem_stream_hold_mem(dev, &queue->push_stream, qs->slm.mem); ++ else ++ nvkmd_mem_unref(qs->slm.mem); ++ } + qs->slm.mem = mem; + qs->slm.bytes_per_warp = bytes_per_warp; + qs->slm.bytes_per_tpc = bytes_per_tpc; diff --git a/packages/mesa/patches/0017-stellux-nvkmd-exec-ctx.patch b/packages/mesa/patches/0017-stellux-nvkmd-exec-ctx.patch new file mode 100644 index 000000000..48557ba57 --- /dev/null +++ b/packages/mesa/patches/0017-stellux-nvkmd-exec-ctx.patch @@ -0,0 +1,670 @@ +Adds the Stellux nvkmd's exec ctx over one queue of the dev's context. A +batch of waits, pushes and signals goes to the kernel in one GPU_SUBMIT, +or past the call's limits of 32 points and 4096 command ranges in a chain +of them. Each submission signals the ctx's private timeline and each later +part of a chain waits on the point before it, so a failed part fails the +rest. The device check reads each ctx's private timeline, so failed work +loses the device, since Vulkan reports failed work only as device loss. A +full queue makes the submit wait for its oldest submission and retry once. + +--- a/src/nouveau/vulkan/meson.build ++++ b/src/nouveau/vulkan/meson.build +@@ -162,6 +162,7 @@ if with_nouveau_stellux + nvk_flags += '-DNVK_STELLUX' + nvk_files += files( + 'nvkmd/stellux/nvkmd_stellux.h', ++ 'nvkmd/stellux/nvkmd_stellux_ctx.c', + 'nvkmd/stellux/nvkmd_stellux_dev.c', + 'nvkmd/stellux/nvkmd_stellux_mem.c', + 'nvkmd/stellux/nvkmd_stellux_pdev.c', +--- a/src/nouveau/vulkan/nvkmd/stellux/nvkmd_stellux.h ++++ b/src/nouveau/vulkan/nvkmd/stellux/nvkmd_stellux.h +@@ -5,6 +5,8 @@ + #define NVKMD_STELLUX_H 1 + + #include "nvkmd/nvkmd.h" ++#include "util/list.h" ++#include "util/u_dynarray.h" + #include "util/vma.h" + #include "vk_sync.h" + #include "vk_sync_binary.h" +@@ -15,7 +17,8 @@ + /* The nvkmd over Stellux's /dev/gpuN files, whose calls + * defines. A pdev owns its GPU's open file, a dev one GPU context, a mem + * one buffer with its whole-buffer GPU mapping, a va one range of GPU +- * addresses from its dev's heaps, and a sync one timeline. ++ * addresses from its dev's heaps, a ctx one queue of its dev's context and ++ * a private timeline, and a sync one timeline. + */ + + /* A dev's heaps: ordinary addresses below 2^38, capture-replay ones above */ +@@ -27,6 +30,8 @@ + /* The calls whose errors nvkmd_stellux_error() turns into VkResults */ + enum nvkmd_stellux_call { + NVKMD_STELLUX_CALL_CONTEXT_CREATE, ++ NVKMD_STELLUX_CALL_QUEUE_CREATE, ++ NVKMD_STELLUX_CALL_SUBMIT, + NVKMD_STELLUX_CALL_BUFFER_CREATE, + NVKMD_STELLUX_CALL_BUFFER_IMPORT, + NVKMD_STELLUX_CALL_MAP, +@@ -75,6 +80,9 @@ struct nvkmd_stellux_dev { + /* When the kernel last reported the context live, accessed atomically */ + uint64_t live_ns; + ++ /* Changed only while NVK makes or destroys the device, so read unlocked */ ++ struct list_head ctxs; ++ + simple_mtx_t heap_mutex; + struct util_vma_heap heap; + struct util_vma_heap replay_heap; +@@ -87,19 +95,23 @@ VkResult nvkmd_stellux_create_dev(struct nvkmd_pdev *pdev, + struct nvkmd_dev **dev_out); + + /* Returns VK_ERROR_DEVICE_LOST, having lost device, once GPU_CONTEXT_QUERY +- * fails or reports the dev's context lost. Unless fresh, a check less than ++ * fails or reports the dev's context lost, or a query of a ctx's private ++ * timeline fails or finds its failed mark. Unless fresh, a check less than + * a millisecond after the kernel's last live answer returns VK_SUCCESS +- * without asking it. A caller must first test device's lost state, as +- * vk_device_check_status() does before calling vk_device::check_status, +- * since a live answer racing a loss can be recorded after it. ++ * without asking it. A caller acting on VK_SUCCESS must first test device's ++ * lost state, as vk_device_check_status() does before calling ++ * vk_device::check_status, since a live answer racing a loss can be ++ * recorded after it. + */ + VkResult nvkmd_stellux_dev_check(struct nvkmd_stellux_dev *dev, + struct vk_device *device, bool fresh); + + /* Logs errno err of call on log_obj and returns a VkResult the Vulkan +- * command making the call lists. EIO from a buffer create, an import or a +- * GPU_MAP also loses log_obj's device, if it has one, since Vulkan lists no +- * device loss for making or binding memory. ++ * command making the call lists, but for GPU_SUBMIT's VK_ERROR_DEVICE_LOST, ++ * which NVK's upload queue passes up to any command that flushes it, even ++ * one listing no device loss, such as vkCreateGraphicsPipelines(). EIO from ++ * a buffer create, an import or a GPU_MAP also loses log_obj's device, if it ++ * has one, since Vulkan lists no device loss for making or binding memory. + */ + VkResult nvkmd_stellux_error(struct vk_object_base *log_obj, + enum nvkmd_stellux_call call, int err); +@@ -143,6 +155,35 @@ VkResult nvkmd_stellux_alloc_va(struct nvkmd_dev *dev, + uint64_t size_B, uint64_t align_B, + uint64_t fixed_addr, struct nvkmd_va **va_out); + ++/* A batch past GPU_SUBMIT's limits goes in a chain of submissions, each ++ * later part waiting on the private point of the one before, so a failed ++ * part fails the rest. ++ */ ++struct nvkmd_stellux_ctx { ++ struct nvkmd_ctx base; ++ ++ /* Link in nvkmd_stellux_dev::ctxs */ ++ struct list_head link; ++ ++ uint32_t queue; ++ ++ /* The private timeline and the point of the last submission taken */ ++ int timeline; ++ uint64_t last_point; ++ ++ /* The batch gathered since the last submission */ ++ struct gpu_point waits[GPU_POINTS_MAX]; ++ uint32_t wait_count; ++ struct util_dynarray ranges; ++}; ++ ++NVKMD_DECL_SUBCLASS(ctx, stellux); ++ ++VkResult nvkmd_stellux_create_ctx(struct nvkmd_dev *dev, ++ struct vk_object_base *log_obj, ++ enum nvkmd_engines engines, ++ struct nvkmd_ctx **ctx_out); ++ + struct nvkmd_stellux_sync { + struct vk_sync base; + +@@ -153,4 +194,15 @@ struct nvkmd_stellux_sync { + /* The template of each pdev's sync type */ + extern const struct vk_sync_type nvkmd_stellux_sync_type; + ++/* Waits for all of points or any one in slices of at most 100 ms that each ++ * end with the device check. Returns VK_TIMEOUT once abs_timeout_ns passes, ++ * and VK_ERROR_DEVICE_LOST, with device lost, once the check finds a loss or ++ * a point's timeline has its failed mark when queried, as every point's is ++ * once the wait succeeds. A failed GPU_TIMELINE_WAIT or query of a point ++ * returns the result nvkmd_stellux_error() gives it. ++ */ ++VkResult nvkmd_stellux_wait(struct vk_device *device, int fd, ++ const struct gpu_point *points, uint32_t count, ++ bool all, uint64_t abs_timeout_ns); ++ + #endif /* NVKMD_STELLUX_H */ +--- /dev/null ++++ b/src/nouveau/vulkan/nvkmd/stellux/nvkmd_stellux_ctx.c +@@ -0,0 +1,335 @@ ++/* ++ * SPDX-License-Identifier: MIT ++ */ ++ ++#include "nvkmd_stellux.h" ++ ++#include "util/u_memory.h" ++#include "vk_log.h" ++ ++#include ++#include ++#include ++ ++/* The command ranges one GPU_SUBMIT takes */ ++#define NVKMD_STELLUX_RANGES_MAX \ ++ ((uint32_t)(GPU_COMMANDS_BYTES_MAX / sizeof(struct gpu_nvidia_range))) ++ ++static int ++sync_timeline(struct vk_sync *sync) ++{ ++ return container_of(sync, struct nvkmd_stellux_sync, base)->timeline; ++} ++ ++static uint32_t ++range_count(const struct nvkmd_stellux_ctx *ctx) ++{ ++ return util_dynarray_num_elements(&ctx->ranges, struct gpu_nvidia_range); ++} ++ ++static void ++drop_batch(struct nvkmd_stellux_ctx *ctx) ++{ ++ util_dynarray_clear(&ctx->ranges); ++ ctx->wait_count = 0; ++} ++ ++static void ++destroy_queue(struct nvkmd_stellux_dev *dev, uint32_t queue) ++{ ++ struct gpu_queue_args args = { ++ .context = dev->context, ++ .queue = queue, ++ }; ++ ++ /* Succeeds for any queue the context holds, lost or not */ ++ ioctl(dev->fd, GPU_QUEUE_DESTROY, &args); ++} ++ ++static int ++submit_ioctl(struct nvkmd_stellux_dev *dev, struct gpu_submit_args *args) ++{ ++ return ioctl(dev->fd, GPU_SUBMIT, args) == 0 ? 0 : errno; ++} ++ ++/* Drops the batch whether or not it is taken. A chained part leaves the next ++ * batch waiting on its private point, so the parts after a failed one fail. ++ */ ++static VkResult ++submit_batch(struct nvkmd_stellux_ctx *ctx, struct vk_object_base *log_obj, ++ uint32_t signal_count, const struct vk_sync_signal *signals, ++ bool chained) ++{ ++ struct nvkmd_stellux_dev *dev = nvkmd_stellux_dev(ctx->base.dev); ++ ++ assert(signal_count < GPU_POINTS_MAX); ++ struct gpu_point points[GPU_POINTS_MAX]; ++ for (uint32_t i = 0; i < signal_count; i++) { ++ points[i] = (struct gpu_point) { ++ .timeline = sync_timeline(signals[i].sync), ++ .value = signals[i].signal_value, ++ }; ++ } ++ ++ const struct gpu_point private_point = { ++ .timeline = ctx->timeline, ++ .value = ctx->last_point + 1, ++ }; ++ points[signal_count] = private_point; ++ ++ struct gpu_submit_args args = { ++ .context = dev->context, ++ .queue = ctx->queue, ++ .commands = (uintptr_t)util_dynarray_begin(&ctx->ranges), ++ .waits = (uintptr_t)ctx->waits, ++ .signals = (uintptr_t)points, ++ .commands_size = ctx->ranges.size, ++ .wait_count = ctx->wait_count, ++ .signal_count = signal_count + 1, ++ }; ++ int err = submit_ioctl(dev, &args); ++ ++ /* A full queue holds the last GPU_QUEUE_PENDING_MAX submissions, and the ++ * kernel takes another once the oldest has reached its private point ++ */ ++ if (err == EBUSY) { ++ const struct gpu_point oldest = { ++ .timeline = ctx->timeline, ++ .value = private_point.value - GPU_QUEUE_PENDING_MAX, ++ }; ++ VkResult result = nvkmd_stellux_wait(log_obj->device, dev->fd, &oldest, ++ 1, true, UINT64_MAX); ++ if (result != VK_SUCCESS) { ++ drop_batch(ctx); ++ return result; ++ } ++ ++ err = submit_ioctl(dev, &args); ++ } ++ ++ drop_batch(ctx); ++ ++ if (err != 0) { ++ /* Loses the device with the kernel's reason for the lost context */ ++ if (err == EIO) ++ nvkmd_stellux_dev_check(dev, log_obj->device, true); ++ ++ return nvkmd_stellux_error(log_obj, NVKMD_STELLUX_CALL_SUBMIT, err); ++ } ++ ++ ctx->last_point = private_point.value; ++ if (chained) { ++ ctx->waits[0] = private_point; ++ ctx->wait_count = 1; ++ } ++ ++ return VK_SUCCESS; ++} ++ ++static void ++nvkmd_stellux_ctx_destroy(struct nvkmd_ctx *_ctx) ++{ ++ struct nvkmd_stellux_ctx *ctx = nvkmd_stellux_ctx(_ctx); ++ struct nvkmd_stellux_dev *dev = nvkmd_stellux_dev(ctx->base.dev); ++ ++ list_del(&ctx->link); ++ ++ destroy_queue(dev, ctx->queue); ++ close(ctx->timeline); ++ ++ util_dynarray_fini(&ctx->ranges); ++ FREE(ctx); ++} ++ ++static VkResult ++nvkmd_stellux_ctx_wait(struct nvkmd_ctx *_ctx, ++ struct vk_object_base *log_obj, ++ uint32_t wait_count, ++ const struct vk_sync_wait *waits) ++{ ++ struct nvkmd_stellux_ctx *ctx = nvkmd_stellux_ctx(_ctx); ++ ++ for (uint32_t i = 0; i < wait_count; i++) { ++ if (ctx->wait_count == GPU_POINTS_MAX) { ++ VkResult result = submit_batch(ctx, log_obj, 0, NULL, true); ++ if (result != VK_SUCCESS) ++ return result; ++ } ++ ++ ctx->waits[ctx->wait_count++] = (struct gpu_point) { ++ .timeline = sync_timeline(waits[i].sync), ++ .value = waits[i].wait_value, ++ }; ++ } ++ ++ return VK_SUCCESS; ++} ++ ++static VkResult ++nvkmd_stellux_ctx_exec(struct nvkmd_ctx *_ctx, ++ struct vk_object_base *log_obj, ++ uint32_t exec_count, ++ const struct nvkmd_ctx_exec *execs) ++{ ++ struct nvkmd_stellux_ctx *ctx = nvkmd_stellux_ctx(_ctx); ++ ++ uint32_t run_start = 0; ++ while (run_start < exec_count) { ++ /* An incomplete push goes in one submission with the push after it */ ++ uint32_t run_end = run_start + 1; ++ while (run_end < exec_count && execs[run_end - 1].incomplete) ++ run_end++; ++ ++ const uint32_t run_count = run_end - run_start; ++ if (run_count > NVKMD_STELLUX_RANGES_MAX) { ++ drop_batch(ctx); ++ return vk_errorf(log_obj, VK_ERROR_OUT_OF_DEVICE_MEMORY, ++ "%u pushes that must go in one GPU_SUBMIT pass " ++ "its limit of %u command ranges", run_count, ++ NVKMD_STELLUX_RANGES_MAX); ++ } ++ ++ if (range_count(ctx) + run_count > NVKMD_STELLUX_RANGES_MAX) { ++ VkResult result = submit_batch(ctx, log_obj, 0, NULL, true); ++ if (result != VK_SUCCESS) ++ return result; ++ } ++ ++ struct gpu_nvidia_range *ranges = ++ util_dynarray_grow(&ctx->ranges, struct gpu_nvidia_range, run_count); ++ if (ranges == NULL) { ++ drop_batch(ctx); ++ return vk_error(log_obj, VK_ERROR_OUT_OF_HOST_MEMORY); ++ } ++ ++ for (uint32_t i = 0; i < run_count; i++) { ++ const struct nvkmd_ctx_exec *exec = &execs[run_start + i]; ++ ranges[i] = (struct gpu_nvidia_range) { ++ .va = exec->addr, ++ .size = exec->size_B, ++ .flags = exec->no_prefetch ? GPU_COMMANDS_GPU_WRITTEN : 0, ++ }; ++ } ++ ++ run_start = run_end; ++ } ++ ++ return VK_SUCCESS; ++} ++ ++static VkResult ++nvkmd_stellux_ctx_flush(struct nvkmd_ctx *_ctx, ++ struct vk_object_base *log_obj) ++{ ++ struct nvkmd_stellux_ctx *ctx = nvkmd_stellux_ctx(_ctx); ++ ++ if (range_count(ctx) == 0 && ctx->wait_count == 0) ++ return VK_SUCCESS; ++ ++ return submit_batch(ctx, log_obj, 0, NULL, false); ++} ++ ++static VkResult ++nvkmd_stellux_ctx_signal(struct nvkmd_ctx *_ctx, ++ struct vk_object_base *log_obj, ++ uint32_t signal_count, ++ const struct vk_sync_signal *signals) ++{ ++ struct nvkmd_stellux_ctx *ctx = nvkmd_stellux_ctx(_ctx); ++ ++ if (signal_count == 0) ++ return nvkmd_stellux_ctx_flush(&ctx->base, log_obj); ++ ++ /* Each part keeps one signal for the private point, and the parts after ++ * the one with the work carry the rest ++ */ ++ for (uint32_t start = 0; start < signal_count; ++ start += GPU_POINTS_MAX - 1) { ++ const uint32_t count = MIN2(signal_count - start, GPU_POINTS_MAX - 1); ++ VkResult result = submit_batch(ctx, log_obj, count, &signals[start], ++ start + count < signal_count); ++ if (result != VK_SUCCESS) ++ return result; ++ } ++ ++ return VK_SUCCESS; ++} ++ ++static VkResult ++nvkmd_stellux_ctx_sync(struct nvkmd_ctx *_ctx, ++ struct vk_object_base *log_obj) ++{ ++ struct nvkmd_stellux_ctx *ctx = nvkmd_stellux_ctx(_ctx); ++ struct nvkmd_stellux_dev *dev = nvkmd_stellux_dev(ctx->base.dev); ++ ++ VkResult result = nvkmd_stellux_ctx_flush(&ctx->base, log_obj); ++ if (result != VK_SUCCESS) ++ return result; ++ ++ const struct gpu_point last = { ++ .timeline = ctx->timeline, ++ .value = ctx->last_point, ++ }; ++ return nvkmd_stellux_wait(log_obj->device, dev->fd, &last, 1, true, ++ UINT64_MAX); ++} ++ ++const struct nvkmd_ctx_ops nvkmd_stellux_ctx_ops = { ++ .destroy = nvkmd_stellux_ctx_destroy, ++ .wait = nvkmd_stellux_ctx_wait, ++ .exec = nvkmd_stellux_ctx_exec, ++ .signal = nvkmd_stellux_ctx_signal, ++ .flush = nvkmd_stellux_ctx_flush, ++ .sync = nvkmd_stellux_ctx_sync, ++}; ++ ++VkResult ++nvkmd_stellux_create_ctx(struct nvkmd_dev *_dev, ++ struct vk_object_base *log_obj, ++ enum nvkmd_engines engines, ++ struct nvkmd_ctx **ctx_out) ++{ ++ struct nvkmd_stellux_dev *dev = nvkmd_stellux_dev(_dev); ++ ++ /* Only sparse binding needs a bind ctx, and the pdev reports no sparse */ ++ assert(!(engines & NVKMD_ENGINE_BIND)); ++ ++ struct nvkmd_stellux_ctx *ctx = CALLOC_STRUCT(nvkmd_stellux_ctx); ++ if (ctx == NULL) ++ return vk_error(log_obj, VK_ERROR_OUT_OF_HOST_MEMORY); ++ ++ const bool graphics = engines & (NVKMD_ENGINE_3D | NVKMD_ENGINE_COMPUTE); ++ struct gpu_queue_args queue_args = { ++ .context = dev->context, ++ .kind = graphics ? GPU_QUEUE_GRAPHICS : GPU_QUEUE_COPY, ++ }; ++ if (ioctl(dev->fd, GPU_QUEUE_CREATE, &queue_args) != 0) { ++ VkResult result = nvkmd_stellux_error(log_obj, ++ NVKMD_STELLUX_CALL_QUEUE_CREATE, ++ errno); ++ FREE(ctx); ++ return result; ++ } ++ ++ struct gpu_timeline_args timeline_args = { 0 }; ++ if (ioctl(dev->fd, GPU_TIMELINE_CREATE, &timeline_args) != 0) { ++ VkResult result = ++ nvkmd_stellux_error(log_obj, NVKMD_STELLUX_CALL_TIMELINE_CREATE, ++ errno); ++ destroy_queue(dev, queue_args.queue); ++ FREE(ctx); ++ return result; ++ } ++ ++ ctx->base.ops = &nvkmd_stellux_ctx_ops; ++ ctx->base.dev = &dev->base; ++ ctx->queue = queue_args.queue; ++ ctx->timeline = timeline_args.timeline; ++ ctx->ranges = UTIL_DYNARRAY_INIT; ++ ++ list_addtail(&ctx->link, &dev->ctxs); ++ ++ *ctx_out = &ctx->base; ++ ++ return VK_SUCCESS; ++} +--- a/src/nouveau/vulkan/nvkmd/stellux/nvkmd_stellux_dev.c ++++ b/src/nouveau/vulkan/nvkmd/stellux/nvkmd_stellux_dev.c +@@ -21,6 +21,8 @@ + + static const char *const nvkmd_stellux_call_names[] = { + [NVKMD_STELLUX_CALL_CONTEXT_CREATE] = "GPU_CONTEXT_CREATE", ++ [NVKMD_STELLUX_CALL_QUEUE_CREATE] = "GPU_QUEUE_CREATE", ++ [NVKMD_STELLUX_CALL_SUBMIT] = "GPU_SUBMIT", + [NVKMD_STELLUX_CALL_BUFFER_CREATE] = "GPU_BUFFER_CREATE", + [NVKMD_STELLUX_CALL_BUFFER_IMPORT] = "GPU_BUFFER_IMPORT", + [NVKMD_STELLUX_CALL_MAP] = "GPU_MAP", +@@ -46,12 +48,22 @@ nvkmd_stellux_error(struct vk_object_base *log_obj, + + switch (call) { + case NVKMD_STELLUX_CALL_CONTEXT_CREATE: +- if (err == EIO) ++ case NVKMD_STELLUX_CALL_QUEUE_CREATE: ++ if (err == EBUSY) ++ result = VK_ERROR_TOO_MANY_OBJECTS; ++ else if (err == EIO) + result = VK_ERROR_DEVICE_LOST; + else if (err == ENOMEM) + result = VK_ERROR_OUT_OF_DEVICE_MEMORY; + break; + ++ case NVKMD_STELLUX_CALL_SUBMIT: ++ if (err == EIO) ++ result = VK_ERROR_DEVICE_LOST; ++ else if (err == ENOMEM) ++ result = VK_ERROR_OUT_OF_HOST_MEMORY; ++ break; ++ + case NVKMD_STELLUX_CALL_BUFFER_CREATE: + case NVKMD_STELLUX_CALL_BUFFER_IMPORT: + case NVKMD_STELLUX_CALL_MAP: +@@ -169,6 +181,24 @@ nvkmd_stellux_dev_check(struct nvkmd_stellux_dev *dev, + nvkmd_stellux_lost_reason(args.lost)); + } + ++ /* A ctx's submission that ended failed leaves its timeline marked */ ++ list_for_each_entry(struct nvkmd_stellux_ctx, ctx, &dev->ctxs, link) { ++ struct gpu_timeline_args timeline_args = { ++ .timeline = ctx->timeline, ++ }; ++ if (ioctl(dev->fd, GPU_TIMELINE_QUERY, &timeline_args) != 0) { ++ return vk_device_set_lost(device, "GPU_TIMELINE_QUERY of a queue's " ++ "private timeline failed: %s", ++ strerror(errno)); ++ } ++ ++ if (timeline_args.flags & GPU_TIMELINE_FAILED) { ++ return vk_device_set_lost(device, "A submission to a queue of the " ++ "device failed, or waited on a timeline " ++ "that failed work had marked"); ++ } ++ } ++ + p_atomic_set(&dev->live_ns, now_ns); + + return VK_SUCCESS; +@@ -204,6 +234,7 @@ nvkmd_stellux_create_dev(struct nvkmd_pdev *_pdev, + + dev->fd = pdev->fd; + dev->context = args.context; ++ list_inithead(&dev->ctxs); + + simple_mtx_init(&dev->heap_mutex, mtx_plain); + util_vma_heap_init(&dev->heap, NVKMD_STELLUX_HEAP_START, +@@ -249,16 +280,6 @@ nvkmd_stellux_dev_check_status(struct nvkmd_dev *dev, + false); + } + +-static VkResult +-nvkmd_stellux_dev_create_ctx(struct nvkmd_dev *dev, +- struct vk_object_base *log_obj, +- enum nvkmd_engines engines, +- struct nvkmd_ctx **ctx_out) +-{ +- return vk_errorf(log_obj, VK_ERROR_INITIALIZATION_FAILED, +- "The Stellux nvkmd cannot create a queue"); +-} +- + const struct nvkmd_dev_ops nvkmd_stellux_dev_ops = { + .destroy = nvkmd_stellux_dev_destroy, + .get_gpu_timestamp = nvkmd_stellux_dev_get_gpu_timestamp, +@@ -266,5 +287,5 @@ const struct nvkmd_dev_ops nvkmd_stellux_dev_ops = { + .alloc_mem = nvkmd_stellux_alloc_mem, + .import_dma_buf = nvkmd_stellux_import_dma_buf, + .alloc_va = nvkmd_stellux_alloc_va, +- .create_ctx = nvkmd_stellux_dev_create_ctx, ++ .create_ctx = nvkmd_stellux_create_ctx, + }; +--- a/src/nouveau/vulkan/nvkmd/stellux/nvkmd_stellux_sync.c ++++ b/src/nouveau/vulkan/nvkmd/stellux/nvkmd_stellux_sync.c +@@ -38,7 +38,8 @@ sync_gpu_fd(const struct vk_sync *sync) + } + + /* The device check: the runtime's lost state, then the kernel's state of the +- * device's context, which a device made with no queues lacks ++ * device's context and of each ctx's private timeline, which a device made ++ * with no queues lacks + */ + static VkResult + check_device(struct vk_device *device, bool fresh) +@@ -140,6 +141,37 @@ wait_points(struct vk_device *device, int fd, + } + } + ++VkResult ++nvkmd_stellux_wait(struct vk_device *device, int fd, ++ const struct gpu_point *points, uint32_t count, bool all, ++ uint64_t abs_timeout_ns) ++{ ++ if (vk_device_is_lost_no_report(device)) ++ return VK_ERROR_DEVICE_LOST; ++ ++ VkResult result = VK_SUCCESS; ++ if (!all) { ++ result = wait_points(device, fd, points, count, false, abs_timeout_ns); ++ } else { ++ for (uint32_t start = 0; start < count && result == VK_SUCCESS; ++ start += GPU_POINTS_MAX) { ++ result = wait_points(device, fd, &points[start], ++ MIN2(count - start, GPU_POINTS_MAX), true, ++ abs_timeout_ns); ++ if (result == VK_SUCCESS && start + GPU_POINTS_MAX < count) ++ result = check_device(device, false); ++ } ++ } ++ ++ for (uint32_t i = 0; i < count && result == VK_SUCCESS; i++) ++ result = query_timeline(device, fd, points[i].timeline, NULL); ++ ++ if (result != VK_SUCCESS) ++ return result; ++ ++ return check_device(device, true); ++} ++ + static VkResult + nvkmd_stellux_sync_init(struct vk_device *device, struct vk_sync *_sync, + uint64_t initial_value) +@@ -208,9 +240,6 @@ nvkmd_stellux_sync_wait_many(struct vk_device *device, uint32_t wait_count, + enum vk_sync_wait_flags wait_flags, + uint64_t abs_timeout_ns) + { +- if (vk_device_is_lost_no_report(device)) +- return VK_ERROR_DEVICE_LOST; +- + STACK_ARRAY(struct gpu_point, points, wait_count); + if (points == NULL) + return vk_error(device, VK_ERROR_OUT_OF_HOST_MEMORY); +@@ -222,32 +251,14 @@ nvkmd_stellux_sync_wait_many(struct vk_device *device, uint32_t wait_count, + }; + } + +- const int fd = sync_gpu_fd(waits[0].sync); +- +- VkResult result = VK_SUCCESS; +- if (wait_flags & VK_SYNC_WAIT_ANY) { +- result = wait_points(device, fd, points, wait_count, false, +- abs_timeout_ns); +- } else { +- for (uint32_t start = 0; start < wait_count && result == VK_SUCCESS; +- start += GPU_POINTS_MAX) { +- result = wait_points(device, fd, &points[start], +- MIN2(wait_count - start, GPU_POINTS_MAX), true, +- abs_timeout_ns); +- if (result == VK_SUCCESS && start + GPU_POINTS_MAX < wait_count) +- result = check_device(device, false); +- } +- } +- +- for (uint32_t i = 0; i < wait_count && result == VK_SUCCESS; i++) +- result = query_timeline(device, fd, points[i].timeline, NULL); ++ VkResult result = nvkmd_stellux_wait(device, sync_gpu_fd(waits[0].sync), ++ points, wait_count, ++ !(wait_flags & VK_SYNC_WAIT_ANY), ++ abs_timeout_ns); + + STACK_ARRAY_FINISH(points); + +- if (result != VK_SUCCESS) +- return result; +- +- return check_device(device, true); ++ return result; + } + + /* Restricts handle to both rights, or to GPU_RIGHT_READ alone when it lacks diff --git a/userland/apps/vktest/src/device.cpp b/userland/apps/vktest/src/device.cpp new file mode 100644 index 000000000..0a258274f --- /dev/null +++ b/userland/apps/vktest/src/device.cpp @@ -0,0 +1,127 @@ +// The device stage: a device with queues makes one GPU context, holding a GPU queue for NVK's upload queue and one for +// each Vulkan queue, and each 3D or compute queue submits its state push when created, before its wait for idle + +#include "vktest.h" + +#include +#include + +struct device_commands { + PFN_vkGetPhysicalDeviceQueueFamilyProperties get_queue_families; + PFN_vkCreateDevice create_device; + PFN_vkDestroyDevice destroy_device; + PFN_vkGetDeviceQueue get_device_queue; + PFN_vkQueueWaitIdle queue_wait_idle; + PFN_vkDeviceWaitIdle device_wait_idle; +}; + +// Whether a device with one queue of each family in `families` is created, its queues each wait idle, and the device +// waits idle, the device destroyed again either way +static bool runs_device( + const device_commands& commands, + VkPhysicalDevice physical_device, + const std::vector& families +) { + float priority = 1.0f; + std::vector queue_infos; + for (uint32_t family : families) { + VkDeviceQueueCreateInfo queue_info = {}; + queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queue_info.queueFamilyIndex = family; + queue_info.queueCount = 1; + queue_info.pQueuePriorities = &priority; + queue_infos.push_back(queue_info); + } + + VkDeviceCreateInfo create_info = {}; + create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + create_info.queueCreateInfoCount = static_cast(queue_infos.size()); + create_info.pQueueCreateInfos = queue_infos.data(); + + VkDevice device = VK_NULL_HANDLE; + if (commands.create_device(physical_device, &create_info, nullptr, &device) != VK_SUCCESS) { + return false; + } + + bool queues_idle = true; + for (uint32_t family : families) { + VkQueue queue = VK_NULL_HANDLE; + commands.get_device_queue(device, family, 0, &queue); + queues_idle &= queue != VK_NULL_HANDLE && commands.queue_wait_idle(queue) == VK_SUCCESS; + } + + bool device_idle = commands.device_wait_idle(device) == VK_SUCCESS; + commands.destroy_device(device, nullptr); + + return queues_idle && device_idle; +} + +VkDevice create_device( + VkInstance instance, + VkPhysicalDevice physical_device, + const std::vector& extensions, + const void* features +) { + auto create = find_command(instance, "vkCreateDevice"); + if (!create) { + return VK_NULL_HANDLE; + } + + float priority = 1.0f; + VkDeviceQueueCreateInfo queue_info = {}; + queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queue_info.queueFamilyIndex = 0; + queue_info.queueCount = 1; + queue_info.pQueuePriorities = &priority; + + VkDeviceCreateInfo create_info = {}; + create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + create_info.pNext = features; + create_info.queueCreateInfoCount = 1; + create_info.pQueueCreateInfos = &queue_info; + + create_info.enabledExtensionCount = static_cast(extensions.size()); + create_info.ppEnabledExtensionNames = extensions.data(); + + VkDevice device = VK_NULL_HANDLE; + if (create(physical_device, &create_info, nullptr, &device) != VK_SUCCESS) { + return VK_NULL_HANDLE; + } + + return device; +} + +void check_device(VkInstance instance, VkPhysicalDevice physical_device) { + device_commands commands = { + .get_queue_families = find_command( + instance, + "vkGetPhysicalDeviceQueueFamilyProperties" + ), + .create_device = find_command(instance, "vkCreateDevice"), + .destroy_device = find_command(instance, "vkDestroyDevice"), + .get_device_queue = find_command(instance, "vkGetDeviceQueue"), + .queue_wait_idle = find_command(instance, "vkQueueWaitIdle"), + .device_wait_idle = find_command(instance, "vkDeviceWaitIdle"), + }; + + bool has_commands = commands.get_queue_families && commands.create_device && commands.destroy_device && + commands.get_device_queue && commands.queue_wait_idle && commands.device_wait_idle; + check("vkGetInstanceProcAddr finds the device commands vktest calls", has_commands); + if (!has_commands) { + return; + } + + uint32_t family_count = 0; + commands.get_queue_families(physical_device, &family_count, nullptr); + + std::vector every_family; + bool runs_each_family = family_count > 0; + for (uint32_t family = 0; family < family_count; family++) { + every_family.push_back(family); + runs_each_family &= runs_device(commands, physical_device, {family}); + } + + bool runs_every_family = family_count > 0 && runs_device(commands, physical_device, every_family); + check("a device with one queue of a single family is created and waits idle, for every family", runs_each_family); + check("a device with one queue of every family is created and waits idle", runs_every_family); +} diff --git a/userland/apps/vktest/src/memory.cpp b/userland/apps/vktest/src/memory.cpp new file mode 100644 index 000000000..2eb0d9e0e --- /dev/null +++ b/userland/apps/vktest/src/memory.cpp @@ -0,0 +1,558 @@ +// The memory stage: memory of every type, the host-visible types mapped and shared with a second device through opaque +// descriptors and the others imported read-only, and a buffer made again at a destroyed buffer's address + +#include "vktest.h" + +#include +#include +#include +#include +#include + +struct memory_commands { + PFN_vkGetPhysicalDeviceMemoryProperties get_memory_properties; + PFN_vkEnumerateDeviceExtensionProperties enumerate_device_extensions; + PFN_vkGetPhysicalDeviceExternalBufferProperties get_external_buffer_properties; + PFN_vkGetPhysicalDeviceImageFormatProperties2 get_image_format_properties; + + PFN_vkDestroyDevice destroy_device; + PFN_vkAllocateMemory allocate_memory; + PFN_vkFreeMemory free_memory; + PFN_vkMapMemory map_memory; + PFN_vkMapMemory2 map_memory2; + PFN_vkUnmapMemory unmap_memory; + PFN_vkUnmapMemory2 unmap_memory2; + PFN_vkGetMemoryFdKHR get_memory_fd; + PFN_vkCreateBuffer create_buffer; + PFN_vkDestroyBuffer destroy_buffer; + PFN_vkGetBufferMemoryRequirements get_buffer_memory_requirements; + PFN_vkBindBufferMemory bind_buffer_memory; + PFN_vkGetBufferOpaqueCaptureAddress get_buffer_opaque_capture_address; +}; + +// Two devices on one GPU, the exporter allocating the memory the importer imports +struct memory_stage { + memory_commands commands; + VkInstance instance; + VkPhysicalDevice physical_device; + VkPhysicalDeviceMemoryProperties properties; + VkDevice exporter; + VkDevice importer; +}; + +// Which memory types a check covers +enum class type_visibility { + any, + host_visible, + device_only, +}; + +constexpr VkDeviceSize MEMORY_SIZE = 65536; +constexpr uint32_t MEMORY_WORD_COUNT = MEMORY_SIZE / sizeof(uint32_t); +constexpr uint32_t CPU_WORDS_START = 0x10000000; +constexpr uint32_t EXPORTER_WORDS_START = 0x20000000; +constexpr uint32_t IMPORTER_WORDS_START = 0x30000000; +constexpr uint32_t PLACED_WORDS_START = 0x40000000; + +static bool is_host_visible(const VkMemoryType& type) { + return (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0; +} + +static bool has_visibility(const VkMemoryType& type, type_visibility visibility) { + switch (visibility) { + case type_visibility::any: + return true; + case type_visibility::host_visible: + return is_host_visible(type); + case type_visibility::device_only: + return !is_host_visible(type); + } + + return false; +} + +// Whether `holds` is true of every memory type of `visibility`, of which there must be at least one +template +static bool holds_for_every_type(const memory_stage& stage, type_visibility visibility, Predicate holds) { + uint32_t covered_count = 0; + bool holds_for_all = true; + for (uint32_t type = 0; type < stage.properties.memoryTypeCount; type++) { + if (!has_visibility(stage.properties.memoryTypes[type], visibility)) { + continue; + } + + covered_count++; + holds_for_all &= holds(stage, type); + } + + return covered_count > 0 && holds_for_all; +} + +static VkResult allocate( + const memory_stage& stage, + VkDevice device, + uint32_t type, + const void* next, + VkDeviceMemory& memory +) { + VkMemoryAllocateInfo allocate_info = {}; + allocate_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocate_info.pNext = next; + allocate_info.allocationSize = MEMORY_SIZE; + allocate_info.memoryTypeIndex = type; + + return stage.commands.allocate_memory(device, &allocate_info, nullptr, &memory); +} + +static void* map(const memory_stage& stage, VkDevice device, VkDeviceMemory memory) { + void* mapping = nullptr; + if (stage.commands.map_memory(device, memory, 0, VK_WHOLE_SIZE, 0, &mapping) != VK_SUCCESS) { + return nullptr; + } + + return mapping; +} + +static void write_words(void* mapping, uint32_t first_word) { + uint32_t* words = static_cast(mapping); + for (uint32_t i = 0; i < MEMORY_WORD_COUNT; i++) { + words[i] = first_word + i; + } +} + +static bool holds_words(const void* mapping, uint32_t first_word) { + const uint32_t* words = static_cast(mapping); + for (uint32_t i = 0; i < MEMORY_WORD_COUNT; i++) { + if (words[i] != first_word + i) { + return false; + } + } + + return true; +} + +// Allocates exportable memory of `type` on the exporter into `memory` and returns an opaque descriptor of it, or -1 +static int export_memory(const memory_stage& stage, uint32_t type, VkDeviceMemory& memory) { + VkExportMemoryAllocateInfo export_info = {}; + export_info.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO; + export_info.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + if (allocate(stage, stage.exporter, type, &export_info, memory) != VK_SUCCESS) { + return -1; + } + + VkMemoryGetFdInfoKHR fd_info = {}; + fd_info.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR; + fd_info.memory = memory; + fd_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + + int descriptor = -1; + if (stage.commands.get_memory_fd(stage.exporter, &fd_info, &descriptor) != VK_SUCCESS) { + return -1; + } + + return descriptor; +} + +// Imports `descriptor` into `memory` on the importer as memory of `type`. The import owns the descriptor once it +// succeeds, and the caller still owns it after a failure. +static VkResult import_memory(const memory_stage& stage, uint32_t type, int descriptor, VkDeviceMemory& memory) { + VkImportMemoryFdInfoKHR import_info = {}; + import_info.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR; + import_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + import_info.fd = descriptor; + + return allocate(stage, stage.importer, type, &import_info, memory); +} + +static bool allocates(const memory_stage& stage, uint32_t type) { + VkDeviceMemory memory = VK_NULL_HANDLE; + bool is_allocated = allocate(stage, stage.exporter, type, nullptr, memory) == VK_SUCCESS; + stage.commands.free_memory(stage.exporter, memory, nullptr); + + return is_allocated; +} + +// Whether memory of `type` still holds what the CPU wrote through one mapping when mapped again +static bool keeps_cpu_writes(const memory_stage& stage, uint32_t type) { + VkDeviceMemory memory = VK_NULL_HANDLE; + if (allocate(stage, stage.exporter, type, nullptr, memory) != VK_SUCCESS) { + return false; + } + + bool keeps_writes = false; + void* written = map(stage, stage.exporter, memory); + if (written != nullptr) { + write_words(written, CPU_WORDS_START + type); + stage.commands.unmap_memory(stage.exporter, memory); + + void* reread = map(stage, stage.exporter, memory); + keeps_writes = reread != nullptr && holds_words(reread, CPU_WORDS_START + type); + } + + stage.commands.free_memory(stage.exporter, memory, nullptr); + + return keeps_writes; +} + +// Whether memory of `type` mapped at a range the program reserved takes the CPU's writes there, and an unmap with +// `VK_MEMORY_UNMAP_RESERVE_BIT_EXT` leaves the range reserved without the memory +static bool maps_at_reserved_range(const memory_stage& stage, uint32_t type) { + void* range = mmap(nullptr, MEMORY_SIZE, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (range == MAP_FAILED) { + return false; + } + + VkDeviceMemory memory = VK_NULL_HANDLE; + bool is_allocated = allocate(stage, stage.exporter, type, nullptr, memory) == VK_SUCCESS; + + VkMemoryMapPlacedInfoEXT placed_info = {}; + placed_info.sType = VK_STRUCTURE_TYPE_MEMORY_MAP_PLACED_INFO_EXT; + placed_info.pPlacedAddress = range; + + VkMemoryMapInfo map_info = {}; + map_info.sType = VK_STRUCTURE_TYPE_MEMORY_MAP_INFO; + map_info.pNext = &placed_info; + map_info.flags = VK_MEMORY_MAP_PLACED_BIT_EXT; + map_info.memory = memory; + map_info.size = VK_WHOLE_SIZE; + + void* placed = nullptr; + bool is_placed = is_allocated && stage.commands.map_memory2(stage.exporter, &map_info, &placed) == VK_SUCCESS && + placed == range; + if (is_placed) { + write_words(placed, PLACED_WORDS_START + type); + } + + VkMemoryUnmapInfo unmap_info = {}; + unmap_info.sType = VK_STRUCTURE_TYPE_MEMORY_UNMAP_INFO; + unmap_info.flags = VK_MEMORY_UNMAP_RESERVE_BIT_EXT; + unmap_info.memory = memory; + bool is_unmapped = is_placed && stage.commands.unmap_memory2(stage.exporter, &unmap_info) == VK_SUCCESS; + + // A mapping that may not replace another fails over a range still reserved + bool keeps_range = false; + if (is_unmapped) { + int no_replace = MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE; + keeps_range = mmap(range, MEMORY_SIZE, PROT_READ, no_replace, -1, 0) == MAP_FAILED && errno == EEXIST; + } + + bool drops_memory = keeps_range && mprotect(range, MEMORY_SIZE, PROT_READ) == 0 && + !holds_words(range, PLACED_WORDS_START + type); + + void* reread = keeps_range ? map(stage, stage.exporter, memory) : nullptr; + bool keeps_writes = reread != nullptr && holds_words(reread, PLACED_WORDS_START + type); + + stage.commands.free_memory(stage.exporter, memory, nullptr); + munmap(range, MEMORY_SIZE); + + return keeps_range && drops_memory && keeps_writes; +} + +// Whether memory of `type` exported by one device and imported by the other shows each device's CPU writes to the +// other device's mapping +static bool shares_through_descriptor(const memory_stage& stage, uint32_t type) { + VkDeviceMemory exported = VK_NULL_HANDLE; + int descriptor = export_memory(stage, type, exported); + + VkDeviceMemory imported = VK_NULL_HANDLE; + bool is_imported = descriptor >= 0 && import_memory(stage, type, descriptor, imported) == VK_SUCCESS; + if (descriptor >= 0 && !is_imported) { + close(descriptor); + } + + bool shares = false; + void* exported_mapping = is_imported ? map(stage, stage.exporter, exported) : nullptr; + void* imported_mapping = is_imported ? map(stage, stage.importer, imported) : nullptr; + if (exported_mapping != nullptr && imported_mapping != nullptr) { + write_words(exported_mapping, EXPORTER_WORDS_START); + bool shows_exporter_writes = holds_words(imported_mapping, EXPORTER_WORDS_START); + + write_words(imported_mapping, IMPORTER_WORDS_START); + shares = shows_exporter_writes && holds_words(exported_mapping, IMPORTER_WORDS_START); + } + + stage.commands.free_memory(stage.importer, imported, nullptr); + stage.commands.free_memory(stage.exporter, exported, nullptr); + + return shares; +} + +// Whether importing memory of `type` through a descriptor with `GPU_RIGHT_READ` alone returns `expected` +static bool imports_read_only_as(const memory_stage& stage, uint32_t type, VkResult expected) { + VkDeviceMemory exported = VK_NULL_HANDLE; + int descriptor = export_memory(stage, type, exported); + int read_only = descriptor >= 0 ? restrict_to_read(stage.instance, stage.physical_device, descriptor) : -1; + if (descriptor >= 0) { + close(descriptor); + } + + VkDeviceMemory imported = VK_NULL_HANDLE; + bool returns_expected = false; + if (read_only >= 0) { + VkResult result = import_memory(stage, type, read_only, imported); + returns_expected = result == expected; + if (result != VK_SUCCESS) { + close(read_only); + } + } + + stage.commands.free_memory(stage.importer, imported, nullptr); + stage.commands.free_memory(stage.exporter, exported, nullptr); + + return returns_expected; +} + +// The CPU could write through a mapping of a host-visible import, so only types it cannot map take one +static bool refuses_read_only_import(const memory_stage& stage, uint32_t type) { + return imports_read_only_as(stage, type, VK_ERROR_INVALID_EXTERNAL_HANDLE); +} + +static bool takes_read_only_import(const memory_stage& stage, uint32_t type) { + return imports_read_only_as(stage, type, VK_SUCCESS); +} + +// A zero `address` lets the driver place the buffer +static VkBuffer create_replayable_buffer(const memory_stage& stage, uint64_t address) { + VkBufferOpaqueCaptureAddressCreateInfo address_info = {}; + address_info.sType = VK_STRUCTURE_TYPE_BUFFER_OPAQUE_CAPTURE_ADDRESS_CREATE_INFO; + address_info.opaqueCaptureAddress = address; + + VkBufferCreateInfo create_info = {}; + create_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + create_info.pNext = &address_info; + create_info.flags = VK_BUFFER_CREATE_DEVICE_ADDRESS_CAPTURE_REPLAY_BIT; + create_info.size = MEMORY_SIZE; + create_info.usage = VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + + VkBuffer buffer = VK_NULL_HANDLE; + if (stage.commands.create_buffer(stage.exporter, &create_info, nullptr, &buffer) != VK_SUCCESS) { + return VK_NULL_HANDLE; + } + + return buffer; +} + +static uint64_t read_capture_address(const memory_stage& stage, VkBuffer buffer) { + VkBufferDeviceAddressInfo address_info = {}; + address_info.sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO; + address_info.buffer = buffer; + + return stage.commands.get_buffer_opaque_capture_address(stage.exporter, &address_info); +} + +// Whether a buffer created at the address of a destroyed buffer binds memory there, which needs the destroyed +// buffer's GPU mapping and address range released +static bool binds_at_released_address(const memory_stage& stage) { + VkBuffer first = create_replayable_buffer(stage, 0); + if (first == VK_NULL_HANDLE) { + return false; + } + + VkMemoryRequirements requirements = {}; + stage.commands.get_buffer_memory_requirements(stage.exporter, first, &requirements); + + VkMemoryAllocateFlagsInfo flags_info = {}; + flags_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO; + flags_info.flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT | VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_CAPTURE_REPLAY_BIT; + bool has_type = requirements.memoryTypeBits != 0; + uint32_t type = has_type ? __builtin_ctz(requirements.memoryTypeBits) : 0; + + VkDeviceMemory memory = VK_NULL_HANDLE; + bool has_memory = has_type && allocate(stage, stage.exporter, type, &flags_info, memory) == VK_SUCCESS; + bool first_binds = has_memory && stage.commands.bind_buffer_memory(stage.exporter, first, memory, 0) == VK_SUCCESS; + uint64_t address = first_binds ? read_capture_address(stage, first) : 0; + stage.commands.destroy_buffer(stage.exporter, first, nullptr); + + VkBuffer second = address != 0 ? create_replayable_buffer(stage, address) : VK_NULL_HANDLE; + bool second_binds = second != VK_NULL_HANDLE && + stage.commands.bind_buffer_memory(stage.exporter, second, memory, 0) == VK_SUCCESS; + bool keeps_address = second_binds && read_capture_address(stage, second) == address; + stage.commands.destroy_buffer(stage.exporter, second, nullptr); + stage.commands.free_memory(stage.exporter, memory, nullptr); + + return keeps_address; +} + +// Stellux GPUs share memory through opaque descriptors alone, so NVK may offer no dma-buf extension +static bool lists_extensions_without_dma_buf(const memory_stage& stage) { + uint32_t extension_count = 0; + VkResult result = + stage.commands.enumerate_device_extensions(stage.physical_device, nullptr, &extension_count, nullptr); + std::vector extensions(extension_count); + if (result == VK_SUCCESS) { + result = stage.commands.enumerate_device_extensions( + stage.physical_device, + nullptr, + &extension_count, + extensions.data() + ); + } + + return result == VK_SUCCESS && extension_count > 0 && + !has_extension(extensions, VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME); +} + +static bool is_opaque_alone(const VkExternalMemoryProperties& properties) { + VkExternalMemoryFeatureFlags features = properties.externalMemoryFeatures; + bool is_exportable = (features & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) != 0; + bool is_importable = (features & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT) != 0; + bool is_compatible_alone = properties.compatibleHandleTypes == VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + + return is_exportable && is_importable && is_compatible_alone; +} + +// Whether the external memory answers for a buffer and a linear image make opaque descriptors exportable and +// importable, and compatible with no other handle type +static bool answers_opaque_descriptors_alone(const memory_stage& stage) { + VkPhysicalDeviceExternalBufferInfo buffer_info = {}; + buffer_info.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_BUFFER_INFO; + buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + buffer_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + + VkExternalBufferProperties buffer_properties = {}; + buffer_properties.sType = VK_STRUCTURE_TYPE_EXTERNAL_BUFFER_PROPERTIES; + stage.commands.get_external_buffer_properties(stage.physical_device, &buffer_info, &buffer_properties); + + VkPhysicalDeviceExternalImageFormatInfo external_info = {}; + external_info.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO; + external_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + + VkPhysicalDeviceImageFormatInfo2 image_info = {}; + image_info.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2; + image_info.pNext = &external_info; + + image_info.format = VK_FORMAT_R8G8B8A8_UNORM; + image_info.type = VK_IMAGE_TYPE_2D; + image_info.tiling = VK_IMAGE_TILING_LINEAR; + image_info.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + + VkExternalImageFormatProperties external_properties = {}; + external_properties.sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES; + + VkImageFormatProperties2 image_properties = {}; + image_properties.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2; + image_properties.pNext = &external_properties; + VkResult image_result = + stage.commands.get_image_format_properties(stage.physical_device, &image_info, &image_properties); + + return is_opaque_alone(buffer_properties.externalMemoryProperties) && image_result == VK_SUCCESS && + is_opaque_alone(external_properties.externalMemoryProperties); +} + +static void check_memory_types(const memory_stage& stage) { + check("every memory type allocates", holds_for_every_type(stage, type_visibility::any, allocates)); + check( + "memory of every host-visible type holds what the CPU wrote through an earlier mapping", + holds_for_every_type(stage, type_visibility::host_visible, keeps_cpu_writes) + ); + check( + "memory of every host-visible type mapped at a reserved range takes the CPU's writes until a reserving unmap", + holds_for_every_type(stage, type_visibility::host_visible, maps_at_reserved_range) + ); + + check( + "memory of every host-visible type shared through an opaque descriptor shows each device's CPU writes", + holds_for_every_type(stage, type_visibility::host_visible, shares_through_descriptor) + ); + check( + "a read-only descriptor is refused as memory of every host-visible type", + holds_for_every_type(stage, type_visibility::host_visible, refuses_read_only_import) + ); + check( + "a read-only descriptor imports as memory of every type the CPU cannot map", + holds_for_every_type(stage, type_visibility::device_only, takes_read_only_import) + ); + + check("a buffer created at a destroyed buffer's address binds memory there", binds_at_released_address(stage)); +} + +void check_memory(VkInstance instance, VkPhysicalDevice physical_device) { + memory_stage stage = {}; + stage.instance = instance; + stage.physical_device = physical_device; + stage.commands = { + .get_memory_properties = find_command( + instance, + "vkGetPhysicalDeviceMemoryProperties" + ), + .enumerate_device_extensions = find_command( + instance, + "vkEnumerateDeviceExtensionProperties" + ), + .get_external_buffer_properties = find_command( + instance, + "vkGetPhysicalDeviceExternalBufferProperties" + ), + .get_image_format_properties = find_command( + instance, + "vkGetPhysicalDeviceImageFormatProperties2" + ), + .destroy_device = find_command(instance, "vkDestroyDevice"), + .allocate_memory = find_command(instance, "vkAllocateMemory"), + .free_memory = find_command(instance, "vkFreeMemory"), + .map_memory = find_command(instance, "vkMapMemory"), + .map_memory2 = find_command(instance, "vkMapMemory2"), + .unmap_memory = find_command(instance, "vkUnmapMemory"), + .unmap_memory2 = find_command(instance, "vkUnmapMemory2"), + .get_memory_fd = find_command(instance, "vkGetMemoryFdKHR"), + .create_buffer = find_command(instance, "vkCreateBuffer"), + .destroy_buffer = find_command(instance, "vkDestroyBuffer"), + .get_buffer_memory_requirements = find_command( + instance, + "vkGetBufferMemoryRequirements" + ), + .bind_buffer_memory = find_command(instance, "vkBindBufferMemory"), + .get_buffer_opaque_capture_address = find_command( + instance, + "vkGetBufferOpaqueCaptureAddress" + ), + }; + + const memory_commands& commands = stage.commands; + bool has_commands = commands.get_memory_properties && commands.enumerate_device_extensions && + commands.get_external_buffer_properties && commands.get_image_format_properties && commands.destroy_device && + commands.allocate_memory && commands.free_memory && commands.map_memory && commands.map_memory2 && + commands.unmap_memory && commands.unmap_memory2 && commands.get_memory_fd && commands.create_buffer && + commands.destroy_buffer && commands.get_buffer_memory_requirements && commands.bind_buffer_memory && + commands.get_buffer_opaque_capture_address; + check("vkGetInstanceProcAddr finds the memory commands vktest calls", has_commands); + if (!has_commands) { + return; + } + + commands.get_memory_properties(physical_device, &stage.properties); + check( + "vkEnumerateDeviceExtensionProperties lists no VK_EXT_external_memory_dma_buf", + lists_extensions_without_dma_buf(stage) + ); + check( + "buffer and linear image queries answer opaque descriptors as exportable, importable and compatible alone", + answers_opaque_descriptors_alone(stage) + ); + + VkPhysicalDeviceMapMemoryPlacedFeaturesEXT placed_features = {}; + placed_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAP_MEMORY_PLACED_FEATURES_EXT; + placed_features.memoryMapPlaced = VK_TRUE; + placed_features.memoryUnmapReserve = VK_TRUE; + + VkPhysicalDeviceVulkan12Features features = {}; + features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + features.pNext = &placed_features; + features.bufferDeviceAddress = VK_TRUE; + features.bufferDeviceAddressCaptureReplay = VK_TRUE; + std::vector extensions = { + VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME, + VK_EXT_MAP_MEMORY_PLACED_EXTENSION_NAME, + }; + + stage.exporter = create_device(instance, physical_device, extensions, &features); + stage.importer = create_device(instance, physical_device, extensions, &features); + bool has_devices = stage.exporter != VK_NULL_HANDLE && stage.importer != VK_NULL_HANDLE; + check("two devices with memory descriptors, replayable buffer addresses and placed maps are created", has_devices); + if (has_devices) { + check_memory_types(stage); + } + + commands.destroy_device(stage.importer, nullptr); + commands.destroy_device(stage.exporter, nullptr); +} diff --git a/userland/apps/vktest/src/timeline.cpp b/userland/apps/vktest/src/timeline.cpp new file mode 100644 index 000000000..4dc34cf70 --- /dev/null +++ b/userland/apps/vktest/src/timeline.cpp @@ -0,0 +1,628 @@ +// The timeline stage: timeline semaphores signalled and waited on from the CPU and from submissions, shared and refused +// through opaque descriptors, and submissions and waits naming more points than one kernel call takes + +#include "vktest.h" + +#include +#include +#include +#include +#include + +struct timeline_commands { + PFN_vkGetPhysicalDeviceExternalSemaphoreProperties get_external_semaphore_properties; + PFN_vkGetPhysicalDeviceExternalFenceProperties get_external_fence_properties; + + PFN_vkCreateDevice create_device; + PFN_vkDestroyDevice destroy_device; + PFN_vkGetDeviceQueue get_device_queue; + PFN_vkQueueSubmit queue_submit; + PFN_vkCreateSemaphore create_semaphore; + PFN_vkDestroySemaphore destroy_semaphore; + PFN_vkGetSemaphoreCounterValue get_counter_value; + PFN_vkSignalSemaphore signal_semaphore; + PFN_vkWaitSemaphores wait_semaphores; + PFN_vkGetSemaphoreFdKHR get_semaphore_fd; + PFN_vkImportSemaphoreFdKHR import_semaphore_fd; + PFN_vkAllocateMemory allocate_memory; + PFN_vkFreeMemory free_memory; + PFN_vkGetMemoryFdKHR get_memory_fd; +}; + +struct timeline_stage { + timeline_commands commands; + VkInstance instance; + VkPhysicalDevice physical_device; + VkDevice device; + VkQueue queue; +}; + +// Which of a wait's points must be reached +enum class wait_mode { + all, + any, +}; + +// More points than one GPU_TIMELINE_WAIT or one GPU_SUBMIT takes +constexpr uint32_t MANY_POINT_COUNT = GPU_POINTS_MAX + 8; + +// Fewer points than one GPU_TIMELINE_WAIT takes, but two, since the runtime drops wait-any from a wait on one point +constexpr uint32_t FEW_POINT_COUNT = 2; + +// How long a wait runs when its points must stay unreached, and when they must be reached +constexpr uint64_t UNREACHED_WAIT_NS = 10000000; +constexpr uint64_t REACHED_WAIT_NS = 1000000000; + +// Bytes of the memory whose opaque descriptor a timeline import must refuse +constexpr VkDeviceSize REFUSED_MEMORY_SIZE = 65536; + +static VkSemaphore create_timeline(const timeline_stage& stage, uint64_t initial_value, bool is_exportable) { + VkExportSemaphoreCreateInfo export_info = {}; + export_info.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO; + export_info.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; + + VkSemaphoreTypeCreateInfo type_info = {}; + type_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; + type_info.pNext = is_exportable ? &export_info : nullptr; + type_info.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + type_info.initialValue = initial_value; + + VkSemaphoreCreateInfo create_info = {}; + create_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + create_info.pNext = &type_info; + + VkSemaphore semaphore = VK_NULL_HANDLE; + if (stage.commands.create_semaphore(stage.device, &create_info, nullptr, &semaphore) != VK_SUCCESS) { + return VK_NULL_HANDLE; + } + + return semaphore; +} + +static std::vector create_timelines(const timeline_stage& stage, uint32_t count) { + std::vector semaphores; + for (uint32_t i = 0; i < count; i++) { + semaphores.push_back(create_timeline(stage, 0, false)); + } + + return semaphores; +} + +static bool are_created(const std::vector& semaphores) { + return std::find(semaphores.begin(), semaphores.end(), VK_NULL_HANDLE) == semaphores.end(); +} + +static void destroy_timelines(const timeline_stage& stage, const std::vector& semaphores) { + for (VkSemaphore semaphore : semaphores) { + stage.commands.destroy_semaphore(stage.device, semaphore, nullptr); + } +} + +static bool reads_value(const timeline_stage& stage, VkSemaphore semaphore, uint64_t expected) { + uint64_t value = 0; + return stage.commands.get_counter_value(stage.device, semaphore, &value) == VK_SUCCESS && value == expected; +} + +static VkResult signal_timeline(const timeline_stage& stage, VkSemaphore semaphore, uint64_t value) { + VkSemaphoreSignalInfo signal_info = {}; + signal_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO; + signal_info.semaphore = semaphore; + signal_info.value = value; + + return stage.commands.signal_semaphore(stage.device, &signal_info); +} + +// Waits for every semaphore of `semaphores`, or for any of them, to reach `value` +static VkResult wait_for_points( + const timeline_stage& stage, + const std::vector& semaphores, + uint64_t value, + wait_mode mode, + uint64_t timeout_ns +) { + std::vector values(semaphores.size(), value); + + VkSemaphoreWaitInfo wait_info = {}; + wait_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + wait_info.flags = mode == wait_mode::any ? VK_SEMAPHORE_WAIT_ANY_BIT : 0; + wait_info.semaphoreCount = static_cast(semaphores.size()); + wait_info.pSemaphores = semaphores.data(); + wait_info.pValues = values.data(); + + return stage.commands.wait_semaphores(stage.device, &wait_info, timeout_ns); +} + +// Submits a batch with no command buffers that waits for every semaphore of `waits` to reach `wait_value` and then +// raises every semaphore of `signals` to `signal_value` +static bool submit_batch( + const timeline_stage& stage, + const std::vector& waits, + uint64_t wait_value, + const std::vector& signals, + uint64_t signal_value +) { + std::vector wait_values(waits.size(), wait_value); + std::vector signal_values(signals.size(), signal_value); + std::vector wait_stages(waits.size(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); + + VkTimelineSemaphoreSubmitInfo timeline_info = {}; + timeline_info.sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO; + timeline_info.waitSemaphoreValueCount = static_cast(wait_values.size()); + timeline_info.pWaitSemaphoreValues = wait_values.data(); + timeline_info.signalSemaphoreValueCount = static_cast(signal_values.size()); + timeline_info.pSignalSemaphoreValues = signal_values.data(); + + VkSubmitInfo submit_info = {}; + submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit_info.pNext = &timeline_info; + submit_info.waitSemaphoreCount = static_cast(waits.size()); + submit_info.pWaitSemaphores = waits.data(); + submit_info.pWaitDstStageMask = wait_stages.data(); + + submit_info.signalSemaphoreCount = static_cast(signals.size()); + submit_info.pSignalSemaphores = signals.data(); + + return stage.commands.queue_submit(stage.queue, 1, &submit_info, VK_NULL_HANDLE) == VK_SUCCESS; +} + +static int export_timeline(const timeline_stage& stage, VkSemaphore semaphore) { + VkSemaphoreGetFdInfoKHR fd_info = {}; + fd_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR; + fd_info.semaphore = semaphore; + fd_info.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; + + int descriptor = -1; + if (stage.commands.get_semaphore_fd(stage.device, &fd_info, &descriptor) != VK_SUCCESS) { + return -1; + } + + return descriptor; +} + +// Imports `descriptor` into `semaphore`. The import owns the descriptor once it succeeds, and the caller still owns it +// after a failure. +static VkResult import_timeline(const timeline_stage& stage, VkSemaphore semaphore, int descriptor) { + VkImportSemaphoreFdInfoKHR import_info = {}; + import_info.sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR; + import_info.semaphore = semaphore; + import_info.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; + import_info.fd = descriptor; + + return stage.commands.import_semaphore_fd(stage.device, &import_info); +} + +// Allocates exportable memory of type 0, which every device has, into `memory` and returns an opaque descriptor of it, +// or -1 +static int export_memory(const timeline_stage& stage, VkDeviceMemory& memory) { + VkExportMemoryAllocateInfo export_info = {}; + export_info.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO; + export_info.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + + VkMemoryAllocateInfo allocate_info = {}; + allocate_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocate_info.pNext = &export_info; + allocate_info.allocationSize = REFUSED_MEMORY_SIZE; + allocate_info.memoryTypeIndex = 0; + if (stage.commands.allocate_memory(stage.device, &allocate_info, nullptr, &memory) != VK_SUCCESS) { + return -1; + } + + VkMemoryGetFdInfoKHR fd_info = {}; + fd_info.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR; + fd_info.memory = memory; + fd_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; + + int descriptor = -1; + if (stage.commands.get_memory_fd(stage.device, &fd_info, &descriptor) != VK_SUCCESS) { + return -1; + } + + return descriptor; +} + +// Creates a device with no queues on the stage's physical device, or returns `VK_NULL_HANDLE` +static VkDevice create_queueless_device(const timeline_stage& stage) { + VkPhysicalDeviceMaintenance9FeaturesKHR maintenance9 = {}; + maintenance9.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_9_FEATURES_KHR; + maintenance9.maintenance9 = VK_TRUE; + + VkPhysicalDeviceVulkan12Features features = {}; + features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + features.pNext = &maintenance9; + features.timelineSemaphore = VK_TRUE; + + const char* extension = VK_KHR_MAINTENANCE_9_EXTENSION_NAME; + VkDeviceCreateInfo create_info = {}; + create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + create_info.pNext = &features; + create_info.enabledExtensionCount = 1; + create_info.ppEnabledExtensionNames = &extension; + + VkDevice device = VK_NULL_HANDLE; + if (stage.commands.create_device(stage.physical_device, &create_info, nullptr, &device) != VK_SUCCESS) { + return VK_NULL_HANDLE; + } + + return device; +} + +// Creates a semaphore holding `exported`'s timeline through a descriptor with `GPU_RIGHT_READ` alone, or returns +// `VK_NULL_HANDLE` +static VkSemaphore import_read_only(const timeline_stage& stage, VkSemaphore exported) { + int descriptor = export_timeline(stage, exported); + int read_only = descriptor >= 0 ? restrict_to_read(stage.instance, stage.physical_device, descriptor) : -1; + if (descriptor >= 0) { + close(descriptor); + } + + VkSemaphore imported = read_only >= 0 ? create_timeline(stage, 0, false) : VK_NULL_HANDLE; + if (imported != VK_NULL_HANDLE && import_timeline(stage, imported, read_only) == VK_SUCCESS) { + return imported; + } + + if (read_only >= 0) { + close(read_only); + } + + stage.commands.destroy_semaphore(stage.device, imported, nullptr); + + return VK_NULL_HANDLE; +} + +// Whether a timeline reads its initial value and then the value the CPU signals, ends a wait for that value and holds +// a wait for a higher one until its timeout +static bool counts_cpu_signals(const timeline_stage& stage) { + const uint64_t initial_value = 5; + const uint64_t signalled_value = 9; + VkSemaphore semaphore = create_timeline(stage, initial_value, false); + if (semaphore == VK_NULL_HANDLE) { + return false; + } + + bool reads_initial_value = reads_value(stage, semaphore, initial_value); + bool reads_signalled_value = signal_timeline(stage, semaphore, signalled_value) == VK_SUCCESS && + reads_value(stage, semaphore, signalled_value); + bool ends_reached_wait = wait_for_points(stage, {semaphore}, signalled_value, wait_mode::all, 0) == VK_SUCCESS; + bool holds_unreached_wait = + wait_for_points(stage, {semaphore}, signalled_value + 1, wait_mode::all, UNREACHED_WAIT_NS) == VK_TIMEOUT; + stage.commands.destroy_semaphore(stage.device, semaphore, nullptr); + + return reads_initial_value && reads_signalled_value && ends_reached_wait && holds_unreached_wait; +} + +// Whether a device created with no queues, which NVK makes without a GPU context, signals a timeline and ends a CPU +// wait for the signalled value +static bool signals_without_queues(const timeline_stage& stage) { + timeline_stage queueless = stage; + queueless.device = create_queueless_device(stage); + queueless.queue = VK_NULL_HANDLE; + if (queueless.device == VK_NULL_HANDLE) { + return false; + } + + VkSemaphore semaphore = create_timeline(queueless, 0, false); + bool ends_wait = semaphore != VK_NULL_HANDLE && signal_timeline(queueless, semaphore, 1) == VK_SUCCESS && + wait_for_points(queueless, {semaphore}, 1, wait_mode::all, REACHED_WAIT_NS) == VK_SUCCESS; + queueless.commands.destroy_semaphore(queueless.device, semaphore, nullptr); + queueless.commands.destroy_device(queueless.device, nullptr); + + return ends_wait; +} + +// Whether a submission waiting on a point nobody has signalled is held, and runs once the CPU signals the point +static bool holds_submission_until_signal(const timeline_stage& stage) { + VkSemaphore awaited = create_timeline(stage, 0, false); + VkSemaphore signalled = create_timeline(stage, 0, false); + bool is_submitted = awaited != VK_NULL_HANDLE && signalled != VK_NULL_HANDLE && + submit_batch(stage, {awaited}, 1, {signalled}, 1); + + bool is_held = is_submitted && + wait_for_points(stage, {signalled}, 1, wait_mode::all, UNREACHED_WAIT_NS) == VK_TIMEOUT; + bool runs = is_submitted && signal_timeline(stage, awaited, 1) == VK_SUCCESS && + wait_for_points(stage, {signalled}, 1, wait_mode::all, REACHED_WAIT_NS) == VK_SUCCESS; + stage.commands.destroy_semaphore(stage.device, signalled, nullptr); + stage.commands.destroy_semaphore(stage.device, awaited, nullptr); + + return is_held && runs; +} + +static VkExternalSemaphoreFeatureFlags semaphore_features( + const timeline_stage& stage, + VkSemaphoreType type, + VkExternalSemaphoreHandleTypeFlagBits handle_type +) { + VkSemaphoreTypeCreateInfo type_info = {}; + type_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; + type_info.semaphoreType = type; + + VkPhysicalDeviceExternalSemaphoreInfo semaphore_info = {}; + semaphore_info.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_SEMAPHORE_INFO; + semaphore_info.pNext = &type_info; + semaphore_info.handleType = handle_type; + + VkExternalSemaphoreProperties properties = {}; + properties.sType = VK_STRUCTURE_TYPE_EXTERNAL_SEMAPHORE_PROPERTIES; + stage.commands.get_external_semaphore_properties(stage.physical_device, &semaphore_info, &properties); + + return properties.externalSemaphoreFeatures; +} + +static VkExternalFenceFeatureFlags fence_features( + const timeline_stage& stage, + VkExternalFenceHandleTypeFlagBits handle_type +) { + VkPhysicalDeviceExternalFenceInfo fence_info = {}; + fence_info.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_FENCE_INFO; + fence_info.handleType = handle_type; + + VkExternalFenceProperties properties = {}; + properties.sType = VK_STRUCTURE_TYPE_EXTERNAL_FENCE_PROPERTIES; + stage.commands.get_external_fence_properties(stage.physical_device, &fence_info, &properties); + + return properties.externalFenceFeatures; +} + +// Whether binary semaphores and fences report no external handle type, and timeline semaphores report opaque +// descriptors as exportable and importable +static bool answers_semaphore_handle_types(const timeline_stage& stage) { + VkExternalSemaphoreFeatureFlags timeline_features = + semaphore_features(stage, VK_SEMAPHORE_TYPE_TIMELINE, VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT); + bool shares_timelines = timeline_features == + (VK_EXTERNAL_SEMAPHORE_FEATURE_EXPORTABLE_BIT | VK_EXTERNAL_SEMAPHORE_FEATURE_IMPORTABLE_BIT); + + bool shares_no_binary = + semaphore_features(stage, VK_SEMAPHORE_TYPE_BINARY, VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT) == 0 && + semaphore_features(stage, VK_SEMAPHORE_TYPE_BINARY, VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT) == 0; + bool shares_no_fence = fence_features(stage, VK_EXTERNAL_FENCE_HANDLE_TYPE_OPAQUE_FD_BIT) == 0 && + fence_features(stage, VK_EXTERNAL_FENCE_HANDLE_TYPE_SYNC_FD_BIT) == 0; + + return shares_timelines && shares_no_binary && shares_no_fence; +} + +// Whether a timeline exported through an opaque descriptor and imported into a second semaphore shows each +// semaphore's signals through the other +static bool shares_value_through_descriptor(const timeline_stage& stage) { + VkSemaphore exported = create_timeline(stage, 0, true); + VkSemaphore imported = create_timeline(stage, 0, false); + int descriptor = exported != VK_NULL_HANDLE ? export_timeline(stage, exported) : -1; + bool is_imported = imported != VK_NULL_HANDLE && descriptor >= 0 && + import_timeline(stage, imported, descriptor) == VK_SUCCESS; + if (descriptor >= 0 && !is_imported) { + close(descriptor); + } + + bool shows_exporter_signal = is_imported && signal_timeline(stage, exported, 3) == VK_SUCCESS && + reads_value(stage, imported, 3); + bool shows_importer_signal = is_imported && signal_timeline(stage, imported, 4) == VK_SUCCESS && + reads_value(stage, exported, 4); + stage.commands.destroy_semaphore(stage.device, imported, nullptr); + stage.commands.destroy_semaphore(stage.device, exported, nullptr); + + return shows_exporter_signal && shows_importer_signal; +} + +// Whether a read-only import of a timeline holds a CPU wait and a submission's wait until the exporter signals, and +// ends both after +static bool waits_on_read_only_import(const timeline_stage& stage) { + VkSemaphore exported = create_timeline(stage, 0, true); + VkSemaphore read_only = exported != VK_NULL_HANDLE ? import_read_only(stage, exported) : VK_NULL_HANDLE; + VkSemaphore signalled = create_timeline(stage, 0, false); + bool has_semaphores = read_only != VK_NULL_HANDLE && signalled != VK_NULL_HANDLE; + bool is_submitted = has_semaphores && submit_batch(stage, {read_only}, 1, {signalled}, 1); + + bool holds_waits = is_submitted && + wait_for_points(stage, {read_only}, 1, wait_mode::all, UNREACHED_WAIT_NS) == VK_TIMEOUT && + reads_value(stage, signalled, 0); + bool is_signalled = is_submitted && signal_timeline(stage, exported, 1) == VK_SUCCESS; + bool ends_waits = is_signalled && + wait_for_points(stage, {read_only}, 1, wait_mode::all, REACHED_WAIT_NS) == VK_SUCCESS && + wait_for_points(stage, {signalled}, 1, wait_mode::all, REACHED_WAIT_NS) == VK_SUCCESS; + stage.commands.destroy_semaphore(stage.device, signalled, nullptr); + stage.commands.destroy_semaphore(stage.device, read_only, nullptr); + stage.commands.destroy_semaphore(stage.device, exported, nullptr); + + return holds_waits && ends_waits; +} + +// Whether a read-only import of a timeline refuses a CPU signal with `VK_ERROR_UNKNOWN`, leaving the timeline's value +static bool refuses_signal_of_read_only_import(const timeline_stage& stage) { + VkSemaphore exported = create_timeline(stage, 0, true); + VkSemaphore read_only = exported != VK_NULL_HANDLE ? import_read_only(stage, exported) : VK_NULL_HANDLE; + + bool refuses_signal = read_only != VK_NULL_HANDLE && signal_timeline(stage, read_only, 1) == VK_ERROR_UNKNOWN; + bool keeps_value = refuses_signal && reads_value(stage, exported, 0); + stage.commands.destroy_semaphore(stage.device, read_only, nullptr); + stage.commands.destroy_semaphore(stage.device, exported, nullptr); + + return keeps_value; +} + +// Whether an opaque descriptor of memory, which the kernel restricts as it does a timeline's, is refused as a timeline +// semaphore's payload with the result for an invalid handle, leaving the caller its descriptor +static bool refuses_memory_descriptor(const timeline_stage& stage) { + VkDeviceMemory memory = VK_NULL_HANDLE; + int descriptor = export_memory(stage, memory); + VkSemaphore semaphore = descriptor >= 0 ? create_timeline(stage, 0, false) : VK_NULL_HANDLE; + + bool is_refused = semaphore != VK_NULL_HANDLE && + import_timeline(stage, semaphore, descriptor) == VK_ERROR_INVALID_EXTERNAL_HANDLE; + bool stays_open = descriptor >= 0 && close(descriptor) == 0; + stage.commands.destroy_semaphore(stage.device, semaphore, nullptr); + stage.commands.free_memory(stage.device, memory, nullptr); + + return is_refused && stays_open; +} + +// Whether a submission with more waits and signals than one GPU_SUBMIT takes is held while only its wait at +// `holding_wait` is unreached, and reaches every signal once that wait is reached +static bool runs_submission_held_by_wait(const timeline_stage& stage, uint32_t holding_wait) { + std::vector waits = create_timelines(stage, MANY_POINT_COUNT); + std::vector signals = create_timelines(stage, MANY_POINT_COUNT); + bool is_submitted = are_created(waits) && are_created(signals) && submit_batch(stage, waits, 1, signals, 1); + + bool signals_other_waits = is_submitted; + for (uint32_t i = 0; is_submitted && i < MANY_POINT_COUNT; i++) { + if (i != holding_wait) { + signals_other_waits &= signal_timeline(stage, waits[i], 1) == VK_SUCCESS; + } + } + + bool is_held = signals_other_waits && + wait_for_points(stage, signals, 1, wait_mode::any, UNREACHED_WAIT_NS) == VK_TIMEOUT; + bool runs = is_held && signal_timeline(stage, waits[holding_wait], 1) == VK_SUCCESS && + wait_for_points(stage, signals, 1, wait_mode::all, REACHED_WAIT_NS) == VK_SUCCESS; + destroy_timelines(stage, signals); + destroy_timelines(stage, waits); + + return runs; +} + +// Whether a submission with more waits and signals than one GPU_SUBMIT takes is held while only its first wait, or its +// first wait past `GPU_POINTS_MAX` in the next GPU_SUBMIT, is unreached, and reaches every signal once it is reached +static bool runs_submission_past_kernel_points(const timeline_stage& stage) { + return runs_submission_held_by_wait(stage, 0) && runs_submission_held_by_wait(stage, GPU_POINTS_MAX); +} + +// Whether a wait for all of more points than one GPU_TIMELINE_WAIT takes is held while only its last point is +// unreached, and ends once that point is reached +static bool waits_for_all_past_kernel_points(const timeline_stage& stage) { + std::vector semaphores = create_timelines(stage, MANY_POINT_COUNT); + bool has_semaphores = are_created(semaphores); + + bool signals_all_but_last = has_semaphores; + for (uint32_t i = 0; has_semaphores && i + 1 < MANY_POINT_COUNT; i++) { + signals_all_but_last &= signal_timeline(stage, semaphores[i], 1) == VK_SUCCESS; + } + + bool is_held = signals_all_but_last && + wait_for_points(stage, semaphores, 1, wait_mode::all, UNREACHED_WAIT_NS) == VK_TIMEOUT; + bool ends = signals_all_but_last && signal_timeline(stage, semaphores.back(), 1) == VK_SUCCESS && + wait_for_points(stage, semaphores, 1, wait_mode::all, REACHED_WAIT_NS) == VK_SUCCESS; + destroy_timelines(stage, semaphores); + + return is_held && ends; +} + +// Whether a wait for any of `count` points is held while none is reached, and ends once the last point is reached +static bool waits_for_any_of(const timeline_stage& stage, uint32_t count) { + std::vector semaphores = create_timelines(stage, count); + bool has_semaphores = are_created(semaphores); + + bool is_held = has_semaphores && + wait_for_points(stage, semaphores, 1, wait_mode::any, UNREACHED_WAIT_NS) == VK_TIMEOUT; + bool ends = has_semaphores && signal_timeline(stage, semaphores.back(), 1) == VK_SUCCESS && + wait_for_points(stage, semaphores, 1, wait_mode::any, REACHED_WAIT_NS) == VK_SUCCESS; + destroy_timelines(stage, semaphores); + + return is_held && ends; +} + +static void check_timelines(const timeline_stage& stage) { + check( + "a timeline reads its initial value and a CPU signal's, ends a wait for it and holds a wait for a higher value", + counts_cpu_signals(stage) + ); + check( + "a device created with no queues signals a timeline semaphore and ends a CPU wait on it", + signals_without_queues(stage) + ); + check( + "a submission waiting on an unsignalled point runs once the CPU signals it", + holds_submission_until_signal(stage) + ); + + check( + "binary semaphores and fences report no external handle type, and timeline semaphores opaque descriptors", + answers_semaphore_handle_types(stage) + ); + check( + "a timeline shared through an opaque descriptor shows each semaphore's signals through the other", + shares_value_through_descriptor(stage) + ); + check( + "a read-only descriptor of a timeline is waited on from the CPU and from a submission", + waits_on_read_only_import(stage) + ); + check("a read-only descriptor of a timeline refuses a CPU signal", refuses_signal_of_read_only_import(stage)); + check( + "a timeline semaphore refuses an opaque memory descriptor as an invalid external handle and leaves it open", + refuses_memory_descriptor(stage) + ); + + check( + "a submission with more waits and signals than one GPU_SUBMIT takes runs once every wait is reached", + runs_submission_past_kernel_points(stage) + ); + check( + "a wait for all of more points than one GPU_TIMELINE_WAIT takes is held until its last point is reached", + waits_for_all_past_kernel_points(stage) + ); + check( + "a wait for any of more points than one GPU_TIMELINE_WAIT takes ends once one is reached", + waits_for_any_of(stage, MANY_POINT_COUNT) + ); + check( + "a wait for any of fewer points than one GPU_TIMELINE_WAIT takes ends once one is reached", + waits_for_any_of(stage, FEW_POINT_COUNT) + ); +} + +void check_timeline(VkInstance instance, VkPhysicalDevice physical_device) { + timeline_stage stage = {}; + stage.instance = instance; + stage.physical_device = physical_device; + stage.commands = { + .get_external_semaphore_properties = find_command( + instance, + "vkGetPhysicalDeviceExternalSemaphoreProperties" + ), + .get_external_fence_properties = find_command( + instance, + "vkGetPhysicalDeviceExternalFenceProperties" + ), + .create_device = find_command(instance, "vkCreateDevice"), + .destroy_device = find_command(instance, "vkDestroyDevice"), + .get_device_queue = find_command(instance, "vkGetDeviceQueue"), + .queue_submit = find_command(instance, "vkQueueSubmit"), + .create_semaphore = find_command(instance, "vkCreateSemaphore"), + .destroy_semaphore = find_command(instance, "vkDestroySemaphore"), + .get_counter_value = find_command(instance, "vkGetSemaphoreCounterValue"), + .signal_semaphore = find_command(instance, "vkSignalSemaphore"), + .wait_semaphores = find_command(instance, "vkWaitSemaphores"), + .get_semaphore_fd = find_command(instance, "vkGetSemaphoreFdKHR"), + .import_semaphore_fd = find_command(instance, "vkImportSemaphoreFdKHR"), + .allocate_memory = find_command(instance, "vkAllocateMemory"), + .free_memory = find_command(instance, "vkFreeMemory"), + .get_memory_fd = find_command(instance, "vkGetMemoryFdKHR"), + }; + + const timeline_commands& commands = stage.commands; + bool has_commands = commands.get_external_semaphore_properties && commands.get_external_fence_properties && + commands.create_device && commands.destroy_device && commands.get_device_queue && + commands.queue_submit && commands.create_semaphore && commands.destroy_semaphore && + commands.get_counter_value && commands.signal_semaphore && commands.wait_semaphores && + commands.get_semaphore_fd && commands.import_semaphore_fd && commands.allocate_memory && + commands.free_memory && commands.get_memory_fd; + check("vkGetInstanceProcAddr finds the timeline commands vktest calls", has_commands); + if (!has_commands) { + return; + } + + VkPhysicalDeviceVulkan12Features features = {}; + features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + features.timelineSemaphore = VK_TRUE; + std::vector extensions = { + VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME, + VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME, + }; + + stage.device = create_device(instance, physical_device, extensions, &features); + check("a device with timeline semaphores and their descriptors is created", stage.device != VK_NULL_HANDLE); + if (stage.device == VK_NULL_HANDLE) { + return; + } + + commands.get_device_queue(stage.device, 0, 0, &stage.queue); + check_timelines(stage); + commands.destroy_device(stage.device, nullptr); +} diff --git a/userland/apps/vktest/src/vktest.cpp b/userland/apps/vktest/src/vktest.cpp index a477e0e4c..1ea9fa53e 100644 --- a/userland/apps/vktest/src/vktest.cpp +++ b/userland/apps/vktest/src/vktest.cpp @@ -1,6 +1,8 @@ // Tests the Vulkan library a Stellux program links, one stage per part of the driver. Every // check prints a PASS or FAIL line, and the program exits with 1 when any check failed. +#include "vktest.h" + #include #include #include @@ -23,7 +25,7 @@ struct gpu_node { static int g_checks = 0; static int g_failed_checks = 0; -static void check(const char* name, bool passed) { +void check(const char* name, bool passed) { printf(" %s: %s\n", passed ? "PASS" : "FAIL", name); g_checks++; @@ -32,7 +34,7 @@ static void check(const char* name, bool passed) { } } -static bool has_extension(const std::vector& extensions, const char* name) { +bool has_extension(const std::vector& extensions, const char* name) { for (const VkExtensionProperties& extension : extensions) { if (strcmp(extension.extensionName, name) == 0) { return true; @@ -42,12 +44,6 @@ static bool has_extension(const std::vector& extensions, return false; } -// The library exports only the global commands, so every other command comes from `vkGetInstanceProcAddr` -template -static Command find_command(VkInstance instance, const char* name) { - return reinterpret_cast(vkGetInstanceProcAddr(instance, name)); -} - template static bool finds_linked_command(const char* name, Command command) { return vkGetInstanceProcAddr(VK_NULL_HANDLE, name) == reinterpret_cast(command); @@ -157,6 +153,47 @@ static VkPhysicalDevicePCIBusInfoPropertiesEXT read_pci_bus( return pci_bus; } +// Opens the /dev/gpuN at `physical_device`'s PCI address, or returns -1 +static int open_gpu_file_of(VkInstance instance, VkPhysicalDevice physical_device) { + auto get_properties = find_command(instance, "vkGetPhysicalDeviceProperties2"); + if (!get_properties) { + return -1; + } + + VkPhysicalDevicePCIBusInfoPropertiesEXT pci_bus = read_pci_bus(get_properties, physical_device); + for (uint32_t index = 0;; index++) { + char path[32]; + snprintf(path, sizeof(path), "/dev/gpu%u", index); + + int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + return -1; + } + + gpu_info info = {}; + if (ioctl(fd, GPU_GET_INFO, &info) == 0 && is_at_address(info, pci_bus)) { + return fd; + } + + close(fd); + } +} + +int restrict_to_read(VkInstance instance, VkPhysicalDevice physical_device, int descriptor) { + int gpu_fd = open_gpu_file_of(instance, physical_device); + if (gpu_fd < 0) { + return -1; + } + + gpu_restrict_args restriction = {}; + restriction.handle = descriptor; + restriction.rights = GPU_RIGHT_READ; + int result = ioctl(gpu_fd, GPU_HANDLE_RESTRICT, &restriction); + close(gpu_fd); + + return result == 0 ? restriction.restricted : -1; +} + // NVK accepts every chip the kernel drives, so its physical devices are the NVIDIA GPUs with a 3D class static bool lists_exactly_the_nvidia_3d_gpus( VkInstance instance, @@ -453,6 +490,15 @@ static void check_instance() { for (VkPhysicalDevice physical_device : physical_devices) { printf("vktest: physical device\n"); check_physical_device(instance, physical_device, nodes); + + printf("vktest: device\n"); + check_device(instance, physical_device); + + printf("vktest: memory\n"); + check_memory(instance, physical_device); + + printf("vktest: timeline\n"); + check_timeline(instance, physical_device); } destroy_instance(instance, nullptr); diff --git a/userland/apps/vktest/src/vktest.h b/userland/apps/vktest/src/vktest.h new file mode 100644 index 000000000..6a16eaef5 --- /dev/null +++ b/userland/apps/vktest/src/vktest.h @@ -0,0 +1,42 @@ +#ifndef VKTEST_H +#define VKTEST_H + +#include +#include + +// Prints a PASS or FAIL line for one check and counts it toward the summary +void check(const char* name, bool passed); + +bool has_extension(const std::vector& extensions, const char* name); + +// The library exports only the global commands, so every other command comes from `vkGetInstanceProcAddr` +template +Command find_command(VkInstance instance, const char* name) { + return reinterpret_cast(vkGetInstanceProcAddr(instance, name)); +} + +// Makes a descriptor with `GPU_RIGHT_READ` alone for the object `descriptor` names, through the /dev/gpuN of +// `physical_device` and outside Vulkan, or returns -1. The caller owns the new descriptor. +int restrict_to_read(VkInstance instance, VkPhysicalDevice physical_device, int descriptor); + +// Creates a device with one queue of family 0, the family with every kind of work on NVK, enabling `extensions` and +// the feature structures chained at `features`, or returns `VK_NULL_HANDLE` +VkDevice create_device( + VkInstance instance, + VkPhysicalDevice physical_device, + const std::vector& extensions, + const void* features +); + +// Creates devices on `physical_device` with each queue family alone and with all of them, each queue and the device +// waiting idle +void check_device(VkInstance instance, VkPhysicalDevice physical_device); + +// Allocates memory of every type on `physical_device`, maps and shares the host-visible types and imports the others +// read-only, and binds a buffer at a destroyed buffer's address +void check_memory(VkInstance instance, VkPhysicalDevice physical_device); + +// Signals, waits on and shares timeline semaphores on `physical_device`, from the CPU and from submissions +void check_timeline(VkInstance instance, VkPhysicalDevice physical_device); + +#endif // VKTEST_H From 65d62dea5aa76d3fc5211c3f0f17463358944d01 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 7 Nov 2026 01:59:05 +0000 Subject: [PATCH 2/2] chore(packages): pinned packages-2026.11.06.1 --- packages/packages.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/packages.lock b/packages/packages.lock index b819cc6e1..46cde3d73 100644 --- a/packages/packages.lock +++ b/packages/packages.lock @@ -8,15 +8,15 @@ source https://github.com/FlareCoding/StelluxOS/releases/download -release packages-2026.11.05.2 +release packages-2026.11.06.1 binutils 2.45-1 aarch64 675fffaaf7fdc88b48fc9fa386703beb4b71a0472fe2c0613aa90692fab4792b binutils 2.45-1 x86_64 dc63ca5ee9a8812814c7d9b504ecf42cc8b7d8f5b31fbf9a44dca37115fc3339 chromium 151.0.7922.137-2 aarch64 5acfbe79d23e45b1f1e55180dd5b990a8e86764e2ee8a607f62d1e59cadff9c1 chromium 151.0.7922.137-2 x86_64 4f076eb153b6ccf86228003d6f0b69985230c28bb644ad60db55da0fc8fe2802 gcc 14.3.0-1 aarch64 db675605a8bda46ce0958c0377b8c2ab84d4f9b0fe071376751f8368028b1359 gcc 14.3.0-1 x86_64 13744f4caa32b9353d16e9c0c4f153a32efb3f701e3ae12578693129b0d9aa11 -mesa 26.2.4-2 aarch64 37641aaff1587f40283649358f2e6c4df4f7bdc26f9e97e0a693ba8f8d5951fe -mesa 26.2.4-2 x86_64 2cc8a57f19709a649fc70ae0954da3ba3de2d8fb4d14220b8c0bd81bdba0380d +mesa 26.2.4-3 aarch64 dccf1f80501a9cf4bbb1310b71e9b9c4c551a33c23e839321adab3bf5d71319d +mesa 26.2.4-3 x86_64 f0bdffd41f790813cae68984a5dd7200f530a59dbf51dde3d8376a9a4a8491bd nvidia-firmware 535.183.01-1 aarch64 11d63d763b4cea869f260d8a9fc2eefc0fd896ac9184b92ba54b8ae4eb39e422 nvidia-firmware 535.183.01-1 x86_64 11d63d763b4cea869f260d8a9fc2eefc0fd896ac9184b92ba54b8ae4eb39e422 python 3.12.8-1 aarch64 28c31549416f02bafc000c9a280ff34459179c26bd9a5f2db49ee30d55cdcbec @@ -25,6 +25,6 @@ qt 6.11.2-1 aarch64 ebb0bc15e39b443ddb4b1fc28335831aaef1ddf00dac19802eddfff47b84 qt 6.11.2-1 x86_64 e3d55f8a443cfd684cfa88f51aa4fb455b552efd99f6acf81f0f4748b92c11c6 recipe toolchain cec0806f9588eaab89d64f7cc51805be050769b6 recipe chromium c824f5221f1d7180704ff224e0c31111c0a4b122 -recipe mesa ea7c9dac512fd95253fb18f06c2a2f5437e3d6cf +recipe mesa c61a502ea972ad4339b250c6519fab3886da42cc recipe nvidia-firmware 52dbd804f7fe7b75a6f36b58080b247fa55f5c1c recipe qt a8d7093c1284021bce9636f7144cf741aed5807e