moved buffer stuff to a separate module
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
mod node_buffer;
|
||||||
|
pub use node_buffer::NodeBuffer;
|
||||||
|
|
||||||
|
mod uniform_buffer;
|
||||||
|
pub use uniform_buffer::UniformBuffer;
|
||||||
|
pub use uniform_buffer::UniformValues;
|
||||||
|
|
||||||
|
mod raster_buffer;
|
||||||
|
pub use raster_buffer::RasterBuffer;
|
||||||
|
pub use raster_buffer::Vertex;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
pub struct NodeBuffer {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeBuffer {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
use wgpu::util::DeviceExt;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct Vertex {
|
||||||
|
pub position: [f32; 3],
|
||||||
|
pub uv: [f32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vertex {
|
||||||
|
pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||||
|
wgpu::VertexBufferLayout {
|
||||||
|
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
|
||||||
|
step_mode: wgpu::InputStepMode::Vertex,
|
||||||
|
attributes: &[
|
||||||
|
// position
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: 0,
|
||||||
|
shader_location: 0,
|
||||||
|
format: wgpu::VertexFormat::Float3,
|
||||||
|
},
|
||||||
|
// color
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
|
||||||
|
shader_location: 1,
|
||||||
|
format: wgpu::VertexFormat::Float2,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const VERTICES: &[Vertex] = &[
|
||||||
|
Vertex { position: [-1.0, 1.0, 0.0], uv: [-1.0, 1.0] }, // top left
|
||||||
|
Vertex { position: [ 1.0, 1.0, 0.0], uv: [1.0, 1.0] }, // top right
|
||||||
|
Vertex { position: [ 1.0,-1.0, 0.0], uv: [1.0, -1.0] }, // bottom right
|
||||||
|
Vertex { position: [-1.0,-1.0, 0.0], uv: [-1.0, -1.0] }, // bottom left
|
||||||
|
];
|
||||||
|
pub const INDICES: &[u16] = &[ 0, 1, 3, 3, 1, 2 ];
|
||||||
|
|
||||||
|
|
||||||
|
pub struct RasterBuffer {
|
||||||
|
pub vertex_buffer: wgpu::Buffer,
|
||||||
|
pub index_buffer: wgpu::Buffer,
|
||||||
|
pub index_len: u32
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RasterBuffer {
|
||||||
|
pub fn new(device: &wgpu::Device) -> Self {
|
||||||
|
|
||||||
|
// Create vertex buffer
|
||||||
|
let vertex_buffer = device.create_buffer_init(
|
||||||
|
&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("Vertex buffer"),
|
||||||
|
contents: bytemuck::cast_slice(VERTICES),
|
||||||
|
usage: wgpu::BufferUsage::VERTEX,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create index buffer
|
||||||
|
let index_buffer = device.create_buffer_init(
|
||||||
|
&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("Index buffer"),
|
||||||
|
contents: bytemuck::cast_slice(INDICES),
|
||||||
|
usage: wgpu::BufferUsage::INDEX,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let index_len = INDICES.len() as u32;
|
||||||
|
|
||||||
|
// Done
|
||||||
|
Self { vertex_buffer, index_buffer, index_len }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
use crate::renderer::camera::Camera;
|
||||||
|
use cgmath::SquareMatrix;
|
||||||
|
use wgpu::util::DeviceExt;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Default, Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct UniformValues {
|
||||||
|
view_inv: [[f32; 4]; 4],
|
||||||
|
proj_inv: [[f32; 4]; 4],
|
||||||
|
cam_pos: [f32; 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniformValues {
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, camera: &Camera) {
|
||||||
|
self.view_inv = camera.view_matrix().invert().unwrap().into();
|
||||||
|
self.proj_inv = camera.proj_matrix().invert().unwrap().into();
|
||||||
|
self.cam_pos = camera.position.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub struct UniformBuffer {
|
||||||
|
pub values: UniformValues,
|
||||||
|
pub buffer: wgpu::Buffer,
|
||||||
|
pub bind_layout: wgpu::BindGroupLayout,
|
||||||
|
pub bind_group: wgpu::BindGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UniformBuffer {
|
||||||
|
pub fn new(device: &wgpu::Device) -> Self {
|
||||||
|
// Create values
|
||||||
|
let values = UniformValues::new();
|
||||||
|
|
||||||
|
// Create buffer
|
||||||
|
let buffer = device.create_buffer_init(
|
||||||
|
&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("Uniform buffer"),
|
||||||
|
contents: bytemuck::cast_slice(&[values]),
|
||||||
|
usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create bind group
|
||||||
|
let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStage::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
label: Some("Uniform buffer layout"),
|
||||||
|
});
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
layout: &bind_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: buffer.as_entire_binding(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
label: Some("Uniform buffer group"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Done
|
||||||
|
UniformBuffer { buffer, values, bind_layout, bind_group }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+14
-131
@@ -1,19 +1,8 @@
|
|||||||
use winit::{window::Window, dpi::PhysicalSize};
|
use winit::{window::Window, dpi::PhysicalSize};
|
||||||
use wgpu::util::DeviceExt;
|
use wgpu::util::DeviceExt;
|
||||||
|
|
||||||
mod vertex; use vertex::Vertex;
|
mod buffers; use buffers::{NodeBuffer, UniformBuffer, RasterBuffer, Vertex};
|
||||||
mod texture; use texture::Texture;
|
|
||||||
pub mod camera; use camera::Camera;
|
pub mod camera; use camera::Camera;
|
||||||
mod uniforms; use uniforms::Uniforms;
|
|
||||||
|
|
||||||
|
|
||||||
const VERTICES: &[Vertex] = &[
|
|
||||||
Vertex { position: [-1.0, 1.0, 0.0], uv: [-1.0, 1.0] }, // top left
|
|
||||||
Vertex { position: [ 1.0, 1.0, 0.0], uv: [1.0, 1.0] }, // top right
|
|
||||||
Vertex { position: [ 1.0,-1.0, 0.0], uv: [1.0, -1.0] }, // bottom right
|
|
||||||
Vertex { position: [-1.0,-1.0, 0.0], uv: [-1.0, -1.0] }, // bottom left
|
|
||||||
];
|
|
||||||
const INDICES: &[u16] = &[ 0, 1, 3, 3, 1, 2 ];
|
|
||||||
|
|
||||||
|
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
@@ -24,16 +13,10 @@ pub struct Renderer {
|
|||||||
swapchain: wgpu::SwapChain,
|
swapchain: wgpu::SwapChain,
|
||||||
pipeline: wgpu::RenderPipeline,
|
pipeline: wgpu::RenderPipeline,
|
||||||
// Buffers
|
// Buffers
|
||||||
vertex_buffer: wgpu::Buffer,
|
raster_buffer: RasterBuffer,
|
||||||
index_buffer: wgpu::Buffer,
|
uniform_buffer: UniformBuffer,
|
||||||
uniform_buffer: wgpu::Buffer,
|
|
||||||
// Uniform bind group
|
|
||||||
uniform_bind_group: wgpu::BindGroup,
|
|
||||||
// Texture
|
|
||||||
bind_group: wgpu::BindGroup,
|
|
||||||
// Camera
|
// Camera
|
||||||
pub camera: Camera,
|
pub camera: Camera,
|
||||||
uniforms: Uniforms
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
@@ -96,101 +79,11 @@ impl Renderer {
|
|||||||
multisample: multisample_state,
|
multisample: multisample_state,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_bind_group(device: &wgpu::Device, tex: &Texture) -> (wgpu::BindGroupLayout, wgpu::BindGroup) {
|
|
||||||
let bind_group_layout = device.create_bind_group_layout(
|
|
||||||
&wgpu::BindGroupLayoutDescriptor {
|
|
||||||
entries: &[
|
|
||||||
wgpu::BindGroupLayoutEntry {
|
|
||||||
binding: 0,
|
|
||||||
visibility: wgpu::ShaderStage::FRAGMENT,
|
|
||||||
ty: wgpu::BindingType::Texture {
|
|
||||||
multisampled: false,
|
|
||||||
view_dimension: wgpu::TextureViewDimension::D2,
|
|
||||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
wgpu::BindGroupLayoutEntry {
|
|
||||||
binding: 1,
|
|
||||||
visibility: wgpu::ShaderStage::FRAGMENT,
|
|
||||||
ty: wgpu::BindingType::Sampler {
|
|
||||||
comparison: false,
|
|
||||||
filtering: true,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
label: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let bind_group = device.create_bind_group(
|
|
||||||
&wgpu::BindGroupDescriptor {
|
|
||||||
layout: &bind_group_layout,
|
|
||||||
entries: &[
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: wgpu::BindingResource::TextureView(&tex.view),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: wgpu::BindingResource::Sampler(&tex.sampler),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
label: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
(bind_group_layout, bind_group)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_uniform_bind_group(device: &wgpu::Device, buf: &wgpu::Buffer) -> (wgpu::BindGroupLayout, wgpu::BindGroup) {
|
|
||||||
let uniform_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
||||||
entries: &[
|
|
||||||
wgpu::BindGroupLayoutEntry {
|
|
||||||
binding: 0,
|
|
||||||
visibility: wgpu::ShaderStage::FRAGMENT,
|
|
||||||
ty: wgpu::BindingType::Buffer {
|
|
||||||
ty: wgpu::BufferBindingType::Uniform,
|
|
||||||
has_dynamic_offset: false,
|
|
||||||
min_binding_size: None,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
label: Some("uniform_bind_group_layout"),
|
|
||||||
});
|
|
||||||
|
|
||||||
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
||||||
layout: &uniform_bind_group_layout,
|
|
||||||
entries: &[
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: buf.as_entire_binding(),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
label: Some("uniform_bind_group"),
|
|
||||||
});
|
|
||||||
|
|
||||||
(uniform_bind_group_layout, uniform_bind_group)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_buffer(device: &wgpu::Device, usage: wgpu::BufferUsage, content: &[u8]) -> wgpu::Buffer {
|
|
||||||
device.create_buffer_init(
|
|
||||||
&wgpu::util::BufferInitDescriptor {
|
|
||||||
label: None,
|
|
||||||
contents: content,
|
|
||||||
usage: usage,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub async fn new(window: &Window) -> Self {
|
pub async fn new(window: &Window) -> Self {
|
||||||
|
|
||||||
// Create surface and pick device
|
// Create surface and pick device
|
||||||
let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY);
|
let instance = wgpu::Instance::new(wgpu::BackendBit::DX11);
|
||||||
let surface = unsafe { instance.create_surface(window) };
|
let surface = unsafe { instance.create_surface(window) };
|
||||||
|
|
||||||
// Pick adapter (physical gpu)
|
// Pick adapter (physical gpu)
|
||||||
@@ -208,26 +101,17 @@ impl Renderer {
|
|||||||
// Cam
|
// Cam
|
||||||
let aspect = swapchain_desc.width as f32 / swapchain_desc.height as f32;
|
let aspect = swapchain_desc.width as f32 / swapchain_desc.height as f32;
|
||||||
let camera = Camera::new(aspect, 60.0);
|
let camera = Camera::new(aspect, 60.0);
|
||||||
let mut uniforms = Uniforms::new();
|
|
||||||
uniforms.update(&camera);
|
|
||||||
|
|
||||||
// Buffers
|
// Buffers
|
||||||
let vertex_buffer = Self::create_buffer(&device, wgpu::BufferUsage::VERTEX, bytemuck::cast_slice(VERTICES));
|
let uniform_buffer = UniformBuffer::new(&device);
|
||||||
let index_buffer = Self::create_buffer(&device, wgpu::BufferUsage::INDEX, bytemuck::cast_slice(INDICES));
|
let raster_buffer = RasterBuffer::new(&device);
|
||||||
let uniform_buffer = Self::create_buffer(&device, wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, bytemuck::cast_slice(&[uniforms]));
|
|
||||||
|
|
||||||
// Load image
|
|
||||||
let tex = Texture::from_file(&device, &queue, "./assets/rick.png");
|
|
||||||
let (bind_group_layout, bind_group) = Self::create_bind_group(&device, &tex);
|
|
||||||
let (uniform_bind_group_layout, uniform_bind_group) = Self::create_uniform_bind_group(&device, &uniform_buffer);
|
|
||||||
|
|
||||||
// Pipeline
|
// Pipeline
|
||||||
let bind_groups = [&bind_group_layout, &uniform_bind_group_layout];
|
let pipeline = Self::create_pipeline(&device, &swapchain_desc, &[&uniform_buffer.bind_layout]);
|
||||||
let pipeline = Self::create_pipeline(&device, &swapchain_desc, &bind_groups);
|
|
||||||
|
|
||||||
// Save values in app
|
// Save values in app
|
||||||
println!("Initialized");
|
println!("Initialized");
|
||||||
Self { surface, device, queue, swapchain_desc, swapchain, pipeline, vertex_buffer, index_buffer, uniform_buffer, bind_group, uniform_bind_group, camera, uniforms }
|
Self { surface, device, queue, swapchain_desc, swapchain, pipeline, raster_buffer, uniform_buffer, camera }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, new_size: Option<PhysicalSize<u32>>) {
|
pub fn resize(&mut self, new_size: Option<PhysicalSize<u32>>) {
|
||||||
@@ -247,8 +131,8 @@ impl Renderer {
|
|||||||
pub fn render(&mut self) -> Result<(), wgpu::SwapChainError> {
|
pub fn render(&mut self) -> Result<(), wgpu::SwapChainError> {
|
||||||
|
|
||||||
// Update uniform buffer
|
// Update uniform buffer
|
||||||
self.uniforms.update(&self.camera);
|
self.uniform_buffer.values.update(&self.camera);
|
||||||
self.queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[self.uniforms]));
|
self.queue.write_buffer(&self.uniform_buffer.buffer, 0, bytemuck::cast_slice(&[self.uniform_buffer.values]));
|
||||||
|
|
||||||
// Get next frame to render to
|
// Get next frame to render to
|
||||||
let frame = self.swapchain.get_current_frame()?.output;
|
let frame = self.swapchain.get_current_frame()?.output;
|
||||||
@@ -273,12 +157,11 @@ impl Renderer {
|
|||||||
|
|
||||||
// Data
|
// Data
|
||||||
render_pass.set_pipeline(&self.pipeline);
|
render_pass.set_pipeline(&self.pipeline);
|
||||||
render_pass.set_bind_group(0, &self.bind_group, &[]);
|
render_pass.set_bind_group(0, &self.uniform_buffer.bind_group, &[]);
|
||||||
render_pass.set_bind_group(1, &self.uniform_bind_group, &[]);
|
|
||||||
// Vertices and indices
|
// Vertices and indices
|
||||||
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
|
render_pass.set_vertex_buffer(0, self.raster_buffer.vertex_buffer.slice(..));
|
||||||
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
|
render_pass.set_index_buffer(self.raster_buffer.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
|
||||||
render_pass.draw_indexed(0..(INDICES.len() as u32), 0, 0..1);
|
render_pass.draw_indexed(0..self.raster_buffer.index_len, 0, 0..1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit encoder (command buffer)
|
// Submit encoder (command buffer)
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
use cgmath::SquareMatrix;
|
|
||||||
use super::camera::Camera;
|
|
||||||
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Default, Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
||||||
pub struct Uniforms {
|
|
||||||
view_inv: [[f32; 4]; 4],
|
|
||||||
proj_inv: [[f32; 4]; 4],
|
|
||||||
cam_pos: [f32; 3]
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Uniforms {
|
|
||||||
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&mut self, camera: &Camera) {
|
|
||||||
self.view_inv = camera.view_matrix().invert().unwrap().into();
|
|
||||||
self.proj_inv = camera.proj_matrix().invert().unwrap().into();
|
|
||||||
self.cam_pos = camera.position.into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
|
||||||
pub struct Vertex {
|
|
||||||
pub position: [f32; 3],
|
|
||||||
pub uv: [f32; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Vertex {
|
|
||||||
pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
|
|
||||||
wgpu::VertexBufferLayout {
|
|
||||||
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
|
|
||||||
step_mode: wgpu::InputStepMode::Vertex,
|
|
||||||
attributes: &[
|
|
||||||
// position
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
offset: 0,
|
|
||||||
shader_location: 0,
|
|
||||||
format: wgpu::VertexFormat::Float3,
|
|
||||||
},
|
|
||||||
// color
|
|
||||||
wgpu::VertexAttribute {
|
|
||||||
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
|
|
||||||
shader_location: 1,
|
|
||||||
format: wgpu::VertexFormat::Float2,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -4,10 +4,8 @@
|
|||||||
// Variables
|
// Variables
|
||||||
layout(location=0) in vec2 vert_uv;
|
layout(location=0) in vec2 vert_uv;
|
||||||
layout(location=0) out vec4 out_color;
|
layout(location=0) out vec4 out_color;
|
||||||
layout(set=0, binding=0) uniform texture2D tex;
|
|
||||||
layout(set=0, binding=1) uniform sampler tex_smp;
|
|
||||||
|
|
||||||
layout(set=1, binding=0) uniform Uniforms
|
layout(set=0, binding=0) uniform Uniforms
|
||||||
{
|
{
|
||||||
mat4 view_matrix;
|
mat4 view_matrix;
|
||||||
mat4 proj_matrix_inv;
|
mat4 proj_matrix_inv;
|
||||||
|
|||||||
Reference in New Issue
Block a user