Tue, 06 Jan 2015 21:39:09 +0100
Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.
michael@0 | 1 | /* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- |
michael@0 | 2 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | #include "2D.h" |
michael@0 | 7 | #include "Filters.h" |
michael@0 | 8 | #include "SIMD.h" |
michael@0 | 9 | |
michael@0 | 10 | namespace mozilla { |
michael@0 | 11 | namespace gfx { |
michael@0 | 12 | |
michael@0 | 13 | template<TurbulenceType Type, bool Stitch, typename f32x4_t, typename i32x4_t, typename u8x16_t> |
michael@0 | 14 | class SVGTurbulenceRenderer |
michael@0 | 15 | { |
michael@0 | 16 | public: |
michael@0 | 17 | SVGTurbulenceRenderer(const Size &aBaseFrequency, int32_t aSeed, |
michael@0 | 18 | int aNumOctaves, const Rect &aTileRect); |
michael@0 | 19 | |
michael@0 | 20 | TemporaryRef<DataSourceSurface> Render(const IntSize &aSize, const Point &aOffset) const; |
michael@0 | 21 | |
michael@0 | 22 | private: |
michael@0 | 23 | /* The turbulence calculation code is an adapted version of what |
michael@0 | 24 | appears in the SVG 1.1 specification: |
michael@0 | 25 | http://www.w3.org/TR/SVG11/filters.html#feTurbulence |
michael@0 | 26 | */ |
michael@0 | 27 | |
michael@0 | 28 | struct StitchInfo { |
michael@0 | 29 | int32_t width; // How much to subtract to wrap for stitching. |
michael@0 | 30 | int32_t height; |
michael@0 | 31 | int32_t wrapX; // Minimum value to wrap. |
michael@0 | 32 | int32_t wrapY; |
michael@0 | 33 | }; |
michael@0 | 34 | |
michael@0 | 35 | const static int sBSize = 0x100; |
michael@0 | 36 | const static int sBM = 0xff; |
michael@0 | 37 | void InitFromSeed(int32_t aSeed); |
michael@0 | 38 | void AdjustBaseFrequencyForStitch(const Rect &aTileRect); |
michael@0 | 39 | IntPoint AdjustForStitch(IntPoint aLatticePoint, const StitchInfo& aStitchInfo) const; |
michael@0 | 40 | StitchInfo CreateStitchInfo(const Rect &aTileRect) const; |
michael@0 | 41 | f32x4_t Noise2(Point aVec, const StitchInfo& aStitchInfo) const; |
michael@0 | 42 | i32x4_t Turbulence(const Point &aPoint) const; |
michael@0 | 43 | Point EquivalentNonNegativeOffset(const Point &aOffset) const; |
michael@0 | 44 | |
michael@0 | 45 | Size mBaseFrequency; |
michael@0 | 46 | int32_t mNumOctaves; |
michael@0 | 47 | StitchInfo mStitchInfo; |
michael@0 | 48 | bool mStitchable; |
michael@0 | 49 | TurbulenceType mType; |
michael@0 | 50 | uint8_t mLatticeSelector[sBSize]; |
michael@0 | 51 | f32x4_t mGradient[sBSize][2]; |
michael@0 | 52 | }; |
michael@0 | 53 | |
michael@0 | 54 | namespace { |
michael@0 | 55 | |
michael@0 | 56 | struct RandomNumberSource |
michael@0 | 57 | { |
michael@0 | 58 | RandomNumberSource(int32_t aSeed) : mLast(SetupSeed(aSeed)) {} |
michael@0 | 59 | int32_t Next() { mLast = Random(mLast); return mLast; } |
michael@0 | 60 | |
michael@0 | 61 | private: |
michael@0 | 62 | static const int32_t RAND_M = 2147483647; /* 2**31 - 1 */ |
michael@0 | 63 | static const int32_t RAND_A = 16807; /* 7**5; primitive root of m */ |
michael@0 | 64 | static const int32_t RAND_Q = 127773; /* m / a */ |
michael@0 | 65 | static const int32_t RAND_R = 2836; /* m % a */ |
michael@0 | 66 | |
michael@0 | 67 | /* Produces results in the range [1, 2**31 - 2]. |
michael@0 | 68 | Algorithm is: r = (a * r) mod m |
michael@0 | 69 | where a = 16807 and m = 2**31 - 1 = 2147483647 |
michael@0 | 70 | See [Park & Miller], CACM vol. 31 no. 10 p. 1195, Oct. 1988 |
michael@0 | 71 | To test: the algorithm should produce the result 1043618065 |
michael@0 | 72 | as the 10,000th generated number if the original seed is 1. |
michael@0 | 73 | */ |
michael@0 | 74 | |
michael@0 | 75 | static int32_t |
michael@0 | 76 | SetupSeed(int32_t aSeed) { |
michael@0 | 77 | if (aSeed <= 0) |
michael@0 | 78 | aSeed = -(aSeed % (RAND_M - 1)) + 1; |
michael@0 | 79 | if (aSeed > RAND_M - 1) |
michael@0 | 80 | aSeed = RAND_M - 1; |
michael@0 | 81 | return aSeed; |
michael@0 | 82 | } |
michael@0 | 83 | |
michael@0 | 84 | static int32_t |
michael@0 | 85 | Random(int32_t aSeed) |
michael@0 | 86 | { |
michael@0 | 87 | int32_t result = RAND_A * (aSeed % RAND_Q) - RAND_R * (aSeed / RAND_Q); |
michael@0 | 88 | if (result <= 0) |
michael@0 | 89 | result += RAND_M; |
michael@0 | 90 | return result; |
michael@0 | 91 | } |
michael@0 | 92 | |
michael@0 | 93 | int32_t mLast; |
michael@0 | 94 | }; |
michael@0 | 95 | |
michael@0 | 96 | } // unnamed namespace |
michael@0 | 97 | |
michael@0 | 98 | template<TurbulenceType Type, bool Stitch, typename f32x4_t, typename i32x4_t, typename u8x16_t> |
michael@0 | 99 | SVGTurbulenceRenderer<Type,Stitch,f32x4_t,i32x4_t,u8x16_t>::SVGTurbulenceRenderer(const Size &aBaseFrequency, int32_t aSeed, |
michael@0 | 100 | int aNumOctaves, const Rect &aTileRect) |
michael@0 | 101 | : mBaseFrequency(aBaseFrequency) |
michael@0 | 102 | , mNumOctaves(aNumOctaves) |
michael@0 | 103 | { |
michael@0 | 104 | InitFromSeed(aSeed); |
michael@0 | 105 | if (Stitch) { |
michael@0 | 106 | AdjustBaseFrequencyForStitch(aTileRect); |
michael@0 | 107 | mStitchInfo = CreateStitchInfo(aTileRect); |
michael@0 | 108 | } |
michael@0 | 109 | } |
michael@0 | 110 | |
michael@0 | 111 | template<typename T> |
michael@0 | 112 | static void |
michael@0 | 113 | Swap(T& a, T& b) { |
michael@0 | 114 | T c = a; |
michael@0 | 115 | a = b; |
michael@0 | 116 | b = c; |
michael@0 | 117 | } |
michael@0 | 118 | |
michael@0 | 119 | template<TurbulenceType Type, bool Stitch, typename f32x4_t, typename i32x4_t, typename u8x16_t> |
michael@0 | 120 | void |
michael@0 | 121 | SVGTurbulenceRenderer<Type,Stitch,f32x4_t,i32x4_t,u8x16_t>::InitFromSeed(int32_t aSeed) |
michael@0 | 122 | { |
michael@0 | 123 | RandomNumberSource rand(aSeed); |
michael@0 | 124 | |
michael@0 | 125 | float gradient[4][sBSize][2]; |
michael@0 | 126 | for (int32_t k = 0; k < 4; k++) { |
michael@0 | 127 | for (int32_t i = 0; i < sBSize; i++) { |
michael@0 | 128 | float a = float((rand.Next() % (sBSize + sBSize)) - sBSize) / sBSize; |
michael@0 | 129 | float b = float((rand.Next() % (sBSize + sBSize)) - sBSize) / sBSize; |
michael@0 | 130 | float s = sqrt(a * a + b * b); |
michael@0 | 131 | gradient[k][i][0] = a / s; |
michael@0 | 132 | gradient[k][i][1] = b / s; |
michael@0 | 133 | } |
michael@0 | 134 | } |
michael@0 | 135 | |
michael@0 | 136 | for (int32_t i = 0; i < sBSize; i++) { |
michael@0 | 137 | mLatticeSelector[i] = i; |
michael@0 | 138 | } |
michael@0 | 139 | for (int32_t i1 = sBSize - 1; i1 > 0; i1--) { |
michael@0 | 140 | int32_t i2 = rand.Next() % sBSize; |
michael@0 | 141 | Swap(mLatticeSelector[i1], mLatticeSelector[i2]); |
michael@0 | 142 | } |
michael@0 | 143 | |
michael@0 | 144 | for (int32_t i = 0; i < sBSize; i++) { |
michael@0 | 145 | // Contrary to the code in the spec, we build the first lattice selector |
michael@0 | 146 | // lookup into mGradient so that we don't need to do it again for every |
michael@0 | 147 | // pixel. |
michael@0 | 148 | // We also change the order of the gradient indexing so that we can process |
michael@0 | 149 | // all four color channels at the same time. |
michael@0 | 150 | uint8_t j = mLatticeSelector[i]; |
michael@0 | 151 | mGradient[i][0] = simd::FromF32<f32x4_t>(gradient[2][j][0], gradient[1][j][0], |
michael@0 | 152 | gradient[0][j][0], gradient[3][j][0]); |
michael@0 | 153 | mGradient[i][1] = simd::FromF32<f32x4_t>(gradient[2][j][1], gradient[1][j][1], |
michael@0 | 154 | gradient[0][j][1], gradient[3][j][1]); |
michael@0 | 155 | } |
michael@0 | 156 | } |
michael@0 | 157 | |
michael@0 | 158 | // Adjust aFreq such that aLength * AdjustForLength(aFreq, aLength) is integer |
michael@0 | 159 | // and as close to aLength * aFreq as possible. |
michael@0 | 160 | static inline float |
michael@0 | 161 | AdjustForLength(float aFreq, float aLength) |
michael@0 | 162 | { |
michael@0 | 163 | float lowFreq = floor(aLength * aFreq) / aLength; |
michael@0 | 164 | float hiFreq = ceil(aLength * aFreq) / aLength; |
michael@0 | 165 | if (aFreq / lowFreq < hiFreq / aFreq) { |
michael@0 | 166 | return lowFreq; |
michael@0 | 167 | } |
michael@0 | 168 | return hiFreq; |
michael@0 | 169 | } |
michael@0 | 170 | |
michael@0 | 171 | template<TurbulenceType Type, bool Stitch, typename f32x4_t, typename i32x4_t, typename u8x16_t> |
michael@0 | 172 | void |
michael@0 | 173 | SVGTurbulenceRenderer<Type,Stitch,f32x4_t,i32x4_t,u8x16_t>::AdjustBaseFrequencyForStitch(const Rect &aTileRect) |
michael@0 | 174 | { |
michael@0 | 175 | mBaseFrequency = Size(AdjustForLength(mBaseFrequency.width, aTileRect.width), |
michael@0 | 176 | AdjustForLength(mBaseFrequency.height, aTileRect.height)); |
michael@0 | 177 | } |
michael@0 | 178 | |
michael@0 | 179 | template<TurbulenceType Type, bool Stitch, typename f32x4_t, typename i32x4_t, typename u8x16_t> |
michael@0 | 180 | typename SVGTurbulenceRenderer<Type,Stitch,f32x4_t,i32x4_t,u8x16_t>::StitchInfo |
michael@0 | 181 | SVGTurbulenceRenderer<Type,Stitch,f32x4_t,i32x4_t,u8x16_t>::CreateStitchInfo(const Rect &aTileRect) const |
michael@0 | 182 | { |
michael@0 | 183 | StitchInfo stitch; |
michael@0 | 184 | stitch.width = int32_t(floorf(aTileRect.width * mBaseFrequency.width + 0.5f)); |
michael@0 | 185 | stitch.height = int32_t(floorf(aTileRect.height * mBaseFrequency.height + 0.5f)); |
michael@0 | 186 | stitch.wrapX = int32_t(aTileRect.x * mBaseFrequency.width) + stitch.width; |
michael@0 | 187 | stitch.wrapY = int32_t(aTileRect.y * mBaseFrequency.height) + stitch.height; |
michael@0 | 188 | return stitch; |
michael@0 | 189 | } |
michael@0 | 190 | |
michael@0 | 191 | static MOZ_ALWAYS_INLINE Float |
michael@0 | 192 | SCurve(Float t) |
michael@0 | 193 | { |
michael@0 | 194 | return t * t * (3 - 2 * t); |
michael@0 | 195 | } |
michael@0 | 196 | |
michael@0 | 197 | static MOZ_ALWAYS_INLINE Point |
michael@0 | 198 | SCurve(Point t) |
michael@0 | 199 | { |
michael@0 | 200 | return Point(SCurve(t.x), SCurve(t.y)); |
michael@0 | 201 | } |
michael@0 | 202 | |
michael@0 | 203 | template<typename f32x4_t> |
michael@0 | 204 | static MOZ_ALWAYS_INLINE f32x4_t |
michael@0 | 205 | BiMix(const f32x4_t& aa, const f32x4_t& ab, |
michael@0 | 206 | const f32x4_t& ba, const f32x4_t& bb, Point s) |
michael@0 | 207 | { |
michael@0 | 208 | return simd::MixF32(simd::MixF32(aa, ab, s.x), |
michael@0 | 209 | simd::MixF32(ba, bb, s.x), s.y); |
michael@0 | 210 | } |
michael@0 | 211 | |
michael@0 | 212 | template<TurbulenceType Type, bool Stitch, typename f32x4_t, typename i32x4_t, typename u8x16_t> |
michael@0 | 213 | IntPoint |
michael@0 | 214 | SVGTurbulenceRenderer<Type,Stitch,f32x4_t,i32x4_t,u8x16_t>::AdjustForStitch(IntPoint aLatticePoint, |
michael@0 | 215 | const StitchInfo& aStitchInfo) const |
michael@0 | 216 | { |
michael@0 | 217 | if (Stitch) { |
michael@0 | 218 | if (aLatticePoint.x >= aStitchInfo.wrapX) { |
michael@0 | 219 | aLatticePoint.x -= aStitchInfo.width; |
michael@0 | 220 | } |
michael@0 | 221 | if (aLatticePoint.y >= aStitchInfo.wrapY) { |
michael@0 | 222 | aLatticePoint.y -= aStitchInfo.height; |
michael@0 | 223 | } |
michael@0 | 224 | } |
michael@0 | 225 | return aLatticePoint; |
michael@0 | 226 | } |
michael@0 | 227 | |
michael@0 | 228 | template<TurbulenceType Type, bool Stitch, typename f32x4_t, typename i32x4_t, typename u8x16_t> |
michael@0 | 229 | f32x4_t |
michael@0 | 230 | SVGTurbulenceRenderer<Type,Stitch,f32x4_t,i32x4_t,u8x16_t>::Noise2(Point aVec, const StitchInfo& aStitchInfo) const |
michael@0 | 231 | { |
michael@0 | 232 | // aVec is guaranteed to be non-negative, so casting to int32_t always |
michael@0 | 233 | // rounds towards negative infinity. |
michael@0 | 234 | IntPoint topLeftLatticePoint(int32_t(aVec.x), int32_t(aVec.y)); |
michael@0 | 235 | Point r = aVec - topLeftLatticePoint; // fractional offset |
michael@0 | 236 | |
michael@0 | 237 | IntPoint b0 = AdjustForStitch(topLeftLatticePoint, aStitchInfo); |
michael@0 | 238 | IntPoint b1 = AdjustForStitch(b0 + IntPoint(1, 1), aStitchInfo); |
michael@0 | 239 | |
michael@0 | 240 | uint8_t i = mLatticeSelector[b0.x & sBM]; |
michael@0 | 241 | uint8_t j = mLatticeSelector[b1.x & sBM]; |
michael@0 | 242 | |
michael@0 | 243 | const f32x4_t* qua = mGradient[(i + b0.y) & sBM]; |
michael@0 | 244 | const f32x4_t* qub = mGradient[(i + b1.y) & sBM]; |
michael@0 | 245 | const f32x4_t* qva = mGradient[(j + b0.y) & sBM]; |
michael@0 | 246 | const f32x4_t* qvb = mGradient[(j + b1.y) & sBM]; |
michael@0 | 247 | |
michael@0 | 248 | return BiMix(simd::WSumF32(qua[0], qua[1], r.x, r.y), |
michael@0 | 249 | simd::WSumF32(qva[0], qva[1], r.x - 1, r.y), |
michael@0 | 250 | simd::WSumF32(qub[0], qub[1], r.x, r.y - 1), |
michael@0 | 251 | simd::WSumF32(qvb[0], qvb[1], r.x - 1, r.y - 1), |
michael@0 | 252 | SCurve(r)); |
michael@0 | 253 | } |
michael@0 | 254 | |
michael@0 | 255 | template<typename f32x4_t, typename i32x4_t, typename u8x16_t> |
michael@0 | 256 | static inline i32x4_t |
michael@0 | 257 | ColorToBGRA(f32x4_t aUnscaledUnpreFloat) |
michael@0 | 258 | { |
michael@0 | 259 | // Color is an unpremultiplied float vector where 1.0f means white. We will |
michael@0 | 260 | // convert it into an integer vector where 255 means white. |
michael@0 | 261 | f32x4_t alpha = simd::SplatF32<3>(aUnscaledUnpreFloat); |
michael@0 | 262 | f32x4_t scaledUnpreFloat = simd::MulF32(aUnscaledUnpreFloat, simd::FromF32<f32x4_t>(255)); |
michael@0 | 263 | i32x4_t scaledUnpreInt = simd::F32ToI32(scaledUnpreFloat); |
michael@0 | 264 | |
michael@0 | 265 | // Multiply all channels with alpha. |
michael@0 | 266 | i32x4_t scaledPreInt = simd::F32ToI32(simd::MulF32(scaledUnpreFloat, alpha)); |
michael@0 | 267 | |
michael@0 | 268 | // Use the premultiplied color channels and the unpremultiplied alpha channel. |
michael@0 | 269 | i32x4_t alphaMask = simd::From32<i32x4_t>(0, 0, 0, -1); |
michael@0 | 270 | return simd::Pick(alphaMask, scaledPreInt, scaledUnpreInt); |
michael@0 | 271 | } |
michael@0 | 272 | |
michael@0 | 273 | template<TurbulenceType Type, bool Stitch, typename f32x4_t, typename i32x4_t, typename u8x16_t> |
michael@0 | 274 | i32x4_t |
michael@0 | 275 | SVGTurbulenceRenderer<Type,Stitch,f32x4_t,i32x4_t,u8x16_t>::Turbulence(const Point &aPoint) const |
michael@0 | 276 | { |
michael@0 | 277 | StitchInfo stitchInfo = mStitchInfo; |
michael@0 | 278 | f32x4_t sum = simd::FromF32<f32x4_t>(0); |
michael@0 | 279 | Point vec(aPoint.x * mBaseFrequency.width, aPoint.y * mBaseFrequency.height); |
michael@0 | 280 | f32x4_t ratio = simd::FromF32<f32x4_t>(1); |
michael@0 | 281 | |
michael@0 | 282 | for (int octave = 0; octave < mNumOctaves; octave++) { |
michael@0 | 283 | f32x4_t thisOctave = Noise2(vec, stitchInfo); |
michael@0 | 284 | if (Type == TURBULENCE_TYPE_TURBULENCE) { |
michael@0 | 285 | thisOctave = simd::AbsF32(thisOctave); |
michael@0 | 286 | } |
michael@0 | 287 | sum = simd::AddF32(sum, simd::DivF32(thisOctave, ratio)); |
michael@0 | 288 | vec = vec * 2; |
michael@0 | 289 | ratio = simd::MulF32(ratio, simd::FromF32<f32x4_t>(2)); |
michael@0 | 290 | |
michael@0 | 291 | if (Stitch) { |
michael@0 | 292 | stitchInfo.width *= 2; |
michael@0 | 293 | stitchInfo.wrapX *= 2; |
michael@0 | 294 | stitchInfo.height *= 2; |
michael@0 | 295 | stitchInfo.wrapY *= 2; |
michael@0 | 296 | } |
michael@0 | 297 | } |
michael@0 | 298 | |
michael@0 | 299 | if (Type == TURBULENCE_TYPE_FRACTAL_NOISE) { |
michael@0 | 300 | sum = simd::DivF32(simd::AddF32(sum, simd::FromF32<f32x4_t>(1)), simd::FromF32<f32x4_t>(2)); |
michael@0 | 301 | } |
michael@0 | 302 | return ColorToBGRA<f32x4_t,i32x4_t,u8x16_t>(sum); |
michael@0 | 303 | } |
michael@0 | 304 | |
michael@0 | 305 | static inline Float |
michael@0 | 306 | MakeNonNegative(Float aValue, Float aIncrementSize) |
michael@0 | 307 | { |
michael@0 | 308 | if (aValue >= 0) { |
michael@0 | 309 | return aValue; |
michael@0 | 310 | } |
michael@0 | 311 | return aValue + ceilf(-aValue / aIncrementSize) * aIncrementSize; |
michael@0 | 312 | } |
michael@0 | 313 | |
michael@0 | 314 | template<TurbulenceType Type, bool Stitch, typename f32x4_t, typename i32x4_t, typename u8x16_t> |
michael@0 | 315 | Point |
michael@0 | 316 | SVGTurbulenceRenderer<Type,Stitch,f32x4_t,i32x4_t,u8x16_t>::EquivalentNonNegativeOffset(const Point &aOffset) const |
michael@0 | 317 | { |
michael@0 | 318 | Size basePeriod = Stitch ? Size(mStitchInfo.width, mStitchInfo.height) : |
michael@0 | 319 | Size(sBSize, sBSize); |
michael@0 | 320 | Size repeatingSize(basePeriod.width / mBaseFrequency.width, |
michael@0 | 321 | basePeriod.height / mBaseFrequency.height); |
michael@0 | 322 | return Point(MakeNonNegative(aOffset.x, repeatingSize.width), |
michael@0 | 323 | MakeNonNegative(aOffset.y, repeatingSize.height)); |
michael@0 | 324 | } |
michael@0 | 325 | |
michael@0 | 326 | template<TurbulenceType Type, bool Stitch, typename f32x4_t, typename i32x4_t, typename u8x16_t> |
michael@0 | 327 | TemporaryRef<DataSourceSurface> |
michael@0 | 328 | SVGTurbulenceRenderer<Type,Stitch,f32x4_t,i32x4_t,u8x16_t>::Render(const IntSize &aSize, const Point &aOffset) const |
michael@0 | 329 | { |
michael@0 | 330 | RefPtr<DataSourceSurface> target = |
michael@0 | 331 | Factory::CreateDataSourceSurface(aSize, SurfaceFormat::B8G8R8A8); |
michael@0 | 332 | if (!target) { |
michael@0 | 333 | return nullptr; |
michael@0 | 334 | } |
michael@0 | 335 | |
michael@0 | 336 | uint8_t* targetData = target->GetData(); |
michael@0 | 337 | uint32_t stride = target->Stride(); |
michael@0 | 338 | |
michael@0 | 339 | Point startOffset = EquivalentNonNegativeOffset(aOffset); |
michael@0 | 340 | |
michael@0 | 341 | for (int32_t y = 0; y < aSize.height; y++) { |
michael@0 | 342 | for (int32_t x = 0; x < aSize.width; x += 4) { |
michael@0 | 343 | int32_t targIndex = y * stride + x * 4; |
michael@0 | 344 | i32x4_t a = Turbulence(startOffset + Point(x, y)); |
michael@0 | 345 | i32x4_t b = Turbulence(startOffset + Point(x + 1, y)); |
michael@0 | 346 | i32x4_t c = Turbulence(startOffset + Point(x + 2, y)); |
michael@0 | 347 | i32x4_t d = Turbulence(startOffset + Point(x + 3, y)); |
michael@0 | 348 | u8x16_t result1234 = simd::PackAndSaturate32To8(a, b, c, d); |
michael@0 | 349 | simd::Store8(&targetData[targIndex], result1234); |
michael@0 | 350 | } |
michael@0 | 351 | } |
michael@0 | 352 | |
michael@0 | 353 | return target; |
michael@0 | 354 | } |
michael@0 | 355 | |
michael@0 | 356 | } // namespace gfx |
michael@0 | 357 | } // namespace mozilla |