blob: bd411bdc76a7eebf6856ff708eae9b61d4b5497d [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 "CAView.h"
#include "CVDisplayLink.h"
#include "Image.h"
#include "WebKitQuartzCoreAdditionsInternal.h"
#include <QuartzCore/CACFContext.h>
#include <QuartzCore/CACFLayer.h>
#include <QuartzCore/CARenderOGL.h>
#include <d3d9.h>
#include <wtf/HashSet.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/StdLibExtras.h>
#include <wtf/Vector.h>
#include <wtf/win/GDIObject.h>
namespace WKQCA {
class CAView::Handle : public ThreadSafeRefCounted<Handle> {
public:
static Ref<Handle> create(CAView* view) { return adoptRef(*new Handle(view)); }
~Handle() { ASSERT(!m_view); }
Lock& lock() { return m_lock; }
CAView* view() const
{
ASSERT_WITH_MESSAGE(!const_cast<Lock&>(m_lock).tryLock(), "CAView::Handle's lock must be held when calling this function");
return m_view;
}
void clear()
{
ASSERT_WITH_MESSAGE(!m_lock.tryLock(), "CAView::Handle's lock must be held when calling this function");
m_view = nullptr;
}
private:
Handle(CAView* view)
: m_view(view) { }
Lock m_lock;
CAView* m_view;
};
static Lock globalStateLock;
static HWND messageWindow;
static const wchar_t messageWindowClassName[] = L"CAViewMessageWindow";
static HashSet<Ref<CAView::Handle>>& views()
{
static NeverDestroyed<HashSet<Ref<CAView::Handle>>> views;
return views.get();
}
static HashSet<Ref<CAView::Handle>>& viewsNeedingUpdate()
{
static NeverDestroyed<HashSet<Ref<CAView::Handle>>> views;
return views.get();
}
static void registerMessageWindowClass()
{
static bool didRegister;
if (didRegister)
return;
didRegister = true;
WNDCLASSW cls = {0};
cls.hInstance = dllInstance();
cls.lpfnWndProc = ::DefWindowProcW;
cls.lpszClassName = messageWindowClassName;
::RegisterClassW(&cls);
}
static HWND createMessageWindow()
{
registerMessageWindowClass();
return ::CreateWindowW(messageWindowClassName, 0, 0, 0, 0, 0, 0, HWND_MESSAGE, 0, 0, 0);
}
void CAView::releaseAllD3DResources()
{
static Lock lock;
if (!lock.tryLock()) {
// Another thread is currently releasing 3D resources.
// Since it will also release resources for the view calling this method, we can just return early.
return;
}
Vector<Ref<Handle>> viewsToRelease;
{
Locker locker { globalStateLock };
viewsToRelease = WTF::map(views(), [] (auto& handle) { return handle.copyRef(); });
}
for (auto& handle : viewsToRelease) {
Locker locker { handle->lock() };
CAView* view = handle->view();
if (!view)
continue;
Locker viewLocker { view->m_lock };
view->m_swapChain = nullptr;
view->m_d3dPostProcessingContext = nullptr;
}
lock.unlock();
}
inline CAView::CAView(DrawingDestination destination)
: m_destination(destination)
, m_handle(Handle::create(this))
, m_context(adoptCF(CACFContextCreate(0)))
{
{
Locker locker { globalStateLock };
views().add(m_handle.copyRef());
}
CARenderNotificationAddObserver(kCARenderContextDidChange, CACFContextGetRenderContext(m_context.get()), contextDidChangeCallback, this);
}
CAView::~CAView()
{
CARenderNotificationRemoveObserver(kCARenderContextDidChange, CACFContextGetRenderContext(m_context.get()), contextDidChangeCallback, this);
m_layer = nullptr;
{
Locker locker { m_lock };
m_context = nullptr;
}
// Avoid stopping the display link while we hold either m_lock or m_displayLinkLock, as doing
// so will wait for displayLinkReachedCAMediaTime to return and that function can take those
// same mutexes.
RefPtr<CVDisplayLink> linkToStop;
{
Locker locker { m_displayLinkLock };
linkToStop = WTFMove(m_displayLink);
}
if (linkToStop)
linkToStop->stop();
update(nullptr, CGRectZero);
{
Locker locker { m_handle->lock() };
m_handle->clear();
}
Locker locker { globalStateLock };
views().remove(m_handle.copyRef());
if (!views().isEmpty())
return;
::DestroyWindow(messageWindow);
messageWindow = nullptr;
}
Ref<CAView> CAView::create(DrawingDestination destination)
{
return adoptRef(*new CAView(destination));
}
void CAView::setContextDidChangeCallback(ContextDidChangeCallbackFunction function, void* info)
{
m_contextDidChangeCallback.function = function;
m_contextDidChangeCallback.info = info;
}
void CAView::setLayer(CACFLayerRef layer)
{
m_layer = layer;
if (!m_layer)
return;
CACFLayerSetFrame(m_layer.get(), m_bounds);
Locker locker { m_lock };
CACFContextSetLayer(m_context.get(), m_layer.get());
}
void CAView::update(CWindow window, const CGRect& bounds)
{
{
Locker locker { globalStateLock };
// Ensure our message window is created on the thread that called CAView::update.
if (!messageWindow)
messageWindow = createMessageWindow();
// CAView::update may only be called from a single thread, and that thread must be the same as
// the window's thread (due to limitations of the D3D API).
ASSERT(::GetCurrentThreadId() == CWindow(messageWindow).GetWindowThreadID());
ASSERT(!window || ::GetCurrentThreadId() == window.GetWindowThreadID());
viewsNeedingUpdate().remove(m_handle.copyRef());
}
bool boundsChanged;
{
Locker locker { m_lock };
boundsChanged = !CGRectEqualToRect(m_bounds, bounds);
if (m_window == window && !boundsChanged && !m_flushing && m_swapChain)
return;
m_window = window;
m_bounds = bounds;
// Invalidate the post-processing context since the window's bounds
// have changed. If needed, the post-processing context will be
// recreated with the correct bounds during the next rendering pass.
m_d3dPostProcessingContext = nullptr;
if (!m_window) {
m_swapChain = nullptr;
// If we don't have a window, we can't draw, so there's no point in having the display
// link fire.
scheduleNextDraw(std::numeric_limits<CFTimeInterval>::infinity());
} else {
// FIXME: We might be able to get better resizing performance by allocating swap chains in
// multiples of some size (say, 256x256) and only reallocating when our window size passes into
// the next size threshold.
m_swapChain = CAD3DRenderer::shared().swapChain(m_window, m_bounds.size);
if (boundsChanged) {
// Our layer's rendered appearance won't be updated until our context is next flushed.
// We shouldn't draw until that happens.
m_drawingProhibited = true;
// There's no point in allowing the display link to fire until drawing becomes
// allowed again (at which time we'll restart the display link).
scheduleNextDraw(std::numeric_limits<CFTimeInterval>::infinity());
} else if (m_destination == DrawingDestinationImage) {
// It is the caller's responsibility to ask us to draw sometime later.
scheduleNextDraw(std::numeric_limits<CFTimeInterval>::infinity());
} else {
// We should draw into the new window and/or swap chain as soon as possible.
scheduleNextDraw(0);
}
}
}
if (m_layer)
CACFLayerSetFrame(m_layer.get(), m_bounds);
if (m_window)
invalidateRects(&m_bounds, 1);
m_flushing = false;
}
void CAView::invalidateRects(const CGRect* rects, size_t count)
{
CARenderContext* renderContext = static_cast<CARenderContext*>(CACFContextGetRenderContext(m_context.get()));
CARenderContextLock(renderContext);
for (size_t i = 0; i < count; ++i)
CARenderContextInvalidateRect(renderContext, &rects[i]);
CARenderContextUnlock(renderContext);
}
void CAView::drawToWindow()
{
Locker locker { m_lock };
drawToWindowInternal();
}
void CAView::drawToWindowInternal()
{
ASSERT_WITH_MESSAGE(!m_lock.tryLock(), "m_lock must be held when calling this function");
CFTimeInterval nextDrawTime;
bool unusedWillUpdateSoon;
if (willDraw(unusedWillUpdateSoon))
didDraw(CAD3DRenderer::shared().renderAndPresent(m_bounds, m_swapChain, m_d3dPostProcessingContext.get(), m_context.get(), nextDrawTime), unusedWillUpdateSoon);
else
nextDrawTime = std::numeric_limits<CFTimeInterval>::infinity();
scheduleNextDraw(nextDrawTime);
}
RefPtr<Image> CAView::drawToImage(CGPoint& imageOrigin, CFTimeInterval& nextDrawTime)
{
ASSERT(m_destination == DrawingDestinationImage);
imageOrigin = CGPointZero;
nextDrawTime = std::numeric_limits<CFTimeInterval>::infinity();
Locker locker { m_lock };
RefPtr<Image> image;
bool willUpdateSoon;
if (willDraw(willUpdateSoon))
didDraw(CAD3DRenderer::shared().renderToImage(m_bounds, m_swapChain, m_d3dPostProcessingContext.get(), m_context.get(), image, imageOrigin, nextDrawTime), willUpdateSoon);
if (willUpdateSoon) {
// We weren't able to draw, and have scheduled an update in the near future. Our caller
// will have to call us again in order to get anything to be drawn. Ideally we'd set
// nextDrawTime to just after we update ourselves, but we don't know exactly when it will
// occur, so we just tell the caller to call us again as soon as they can.
nextDrawTime = 0;
}
return image;
}
bool CAView::willDraw(bool& willUpdateSoon)
{
ASSERT_WITH_MESSAGE(!m_lock.tryLock(), "m_lock must be held when calling this function");
willUpdateSoon = false;
if (!m_window)
return false;
if (m_drawingProhibited)
return false;
if (!m_swapChain) {
// We've lost our swap chain. This probably means the Direct3D device became lost.
updateSoon();
willUpdateSoon = true;
// We'll schedule a new draw once we've updated. Until then, there's no point in trying to draw.
return false;
}
if (m_shouldInvertColors && !m_d3dPostProcessingContext)
m_d3dPostProcessingContext = CAD3DRenderer::shared().createD3DPostProcessingContext(m_swapChain, m_bounds.size);
else if (!m_shouldInvertColors)
m_d3dPostProcessingContext = nullptr;
return true;
}
void CAView::didDraw(CAD3DRenderer::RenderResult result, bool& willUpdateSoon)
{
willUpdateSoon = false;
switch (result) {
case CAD3DRenderer::DeviceBecameLost:
// MSDN says we have to release any existing D3D resources before we try to recover a lost
// device. That includes swap chains. See
// <http://msdn.microsoft.com/en-us/library/bb174425(v=VS.85).aspx>.
releaseAllD3DResources();
updateSoon();
willUpdateSoon = true;
return;
case CAD3DRenderer::DeviceIsLost:
updateSoon();
willUpdateSoon = true;
return;
case CAD3DRenderer::OtherFailure:
case CAD3DRenderer::NoRenderingRequired:
case CAD3DRenderer::Success:
return;
}
ASSERT_NOT_REACHED();
}
void CAView::drawIntoDC(HDC dc)
{
auto memoryDC = adoptGDIObject(::CreateCompatibleDC(dc));
// Create a bitmap and select it into our memoryDC.
BITMAPINFO info = {0};
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = CGRectGetWidth(m_bounds);
info.bmiHeader.biHeight = CGRectGetHeight(m_bounds);
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
void* bits = nullptr;
auto bitmap = adoptGDIObject(::CreateDIBSection(memoryDC.get(), &info, DIB_RGB_COLORS, &bits, 0, 0));
if (!bitmap)
return;
HBITMAP oldBitmap = static_cast<HBITMAP>(::SelectObject(memoryDC.get(), bitmap.get()));
BITMAP bmp;
::GetObject(bitmap.get(), sizeof(bmp), &bmp);
// Create a software renderer.
CARenderOGLContext* renderer = CARenderOGLNew(&kCARenderSoftwareCallbacks, 0, kCARenderFlippedGeometry);
// Tell it to render into our bitmap.
if (!CARenderSoftwareSetDestination(renderer, bmp.bmBits, bmp.bmWidthBytes, bmp.bmBitsPixel, 0, 0, 0, 0, CGRectGetWidth(m_bounds), CGRectGetHeight(m_bounds))) {
::SelectObject(memoryDC.get(), oldBitmap);
CARenderOGLDestroy(renderer);
return;
}
// Prepare for rendering.
char space[4096];
CARenderUpdate* u = CARenderUpdateBegin(space, sizeof(space), CACurrentMediaTime(), 0, 0, &m_bounds);
if (!u) {
::SelectObject(memoryDC.get(), oldBitmap);
CARenderOGLDestroy(renderer);
return;
}
{
Locker locker { m_lock };
CARenderContext* renderContext = static_cast<CARenderContext*>(CACFContextGetRenderContext(m_context.get()));
CARenderContextLock(renderContext);
CARenderUpdateAddContext(u, renderContext);
CARenderContextUnlock(renderContext);
// We always want to render our entire contents.
CARenderUpdateAddRect(u, &m_bounds);
CARenderOGLRender(renderer, u);
CARenderUpdateFinish(u);
}
// Copy the rendered image into the destination DC.
::BitBlt(dc, 0, 0, CGRectGetWidth(m_bounds), CGRectGetHeight(m_bounds), memoryDC.get(), 0, 0, SRCCOPY);
// Clean up.
::SelectObject(memoryDC.get(), oldBitmap);
CARenderOGLDestroy(renderer);
}
void CAView::setShouldInvertColors(bool shouldInvertColors)
{
Locker locker { m_lock };
m_shouldInvertColors = shouldInvertColors;
}
void CAView::scheduleNextDraw(CFTimeInterval mediaTime)
{
Locker locker { m_displayLinkLock };
if (!m_context)
return;
m_nextDrawTime = mediaTime;
// We use !< here to ensure that we bail out when mediaTime is NaN.
// (Comparisons with NaN always yield false.)
if (!(m_nextDrawTime < std::numeric_limits<CFTimeInterval>::infinity())) {
if (m_displayLink)
m_displayLink->setPaused(true);
return;
}
ASSERT(m_destination == DrawingDestinationWindow);
if (!m_displayLink) {
m_displayLink = CVDisplayLink::create(this);
m_displayLink->start();
} else
m_displayLink->setPaused(false);
}
void CAView::displayLinkReachedCAMediaTime(CVDisplayLink* displayLink, CFTimeInterval time)
{
ASSERT(m_destination == DrawingDestinationWindow);
{
Locker locker { m_displayLinkLock };
if (!m_displayLink)
return;
ASSERT_UNUSED(displayLink, displayLink == m_displayLink);
if (time < m_nextDrawTime)
return;
}
Locker locker { m_lock };
drawToWindowInternal();
}
void CAView::contextDidChangeCallback(void* object, void* info, void*)
{
ASSERT_ARG(info, info);
ASSERT_ARG(object, object);
CAView* view = static_cast<CAView*>(info);
ASSERT_UNUSED(object, CACFContextGetRenderContext(view->context()) == object);
view->contextDidChange();
}
void CAView::contextDidChange()
{
{
Locker locker { m_lock };
// Our layer's rendered appearance once again matches our bounds, so it's safe to draw.
m_drawingProhibited = false;
m_flushing = true;
if (m_destination == DrawingDestinationWindow)
scheduleNextDraw(0);
}
if (m_contextDidChangeCallback.function)
m_contextDidChangeCallback.function(this, m_contextDidChangeCallback.info);
}
void CAView::updateSoon()
{
{
Locker locker { globalStateLock };
viewsNeedingUpdate().add(m_handle.copyRef());
}
// It doesn't matter what timer ID we pass here, as long as it's nonzero.
ASSERT(messageWindow);
::SetTimer(messageWindow, 1, 0, updateViewsNow);
}
void CAView::updateViewsNow(HWND window, UINT, UINT_PTR timerID, DWORD)
{
ASSERT_ARG(window, window == messageWindow);
::KillTimer(window, timerID);
HashSet<Ref<CAView::Handle>> viewsToUpdate;
{
Locker locker { globalStateLock };
viewsNeedingUpdate().swap(viewsToUpdate);
}
for (auto& handle : viewsToUpdate) {
Locker locker { handle->lock() };
CAView* view = handle->view();
if (!view)
continue;
Locker viewLocker { view->m_lock };
view->update(view->m_window, view->m_bounds);
}
}
IDirect3DDevice9* CAView::d3dDevice9()
{
// Hold the lock while we return the shared d3d device. The caller is responsible for retaining
// the device before returning to ensure that it is not released.
Locker locker { m_lock };
return CAD3DRenderer::shared().d3dDevice9();
}
} // namespace WKQCA