mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
5b899ebc2d
MozReview-Commit-ID: AzUbKzQp9lT
536 lines
19 KiB
C++
536 lines
19 KiB
C++
/* -*- Mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40; -*- */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "SharedSurfaceD3D11Interop.h"
|
|
|
|
#include <d3d11.h>
|
|
#include <d3d11_1.h>
|
|
#include "gfxPrefs.h"
|
|
#include "GLContext.h"
|
|
#include "WGLLibrary.h"
|
|
#include "nsPrintfCString.h"
|
|
#include "mozilla/gfx/DeviceManagerDx.h"
|
|
|
|
namespace mozilla {
|
|
namespace gl {
|
|
|
|
/*
|
|
Sample Code for WGL_NV_DX_interop2:
|
|
Example: Render to Direct3D 11 backbuffer with openGL:
|
|
|
|
// create D3D11 device, context and swap chain.
|
|
ID3D11Device *device;
|
|
ID3D11DeviceContext *devCtx;
|
|
IDXGISwapChain *swapChain;
|
|
|
|
DXGI_SWAP_CHAIN_DESC scd;
|
|
|
|
<set appropriate swap chain parameters in scd>
|
|
|
|
hr = D3D11CreateDeviceAndSwapChain(NULL, // pAdapter
|
|
D3D_DRIVER_TYPE_HARDWARE, // DriverType
|
|
NULL, // Software
|
|
0, // Flags (Do not set D3D11_CREATE_DEVICE_SINGLETHREADED)
|
|
NULL, // pFeatureLevels
|
|
0, // FeatureLevels
|
|
D3D11_SDK_VERSION, // SDKVersion
|
|
&scd, // pSwapChainDesc
|
|
&swapChain, // ppSwapChain
|
|
&device, // ppDevice
|
|
NULL, // pFeatureLevel
|
|
&devCtx); // ppImmediateContext
|
|
|
|
// Fetch the swapchain backbuffer
|
|
ID3D11Texture2D *dxColorbuffer;
|
|
swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID *)&dxColorbuffer);
|
|
|
|
// Create depth stencil texture
|
|
ID3D11Texture2D *dxDepthBuffer;
|
|
D3D11_TEXTURE2D_DESC depthDesc;
|
|
depthDesc.Usage = D3D11_USAGE_DEFAULT;
|
|
<set other depthDesc parameters appropriately>
|
|
|
|
// Create Views
|
|
ID3D11RenderTargetView *colorBufferView;
|
|
D3D11_RENDER_TARGET_VIEW_DESC rtd;
|
|
<set rtd parameters appropriately>
|
|
device->CreateRenderTargetView(dxColorbuffer, &rtd, &colorBufferView);
|
|
|
|
ID3D11DepthStencilView *depthBufferView;
|
|
D3D11_DEPTH_STENCIL_VIEW_DESC dsd;
|
|
<set dsd parameters appropriately>
|
|
device->CreateDepthStencilView(dxDepthBuffer, &dsd, &depthBufferView);
|
|
|
|
// Attach back buffer and depth texture to redertarget for the device.
|
|
devCtx->OMSetRenderTargets(1, &colorBufferView, depthBufferView);
|
|
|
|
// Register D3D11 device with GL
|
|
HANDLE gl_handleD3D;
|
|
gl_handleD3D = wglDXOpenDeviceNV(device);
|
|
|
|
// register the Direct3D color and depth/stencil buffers as
|
|
// renderbuffers in opengl
|
|
GLuint gl_names[2];
|
|
HANDLE gl_handles[2];
|
|
|
|
glGenRenderbuffers(2, gl_names);
|
|
|
|
gl_handles[0] = wglDXRegisterObjectNV(gl_handleD3D, dxColorBuffer,
|
|
gl_names[0],
|
|
GL_RENDERBUFFER,
|
|
WGL_ACCESS_READ_WRITE_NV);
|
|
|
|
gl_handles[1] = wglDXRegisterObjectNV(gl_handleD3D, dxDepthBuffer,
|
|
gl_names[1],
|
|
GL_RENDERBUFFER,
|
|
WGL_ACCESS_READ_WRITE_NV);
|
|
|
|
// attach the Direct3D buffers to an FBO
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
GL_RENDERBUFFER, gl_names[0]);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
|
|
GL_RENDERBUFFER, gl_names[1]);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT,
|
|
GL_RENDERBUFFER, gl_names[1]);
|
|
|
|
while (!done) {
|
|
<direct3d renders to the render targets>
|
|
|
|
// lock the render targets for GL access
|
|
wglDXLockObjectsNVX(gl_handleD3D, 2, gl_handles);
|
|
|
|
<opengl renders to the render targets>
|
|
|
|
// unlock the render targets
|
|
wglDXUnlockObjectsNVX(gl_handleD3D, 2, gl_handles);
|
|
|
|
<direct3d renders to the render targets and presents
|
|
the results on the screen>
|
|
}
|
|
*/
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// DXInterop2Device
|
|
|
|
class ScopedContextState final
|
|
{
|
|
ID3D11DeviceContext1* const mD3DContext;
|
|
RefPtr<ID3DDeviceContextState> mOldContextState;
|
|
|
|
public:
|
|
ScopedContextState(ID3D11DeviceContext1* d3dContext,
|
|
ID3DDeviceContextState* newContextState)
|
|
: mD3DContext(d3dContext)
|
|
, mOldContextState(nullptr)
|
|
{
|
|
if (!mD3DContext)
|
|
return;
|
|
|
|
mD3DContext->SwapDeviceContextState(newContextState,
|
|
getter_AddRefs(mOldContextState));
|
|
}
|
|
|
|
~ScopedContextState()
|
|
{
|
|
if (!mD3DContext)
|
|
return;
|
|
|
|
mD3DContext->SwapDeviceContextState(mOldContextState, nullptr);
|
|
}
|
|
};
|
|
|
|
class DXInterop2Device : public RefCounted<DXInterop2Device>
|
|
{
|
|
public:
|
|
MOZ_DECLARE_REFCOUNTED_TYPENAME(DXInterop2Device)
|
|
|
|
WGLLibrary* const mWGL;
|
|
const RefPtr<ID3D11Device> mD3D; // Only needed for lifetime guarantee.
|
|
const HANDLE mInteropDevice;
|
|
GLContext* const mGL;
|
|
|
|
// AMD workaround.
|
|
const RefPtr<ID3D11DeviceContext1> mD3DContext;
|
|
const RefPtr<ID3DDeviceContextState> mContextState;
|
|
|
|
static already_AddRefed<DXInterop2Device> Open(WGLLibrary* wgl, GLContext* gl)
|
|
{
|
|
MOZ_ASSERT(wgl->HasDXInterop2());
|
|
|
|
const RefPtr<ID3D11Device> d3d = gfx::DeviceManagerDx::Get()->GetContentDevice();
|
|
if (!d3d) {
|
|
gfxCriticalNote << "DXInterop2Device::Open: Failed to create D3D11 device.";
|
|
return nullptr;
|
|
}
|
|
|
|
if (!gl->MakeCurrent())
|
|
return nullptr;
|
|
|
|
RefPtr<ID3D11DeviceContext1> d3dContext;
|
|
RefPtr<ID3DDeviceContextState> contextState;
|
|
if (gl->WorkAroundDriverBugs() && gl->Vendor() == GLVendor::ATI) {
|
|
// AMD calls ID3D10Device::Flush, so we need to be in ID3D10Device mode.
|
|
RefPtr<ID3D11Device1> d3d11_1;
|
|
auto hr = d3d->QueryInterface(__uuidof(ID3D11Device1),
|
|
getter_AddRefs(d3d11_1));
|
|
if (!SUCCEEDED(hr))
|
|
return nullptr;
|
|
|
|
d3d11_1->GetImmediateContext1(getter_AddRefs(d3dContext));
|
|
MOZ_ASSERT(d3dContext);
|
|
|
|
const D3D_FEATURE_LEVEL featureLevel10_0 = D3D_FEATURE_LEVEL_10_0;
|
|
hr = d3d11_1->CreateDeviceContextState(0, &featureLevel10_0, 1,
|
|
D3D11_SDK_VERSION,
|
|
__uuidof(ID3D10Device), nullptr,
|
|
getter_AddRefs(contextState));
|
|
if (!SUCCEEDED(hr))
|
|
return nullptr;
|
|
}
|
|
|
|
const auto interopDevice = wgl->mSymbols.fDXOpenDeviceNV(d3d);
|
|
if (!interopDevice) {
|
|
gfxCriticalNote << "DXInterop2Device::Open: DXOpenDevice failed.";
|
|
return nullptr;
|
|
}
|
|
|
|
return MakeAndAddRef<DXInterop2Device>(wgl, d3d, interopDevice, gl, d3dContext,
|
|
contextState);
|
|
}
|
|
|
|
DXInterop2Device(WGLLibrary* wgl, ID3D11Device* d3d, HANDLE interopDevice,
|
|
GLContext* gl, ID3D11DeviceContext1* d3dContext,
|
|
ID3DDeviceContextState* contextState)
|
|
: mWGL(wgl)
|
|
, mD3D(d3d)
|
|
, mInteropDevice(interopDevice)
|
|
, mGL(gl)
|
|
, mD3DContext(d3dContext)
|
|
, mContextState(contextState)
|
|
{ }
|
|
|
|
~DXInterop2Device() {
|
|
const auto isCurrent = mGL->MakeCurrent();
|
|
|
|
if (mWGL->mSymbols.fDXCloseDeviceNV(mInteropDevice))
|
|
return;
|
|
|
|
if (isCurrent) {
|
|
// That shouldn't have failed.
|
|
const uint32_t error = GetLastError();
|
|
const nsPrintfCString errorMessage("wglDXCloseDevice(0x%p) failed:"
|
|
" GetLastError(): %u\n",
|
|
mInteropDevice, error);
|
|
gfxCriticalError() << errorMessage.BeginReading();
|
|
}
|
|
}
|
|
|
|
HANDLE RegisterObject(void* d3dObject, GLuint name, GLenum type,
|
|
GLenum access) const
|
|
{
|
|
if (!mGL->MakeCurrent())
|
|
return nullptr;
|
|
|
|
const ScopedContextState autoCS(mD3DContext, mContextState);
|
|
const auto ret = mWGL->mSymbols.fDXRegisterObjectNV(mInteropDevice, d3dObject,
|
|
name, type, access);
|
|
if (ret)
|
|
return ret;
|
|
|
|
const uint32_t error = GetLastError();
|
|
const nsPrintfCString errorMessage("wglDXRegisterObject(0x%p, 0x%p, %u, 0x%04x,"
|
|
" 0x%04x) failed: GetLastError(): %u\n",
|
|
mInteropDevice, d3dObject, name, type, access,
|
|
error);
|
|
gfxCriticalNote << errorMessage.BeginReading();
|
|
return nullptr;
|
|
}
|
|
|
|
bool UnregisterObject(HANDLE lockHandle) const {
|
|
const auto isCurrent = mGL->MakeCurrent();
|
|
|
|
const ScopedContextState autoCS(mD3DContext, mContextState);
|
|
if (mWGL->mSymbols.fDXUnregisterObjectNV(mInteropDevice, lockHandle))
|
|
return true;
|
|
|
|
if (!isCurrent) {
|
|
// That shouldn't have failed.
|
|
const uint32_t error = GetLastError();
|
|
const nsPrintfCString errorMessage("wglDXUnregisterObject(0x%p, 0x%p) failed:"
|
|
" GetLastError(): %u\n",
|
|
mInteropDevice, lockHandle, error);
|
|
gfxCriticalError() << errorMessage.BeginReading();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool LockObject(HANDLE lockHandle) const {
|
|
MOZ_ASSERT(mGL->IsCurrent());
|
|
|
|
if (mWGL->mSymbols.fDXLockObjectsNV(mInteropDevice, 1, &lockHandle))
|
|
return true;
|
|
|
|
if (!mGL->MakeCurrent())
|
|
return false;
|
|
|
|
gfxCriticalNote << "wglDXLockObjects called without mGL being current."
|
|
<< " Retrying after MakeCurrent.";
|
|
|
|
if (mWGL->mSymbols.fDXLockObjectsNV(mInteropDevice, 1, &lockHandle))
|
|
return true;
|
|
|
|
const uint32_t error = GetLastError();
|
|
const nsPrintfCString errorMessage("wglDXLockObjects(0x%p, 1, {0x%p}) failed:"
|
|
" GetLastError(): %u\n",
|
|
mInteropDevice, lockHandle, error);
|
|
gfxCriticalError() << errorMessage.BeginReading();
|
|
return false;
|
|
}
|
|
|
|
bool UnlockObject(HANDLE lockHandle) const {
|
|
MOZ_ASSERT(mGL->IsCurrent());
|
|
|
|
if (mWGL->mSymbols.fDXUnlockObjectsNV(mInteropDevice, 1, &lockHandle))
|
|
return true;
|
|
|
|
if (!mGL->MakeCurrent())
|
|
return false;
|
|
|
|
gfxCriticalNote << "wglDXUnlockObjects called without mGL being current."
|
|
<< " Retrying after MakeCurrent.";
|
|
|
|
if (mWGL->mSymbols.fDXUnlockObjectsNV(mInteropDevice, 1, &lockHandle))
|
|
return true;
|
|
|
|
const uint32_t error = GetLastError();
|
|
const nsPrintfCString errorMessage("wglDXUnlockObjects(0x%p, 1, {0x%p}) failed:"
|
|
" GetLastError(): %u\n",
|
|
mInteropDevice, lockHandle, error);
|
|
gfxCriticalError() << errorMessage.BeginReading();
|
|
return false;
|
|
}
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Shared Surface
|
|
|
|
/*static*/ UniquePtr<SharedSurface_D3D11Interop>
|
|
SharedSurface_D3D11Interop::Create(DXInterop2Device* interop,
|
|
GLContext* gl,
|
|
const gfx::IntSize& size,
|
|
bool hasAlpha)
|
|
{
|
|
const auto& d3d = interop->mD3D;
|
|
|
|
// Create a texture in case we need to readback.
|
|
DXGI_FORMAT format = hasAlpha ? DXGI_FORMAT_B8G8R8A8_UNORM
|
|
: DXGI_FORMAT_B8G8R8X8_UNORM;
|
|
CD3D11_TEXTURE2D_DESC desc(format, size.width, size.height, 1, 1);
|
|
desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
|
|
|
|
RefPtr<ID3D11Texture2D> texD3D;
|
|
auto hr = d3d->CreateTexture2D(&desc, nullptr, getter_AddRefs(texD3D));
|
|
if (FAILED(hr)) {
|
|
NS_WARNING("Failed to create texture for CanvasLayer!");
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<IDXGIResource> texDXGI;
|
|
hr = texD3D->QueryInterface(__uuidof(IDXGIResource), getter_AddRefs(texDXGI));
|
|
if (FAILED(hr)) {
|
|
NS_WARNING("Failed to open texture for sharing!");
|
|
return nullptr;
|
|
}
|
|
|
|
HANDLE dxgiHandle;
|
|
texDXGI->GetSharedHandle(&dxgiHandle);
|
|
|
|
////
|
|
|
|
if (!gl->MakeCurrent()) {
|
|
NS_WARNING("MakeCurrent failed.");
|
|
return nullptr;
|
|
}
|
|
|
|
GLuint interopRB = 0;
|
|
gl->fGenRenderbuffers(1, &interopRB);
|
|
const auto lockHandle = interop->RegisterObject(texD3D, interopRB,
|
|
LOCAL_GL_RENDERBUFFER,
|
|
LOCAL_WGL_ACCESS_WRITE_DISCARD_NV);
|
|
if (!lockHandle) {
|
|
NS_WARNING("Failed to register D3D object with WGL.");
|
|
gl->fDeleteRenderbuffers(1, &interopRB);
|
|
return nullptr;
|
|
}
|
|
|
|
////
|
|
|
|
GLuint prodTex = 0;
|
|
GLuint interopFB = 0;
|
|
{
|
|
GLint samples = 0;
|
|
{
|
|
const ScopedBindRenderbuffer bindRB(gl, interopRB);
|
|
gl->fGetRenderbufferParameteriv(LOCAL_GL_RENDERBUFFER,
|
|
LOCAL_GL_RENDERBUFFER_SAMPLES, &samples);
|
|
}
|
|
if (samples > 0) { // Intel
|
|
// Intel's dx_interop GL-side textures have SAMPLES=1, likely because that's
|
|
// what the D3DTextures technically have. However, SAMPLES=1 is very different
|
|
// from SAMPLES=0 in GL.
|
|
// For more, see https://bugzilla.mozilla.org/show_bug.cgi?id=1325835
|
|
|
|
// Our ShSurf tex or rb must be single-sampled.
|
|
gl->fGenTextures(1, &prodTex);
|
|
const ScopedBindTexture bindTex(gl, prodTex);
|
|
gl->TexParams_SetClampNoMips();
|
|
|
|
const GLenum format = (hasAlpha ? LOCAL_GL_RGBA : LOCAL_GL_RGB);
|
|
const ScopedBindPBO nullPBO(gl, LOCAL_GL_PIXEL_UNPACK_BUFFER);
|
|
gl->fTexImage2D(LOCAL_GL_TEXTURE_2D, 0, format, size.width, size.height, 0,
|
|
format, LOCAL_GL_UNSIGNED_BYTE, nullptr);
|
|
|
|
gl->fGenFramebuffers(1, &interopFB);
|
|
ScopedBindFramebuffer bindFB(gl, interopFB);
|
|
gl->fFramebufferRenderbuffer(LOCAL_GL_FRAMEBUFFER, LOCAL_GL_COLOR_ATTACHMENT0,
|
|
LOCAL_GL_RENDERBUFFER, interopRB);
|
|
MOZ_ASSERT(gl->fCheckFramebufferStatus(LOCAL_GL_FRAMEBUFFER) ==
|
|
LOCAL_GL_FRAMEBUFFER_COMPLETE);
|
|
}
|
|
}
|
|
|
|
////
|
|
|
|
typedef SharedSurface_D3D11Interop ptrT;
|
|
UniquePtr<ptrT> ret ( new ptrT(gl, size, hasAlpha, prodTex, interopFB, interopRB,
|
|
interop, lockHandle, texD3D, dxgiHandle) );
|
|
return Move(ret);
|
|
}
|
|
|
|
SharedSurface_D3D11Interop::SharedSurface_D3D11Interop(GLContext* gl,
|
|
const gfx::IntSize& size,
|
|
bool hasAlpha, GLuint prodTex,
|
|
GLuint interopFB, GLuint interopRB,
|
|
DXInterop2Device* interop,
|
|
HANDLE lockHandle,
|
|
ID3D11Texture2D* texD3D,
|
|
HANDLE dxgiHandle)
|
|
: SharedSurface(SharedSurfaceType::DXGLInterop2,
|
|
prodTex ? AttachmentType::GLTexture
|
|
: AttachmentType::GLRenderbuffer,
|
|
gl,
|
|
size,
|
|
hasAlpha,
|
|
true)
|
|
, mProdTex(prodTex)
|
|
, mInteropFB(interopFB)
|
|
, mInteropRB(interopRB)
|
|
, mInterop(interop)
|
|
, mLockHandle(lockHandle)
|
|
, mTexD3D(texD3D)
|
|
, mDXGIHandle(dxgiHandle)
|
|
, mNeedsFinish(gfxPrefs::WebGLDXGLNeedsFinish())
|
|
, mLockedForGL(false)
|
|
{
|
|
MOZ_ASSERT(bool(mProdTex) == bool(mInteropFB));
|
|
}
|
|
|
|
SharedSurface_D3D11Interop::~SharedSurface_D3D11Interop()
|
|
{
|
|
MOZ_ASSERT(!IsProducerAcquired());
|
|
|
|
if (!mGL || !mGL->MakeCurrent())
|
|
return;
|
|
|
|
if (!mInterop->UnregisterObject(mLockHandle)) {
|
|
NS_WARNING("Failed to release mLockHandle, possibly leaking it.");
|
|
}
|
|
|
|
mGL->fDeleteTextures(1, &mProdTex);
|
|
mGL->fDeleteFramebuffers(1, &mInteropFB);
|
|
mGL->fDeleteRenderbuffers(1, &mInteropRB);
|
|
}
|
|
|
|
void
|
|
SharedSurface_D3D11Interop::ProducerAcquireImpl()
|
|
{
|
|
MOZ_ASSERT(!mLockedForGL);
|
|
|
|
// Now we have the mutex, we can lock for GL.
|
|
MOZ_ALWAYS_TRUE( mInterop->LockObject(mLockHandle) );
|
|
|
|
mLockedForGL = true;
|
|
}
|
|
|
|
void
|
|
SharedSurface_D3D11Interop::ProducerReleaseImpl()
|
|
{
|
|
MOZ_ASSERT(mLockedForGL);
|
|
|
|
if (mProdTex) {
|
|
mGL->BlitHelper()->DrawBlitTextureToFramebuffer(mProdTex, mInteropFB, mSize,
|
|
mSize);
|
|
}
|
|
|
|
if (mNeedsFinish) {
|
|
mGL->fFinish();
|
|
} else {
|
|
// We probably don't even need this.
|
|
mGL->fFlush();
|
|
}
|
|
MOZ_ALWAYS_TRUE( mInterop->UnlockObject(mLockHandle) );
|
|
|
|
mLockedForGL = false;
|
|
}
|
|
|
|
bool
|
|
SharedSurface_D3D11Interop::ToSurfaceDescriptor(layers::SurfaceDescriptor* const out_descriptor)
|
|
{
|
|
const auto format = (mHasAlpha ? gfx::SurfaceFormat::B8G8R8A8
|
|
: gfx::SurfaceFormat::B8G8R8X8);
|
|
*out_descriptor = layers::SurfaceDescriptorD3D10(WindowsHandle(mDXGIHandle), format,
|
|
mSize);
|
|
return true;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
// Factory
|
|
|
|
/*static*/ UniquePtr<SurfaceFactory_D3D11Interop>
|
|
SurfaceFactory_D3D11Interop::Create(GLContext* gl, const SurfaceCaps& caps,
|
|
layers::LayersIPCChannel* allocator,
|
|
const layers::TextureFlags& flags)
|
|
{
|
|
WGLLibrary* wgl = &sWGLLib;
|
|
if (!wgl || !wgl->HasDXInterop2())
|
|
return nullptr;
|
|
|
|
const RefPtr<DXInterop2Device> interop = DXInterop2Device::Open(wgl, gl);
|
|
if (!interop) {
|
|
NS_WARNING("Failed to open D3D device for use by WGL.");
|
|
return nullptr;
|
|
}
|
|
|
|
typedef SurfaceFactory_D3D11Interop ptrT;
|
|
UniquePtr<ptrT> ret(new ptrT(gl, caps, allocator, flags, interop));
|
|
return Move(ret);
|
|
}
|
|
|
|
SurfaceFactory_D3D11Interop::SurfaceFactory_D3D11Interop(GLContext* gl,
|
|
const SurfaceCaps& caps,
|
|
layers::LayersIPCChannel* allocator,
|
|
const layers::TextureFlags& flags,
|
|
DXInterop2Device* interop)
|
|
: SurfaceFactory(SharedSurfaceType::DXGLInterop2, gl, caps, allocator, flags)
|
|
, mInterop(interop)
|
|
{ }
|
|
|
|
SurfaceFactory_D3D11Interop::~SurfaceFactory_D3D11Interop()
|
|
{ }
|
|
|
|
} // namespace gl
|
|
} // namespace mozilla
|