1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 extern "C" { 6 #include <X11/Xlib.h> 7 } 8 9 #include "ui/gl/gl_surface_glx.h" 10 11 #include "base/basictypes.h" 12 #include "base/debug/trace_event.h" 13 #include "base/lazy_instance.h" 14 #include "base/logging.h" 15 #include "base/memory/scoped_ptr.h" 16 #include "base/memory/weak_ptr.h" 17 #include "base/message_loop/message_loop.h" 18 #include "base/single_thread_task_runner.h" 19 #include "base/synchronization/cancellation_flag.h" 20 #include "base/synchronization/lock.h" 21 #include "base/thread_task_runner_handle.h" 22 #include "base/threading/non_thread_safe.h" 23 #include "base/threading/thread.h" 24 #include "base/time/time.h" 25 #include "ui/events/platform/platform_event_source.h" 26 #include "ui/gfx/x/x11_connection.h" 27 #include "ui/gfx/x/x11_types.h" 28 #include "ui/gl/gl_bindings.h" 29 #include "ui/gl/gl_implementation.h" 30 #include "ui/gl/sync_control_vsync_provider.h" 31 32 namespace gfx { 33 34 namespace { 35 36 // scoped_ptr functor for XFree(). Use as follows: 37 // scoped_ptr<XVisualInfo, ScopedPtrXFree> foo(...); 38 // where "XVisualInfo" is any X type that is freed with XFree. 39 struct ScopedPtrXFree { 40 void operator()(void* x) const { 41 ::XFree(x); 42 } 43 }; 44 45 Display* g_display = NULL; 46 const char* g_glx_extensions = NULL; 47 bool g_glx_context_create = false; 48 bool g_glx_create_context_robustness_supported = false; 49 bool g_glx_texture_from_pixmap_supported = false; 50 bool g_glx_oml_sync_control_supported = false; 51 52 // Track support of glXGetMscRateOML separately from GLX_OML_sync_control as a 53 // whole since on some platforms (e.g. crosbug.com/34585), glXGetMscRateOML 54 // always fails even though GLX_OML_sync_control is reported as being supported. 55 bool g_glx_get_msc_rate_oml_supported = false; 56 57 bool g_glx_sgi_video_sync_supported = false; 58 59 static const base::TimeDelta kGetVSyncParametersMinPeriod = 60 #if defined(OS_LINUX) 61 // See crbug.com/373489 62 // On Linux, querying the vsync parameters might burn CPU for up to an 63 // entire vsync, so we only query periodically to reduce CPU usage. 64 // 5 seconds is chosen somewhat abitrarily as a balance between: 65 // a) Drift in the phase of our signal. 66 // b) Potential janks from periodically pegging the CPU. 67 base::TimeDelta::FromSeconds(5); 68 #else 69 base::TimeDelta::FromSeconds(0); 70 #endif 71 72 class OMLSyncControlVSyncProvider 73 : public gfx::SyncControlVSyncProvider { 74 public: 75 explicit OMLSyncControlVSyncProvider(gfx::AcceleratedWidget window) 76 : SyncControlVSyncProvider(), 77 window_(window) { 78 } 79 80 virtual ~OMLSyncControlVSyncProvider() { } 81 82 protected: 83 virtual bool GetSyncValues(int64* system_time, 84 int64* media_stream_counter, 85 int64* swap_buffer_counter) OVERRIDE { 86 return glXGetSyncValuesOML(g_display, window_, system_time, 87 media_stream_counter, swap_buffer_counter); 88 } 89 90 virtual bool GetMscRate(int32* numerator, int32* denominator) OVERRIDE { 91 if (!g_glx_get_msc_rate_oml_supported) 92 return false; 93 94 if (!glXGetMscRateOML(g_display, window_, numerator, denominator)) { 95 // Once glXGetMscRateOML has been found to fail, don't try again, 96 // since each failing call may spew an error message. 97 g_glx_get_msc_rate_oml_supported = false; 98 return false; 99 } 100 101 return true; 102 } 103 104 private: 105 XID window_; 106 107 DISALLOW_COPY_AND_ASSIGN(OMLSyncControlVSyncProvider); 108 }; 109 110 class SGIVideoSyncThread 111 : public base::Thread, 112 public base::NonThreadSafe, 113 public base::RefCounted<SGIVideoSyncThread> { 114 public: 115 static scoped_refptr<SGIVideoSyncThread> Create() { 116 if (!g_video_sync_thread) { 117 g_video_sync_thread = new SGIVideoSyncThread(); 118 g_video_sync_thread->Start(); 119 } 120 return g_video_sync_thread; 121 } 122 123 private: 124 friend class base::RefCounted<SGIVideoSyncThread>; 125 126 SGIVideoSyncThread() : base::Thread("SGI_video_sync") { 127 DCHECK(CalledOnValidThread()); 128 } 129 130 virtual ~SGIVideoSyncThread() { 131 DCHECK(CalledOnValidThread()); 132 g_video_sync_thread = NULL; 133 Stop(); 134 } 135 136 static SGIVideoSyncThread* g_video_sync_thread; 137 138 DISALLOW_COPY_AND_ASSIGN(SGIVideoSyncThread); 139 }; 140 141 class SGIVideoSyncProviderThreadShim { 142 public: 143 explicit SGIVideoSyncProviderThreadShim(XID window) 144 : window_(window), 145 context_(NULL), 146 task_runner_(base::ThreadTaskRunnerHandle::Get()), 147 cancel_vsync_flag_(), 148 vsync_lock_() { 149 // This ensures that creation of |window_| has occured when this shim 150 // is executing in the same process as the call to create |window_|. 151 XSync(g_display, False); 152 } 153 154 virtual ~SGIVideoSyncProviderThreadShim() { 155 if (context_) { 156 glXDestroyContext(display_, context_); 157 context_ = NULL; 158 } 159 } 160 161 base::CancellationFlag* cancel_vsync_flag() { 162 return &cancel_vsync_flag_; 163 } 164 165 base::Lock* vsync_lock() { 166 return &vsync_lock_; 167 } 168 169 void Initialize() { 170 DCHECK(display_); 171 172 XWindowAttributes attributes; 173 if (!XGetWindowAttributes(display_, window_, &attributes)) { 174 LOG(ERROR) << "XGetWindowAttributes failed for window " << 175 window_ << "."; 176 return; 177 } 178 179 XVisualInfo visual_info_template; 180 visual_info_template.visualid = XVisualIDFromVisual(attributes.visual); 181 182 int visual_info_count = 0; 183 scoped_ptr<XVisualInfo, ScopedPtrXFree> visual_info_list( 184 XGetVisualInfo(display_, VisualIDMask, 185 &visual_info_template, &visual_info_count)); 186 187 DCHECK(visual_info_list.get()); 188 if (visual_info_count == 0) { 189 LOG(ERROR) << "No visual info for visual ID."; 190 return; 191 } 192 193 context_ = glXCreateContext(display_, visual_info_list.get(), NULL, True); 194 195 DCHECK(NULL != context_); 196 } 197 198 void GetVSyncParameters(const VSyncProvider::UpdateVSyncCallback& callback) { 199 base::TimeTicks now; 200 { 201 // Don't allow |window_| destruction while we're probing vsync. 202 base::AutoLock locked(vsync_lock_); 203 204 if (!context_ || cancel_vsync_flag_.IsSet()) 205 return; 206 207 glXMakeCurrent(display_, window_, context_); 208 209 unsigned int retrace_count = 0; 210 if (glXWaitVideoSyncSGI(1, 0, &retrace_count) != 0) 211 return; 212 213 TRACE_EVENT_INSTANT0("gpu", "vblank", TRACE_EVENT_SCOPE_THREAD); 214 now = base::TimeTicks::HighResNow(); 215 216 glXMakeCurrent(display_, 0, 0); 217 } 218 219 const base::TimeDelta kDefaultInterval = 220 base::TimeDelta::FromSeconds(1) / 60; 221 222 task_runner_->PostTask( 223 FROM_HERE, base::Bind(callback, now, kDefaultInterval)); 224 } 225 226 private: 227 // For initialization of display_ in GLSurface::InitializeOneOff before 228 // the sandbox goes up. 229 friend class gfx::GLSurfaceGLX; 230 231 static Display* display_; 232 233 XID window_; 234 GLXContext context_; 235 236 scoped_refptr<base::SingleThreadTaskRunner> task_runner_; 237 238 base::CancellationFlag cancel_vsync_flag_; 239 base::Lock vsync_lock_; 240 241 DISALLOW_COPY_AND_ASSIGN(SGIVideoSyncProviderThreadShim); 242 }; 243 244 class SGIVideoSyncVSyncProvider 245 : public gfx::VSyncProvider, 246 public base::SupportsWeakPtr<SGIVideoSyncVSyncProvider> { 247 public: 248 explicit SGIVideoSyncVSyncProvider(gfx::AcceleratedWidget window) 249 : vsync_thread_(SGIVideoSyncThread::Create()), 250 shim_(new SGIVideoSyncProviderThreadShim(window)), 251 cancel_vsync_flag_(shim_->cancel_vsync_flag()), 252 vsync_lock_(shim_->vsync_lock()) { 253 vsync_thread_->message_loop()->PostTask( 254 FROM_HERE, 255 base::Bind(&SGIVideoSyncProviderThreadShim::Initialize, 256 base::Unretained(shim_.get()))); 257 } 258 259 virtual ~SGIVideoSyncVSyncProvider() { 260 { 261 base::AutoLock locked(*vsync_lock_); 262 cancel_vsync_flag_->Set(); 263 } 264 265 // Hand-off |shim_| to be deleted on the |vsync_thread_|. 266 vsync_thread_->message_loop()->DeleteSoon( 267 FROM_HERE, 268 shim_.release()); 269 } 270 271 virtual void GetVSyncParameters( 272 const VSyncProvider::UpdateVSyncCallback& callback) OVERRIDE { 273 if (kGetVSyncParametersMinPeriod > base::TimeDelta()) { 274 base::TimeTicks now = base::TimeTicks::Now(); 275 base::TimeDelta delta = now - last_get_vsync_parameters_time_; 276 if (delta < kGetVSyncParametersMinPeriod) 277 return; 278 last_get_vsync_parameters_time_ = now; 279 } 280 281 // Only one outstanding request per surface. 282 if (!pending_callback_) { 283 pending_callback_.reset( 284 new VSyncProvider::UpdateVSyncCallback(callback)); 285 vsync_thread_->message_loop()->PostTask( 286 FROM_HERE, 287 base::Bind(&SGIVideoSyncProviderThreadShim::GetVSyncParameters, 288 base::Unretained(shim_.get()), 289 base::Bind( 290 &SGIVideoSyncVSyncProvider::PendingCallbackRunner, 291 AsWeakPtr()))); 292 } 293 } 294 295 private: 296 void PendingCallbackRunner(const base::TimeTicks timebase, 297 const base::TimeDelta interval) { 298 DCHECK(pending_callback_); 299 pending_callback_->Run(timebase, interval); 300 pending_callback_.reset(); 301 } 302 303 scoped_refptr<SGIVideoSyncThread> vsync_thread_; 304 305 // Thread shim through which the sync provider is accessed on |vsync_thread_|. 306 scoped_ptr<SGIVideoSyncProviderThreadShim> shim_; 307 308 scoped_ptr<VSyncProvider::UpdateVSyncCallback> pending_callback_; 309 310 // Raw pointers to sync primitives owned by the shim_. 311 // These will only be referenced before we post a task to destroy 312 // the shim_, so they are safe to access. 313 base::CancellationFlag* cancel_vsync_flag_; 314 base::Lock* vsync_lock_; 315 316 base::TimeTicks last_get_vsync_parameters_time_; 317 318 DISALLOW_COPY_AND_ASSIGN(SGIVideoSyncVSyncProvider); 319 }; 320 321 SGIVideoSyncThread* SGIVideoSyncThread::g_video_sync_thread = NULL; 322 323 // In order to take advantage of GLX_SGI_video_sync, we need a display 324 // for use on a separate thread. We must allocate this before the sandbox 325 // goes up (rather than on-demand when we start the thread). 326 Display* SGIVideoSyncProviderThreadShim::display_ = NULL; 327 328 } // namespace 329 330 GLSurfaceGLX::GLSurfaceGLX() {} 331 332 bool GLSurfaceGLX::InitializeOneOff() { 333 static bool initialized = false; 334 if (initialized) 335 return true; 336 337 // http://crbug.com/245466 338 setenv("force_s3tc_enable", "true", 1); 339 340 // SGIVideoSyncProviderShim (if instantiated) will issue X commands on 341 // it's own thread. 342 gfx::InitializeThreadedX11(); 343 g_display = gfx::GetXDisplay(); 344 345 if (!g_display) { 346 LOG(ERROR) << "XOpenDisplay failed."; 347 return false; 348 } 349 350 int major, minor; 351 if (!glXQueryVersion(g_display, &major, &minor)) { 352 LOG(ERROR) << "glxQueryVersion failed"; 353 return false; 354 } 355 356 if (major == 1 && minor < 3) { 357 LOG(ERROR) << "GLX 1.3 or later is required."; 358 return false; 359 } 360 361 g_glx_extensions = glXQueryExtensionsString(g_display, 0); 362 g_glx_context_create = 363 HasGLXExtension("GLX_ARB_create_context"); 364 g_glx_create_context_robustness_supported = 365 HasGLXExtension("GLX_ARB_create_context_robustness"); 366 g_glx_texture_from_pixmap_supported = 367 HasGLXExtension("GLX_EXT_texture_from_pixmap"); 368 g_glx_oml_sync_control_supported = 369 HasGLXExtension("GLX_OML_sync_control"); 370 g_glx_get_msc_rate_oml_supported = g_glx_oml_sync_control_supported; 371 g_glx_sgi_video_sync_supported = 372 HasGLXExtension("GLX_SGI_video_sync"); 373 374 if (!g_glx_get_msc_rate_oml_supported && g_glx_sgi_video_sync_supported) 375 SGIVideoSyncProviderThreadShim::display_ = gfx::OpenNewXDisplay(); 376 377 initialized = true; 378 return true; 379 } 380 381 // static 382 const char* GLSurfaceGLX::GetGLXExtensions() { 383 return g_glx_extensions; 384 } 385 386 // static 387 bool GLSurfaceGLX::HasGLXExtension(const char* name) { 388 return ExtensionsContain(GetGLXExtensions(), name); 389 } 390 391 // static 392 bool GLSurfaceGLX::IsCreateContextSupported() { 393 return g_glx_context_create; 394 } 395 396 // static 397 bool GLSurfaceGLX::IsCreateContextRobustnessSupported() { 398 return g_glx_create_context_robustness_supported; 399 } 400 401 // static 402 bool GLSurfaceGLX::IsTextureFromPixmapSupported() { 403 return g_glx_texture_from_pixmap_supported; 404 } 405 406 // static 407 bool GLSurfaceGLX::IsOMLSyncControlSupported() { 408 return g_glx_oml_sync_control_supported; 409 } 410 411 void* GLSurfaceGLX::GetDisplay() { 412 return g_display; 413 } 414 415 GLSurfaceGLX::~GLSurfaceGLX() {} 416 417 NativeViewGLSurfaceGLX::NativeViewGLSurfaceGLX(gfx::AcceleratedWidget window) 418 : parent_window_(window), 419 window_(0), 420 config_(NULL) { 421 } 422 423 gfx::AcceleratedWidget NativeViewGLSurfaceGLX::GetDrawableHandle() const { 424 return window_; 425 } 426 427 bool NativeViewGLSurfaceGLX::Initialize() { 428 XWindowAttributes attributes; 429 if (!XGetWindowAttributes(g_display, parent_window_, &attributes)) { 430 LOG(ERROR) << "XGetWindowAttributes failed for window " << parent_window_ 431 << "."; 432 return false; 433 } 434 size_ = gfx::Size(attributes.width, attributes.height); 435 // Create a child window, with a CopyFromParent visual (to avoid inducing 436 // extra blits in the driver), that we can resize exactly in Resize(), 437 // correctly ordered with GL, so that we don't have invalid transient states. 438 // See https://crbug.com/326995. 439 window_ = XCreateWindow(g_display, 440 parent_window_, 441 0, 442 0, 443 size_.width(), 444 size_.height(), 445 0, 446 CopyFromParent, 447 InputOutput, 448 CopyFromParent, 449 0, 450 NULL); 451 XMapWindow(g_display, window_); 452 453 ui::PlatformEventSource* event_source = 454 ui::PlatformEventSource::GetInstance(); 455 // Can be NULL in tests, when we don't care about Exposes. 456 if (event_source) { 457 XSelectInput(g_display, window_, ExposureMask); 458 ui::PlatformEventSource::GetInstance()->AddPlatformEventDispatcher(this); 459 } 460 XFlush(g_display); 461 462 gfx::AcceleratedWidget window_for_vsync = window_; 463 464 if (g_glx_oml_sync_control_supported) 465 vsync_provider_.reset(new OMLSyncControlVSyncProvider(window_for_vsync)); 466 else if (g_glx_sgi_video_sync_supported) 467 vsync_provider_.reset(new SGIVideoSyncVSyncProvider(window_for_vsync)); 468 469 return true; 470 } 471 472 void NativeViewGLSurfaceGLX::Destroy() { 473 if (window_) { 474 ui::PlatformEventSource* event_source = 475 ui::PlatformEventSource::GetInstance(); 476 if (event_source) 477 event_source->RemovePlatformEventDispatcher(this); 478 XDestroyWindow(g_display, window_); 479 XFlush(g_display); 480 } 481 } 482 483 bool NativeViewGLSurfaceGLX::CanDispatchEvent(const ui::PlatformEvent& event) { 484 return event->type == Expose && event->xexpose.window == window_; 485 } 486 487 uint32_t NativeViewGLSurfaceGLX::DispatchEvent(const ui::PlatformEvent& event) { 488 XEvent forwarded_event = *event; 489 forwarded_event.xexpose.window = parent_window_; 490 XSendEvent(g_display, parent_window_, False, ExposureMask, 491 &forwarded_event); 492 XFlush(g_display); 493 return ui::POST_DISPATCH_STOP_PROPAGATION; 494 } 495 496 bool NativeViewGLSurfaceGLX::Resize(const gfx::Size& size) { 497 size_ = size; 498 glXWaitGL(); 499 XResizeWindow(g_display, window_, size.width(), size.height()); 500 glXWaitX(); 501 return true; 502 } 503 504 bool NativeViewGLSurfaceGLX::IsOffscreen() { 505 return false; 506 } 507 508 bool NativeViewGLSurfaceGLX::SwapBuffers() { 509 TRACE_EVENT2("gpu", "NativeViewGLSurfaceGLX:RealSwapBuffers", 510 "width", GetSize().width(), 511 "height", GetSize().height()); 512 513 glXSwapBuffers(g_display, GetDrawableHandle()); 514 return true; 515 } 516 517 gfx::Size NativeViewGLSurfaceGLX::GetSize() { 518 return size_; 519 } 520 521 void* NativeViewGLSurfaceGLX::GetHandle() { 522 return reinterpret_cast<void*>(GetDrawableHandle()); 523 } 524 525 bool NativeViewGLSurfaceGLX::SupportsPostSubBuffer() { 526 return gfx::g_driver_glx.ext.b_GLX_MESA_copy_sub_buffer; 527 } 528 529 void* NativeViewGLSurfaceGLX::GetConfig() { 530 if (!config_) { 531 // This code path is expensive, but we only take it when 532 // attempting to use GLX_ARB_create_context_robustness, in which 533 // case we need a GLXFBConfig for the window in order to create a 534 // context for it. 535 // 536 // TODO(kbr): this is not a reliable code path. On platforms which 537 // support it, we should use glXChooseFBConfig in the browser 538 // process to choose the FBConfig and from there the X Visual to 539 // use when creating the window in the first place. Then we can 540 // pass that FBConfig down rather than attempting to reconstitute 541 // it. 542 543 XWindowAttributes attributes; 544 if (!XGetWindowAttributes( 545 g_display, 546 window_, 547 &attributes)) { 548 LOG(ERROR) << "XGetWindowAttributes failed for window " << 549 window_ << "."; 550 return NULL; 551 } 552 553 int visual_id = XVisualIDFromVisual(attributes.visual); 554 555 int num_elements = 0; 556 scoped_ptr<GLXFBConfig, ScopedPtrXFree> configs( 557 glXGetFBConfigs(g_display, 558 DefaultScreen(g_display), 559 &num_elements)); 560 if (!configs.get()) { 561 LOG(ERROR) << "glXGetFBConfigs failed."; 562 return NULL; 563 } 564 if (!num_elements) { 565 LOG(ERROR) << "glXGetFBConfigs returned 0 elements."; 566 return NULL; 567 } 568 bool found = false; 569 int i; 570 for (i = 0; i < num_elements; ++i) { 571 int value; 572 if (glXGetFBConfigAttrib( 573 g_display, configs.get()[i], GLX_VISUAL_ID, &value)) { 574 LOG(ERROR) << "glXGetFBConfigAttrib failed."; 575 return NULL; 576 } 577 if (value == visual_id) { 578 found = true; 579 break; 580 } 581 } 582 if (found) { 583 config_ = configs.get()[i]; 584 } 585 } 586 587 return config_; 588 } 589 590 bool NativeViewGLSurfaceGLX::PostSubBuffer( 591 int x, int y, int width, int height) { 592 DCHECK(gfx::g_driver_glx.ext.b_GLX_MESA_copy_sub_buffer); 593 glXCopySubBufferMESA(g_display, GetDrawableHandle(), x, y, width, height); 594 return true; 595 } 596 597 VSyncProvider* NativeViewGLSurfaceGLX::GetVSyncProvider() { 598 return vsync_provider_.get(); 599 } 600 601 NativeViewGLSurfaceGLX::~NativeViewGLSurfaceGLX() { 602 Destroy(); 603 } 604 605 PbufferGLSurfaceGLX::PbufferGLSurfaceGLX(const gfx::Size& size) 606 : size_(size), 607 config_(NULL), 608 pbuffer_(0) { 609 // Some implementations of Pbuffer do not support having a 0 size. For such 610 // cases use a (1, 1) surface. 611 if (size_.GetArea() == 0) 612 size_.SetSize(1, 1); 613 } 614 615 bool PbufferGLSurfaceGLX::Initialize() { 616 DCHECK(!pbuffer_); 617 618 static const int config_attributes[] = { 619 GLX_BUFFER_SIZE, 32, 620 GLX_ALPHA_SIZE, 8, 621 GLX_BLUE_SIZE, 8, 622 GLX_GREEN_SIZE, 8, 623 GLX_RED_SIZE, 8, 624 GLX_RENDER_TYPE, GLX_RGBA_BIT, 625 GLX_DRAWABLE_TYPE, GLX_PBUFFER_BIT, 626 GLX_DOUBLEBUFFER, False, 627 0 628 }; 629 630 int num_elements = 0; 631 scoped_ptr<GLXFBConfig, ScopedPtrXFree> configs( 632 glXChooseFBConfig(g_display, 633 DefaultScreen(g_display), 634 config_attributes, 635 &num_elements)); 636 if (!configs.get()) { 637 LOG(ERROR) << "glXChooseFBConfig failed."; 638 return false; 639 } 640 if (!num_elements) { 641 LOG(ERROR) << "glXChooseFBConfig returned 0 elements."; 642 return false; 643 } 644 645 config_ = configs.get()[0]; 646 647 const int pbuffer_attributes[] = { 648 GLX_PBUFFER_WIDTH, size_.width(), 649 GLX_PBUFFER_HEIGHT, size_.height(), 650 0 651 }; 652 pbuffer_ = glXCreatePbuffer(g_display, 653 static_cast<GLXFBConfig>(config_), 654 pbuffer_attributes); 655 if (!pbuffer_) { 656 Destroy(); 657 LOG(ERROR) << "glXCreatePbuffer failed."; 658 return false; 659 } 660 661 return true; 662 } 663 664 void PbufferGLSurfaceGLX::Destroy() { 665 if (pbuffer_) { 666 glXDestroyPbuffer(g_display, pbuffer_); 667 pbuffer_ = 0; 668 } 669 670 config_ = NULL; 671 } 672 673 bool PbufferGLSurfaceGLX::IsOffscreen() { 674 return true; 675 } 676 677 bool PbufferGLSurfaceGLX::SwapBuffers() { 678 NOTREACHED() << "Attempted to call SwapBuffers on a pbuffer."; 679 return false; 680 } 681 682 gfx::Size PbufferGLSurfaceGLX::GetSize() { 683 return size_; 684 } 685 686 void* PbufferGLSurfaceGLX::GetHandle() { 687 return reinterpret_cast<void*>(pbuffer_); 688 } 689 690 void* PbufferGLSurfaceGLX::GetConfig() { 691 return config_; 692 } 693 694 PbufferGLSurfaceGLX::~PbufferGLSurfaceGLX() { 695 Destroy(); 696 } 697 698 } // namespace gfx 699