diff options
Diffstat (limited to 'widget/windows/RemoteBackbuffer.cpp')
-rw-r--r-- | widget/windows/RemoteBackbuffer.cpp | 711 |
1 files changed, 711 insertions, 0 deletions
diff --git a/widget/windows/RemoteBackbuffer.cpp b/widget/windows/RemoteBackbuffer.cpp new file mode 100644 index 0000000000..4fc857e96b --- /dev/null +++ b/widget/windows/RemoteBackbuffer.cpp @@ -0,0 +1,711 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "RemoteBackbuffer.h" +#include "GeckoProfiler.h" +#include "nsThreadUtils.h" +#include "mozilla/Span.h" +#include <algorithm> +#include <type_traits> + +namespace mozilla { +namespace widget { +namespace remote_backbuffer { + +// This number can be adjusted as a time-memory tradeoff +constexpr uint8_t kMaxDirtyRects = 8; + +struct IpcSafeRect { + explicit IpcSafeRect(const gfx::IntRect& aRect) + : x(aRect.x), y(aRect.y), width(aRect.width), height(aRect.height) {} + int32_t x; + int32_t y; + int32_t width; + int32_t height; +}; + +enum class ResponseResult { + Unknown, + Error, + BorrowSuccess, + BorrowSameBuffer, + PresentSuccess +}; + +enum class SharedDataType { + BorrowRequest, + BorrowRequestAllowSameBuffer, + BorrowResponse, + PresentRequest, + PresentResponse +}; + +struct BorrowResponseData { + ResponseResult result; + int32_t width; + int32_t height; + HANDLE fileMapping; +}; + +struct PresentRequestData { + uint8_t lenDirtyRects; + IpcSafeRect dirtyRects[kMaxDirtyRects]; +}; + +struct PresentResponseData { + ResponseResult result; +}; + +struct SharedData { + SharedDataType dataType; + union { + BorrowResponseData borrowResponse; + PresentRequestData presentRequest; + PresentResponseData presentResponse; + } data; +}; + +static_assert(std::is_trivially_copyable<SharedData>::value && + std::is_standard_layout<SharedData>::value, + "SharedData must be safe to pass over IPC boundaries"); + +class SharedImage { + public: + SharedImage() + : mWidth(0), mHeight(0), mFileMapping(nullptr), mPixelData(nullptr) {} + + ~SharedImage() { + if (mPixelData) { + MOZ_ALWAYS_TRUE(::UnmapViewOfFile(mPixelData)); + } + + if (mFileMapping) { + MOZ_ALWAYS_TRUE(::CloseHandle(mFileMapping)); + } + } + + bool Initialize(int32_t aWidth, int32_t aHeight) { + MOZ_ASSERT(aWidth > 0); + MOZ_ASSERT(aHeight > 0); + + mWidth = aWidth; + mHeight = aHeight; + + DWORD bufferSize = static_cast<DWORD>(mHeight * GetStride()); + + mFileMapping = ::CreateFileMappingW( + INVALID_HANDLE_VALUE, nullptr /*secattr*/, PAGE_READWRITE, + 0 /*sizeHigh*/, bufferSize, nullptr /*name*/); + if (!mFileMapping) { + return false; + } + + void* mappedFilePtr = + ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/, + 0 /*offsetLow*/, 0 /*bytesToMap*/); + if (!mappedFilePtr) { + return false; + } + + mPixelData = reinterpret_cast<unsigned char*>(mappedFilePtr); + + return true; + } + + bool InitializeRemote(int32_t aWidth, int32_t aHeight, HANDLE aFileMapping) { + MOZ_ASSERT(aWidth > 0); + MOZ_ASSERT(aHeight > 0); + MOZ_ASSERT(aFileMapping); + + mWidth = aWidth; + mHeight = aHeight; + mFileMapping = aFileMapping; + + void* mappedFilePtr = + ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/, + 0 /*offsetLow*/, 0 /*bytesToMap*/); + if (!mappedFilePtr) { + return false; + } + + mPixelData = reinterpret_cast<unsigned char*>(mappedFilePtr); + + return true; + } + + HBITMAP CreateDIBSection() { + BITMAPINFO bitmapInfo = {}; + bitmapInfo.bmiHeader.biSize = sizeof(bitmapInfo.bmiHeader); + bitmapInfo.bmiHeader.biWidth = mWidth; + bitmapInfo.bmiHeader.biHeight = -mHeight; + bitmapInfo.bmiHeader.biPlanes = 1; + bitmapInfo.bmiHeader.biBitCount = 32; + bitmapInfo.bmiHeader.biCompression = BI_RGB; + void* dummy = nullptr; + return ::CreateDIBSection(nullptr /*paletteDC*/, &bitmapInfo, + DIB_RGB_COLORS, &dummy, mFileMapping, + 0 /*offset*/); + } + + HANDLE CreateRemoteFileMapping(HANDLE aTargetProcess) { + MOZ_ASSERT(aTargetProcess); + + HANDLE fileMapping = nullptr; + if (!::DuplicateHandle(GetCurrentProcess(), mFileMapping, aTargetProcess, + &fileMapping, 0 /*desiredAccess*/, + FALSE /*inheritHandle*/, DUPLICATE_SAME_ACCESS)) { + return nullptr; + } + return fileMapping; + } + + already_AddRefed<gfx::DrawTarget> CreateDrawTarget() { + return gfx::Factory::CreateDrawTargetForData( + gfx::BackendType::CAIRO, mPixelData, IntSize(mWidth, mHeight), + GetStride(), gfx::SurfaceFormat::B8G8R8A8); + } + + void CopyPixelsFrom(const SharedImage& other) { + const unsigned char* src = other.mPixelData; + unsigned char* dst = mPixelData; + + int32_t width = std::min(mWidth, other.mWidth); + int32_t height = std::min(mHeight, other.mHeight); + + for (int32_t row = 0; row < height; ++row) { + memcpy(dst, src, static_cast<uint32_t>(width * kBytesPerPixel)); + src += other.GetStride(); + dst += GetStride(); + } + } + + int32_t GetWidth() { return mWidth; } + + int32_t GetHeight() { return mHeight; } + + SharedImage(const SharedImage&) = delete; + SharedImage(SharedImage&&) = delete; + SharedImage& operator=(const SharedImage&) = delete; + SharedImage& operator=(SharedImage&&) = delete; + + private: + static constexpr int32_t kBytesPerPixel = 4; + + int32_t GetStride() const { + // DIB requires 32-bit row alignment + return (((mWidth * kBytesPerPixel) + 3) / 4) * 4; + } + + int32_t mWidth; + int32_t mHeight; + HANDLE mFileMapping; + unsigned char* mPixelData; +}; + +class PresentableSharedImage { + public: + PresentableSharedImage() + : mSharedImage(), + mDeviceContext(nullptr), + mDIBSection(nullptr), + mSavedObject(nullptr) {} + + ~PresentableSharedImage() { + if (mSavedObject) { + MOZ_ALWAYS_TRUE(::SelectObject(mDeviceContext, mSavedObject)); + } + + if (mDIBSection) { + MOZ_ALWAYS_TRUE(::DeleteObject(mDIBSection)); + } + + if (mDeviceContext) { + MOZ_ALWAYS_TRUE(::DeleteDC(mDeviceContext)); + } + } + + bool Initialize(int32_t aWidth, int32_t aHeight) { + if (!mSharedImage.Initialize(aWidth, aHeight)) { + return false; + } + + mDeviceContext = ::CreateCompatibleDC(nullptr); + if (!mDeviceContext) { + return false; + } + + mDIBSection = mSharedImage.CreateDIBSection(); + if (!mDIBSection) { + return false; + } + + mSavedObject = ::SelectObject(mDeviceContext, mDIBSection); + if (!mSavedObject) { + return false; + } + + return true; + } + + bool PresentToWindow(HWND aWindowHandle, nsTransparencyMode aTransparencyMode, + Span<const IpcSafeRect> aDirtyRects) { + if (aTransparencyMode == eTransparencyTransparent) { + // If our window is a child window or a child-of-a-child, the window + // that needs to be updated is the top level ancestor of the tree + HWND topLevelWindow = WinUtils::GetTopLevelHWND(aWindowHandle, true); + MOZ_ASSERT(::GetWindowLongPtr(topLevelWindow, GWL_EXSTYLE) & + WS_EX_LAYERED); + + BLENDFUNCTION bf = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA}; + POINT srcPos = {0, 0}; + RECT clientRect = {}; + if (!::GetClientRect(aWindowHandle, &clientRect)) { + return false; + } + MOZ_ASSERT(clientRect.left == 0); + MOZ_ASSERT(clientRect.top == 0); + int32_t width = clientRect.right; + int32_t height = clientRect.bottom; + SIZE winSize = {width, height}; + // Window resize could cause the client area to be different than + // mSharedImage's size. If the client area doesn't match, + // PresentToWindow() returns false without calling UpdateLayeredWindow(). + // Another call to UpdateLayeredWindow() will follow shortly, since the + // resize will eventually force the backbuffer to repaint itself again. + // When client area is larger than mSharedImage's size, + // UpdateLayeredWindow() draws the window completely invisible. But it + // does not return false. + if (width != mSharedImage.GetWidth() || + height != mSharedImage.GetHeight()) { + return false; + } + + return !!::UpdateLayeredWindow( + topLevelWindow, nullptr /*paletteDC*/, nullptr /*newPos*/, &winSize, + mDeviceContext, &srcPos, 0 /*colorKey*/, &bf, ULW_ALPHA); + } + + IntRect sharedImageRect{0, 0, mSharedImage.GetWidth(), + mSharedImage.GetHeight()}; + + bool result = true; + + HDC windowDC = ::GetDC(aWindowHandle); + if (!windowDC) { + return false; + } + + for (auto& ipcDirtyRect : aDirtyRects) { + IntRect dirtyRect{ipcDirtyRect.x, ipcDirtyRect.y, ipcDirtyRect.width, + ipcDirtyRect.height}; + IntRect bltRect = dirtyRect.Intersect(sharedImageRect); + + if (!::BitBlt(windowDC, bltRect.x /*dstX*/, bltRect.y /*dstY*/, + bltRect.width, bltRect.height, mDeviceContext, + bltRect.x /*srcX*/, bltRect.y /*srcY*/, SRCCOPY)) { + result = false; + break; + } + } + + MOZ_ALWAYS_TRUE(::ReleaseDC(aWindowHandle, windowDC)); + + return result; + } + + HANDLE CreateRemoteFileMapping(HANDLE aTargetProcess) { + return mSharedImage.CreateRemoteFileMapping(aTargetProcess); + } + + already_AddRefed<gfx::DrawTarget> CreateDrawTarget() { + return mSharedImage.CreateDrawTarget(); + } + + void CopyPixelsFrom(const PresentableSharedImage& other) { + mSharedImage.CopyPixelsFrom(other.mSharedImage); + } + + int32_t GetWidth() { return mSharedImage.GetWidth(); } + + int32_t GetHeight() { return mSharedImage.GetHeight(); } + + PresentableSharedImage(const PresentableSharedImage&) = delete; + PresentableSharedImage(PresentableSharedImage&&) = delete; + PresentableSharedImage& operator=(const PresentableSharedImage&) = delete; + PresentableSharedImage& operator=(PresentableSharedImage&&) = delete; + + private: + SharedImage mSharedImage; + HDC mDeviceContext; + HBITMAP mDIBSection; + HGDIOBJ mSavedObject; +}; + +Provider::Provider() + : mWindowHandle(nullptr), + mTargetProcess(nullptr), + mFileMapping(nullptr), + mRequestReadyEvent(nullptr), + mResponseReadyEvent(nullptr), + mSharedDataPtr(nullptr), + mStopServiceThread(false), + mServiceThread(nullptr), + mBackbuffer() {} + +Provider::~Provider() { + mBackbuffer.reset(); + + if (mServiceThread) { + mStopServiceThread = true; + MOZ_ALWAYS_TRUE(::SetEvent(mRequestReadyEvent)); + MOZ_ALWAYS_TRUE(PR_JoinThread(mServiceThread) == PR_SUCCESS); + } + + if (mSharedDataPtr) { + MOZ_ALWAYS_TRUE(::UnmapViewOfFile(mSharedDataPtr)); + } + + if (mResponseReadyEvent) { + MOZ_ALWAYS_TRUE(::CloseHandle(mResponseReadyEvent)); + } + + if (mRequestReadyEvent) { + MOZ_ALWAYS_TRUE(::CloseHandle(mRequestReadyEvent)); + } + + if (mFileMapping) { + MOZ_ALWAYS_TRUE(::CloseHandle(mFileMapping)); + } + + if (mTargetProcess) { + MOZ_ALWAYS_TRUE(::CloseHandle(mTargetProcess)); + } +} + +bool Provider::Initialize(HWND aWindowHandle, DWORD aTargetProcessId, + nsTransparencyMode aTransparencyMode) { + MOZ_ASSERT(aWindowHandle); + MOZ_ASSERT(aTargetProcessId); + + mWindowHandle = aWindowHandle; + + mTargetProcess = ::OpenProcess(PROCESS_DUP_HANDLE, FALSE /*inheritHandle*/, + aTargetProcessId); + if (!mTargetProcess) { + return false; + } + + mFileMapping = ::CreateFileMappingW( + INVALID_HANDLE_VALUE, nullptr /*secattr*/, PAGE_READWRITE, 0 /*sizeHigh*/, + static_cast<DWORD>(sizeof(SharedData)), nullptr /*name*/); + if (!mFileMapping) { + return false; + } + + mRequestReadyEvent = + ::CreateEventW(nullptr /*secattr*/, FALSE /*manualReset*/, + FALSE /*initialState*/, nullptr /*name*/); + if (!mRequestReadyEvent) { + return false; + } + + mResponseReadyEvent = + ::CreateEventW(nullptr /*secattr*/, FALSE /*manualReset*/, + FALSE /*initialState*/, nullptr /*name*/); + if (!mResponseReadyEvent) { + return false; + } + + void* mappedFilePtr = + ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/, + 0 /*offsetLow*/, 0 /*bytesToMap*/); + if (!mappedFilePtr) { + return false; + } + + mSharedDataPtr = reinterpret_cast<SharedData*>(mappedFilePtr); + + mStopServiceThread = false; + + // Use a raw NSPR OS-level thread here instead of nsThread because we are + // performing low-level synchronization across processes using Win32 Events, + // and nsThread is designed around an incompatible "in-process task queue" + // model + mServiceThread = PR_CreateThread( + PR_USER_THREAD, [](void* p) { static_cast<Provider*>(p)->ThreadMain(); }, + this, PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, + 0 /*default stack size*/); + if (!mServiceThread) { + return false; + } + + mTransparencyMode = aTransparencyMode; + + return true; +} + +Maybe<RemoteBackbufferHandles> Provider::CreateRemoteHandles() { + return Some( + RemoteBackbufferHandles(ipc::FileDescriptor(mFileMapping), + ipc::FileDescriptor(mRequestReadyEvent), + ipc::FileDescriptor(mResponseReadyEvent))); +} + +void Provider::UpdateTransparencyMode(nsTransparencyMode aTransparencyMode) { + mTransparencyMode = aTransparencyMode; +} + +void Provider::ThreadMain() { + AUTO_PROFILER_REGISTER_THREAD("RemoteBackbuffer"); + NS_SetCurrentThreadName("RemoteBackbuffer"); + + while (true) { + { + AUTO_PROFILER_THREAD_SLEEP; + MOZ_ALWAYS_TRUE(::WaitForSingleObject(mRequestReadyEvent, INFINITE) == + WAIT_OBJECT_0); + } + + if (mStopServiceThread) { + break; + } + + switch (mSharedDataPtr->dataType) { + case SharedDataType::BorrowRequest: + case SharedDataType::BorrowRequestAllowSameBuffer: { + BorrowResponseData responseData = {}; + + HandleBorrowRequest(&responseData, + mSharedDataPtr->dataType == + SharedDataType::BorrowRequestAllowSameBuffer); + + mSharedDataPtr->dataType = SharedDataType::BorrowResponse; + mSharedDataPtr->data.borrowResponse = responseData; + + MOZ_ALWAYS_TRUE(::SetEvent(mResponseReadyEvent)); + + break; + } + case SharedDataType::PresentRequest: { + PresentRequestData requestData = mSharedDataPtr->data.presentRequest; + PresentResponseData responseData = {}; + + HandlePresentRequest(requestData, &responseData); + + mSharedDataPtr->dataType = SharedDataType::PresentResponse; + mSharedDataPtr->data.presentResponse = responseData; + + MOZ_ALWAYS_TRUE(::SetEvent(mResponseReadyEvent)); + + break; + } + default: + break; + }; + } +} + +void Provider::HandleBorrowRequest(BorrowResponseData* aResponseData, + bool aAllowSameBuffer) { + MOZ_ASSERT(aResponseData); + + aResponseData->result = ResponseResult::Error; + + RECT clientRect = {}; + if (!::GetClientRect(mWindowHandle, &clientRect)) { + return; + } + + MOZ_ASSERT(clientRect.left == 0); + MOZ_ASSERT(clientRect.top == 0); + + int32_t width = clientRect.right ? clientRect.right : 1; + int32_t height = clientRect.bottom ? clientRect.bottom : 1; + + bool needNewBackbuffer = !aAllowSameBuffer || !mBackbuffer || + (mBackbuffer->GetWidth() != width) || + (mBackbuffer->GetHeight() != height); + + if (!needNewBackbuffer) { + aResponseData->result = ResponseResult::BorrowSameBuffer; + return; + } + + auto newBackbuffer = std::make_unique<PresentableSharedImage>(); + if (!newBackbuffer->Initialize(width, height)) { + return; + } + + // Preserve the contents of the old backbuffer (if it exists) + if (mBackbuffer) { + newBackbuffer->CopyPixelsFrom(*mBackbuffer); + mBackbuffer.reset(); + } + + HANDLE remoteFileMapping = + newBackbuffer->CreateRemoteFileMapping(mTargetProcess); + if (!remoteFileMapping) { + return; + } + + aResponseData->result = ResponseResult::BorrowSuccess; + aResponseData->width = width; + aResponseData->height = height; + aResponseData->fileMapping = remoteFileMapping; + + mBackbuffer = std::move(newBackbuffer); +} + +void Provider::HandlePresentRequest(const PresentRequestData& aRequestData, + PresentResponseData* aResponseData) { + MOZ_ASSERT(aResponseData); + + Span rectSpan(aRequestData.dirtyRects, kMaxDirtyRects); + + aResponseData->result = ResponseResult::Error; + + if (!mBackbuffer) { + return; + } + + if (!mBackbuffer->PresentToWindow( + mWindowHandle, mTransparencyMode, + rectSpan.First(aRequestData.lenDirtyRects))) { + return; + } + + aResponseData->result = ResponseResult::PresentSuccess; +} + +Client::Client() + : mFileMapping(nullptr), + mRequestReadyEvent(nullptr), + mResponseReadyEvent(nullptr), + mSharedDataPtr(nullptr), + mBackbuffer() {} + +Client::~Client() { + mBackbuffer.reset(); + + if (mSharedDataPtr) { + MOZ_ALWAYS_TRUE(::UnmapViewOfFile(mSharedDataPtr)); + } + + if (mResponseReadyEvent) { + MOZ_ALWAYS_TRUE(::CloseHandle(mResponseReadyEvent)); + } + + if (mRequestReadyEvent) { + MOZ_ALWAYS_TRUE(::CloseHandle(mRequestReadyEvent)); + } + + if (mFileMapping) { + MOZ_ALWAYS_TRUE(::CloseHandle(mFileMapping)); + } +} + +bool Client::Initialize(const RemoteBackbufferHandles& aRemoteHandles) { + MOZ_ASSERT(aRemoteHandles.fileMapping().IsValid()); + MOZ_ASSERT(aRemoteHandles.requestReadyEvent().IsValid()); + MOZ_ASSERT(aRemoteHandles.responseReadyEvent().IsValid()); + + // FIXME: Due to PCompositorWidget using virtual Recv methods, + // RemoteBackbufferHandles is passed by const reference, and cannot have its + // signature customized, meaning that we need to clone the handles here. + // + // Once PCompositorWidget is migrated to use direct call semantics, the + // signature can be changed to accept RemoteBackbufferHandles by rvalue + // reference or value, and the DuplicateHandle calls here can be avoided. + mFileMapping = aRemoteHandles.fileMapping().ClonePlatformHandle().release(); + mRequestReadyEvent = + aRemoteHandles.requestReadyEvent().ClonePlatformHandle().release(); + mResponseReadyEvent = + aRemoteHandles.responseReadyEvent().ClonePlatformHandle().release(); + + void* mappedFilePtr = + ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/, + 0 /*offsetLow*/, 0 /*bytesToMap*/); + if (!mappedFilePtr) { + return false; + } + + mSharedDataPtr = reinterpret_cast<SharedData*>(mappedFilePtr); + + return true; +} + +already_AddRefed<gfx::DrawTarget> Client::BorrowDrawTarget() { + mSharedDataPtr->dataType = mBackbuffer + ? SharedDataType::BorrowRequestAllowSameBuffer + : SharedDataType::BorrowRequest; + + MOZ_ALWAYS_TRUE(::SetEvent(mRequestReadyEvent)); + MOZ_ALWAYS_TRUE(::WaitForSingleObject(mResponseReadyEvent, INFINITE) == + WAIT_OBJECT_0); + + if (mSharedDataPtr->dataType != SharedDataType::BorrowResponse) { + return nullptr; + } + + BorrowResponseData responseData = mSharedDataPtr->data.borrowResponse; + + if ((responseData.result != ResponseResult::BorrowSameBuffer) && + (responseData.result != ResponseResult::BorrowSuccess)) { + return nullptr; + } + + if (responseData.result == ResponseResult::BorrowSuccess) { + mBackbuffer.reset(); + + auto newBackbuffer = std::make_unique<SharedImage>(); + if (!newBackbuffer->InitializeRemote(responseData.width, + responseData.height, + responseData.fileMapping)) { + return nullptr; + } + + mBackbuffer = std::move(newBackbuffer); + } + + MOZ_ASSERT(mBackbuffer); + + return mBackbuffer->CreateDrawTarget(); +} + +bool Client::PresentDrawTarget(gfx::IntRegion aDirtyRegion) { + mSharedDataPtr->dataType = SharedDataType::PresentRequest; + + // Simplify the region until it has <= kMaxDirtyRects + aDirtyRegion.SimplifyOutward(kMaxDirtyRects); + + Span rectSpan(mSharedDataPtr->data.presentRequest.dirtyRects, kMaxDirtyRects); + + uint8_t rectIndex = 0; + for (auto iter = aDirtyRegion.RectIter(); !iter.Done(); iter.Next()) { + rectSpan[rectIndex] = IpcSafeRect(iter.Get()); + ++rectIndex; + } + + mSharedDataPtr->data.presentRequest.lenDirtyRects = rectIndex; + + MOZ_ALWAYS_TRUE(::SetEvent(mRequestReadyEvent)); + MOZ_ALWAYS_TRUE(::WaitForSingleObject(mResponseReadyEvent, INFINITE) == + WAIT_OBJECT_0); + + if (mSharedDataPtr->dataType != SharedDataType::PresentResponse) { + return false; + } + + if (mSharedDataPtr->data.presentResponse.result != + ResponseResult::PresentSuccess) { + return false; + } + + return true; +} + +} // namespace remote_backbuffer +} // namespace widget +} // namespace mozilla |