From 5219a06d61ac4517506500363c5e8a5972dd7ac9 Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Tue, 26 Feb 2013 16:37:53 -0800 Subject: set correct crop rectangle in LayerBase::setCrop The crop always had left=top=0, because the crop position and the layer's transform were merged together in computeBounds() (which really used to compute the bounds in screen space, which we usually call the "frame" elsewhere in the code) Note: in practice this crop value is not used by hwc, because it's overridden in Layer::setGeometry(), which is why this bug was never apparent. Change-Id: I1ec6400a8fc8314408e4252708f43ea98c2fe64e --- services/surfaceflinger/LayerBase.cpp | 17 ++++++++++------- services/surfaceflinger/SurfaceFlinger.cpp | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/services/surfaceflinger/LayerBase.cpp b/services/surfaceflinger/LayerBase.cpp index dfdbf300fd..24b122ab8a 100644 --- a/services/surfaceflinger/LayerBase.cpp +++ b/services/surfaceflinger/LayerBase.cpp @@ -259,7 +259,7 @@ Rect LayerBase::computeBounds() const { if (!s.active.crop.isEmpty()) { win.intersect(s.active.crop, &win); } - return s.transform.transform(win); + return win; } Region LayerBase::latchBuffer(bool& recomputeVisibleRegions) { @@ -289,13 +289,16 @@ void LayerBase::setGeometry( HWC_BLENDING_COVERAGE); } - const Transform& tr = hw->getTransform(); - Rect transformedBounds(computeBounds()); - transformedBounds = tr.transform(transformedBounds); - // scaling is already applied in transformedBounds - layer.setFrame(transformedBounds); - layer.setCrop(transformedBounds.getBounds()); + Rect bounds(computeBounds()); + + // apply the layer's transform, followed by the display's global transform + // here we're guaranteed that the layer's transform preserves rects + + const Transform& tr = hw->getTransform(); + Rect frame(tr.transform(s.transform.transform(bounds))); + layer.setFrame(frame); + layer.setCrop(bounds); } void LayerBase::setPerFrameData(const sp& hw, diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp index ee3e93b1a4..bdeffdfcac 100644 --- a/services/surfaceflinger/SurfaceFlinger.cpp +++ b/services/surfaceflinger/SurfaceFlinger.cpp @@ -1384,7 +1384,7 @@ void SurfaceFlinger::computeVisibleRegions( // handle hidden surfaces by setting the visible region to empty if (CC_LIKELY(layer->isVisible())) { const bool translucent = !layer->isOpaque(); - Rect bounds(layer->computeBounds()); + Rect bounds(s.transform.transform(layer->computeBounds())); visibleRegion.set(bounds); if (!visibleRegion.isEmpty()) { // Remove the transparent area from the visible region -- cgit v1.2.3-59-g8ed1b From f5f714aa188884098aaecbe39d0bc61b40311c0d Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Tue, 26 Feb 2013 16:54:05 -0800 Subject: apply the projection's viewport to the visibleregion passed to hwc each desplay's projection's viewport essentially clips each layer, so this should be reflected in the visibleregion passed to h/w composer. DisplayDevice getViewport and getFrame are now guaranteed to return valid Rects. Change-Id: I4c25f34fb26af10179eb26d429ca6c384c671e91 --- services/surfaceflinger/DisplayDevice.cpp | 125 +++++++++++++++--------------- services/surfaceflinger/DisplayDevice.h | 6 +- services/surfaceflinger/LayerBase.cpp | 5 +- 3 files changed, 68 insertions(+), 68 deletions(-) diff --git a/services/surfaceflinger/DisplayDevice.cpp b/services/surfaceflinger/DisplayDevice.cpp index 9466944490..f9cc341811 100644 --- a/services/surfaceflinger/DisplayDevice.cpp +++ b/services/surfaceflinger/DisplayDevice.cpp @@ -287,7 +287,8 @@ void DisplayDevice::setVisibleLayersSortedByZ(const Vector< sp >& lay mSecureLayerVisible = false; size_t count = layers.size(); for (size_t i=0 ; iisSecure()) { + const sp& layer(layers[i]); + if (layer->isSecure()) { mSecureLayerVisible = true; } } @@ -365,74 +366,72 @@ status_t DisplayDevice::orientationToTransfrom( } void DisplayDevice::setProjection(int orientation, - const Rect& viewport, const Rect& frame) { - mOrientation = orientation; - mViewport = viewport; - mFrame = frame; - updateGeometryTransform(); -} + const Rect& newViewport, const Rect& newFrame) { + Rect viewport(newViewport); + Rect frame(newFrame); -void DisplayDevice::updateGeometryTransform() { - int w = mDisplayWidth; - int h = mDisplayHeight; - Transform TL, TP, R, S; - if (DisplayDevice::orientationToTransfrom( - mOrientation, w, h, &R) == NO_ERROR) { - dirtyRegion.set(bounds()); - - Rect viewport(mViewport); - Rect frame(mFrame); - - if (!frame.isValid()) { - // the destination frame can be invalid if it has never been set, - // in that case we assume the whole display frame. - frame = Rect(w, h); - } + const int w = mDisplayWidth; + const int h = mDisplayHeight; - if (viewport.isEmpty()) { - // viewport can be invalid if it has never been set, in that case - // we assume the whole display size. - // it's also invalid to have an empty viewport, so we handle that - // case in the same way. - viewport = Rect(w, h); - if (R.getOrientation() & Transform::ROT_90) { - // viewport is always specified in the logical orientation - // of the display (ie: post-rotation). - swap(viewport.right, viewport.bottom); - } - } + Transform R; + DisplayDevice::orientationToTransfrom(orientation, w, h, &R); - float src_width = viewport.width(); - float src_height = viewport.height(); - float dst_width = frame.width(); - float dst_height = frame.height(); - if (src_width != dst_width || src_height != dst_height) { - float sx = dst_width / src_width; - float sy = dst_height / src_height; - S.set(sx, 0, 0, sy); - } + if (!frame.isValid()) { + // the destination frame can be invalid if it has never been set, + // in that case we assume the whole display frame. + frame = Rect(w, h); + } - float src_x = viewport.left; - float src_y = viewport.top; - float dst_x = frame.left; - float dst_y = frame.top; - TL.set(-src_x, -src_y); - TP.set(dst_x, dst_y); - - // The viewport and frame are both in the logical orientation. - // Apply the logical translation, scale to physical size, apply the - // physical translation and finally rotate to the physical orientation. - mGlobalTransform = R * TP * S * TL; - - const uint8_t type = mGlobalTransform.getType(); - mNeedsFiltering = (!mGlobalTransform.preserveRects() || - (type >= Transform::SCALE)); - - mScissor = mGlobalTransform.transform(mViewport); - if (mScissor.isEmpty()) { - mScissor.set(getBounds()); + if (viewport.isEmpty()) { + // viewport can be invalid if it has never been set, in that case + // we assume the whole display size. + // it's also invalid to have an empty viewport, so we handle that + // case in the same way. + viewport = Rect(w, h); + if (R.getOrientation() & Transform::ROT_90) { + // viewport is always specified in the logical orientation + // of the display (ie: post-rotation). + swap(viewport.right, viewport.bottom); } } + + dirtyRegion.set(getBounds()); + + Transform TL, TP, S; + float src_width = viewport.width(); + float src_height = viewport.height(); + float dst_width = frame.width(); + float dst_height = frame.height(); + if (src_width != dst_width || src_height != dst_height) { + float sx = dst_width / src_width; + float sy = dst_height / src_height; + S.set(sx, 0, 0, sy); + } + + float src_x = viewport.left; + float src_y = viewport.top; + float dst_x = frame.left; + float dst_y = frame.top; + TL.set(-src_x, -src_y); + TP.set(dst_x, dst_y); + + // The viewport and frame are both in the logical orientation. + // Apply the logical translation, scale to physical size, apply the + // physical translation and finally rotate to the physical orientation. + mGlobalTransform = R * TP * S * TL; + + const uint8_t type = mGlobalTransform.getType(); + mNeedsFiltering = (!mGlobalTransform.preserveRects() || + (type >= Transform::SCALE)); + + mScissor = mGlobalTransform.transform(viewport); + if (mScissor.isEmpty()) { + mScissor.set(getBounds()); + } + + mOrientation = orientation; + mViewport = viewport; + mFrame = frame; } void DisplayDevice::dump(String8& result, char* buffer, size_t SIZE) const { diff --git a/services/surfaceflinger/DisplayDevice.h b/services/surfaceflinger/DisplayDevice.h index bb6eb70e91..c7534af61c 100644 --- a/services/surfaceflinger/DisplayDevice.h +++ b/services/surfaceflinger/DisplayDevice.h @@ -105,8 +105,8 @@ public: int getOrientation() const { return mOrientation; } const Transform& getTransform() const { return mGlobalTransform; } - const Rect& getViewport() const { return mViewport; } - const Rect& getFrame() const { return mFrame; } + const Rect getViewport() const { return mViewport; } + const Rect getFrame() const { return mFrame; } const Rect& getScissor() const { return mScissor; } bool needsFiltering() const { return mNeedsFiltering; } @@ -197,8 +197,6 @@ private: static status_t orientationToTransfrom(int orientation, int w, int h, Transform* tr); - void updateGeometryTransform(); - uint32_t mLayerStack; int mOrientation; // user-provided visible area of the layer stack diff --git a/services/surfaceflinger/LayerBase.cpp b/services/surfaceflinger/LayerBase.cpp index 24b122ab8a..ba56c23102 100644 --- a/services/surfaceflinger/LayerBase.cpp +++ b/services/surfaceflinger/LayerBase.cpp @@ -306,8 +306,11 @@ void LayerBase::setPerFrameData(const sp& hw, // we have to set the visible region on every frame because // we currently free it during onLayerDisplayed(), which is called // after HWComposer::commit() -- every frame. + // Apply this display's projection's viewport to the visible region + // before giving it to the HWC HAL. const Transform& tr = hw->getTransform(); - layer.setVisibleRegionScreen(tr.transform(visibleRegion)); + Region visible = tr.transform(visibleRegion.intersect(hw->getViewport())); + layer.setVisibleRegionScreen(visible); } void LayerBase::setAcquireFence(const sp& hw, -- cgit v1.2.3-59-g8ed1b From a8bca8d84b559e7dcca010f7d6514333004020c7 Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Wed, 27 Feb 2013 22:03:19 -0800 Subject: refactor the crop region for hwc is calculated/set - the crop region is now always calculated and set in LayerBase::setGeometry which uses new virtuals to access the "content" crop and transform (which are provided by the Layer subclass) Change-Id: Ib7769bdec0917dd248f926600c14ddf9ea84897a --- services/surfaceflinger/Layer.cpp | 47 +++++++------------------ services/surfaceflinger/Layer.h | 4 ++- services/surfaceflinger/LayerBase.cpp | 66 +++++++++++++++++++++++++++++++---- services/surfaceflinger/LayerBase.h | 15 ++++++++ 4 files changed, 90 insertions(+), 42 deletions(-) diff --git a/services/surfaceflinger/Layer.cpp b/services/surfaceflinger/Layer.cpp index 439acb52ae..c9f1eb51d8 100644 --- a/services/surfaceflinger/Layer.cpp +++ b/services/surfaceflinger/Layer.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include @@ -200,48 +199,27 @@ status_t Layer::setBuffers( uint32_t w, uint32_t h, return NO_ERROR; } -Rect Layer::computeBufferCrop() const { - // Start with the SurfaceFlingerConsumer's buffer crop... +Rect Layer::getContentCrop() const { + // this is the crop rectangle that applies to the buffer + // itself (as opposed to the window) Rect crop; if (!mCurrentCrop.isEmpty()) { + // if the buffer crop is defined, we use that crop = mCurrentCrop; - } else if (mActiveBuffer != NULL){ - crop = Rect(mActiveBuffer->getWidth(), mActiveBuffer->getHeight()); + } else if (mActiveBuffer != NULL) { + // otherwise we use the whole buffer + crop = mActiveBuffer->getBounds(); } else { + // if we don't have a buffer yet, we use an empty/invalid crop crop.makeInvalid(); - return crop; } - - // ... then reduce that in the same proportions as the window crop reduces - // the window size. - const State& s(drawingState()); - if (!s.active.crop.isEmpty()) { - // Transform the window crop to match the buffer coordinate system, - // which means using the inverse of the current transform set on the - // SurfaceFlingerConsumer. - uint32_t invTransform = mCurrentTransform; - int winWidth = s.active.w; - int winHeight = s.active.h; - if (invTransform & NATIVE_WINDOW_TRANSFORM_ROT_90) { - invTransform ^= NATIVE_WINDOW_TRANSFORM_FLIP_V | - NATIVE_WINDOW_TRANSFORM_FLIP_H; - winWidth = s.active.h; - winHeight = s.active.w; - } - Rect winCrop = s.active.crop.transform(invTransform, - s.active.w, s.active.h); - - float xScale = float(crop.width()) / float(winWidth); - float yScale = float(crop.height()) / float(winHeight); - crop.left += int(ceilf(float(winCrop.left) * xScale)); - crop.top += int(ceilf(float(winCrop.top) * yScale)); - crop.right -= int(ceilf(float(winWidth - winCrop.right) * xScale)); - crop.bottom -= int(ceilf(float(winHeight - winCrop.bottom) * yScale)); - } - return crop; } +uint32_t Layer::getContentTransform() const { + return mCurrentTransform; +} + void Layer::setGeometry( const sp& hw, HWComposer::HWCLayerInterface& layer) @@ -278,7 +256,6 @@ void Layer::setGeometry( } else { layer.setTransform(finalTransform); } - layer.setCrop(computeBufferCrop()); } void Layer::setPerFrameData(const sp& hw, diff --git a/services/surfaceflinger/Layer.h b/services/surfaceflinger/Layer.h index a82767b755..242493df9b 100644 --- a/services/surfaceflinger/Layer.h +++ b/services/surfaceflinger/Layer.h @@ -103,6 +103,9 @@ public: // the current orientation of the display device. virtual void updateTransformHint(const sp& hw) const; + virtual Rect getContentCrop() const; + virtual uint32_t getContentTransform() const; + protected: virtual void onFirstRef(); virtual void dump(String8& result, char* scratch, size_t size) const; @@ -115,7 +118,6 @@ private: uint32_t getEffectiveUsage(uint32_t usage) const; bool isCropped() const; - Rect computeBufferCrop() const; static bool getOpacityForFormat(uint32_t format); // Interface implementation for SurfaceFlingerConsumer::FrameAvailableListener diff --git a/services/surfaceflinger/LayerBase.cpp b/services/surfaceflinger/LayerBase.cpp index ba56c23102..572e7c8913 100644 --- a/services/surfaceflinger/LayerBase.cpp +++ b/services/surfaceflinger/LayerBase.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -267,6 +268,61 @@ Region LayerBase::latchBuffer(bool& recomputeVisibleRegions) { return result; } + +Rect LayerBase::getContentCrop() const { + // regular layers just use their active area as the content crop + const State& s(drawingState()); + return Rect(s.active.w, s.active.h); +} + +uint32_t LayerBase::getContentTransform() const { + // regular layers don't have a content transform + return 0; +} + +Rect LayerBase::computeCrop(const sp& hw) const { + // the content crop is the area of the content that gets scaled to the + // layer's size. + Rect crop(getContentCrop()); + + // the active.crop is the area of the window that gets cropped, but not + // scaled in any ways. + const State& s(drawingState()); + Rect activeCrop(s.active.crop); + if (!activeCrop.isEmpty()) { + // Transform the window crop to match the buffer coordinate system, + // which means using the inverse of the current transform set on the + // SurfaceFlingerConsumer. + uint32_t invTransform = getContentTransform(); + int winWidth = s.active.w; + int winHeight = s.active.h; + if (invTransform & NATIVE_WINDOW_TRANSFORM_ROT_90) { + invTransform ^= NATIVE_WINDOW_TRANSFORM_FLIP_V | + NATIVE_WINDOW_TRANSFORM_FLIP_H; + winWidth = s.active.h; + winHeight = s.active.w; + } + const Rect winCrop = activeCrop.transform( + invTransform, s.active.w, s.active.h); + + // the code below essentially performs a scaled intersection + // of crop and winCrop + float xScale = float(crop.width()) / float(winWidth); + float yScale = float(crop.height()) / float(winHeight); + + int insetL = int(ceilf( winCrop.left * xScale)); + int insetT = int(ceilf( winCrop.top * yScale)); + int insetR = int(ceilf((winWidth - winCrop.right ) * xScale)); + int insetB = int(ceilf((winHeight - winCrop.bottom) * yScale)); + + crop.left += insetL; + crop.top += insetT; + crop.right -= insetR; + crop.bottom -= insetB; + } + return crop; +} + void LayerBase::setGeometry( const sp& hw, HWComposer::HWCLayerInterface& layer) @@ -290,15 +346,13 @@ void LayerBase::setGeometry( } - Rect bounds(computeBounds()); - // apply the layer's transform, followed by the display's global transform // here we're guaranteed that the layer's transform preserves rects - const Transform& tr = hw->getTransform(); - Rect frame(tr.transform(s.transform.transform(bounds))); - layer.setFrame(frame); - layer.setCrop(bounds); + Rect frame(s.transform.transform(computeBounds())); + const Transform& tr(hw->getTransform()); + layer.setFrame(tr.transform(frame)); + layer.setCrop(computeCrop(hw)); } void LayerBase::setPerFrameData(const sp& hw, diff --git a/services/surfaceflinger/LayerBase.h b/services/surfaceflinger/LayerBase.h index c2624df5d9..ecae2d99ce 100644 --- a/services/surfaceflinger/LayerBase.h +++ b/services/surfaceflinger/LayerBase.h @@ -260,6 +260,18 @@ public: */ virtual void updateTransformHint(const sp& hw) const { } + /** + * returns the rectangle that crops the content of the layer and scales it + * to the layer's size. + */ + virtual Rect getContentCrop() const; + + /* + * returns the transform bits (90 rotation / h-flip / v-flip) of the + * layer's content + */ + virtual uint32_t getContentTransform() const; + /** always call base class first */ virtual void dump(String8& result, char* scratch, size_t size) const; virtual void shortDump(String8& result, char* scratch, size_t size) const; @@ -282,6 +294,9 @@ public: void setFiltering(bool filtering); bool getFiltering() const; +private: + Rect computeCrop(const sp& hw) const; + protected: void clearWithOpenGL(const sp& hw, const Region& clip, GLclampf r, GLclampf g, GLclampf b, GLclampf alpha) const; -- cgit v1.2.3-59-g8ed1b From 3da1672acbe6a84f1d69f1e21096115c60826aea Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Thu, 28 Feb 2013 17:12:07 -0800 Subject: implement display projection clipping in h/w composer - cropping to the projection's "viewport" is "simply" accomplished by intersecting it with the window crop expressed in layerstack space. Bug: 7149437 Change-Id: I0e90b3f37945292314b5d78a8f134935967e8053 --- services/surfaceflinger/LayerBase.cpp | 45 +++++++++++++++++++++++++++++++---- services/surfaceflinger/Transform.cpp | 38 +++++++++++++++++++++++++++++ services/surfaceflinger/Transform.h | 2 ++ 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/services/surfaceflinger/LayerBase.cpp b/services/surfaceflinger/LayerBase.cpp index 572e7c8913..db2b20eaaf 100644 --- a/services/surfaceflinger/LayerBase.cpp +++ b/services/surfaceflinger/LayerBase.cpp @@ -281,6 +281,14 @@ uint32_t LayerBase::getContentTransform() const { } Rect LayerBase::computeCrop(const sp& hw) const { + /* + * The way we compute the crop (aka. texture coordinates when we have a + * Layer) produces a different output from the GL code in + * drawWithOpenGL() due to HWC being limited to integers. The difference + * can be large if getContentTransform() contains a large scale factor. + * See comments in drawWithOpenGL() for more details. + */ + // the content crop is the area of the content that gets scaled to the // layer's size. Rect crop(getContentCrop()); @@ -288,7 +296,21 @@ Rect LayerBase::computeCrop(const sp& hw) const { // the active.crop is the area of the window that gets cropped, but not // scaled in any ways. const State& s(drawingState()); - Rect activeCrop(s.active.crop); + + // apply the projection's clipping to the window crop in + // layerstack space, and convert-back to layer space. + // if there are no window scaling (or content scaling) involved, + // this operation will map to full pixels in the buffer. + // NOTE: should we revert to GL composition if a scaling is involved + // since it cannot be represented in the HWC API? + Rect activeCrop(s.transform.transform(s.active.crop)); + activeCrop.intersect(hw->getViewport(), &activeCrop); + activeCrop = s.transform.inverse().transform(activeCrop); + + // paranoia: make sure the window-crop is constrained in the + // window's bounds + activeCrop.intersect(Rect(s.active.w, s.active.h), &activeCrop); + if (!activeCrop.isEmpty()) { // Transform the window crop to match the buffer coordinate system, // which means using the inverse of the current transform set on the @@ -350,6 +372,7 @@ void LayerBase::setGeometry( // here we're guaranteed that the layer's transform preserves rects Rect frame(s.transform.transform(computeBounds())); + frame.intersect(hw->getViewport(), &frame); const Transform& tr(hw->getTransform()); layer.setFrame(tr.transform(frame)); layer.setCrop(computeCrop(hw)); @@ -464,10 +487,22 @@ void LayerBase::drawWithOpenGL(const sp& hw, const Region& GLfloat v; }; - Rect win(s.active.w, s.active.h); - if (!s.active.crop.isEmpty()) { - win.intersect(s.active.crop, &win); - } + + /* + * NOTE: the way we compute the texture coordinates here produces + * different results than when we take the HWC path -- in the later case + * the "source crop" is rounded to texel boundaries. + * This can produce significantly different results when the texture + * is scaled by a large amount. + * + * The GL code below is more logical (imho), and the difference with + * HWC is due to a limitation of the HWC API to integers -- a question + * is suspend is wether we should ignore this problem or revert to + * GL composition when a buffer scaling is applied (maybe with some + * minimal value)? Or, we could make GL behave like HWC -- but this feel + * like more of a hack. + */ + const Rect win(computeBounds()); GLfloat left = GLfloat(win.left) / GLfloat(s.active.w); GLfloat top = GLfloat(win.top) / GLfloat(s.active.h); diff --git a/services/surfaceflinger/Transform.cpp b/services/surfaceflinger/Transform.cpp index aca90e016b..315720e1cf 100644 --- a/services/surfaceflinger/Transform.cpp +++ b/services/surfaceflinger/Transform.cpp @@ -298,6 +298,44 @@ uint32_t Transform::type() const return mType; } +Transform Transform::inverse() const { + // our 3x3 matrix is always of the form of a 2x2 transformation + // followed by a translation: T*M, therefore: + // (T*M)^-1 = M^-1 * T^-1 + Transform result; + if (mType <= TRANSLATE) { + // 1 0 x + // 0 1 y + // 0 0 1 + result = *this; + result.mMatrix[2][0] = -result.mMatrix[2][0]; + result.mMatrix[2][1] = -result.mMatrix[2][1]; + } else { + // a c x + // b d y + // 0 0 1 + const mat33& M(mMatrix); + const float a = M[0][0]; + const float b = M[1][0]; + const float c = M[0][1]; + const float d = M[1][1]; + const float x = M[2][0]; + const float y = M[2][1]; + + Transform R, T; + const float idet = 1.0 / (a*d - b*c); + R.mMatrix[0][0] = d*idet; R.mMatrix[0][1] = -c*idet; + R.mMatrix[1][0] = -b*idet; R.mMatrix[1][1] = a*idet; + R.mType = mType &= ~TRANSLATE; + + T.mMatrix[2][0] = -x; + T.mMatrix[2][1] = -y; + T.mType = TRANSLATE; + result = R * T; + } + return result; +} + uint32_t Transform::getType() const { return type() & 0xFF; } diff --git a/services/surfaceflinger/Transform.h b/services/surfaceflinger/Transform.h index 4fe261af72..c4efade765 100644 --- a/services/surfaceflinger/Transform.h +++ b/services/surfaceflinger/Transform.h @@ -80,6 +80,8 @@ public: Rect transform(const Rect& bounds) const; Transform operator * (const Transform& rhs) const; + Transform inverse() const; + // for debugging void dump(const char* name) const; -- cgit v1.2.3-59-g8ed1b