1 /*
2 * Copyright 2017 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "include/core/SkTypes.h"
9
10 #include "include/core/SkCanvas.h"
11 #include "include/core/SkPoint.h"
12 #include "include/core/SkSurface.h"
13 #include "include/gpu/GrBackendSurface.h"
14 #include "include/gpu/GrContext.h"
15 #include "src/gpu/GrBackendTextureImageGenerator.h"
16 #include "src/gpu/GrContextPriv.h"
17 #include "src/gpu/GrDrawingManager.h"
18 #include "src/gpu/GrGpu.h"
19 #include "src/gpu/GrRecordingContextPriv.h"
20 #include "src/gpu/GrRenderTargetContext.h"
21 #include "src/gpu/GrSemaphore.h"
22 #include "src/gpu/GrSurfaceProxyPriv.h"
23 #include "src/gpu/GrTexturePriv.h"
24 #include "src/gpu/GrTextureProxy.h"
25 #include "src/gpu/SkGpuDevice.h"
26 #include "src/image/SkImage_Base.h"
27 #include "src/image/SkSurface_Gpu.h"
28 #include "tests/Test.h"
29
30 static constexpr int kSize = 8;
31
32 // Test that the correct mip map states are on the GrTextures when wrapping GrBackendTextures in
33 // SkImages and SkSurfaces
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrWrappedMipMappedTest,reporter,ctxInfo)34 DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrWrappedMipMappedTest, reporter, ctxInfo) {
35 GrContext* context = ctxInfo.grContext();
36 if (!context->priv().caps()->mipMapSupport()) {
37 return;
38 }
39
40 for (auto mipMapped : {GrMipMapped::kNo, GrMipMapped::kYes}) {
41 for (auto renderable : {GrRenderable::kNo, GrRenderable::kYes}) {
42 // createBackendTexture currently doesn't support uploading data to mip maps
43 // so we don't send any. However, we pretend there is data for the checks below which is
44 // fine since we are never actually using these textures for any work on the gpu.
45 GrBackendTexture backendTex = context->createBackendTexture(
46 kSize, kSize, kRGBA_8888_SkColorType,
47 SkColors::kTransparent, mipMapped, renderable, GrProtected::kNo);
48
49 sk_sp<GrTextureProxy> proxy;
50 sk_sp<SkImage> image;
51 if (GrRenderable::kYes == renderable) {
52 sk_sp<SkSurface> surface = SkSurface::MakeFromBackendTexture(
53 context,
54 backendTex,
55 kTopLeft_GrSurfaceOrigin,
56 0,
57 kRGBA_8888_SkColorType,
58 nullptr,
59 nullptr);
60
61 SkGpuDevice* device = ((SkSurface_Gpu*)surface.get())->getDevice();
62 proxy = device->accessRenderTargetContext()->asTextureProxyRef();
63 } else {
64 image = SkImage::MakeFromTexture(context, backendTex,
65 kTopLeft_GrSurfaceOrigin,
66 kRGBA_8888_SkColorType,
67 kPremul_SkAlphaType, nullptr,
68 nullptr, nullptr);
69 const GrSurfaceProxyView* view = as_IB(image)->view(context);
70 REPORTER_ASSERT(reporter, view);
71 if (!view) {
72 context->deleteBackendTexture(backendTex);
73 return;
74 }
75 proxy = view->asTextureProxyRef();
76 }
77 REPORTER_ASSERT(reporter, proxy);
78 if (!proxy) {
79 context->deleteBackendTexture(backendTex);
80 return;
81 }
82
83 REPORTER_ASSERT(reporter, proxy->isInstantiated());
84
85 GrTexture* texture = proxy->peekTexture();
86 REPORTER_ASSERT(reporter, texture);
87 if (!texture) {
88 context->deleteBackendTexture(backendTex);
89 return;
90 }
91
92 if (GrMipMapped::kYes == mipMapped) {
93 REPORTER_ASSERT(reporter, GrMipMapped::kYes == texture->texturePriv().mipMapped());
94 if (GrRenderable::kYes == renderable) {
95 REPORTER_ASSERT(reporter, texture->texturePriv().mipMapsAreDirty());
96 } else {
97 REPORTER_ASSERT(reporter, !texture->texturePriv().mipMapsAreDirty());
98 }
99 } else {
100 REPORTER_ASSERT(reporter, GrMipMapped::kNo == texture->texturePriv().mipMapped());
101 }
102 context->deleteBackendTexture(backendTex);
103 }
104 }
105 }
106
107 // Test that we correctly copy or don't copy GrBackendTextures in the GrBackendTextureImageGenerator
108 // based on if we will use mips in the draw and the mip status of the GrBackendTexture.
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrBackendTextureImageMipMappedTest,reporter,ctxInfo)109 DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrBackendTextureImageMipMappedTest, reporter, ctxInfo) {
110 GrContext* context = ctxInfo.grContext();
111 if (!context->priv().caps()->mipMapSupport()) {
112 return;
113 }
114
115 for (auto mipMapped : {GrMipMapped::kNo, GrMipMapped::kYes}) {
116 for (auto willUseMips : {false, true}) {
117 GrBackendTexture backendTex = context->createBackendTexture(
118 kSize, kSize, kRGBA_8888_SkColorType,
119 SkColors::kTransparent, mipMapped, GrRenderable::kNo, GrProtected::kNo);
120
121 sk_sp<SkImage> image = SkImage::MakeFromTexture(context, backendTex,
122 kTopLeft_GrSurfaceOrigin,
123 kRGBA_8888_SkColorType,
124 kPremul_SkAlphaType, nullptr,
125 nullptr, nullptr);
126
127 GrTextureProxy* proxy = as_IB(image)->peekProxy();
128 REPORTER_ASSERT(reporter, proxy);
129 if (!proxy) {
130 context->deleteBackendTexture(backendTex);
131 return;
132 }
133
134 REPORTER_ASSERT(reporter, proxy->isInstantiated());
135
136 sk_sp<GrTexture> texture = sk_ref_sp(proxy->peekTexture());
137 REPORTER_ASSERT(reporter, texture);
138 if (!texture) {
139 context->deleteBackendTexture(backendTex);
140 return;
141 }
142
143 std::unique_ptr<SkImageGenerator> imageGen = GrBackendTextureImageGenerator::Make(
144 texture, kTopLeft_GrSurfaceOrigin, nullptr, kRGBA_8888_SkColorType,
145 kPremul_SkAlphaType, nullptr);
146 REPORTER_ASSERT(reporter, imageGen);
147 if (!imageGen) {
148 context->deleteBackendTexture(backendTex);
149 return;
150 }
151
152 SkIPoint origin = SkIPoint::Make(0,0);
153 SkImageInfo imageInfo = SkImageInfo::Make(kSize, kSize, kRGBA_8888_SkColorType,
154 kPremul_SkAlphaType);
155 GrSurfaceProxyView genView = imageGen->generateTexture(context, imageInfo, origin,
156 willUseMips);
157 GrSurfaceProxy* genProxy = genView.proxy();
158
159 REPORTER_ASSERT(reporter, genProxy);
160 if (!genProxy) {
161 context->deleteBackendTexture(backendTex);
162 return;
163 }
164
165 if (genProxy->isLazy()) {
166 genProxy->priv().doLazyInstantiation(context->priv().resourceProvider());
167 } else if (!genProxy->isInstantiated()) {
168 genProxy->instantiate(context->priv().resourceProvider());
169 }
170
171 REPORTER_ASSERT(reporter, genProxy->isInstantiated());
172 if (!genProxy->isInstantiated()) {
173 context->deleteBackendTexture(backendTex);
174 return;
175 }
176
177 GrTexture* genTexture = genProxy->peekTexture();
178 REPORTER_ASSERT(reporter, genTexture);
179 if (!genTexture) {
180 context->deleteBackendTexture(backendTex);
181 return;
182 }
183
184 GrBackendTexture genBackendTex = genTexture->getBackendTexture();
185
186 if (GrBackendApi::kOpenGL == genBackendTex.backend()) {
187 GrGLTextureInfo genTexInfo;
188 GrGLTextureInfo origTexInfo;
189 if (genBackendTex.getGLTextureInfo(&genTexInfo) &&
190 backendTex.getGLTextureInfo(&origTexInfo)) {
191 if (willUseMips && GrMipMapped::kNo == mipMapped) {
192 // We did a copy so the texture IDs should be different
193 REPORTER_ASSERT(reporter, origTexInfo.fID != genTexInfo.fID);
194 } else {
195 REPORTER_ASSERT(reporter, origTexInfo.fID == genTexInfo.fID);
196 }
197 } else {
198 ERRORF(reporter, "Failed to get GrGLTextureInfo");
199 }
200 #ifdef SK_VULKAN
201 } else if (GrBackendApi::kVulkan == genBackendTex.backend()) {
202 GrVkImageInfo genImageInfo;
203 GrVkImageInfo origImageInfo;
204 if (genBackendTex.getVkImageInfo(&genImageInfo) &&
205 backendTex.getVkImageInfo(&origImageInfo)) {
206 if (willUseMips && GrMipMapped::kNo == mipMapped) {
207 // We did a copy so the texture IDs should be different
208 REPORTER_ASSERT(reporter, origImageInfo.fImage != genImageInfo.fImage);
209 } else {
210 REPORTER_ASSERT(reporter, origImageInfo.fImage == genImageInfo.fImage);
211 }
212 } else {
213 ERRORF(reporter, "Failed to get GrVkImageInfo");
214 }
215 #endif
216 #ifdef SK_METAL
217 } else if (GrBackendApi::kMetal == genBackendTex.backend()) {
218 GrMtlTextureInfo genImageInfo;
219 GrMtlTextureInfo origImageInfo;
220 if (genBackendTex.getMtlTextureInfo(&genImageInfo) &&
221 backendTex.getMtlTextureInfo(&origImageInfo)) {
222 if (willUseMips && GrMipMapped::kNo == mipMapped) {
223 // We did a copy so the texture IDs should be different
224 REPORTER_ASSERT(reporter, origImageInfo.fTexture != genImageInfo.fTexture);
225 } else {
226 REPORTER_ASSERT(reporter, origImageInfo.fTexture == genImageInfo.fTexture);
227 }
228 } else {
229 ERRORF(reporter, "Failed to get GrMtlTextureInfo");
230 }
231 #endif
232 } else {
233 REPORTER_ASSERT(reporter, false);
234 }
235
236 // Must make sure the uses of the backend texture have finished (we possibly have a
237 // queued up copy) before we delete the backend texture.
238 context->flush();
239
240 context->priv().getGpu()->testingOnly_flushGpuAndSync();
241
242 context->deleteBackendTexture(backendTex);
243 }
244 }
245 }
246
247 // Test that when we call makeImageSnapshot on an SkSurface we retains the same mip status as the
248 // resource we took the snapshot of.
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrImageSnapshotMipMappedTest,reporter,ctxInfo)249 DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrImageSnapshotMipMappedTest, reporter, ctxInfo) {
250 GrContext* context = ctxInfo.grContext();
251 if (!context->priv().caps()->mipMapSupport()) {
252 return;
253 }
254
255 auto resourceProvider = context->priv().resourceProvider();
256
257 for (auto willUseMips : {false, true}) {
258 for (auto isWrapped : {false, true}) {
259 GrMipMapped mipMapped = willUseMips ? GrMipMapped::kYes : GrMipMapped::kNo;
260 sk_sp<SkSurface> surface;
261 GrBackendTexture backendTex = context->createBackendTexture(
262 kSize, kSize, kRGBA_8888_SkColorType,
263 SkColors::kTransparent, mipMapped, GrRenderable::kYes, GrProtected::kNo);
264 if (isWrapped) {
265 surface = SkSurface::MakeFromBackendTexture(context,
266 backendTex,
267 kTopLeft_GrSurfaceOrigin,
268 0,
269 kRGBA_8888_SkColorType,
270 nullptr,
271 nullptr);
272 } else {
273 SkImageInfo info = SkImageInfo::Make(kSize, kSize, kRGBA_8888_SkColorType,
274 kPremul_SkAlphaType);
275 surface = SkSurface::MakeRenderTarget(context, SkBudgeted::kYes, info, 0,
276 kTopLeft_GrSurfaceOrigin, nullptr,
277 willUseMips);
278 }
279 REPORTER_ASSERT(reporter, surface);
280 if (!surface) {
281 context->deleteBackendTexture(backendTex);
282 }
283 SkGpuDevice* device = ((SkSurface_Gpu*)surface.get())->getDevice();
284 GrTextureProxy* texProxy = device->accessRenderTargetContext()->asTextureProxy();
285 REPORTER_ASSERT(reporter, mipMapped == texProxy->mipMapped());
286
287 texProxy->instantiate(resourceProvider);
288 GrTexture* texture = texProxy->peekTexture();
289 REPORTER_ASSERT(reporter, mipMapped == texture->texturePriv().mipMapped());
290
291 sk_sp<SkImage> image = surface->makeImageSnapshot();
292 REPORTER_ASSERT(reporter, image);
293 if (!image) {
294 context->deleteBackendTexture(backendTex);
295 }
296 texProxy = as_IB(image)->peekProxy();
297 REPORTER_ASSERT(reporter, mipMapped == texProxy->mipMapped());
298
299 texProxy->instantiate(resourceProvider);
300 texture = texProxy->peekTexture();
301 REPORTER_ASSERT(reporter, mipMapped == texture->texturePriv().mipMapped());
302
303 // Must flush the context to make sure all the cmds (copies, etc.) from above are sent
304 // to the gpu before we delete the backendHandle.
305 context->flush();
306 context->priv().getGpu()->testingOnly_flushGpuAndSync();
307 context->deleteBackendTexture(backendTex);
308 }
309 }
310 }
311
312 // Test that we don't create a mip mapped texture if the size is 1x1 even if the filter mode is set
313 // to use mips. This test passes by not crashing or hitting asserts in code.
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(Gr1x1TextureMipMappedTest,reporter,ctxInfo)314 DEF_GPUTEST_FOR_RENDERING_CONTEXTS(Gr1x1TextureMipMappedTest, reporter, ctxInfo) {
315 GrContext* context = ctxInfo.grContext();
316 if (!context->priv().caps()->mipMapSupport()) {
317 return;
318 }
319
320 // Make surface to draw into
321 SkImageInfo info = SkImageInfo::MakeN32(16, 16, kPremul_SkAlphaType);
322 sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(context, SkBudgeted::kNo, info);
323
324 // Make 1x1 raster bitmap
325 SkBitmap bmp;
326 bmp.allocN32Pixels(1, 1);
327 SkPMColor* pixel = reinterpret_cast<SkPMColor*>(bmp.getPixels());
328 *pixel = 0;
329
330 sk_sp<SkImage> bmpImage = SkImage::MakeFromBitmap(bmp);
331
332 // Make sure we scale so we don't optimize out the use of mips.
333 surface->getCanvas()->scale(0.5f, 0.5f);
334
335 SkPaint paint;
336 // This should upload the image to a non mipped GrTextureProxy.
337 surface->getCanvas()->drawImage(bmpImage, 0, 0, &paint);
338 surface->flush();
339
340 // Now set the filter quality to high so we use mip maps. We should find the non mipped texture
341 // in the cache for the SkImage. Since the texture is 1x1 we should just use that texture
342 // instead of trying to do a copy to a mipped texture.
343 paint.setFilterQuality(kHigh_SkFilterQuality);
344 surface->getCanvas()->drawImage(bmpImage, 0, 0, &paint);
345 surface->flush();
346 }
347
348 // Create a new render target and draw 'mipmapView' into it using the provided 'filter'.
draw_mipmap_into_new_render_target(GrRecordingContext * context,GrProxyProvider * proxyProvider,GrColorType colorType,SkAlphaType alphaType,GrSurfaceProxyView mipmapView,GrSamplerState::Filter filter)349 static std::unique_ptr<GrRenderTargetContext> draw_mipmap_into_new_render_target(
350 GrRecordingContext* context, GrProxyProvider* proxyProvider, GrColorType colorType,
351 SkAlphaType alphaType, GrSurfaceProxyView mipmapView, GrSamplerState::Filter filter) {
352 sk_sp<GrSurfaceProxy> renderTarget = proxyProvider->createProxy(
353 mipmapView.proxy()->backendFormat(), {1, 1}, mipmapView.swizzle(), GrRenderable::kYes,
354 1, GrMipMapped::kNo, SkBackingFit::kApprox, SkBudgeted::kYes, GrProtected::kNo);
355
356 auto rtc = GrRenderTargetContext::Make(
357 context, colorType, nullptr, std::move(renderTarget), kTopLeft_GrSurfaceOrigin,
358 nullptr);
359
360 rtc->drawTexture(GrNoClip(), std::move(mipmapView), alphaType, filter, SkBlendMode::kSrcOver,
361 {1,1,1,1}, SkRect::MakeWH(4, 4), SkRect::MakeWH(1,1), GrAA::kYes,
362 GrQuadAAFlags::kAll, SkCanvas::kFast_SrcRectConstraint, SkMatrix::I(),
363 nullptr);
364 return rtc;
365 }
366
367 // Test that two opsTasks using the same mipmaps both depend on the same GrTextureResolveRenderTask.
368 DEF_GPUTEST(GrManyDependentsMipMappedTest, reporter, /* options */) {
369 using CanClearFullscreen = GrRenderTargetContext::CanClearFullscreen;
370 using Enable = GrContextOptions::Enable;
371 using Filter = GrSamplerState::Filter;
372
373 for (auto enableSortingAndReduction : {Enable::kYes, Enable::kNo}) {
374 GrMockOptions mockOptions;
375 mockOptions.fMipMapSupport = true;
376 GrContextOptions ctxOptions;
377 ctxOptions.fReduceOpsTaskSplitting = enableSortingAndReduction;
378 sk_sp<GrContext> context = GrContext::MakeMock(&mockOptions, ctxOptions);
379 if (!context) {
380 ERRORF(reporter, "could not create mock context with fReduceOpsTaskSplitting %s.",
381 (Enable::kYes == enableSortingAndReduction) ? "enabled" : "disabled");
382 continue;
383 }
384
385 SkASSERT(context->priv().caps()->mipMapSupport());
386
387 GrBackendFormat format = context->defaultBackendFormat(
388 kRGBA_8888_SkColorType, GrRenderable::kYes);
389 GrColorType colorType = GrColorType::kRGBA_8888;
390 SkAlphaType alphaType = kPremul_SkAlphaType;
391
392 GrProxyProvider* proxyProvider = context->priv().proxyProvider();
393
394 // Create a mipmapped render target.
395
396 GrSwizzle swizzle = context->priv().caps()->getReadSwizzle(format, colorType);
397
398 sk_sp<GrTextureProxy> mipmapProxy = proxyProvider->createProxy(
399 format, {4, 4}, swizzle, GrRenderable::kYes, 1, GrMipMapped::kYes,
400 SkBackingFit::kExact, SkBudgeted::kYes, GrProtected::kNo);
401
402 // Mark the mipmaps clean to ensure things still work properly when they won't be marked
403 // dirty again until GrRenderTask::makeClosed().
404 mipmapProxy->markMipMapsClean();
405
406 auto mipmapRTC = GrRenderTargetContext::Make(
407 context.get(), colorType, nullptr, mipmapProxy, kTopLeft_GrSurfaceOrigin, nullptr);
408
409 mipmapRTC->clear(nullptr, {.1f,.2f,.3f,.4f}, CanClearFullscreen::kYes);
410 REPORTER_ASSERT(reporter, mipmapProxy->getLastRenderTask());
411 // mipmapProxy's last render task should now just be the opsTask containing the clear.
412 REPORTER_ASSERT(reporter,
413 mipmapRTC->testingOnly_PeekLastOpsTask() == mipmapProxy->getLastRenderTask());
414
415 // Mipmaps don't get marked dirty until makeClosed().
416 REPORTER_ASSERT(reporter, !mipmapProxy->mipMapsAreDirty());
417
418 GrSurfaceProxyView mipmapView(mipmapProxy, kTopLeft_GrSurfaceOrigin, swizzle);
419
420 // Draw the dirty mipmap texture into a render target.
421 auto rtc1 = draw_mipmap_into_new_render_target(context.get(), proxyProvider, colorType,
422 alphaType, mipmapView, Filter::kMipMap);
423
424 // Mipmaps should have gotten marked dirty during makeClosed, then marked clean again as
425 // soon as a GrTextureResolveRenderTask was inserted. The way we know they were resolved is
426 // if mipmapProxy->getLastRenderTask() has switched from the opsTask that drew to it, to the
427 // task that resolved its mips.
428 GrRenderTask* initialMipmapRegenTask = mipmapProxy->getLastRenderTask();
429 REPORTER_ASSERT(reporter, initialMipmapRegenTask);
430 REPORTER_ASSERT(reporter,
431 initialMipmapRegenTask != mipmapRTC->testingOnly_PeekLastOpsTask());
432 REPORTER_ASSERT(reporter, !mipmapProxy->mipMapsAreDirty());
433
434 // Draw the now-clean mipmap texture into a second target.
435 auto rtc2 = draw_mipmap_into_new_render_target(context.get(), proxyProvider, colorType,
436 alphaType, mipmapView, Filter::kMipMap);
437
438 // Make sure the mipmap texture still has the same regen task.
439 REPORTER_ASSERT(reporter, mipmapProxy->getLastRenderTask() == initialMipmapRegenTask);
440 SkASSERT(!mipmapProxy->mipMapsAreDirty());
441
442 // Reset everything so we can go again, this time with the first draw not mipmapped.
443 context->flush();
444
445 // Mip regen tasks don't get added as dependencies until makeClosed().
446 REPORTER_ASSERT(reporter,
447 rtc1->testingOnly_PeekLastOpsTask()->dependsOn(initialMipmapRegenTask));
448 REPORTER_ASSERT(reporter,
449 rtc2->testingOnly_PeekLastOpsTask()->dependsOn(initialMipmapRegenTask));
450
451 // Render something to dirty the mips.
452 mipmapRTC->clear(nullptr, {.1f,.2f,.3f,.4f}, CanClearFullscreen::kYes);
453 REPORTER_ASSERT(reporter, mipmapProxy->getLastRenderTask());
454 // mipmapProxy's last render task should now just be the opsTask containing the clear.
455 REPORTER_ASSERT(reporter,
456 mipmapRTC->testingOnly_PeekLastOpsTask() == mipmapProxy->getLastRenderTask());
457
458 // Mipmaps don't get marked dirty until makeClosed().
459 REPORTER_ASSERT(reporter, !mipmapProxy->mipMapsAreDirty());
460
461 // Draw the dirty mipmap texture into a render target, but don't do mipmap filtering.
462 rtc1 = draw_mipmap_into_new_render_target(context.get(), proxyProvider, colorType,
463 alphaType, mipmapView, Filter::kBilerp);
464
465 // Mipmaps should have gotten marked dirty during makeClosed() when adding the dependency.
466 // Since the last draw did not use mips, they will not have been regenerated and should
467 // therefore still be dirty.
468 REPORTER_ASSERT(reporter, mipmapProxy->mipMapsAreDirty());
469
470 // Since mips weren't regenerated, the last render task shouldn't have changed.
471 REPORTER_ASSERT(reporter,
472 mipmapRTC->testingOnly_PeekLastOpsTask() == mipmapProxy->getLastRenderTask());
473
474 // Draw the stil-dirty mipmap texture into a second target with mipmap filtering.
475 rtc2 = draw_mipmap_into_new_render_target(context.get(), proxyProvider, colorType,
476 alphaType, std::move(mipmapView),
477 Filter::kMipMap);
478
479 // Make sure the mipmap texture now has a new last render task that regenerates the mips,
480 // and that the mipmaps are now clean.
481 auto mipRegenTask2 = mipmapProxy->getLastRenderTask();
482 REPORTER_ASSERT(reporter, mipRegenTask2);
483 REPORTER_ASSERT(reporter,
484 mipmapRTC->testingOnly_PeekLastOpsTask() != mipRegenTask2);
485 SkASSERT(!mipmapProxy->mipMapsAreDirty());
486
487 // Mip regen tasks don't get added as dependencies until makeClosed().
488 context->flush();
489 REPORTER_ASSERT(reporter, rtc2->testingOnly_PeekLastOpsTask()->dependsOn(mipRegenTask2));
490 }
491 }
492