1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 #ifndef RENDERNODEPROPERTIES_H
17 #define RENDERNODEPROPERTIES_H
18 
19 #include <algorithm>
20 #include <stddef.h>
21 #include <vector>
22 #include <cutils/compiler.h>
23 #include <androidfw/ResourceTypes.h>
24 #include <utils/Log.h>
25 
26 #include <SkCamera.h>
27 #include <SkMatrix.h>
28 #include <SkRegion.h>
29 #include <SkXfermode.h>
30 
31 #include "Caches.h"
32 #include "Rect.h"
33 #include "RevealClip.h"
34 #include "Outline.h"
35 #include "utils/MathUtils.h"
36 
37 class SkBitmap;
38 class SkColorFilter;
39 class SkPaint;
40 
41 namespace android {
42 namespace uirenderer {
43 
44 class Matrix4;
45 class RenderNode;
46 class RenderProperties;
47 
48 // The __VA_ARGS__ will be executed if a & b are not equal
49 #define RP_SET(a, b, ...) (a != b ? (a = b, ##__VA_ARGS__, true) : false)
50 #define RP_SET_AND_DIRTY(a, b) RP_SET(a, b, mPrimitiveFields.mMatrixOrPivotDirty = true)
51 
52 // Keep in sync with View.java:LAYER_TYPE_*
53 enum class LayerType {
54     None = 0,
55     // Although we cannot build the software layer directly (must be done at
56     // record time), this information is used when applying alpha.
57     Software = 1,
58     RenderLayer = 2,
59     // TODO: LayerTypeSurfaceTexture? Maybe?
60 };
61 
62 enum ClippingFlags {
63     CLIP_TO_BOUNDS =      0x1 << 0,
64     CLIP_TO_CLIP_BOUNDS = 0x1 << 1,
65 };
66 
67 class ANDROID_API LayerProperties {
68 public:
setType(LayerType type)69     bool setType(LayerType type) {
70         if (RP_SET(mType, type)) {
71             reset();
72             return true;
73         }
74         return false;
75     }
76 
setOpaque(bool opaque)77     bool setOpaque(bool opaque) {
78         return RP_SET(mOpaque, opaque);
79     }
80 
opaque()81     bool opaque() const {
82         return mOpaque;
83     }
84 
setAlpha(uint8_t alpha)85     bool setAlpha(uint8_t alpha) {
86         return RP_SET(mAlpha, alpha);
87     }
88 
alpha()89     uint8_t alpha() const {
90         return mAlpha;
91     }
92 
setXferMode(SkXfermode::Mode mode)93     bool setXferMode(SkXfermode::Mode mode) {
94         return RP_SET(mMode, mode);
95     }
96 
xferMode()97     SkXfermode::Mode xferMode() const {
98         return mMode;
99     }
100 
101     bool setColorFilter(SkColorFilter* filter);
102 
colorFilter()103     SkColorFilter* colorFilter() const {
104         return mColorFilter;
105     }
106 
107     // Sets alpha, xfermode, and colorfilter from an SkPaint
108     // paint may be NULL, in which case defaults will be set
109     bool setFromPaint(const SkPaint* paint);
110 
needsBlending()111     bool needsBlending() const {
112         return !opaque() || alpha() < 255;
113     }
114 
115     LayerProperties& operator=(const LayerProperties& other);
116 
117 private:
118     LayerProperties();
119     ~LayerProperties();
120     void reset();
121 
122     // Private since external users should go through properties().effectiveLayerType()
type()123     LayerType type() const {
124         return mType;
125     }
126 
127     friend class RenderProperties;
128 
129     LayerType mType = LayerType::None;
130     // Whether or not that Layer's content is opaque, doesn't include alpha
131     bool mOpaque;
132     uint8_t mAlpha;
133     SkXfermode::Mode mMode;
134     SkColorFilter* mColorFilter = nullptr;
135 };
136 
137 /*
138  * Data structure that holds the properties for a RenderNode
139  */
140 class ANDROID_API RenderProperties {
141 public:
142     RenderProperties();
143     virtual ~RenderProperties();
144 
setFlag(int flag,bool newValue,int * outFlags)145     static bool setFlag(int flag, bool newValue, int* outFlags) {
146         if (newValue) {
147             if (!(flag & *outFlags)) {
148                 *outFlags |= flag;
149                 return true;
150             }
151             return false;
152         } else {
153             if (flag & *outFlags) {
154                 *outFlags &= ~flag;
155                 return true;
156             }
157             return false;
158         }
159     }
160 
161     /**
162      * Set internal layer state based on whether this layer
163      *
164      * Additionally, returns true if child RenderNodes with functors will need to use a layer
165      * to support clipping.
166      */
prepareForFunctorPresence(bool willHaveFunctor,bool ancestorDictatesFunctorsNeedLayer)167     bool prepareForFunctorPresence(bool willHaveFunctor, bool ancestorDictatesFunctorsNeedLayer) {
168         // parent may have already dictated that a descendant layer is needed
169         bool functorsNeedLayer = ancestorDictatesFunctorsNeedLayer
170 
171                 // Round rect clipping forces layer for functors
172                 || CC_UNLIKELY(getOutline().willRoundRectClip())
173                 || CC_UNLIKELY(getRevealClip().willClip())
174 
175                 // Complex matrices forces layer, due to stencil clipping
176                 || CC_UNLIKELY(getTransformMatrix() && !getTransformMatrix()->isScaleTranslate())
177                 || CC_UNLIKELY(getAnimationMatrix() && !getAnimationMatrix()->isScaleTranslate())
178                 || CC_UNLIKELY(getStaticMatrix() && !getStaticMatrix()->isScaleTranslate());
179 
180         mComputedFields.mNeedLayerForFunctors = (willHaveFunctor && functorsNeedLayer);
181 
182         // If on a layer, will have consumed the need for isolating functors from stencil.
183         // Thus, it's safe to reset the flag until some descendent sets it.
184         return CC_LIKELY(effectiveLayerType() == LayerType::None) && functorsNeedLayer;
185     }
186 
187     RenderProperties& operator=(const RenderProperties& other);
188 
setClipToBounds(bool clipToBounds)189     bool setClipToBounds(bool clipToBounds) {
190         return setFlag(CLIP_TO_BOUNDS, clipToBounds, &mPrimitiveFields.mClippingFlags);
191     }
192 
setClipBounds(const Rect & clipBounds)193     bool setClipBounds(const Rect& clipBounds) {
194         bool ret = setFlag(CLIP_TO_CLIP_BOUNDS, true, &mPrimitiveFields.mClippingFlags);
195         return RP_SET(mPrimitiveFields.mClipBounds, clipBounds) || ret;
196     }
197 
setClipBoundsEmpty()198     bool setClipBoundsEmpty() {
199         return setFlag(CLIP_TO_CLIP_BOUNDS, false, &mPrimitiveFields.mClippingFlags);
200     }
201 
setProjectBackwards(bool shouldProject)202     bool setProjectBackwards(bool shouldProject) {
203         return RP_SET(mPrimitiveFields.mProjectBackwards, shouldProject);
204     }
205 
setProjectionReceiver(bool shouldRecieve)206     bool setProjectionReceiver(bool shouldRecieve) {
207         return RP_SET(mPrimitiveFields.mProjectionReceiver, shouldRecieve);
208     }
209 
isProjectionReceiver()210     bool isProjectionReceiver() const {
211         return mPrimitiveFields.mProjectionReceiver;
212     }
213 
setStaticMatrix(const SkMatrix * matrix)214     bool setStaticMatrix(const SkMatrix* matrix) {
215         delete mStaticMatrix;
216         if (matrix) {
217             mStaticMatrix = new SkMatrix(*matrix);
218         } else {
219             mStaticMatrix = nullptr;
220         }
221         return true;
222     }
223 
224     // Can return NULL
getStaticMatrix()225     const SkMatrix* getStaticMatrix() const {
226         return mStaticMatrix;
227     }
228 
setAnimationMatrix(const SkMatrix * matrix)229     bool setAnimationMatrix(const SkMatrix* matrix) {
230         delete mAnimationMatrix;
231         if (matrix) {
232             mAnimationMatrix = new SkMatrix(*matrix);
233         } else {
234             mAnimationMatrix = nullptr;
235         }
236         return true;
237     }
238 
setAlpha(float alpha)239     bool setAlpha(float alpha) {
240         alpha = MathUtils::clampAlpha(alpha);
241         return RP_SET(mPrimitiveFields.mAlpha, alpha);
242     }
243 
getAlpha()244     float getAlpha() const {
245         return mPrimitiveFields.mAlpha;
246     }
247 
setHasOverlappingRendering(bool hasOverlappingRendering)248     bool setHasOverlappingRendering(bool hasOverlappingRendering) {
249         return RP_SET(mPrimitiveFields.mHasOverlappingRendering, hasOverlappingRendering);
250     }
251 
hasOverlappingRendering()252     bool hasOverlappingRendering() const {
253         return mPrimitiveFields.mHasOverlappingRendering;
254     }
255 
setElevation(float elevation)256     bool setElevation(float elevation) {
257         return RP_SET(mPrimitiveFields.mElevation, elevation);
258         // Don't dirty matrix/pivot, since they don't respect Z
259     }
260 
getElevation()261     float getElevation() const {
262         return mPrimitiveFields.mElevation;
263     }
264 
setTranslationX(float translationX)265     bool setTranslationX(float translationX) {
266         return RP_SET_AND_DIRTY(mPrimitiveFields.mTranslationX, translationX);
267     }
268 
getTranslationX()269     float getTranslationX() const {
270         return mPrimitiveFields.mTranslationX;
271     }
272 
setTranslationY(float translationY)273     bool setTranslationY(float translationY) {
274         return RP_SET_AND_DIRTY(mPrimitiveFields.mTranslationY, translationY);
275     }
276 
getTranslationY()277     float getTranslationY() const {
278         return mPrimitiveFields.mTranslationY;
279     }
280 
setTranslationZ(float translationZ)281     bool setTranslationZ(float translationZ) {
282         return RP_SET(mPrimitiveFields.mTranslationZ, translationZ);
283         // mMatrixOrPivotDirty not set, since matrix doesn't respect Z
284     }
285 
getTranslationZ()286     float getTranslationZ() const {
287         return mPrimitiveFields.mTranslationZ;
288     }
289 
290     // Animation helper
setX(float value)291     bool setX(float value) {
292         return setTranslationX(value - getLeft());
293     }
294 
295     // Animation helper
getX()296     float getX() const {
297         return getLeft() + getTranslationX();
298     }
299 
300     // Animation helper
setY(float value)301     bool setY(float value) {
302         return setTranslationY(value - getTop());
303     }
304 
305     // Animation helper
getY()306     float getY() const {
307         return getTop() + getTranslationY();
308     }
309 
310     // Animation helper
setZ(float value)311     bool setZ(float value) {
312         return setTranslationZ(value - getElevation());
313     }
314 
getZ()315     float getZ() const {
316         return getElevation() + getTranslationZ();
317     }
318 
setRotation(float rotation)319     bool setRotation(float rotation) {
320         return RP_SET_AND_DIRTY(mPrimitiveFields.mRotation, rotation);
321     }
322 
getRotation()323     float getRotation() const {
324         return mPrimitiveFields.mRotation;
325     }
326 
setRotationX(float rotationX)327     bool setRotationX(float rotationX) {
328         return RP_SET_AND_DIRTY(mPrimitiveFields.mRotationX, rotationX);
329     }
330 
getRotationX()331     float getRotationX() const {
332         return mPrimitiveFields.mRotationX;
333     }
334 
setRotationY(float rotationY)335     bool setRotationY(float rotationY) {
336         return RP_SET_AND_DIRTY(mPrimitiveFields.mRotationY, rotationY);
337     }
338 
getRotationY()339     float getRotationY() const {
340         return mPrimitiveFields.mRotationY;
341     }
342 
setScaleX(float scaleX)343     bool setScaleX(float scaleX) {
344         return RP_SET_AND_DIRTY(mPrimitiveFields.mScaleX, scaleX);
345     }
346 
getScaleX()347     float getScaleX() const {
348         return mPrimitiveFields.mScaleX;
349     }
350 
setScaleY(float scaleY)351     bool setScaleY(float scaleY) {
352         return RP_SET_AND_DIRTY(mPrimitiveFields.mScaleY, scaleY);
353     }
354 
getScaleY()355     float getScaleY() const {
356         return mPrimitiveFields.mScaleY;
357     }
358 
setPivotX(float pivotX)359     bool setPivotX(float pivotX) {
360         if (RP_SET(mPrimitiveFields.mPivotX, pivotX)
361                 || !mPrimitiveFields.mPivotExplicitlySet) {
362             mPrimitiveFields.mMatrixOrPivotDirty = true;
363             mPrimitiveFields.mPivotExplicitlySet = true;
364             return true;
365         }
366         return false;
367     }
368 
369     /* Note that getPivotX and getPivotY are adjusted by updateMatrix(),
370      * so the value returned may be stale if the RenderProperties has been
371      * modified since the last call to updateMatrix()
372      */
getPivotX()373     float getPivotX() const {
374         return mPrimitiveFields.mPivotX;
375     }
376 
setPivotY(float pivotY)377     bool setPivotY(float pivotY) {
378         if (RP_SET(mPrimitiveFields.mPivotY, pivotY)
379                 || !mPrimitiveFields.mPivotExplicitlySet) {
380             mPrimitiveFields.mMatrixOrPivotDirty = true;
381             mPrimitiveFields.mPivotExplicitlySet = true;
382             return true;
383         }
384         return false;
385     }
386 
getPivotY()387     float getPivotY() const {
388         return mPrimitiveFields.mPivotY;
389     }
390 
isPivotExplicitlySet()391     bool isPivotExplicitlySet() const {
392         return mPrimitiveFields.mPivotExplicitlySet;
393     }
394 
setCameraDistance(float distance)395     bool setCameraDistance(float distance) {
396         if (distance != getCameraDistance()) {
397             mPrimitiveFields.mMatrixOrPivotDirty = true;
398             mComputedFields.mTransformCamera.setCameraLocation(0, 0, distance);
399             return true;
400         }
401         return false;
402     }
403 
getCameraDistance()404     float getCameraDistance() const {
405         // TODO: update getCameraLocationZ() to be const
406         return const_cast<Sk3DView*>(&mComputedFields.mTransformCamera)->getCameraLocationZ();
407     }
408 
setLeft(int left)409     bool setLeft(int left) {
410         if (RP_SET(mPrimitiveFields.mLeft, left)) {
411             mPrimitiveFields.mWidth = mPrimitiveFields.mRight - mPrimitiveFields.mLeft;
412             if (!mPrimitiveFields.mPivotExplicitlySet) {
413                 mPrimitiveFields.mMatrixOrPivotDirty = true;
414             }
415             return true;
416         }
417         return false;
418     }
419 
getLeft()420     float getLeft() const {
421         return mPrimitiveFields.mLeft;
422     }
423 
setTop(int top)424     bool setTop(int top) {
425         if (RP_SET(mPrimitiveFields.mTop, top)) {
426             mPrimitiveFields.mHeight = mPrimitiveFields.mBottom - mPrimitiveFields.mTop;
427             if (!mPrimitiveFields.mPivotExplicitlySet) {
428                 mPrimitiveFields.mMatrixOrPivotDirty = true;
429             }
430             return true;
431         }
432         return false;
433     }
434 
getTop()435     float getTop() const {
436         return mPrimitiveFields.mTop;
437     }
438 
setRight(int right)439     bool setRight(int right) {
440         if (RP_SET(mPrimitiveFields.mRight, right)) {
441             mPrimitiveFields.mWidth = mPrimitiveFields.mRight - mPrimitiveFields.mLeft;
442             if (!mPrimitiveFields.mPivotExplicitlySet) {
443                 mPrimitiveFields.mMatrixOrPivotDirty = true;
444             }
445             return true;
446         }
447         return false;
448     }
449 
getRight()450     float getRight() const {
451         return mPrimitiveFields.mRight;
452     }
453 
setBottom(int bottom)454     bool setBottom(int bottom) {
455         if (RP_SET(mPrimitiveFields.mBottom, bottom)) {
456             mPrimitiveFields.mHeight = mPrimitiveFields.mBottom - mPrimitiveFields.mTop;
457             if (!mPrimitiveFields.mPivotExplicitlySet) {
458                 mPrimitiveFields.mMatrixOrPivotDirty = true;
459             }
460             return true;
461         }
462         return false;
463     }
464 
getBottom()465     float getBottom() const {
466         return mPrimitiveFields.mBottom;
467     }
468 
setLeftTop(int left,int top)469     bool setLeftTop(int left, int top) {
470         bool leftResult = setLeft(left);
471         bool topResult = setTop(top);
472         return leftResult || topResult;
473     }
474 
setLeftTopRightBottom(int left,int top,int right,int bottom)475     bool setLeftTopRightBottom(int left, int top, int right, int bottom) {
476         if (left != mPrimitiveFields.mLeft || top != mPrimitiveFields.mTop
477                 || right != mPrimitiveFields.mRight || bottom != mPrimitiveFields.mBottom) {
478             mPrimitiveFields.mLeft = left;
479             mPrimitiveFields.mTop = top;
480             mPrimitiveFields.mRight = right;
481             mPrimitiveFields.mBottom = bottom;
482             mPrimitiveFields.mWidth = mPrimitiveFields.mRight - mPrimitiveFields.mLeft;
483             mPrimitiveFields.mHeight = mPrimitiveFields.mBottom - mPrimitiveFields.mTop;
484             if (!mPrimitiveFields.mPivotExplicitlySet) {
485                 mPrimitiveFields.mMatrixOrPivotDirty = true;
486             }
487             return true;
488         }
489         return false;
490     }
491 
offsetLeftRight(int offset)492     bool offsetLeftRight(int offset) {
493         if (offset != 0) {
494             mPrimitiveFields.mLeft += offset;
495             mPrimitiveFields.mRight += offset;
496             return true;
497         }
498         return false;
499     }
500 
offsetTopBottom(int offset)501     bool offsetTopBottom(int offset) {
502         if (offset != 0) {
503             mPrimitiveFields.mTop += offset;
504             mPrimitiveFields.mBottom += offset;
505             return true;
506         }
507         return false;
508     }
509 
getWidth()510     int getWidth() const {
511         return mPrimitiveFields.mWidth;
512     }
513 
getHeight()514     int getHeight() const {
515         return mPrimitiveFields.mHeight;
516     }
517 
getAnimationMatrix()518     const SkMatrix* getAnimationMatrix() const {
519         return mAnimationMatrix;
520     }
521 
hasTransformMatrix()522     bool hasTransformMatrix() const {
523         return getTransformMatrix() && !getTransformMatrix()->isIdentity();
524     }
525 
526     // May only call this if hasTransformMatrix() is true
isTransformTranslateOnly()527     bool isTransformTranslateOnly() const {
528         return getTransformMatrix()->getType() == SkMatrix::kTranslate_Mask;
529     }
530 
getTransformMatrix()531     const SkMatrix* getTransformMatrix() const {
532         LOG_ALWAYS_FATAL_IF(mPrimitiveFields.mMatrixOrPivotDirty, "Cannot get a dirty matrix!");
533         return mComputedFields.mTransformMatrix;
534     }
535 
getClippingFlags()536     int getClippingFlags() const {
537         return mPrimitiveFields.mClippingFlags;
538     }
539 
getClipToBounds()540     bool getClipToBounds() const {
541         return mPrimitiveFields.mClippingFlags & CLIP_TO_BOUNDS;
542     }
543 
getClippingRectForFlags(uint32_t flags,Rect * outRect)544     void getClippingRectForFlags(uint32_t flags, Rect* outRect) const {
545         if (flags & CLIP_TO_BOUNDS) {
546             outRect->set(0, 0, getWidth(), getHeight());
547             if (flags & CLIP_TO_CLIP_BOUNDS) {
548                 outRect->intersect(mPrimitiveFields.mClipBounds);
549             }
550         } else {
551             outRect->set(mPrimitiveFields.mClipBounds);
552         }
553     }
554 
getHasOverlappingRendering()555     bool getHasOverlappingRendering() const {
556         return mPrimitiveFields.mHasOverlappingRendering;
557     }
558 
getOutline()559     const Outline& getOutline() const {
560         return mPrimitiveFields.mOutline;
561     }
562 
getRevealClip()563     const RevealClip& getRevealClip() const {
564         return mPrimitiveFields.mRevealClip;
565     }
566 
getProjectBackwards()567     bool getProjectBackwards() const {
568         return mPrimitiveFields.mProjectBackwards;
569     }
570 
571     void debugOutputProperties(const int level) const;
572 
573     void updateMatrix();
574 
mutableOutline()575     Outline& mutableOutline() {
576         return mPrimitiveFields.mOutline;
577     }
578 
mutableRevealClip()579     RevealClip& mutableRevealClip() {
580         return mPrimitiveFields.mRevealClip;
581     }
582 
layerProperties()583     const LayerProperties& layerProperties() const {
584         return mLayerProperties;
585     }
586 
mutateLayerProperties()587     LayerProperties& mutateLayerProperties() {
588         return mLayerProperties;
589     }
590 
591     // Returns true if damage calculations should be clipped to bounds
592     // TODO: Figure out something better for getZ(), as children should still be
593     // clipped to this RP's bounds. But as we will damage -INT_MAX to INT_MAX
594     // for this RP's getZ() anyway, this can be optimized when we have a
595     // Z damage estimate instead of INT_MAX
getClipDamageToBounds()596     bool getClipDamageToBounds() const {
597         return getClipToBounds() && (getZ() <= 0 || getOutline().isEmpty());
598     }
599 
hasShadow()600     bool hasShadow() const {
601         return getZ() > 0.0f
602                 && getOutline().getPath() != nullptr
603                 && getOutline().getAlpha() != 0.0f;
604     }
605 
promotedToLayer()606     bool promotedToLayer() const {
607         const int maxTextureSize = Caches::getInstance().maxTextureSize;
608         return mLayerProperties.mType == LayerType::None
609                 && mPrimitiveFields.mWidth <= maxTextureSize
610                 && mPrimitiveFields.mHeight <= maxTextureSize
611                 && (mComputedFields.mNeedLayerForFunctors
612                         || (!MathUtils::isZero(mPrimitiveFields.mAlpha)
613                                 && mPrimitiveFields.mAlpha < 1
614                                 && mPrimitiveFields.mHasOverlappingRendering));
615     }
616 
effectiveLayerType()617     LayerType effectiveLayerType() const {
618         return CC_UNLIKELY(promotedToLayer()) ? LayerType::RenderLayer : mLayerProperties.mType;
619     }
620 
621 private:
622     // Rendering properties
623     struct PrimitiveFields {
624         PrimitiveFields();
625 
626         Outline mOutline;
627         RevealClip mRevealClip;
628         int mClippingFlags;
629         bool mProjectBackwards;
630         bool mProjectionReceiver;
631         float mAlpha;
632         bool mHasOverlappingRendering;
633         float mElevation;
634         float mTranslationX, mTranslationY, mTranslationZ;
635         float mRotation, mRotationX, mRotationY;
636         float mScaleX, mScaleY;
637         float mPivotX, mPivotY;
638         int mLeft, mTop, mRight, mBottom;
639         int mWidth, mHeight;
640         bool mPivotExplicitlySet;
641         bool mMatrixOrPivotDirty;
642         Rect mClipBounds;
643     } mPrimitiveFields;
644 
645     SkMatrix* mStaticMatrix;
646     SkMatrix* mAnimationMatrix;
647     LayerProperties mLayerProperties;
648 
649     /**
650      * These fields are all generated from other properties and are not set directly.
651      */
652     struct ComputedFields {
653         ComputedFields();
654         ~ComputedFields();
655 
656         /**
657          * Stores the total transformation of the DisplayList based upon its scalar
658          * translate/rotate/scale properties.
659          *
660          * In the common translation-only case, the matrix isn't necessarily allocated,
661          * and the mTranslation properties are used directly.
662          */
663         SkMatrix* mTransformMatrix;
664 
665         Sk3DView mTransformCamera;
666 
667         // Force layer on for functors to enable render features they don't yet support (clipping)
668         bool mNeedLayerForFunctors = false;
669     } mComputedFields;
670 };
671 
672 } /* namespace uirenderer */
673 } /* namespace android */
674 
675 #endif /* RENDERNODEPROPERTIES_H */
676