early-access version 2212
This commit is contained in:
parent
964516ac70
commit
27d8331649
@ -1,7 +1,7 @@
|
|||||||
yuzu emulator early access
|
yuzu emulator early access
|
||||||
=============
|
=============
|
||||||
|
|
||||||
This is the source code for early-access 2211.
|
This is the source code for early-access 2212.
|
||||||
|
|
||||||
## Legal Notice
|
## Legal Notice
|
||||||
|
|
||||||
|
@ -83,6 +83,7 @@ enum class DepthFormat : u32 {
|
|||||||
S8_UINT_Z24_UNORM = 0x14,
|
S8_UINT_Z24_UNORM = 0x14,
|
||||||
D24X8_UNORM = 0x15,
|
D24X8_UNORM = 0x15,
|
||||||
D24S8_UNORM = 0x16,
|
D24S8_UNORM = 0x16,
|
||||||
|
S8_UINT = 0x17,
|
||||||
D24C8_UNORM = 0x18,
|
D24C8_UNORM = 0x18,
|
||||||
D32_FLOAT_S8X24_UINT = 0x19,
|
D32_FLOAT_S8X24_UINT = 0x19,
|
||||||
};
|
};
|
||||||
|
@ -148,6 +148,8 @@ GLenum AttachmentType(PixelFormat format) {
|
|||||||
switch (const SurfaceType type = VideoCore::Surface::GetFormatType(format); type) {
|
switch (const SurfaceType type = VideoCore::Surface::GetFormatType(format); type) {
|
||||||
case SurfaceType::Depth:
|
case SurfaceType::Depth:
|
||||||
return GL_DEPTH_ATTACHMENT;
|
return GL_DEPTH_ATTACHMENT;
|
||||||
|
case SurfaceType::Stencil:
|
||||||
|
return GL_STENCIL_ATTACHMENT;
|
||||||
case SurfaceType::DepthStencil:
|
case SurfaceType::DepthStencil:
|
||||||
return GL_DEPTH_STENCIL_ATTACHMENT;
|
return GL_DEPTH_STENCIL_ATTACHMENT;
|
||||||
default:
|
default:
|
||||||
@ -395,6 +397,10 @@ OGLTexture MakeImage(const VideoCommon::ImageInfo& info, GLenum gl_internal_form
|
|||||||
UNREACHABLE_MSG("Invalid image format={}", format);
|
UNREACHABLE_MSG("Invalid image format={}", format);
|
||||||
return GL_R32UI;
|
return GL_R32UI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] u32 NextPow2(u32 value) {
|
||||||
|
return 1U << (32U - std::countl_zero(value - 1U));
|
||||||
|
}
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
||||||
ImageBufferMap::~ImageBufferMap() {
|
ImageBufferMap::~ImageBufferMap() {
|
||||||
@ -910,6 +916,8 @@ void Image::Scale(bool up_scale) {
|
|||||||
return GL_COLOR_ATTACHMENT0;
|
return GL_COLOR_ATTACHMENT0;
|
||||||
case SurfaceType::Depth:
|
case SurfaceType::Depth:
|
||||||
return GL_DEPTH_ATTACHMENT;
|
return GL_DEPTH_ATTACHMENT;
|
||||||
|
case SurfaceType::Stencil:
|
||||||
|
return GL_STENCIL_ATTACHMENT;
|
||||||
case SurfaceType::DepthStencil:
|
case SurfaceType::DepthStencil:
|
||||||
return GL_DEPTH_STENCIL_ATTACHMENT;
|
return GL_DEPTH_STENCIL_ATTACHMENT;
|
||||||
default:
|
default:
|
||||||
@ -923,8 +931,10 @@ void Image::Scale(bool up_scale) {
|
|||||||
return GL_COLOR_BUFFER_BIT;
|
return GL_COLOR_BUFFER_BIT;
|
||||||
case SurfaceType::Depth:
|
case SurfaceType::Depth:
|
||||||
return GL_DEPTH_BUFFER_BIT;
|
return GL_DEPTH_BUFFER_BIT;
|
||||||
|
case SurfaceType::Stencil:
|
||||||
|
return GL_STENCIL_BUFFER_BIT;
|
||||||
case SurfaceType::DepthStencil:
|
case SurfaceType::DepthStencil:
|
||||||
return GL_STENCIL_BUFFER_BIT | GL_DEPTH_BUFFER_BIT;
|
return GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
|
||||||
default:
|
default:
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
return GL_COLOR_BUFFER_BIT;
|
return GL_COLOR_BUFFER_BIT;
|
||||||
@ -936,8 +946,10 @@ void Image::Scale(bool up_scale) {
|
|||||||
return 0;
|
return 0;
|
||||||
case SurfaceType::Depth:
|
case SurfaceType::Depth:
|
||||||
return 1;
|
return 1;
|
||||||
case SurfaceType::DepthStencil:
|
case SurfaceType::Stencil:
|
||||||
return 2;
|
return 2;
|
||||||
|
case SurfaceType::DepthStencil:
|
||||||
|
return 3;
|
||||||
default:
|
default:
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
return 0;
|
return 0;
|
||||||
@ -1267,10 +1279,20 @@ Framebuffer::Framebuffer(TextureCacheRuntime& runtime, std::span<ImageView*, NUM
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (const ImageView* const image_view = depth_buffer; image_view) {
|
if (const ImageView* const image_view = depth_buffer; image_view) {
|
||||||
if (GetFormatType(image_view->format) == SurfaceType::DepthStencil) {
|
switch (GetFormatType(image_view->format)) {
|
||||||
buffer_bits |= GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
|
case SurfaceType::Depth:
|
||||||
} else {
|
|
||||||
buffer_bits |= GL_DEPTH_BUFFER_BIT;
|
buffer_bits |= GL_DEPTH_BUFFER_BIT;
|
||||||
|
break;
|
||||||
|
case SurfaceType::Stencil:
|
||||||
|
buffer_bits |= GL_STENCIL_BUFFER_BIT;
|
||||||
|
break;
|
||||||
|
case SurfaceType::DepthStencil:
|
||||||
|
buffer_bits |= GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
UNREACHABLE();
|
||||||
|
buffer_bits |= GL_DEPTH_BUFFER_BIT;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
const GLenum attachment = AttachmentType(image_view->format);
|
const GLenum attachment = AttachmentType(image_view->format);
|
||||||
AttachTexture(handle, attachment, image_view);
|
AttachTexture(handle, attachment, image_view);
|
||||||
@ -1311,7 +1333,7 @@ void FormatConversionPass::ConvertImage(Image& dst_image, Image& src_image,
|
|||||||
const u32 copy_size = region.width * region.height * region.depth * img_bpp;
|
const u32 copy_size = region.width * region.height * region.depth * img_bpp;
|
||||||
if (pbo_size < copy_size) {
|
if (pbo_size < copy_size) {
|
||||||
intermediate_pbo.Create();
|
intermediate_pbo.Create();
|
||||||
pbo_size = copy_size;
|
pbo_size = NextPow2(copy_size);
|
||||||
glNamedBufferData(intermediate_pbo.handle, pbo_size, nullptr, GL_STREAM_COPY);
|
glNamedBufferData(intermediate_pbo.handle, pbo_size, nullptr, GL_STREAM_COPY);
|
||||||
}
|
}
|
||||||
// Copy from source to PBO
|
// Copy from source to PBO
|
||||||
|
@ -164,8 +164,8 @@ private:
|
|||||||
|
|
||||||
std::array<GLuint, Shader::NUM_TEXTURE_TYPES> null_image_views{};
|
std::array<GLuint, Shader::NUM_TEXTURE_TYPES> null_image_views{};
|
||||||
|
|
||||||
std::array<OGLFramebuffer, 3> rescale_draw_fbos;
|
std::array<OGLFramebuffer, 4> rescale_draw_fbos;
|
||||||
std::array<OGLFramebuffer, 3> rescale_read_fbos;
|
std::array<OGLFramebuffer, 4> rescale_read_fbos;
|
||||||
const Settings::ResolutionScalingInfo& resolution;
|
const Settings::ResolutionScalingInfo& resolution;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ constexpr std::array<FormatTuple, VideoCore::Surface::MaxPixelFormat> FORMAT_TAB
|
|||||||
{GL_RGB9_E5, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV}, // E5B9G9R9_FLOAT
|
{GL_RGB9_E5, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV}, // E5B9G9R9_FLOAT
|
||||||
{GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT}, // D32_FLOAT
|
{GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT}, // D32_FLOAT
|
||||||
{GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT}, // D16_UNORM
|
{GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT}, // D16_UNORM
|
||||||
|
{GL_STENCIL_INDEX8, GL_STENCIL, GL_UNSIGNED_BYTE}, // S8_UINT
|
||||||
{GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8}, // D24_UNORM_S8_UINT
|
{GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8}, // D24_UNORM_S8_UINT
|
||||||
{GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8}, // S8_UINT_D24_UNORM
|
{GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8}, // S8_UINT_D24_UNORM
|
||||||
{GL_DEPTH32F_STENCIL8, GL_DEPTH_STENCIL,
|
{GL_DEPTH32F_STENCIL8, GL_DEPTH_STENCIL,
|
||||||
|
@ -208,6 +208,9 @@ struct FormatTuple {
|
|||||||
{VK_FORMAT_D32_SFLOAT, Attachable}, // D32_FLOAT
|
{VK_FORMAT_D32_SFLOAT, Attachable}, // D32_FLOAT
|
||||||
{VK_FORMAT_D16_UNORM, Attachable}, // D16_UNORM
|
{VK_FORMAT_D16_UNORM, Attachable}, // D16_UNORM
|
||||||
|
|
||||||
|
// Stencil formats
|
||||||
|
{VK_FORMAT_S8_UINT, Attachable}, // S8_UINT
|
||||||
|
|
||||||
// DepthStencil formats
|
// DepthStencil formats
|
||||||
{VK_FORMAT_D24_UNORM_S8_UINT, Attachable}, // D24_UNORM_S8_UINT
|
{VK_FORMAT_D24_UNORM_S8_UINT, Attachable}, // D24_UNORM_S8_UINT
|
||||||
{VK_FORMAT_D24_UNORM_S8_UINT, Attachable}, // S8_UINT_D24_UNORM (emulated)
|
{VK_FORMAT_D24_UNORM_S8_UINT, Attachable}, // S8_UINT_D24_UNORM (emulated)
|
||||||
|
@ -102,6 +102,7 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
|
|||||||
usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
||||||
break;
|
break;
|
||||||
case VideoCore::Surface::SurfaceType::Depth:
|
case VideoCore::Surface::SurfaceType::Depth:
|
||||||
|
case VideoCore::Surface::SurfaceType::Stencil:
|
||||||
case VideoCore::Surface::SurfaceType::DepthStencil:
|
case VideoCore::Surface::SurfaceType::DepthStencil:
|
||||||
usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
||||||
break;
|
break;
|
||||||
@ -173,6 +174,8 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
|
|||||||
return VK_IMAGE_ASPECT_COLOR_BIT;
|
return VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
case VideoCore::Surface::SurfaceType::Depth:
|
case VideoCore::Surface::SurfaceType::Depth:
|
||||||
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||||
|
case VideoCore::Surface::SurfaceType::Stencil:
|
||||||
|
return VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||||
case VideoCore::Surface::SurfaceType::DepthStencil:
|
case VideoCore::Surface::SurfaceType::DepthStencil:
|
||||||
return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||||
default:
|
default:
|
||||||
@ -195,6 +198,8 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
|
|||||||
case PixelFormat::D16_UNORM:
|
case PixelFormat::D16_UNORM:
|
||||||
case PixelFormat::D32_FLOAT:
|
case PixelFormat::D32_FLOAT:
|
||||||
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
return VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||||
|
case PixelFormat::S8_UINT:
|
||||||
|
return VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||||
default:
|
default:
|
||||||
return VK_IMAGE_ASPECT_COLOR_BIT;
|
return VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,8 @@ PixelFormat PixelFormatFromDepthFormat(Tegra::DepthFormat format) {
|
|||||||
return PixelFormat::D32_FLOAT;
|
return PixelFormat::D32_FLOAT;
|
||||||
case Tegra::DepthFormat::D16_UNORM:
|
case Tegra::DepthFormat::D16_UNORM:
|
||||||
return PixelFormat::D16_UNORM;
|
return PixelFormat::D16_UNORM;
|
||||||
|
case Tegra::DepthFormat::S8_UINT:
|
||||||
|
return PixelFormat::S8_UINT;
|
||||||
case Tegra::DepthFormat::D32_FLOAT_S8X24_UINT:
|
case Tegra::DepthFormat::D32_FLOAT_S8X24_UINT:
|
||||||
return PixelFormat::D32_FLOAT_S8_UINT;
|
return PixelFormat::D32_FLOAT_S8_UINT;
|
||||||
default:
|
default:
|
||||||
@ -213,6 +215,11 @@ SurfaceType GetFormatType(PixelFormat pixel_format) {
|
|||||||
return SurfaceType::Depth;
|
return SurfaceType::Depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (static_cast<std::size_t>(pixel_format) <
|
||||||
|
static_cast<std::size_t>(PixelFormat::MaxStencilFormat)) {
|
||||||
|
return SurfaceType::Stencil;
|
||||||
|
}
|
||||||
|
|
||||||
if (static_cast<std::size_t>(pixel_format) <
|
if (static_cast<std::size_t>(pixel_format) <
|
||||||
static_cast<std::size_t>(PixelFormat::MaxDepthStencilFormat)) {
|
static_cast<std::size_t>(PixelFormat::MaxDepthStencilFormat)) {
|
||||||
return SurfaceType::DepthStencil;
|
return SurfaceType::DepthStencil;
|
||||||
|
@ -110,8 +110,12 @@ enum class PixelFormat {
|
|||||||
|
|
||||||
MaxDepthFormat,
|
MaxDepthFormat,
|
||||||
|
|
||||||
|
// Stencil formats
|
||||||
|
S8_UINT = MaxDepthFormat,
|
||||||
|
MaxStencilFormat,
|
||||||
|
|
||||||
// DepthStencil formats
|
// DepthStencil formats
|
||||||
D24_UNORM_S8_UINT = MaxDepthFormat,
|
D24_UNORM_S8_UINT = MaxStencilFormat,
|
||||||
S8_UINT_D24_UNORM,
|
S8_UINT_D24_UNORM,
|
||||||
D32_FLOAT_S8_UINT,
|
D32_FLOAT_S8_UINT,
|
||||||
|
|
||||||
@ -125,8 +129,9 @@ constexpr std::size_t MaxPixelFormat = static_cast<std::size_t>(PixelFormat::Max
|
|||||||
enum class SurfaceType {
|
enum class SurfaceType {
|
||||||
ColorTexture = 0,
|
ColorTexture = 0,
|
||||||
Depth = 1,
|
Depth = 1,
|
||||||
DepthStencil = 2,
|
Stencil = 2,
|
||||||
Invalid = 3,
|
DepthStencil = 3,
|
||||||
|
Invalid = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class SurfaceTarget {
|
enum class SurfaceTarget {
|
||||||
@ -229,6 +234,7 @@ constexpr std::array<u32, MaxPixelFormat> BLOCK_WIDTH_TABLE = {{
|
|||||||
1, // E5B9G9R9_FLOAT
|
1, // E5B9G9R9_FLOAT
|
||||||
1, // D32_FLOAT
|
1, // D32_FLOAT
|
||||||
1, // D16_UNORM
|
1, // D16_UNORM
|
||||||
|
1, // S8_UINT
|
||||||
1, // D24_UNORM_S8_UINT
|
1, // D24_UNORM_S8_UINT
|
||||||
1, // S8_UINT_D24_UNORM
|
1, // S8_UINT_D24_UNORM
|
||||||
1, // D32_FLOAT_S8_UINT
|
1, // D32_FLOAT_S8_UINT
|
||||||
@ -328,6 +334,7 @@ constexpr std::array<u32, MaxPixelFormat> BLOCK_HEIGHT_TABLE = {{
|
|||||||
1, // E5B9G9R9_FLOAT
|
1, // E5B9G9R9_FLOAT
|
||||||
1, // D32_FLOAT
|
1, // D32_FLOAT
|
||||||
1, // D16_UNORM
|
1, // D16_UNORM
|
||||||
|
1, // S8_UINT
|
||||||
1, // D24_UNORM_S8_UINT
|
1, // D24_UNORM_S8_UINT
|
||||||
1, // S8_UINT_D24_UNORM
|
1, // S8_UINT_D24_UNORM
|
||||||
1, // D32_FLOAT_S8_UINT
|
1, // D32_FLOAT_S8_UINT
|
||||||
@ -427,6 +434,7 @@ constexpr std::array<u32, MaxPixelFormat> BITS_PER_BLOCK_TABLE = {{
|
|||||||
32, // E5B9G9R9_FLOAT
|
32, // E5B9G9R9_FLOAT
|
||||||
32, // D32_FLOAT
|
32, // D32_FLOAT
|
||||||
16, // D16_UNORM
|
16, // D16_UNORM
|
||||||
|
8, // S8_UINT
|
||||||
32, // D24_UNORM_S8_UINT
|
32, // D24_UNORM_S8_UINT
|
||||||
32, // S8_UINT_D24_UNORM
|
32, // S8_UINT_D24_UNORM
|
||||||
64, // D32_FLOAT_S8_UINT
|
64, // D32_FLOAT_S8_UINT
|
||||||
|
@ -194,6 +194,8 @@ struct fmt::formatter<VideoCore::Surface::PixelFormat> : fmt::formatter<fmt::str
|
|||||||
return "D32_FLOAT";
|
return "D32_FLOAT";
|
||||||
case PixelFormat::D16_UNORM:
|
case PixelFormat::D16_UNORM:
|
||||||
return "D16_UNORM";
|
return "D16_UNORM";
|
||||||
|
case PixelFormat::S8_UINT:
|
||||||
|
return "S8_UINT";
|
||||||
case PixelFormat::D24_UNORM_S8_UINT:
|
case PixelFormat::D24_UNORM_S8_UINT:
|
||||||
return "D24_UNORM_S8_UINT";
|
return "D24_UNORM_S8_UINT";
|
||||||
case PixelFormat::S8_UINT_D24_UNORM:
|
case PixelFormat::S8_UINT_D24_UNORM:
|
||||||
|
@ -21,6 +21,13 @@
|
|||||||
namespace Vulkan {
|
namespace Vulkan {
|
||||||
namespace {
|
namespace {
|
||||||
namespace Alternatives {
|
namespace Alternatives {
|
||||||
|
constexpr std::array S8_UINT{
|
||||||
|
VK_FORMAT_D16_UNORM_S8_UINT,
|
||||||
|
VK_FORMAT_D24_UNORM_S8_UINT,
|
||||||
|
VK_FORMAT_D32_SFLOAT_S8_UINT,
|
||||||
|
VK_FORMAT_UNDEFINED,
|
||||||
|
};
|
||||||
|
|
||||||
constexpr std::array DEPTH24_UNORM_STENCIL8_UINT{
|
constexpr std::array DEPTH24_UNORM_STENCIL8_UINT{
|
||||||
VK_FORMAT_D32_SFLOAT_S8_UINT,
|
VK_FORMAT_D32_SFLOAT_S8_UINT,
|
||||||
VK_FORMAT_D16_UNORM_S8_UINT,
|
VK_FORMAT_D16_UNORM_S8_UINT,
|
||||||
@ -74,6 +81,8 @@ void SetNext(void**& next, T& data) {
|
|||||||
|
|
||||||
constexpr const VkFormat* GetFormatAlternatives(VkFormat format) {
|
constexpr const VkFormat* GetFormatAlternatives(VkFormat format) {
|
||||||
switch (format) {
|
switch (format) {
|
||||||
|
case VK_FORMAT_S8_UINT:
|
||||||
|
return Alternatives::S8_UINT.data();
|
||||||
case VK_FORMAT_D24_UNORM_S8_UINT:
|
case VK_FORMAT_D24_UNORM_S8_UINT:
|
||||||
return Alternatives::DEPTH24_UNORM_STENCIL8_UINT.data();
|
return Alternatives::DEPTH24_UNORM_STENCIL8_UINT.data();
|
||||||
case VK_FORMAT_D16_UNORM_S8_UINT:
|
case VK_FORMAT_D16_UNORM_S8_UINT:
|
||||||
@ -145,6 +154,7 @@ std::unordered_map<VkFormat, VkFormatProperties> GetFormatProperties(vk::Physica
|
|||||||
VK_FORMAT_R4G4B4A4_UNORM_PACK16,
|
VK_FORMAT_R4G4B4A4_UNORM_PACK16,
|
||||||
VK_FORMAT_D32_SFLOAT,
|
VK_FORMAT_D32_SFLOAT,
|
||||||
VK_FORMAT_D16_UNORM,
|
VK_FORMAT_D16_UNORM,
|
||||||
|
VK_FORMAT_S8_UINT,
|
||||||
VK_FORMAT_D16_UNORM_S8_UINT,
|
VK_FORMAT_D16_UNORM_S8_UINT,
|
||||||
VK_FORMAT_D24_UNORM_S8_UINT,
|
VK_FORMAT_D24_UNORM_S8_UINT,
|
||||||
VK_FORMAT_D32_SFLOAT_S8_UINT,
|
VK_FORMAT_D32_SFLOAT_S8_UINT,
|
||||||
|
Loading…
Reference in New Issue
Block a user