2011-07-28 14:26:00 +00:00
|
|
|
/*
|
2014-07-25 14:32:33 +00:00
|
|
|
* Copyright 2014 Google Inc.
|
2011-07-28 14:26:00 +00:00
|
|
|
*
|
|
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
|
|
* found in the LICENSE file.
|
2011-03-30 21:26:44 +00:00
|
|
|
*/
|
|
|
|
|
2014-07-25 15:35:45 +00:00
|
|
|
#ifndef GrGpuResource_DEFINED
|
|
|
|
#define GrGpuResource_DEFINED
|
2011-03-30 21:26:44 +00:00
|
|
|
|
2014-10-08 15:40:09 +00:00
|
|
|
#include "GrResourceKey.h"
|
|
|
|
#include "GrTypesPriv.h"
|
2014-12-11 22:59:31 +00:00
|
|
|
#include "SkData.h"
|
2012-09-04 13:34:32 +00:00
|
|
|
|
2011-11-15 19:42:07 +00:00
|
|
|
class GrContext;
|
2014-11-03 16:47:23 +00:00
|
|
|
class GrGpu;
|
2015-02-11 18:49:59 +00:00
|
|
|
class GrResourceCache;
|
2015-09-15 21:16:10 +00:00
|
|
|
class SkTraceMemoryDump;
|
2011-03-30 21:26:44 +00:00
|
|
|
|
2012-04-27 17:24:09 +00:00
|
|
|
/**
|
2014-09-03 21:05:49 +00:00
|
|
|
* Base class for GrGpuResource. Handles the various types of refs we need. Separated out as a base
|
|
|
|
* class to isolate the ref-cnting behavior and provide friendship without exposing all of
|
|
|
|
* GrGpuResource.
|
2015-01-13 16:22:43 +00:00
|
|
|
*
|
2014-09-03 21:05:49 +00:00
|
|
|
* Gpu resources can have three types of refs:
|
|
|
|
* 1) Normal ref (+ by ref(), - by unref()): These are used by code that is issuing draw calls
|
|
|
|
* that read and write the resource via GrDrawTarget and by any object that must own a
|
|
|
|
* GrGpuResource and is itself owned (directly or indirectly) by Skia-client code.
|
2014-10-08 15:40:09 +00:00
|
|
|
* 2) Pending read (+ by addPendingRead(), - by completedRead()): GrContext has scheduled a read
|
2014-09-03 21:05:49 +00:00
|
|
|
* of the resource by the GPU as a result of a skia API call but hasn't executed it yet.
|
2014-10-08 15:40:09 +00:00
|
|
|
* 3) Pending write (+ by addPendingWrite(), - by completedWrite()): GrContext has scheduled a
|
2014-09-03 21:05:49 +00:00
|
|
|
* write to the resource by the GPU as a result of a skia API call but hasn't executed it yet.
|
|
|
|
*
|
|
|
|
* The latter two ref types are private and intended only for Gr core code.
|
2014-10-08 15:40:09 +00:00
|
|
|
*
|
2015-04-08 18:01:54 +00:00
|
|
|
* When all the ref/io counts reach zero DERIVED::notifyAllCntsAreZero() will be called (static poly
|
|
|
|
* morphism using CRTP). Similarly when the ref (but not necessarily pending read/write) count
|
|
|
|
* reaches 0 DERIVED::notifyRefCountIsZero() will be called. In the case when an unref() causes both
|
|
|
|
* the ref cnt to reach zero and the other counts are zero, notifyRefCountIsZero() will be called
|
|
|
|
* before notifyIsPurgeable(). Moreover, if notifyRefCountIsZero() returns false then
|
|
|
|
* notifyAllRefCntsAreZero() won't be called at all. notifyRefCountIsZero() must return false if the
|
|
|
|
* object may be deleted after notifyRefCntIsZero() returns.
|
|
|
|
*
|
|
|
|
* GrIORef and GrGpuResource are separate classes for organizational reasons and to be
|
2014-10-08 15:40:09 +00:00
|
|
|
* able to give access via friendship to only the functions related to pending IO operations.
|
2012-04-27 17:24:09 +00:00
|
|
|
*/
|
2014-10-08 15:40:09 +00:00
|
|
|
template <typename DERIVED> class GrIORef : public SkNoncopyable {
|
2011-03-30 21:26:44 +00:00
|
|
|
public:
|
2014-09-03 21:05:49 +00:00
|
|
|
// Some of the signatures are written to mirror SkRefCnt so that GrGpuResource can work with
|
|
|
|
// templated helper classes (e.g. SkAutoTUnref). However, we have different categories of
|
|
|
|
// refs (e.g. pending reads). We also don't require thread safety as GrCacheable objects are
|
|
|
|
// not intended to cross thread boundaries.
|
|
|
|
void ref() const {
|
2014-10-07 19:07:38 +00:00
|
|
|
this->validate();
|
2014-10-08 15:40:09 +00:00
|
|
|
++fRefCnt;
|
2014-09-03 21:05:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void unref() const {
|
|
|
|
this->validate();
|
2015-06-26 18:45:03 +00:00
|
|
|
|
2015-04-08 18:01:54 +00:00
|
|
|
if (!(--fRefCnt)) {
|
|
|
|
if (!static_cast<const DERIVED*>(this)->notifyRefCountIsZero()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this->didRemoveRefOrPendingIO(kRef_CntType);
|
2014-09-03 21:05:49 +00:00
|
|
|
}
|
|
|
|
|
2014-07-25 14:32:33 +00:00
|
|
|
void validate() const {
|
2014-09-03 21:05:49 +00:00
|
|
|
#ifdef SK_DEBUG
|
|
|
|
SkASSERT(fRefCnt >= 0);
|
|
|
|
SkASSERT(fPendingReads >= 0);
|
|
|
|
SkASSERT(fPendingWrites >= 0);
|
2014-11-14 21:33:09 +00:00
|
|
|
SkASSERT(fRefCnt + fPendingReads + fPendingWrites >= 0);
|
2014-07-25 14:32:33 +00:00
|
|
|
#endif
|
2014-09-03 21:05:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
2014-10-09 16:57:18 +00:00
|
|
|
GrIORef() : fRefCnt(1), fPendingReads(0), fPendingWrites(0) { }
|
2014-09-03 21:05:49 +00:00
|
|
|
|
2015-04-08 18:01:54 +00:00
|
|
|
enum CntType {
|
|
|
|
kRef_CntType,
|
|
|
|
kPendingRead_CntType,
|
|
|
|
kPendingWrite_CntType,
|
|
|
|
};
|
|
|
|
|
2015-01-23 20:47:59 +00:00
|
|
|
bool isPurgeable() const { return !this->internalHasRef() && !this->internalHasPendingIO(); }
|
2014-11-14 21:33:09 +00:00
|
|
|
|
2014-09-22 19:21:08 +00:00
|
|
|
bool internalHasPendingRead() const { return SkToBool(fPendingReads); }
|
|
|
|
bool internalHasPendingWrite() const { return SkToBool(fPendingWrites); }
|
|
|
|
bool internalHasPendingIO() const { return SkToBool(fPendingWrites | fPendingReads); }
|
|
|
|
|
2014-11-11 15:27:16 +00:00
|
|
|
bool internalHasRef() const { return SkToBool(fRefCnt); }
|
|
|
|
|
2014-09-03 21:05:49 +00:00
|
|
|
private:
|
|
|
|
void addPendingRead() const {
|
|
|
|
this->validate();
|
|
|
|
++fPendingReads;
|
|
|
|
}
|
|
|
|
|
|
|
|
void completedRead() const {
|
|
|
|
this->validate();
|
|
|
|
--fPendingReads;
|
2015-04-08 18:01:54 +00:00
|
|
|
this->didRemoveRefOrPendingIO(kPendingRead_CntType);
|
2014-09-03 21:05:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void addPendingWrite() const {
|
|
|
|
this->validate();
|
|
|
|
++fPendingWrites;
|
|
|
|
}
|
|
|
|
|
|
|
|
void completedWrite() const {
|
|
|
|
this->validate();
|
|
|
|
--fPendingWrites;
|
2015-04-08 18:01:54 +00:00
|
|
|
this->didRemoveRefOrPendingIO(kPendingWrite_CntType);
|
2014-09-03 21:05:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2015-04-08 18:01:54 +00:00
|
|
|
void didRemoveRefOrPendingIO(CntType cntTypeRemoved) const {
|
2014-11-14 21:33:09 +00:00
|
|
|
if (0 == fPendingReads && 0 == fPendingWrites && 0 == fRefCnt) {
|
2015-04-08 18:01:54 +00:00
|
|
|
static_cast<const DERIVED*>(this)->notifyAllCntsAreZero(cntTypeRemoved);
|
2014-10-08 15:40:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-09-03 21:05:49 +00:00
|
|
|
mutable int32_t fRefCnt;
|
|
|
|
mutable int32_t fPendingReads;
|
|
|
|
mutable int32_t fPendingWrites;
|
|
|
|
|
2014-09-04 21:13:44 +00:00
|
|
|
// This class is used to manage conversion of refs to pending reads/writes.
|
2014-09-17 15:05:40 +00:00
|
|
|
friend class GrGpuResourceRef;
|
2015-02-11 18:49:59 +00:00
|
|
|
friend class GrResourceCache; // to check IO ref counts.
|
2014-10-08 15:40:09 +00:00
|
|
|
|
|
|
|
template <typename, GrIOType> friend class GrPendingIOResource;
|
2014-09-03 21:05:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2015-02-11 18:49:59 +00:00
|
|
|
* Base class for objects that can be kept in the GrResourceCache.
|
2014-09-03 21:05:49 +00:00
|
|
|
*/
|
2014-10-08 17:24:07 +00:00
|
|
|
class SK_API GrGpuResource : public GrIORef<GrGpuResource> {
|
2014-09-03 21:05:49 +00:00
|
|
|
public:
|
2015-09-15 21:16:10 +00:00
|
|
|
|
2012-06-05 19:35:09 +00:00
|
|
|
|
2015-01-14 18:42:08 +00:00
|
|
|
enum LifeCycle {
|
|
|
|
/**
|
|
|
|
* The resource is cached and owned by Skia. Resources with this status may be kept alive
|
2015-02-19 15:24:21 +00:00
|
|
|
* by the cache as either scratch or unique resources even when there are no refs to them.
|
2015-01-14 18:42:08 +00:00
|
|
|
* The cache may release them whenever there are no refs.
|
|
|
|
*/
|
|
|
|
kCached_LifeCycle,
|
2015-06-18 16:12:16 +00:00
|
|
|
|
2015-01-14 18:42:08 +00:00
|
|
|
/**
|
|
|
|
* The resource is uncached. As soon as there are no more refs to it, it is released. Under
|
|
|
|
* the hood the cache may opaquely recycle it as a cached resource.
|
|
|
|
*/
|
|
|
|
kUncached_LifeCycle,
|
2015-06-18 16:12:16 +00:00
|
|
|
|
2015-01-14 18:42:08 +00:00
|
|
|
/**
|
|
|
|
* Similar to uncached, but Skia does not manage the lifetime of the underlying backend
|
|
|
|
* 3D API object(s). The client is responsible for freeing those. Used to inject client-
|
|
|
|
* created GPU resources into Skia (e.g. to render to a client-created texture).
|
|
|
|
*/
|
2015-06-18 16:12:16 +00:00
|
|
|
kBorrowed_LifeCycle,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An external resource with ownership transfered into Skia. Skia will free the resource.
|
|
|
|
*/
|
|
|
|
kAdopted_LifeCycle,
|
2015-01-14 18:42:08 +00:00
|
|
|
};
|
|
|
|
|
2011-03-30 21:26:44 +00:00
|
|
|
/**
|
2014-05-02 21:38:22 +00:00
|
|
|
* Tests whether a object has been abandoned or released. All objects will
|
|
|
|
* be in this state after their creating GrContext is destroyed or has
|
|
|
|
* contextLost called. It's up to the client to test wasDestroyed() before
|
|
|
|
* attempting to use an object if it holds refs on objects across
|
2011-03-30 21:26:44 +00:00
|
|
|
* ~GrContext, freeResources with the force flag, or contextLost.
|
|
|
|
*
|
2014-05-02 21:38:22 +00:00
|
|
|
* @return true if the object has been released or abandoned,
|
2011-03-30 21:26:44 +00:00
|
|
|
* false otherwise.
|
|
|
|
*/
|
2014-05-02 21:38:22 +00:00
|
|
|
bool wasDestroyed() const { return NULL == fGpu; }
|
2011-03-30 21:26:44 +00:00
|
|
|
|
2011-07-26 12:32:36 +00:00
|
|
|
/**
|
2014-05-02 21:38:22 +00:00
|
|
|
* Retrieves the context that owns the object. Note that it is possible for
|
|
|
|
* this to return NULL. When objects have been release()ed or abandon()ed
|
|
|
|
* they no longer have an owning context. Destroying a GrContext
|
|
|
|
* automatically releases all its resources.
|
2013-01-23 21:37:01 +00:00
|
|
|
*/
|
2012-08-16 14:49:16 +00:00
|
|
|
const GrContext* getContext() const;
|
|
|
|
GrContext* getContext();
|
|
|
|
|
2014-07-25 14:32:33 +00:00
|
|
|
/**
|
|
|
|
* Retrieves the amount of GPU memory used by this resource in bytes. It is
|
|
|
|
* approximate since we aren't aware of additional padding or copies made
|
|
|
|
* by the driver.
|
|
|
|
*
|
|
|
|
* @return the amount of GPU memory used in bytes
|
|
|
|
*/
|
2014-11-12 19:13:39 +00:00
|
|
|
size_t gpuMemorySize() const {
|
|
|
|
if (kInvalidGpuMemorySize == fGpuMemorySize) {
|
|
|
|
fGpuMemorySize = this->onGpuMemorySize();
|
|
|
|
SkASSERT(kInvalidGpuMemorySize != fGpuMemorySize);
|
|
|
|
}
|
|
|
|
return fGpuMemorySize;
|
|
|
|
}
|
2014-07-25 14:32:33 +00:00
|
|
|
|
2013-01-23 20:25:22 +00:00
|
|
|
/**
|
2014-09-05 19:23:12 +00:00
|
|
|
* Gets an id that is unique for this GrGpuResource object. It is static in that it does
|
|
|
|
* not change when the content of the GrGpuResource object changes. This will never return
|
2014-07-25 14:32:33 +00:00
|
|
|
* 0.
|
2013-01-23 20:25:22 +00:00
|
|
|
*/
|
2014-07-25 14:32:33 +00:00
|
|
|
uint32_t getUniqueID() const { return fUniqueID; }
|
|
|
|
|
2015-02-19 15:24:21 +00:00
|
|
|
/** Returns the current unique key for the resource. It will be invalid if the resource has no
|
|
|
|
associated unique key. */
|
|
|
|
const GrUniqueKey& getUniqueKey() const { return fUniqueKey; }
|
2015-02-03 01:25:26 +00:00
|
|
|
|
2014-12-11 22:59:31 +00:00
|
|
|
/**
|
|
|
|
* Attach a custom data object to this resource. The data will remain attached
|
|
|
|
* for the lifetime of this resource (until it is abandoned or released).
|
|
|
|
* Takes a ref on data. Previously attached data, if any, is unrefed.
|
|
|
|
* Returns the data argument, for convenience.
|
|
|
|
*/
|
|
|
|
const SkData* setCustomData(const SkData* data);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the custom data object that was attached to this resource by
|
|
|
|
* calling setCustomData.
|
|
|
|
*/
|
|
|
|
const SkData* getCustomData() const { return fData.get(); }
|
|
|
|
|
2014-11-11 22:15:57 +00:00
|
|
|
/**
|
2015-02-13 22:20:05 +00:00
|
|
|
* Internal-only helper class used for manipulations of the resource by the cache.
|
2014-11-11 22:15:57 +00:00
|
|
|
*/
|
|
|
|
class CacheAccess;
|
|
|
|
inline CacheAccess cacheAccess();
|
|
|
|
inline const CacheAccess cacheAccess() const;
|
|
|
|
|
2015-02-13 22:20:05 +00:00
|
|
|
/**
|
|
|
|
* Internal-only helper class used for manipulations of the resource by internal code.
|
|
|
|
*/
|
|
|
|
class ResourcePriv;
|
|
|
|
inline ResourcePriv resourcePriv();
|
|
|
|
inline const ResourcePriv resourcePriv() const;
|
|
|
|
|
2014-12-11 18:32:32 +00:00
|
|
|
/**
|
|
|
|
* Removes references to objects in the underlying 3D API without freeing them.
|
|
|
|
* Called by CacheAccess.
|
|
|
|
* In general this method should not be called outside of skia. It was
|
|
|
|
* made by public for a special case where it needs to be called in Blink
|
|
|
|
* when a texture becomes unsafe to use after having been shared through
|
|
|
|
* a texture mailbox.
|
|
|
|
*/
|
|
|
|
void abandon();
|
|
|
|
|
2015-09-15 21:16:10 +00:00
|
|
|
/**
|
|
|
|
* Dumps memory usage information for this GrGpuResource to traceMemoryDump.
|
|
|
|
* Typically, subclasses should not need to override this, and should only
|
|
|
|
* need to override setMemoryBacking.
|
|
|
|
**/
|
|
|
|
virtual void dumpMemoryStatistics(SkTraceMemoryDump* traceMemoryDump) const;
|
|
|
|
|
2014-07-25 14:32:33 +00:00
|
|
|
protected:
|
2014-08-26 21:01:07 +00:00
|
|
|
// This must be called by every GrGpuObject. It should be called once the object is fully
|
|
|
|
// initialized (i.e. not in a base class constructor).
|
|
|
|
void registerWithCache();
|
|
|
|
|
2015-01-14 18:42:08 +00:00
|
|
|
GrGpuResource(GrGpu*, LifeCycle);
|
2014-07-25 15:35:45 +00:00
|
|
|
virtual ~GrGpuResource();
|
2012-04-27 17:24:09 +00:00
|
|
|
|
|
|
|
GrGpu* getGpu() const { return fGpu; }
|
2011-03-30 21:26:44 +00:00
|
|
|
|
2014-11-14 21:33:09 +00:00
|
|
|
/** Overridden to free GPU resources in the backend API. */
|
|
|
|
virtual void onRelease() { }
|
|
|
|
/** Overridden to abandon any internal handles, ptrs, etc to backend API resources.
|
|
|
|
This may be called when the underlying 3D context is no longer valid and so no
|
|
|
|
backend API calls should be made. */
|
|
|
|
virtual void onAbandon() { }
|
2011-03-30 21:26:44 +00:00
|
|
|
|
2015-06-18 16:12:16 +00:00
|
|
|
bool shouldFreeResources() const { return fLifeCycle != kBorrowed_LifeCycle; }
|
|
|
|
|
|
|
|
bool isExternal() const {
|
|
|
|
return GrGpuResource::kAdopted_LifeCycle == fLifeCycle ||
|
|
|
|
GrGpuResource::kBorrowed_LifeCycle == fLifeCycle;
|
|
|
|
}
|
2012-08-28 12:34:17 +00:00
|
|
|
|
2014-07-25 14:32:33 +00:00
|
|
|
/**
|
2014-11-12 19:13:39 +00:00
|
|
|
* This entry point should be called whenever gpuMemorySize() should report a different size.
|
|
|
|
* The cache will call gpuMemorySize() to update the current size of the resource.
|
2014-07-25 14:32:33 +00:00
|
|
|
*/
|
|
|
|
void didChangeGpuMemorySize() const;
|
|
|
|
|
2014-08-28 16:54:34 +00:00
|
|
|
/**
|
|
|
|
* Optionally called by the GrGpuResource subclass if the resource can be used as scratch.
|
|
|
|
* By default resources are not usable as scratch. This should only be called once.
|
|
|
|
**/
|
2014-12-30 20:50:52 +00:00
|
|
|
void setScratchKey(const GrScratchKey& scratchKey);
|
2014-08-28 16:54:34 +00:00
|
|
|
|
2015-09-15 21:16:10 +00:00
|
|
|
/**
|
|
|
|
* Allows subclasses to add additional backing information to the SkTraceMemoryDump. Called by
|
|
|
|
* onMemoryDump. The default implementation adds no backing information.
|
|
|
|
**/
|
|
|
|
virtual void setMemoryBacking(SkTraceMemoryDump*, const SkString&) const {}
|
|
|
|
|
2011-03-30 21:26:44 +00:00
|
|
|
private:
|
2014-11-14 21:33:09 +00:00
|
|
|
/**
|
|
|
|
* Frees the object in the underlying 3D API. Called by CacheAccess.
|
|
|
|
*/
|
|
|
|
void release();
|
|
|
|
|
2014-11-12 19:13:39 +00:00
|
|
|
virtual size_t onGpuMemorySize() const = 0;
|
|
|
|
|
2015-02-17 19:47:40 +00:00
|
|
|
// See comments in CacheAccess and ResourcePriv.
|
2015-02-19 16:24:16 +00:00
|
|
|
void setUniqueKey(const GrUniqueKey&);
|
2015-02-19 15:24:21 +00:00
|
|
|
void removeUniqueKey();
|
2015-04-08 18:01:54 +00:00
|
|
|
void notifyAllCntsAreZero(CntType) const;
|
|
|
|
bool notifyRefCountIsZero() const;
|
2014-11-25 13:52:06 +00:00
|
|
|
void removeScratchKey();
|
2015-01-16 15:32:33 +00:00
|
|
|
void makeBudgeted();
|
2015-01-23 15:19:22 +00:00
|
|
|
void makeUnbudgeted();
|
2014-10-08 15:40:09 +00:00
|
|
|
|
2013-08-28 14:17:03 +00:00
|
|
|
#ifdef SK_DEBUG
|
2012-09-04 13:34:32 +00:00
|
|
|
friend class GrGpu; // for assert in GrGpu to access getGpu
|
|
|
|
#endif
|
2011-03-30 21:26:44 +00:00
|
|
|
|
2014-07-25 14:32:33 +00:00
|
|
|
static uint32_t CreateUniqueID();
|
|
|
|
|
2015-02-17 23:09:34 +00:00
|
|
|
// An index into a heap when this resource is purgeable or an array when not. This is maintained
|
|
|
|
// by the cache.
|
2015-02-17 19:47:40 +00:00
|
|
|
int fCacheArrayIndex;
|
|
|
|
// This value reflects how recently this resource was accessed in the cache. This is maintained
|
|
|
|
// by the cache.
|
|
|
|
uint32_t fTimestamp;
|
2014-08-21 20:02:13 +00:00
|
|
|
|
2014-11-17 17:33:27 +00:00
|
|
|
static const size_t kInvalidGpuMemorySize = ~static_cast<size_t>(0);
|
2015-01-23 12:24:04 +00:00
|
|
|
GrScratchKey fScratchKey;
|
2015-02-19 15:24:21 +00:00
|
|
|
GrUniqueKey fUniqueKey;
|
2014-11-17 17:33:27 +00:00
|
|
|
|
|
|
|
// This is not ref'ed but abandon() or release() will be called before the GrGpu object
|
|
|
|
// is destroyed. Those calls set will this to NULL.
|
2015-01-23 12:24:04 +00:00
|
|
|
GrGpu* fGpu;
|
|
|
|
mutable size_t fGpuMemorySize;
|
2014-11-17 17:33:27 +00:00
|
|
|
|
2015-01-23 12:24:04 +00:00
|
|
|
LifeCycle fLifeCycle;
|
|
|
|
const uint32_t fUniqueID;
|
2014-08-28 16:54:34 +00:00
|
|
|
|
2015-01-23 12:24:04 +00:00
|
|
|
SkAutoTUnref<const SkData> fData;
|
2014-12-11 22:59:31 +00:00
|
|
|
|
2014-10-08 15:40:09 +00:00
|
|
|
typedef GrIORef<GrGpuResource> INHERITED;
|
2015-04-08 18:01:54 +00:00
|
|
|
friend class GrIORef<GrGpuResource>; // to access notifyAllCntsAreZero and notifyRefCntIsZero.
|
2011-03-30 21:26:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
#endif
|