1 // Copyright 2016 The SwiftShader Authors. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Texture.cpp: Implements the Texture class and its derived classes 16 // Texture2D and TextureCubeMap. Implements GL texture objects and related 17 // functionality. [OpenGL ES 2.0.24] section 3.7 page 63. 18 19 #include "Texture.h" 20 21 #include "main.h" 22 #include "mathutil.h" 23 #include "Framebuffer.h" 24 #include "Device.hpp" 25 #include "libEGL/Display.h" 26 #include "libEGL/Surface.h" 27 #include "common/debug.h" 28 29 #include <algorithm> 30 31 namespace es1 32 { 33 34 Texture::Texture(GLuint name) : egl::Texture(name) 35 { 36 mMinFilter = GL_NEAREST_MIPMAP_LINEAR; 37 mMagFilter = GL_LINEAR; 38 mWrapS = GL_REPEAT; 39 mWrapT = GL_REPEAT; 40 mMaxAnisotropy = 1.0f; 41 generateMipmap = GL_FALSE; 42 cropRectU = 0; 43 cropRectV = 0; 44 cropRectW = 0; 45 cropRectH = 0; 46 47 resource = new sw::Resource(0); 48 } 49 50 Texture::~Texture() 51 { 52 resource->destruct(); 53 } 54 55 sw::Resource *Texture::getResource() const 56 { 57 return resource; 58 } 59 60 // Returns true on successful filter state update (valid enum parameter) 61 bool Texture::setMinFilter(GLenum filter) 62 { 63 switch(filter) 64 { 65 case GL_NEAREST_MIPMAP_NEAREST: 66 case GL_LINEAR_MIPMAP_NEAREST: 67 case GL_NEAREST_MIPMAP_LINEAR: 68 case GL_LINEAR_MIPMAP_LINEAR: 69 if(getTarget() == GL_TEXTURE_EXTERNAL_OES) 70 { 71 return false; 72 } 73 // Fall through 74 case GL_NEAREST: 75 case GL_LINEAR: 76 mMinFilter = filter; 77 return true; 78 default: 79 return false; 80 } 81 } 82 83 // Returns true on successful filter state update (valid enum parameter) 84 bool Texture::setMagFilter(GLenum filter) 85 { 86 switch(filter) 87 { 88 case GL_NEAREST: 89 case GL_LINEAR: 90 mMagFilter = filter; 91 return true; 92 default: 93 return false; 94 } 95 } 96 97 // Returns true on successful wrap state update (valid enum parameter) 98 bool Texture::setWrapS(GLenum wrap) 99 { 100 switch(wrap) 101 { 102 case GL_REPEAT: 103 case GL_MIRRORED_REPEAT_OES: 104 if(getTarget() == GL_TEXTURE_EXTERNAL_OES) 105 { 106 return false; 107 } 108 // Fall through 109 case GL_CLAMP_TO_EDGE: 110 mWrapS = wrap; 111 return true; 112 default: 113 return false; 114 } 115 } 116 117 // Returns true on successful wrap state update (valid enum parameter) 118 bool Texture::setWrapT(GLenum wrap) 119 { 120 switch(wrap) 121 { 122 case GL_REPEAT: 123 case GL_MIRRORED_REPEAT_OES: 124 if(getTarget() == GL_TEXTURE_EXTERNAL_OES) 125 { 126 return false; 127 } 128 // Fall through 129 case GL_CLAMP_TO_EDGE: 130 mWrapT = wrap; 131 return true; 132 default: 133 return false; 134 } 135 } 136 137 // Returns true on successful max anisotropy update (valid anisotropy value) 138 bool Texture::setMaxAnisotropy(float textureMaxAnisotropy) 139 { 140 textureMaxAnisotropy = std::min(textureMaxAnisotropy, MAX_TEXTURE_MAX_ANISOTROPY); 141 142 if(textureMaxAnisotropy < 1.0f) 143 { 144 return false; 145 } 146 147 if(mMaxAnisotropy != textureMaxAnisotropy) 148 { 149 mMaxAnisotropy = textureMaxAnisotropy; 150 } 151 152 return true; 153 } 154 155 void Texture::setGenerateMipmap(GLboolean enable) 156 { 157 generateMipmap = enable; 158 } 159 160 void Texture::setCropRect(GLint u, GLint v, GLint w, GLint h) 161 { 162 cropRectU = u; 163 cropRectV = v; 164 cropRectW = w; 165 cropRectH = h; 166 } 167 168 GLenum Texture::getMinFilter() const 169 { 170 return mMinFilter; 171 } 172 173 GLenum Texture::getMagFilter() const 174 { 175 return mMagFilter; 176 } 177 178 GLenum Texture::getWrapS() const 179 { 180 return mWrapS; 181 } 182 183 GLenum Texture::getWrapT() const 184 { 185 return mWrapT; 186 } 187 188 GLfloat Texture::getMaxAnisotropy() const 189 { 190 return mMaxAnisotropy; 191 } 192 193 GLboolean Texture::getGenerateMipmap() const 194 { 195 return generateMipmap; 196 } 197 198 GLint Texture::getCropRectU() const 199 { 200 return cropRectU; 201 } 202 203 GLint Texture::getCropRectV() const 204 { 205 return cropRectV; 206 } 207 208 GLint Texture::getCropRectW() const 209 { 210 return cropRectW; 211 } 212 213 GLint Texture::getCropRectH() const 214 { 215 return cropRectH; 216 } 217 218 egl::Image *Texture::createSharedImage(GLenum target, unsigned int level) 219 { 220 egl::Image *image = getRenderTarget(target, level); // Increments reference count 221 222 if(image) 223 { 224 image->markShared(); 225 } 226 227 return image; 228 } 229 230 void Texture::setImage(GLenum format, GLenum type, GLint unpackAlignment, const void *pixels, egl::Image *image) 231 { 232 if(pixels && image) 233 { 234 egl::Image::UnpackInfo unpackInfo; 235 unpackInfo.alignment = unpackAlignment; 236 image->loadImageData(0, 0, 0, image->getWidth(), image->getHeight(), 1, format, type, unpackInfo, pixels); 237 } 238 } 239 240 void Texture::setCompressedImage(GLsizei imageSize, const void *pixels, egl::Image *image) 241 { 242 if(pixels && image) 243 { 244 image->loadCompressedData(0, 0, 0, image->getWidth(), image->getHeight(), 1, imageSize, pixels); 245 } 246 } 247 248 void Texture::subImage(GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, GLint unpackAlignment, const void *pixels, egl::Image *image) 249 { 250 if(!image) 251 { 252 return error(GL_INVALID_OPERATION); 253 } 254 255 if(width + xoffset > image->getWidth() || height + yoffset > image->getHeight()) 256 { 257 return error(GL_INVALID_VALUE); 258 } 259 260 if(IsCompressed(image->getFormat())) 261 { 262 return error(GL_INVALID_OPERATION); 263 } 264 265 if(format != image->getFormat()) 266 { 267 return error(GL_INVALID_OPERATION); 268 } 269 270 if(pixels) 271 { 272 egl::Image::UnpackInfo unpackInfo; 273 unpackInfo.alignment = unpackAlignment; 274 image->loadImageData(xoffset, yoffset, 0, width, height, 1, format, type, unpackInfo, pixels); 275 } 276 } 277 278 void Texture::subImageCompressed(GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void *pixels, egl::Image *image) 279 { 280 if(!image) 281 { 282 return error(GL_INVALID_OPERATION); 283 } 284 285 if(width + xoffset > image->getWidth() || height + yoffset > image->getHeight()) 286 { 287 return error(GL_INVALID_VALUE); 288 } 289 290 if(format != image->getFormat()) 291 { 292 return error(GL_INVALID_OPERATION); 293 } 294 295 if(pixels) 296 { 297 image->loadCompressedData(xoffset, yoffset, 0, width, height, 1, imageSize, pixels); 298 } 299 } 300 301 bool Texture::copy(egl::Image *source, const sw::Rect &sourceRect, GLenum destFormat, GLint xoffset, GLint yoffset, egl::Image *dest) 302 { 303 Device *device = getDevice(); 304 305 sw::SliceRect destRect(xoffset, yoffset, xoffset + (sourceRect.x1 - sourceRect.x0), yoffset + (sourceRect.y1 - sourceRect.y0), 0); 306 sw::SliceRect sourceSliceRect(sourceRect); 307 bool success = device->stretchRect(source, &sourceSliceRect, dest, &destRect, false); 308 309 if(!success) 310 { 311 return error(GL_OUT_OF_MEMORY, false); 312 } 313 314 return true; 315 } 316 317 bool Texture::isMipmapFiltered() const 318 { 319 switch(mMinFilter) 320 { 321 case GL_NEAREST: 322 case GL_LINEAR: 323 return false; 324 case GL_NEAREST_MIPMAP_NEAREST: 325 case GL_LINEAR_MIPMAP_NEAREST: 326 case GL_NEAREST_MIPMAP_LINEAR: 327 case GL_LINEAR_MIPMAP_LINEAR: 328 return true; 329 default: UNREACHABLE(mMinFilter); 330 } 331 332 return false; 333 } 334 335 Texture2D::Texture2D(GLuint name) : Texture(name) 336 { 337 for(int i = 0; i < IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++) 338 { 339 image[i] = nullptr; 340 } 341 342 mSurface = nullptr; 343 344 mColorbufferProxy = nullptr; 345 mProxyRefs = 0; 346 } 347 348 Texture2D::~Texture2D() 349 { 350 resource->lock(sw::DESTRUCT); 351 352 for(int i = 0; i < IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++) 353 { 354 if(image[i]) 355 { 356 image[i]->unbind(this); 357 image[i] = nullptr; 358 } 359 } 360 361 resource->unlock(); 362 363 if(mSurface) 364 { 365 mSurface->setBoundTexture(nullptr); 366 mSurface = nullptr; 367 } 368 369 mColorbufferProxy = nullptr; 370 } 371 372 // We need to maintain a count of references to renderbuffers acting as 373 // proxies for this texture, so that we do not attempt to use a pointer 374 // to a renderbuffer proxy which has been deleted. 375 void Texture2D::addProxyRef(const Renderbuffer *proxy) 376 { 377 mProxyRefs++; 378 } 379 380 void Texture2D::releaseProxy(const Renderbuffer *proxy) 381 { 382 if(mProxyRefs > 0) 383 { 384 mProxyRefs--; 385 } 386 387 if(mProxyRefs == 0) 388 { 389 mColorbufferProxy = nullptr; 390 } 391 } 392 393 void Texture2D::sweep() 394 { 395 int imageCount = 0; 396 397 for(int i = 0; i < IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++) 398 { 399 if(image[i] && image[i]->isChildOf(this)) 400 { 401 if(!image[i]->hasSingleReference()) 402 { 403 return; 404 } 405 406 imageCount++; 407 } 408 } 409 410 if(imageCount == referenceCount) 411 { 412 destroy(); 413 } 414 } 415 416 GLenum Texture2D::getTarget() const 417 { 418 return GL_TEXTURE_2D; 419 } 420 421 GLsizei Texture2D::getWidth(GLenum target, GLint level) const 422 { 423 ASSERT(target == GL_TEXTURE_2D); 424 return image[level] ? image[level]->getWidth() : 0; 425 } 426 427 GLsizei Texture2D::getHeight(GLenum target, GLint level) const 428 { 429 ASSERT(target == GL_TEXTURE_2D); 430 return image[level] ? image[level]->getHeight() : 0; 431 } 432 433 GLenum Texture2D::getFormat(GLenum target, GLint level) const 434 { 435 ASSERT(target == GL_TEXTURE_2D); 436 return image[level] ? image[level]->getFormat() : GL_NONE; 437 } 438 439 GLenum Texture2D::getType(GLenum target, GLint level) const 440 { 441 ASSERT(target == GL_TEXTURE_2D); 442 return image[level] ? image[level]->getType() : GL_NONE; 443 } 444 445 sw::Format Texture2D::getInternalFormat(GLenum target, GLint level) const 446 { 447 ASSERT(target == GL_TEXTURE_2D); 448 return image[level] ? image[level]->getInternalFormat() : sw::FORMAT_NULL; 449 } 450 451 int Texture2D::getLevelCount() const 452 { 453 ASSERT(isSamplerComplete()); 454 int levels = 0; 455 456 while(levels < IMPLEMENTATION_MAX_TEXTURE_LEVELS && image[levels]) 457 { 458 levels++; 459 } 460 461 return levels; 462 } 463 464 void Texture2D::setImage(GLint level, GLsizei width, GLsizei height, GLenum format, GLenum type, GLint unpackAlignment, const void *pixels) 465 { 466 if(image[level]) 467 { 468 image[level]->release(); 469 } 470 471 image[level] = new egl::Image(this, width, height, format, type); 472 473 if(!image[level]) 474 { 475 return error(GL_OUT_OF_MEMORY); 476 } 477 478 Texture::setImage(format, type, unpackAlignment, pixels, image[level]); 479 } 480 481 void Texture2D::bindTexImage(egl::Surface *surface) 482 { 483 GLenum format; 484 485 switch(surface->getInternalFormat()) 486 { 487 case sw::FORMAT_A8R8G8B8: 488 format = GL_BGRA_EXT; 489 break; 490 case sw::FORMAT_A8B8G8R8: 491 format = GL_RGBA; 492 break; 493 case sw::FORMAT_X8B8G8R8: 494 case sw::FORMAT_X8R8G8B8: 495 format = GL_RGB; 496 break; 497 default: 498 UNIMPLEMENTED(); 499 return; 500 } 501 502 for(int level = 0; level < IMPLEMENTATION_MAX_TEXTURE_LEVELS; level++) 503 { 504 if(image[level]) 505 { 506 image[level]->release(); 507 image[level] = nullptr; 508 } 509 } 510 511 image[0] = surface->getRenderTarget(); 512 513 mSurface = surface; 514 mSurface->setBoundTexture(this); 515 } 516 517 void Texture2D::releaseTexImage() 518 { 519 for(int level = 0; level < IMPLEMENTATION_MAX_TEXTURE_LEVELS; level++) 520 { 521 if(image[level]) 522 { 523 image[level]->release(); 524 image[level] = nullptr; 525 } 526 } 527 } 528 529 void Texture2D::setCompressedImage(GLint level, GLenum format, GLsizei width, GLsizei height, GLsizei imageSize, const void *pixels) 530 { 531 if(image[level]) 532 { 533 image[level]->release(); 534 } 535 536 image[level] = new egl::Image(this, width, height, format, GL_UNSIGNED_BYTE); 537 538 if(!image[level]) 539 { 540 return error(GL_OUT_OF_MEMORY); 541 } 542 543 Texture::setCompressedImage(imageSize, pixels, image[level]); 544 } 545 546 void Texture2D::subImage(GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, GLint unpackAlignment, const void *pixels) 547 { 548 Texture::subImage(xoffset, yoffset, width, height, format, type, unpackAlignment, pixels, image[level]); 549 } 550 551 void Texture2D::subImageCompressed(GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void *pixels) 552 { 553 Texture::subImageCompressed(xoffset, yoffset, width, height, format, imageSize, pixels, image[level]); 554 } 555 556 void Texture2D::copyImage(GLint level, GLenum format, GLint x, GLint y, GLsizei width, GLsizei height, Framebuffer *source) 557 { 558 egl::Image *renderTarget = source->getRenderTarget(); 559 560 if(!renderTarget) 561 { 562 ERR("Failed to retrieve the render target."); 563 return error(GL_OUT_OF_MEMORY); 564 } 565 566 if(image[level]) 567 { 568 image[level]->release(); 569 } 570 571 image[level] = new egl::Image(this, width, height, format, GL_UNSIGNED_BYTE); 572 573 if(!image[level]) 574 { 575 return error(GL_OUT_OF_MEMORY); 576 } 577 578 if(width != 0 && height != 0) 579 { 580 sw::Rect sourceRect = {x, y, x + width, y + height}; 581 sourceRect.clip(0, 0, source->getColorbuffer()->getWidth(), source->getColorbuffer()->getHeight()); 582 583 copy(renderTarget, sourceRect, format, 0, 0, image[level]); 584 } 585 586 renderTarget->release(); 587 } 588 589 void Texture2D::copySubImage(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height, Framebuffer *source) 590 { 591 if(!image[level]) 592 { 593 return error(GL_INVALID_OPERATION); 594 } 595 596 if(xoffset + width > image[level]->getWidth() || yoffset + height > image[level]->getHeight()) 597 { 598 return error(GL_INVALID_VALUE); 599 } 600 601 egl::Image *renderTarget = source->getRenderTarget(); 602 603 if(!renderTarget) 604 { 605 ERR("Failed to retrieve the render target."); 606 return error(GL_OUT_OF_MEMORY); 607 } 608 609 sw::Rect sourceRect = {x, y, x + width, y + height}; 610 sourceRect.clip(0, 0, source->getColorbuffer()->getWidth(), source->getColorbuffer()->getHeight()); 611 612 copy(renderTarget, sourceRect, image[level]->getFormat(), xoffset, yoffset, image[level]); 613 614 renderTarget->release(); 615 } 616 617 void Texture2D::setImage(egl::Image *sharedImage) 618 { 619 sharedImage->addRef(); 620 621 if(image[0]) 622 { 623 image[0]->release(); 624 } 625 626 image[0] = sharedImage; 627 } 628 629 // Tests for 2D texture sampling completeness. [OpenGL ES 2.0.24] section 3.8.2 page 85. 630 bool Texture2D::isSamplerComplete() const 631 { 632 if(!image[0]) 633 { 634 return false; 635 } 636 637 GLsizei width = image[0]->getWidth(); 638 GLsizei height = image[0]->getHeight(); 639 640 if(width <= 0 || height <= 0) 641 { 642 return false; 643 } 644 645 if(isMipmapFiltered()) 646 { 647 if(!generateMipmap && !isMipmapComplete()) 648 { 649 return false; 650 } 651 } 652 653 return true; 654 } 655 656 // Tests for 2D texture (mipmap) completeness. [OpenGL ES 2.0.24] section 3.7.10 page 81. 657 bool Texture2D::isMipmapComplete() const 658 { 659 GLsizei width = image[0]->getWidth(); 660 GLsizei height = image[0]->getHeight(); 661 662 int q = log2(std::max(width, height)); 663 664 for(int level = 1; level <= q; level++) 665 { 666 if(!image[level]) 667 { 668 return false; 669 } 670 671 if(image[level]->getFormat() != image[0]->getFormat()) 672 { 673 return false; 674 } 675 676 if(image[level]->getType() != image[0]->getType()) 677 { 678 return false; 679 } 680 681 if(image[level]->getWidth() != std::max(1, width >> level)) 682 { 683 return false; 684 } 685 686 if(image[level]->getHeight() != std::max(1, height >> level)) 687 { 688 return false; 689 } 690 } 691 692 return true; 693 } 694 695 bool Texture2D::isCompressed(GLenum target, GLint level) const 696 { 697 return IsCompressed(getFormat(target, level)); 698 } 699 700 bool Texture2D::isDepth(GLenum target, GLint level) const 701 { 702 return IsDepthTexture(getFormat(target, level)); 703 } 704 705 void Texture2D::generateMipmaps() 706 { 707 if(!image[0]) 708 { 709 return; // FIXME: error? 710 } 711 712 unsigned int q = log2(std::max(image[0]->getWidth(), image[0]->getHeight())); 713 714 for(unsigned int i = 1; i <= q; i++) 715 { 716 if(image[i]) 717 { 718 image[i]->release(); 719 } 720 721 image[i] = new egl::Image(this, std::max(image[0]->getWidth() >> i, 1), std::max(image[0]->getHeight() >> i, 1), image[0]->getFormat(), image[0]->getType()); 722 723 if(!image[i]) 724 { 725 return error(GL_OUT_OF_MEMORY); 726 } 727 728 getDevice()->stretchRect(image[i - 1], 0, image[i], 0, true); 729 } 730 } 731 732 void Texture2D::autoGenerateMipmaps() 733 { 734 if(generateMipmap && image[0]->hasDirtyMipmaps()) 735 { 736 generateMipmaps(); 737 image[0]->cleanMipmaps(); 738 } 739 } 740 741 egl::Image *Texture2D::getImage(unsigned int level) 742 { 743 return image[level]; 744 } 745 746 Renderbuffer *Texture2D::getRenderbuffer(GLenum target) 747 { 748 if(target != GL_TEXTURE_2D) 749 { 750 return error(GL_INVALID_OPERATION, (Renderbuffer*)nullptr); 751 } 752 753 if(!mColorbufferProxy) 754 { 755 mColorbufferProxy = new Renderbuffer(name, new RenderbufferTexture2D(this)); 756 } 757 758 return mColorbufferProxy; 759 } 760 761 egl::Image *Texture2D::getRenderTarget(GLenum target, unsigned int level) 762 { 763 ASSERT(target == GL_TEXTURE_2D); 764 ASSERT(level < IMPLEMENTATION_MAX_TEXTURE_LEVELS); 765 766 if(image[level]) 767 { 768 image[level]->addRef(); 769 } 770 771 return image[level]; 772 } 773 774 bool Texture2D::isShared(GLenum target, unsigned int level) const 775 { 776 ASSERT(target == GL_TEXTURE_2D); 777 ASSERT(level < IMPLEMENTATION_MAX_TEXTURE_LEVELS); 778 779 if(mSurface) // Bound to an EGLSurface 780 { 781 return true; 782 } 783 784 if(!image[level]) 785 { 786 return false; 787 } 788 789 return image[level]->isShared(); 790 } 791 792 TextureExternal::TextureExternal(GLuint name) : Texture2D(name) 793 { 794 mMinFilter = GL_LINEAR; 795 mMagFilter = GL_LINEAR; 796 mWrapS = GL_CLAMP_TO_EDGE; 797 mWrapT = GL_CLAMP_TO_EDGE; 798 } 799 800 TextureExternal::~TextureExternal() 801 { 802 } 803 804 GLenum TextureExternal::getTarget() const 805 { 806 return GL_TEXTURE_EXTERNAL_OES; 807 } 808 809 } 810 811 egl::Image *createBackBuffer(int width, int height, const egl::Config *config) 812 { 813 if(config) 814 { 815 return new egl::Image(width, height, config->mRenderTargetFormat, config->mSamples, false); 816 } 817 818 return nullptr; 819 } 820 821 egl::Image *createDepthStencil(unsigned int width, unsigned int height, sw::Format format, int multiSampleDepth, bool discard) 822 { 823 if(height > sw::OUTLINE_RESOLUTION) 824 { 825 ERR("Invalid parameters: %dx%d", width, height); 826 return 0; 827 } 828 829 bool lockable = true; 830 831 switch(format) 832 { 833 // case sw::FORMAT_D15S1: 834 case sw::FORMAT_D24S8: 835 case sw::FORMAT_D24X8: 836 // case sw::FORMAT_D24X4S4: 837 case sw::FORMAT_D24FS8: 838 case sw::FORMAT_D32: 839 case sw::FORMAT_D16: 840 lockable = false; 841 break; 842 // case sw::FORMAT_S8_LOCKABLE: 843 // case sw::FORMAT_D16_LOCKABLE: 844 case sw::FORMAT_D32F_LOCKABLE: 845 // case sw::FORMAT_D32_LOCKABLE: 846 case sw::FORMAT_DF24S8: 847 case sw::FORMAT_DF16S8: 848 lockable = true; 849 break; 850 default: 851 UNREACHABLE(format); 852 } 853 854 egl::Image *surface = new egl::Image(width, height, format, multiSampleDepth, lockable); 855 856 if(!surface) 857 { 858 ERR("Out of memory"); 859 return nullptr; 860 } 861 862 return surface; 863 } 864