mirror of
https://github.com/EduApps-CDG/OpenDX
synced 2024-12-30 09:45:37 +01:00
[dxbc] Use raw SSBOs for raw and structured buffers if appropriate
This commit is contained in:
parent
48548eb894
commit
01b8e74457
@ -1041,43 +1041,67 @@ namespace dxvk {
|
|||||||
const bool isStructured = ins.op == DxbcOpcode::DclUavStructured
|
const bool isStructured = ins.op == DxbcOpcode::DclUavStructured
|
||||||
|| ins.op == DxbcOpcode::DclResourceStructured;
|
|| ins.op == DxbcOpcode::DclResourceStructured;
|
||||||
|
|
||||||
// Structured and raw buffers are represented as
|
|
||||||
// texel buffers consisting of 32-bit integers.
|
|
||||||
m_module.enableCapability(spv::CapabilityImageBuffer);
|
|
||||||
|
|
||||||
const DxbcScalarType sampledType = DxbcScalarType::Uint32;
|
const DxbcScalarType sampledType = DxbcScalarType::Uint32;
|
||||||
const uint32_t sampledTypeId = getScalarTypeId(sampledType);
|
const uint32_t sampledTypeId = getScalarTypeId(sampledType);
|
||||||
|
|
||||||
const DxbcImageInfo typeInfo = { spv::DimBuffer, 0, 0, isUav ? 2u : 1u, VK_IMAGE_VIEW_TYPE_MAX_ENUM };
|
const DxbcImageInfo typeInfo = { spv::DimBuffer, 0, 0, isUav ? 2u : 1u, VK_IMAGE_VIEW_TYPE_MAX_ENUM };
|
||||||
|
|
||||||
// Declare the resource type
|
// Declare the resource type
|
||||||
const uint32_t resTypeId = m_module.defImageType(sampledTypeId,
|
uint32_t resTypeId = 0;
|
||||||
typeInfo.dim, 0, typeInfo.array, typeInfo.ms, typeInfo.sampled,
|
uint32_t varId = 0;
|
||||||
spv::ImageFormatR32ui);
|
|
||||||
|
|
||||||
const uint32_t varId = m_module.newVar(
|
|
||||||
m_module.defPointerType(resTypeId, spv::StorageClassUniformConstant),
|
|
||||||
spv::StorageClassUniformConstant);
|
|
||||||
|
|
||||||
m_module.setDebugName(varId,
|
|
||||||
str::format(isUav ? "u" : "t", registerId).c_str());
|
|
||||||
|
|
||||||
// Write back resource info
|
// Write back resource info
|
||||||
const DxbcResourceType resType = isStructured
|
DxbcResourceType resType = isStructured
|
||||||
? DxbcResourceType::Structured
|
? DxbcResourceType::Structured
|
||||||
: DxbcResourceType::Raw;
|
: DxbcResourceType::Raw;
|
||||||
|
|
||||||
const uint32_t resStride = isStructured
|
uint32_t resStride = isStructured
|
||||||
? ins.imm[0].u32
|
? ins.imm[0].u32
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Compute the DXVK binding slot index for the resource.
|
// Compute the DXVK binding slot index for the resource.
|
||||||
const uint32_t bindingId = computeResourceSlotId(
|
uint32_t bindingId = computeResourceSlotId(
|
||||||
m_programInfo.type(), isUav
|
m_programInfo.type(), isUav
|
||||||
? DxbcBindingType::UnorderedAccessView
|
? DxbcBindingType::UnorderedAccessView
|
||||||
: DxbcBindingType::ShaderResource,
|
: DxbcBindingType::ShaderResource,
|
||||||
registerId);
|
registerId);
|
||||||
|
|
||||||
|
if (m_moduleInfo.options.useRawSsbo) {
|
||||||
|
uint32_t elemType = getScalarTypeId(DxbcScalarType::Uint32);
|
||||||
|
uint32_t arrayType = m_module.defRuntimeArrayTypeUnique(elemType);
|
||||||
|
uint32_t structType = m_module.defStructTypeUnique(1, &arrayType);
|
||||||
|
uint32_t ptrType = m_module.defPointerType(structType, spv::StorageClassUniform);
|
||||||
|
|
||||||
|
resTypeId = m_module.defPointerType(elemType, spv::StorageClassUniform);
|
||||||
|
varId = m_module.newVar(ptrType, spv::StorageClassUniform);
|
||||||
|
|
||||||
|
m_module.decorateArrayStride(arrayType, sizeof(uint32_t));
|
||||||
|
m_module.decorate(structType, spv::DecorationBufferBlock);
|
||||||
|
m_module.memberDecorateOffset(structType, 0, 0);
|
||||||
|
|
||||||
|
m_module.setDebugName(structType,
|
||||||
|
str::format("struct_", isUav ? "u" : "t", registerId).c_str());
|
||||||
|
m_module.setDebugMemberName(structType, 0, "m");
|
||||||
|
|
||||||
|
if (!isUav)
|
||||||
|
m_module.decorate(varId, spv::DecorationNonWritable);
|
||||||
|
} else {
|
||||||
|
// Structured and raw buffers are represented as
|
||||||
|
// texel buffers consisting of 32-bit integers.
|
||||||
|
m_module.enableCapability(spv::CapabilityImageBuffer);
|
||||||
|
|
||||||
|
resTypeId = m_module.defImageType(sampledTypeId,
|
||||||
|
typeInfo.dim, 0, typeInfo.array, typeInfo.ms, typeInfo.sampled,
|
||||||
|
spv::ImageFormatR32ui);
|
||||||
|
|
||||||
|
varId = m_module.newVar(
|
||||||
|
m_module.defPointerType(resTypeId, spv::StorageClassUniformConstant),
|
||||||
|
spv::StorageClassUniformConstant);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_module.setDebugName(varId,
|
||||||
|
str::format(isUav ? "u" : "t", registerId).c_str());
|
||||||
|
|
||||||
m_module.decorateDescriptorSet(varId, 0);
|
m_module.decorateDescriptorSet(varId, 0);
|
||||||
m_module.decorateBinding(varId, bindingId);
|
m_module.decorateBinding(varId, bindingId);
|
||||||
|
|
||||||
@ -1121,9 +1145,11 @@ namespace dxvk {
|
|||||||
// Store descriptor info for the shader interface
|
// Store descriptor info for the shader interface
|
||||||
DxvkResourceSlot resource;
|
DxvkResourceSlot resource;
|
||||||
resource.slot = bindingId;
|
resource.slot = bindingId;
|
||||||
resource.type = isUav
|
resource.type = m_moduleInfo.options.useRawSsbo
|
||||||
? VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER
|
? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
|
||||||
: VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
|
: (isUav
|
||||||
|
? VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER
|
||||||
|
: VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER);
|
||||||
resource.view = VK_IMAGE_VIEW_TYPE_MAX_ENUM;
|
resource.view = VK_IMAGE_VIEW_TYPE_MAX_ENUM;
|
||||||
resource.access = VK_ACCESS_SHADER_READ_BIT;
|
resource.access = VK_ACCESS_SHADER_READ_BIT;
|
||||||
|
|
||||||
@ -2568,10 +2594,16 @@ namespace dxvk {
|
|||||||
// (src0) The buffer register to query
|
// (src0) The buffer register to query
|
||||||
// TODO Check if resource is bound
|
// TODO Check if resource is bound
|
||||||
const DxbcBufferInfo bufferInfo = getBufferInfo(ins.src[0]);
|
const DxbcBufferInfo bufferInfo = getBufferInfo(ins.src[0]);
|
||||||
|
|
||||||
|
bool isSsbo = m_moduleInfo.options.useRawSsbo
|
||||||
|
&& bufferInfo.type != DxbcResourceType::Typed;
|
||||||
|
|
||||||
// We'll store this as a scalar unsigned integer
|
// We'll store this as a scalar unsigned integer
|
||||||
DxbcRegisterValue result = emitQueryTexelBufferSize(ins.src[0]);
|
DxbcRegisterValue result = isSsbo
|
||||||
const uint32_t typeId = getVectorTypeId(result.type);
|
? emitQueryBufferSize(ins.src[0])
|
||||||
|
: emitQueryTexelBufferSize(ins.src[0]);
|
||||||
|
|
||||||
|
uint32_t typeId = getVectorTypeId(result.type);
|
||||||
|
|
||||||
// Adjust returned size if this is a raw or structured
|
// Adjust returned size if this is a raw or structured
|
||||||
// buffer, as emitQueryTexelBufferSize only returns the
|
// buffer, as emitQueryTexelBufferSize only returns the
|
||||||
@ -4714,7 +4746,10 @@ namespace dxvk {
|
|||||||
|
|
||||||
// For UAVs and shared memory, different methods
|
// For UAVs and shared memory, different methods
|
||||||
// of obtaining the final pointer are used.
|
// of obtaining the final pointer are used.
|
||||||
const bool isUav = operand.type == DxbcOperandType::UnorderedAccessView;
|
bool isTgsm = operand.type == DxbcOperandType::ThreadGroupSharedMemory;
|
||||||
|
bool isSsbo = m_moduleInfo.options.useRawSsbo
|
||||||
|
&& resourceInfo.type != DxbcResourceType::Typed
|
||||||
|
&& !isTgsm;
|
||||||
|
|
||||||
// Compute the actual address into the resource
|
// Compute the actual address into the resource
|
||||||
const DxbcRegisterValue addressValue = [&] {
|
const DxbcRegisterValue addressValue = [&] {
|
||||||
@ -4734,7 +4769,7 @@ namespace dxvk {
|
|||||||
};
|
};
|
||||||
|
|
||||||
case DxbcResourceType::Typed: {
|
case DxbcResourceType::Typed: {
|
||||||
if (!isUav)
|
if (isTgsm)
|
||||||
throw DxvkError("DxbcCompiler: TGSM cannot be typed");
|
throw DxvkError("DxbcCompiler: TGSM cannot be typed");
|
||||||
|
|
||||||
return emitLoadTexCoord(address,
|
return emitLoadTexCoord(address,
|
||||||
@ -4750,13 +4785,20 @@ namespace dxvk {
|
|||||||
DxbcRegisterPointer result;
|
DxbcRegisterPointer result;
|
||||||
result.type.ctype = resourceInfo.stype;
|
result.type.ctype = resourceInfo.stype;
|
||||||
result.type.ccount = 1;
|
result.type.ccount = 1;
|
||||||
result.id = isUav
|
|
||||||
? m_module.opImageTexelPointer(
|
if (isTgsm) {
|
||||||
m_module.defPointerType(getVectorTypeId(result.type), spv::StorageClassImage),
|
result.id = m_module.opAccessChain(resourceInfo.typeId,
|
||||||
m_uavs.at(registerId).varId, addressValue.id, m_module.constu32(0))
|
resourceInfo.varId, 1, &addressValue.id);
|
||||||
: m_module.opAccessChain(
|
} else if (isSsbo) {
|
||||||
m_module.defPointerType(getVectorTypeId(result.type), spv::StorageClassWorkgroup),
|
uint32_t indices[2] = { m_module.constu32(0), addressValue.id };
|
||||||
m_gRegs.at(registerId).varId, 1, &addressValue.id);
|
result.id = m_module.opAccessChain(resourceInfo.typeId,
|
||||||
|
resourceInfo.varId, 2, indices);
|
||||||
|
} else {
|
||||||
|
result.id = m_module.opImageTexelPointer(
|
||||||
|
m_module.defPointerType(getVectorTypeId(result.type), spv::StorageClassImage),
|
||||||
|
resourceInfo.varId, addressValue.id, m_module.constu32(0));
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4770,9 +4812,10 @@ namespace dxvk {
|
|||||||
// Shared memory is the only type of buffer that
|
// Shared memory is the only type of buffer that
|
||||||
// is not accessed through a texel buffer view
|
// is not accessed through a texel buffer view
|
||||||
bool isTgsm = operand.type == DxbcOperandType::ThreadGroupSharedMemory;
|
bool isTgsm = operand.type == DxbcOperandType::ThreadGroupSharedMemory;
|
||||||
|
bool isSsbo = m_moduleInfo.options.useRawSsbo && !isTgsm;
|
||||||
|
|
||||||
// Common types and IDs used while loading the data
|
// Common types and IDs used while loading the data
|
||||||
uint32_t bufferId = isTgsm ? 0 : m_module.opLoad(bufferInfo.typeId, bufferInfo.varId);
|
uint32_t bufferId = isTgsm || isSsbo ? 0 : m_module.opLoad(bufferInfo.typeId, bufferInfo.varId);
|
||||||
|
|
||||||
uint32_t vectorTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 4 });
|
uint32_t vectorTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 4 });
|
||||||
uint32_t scalarTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 1 });
|
uint32_t scalarTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 1 });
|
||||||
@ -4796,32 +4839,29 @@ namespace dxvk {
|
|||||||
|
|
||||||
// Load requested component from the buffer
|
// Load requested component from the buffer
|
||||||
uint32_t zero = 0;
|
uint32_t zero = 0;
|
||||||
|
|
||||||
switch (operand.type) {
|
|
||||||
case DxbcOperandType::Resource:
|
|
||||||
ccomps[sindex] = m_module.opCompositeExtract(scalarTypeId,
|
|
||||||
m_module.opImageFetch(vectorTypeId,
|
|
||||||
bufferId, elementIndexAdjusted,
|
|
||||||
SpirvImageOperands()), 1, &zero);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DxbcOperandType::UnorderedAccessView:
|
|
||||||
ccomps[sindex] = m_module.opCompositeExtract(scalarTypeId,
|
|
||||||
m_module.opImageRead(vectorTypeId,
|
|
||||||
bufferId, elementIndexAdjusted,
|
|
||||||
SpirvImageOperands()), 1, &zero);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DxbcOperandType::ThreadGroupSharedMemory:
|
|
||||||
ccomps[sindex] = m_module.opLoad(scalarTypeId,
|
|
||||||
m_module.opAccessChain(bufferInfo.typeId,
|
|
||||||
bufferInfo.varId, 1, &elementIndexAdjusted));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw DxvkError("DxbcCompiler: Invalid operand type for strucured/raw load");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (isTgsm) {
|
||||||
|
ccomps[sindex] = m_module.opLoad(scalarTypeId,
|
||||||
|
m_module.opAccessChain(bufferInfo.typeId,
|
||||||
|
bufferInfo.varId, 1, &elementIndexAdjusted));
|
||||||
|
} else if (isSsbo) {
|
||||||
|
uint32_t indices[2] = { m_module.constu32(0), elementIndexAdjusted };
|
||||||
|
ccomps[sindex] = m_module.opLoad(scalarTypeId,
|
||||||
|
m_module.opAccessChain(bufferInfo.typeId,
|
||||||
|
bufferInfo.varId, 2, indices));
|
||||||
|
} else if (operand.type == DxbcOperandType::Resource) {
|
||||||
|
ccomps[sindex] = m_module.opCompositeExtract(scalarTypeId,
|
||||||
|
m_module.opImageFetch(vectorTypeId,
|
||||||
|
bufferId, elementIndexAdjusted,
|
||||||
|
SpirvImageOperands()), 1, &zero);
|
||||||
|
} else if (operand.type == DxbcOperandType::UnorderedAccessView) {
|
||||||
|
ccomps[sindex] = m_module.opCompositeExtract(scalarTypeId,
|
||||||
|
m_module.opImageRead(vectorTypeId,
|
||||||
|
bufferId, elementIndexAdjusted,
|
||||||
|
SpirvImageOperands()), 1, &zero);
|
||||||
|
} else {
|
||||||
|
throw DxvkError("DxbcCompiler: Invalid operand type for strucured/raw load");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4857,13 +4897,14 @@ namespace dxvk {
|
|||||||
value = emitRegisterBitcast(value, DxbcScalarType::Uint32);
|
value = emitRegisterBitcast(value, DxbcScalarType::Uint32);
|
||||||
|
|
||||||
// Thread Group Shared Memory is not accessed through a texel buffer view
|
// Thread Group Shared Memory is not accessed through a texel buffer view
|
||||||
const bool isUav = operand.type == DxbcOperandType::UnorderedAccessView;
|
bool isTgsm = operand.type == DxbcOperandType::ThreadGroupSharedMemory;
|
||||||
|
bool isSsbo = m_moduleInfo.options.useRawSsbo && !isTgsm;
|
||||||
|
|
||||||
// Perform UAV writes only if the UAV is bound and if there
|
// Perform UAV writes only if the UAV is bound and if there
|
||||||
// is nothing else preventing us from writing to it.
|
// is nothing else preventing us from writing to it.
|
||||||
DxbcConditional cond;
|
DxbcConditional cond;
|
||||||
|
|
||||||
if (isUav) {
|
if (!isTgsm) {
|
||||||
uint32_t writeTest = emitUavWriteTest(bufferInfo);
|
uint32_t writeTest = emitUavWriteTest(bufferInfo);
|
||||||
|
|
||||||
cond.labelIf = m_module.allocateId();
|
cond.labelIf = m_module.allocateId();
|
||||||
@ -4876,49 +4917,50 @@ namespace dxvk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform the actual write operation
|
// Perform the actual write operation
|
||||||
const uint32_t bufferId = isUav ? m_module.opLoad(bufferInfo.typeId, bufferInfo.varId) : 0;
|
uint32_t bufferId = isTgsm || isSsbo ? 0 : m_module.opLoad(bufferInfo.typeId, bufferInfo.varId);
|
||||||
|
|
||||||
const uint32_t scalarTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 1 });
|
uint32_t scalarTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 1 });
|
||||||
const uint32_t vectorTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 4 });
|
uint32_t vectorTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 4 });
|
||||||
|
|
||||||
uint32_t srcComponentIndex = 0;
|
uint32_t srcComponentIndex = 0;
|
||||||
|
|
||||||
for (uint32_t i = 0; i < 4; i++) {
|
for (uint32_t i = 0; i < 4; i++) {
|
||||||
if (operand.mask[i]) {
|
if (operand.mask[i]) {
|
||||||
const uint32_t srcComponentId = value.type.ccount > 1
|
uint32_t srcComponentId = value.type.ccount > 1
|
||||||
? m_module.opCompositeExtract(scalarTypeId,
|
? m_module.opCompositeExtract(scalarTypeId,
|
||||||
value.id, 1, &srcComponentIndex)
|
value.id, 1, &srcComponentIndex)
|
||||||
: value.id;
|
: value.id;
|
||||||
|
|
||||||
// Add the component offset to the element index
|
// Add the component offset to the element index
|
||||||
const uint32_t elementIndexAdjusted = i != 0
|
uint32_t elementIndexAdjusted = i != 0
|
||||||
? m_module.opIAdd(getVectorTypeId(elementIndex.type),
|
? m_module.opIAdd(getVectorTypeId(elementIndex.type),
|
||||||
elementIndex.id, m_module.consti32(i))
|
elementIndex.id, m_module.consti32(i))
|
||||||
: elementIndex.id;
|
: elementIndex.id;
|
||||||
|
|
||||||
switch (operand.type) {
|
if (isTgsm) {
|
||||||
case DxbcOperandType::UnorderedAccessView: {
|
m_module.opStore(
|
||||||
const std::array<uint32_t, 4> srcVectorIds = {
|
m_module.opAccessChain(bufferInfo.typeId,
|
||||||
srcComponentId, srcComponentId,
|
bufferInfo.varId, 1, &elementIndexAdjusted),
|
||||||
srcComponentId, srcComponentId,
|
srcComponentId);
|
||||||
};
|
} else if (isSsbo) {
|
||||||
|
uint32_t indices[2] = { m_module.constu32(0), elementIndexAdjusted };
|
||||||
m_module.opImageWrite(
|
m_module.opStore(
|
||||||
bufferId, elementIndexAdjusted,
|
m_module.opAccessChain(bufferInfo.typeId,
|
||||||
m_module.opCompositeConstruct(vectorTypeId,
|
bufferInfo.varId, 2, indices),
|
||||||
4, srcVectorIds.data()),
|
srcComponentId);
|
||||||
SpirvImageOperands());
|
} else if (operand.type == DxbcOperandType::UnorderedAccessView) {
|
||||||
} break;
|
const std::array<uint32_t, 4> srcVectorIds = {
|
||||||
|
srcComponentId, srcComponentId,
|
||||||
case DxbcOperandType::ThreadGroupSharedMemory:
|
srcComponentId, srcComponentId,
|
||||||
m_module.opStore(
|
};
|
||||||
m_module.opAccessChain(bufferInfo.typeId,
|
|
||||||
bufferInfo.varId, 1, &elementIndexAdjusted),
|
m_module.opImageWrite(
|
||||||
srcComponentId);
|
bufferId, elementIndexAdjusted,
|
||||||
break;
|
m_module.opCompositeConstruct(vectorTypeId,
|
||||||
|
4, srcVectorIds.data()),
|
||||||
default:
|
SpirvImageOperands());
|
||||||
throw DxvkError("DxbcCompiler: Invalid operand type for strucured/raw store");
|
} else {
|
||||||
|
throw DxvkError("DxbcCompiler: Invalid operand type for strucured/raw store");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write next component
|
// Write next component
|
||||||
@ -4927,11 +4969,25 @@ namespace dxvk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// End conditional block
|
// End conditional block
|
||||||
if (isUav) {
|
if (!isTgsm) {
|
||||||
m_module.opBranch(cond.labelEnd);
|
m_module.opBranch(cond.labelEnd);
|
||||||
m_module.opLabel (cond.labelEnd);
|
m_module.opLabel (cond.labelEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DxbcRegisterValue DxbcCompiler::emitQueryBufferSize(
|
||||||
|
const DxbcRegister& resource) {
|
||||||
|
const DxbcBufferInfo bufferInfo = getBufferInfo(resource);
|
||||||
|
|
||||||
|
DxbcRegisterValue result;
|
||||||
|
result.type.ctype = DxbcScalarType::Uint32;
|
||||||
|
result.type.ccount = 1;
|
||||||
|
result.id = m_module.opArrayLength(
|
||||||
|
getVectorTypeId(result.type),
|
||||||
|
bufferInfo.varId, 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
DxbcRegisterValue DxbcCompiler::emitQueryTexelBufferSize(
|
DxbcRegisterValue DxbcCompiler::emitQueryTexelBufferSize(
|
||||||
|
@ -920,6 +920,9 @@ namespace dxvk {
|
|||||||
|
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
// Resource query methods
|
// Resource query methods
|
||||||
|
DxbcRegisterValue emitQueryBufferSize(
|
||||||
|
const DxbcRegister& resource);
|
||||||
|
|
||||||
DxbcRegisterValue emitQueryTexelBufferSize(
|
DxbcRegisterValue emitQueryTexelBufferSize(
|
||||||
const DxbcRegister& resource);
|
const DxbcRegister& resource);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user