Adding Pipeline statistics queries
This commit is contained in:
@@ -87,6 +87,7 @@ void BasePass::render() {
|
||||
opaqueCulling->writeChanges();
|
||||
transparentCulling->writeChanges();
|
||||
|
||||
query->beginQuery();
|
||||
graphics->beginRenderPass(renderPass);
|
||||
Array<Gfx::ORenderCommand> commands;
|
||||
|
||||
@@ -184,6 +185,7 @@ void BasePass::render() {
|
||||
|
||||
graphics->executeCommands(std::move(commands));
|
||||
graphics->endRenderPass();
|
||||
query->endQuery();
|
||||
}
|
||||
|
||||
void BasePass::endFrame() {}
|
||||
@@ -205,6 +207,9 @@ void BasePass::publishOutputs() {
|
||||
Gfx::RenderTargetAttachment(basePassDepth, Gfx::SE_IMAGE_LAYOUT_UNDEFINED, Gfx::SE_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
||||
Gfx::SE_ATTACHMENT_LOAD_OP_CLEAR, Gfx::SE_ATTACHMENT_STORE_OP_STORE);
|
||||
resources->registerRenderPassOutput("BASEPASS_DEPTH", depthAttachment);
|
||||
|
||||
query = graphics->createPipelineStatisticsQuery();
|
||||
resources->registerQueryOutput("BASEPASS_QUERY", query);
|
||||
}
|
||||
|
||||
void BasePass::createRenderPass() {
|
||||
|
||||
@@ -35,6 +35,8 @@ class BasePass : public RenderPass {
|
||||
Gfx::OPipelineLayout basePassLayout;
|
||||
Gfx::ODescriptorLayout lightCullingLayout;
|
||||
|
||||
Gfx::OPipelineStatisticsQuery query;
|
||||
|
||||
Gfx::PShaderBuffer cullingBuffer;
|
||||
};
|
||||
DEFINE_REF(BasePass)
|
||||
|
||||
@@ -46,6 +46,7 @@ CachedDepthPass::~CachedDepthPass() {}
|
||||
void CachedDepthPass::beginFrame(const Component::Camera& cam) { RenderPass::beginFrame(cam); }
|
||||
|
||||
void CachedDepthPass::render() {
|
||||
query->beginQuery();
|
||||
graphics->beginRenderPass(renderPass);
|
||||
Array<Gfx::ORenderCommand> commands;
|
||||
|
||||
@@ -141,6 +142,7 @@ void CachedDepthPass::render() {
|
||||
|
||||
graphics->executeCommands(std::move(commands));
|
||||
graphics->endRenderPass();
|
||||
query->endQuery();
|
||||
}
|
||||
|
||||
void CachedDepthPass::endFrame() {}
|
||||
@@ -171,6 +173,8 @@ void CachedDepthPass::publishOutputs() {
|
||||
Gfx::RenderTargetAttachment(visibilityBuffer, Gfx::SE_IMAGE_LAYOUT_UNDEFINED, Gfx::SE_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
|
||||
Gfx::SE_ATTACHMENT_LOAD_OP_CLEAR, Gfx::SE_ATTACHMENT_STORE_OP_STORE);
|
||||
resources->registerRenderPassOutput("VISIBILITY", visibilityAttachment);
|
||||
query = graphics->createPipelineStatisticsQuery();
|
||||
resources->registerQueryOutput("CACHED_QUERY", query);
|
||||
}
|
||||
|
||||
void CachedDepthPass::createRenderPass() {
|
||||
|
||||
@@ -21,6 +21,7 @@ class CachedDepthPass : public RenderPass {
|
||||
Gfx::OTexture2D depthBuffer;
|
||||
Gfx::OTexture2D visibilityBuffer;
|
||||
Gfx::OPipelineLayout depthPrepassLayout;
|
||||
Gfx::OPipelineStatisticsQuery query;
|
||||
|
||||
Gfx::PShaderBuffer cullingBuffer;
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@ void DepthCullingPass::render() {
|
||||
Gfx::PDescriptorSet set = depthTextureLayout->allocateDescriptorSet();
|
||||
set->updateTexture(0, Gfx::PTexture2D(depthMipTexture));
|
||||
set->writeChanges();
|
||||
|
||||
query->beginQuery();
|
||||
graphics->beginRenderPass(renderPass);
|
||||
if (useDepthCulling) {
|
||||
|
||||
@@ -173,6 +173,7 @@ void DepthCullingPass::render() {
|
||||
graphics->executeCommands(std::move(commands));
|
||||
}
|
||||
graphics->endRenderPass();
|
||||
query->endQuery();
|
||||
// Sync depth read/write with compute read
|
||||
depthAttachment.getTexture()->pipelineBarrier(
|
||||
Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
@@ -192,6 +193,8 @@ void DepthCullingPass::publishOutputs() {
|
||||
TextureCreateInfo depthMipInfo = {
|
||||
.format = Gfx::SE_FORMAT_D32_SFLOAT, .width = width, .height = height, .mipLevels = mipLevels, .name = "DepthMipTexture"};
|
||||
depthMipTexture = graphics->createTexture2D(depthMipInfo);
|
||||
query = graphics->createPipelineStatisticsQuery();
|
||||
resources->registerQueryOutput("DEPTH_QUERY", query);
|
||||
}
|
||||
|
||||
void DepthCullingPass::createRenderPass() {
|
||||
|
||||
@@ -22,6 +22,7 @@ class DepthCullingPass : public RenderPass {
|
||||
Gfx::RenderTargetAttachment visibilityAttachment;
|
||||
Gfx::ODescriptorLayout depthTextureLayout;
|
||||
Gfx::OPipelineLayout depthPrepassLayout;
|
||||
Gfx::OPipelineStatisticsQuery query;
|
||||
|
||||
Gfx::PShaderBuffer cullingBuffer;
|
||||
};
|
||||
|
||||
@@ -40,6 +40,7 @@ void LightCullingPass::beginFrame(const Component::Camera& cam) {
|
||||
}
|
||||
|
||||
void LightCullingPass::render() {
|
||||
query->beginQuery();
|
||||
depthAttachment->transferOwnership(Gfx::QueueType::COMPUTE);
|
||||
cullingDescriptorSet->updateTexture(0, depthAttachment);
|
||||
cullingDescriptorSet->updateBuffer(1, oLightIndexCounter);
|
||||
@@ -61,7 +62,7 @@ void LightCullingPass::render() {
|
||||
commands.add(std::move(computeCommand));
|
||||
// std::cout << "Execute" << std::endl;
|
||||
graphics->executeCommands(std::move(commands));
|
||||
|
||||
query->endQuery();
|
||||
oLightIndexList->pipelineBarrier(Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
Gfx::SE_ACCESS_SHADER_READ_BIT, Gfx::SE_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
||||
tLightIndexList->pipelineBarrier(Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
@@ -212,7 +213,11 @@ void LightCullingPass::publishOutputs() {
|
||||
resources->registerTextureOutput("LIGHTCULLING_TLIGHTGRID", Gfx::PTexture2D(tLightGrid));
|
||||
}
|
||||
|
||||
void LightCullingPass::createRenderPass() { depthAttachment = resources->requestRenderTarget("DEPTHPREPASS_DEPTH").getTexture(); }
|
||||
void LightCullingPass::createRenderPass() {
|
||||
depthAttachment = resources->requestRenderTarget("DEPTHPREPASS_DEPTH").getTexture();
|
||||
query = graphics->createPipelineStatisticsQuery();
|
||||
resources->registerQueryOutput("LIGHTCULL_QUERY", query);
|
||||
}
|
||||
|
||||
void LightCullingPass::setupFrustums() {
|
||||
uint32_t viewportWidth = viewport->getWidth();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "Graphics/Query.h"
|
||||
#include "Graphics/Shader.h"
|
||||
#include "RenderPass.h"
|
||||
#include "Scene/Scene.h"
|
||||
@@ -63,6 +64,7 @@ class LightCullingPass : public RenderPass {
|
||||
Gfx::OComputeShader cullingEnabledShader;
|
||||
Gfx::PComputePipeline cullingPipeline;
|
||||
Gfx::PComputePipeline cullingEnabledPipeline;
|
||||
Gfx::OPipelineStatisticsQuery query;
|
||||
};
|
||||
DEFINE_REF(LightCullingPass)
|
||||
} // namespace Seele
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "RenderGraphResources.h"
|
||||
#include "RenderPass.h"
|
||||
|
||||
namespace Seele {
|
||||
@@ -33,7 +34,9 @@ class RenderGraph {
|
||||
pass->endFrame();
|
||||
}
|
||||
}
|
||||
|
||||
PRenderGraphResources getResources() {
|
||||
return res;
|
||||
}
|
||||
private:
|
||||
PRenderGraphResources res;
|
||||
List<ORenderPass> passes;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "RenderGraphResources.h"
|
||||
#include "Graphics/Query.h"
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
using namespace Seele;
|
||||
|
||||
@@ -17,6 +19,8 @@ Gfx::PShaderBuffer RenderGraphResources::requestBuffer(const std::string& output
|
||||
|
||||
Gfx::PUniformBuffer RenderGraphResources::requestUniform(const std::string& outputName) { return registeredUniforms.at(outputName); }
|
||||
|
||||
Gfx::PPipelineStatisticsQuery RenderGraphResources::requestQuery(const std::string& outputName) { return registeredQueries.at(outputName); }
|
||||
|
||||
void RenderGraphResources::registerRenderPassOutput(const std::string& outputName, Gfx::RenderTargetAttachment attachment) {
|
||||
registeredAttachments[outputName] = attachment;
|
||||
}
|
||||
@@ -30,4 +34,8 @@ void RenderGraphResources::registerBufferOutput(const std::string& outputName, G
|
||||
}
|
||||
void RenderGraphResources::registerUniformOutput(const std::string& outputName, Gfx::PUniformBuffer buffer) {
|
||||
registeredUniforms[outputName] = buffer;
|
||||
}
|
||||
|
||||
void RenderGraphResources::registerQueryOutput(const std::string& outputName, Gfx::PPipelineStatisticsQuery query) {
|
||||
registeredQueries[outputName] = query;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "Graphics/Buffer.h"
|
||||
#include "Graphics/Query.h"
|
||||
#include "Graphics/RenderTarget.h"
|
||||
#include "Graphics/Texture.h"
|
||||
#include "MinimalEngine.h"
|
||||
@@ -16,16 +17,19 @@ class RenderGraphResources {
|
||||
Gfx::PTexture requestTexture(const std::string& outputName);
|
||||
Gfx::PShaderBuffer requestBuffer(const std::string& outputName);
|
||||
Gfx::PUniformBuffer requestUniform(const std::string& outputName);
|
||||
Gfx::PPipelineStatisticsQuery requestQuery(const std::string& outputName);
|
||||
void registerRenderPassOutput(const std::string& outputName, Gfx::RenderTargetAttachment attachment);
|
||||
void registerTextureOutput(const std::string& outputName, Gfx::PTexture buffer);
|
||||
void registerBufferOutput(const std::string& outputName, Gfx::PShaderBuffer buffer);
|
||||
void registerUniformOutput(const std::string& outputName, Gfx::PUniformBuffer buffer);
|
||||
void registerQueryOutput(const std::string& outputName, Gfx::PPipelineStatisticsQuery query);
|
||||
|
||||
protected:
|
||||
Map<std::string, Gfx::RenderTargetAttachment> registeredAttachments;
|
||||
Map<std::string, Gfx::PTexture> registeredTextures;
|
||||
Map<std::string, Gfx::PShaderBuffer> registeredBuffers;
|
||||
Map<std::string, Gfx::PUniformBuffer> registeredUniforms;
|
||||
Map<std::string, Gfx::PPipelineStatisticsQuery> registeredQueries;
|
||||
};
|
||||
DEFINE_REF(RenderGraphResources)
|
||||
} // namespace Seele
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
#include "Graphics/Command.h"
|
||||
#include "Graphics/Graphics.h"
|
||||
|
||||
|
||||
using namespace Seele;
|
||||
|
||||
SkyboxRenderPass::SkyboxRenderPass(Gfx::PGraphics graphics, PScene scene) : RenderPass(graphics, scene) {
|
||||
skybox = Seele::Component::Skybox{
|
||||
//.day = AssetRegistry::findTexture("FS000_Day_01")->getTexture().cast<Gfx::TextureCube>(),
|
||||
//.night = AssetRegistry::findTexture("FS000_Night_01")->getTexture().cast<Gfx::TextureCube>(),
|
||||
.day = AssetRegistry::findTexture("", "skyboxsun5deg_tn")->getTexture().cast<Gfx::TextureCube>(),
|
||||
.night = AssetRegistry::findTexture("", "skyboxsun5deg_tn")->getTexture().cast<Gfx::TextureCube>(),
|
||||
.fogColor = Vector(0.1, 0.1, 0.8),
|
||||
.blendFactor = 0,
|
||||
};
|
||||
@@ -86,14 +85,41 @@ void SkyboxRenderPass::publishOutputs() {
|
||||
|
||||
void SkyboxRenderPass::createRenderPass() {
|
||||
colorAttachment = resources->requestRenderTarget("BASEPASS_COLOR");
|
||||
// colorAttachment->loadOp = Gfx::SE_ATTACHMENT_LOAD_OP_LOAD;
|
||||
depthAttachment = resources->requestRenderTarget("DEPTHPREPASS_DEPTH");
|
||||
// depthAttachment->loadOp = Gfx::SE_ATTACHMENT_LOAD_OP_LOAD;
|
||||
// Gfx::ORenderTargetLayout layout = new Gfx::RenderTargetLayout{
|
||||
// .colorAttachments = { colorAttachment },
|
||||
// .depthAttachment = depthAttachment
|
||||
// };
|
||||
// renderPass = graphics->createRenderPass(std::move(layout), viewport);
|
||||
colorAttachment.setLoadOp(Gfx::SE_ATTACHMENT_LOAD_OP_LOAD);
|
||||
colorAttachment.setInitialLayout(Gfx::SE_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
colorAttachment.setInitialLayout(Gfx::SE_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
|
||||
depthAttachment = resources->requestRenderTarget("BASEPASS_DEPTH");
|
||||
depthAttachment.setLoadOp(Gfx::SE_ATTACHMENT_LOAD_OP_LOAD);
|
||||
depthAttachment.setInitialLayout(Gfx::SE_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
|
||||
depthAttachment.setFinalLayout(Gfx::SE_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
|
||||
Gfx::RenderTargetLayout layout = Gfx::RenderTargetLayout{
|
||||
.colorAttachments = {colorAttachment},
|
||||
.depthAttachment = depthAttachment,
|
||||
};
|
||||
Array<Gfx::SubPassDependency> dependency = {
|
||||
{
|
||||
.srcSubpass = ~0U,
|
||||
.dstSubpass = 0,
|
||||
.srcStage = Gfx::SE_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | Gfx::SE_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||||
.dstStage = Gfx::SE_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | Gfx::SE_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT,
|
||||
.srcAccess = Gfx::SE_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
|
||||
Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
.dstAccess = Gfx::SE_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
|
||||
Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
},
|
||||
{
|
||||
.srcSubpass = 0,
|
||||
.dstSubpass = ~0U,
|
||||
.srcStage = Gfx::SE_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | Gfx::SE_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||||
.dstStage = Gfx::SE_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | Gfx::SE_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT,
|
||||
.srcAccess = Gfx::SE_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
|
||||
Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
.dstAccess = Gfx::SE_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
|
||||
Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
},
|
||||
};
|
||||
renderPass = graphics->createRenderPass(std::move(layout), dependency, viewport);
|
||||
|
||||
skyboxData.transformMatrix = Matrix4(1);
|
||||
skyboxData.fogColor = skybox.fogColor;
|
||||
@@ -108,10 +134,16 @@ void SkyboxRenderPass::createRenderPass() {
|
||||
.dynamic = true,
|
||||
});
|
||||
|
||||
pipelineLayout = graphics->createPipelineLayout("SkyboxLayout");
|
||||
pipelineLayout->addDescriptorLayout(viewParamsLayout);
|
||||
pipelineLayout->addDescriptorLayout(skyboxDataLayout);
|
||||
pipelineLayout->addDescriptorLayout(textureLayout);
|
||||
|
||||
ShaderCreateInfo createInfo = {
|
||||
.name = "SkyboxVertex",
|
||||
.mainModule = "Skybox",
|
||||
.entryPoint = "vertexMain",
|
||||
.rootSignature = pipelineLayout,
|
||||
};
|
||||
vertexShader = graphics->createVertexShader(createInfo);
|
||||
|
||||
@@ -119,19 +151,26 @@ void SkyboxRenderPass::createRenderPass() {
|
||||
createInfo.entryPoint = "fragmentMain";
|
||||
fragmentShader = graphics->createFragmentShader(createInfo);
|
||||
|
||||
pipelineLayout = graphics->createPipelineLayout("SkyboxLayout");
|
||||
pipelineLayout->addDescriptorLayout(viewParamsLayout);
|
||||
pipelineLayout->addDescriptorLayout(skyboxDataLayout);
|
||||
pipelineLayout->addDescriptorLayout(textureLayout);
|
||||
pipelineLayout->create();
|
||||
|
||||
Gfx::LegacyPipelineCreateInfo gfxInfo;
|
||||
gfxInfo.vertexShader = vertexShader;
|
||||
gfxInfo.fragmentShader = fragmentShader;
|
||||
gfxInfo.rasterizationState.polygonMode = Gfx::SE_POLYGON_MODE_FILL;
|
||||
gfxInfo.topology = Gfx::SE_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
gfxInfo.pipelineLayout = pipelineLayout;
|
||||
gfxInfo.renderPass = renderPass;
|
||||
gfxInfo.multisampleState.samples = viewport->getSamples();
|
||||
Gfx::LegacyPipelineCreateInfo gfxInfo = {
|
||||
.topology = Gfx::SE_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
|
||||
.vertexShader = vertexShader,
|
||||
.fragmentShader = fragmentShader,
|
||||
.renderPass = renderPass,
|
||||
.pipelineLayout = pipelineLayout,
|
||||
.multisampleState =
|
||||
{
|
||||
.samples = viewport->getSamples(),
|
||||
},
|
||||
.rasterizationState =
|
||||
{
|
||||
.polygonMode = Gfx::SE_POLYGON_MODE_FILL,
|
||||
},
|
||||
.colorBlend =
|
||||
{
|
||||
.attachmentCount = 1,
|
||||
},
|
||||
};
|
||||
pipeline = graphics->createGraphicsPipeline(std::move(gfxInfo));
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ void VisibilityPass::render() {
|
||||
visibilitySet->updateBuffer(1, cullingBuffer);
|
||||
visibilitySet->writeChanges();
|
||||
|
||||
query->beginQuery();
|
||||
Gfx::OComputeCommand command = graphics->createComputeCommand("VisibilityCommand");
|
||||
command->bindPipeline(visibilityPipeline);
|
||||
command->bindDescriptor({viewParamsSet, visibilitySet});
|
||||
@@ -34,6 +35,7 @@ void VisibilityPass::render() {
|
||||
Array<Gfx::OComputeCommand> commands;
|
||||
commands.add(std::move(command));
|
||||
graphics->executeCommands(std::move(commands));
|
||||
query->endQuery();
|
||||
cullingBuffer->pipelineBarrier(Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
Gfx::SE_ACCESS_SHADER_READ_BIT,
|
||||
Gfx::SE_PIPELINE_STAGE_TASK_SHADER_BIT_EXT | Gfx::SE_PIPELINE_STAGE_MESH_SHADER_BIT_EXT);
|
||||
@@ -77,6 +79,9 @@ void VisibilityPass::publishOutputs() {
|
||||
|
||||
cullingBuffer = graphics->createShaderBuffer(ShaderBufferCreateInfo{.dynamic = true, .name = "CullingBuffer"});
|
||||
resources->registerBufferOutput("CULLINGBUFFER", cullingBuffer);
|
||||
|
||||
query = graphics->createPipelineStatisticsQuery();
|
||||
resources->registerQueryOutput("VISIBILITY_QUERY", query);
|
||||
}
|
||||
|
||||
void VisibilityPass::createRenderPass() { visibilityAttachment = resources->requestRenderTarget("VISIBILITY"); }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "Graphics/Query.h"
|
||||
#include "RenderPass.h"
|
||||
|
||||
namespace Seele {
|
||||
@@ -22,6 +23,7 @@ class VisibilityPass : public RenderPass {
|
||||
Gfx::OPipelineLayout visibilityLayout;
|
||||
Gfx::OComputeShader visibilityShader;
|
||||
Gfx::PComputePipeline visibilityPipeline;
|
||||
Gfx::OPipelineStatisticsQuery query;
|
||||
|
||||
// Holds culling information for every meshlet for each instance
|
||||
Gfx::OShaderBuffer cullingBuffer;
|
||||
|
||||
Reference in New Issue
Block a user