2012-03-22 01:36:20 +08:00
|
|
|
/* -*-c++-*- OpenSceneGraph - Copyright (C) 1998-2006 Robert Osfield
|
2005-02-24 21:33:35 +08:00
|
|
|
* Copyright (C) 2003-2005 3Dlabs Inc. Ltd.
|
2005-03-24 17:37:45 +08:00
|
|
|
* Copyright (C) 2004-2005 Nathan Cournia
|
2008-01-08 22:29:44 +08:00
|
|
|
* Copyright (C) 2008 Zebra Imaging
|
2010-11-23 22:50:31 +08:00
|
|
|
* Copyright (C) 2010 VIRES Simulationstechnologie GmbH
|
2005-02-24 21:33:35 +08:00
|
|
|
*
|
2012-03-22 01:36:20 +08:00
|
|
|
* This application is open source and may be redistributed and/or modified
|
2010-11-22 19:22:03 +08:00
|
|
|
* freely and without restriction, both in commercial and non commercial
|
2005-02-24 21:33:35 +08:00
|
|
|
* applications, as long as this copyright notice is maintained.
|
2012-03-22 01:36:20 +08:00
|
|
|
*
|
2005-02-24 21:33:35 +08:00
|
|
|
* This application is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
|
|
*/
|
|
|
|
|
2005-06-20 18:36:06 +08:00
|
|
|
/* file: include/osg/Shader
|
2008-01-08 22:29:44 +08:00
|
|
|
* author: Mike Weiblen 2008-01-02
|
2010-11-23 22:50:31 +08:00
|
|
|
* Holger Helmich 2010-10-21
|
2005-02-24 21:33:35 +08:00
|
|
|
*/
|
|
|
|
|
|
|
|
#ifndef OSG_SHADER
|
|
|
|
#define OSG_SHADER 1
|
|
|
|
|
2005-05-12 03:59:21 +08:00
|
|
|
|
2014-12-10 18:38:12 +08:00
|
|
|
#include <osg/GLExtensions>
|
2005-05-12 04:37:22 +08:00
|
|
|
#include <osg/Object>
|
|
|
|
#include <osg/buffered_value>
|
2005-05-12 03:59:21 +08:00
|
|
|
|
2005-03-24 17:37:45 +08:00
|
|
|
#include <set>
|
2010-07-06 00:32:58 +08:00
|
|
|
#include <map>
|
2005-03-24 17:37:45 +08:00
|
|
|
|
2005-02-24 21:33:35 +08:00
|
|
|
namespace osg {
|
|
|
|
|
2005-06-20 18:36:06 +08:00
|
|
|
class Program;
|
|
|
|
|
2015-02-11 01:04:02 +08:00
|
|
|
// set of shader define strings that the shader is dependent upon.
|
|
|
|
typedef std::set<std::string> ShaderDefines;
|
|
|
|
|
2012-03-22 01:36:20 +08:00
|
|
|
/** Simple class for wrapping up the data used in OpenGL ES 2's glShaderBinary calls.
|
2009-11-16 20:32:41 +08:00
|
|
|
* ShaderBinary is set up with the binary data then assigned to one or more osg::Shader. */
|
|
|
|
class OSG_EXPORT ShaderBinary : public osg::Object
|
|
|
|
{
|
|
|
|
public:
|
2009-11-18 19:25:28 +08:00
|
|
|
|
2009-11-16 20:32:41 +08:00
|
|
|
ShaderBinary();
|
|
|
|
|
|
|
|
/** Copy constructor using CopyOp to manage deep vs shallow copy.*/
|
|
|
|
ShaderBinary(const ShaderBinary& rhs, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY);
|
|
|
|
|
|
|
|
META_Object(osg, ShaderBinary);
|
|
|
|
|
|
|
|
/** Allocated a data buffer of specified size/*/
|
|
|
|
void allocate(unsigned int size);
|
2009-11-18 19:25:28 +08:00
|
|
|
|
2009-11-16 20:32:41 +08:00
|
|
|
/** Assign shader binary data, copying the specified data into locally stored data buffer, the original data can then be deleted.*/
|
|
|
|
void assign(unsigned int size, const unsigned char* data);
|
2009-11-18 19:25:28 +08:00
|
|
|
|
2009-11-16 20:32:41 +08:00
|
|
|
/** Get the size of the shader binary data.*/
|
2013-05-15 00:12:21 +08:00
|
|
|
unsigned int getSize() const { return static_cast<unsigned int>(_data.size()); }
|
2009-11-16 20:32:41 +08:00
|
|
|
|
|
|
|
/** Get a ptr to the shader binary data.*/
|
|
|
|
unsigned char* getData() { return _data.empty() ? 0 : &(_data.front()); }
|
|
|
|
|
|
|
|
/** Get a const ptr to the shader binary data.*/
|
|
|
|
const unsigned char* getData() const { return _data.empty() ? 0 : &(_data.front()); }
|
2009-11-18 19:25:28 +08:00
|
|
|
|
|
|
|
/** Read shader binary from file.
|
|
|
|
* Return the resulting Shader or 0 if no valid shader binary could be read.*/
|
|
|
|
static ShaderBinary* readShaderBinaryFile(const std::string& fileName);
|
|
|
|
|
2009-11-16 20:32:41 +08:00
|
|
|
protected:
|
2009-11-18 19:25:28 +08:00
|
|
|
|
2009-11-16 20:32:41 +08:00
|
|
|
typedef std::vector<unsigned char> Data;
|
|
|
|
Data _data;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2005-02-24 21:33:35 +08:00
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
/** osg::Shader is an application-level abstraction of an OpenGL glShader.
|
|
|
|
* It is a container to load the shader source code text and manage its
|
|
|
|
* compilation.
|
2005-03-24 17:37:45 +08:00
|
|
|
* An osg::Shader may be attached to more than one osg::Program.
|
2005-02-24 21:33:35 +08:00
|
|
|
* Shader will automatically manage per-context instancing of the
|
|
|
|
* internal objects, if that is necessary for a particular display
|
|
|
|
* configuration.
|
|
|
|
*/
|
|
|
|
|
2005-04-12 01:14:17 +08:00
|
|
|
class OSG_EXPORT Shader : public osg::Object
|
2005-02-24 21:33:35 +08:00
|
|
|
{
|
|
|
|
public:
|
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
enum Type {
|
|
|
|
VERTEX = GL_VERTEX_SHADER,
|
2010-11-23 22:50:31 +08:00
|
|
|
TESSCONTROL = GL_TESS_CONTROL_SHADER,
|
|
|
|
TESSEVALUATION = GL_TESS_EVALUATION_SHADER,
|
2015-02-04 00:36:19 +08:00
|
|
|
GEOMETRY = GL_GEOMETRY_SHADER,
|
2010-11-23 22:50:31 +08:00
|
|
|
FRAGMENT = GL_FRAGMENT_SHADER,
|
2013-01-25 19:54:03 +08:00
|
|
|
COMPUTE = GL_COMPUTE_SHADER,
|
2005-04-30 15:02:02 +08:00
|
|
|
UNDEFINED = -1
|
|
|
|
};
|
2005-02-24 21:33:35 +08:00
|
|
|
|
2009-11-16 20:32:41 +08:00
|
|
|
Shader(Type type = UNDEFINED);
|
|
|
|
Shader(Type type, const std::string& source );
|
|
|
|
Shader(Type type, ShaderBinary* shaderBinary );
|
2005-02-24 21:33:35 +08:00
|
|
|
|
|
|
|
/** Copy constructor using CopyOp to manage deep vs shallow copy.*/
|
|
|
|
Shader(const Shader& rhs, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY);
|
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
META_Object(osg, Shader);
|
2005-03-24 17:37:45 +08:00
|
|
|
|
2005-02-24 21:33:35 +08:00
|
|
|
int compare(const Shader& rhs) const;
|
|
|
|
|
2009-11-16 20:32:41 +08:00
|
|
|
/** Set the Shader type as an enum. */
|
|
|
|
bool setType(Type t);
|
2005-02-24 21:33:35 +08:00
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
/** Get the Shader type as an enum. */
|
|
|
|
inline Type getType() const { return _type; }
|
2005-02-24 21:33:35 +08:00
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
/** Get the Shader type as a descriptive string. */
|
|
|
|
const char* getTypename() const;
|
2012-03-22 01:36:20 +08:00
|
|
|
|
2005-02-24 21:33:35 +08:00
|
|
|
|
2008-03-04 22:04:48 +08:00
|
|
|
/** Set file name for the shader source code. */
|
|
|
|
inline void setFileName(const std::string& fileName) { _shaderFileName = fileName; }
|
|
|
|
|
|
|
|
/** Get filename to which the shader source code belongs. */
|
|
|
|
inline const std::string& getFileName() const { return _shaderFileName; }
|
|
|
|
|
2009-11-16 20:32:41 +08:00
|
|
|
|
|
|
|
/** Set the Shader's source code text from a string. */
|
|
|
|
void setShaderSource(const std::string& sourceText);
|
|
|
|
|
|
|
|
/** Query the shader's source code text */
|
|
|
|
inline const std::string& getShaderSource() const { return _shaderSource; }
|
|
|
|
|
|
|
|
|
2015-02-11 01:04:02 +08:00
|
|
|
enum ShaderDefinesMode
|
|
|
|
{
|
2015-04-13 18:43:56 +08:00
|
|
|
USE_SHADER_PRAGMA,
|
2015-02-11 01:04:02 +08:00
|
|
|
USE_MANUAL_SETTINGS
|
|
|
|
};
|
|
|
|
|
|
|
|
void setShaderDefinesMode(ShaderDefinesMode sdm) { _shaderDefinesMode = sdm; }
|
|
|
|
ShaderDefinesMode getShaderDefinesMode() const { return _shaderDefinesMode; }
|
|
|
|
|
|
|
|
|
|
|
|
void setShaderDefines(const ShaderDefines& shaderDefs) { _shaderDefines = shaderDefs; }
|
|
|
|
ShaderDefines& getShaderDefines() { return _shaderDefines; }
|
|
|
|
const ShaderDefines& getShaderDefines() const { return _shaderDefines; }
|
|
|
|
|
|
|
|
void setShaderRequirements(const ShaderDefines& shaderDefs) { _shaderRequirements = shaderDefs; }
|
|
|
|
ShaderDefines& getShaderRequirements() { return _shaderRequirements; }
|
|
|
|
const ShaderDefines& getShaderRequirements() const { return _shaderRequirements; }
|
|
|
|
|
|
|
|
|
2009-11-16 20:32:41 +08:00
|
|
|
/** Set the Shader using a ShaderBinary. */
|
2012-03-22 01:36:20 +08:00
|
|
|
void setShaderBinary(ShaderBinary* shaderBinary) { _shaderBinary = shaderBinary; }
|
|
|
|
|
2009-11-16 20:32:41 +08:00
|
|
|
/** Get the Shader's ShaderBinary, return NULL if none is assigned. */
|
2012-03-22 01:36:20 +08:00
|
|
|
ShaderBinary* getShaderBinary() { return _shaderBinary.get(); }
|
2009-11-16 20:32:41 +08:00
|
|
|
|
|
|
|
/** Get the const Shader's ShaderBinary, return NULL if none is assigned. */
|
2012-03-22 01:36:20 +08:00
|
|
|
const ShaderBinary* getShaderBinary() const { return _shaderBinary.get(); }
|
2009-11-16 20:32:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
/** Read shader source from file and then constructor shader of specified type.
|
2009-11-18 19:25:28 +08:00
|
|
|
* Return the resulting Shader or 0 if no valid shader source could be read.*/
|
2009-11-16 20:32:41 +08:00
|
|
|
static Shader* readShaderFile( Type type, const std::string& fileName );
|
|
|
|
|
|
|
|
/** Load the Shader's source code text from a file. */
|
|
|
|
bool loadShaderSourceFromFile( const std::string& fileName );
|
|
|
|
|
|
|
|
|
2010-07-06 00:32:58 +08:00
|
|
|
/** The code injection map used when generating the main shader during main shader composition.*/
|
|
|
|
typedef std::multimap<float, std::string> CodeInjectionMap;
|
|
|
|
|
|
|
|
/** Add code injection that will be placed in the main shader to enable support for this shader.
|
|
|
|
* The position is set up so that code to be inserted before the main() will have a negative value,
|
|
|
|
* a position between 0 and 1.0 will be inserted in main() and a position greater than 1.0 will
|
|
|
|
* be placed after the main().
|
|
|
|
* During shader composition all the code injections are sorted in ascending order and then
|
|
|
|
* placed in the appropriate section of the main shader. */
|
|
|
|
void addCodeInjection(float position, const std::string& code) { _codeInjectionMap.insert(CodeInjectionMap::value_type(position, code)); }
|
|
|
|
|
|
|
|
/** Get the code injection map.*/
|
|
|
|
CodeInjectionMap& getCodeInjectionMap() { return _codeInjectionMap; }
|
|
|
|
|
|
|
|
/** Get the const code injection map.*/
|
|
|
|
const CodeInjectionMap& getCodeInjectionMap() const { return _codeInjectionMap; }
|
|
|
|
|
|
|
|
|
2009-11-16 20:32:41 +08:00
|
|
|
|
2007-01-04 22:11:51 +08:00
|
|
|
/** Resize any per context GLObject buffers to specified size. */
|
|
|
|
virtual void resizeGLObjectBuffers(unsigned int maxSize);
|
|
|
|
|
2006-01-17 01:05:17 +08:00
|
|
|
/** release OpenGL objects in specified graphics context if State
|
|
|
|
object is passed, otherwise release OpenGL objects for all graphics context if
|
|
|
|
State object pointer NULL.*/
|
|
|
|
void releaseGLObjects(osg::State* state=0) const;
|
|
|
|
|
2005-03-24 17:37:45 +08:00
|
|
|
/** Mark our PCSs as needing recompilation.
|
2005-04-30 15:02:02 +08:00
|
|
|
* Also mark Programs that depend on us as needing relink */
|
2012-03-22 01:36:20 +08:00
|
|
|
void dirtyShader();
|
2005-02-24 21:33:35 +08:00
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
/** If needed, compile the PCS's glShader */
|
2009-10-17 00:26:27 +08:00
|
|
|
void compileShader(osg::State& state) const;
|
2005-02-24 21:33:35 +08:00
|
|
|
|
2005-03-24 17:37:45 +08:00
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
/** Mark internal glShader for deletion.
|
2007-12-11 01:30:18 +08:00
|
|
|
* Deletion requests are queued until they can be executed
|
2005-04-30 15:02:02 +08:00
|
|
|
* in the proper GL context. */
|
|
|
|
static void deleteGlShader(unsigned int contextID, GLuint shader);
|
2005-03-24 17:37:45 +08:00
|
|
|
|
|
|
|
/** flush all the cached glShaders which need to be deleted
|
|
|
|
* in the OpenGL context related to contextID.*/
|
|
|
|
static void flushDeletedGlShaders(unsigned int contextID,double currentTime, double& availableTime);
|
2005-02-24 21:33:35 +08:00
|
|
|
|
2008-01-08 21:24:29 +08:00
|
|
|
/** discard all the cached glShaders which need to be deleted in the OpenGL context related to contextID.
|
|
|
|
* Note, unlike flush no OpenGL calls are made, instead the handles are all removed.
|
|
|
|
* this call is useful for when an OpenGL context has been destroyed. */
|
|
|
|
static void discardDeletedGlShaders(unsigned int contextID);
|
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
static Shader::Type getTypeId( const std::string& tname );
|
2005-04-08 04:23:58 +08:00
|
|
|
|
2010-03-05 19:30:50 +08:00
|
|
|
public:
|
2005-04-30 15:02:02 +08:00
|
|
|
/** PerContextShader (PCS) is an OSG-internal encapsulation of glShader per-GL context. */
|
2010-03-11 22:48:54 +08:00
|
|
|
class OSG_EXPORT PerContextShader : public osg::Referenced
|
2005-04-30 15:02:02 +08:00
|
|
|
{
|
|
|
|
public:
|
|
|
|
PerContextShader(const Shader* shader, unsigned int contextID);
|
|
|
|
|
2015-02-11 01:04:02 +08:00
|
|
|
void setDefineString(const std::string& defStr) { _defineStr = defStr; }
|
|
|
|
const std::string& getDefineString() const { return _defineStr; }
|
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
GLuint getHandle() const {return _glShaderHandle;}
|
|
|
|
|
|
|
|
void requestCompile();
|
2009-10-17 00:26:27 +08:00
|
|
|
void compileShader(osg::State& state);
|
2005-05-13 04:59:53 +08:00
|
|
|
bool needsCompile() const {return _needsCompile;}
|
|
|
|
bool isCompiled() const {return _isCompiled;}
|
2005-04-30 15:02:02 +08:00
|
|
|
bool getInfoLog( std::string& infoLog ) const;
|
|
|
|
|
|
|
|
/** Attach our glShader to a glProgram */
|
|
|
|
void attachShader(GLuint program) const;
|
|
|
|
|
|
|
|
/** Detach our glShader from a glProgram */
|
|
|
|
void detachShader(GLuint program) const;
|
|
|
|
|
|
|
|
protected: /*methods*/
|
|
|
|
~PerContextShader();
|
|
|
|
|
|
|
|
protected: /*data*/
|
|
|
|
/** Pointer to our parent osg::Shader */
|
|
|
|
const Shader* _shader;
|
2015-02-11 01:04:02 +08:00
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
/** Pointer to this context's extension functions. */
|
2014-12-10 18:38:12 +08:00
|
|
|
osg::ref_ptr<osg::GLExtensions> _extensions;
|
2015-02-11 01:04:02 +08:00
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
/** Handle to the actual glShader. */
|
|
|
|
GLuint _glShaderHandle;
|
2015-02-11 01:04:02 +08:00
|
|
|
|
|
|
|
/** Define string passed on to Shaders to help configure them.*/
|
|
|
|
std::string _defineStr;
|
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
/** Does our glShader need to be recompiled? */
|
|
|
|
bool _needsCompile;
|
2015-02-11 01:04:02 +08:00
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
/** Is our glShader successfully compiled? */
|
|
|
|
bool _isCompiled;
|
2015-02-11 01:04:02 +08:00
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
const unsigned int _contextID;
|
|
|
|
|
|
|
|
private:
|
|
|
|
PerContextShader(); // disallowed
|
|
|
|
PerContextShader(const PerContextShader&); // disallowed
|
|
|
|
PerContextShader& operator=(const PerContextShader&); // disallowed
|
|
|
|
};
|
|
|
|
|
2015-02-11 01:04:02 +08:00
|
|
|
|
|
|
|
struct OSG_EXPORT ShaderObjects : public osg::Referenced
|
|
|
|
{
|
|
|
|
typedef std::vector< osg::ref_ptr<PerContextShader> > PerContextShaders;
|
|
|
|
|
|
|
|
ShaderObjects(const Shader* shader, unsigned int contextID);
|
|
|
|
|
|
|
|
unsigned int _contextID;
|
|
|
|
const Shader* _shader;
|
|
|
|
mutable PerContextShaders _perContextShaders;
|
|
|
|
|
|
|
|
PerContextShader* getPCS(const std::string& defineStr) const;
|
|
|
|
PerContextShader* createPerContextShader(const std::string& defineStr);
|
|
|
|
void requestCompile();
|
|
|
|
};
|
|
|
|
|
|
|
|
PerContextShader* getPCS(osg::State& state) const;
|
2010-03-05 19:30:50 +08:00
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
protected: /*methods*/
|
2005-02-24 21:33:35 +08:00
|
|
|
virtual ~Shader();
|
|
|
|
|
2005-03-24 17:37:45 +08:00
|
|
|
|
2005-06-20 18:36:06 +08:00
|
|
|
friend class osg::Program;
|
|
|
|
bool addProgramRef( osg::Program* program );
|
|
|
|
bool removeProgramRef( osg::Program* program );
|
2005-04-30 15:02:02 +08:00
|
|
|
|
2015-02-11 01:04:02 +08:00
|
|
|
void _computeShaderDefines();
|
|
|
|
void _parseShaderDefines(const std::string& str, ShaderDefines& defines);
|
|
|
|
|
|
|
|
|
|
|
|
protected: /*data*/
|
2009-11-16 20:32:41 +08:00
|
|
|
Type _type;
|
|
|
|
std::string _shaderFileName;
|
|
|
|
std::string _shaderSource;
|
|
|
|
osg::ref_ptr<ShaderBinary> _shaderBinary;
|
2010-07-06 00:32:58 +08:00
|
|
|
|
|
|
|
CodeInjectionMap _codeInjectionMap;
|
2012-03-22 01:36:20 +08:00
|
|
|
|
2015-02-11 01:04:02 +08:00
|
|
|
// ShaderDefines variables
|
|
|
|
ShaderDefinesMode _shaderDefinesMode;
|
|
|
|
ShaderDefines _shaderDefines;
|
|
|
|
ShaderDefines _shaderRequirements;
|
|
|
|
|
2005-04-30 15:02:02 +08:00
|
|
|
/** osg::Programs that this osg::Shader is attached to */
|
2005-06-20 18:36:06 +08:00
|
|
|
typedef std::set< osg::Program* > ProgramSet;
|
2009-11-16 20:32:41 +08:00
|
|
|
ProgramSet _programSet;
|
2015-02-11 01:04:02 +08:00
|
|
|
mutable osg::buffered_value< osg::ref_ptr<ShaderObjects> > _pcsList;
|
2005-02-24 21:33:35 +08:00
|
|
|
|
|
|
|
private:
|
2005-04-30 15:02:02 +08:00
|
|
|
Shader& operator=(const Shader&); // disallowed
|
2005-02-24 21:33:35 +08:00
|
|
|
};
|
|
|
|
|
2010-07-02 20:04:20 +08:00
|
|
|
|
|
|
|
class OSG_EXPORT ShaderComponent : public osg::Object
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
|
|
|
|
ShaderComponent();
|
|
|
|
ShaderComponent(const ShaderComponent& sc,const CopyOp& copyop=CopyOp::SHALLOW_COPY);
|
|
|
|
|
|
|
|
META_Object(osg, ShaderComponent)
|
|
|
|
|
|
|
|
unsigned int addShader(osg::Shader* shader);
|
|
|
|
void removeShader(unsigned int i);
|
|
|
|
|
|
|
|
osg::Shader* getShader(unsigned int i) { return _shaders[i].get(); }
|
|
|
|
const osg::Shader* getShader(unsigned int i) const { return _shaders[i].get(); }
|
|
|
|
|
2013-05-15 00:12:21 +08:00
|
|
|
unsigned int getNumShaders() const { return static_cast<unsigned int>(_shaders.size()); }
|
2010-07-02 20:04:20 +08:00
|
|
|
|
2010-07-06 20:19:26 +08:00
|
|
|
virtual void compileGLObjects(State& state) const;
|
|
|
|
virtual void resizeGLObjectBuffers(unsigned int maxSize);
|
|
|
|
virtual void releaseGLObjects(State* state=0) const;
|
|
|
|
|
2010-07-02 20:04:20 +08:00
|
|
|
protected:
|
|
|
|
|
|
|
|
typedef std::vector< osg::ref_ptr<osg::Shader> > Shaders;
|
|
|
|
Shaders _shaders;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2005-02-24 21:33:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#endif
|