mirror of
https://github.com/TomHarte/CLK.git
synced 2025-01-11 08:30:55 +00:00
Made an attempt to switch to a triple-buffering scheme for CRT outputs, with an eye towards asynchronicity.
This commit is contained in:
parent
b503e13380
commit
065050115f
@ -83,8 +83,12 @@ void Machine::output_state(OutputState state, uint8_t *pixel)
|
|||||||
case OutputState::Blank: {
|
case OutputState::Blank: {
|
||||||
_crt->allocate_write_area(1);
|
_crt->allocate_write_area(1);
|
||||||
_outputBuffer = _crt->get_write_target_for_buffer(0);
|
_outputBuffer = _crt->get_write_target_for_buffer(0);
|
||||||
|
|
||||||
|
if(_outputBuffer)
|
||||||
|
{
|
||||||
_outputBuffer[0] = _outputBuffer[1] = _outputBuffer[2] = 0;
|
_outputBuffer[0] = _outputBuffer[1] = _outputBuffer[2] = 0;
|
||||||
_outputBuffer[3] = 0xff;
|
_outputBuffer[3] = 0xff;
|
||||||
|
}
|
||||||
_crt->output_level(_lastOutputStateDuration, atari2600DataType);
|
_crt->output_level(_lastOutputStateDuration, atari2600DataType);
|
||||||
} break;
|
} break;
|
||||||
case OutputState::Sync: _crt->output_sync(_lastOutputStateDuration); break;
|
case OutputState::Sync: _crt->output_sync(_lastOutputStateDuration); break;
|
||||||
@ -100,7 +104,7 @@ void Machine::output_state(OutputState state, uint8_t *pixel)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(state == OutputState::Pixel)
|
if(state == OutputState::Pixel && _outputBuffer)
|
||||||
{
|
{
|
||||||
_outputBuffer[(_lastOutputStateDuration * 4) + 0] = pixel[0];
|
_outputBuffer[(_lastOutputStateDuration * 4) + 0] = pixel[0];
|
||||||
_outputBuffer[(_lastOutputStateDuration * 4) + 1] = pixel[1];
|
_outputBuffer[(_lastOutputStateDuration * 4) + 1] = pixel[1];
|
||||||
|
@ -10,34 +10,36 @@
|
|||||||
#import "Atari2600.hpp"
|
#import "Atari2600.hpp"
|
||||||
|
|
||||||
class Atari2600CRTDelegate: public Outputs::CRT::CRTDelegate {
|
class Atari2600CRTDelegate: public Outputs::CRT::CRTDelegate {
|
||||||
void crt_did_start_vertical_retrace_with_runs(Outputs::CRT::CRTRun *runs, int runs_to_draw)
|
void crt_did_end_frame(Outputs::CRT *crt, Outputs::CRTFrame *frame)
|
||||||
{
|
{
|
||||||
printf("===\n\n");
|
// printf("===\n\n");
|
||||||
for(int run = 0; run < runs_to_draw; run++)
|
// for(int run = 0; run < runs_to_draw; run++)
|
||||||
{
|
// {
|
||||||
char character = ' ';
|
// char character = ' ';
|
||||||
switch(runs[run].type)
|
// switch(runs[run].type)
|
||||||
{
|
// {
|
||||||
case Outputs::CRT::CRTRun::Type::Sync: character = '<'; break;
|
// case Outputs::CRTRun::Type::Sync: character = '<'; break;
|
||||||
case Outputs::CRT::CRTRun::Type::Level: character = '_'; break;
|
// case Outputs::CRTRun::Type::Level: character = '_'; break;
|
||||||
case Outputs::CRT::CRTRun::Type::Data: character = '-'; break;
|
// case Outputs::CRTRun::Type::Data: character = '-'; break;
|
||||||
case Outputs::CRT::CRTRun::Type::Blank: character = ' '; break;
|
// case Outputs::CRTRun::Type::Blank: character = ' '; break;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// if(runs[run].start_point.dst_x > runs[run].end_point.dst_x)
|
// if(runs[run].start_point.dst_x > runs[run].end_point.dst_x)
|
||||||
// {
|
// {
|
||||||
// printf("\n");
|
// printf("\n");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// float length = fabsf(runs[run].end_point.dst_x - runs[run].start_point.dst_x);
|
||||||
|
// int iLength = (int)(length * 64.0);
|
||||||
|
// for(int c = 0; c < iLength; c++)
|
||||||
|
// {
|
||||||
|
// putc(character, stdout);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (runs[run].type == Outputs::CRTRun::Type::Sync) printf("\n");
|
||||||
// }
|
// }
|
||||||
|
|
||||||
float length = fabsf(runs[run].end_point.dst_x - runs[run].start_point.dst_x);
|
crt->return_frame();
|
||||||
int iLength = (int)(length * 64.0);
|
|
||||||
for(int c = 0; c < iLength; c++)
|
|
||||||
{
|
|
||||||
putc(character, stdout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runs[run].type == Outputs::CRT::CRTRun::Type::Sync) printf("\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
150
Outputs/CRT.cpp
150
Outputs/CRT.cpp
@ -9,16 +9,14 @@
|
|||||||
#include "CRT.hpp"
|
#include "CRT.hpp"
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
|
||||||
static const int bufferWidth = 512;
|
|
||||||
static const int bufferHeight = 512;
|
|
||||||
|
|
||||||
using namespace Outputs;
|
using namespace Outputs;
|
||||||
|
|
||||||
CRT::CRT(int cycles_per_line, int height_of_display, int number_of_buffers, ...)
|
CRT::CRT(int cycles_per_line, int height_of_display, int number_of_buffers, ...)
|
||||||
{
|
{
|
||||||
static const int syncCapacityLineChargeThreshold = 3;
|
const int syncCapacityLineChargeThreshold = 3;
|
||||||
static const int millisecondsHorizontalRetraceTime = 16;
|
const int millisecondsHorizontalRetraceTime = 16;
|
||||||
static const int scanlinesVerticalRetraceTime = 26;
|
const int scanlinesVerticalRetraceTime = 26;
|
||||||
|
|
||||||
// store fundamental display configuration properties
|
// store fundamental display configuration properties
|
||||||
_height_of_display = height_of_display;
|
_height_of_display = height_of_display;
|
||||||
@ -38,24 +36,18 @@ CRT::CRT(int cycles_per_line, int height_of_display, int number_of_buffers, ...)
|
|||||||
|
|
||||||
// generate buffers for signal storage as requested — format is
|
// generate buffers for signal storage as requested — format is
|
||||||
// number of buffers, size of buffer 1, size of buffer 2...
|
// number of buffers, size of buffer 1, size of buffer 2...
|
||||||
_numberOfBuffers = number_of_buffers;
|
const int bufferWidth = 512;
|
||||||
_bufferSizes = new int[_numberOfBuffers];
|
const int bufferHeight = 512;
|
||||||
_buffers = new uint8_t *[_numberOfBuffers];
|
for(int frame = 0; frame < 3; frame++)
|
||||||
|
{
|
||||||
va_list va;
|
va_list va;
|
||||||
va_start(va, number_of_buffers);
|
va_start(va, number_of_buffers);
|
||||||
for(int c = 0; c < _numberOfBuffers; c++)
|
_frames[frame] = new CRTFrame(bufferWidth, bufferHeight, number_of_buffers, va);
|
||||||
{
|
|
||||||
_bufferSizes[c] = va_arg(va, int);
|
|
||||||
_buffers[c] = new uint8_t[bufferHeight * bufferWidth * _bufferSizes[c]];
|
|
||||||
}
|
|
||||||
va_end(va);
|
va_end(va);
|
||||||
|
}
|
||||||
// reset pointer into output buffers
|
_frames_with_delegate = 0;
|
||||||
_write_allocation_pointer = 0;
|
_frame_read_pointer = 0;
|
||||||
|
_current_frame = _frames[0];
|
||||||
// reset the run buffer pointer
|
|
||||||
_run_pointer = 0;
|
|
||||||
|
|
||||||
// reset raster position
|
// reset raster position
|
||||||
_rasterPosition.x = _rasterPosition.y = 0.0f;
|
_rasterPosition.x = _rasterPosition.y = 0.0f;
|
||||||
@ -75,12 +67,10 @@ CRT::CRT(int cycles_per_line, int height_of_display, int number_of_buffers, ...)
|
|||||||
|
|
||||||
CRT::~CRT()
|
CRT::~CRT()
|
||||||
{
|
{
|
||||||
delete[] _bufferSizes;
|
for(int frame = 0; frame < 3; frame++)
|
||||||
for(int c = 0; c < _numberOfBuffers; c++)
|
|
||||||
{
|
{
|
||||||
delete[] _buffers[c];
|
delete _frames[frame];
|
||||||
}
|
}
|
||||||
delete[] _buffers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Sync loop
|
#pragma mark - Sync loop
|
||||||
@ -154,14 +144,9 @@ void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, const bool
|
|||||||
SyncEvent next_event = advance_to_next_sync_event(hsync_requested, vsync_charging, number_of_cycles, &next_run_length);
|
SyncEvent next_event = advance_to_next_sync_event(hsync_requested, vsync_charging, number_of_cycles, &next_run_length);
|
||||||
hsync_requested = false;
|
hsync_requested = false;
|
||||||
|
|
||||||
// get a run from the allocated list, allocating more if we're about to overrun
|
if(_current_frame)
|
||||||
if(_run_pointer >= _all_runs.size())
|
|
||||||
{
|
{
|
||||||
_all_runs.resize((_all_runs.size() * 2)+1);
|
CRTRun *nextRun = _current_frame->get_next_run();
|
||||||
}
|
|
||||||
|
|
||||||
CRTRun *nextRun = &_all_runs[_run_pointer];
|
|
||||||
_run_pointer++;
|
|
||||||
|
|
||||||
// set the type, initial raster position and type of this run
|
// set the type, initial raster position and type of this run
|
||||||
nextRun->type = type;
|
nextRun->type = type;
|
||||||
@ -172,8 +157,8 @@ void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, const bool
|
|||||||
// if this is a data or level run then store a starting data position
|
// if this is a data or level run then store a starting data position
|
||||||
if(type == CRTRun::Type::Data || type == CRTRun::Type::Level)
|
if(type == CRTRun::Type::Data || type == CRTRun::Type::Level)
|
||||||
{
|
{
|
||||||
nextRun->start_point.src_x = (_write_target_pointer + buffer_offset) & (bufferWidth - 1);
|
nextRun->start_point.src_x = (_current_frame->_write_target_pointer + buffer_offset) & (_current_frame->size.width - 1);
|
||||||
nextRun->start_point.src_y = (_write_target_pointer + buffer_offset) / bufferWidth;
|
nextRun->start_point.src_y = (_current_frame->_write_target_pointer + buffer_offset) / _current_frame->size.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
// advance the raster position as dictated by current sync status
|
// advance the raster position as dictated by current sync status
|
||||||
@ -200,8 +185,9 @@ void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, const bool
|
|||||||
// if this is a data or level run then store the end point
|
// if this is a data or level run then store the end point
|
||||||
if(type == CRTRun::Type::Data || type == CRTRun::Type::Level)
|
if(type == CRTRun::Type::Data || type == CRTRun::Type::Level)
|
||||||
{
|
{
|
||||||
nextRun->end_point.src_x = (_write_target_pointer + buffer_offset) & (bufferWidth - 1);
|
nextRun->end_point.src_x = (_current_frame->_write_target_pointer + buffer_offset) & (_current_frame->size.width - 1);
|
||||||
nextRun->end_point.src_y = (_write_target_pointer + buffer_offset) / bufferWidth;
|
nextRun->end_point.src_y = (_current_frame->_write_target_pointer + buffer_offset) / _current_frame->size.width;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decrement the number of cycles left to run for and increment the
|
// decrement the number of cycles left to run for and increment the
|
||||||
@ -247,9 +233,19 @@ void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, const bool
|
|||||||
// end of vertical sync: tell the delegate that we finished vertical sync,
|
// end of vertical sync: tell the delegate that we finished vertical sync,
|
||||||
// releasing all runs back into the common pool
|
// releasing all runs back into the common pool
|
||||||
case SyncEvent::EndVSync:
|
case SyncEvent::EndVSync:
|
||||||
if(_delegate != nullptr)
|
if(_delegate && _current_frame)
|
||||||
_delegate->crt_did_start_vertical_retrace_with_runs(&_all_runs[0], _run_pointer);
|
{
|
||||||
_run_pointer = 0;
|
_frames_with_delegate++;
|
||||||
|
_delegate->crt_did_end_frame(this, _current_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_frames_with_delegate < kCRTNumberOfFrames)
|
||||||
|
{
|
||||||
|
_frame_read_pointer = (_frame_read_pointer + 1)%kCRTNumberOfFrames;
|
||||||
|
_current_frame = _frames[_frame_read_pointer];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_current_frame = nullptr;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default: break;
|
default: break;
|
||||||
@ -257,6 +253,11 @@ void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, const bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CRT::return_frame()
|
||||||
|
{
|
||||||
|
_frames_with_delegate--;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - delegate
|
#pragma mark - delegate
|
||||||
|
|
||||||
void CRT::set_delegate(CRTDelegate *delegate)
|
void CRT::set_delegate(CRTDelegate *delegate)
|
||||||
@ -298,18 +299,79 @@ void CRT::output_data(int number_of_cycles, const char *type)
|
|||||||
|
|
||||||
void CRT::allocate_write_area(int required_length)
|
void CRT::allocate_write_area(int required_length)
|
||||||
{
|
{
|
||||||
int xPos = _write_allocation_pointer & (bufferWidth - 1);
|
if(_current_frame) _current_frame->allocate_write_area(required_length);
|
||||||
if (xPos + required_length > bufferWidth)
|
}
|
||||||
|
|
||||||
|
uint8_t *CRT::get_write_target_for_buffer(int buffer)
|
||||||
{
|
{
|
||||||
_write_allocation_pointer &= ~(bufferWidth - 1);
|
if (!_current_frame) return nullptr;
|
||||||
_write_allocation_pointer = (_write_allocation_pointer + bufferWidth) & ((bufferHeight-1) * bufferWidth);
|
return _current_frame->get_write_target_for_buffer(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - CRTFrame
|
||||||
|
|
||||||
|
CRTFrame::CRTFrame(int width, int height, int number_of_buffers, va_list buffer_sizes)
|
||||||
|
{
|
||||||
|
size.width = width;
|
||||||
|
size.height = height;
|
||||||
|
this->number_of_buffers = number_of_buffers;
|
||||||
|
buffers = new CRTBuffer[number_of_buffers];
|
||||||
|
|
||||||
|
for(int buffer = 0; buffer < number_of_buffers; buffer++)
|
||||||
|
{
|
||||||
|
buffers[buffer].depth = va_arg(buffer_sizes, int);
|
||||||
|
buffers[buffer].data = new uint8_t[width * height * buffers[buffer].depth];
|
||||||
|
}
|
||||||
|
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
CRTFrame::~CRTFrame()
|
||||||
|
{
|
||||||
|
for(int buffer = 0; buffer < number_of_buffers; buffer++)
|
||||||
|
delete[] buffers[buffer].data;
|
||||||
|
delete buffers;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CRTFrame::reset()
|
||||||
|
{
|
||||||
|
number_of_runs = 0;
|
||||||
|
_write_allocation_pointer = _write_target_pointer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CRTFrame::complete()
|
||||||
|
{
|
||||||
|
runs = &_all_runs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
CRTRun *CRTFrame::get_next_run()
|
||||||
|
{
|
||||||
|
// get a run from the allocated list, allocating more if we're about to overrun
|
||||||
|
if(number_of_runs >= _all_runs.size())
|
||||||
|
{
|
||||||
|
_all_runs.resize((_all_runs.size() * 2)+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
CRTRun *nextRun = &_all_runs[number_of_runs];
|
||||||
|
number_of_runs++;
|
||||||
|
|
||||||
|
return nextRun;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CRTFrame::allocate_write_area(int required_length)
|
||||||
|
{
|
||||||
|
int xPos = _write_allocation_pointer & (size.width - 1);
|
||||||
|
if (xPos + required_length > size.width)
|
||||||
|
{
|
||||||
|
_write_allocation_pointer &= ~(size.width - 1);
|
||||||
|
_write_allocation_pointer = (_write_allocation_pointer + size.width) & ((size.height-1) * size.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
_write_target_pointer = _write_allocation_pointer;
|
_write_target_pointer = _write_allocation_pointer;
|
||||||
_write_allocation_pointer += required_length;
|
_write_allocation_pointer += required_length;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t *CRT::get_write_target_for_buffer(int buffer)
|
uint8_t *CRTFrame::get_write_target_for_buffer(int buffer)
|
||||||
{
|
{
|
||||||
return &_buffers[buffer][_write_target_pointer * _bufferSizes[buffer]];
|
return &buffers[buffer].data[_write_target_pointer * buffers[buffer].depth];
|
||||||
}
|
}
|
||||||
|
@ -10,20 +10,16 @@
|
|||||||
#define CRT_cpp
|
#define CRT_cpp
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <stdarg.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Outputs {
|
namespace Outputs {
|
||||||
|
|
||||||
class CRT {
|
struct CRTBuffer {
|
||||||
public:
|
uint8_t *data;
|
||||||
CRT(int cycles_per_line, int height_of_display, int number_of_buffers, ...);
|
int depth;
|
||||||
~CRT();
|
};
|
||||||
|
|
||||||
void output_sync(int number_of_cycles);
|
|
||||||
void output_blank(int number_of_cycles);
|
|
||||||
void output_level(int number_of_cycles, const char *type);
|
|
||||||
void output_data(int number_of_cycles, const char *type);
|
|
||||||
|
|
||||||
struct CRTRun {
|
struct CRTRun {
|
||||||
struct Point {
|
struct Point {
|
||||||
@ -38,11 +34,56 @@ class CRT {
|
|||||||
const char *data_type;
|
const char *data_type;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class CRT;
|
||||||
|
struct CRTFrame {
|
||||||
|
struct {
|
||||||
|
int width, height;
|
||||||
|
} size;
|
||||||
|
|
||||||
|
int number_of_buffers;
|
||||||
|
CRTBuffer *buffers;
|
||||||
|
|
||||||
|
int number_of_runs;
|
||||||
|
CRTRun *runs;
|
||||||
|
|
||||||
|
CRTFrame(int width, int height, int number_of_buffers, va_list buffer_sizes);
|
||||||
|
~CRTFrame();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<CRTRun> _all_runs;
|
||||||
|
|
||||||
|
void reset();
|
||||||
|
void complete();
|
||||||
|
|
||||||
|
CRTRun *get_next_run();
|
||||||
|
friend CRT;
|
||||||
|
|
||||||
|
void allocate_write_area(int required_length);
|
||||||
|
uint8_t *get_write_target_for_buffer(int buffer);
|
||||||
|
|
||||||
|
// a pointer to the section of content buffer currently being
|
||||||
|
// returned and to where the next section will begin
|
||||||
|
int _write_allocation_pointer, _write_target_pointer;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const int kCRTNumberOfFrames = 3;
|
||||||
|
|
||||||
|
class CRT {
|
||||||
|
public:
|
||||||
|
CRT(int cycles_per_line, int height_of_display, int number_of_buffers, ...);
|
||||||
|
~CRT();
|
||||||
|
|
||||||
|
void output_sync(int number_of_cycles);
|
||||||
|
void output_blank(int number_of_cycles);
|
||||||
|
void output_level(int number_of_cycles, const char *type);
|
||||||
|
void output_data(int number_of_cycles, const char *type);
|
||||||
|
|
||||||
class CRTDelegate {
|
class CRTDelegate {
|
||||||
public:
|
public:
|
||||||
virtual void crt_did_start_vertical_retrace_with_runs(CRTRun *runs, int runs_to_draw) = 0;
|
virtual void crt_did_end_frame(CRT *crt, CRTFrame *frame) = 0;
|
||||||
};
|
};
|
||||||
void set_delegate(CRTDelegate *delegate);
|
void set_delegate(CRTDelegate *delegate);
|
||||||
|
void return_frame();
|
||||||
|
|
||||||
void allocate_write_area(int required_length);
|
void allocate_write_area(int required_length);
|
||||||
uint8_t *get_write_target_for_buffer(int buffer);
|
uint8_t *get_write_target_for_buffer(int buffer);
|
||||||
@ -55,24 +96,17 @@ class CRT {
|
|||||||
// properties directly derived from there
|
// properties directly derived from there
|
||||||
int _hsync_error_window; // the permitted window around the expected sync position in which a sync pulse will be recognised; calculated once at init
|
int _hsync_error_window; // the permitted window around the expected sync position in which a sync pulse will be recognised; calculated once at init
|
||||||
|
|
||||||
// the run delegate, buffer and buffer pointer
|
|
||||||
CRTDelegate *_delegate;
|
|
||||||
std::vector<CRTRun> _all_runs;
|
|
||||||
int _run_pointer;
|
|
||||||
|
|
||||||
// the current scanning position
|
// the current scanning position
|
||||||
struct Vector {
|
struct Vector {
|
||||||
float x, y;
|
float x, y;
|
||||||
} _rasterPosition, _scanSpeed, _retraceSpeed;
|
} _rasterPosition, _scanSpeed, _retraceSpeed;
|
||||||
|
|
||||||
// the content buffers
|
// the run delegate and the triple buffer
|
||||||
uint8_t **_buffers;
|
CRTFrame *_frames[kCRTNumberOfFrames];
|
||||||
int *_bufferSizes;
|
CRTFrame *_current_frame;
|
||||||
int _numberOfBuffers;
|
int _frames_with_delegate;
|
||||||
|
int _frame_read_pointer;
|
||||||
// a pointer to the section of content buffer currently being
|
CRTDelegate *_delegate;
|
||||||
// returned and to where the next section will begin
|
|
||||||
int _write_allocation_pointer, _write_target_pointer;
|
|
||||||
|
|
||||||
// outer elements of sync separation
|
// outer elements of sync separation
|
||||||
bool _is_receiving_sync; // true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync)
|
bool _is_receiving_sync; // true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user