merge
commit
b6af8aae5b
|
|
@ -62,6 +62,9 @@
|
|||
//
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
U32 ll_thread_local sThreadID = 0;
|
||||
U32 LLThread::sIDIter = 0;
|
||||
|
||||
//
|
||||
// Handed to the APR thread creation function
|
||||
//
|
||||
|
|
@ -72,6 +75,8 @@ void *APR_THREAD_FUNC LLThread::staticRun(apr_thread_t *apr_threadp, void *datap
|
|||
// Set thread state to running
|
||||
threadp->mStatus = RUNNING;
|
||||
|
||||
sThreadID = threadp->mID;
|
||||
|
||||
// Run the user supplied function
|
||||
threadp->run();
|
||||
|
||||
|
|
@ -90,6 +95,8 @@ LLThread::LLThread(const std::string& name, apr_pool_t *poolp) :
|
|||
mAPRThreadp(NULL),
|
||||
mStatus(STOPPED)
|
||||
{
|
||||
mID = ++sIDIter;
|
||||
|
||||
// Thread creation probably CAN be paranoid about APR being initialized, if necessary
|
||||
if (poolp)
|
||||
{
|
||||
|
|
@ -273,7 +280,7 @@ void LLThread::wakeLocked()
|
|||
//============================================================================
|
||||
|
||||
LLMutex::LLMutex(apr_pool_t *poolp) :
|
||||
mAPRMutexp(NULL)
|
||||
mAPRMutexp(NULL), mCount(0), mLockingThread(NO_THREAD)
|
||||
{
|
||||
//if (poolp)
|
||||
//{
|
||||
|
|
@ -305,7 +312,14 @@ LLMutex::~LLMutex()
|
|||
|
||||
void LLMutex::lock()
|
||||
{
|
||||
if (mLockingThread == sThreadID)
|
||||
{ //redundant lock
|
||||
mCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
apr_thread_mutex_lock(mAPRMutexp);
|
||||
|
||||
#if MUTEX_DEBUG
|
||||
// Have to have the lock before we can access the debug info
|
||||
U32 id = LLThread::currentID();
|
||||
|
|
@ -313,10 +327,18 @@ void LLMutex::lock()
|
|||
llerrs << "Already locked in Thread: " << id << llendl;
|
||||
mIsLocked[id] = TRUE;
|
||||
#endif
|
||||
|
||||
mLockingThread = sThreadID;
|
||||
}
|
||||
|
||||
void LLMutex::unlock()
|
||||
{
|
||||
if (mCount > 0)
|
||||
{ //not the root unlock
|
||||
mCount--;
|
||||
return;
|
||||
}
|
||||
|
||||
#if MUTEX_DEBUG
|
||||
// Access the debug info while we have the lock
|
||||
U32 id = LLThread::currentID();
|
||||
|
|
@ -324,6 +346,8 @@ void LLMutex::unlock()
|
|||
llerrs << "Not locked in Thread: " << id << llendl;
|
||||
mIsLocked[id] = FALSE;
|
||||
#endif
|
||||
|
||||
mLockingThread = NO_THREAD;
|
||||
apr_thread_mutex_unlock(mAPRMutexp);
|
||||
}
|
||||
|
||||
|
|
@ -341,6 +365,11 @@ bool LLMutex::isLocked()
|
|||
}
|
||||
}
|
||||
|
||||
U32 LLMutex::lockingThread() const
|
||||
{
|
||||
return mLockingThread;
|
||||
}
|
||||
|
||||
//============================================================================
|
||||
|
||||
LLCondition::LLCondition(apr_pool_t *poolp) :
|
||||
|
|
|
|||
|
|
@ -1,262 +1,284 @@
|
|||
/**
|
||||
* @file llthread.h
|
||||
* @brief Base classes for thread, mutex and condition handling.
|
||||
*
|
||||
* $LicenseInfo:firstyear=2004&license=viewergpl$
|
||||
*
|
||||
* Copyright (c) 2004-2009, Linden Research, Inc.
|
||||
*
|
||||
* Second Life Viewer Source Code
|
||||
* The source code in this file ("Source Code") is provided by Linden Lab
|
||||
* to you under the terms of the GNU General Public License, version 2.0
|
||||
* ("GPL"), unless you have obtained a separate licensing agreement
|
||||
* ("Other License"), formally executed by you and Linden Lab. Terms of
|
||||
* the GPL can be found in doc/GPL-license.txt in this distribution, or
|
||||
* online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
|
||||
*
|
||||
* There are special exceptions to the terms and conditions of the GPL as
|
||||
* it is applied to this Source Code. View the full text of the exception
|
||||
* in the file doc/FLOSS-exception.txt in this software distribution, or
|
||||
* online at
|
||||
* http://secondlifegrid.net/programs/open_source/licensing/flossexception
|
||||
*
|
||||
* By copying, modifying or distributing this software, you acknowledge
|
||||
* that you have read and understood your obligations described above,
|
||||
* and agree to abide by those obligations.
|
||||
*
|
||||
* ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
|
||||
* WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
|
||||
* COMPLETENESS OR PERFORMANCE.
|
||||
* $/LicenseInfo$
|
||||
*/
|
||||
|
||||
#ifndef LL_LLTHREAD_H
|
||||
#define LL_LLTHREAD_H
|
||||
|
||||
#include "llapp.h"
|
||||
#include "apr_thread_cond.h"
|
||||
|
||||
class LLThread;
|
||||
class LLMutex;
|
||||
class LLCondition;
|
||||
|
||||
class LL_COMMON_API LLThread
|
||||
{
|
||||
public:
|
||||
typedef enum e_thread_status
|
||||
{
|
||||
STOPPED = 0, // The thread is not running. Not started, or has exited its run function
|
||||
RUNNING = 1, // The thread is currently running
|
||||
QUITTING= 2 // Someone wants this thread to quit
|
||||
} EThreadStatus;
|
||||
|
||||
LLThread(const std::string& name, apr_pool_t *poolp = NULL);
|
||||
virtual ~LLThread(); // Warning! You almost NEVER want to destroy a thread unless it's in the STOPPED state.
|
||||
virtual void shutdown(); // stops the thread
|
||||
|
||||
bool isQuitting() const { return (QUITTING == mStatus); }
|
||||
bool isStopped() const { return (STOPPED == mStatus); }
|
||||
|
||||
static U32 currentID(); // Return ID of current thread
|
||||
static void yield(); // Static because it can be called by the main thread, which doesn't have an LLThread data structure.
|
||||
|
||||
public:
|
||||
// PAUSE / RESUME functionality. See source code for important usage notes.
|
||||
// Called from MAIN THREAD.
|
||||
void pause();
|
||||
void unpause();
|
||||
bool isPaused() { return isStopped() || mPaused == TRUE; }
|
||||
|
||||
// Cause the thread to wake up and check its condition
|
||||
void wake();
|
||||
|
||||
// Same as above, but to be used when the condition is already locked.
|
||||
void wakeLocked();
|
||||
|
||||
// Called from run() (CHILD THREAD). Pause the thread if requested until unpaused.
|
||||
void checkPause();
|
||||
|
||||
// this kicks off the apr thread
|
||||
void start(void);
|
||||
|
||||
apr_pool_t *getAPRPool() { return mAPRPoolp; }
|
||||
LLVolatileAPRPool* getLocalAPRFilePool() { return mLocalAPRFilePoolp ; }
|
||||
|
||||
private:
|
||||
BOOL mPaused;
|
||||
|
||||
// static function passed to APR thread creation routine
|
||||
static void *APR_THREAD_FUNC staticRun(apr_thread_t *apr_threadp, void *datap);
|
||||
|
||||
protected:
|
||||
std::string mName;
|
||||
LLCondition* mRunCondition;
|
||||
|
||||
apr_thread_t *mAPRThreadp;
|
||||
apr_pool_t *mAPRPoolp;
|
||||
BOOL mIsLocalPool;
|
||||
EThreadStatus mStatus;
|
||||
|
||||
//a local apr_pool for APRFile operations in this thread. If it exists, LLAPRFile::sAPRFilePoolp should not be used.
|
||||
//Note: this pool is used by APRFile ONLY, do NOT use it for any other purposes.
|
||||
// otherwise it will cause severe memory leaking!!! --bao
|
||||
LLVolatileAPRPool *mLocalAPRFilePoolp ;
|
||||
|
||||
void setQuitting();
|
||||
|
||||
// virtual function overridden by subclass -- this will be called when the thread runs
|
||||
virtual void run(void) = 0;
|
||||
|
||||
// virtual predicate function -- returns true if the thread should wake up, false if it should sleep.
|
||||
virtual bool runCondition(void);
|
||||
|
||||
// Lock/Unlock Run Condition -- use around modification of any variable used in runCondition()
|
||||
inline void lockData();
|
||||
inline void unlockData();
|
||||
|
||||
// This is the predicate that decides whether the thread should sleep.
|
||||
// It should only be called with mRunCondition locked, since the virtual runCondition() function may need to access
|
||||
// data structures that are thread-unsafe.
|
||||
bool shouldSleep(void) { return (mStatus == RUNNING) && (isPaused() || (!runCondition())); }
|
||||
|
||||
// To avoid spurious signals (and the associated context switches) when the condition may or may not have changed, you can do the following:
|
||||
// mRunCondition->lock();
|
||||
// if(!shouldSleep())
|
||||
// mRunCondition->signal();
|
||||
// mRunCondition->unlock();
|
||||
};
|
||||
|
||||
//============================================================================
|
||||
|
||||
#define MUTEX_DEBUG (LL_DEBUG || LL_RELEASE_WITH_DEBUG_INFO)
|
||||
|
||||
class LL_COMMON_API LLMutex
|
||||
{
|
||||
public:
|
||||
LLMutex(apr_pool_t *apr_poolp); // NULL pool constructs a new pool for the mutex
|
||||
~LLMutex();
|
||||
|
||||
void lock(); // blocks
|
||||
void unlock();
|
||||
bool isLocked(); // non-blocking, but does do a lock/unlock so not free
|
||||
|
||||
protected:
|
||||
apr_thread_mutex_t *mAPRMutexp;
|
||||
apr_pool_t *mAPRPoolp;
|
||||
BOOL mIsLocalPool;
|
||||
#if MUTEX_DEBUG
|
||||
std::map<U32, BOOL> mIsLocked;
|
||||
#endif
|
||||
};
|
||||
|
||||
// Actually a condition/mutex pair (since each condition needs to be associated with a mutex).
|
||||
class LL_COMMON_API LLCondition : public LLMutex
|
||||
{
|
||||
public:
|
||||
LLCondition(apr_pool_t *apr_poolp); // Defaults to global pool, could use the thread pool as well.
|
||||
~LLCondition();
|
||||
|
||||
void wait(); // blocks
|
||||
void signal();
|
||||
void broadcast();
|
||||
|
||||
protected:
|
||||
apr_thread_cond_t *mAPRCondp;
|
||||
};
|
||||
|
||||
class LLMutexLock
|
||||
{
|
||||
public:
|
||||
LLMutexLock(LLMutex* mutex)
|
||||
{
|
||||
mMutex = mutex;
|
||||
mMutex->lock();
|
||||
}
|
||||
~LLMutexLock()
|
||||
{
|
||||
mMutex->unlock();
|
||||
}
|
||||
private:
|
||||
LLMutex* mMutex;
|
||||
};
|
||||
|
||||
//============================================================================
|
||||
|
||||
void LLThread::lockData()
|
||||
{
|
||||
mRunCondition->lock();
|
||||
}
|
||||
|
||||
void LLThread::unlockData()
|
||||
{
|
||||
mRunCondition->unlock();
|
||||
}
|
||||
|
||||
|
||||
//============================================================================
|
||||
|
||||
// see llmemory.h for LLPointer<> definition
|
||||
|
||||
class LL_COMMON_API LLThreadSafeRefCount
|
||||
{
|
||||
public:
|
||||
static void initThreadSafeRefCount(); // creates sMutex
|
||||
static void cleanupThreadSafeRefCount(); // destroys sMutex
|
||||
|
||||
private:
|
||||
static LLMutex* sMutex;
|
||||
|
||||
private:
|
||||
LLThreadSafeRefCount(const LLThreadSafeRefCount&); // not implemented
|
||||
LLThreadSafeRefCount&operator=(const LLThreadSafeRefCount&); // not implemented
|
||||
|
||||
protected:
|
||||
virtual ~LLThreadSafeRefCount(); // use unref()
|
||||
|
||||
public:
|
||||
LLThreadSafeRefCount();
|
||||
|
||||
void ref()
|
||||
{
|
||||
if (sMutex) sMutex->lock();
|
||||
mRef++;
|
||||
if (sMutex) sMutex->unlock();
|
||||
}
|
||||
|
||||
S32 unref()
|
||||
{
|
||||
llassert(mRef >= 1);
|
||||
if (sMutex) sMutex->lock();
|
||||
S32 res = --mRef;
|
||||
if (sMutex) sMutex->unlock();
|
||||
if (0 == res)
|
||||
{
|
||||
delete this;
|
||||
return 0;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
S32 getNumRefs() const
|
||||
{
|
||||
return mRef;
|
||||
}
|
||||
|
||||
private:
|
||||
S32 mRef;
|
||||
};
|
||||
|
||||
//============================================================================
|
||||
|
||||
// Simple responder for self destructing callbacks
|
||||
// Pure virtual class
|
||||
class LL_COMMON_API LLResponder : public LLThreadSafeRefCount
|
||||
{
|
||||
protected:
|
||||
virtual ~LLResponder();
|
||||
public:
|
||||
virtual void completed(bool success) = 0;
|
||||
};
|
||||
|
||||
//============================================================================
|
||||
|
||||
#endif // LL_LLTHREAD_H
|
||||
/**
|
||||
* @file llthread.h
|
||||
* @brief Base classes for thread, mutex and condition handling.
|
||||
*
|
||||
* $LicenseInfo:firstyear=2004&license=viewergpl$
|
||||
*
|
||||
* Copyright (c) 2004-2009, Linden Research, Inc.
|
||||
*
|
||||
* Second Life Viewer Source Code
|
||||
* The source code in this file ("Source Code") is provided by Linden Lab
|
||||
* to you under the terms of the GNU General Public License, version 2.0
|
||||
* ("GPL"), unless you have obtained a separate licensing agreement
|
||||
* ("Other License"), formally executed by you and Linden Lab. Terms of
|
||||
* the GPL can be found in doc/GPL-license.txt in this distribution, or
|
||||
* online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
|
||||
*
|
||||
* There are special exceptions to the terms and conditions of the GPL as
|
||||
* it is applied to this Source Code. View the full text of the exception
|
||||
* in the file doc/FLOSS-exception.txt in this software distribution, or
|
||||
* online at
|
||||
* http://secondlifegrid.net/programs/open_source/licensing/flossexception
|
||||
*
|
||||
* By copying, modifying or distributing this software, you acknowledge
|
||||
* that you have read and understood your obligations described above,
|
||||
* and agree to abide by those obligations.
|
||||
*
|
||||
* ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
|
||||
* WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
|
||||
* COMPLETENESS OR PERFORMANCE.
|
||||
* $/LicenseInfo$
|
||||
*/
|
||||
|
||||
#ifndef LL_LLTHREAD_H
|
||||
#define LL_LLTHREAD_H
|
||||
|
||||
#include "llapp.h"
|
||||
#include "apr_thread_cond.h"
|
||||
|
||||
class LLThread;
|
||||
class LLMutex;
|
||||
class LLCondition;
|
||||
|
||||
#if LL_WINDOWS
|
||||
#define ll_thread_local __declspec(thread)
|
||||
#else
|
||||
#define ll_thread_local __thread
|
||||
#endif
|
||||
|
||||
class LL_COMMON_API LLThread
|
||||
{
|
||||
private:
|
||||
static U32 sIDIter;
|
||||
|
||||
public:
|
||||
typedef enum e_thread_status
|
||||
{
|
||||
STOPPED = 0, // The thread is not running. Not started, or has exited its run function
|
||||
RUNNING = 1, // The thread is currently running
|
||||
QUITTING= 2 // Someone wants this thread to quit
|
||||
} EThreadStatus;
|
||||
|
||||
LLThread(const std::string& name, apr_pool_t *poolp = NULL);
|
||||
virtual ~LLThread(); // Warning! You almost NEVER want to destroy a thread unless it's in the STOPPED state.
|
||||
virtual void shutdown(); // stops the thread
|
||||
|
||||
bool isQuitting() const { return (QUITTING == mStatus); }
|
||||
bool isStopped() const { return (STOPPED == mStatus); }
|
||||
|
||||
static U32 currentID(); // Return ID of current thread
|
||||
static void yield(); // Static because it can be called by the main thread, which doesn't have an LLThread data structure.
|
||||
|
||||
public:
|
||||
// PAUSE / RESUME functionality. See source code for important usage notes.
|
||||
// Called from MAIN THREAD.
|
||||
void pause();
|
||||
void unpause();
|
||||
bool isPaused() { return isStopped() || mPaused == TRUE; }
|
||||
|
||||
// Cause the thread to wake up and check its condition
|
||||
void wake();
|
||||
|
||||
// Same as above, but to be used when the condition is already locked.
|
||||
void wakeLocked();
|
||||
|
||||
// Called from run() (CHILD THREAD). Pause the thread if requested until unpaused.
|
||||
void checkPause();
|
||||
|
||||
// this kicks off the apr thread
|
||||
void start(void);
|
||||
|
||||
apr_pool_t *getAPRPool() { return mAPRPoolp; }
|
||||
LLVolatileAPRPool* getLocalAPRFilePool() { return mLocalAPRFilePoolp ; }
|
||||
|
||||
U32 getID() const { return mID; }
|
||||
|
||||
private:
|
||||
BOOL mPaused;
|
||||
|
||||
// static function passed to APR thread creation routine
|
||||
static void *APR_THREAD_FUNC staticRun(apr_thread_t *apr_threadp, void *datap);
|
||||
|
||||
protected:
|
||||
std::string mName;
|
||||
LLCondition* mRunCondition;
|
||||
|
||||
apr_thread_t *mAPRThreadp;
|
||||
apr_pool_t *mAPRPoolp;
|
||||
BOOL mIsLocalPool;
|
||||
EThreadStatus mStatus;
|
||||
U32 mID;
|
||||
|
||||
//a local apr_pool for APRFile operations in this thread. If it exists, LLAPRFile::sAPRFilePoolp should not be used.
|
||||
//Note: this pool is used by APRFile ONLY, do NOT use it for any other purposes.
|
||||
// otherwise it will cause severe memory leaking!!! --bao
|
||||
LLVolatileAPRPool *mLocalAPRFilePoolp ;
|
||||
|
||||
void setQuitting();
|
||||
|
||||
// virtual function overridden by subclass -- this will be called when the thread runs
|
||||
virtual void run(void) = 0;
|
||||
|
||||
// virtual predicate function -- returns true if the thread should wake up, false if it should sleep.
|
||||
virtual bool runCondition(void);
|
||||
|
||||
// Lock/Unlock Run Condition -- use around modification of any variable used in runCondition()
|
||||
inline void lockData();
|
||||
inline void unlockData();
|
||||
|
||||
// This is the predicate that decides whether the thread should sleep.
|
||||
// It should only be called with mRunCondition locked, since the virtual runCondition() function may need to access
|
||||
// data structures that are thread-unsafe.
|
||||
bool shouldSleep(void) { return (mStatus == RUNNING) && (isPaused() || (!runCondition())); }
|
||||
|
||||
// To avoid spurious signals (and the associated context switches) when the condition may or may not have changed, you can do the following:
|
||||
// mRunCondition->lock();
|
||||
// if(!shouldSleep())
|
||||
// mRunCondition->signal();
|
||||
// mRunCondition->unlock();
|
||||
};
|
||||
|
||||
//============================================================================
|
||||
|
||||
#define MUTEX_DEBUG (LL_DEBUG || LL_RELEASE_WITH_DEBUG_INFO)
|
||||
|
||||
class LL_COMMON_API LLMutex
|
||||
{
|
||||
public:
|
||||
typedef enum
|
||||
{
|
||||
NO_THREAD = 0xFFFFFFFF
|
||||
} e_locking_thread;
|
||||
|
||||
LLMutex(apr_pool_t *apr_poolp); // NULL pool constructs a new pool for the mutex
|
||||
~LLMutex();
|
||||
|
||||
void lock(); // blocks
|
||||
void unlock();
|
||||
bool isLocked(); // non-blocking, but does do a lock/unlock so not free
|
||||
U32 lockingThread() const; //get ID of locking thread
|
||||
|
||||
protected:
|
||||
apr_thread_mutex_t *mAPRMutexp;
|
||||
mutable U32 mCount;
|
||||
mutable U32 mLockingThread;
|
||||
|
||||
apr_pool_t *mAPRPoolp;
|
||||
BOOL mIsLocalPool;
|
||||
S32 mLockCount;
|
||||
#if MUTEX_DEBUG
|
||||
std::map<U32, BOOL> mIsLocked;
|
||||
#endif
|
||||
};
|
||||
|
||||
// Actually a condition/mutex pair (since each condition needs to be associated with a mutex).
|
||||
class LL_COMMON_API LLCondition : public LLMutex
|
||||
{
|
||||
public:
|
||||
LLCondition(apr_pool_t *apr_poolp); // Defaults to global pool, could use the thread pool as well.
|
||||
~LLCondition();
|
||||
|
||||
void wait(); // blocks
|
||||
void signal();
|
||||
void broadcast();
|
||||
|
||||
protected:
|
||||
apr_thread_cond_t *mAPRCondp;
|
||||
};
|
||||
|
||||
class LLMutexLock
|
||||
{
|
||||
public:
|
||||
LLMutexLock(LLMutex* mutex)
|
||||
{
|
||||
mMutex = mutex;
|
||||
mMutex->lock();
|
||||
}
|
||||
~LLMutexLock()
|
||||
{
|
||||
mMutex->unlock();
|
||||
}
|
||||
private:
|
||||
LLMutex* mMutex;
|
||||
};
|
||||
|
||||
//============================================================================
|
||||
|
||||
void LLThread::lockData()
|
||||
{
|
||||
mRunCondition->lock();
|
||||
}
|
||||
|
||||
void LLThread::unlockData()
|
||||
{
|
||||
mRunCondition->unlock();
|
||||
}
|
||||
|
||||
|
||||
//============================================================================
|
||||
|
||||
// see llmemory.h for LLPointer<> definition
|
||||
|
||||
class LL_COMMON_API LLThreadSafeRefCount
|
||||
{
|
||||
public:
|
||||
static void initThreadSafeRefCount(); // creates sMutex
|
||||
static void cleanupThreadSafeRefCount(); // destroys sMutex
|
||||
|
||||
private:
|
||||
static LLMutex* sMutex;
|
||||
|
||||
private:
|
||||
LLThreadSafeRefCount(const LLThreadSafeRefCount&); // not implemented
|
||||
LLThreadSafeRefCount&operator=(const LLThreadSafeRefCount&); // not implemented
|
||||
|
||||
protected:
|
||||
virtual ~LLThreadSafeRefCount(); // use unref()
|
||||
|
||||
public:
|
||||
LLThreadSafeRefCount();
|
||||
|
||||
void ref()
|
||||
{
|
||||
if (sMutex) sMutex->lock();
|
||||
mRef++;
|
||||
if (sMutex) sMutex->unlock();
|
||||
}
|
||||
|
||||
S32 unref()
|
||||
{
|
||||
llassert(mRef >= 1);
|
||||
if (sMutex) sMutex->lock();
|
||||
S32 res = --mRef;
|
||||
if (sMutex) sMutex->unlock();
|
||||
if (0 == res)
|
||||
{
|
||||
delete this;
|
||||
return 0;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
S32 getNumRefs() const
|
||||
{
|
||||
return mRef;
|
||||
}
|
||||
|
||||
private:
|
||||
S32 mRef;
|
||||
};
|
||||
|
||||
//============================================================================
|
||||
|
||||
// Simple responder for self destructing callbacks
|
||||
// Pure virtual class
|
||||
class LL_COMMON_API LLResponder : public LLThreadSafeRefCount
|
||||
{
|
||||
protected:
|
||||
virtual ~LLResponder();
|
||||
public:
|
||||
virtual void completed(bool success) = 0;
|
||||
};
|
||||
|
||||
//============================================================================
|
||||
|
||||
#endif // LL_LLTHREAD_H
|
||||
|
|
|
|||
|
|
@ -1952,8 +1952,12 @@ BOOL LLVolume::createVolumeFacesFromStream(std::istream& is)
|
|||
}
|
||||
|
||||
is.seekg(header[nm[lod]]["offset"].asInteger(), std::ios_base::cur);
|
||||
|
||||
|
||||
return unpackVolumeFaces(is, header[nm[lod]]["size"].asInteger());
|
||||
}
|
||||
|
||||
BOOL LLVolume::unpackVolumeFaces(std::istream& is, S32 size)
|
||||
{
|
||||
U8* result = NULL;
|
||||
U32 cur_size = 0;
|
||||
|
||||
|
|
@ -1964,7 +1968,6 @@ BOOL LLVolume::createVolumeFacesFromStream(std::istream& is)
|
|||
|
||||
const U32 CHUNK = 65536;
|
||||
|
||||
S32 size = header[nm[lod]]["size"].asInteger();
|
||||
U8 *in = new U8[size];
|
||||
is.read((char*) in, size);
|
||||
|
||||
|
|
@ -2168,7 +2171,7 @@ void LLVolume::makeTetrahedron()
|
|||
|
||||
LLVolumeFace face;
|
||||
|
||||
F32 x = 0.5f;
|
||||
F32 x = 0.25f;
|
||||
LLVector3 p[] =
|
||||
{ //unit tetrahedron corners
|
||||
LLVector3(x,x,x),
|
||||
|
|
|
|||
|
|
@ -971,6 +971,8 @@ protected:
|
|||
public:
|
||||
virtual BOOL createVolumeFacesFromFile(const std::string& file_name);
|
||||
virtual BOOL createVolumeFacesFromStream(std::istream& is);
|
||||
virtual BOOL unpackVolumeFaces(std::istream& is, S32 size);
|
||||
|
||||
virtual void makeTetrahedron();
|
||||
virtual BOOL isTetrahedron();
|
||||
|
||||
|
|
|
|||
|
|
@ -285,6 +285,7 @@ set(viewer_SOURCE_FILES
|
|||
llmediaremotectrl.cpp
|
||||
llmemoryview.cpp
|
||||
llmenucommands.cpp
|
||||
llmeshrepository.cpp
|
||||
llmetricperformancetester.cpp
|
||||
llmimetypes.cpp
|
||||
llmorphview.cpp
|
||||
|
|
@ -797,6 +798,7 @@ set(viewer_HEADER_FILES
|
|||
llmediaremotectrl.h
|
||||
llmemoryview.h
|
||||
llmenucommands.h
|
||||
llmeshrepository.h
|
||||
llmetricperformancetester.h
|
||||
llmimetypes.h
|
||||
llmorphview.h
|
||||
|
|
|
|||
|
|
@ -7495,6 +7495,17 @@
|
|||
<key>Value</key>
|
||||
<integer>8</integer>
|
||||
</map>
|
||||
<key>MeshMaxConcurrentRequests</key>
|
||||
<map>
|
||||
<key>Comment</key>
|
||||
<string>Number of threads to use for loading meshes.</string>
|
||||
<key>Persist</key>
|
||||
<integer>1</integer>
|
||||
<key>Type</key>
|
||||
<string>U32</string>
|
||||
<key>Value</key>
|
||||
<integer>32</integer>
|
||||
</map>
|
||||
|
||||
<key>SafeMode</key>
|
||||
<map>
|
||||
|
|
@ -10063,7 +10074,7 @@
|
|||
<key>Type</key>
|
||||
<string>Boolean</string>
|
||||
<key>Value</key>
|
||||
<integer>1</integer>
|
||||
<integer>0</integer>
|
||||
</map>
|
||||
<key>UseStartScreen</key>
|
||||
<map>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
#include "llwindow.h"
|
||||
#include "llviewerstats.h"
|
||||
#include "llmd5.h"
|
||||
#include "llmeshrepository.h"
|
||||
#include "llpumpio.h"
|
||||
#include "llmimetypes.h"
|
||||
#include "llslurl.h"
|
||||
|
|
@ -1273,6 +1274,9 @@ bool LLAppViewer::cleanup()
|
|||
|
||||
llinfos << "Cleaning Up" << llendflush;
|
||||
|
||||
// shut down mesh streamer
|
||||
gMeshRepo.shutdown();
|
||||
|
||||
// Must clean up texture references before viewer window is destroyed.
|
||||
LLHUDManager::getInstance()->updateEffects();
|
||||
LLHUDObject::updateAll();
|
||||
|
|
@ -1685,6 +1689,9 @@ bool LLAppViewer::initThreads()
|
|||
mFastTimerLogThread->start();
|
||||
}
|
||||
|
||||
// Mesh streaming and caching
|
||||
gMeshRepo.init();
|
||||
|
||||
// *FIX: no error handling here!
|
||||
return true;
|
||||
}
|
||||
|
|
@ -2401,6 +2408,7 @@ bool LLAppViewer::initWindow()
|
|||
gSavedSettings.saveToFile( gSavedSettings.getString("ClientSettingsFile"), TRUE );
|
||||
|
||||
gPipeline.init();
|
||||
|
||||
stop_glerror();
|
||||
gViewerWindow->initGLDefaults();
|
||||
|
||||
|
|
|
|||
|
|
@ -1432,6 +1432,7 @@ void LLViewerRegion::setSeedCapability(const std::string& url)
|
|||
capabilityNames.append("FetchLib");
|
||||
capabilityNames.append("FetchLibDescendents");
|
||||
capabilityNames.append("GetTexture");
|
||||
capabilityNames.append("GetMesh");
|
||||
capabilityNames.append("GroupProposalBallot");
|
||||
capabilityNames.append("HomeLocation");
|
||||
capabilityNames.append("MapLayer");
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@
|
|||
#include "llsdutil.h"
|
||||
#include "llmediaentry.h"
|
||||
#include "llmediadataclient.h"
|
||||
#include "llmeshrepository.h"
|
||||
#include "llagent.h"
|
||||
|
||||
const S32 MIN_QUIET_FRAMES_COALESCE = 30;
|
||||
|
|
@ -880,6 +881,24 @@ BOOL LLVOVolume::setVolume(const LLVolumeParams ¶ms, const S32 detail, bool
|
|||
{
|
||||
LLVolumeParams volume_params = params;
|
||||
|
||||
S32 lod = mLOD;
|
||||
|
||||
if (isSculpted())
|
||||
{
|
||||
// if it's a mesh
|
||||
if ((volume_params.getSculptType() & LL_SCULPT_TYPE_MASK) == LL_SCULPT_TYPE_MESH)
|
||||
{ //meshes might not have all LODs, get the force detail to best existing LOD
|
||||
LLUUID mesh_id = params.getSculptID();
|
||||
|
||||
//profile and path params don't matter for meshes
|
||||
volume_params = LLVolumeParams();
|
||||
volume_params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE);
|
||||
volume_params.setSculptID(mesh_id, LL_SCULPT_TYPE_MESH);
|
||||
|
||||
lod = gMeshRepo.getActualMeshLOD(mesh_id, lod);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we need to change implementations
|
||||
bool is_flexible = (volume_params.getPathParams().getCurveType() == LL_PCODE_PATH_FLEXIBLE);
|
||||
if (is_flexible)
|
||||
|
|
@ -907,13 +926,15 @@ BOOL LLVOVolume::setVolume(const LLVolumeParams ¶ms, const S32 detail, bool
|
|||
}
|
||||
}
|
||||
|
||||
if ((LLPrimitive::setVolume(volume_params, mLOD, (mVolumeImpl && mVolumeImpl->isVolumeUnique()))) || mSculptChanged)
|
||||
|
||||
|
||||
if ((LLPrimitive::setVolume(volume_params, lod, (mVolumeImpl && mVolumeImpl->isVolumeUnique()))) || mSculptChanged)
|
||||
{
|
||||
mFaceMappingChanged = TRUE;
|
||||
|
||||
if (mVolumeImpl)
|
||||
{
|
||||
mVolumeImpl->onSetVolume(volume_params, detail);
|
||||
mVolumeImpl->onSetVolume(volume_params, mLOD);
|
||||
}
|
||||
|
||||
if (isSculpted())
|
||||
|
|
@ -925,7 +946,11 @@ BOOL LLVOVolume::setVolume(const LLVolumeParams ¶ms, const S32 detail, bool
|
|||
{
|
||||
//load request not yet issued, request pipeline load this mesh
|
||||
LLUUID asset_id = volume_params.getSculptID();
|
||||
gPipeline.loadMesh(this, asset_id, LLVolumeLODGroup::getVolumeDetailFromScale(getVolume()->getDetail()));
|
||||
S32 available_lod = gMeshRepo.loadMesh(this, asset_id, lod);
|
||||
if (available_lod != lod)
|
||||
{
|
||||
LLPrimitive::setVolume(volume_params, available_lod);
|
||||
}
|
||||
}
|
||||
}
|
||||
else // otherwise is sculptie
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@
|
|||
#include "llgldbg.h"
|
||||
#include "llhudmanager.h"
|
||||
#include "lllightconstants.h"
|
||||
#include "llmeshrepository.h"
|
||||
#include "llresmgr.h"
|
||||
#include "llselectmgr.h"
|
||||
#include "llsky.h"
|
||||
|
|
@ -103,6 +104,7 @@
|
|||
#include "llspatialpartition.h"
|
||||
#include "llmutelist.h"
|
||||
#include "lltoolpie.h"
|
||||
#include "llcurl.h"
|
||||
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
|
@ -342,8 +344,6 @@ LLPipeline::LLPipeline() :
|
|||
mGlowPool(NULL),
|
||||
mBumpPool(NULL),
|
||||
mWLSkyPool(NULL),
|
||||
mMeshMutex(NULL),
|
||||
mMeshThreadCount(0),
|
||||
mLightMask(0),
|
||||
mLightMovingMask(0),
|
||||
mLightingDetail(0),
|
||||
|
|
@ -403,7 +403,6 @@ void LLPipeline::init()
|
|||
|
||||
stop_glerror();
|
||||
|
||||
mMeshMutex = new LLMutex(NULL);
|
||||
for (U32 i = 0; i < 2; ++i)
|
||||
{
|
||||
mSpotLightFade[i] = 1.f;
|
||||
|
|
@ -478,9 +477,6 @@ void LLPipeline::cleanup()
|
|||
//delete mWLSkyPool;
|
||||
mWLSkyPool = NULL;
|
||||
|
||||
delete mMeshMutex;
|
||||
mMeshMutex = NULL;
|
||||
|
||||
releaseGLBuffers();
|
||||
|
||||
mBloomImagep = NULL;
|
||||
|
|
@ -1802,6 +1798,8 @@ void LLPipeline::rebuildPriorityGroups()
|
|||
|
||||
assertInitialized();
|
||||
|
||||
gMeshRepo.notifyLoadedMeshes();
|
||||
|
||||
// Iterate through all drawables on the priority build queue,
|
||||
for (LLSpatialGroup::sg_list_t::iterator iter = mGroupQ1.begin();
|
||||
iter != mGroupQ1.end(); ++iter)
|
||||
|
|
@ -1881,8 +1879,6 @@ void LLPipeline::updateGeom(F32 max_dtime)
|
|||
// for now, only LLVOVolume does this to throttle LOD changes
|
||||
LLVOVolume::preUpdateGeom();
|
||||
|
||||
notifyLoadedMeshes();
|
||||
|
||||
// Iterate through all drawables on the priority build queue,
|
||||
for (LLDrawable::drawable_list_t::iterator iter = mBuildQ1.begin();
|
||||
iter != mBuildQ1.end();)
|
||||
|
|
@ -8912,246 +8908,3 @@ LLCullResult::sg_list_t::iterator LLPipeline::endAlphaGroups()
|
|||
}
|
||||
|
||||
|
||||
void LLPipeline::loadMesh(LLVOVolume* vobj, LLUUID mesh_id, S32 detail)
|
||||
{
|
||||
if (detail < 0 || detail > 4)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
LLMutexLock lock(mMeshMutex);
|
||||
//add volume to list of loading meshes
|
||||
mesh_load_map::iterator iter = mLoadingMeshes[detail].find(mesh_id);
|
||||
if (iter != mLoadingMeshes[detail].end())
|
||||
{ //request pending for this mesh, append volume id to list
|
||||
iter->second.insert(vobj->getID());
|
||||
return;
|
||||
}
|
||||
|
||||
//first request for this mesh
|
||||
mLoadingMeshes[detail][mesh_id].insert(vobj->getID());
|
||||
}
|
||||
|
||||
if (gAssetStorage->hasLocalAsset(mesh_id, LLAssetType::AT_MESH))
|
||||
{ //already have asset, load desired LOD in background
|
||||
mPendingMeshes.push_back(new LLMeshThread(mesh_id, vobj->getVolume(), detail));
|
||||
}
|
||||
else
|
||||
{ //fetch asset and load when done
|
||||
gAssetStorage->getAssetData(mesh_id, LLAssetType::AT_MESH,
|
||||
getMeshAssetCallback, vobj->getVolume(), TRUE);
|
||||
}
|
||||
|
||||
//do a quick search to see if we can't display something while we wait for this mesh to load
|
||||
LLVolume* volume = vobj->getVolume();
|
||||
|
||||
if (volume)
|
||||
{
|
||||
LLVolumeParams params = volume->getParams();
|
||||
|
||||
LLVolumeLODGroup* group = LLPrimitive::getVolumeManager()->getGroup(params);
|
||||
|
||||
if (group)
|
||||
{
|
||||
//first see what the next lowest LOD available might be
|
||||
for (S32 i = detail-1; i >= 0; --i)
|
||||
{
|
||||
LLVolume* lod = group->refLOD(i);
|
||||
if (lod && !lod->isTetrahedron() && lod->getNumVolumeFaces() > 0)
|
||||
{
|
||||
volume->copyVolumeFaces(lod);
|
||||
group->derefLOD(lod);
|
||||
return;
|
||||
}
|
||||
|
||||
group->derefLOD(lod);
|
||||
}
|
||||
|
||||
//no lower LOD is a available, is a higher lod available?
|
||||
for (S32 i = detail+1; i < 4; ++i)
|
||||
{
|
||||
LLVolume* lod = group->refLOD(i);
|
||||
if (lod && !lod->isTetrahedron() && lod->getNumVolumeFaces() > 0)
|
||||
{
|
||||
volume->copyVolumeFaces(lod);
|
||||
group->derefLOD(lod);
|
||||
return;
|
||||
}
|
||||
|
||||
group->derefLOD(lod);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
llerrs << "WTF?" << llendl;
|
||||
}
|
||||
|
||||
//nothing found, so make a tetrahedron
|
||||
volume->makeTetrahedron();
|
||||
}
|
||||
}
|
||||
|
||||
//static
|
||||
void LLPipeline::getMeshAssetCallback(LLVFS *vfs,
|
||||
const LLUUID& asset_uuid,
|
||||
LLAssetType::EType type,
|
||||
void* user_data, S32 status, LLExtStat ext_status)
|
||||
{
|
||||
gPipeline.mPendingMeshes.push_back(new LLMeshThread(asset_uuid, (LLVolume*) user_data));
|
||||
}
|
||||
|
||||
|
||||
LLPipeline::LLMeshThread::LLMeshThread(LLUUID mesh_id, LLVolume* target, S32 detail)
|
||||
: LLThread("mesh_loading_thread")
|
||||
{
|
||||
mMeshID = mesh_id;
|
||||
mVolume = NULL;
|
||||
mDetail = target->getDetail();
|
||||
|
||||
if (detail == -1)
|
||||
{
|
||||
mDetailIndex = LLVolumeLODGroup::getVolumeDetailFromScale(target->getDetail());
|
||||
}
|
||||
else
|
||||
{
|
||||
mDetailIndex = detail;
|
||||
}
|
||||
|
||||
mTargetVolume = target;
|
||||
}
|
||||
|
||||
LLPipeline::LLMeshThread::~LLMeshThread()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void LLPipeline::LLMeshThread::run()
|
||||
{
|
||||
if (!gAssetStorage || LLApp::instance()->isQuitting())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
char* buffer = NULL;
|
||||
S32 size = 0;
|
||||
|
||||
LLVFS* vfs = gAssetStorage->mVFS;
|
||||
|
||||
{
|
||||
LLVFile file(vfs, mMeshID, LLAssetType::AT_MESH, LLVFile::READ);
|
||||
file.waitForLock(VFSLOCK_READ);
|
||||
size = file.getSize();
|
||||
|
||||
if (size == 0)
|
||||
{
|
||||
gPipeline.meshLoaded(this);
|
||||
return;
|
||||
}
|
||||
|
||||
buffer = new char[size];
|
||||
file.read((U8*)&buffer[0], size);
|
||||
}
|
||||
|
||||
{
|
||||
std::string buffer_string(buffer, size);
|
||||
std::istringstream buffer_stream(buffer_string);
|
||||
|
||||
{
|
||||
LLVolumeParams volume_params;
|
||||
volume_params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE);
|
||||
volume_params.setSculptID(mMeshID, LL_SCULPT_TYPE_MESH);
|
||||
mVolume = new LLVolume(volume_params, mDetail);
|
||||
mVolume->createVolumeFacesFromStream(buffer_stream);
|
||||
}
|
||||
}
|
||||
delete[] buffer;
|
||||
|
||||
gPipeline.meshLoaded(this);
|
||||
}
|
||||
|
||||
void LLPipeline::meshLoaded(LLPipeline::LLMeshThread* mesh_thread)
|
||||
{
|
||||
LLMutexLock lock(mMeshMutex);
|
||||
mLoadedMeshes.push_back(mesh_thread);
|
||||
}
|
||||
|
||||
void LLPipeline::notifyLoadedMeshes()
|
||||
{ //called from main thread
|
||||
|
||||
U32 max_thread_count = llmax(gSavedSettings.getU32("MeshThreadCount"), (U32) 1);
|
||||
while (mMeshThreadCount < max_thread_count && !mPendingMeshes.empty())
|
||||
{
|
||||
LLMeshThread* mesh_thread = mPendingMeshes.front();
|
||||
mesh_thread->start();
|
||||
++mMeshThreadCount;
|
||||
mPendingMeshes.pop_front();
|
||||
}
|
||||
|
||||
LLMutexLock lock(mMeshMutex);
|
||||
std::list<LLMeshThread*> stopping_threads;
|
||||
|
||||
for (std::list<LLMeshThread*>::iterator iter = mLoadedMeshes.begin(); iter != mLoadedMeshes.end(); ++iter)
|
||||
{ //for each mesh done loading
|
||||
LLMeshThread* mesh = *iter;
|
||||
|
||||
if (!mesh->isStopped())
|
||||
{ //don't process a LLMeshThread until it's stopped
|
||||
stopping_threads.push_back(mesh);
|
||||
continue;
|
||||
}
|
||||
|
||||
S32 detail = mesh->mDetailIndex;
|
||||
|
||||
//get list of objects waiting to be notified this mesh is loaded
|
||||
mesh_load_map::iterator obj_iter = mLoadingMeshes[detail].find(mesh->mMeshID);
|
||||
|
||||
if (mesh->mVolume && obj_iter != mLoadingMeshes[detail].end())
|
||||
{
|
||||
//make sure target volume is still valid
|
||||
BOOL valid = FALSE;
|
||||
|
||||
for (std::set<LLUUID>::iterator vobj_iter = obj_iter->second.begin(); vobj_iter != obj_iter->second.end(); ++vobj_iter)
|
||||
{
|
||||
LLVOVolume* vobj = (LLVOVolume*) gObjectList.findObject(*vobj_iter);
|
||||
|
||||
if (vobj)
|
||||
{
|
||||
if (vobj->getVolume() == mesh->mTargetVolume)
|
||||
{
|
||||
valid = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (valid)
|
||||
{
|
||||
if (mesh->mVolume->getNumVolumeFaces() <= 0)
|
||||
{
|
||||
llwarns << "Mesh loading returned empty volume." << llendl;
|
||||
mesh->mVolume->makeTetrahedron();
|
||||
}
|
||||
|
||||
mesh->mTargetVolume->copyVolumeFaces(mesh->mVolume);
|
||||
|
||||
for (std::set<LLUUID>::iterator vobj_iter = obj_iter->second.begin(); vobj_iter != obj_iter->second.end(); ++vobj_iter)
|
||||
{
|
||||
LLVOVolume* vobj = (LLVOVolume*) gObjectList.findObject(*vobj_iter);
|
||||
if (vobj)
|
||||
{
|
||||
vobj->notifyMeshLoaded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mLoadingMeshes[detail].erase(mesh->mMeshID);
|
||||
}
|
||||
|
||||
delete mesh;
|
||||
--mMeshThreadCount;
|
||||
}
|
||||
|
||||
mLoadedMeshes = stopping_threads;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ class LLCubeMap;
|
|||
class LLCullResult;
|
||||
class LLVOAvatar;
|
||||
class LLGLSLShader;
|
||||
class LLCurlRequest;
|
||||
class LLMeshResponder;
|
||||
|
||||
typedef enum e_avatar_skinning_method
|
||||
{
|
||||
|
|
@ -278,10 +280,6 @@ public:
|
|||
LLCullResult::sg_list_t::iterator beginAlphaGroups();
|
||||
LLCullResult::sg_list_t::iterator endAlphaGroups();
|
||||
|
||||
|
||||
//mesh management functions
|
||||
void loadMesh(LLVOVolume* volume, LLUUID mesh_id, S32 detail = 0);
|
||||
|
||||
void addTrianglesDrawn(S32 count);
|
||||
BOOL hasRenderType(const U32 type) const { return (type && (mRenderTypeMask & (1<<type))) ? TRUE : FALSE; }
|
||||
BOOL hasRenderDebugFeatureMask(const U32 mask) const { return (mRenderDebugFeatureMask & mask) ? TRUE : FALSE; }
|
||||
|
|
@ -683,32 +681,8 @@ protected:
|
|||
typedef std::map<LLUUID, std::set<LLUUID> > mesh_load_map;
|
||||
mesh_load_map mLoadingMeshes[4];
|
||||
|
||||
LLMutex* mMeshMutex;
|
||||
|
||||
class LLMeshThread : public LLThread
|
||||
{
|
||||
public:
|
||||
LLPointer<LLVolume> mVolume;
|
||||
LLVolume* mTargetVolume;
|
||||
LLUUID mMeshID;
|
||||
F32 mDetail;
|
||||
S32 mDetailIndex;
|
||||
LLMeshThread(LLUUID mesh_id, LLVolume* target, S32 detail = -1);
|
||||
~LLMeshThread();
|
||||
void run();
|
||||
};
|
||||
|
||||
static void getMeshAssetCallback(LLVFS *vfs,
|
||||
const LLUUID& asset_uuid,
|
||||
LLAssetType::EType type,
|
||||
void* user_data, S32 status, LLExtStat ext_status);
|
||||
|
||||
std::list<LLMeshThread*> mLoadedMeshes;
|
||||
std::list<LLMeshThread*> mPendingMeshes;
|
||||
U32 mMeshThreadCount;
|
||||
|
||||
void meshLoaded(LLMeshThread* mesh_thread);
|
||||
void notifyLoadedMeshes();
|
||||
typedef std::list<LLMeshResponder*> mesh_response_list;
|
||||
mesh_response_list mMeshResponseList;
|
||||
|
||||
LLPointer<LLViewerFetchedTexture> mFaceSelectImagep;
|
||||
LLPointer<LLViewerTexture> mBloomImagep;
|
||||
|
|
|
|||
Loading…
Reference in New Issue