blob: d65de19ee99c451445dfbf1b1359dc2a8ba38c85 [file] [log] [blame]
/*
* Copyright (C) 2017 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "CAD3DRenderer.h"
#include "Image.h"
#include "ImageConversion.h"
#include "InvertColorPS.h"
#include <CoreGraphics/CGSRegion.h>
#include <QuartzCore/CACFContext.h>
#include <QuartzCore/CARenderOGL.h>
#include <limits>
#include <string>
#include <wtf/RefPtr.h>
// Define strings from the HLSL effect that are referenced later
#define HLSL_TECHNIQUE "InvertColor"
#define SCENE_TEXTURE "sceneTexture"
typedef HRESULT (CALLBACK *LPFNDLL_Direct3DCreate9Ex)(UINT, void **);
namespace WKQCA {
static const DWORD customVertexFormat = D3DFVF_XYZRHW | D3DFVF_TEX1;
D3DPostProcessingContext::D3DPostProcessingContext(const CComPtr<IDirect3DTexture9>& sceneTexture, const CComPtr<IDirect3DVertexBuffer9>& overlayQuad)
: m_sceneTexture(sceneTexture)
, m_overlayQuad(overlayQuad)
{
}
static IDirect3D9* s_d3d;
static IDirect3D9* d3d()
{
static bool initialized;
if (initialized)
return s_d3d;
initialized = true;
HMODULE d3dLibHandle = LoadLibrary(TEXT("d3d9.dll"));
if (!d3dLibHandle)
return nullptr;
// AVFoundationCF requires us to create an IDirect3DDevice9Ex instead of an IDirect3DDevice9 for HW
// video decoding to work, because IDirect3DDevice9Ex::ResetEx() allows the HW decoder to persist
// during a reset, while IDirect3DDevice9::Reset() requires that the HW decoder be destroyed.
LPFNDLL_Direct3DCreate9Ex d3dCreate9Ex = (LPFNDLL_Direct3DCreate9Ex)GetProcAddress(d3dLibHandle, "Direct3DCreate9Ex");
if (d3dCreate9Ex) {
d3dCreate9Ex(D3D_SDK_VERSION, (void **)&s_d3d);
if (s_d3d) {
D3DCAPS9 caps;
HRESULT hr = s_d3d->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps);
// HW video decoding requires D3DCAPS2_DYNAMICTEXTURES, which not all d3d9 devices support,
// so fall back to plain old IDirect3DDevice9 if it isn't available.
if (!FAILED(hr) && (caps.Caps2 & D3DCAPS2_DYNAMICTEXTURES))
return s_d3d;
// Release the IDirect3DDevice9Ex, it doesn't have the features we require so we should
// allocate and use a plain old IDirect3D9.
s_d3d->Release();
}
}
s_d3d = Direct3DCreate9(D3D_SDK_VERSION);
return s_d3d;
}
static D3DPRESENT_PARAMETERS initialPresentationParameters(const CGSize& size)
{
D3DPRESENT_PARAMETERS parameters = {0};
parameters.Windowed = TRUE;
parameters.SwapEffect = D3DSWAPEFFECT_COPY;
parameters.BackBufferCount = 1;
parameters.BackBufferFormat = D3DFMT_UNKNOWN;
parameters.BackBufferHeight = size.height;
parameters.BackBufferWidth = size.width;
parameters.MultiSampleType = D3DMULTISAMPLE_NONE;
return parameters;
}
CAD3DRenderer& CAD3DRenderer::shared()
{
static CAD3DRenderer& shared = *new CAD3DRenderer;
return shared;
}
CAD3DRenderer::CAD3DRenderer()
{
}
CComPtr<IDirect3DSwapChain9> CAD3DRenderer::swapChain(CWindow window, const CGSize& size)
{
auto locker = holdLock(m_lock);
bool useDefaultSwapChain = false;
if (!m_d3dDevice) {
if (!initialize(window, size))
return nullptr;
ASSERT(m_d3dDevice);
// Since we used this window to initialize the device, the device's default swap chain
// should be associated with this window.
useDefaultSwapChain = true;
}
if (m_deviceIsLost) {
if (!resetD3DDevice(window, size))
return nullptr;
ASSERT(!m_deviceIsLost);
// Since we used this window to reset the device, the device's default swap chain should be
// associated with this window.
useDefaultSwapChain = true;
}
if (useDefaultSwapChain) {
CComPtr<IDirect3DSwapChain9> defaultSwapChain;
if (SUCCEEDED(m_d3dDevice->GetSwapChain(0, &defaultSwapChain))) {
#if !ASSERT_DISABLED
// Since we just initialized or reset the device, its default swap chain should be
// associated with and sized to match this window.
D3DPRESENT_PARAMETERS parameters;
ASSERT(SUCCEEDED(defaultSwapChain->GetPresentParameters(&parameters)));
ASSERT(parameters.hDeviceWindow == window);
CRect clientRect;
ASSERT(window.GetClientRect(clientRect));
ASSERT(parameters.BackBufferHeight == clientRect.Height());
ASSERT(parameters.BackBufferWidth == clientRect.Width());
#endif
return defaultSwapChain;
}
}
D3DPRESENT_PARAMETERS parameters = initialPresentationParameters(size);
parameters.hDeviceWindow = window;
CComPtr<IDirect3DSwapChain9> result;
if (FAILED(m_d3dDevice->CreateAdditionalSwapChain(&parameters, &result)))
return nullptr;
return result;
}
struct CustomVertex {
// position
float x;
float y;
float z;
float rhw; // indicates that the position is already transformed to screen coordinates
// texture coordinates
float tu;
float tv;
};
std::unique_ptr<D3DPostProcessingContext> CAD3DRenderer::createD3DPostProcessingContext(IDirect3DSwapChain9* swapChain, const CGSize& size)
{
ASSERT_ARG(swapChain, swapChain);
// Get the back buffer format of the swap chain; we want to create the scene texture using the same format.
D3DPRESENT_PARAMETERS swapChainParameters;
if (FAILED(swapChain->GetPresentParameters(&swapChainParameters)))
return nullptr;
// FIXME: We should fall back to software rendering if CreateTexture() returns D3DERR_OUTOFVIDEOMEMORY.
CComPtr<IDirect3DTexture9> sceneTexture;
HRESULT hr = m_d3dDevice->CreateTexture(size.width, size.height, 1, D3DUSAGE_RENDERTARGET, swapChainParameters.BackBufferFormat, D3DPOOL_DEFAULT, &sceneTexture, 0);
if (FAILED(hr)) {
ASSERT(hr != D3DERR_OUTOFVIDEOMEMORY);
return nullptr;
}
// Create a quad in screen coordinates onto which sceneTexture will be
// applied. In D3D, pixels are referenced from the center of a 1x1 cell, so
// to properly encapsulate the cell we need to offset the quad boundaries by
// 0.5 pixels.
const float left = -0.5;
const float right = size.width - 0.5;
const float top = -0.5;
const float bottom = size.height - 0.5;
const float z = 1;
const float rhw = 1;
CustomVertex overlayQuadVertices[4] = {
{ left, top, z, rhw, 0, 0 },
{ right, top, z, rhw, 1, 0 },
{ left, bottom, z, rhw, 0, 1 },
{ right, bottom, z, rhw, 1, 1 }
};
CComPtr<IDirect3DVertexBuffer9> overlayQuad;
if (FAILED(m_d3dDevice->CreateVertexBuffer(sizeof(overlayQuadVertices), D3DUSAGE_WRITEONLY, customVertexFormat, D3DPOOL_DEFAULT, &overlayQuad, 0)))
return nullptr;
void* overlayQuadBuffer;
if (FAILED(overlayQuad->Lock(0, 0, &overlayQuadBuffer, 0)))
return nullptr;
memcpy(overlayQuadBuffer, overlayQuadVertices, sizeof(overlayQuadVertices));
overlayQuad->Unlock();
return makeUnique<D3DPostProcessingContext>(sceneTexture, overlayQuad);
}
// FIXME: <rdar://6507851> Share this code with CoreAnimation's
// CA::OGL::DX9Context::update function.
static bool hardwareCapabilitiesIndicateCoreAnimationSupport(const D3DCAPS9& caps)
{
// CoreAnimation needs two or more texture units.
if (caps.MaxTextureBlendStages < 2)
return false;
// CoreAnimation needs non-power-of-two textures.
if ((caps.TextureCaps & D3DPTEXTURECAPS_POW2) && !(caps.TextureCaps & D3DPTEXTURECAPS_NONPOW2CONDITIONAL))
return false;
// CoreAnimation needs vertex shader 2.0 or greater.
if (D3DSHADER_VERSION_MAJOR(caps.VertexShaderVersion) < 2)
return false;
// CoreAnimation needs pixel shader 2.0 or greater.
if (D3DSHADER_VERSION_MAJOR(caps.PixelShaderVersion) < 2)
return false;
return true;
}
bool CAD3DRenderer::initialize(CWindow window, const CGSize& size)
{
#if !ASSERT_DISABLED
if (!m_deviceThreadID)
m_deviceThreadID = ::GetCurrentThreadId();
// MSDN says that IDirect3D9::CreateDevice must "be called from the same thread that handles
// window messages." <http://msdn.microsoft.com/en-us/library/bb147224(VS.85).aspx>.
ASSERT(::GetCurrentThreadId() == m_deviceThreadID);
ASSERT(::GetCurrentThreadId() == window.GetWindowThreadID());
#endif
if (m_initialized)
return m_d3dDevice;
m_initialized = true;
if (!d3d())
return false;
D3DCAPS9 d3dCaps;
if (FAILED(d3d()->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &d3dCaps)))
return false;
DWORD behaviorFlags = D3DCREATE_FPU_PRESERVE | D3DCREATE_MULTITHREADED;
if ((d3dCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) && d3dCaps.VertexProcessingCaps)
behaviorFlags |= D3DCREATE_HARDWARE_VERTEXPROCESSING;
else
behaviorFlags |= D3DCREATE_SOFTWARE_VERTEXPROCESSING;
D3DPRESENT_PARAMETERS parameters = initialPresentationParameters(size);
parameters.hDeviceWindow = window;
CComPtr<IDirect3DDevice9> device;
CComQIPtr<IDirect3D9Ex> d3d9Ex = d3d();
HRESULT hr;
if (d3d9Ex)
hr = d3d9Ex->CreateDeviceEx(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, 0, behaviorFlags, &parameters, 0, (IDirect3DDevice9Ex **)&device);
else
hr = s_d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, 0, behaviorFlags, &parameters, &device);
if (FAILED(hr))
return false;
// Now that we've created the IDirect3DDevice9 based on the capabilities we
// got from the IDirect3D9 global object, we requery the device for its
// actual capabilities. The capabilities returned by the device can
// sometimes be more complete, for example when using software vertex
// processing.
D3DCAPS9 deviceCaps;
if (FAILED(device->GetDeviceCaps(&deviceCaps)))
return false;
if (!hardwareCapabilitiesIndicateCoreAnimationSupport(deviceCaps))
return false;
m_d3dDevice.Attach(device.Detach());
m_usingDirect3D9Ex = d3d9Ex;
m_renderOGLContext = CARenderOGLNew(&kCARenderDX9Callbacks, m_d3dDevice, 0);
return true;
}
static void D3DMatrixOrthoOffCenterRH(D3DMATRIX* pOut, float l, float r, float b, float t, float zn, float zf)
{
pOut->_11 = 2 / (r-l);
pOut->_12 = 0;
pOut->_13 = 0;
pOut->_14 = 0;
pOut->_21 = 0;
pOut->_22 = 2 / (t-b);
pOut->_23 = 0;
pOut->_24 = 0;
pOut->_31 = 0;
pOut->_32 = 0;
pOut->_33 = 1 / (zn-zf);
pOut->_34 = 0;
pOut->_41 = (l+r) / (l-r);
pOut->_42 = (t+b) / (b-t);
pOut->_43 = zn / (zn-zf);
pOut->_44 = 1;
}
static bool prepareDevice(IDirect3DDevice9* device, const CGRect& bounds, IDirect3DSwapChain9* swapChain, D3DPostProcessingContext* postProcessingContext)
{
CComPtr<IDirect3DSurface9> backBuffer;
// If we will post-process the scene, we want to render to the scene texture rather than the back buffer.
if (postProcessingContext) {
if (FAILED(postProcessingContext->sceneTexture()->GetSurfaceLevel(0, &backBuffer)))
return false;
} else {
if (FAILED(swapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &backBuffer)))
return false;
}
CComPtr<IDirect3DSurface9> currentRenderTarget;
if (FAILED(device->GetRenderTarget(0, &currentRenderTarget)))
return false;
if (backBuffer != currentRenderTarget && FAILED(device->SetRenderTarget(0, backBuffer)))
return false;
float x0 = bounds.origin.x;
float y0 = bounds.origin.y;
float x1 = x0 + bounds.size.width;
float y1 = y0 + bounds.size.height;
D3DMATRIX projection;
D3DMatrixOrthoOffCenterRH(&projection, x0, x1, y0, y1, -1.0f, 1.0f);
if (FAILED(device->SetTransform(D3DTS_PROJECTION, &projection)))
return false;
return true;
}
static CGRect updateBounds(CARenderUpdate* update)
{
CGSRegionObj rgn = CARenderUpdateCopyRegion(update);
if (!rgn)
return CGRectZero;
CGRect result;
CGError error = CGSGetRegionBounds(rgn, &result);
CGSReleaseRegion(rgn);
return error == kCGErrorSuccess ? result : CGRectZero;
}
CAD3DRenderer::RenderResult CAD3DRenderer::renderAndPresent(const CGRect& bounds, IDirect3DSwapChain9* swapChain, D3DPostProcessingContext* postProcessingContext, CACFContextRef context, CFTimeInterval& nextRenderTime)
{
ASSERT_ARG(swapChain, swapChain);
ASSERT_ARG(context, context);
auto locker = holdLock(m_lock);
CGRect unusedDirtyRect;
RenderResult result = renderInternal(bounds, swapChain, postProcessingContext, context, unusedDirtyRect, nextRenderTime);
switch (result) {
case DeviceBecameLost:
ASSERT_NOT_REACHED();
case DeviceIsLost:
case OtherFailure:
return result;
case NoRenderingRequired:
case Success:
break;
}
// Note that we only use the bounds size, not its origin, when deciding where in the
// window to render. As with NSViews, the bounds origin determines where content is
// positioned within the view, not where the view is positioned in its parent.
CRect destinationRect(0, 0, CGRectGetWidth(bounds), CGRectGetHeight(bounds));
// FIXME: Passing a pDirtyRegion based on the CARenderUpdate's dirty region to
// IDirect3DSwapChain9::Present might increase performance.
if (swapChain->Present(0, destinationRect, 0, 0, 0) != D3DERR_DEVICELOST)
return Success;
// The device was lost. Record that this happened so that we'll try to reset it before
// we try to create a new swap chain.
setDeviceIsLost(true);
// We can't render until the device is reset, which the caller will have to take care of.
nextRenderTime = std::numeric_limits<CFTimeInterval>::infinity();
return DeviceBecameLost;
}
CAD3DRenderer::RenderResult CAD3DRenderer::renderToImage(const CGRect& bounds, IDirect3DSwapChain9* swapChain, D3DPostProcessingContext* postProcessingContext, CACFContextRef context, RefPtr<Image>& outImage, CGPoint& imageOrigin, CFTimeInterval& nextRenderTime)
{
ASSERT_ARG(swapChain, swapChain);
ASSERT_ARG(context, context);
auto locker = holdLock(m_lock);
CGRect dirtyRect;
RenderResult result = renderInternal(bounds, swapChain, postProcessingContext, context, dirtyRect, nextRenderTime);
switch (result) {
case DeviceBecameLost:
ASSERT_NOT_REACHED();
case DeviceIsLost:
case OtherFailure:
case NoRenderingRequired:
return result;
case Success:
break;
}
ASSERT(!CGRectIsEmpty(dirtyRect));
dirtyRect = CGRectIntersection(dirtyRect, bounds);
CRect dirtyWinRect(CPoint(CGRectGetMinX(dirtyRect), CGRectGetHeight(bounds) - CGRectGetMaxY(dirtyRect)), CSize(CGRectGetWidth(dirtyRect), CGRectGetHeight(dirtyRect)));
RefPtr<Image> image;
HRESULT hr = getBackBufferRectAsImage(m_d3dDevice, swapChain, dirtyWinRect, image);
if (SUCCEEDED(hr)) {
outImage = WTFMove(image);
imageOrigin = dirtyRect.origin;
return Success;
}
if (hr != D3DERR_DEVICELOST)
return OtherFailure;
// The device was lost. Record that this happened so that we'll try to reset it before
// we try to create a new swap chain.
setDeviceIsLost(true);
// We can't render until the device is reset, which the caller will have to take care of.
nextRenderTime = std::numeric_limits<CFTimeInterval>::infinity();
return DeviceBecameLost;
}
void CAD3DRenderer::setDeviceIsLost(bool lost)
{
ASSERT_WITH_MESSAGE(!m_lock.tryLock(), "m_lock must be held when calling this function");
if (m_deviceIsLost == lost)
return;
m_deviceIsLost = lost;
if (!m_deviceIsLost)
return;
// We have to release any existing D3D resources before we try to reset the device.
// We'll reallocate them as needed. See
// <http://msdn.microsoft.com/en-us/library/bb174425(v=VS.85).aspx>.
// This will cause CA to release its D3D resources.
CARenderOGLPurge(m_renderOGLContext);
}
CAD3DRenderer::RenderResult CAD3DRenderer::renderInternal(const CGRect& bounds, IDirect3DSwapChain9* swapChain, D3DPostProcessingContext* postProcessingContext, CACFContextRef context, CGRect& dirtyRect, CFTimeInterval& nextRenderTime)
{
ASSERT_ARG(swapChain, swapChain);
ASSERT_ARG(context, context);
ASSERT_WITH_MESSAGE(!m_lock.tryLock(), "m_lock must be held when calling this function");
ASSERT(m_d3dDevice);
ASSERT(m_renderOGLContext);
nextRenderTime = std::numeric_limits<CFTimeInterval>::infinity();
if (m_deviceIsLost)
return DeviceIsLost;
if (!prepareDevice(m_d3dDevice, bounds, swapChain, postProcessingContext))
return OtherFailure;
char space[4096];
CARenderUpdate* u = CARenderUpdateBegin(space, sizeof(space), CACurrentMediaTime(), 0, 0, &bounds);
if (!u)
return OtherFailure;
CARenderContext* renderContext = static_cast<CARenderContext*>(CACFContextGetRenderContext(context));
CARenderContextLock(renderContext);
CARenderUpdateAddContext(u, renderContext);
CARenderContextUnlock(renderContext);
nextRenderTime = CARenderUpdateGetNextTime(u);
bool didRender = false;
dirtyRect = updateBounds(u);
if (!CGRectIsEmpty(dirtyRect)) {
m_d3dDevice->BeginScene();
CARenderOGLRender(m_renderOGLContext, u);
if (postProcessingContext)
postProcess(bounds, swapChain, postProcessingContext);
m_d3dDevice->EndScene();
didRender = true;
}
CARenderUpdateFinish(u);
return didRender ? Success : NoRenderingRequired;
}
void CAD3DRenderer::postProcess(const CGRect& bounds, IDirect3DSwapChain9* swapChain, D3DPostProcessingContext* context)
{
// The scene has been rendered to the scene texture, so now draw a quad to
// the back buffer and apply the scene texture to the quad with the
// InvertColor HLSL technique.
ASSERT_ARG(swapChain, swapChain);
ASSERT_ARG(context, context);
CComPtr<IDirect3DSurface9> backBuffer;
if (FAILED(swapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &backBuffer)))
return;
if (FAILED(m_d3dDevice->SetRenderTarget(0, backBuffer)))
return;
if (FAILED(m_d3dDevice->SetFVF(customVertexFormat)))
return;
if (FAILED(m_d3dDevice->SetStreamSource(0, context->overlayQuad(), 0, sizeof(CustomVertex))))
return;
createShaderIfNeeded();
m_d3dDevice->SetTexture(0, context->sceneTexture());
m_d3dDevice->SetPixelShader(m_pixelShader);
m_d3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
m_d3dDevice->SetPixelShader(nullptr);
}
bool CAD3DRenderer::resetD3DDevice(CWindow window, const CGSize& size)
{
ASSERT(m_d3dDevice);
ASSERT(m_renderOGLContext);
ASSERT(m_deviceIsLost);
// MSDN says that IDirect3DDevice9::Reset must "be called from the same thread that handles
// window messages." <http://msdn.microsoft.com/en-us/library/bb147224(VS.85).aspx>.
ASSERT(::GetCurrentThreadId() == m_deviceThreadID);
ASSERT(::GetCurrentThreadId() == window.GetWindowThreadID());
HRESULT hr = m_d3dDevice->TestCooperativeLevel();
if (hr == D3DERR_DEVICELOST || hr == D3DERR_DRIVERINTERNALERROR) {
// The device cannot be reset at this time. We'll try again later.
return false;
}
setDeviceIsLost(false);
if (hr == D3D_OK) {
// The device wasn't lost after all.
return true;
}
// We can reset the device.
D3DPRESENT_PARAMETERS parameters = initialPresentationParameters(size);
parameters.hDeviceWindow = window;
if (m_usingDirect3D9Ex) {
CComQIPtr<IDirect3DDevice9Ex> d3d9Ex = m_d3dDevice;
ASSERT(d3d9Ex);
hr = d3d9Ex->ResetEx(&parameters, nullptr);
} else
hr = m_d3dDevice->Reset(&parameters);
// TestCooperativeLevel told us the device may be reset now, so we should
// not be told here that the device is lost.
ASSERT(hr != D3DERR_DEVICELOST);
return true;
}
void CAD3DRenderer::createShaderIfNeeded()
{
if (m_pixelShader)
return;
m_d3dDevice->CreatePixelShader(reinterpret_cast<const DWORD*>(g_ps20_InvertColorPS), &m_pixelShader);
}
void CAD3DRenderer::release()
{
if (m_renderOGLContext)
CARenderOGLDestroy(m_renderOGLContext);
m_d3dDevice = nullptr;
if (s_d3d)
s_d3d->Release();
}
} // namespace WKQCA