OLD | NEW |
| (Empty) |
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 #include "content/common/gpu/image_transport_surface.h" | |
6 | |
7 #include "base/mac/scoped_cftyperef.h" | |
8 #include "base/memory/scoped_ptr.h" | |
9 #include "content/common/gpu/gpu_command_buffer_stub.h" | |
10 #include "content/common/gpu/gpu_messages.h" | |
11 #include "ui/gfx/native_widget_types.h" | |
12 #include "ui/gl/gl_bindings.h" | |
13 #include "ui/gl/gl_context.h" | |
14 #include "ui/gl/gl_implementation.h" | |
15 #include "ui/gl/gl_surface_cgl.h" | |
16 #include "ui/gl/gl_surface_osmesa.h" | |
17 | |
18 // Note that this must be included after gl_bindings.h to avoid conflicts. | |
19 #include <OpenGL/CGLIOSurface.h> | |
20 | |
21 namespace content { | |
22 namespace { | |
23 | |
24 // IOSurface dimensions will be rounded up to a multiple of this value in order | |
25 // to reduce memory thrashing during resize. This must be a power of 2. | |
26 const uint32 kIOSurfaceDimensionRoundup = 64; | |
27 | |
28 int RoundUpSurfaceDimension(int number) { | |
29 DCHECK(number >= 0); | |
30 // Cast into unsigned space for portable bitwise ops. | |
31 uint32 unsigned_number = static_cast<uint32>(number); | |
32 uint32 roundup_sub_1 = kIOSurfaceDimensionRoundup - 1; | |
33 unsigned_number = (unsigned_number + roundup_sub_1) & ~roundup_sub_1; | |
34 return static_cast<int>(unsigned_number); | |
35 } | |
36 | |
37 // We are backed by an offscreen surface for the purposes of creating | |
38 // a context, but use FBOs to render to texture backed IOSurface | |
39 class IOSurfaceImageTransportSurface | |
40 : public gfx::NoOpGLSurfaceCGL, | |
41 public ImageTransportSurface, | |
42 public GpuCommandBufferStub::DestructionObserver { | |
43 public: | |
44 IOSurfaceImageTransportSurface(GpuChannelManager* manager, | |
45 GpuCommandBufferStub* stub, | |
46 gfx::PluginWindowHandle handle); | |
47 | |
48 // GLSurface implementation | |
49 virtual bool Initialize() OVERRIDE; | |
50 virtual void Destroy() OVERRIDE; | |
51 virtual bool DeferDraws() OVERRIDE; | |
52 virtual bool IsOffscreen() OVERRIDE; | |
53 virtual bool SwapBuffers() OVERRIDE; | |
54 virtual bool PostSubBuffer(int x, int y, int width, int height) OVERRIDE; | |
55 virtual bool SupportsPostSubBuffer() OVERRIDE; | |
56 virtual gfx::Size GetSize() OVERRIDE; | |
57 virtual bool OnMakeCurrent(gfx::GLContext* context) OVERRIDE; | |
58 virtual unsigned int GetBackingFrameBufferObject() OVERRIDE; | |
59 virtual bool SetBackbufferAllocation(bool allocated) OVERRIDE; | |
60 virtual void SetFrontbufferAllocation(bool allocated) OVERRIDE; | |
61 | |
62 protected: | |
63 // ImageTransportSurface implementation | |
64 virtual void OnBufferPresented( | |
65 const AcceleratedSurfaceMsg_BufferPresented_Params& params) OVERRIDE; | |
66 virtual void OnResize(gfx::Size size, float scale_factor) OVERRIDE; | |
67 virtual void SetLatencyInfo( | |
68 const std::vector<ui::LatencyInfo>&) OVERRIDE; | |
69 virtual void WakeUpGpu() OVERRIDE; | |
70 | |
71 // GpuCommandBufferStub::DestructionObserver implementation. | |
72 virtual void OnWillDestroyStub() OVERRIDE; | |
73 | |
74 private: | |
75 virtual ~IOSurfaceImageTransportSurface() OVERRIDE; | |
76 | |
77 void AdjustBufferAllocation(); | |
78 void UnrefIOSurface(); | |
79 void CreateIOSurface(); | |
80 | |
81 // Tracks the current buffer allocation state. | |
82 bool backbuffer_suggested_allocation_; | |
83 bool frontbuffer_suggested_allocation_; | |
84 | |
85 uint32 fbo_id_; | |
86 GLuint texture_id_; | |
87 GLuint depth_stencil_renderbuffer_id_; | |
88 | |
89 base::ScopedCFTypeRef<IOSurfaceRef> io_surface_; | |
90 | |
91 // The id of |io_surface_| or 0 if that's NULL. | |
92 uint64 io_surface_handle_; | |
93 | |
94 // Weak pointer to the context that this was last made current to. | |
95 gfx::GLContext* context_; | |
96 | |
97 gfx::Size size_; | |
98 gfx::Size rounded_size_; | |
99 float scale_factor_; | |
100 | |
101 // Whether or not we've successfully made the surface current once. | |
102 bool made_current_; | |
103 | |
104 // Whether a SwapBuffers is pending. | |
105 bool is_swap_buffers_pending_; | |
106 | |
107 // Whether we unscheduled command buffer because of pending SwapBuffers. | |
108 bool did_unschedule_; | |
109 | |
110 std::vector<ui::LatencyInfo> latency_info_; | |
111 | |
112 scoped_ptr<ImageTransportHelper> helper_; | |
113 | |
114 DISALLOW_COPY_AND_ASSIGN(IOSurfaceImageTransportSurface); | |
115 }; | |
116 | |
117 void AddBooleanValue(CFMutableDictionaryRef dictionary, | |
118 const CFStringRef key, | |
119 bool value) { | |
120 CFDictionaryAddValue(dictionary, key, | |
121 (value ? kCFBooleanTrue : kCFBooleanFalse)); | |
122 } | |
123 | |
124 void AddIntegerValue(CFMutableDictionaryRef dictionary, | |
125 const CFStringRef key, | |
126 int32 value) { | |
127 base::ScopedCFTypeRef<CFNumberRef> number( | |
128 CFNumberCreate(NULL, kCFNumberSInt32Type, &value)); | |
129 CFDictionaryAddValue(dictionary, key, number.get()); | |
130 } | |
131 | |
132 IOSurfaceImageTransportSurface::IOSurfaceImageTransportSurface( | |
133 GpuChannelManager* manager, | |
134 GpuCommandBufferStub* stub, | |
135 gfx::PluginWindowHandle handle) | |
136 : gfx::NoOpGLSurfaceCGL(gfx::Size(1, 1)), | |
137 backbuffer_suggested_allocation_(true), | |
138 frontbuffer_suggested_allocation_(true), | |
139 fbo_id_(0), | |
140 texture_id_(0), | |
141 depth_stencil_renderbuffer_id_(0), | |
142 io_surface_handle_(0), | |
143 context_(NULL), | |
144 scale_factor_(1.f), | |
145 made_current_(false), | |
146 is_swap_buffers_pending_(false), | |
147 did_unschedule_(false) { | |
148 helper_.reset(new ImageTransportHelper(this, manager, stub, handle)); | |
149 } | |
150 | |
151 IOSurfaceImageTransportSurface::~IOSurfaceImageTransportSurface() { | |
152 } | |
153 | |
154 bool IOSurfaceImageTransportSurface::Initialize() { | |
155 // Only support IOSurfaces if the GL implementation is the native desktop GL. | |
156 // IO surfaces will not work with, for example, OSMesa software renderer | |
157 // GL contexts. | |
158 if (gfx::GetGLImplementation() != gfx::kGLImplementationDesktopGL && | |
159 gfx::GetGLImplementation() != gfx::kGLImplementationAppleGL) | |
160 return false; | |
161 | |
162 if (!helper_->Initialize()) | |
163 return false; | |
164 | |
165 if (!NoOpGLSurfaceCGL::Initialize()) { | |
166 helper_->Destroy(); | |
167 return false; | |
168 } | |
169 | |
170 helper_->stub()->AddDestructionObserver(this); | |
171 return true; | |
172 } | |
173 | |
174 void IOSurfaceImageTransportSurface::Destroy() { | |
175 UnrefIOSurface(); | |
176 | |
177 helper_->Destroy(); | |
178 NoOpGLSurfaceCGL::Destroy(); | |
179 } | |
180 | |
181 bool IOSurfaceImageTransportSurface::DeferDraws() { | |
182 // The command buffer hit a draw/clear command that could clobber the | |
183 // IOSurface in use by an earlier SwapBuffers. If a Swap is pending, abort | |
184 // processing of the command by returning true and unschedule until the Swap | |
185 // Ack arrives. | |
186 if(did_unschedule_) | |
187 return true; // Still unscheduled, so just return true. | |
188 if (is_swap_buffers_pending_) { | |
189 did_unschedule_ = true; | |
190 helper_->SetScheduled(false); | |
191 return true; | |
192 } | |
193 return false; | |
194 } | |
195 | |
196 bool IOSurfaceImageTransportSurface::IsOffscreen() { | |
197 return false; | |
198 } | |
199 | |
200 bool IOSurfaceImageTransportSurface::OnMakeCurrent(gfx::GLContext* context) { | |
201 context_ = context; | |
202 | |
203 if (made_current_) | |
204 return true; | |
205 | |
206 OnResize(gfx::Size(1, 1), 1.f); | |
207 | |
208 made_current_ = true; | |
209 return true; | |
210 } | |
211 | |
212 unsigned int IOSurfaceImageTransportSurface::GetBackingFrameBufferObject() { | |
213 return fbo_id_; | |
214 } | |
215 | |
216 bool IOSurfaceImageTransportSurface::SetBackbufferAllocation(bool allocation) { | |
217 if (backbuffer_suggested_allocation_ == allocation) | |
218 return true; | |
219 backbuffer_suggested_allocation_ = allocation; | |
220 AdjustBufferAllocation(); | |
221 return true; | |
222 } | |
223 | |
224 void IOSurfaceImageTransportSurface::SetFrontbufferAllocation(bool allocation) { | |
225 if (frontbuffer_suggested_allocation_ == allocation) | |
226 return; | |
227 frontbuffer_suggested_allocation_ = allocation; | |
228 AdjustBufferAllocation(); | |
229 } | |
230 | |
231 void IOSurfaceImageTransportSurface::AdjustBufferAllocation() { | |
232 // On mac, the frontbuffer and backbuffer are the same buffer. The buffer is | |
233 // free'd when both the browser and gpu processes have Unref'd the IOSurface. | |
234 if (!backbuffer_suggested_allocation_ && | |
235 !frontbuffer_suggested_allocation_ && | |
236 io_surface_.get()) { | |
237 UnrefIOSurface(); | |
238 helper_->Suspend(); | |
239 } else if (backbuffer_suggested_allocation_ && !io_surface_) { | |
240 CreateIOSurface(); | |
241 } | |
242 } | |
243 | |
244 bool IOSurfaceImageTransportSurface::SwapBuffers() { | |
245 DCHECK(backbuffer_suggested_allocation_); | |
246 if (!frontbuffer_suggested_allocation_) | |
247 return true; | |
248 glFlush(); | |
249 | |
250 GpuHostMsg_AcceleratedSurfaceBuffersSwapped_Params params; | |
251 params.surface_handle = io_surface_handle_; | |
252 params.size = GetSize(); | |
253 params.scale_factor = scale_factor_; | |
254 params.latency_info.swap(latency_info_); | |
255 helper_->SendAcceleratedSurfaceBuffersSwapped(params); | |
256 | |
257 DCHECK(!is_swap_buffers_pending_); | |
258 is_swap_buffers_pending_ = true; | |
259 return true; | |
260 } | |
261 | |
262 bool IOSurfaceImageTransportSurface::PostSubBuffer( | |
263 int x, int y, int width, int height) { | |
264 DCHECK(backbuffer_suggested_allocation_); | |
265 if (!frontbuffer_suggested_allocation_) | |
266 return true; | |
267 glFlush(); | |
268 | |
269 GpuHostMsg_AcceleratedSurfacePostSubBuffer_Params params; | |
270 params.surface_handle = io_surface_handle_; | |
271 params.x = x; | |
272 params.y = y; | |
273 params.width = width; | |
274 params.height = height; | |
275 params.surface_size = GetSize(); | |
276 params.surface_scale_factor = scale_factor_; | |
277 params.latency_info.swap(latency_info_); | |
278 helper_->SendAcceleratedSurfacePostSubBuffer(params); | |
279 | |
280 DCHECK(!is_swap_buffers_pending_); | |
281 is_swap_buffers_pending_ = true; | |
282 return true; | |
283 } | |
284 | |
285 bool IOSurfaceImageTransportSurface::SupportsPostSubBuffer() { | |
286 return true; | |
287 } | |
288 | |
289 gfx::Size IOSurfaceImageTransportSurface::GetSize() { | |
290 return size_; | |
291 } | |
292 | |
293 void IOSurfaceImageTransportSurface::OnBufferPresented( | |
294 const AcceleratedSurfaceMsg_BufferPresented_Params& params) { | |
295 DCHECK(is_swap_buffers_pending_); | |
296 | |
297 context_->share_group()->SetRendererID(params.renderer_id); | |
298 is_swap_buffers_pending_ = false; | |
299 if (did_unschedule_) { | |
300 did_unschedule_ = false; | |
301 helper_->SetScheduled(true); | |
302 } | |
303 } | |
304 | |
305 void IOSurfaceImageTransportSurface::OnResize(gfx::Size size, | |
306 float scale_factor) { | |
307 // This trace event is used in gpu_feature_browsertest.cc - the test will need | |
308 // to be updated if this event is changed or moved. | |
309 TRACE_EVENT2("gpu", "IOSurfaceImageTransportSurface::OnResize", | |
310 "old_width", size_.width(), "new_width", size.width()); | |
311 // Caching |context_| from OnMakeCurrent. It should still be current. | |
312 DCHECK(context_->IsCurrent(this)); | |
313 | |
314 size_ = size; | |
315 scale_factor_ = scale_factor; | |
316 | |
317 CreateIOSurface(); | |
318 } | |
319 | |
320 void IOSurfaceImageTransportSurface::SetLatencyInfo( | |
321 const std::vector<ui::LatencyInfo>& latency_info) { | |
322 for (size_t i = 0; i < latency_info.size(); i++) | |
323 latency_info_.push_back(latency_info[i]); | |
324 } | |
325 | |
326 void IOSurfaceImageTransportSurface::WakeUpGpu() { | |
327 NOTIMPLEMENTED(); | |
328 } | |
329 | |
330 void IOSurfaceImageTransportSurface::OnWillDestroyStub() { | |
331 helper_->stub()->RemoveDestructionObserver(this); | |
332 Destroy(); | |
333 } | |
334 | |
335 void IOSurfaceImageTransportSurface::UnrefIOSurface() { | |
336 // If we have resources to destroy, then make sure that we have a current | |
337 // context which we can use to delete the resources. | |
338 if (context_ || fbo_id_ || texture_id_ || depth_stencil_renderbuffer_id_) { | |
339 DCHECK(gfx::GLContext::GetCurrent() == context_); | |
340 DCHECK(context_->IsCurrent(this)); | |
341 DCHECK(CGLGetCurrentContext()); | |
342 } | |
343 | |
344 if (fbo_id_) { | |
345 glDeleteFramebuffersEXT(1, &fbo_id_); | |
346 fbo_id_ = 0; | |
347 } | |
348 | |
349 if (texture_id_) { | |
350 glDeleteTextures(1, &texture_id_); | |
351 texture_id_ = 0; | |
352 } | |
353 | |
354 if (depth_stencil_renderbuffer_id_) { | |
355 glDeleteRenderbuffersEXT(1, &depth_stencil_renderbuffer_id_); | |
356 depth_stencil_renderbuffer_id_ = 0; | |
357 } | |
358 | |
359 io_surface_.reset(); | |
360 io_surface_handle_ = 0; | |
361 } | |
362 | |
363 void IOSurfaceImageTransportSurface::CreateIOSurface() { | |
364 gfx::Size new_rounded_size(RoundUpSurfaceDimension(size_.width()), | |
365 RoundUpSurfaceDimension(size_.height())); | |
366 | |
367 // Only recreate surface when the rounded up size has changed. | |
368 if (io_surface_.get() && new_rounded_size == rounded_size_) | |
369 return; | |
370 | |
371 // This trace event is used in gpu_feature_browsertest.cc - the test will need | |
372 // to be updated if this event is changed or moved. | |
373 TRACE_EVENT2("gpu", "IOSurfaceImageTransportSurface::CreateIOSurface", | |
374 "width", new_rounded_size.width(), | |
375 "height", new_rounded_size.height()); | |
376 | |
377 rounded_size_ = new_rounded_size; | |
378 | |
379 GLint previous_texture_id = 0; | |
380 glGetIntegerv(GL_TEXTURE_BINDING_RECTANGLE_ARB, &previous_texture_id); | |
381 | |
382 // Free the old IO Surface first to reduce memory fragmentation. | |
383 UnrefIOSurface(); | |
384 | |
385 glGenFramebuffersEXT(1, &fbo_id_); | |
386 glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo_id_); | |
387 | |
388 glGenTextures(1, &texture_id_); | |
389 | |
390 // GL_TEXTURE_RECTANGLE_ARB is the best supported render target on | |
391 // Mac OS X and is required for IOSurface interoperability. | |
392 GLenum target = GL_TEXTURE_RECTANGLE_ARB; | |
393 glBindTexture(target, texture_id_); | |
394 glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST); | |
395 glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST); | |
396 glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
397 glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
398 | |
399 glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, | |
400 GL_COLOR_ATTACHMENT0_EXT, | |
401 target, | |
402 texture_id_, | |
403 0); | |
404 | |
405 | |
406 // Search through the provided attributes; if the caller has | |
407 // requested a stencil buffer, try to get one. | |
408 | |
409 int32 stencil_bits = | |
410 helper_->stub()->GetRequestedAttribute(EGL_STENCIL_SIZE); | |
411 if (stencil_bits > 0) { | |
412 // Create and bind the stencil buffer | |
413 bool has_packed_depth_stencil = | |
414 GLSurface::ExtensionsContain( | |
415 reinterpret_cast<const char *>(glGetString(GL_EXTENSIONS)), | |
416 "GL_EXT_packed_depth_stencil"); | |
417 | |
418 if (has_packed_depth_stencil) { | |
419 glGenRenderbuffersEXT(1, &depth_stencil_renderbuffer_id_); | |
420 glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, | |
421 depth_stencil_renderbuffer_id_); | |
422 glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH24_STENCIL8_EXT, | |
423 rounded_size_.width(), rounded_size_.height()); | |
424 glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, | |
425 GL_STENCIL_ATTACHMENT_EXT, | |
426 GL_RENDERBUFFER_EXT, | |
427 depth_stencil_renderbuffer_id_); | |
428 } | |
429 | |
430 // If we asked for stencil but the extension isn't present, | |
431 // it's OK to silently fail; subsequent code will/must check | |
432 // for the presence of a stencil buffer before attempting to | |
433 // do stencil-based operations. | |
434 } | |
435 | |
436 // Allocate a new IOSurface, which is the GPU resource that can be | |
437 // shared across processes. | |
438 base::ScopedCFTypeRef<CFMutableDictionaryRef> properties; | |
439 properties.reset(CFDictionaryCreateMutable(kCFAllocatorDefault, | |
440 0, | |
441 &kCFTypeDictionaryKeyCallBacks, | |
442 &kCFTypeDictionaryValueCallBacks)); | |
443 AddIntegerValue(properties, | |
444 kIOSurfaceWidth, | |
445 rounded_size_.width()); | |
446 AddIntegerValue(properties, | |
447 kIOSurfaceHeight, | |
448 rounded_size_.height()); | |
449 AddIntegerValue(properties, | |
450 kIOSurfaceBytesPerElement, 4); | |
451 AddBooleanValue(properties, | |
452 kIOSurfaceIsGlobal, true); | |
453 // I believe we should be able to unreference the IOSurfaces without | |
454 // synchronizing with the browser process because they are | |
455 // ultimately reference counted by the operating system. | |
456 io_surface_.reset(IOSurfaceCreate(properties)); | |
457 io_surface_handle_ = IOSurfaceGetID(io_surface_); | |
458 | |
459 // Don't think we need to identify a plane. | |
460 GLuint plane = 0; | |
461 CGLError cglerror = | |
462 CGLTexImageIOSurface2D( | |
463 static_cast<CGLContextObj>(context_->GetHandle()), | |
464 target, | |
465 GL_RGBA, | |
466 rounded_size_.width(), | |
467 rounded_size_.height(), | |
468 GL_BGRA, | |
469 GL_UNSIGNED_INT_8_8_8_8_REV, | |
470 io_surface_.get(), | |
471 plane); | |
472 if (cglerror != kCGLNoError) { | |
473 UnrefIOSurface(); | |
474 return; | |
475 } | |
476 | |
477 glFlush(); | |
478 | |
479 GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); | |
480 if (status != GL_FRAMEBUFFER_COMPLETE_EXT) { | |
481 DLOG(ERROR) << "Framebuffer was incomplete: " << status; | |
482 UnrefIOSurface(); | |
483 return; | |
484 } | |
485 | |
486 glBindTexture(target, previous_texture_id); | |
487 // The FBO remains bound for this GL context. | |
488 } | |
489 | |
490 // A subclass of GLSurfaceOSMesa that doesn't print an error message when | |
491 // SwapBuffers() is called. | |
492 class DRTSurfaceOSMesa : public gfx::GLSurfaceOSMesa { | |
493 public: | |
494 // Size doesn't matter, the surface is resized to the right size later. | |
495 DRTSurfaceOSMesa() : GLSurfaceOSMesa(GL_RGBA, gfx::Size(1, 1)) {} | |
496 | |
497 // Implement a subset of GLSurface. | |
498 virtual bool SwapBuffers() OVERRIDE; | |
499 | |
500 private: | |
501 virtual ~DRTSurfaceOSMesa() {} | |
502 DISALLOW_COPY_AND_ASSIGN(DRTSurfaceOSMesa); | |
503 }; | |
504 | |
505 bool DRTSurfaceOSMesa::SwapBuffers() { | |
506 return true; | |
507 } | |
508 | |
509 bool g_allow_os_mesa = false; | |
510 | |
511 } // namespace | |
512 | |
513 // static | |
514 scoped_refptr<gfx::GLSurface> ImageTransportSurface::CreateNativeSurface( | |
515 GpuChannelManager* manager, | |
516 GpuCommandBufferStub* stub, | |
517 const gfx::GLSurfaceHandle& surface_handle) { | |
518 DCHECK(surface_handle.transport_type == gfx::NATIVE_DIRECT || | |
519 surface_handle.transport_type == gfx::NATIVE_TRANSPORT); | |
520 | |
521 switch (gfx::GetGLImplementation()) { | |
522 case gfx::kGLImplementationDesktopGL: | |
523 case gfx::kGLImplementationAppleGL: | |
524 return scoped_refptr<gfx::GLSurface>(new IOSurfaceImageTransportSurface( | |
525 manager, stub, surface_handle.handle)); | |
526 | |
527 default: | |
528 // Content shell in DRT mode spins up a gpu process which needs an | |
529 // image transport surface, but that surface isn't used to read pixel | |
530 // baselines. So this is mostly a dummy surface. | |
531 if (!g_allow_os_mesa) { | |
532 NOTREACHED(); | |
533 return scoped_refptr<gfx::GLSurface>(); | |
534 } | |
535 scoped_refptr<gfx::GLSurface> surface(new DRTSurfaceOSMesa()); | |
536 if (!surface.get() || !surface->Initialize()) | |
537 return surface; | |
538 return scoped_refptr<gfx::GLSurface>(new PassThroughImageTransportSurface( | |
539 manager, stub, surface.get())); | |
540 } | |
541 } | |
542 | |
543 // static | |
544 void ImageTransportSurface::SetAllowOSMesaForTesting(bool allow) { | |
545 g_allow_os_mesa = allow; | |
546 } | |
547 | |
548 } // namespace content | |
OLD | NEW |