476 lines
17 KiB
Text
476 lines
17 KiB
Text
|
/**
|
||
|
* PANDA 3D SOFTWARE
|
||
|
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
||
|
*
|
||
|
* All use of this software is subject to the terms of the revised BSD
|
||
|
* license. You should have received a copy of this license along
|
||
|
* with this source code in a file named "LICENSE."
|
||
|
*
|
||
|
* @file pipelineCyclerTrueImpl.I
|
||
|
* @author drose
|
||
|
* @date 2006-01-31
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Grabs an overall lock on the cycler. Release it with a call to release().
|
||
|
* This lock should be held while walking the list of stages.
|
||
|
*/
|
||
|
INLINE void PipelineCyclerTrueImpl::
|
||
|
acquire() {
|
||
|
TAU_PROFILE("void PipelineCyclerTrueImpl::acquire()", " ", TAU_USER);
|
||
|
_lock.acquire();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Grabs an overall lock on the cycler. Release it with a call to release().
|
||
|
* This lock should be held while walking the list of stages.
|
||
|
*/
|
||
|
INLINE void PipelineCyclerTrueImpl::
|
||
|
acquire(Thread *current_thread) {
|
||
|
TAU_PROFILE("void PipelineCyclerTrueImpl::acquire(Thread *)", " ", TAU_USER);
|
||
|
_lock.acquire(current_thread);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Release the overall lock on the cycler that was grabbed via acquire().
|
||
|
*/
|
||
|
INLINE void PipelineCyclerTrueImpl::
|
||
|
release() {
|
||
|
TAU_PROFILE("void PipelineCyclerTrueImpl::release()", " ", TAU_USER);
|
||
|
_lock.release();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a const CycleData pointer, filled with the data for the current
|
||
|
* stage of the pipeline as seen by this thread. No lock is made on the
|
||
|
* contents; there is no guarantee that some other thread won't modify this
|
||
|
* object's data while you are working on it. (However, the data within the
|
||
|
* returned CycleData object itself is safe from modification; if another
|
||
|
* thread modifies the data, it will perform a copy-on-write, and thereby
|
||
|
* change the pointer stored within the object.)
|
||
|
*/
|
||
|
INLINE const CycleData *PipelineCyclerTrueImpl::
|
||
|
read_unlocked(Thread *current_thread) const {
|
||
|
TAU_PROFILE("const CycleData *PipelineCyclerTrueImpl::read_unlocked(Thread *)", " ", TAU_USER);
|
||
|
int pipeline_stage = current_thread->get_pipeline_stage();
|
||
|
#ifdef _DEBUG
|
||
|
nassertr(pipeline_stage >= 0 && pipeline_stage < _num_stages, nullptr);
|
||
|
#endif
|
||
|
return _data[pipeline_stage]._cdata;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a const CycleData pointer, filled with the data for the current
|
||
|
* stage of the pipeline as seen by this thread. This pointer should
|
||
|
* eventually be released by calling release_read().
|
||
|
*
|
||
|
* There should be no outstanding write pointers on the data when this
|
||
|
* function is called.
|
||
|
*/
|
||
|
INLINE const CycleData *PipelineCyclerTrueImpl::
|
||
|
read(Thread *current_thread) const {
|
||
|
TAU_PROFILE("const CycleData *PipelineCyclerTrueImpl::read(Thread *)", " ", TAU_USER);
|
||
|
int pipeline_stage = current_thread->get_pipeline_stage();
|
||
|
#ifdef _DEBUG
|
||
|
nassertr(pipeline_stage >= 0 && pipeline_stage < _num_stages, nullptr);
|
||
|
#endif
|
||
|
_lock.acquire(current_thread);
|
||
|
return _data[pipeline_stage]._cdata;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Increments the count on a pointer previously retrieved by read(); now the
|
||
|
* pointer will need to be released twice.
|
||
|
*/
|
||
|
INLINE void PipelineCyclerTrueImpl::
|
||
|
increment_read(const CycleData *pointer) const {
|
||
|
TAU_PROFILE("void PipelineCyclerTrueImpl::increment_read(const CycleData *)", " ", TAU_USER);
|
||
|
#ifdef _DEBUG
|
||
|
int pipeline_stage = Thread::get_current_pipeline_stage();
|
||
|
nassertv(pipeline_stage >= 0 && pipeline_stage < _num_stages);
|
||
|
nassertv(_data[pipeline_stage]._cdata == pointer);
|
||
|
#endif
|
||
|
_lock.elevate_lock();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Releases a pointer previously obtained via a call to read().
|
||
|
*/
|
||
|
INLINE void PipelineCyclerTrueImpl::
|
||
|
release_read(const CycleData *pointer) const {
|
||
|
TAU_PROFILE("void PipelineCyclerTrueImpl::release_read(const CycleData *)", " ", TAU_USER);
|
||
|
#ifdef _DEBUG
|
||
|
int pipeline_stage = Thread::get_current_pipeline_stage();
|
||
|
nassertv(pipeline_stage >= 0 && pipeline_stage < _num_stages);
|
||
|
nassertv(_data[pipeline_stage]._cdata == pointer);
|
||
|
#endif
|
||
|
_lock.release();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a non-const CycleData pointer, filled with a unique copy of the
|
||
|
* data for the current stage of the pipeline as seen by this thread. This
|
||
|
* pointer may now be used to write to the data, and that copy of the data
|
||
|
* will be propagated to all later stages of the pipeline. This pointer
|
||
|
* should eventually be released by calling release_write().
|
||
|
*
|
||
|
* There may only be one outstanding write pointer on a given stage at a time,
|
||
|
* and if there is a write pointer there may be no read pointers on the same
|
||
|
* stage (but see elevate_read).
|
||
|
*/
|
||
|
INLINE CycleData *PipelineCyclerTrueImpl::
|
||
|
write(Thread *current_thread) {
|
||
|
TAU_PROFILE("CycleData *PipelineCyclerTrueImpl::write(Thread *)", " ", TAU_USER);
|
||
|
return write_stage(current_thread->get_pipeline_stage(), current_thread);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This special variant on write() will automatically propagate changes back
|
||
|
* to upstream pipeline stages. If force_to_0 is false, then it propagates
|
||
|
* back only as long as the CycleData pointers are equivalent, guaranteeing
|
||
|
* that it does not modify upstream data (other than the modification that
|
||
|
* will be performed by the code that returns this pointer). This is
|
||
|
* particularly appropriate for minor updates, where it doesn't matter much if
|
||
|
* the update is lost, such as storing a cached value.
|
||
|
*
|
||
|
* If force_to_0 is true, then the CycleData pointer for the current pipeline
|
||
|
* stage is propagated all the way back up to stage 0; after this call, there
|
||
|
* will be only one CycleData pointer that is duplicated in all stages between
|
||
|
* stage 0 and the current stage. This may undo some recent changes that were
|
||
|
* made independently at pipeline stage 0 (or any other upstream stage).
|
||
|
* However, it guarantees that the change that is to be applied at this
|
||
|
* pipeline stage will stick. This is slightly dangerous because of the risk
|
||
|
* of losing upstream changes; generally, this should only be done when you
|
||
|
* are confident that there are no upstream changes to be lost (for instance,
|
||
|
* for an object that has been recently created).
|
||
|
*/
|
||
|
INLINE CycleData *PipelineCyclerTrueImpl::
|
||
|
write_upstream(bool force_to_0, Thread *current_thread) {
|
||
|
TAU_PROFILE("CycleData *PipelineCyclerTrueImpl::write_upstream(bool, Thread *)", " ", TAU_USER);
|
||
|
return write_stage_upstream(current_thread->get_pipeline_stage(), force_to_0,
|
||
|
current_thread);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Elevates a currently-held read pointer into a write pointer. This may or
|
||
|
* may not change the value of the pointer. It is only valid to do this if
|
||
|
* this is the only currently-outstanding read pointer on the current stage.
|
||
|
*/
|
||
|
INLINE CycleData *PipelineCyclerTrueImpl::
|
||
|
elevate_read(const CycleData *pointer, Thread *current_thread) {
|
||
|
TAU_PROFILE("CycleData *PipelineCyclerTrueImpl::elevate_read(const CycleData *)", " ", TAU_USER);
|
||
|
#ifdef _DEBUG
|
||
|
int pipeline_stage = current_thread->get_pipeline_stage();
|
||
|
nassertr(pipeline_stage >= 0 && pipeline_stage < _num_stages, nullptr);
|
||
|
nassertr(_data[pipeline_stage]._cdata == pointer, nullptr);
|
||
|
#endif
|
||
|
CycleData *new_pointer = write(current_thread);
|
||
|
_lock.release();
|
||
|
return new_pointer;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Elevates a currently-held read pointer into a write pointer, like
|
||
|
* elevate_read(), but also propagates the pointer back to upstream stages,
|
||
|
* like write_upstream().
|
||
|
*/
|
||
|
INLINE CycleData *PipelineCyclerTrueImpl::
|
||
|
elevate_read_upstream(const CycleData *pointer, bool force_to_0, Thread *current_thread) {
|
||
|
TAU_PROFILE("CycleData *PipelineCyclerTrueImpl::elevate_read_upstream(const CycleData *, bool)", " ", TAU_USER);
|
||
|
#ifdef _DEBUG
|
||
|
int pipeline_stage = current_thread->get_pipeline_stage();
|
||
|
nassertr(pipeline_stage >= 0 && pipeline_stage < _num_stages, nullptr);
|
||
|
nassertr(_data[pipeline_stage]._cdata == pointer, nullptr);
|
||
|
#endif
|
||
|
CycleData *new_pointer = write_upstream(force_to_0, current_thread);
|
||
|
_lock.release();
|
||
|
return new_pointer;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Increments the count on a pointer previously retrieved by write(); now the
|
||
|
* pointer will need to be released twice.
|
||
|
*/
|
||
|
INLINE void PipelineCyclerTrueImpl::
|
||
|
increment_write(CycleData *pointer) const {
|
||
|
TAU_PROFILE("void PipelineCyclerTrueImpl::increment_write(CycleData *)", " ", TAU_USER);
|
||
|
int pipeline_stage = Thread::get_current_pipeline_stage();
|
||
|
#ifdef _DEBUG
|
||
|
nassertv(pipeline_stage >= 0 && pipeline_stage < _num_stages);
|
||
|
nassertv(_data[pipeline_stage]._cdata == pointer);
|
||
|
#endif
|
||
|
++(_data[pipeline_stage]._writes_outstanding);
|
||
|
_lock.elevate_lock();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Releases a pointer previously obtained via a call to write().
|
||
|
*/
|
||
|
INLINE void PipelineCyclerTrueImpl::
|
||
|
release_write(CycleData *pointer) {
|
||
|
TAU_PROFILE("void PipelineCyclerTrueImpl::release_write(CycleData *)", " ", TAU_USER);
|
||
|
int pipeline_stage = Thread::get_current_pipeline_stage();
|
||
|
return release_write_stage(pipeline_stage, pointer);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the number of stages in the pipeline.
|
||
|
*/
|
||
|
INLINE int PipelineCyclerTrueImpl::
|
||
|
get_num_stages() {
|
||
|
return _num_stages;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a const CycleData pointer, filled with the data for the indicated
|
||
|
* stage of the pipeline. As in read_unlocked(), no lock is held on the
|
||
|
* returned pointer.
|
||
|
*/
|
||
|
INLINE const CycleData *PipelineCyclerTrueImpl::
|
||
|
read_stage_unlocked(int pipeline_stage) const {
|
||
|
TAU_PROFILE("const CycleData *PipelineCyclerTrueImpl::read_stage_unlocked(int)", " ", TAU_USER);
|
||
|
#ifdef _DEBUG
|
||
|
nassertr(pipeline_stage >= 0 && pipeline_stage < _num_stages, nullptr);
|
||
|
#elif defined(__has_builtin) && __has_builtin(__builtin_assume)
|
||
|
__builtin_assume(pipeline_stage >= 0);
|
||
|
#endif
|
||
|
return _data[pipeline_stage]._cdata;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a const CycleData pointer, filled with the data for the indicated
|
||
|
* stage of the pipeline. This pointer should eventually be released by
|
||
|
* calling release_read_stage().
|
||
|
*
|
||
|
* There should be no outstanding write pointers on the data when this
|
||
|
* function is called.
|
||
|
*/
|
||
|
INLINE const CycleData *PipelineCyclerTrueImpl::
|
||
|
read_stage(int pipeline_stage, Thread *current_thread) const {
|
||
|
TAU_PROFILE("const CycleData *PipelineCyclerTrueImpl::read_stage(int, Thread *)", " ", TAU_USER);
|
||
|
#ifdef _DEBUG
|
||
|
nassertr(pipeline_stage >= 0 && pipeline_stage < _num_stages, nullptr);
|
||
|
#elif defined(__has_builtin) && __has_builtin(__builtin_assume)
|
||
|
__builtin_assume(pipeline_stage >= 0);
|
||
|
#endif
|
||
|
_lock.acquire(current_thread);
|
||
|
return _data[pipeline_stage]._cdata;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Releases a pointer previously obtained via a call to read_stage().
|
||
|
*/
|
||
|
INLINE void PipelineCyclerTrueImpl::
|
||
|
release_read_stage(int pipeline_stage, const CycleData *pointer) const {
|
||
|
TAU_PROFILE("void PipelineCyclerTrueImpl::release_read_stage(int, const CycleData *)", " ", TAU_USER);
|
||
|
#ifdef _DEBUG
|
||
|
nassertv(pipeline_stage >= 0 && pipeline_stage < _num_stages);
|
||
|
nassertv(_data[pipeline_stage]._cdata == pointer);
|
||
|
#endif
|
||
|
_lock.release();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Elevates a currently-held read pointer into a write pointer. This may or
|
||
|
* may not change the value of the pointer. It is only valid to do this if
|
||
|
* this is the only currently-outstanding read pointer on the indicated stage.
|
||
|
*/
|
||
|
INLINE CycleData *PipelineCyclerTrueImpl::
|
||
|
elevate_read_stage(int pipeline_stage, const CycleData *pointer,
|
||
|
Thread *current_thread) {
|
||
|
TAU_PROFILE("CycleData *PipelineCyclerTrueImpl::elevate_read_stage(int, const CycleData *)", " ", TAU_USER);
|
||
|
#ifdef _DEBUG
|
||
|
nassertr(pipeline_stage >= 0 && pipeline_stage < _num_stages, nullptr);
|
||
|
nassertr(_data[pipeline_stage]._cdata == pointer, nullptr);
|
||
|
#elif defined(__has_builtin) && __has_builtin(__builtin_assume)
|
||
|
__builtin_assume(pipeline_stage >= 0);
|
||
|
#endif
|
||
|
CycleData *new_pointer = write_stage(pipeline_stage, current_thread);
|
||
|
_lock.release();
|
||
|
return new_pointer;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Elevates a currently-held read pointer into a write pointer. This may or
|
||
|
* may not change the value of the pointer. It is only valid to do this if
|
||
|
* this is the only currently-outstanding read pointer on the indicated stage.
|
||
|
*/
|
||
|
INLINE CycleData *PipelineCyclerTrueImpl::
|
||
|
elevate_read_stage_upstream(int pipeline_stage, const CycleData *pointer,
|
||
|
bool force_to_0, Thread *current_thread) {
|
||
|
TAU_PROFILE("CycleData *PipelineCyclerTrueImpl::elevate_read_stage(int, const CycleData *)", " ", TAU_USER);
|
||
|
#ifdef _DEBUG
|
||
|
nassertr(pipeline_stage >= 0 && pipeline_stage < _num_stages, nullptr);
|
||
|
nassertr(_data[pipeline_stage]._cdata == pointer, nullptr);
|
||
|
#elif defined(__has_builtin) && __has_builtin(__builtin_assume)
|
||
|
__builtin_assume(pipeline_stage >= 0);
|
||
|
#endif
|
||
|
CycleData *new_pointer =
|
||
|
write_stage_upstream(pipeline_stage, force_to_0, current_thread);
|
||
|
_lock.release();
|
||
|
return new_pointer;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Releases a pointer previously obtained via a call to write_stage().
|
||
|
*/
|
||
|
INLINE void PipelineCyclerTrueImpl::
|
||
|
release_write_stage(int pipeline_stage, CycleData *pointer) {
|
||
|
TAU_PROFILE("void PipelineCyclerTrueImpl::release_write_stage(int, const CycleData *)", " ", TAU_USER);
|
||
|
#ifdef _DEBUG
|
||
|
nassertv(pipeline_stage >= 0 && pipeline_stage < _num_stages);
|
||
|
nassertv(_data[pipeline_stage]._cdata == pointer);
|
||
|
nassertv(_data[pipeline_stage]._writes_outstanding > 0);
|
||
|
#elif defined(__has_builtin) && __has_builtin(__builtin_assume)
|
||
|
__builtin_assume(pipeline_stage >= 0);
|
||
|
#endif
|
||
|
--(_data[pipeline_stage]._writes_outstanding);
|
||
|
_lock.release();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the type of object that owns this cycler, as reported by
|
||
|
* CycleData::get_parent_type().
|
||
|
*/
|
||
|
INLINE TypeHandle PipelineCyclerTrueImpl::
|
||
|
get_parent_type() const {
|
||
|
return _data[0]._cdata->get_parent_type();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a pointer without counting it. This is only intended for use as
|
||
|
* the return value for certain nassertr() functions, so the application can
|
||
|
* recover after a failure to manage the read and write pointers correctly.
|
||
|
* You should never call this function directly.
|
||
|
*/
|
||
|
INLINE CycleData *PipelineCyclerTrueImpl::
|
||
|
cheat() const {
|
||
|
TAU_PROFILE("CycleData *PipelineCyclerTrueImpl::cheat()", " ", TAU_USER);
|
||
|
int pipeline_stage = Thread::get_current_pipeline_stage();
|
||
|
nassertr(pipeline_stage >= 0 && pipeline_stage < _num_stages, nullptr);
|
||
|
return _data[pipeline_stage]._cdata;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the number of handles currently outstanding to read the current
|
||
|
* stage of the data. This should only be used for debugging purposes.
|
||
|
*/
|
||
|
INLINE int PipelineCyclerTrueImpl::
|
||
|
get_read_count() const {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the number of handles currently outstanding to read the current
|
||
|
* stage of the data. This will normally only be either 0 or 1. This should
|
||
|
* only be used for debugging purposes.
|
||
|
*/
|
||
|
INLINE int PipelineCyclerTrueImpl::
|
||
|
get_write_count() const {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This is a special implementation of cycle() for the special case of just
|
||
|
* two stages to the pipeline. It does the same thing as cycle(), but is a
|
||
|
* little bit faster because it knows there are exactly two stages.
|
||
|
*/
|
||
|
INLINE PT(CycleData) PipelineCyclerTrueImpl::
|
||
|
cycle_2() {
|
||
|
TAU_PROFILE("PT(CycleData) PipelineCyclerTrueImpl::cycle_2()", " ", TAU_USER);
|
||
|
|
||
|
// This trick moves an NPT into a PT without unnecessarily incrementing and
|
||
|
// subsequently decrementing the regular reference count.
|
||
|
PT(CycleData) last_val;
|
||
|
last_val.swap(_data[1]._cdata);
|
||
|
last_val->node_unref_only();
|
||
|
|
||
|
nassertr(_lock.debug_is_locked(), last_val);
|
||
|
nassertr(_dirty, last_val);
|
||
|
nassertr(_num_stages == 2, last_val);
|
||
|
|
||
|
nassertr(_data[1]._writes_outstanding == 0, last_val);
|
||
|
_data[1]._cdata = _data[0]._cdata;
|
||
|
|
||
|
// No longer dirty.
|
||
|
_dirty = 0;
|
||
|
return last_val;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This is a special implementation of cycle() for the special case of exactly
|
||
|
* three stages to the pipeline. It does the same thing as cycle(), but is a
|
||
|
* little bit faster because it knows there are exactly three stages.
|
||
|
*/
|
||
|
INLINE PT(CycleData) PipelineCyclerTrueImpl::
|
||
|
cycle_3() {
|
||
|
TAU_PROFILE("PT(CycleData) PipelineCyclerTrueImpl::cycle_3()", " ", TAU_USER);
|
||
|
|
||
|
// This trick moves an NPT into a PT without unnecessarily incrementing and
|
||
|
// subsequently decrementing the regular reference count.
|
||
|
PT(CycleData) last_val;
|
||
|
last_val.swap(_data[2]._cdata);
|
||
|
last_val->node_unref_only();
|
||
|
|
||
|
nassertr(_lock.debug_is_locked(), last_val);
|
||
|
nassertr(_dirty, last_val);
|
||
|
nassertr(_num_stages == 3, last_val);
|
||
|
|
||
|
nassertr(_data[2]._writes_outstanding == 0, last_val);
|
||
|
nassertr(_data[1]._writes_outstanding == 0, last_val);
|
||
|
_data[2]._cdata = _data[1]._cdata;
|
||
|
_data[1]._cdata = _data[0]._cdata;
|
||
|
|
||
|
if (_data[2]._cdata == _data[1]._cdata) {
|
||
|
// No longer dirty.
|
||
|
_dirty = 0;
|
||
|
}
|
||
|
|
||
|
return last_val;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
*/
|
||
|
INLINE PipelineCyclerTrueImpl::CyclerMutex::
|
||
|
CyclerMutex(PipelineCyclerTrueImpl *cycler) {
|
||
|
#ifdef DEBUG_THREADS
|
||
|
_cycler = cycler;
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
*/
|
||
|
INLINE PipelineCyclerTrueImpl::CycleDataNode::
|
||
|
CycleDataNode() :
|
||
|
_writes_outstanding(0)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
*/
|
||
|
INLINE PipelineCyclerTrueImpl::CycleDataNode::
|
||
|
CycleDataNode(const PipelineCyclerTrueImpl::CycleDataNode ©) :
|
||
|
_cdata(copy._cdata),
|
||
|
_writes_outstanding(0)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
*/
|
||
|
INLINE PipelineCyclerTrueImpl::CycleDataNode::
|
||
|
~CycleDataNode() {
|
||
|
nassertv(_writes_outstanding == 0);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
*/
|
||
|
INLINE void PipelineCyclerTrueImpl::CycleDataNode::
|
||
|
operator = (const PipelineCyclerTrueImpl::CycleDataNode ©) {
|
||
|
_cdata = copy._cdata;
|
||
|
}
|