Windows build ready. Program icon, however, exceeds string length on MSVC.
This commit is contained in:
+140
-96
@@ -1,31 +1,47 @@
|
||||
/* NOTE: This layer only supports MacOS and Linux, for now. */
|
||||
|
||||
#include "core.h"
|
||||
|
||||
// Remove SDL's entry point.
|
||||
#if OS_WINDOWS
|
||||
#define SDL_MAIN_HANDLED
|
||||
#endif
|
||||
|
||||
#include "handmade.h"
|
||||
#include "sdl_handmade.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include <sys/mman.h> /* mmap */
|
||||
|
||||
#include <stdlib.h> /* malloc, calloc */
|
||||
#include <string.h> /* memset, memcpy */
|
||||
#include <stdio.h>
|
||||
|
||||
#include <sys/stat.h> /* fstat */
|
||||
#include <fcntl.h> /* open */
|
||||
#include <unistd.h> /* close, write, read, readlink, unlink */
|
||||
#include <dlfcn.h> /* dlopen */
|
||||
#include <sys/stat.h> /* stat */
|
||||
|
||||
#if OS_MACOS || OS_LINUX
|
||||
#include <dlfcn.h> /* dlopen */
|
||||
#include <sys/mman.h> /* mmap */
|
||||
#include <unistd.h> /* close, write, read, readlink, unlink */
|
||||
#endif
|
||||
|
||||
#if OS_WINDOWS
|
||||
#include <windows.h> /* VirtualAlloc */
|
||||
#endif
|
||||
|
||||
#if OS_MACOS
|
||||
#include <mach-o/dyld.h> /* _NSGetExecutablePath */
|
||||
#endif
|
||||
|
||||
/* NOTE: This is so it compiles on ARM. */
|
||||
#if ARCHITECTURE_X64 || ARCHITECTURE_X86
|
||||
#include <x86intrin.h>
|
||||
#if OS_MACOS || OS_LINUX
|
||||
#include <x86intrin.h> // TODO: What is the equivalent on Windows?
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
#include "program_icon.c"
|
||||
#endif
|
||||
|
||||
#define RESOLUTION_WIDTH 960
|
||||
#define RESOLUTION_HEIGHT 540
|
||||
@@ -61,77 +77,65 @@ static U32 SafeTruncateUInt64(U64 Value)
|
||||
|
||||
debug_read_file_result DEBUGPlatformReadEntireFile(thread_context *Thread, const char *Filename)
|
||||
{
|
||||
(void)Thread;
|
||||
|
||||
debug_read_file_result Result;
|
||||
memset(&Result, 0, sizeof(Result));
|
||||
|
||||
S32 FileHandle = open(Filename, O_RDONLY);
|
||||
if(FileHandle == -1) {
|
||||
return Result;
|
||||
}
|
||||
SDL_RWops *FileHandle = SDL_RWFromFile(Filename, "r");
|
||||
if(FileHandle) {
|
||||
S64 FileSize = SDL_RWsize(FileHandle);
|
||||
if(FileSize >= 0) {
|
||||
Result.ContentsSize = SafeTruncateUInt64(FileSize);
|
||||
} else {
|
||||
Result.ContentsSize = 0;
|
||||
SDL_RWclose(FileHandle);
|
||||
return Result;
|
||||
}
|
||||
|
||||
struct stat FileStatus;
|
||||
if(fstat(FileHandle, &FileStatus) == -1) {
|
||||
close(FileHandle);
|
||||
return Result;
|
||||
}
|
||||
Result.ContentsSize = SafeTruncateUInt64(FileStatus.st_size);
|
||||
Result.Contents = malloc(Result.ContentsSize);
|
||||
if(!Result.Contents) {
|
||||
Result.ContentsSize = 0;
|
||||
SDL_RWclose(FileHandle);
|
||||
return Result;
|
||||
}
|
||||
|
||||
Result.Contents = malloc(Result.ContentsSize);
|
||||
if(!Result.Contents) {
|
||||
Result.ContentsSize = 0;
|
||||
close(FileHandle);
|
||||
return Result;
|
||||
}
|
||||
|
||||
U32 BytesToRead = Result.ContentsSize;
|
||||
U8 *NextByteLocation = (U8 *)Result.Contents;
|
||||
while(BytesToRead) {
|
||||
U32 BytesRead = read(FileHandle, NextByteLocation, BytesToRead);
|
||||
if(BytesRead == (U32)-1) {
|
||||
size_t ObjectsRead = SDL_RWread(FileHandle, (void *)Result.Contents, (size_t)Result.ContentsSize, 1);
|
||||
if(ObjectsRead == 0) {
|
||||
free(Result.Contents);
|
||||
Result.Contents = 0;
|
||||
Result.ContentsSize = 0;
|
||||
close(FileHandle);
|
||||
SDL_RWclose(FileHandle);
|
||||
return Result;
|
||||
}
|
||||
BytesToRead -= BytesRead;
|
||||
NextByteLocation += BytesRead;
|
||||
}
|
||||
|
||||
close(FileHandle);
|
||||
SDL_RWclose(FileHandle);
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
B32 DEBUGPlatformWriteEntireFile(thread_context *Thread, const char *Filename, U32 MemorySize, void *Memory)
|
||||
{
|
||||
S32 FileHandle = open(Filename, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||
if(FileHandle == -1)
|
||||
return FALSE;
|
||||
|
||||
U32 BytesToWrite = MemorySize;
|
||||
U8 *NextByteLocation = (U8*)Memory;
|
||||
while(BytesToWrite) {
|
||||
U32 BytesWritten;
|
||||
|
||||
BytesWritten = write(FileHandle, NextByteLocation, BytesToWrite);
|
||||
if(BytesWritten == (U32)-1) {
|
||||
close(FileHandle);
|
||||
SDL_RWops *FileHandle = SDL_RWFromFile(Filename, "w");
|
||||
if(FileHandle) {
|
||||
size_t ObjectsWritten = SDL_RWwrite(FileHandle, (void *)Memory, (size_t)MemorySize, 1);
|
||||
if(ObjectsWritten == 0) {
|
||||
SDL_RWclose(FileHandle);
|
||||
return FALSE;
|
||||
}
|
||||
BytesToWrite -= BytesWritten;
|
||||
NextByteLocation += BytesWritten;
|
||||
|
||||
SDL_RWclose(FileHandle);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
close(FileHandle);
|
||||
|
||||
return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void DEBUGPlatformFreeFileMemory(thread_context *Thread, void *Memory)
|
||||
{
|
||||
if(Memory)
|
||||
free(Memory);
|
||||
free(Memory); // TODO: Should we accept debug_read_file_result instead of just memory.
|
||||
}
|
||||
|
||||
static sdl_window_size SDLGetWindowSize(SDL_Window *Window)
|
||||
@@ -187,8 +191,8 @@ static void SDLDisplayBufferInWindow(offscreen_buffer *Buffer, SDL_Window *Windo
|
||||
|
||||
/* TODO: We temporarily introduce the target rectangle to avoid stretching the canvas. */
|
||||
/* SDL_RenderCopy(Renderer, Buffer.Texture, 0, 0); */
|
||||
sdl_window_size WindowSize = SDLGetWindowSize(Window);
|
||||
if(WindowSize.Width >= Buffer->Width*2 && WindowSize.Height >= Buffer->Height*2) {
|
||||
sdl_window_size WinSize = SDLGetWindowSize(Window);
|
||||
if(WinSize.Width >= Buffer->Width*2 && WinSize.Height >= Buffer->Height*2) {
|
||||
SDL_Rect dest_rect = { OffsetX, OffsetY, Buffer->Width*2, Buffer->Height*2 };
|
||||
SDL_RenderCopy(Renderer, Buffer->Texture, 0, (const SDL_Rect *)(&dest_rect));
|
||||
} else {
|
||||
@@ -230,7 +234,7 @@ static void SDLInitSound(S32 SamplesPerSecond, S32 BufferSize)
|
||||
AudioSettings.freq = SamplesPerSecond;
|
||||
AudioSettings.format = AUDIO_S16LSB;
|
||||
AudioSettings.channels = 2;
|
||||
AudioSettings.samples = BufferSize;
|
||||
AudioSettings.samples = (U16)BufferSize; // TODO: Unsafe truncate from S32 to U16
|
||||
|
||||
SDL_OpenAudio(&AudioSettings, 0);
|
||||
|
||||
@@ -381,10 +385,8 @@ static void SDLProcessMessages(sdl_state *SDLState, game_controller_input *Keybo
|
||||
}
|
||||
|
||||
if(WasDown) {
|
||||
/* NOTE: If your window manager already uses an Alt+F4
|
||||
* keybind to close programs, then we are likely
|
||||
* to see SDL_QUIT event occur before our
|
||||
* keybind. */
|
||||
/* NOTE: If your window manager already uses an Alt+F4 keybind to close programs, then we are likely
|
||||
* to see SDL_QUIT event occur before our keybind. */
|
||||
B32 AltKeyWasDown = (Event.key.keysym.mod & KMOD_ALT);
|
||||
|
||||
if(KeyCode == SDLK_F4 && AltKeyWasDown)
|
||||
@@ -463,18 +465,38 @@ static U32 GetLastWriteTime(const char *FileName)
|
||||
return (U32)file_info.st_mtime;
|
||||
}
|
||||
|
||||
#if OS_MACOS || OS_LINUX
|
||||
#define GAME_DLL_NAME "libhandmade.so"
|
||||
#elif OS_WINDOWS
|
||||
#define GAME_DLL_NAME "handmade.dll"
|
||||
#else
|
||||
#error Unknown OS: please specify GAME_DLL_NAME
|
||||
#endif
|
||||
|
||||
static sdl_game_code SDLLoadGameCode(char *DLLPath)
|
||||
{
|
||||
sdl_game_code Result;
|
||||
memset(&Result, 0, sizeof(Result));
|
||||
|
||||
#if OS_MACOS || OS_LINUX
|
||||
Result.GameCodeDLL = dlopen(DLLPath, RTLD_NOW);
|
||||
#elif OS_WINDOWS
|
||||
Result.GameCodeDLL = LoadLibraryA(DLLPath);
|
||||
#else
|
||||
#error Unknown OS: implement DLL loading...
|
||||
#endif
|
||||
|
||||
if(Result.GameCodeDLL) {
|
||||
Result.DLLLastWriteTime = GetLastWriteTime(DLLPath);
|
||||
|
||||
Result.UpdateAndRender = (void(*)(thread_context *, game_memory *, game_input *, game_offscreen_buffer *, game_sound_output_buffer *)) dlsym(Result.GameCodeDLL, "UpdateAndRender");
|
||||
#if OS_MACOS || OS_LINUX
|
||||
Result.UpdateAndRender = (void(*)(thread_context *, game_memory *, game_input *, game_offscreen_buffer *, game_sound_output_buffer *))dlsym(Result.GameCodeDLL, "UpdateAndRender");
|
||||
#elif OS_WINDOWS
|
||||
Result.UpdateAndRender = (void(*)(thread_context *, game_memory *, game_input *, game_offscreen_buffer *, game_sound_output_buffer *))GetProcAddress(Result.GameCodeDLL, "UpdateAndRender");
|
||||
#else
|
||||
#error Unknown OS: implement DLL loading...
|
||||
#endif
|
||||
|
||||
if(!Result.UpdateAndRender) {
|
||||
/* TODO: Diagnostic. dlerror() */
|
||||
}
|
||||
@@ -493,8 +515,15 @@ static sdl_game_code SDLLoadGameCode(char *DLLPath)
|
||||
|
||||
static void SDLUnloadGameCode(sdl_game_code *GameCode)
|
||||
{
|
||||
if(GameCode->GameCodeDLL)
|
||||
if(GameCode->GameCodeDLL) {
|
||||
#if OS_MACOS || OS_LINUX
|
||||
dlclose(GameCode->GameCodeDLL); /* NOTE: Might fail. */
|
||||
#elif OS_WINDOWS
|
||||
FreeLibrary(GameCode->GameCodeDLL);
|
||||
#else
|
||||
#error Unknown OS: implement closing of DLL
|
||||
#endif
|
||||
}
|
||||
GameCode->IsValid = FALSE;
|
||||
GameCode->UpdateAndRender = NULL;
|
||||
}
|
||||
@@ -503,7 +532,7 @@ void GetExecutablePath(char *path, size_t path_size)
|
||||
{
|
||||
#if OS_MACOS
|
||||
U32 size;
|
||||
#else
|
||||
#elif OS_LINUX
|
||||
ssize_t size;
|
||||
#endif
|
||||
|
||||
@@ -512,18 +541,20 @@ void GetExecutablePath(char *path, size_t path_size)
|
||||
|
||||
#if OS_MACOS
|
||||
size = path_size;
|
||||
if(_NSGetExecutablePath(path, &size) != 0) { /* NOTE: Not an
|
||||
absolute
|
||||
path. */
|
||||
if(_NSGetExecutablePath(path, &size) != 0) { /* NOTE: Not an absolute path. */
|
||||
/* TODO: Diagnostics. */
|
||||
}
|
||||
#else
|
||||
#elif OS_LINUX
|
||||
size = readlink("/proc/self/exe", path, path_size - 1);
|
||||
if(size != -1) {
|
||||
path[size] = '\0';
|
||||
} else {
|
||||
/* TODO: Diagnostics. */
|
||||
}
|
||||
#elif OS_WINDOWS
|
||||
GetModuleFileName(NULL, path, (DWORD)path_size); // TODO: hm...
|
||||
#else
|
||||
#error Unknown OS: Implement getting executable location.
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -533,7 +564,16 @@ static void SDLGetEXEDirPath(char *path, size_t path_size)
|
||||
GetExecutablePath(path, path_size);
|
||||
U32 len = str_len(path);
|
||||
U32 i = len;
|
||||
while(path[i] != '/' || i == 0) {
|
||||
|
||||
#if OS_MACOS || OS_LINUX
|
||||
char delimeter = '/';
|
||||
#elif OS_WINDOWS
|
||||
char delimeter = '\\';
|
||||
#else
|
||||
#error Unknown OS: specify whether your OS uses forward or backward slashes for paths
|
||||
#endif
|
||||
|
||||
while(path[i] != delimeter || i == 0) {
|
||||
i--;
|
||||
}
|
||||
path[i+1] = '\0';
|
||||
@@ -557,6 +597,7 @@ U64 __rdtsc()
|
||||
* drawing into a bitmap. */
|
||||
void SDLSetProgramIcon(SDL_Window *Window)
|
||||
{
|
||||
#if 0
|
||||
U32 Rmask, Gmask, Bmask, Amask;
|
||||
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
|
||||
S32 shift_by = (program_icon.bytes_per_pixel == 3) ? 8 : 0;
|
||||
@@ -571,19 +612,15 @@ void SDLSetProgramIcon(SDL_Window *Window)
|
||||
Amask = (program_icon.bytes_per_pixel == 3) ? 0 : 0xff000000;
|
||||
#endif
|
||||
|
||||
SDL_Surface * icon = SDL_CreateRGBSurfaceFrom(
|
||||
(void *)program_icon.pixel_data,
|
||||
program_icon.width, program_icon.height,
|
||||
program_icon.bytes_per_pixel * 8,
|
||||
program_icon.bytes_per_pixel * program_icon.width,
|
||||
Rmask, Gmask, Bmask, Amask);
|
||||
SDL_Surface * icon = SDL_CreateRGBSurfaceFrom((void *)program_icon.pixel_data, program_icon.width, program_icon.height,
|
||||
program_icon.bytes_per_pixel * 8, program_icon.bytes_per_pixel * program_icon.width, Rmask, Gmask, Bmask, Amask);
|
||||
if(icon) {
|
||||
SDL_SetWindowIcon(Window, icon);
|
||||
SDL_FreeSurface(icon);
|
||||
} else {
|
||||
/* TODO: This didn't work . . . SDL_GetError() */
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
#include "handmade_intrinsics.h"
|
||||
@@ -595,7 +632,7 @@ S32 main(S32 argc, char *argv[])
|
||||
SDLGetEXEDirPath(SDLState.EXEDirPath, sizeof(SDLState.EXEDirPath));
|
||||
SDLGetDLLPath(SDLState.DLLPath, sizeof(SDLState.DLLPath), SDLState.EXEDirPath);
|
||||
|
||||
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO)) {
|
||||
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER)) {
|
||||
/* TODO: This didn't work . . . */
|
||||
}
|
||||
|
||||
@@ -616,9 +653,7 @@ S32 main(S32 argc, char *argv[])
|
||||
|
||||
SDL_Renderer *Renderer = SDL_CreateRenderer(Window, -1, SDL_RENDERER_PRESENTVSYNC);
|
||||
|
||||
/* NOTE: For future reference, the custom window bar should use
|
||||
* SDL_SetWindowHitTest to define areas that will be used to
|
||||
* drag the window. */
|
||||
/* NOTE: For future reference, the custom window bar should use SDL_SetWindowHitTest to define areas that will be used to drag the window. */
|
||||
|
||||
/* "Wayland needs an event loop and rendering or it won't function."
|
||||
* https://github.com/libsdl-org/SDL/issues/7699#issuecomment-1545684792 */
|
||||
@@ -653,7 +688,7 @@ S32 main(S32 argc, char *argv[])
|
||||
SoundOutput.LatencySampleCount = SoundOutput.SamplesPerSecond / 15;
|
||||
|
||||
SDLInitSound(SoundOutput.SamplesPerSecond, SoundOutput.SamplesPerSecond * SoundOutput.BytesPerSample / 60);
|
||||
SDL_PauseAudio(0);
|
||||
SDL_PauseAudio(0); // TODO: This should be paused until we have some actual sound to play.
|
||||
|
||||
SoundOutput.Samples = calloc(SoundOutput.SamplesPerSecond, SoundOutput.BytesPerSample);
|
||||
/* SoundOutput.Samples =
|
||||
@@ -672,13 +707,18 @@ S32 main(S32 argc, char *argv[])
|
||||
GameMemory.PermanentStorageSize = MB(64);
|
||||
GameMemory.TransientStorageSize = GB(1);
|
||||
GameMemory.DEBUGPlatformReadEntireFile = &DEBUGPlatformReadEntireFile;
|
||||
GameMemory.DEBUGPlatformWriteEntireFile = &DEBUGPlatformWriteEntireFile;
|
||||
//GameMemory.DEBUGPlatformWriteEntireFile = &DEBUGPlatformWriteEntireFile;
|
||||
GameMemory.DEBUGPlatformFreeFileMemory = &DEBUGPlatformFreeFileMemory;
|
||||
|
||||
SDLState.TotalSize = GameMemory.PermanentStorageSize + GameMemory.TransientStorageSize;
|
||||
/* NOTE: On MacOS and Linux, mmap seems to zero-fill anonymous
|
||||
* memory as a side-effect of security. */
|
||||
#if OS_MACOS || OS_LINUX
|
||||
/* NOTE: On MacOS and Linux, mmap seems to zero-fill anonymous memory as a side-effect of security. */
|
||||
SDLState.GameMmemoryBlock = mmap(BaseAddress, (size_t)SDLState.TotalSize, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
|
||||
#elif OS_WINDOWS
|
||||
SDLState.GameMmemoryBlock = VirtualAlloc(BaseAddress, SDLState.TotalSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
||||
#else
|
||||
#error Unknown OS: please, allocate memory for arena
|
||||
#endif
|
||||
GameMemory.PermanentStorage = SDLState.GameMmemoryBlock;
|
||||
GameMemory.TransientStorage = (U8 *)(GameMemory.PermanentStorage) + GameMemory.PermanentStorageSize;
|
||||
|
||||
@@ -701,18 +741,17 @@ S32 main(S32 argc, char *argv[])
|
||||
|
||||
sdl_game_code Game = SDLLoadGameCode(SDLState.DLLPath);
|
||||
|
||||
SDL_ShowWindow(Window); // NOTE: Everything is ready; display the window.
|
||||
SDL_ShowWindow(Window); // Everything is ready; display the window.
|
||||
|
||||
U64 FPSLastCounter = SDLGetWallClock();
|
||||
|
||||
while(GlobalRunning) {
|
||||
NewInput->dtForFrame = TargetSecondsPerFrame;
|
||||
|
||||
LastCounter = SDLGetWallClock();
|
||||
|
||||
/* NOTE: rdtsc reports clock cylces, however it is not
|
||||
* meant for really precise profiler work as the
|
||||
* value returned is varied. */
|
||||
/* Also, __rdtsc is not available on MacOS ARM; I
|
||||
* have not checked MacOS x64. */
|
||||
/* NOTE: rdtsc reports clock cylces, however it is not meant for really precise profiler work as the
|
||||
* value returned is varied. Also, __rdtsc is not available on MacOS ARM; I have not checked MacOS x64. */
|
||||
LastCycleCount = __rdtsc();
|
||||
|
||||
U32 NewDLLWriteTime = GetLastWriteTime(SDLState.DLLPath);
|
||||
@@ -842,7 +881,8 @@ S32 main(S32 argc, char *argv[])
|
||||
if(SecondsElapsedForFrame < TargetSecondsPerFrame) {
|
||||
while(SecondsElapsedForFrame < TargetSecondsPerFrame) {
|
||||
SecondsElapsedForFrame = SDLGetSecondsElapsed(LastCounter, SDLGetWallClock());
|
||||
SDL_Delay((U32)((TargetSecondsPerFrame - SecondsElapsedForFrame) * 1000.0));
|
||||
if((TargetSecondsPerFrame - SecondsElapsedForFrame) >= 0.0f)
|
||||
SDL_Delay((U32)((TargetSecondsPerFrame - SecondsElapsedForFrame) * 1000.0));
|
||||
}
|
||||
} else {
|
||||
/* TODO: Missed frame rate! */
|
||||
@@ -859,17 +899,21 @@ S32 main(S32 argc, char *argv[])
|
||||
LastCounter = EndCounter;
|
||||
|
||||
#if 1
|
||||
U64 EndCycleCount = __rdtsc();
|
||||
U64 CyclesElapsed = EndCycleCount - LastCycleCount;
|
||||
F64 MCPF = ((F64)CyclesElapsed / (1000.0 * 1000.0));
|
||||
if(SDLGetSecondsElapsed(FPSLastCounter, SDLGetWallClock()) > 0.1f) {
|
||||
U64 EndCycleCount = __rdtsc();
|
||||
U64 CyclesElapsed = EndCycleCount - LastCycleCount;
|
||||
F64 MCPF = ((F64)CyclesElapsed / (1000.0 * 1000.0));
|
||||
|
||||
//fprintf(stderr, "%.02f ms/f, %.02f f/s, %.02f mc/f\n", MSPerFrame, FPS, MCPF);
|
||||
//fprintf(stderr, "%.02f ms/f, %.02f f/s, %.02f mc/f\n", MSPerFrame, FPS, MCPF);
|
||||
|
||||
static char fps_buffer[128];
|
||||
snprintf(fps_buffer, sizeof(fps_buffer), "%.02f ms/f, %.02f f/s, %.02f mc/f", MSPerFrame, FPS, MCPF);
|
||||
SDL_SetWindowTitle(Window, (const char *)fps_buffer);
|
||||
static char fps_buffer[128];
|
||||
snprintf(fps_buffer, sizeof(fps_buffer), "%.02f ms/f, %.02f f/s, %.02f mc/f", MSPerFrame, FPS, MCPF);
|
||||
SDL_SetWindowTitle(Window, (const char *)fps_buffer);
|
||||
|
||||
LastCycleCount = EndCycleCount;
|
||||
LastCycleCount = EndCycleCount;
|
||||
|
||||
FPSLastCounter = SDLGetWallClock();
|
||||
}
|
||||
#endif
|
||||
|
||||
SWAP(game_input *, OldInput, NewInput);
|
||||
|
||||
Reference in New Issue
Block a user