mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 03:15:11 +00:00
Bug 1101552 - Remove the double-buffered ImageClient. r=sotaro
This commit is contained in:
parent
6cee3ccac1
commit
f494468d40
@ -159,17 +159,15 @@ MOZ_END_ENUM_CLASS(EffectTypes)
|
||||
MOZ_BEGIN_ENUM_CLASS(CompositableType, uint8_t)
|
||||
BUFFER_UNKNOWN,
|
||||
// the deprecated compositable types
|
||||
BUFFER_IMAGE_SINGLE, // image/canvas with a single texture, single buffered
|
||||
BUFFER_IMAGE_BUFFERED, // canvas, double buffered
|
||||
BUFFER_BRIDGE, // image bridge protocol
|
||||
BUFFER_CONTENT_INC, // painted layer interface, only sends incremental
|
||||
// updates to a texture on the compositor side.
|
||||
// somewhere in the middle
|
||||
BUFFER_TILED, // tiled painted layer
|
||||
BUFFER_SIMPLE_TILED,
|
||||
// the new compositable types
|
||||
IMAGE, // image with single buffering
|
||||
IMAGE_OVERLAY, // image without buffer
|
||||
IMAGE, // image with single buffering
|
||||
IMAGE_OVERLAY, // image without buffer
|
||||
IMAGE_BRIDGE, // ImageBridge protocol
|
||||
CONTENT_SINGLE, // painted layer interface, single buffering
|
||||
CONTENT_DOUBLE, // painted layer interface, double buffering
|
||||
BUFFER_COUNT
|
||||
|
@ -137,7 +137,7 @@ ImageContainer::ImageContainer(int flag)
|
||||
if (flag == ENABLE_ASYNC && ImageBridgeChild::IsCreated()) {
|
||||
// the refcount of this ImageClient is 1. we don't use a RefPtr here because the refcount
|
||||
// of this class must be done on the ImageBridge thread.
|
||||
mImageClient = ImageBridgeChild::GetSingleton()->CreateImageClient(CompositableType::BUFFER_IMAGE_SINGLE).drop();
|
||||
mImageClient = ImageBridgeChild::GetSingleton()->CreateImageClient(CompositableType::IMAGE).drop();
|
||||
MOZ_ASSERT(mImageClient);
|
||||
}
|
||||
}
|
||||
|
@ -101,33 +101,22 @@ protected:
|
||||
}
|
||||
|
||||
if (mContainer->IsAsync()) {
|
||||
mImageClientTypeContainer = CompositableType::BUFFER_BRIDGE;
|
||||
return mImageClientTypeContainer;
|
||||
}
|
||||
|
||||
// Since D3D11 TextureClient doesn't have an internal buffer, modifying the
|
||||
// front buffer directly may break the transactional property of layer updates.
|
||||
if (ClientManager()->GetCompositorBackendType() == LayersBackend::LAYERS_D3D11) {
|
||||
mImageClientTypeContainer = CompositableType::BUFFER_IMAGE_BUFFERED;
|
||||
mImageClientTypeContainer = CompositableType::IMAGE_BRIDGE;
|
||||
return mImageClientTypeContainer;
|
||||
}
|
||||
|
||||
AutoLockImage autoLock(mContainer);
|
||||
|
||||
#ifdef MOZ_WIDGET_GONK
|
||||
// gralloc buffer needs CompositableType::BUFFER_IMAGE_BUFFERED to prevent
|
||||
// the buffer's usage conflict.
|
||||
if (autoLock.GetImage()->GetFormat() == ImageFormat::OVERLAY_IMAGE) {
|
||||
mImageClientTypeContainer = CompositableType::IMAGE_OVERLAY;
|
||||
return mImageClientTypeContainer;
|
||||
}
|
||||
|
||||
mImageClientTypeContainer = autoLock.GetImage() ?
|
||||
CompositableType::BUFFER_IMAGE_BUFFERED : CompositableType::BUFFER_UNKNOWN;
|
||||
#else
|
||||
mImageClientTypeContainer = autoLock.GetImage() ?
|
||||
CompositableType::BUFFER_IMAGE_SINGLE : CompositableType::BUFFER_UNKNOWN;
|
||||
#endif
|
||||
|
||||
mImageClientTypeContainer = autoLock.GetImage()
|
||||
? CompositableType::IMAGE
|
||||
: CompositableType::BUFFER_UNKNOWN;
|
||||
return mImageClientTypeContainer;
|
||||
}
|
||||
|
||||
@ -163,7 +152,7 @@ ClientImageLayer::RenderLayer()
|
||||
mImageClient = ImageClient::CreateImageClient(type,
|
||||
ClientManager()->AsShadowForwarder(),
|
||||
flags);
|
||||
if (type == CompositableType::BUFFER_BRIDGE) {
|
||||
if (type == CompositableType::IMAGE_BRIDGE) {
|
||||
static_cast<ImageClientBridge*>(mImageClient.get())->SetLayer(this);
|
||||
}
|
||||
|
||||
|
@ -49,13 +49,9 @@ ImageClient::CreateImageClient(CompositableType aCompositableHostType,
|
||||
RefPtr<ImageClient> result = nullptr;
|
||||
switch (aCompositableHostType) {
|
||||
case CompositableType::IMAGE:
|
||||
case CompositableType::BUFFER_IMAGE_SINGLE:
|
||||
result = new ImageClientSingle(aForwarder, aFlags, CompositableType::IMAGE);
|
||||
break;
|
||||
case CompositableType::BUFFER_IMAGE_BUFFERED:
|
||||
result = new ImageClientBuffered(aForwarder, aFlags, CompositableType::IMAGE);
|
||||
break;
|
||||
case CompositableType::BUFFER_BRIDGE:
|
||||
case CompositableType::IMAGE_BRIDGE:
|
||||
result = new ImageClientBridge(aForwarder, aFlags);
|
||||
break;
|
||||
case CompositableType::BUFFER_UNKNOWN:
|
||||
@ -116,13 +112,6 @@ ImageClientSingle::ImageClientSingle(CompositableForwarder* aFwd,
|
||||
{
|
||||
}
|
||||
|
||||
ImageClientBuffered::ImageClientBuffered(CompositableForwarder* aFwd,
|
||||
TextureFlags aFlags,
|
||||
CompositableType aType)
|
||||
: ImageClientSingle(aFwd, aFlags, aType)
|
||||
{
|
||||
}
|
||||
|
||||
TextureInfo ImageClientSingle::GetTextureInfo() const
|
||||
{
|
||||
return TextureInfo(CompositableType::IMAGE);
|
||||
@ -148,34 +137,10 @@ ImageClientSingle::FlushAllImages(bool aExceptFront,
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ImageClientBuffered::FlushAllImages(bool aExceptFront,
|
||||
AsyncTransactionTracker* aAsyncTransactionTracker)
|
||||
{
|
||||
if (!aExceptFront && mFrontBuffer) {
|
||||
RemoveTexture(mFrontBuffer);
|
||||
mFrontBuffer = nullptr;
|
||||
}
|
||||
if (mBackBuffer) {
|
||||
RemoveTextureWithTracker(mBackBuffer, aAsyncTransactionTracker);
|
||||
mBackBuffer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ImageClientSingle::UpdateImage(ImageContainer* aContainer,
|
||||
uint32_t aContentFlags)
|
||||
{
|
||||
bool isSwapped = false;
|
||||
return UpdateImageInternal(aContainer, aContentFlags, &isSwapped);
|
||||
}
|
||||
|
||||
bool
|
||||
ImageClientSingle::UpdateImageInternal(ImageContainer* aContainer,
|
||||
uint32_t aContentFlags, bool* aIsSwapped)
|
||||
ImageClientSingle::UpdateImage(ImageContainer* aContainer, uint32_t aContentFlags)
|
||||
{
|
||||
AutoLockImage autoLock(aContainer);
|
||||
*aIsSwapped = false;
|
||||
|
||||
Image *image = autoLock.GetImage();
|
||||
if (!image) {
|
||||
@ -186,186 +151,101 @@ ImageClientSingle::UpdateImageInternal(ImageContainer* aContainer,
|
||||
return true;
|
||||
}
|
||||
|
||||
RefPtr<TextureClient> texture = image->AsSharedImage()
|
||||
? image->AsSharedImage()->GetTextureClient(this)
|
||||
: nullptr;
|
||||
|
||||
AutoRemoveTexture autoRemoveTexture(this);
|
||||
|
||||
if (image->AsSharedImage() && image->AsSharedImage()->GetTextureClient(this)) {
|
||||
// fast path: no need to allocate and/or copy image data
|
||||
RefPtr<TextureClient> texture = image->AsSharedImage()->GetTextureClient(this);
|
||||
if (texture != mFrontBuffer) {
|
||||
autoRemoveTexture.mTexture = mFrontBuffer;
|
||||
}
|
||||
mFrontBuffer = texture;
|
||||
if (!AddTextureClient(texture)) {
|
||||
mFrontBuffer = nullptr;
|
||||
return false;
|
||||
}
|
||||
GetForwarder()->UpdatedTexture(this, texture, nullptr);
|
||||
GetForwarder()->UseTexture(this, texture);
|
||||
} else if (image->GetFormat() == ImageFormat::PLANAR_YCBCR) {
|
||||
PlanarYCbCrImage* ycbcr = static_cast<PlanarYCbCrImage*>(image);
|
||||
const PlanarYCbCrData* data = ycbcr->GetData();
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mFrontBuffer && mFrontBuffer->IsImmutable()) {
|
||||
autoRemoveTexture.mTexture = mFrontBuffer;
|
||||
mFrontBuffer = nullptr;
|
||||
}
|
||||
|
||||
bool bufferCreated = false;
|
||||
if (!mFrontBuffer) {
|
||||
gfx::IntSize ySize(data->mYSize.width, data->mYSize.height);
|
||||
gfx::IntSize cbCrSize(data->mCbCrSize.width, data->mCbCrSize.height);
|
||||
mFrontBuffer = TextureClient::CreateForYCbCr(GetForwarder(),
|
||||
ySize, cbCrSize, data->mStereoMode,
|
||||
TextureFlags::DEFAULT|mTextureFlags);
|
||||
if (!mFrontBuffer) {
|
||||
return false;
|
||||
}
|
||||
bufferCreated = true;
|
||||
}
|
||||
|
||||
if (!mFrontBuffer->Lock(OpenMode::OPEN_WRITE_ONLY)) {
|
||||
mFrontBuffer = nullptr;
|
||||
return false;
|
||||
}
|
||||
bool status = mFrontBuffer->AsTextureClientYCbCr()->UpdateYCbCr(*data);
|
||||
mFrontBuffer->Unlock();
|
||||
|
||||
if (bufferCreated) {
|
||||
if (!AddTextureClient(mFrontBuffer)) {
|
||||
mFrontBuffer = nullptr;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (status) {
|
||||
GetForwarder()->UpdatedTexture(this, mFrontBuffer, nullptr);
|
||||
GetForwarder()->UseTexture(this, mFrontBuffer);
|
||||
} else {
|
||||
MOZ_ASSERT(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
} else if (image->GetFormat() == ImageFormat::SURFACE_TEXTURE ||
|
||||
image->GetFormat() == ImageFormat::EGLIMAGE)
|
||||
{
|
||||
gfx::IntSize size = gfx::IntSize(image->GetSize().width, image->GetSize().height);
|
||||
|
||||
if (mFrontBuffer) {
|
||||
autoRemoveTexture.mTexture = mFrontBuffer;
|
||||
mFrontBuffer = nullptr;
|
||||
}
|
||||
|
||||
RefPtr<TextureClient> buffer;
|
||||
|
||||
if (image->GetFormat() == ImageFormat::EGLIMAGE) {
|
||||
EGLImageImage* typedImage = static_cast<EGLImageImage*>(image);
|
||||
const EGLImageImage::Data* data = typedImage->GetData();
|
||||
|
||||
buffer = new EGLImageTextureClient(mTextureFlags,
|
||||
data->mImage,
|
||||
size,
|
||||
data->mInverted);
|
||||
#ifdef MOZ_WIDGET_ANDROID
|
||||
} else if (image->GetFormat() == ImageFormat::SURFACE_TEXTURE) {
|
||||
SurfaceTextureImage* typedImage = static_cast<SurfaceTextureImage*>(image);
|
||||
const SurfaceTextureImage::Data* data = typedImage->GetData();
|
||||
|
||||
buffer = new SurfaceTextureClient(mTextureFlags,
|
||||
data->mSurfTex,
|
||||
size,
|
||||
data->mInverted);
|
||||
#endif
|
||||
} else {
|
||||
MOZ_ASSERT(false, "Bad ImageFormat.");
|
||||
}
|
||||
|
||||
mFrontBuffer = buffer;
|
||||
if (!AddTextureClient(mFrontBuffer)) {
|
||||
mFrontBuffer = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
GetForwarder()->UseTexture(this, mFrontBuffer);
|
||||
|
||||
} else {
|
||||
RefPtr<gfx::SourceSurface> surface = image->GetAsSourceSurface();
|
||||
MOZ_ASSERT(surface);
|
||||
|
||||
gfx::IntSize size = image->GetSize();
|
||||
|
||||
if (mFrontBuffer &&
|
||||
(mFrontBuffer->IsImmutable() || mFrontBuffer->GetSize() != size)) {
|
||||
autoRemoveTexture.mTexture = mFrontBuffer;
|
||||
mFrontBuffer = nullptr;
|
||||
}
|
||||
|
||||
bool bufferCreated = false;
|
||||
if (!mFrontBuffer) {
|
||||
mFrontBuffer = CreateTextureClientForDrawing(surface->GetFormat(), size,
|
||||
gfx::BackendType::NONE, mTextureFlags);
|
||||
if (!mFrontBuffer) {
|
||||
return false;
|
||||
}
|
||||
MOZ_ASSERT(mFrontBuffer->CanExposeDrawTarget());
|
||||
bufferCreated = true;
|
||||
}
|
||||
|
||||
if (!mFrontBuffer->Lock(OpenMode::OPEN_WRITE_ONLY)) {
|
||||
mFrontBuffer = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
// We must not keep a reference to the DrawTarget after it has been unlocked.
|
||||
DrawTarget* dt = mFrontBuffer->BorrowDrawTarget();
|
||||
MOZ_ASSERT(surface.get());
|
||||
dt->CopySurface(surface, IntRect(IntPoint(), surface->GetSize()), IntPoint());
|
||||
}
|
||||
|
||||
mFrontBuffer->Unlock();
|
||||
|
||||
if (bufferCreated) {
|
||||
if (!AddTextureClient(mFrontBuffer)) {
|
||||
mFrontBuffer = nullptr;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
GetForwarder()->UpdatedTexture(this, mFrontBuffer, nullptr);
|
||||
GetForwarder()->UseTexture(this, mFrontBuffer);
|
||||
if (texture != mFrontBuffer) {
|
||||
autoRemoveTexture.mTexture = mFrontBuffer;
|
||||
mFrontBuffer = nullptr;
|
||||
}
|
||||
|
||||
if (!texture) {
|
||||
// Slow path, we should not be hitting it very often and if we do it means
|
||||
// we are using an Image class that is not backed by textureClient and we
|
||||
// should fix it.
|
||||
if (image->GetFormat() == ImageFormat::PLANAR_YCBCR) {
|
||||
PlanarYCbCrImage* ycbcr = static_cast<PlanarYCbCrImage*>(image);
|
||||
const PlanarYCbCrData* data = ycbcr->GetData();
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
texture = TextureClient::CreateForYCbCr(GetForwarder(),
|
||||
data->mYSize, data->mCbCrSize, data->mStereoMode,
|
||||
TextureFlags::DEFAULT | mTextureFlags
|
||||
);
|
||||
if (!texture || !texture->Lock(OpenMode::OPEN_WRITE_ONLY)) {
|
||||
return false;
|
||||
}
|
||||
bool status = texture->AsTextureClientYCbCr()->UpdateYCbCr(*data);
|
||||
MOZ_ASSERT(status);
|
||||
|
||||
texture->Unlock();
|
||||
if (!status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else if (image->GetFormat() == ImageFormat::SURFACE_TEXTURE ||
|
||||
image->GetFormat() == ImageFormat::EGLIMAGE) {
|
||||
gfx::IntSize size = image->GetSize();
|
||||
|
||||
if (image->GetFormat() == ImageFormat::EGLIMAGE) {
|
||||
EGLImageImage* typedImage = static_cast<EGLImageImage*>(image);
|
||||
const EGLImageImage::Data* data = typedImage->GetData();
|
||||
|
||||
texture = new EGLImageTextureClient(mTextureFlags, data->mImage,
|
||||
size, data->mInverted);
|
||||
#ifdef MOZ_WIDGET_ANDROID
|
||||
} else if (image->GetFormat() == ImageFormat::SURFACE_TEXTURE) {
|
||||
SurfaceTextureImage* typedImage = static_cast<SurfaceTextureImage*>(image);
|
||||
const SurfaceTextureImage::Data* data = typedImage->GetData();
|
||||
texture = new SurfaceTextureClient(mTextureFlags, data->mSurfTex,
|
||||
size, data->mInverted);
|
||||
#endif
|
||||
} else {
|
||||
MOZ_ASSERT(false, "Bad ImageFormat.");
|
||||
}
|
||||
} else {
|
||||
RefPtr<gfx::SourceSurface> surface = image->GetAsSourceSurface();
|
||||
MOZ_ASSERT(surface);
|
||||
texture = CreateTextureClientForDrawing(surface->GetFormat(), image->GetSize(),
|
||||
gfx::BackendType::NONE, mTextureFlags);
|
||||
if (!texture) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MOZ_ASSERT(texture->CanExposeDrawTarget());
|
||||
|
||||
if (!texture->Lock(OpenMode::OPEN_WRITE_ONLY)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
// We must not keep a reference to the DrawTarget after it has been unlocked.
|
||||
DrawTarget* dt = texture->BorrowDrawTarget();
|
||||
MOZ_ASSERT(surface.get());
|
||||
dt->CopySurface(surface, IntRect(IntPoint(), surface->GetSize()), IntPoint());
|
||||
}
|
||||
|
||||
texture->Unlock();
|
||||
}
|
||||
}
|
||||
if (!texture || !AddTextureClient(texture)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mFrontBuffer = texture;
|
||||
GetForwarder()->UpdatedTexture(this, texture, nullptr);
|
||||
GetForwarder()->UseTexture(this, texture);
|
||||
|
||||
UpdatePictureRect(image->GetPictureRect());
|
||||
|
||||
mLastPaintedImageSerial = image->GetSerial();
|
||||
aContainer->NotifyPaintedImage(image);
|
||||
*aIsSwapped = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
ImageClientBuffered::UpdateImage(ImageContainer* aContainer,
|
||||
uint32_t aContentFlags)
|
||||
{
|
||||
RefPtr<TextureClient> temp = mFrontBuffer;
|
||||
mFrontBuffer = mBackBuffer;
|
||||
mBackBuffer = temp;
|
||||
|
||||
bool isSwapped = false;
|
||||
bool ret = ImageClientSingle::UpdateImageInternal(aContainer, aContentFlags, &isSwapped);
|
||||
|
||||
if (!isSwapped) {
|
||||
// If buffer swap did not happen at Host side, swap back the buffers.
|
||||
RefPtr<TextureClient> temp = mFrontBuffer;
|
||||
mFrontBuffer = mBackBuffer;
|
||||
mBackBuffer = temp;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool
|
||||
ImageClientSingle::AddTextureClient(TextureClient* aTexture)
|
||||
{
|
||||
@ -379,13 +259,6 @@ ImageClientSingle::OnDetach()
|
||||
mFrontBuffer = nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
ImageClientBuffered::OnDetach()
|
||||
{
|
||||
mFrontBuffer = nullptr;
|
||||
mBackBuffer = nullptr;
|
||||
}
|
||||
|
||||
ImageClient::ImageClient(CompositableForwarder* aFwd, TextureFlags aFlags,
|
||||
CompositableType aType)
|
||||
: CompositableClient(aFwd, aFlags)
|
||||
@ -406,7 +279,7 @@ ImageClient::UpdatePictureRect(nsIntRect aRect)
|
||||
|
||||
ImageClientBridge::ImageClientBridge(CompositableForwarder* aFwd,
|
||||
TextureFlags aFlags)
|
||||
: ImageClient(aFwd, aFlags, CompositableType::BUFFER_BRIDGE)
|
||||
: ImageClient(aFwd, aFlags, CompositableType::IMAGE_BRIDGE)
|
||||
, mAsyncContainerID(0)
|
||||
, mLayer(nullptr)
|
||||
{
|
||||
|
@ -114,34 +114,10 @@ public:
|
||||
virtual void FlushAllImages(bool aExceptFront,
|
||||
AsyncTransactionTracker* aAsyncTransactionTracker) MOZ_OVERRIDE;
|
||||
|
||||
protected:
|
||||
virtual bool UpdateImageInternal(ImageContainer* aContainer, uint32_t aContentFlags, bool* aIsSwapped);
|
||||
|
||||
protected:
|
||||
RefPtr<TextureClient> mFrontBuffer;
|
||||
};
|
||||
|
||||
/**
|
||||
* An image client which uses two texture clients.
|
||||
*/
|
||||
class ImageClientBuffered : public ImageClientSingle
|
||||
{
|
||||
public:
|
||||
ImageClientBuffered(CompositableForwarder* aFwd,
|
||||
TextureFlags aFlags,
|
||||
CompositableType aType);
|
||||
|
||||
virtual bool UpdateImage(ImageContainer* aContainer, uint32_t aContentFlags);
|
||||
|
||||
virtual void OnDetach() MOZ_OVERRIDE;
|
||||
|
||||
virtual void FlushAllImages(bool aExceptFront,
|
||||
AsyncTransactionTracker* aAsyncTransactionTracker) MOZ_OVERRIDE;
|
||||
|
||||
protected:
|
||||
RefPtr<TextureClient> mBackBuffer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Image class to be used for async image uploads using the image bridge
|
||||
* protocol.
|
||||
|
@ -45,8 +45,6 @@ bool
|
||||
CanvasLayerComposite::SetCompositableHost(CompositableHost* aHost)
|
||||
{
|
||||
switch (aHost->GetType()) {
|
||||
case CompositableType::BUFFER_IMAGE_SINGLE:
|
||||
case CompositableType::BUFFER_IMAGE_BUFFERED:
|
||||
case CompositableType::IMAGE:
|
||||
mImageHost = aHost;
|
||||
return true;
|
||||
|
@ -178,7 +178,7 @@ CompositableHost::Create(const TextureInfo& aTextureInfo)
|
||||
{
|
||||
RefPtr<CompositableHost> result;
|
||||
switch (aTextureInfo.mCompositableType) {
|
||||
case CompositableType::BUFFER_BRIDGE:
|
||||
case CompositableType::IMAGE_BRIDGE:
|
||||
NS_ERROR("Cannot create an image bridge compositable this way");
|
||||
break;
|
||||
case CompositableType::BUFFER_CONTENT_INC:
|
||||
|
@ -51,8 +51,6 @@ bool
|
||||
ImageLayerComposite::SetCompositableHost(CompositableHost* aHost)
|
||||
{
|
||||
switch (aHost->GetType()) {
|
||||
case CompositableType::BUFFER_IMAGE_SINGLE:
|
||||
case CompositableType::BUFFER_IMAGE_BUFFERED:
|
||||
case CompositableType::IMAGE:
|
||||
case CompositableType::IMAGE_OVERLAY:
|
||||
mImageHost = aHost;
|
||||
|
Loading…
Reference in New Issue
Block a user