From 5f25544f113b8869df30e82a518f19a469291748 Mon Sep 17 00:00:00 2001 From: Callum Prentice Date: Fri, 10 Jul 2026 10:53:10 -0700 Subject: [PATCH 01/10] Update the version of CEF used (media enabled) to 150.0.11 for Windows and macOS by pulling in the updated CEF 150 autobuild package. (Linux to follow). Also update Dullahan version to 1.40.0 to disambiguate from versions with older CEFs --- autobuild.xml | 10 +++++----- src/dullahan_version.h.in | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) 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/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 From 3b0d4427bf89e384ad2099301a46924facd69fd0 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Sat, 28 Mar 2026 21:17:02 -0700 Subject: [PATCH 02/10] Add OnAcceleratedPaint support with D3D11/GL interop for GPU-resident texture sharing Enable CEF's shared texture mode and add an OnAcceleratedPaint override to dullahan_render_handler that passes the D3D11 shared texture handle through a new onAcceleratedPageChanged callback. On Windows, the opengl-example now uses WGL_NV_DX_interop2 to display the shared texture directly via GL without CPU readback. CEF's keyed-mutex shared textures are copied to a plain local D3D11 texture for interop compatibility. Mac/Linux accelerated paint paths are stubbed out. Also extracts GetParentProcess into a shared dullahan_platform_utils.h header, adds RelWithDebInfo build support with /DEBUG:FULL PDBs, and links d3d11/dxgi for the opengl-example on Windows. Co-Authored-By: Claude Opus 4.6 (1M context) --- CMakeLists.txt | 15 +- .../opengl-example/src/opengl-example.cpp | 178 +++++++++++++++++- examples/opengl-example/src/opengl-example.h | 38 +++- src/dullahan.cpp | 5 + src/dullahan.h | 11 ++ src/dullahan_callback_manager.cpp | 16 ++ src/dullahan_callback_manager.h | 4 + src/dullahan_impl.cpp | 1 + src/dullahan_platform_utils.h | 65 +++++++ src/dullahan_render_handler.cpp | 31 +++ src/dullahan_render_handler.h | 3 + src/host/dullahan_host.cpp | 54 +++++- 12 files changed, 412 insertions(+), 9 deletions(-) create mode 100644 src/dullahan_platform_utils.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f61c165..a987f8f 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,6 +202,7 @@ 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} @@ -259,6 +263,7 @@ if(IS_WINDOWS) target_include_directories( dullahan_host PUBLIC + ${CMAKE_SOURCE_DIR}/src ${CEF_INCLUDE_DIR} ${CEF_INCLUDE_DIR}/.. ) @@ -313,6 +318,7 @@ elseif(IS_MACOS) target_include_directories( ${_helper_target} PUBLIC + ${CMAKE_SOURCE_DIR}/src ${CEF_INCLUDE_DIR} ${CEF_INCLUDE_DIR}/.. ) @@ -330,6 +336,7 @@ elseif(IS_LINUX) target_include_directories( dullahan_host PUBLIC + ${CMAKE_SOURCE_DIR}/src ${CEF_INCLUDE_DIR} ${CEF_INCLUDE_DIR}/.. ) @@ -348,7 +355,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 +415,8 @@ if (BUILD_EXAMPLES) dullahan opengl32 comctl32 + d3d11 + dxgi ${CEF_LIBRARY} ${CEF_DLL_LIBRARY} ${THIRD-PARTY-PREFIX}/${GLFW_FOLDER}/${GLFW_LIB_PATH} @@ -448,7 +457,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/examples/opengl-example/src/opengl-example.cpp b/examples/opengl-example/src/opengl-example.cpp index cfac92a..2de1ff8 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 @@ -327,7 +326,45 @@ bool openglExample::init() { resizeBrowser(mTextureWidth, mTextureHeight); +#ifdef WIN32 + // Create D3D11 device for shared texture interop + ID3D11Device* baseDevice = nullptr; + D3D_FEATURE_LEVEL featureLevel; + 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(); + } + + // 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 +495,110 @@ 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; + } + + // Open the shared D3D11 texture from the NT handle + ID3D11Texture2D* sharedTexture = nullptr; + HRESULT hr = mD3DDevice->OpenSharedResource1((HANDLE)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 +615,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 +850,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..04976d9 100644 --- a/examples/opengl-example/src/opengl-example.h +++ b/examples/opengl-example/src/opengl-example.h @@ -41,12 +41,25 @@ #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 +80,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 +123,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..b3eb6e8 100644 --- a/src/dullahan.cpp +++ b/src/dullahan.cpp @@ -450,4 +450,9 @@ void dullahan::setOnJSBeforeUnloadCallback(std::function callback) void dullahan::setOnJStoCPPMsgCallback(std::function callback) { mImpl->getCallbackManager()->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..b5e16d1 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 @@ -399,6 +407,9 @@ 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 + 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..6e79e20 100644 --- a/src/dullahan_impl.cpp +++ b/src/dullahan_impl.cpp @@ -443,6 +443,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 = true; const int width = user_settings.initial_width; const int height = user_settings.initial_height; window_info.bounds = { 0, 0, width, height }; 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..45fad3d 100644 --- a/src/dullahan_render_handler.cpp +++ b/src/dullahan_render_handler.cpp @@ -33,6 +33,7 @@ #include "dullahan_impl.h" #include "dullahan_callback_manager.h" + dullahan_render_handler::dullahan_render_handler(dullahan_impl* parent) : mParent(parent) { @@ -199,4 +200,34 @@ bool dullahan_render_handler::GetScreenInfo(CefRefPtr browser, CefSc // indicate we changed the structure return true; +} + +// 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); + } + +#ifdef WIN32 + mParent->getCallbackManager()->onAcceleratedPageChanged(info.shared_texture_handle, dullahan_dirty_rects); +#elif defined(__APPLE__) || defined(__linux__) + // TODO: implement accelerated paint for this platform + (void)info; + mParent->getCallbackManager()->onAcceleratedPageChanged(nullptr, dullahan_dirty_rects); +#endif } \ No newline at end of file diff --git a/src/dullahan_render_handler.h b/src/dullahan_render_handler.h index 1fe24bf..4a153ca 100644 --- a/src/dullahan_render_handler.h +++ b/src/dullahan_render_handler.h @@ -46,6 +46,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); diff --git a/src/host/dullahan_host.cpp b/src/host/dullahan_host.cpp index 56bbaf8..8e3a459 100644 --- a/src/host/dullahan_host.cpp +++ b/src/host/dullahan_host.cpp @@ -135,7 +135,8 @@ 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 + +#include "dullahan_platform_utils.h" // 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 @@ -166,6 +167,57 @@ HANDLE GetParentProcess() return OpenProcess(SYNCHRONIZE, FALSE, ProcessEntry.th32ParentProcessID); } + +/* + 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) +{ + if (!lpCmdLine) + { + return; + } + + std::string strCmdLine = lpCmdLine; + + std::string strType = "--type=ppapi"; + std::string::size_type i = strCmdLine.find(strType); + + if (i == std::string::npos) + { + return; + } + + HANDLE hJob = CreateJobObject(nullptr, nullptr); + HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ::GetCurrentProcessId()); + + if (!AssignProcessToJobObject(hJob, hProc)) + { + ::CloseHandle(hProc); + ::CloseHandle(hJob); + return; + } + + JOBOBJECT_BASIC_LIMIT_INFORMATION baseLimits = {}; + baseLimits.LimitFlags = JOB_OBJECT_LIMIT_ACTIVE_PROCESS; + baseLimits.ActiveProcessLimit = 1; + + SetInformationJobObject(hJob, JobObjectBasicLimitInformation, &baseLimits, sizeof(baseLimits)); + + ::CloseHandle(hProc); + ::CloseHandle(hJob); +} #endif From 390be76a17f7dad3c6875d805563c119f3d05040 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Sun, 29 Mar 2026 15:32:16 -0700 Subject: [PATCH 03/10] Pass LUID adapter ID in during configuration and tell CEF to use it. --- src/dullahan.h | 9 +++++++++ src/dullahan_impl.cpp | 14 ++++++++++++++ src/dullahan_impl.h | 2 ++ 3 files changed, 25 insertions(+) diff --git a/src/dullahan.h b/src/dullahan.h index b5e16d1..f00a74c 100644 --- a/src/dullahan.h +++ b/src/dullahan.h @@ -192,6 +192,15 @@ class dullahan // host name:port to use as a web proxy std::string proxy_host_port = std::string(); + // 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; + // background color displayed before first page loaded (RRGGBB) unsigned int background_color = 0xffffff; diff --git a/src/dullahan_impl.cpp b/src/dullahan_impl.cpp index 6e79e20..71cfb8e 100644 --- a/src/dullahan_impl.cpp +++ b/src/dullahan_impl.cpp @@ -158,6 +158,11 @@ void dullahan_impl::OnBeforeCommandLineProcessing(const CefString& process_type, command_line->AppendSwitchWithValue("--proxy-server", mProxyHostPort); } + if (mUseAdapterLUID) + { + command_line->AppendSwitchWithValue("use-adapter-luid", mAdapterLUIDStr); + } + // 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 +398,15 @@ bool dullahan_impl::initCEF(dullahan::dullahan_settings& user_settings) // provide their own ("Allow, "Disallow") UI. mFakeUIForMediaStream = user_settings.fake_ui_for_media_stream; + // the adapter LUID to use for GPU rendering - pass through to CEF command line + mUseAdapterLUID = user_settings.use_adapter_luid; + if (mUseAdapterLUID) + { + int64_t luid_val = ((int64_t)user_settings.adapter_luid.high_part << 32) | + (int64_t)(uint32_t)user_settings.adapter_luid.low_part; + mAdapterLUIDStr = std::to_string(luid_val); + } + // 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; diff --git a/src/dullahan_impl.h b/src/dullahan_impl.h index a72a7fb..f033e50 100644 --- a/src/dullahan_impl.h +++ b/src/dullahan_impl.h @@ -178,6 +178,8 @@ class dullahan_impl : bool mUseMockKeyChain; bool mAutoPlayWithoutGesture; bool mFakeUIForMediaStream; + bool mUseAdapterLUID; + std::string mAdapterLUIDStr; bool mFlipPixelsY; bool mFlipMouseY; double mRequestedPageZoom; From da96276faadb6e0cb6b20acc1c4e950bf5ada20a Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Tue, 31 Mar 2026 21:01:15 -0700 Subject: [PATCH 04/10] Allow Accelerated Painting for mac and linux clients --- .../opengl-example/src/opengl-example.cpp | 85 ++++++++++++++++--- examples/opengl-example/src/opengl-example.h | 1 + src/dullahan.h | 1 + src/dullahan_impl.cpp | 10 ++- src/dullahan_render_handler.cpp | 8 +- 5 files changed, 80 insertions(+), 25 deletions(-) diff --git a/examples/opengl-example/src/opengl-example.cpp b/examples/opengl-example/src/opengl-example.cpp index 2de1ff8..14765a0 100644 --- a/examples/opengl-example/src/opengl-example.cpp +++ b/examples/opengl-example/src/opengl-example.cpp @@ -321,24 +321,73 @@ bool openglExample::init() settings.use_mock_keychain = true; #endif - bool result = mDullahan->init(settings); - if (result) - { - resizeBrowser(mTextureWidth, mTextureHeight); +#ifdef WIN32 + settings.shared_texture_enable = true; +#endif #ifdef WIN32 - // Create D3D11 device for shared texture interop - ID3D11Device* baseDevice = nullptr; - D3D_FEATURE_LEVEL featureLevel; + // 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 (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"); @@ -503,10 +552,18 @@ void openglExample::onAcceleratedPageChanged(void* handle, const std::vectorOpenSharedResource1((HANDLE)handle, __uuidof(ID3D11Texture2D), (void**)&sharedTexture); + HRESULT hr = mD3DDevice->OpenSharedResource1((HANDLE)viewer_handle, __uuidof(ID3D11Texture2D), (void**)&sharedTexture); if (FAILED(hr)) { return; diff --git a/examples/opengl-example/src/opengl-example.h b/examples/opengl-example/src/opengl-example.h index 04976d9..e254166 100644 --- a/examples/opengl-example/src/opengl-example.h +++ b/examples/opengl-example/src/opengl-example.h @@ -42,6 +42,7 @@ #include #include #include +#include #else #define GLFW_INCLUDE_NONE #include diff --git a/src/dullahan.h b/src/dullahan.h index f00a74c..e98d492 100644 --- a/src/dullahan.h +++ b/src/dullahan.h @@ -176,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 diff --git a/src/dullahan_impl.cpp b/src/dullahan_impl.cpp index 71cfb8e..f57e34d 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__ @@ -402,9 +403,10 @@ bool dullahan_impl::initCEF(dullahan::dullahan_settings& user_settings) mUseAdapterLUID = user_settings.use_adapter_luid; if (mUseAdapterLUID) { - int64_t luid_val = ((int64_t)user_settings.adapter_luid.high_part << 32) | - (int64_t)(uint32_t)user_settings.adapter_luid.low_part; - mAdapterLUIDStr = std::to_string(luid_val); + 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(); } // if true, this setting inverts the pixels in Y direction - useful if your texture @@ -457,7 +459,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 = 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 }; diff --git a/src/dullahan_render_handler.cpp b/src/dullahan_render_handler.cpp index 45fad3d..af665e1 100644 --- a/src/dullahan_render_handler.cpp +++ b/src/dullahan_render_handler.cpp @@ -223,11 +223,5 @@ void dullahan_render_handler::OnAcceleratedPaint(CefRefPtr browser, dullahan_dirty_rects.push_back(dr); } -#ifdef WIN32 mParent->getCallbackManager()->onAcceleratedPageChanged(info.shared_texture_handle, dullahan_dirty_rects); -#elif defined(__APPLE__) || defined(__linux__) - // TODO: implement accelerated paint for this platform - (void)info; - mParent->getCallbackManager()->onAcceleratedPageChanged(nullptr, dullahan_dirty_rects); -#endif -} \ No newline at end of file +} From 51bd4b44a99f2d68bd9a43652d0a4488493b4b34 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Tue, 31 Mar 2026 22:53:36 -0700 Subject: [PATCH 05/10] Use platform-specific shared texture handle in OnAcceleratedPaint Use shared_texture_io_surface on macOS instead of shared_texture_handle. Linux does not support accelerated painting and falls back to OnPaint. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dullahan_render_handler.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/dullahan_render_handler.cpp b/src/dullahan_render_handler.cpp index af665e1..023cbfc 100644 --- a/src/dullahan_render_handler.cpp +++ b/src/dullahan_render_handler.cpp @@ -223,5 +223,10 @@ void dullahan_render_handler::OnAcceleratedPaint(CefRefPtr browser, dullahan_dirty_rects.push_back(dr); } + // Dullahan for Linux does not currently support accelerated painting and will fall back to OnPaint +#ifdef WIN32 mParent->getCallbackManager()->onAcceleratedPageChanged(info.shared_texture_handle, dullahan_dirty_rects); +#elif defined(__APPLE__) + mParent->getCallbackManager()->onAcceleratedPageChanged(info.shared_texture_io_surface, dullahan_dirty_rects); +#endif } From 1b051db019296e2bff495351559c868788df144d Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Tue, 31 Mar 2026 23:27:30 -0700 Subject: [PATCH 06/10] Restrict adapter LUID settings to WIN32 only LUIDs are a Windows-specific concept used by DXGI to identify GPU adapters and have no equivalent on macOS or Linux. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dullahan.h | 2 ++ src/dullahan_impl.cpp | 4 ++++ src/dullahan_impl.h | 2 ++ 3 files changed, 8 insertions(+) diff --git a/src/dullahan.h b/src/dullahan.h index e98d492..11f0590 100644 --- a/src/dullahan.h +++ b/src/dullahan.h @@ -193,6 +193,7 @@ 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 @@ -201,6 +202,7 @@ class dullahan 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; diff --git a/src/dullahan_impl.cpp b/src/dullahan_impl.cpp index f57e34d..ea0e85b 100644 --- a/src/dullahan_impl.cpp +++ b/src/dullahan_impl.cpp @@ -159,10 +159,12 @@ 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 @@ -399,6 +401,7 @@ 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; if (mUseAdapterLUID) @@ -408,6 +411,7 @@ bool dullahan_impl::initCEF(dullahan::dullahan_settings& user_settings) << ":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 diff --git a/src/dullahan_impl.h b/src/dullahan_impl.h index f033e50..8aa3f00 100644 --- a/src/dullahan_impl.h +++ b/src/dullahan_impl.h @@ -178,8 +178,10 @@ class dullahan_impl : bool mUseMockKeyChain; bool mAutoPlayWithoutGesture; bool mFakeUIForMediaStream; +#ifdef WIN32 bool mUseAdapterLUID; std::string mAdapterLUIDStr; +#endif bool mFlipPixelsY; bool mFlipMouseY; double mRequestedPageZoom; From 94a767a00d8eef004f879f492de9e2332b1bb1ab Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Fri, 10 Jul 2026 14:59:04 -0700 Subject: [PATCH 07/10] Flip accelerated shared texture on GPU when flip_pixels_y is set Previously the accelerated (shared-texture) paint path ignored flip_pixels_y and always forwarded CEF's top-down texture handle, forcing every consumer to flip on its side. Now, when flip_pixels_y is set, dullahan performs a GPU vertical flip on Windows and hands the consumer a bottom-up texture instead. The flip is done by a new WIN32-only dullahan_shared_texture_flipper that owns a D3D11 device, a fullscreen-triangle flip pipeline, and a reusable shareable destination texture. The device and pipeline are created once; the destination is reallocated only when the incoming texture's size or format changes. Transient per-frame failures skip the frame without tearing down the device. The device is created on the configured adapter LUID (when set) so the CEF shared texture can be opened without a cross-adapter copy. COORDINATION: this is a behavior change. Consumers that flip the accelerated texture themselves must remove that flip when flip_pixels_y is enabled or the image will be flipped twice. Documented in dullahan.h on flip_pixels_y and setOnAcceleratedPageChangedCallback; new in 1.26. Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 7 + src/dullahan.h | 23 +- src/dullahan_impl.cpp | 11 + src/dullahan_impl.h | 9 + src/dullahan_render_handler.cpp | 60 +++- src/dullahan_render_handler.h | 10 + src/dullahan_shared_texture_flipper.cpp | 353 ++++++++++++++++++++++++ src/dullahan_shared_texture_flipper.h | 97 +++++++ 8 files changed, 568 insertions(+), 2 deletions(-) create mode 100644 src/dullahan_shared_texture_flipper.cpp create mode 100644 src/dullahan_shared_texture_flipper.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a987f8f..b15c0f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -209,6 +209,8 @@ add_library( 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 @@ -222,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() ############################################################################### diff --git a/src/dullahan.h b/src/dullahan.h index 11f0590..c5e2648 100644 --- a/src/dullahan.h +++ b/src/dullahan.h @@ -207,7 +207,17 @@ class dullahan // 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 @@ -420,6 +430,17 @@ 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); diff --git a/src/dullahan_impl.cpp b/src/dullahan_impl.cpp index ea0e85b..48ab407 100644 --- a/src/dullahan_impl.cpp +++ b/src/dullahan_impl.cpp @@ -404,6 +404,8 @@ bool dullahan_impl::initCEF(dullahan::dullahan_settings& user_settings) #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; @@ -554,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 8aa3f00..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); @@ -181,6 +188,8 @@ class dullahan_impl : #ifdef WIN32 bool mUseAdapterLUID; std::string mAdapterLUIDStr; + uint32_t mAdapterLUIDLow; + int32_t mAdapterLUIDHigh; #endif bool mFlipPixelsY; bool mFlipMouseY; diff --git a/src/dullahan_render_handler.cpp b/src/dullahan_render_handler.cpp index 023cbfc..d9b1673 100644 --- a/src/dullahan_render_handler.cpp +++ b/src/dullahan_render_handler.cpp @@ -33,6 +33,10 @@ #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) @@ -53,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() @@ -62,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) @@ -225,7 +238,52 @@ void dullahan_render_handler::OnAcceleratedPaint(CefRefPtr browser, // Dullahan for Linux does not currently support accelerated painting and will fall back to OnPaint #ifdef WIN32 - mParent->getCallbackManager()->onAcceleratedPageChanged(info.shared_texture_handle, dullahan_dirty_rects); + 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 4a153ca..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 : @@ -66,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..a4bd773 --- /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_KEYED_MUTEX; + + 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 From 114e704cf9ea28737ee6d14ee1e20ad958e1d9ac Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Fri, 10 Jul 2026 15:41:14 -0700 Subject: [PATCH 08/10] Fix merge error --- src/dullahan.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dullahan.cpp b/src/dullahan.cpp index b3eb6e8..0f003bf 100644 --- a/src/dullahan.cpp +++ b/src/dullahan.cpp @@ -450,6 +450,7 @@ void dullahan::setOnJSBeforeUnloadCallback(std::function callback) void dullahan::setOnJStoCPPMsgCallback(std::function callback) { mImpl->getCallbackManager()->setOnJStoCPPMsgCallback(callback); +} void dullahan::setOnAcceleratedPageChangedCallback(std::function& dirty_rects)> callback) From 85df9dc6bd10ebda3534b8cf29b733e47c7065df Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Fri, 10 Jul 2026 15:53:12 -0700 Subject: [PATCH 09/10] Fix misspelled D3D11 keyed-mutex misc flag The destination texture MiscFlags used D3D11_RESOURCE_MISC_SHARED_KEYED_MUTEX, which is not a valid identifier (C2065). The correct enum is D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX (no underscore between KEYED and MUTEX). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/dullahan_shared_texture_flipper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dullahan_shared_texture_flipper.cpp b/src/dullahan_shared_texture_flipper.cpp index a4bd773..787b6e8 100644 --- a/src/dullahan_shared_texture_flipper.cpp +++ b/src/dullahan_shared_texture_flipper.cpp @@ -215,7 +215,7 @@ bool dullahan_shared_texture_flipper::ensureDestination(uint32_t width, uint32_t 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_KEYED_MUTEX; + desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX; if (FAILED(mDevice->CreateTexture2D(&desc, nullptr, mDestTexture.GetAddressOf()))) { From acf6fd7bd806196bc3ea4a18cb539b374d0e41b0 Mon Sep 17 00:00:00 2001 From: Roxie Linden Date: Sat, 11 Jul 2026 12:30:29 -0700 Subject: [PATCH 10/10] Remove duplicate GetParentProcess in host to fix ambiguous call dullahan_host.cpp defined its own GetParentProcess() while also including dullahan_platform_utils.h, which declares an identical global GetParentProcess(DWORD = SYNCHRONIZE). A zero-argument call was viable against both overloads, so the call in WinMain() failed to compile as an ambiguous call. Drop the host's local copy and use the shared header one; behavior is unchanged (OpenProcess with SYNCHRONIZE). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/host/dullahan_host.cpp | 32 +++----------------------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/src/host/dullahan_host.cpp b/src/host/dullahan_host.cpp index 8e3a459..5ddd745 100644 --- a/src/host/dullahan_host.cpp +++ b/src/host/dullahan_host.cpp @@ -138,35 +138,9 @@ int main(int argc, char* argv[]) #include "dullahan_platform_utils.h" -// 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() -{ - 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(SYNCHRONIZE, FALSE, ProcessEntry.th32ParentProcessID); -} +// 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"