mirror of
https://gitlab.freedesktop.org/wlroots/wlroots.git
synced 2024-11-24 08:02:21 +00:00
render/vulkan: add support for explicit sync
This commit is contained in:
parent
9351c78d70
commit
3bbfae73ae
@ -40,6 +40,7 @@ struct wlr_vk_device {
|
||||
|
||||
int drm_fd;
|
||||
|
||||
bool sync_file_import_export;
|
||||
bool implicit_sync_interop;
|
||||
bool sampler_ycbcr_conversion;
|
||||
|
||||
@ -253,6 +254,8 @@ struct wlr_vk_command_buffer {
|
||||
|
||||
// For DMA-BUF implicit sync interop, may be NULL
|
||||
VkSemaphore binary_semaphore;
|
||||
|
||||
struct wl_array wait_semaphores; // VkSemaphore
|
||||
};
|
||||
|
||||
#define VULKAN_COMMAND_BUFFERS_CAP 64
|
||||
@ -367,6 +370,13 @@ VkCommandBuffer vulkan_record_stage_cb(struct wlr_vk_renderer *renderer);
|
||||
// finished execution.
|
||||
bool vulkan_submit_stage_wait(struct wlr_vk_renderer *renderer);
|
||||
|
||||
struct wlr_vk_render_pass_texture {
|
||||
struct wlr_vk_texture *texture;
|
||||
|
||||
struct wlr_drm_syncobj_timeline *wait_timeline;
|
||||
uint64_t wait_point;
|
||||
};
|
||||
|
||||
struct wlr_vk_render_pass {
|
||||
struct wlr_render_pass base;
|
||||
struct wlr_vk_renderer *renderer;
|
||||
@ -378,6 +388,11 @@ struct wlr_vk_render_pass {
|
||||
bool failed;
|
||||
bool srgb_pathway; // if false, rendering via intermediate blending buffer
|
||||
struct wlr_color_transform *color_transform;
|
||||
|
||||
struct wlr_drm_syncobj_timeline *signal_timeline;
|
||||
uint64_t signal_point;
|
||||
|
||||
struct wl_array textures; // struct wlr_vk_render_pass_texture
|
||||
};
|
||||
|
||||
struct wlr_vk_render_pass *vulkan_begin_render_pass(struct wlr_vk_renderer *renderer,
|
||||
@ -419,8 +434,10 @@ bool vulkan_wait_command_buffer(struct wlr_vk_command_buffer *cb,
|
||||
struct wlr_vk_renderer *renderer);
|
||||
|
||||
bool vulkan_sync_render_buffer(struct wlr_vk_renderer *renderer,
|
||||
struct wlr_vk_render_buffer *render_buffer, struct wlr_vk_command_buffer *cb);
|
||||
bool vulkan_sync_foreign_texture(struct wlr_vk_texture *texture);
|
||||
struct wlr_vk_render_buffer *render_buffer, struct wlr_vk_command_buffer *cb,
|
||||
struct wlr_drm_syncobj_timeline *signal_timeline, uint64_t signal_point);
|
||||
bool vulkan_sync_foreign_texture(struct wlr_vk_texture *texture,
|
||||
int sync_file_fds[static WLR_DMABUF_MAX_PLANES]);
|
||||
|
||||
bool vulkan_read_pixels(struct wlr_vk_renderer *vk_renderer,
|
||||
VkFormat src_format, VkImage src_image,
|
||||
@ -450,8 +467,6 @@ struct wlr_vk_texture {
|
||||
// If imported from a wlr_buffer
|
||||
struct wlr_buffer *buffer;
|
||||
struct wlr_addon buffer_addon;
|
||||
// For DMA-BUF implicit sync interop
|
||||
VkSemaphore foreign_semaphores[WLR_DMABUF_MAX_PLANES];
|
||||
|
||||
struct wl_list views; // struct wlr_vk_texture_ds.link
|
||||
};
|
||||
|
@ -1,8 +1,10 @@
|
||||
#include <assert.h>
|
||||
#include <drm_fourcc.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <wlr/util/log.h>
|
||||
#include <wlr/render/color.h>
|
||||
#include <wlr/render/drm_syncobj.h>
|
||||
|
||||
#include "render/color.h"
|
||||
#include "render/vulkan.h"
|
||||
@ -80,11 +82,65 @@ static void mat3_to_mat4(const float mat3[9], float mat4[4][4]) {
|
||||
}
|
||||
|
||||
static void render_pass_destroy(struct wlr_vk_render_pass *pass) {
|
||||
struct wlr_vk_render_pass_texture *pass_texture;
|
||||
wl_array_for_each(pass_texture, &pass->textures) {
|
||||
wlr_drm_syncobj_timeline_unref(pass_texture->wait_timeline);
|
||||
}
|
||||
|
||||
wlr_color_transform_unref(pass->color_transform);
|
||||
wlr_drm_syncobj_timeline_unref(pass->signal_timeline);
|
||||
rect_union_finish(&pass->updated_region);
|
||||
wl_array_release(&pass->textures);
|
||||
free(pass);
|
||||
}
|
||||
|
||||
static VkSemaphore render_pass_wait_sync_file(struct wlr_vk_render_pass *pass,
|
||||
size_t sem_index, int sync_file_fd) {
|
||||
struct wlr_vk_renderer *renderer = pass->renderer;
|
||||
struct wlr_vk_command_buffer *render_cb = pass->command_buffer;
|
||||
VkResult res;
|
||||
|
||||
VkSemaphore *wait_semaphores = render_cb->wait_semaphores.data;
|
||||
size_t wait_semaphores_len = render_cb->wait_semaphores.size / sizeof(wait_semaphores[0]);
|
||||
|
||||
VkSemaphore *sem_ptr;
|
||||
if (sem_index >= wait_semaphores_len) {
|
||||
sem_ptr = wl_array_add(&render_cb->wait_semaphores, sizeof(*sem_ptr));
|
||||
if (sem_ptr == NULL) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
*sem_ptr = VK_NULL_HANDLE;
|
||||
} else {
|
||||
sem_ptr = &wait_semaphores[sem_index];
|
||||
}
|
||||
|
||||
if (*sem_ptr == VK_NULL_HANDLE) {
|
||||
VkSemaphoreCreateInfo semaphore_info = {
|
||||
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
|
||||
};
|
||||
res = vkCreateSemaphore(renderer->dev->dev, &semaphore_info, NULL, sem_ptr);
|
||||
if (res != VK_SUCCESS) {
|
||||
wlr_vk_error("vkCreateSemaphore", res);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
|
||||
VkImportSemaphoreFdInfoKHR import_info = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR,
|
||||
.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT,
|
||||
.flags = VK_SEMAPHORE_IMPORT_TEMPORARY_BIT,
|
||||
.semaphore = *sem_ptr,
|
||||
.fd = sync_file_fd,
|
||||
};
|
||||
res = renderer->dev->api.vkImportSemaphoreFdKHR(renderer->dev->dev, &import_info);
|
||||
if (res != VK_SUCCESS) {
|
||||
wlr_vk_error("vkImportSemaphoreFdKHR", res);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
return *sem_ptr;
|
||||
}
|
||||
|
||||
static bool render_pass_submit(struct wlr_render_pass *wlr_pass) {
|
||||
struct wlr_vk_render_pass *pass = get_render_pass(wlr_pass);
|
||||
struct wlr_vk_renderer *renderer = pass->renderer;
|
||||
@ -179,14 +235,15 @@ static bool render_pass_submit(struct wlr_render_pass *wlr_pass) {
|
||||
|
||||
vkCmdEndRenderPass(render_cb->vk);
|
||||
|
||||
// insert acquire and release barriers for dmabuf-images
|
||||
uint32_t barrier_count = wl_list_length(&renderer->foreign_textures) + 1;
|
||||
render_wait = calloc(barrier_count * WLR_DMABUF_MAX_PLANES, sizeof(*render_wait));
|
||||
size_t pass_textures_len = pass->textures.size / sizeof(struct wlr_vk_render_pass_texture);
|
||||
size_t render_wait_cap = pass_textures_len * WLR_DMABUF_MAX_PLANES;
|
||||
render_wait = calloc(render_wait_cap, sizeof(*render_wait));
|
||||
if (render_wait == NULL) {
|
||||
wlr_log_errno(WLR_ERROR, "Allocation failed");
|
||||
goto error;
|
||||
}
|
||||
|
||||
uint32_t barrier_count = wl_list_length(&renderer->foreign_textures) + 1;
|
||||
VkImageMemoryBarrier *acquire_barriers = calloc(barrier_count, sizeof(*acquire_barriers));
|
||||
VkImageMemoryBarrier *release_barriers = calloc(barrier_count, sizeof(*release_barriers));
|
||||
if (acquire_barriers == NULL || release_barriers == NULL) {
|
||||
@ -198,7 +255,6 @@ static bool render_pass_submit(struct wlr_render_pass *wlr_pass) {
|
||||
|
||||
struct wlr_vk_texture *texture, *tmp_tex;
|
||||
size_t idx = 0;
|
||||
uint32_t render_wait_len = 0;
|
||||
wl_list_for_each_safe(texture, tmp_tex, &renderer->foreign_textures, foreign_link) {
|
||||
if (!texture->transitioned) {
|
||||
texture->transitioned = true;
|
||||
@ -236,23 +292,53 @@ static bool render_pass_submit(struct wlr_render_pass *wlr_pass) {
|
||||
|
||||
++idx;
|
||||
|
||||
if (!vulkan_sync_foreign_texture(texture)) {
|
||||
wlr_log(WLR_ERROR, "Failed to wait for foreign texture DMA-BUF fence");
|
||||
wl_list_remove(&texture->foreign_link);
|
||||
texture->owned = false;
|
||||
}
|
||||
|
||||
uint32_t render_wait_len = 0;
|
||||
struct wlr_vk_render_pass_texture *pass_texture;
|
||||
wl_array_for_each(pass_texture, &pass->textures) {
|
||||
int sync_file_fds[WLR_DMABUF_MAX_PLANES];
|
||||
for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) {
|
||||
sync_file_fds[i] = -1;
|
||||
}
|
||||
|
||||
if (pass_texture->wait_timeline) {
|
||||
int sync_file_fd = wlr_drm_syncobj_timeline_export_sync_file(pass_texture->wait_timeline, pass_texture->wait_point);
|
||||
if (sync_file_fd < 0) {
|
||||
wlr_log(WLR_ERROR, "Failed to export wait timeline point as sync_file");
|
||||
continue;
|
||||
}
|
||||
|
||||
sync_file_fds[0] = sync_file_fd;
|
||||
} else {
|
||||
for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) {
|
||||
if (texture->foreign_semaphores[i] != VK_NULL_HANDLE) {
|
||||
assert(render_wait_len < barrier_count * WLR_DMABUF_MAX_PLANES);
|
||||
render_wait[render_wait_len++] = (VkSemaphoreSubmitInfoKHR){
|
||||
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO_KHR,
|
||||
.semaphore = texture->foreign_semaphores[i],
|
||||
.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT_KHR,
|
||||
};
|
||||
}
|
||||
struct wlr_vk_texture *texture = pass_texture->texture;
|
||||
if (!vulkan_sync_foreign_texture(texture, sync_file_fds)) {
|
||||
wlr_log(WLR_ERROR, "Failed to wait for foreign texture DMA-BUF fence");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
wl_list_remove(&texture->foreign_link);
|
||||
texture->owned = false;
|
||||
for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) {
|
||||
if (sync_file_fds[i] < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
VkSemaphore sem = render_pass_wait_sync_file(pass, render_wait_len, sync_file_fds[i]);
|
||||
if (sem == VK_NULL_HANDLE) {
|
||||
close(sync_file_fds[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
render_wait[render_wait_len] = (VkSemaphoreSubmitInfoKHR){
|
||||
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO_KHR,
|
||||
.semaphore = sem,
|
||||
.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT_KHR,
|
||||
};
|
||||
|
||||
render_wait_len++;
|
||||
}
|
||||
}
|
||||
|
||||
// also add acquire/release barriers for the current render buffer
|
||||
@ -452,7 +538,8 @@ static bool render_pass_submit(struct wlr_render_pass *wlr_pass) {
|
||||
wl_list_insert(&stage_cb->stage_buffers, &stage_buf->link);
|
||||
}
|
||||
|
||||
if (!vulkan_sync_render_buffer(renderer, render_buffer, render_cb)) {
|
||||
if (!vulkan_sync_render_buffer(renderer, render_buffer, render_cb,
|
||||
pass->signal_timeline, pass->signal_point)) {
|
||||
wlr_log(WLR_ERROR, "Failed to sync render buffer");
|
||||
}
|
||||
|
||||
@ -704,6 +791,28 @@ static void render_pass_add_texture(struct wlr_render_pass *wlr_pass,
|
||||
texture->last_used_cb = pass->command_buffer;
|
||||
|
||||
pixman_region32_fini(&clip);
|
||||
|
||||
if (texture->dmabuf_imported || (options != NULL && options->wait_timeline != NULL)) {
|
||||
struct wlr_vk_render_pass_texture *pass_texture =
|
||||
wl_array_add(&pass->textures, sizeof(*pass_texture));
|
||||
if (pass_texture == NULL) {
|
||||
pass->failed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
struct wlr_drm_syncobj_timeline *wait_timeline = NULL;
|
||||
uint64_t wait_point = 0;
|
||||
if (options != NULL && options->wait_timeline != NULL) {
|
||||
wait_timeline = wlr_drm_syncobj_timeline_ref(options->wait_timeline);
|
||||
wait_point = options->wait_point;
|
||||
}
|
||||
|
||||
*pass_texture = (struct wlr_vk_render_pass_texture){
|
||||
.texture = texture,
|
||||
.wait_timeline = wait_timeline,
|
||||
.wait_point = wait_point,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static const struct wlr_render_pass_impl render_pass_impl = {
|
||||
@ -967,6 +1076,10 @@ struct wlr_vk_render_pass *vulkan_begin_render_pass(struct wlr_vk_renderer *rend
|
||||
if (options != NULL && options->color_transform != NULL) {
|
||||
pass->color_transform = wlr_color_transform_ref(options->color_transform);
|
||||
}
|
||||
if (options != NULL && options->signal_timeline != NULL) {
|
||||
pass->signal_timeline = wlr_drm_syncobj_timeline_ref(options->signal_timeline);
|
||||
pass->signal_point = options->signal_point;
|
||||
}
|
||||
|
||||
rect_union_init(&pass->updated_region);
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include <wlr/types/wlr_drm.h>
|
||||
#include <wlr/util/box.h>
|
||||
#include <wlr/util/log.h>
|
||||
#include <wlr/render/drm_syncobj.h>
|
||||
#include <wlr/render/vulkan.h>
|
||||
#include <wlr/backend/interface.h>
|
||||
#include <wlr/types/wlr_linux_dmabuf_v1.h>
|
||||
@ -921,9 +922,9 @@ static struct wlr_vk_render_buffer *get_render_buffer(
|
||||
return buffer;
|
||||
}
|
||||
|
||||
bool vulkan_sync_foreign_texture(struct wlr_vk_texture *texture) {
|
||||
bool vulkan_sync_foreign_texture(struct wlr_vk_texture *texture,
|
||||
int sync_file_fds[static WLR_DMABUF_MAX_PLANES]) {
|
||||
struct wlr_vk_renderer *renderer = texture->renderer;
|
||||
VkResult res;
|
||||
|
||||
struct wlr_dmabuf_attributes dmabuf = {0};
|
||||
if (!wlr_buffer_get_dmabuf(texture->buffer, &dmabuf)) {
|
||||
@ -960,52 +961,22 @@ bool vulkan_sync_foreign_texture(struct wlr_vk_texture *texture) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (texture->foreign_semaphores[i] == VK_NULL_HANDLE) {
|
||||
VkSemaphoreCreateInfo semaphore_info = {
|
||||
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
|
||||
};
|
||||
res = vkCreateSemaphore(renderer->dev->dev, &semaphore_info, NULL,
|
||||
&texture->foreign_semaphores[i]);
|
||||
if (res != VK_SUCCESS) {
|
||||
close(sync_file_fd);
|
||||
wlr_vk_error("vkCreateSemaphore", res);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
VkImportSemaphoreFdInfoKHR import_info = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR,
|
||||
.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT,
|
||||
.flags = VK_SEMAPHORE_IMPORT_TEMPORARY_BIT,
|
||||
.semaphore = texture->foreign_semaphores[i],
|
||||
.fd = sync_file_fd,
|
||||
};
|
||||
res = renderer->dev->api.vkImportSemaphoreFdKHR(renderer->dev->dev, &import_info);
|
||||
if (res != VK_SUCCESS) {
|
||||
close(sync_file_fd);
|
||||
wlr_vk_error("vkImportSemaphoreFdKHR", res);
|
||||
return false;
|
||||
}
|
||||
sync_file_fds[i] = sync_file_fd;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool vulkan_sync_render_buffer(struct wlr_vk_renderer *renderer,
|
||||
struct wlr_vk_render_buffer *render_buffer, struct wlr_vk_command_buffer *cb) {
|
||||
struct wlr_vk_render_buffer *render_buffer, struct wlr_vk_command_buffer *cb,
|
||||
struct wlr_drm_syncobj_timeline *signal_timeline, uint64_t signal_point) {
|
||||
VkResult res;
|
||||
|
||||
if (!renderer->dev->implicit_sync_interop) {
|
||||
if (!renderer->dev->implicit_sync_interop && signal_timeline == NULL) {
|
||||
// We have no choice but to block here sadly
|
||||
return vulkan_wait_command_buffer(cb, renderer);
|
||||
}
|
||||
|
||||
struct wlr_dmabuf_attributes dmabuf = {0};
|
||||
if (!wlr_buffer_get_dmabuf(render_buffer->wlr_buffer, &dmabuf)) {
|
||||
wlr_log(WLR_ERROR, "wlr_buffer_get_dmabuf failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Note: vkGetSemaphoreFdKHR implicitly resets the semaphore
|
||||
const VkSemaphoreGetFdInfoKHR get_fence_fd_info = {
|
||||
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR,
|
||||
@ -1020,17 +991,32 @@ bool vulkan_sync_render_buffer(struct wlr_vk_renderer *renderer,
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < dmabuf.n_planes; i++) {
|
||||
if (!dmabuf_import_sync_file(dmabuf.fd[i], DMA_BUF_SYNC_WRITE,
|
||||
sync_file_fd)) {
|
||||
close(sync_file_fd);
|
||||
return false;
|
||||
bool ok = false;
|
||||
if (signal_timeline != NULL) {
|
||||
if (!wlr_drm_syncobj_timeline_import_sync_file(signal_timeline,
|
||||
signal_point, sync_file_fd)) {
|
||||
goto out;
|
||||
}
|
||||
} else {
|
||||
struct wlr_dmabuf_attributes dmabuf = {0};
|
||||
if (!wlr_buffer_get_dmabuf(render_buffer->wlr_buffer, &dmabuf)) {
|
||||
wlr_log(WLR_ERROR, "wlr_buffer_get_dmabuf failed");
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int i = 0; i < dmabuf.n_planes; i++) {
|
||||
if (!dmabuf_import_sync_file(dmabuf.fd[i], DMA_BUF_SYNC_WRITE,
|
||||
sync_file_fd)) {
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(sync_file_fd);
|
||||
ok = true;
|
||||
|
||||
return true;
|
||||
out:
|
||||
close(sync_file_fd);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static const struct wlr_drm_format_set *vulkan_get_texture_formats(
|
||||
@ -1073,6 +1059,11 @@ static void vulkan_destroy(struct wlr_renderer *wlr_renderer) {
|
||||
if (cb->binary_semaphore != VK_NULL_HANDLE) {
|
||||
vkDestroySemaphore(renderer->dev->dev, cb->binary_semaphore, NULL);
|
||||
}
|
||||
VkSemaphore *sem_ptr;
|
||||
wl_array_for_each(sem_ptr, &cb->wait_semaphores) {
|
||||
vkDestroySemaphore(renderer->dev->dev, *sem_ptr, NULL);
|
||||
}
|
||||
wl_array_release(&cb->wait_semaphores);
|
||||
}
|
||||
|
||||
// stage.cb automatically freed with command pool
|
||||
@ -2436,6 +2427,11 @@ struct wlr_renderer *vulkan_renderer_create_for_device(struct wlr_vk_device *dev
|
||||
wl_list_init(&renderer->color_transforms);
|
||||
wl_list_init(&renderer->pipeline_layouts);
|
||||
|
||||
uint64_t cap_syncobj_timeline;
|
||||
if (dev->drm_fd >= 0 && drmGetCap(dev->drm_fd, DRM_CAP_SYNCOBJ_TIMELINE, &cap_syncobj_timeline) == 0) {
|
||||
renderer->wlr_renderer.features.timeline = dev->sync_file_import_export && cap_syncobj_timeline != 0;
|
||||
}
|
||||
|
||||
if (!init_static_render_data(renderer)) {
|
||||
goto error;
|
||||
}
|
||||
|
@ -208,12 +208,6 @@ void vulkan_texture_destroy(struct wlr_vk_texture *texture) {
|
||||
free(view);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) {
|
||||
if (texture->foreign_semaphores[i] != VK_NULL_HANDLE) {
|
||||
vkDestroySemaphore(dev, texture->foreign_semaphores[i], NULL);
|
||||
}
|
||||
}
|
||||
|
||||
vkDestroyImage(dev, texture->image, NULL);
|
||||
|
||||
for (unsigned i = 0u; i < texture->mem_count; ++i) {
|
||||
|
@ -527,6 +527,7 @@ struct wlr_vk_device *vulkan_device_create(struct wlr_vk_instance *ini,
|
||||
wlr_log(WLR_DEBUG, "DMA-BUF sync_file import/export not supported");
|
||||
}
|
||||
|
||||
dev->sync_file_import_export = exportable_semaphore && importable_semaphore;
|
||||
dev->implicit_sync_interop =
|
||||
exportable_semaphore && importable_semaphore && dmabuf_sync_file_import_export;
|
||||
if (dev->implicit_sync_interop) {
|
||||
|
Loading…
Reference in New Issue
Block a user