diff --git a/CMakeLists.txt b/CMakeLists.txt index f61c165..b15c0f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.13...4.0) if(CMAKE_CONFIGURATION_TYPES) - set(CMAKE_CONFIGURATION_TYPES "Release") + set(CMAKE_CONFIGURATION_TYPES "Release;RelWithDebInfo") endif() set(CMAKE_CXX_STANDARD 20) @@ -169,8 +169,11 @@ check_exists(CEF_DLL_LIBRARY) # 4505 "unreferenced local function has been removed" - supress meaningless freeglut warning if(IS_WINDOWS) set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -W4 -wd4100 -wd4127 -wd4505") + set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -W4 -wd4100 -wd4127 -wd4505") + set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} /DEBUG:FULL") elseif(IS_MACOS) set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -xobjective-c++") + set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -xobjective-c++") elseif(IS_LINUX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-array-bounds") endif() @@ -199,12 +202,15 @@ add_library( src/dullahan_debug.h src/dullahan_impl.cpp src/dullahan_impl.h + src/dullahan_platform_utils.h src/dullahan_version.h src/dullahan_version.h.in ${KEYBOARD_IMPL_SRC_FILE} src/dullahan_impl_mouse.cpp src/dullahan_render_handler.cpp src/dullahan_render_handler.h + src/dullahan_shared_texture_flipper.cpp + src/dullahan_shared_texture_flipper.h ) # define which include directories to pull in @@ -218,6 +224,11 @@ target_include_directories( # turn off spurious linker warnings if(IS_WINDOWS) set_target_properties(dullahan PROPERTIES LINK_FLAGS "/ignore:4099") + + # the accelerated-paint flip (dullahan_shared_texture_flipper) does its GPU + # work with D3D11/DXGI and compiles its shaders at runtime with d3dcompiler. + # PUBLIC so these propagate to anything linking the (static) dullahan lib. + target_link_libraries(dullahan PUBLIC d3d11 dxgi dxguid d3dcompiler) endif() ############################################################################### @@ -259,6 +270,7 @@ if(IS_WINDOWS) target_include_directories( dullahan_host PUBLIC + ${CMAKE_SOURCE_DIR}/src ${CEF_INCLUDE_DIR} ${CEF_INCLUDE_DIR}/.. ) @@ -313,6 +325,7 @@ elseif(IS_MACOS) target_include_directories( ${_helper_target} PUBLIC + ${CMAKE_SOURCE_DIR}/src ${CEF_INCLUDE_DIR} ${CEF_INCLUDE_DIR}/.. ) @@ -330,6 +343,7 @@ elseif(IS_LINUX) target_include_directories( dullahan_host PUBLIC + ${CMAKE_SOURCE_DIR}/src ${CEF_INCLUDE_DIR} ${CEF_INCLUDE_DIR}/.. ) @@ -348,7 +362,7 @@ if(IS_WINDOWS) add_custom_command( TARGET dullahan_host POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory - "$<$:${CEF_RELEASE_BIN_DIR}>" + "$<$,$>:${CEF_RELEASE_BIN_DIR}>" "$" COMMENT "Copying runtime files to executable directory") @@ -408,6 +422,8 @@ if (BUILD_EXAMPLES) dullahan opengl32 comctl32 + d3d11 + dxgi ${CEF_LIBRARY} ${CEF_DLL_LIBRARY} ${THIRD-PARTY-PREFIX}/${GLFW_FOLDER}/${GLFW_LIB_PATH} @@ -448,7 +464,7 @@ if (BUILD_EXAMPLES) endif() if(CMAKE_CONFIGURATION_TYPES) - set(CMAKE_CONFIGURATION_TYPES "Release") + set(CMAKE_CONFIGURATION_TYPES "Release;RelWithDebInfo") endif() # Install the Dullahan library and host executable diff --git a/autobuild.xml b/autobuild.xml index ed3aee1..1fd9f70 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -19,11 +19,11 @@ archive hash - 3218ee0b5acc5e3fa4d5e9e9e24929b9913688ad + dbf4ae3aa5627261588e1e866c942d40686ac6c8 hash_algorithm sha1 url - https://automated-builds-secondlife-com.s3.us-east-1.amazonaws.com/gh/secondlife/cef/cef_bin-148.0.9_g0d9d52a_chromium-148.0.7778.180-darwin64-261471908.tar.zst + https://automated-builds-secondlife-com.s3.us-east-1.amazonaws.com/gh/secondlife/cef/cef_bin-150.0.11_gb887805_chromium-150.0.7871.115-darwin64-261901938.tar.zst name darwin64 @@ -33,18 +33,18 @@ archive hash - f677be20cd859b2a7cf6925cf60bd6ab4829266a + 1900ee8ae7c67abf1c9ddb9fea7c540b4ac2ff6d hash_algorithm sha1 url - https://automated-builds-secondlife-com.s3.us-east-1.amazonaws.com/gh/secondlife/cef/cef_bin-148.0.9_g0d9d52a_chromium-148.0.7778.180-windows64-261470050.tar.zst + https://automated-builds-secondlife-com.s3.us-east-1.amazonaws.com/gh/secondlife/cef/cef_bin-150.0.11_gb887805_chromium-150.0.7871.115-windows64-261901728.tar.zst name windows64 version - cef_bin-148.0.9_g0d9d52a_chromium-148.0.7778.180 + cef_bin-150.0.11_gb887805_chromium-150.0.7871.115 package_description diff --git a/examples/opengl-example/src/opengl-example.cpp b/examples/opengl-example/src/opengl-example.cpp index cfac92a..14765a0 100644 --- a/examples/opengl-example/src/opengl-example.cpp +++ b/examples/opengl-example/src/opengl-example.cpp @@ -36,8 +36,6 @@ #include "opengl-example.h" -#include "dullahan.h" - void errorCallback(int error, const char* description) { std::cerr << "GLFW error: (" << error << ") - " << description << std::endl; @@ -290,8 +288,9 @@ bool openglExample::init() resizeCallback(width, height); // Texture used to display browser output on the quad + // Only allocate the name - do not bind, as wglDXRegisterObjectNV + // requires the texture object to not have existing storage glGenTextures(1, &mTextureId); - glBindTexture(GL_TEXTURE_2D, mTextureId); // Generates the picking texture - each pixel in the texture // holds the coordinates of its location for mouse picking @@ -322,12 +321,99 @@ bool openglExample::init() settings.use_mock_keychain = true; #endif +#ifdef WIN32 + settings.shared_texture_enable = true; +#endif + +#ifdef WIN32 + // Enumerate DXGI adapters to find the default GPU, create a D3D11 + // device on it, and pass its LUID to dullahan so CEF renders on + // the same adapter. + IDXGIFactory1* dxgiFactory = nullptr; + IDXGIAdapter1* dxgiAdapter = nullptr; + CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&dxgiFactory); + if (dxgiFactory) + { + // Use adapter index 1 (secondary GPU) + if (dxgiFactory->EnumAdapters1(0, &dxgiAdapter) != DXGI_ERROR_NOT_FOUND) + { + DXGI_ADAPTER_DESC1 adapterDesc; + dxgiAdapter->GetDesc1(&adapterDesc); + + settings.use_adapter_luid = true; + settings.adapter_luid.low_part = adapterDesc.AdapterLuid.LowPart; + settings.adapter_luid.high_part = adapterDesc.AdapterLuid.HighPart; + + std::cerr << "Using GPU adapter: "; + std::wcerr << adapterDesc.Description; + std::cerr << " LUID=" << adapterDesc.AdapterLuid.HighPart + << ":" << adapterDesc.AdapterLuid.LowPart << std::endl; + } + } + + // Create D3D11 device on the selected adapter for shared texture interop + ID3D11Device* baseDevice = nullptr; + D3D_FEATURE_LEVEL featureLevel; + if (dxgiAdapter) + { + D3D11CreateDevice(dxgiAdapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, + nullptr, 0, D3D11_SDK_VERSION, + &baseDevice, &featureLevel, &mD3DContext); + } + else + { + D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, + nullptr, 0, D3D11_SDK_VERSION, + &baseDevice, &featureLevel, &mD3DContext); + } + if (baseDevice) + { + baseDevice->QueryInterface(__uuidof(ID3D11Device1), (void**)&mD3DDevice); + baseDevice->Release(); + } + + if (dxgiAdapter) + { + dxgiAdapter->Release(); + } + if (dxgiFactory) + { + dxgiFactory->Release(); + } +#endif + bool result = mDullahan->init(settings); if (result) { resizeBrowser(mTextureWidth, mTextureHeight); +#ifdef WIN32 + // Load WGL_NV_DX_interop2 extension functions + wglDXOpenDeviceNV = (PFNWGLDXOPENDEVICENVPROC)wglGetProcAddress("wglDXOpenDeviceNV"); + wglDXCloseDeviceNV = (PFNWGLDXCLOSEDEVICENVPROC)wglGetProcAddress("wglDXCloseDeviceNV"); + wglDXRegisterObjectNV = (PFNWGLDXREGISTEROBJECTNVPROC)wglGetProcAddress("wglDXRegisterObjectNV"); + wglDXUnregisterObjectNV = (PFNWGLDXUNREGISTEROBJECTNVPROC)wglGetProcAddress("wglDXUnregisterObjectNV"); + wglDXLockObjectsNV = (PFNWGLDXLOCKOBJECTSNVPROC)wglGetProcAddress("wglDXLockObjectsNV"); + wglDXUnlockObjectsNV = (PFNWGLDXUNLOCKOBJECTSNVPROC)wglGetProcAddress("wglDXUnlockObjectsNV"); + + // Open D3D11 device for GL interop + if (mD3DDevice && wglDXOpenDeviceNV) + { + mInteropDevice = wglDXOpenDeviceNV(mD3DDevice); + } + + std::cerr << "D3D11/GL interop setup: " + << "D3DDevice=" << mD3DDevice + << " wglDXOpenDeviceNV=" << (void*)wglDXOpenDeviceNV + << " InteropDevice=" << mInteropDevice + << std::endl; + + mDullahan->setOnAcceleratedPageChangedCallback( + std::bind(&openglExample::onAcceleratedPageChanged, this, + std::placeholders::_1, std::placeholders::_2)); +#else mDullahan->setOnPageChangedCallback(std::bind(&openglExample::onPageChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); +#endif mDullahan->setOnRequestExitCallback(std::bind(&openglExample::onRequestExitCallback, this)); mDullahan->setOnJStoCPPMsgCallback(std::bind(&openglExample::onJStoCPPMsgCallback, this, std::placeholders::_1, std::placeholders::_2)); @@ -458,7 +544,118 @@ bool openglExample::draw(int* tx, int* ty) return hit_browser; } -// Triggered when browser page content changes +#ifdef WIN32 +// Triggered when accelerated (GPU) page content changes +void openglExample::onAcceleratedPageChanged(void* handle, const std::vector& dirty_rects) +{ + if (!handle || !mInteropDevice) + { + return; + } + HANDLE viewer_handle = 0; + DuplicateHandle( + GetCurrentProcess(), + handle, + GetCurrentProcess(), + &viewer_handle, + 0, + FALSE, + DUPLICATE_SAME_ACCESS); + // Open the shared D3D11 texture from the NT handle + ID3D11Texture2D* sharedTexture = nullptr; + HRESULT hr = mD3DDevice->OpenSharedResource1((HANDLE)viewer_handle, __uuidof(ID3D11Texture2D), (void**)&sharedTexture); + if (FAILED(hr)) + { + return; + } + + D3D11_TEXTURE2D_DESC desc; + sharedTexture->GetDesc(&desc); + + // Acquire keyed mutex if present (CEF shared textures use keyed mutexes) + IDXGIKeyedMutex* keyedMutex = nullptr; + sharedTexture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void**)&keyedMutex); + if (keyedMutex) + { + hr = keyedMutex->AcquireSync(0, 16); + if (FAILED(hr)) + { + keyedMutex->Release(); + sharedTexture->Release(); + return; + } + } + + // Recreate local texture and GL interop if dimensions changed + if ((int)desc.Width != mLocalTextureWidth || (int)desc.Height != mLocalTextureHeight) + { + // Tear down old interop + if (mInteropObject) + { + wglDXUnlockObjectsNV(mInteropDevice, 1, &mInteropObject); + wglDXUnregisterObjectNV(mInteropDevice, mInteropObject); + mInteropObject = nullptr; + } + if (mLocalTexture) + { + mLocalTexture->Release(); + mLocalTexture = nullptr; + } + + // Create a local texture without keyed mutex for GL interop + D3D11_TEXTURE2D_DESC localDesc = {}; + localDesc.Width = desc.Width; + localDesc.Height = desc.Height; + localDesc.MipLevels = 1; + localDesc.ArraySize = 1; + localDesc.Format = desc.Format; + localDesc.SampleDesc.Count = 1; + localDesc.Usage = D3D11_USAGE_DEFAULT; + localDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + mD3DDevice->CreateTexture2D(&localDesc, nullptr, &mLocalTexture); + + if (mLocalTexture) + { + mInteropObject = wglDXRegisterObjectNV(mInteropDevice, mLocalTexture, + mTextureId, GL_TEXTURE_2D, + WGL_ACCESS_READ_ONLY_NV); + } + + mLocalTextureWidth = desc.Width; + mLocalTextureHeight = desc.Height; + mTextureWidth = desc.Width; + mTextureHeight = desc.Height; + resizeBrowser(mTextureWidth, mTextureHeight); + } + + // Unlock GL access so D3D11 can write to the local texture + if (mInteropObject) + { + wglDXUnlockObjectsNV(mInteropDevice, 1, &mInteropObject); + } + + // Copy from shared texture to local texture (GPU-to-GPU) + if (mLocalTexture) + { + mD3DContext->CopyResource(mLocalTexture, sharedTexture); + } + + // Release the keyed mutex and shared texture + if (keyedMutex) + { + keyedMutex->ReleaseSync(0); + keyedMutex->Release(); + } + sharedTexture->Release(); + + // Lock the local texture for GL access + if (mInteropObject) + { + wglDXLockObjectsNV(mInteropDevice, 1, &mInteropObject); + } +} +#else +// Triggered when browser page content changes (software path) void openglExample::onPageChanged(const unsigned char* pixels, int x, int y, const int width, const int height) { if (width != mTextureWidth || height != mTextureHeight) @@ -475,6 +672,7 @@ void openglExample::onPageChanged(const unsigned char* pixels, int x, int y, con glBindTexture(GL_TEXTURE_2D, (GLuint)mTextureId); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)mTextureWidth, (GLsizei)mTextureHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels); } +#endif // Triggered by Dullahan when cleanup is complete and it's okay to exit void openglExample::onRequestExitCallback() @@ -709,6 +907,35 @@ bool openglExample::run() bool openglExample::reset() { +#ifdef WIN32 + if (mInteropObject) + { + wglDXUnlockObjectsNV(mInteropDevice, 1, &mInteropObject); + wglDXUnregisterObjectNV(mInteropDevice, mInteropObject); + mInteropObject = nullptr; + } + if (mLocalTexture) + { + mLocalTexture->Release(); + mLocalTexture = nullptr; + } + if (mInteropDevice) + { + wglDXCloseDeviceNV(mInteropDevice); + mInteropDevice = nullptr; + } + if (mD3DContext) + { + mD3DContext->Release(); + mD3DContext = nullptr; + } + if (mD3DDevice) + { + mD3DDevice->Release(); + mD3DDevice = nullptr; + } +#endif + resetUI(); glfwDestroyWindow(mWindow); diff --git a/examples/opengl-example/src/opengl-example.h b/examples/opengl-example/src/opengl-example.h index d234f07..e254166 100644 --- a/examples/opengl-example/src/opengl-example.h +++ b/examples/opengl-example/src/opengl-example.h @@ -41,12 +41,26 @@ #include #include #include +#include +#include #else #define GLFW_INCLUDE_NONE #include #endif -class dullahan; +#include "dullahan.h" + +#ifdef WIN32 +// WGL_NV_DX_interop2 function typedefs +typedef HANDLE (WINAPI *PFNWGLDXOPENDEVICENVPROC)(void*); +typedef BOOL (WINAPI *PFNWGLDXCLOSEDEVICENVPROC)(HANDLE); +typedef HANDLE (WINAPI *PFNWGLDXREGISTEROBJECTNVPROC)(HANDLE, void*, GLuint, GLenum, GLenum); +typedef BOOL (WINAPI *PFNWGLDXUNREGISTEROBJECTNVPROC)(HANDLE, HANDLE); +typedef BOOL (WINAPI *PFNWGLDXLOCKOBJECTSNVPROC)(HANDLE, GLint, HANDLE*); +typedef BOOL (WINAPI *PFNWGLDXUNLOCKOBJECTSNVPROC)(HANDLE, GLint, HANDLE*); + +#define WGL_ACCESS_READ_ONLY_NV 0x0000 +#endif class openglExample { @@ -67,7 +81,11 @@ class openglExample bool reset(); // callbacks +#ifdef WIN32 + void onAcceleratedPageChanged(void* handle, const std::vector& dirty_rects); +#else void onPageChanged(const unsigned char* pixels, int x, int y, const int width, const int height); +#endif void onRequestExitCallback(); std::string onJStoCPPMsgCallback(const std::string id, const std::string msg); @@ -106,6 +124,25 @@ class openglExample const unsigned char mBrowserId = 23; dullahan* mDullahan; +#ifdef WIN32 + // D3D11/GL interop for accelerated paint + ID3D11Device1* mD3DDevice = nullptr; + ID3D11DeviceContext* mD3DContext = nullptr; + HANDLE mInteropDevice = nullptr; + HANDLE mInteropObject = nullptr; + ID3D11Texture2D* mSharedTexture = nullptr; + ID3D11Texture2D* mLocalTexture = nullptr; + int mLocalTextureWidth = 0; + int mLocalTextureHeight = 0; + + PFNWGLDXOPENDEVICENVPROC wglDXOpenDeviceNV = nullptr; + PFNWGLDXCLOSEDEVICENVPROC wglDXCloseDeviceNV = nullptr; + PFNWGLDXREGISTEROBJECTNVPROC wglDXRegisterObjectNV = nullptr; + PFNWGLDXUNREGISTEROBJECTNVPROC wglDXUnregisterObjectNV = nullptr; + PFNWGLDXLOCKOBJECTSNVPROC wglDXLockObjectsNV = nullptr; + PFNWGLDXUNLOCKOBJECTSNVPROC wglDXUnlockObjectsNV = nullptr; +#endif + void generatePickTexture(); bool mousePosToTexturePos(int* tx, int* ty); void resizeBrowser(int width, int height); diff --git a/src/dullahan.cpp b/src/dullahan.cpp index 5b9dbe4..0f003bf 100644 --- a/src/dullahan.cpp +++ b/src/dullahan.cpp @@ -451,3 +451,9 @@ void dullahan::setOnJStoCPPMsgCallback(std::functiongetCallbackManager()->setOnJStoCPPMsgCallback(callback); } + +void dullahan::setOnAcceleratedPageChangedCallback(std::function& dirty_rects)> callback) +{ + mImpl->getCallbackManager()->setOnAcceleratedPageChangedCallback(callback); +} diff --git a/src/dullahan.h b/src/dullahan.h index 06911e5..c5e2648 100644 --- a/src/dullahan.h +++ b/src/dullahan.h @@ -130,6 +130,14 @@ class dullahan FD_SAVE_FILE, } EFileDialogType; + struct dullahan_rect + { + int x; + int y; + int width; + int height; + }; + public: //////////// initialization settings //////////// struct dullahan_settings @@ -168,6 +176,7 @@ class dullahan bool javascript_enabled = true; // javascript bool media_stream_enabled = false; // web cams etc. (caution) bool plugins_enabled = true; // all plugins + bool shared_texture_enable = false; // enable shared texture for accelerated rendering bool webgl_enabled = true; // webgl // explicitly set the path to the locales folder since defaults no longer work on some systems @@ -184,10 +193,31 @@ class dullahan // host name:port to use as a web proxy std::string proxy_host_port = std::string(); +#ifdef WIN32 + // adapter LUID to use for GPU rendering + // like adding --use-adapter-luid=VALUE to Chrome command line + // set use_adapter_luid to true and populate adapter_luid to enable + bool use_adapter_luid = false; + struct { + uint32_t low_part = 0; + int32_t high_part = 0; + } adapter_luid; +#endif + // background color displayed before first page loaded (RRGGBB) unsigned int background_color = 0xffffff; - // flip pixel buffer in Y direction + // Flip the emitted page in the Y direction so its origin is + // bottom-left (useful when your texture coordinates are upside down + // relative to Dullahan's default top-left origin). + // + // As of Dullahan 1.26 this ALSO applies to the accelerated + // (shared-texture) path on Windows: when set, Dullahan performs a + // GPU flip and the handle delivered to + // setOnAcceleratedPageChangedCallback references a bottom-up + // texture. Consumers that previously flipped the accelerated + // texture themselves MUST remove that flip when this is enabled or + // the image will be flipped twice. See the callback's comment. bool flip_pixels_y = false; // flip mouse input in Y direction @@ -399,6 +429,20 @@ class dullahan // Message from JS to CPP void setOnJStoCPPMsgCallback(std::function callback); + // accelerated (GPU) page contents change - handle is a duplicated D3D11 shared texture HANDLE on Windows + + // accelerated (GPU) page contents change - handle is a D3D11 shared texture HANDLE on Windows + // (an IOSurface on macOS). + // + // Orientation (Windows): by default the handle references CEF's own + // top-down texture (origin top-left). If flip_pixels_y was set at init, + // Dullahan instead performs a GPU flip and the handle references a + // Dullahan-owned bottom-up texture (origin bottom-left); in that case + // the consumer must NOT flip again on its side. This flipped texture is + // created with an NT shared handle and a keyed mutex (acquire/release + // key 0 around your read). New in Dullahan 1.26. + void setOnAcceleratedPageChangedCallback(std::function& dirty_rects)> callback); private: std::unique_ptr mImpl; diff --git a/src/dullahan_callback_manager.cpp b/src/dullahan_callback_manager.cpp index d350fad..88bdc3b 100644 --- a/src/dullahan_callback_manager.cpp +++ b/src/dullahan_callback_manager.cpp @@ -292,6 +292,7 @@ bool dullahan_callback_manager::onJSBeforeUnloadCallback() return false; } + void dullahan_callback_manager::setOnJStoCPPMsgCallback( std::function callback) { @@ -307,3 +308,18 @@ std::string dullahan_callback_manager::onJStoCPPMsgCallback(const std::string id return std::string(); } + +void dullahan_callback_manager::setOnAcceleratedPageChangedCallback( + std::function& dirty_rects)> callback) +{ + mOnAcceleratedPageChangedCallbackFunc = callback; +} + +void dullahan_callback_manager::onAcceleratedPageChanged(void* handle, const std::vector& dirty_rects) +{ + if (mOnAcceleratedPageChangedCallbackFunc) + { + mOnAcceleratedPageChangedCallbackFunc(handle, dirty_rects); + } +} + diff --git a/src/dullahan_callback_manager.h b/src/dullahan_callback_manager.h index 86a098f..043ca57 100644 --- a/src/dullahan_callback_manager.h +++ b/src/dullahan_callback_manager.h @@ -94,6 +94,9 @@ class dullahan_callback_manager void setOnJStoCPPMsgCallback(std::function callback); std::string onJStoCPPMsgCallback(const std::string id, const std::string msg); + void setOnAcceleratedPageChangedCallback(std::function& dirty_rects)> callback); + void onAcceleratedPageChanged(void* handle, const std::vector& dirty_rects); + private: std::function mOnAddressChangeCallbackFunc; std::function mOnConsoleMessageCallbackFunc; @@ -115,6 +118,7 @@ class dullahan_callback_manager std::function mOnJSDialogCallbackFunc; std::function mOnJSBeforeUnloadCallbackFunc; std::function mOnJStoCPPMsgCallbackFunc; + std::function&)> mOnAcceleratedPageChangedCallbackFunc; }; #endif //_DULLAHAN_CALLBACK_MANAGER diff --git a/src/dullahan_impl.cpp b/src/dullahan_impl.cpp index a5e0123..48ab407 100644 --- a/src/dullahan_impl.cpp +++ b/src/dullahan_impl.cpp @@ -38,6 +38,7 @@ #include "include/cef_request_context_handler.h" #include "include/cef_waitable_event.h" #include "include/base/cef_logging.h" +#include // Required for std::hex, std::setw, std::setfill #include "dullahan_version.h" #ifdef __APPLE__ @@ -158,6 +159,13 @@ void dullahan_impl::OnBeforeCommandLineProcessing(const CefString& process_type, command_line->AppendSwitchWithValue("--proxy-server", mProxyHostPort); } +#ifdef WIN32 + if (mUseAdapterLUID) + { + command_line->AppendSwitchWithValue("use-adapter-luid", mAdapterLUIDStr); + } +#endif + // Hardcode the switch to turn off the HTTP Basic Auth dialogs // as per this issue: https://github.com/chromiumembedded/cef/issues/3603 // Having these dialogs appear with new (139) version of the CEF is @@ -393,6 +401,20 @@ bool dullahan_impl::initCEF(dullahan::dullahan_settings& user_settings) // provide their own ("Allow, "Disallow") UI. mFakeUIForMediaStream = user_settings.fake_ui_for_media_stream; +#ifdef WIN32 + // the adapter LUID to use for GPU rendering - pass through to CEF command line + mUseAdapterLUID = user_settings.use_adapter_luid; + mAdapterLUIDLow = user_settings.adapter_luid.low_part; + mAdapterLUIDHigh = user_settings.adapter_luid.high_part; + if (mUseAdapterLUID) + { + std::ostringstream oss; + oss << "0x" << std::hex << std::setw(8) << std::setfill('0') << user_settings.adapter_luid.high_part + << ":0x" << std::hex << std::setw(8) << std::setfill('0') << user_settings.adapter_luid.low_part; + mAdapterLUIDStr = oss.str(); + } +#endif + // if true, this setting inverts the pixels in Y direction - useful if your texture // coords are upside down compared to default for Dullahan mFlipPixelsY = user_settings.flip_pixels_y; @@ -443,6 +465,7 @@ bool dullahan_impl::init(dullahan::dullahan_settings& user_settings) CefWindowInfo window_info; window_info.SetAsWindowless(0); window_info.windowless_rendering_enabled = true; + window_info.shared_texture_enabled = user_settings.shared_texture_enable; const int width = user_settings.initial_width; const int height = user_settings.initial_height; window_info.bounds = { 0, 0, width, height }; @@ -533,6 +556,15 @@ bool dullahan_impl::getFlipMouseY() return mFlipMouseY; } +#ifdef WIN32 +void dullahan_impl::getAdapterLUID(bool& use, uint32_t& low_part, int32_t& high_part) +{ + use = mUseAdapterLUID; + low_part = mAdapterLUIDLow; + high_part = mAdapterLUIDHigh; +} +#endif + void dullahan_impl::run() { CefRunMessageLoop(); diff --git a/src/dullahan_impl.h b/src/dullahan_impl.h index a72a7fb..1ae235b 100644 --- a/src/dullahan_impl.h +++ b/src/dullahan_impl.h @@ -135,6 +135,13 @@ class dullahan_impl : bool getFlipPixelsY(); bool getFlipMouseY(); +#ifdef WIN32 + // adapter LUID CEF was told to render on (if any) - used to create the + // accelerated-paint flip device on the same adapter. use is false when + // no explicit adapter was configured. + void getAdapterLUID(bool& use, uint32_t& low_part, int32_t& high_part); +#endif + void requestPageZoom(); void setCustomSchemes(std::vector custom_schemes); @@ -178,6 +185,12 @@ class dullahan_impl : bool mUseMockKeyChain; bool mAutoPlayWithoutGesture; bool mFakeUIForMediaStream; +#ifdef WIN32 + bool mUseAdapterLUID; + std::string mAdapterLUIDStr; + uint32_t mAdapterLUIDLow; + int32_t mAdapterLUIDHigh; +#endif bool mFlipPixelsY; bool mFlipMouseY; double mRequestedPageZoom; diff --git a/src/dullahan_platform_utils.h b/src/dullahan_platform_utils.h new file mode 100644 index 0000000..6c14bbf --- /dev/null +++ b/src/dullahan_platform_utils.h @@ -0,0 +1,65 @@ +/* + @brief Dullahan - a headless browser rendering engine + based around the Chromium Embedded Framework + @author Callum Prentice 2017 + + Copyright (c) 2017, Linden Research, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef _DULLAHAN_PLATFORM_UTILS +#define _DULLAHAN_PLATFORM_UTILS + +#ifdef WIN32 +#include +#include + +// Retrieves a handle to the parent process with the specified access rights. +// Taken from http://magpcss.org/ceforum/viewtopic.php?f=6&t=15817&start=10#p37820 +// Works around a CEF issue where the host process is not destroyed +// after CEF exits in some cases on Windows 7. +inline HANDLE GetParentProcess(DWORD desired_access = SYNCHRONIZE) +{ + HANDLE Snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + + PROCESSENTRY32 ProcessEntry = {}; + ProcessEntry.dwSize = sizeof(PROCESSENTRY32); + + if (Process32First(Snapshot, &ProcessEntry)) + { + DWORD CurrentProcessId = GetCurrentProcessId(); + + do + { + if (ProcessEntry.th32ProcessID == CurrentProcessId) + { + break; + } + } + while (Process32Next(Snapshot, &ProcessEntry)); + } + + CloseHandle(Snapshot); + + return OpenProcess(desired_access, FALSE, ProcessEntry.th32ParentProcessID); +} +#endif // WIN32 + +#endif // _DULLAHAN_PLATFORM_UTILS diff --git a/src/dullahan_render_handler.cpp b/src/dullahan_render_handler.cpp index 6b305a7..d9b1673 100644 --- a/src/dullahan_render_handler.cpp +++ b/src/dullahan_render_handler.cpp @@ -33,6 +33,11 @@ #include "dullahan_impl.h" #include "dullahan_callback_manager.h" +#ifdef WIN32 +#include "dullahan_shared_texture_flipper.h" +#endif + + dullahan_render_handler::dullahan_render_handler(dullahan_impl* parent) : mParent(parent) { @@ -52,6 +57,11 @@ dullahan_render_handler::dullahan_render_handler(dullahan_impl* parent) : // depth is same for all buffer mBufferDepth = parent->getDepth(); + +#ifdef WIN32 + // created on demand in OnAcceleratedPaint (only when flipping is enabled) + mSharedTextureFlipper = nullptr; +#endif } dullahan_render_handler::~dullahan_render_handler() @@ -61,6 +71,10 @@ dullahan_render_handler::~dullahan_render_handler() delete[] mPopupBuffer; delete[] mPixelBufferRow; + +#ifdef WIN32 + delete mSharedTextureFlipper; +#endif } void dullahan_render_handler::resizePixelBuffer(int width, int height) @@ -199,4 +213,78 @@ bool dullahan_render_handler::GetScreenInfo(CefRefPtr browser, CefSc // indicate we changed the structure return true; -} \ No newline at end of file +} + +// CefRenderHandler override +void dullahan_render_handler::OnAcceleratedPaint(CefRefPtr browser, + PaintElementType type, + const RectList& dirtyRects, + const CefAcceleratedPaintInfo& info) +{ + CEF_REQUIRE_UI_THREAD(); + + // convert CEF dirty rects to dullahan rects + std::vector dullahan_dirty_rects; + dullahan_dirty_rects.reserve(dirtyRects.size()); + for (const auto& rect : dirtyRects) + { + dullahan::dullahan_rect dr; + dr.x = rect.x; + dr.y = rect.y; + dr.width = rect.width; + dr.height = rect.height; + dullahan_dirty_rects.push_back(dr); + } + + // Dullahan for Linux does not currently support accelerated painting and will fall back to OnPaint +#ifdef WIN32 + void* handle = info.shared_texture_handle; + + // When flip_pixels_y is set we hand the consumer a bottom-up texture so it + // no longer has to flip on its side (see the header comment on + // setOnAcceleratedPageChangedCallback). This is a GPU blit through a device + // we own; the device, pipeline and destination texture are created once and + // reused across frames. + if (mFlipYPixels) + { + if (!mSharedTextureFlipper) + { + mSharedTextureFlipper = new dullahan_shared_texture_flipper(); + + bool use_luid = false; + uint32_t luid_low = 0; + int32_t luid_high = 0; + mParent->getAdapterLUID(use_luid, luid_low, luid_high); + + if (!mSharedTextureFlipper->init(use_luid, luid_low, luid_high)) + { + // Could not stand up the flip device (a hard failure). Fall back + // to forwarding CEF's top-down handle so the consumer still gets + // content rather than a black frame. + DLNOUT("accelerated flip disabled - could not create flip device"); + delete mSharedTextureFlipper; + mSharedTextureFlipper = nullptr; + } + } + + if (mSharedTextureFlipper) + { + void* flipped = mSharedTextureFlipper->flip(info.shared_texture_handle); + if (flipped) + { + handle = flipped; + } + else + { + // Transient flip failure (e.g. mutex timeout). Skip this frame + // rather than emit an unflipped (upside-down) texture. + return; + } + } + } + + mParent->getCallbackManager()->onAcceleratedPageChanged(handle, dullahan_dirty_rects); +#elif defined(__APPLE__) + mParent->getCallbackManager()->onAcceleratedPageChanged(info.shared_texture_io_surface, dullahan_dirty_rects); +#endif +} diff --git a/src/dullahan_render_handler.h b/src/dullahan_render_handler.h index 1fe24bf..1da7631 100644 --- a/src/dullahan_render_handler.h +++ b/src/dullahan_render_handler.h @@ -29,6 +29,10 @@ #include "cef_render_handler.h" +#ifdef WIN32 +class dullahan_shared_texture_flipper; +#endif + class dullahan_impl; class dullahan_render_handler : @@ -46,6 +50,9 @@ class dullahan_render_handler : void OnPopupShow(CefRefPtr browser, bool show) override; void OnPopupSize(CefRefPtr browser, const CefRect& rect) override; bool GetScreenInfo(CefRefPtr browser, CefScreenInfo& screen_info) override; + void OnAcceleratedPaint(CefRefPtr browser, PaintElementType type, + const RectList& dirtyRects, + const CefAcceleratedPaintInfo& info) override; IMPLEMENT_REFCOUNTING(dullahan_render_handler); @@ -63,6 +70,12 @@ class dullahan_render_handler : bool mFlipYPixels; +#ifdef WIN32 + // GPU-side vertical flip of the accelerated shared texture, created + // lazily on the first accelerated paint when mFlipYPixels is set. + dullahan_shared_texture_flipper* mSharedTextureFlipper; +#endif + dullahan_impl* mParent; }; diff --git a/src/dullahan_shared_texture_flipper.cpp b/src/dullahan_shared_texture_flipper.cpp new file mode 100644 index 0000000..787b6e8 --- /dev/null +++ b/src/dullahan_shared_texture_flipper.cpp @@ -0,0 +1,353 @@ +/* + @brief Dullahan - a headless browser rendering engine + based around the Chromium Embedded Framework + @author Roxie Linden 2026 + + Copyright (c) 2026, Linden Research, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifdef WIN32 + +#include "dullahan_shared_texture_flipper.h" + +#include "dullahan_debug.h" + +#include + +#include +#include + +// Key used for the destination keyed mutex. Producer (this class) and consumer +// both acquire/release key 0 - a single key is enough for a strict +// produce-then-consume handoff of one frame. +static const UINT64 DEST_MUTEX_KEY = 0; + +// How long the consumer-facing side is willing to wait on the source keyed +// mutex before giving up on this frame (matches the value the sample consumer +// uses on its side). +static const DWORD SOURCE_MUTEX_TIMEOUT_MS = 16; + +// Fullscreen-triangle vertex shader. Generates three vertices from the vertex +// id (no vertex/index buffer needed) that cover the whole render target, and +// emits texture coordinates whose V axis is inverted so the sampled image lands +// upside-down in the destination. +static const char* kFlipVertexShader = + "struct VSOut { float4 pos : SV_Position; float2 uv : TEXCOORD0; }; \n" + "VSOut main(uint id : SV_VertexID) \n" + "{ \n" + " VSOut o; \n" + " float2 uv = float2((id << 1) & 2, id & 2); \n" + " o.pos = float4(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.0, 1.0); \n" + " o.uv = float2(uv.x, 1.0 - uv.y); /* flip vertically */ \n" + " return o; \n" + "} \n"; + +static const char* kFlipPixelShader = + "Texture2D tex : register(t0); \n" + "SamplerState smp : register(s0); \n" + "float4 main(float4 pos : SV_Position, float2 uv : TEXCOORD0) : SV_Target\n" + "{ \n" + " return tex.Sample(smp, uv); \n" + "} \n"; + +dullahan_shared_texture_flipper::dullahan_shared_texture_flipper() : + mDestSharedHandle(nullptr), + mDestWidth(0), + mDestHeight(0), + mDestFormat(DXGI_FORMAT_UNKNOWN), + mInitialized(false) +{ +} + +dullahan_shared_texture_flipper::~dullahan_shared_texture_flipper() +{ + releaseDestination(); +} + +bool dullahan_shared_texture_flipper::init(bool use_luid, uint32_t luid_low, int32_t luid_high) +{ + if (mInitialized) + { + return true; + } + + // If we were told which adapter CEF is using, create our device on the same + // one so the shared texture can be opened directly (no cross-adapter copy). + ComPtr adapter; + if (use_luid) + { + ComPtr factory; + if (SUCCEEDED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)factory.GetAddressOf()))) + { + ComPtr candidate; + for (UINT i = 0; factory->EnumAdapters1(i, candidate.ReleaseAndGetAddressOf()) != DXGI_ERROR_NOT_FOUND; ++i) + { + DXGI_ADAPTER_DESC1 desc; + if (SUCCEEDED(candidate->GetDesc1(&desc)) && + desc.AdapterLuid.LowPart == luid_low && + desc.AdapterLuid.HighPart == luid_high) + { + candidate.As(&adapter); + break; + } + } + } + } + + // When an explicit adapter is supplied the driver type MUST be UNKNOWN. + const D3D_DRIVER_TYPE driver_type = adapter ? D3D_DRIVER_TYPE_UNKNOWN : D3D_DRIVER_TYPE_HARDWARE; + + ComPtr device; + ComPtr context; + HRESULT hr = D3D11CreateDevice(adapter.Get(), driver_type, nullptr, + D3D11_CREATE_DEVICE_BGRA_SUPPORT, + nullptr, 0, D3D11_SDK_VERSION, + device.GetAddressOf(), nullptr, context.GetAddressOf()); + if (FAILED(hr)) + { + DLNOUT("shared texture flipper: D3D11CreateDevice failed 0x" << std::hex << hr); + return false; + } + + // We need ID3D11Device1 for OpenSharedResource1 (NT-handle shared textures). + if (FAILED(device.As(&mDevice))) + { + DLNOUT("shared texture flipper: ID3D11Device1 not available"); + return false; + } + mContext = context; + + // Compile the flip pipeline once. + ComPtr vs_blob, ps_blob, errors; + hr = D3DCompile(kFlipVertexShader, strlen(kFlipVertexShader), nullptr, nullptr, nullptr, + "main", "vs_4_0", 0, 0, vs_blob.GetAddressOf(), errors.GetAddressOf()); + if (FAILED(hr)) + { + DLNOUT("shared texture flipper: vertex shader compile failed"); + return false; + } + + hr = D3DCompile(kFlipPixelShader, strlen(kFlipPixelShader), nullptr, nullptr, nullptr, + "main", "ps_4_0", 0, 0, ps_blob.ReleaseAndGetAddressOf(), errors.ReleaseAndGetAddressOf()); + if (FAILED(hr)) + { + DLNOUT("shared texture flipper: pixel shader compile failed"); + return false; + } + + if (FAILED(mDevice->CreateVertexShader(vs_blob->GetBufferPointer(), vs_blob->GetBufferSize(), + nullptr, mVertexShader.GetAddressOf())) || + FAILED(mDevice->CreatePixelShader(ps_blob->GetBufferPointer(), ps_blob->GetBufferSize(), + nullptr, mPixelShader.GetAddressOf()))) + { + DLNOUT("shared texture flipper: shader creation failed"); + return false; + } + + D3D11_SAMPLER_DESC samp = {}; + samp.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT; + samp.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + samp.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + samp.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + samp.ComparisonFunc = D3D11_COMPARISON_NEVER; + samp.MaxLOD = D3D11_FLOAT32_MAX; + if (FAILED(mDevice->CreateSamplerState(&samp, mSampler.GetAddressOf()))) + { + DLNOUT("shared texture flipper: sampler creation failed"); + return false; + } + + mInitialized = true; + return true; +} + +void dullahan_shared_texture_flipper::releaseDestination() +{ + if (mDestSharedHandle) + { + ::CloseHandle(mDestSharedHandle); + mDestSharedHandle = nullptr; + } + mDestMutex.Reset(); + mDestRTV.Reset(); + mDestTexture.Reset(); + mDestWidth = 0; + mDestHeight = 0; + mDestFormat = DXGI_FORMAT_UNKNOWN; +} + +bool dullahan_shared_texture_flipper::ensureDestination(uint32_t width, uint32_t height, DXGI_FORMAT format) +{ + // Reuse the existing destination if nothing about the surface changed. + if (mDestTexture && width == mDestWidth && height == mDestHeight && format == mDestFormat) + { + return true; + } + + releaseDestination(); + + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = format; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + // NT handle so the consumer can DuplicateHandle/OpenSharedResource1; keyed + // mutex so producer and consumer serialize access to the one frame. + desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX; + + if (FAILED(mDevice->CreateTexture2D(&desc, nullptr, mDestTexture.GetAddressOf()))) + { + DLNOUT("shared texture flipper: destination CreateTexture2D failed"); + return false; + } + + if (FAILED(mDevice->CreateRenderTargetView(mDestTexture.Get(), nullptr, mDestRTV.GetAddressOf()))) + { + DLNOUT("shared texture flipper: destination RTV creation failed"); + releaseDestination(); + return false; + } + + if (FAILED(mDestTexture.As(&mDestMutex))) + { + DLNOUT("shared texture flipper: destination keyed mutex query failed"); + releaseDestination(); + return false; + } + + ComPtr dxgi_res; + if (FAILED(mDestTexture.As(&dxgi_res)) || + FAILED(dxgi_res->CreateSharedHandle(nullptr, + DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, + nullptr, &mDestSharedHandle))) + { + DLNOUT("shared texture flipper: destination CreateSharedHandle failed"); + releaseDestination(); + return false; + } + + mDestWidth = width; + mDestHeight = height; + mDestFormat = format; + return true; +} + +void* dullahan_shared_texture_flipper::flip(void* cef_handle) +{ + if (!mInitialized || !cef_handle) + { + return nullptr; + } + + // Open CEF's shared texture. It is consumed synchronously here, within the + // OnAcceleratedPaint callback, so we do not need to duplicate the handle. + ComPtr source; + if (FAILED(mDevice->OpenSharedResource1((HANDLE)cef_handle, __uuidof(ID3D11Texture2D), + (void**)source.GetAddressOf()))) + { + // Transient - leave the device/pipeline intact and try again next frame. + return nullptr; + } + + D3D11_TEXTURE2D_DESC src_desc; + source->GetDesc(&src_desc); + + if (!ensureDestination(src_desc.Width, src_desc.Height, src_desc.Format)) + { + return nullptr; + } + + ComPtr srv; + if (FAILED(mDevice->CreateShaderResourceView(source.Get(), nullptr, srv.GetAddressOf()))) + { + return nullptr; + } + + // CEF documents its texture as having no keyed mutex, but older/other + // configurations may still provide one - acquire it if it is there. + ComPtr src_mutex; + bool src_locked = false; + if (SUCCEEDED(source.As(&src_mutex))) + { + if (SUCCEEDED(src_mutex->AcquireSync(DEST_MUTEX_KEY, SOURCE_MUTEX_TIMEOUT_MS))) + { + src_locked = true; + } + else + { + // Could not get the frame in time - skip it, keep everything alive. + return nullptr; + } + } + + // Take the destination for writing. + if (FAILED(mDestMutex->AcquireSync(DEST_MUTEX_KEY, SOURCE_MUTEX_TIMEOUT_MS))) + { + if (src_locked) + { + src_mutex->ReleaseSync(DEST_MUTEX_KEY); + } + return nullptr; + } + + // Draw the source into the destination, flipped in Y by the vertex shader. + D3D11_VIEWPORT vp = {}; + vp.Width = (float)mDestWidth; + vp.Height = (float)mDestHeight; + vp.MaxDepth = 1.0f; + + ID3D11RenderTargetView* rtv = mDestRTV.Get(); + ID3D11ShaderResourceView* srv_raw = srv.Get(); + ID3D11SamplerState* samp_raw = mSampler.Get(); + + mContext->OMSetRenderTargets(1, &rtv, nullptr); + mContext->RSSetViewports(1, &vp); + mContext->IASetInputLayout(nullptr); + mContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + mContext->VSSetShader(mVertexShader.Get(), nullptr, 0); + mContext->PSSetShader(mPixelShader.Get(), nullptr, 0); + mContext->PSSetShaderResources(0, 1, &srv_raw); + mContext->PSSetSamplers(0, 1, &samp_raw); + mContext->Draw(3, 0); + + // Unbind the source SRV before we release the mutexes so the runtime does + // not hold a reference to a resource we are about to hand back. + ID3D11ShaderResourceView* null_srv = nullptr; + mContext->PSSetShaderResources(0, 1, &null_srv); + ID3D11RenderTargetView* null_rtv = nullptr; + mContext->OMSetRenderTargets(1, &null_rtv, nullptr); + + mContext->Flush(); + + mDestMutex->ReleaseSync(DEST_MUTEX_KEY); + if (src_locked) + { + src_mutex->ReleaseSync(DEST_MUTEX_KEY); + } + + return mDestSharedHandle; +} + +#endif // WIN32 diff --git a/src/dullahan_shared_texture_flipper.h b/src/dullahan_shared_texture_flipper.h new file mode 100644 index 0000000..72d4fb8 --- /dev/null +++ b/src/dullahan_shared_texture_flipper.h @@ -0,0 +1,97 @@ +/* + @brief Dullahan - a headless browser rendering engine + based around the Chromium Embedded Framework + @author Roxie Linden 2026 + + Copyright (c) 2026, Linden Research, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef _DULLAHAN_SHARED_TEXTURE_FLIPPER +#define _DULLAHAN_SHARED_TEXTURE_FLIPPER + +// This helper only exists on Windows where accelerated paint hands us a +// D3D11 shared texture. On other platforms the accelerated handle is passed +// through unchanged (see dullahan_render_handler::OnAcceleratedPaint). +#ifdef WIN32 + +#include + +#include +#include + +// Vertically flips the D3D11 shared texture that CEF emits from +// OnAcceleratedPaint and republishes the result as a new shared texture whose +// origin is bottom-left (matching what OpenGL-style consumers expect when +// flip_pixels_y is set). +// +// The GPU device, the flip pipeline (shaders + sampler) and the destination +// texture are created ONCE and reused for every frame. The destination is only +// reallocated when the incoming texture's width, height or format changes. A +// transient per-frame failure (e.g. a keyed-mutex timeout) returns null but +// leaves the device and pipeline intact so the next frame can try again. +class dullahan_shared_texture_flipper +{ + public: + dullahan_shared_texture_flipper(); + ~dullahan_shared_texture_flipper(); + + // Create the device and fixed pipeline. When use_luid is true the + // device is created on the adapter identified by (luid_low, luid_high) + // so it matches the adapter CEF renders on and the shared texture can + // be opened without a cross-adapter copy. Returns false on failure. + bool init(bool use_luid, uint32_t luid_low, int32_t luid_high); + + // Flip the CEF shared texture referenced by cef_handle and return a + // shareable NT handle to the flipped copy, or nullptr on transient + // failure. The returned handle is owned by this object and remains + // valid until the next flip() that resizes the destination or until + // this object is destroyed. + void* flip(void* cef_handle); + + private: + template using ComPtr = Microsoft::WRL::ComPtr; + + // (re)create the destination texture, its render target view and its + // shared handle when the requested size/format differs from the cache. + bool ensureDestination(uint32_t width, uint32_t height, DXGI_FORMAT format); + void releaseDestination(); + + ComPtr mDevice; + ComPtr mContext; + ComPtr mVertexShader; + ComPtr mPixelShader; + ComPtr mSampler; + + // reused destination - owned by us and shared with the consumer + ComPtr mDestTexture; + ComPtr mDestRTV; + ComPtr mDestMutex; + HANDLE mDestSharedHandle; + uint32_t mDestWidth; + uint32_t mDestHeight; + DXGI_FORMAT mDestFormat; + + bool mInitialized; +}; + +#endif // WIN32 + +#endif // _DULLAHAN_SHARED_TEXTURE_FLIPPER diff --git a/src/dullahan_version.h.in b/src/dullahan_version.h.in index 7f5bf62..5625ffd 100644 --- a/src/dullahan_version.h.in +++ b/src/dullahan_version.h.in @@ -37,7 +37,7 @@ // version of this package #define DULLAHAN_VERSION_MAJOR 1 -#define DULLAHAN_VERSION_MINOR 35 +#define DULLAHAN_VERSION_MINOR 40 #define DULLAHAN_VERSION_POINT 0 // The build version number as of v1.2 is now the date/time the build was made diff --git a/src/host/dullahan_host.cpp b/src/host/dullahan_host.cpp index 56bbaf8..5ddd745 100644 --- a/src/host/dullahan_host.cpp +++ b/src/host/dullahan_host.cpp @@ -135,36 +135,62 @@ int main(int argc, char* argv[]) // Ignore c:\program files (x86)\microsoft visual studio 12.0\vc\include\thr\xthread(196): warning C4702: unreachable code #pragma warning( disable : 4702) #include -#include -// taken from http://magpcss.org/ceforum/viewtopic.php?f=6&t=15817&start=10#p37820 -// works around a CEF issue (yet to be filed) where the host process is not destroyed -// after CEF exits in some case on Windows 7 -// Making it switchable for now while I investigate it a bit -HANDLE GetParentProcess() +#include "dullahan_platform_utils.h" + +// GetParentProcess() is provided by dullahan_platform_utils.h (included above). +// It used to be duplicated here, which made the call in WinMain() ambiguous +// against the header's version, so the local copy has been removed. + +/* + Nasty hack to stop flash from displaying a popup with "NO SANDBOX" + Flashplayer will try to spawn a cmd.exe and echo this message into it, we + use a process group to limit the number of processes allowed to 1, thus preventing + popup. + + Limitation: NeedsWindows 8 or higher, the viewer already does put SLPlugin (and with that + all sub processes) into a job, so all plugin instances get killed when the viewer does exit. + Anything before Windows 8 will not allow a process being part of more than one job. + + Using the sandbox would fix this problem, but for using the sandbox the same executable + must be used for browser and all sub processes (see cef_sandbox_win.h); but the viewer + uses slplugin.exe and llceflib_host.exe. +*/ +void enablePPAPIFlashHack(LPSTR lpCmdLine) { - HANDLE Snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (!lpCmdLine) + { + return; + } - PROCESSENTRY32 ProcessEntry = {}; - ProcessEntry.dwSize = sizeof(PROCESSENTRY32); + std::string strCmdLine = lpCmdLine; - if (Process32First(Snapshot, &ProcessEntry)) + std::string strType = "--type=ppapi"; + std::string::size_type i = strCmdLine.find(strType); + + if (i == std::string::npos) { - DWORD CurrentProcessId = GetCurrentProcessId(); + return; + } - do - { - if (ProcessEntry.th32ProcessID == CurrentProcessId) - { - break; - } - } - while (Process32Next(Snapshot, &ProcessEntry)); + HANDLE hJob = CreateJobObject(nullptr, nullptr); + HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ::GetCurrentProcessId()); + + if (!AssignProcessToJobObject(hJob, hProc)) + { + ::CloseHandle(hProc); + ::CloseHandle(hJob); + return; } - CloseHandle(Snapshot); + JOBOBJECT_BASIC_LIMIT_INFORMATION baseLimits = {}; + baseLimits.LimitFlags = JOB_OBJECT_LIMIT_ACTIVE_PROCESS; + baseLimits.ActiveProcessLimit = 1; + + SetInformationJobObject(hJob, JobObjectBasicLimitInformation, &baseLimits, sizeof(baseLimits)); - return OpenProcess(SYNCHRONIZE, FALSE, ProcessEntry.th32ParentProcessID); + ::CloseHandle(hProc); + ::CloseHandle(hJob); } #endif