1/*
2 * Copyright (C) 2015 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
17#include <gtest/gtest.h>
18
19#include <BakedOpState.h>
20#include <ClipArea.h>
21#include <RecordedOp.h>
22#include <tests/common/TestUtils.h>
23
24namespace android {
25namespace uirenderer {
26
27TEST(ResolvedRenderState, construct) {
28    LinearAllocator allocator;
29    Matrix4 translate10x20;
30    translate10x20.loadTranslate(10, 20, 0);
31
32    SkPaint paint;
33    ClipRect clip(Rect(100, 200));
34    RectOp recordedOp(Rect(30, 40, 100, 200), translate10x20, &clip, &paint);
35    {
36        // recorded with transform, no parent transform
37        auto parentSnapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200));
38        ResolvedRenderState state(allocator, *parentSnapshot, recordedOp, false, false);
39        EXPECT_MATRIX_APPROX_EQ(state.transform, translate10x20);
40        EXPECT_EQ(Rect(100, 200), state.clipRect());
41        EXPECT_EQ(Rect(40, 60, 100, 200), state.clippedBounds); // translated and also clipped
42        EXPECT_EQ(OpClipSideFlags::Right | OpClipSideFlags::Bottom, state.clipSideFlags);
43    }
44    {
45        // recorded with transform and parent transform
46        auto parentSnapshot = TestUtils::makeSnapshot(translate10x20, Rect(100, 200));
47        ResolvedRenderState state(allocator, *parentSnapshot, recordedOp, false, false);
48
49        Matrix4 expectedTranslate;
50        expectedTranslate.loadTranslate(20, 40, 0);
51        EXPECT_MATRIX_APPROX_EQ(expectedTranslate, state.transform);
52
53        // intersection of parent & transformed child clip
54        EXPECT_EQ(Rect(10, 20, 100, 200), state.clipRect());
55
56        // translated and also clipped
57        EXPECT_EQ(Rect(50, 80, 100, 200), state.clippedBounds);
58        EXPECT_EQ(OpClipSideFlags::Right | OpClipSideFlags::Bottom, state.clipSideFlags);
59    }
60}
61
62TEST(ResolvedRenderState, computeLocalSpaceClip) {
63    LinearAllocator allocator;
64    Matrix4 translate10x20;
65    translate10x20.loadTranslate(10, 20, 0);
66
67    SkPaint paint;
68    ClipRect clip(Rect(100, 200));
69    RectOp recordedOp(Rect(1000, 1000), translate10x20, &clip, &paint);
70    {
71        // recorded with transform, no parent transform
72        auto parentSnapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200));
73        ResolvedRenderState state(allocator, *parentSnapshot, recordedOp, false, false);
74        EXPECT_EQ(Rect(-10, -20, 90, 180), state.computeLocalSpaceClip())
75            << "Local clip rect should be 100x200, offset by -10,-20";
76    }
77    {
78        // recorded with transform + parent transform
79        auto parentSnapshot = TestUtils::makeSnapshot(translate10x20, Rect(100, 200));
80        ResolvedRenderState state(allocator, *parentSnapshot, recordedOp, false, false);
81        EXPECT_EQ(Rect(-10, -20, 80, 160), state.computeLocalSpaceClip())
82            << "Local clip rect should be 90x190, offset by -10,-20";
83    }
84}
85
86const float HAIRLINE = 0.0f;
87
88// Note: bounds will be conservative, but not precise for non-hairline
89// - use approx bounds checks for these
90const float SEMI_HAIRLINE = 0.3f;
91
92struct StrokeTestCase {
93    float scale;
94    float strokeWidth;
95    const std::function<void(const ResolvedRenderState&)> validator;
96};
97
98const static StrokeTestCase sStrokeTestCases[] = {
99    {
100        1, HAIRLINE, [](const ResolvedRenderState& state) {
101            EXPECT_EQ(Rect(49.5f, 49.5f, 150.5f, 150.5f), state.clippedBounds);
102        }
103    },
104    {
105        1, SEMI_HAIRLINE, [](const ResolvedRenderState& state) {
106            EXPECT_TRUE(state.clippedBounds.contains(49.5f, 49.5f, 150.5f, 150.5f));
107            EXPECT_TRUE(Rect(49, 49, 151, 151).contains(state.clippedBounds));
108        }
109    },
110    {
111        1, 20, [](const ResolvedRenderState& state) {
112            EXPECT_EQ(Rect(40, 40, 160, 160), state.clippedBounds);
113        }
114    },
115
116    // 3x3 scale:
117    {
118        3, HAIRLINE, [](const ResolvedRenderState& state) {
119            EXPECT_EQ(Rect(149.5f, 149.5f, 200, 200), state.clippedBounds);
120            EXPECT_EQ(OpClipSideFlags::Right | OpClipSideFlags::Bottom, state.clipSideFlags);
121        }
122    },
123    {
124        3, SEMI_HAIRLINE, [](const ResolvedRenderState& state) {
125            EXPECT_TRUE(state.clippedBounds.contains(149.5f, 149.5f, 200, 200));
126            EXPECT_TRUE(Rect(149, 149, 200, 200).contains(state.clippedBounds));
127        }
128    },
129    {
130        3, 20, [](const ResolvedRenderState& state) {
131            EXPECT_TRUE(state.clippedBounds.contains(120, 120, 200, 200));
132            EXPECT_TRUE(Rect(119, 119, 200, 200).contains(state.clippedBounds));
133        }
134    },
135
136    // 0.5f x 0.5f scale
137    {
138        0.5f, HAIRLINE, [](const ResolvedRenderState& state) {
139            EXPECT_EQ(Rect(24.5f, 24.5f, 75.5f, 75.5f), state.clippedBounds);
140        }
141    },
142    {
143        0.5f, SEMI_HAIRLINE, [](const ResolvedRenderState& state) {
144            EXPECT_TRUE(state.clippedBounds.contains(24.5f, 24.5f, 75.5f, 75.5f));
145            EXPECT_TRUE(Rect(24, 24, 76, 76).contains(state.clippedBounds));
146        }
147    },
148    {
149        0.5f, 20, [](const ResolvedRenderState& state) {
150            EXPECT_TRUE(state.clippedBounds.contains(19.5f, 19.5f, 80.5f, 80.5f));
151            EXPECT_TRUE(Rect(19, 19, 81, 81).contains(state.clippedBounds));
152        }
153    }
154};
155
156TEST(ResolvedRenderState, construct_expandForStroke) {
157    LinearAllocator allocator;
158    // Loop over table of test cases and verify different combinations of stroke width and transform
159    for (auto&& testCase : sStrokeTestCases) {
160        SkPaint strokedPaint;
161        strokedPaint.setAntiAlias(true);
162        strokedPaint.setStyle(SkPaint::kStroke_Style);
163        strokedPaint.setStrokeWidth(testCase.strokeWidth);
164
165        ClipRect clip(Rect(200, 200));
166        RectOp recordedOp(Rect(50, 50, 150, 150),
167                Matrix4::identity(), &clip, &strokedPaint);
168
169        Matrix4 snapshotMatrix;
170        snapshotMatrix.loadScale(testCase.scale, testCase.scale, 1);
171        auto parentSnapshot = TestUtils::makeSnapshot(snapshotMatrix, Rect(200, 200));
172
173        ResolvedRenderState state(allocator, *parentSnapshot, recordedOp, true, false);
174        testCase.validator(state);
175    }
176}
177
178TEST(BakedOpState, tryConstruct) {
179    Matrix4 translate100x0;
180    translate100x0.loadTranslate(100, 0, 0);
181
182    SkPaint paint;
183    ClipRect clip(Rect(100, 200));
184
185    LinearAllocator allocator;
186    RectOp successOp(Rect(30, 40, 100, 200), Matrix4::identity(), &clip, &paint);
187    auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200));
188    EXPECT_NE(nullptr, BakedOpState::tryConstruct(allocator, *snapshot, successOp))
189            << "successOp NOT rejected by clip, so should be constructed";
190    size_t successAllocSize = allocator.usedSize();
191    EXPECT_LE(64u, successAllocSize) << "relatively large alloc for non-rejected op";
192
193    RectOp rejectOp(Rect(30, 40, 100, 200), translate100x0, &clip, &paint);
194    EXPECT_EQ(nullptr, BakedOpState::tryConstruct(allocator, *snapshot, rejectOp))
195            << "rejectOp rejected by clip, so should not be constructed";
196
197    // NOTE: this relies on the clip having already been serialized by the op above
198    EXPECT_EQ(successAllocSize, allocator.usedSize()) << "no extra allocation used for rejected op";
199}
200
201TEST(BakedOpState, tryShadowOpConstruct) {
202    Matrix4 translate10x20;
203    translate10x20.loadTranslate(10, 20, 0);
204
205    LinearAllocator allocator;
206    {
207        auto snapshot = TestUtils::makeSnapshot(translate10x20, Rect()); // Note: empty clip
208        BakedOpState* bakedState = BakedOpState::tryShadowOpConstruct(allocator, *snapshot, (ShadowOp*)0x1234);
209
210        EXPECT_EQ(nullptr, bakedState) << "op should be rejected by clip, so not constructed";
211        EXPECT_EQ(0u, allocator.usedSize()) << "no serialization, even for clip,"
212                "since op is quick rejected based on snapshot clip";
213    }
214    {
215        auto snapshot = TestUtils::makeSnapshot(translate10x20, Rect(100, 200));
216        BakedOpState* bakedState = BakedOpState::tryShadowOpConstruct(allocator, *snapshot, (ShadowOp*)0x1234);
217
218        ASSERT_NE(nullptr, bakedState) << "NOT rejected by clip, so op should be constructed";
219        EXPECT_LE(64u, allocator.usedSize()) << "relatively large alloc for non-rejected op";
220
221        EXPECT_MATRIX_APPROX_EQ(translate10x20, bakedState->computedState.transform);
222        EXPECT_EQ(Rect(100, 200), bakedState->computedState.clippedBounds);
223    }
224}
225
226TEST(BakedOpState, tryStrokeableOpConstruct) {
227    LinearAllocator allocator;
228    {
229        // check regular rejection
230        SkPaint paint;
231        paint.setStyle(SkPaint::kStrokeAndFill_Style);
232        paint.setStrokeWidth(0.0f);
233        ClipRect clip(Rect(100, 200));
234        RectOp rejectOp(Rect(100, 200), Matrix4::identity(), &clip, &paint);
235        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect()); // Note: empty clip
236        auto bakedState = BakedOpState::tryStrokeableOpConstruct(allocator, *snapshot, rejectOp,
237                BakedOpState::StrokeBehavior::StyleDefined, false);
238
239        EXPECT_EQ(nullptr, bakedState);
240        EXPECT_GT(8u, allocator.usedSize()); // no significant allocation space used for rejected op
241    }
242    {
243        // check simple unscaled expansion
244        SkPaint paint;
245        paint.setStyle(SkPaint::kStrokeAndFill_Style);
246        paint.setStrokeWidth(10.0f);
247        ClipRect clip(Rect(200, 200));
248        RectOp rejectOp(Rect(50, 50, 150, 150), Matrix4::identity(), &clip, &paint);
249        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(200, 200));
250        auto bakedState = BakedOpState::tryStrokeableOpConstruct(allocator, *snapshot, rejectOp,
251                BakedOpState::StrokeBehavior::StyleDefined, false);
252
253        ASSERT_NE(nullptr, bakedState);
254        EXPECT_EQ(Rect(45, 45, 155, 155), bakedState->computedState.clippedBounds);
255        EXPECT_EQ(0, bakedState->computedState.clipSideFlags);
256    }
257    {
258        // check simple unscaled expansion, and fill style with stroke forced
259        SkPaint paint;
260        paint.setStyle(SkPaint::kFill_Style);
261        paint.setStrokeWidth(10.0f);
262        ClipRect clip(Rect(200, 200));
263        RectOp rejectOp(Rect(50, 50, 150, 150), Matrix4::identity(), &clip, &paint);
264        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(200, 200));
265        auto bakedState = BakedOpState::tryStrokeableOpConstruct(allocator, *snapshot, rejectOp,
266                BakedOpState::StrokeBehavior::Forced, false);
267
268        ASSERT_NE(nullptr, bakedState);
269        EXPECT_EQ(Rect(45, 45, 155, 155), bakedState->computedState.clippedBounds);
270        EXPECT_EQ(0, bakedState->computedState.clipSideFlags);
271    }
272}
273
274} // namespace uirenderer
275} // namespace android
276