From 8a11b3ce0b5610845a37487acbb5c5713598a9f7 Mon Sep 17 00:00:00 2001 From: Lpsd <40902730+Lpsd@users.noreply.github.com> Date: Fri, 24 May 2024 10:35:23 +0100 Subject: [PATCH 01/12] mbedTLS fix for cURL 8.8.0 https://github.com/curl/curl/issues/13748 --- vendor/curl/lib/config-linux.h | 2 +- vendor/curl/lib/config-macos.h | 2 +- vendor/curl/lib/vtls/mbedtls.c | 21 +++++++++++++-------- vendor/curl/premake5.lua | 3 +++ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/vendor/curl/lib/config-linux.h b/vendor/curl/lib/config-linux.h index 02a1306ed00..b0084d92dd5 100644 --- a/vendor/curl/lib/config-linux.h +++ b/vendor/curl/lib/config-linux.h @@ -869,7 +869,7 @@ /* #undef USE_MANUAL */ /* if mbedTLS is enabled */ -/* #undef USE_MBEDTLS */ +#define USE_MBEDTLS 1 /* if msh3 is in use */ /* #undef USE_MSH3 */ diff --git a/vendor/curl/lib/config-macos.h b/vendor/curl/lib/config-macos.h index bf5733fccac..c46b5ec014d 100644 --- a/vendor/curl/lib/config-macos.h +++ b/vendor/curl/lib/config-macos.h @@ -869,7 +869,7 @@ /* #undef USE_MANUAL */ /* if mbedTLS is enabled */ -/* #undef USE_MBEDTLS */ +#define USE_MBEDTLS 1 /* if msh3 is in use */ /* #undef USE_MSH3 */ diff --git a/vendor/curl/lib/vtls/mbedtls.c b/vendor/curl/lib/vtls/mbedtls.c index ec0b10dd9a9..1197d2956de 100644 --- a/vendor/curl/lib/vtls/mbedtls.c +++ b/vendor/curl/lib/vtls/mbedtls.c @@ -902,8 +902,6 @@ mbed_connect_step2(struct Curl_cfilter *cf, struct Curl_easy *data) (struct mbed_ssl_backend_data *)connssl->backend; struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf); const mbedtls_x509_crt *peercert; - char cipher_str[64]; - uint16_t cipher_id; #ifndef CURL_DISABLE_PROXY const char * const pinnedpubkey = Curl_ssl_cf_is_proxy(cf)? data->set.str[STRING_SSL_PINNEDPUBLICKEY_PROXY]: @@ -932,11 +930,18 @@ mbed_connect_step2(struct Curl_cfilter *cf, struct Curl_easy *data) return CURLE_SSL_CONNECT_ERROR; } - cipher_id = (uint16_t) - mbedtls_ssl_get_ciphersuite_id_from_ssl(&backend->ssl); - mbed_cipher_suite_get_str(cipher_id, cipher_str, sizeof(cipher_str), true); - infof(data, "mbedTLS: Handshake complete, cipher is %s", cipher_str); - +#if MBEDTLS_VERSION_NUMBER >= 0x03020000 + { + char cipher_str[64]; + uint16_t cipher_id; + cipher_id = (uint16_t) + mbedtls_ssl_get_ciphersuite_id_from_ssl(&backend->ssl); + mbed_cipher_suite_get_str(cipher_id, cipher_str, sizeof(cipher_str), true); + infof(data, "mbedTLS: Handshake complete, cipher is %s", cipher_str); + } +#else + infof(data, "mbedTLS: Handshake complete"); +#endif ret = mbedtls_ssl_get_verify_result(&backend->ssl); if(!conn_config->verifyhost) @@ -1506,4 +1511,4 @@ const struct Curl_ssl Curl_ssl_mbedtls = { mbed_send, /* send data to encrypt */ }; -#endif /* USE_MBEDTLS */ +#endif /* USE_MBEDTLS */ \ No newline at end of file diff --git a/vendor/curl/premake5.lua b/vendor/curl/premake5.lua index d86088f267c..b3ac6a517f7 100644 --- a/vendor/curl/premake5.lua +++ b/vendor/curl/premake5.lua @@ -32,6 +32,9 @@ project "curl" defines { "USE_SCHANNEL", "USE_WINDOWS_SSPI", "USE_WIN32_IDN" } links { "crypt32", "Normaliz" } + filter { "system:not windows" } + defines { "USE_MBEDTLS" } + filter { "system:linux or bsd or macosx" } defines { "CURL_HIDDEN_SYMBOLS" } From ee4475e1c8577727b1f7918c3d83d8da39bf1d8c Mon Sep 17 00:00:00 2001 From: lopsi <40902730+Lpsd@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:31:45 +0000 Subject: [PATCH 02/12] External begin frame scheduling --- Client/cefweb/CWebCore.cpp | 69 +++++++----- Client/cefweb/CWebView.cpp | 219 ++++++++----------------------------- Client/cefweb/CWebView.h | 17 +-- 3 files changed, 93 insertions(+), 212 deletions(-) diff --git a/Client/cefweb/CWebCore.cpp b/Client/cefweb/CWebCore.cpp index dc017e1d7a7..988734ff1a4 100644 --- a/Client/cefweb/CWebCore.cpp +++ b/Client/cefweb/CWebCore.cpp @@ -77,12 +77,12 @@ bool CWebCore::Initialise(bool gpuEnabled) // CefInitialize() can only be called once per process lifetime // Do not call this function again or recreate CWebCore if initialization fails // Repeated calls cause "Timeout of new browser info response for frame" errors - + m_bGPUEnabled = gpuEnabled; // Get MTA base directory SString strBaseDir = SharedUtil::GetMTAProcessBaseDir(); - + if (strBaseDir.empty()) { g_pCore->GetConsole()->Printf("CEF initialization skipped - Unable to determine MTA base directory"); @@ -90,15 +90,15 @@ bool CWebCore::Initialise(bool gpuEnabled) m_bInitialised = false; return false; } - + SString strMTADir = PathJoin(strBaseDir, "MTA"); - + #ifndef MTA_DEBUG SString strLauncherPath = PathJoin(strMTADir, "CEF", "CEFLauncher.exe"); #else SString strLauncherPath = PathJoin(strMTADir, "CEF", "CEFLauncher_d.exe"); #endif - + // Set DLL directory for CEFLauncher subprocess to locate required libraries SString strCEFDir = PathJoin(strMTADir, "CEF"); #ifdef _WIN32 @@ -110,23 +110,23 @@ bool CWebCore::Initialise(bool gpuEnabled) if (existingPath) { newPath = SString("%s:%s", strCEFDir.c_str(), existingPath); } - // Note: setenv is not available in MSVC, but _putenv is. + // Note: setenv is not available in MSVC, but _putenv is. // However, since we are compiling for Windows (running on Wine), we use Windows APIs. // Wine maps Windows environment variables. // But LD_LIBRARY_PATH is a Linux variable. // If we are in Wine, we might want to set PATH instead or as well. // SetDllDirectoryW handles the Windows loader. - + // Log for debugging if (std::getenv("WINE") || std::getenv("WINEPREFIX")) { g_pCore->GetConsole()->Printf("DEBUG: CEF library path set via SetDllDirectoryW: %s", strCEFDir.c_str()); } #endif - + // Read GTA path from registry to pass to CEF subprocess int iRegistryResult = 0; const SString strGTAPath = GetCommonRegistryValue("", "GTA:SA Path", &iRegistryResult); - + // Check if process is running with elevated privileges // CEF subprocesses may have communication issues when running elevated const bool bIsElevated = []() -> bool { @@ -140,18 +140,18 @@ bool CWebCore::Initialise(bool gpuEnabled) HANDLE hToken = nullptr; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) return false; - + // RAII wrapper for token handle const std::unique_ptr tokenGuard(hToken, &CloseHandle); - + TOKEN_ELEVATION elevation{}; DWORD dwSize = sizeof(elevation); if (!GetTokenInformation(hToken, TokenElevation, &elevation, sizeof(elevation), &dwSize)) return false; - + return elevation.TokenIsElevated != 0; }(); - + if (bIsElevated && !std::getenv("WINE")) { AddReportLog(8021, "WARNING: Process is running with elevated privileges (Administrator)"); @@ -159,7 +159,7 @@ bool CWebCore::Initialise(bool gpuEnabled) AddReportLog(8023, "Consider running MTA without Administrator privileges for full browser functionality"); g_pCore->GetConsole()->Printf("WARNING: Running as Administrator - browser features may be limited"); } - + // Verify CEFLauncher can run in current environment auto CanExecuteCEFLauncher = []() -> bool { #ifdef _WIN32 @@ -167,7 +167,7 @@ bool CWebCore::Initialise(bool gpuEnabled) if (!std::getenv("WINE") && !std::getenv("WINEPREFIX") && !std::getenv("PROTON_VERSION")) return true; #endif - + // Check if Wine can execute the launcher // This is a basic check - if we are in Wine, we assume it works unless proven otherwise // But we can log if we are in a mixed environment @@ -192,7 +192,7 @@ bool CWebCore::Initialise(bool gpuEnabled) // Ensure cache directory can be created const SString strCachePath = PathJoin(strMTADir, "CEF", "cache"); MakeSureDirExists(strCachePath); - + // Verify locales directory exists const SString strLocalesPath = PathJoin(strMTADir, "CEF", "locales"); if (!DirectoryExists(strLocalesPath)) @@ -215,7 +215,7 @@ bool CWebCore::Initialise(bool gpuEnabled) m_bInitialised = false; return false; } - + // RAII scope guard to restore CWD, even if CefInitialize throws or returns early struct CwdGuard { fs::path savedPath; @@ -225,7 +225,7 @@ bool CWebCore::Initialise(bool gpuEnabled) fs::current_path(savedPath, restoreEc); } } cwdGuard(savedCwd); - + // Temporarily change CWD to MTA directory for CefInitialize // CEFLauncher.exe requires this to locate CEF dependencies fs::current_path(fs::path(FromUTF8(strMTADir)), ec); @@ -235,7 +235,7 @@ bool CWebCore::Initialise(bool gpuEnabled) m_bInitialised = false; return false; } - + CefMainArgs mainArgs; void* sandboxInfo = nullptr; @@ -278,7 +278,7 @@ bool CWebCore::Initialise(bool gpuEnabled) } // CWD will be restored by cwdGuard destructor when this function returns - + if (m_bInitialised) { // Register custom scheme handler factory only if initialization succeeded @@ -290,7 +290,7 @@ bool CWebCore::Initialise(bool gpuEnabled) g_pCore->GetConsole()->Printf("CefInitialize failed - CEF features will be disabled"); AddReportLog(8004, "CefInitialize failed - CEF features will be disabled"); } - + return m_bInitialised; } @@ -315,18 +315,18 @@ void CWebCore::DestroyWebView(CWebViewInterface* pWebViewInterface) { // Mark as being destroyed to prevent new events/tasks pWebView->SetBeingDestroyed(true); - + // Ensure that no attached events or tasks are in the queue RemoveWebViewEvents(pWebView.get()); RemoveWebViewTasks(pWebView.get()); // Remove from list before closing to break reference cycles early m_WebViews.remove(pWebView); - + // CloseBrowser will eventually trigger OnBeforeClose which clears m_pWebView // This breaks the circular reference: CWebView -> CefBrowser -> CWebView pWebView->CloseBrowser(); - + // Note: Do not call Release() - let CefRefPtr manage the lifecycle // The circular reference is broken via OnBeforeClose setting m_pWebView = nullptr } @@ -382,7 +382,7 @@ void CWebCore::AddEventToEventQueue(std::function event, CWebView* pWebV { // Log warning even in release builds as this indicates a serious issue g_pCore->GetConsole()->Printf("WARNING: Browser event queue size limit reached (%d), dropping oldest events", MAX_EVENT_QUEUE_SIZE); - + // Remove oldest 10% of events to make room auto removeCount = static_cast(MAX_EVENT_QUEUE_SIZE / 10); for (auto i = size_t{0}; i < removeCount && !m_EventQueue.empty(); ++i) @@ -426,7 +426,20 @@ void CWebCore::DoEventQueuePulse() event.callback(); } - // Invoke paint method if necessary on the main thread + // Request new frames from CEF using external begin frame scheduling + // This synchronizes CEF rendering with MTA's render loop, eliminating + // the previous 250ms blocking wait in OnPaint + for (auto& view : m_WebViews) + { + if (view->IsBeingDestroyed() || view->GetRenderingPaused()) + continue; + + auto browser = view->GetCefBrowser(); + if (browser) + browser->GetHost()->SendExternalBeginFrame(); + } + + // Copy rendered data to D3D textures on the main thread for (auto& view : m_WebViews) { view->UpdateTexture(); @@ -444,7 +457,7 @@ void CWebCore::WaitForTask(std::function task, CWebView* webView) std::future result; { std::scoped_lock lock(m_TaskQueueMutex); - + // Prevent unbounded queue growth - abort new task if queue is too large if (m_TaskQueue.size() >= MAX_TASK_QUEUE_SIZE) [[unlikely]] { @@ -457,7 +470,7 @@ void CWebCore::WaitForTask(std::function task, CWebView* webView) task(true); return; } - + m_TaskQueue.emplace_back(TaskEntry{task, webView}); result = m_TaskQueue.back().task.get_future(); } diff --git a/Client/cefweb/CWebView.cpp b/Client/cefweb/CWebView.cpp index 76368819971..48a084da24b 100644 --- a/Client/cefweb/CWebView.cpp +++ b/Client/cefweb/CWebView.cpp @@ -13,7 +13,6 @@ #include #include #include "CWebDevTools.h" -#include #include "CWebViewAuth.h" // AUTH: IPC validation helpers #include @@ -62,9 +61,6 @@ CWebView::~CWebView() m_pWebBrowserRenderItem = nullptr; } - // Make sure we don't dead lock the CEF render thread - ResumeCefThread(); - // Clean up AJAX handlers to prevent accumulation m_AjaxHandlers.clear(); @@ -149,6 +145,9 @@ void CWebView::Initialise() CefWindowInfo windowInfo; windowInfo.SetAsWindowless(g_pCore->GetHookedWindow()); + // Enable external begin frame scheduling - allows MTA to control when CEF renders + windowInfo.external_begin_frame_enabled = true; + CefBrowserHost::CreateBrowser(windowInfo, this, "", browserSettings, nullptr, nullptr); } @@ -157,9 +156,6 @@ void CWebView::CloseBrowser() // CefBrowserHost::CloseBrowser calls the destructor after the browser has been destroyed m_bBeingDestroyed = true; - // Make sure we don't dead lock the CEF render thread - ResumeCefThread(); - // Clear AJAX handlers early to prevent late event processing m_AjaxHandlers.clear(); @@ -268,14 +264,11 @@ void CWebView::SetRenderingPaused(bool bPaused) std::lock_guard lock{m_RenderData.dataMutex}; m_RenderData.changed = false; m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; + m_RenderData.buffer.reset(); + m_RenderData.bufferSize = 0; m_RenderData.dirtyRects.clear(); m_RenderData.dirtyRects.shrink_to_fit(); m_RenderData.popupBuffer.reset(); - - // Release any waiting CEF thread - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); } } } @@ -293,7 +286,7 @@ void CWebView::Focus(bool state) auto pWebCore = g_pCore->GetWebCore(); if (!pWebCore) return; - + if (state) pWebCore->SetFocusedWebView(this); else if (pWebCore->GetFocusedWebView() == this) @@ -304,7 +297,7 @@ void CWebView::ClearTexture() { if (!m_pWebBrowserRenderItem) [[unlikely]] return; - + auto* const pD3DSurface = m_pWebBrowserRenderItem->m_pD3DRenderTargetSurface; if (!pD3DSurface) [[unlikely]] return; @@ -318,7 +311,7 @@ void CWebView::ClearTexture() { // Check for integer overflow in size calculation: height * pitch must fit in size_t // Ensure both are positive and that multiplication won't overflow - if (SurfaceDesc.Height > 0 && LockedRect.Pitch > 0 && + if (SurfaceDesc.Height > 0 && LockedRect.Pitch > 0 && static_cast(SurfaceDesc.Height) <= SIZE_MAX / static_cast(LockedRect.Pitch)) [[likely]] { const auto memsetSize = static_cast(SurfaceDesc.Height) * static_cast(LockedRect.Pitch); @@ -336,11 +329,6 @@ void CWebView::UpdateTexture() if (!m_pWebBrowserRenderItem) [[unlikely]] { m_RenderData.changed = m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } @@ -348,11 +336,6 @@ void CWebView::UpdateTexture() if (m_bBeingDestroyed || !pSurface) [[unlikely]] { m_RenderData.changed = m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } @@ -361,9 +344,6 @@ void CWebView::UpdateTexture() if (m_RenderData.changed && (m_pWebBrowserRenderItem->m_uiSizeX != m_RenderData.width || m_pWebBrowserRenderItem->m_uiSizeY != m_RenderData.height)) { m_RenderData.changed = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); } if (m_RenderData.changed || m_RenderData.popupShown) [[likely]] @@ -372,9 +352,8 @@ void CWebView::UpdateTexture() D3DLOCKED_RECT LockedRect; if (SUCCEEDED(pSurface->LockRect(&LockedRect, nullptr, 0))) { - // Dirty rect implementation, don't use this as loops are significantly slower than memcpy auto* const destData = static_cast(LockedRect.pBits); - const auto* const sourceData = static_cast(m_RenderData.buffer); + const auto* const sourceData = m_RenderData.buffer.get(); const auto destPitch = LockedRect.Pitch; // Validate destination pitch @@ -383,14 +362,9 @@ void CWebView::UpdateTexture() pSurface->UnlockRect(); m_RenderData.changed = false; m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } - + // Validate sourcePitch calculation won't overflow constexpr auto maxWidthForPitch = INT_MAX / CEF_PIXEL_STRIDE; if (m_RenderData.width > maxWidthForPitch) [[unlikely]] @@ -398,11 +372,6 @@ void CWebView::UpdateTexture() pSurface->UnlockRect(); m_RenderData.changed = false; m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } const auto sourcePitch = m_RenderData.width * CEF_PIXEL_STRIDE; @@ -413,79 +382,54 @@ void CWebView::UpdateTexture() pSurface->UnlockRect(); m_RenderData.changed = false; m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } // Update view area if (m_RenderData.changed) [[likely]] { - // Update changed state m_RenderData.changed = false; const auto& dirtyRects = m_RenderData.dirtyRects; if (!dirtyRects.empty() && dirtyRects[0].width == m_RenderData.width && dirtyRects[0].height == m_RenderData.height) { - // Note that D3D texture size can be hardware dependent(especially with dynamic texture) - // When destination and source pitches differ we must copy pixels row by row + // Full frame update - copy entire buffer if (destPitch == sourcePitch) [[likely]] { - // Check for integer overflow in size calculation: height * pitch must fit in size_t - if (m_RenderData.height > 0 && + if (m_RenderData.height > 0 && static_cast(m_RenderData.height) > SIZE_MAX / static_cast(destPitch)) [[unlikely]] { pSurface->UnlockRect(); m_RenderData.changed = false; m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } std::memcpy(destData, sourceData, static_cast(destPitch) * static_cast(m_RenderData.height)); } else { - // Ensure both pitches are positive before row-by-row copy + // Row-by-row copy when pitches differ if (destPitch <= 0 || sourcePitch <= 0) [[unlikely]] { pSurface->UnlockRect(); m_RenderData.changed = false; m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } - // Check for integer overflow in size calculation for row-by-row copy - if (m_RenderData.height > 0 && + if (m_RenderData.height > 0 && (static_cast(m_RenderData.height) > SIZE_MAX / static_cast(destPitch) || static_cast(m_RenderData.height) > SIZE_MAX / static_cast(sourcePitch))) [[unlikely]] { pSurface->UnlockRect(); m_RenderData.changed = false; m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } - + for (int y = 0; y < m_RenderData.height; ++y) { - // Use size_t for all calculations to prevent overflow const auto sourceIndex = static_cast(y) * static_cast(sourcePitch); const auto destIndex = static_cast(y) * static_cast(destPitch); const auto copySize = std::min(static_cast(sourcePitch), static_cast(destPitch)); @@ -496,50 +440,36 @@ void CWebView::UpdateTexture() } else { - // Check for integer overflow in destination size calculation - if (m_RenderData.height > 0 && + // Partial update using dirty rects + if (m_RenderData.height > 0 && static_cast(m_RenderData.height) > SIZE_MAX / static_cast(destPitch)) [[unlikely]] { pSurface->UnlockRect(); m_RenderData.changed = false; m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } - // Update dirty rects for (const auto& rect : dirtyRects) { - // Validate dirty rect bounds to prevent buffer overflow if (rect.x < 0 || rect.y < 0 || rect.width <= 0 || rect.height <= 0) [[unlikely]] continue; - - // Check bounds using addition to prevent subtraction underflow - // rect.x + rect.width could overflow, so check rect.x and rect.width separately + if (rect.x >= m_RenderData.width || rect.y >= m_RenderData.height || rect.width > m_RenderData.width || rect.height > m_RenderData.height || rect.x > m_RenderData.width - rect.width || rect.y > m_RenderData.height - rect.height) [[unlikely]] continue; - // Pre-calculate end to prevent overflow in loop condition const auto rectEndY = rect.y + rect.height; - // Ensure we don't write past the destination pitch if (static_cast(destPitch) < static_cast(rect.x + rect.width) * CEF_PIXEL_STRIDE) [[unlikely]] continue; for (int y = rect.y; y < rectEndY; ++y) { - // Note that D3D texture size can be hardware dependent(especially with dynamic texture) - // We cannot be sure that source and destination pitches are the same - // Use size_t for all calculations to prevent integer overflow - const auto sourceIndex = static_cast(y) * static_cast(sourcePitch) + + const auto sourceIndex = static_cast(y) * static_cast(sourcePitch) + static_cast(rect.x) * CEF_PIXEL_STRIDE; - const auto destIndex = static_cast(y) * static_cast(destPitch) + + const auto destIndex = static_cast(y) * static_cast(destPitch) + static_cast(rect.x) * CEF_PIXEL_STRIDE; std::memcpy(&destData[destIndex], &sourceData[sourceIndex], static_cast(rect.width) * CEF_PIXEL_STRIDE); @@ -548,12 +478,11 @@ void CWebView::UpdateTexture() } } - // Update popup area (override certain areas of the view texture) - // Validate popup rect bounds to prevent integer overflow and out-of-bounds access + // Update popup area const auto& popupRect = m_RenderData.popupRect; const auto renderWidth = static_cast(m_pWebBrowserRenderItem->m_uiSizeX); const auto renderHeight = static_cast(m_pWebBrowserRenderItem->m_uiSizeY); - const auto popupSizeMismatches = + const auto popupSizeMismatches = popupRect.x < 0 || popupRect.y < 0 || popupRect.width <= 0 || popupRect.height <= 0 || popupRect.x >= renderWidth || popupRect.y >= renderHeight || @@ -561,71 +490,48 @@ void CWebView::UpdateTexture() popupRect.x > renderWidth - popupRect.width || popupRect.y > renderHeight - popupRect.height; - // Verify popup buffer exists before accessing it if (m_RenderData.popupShown && !popupSizeMismatches && m_RenderData.popupBuffer) [[likely]] { - // Validate popup pitch calculation won't overflow constexpr auto maxWidthForPopupPitch = INT_MAX / CEF_PIXEL_STRIDE; if (popupRect.width > maxWidthForPopupPitch) [[unlikely]] { pSurface->UnlockRect(); m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } const auto popupPitch = popupRect.width * CEF_PIXEL_STRIDE; - // Ensure we don't write past the destination pitch if (static_cast(destPitch) < static_cast(popupRect.x + popupRect.width) * CEF_PIXEL_STRIDE) [[unlikely]] { pSurface->UnlockRect(); m_RenderData.popupShown = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } - + for (int y = 0; y < popupRect.height; ++y) { - // Use size_t for all calculations to prevent integer overflow const auto sourceIndex = static_cast(y) * static_cast(popupPitch); - // Calculate destination y coordinate safely const auto destY = static_cast(popupRect.y) + static_cast(y); - const auto destIndex = destY * static_cast(destPitch) + + const auto destIndex = destY * static_cast(destPitch) + static_cast(popupRect.x) * CEF_PIXEL_STRIDE; std::memcpy(&destData[destIndex], &m_RenderData.popupBuffer[sourceIndex], static_cast(popupPitch)); } } - // Unlock surface pSurface->UnlockRect(); } else { OutputDebugLine("[CWebView] UpdateTexture: LockRect failed"); - // Clear flags to prevent re-attempting to render stale buffer m_RenderData.changed = false; m_RenderData.popupShown = false; } - - // Clear buffer pointer - it's only valid during OnPaint callback and we've used it - m_RenderData.buffer = nullptr; - - // Clear dirty rects and release capacity to prevent memory accumulation + + // Clear dirty rects to prevent memory accumulation m_RenderData.dirtyRects.clear(); m_RenderData.dirtyRects.shrink_to_fit(); } - - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); } void CWebView::ExecuteJavascript(const SString& strJavascriptCode) @@ -775,7 +681,7 @@ void CWebView::GetSourceCode(const std::function& // Check if webview is being destroyed to prevent UAF if (webView->IsBeingDestroyed()) return; - + // Limit to 2MiB for now to prevent freezes (TODO: Optimize that and increase later) if (code.size() <= 2097152) { @@ -796,22 +702,20 @@ void CWebView::Resize(const CVector2D& size) // Validate render item exists if (!m_pWebBrowserRenderItem) [[unlikely]] return; - + // Resize underlying texture m_pWebBrowserRenderItem->Resize(size); // Send resize event to CEF if (m_pWebView) m_pWebView->GetHost()->WasResized(); - - ResumeCefThread(); } CVector2D CWebView::GetSize() { if (!m_pWebBrowserRenderItem) [[unlikely]] return CVector2D(0.0f, 0.0f); - + return CVector2D(static_cast(m_pWebBrowserRenderItem->m_uiSizeX), static_cast(m_pWebBrowserRenderItem->m_uiSizeY)); } @@ -1065,9 +969,9 @@ void CWebView::OnPaint(CefRefPtr browser, CefRenderHandler::PaintEle return; // Individual dimension too large if (static_cast(width) > SIZE_MAX / (static_cast(height) * CEF_PIXEL_STRIDE)) [[unlikely]] return; // width * height * stride would overflow - + const auto requiredSize = static_cast(width) * static_cast(height) * CEF_PIXEL_STRIDE; - + // Calculate current size safely to avoid overflow size_t currentSize = 0; const auto& popupRect = m_RenderData.popupRect; @@ -1075,10 +979,10 @@ void CWebView::OnPaint(CefRefPtr browser, CefRenderHandler::PaintEle popupRect.width <= maxDimension && popupRect.height <= maxDimension && static_cast(popupRect.width) <= SIZE_MAX / (static_cast(popupRect.height) * CEF_PIXEL_STRIDE)) [[likely]] { - currentSize = static_cast(popupRect.width) * + currentSize = static_cast(popupRect.width) * static_cast(popupRect.height) * CEF_PIXEL_STRIDE; } - + // Reallocate if size changed or buffer doesn't exist if (!m_RenderData.popupBuffer || requiredSize != currentSize) [[unlikely]] { @@ -1087,10 +991,9 @@ void CWebView::OnPaint(CefRefPtr browser, CefRenderHandler::PaintEle m_RenderData.popupRect.width = width; m_RenderData.popupRect.height = height; } - + std::memcpy(m_RenderData.popupBuffer.get(), buffer, requiredSize); - // Popup path doesn't wait, so no need to signal return; } @@ -1098,11 +1001,6 @@ void CWebView::OnPaint(CefRefPtr browser, CefRenderHandler::PaintEle if (!buffer || width <= 0 || height <= 0) [[unlikely]] { m_RenderData.changed = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } @@ -1111,45 +1009,32 @@ void CWebView::OnPaint(CefRefPtr browser, CefRenderHandler::PaintEle if (width > maxDimension || height > maxDimension) [[unlikely]] { m_RenderData.changed = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } + + const auto requiredSize = static_cast(width) * static_cast(height) * CEF_PIXEL_STRIDE; if (static_cast(width) > SIZE_MAX / (static_cast(height) * CEF_PIXEL_STRIDE)) [[unlikely]] { m_RenderData.changed = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); return; } - // Store render data - m_RenderData.buffer = buffer; + // Allocate or reallocate buffer if size changed + if (!m_RenderData.buffer || m_RenderData.bufferSize != requiredSize) [[unlikely]] + { + m_RenderData.buffer = std::make_unique(requiredSize); + m_RenderData.bufferSize = requiredSize; + } + + // Copy the buffer immediately - with external_begin_frame_enabled, we control timing + // so we copy here rather than storing a pointer and blocking + std::memcpy(m_RenderData.buffer.get(), buffer, requiredSize); + m_RenderData.width = width; m_RenderData.height = height; m_RenderData.dirtyRects = dirtyRects; - // Prevent vector capacity growth memory leak - shrink excess capacity m_RenderData.dirtyRects.shrink_to_fit(); m_RenderData.changed = true; - - // Wait for the main thread to handle drawing the texture - m_RenderData.cefThreadState = ECefThreadState::Wait; - if (!m_RenderData.cefThreadCv.wait_for(lock, std::chrono::milliseconds(250), [&]() { return m_RenderData.cefThreadState == ECefThreadState::Running; })) - { - // Timed out - rendering is likely stalled or stopped - // Clear data to prevent UpdateTexture from using stale buffer and allow CEF to free it - m_RenderData.changed = false; - m_RenderData.buffer = nullptr; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); - m_RenderData.cefThreadState = ECefThreadState::Running; - } } //////////////////////////////////////////////////////////////////// @@ -1554,13 +1439,3 @@ void CWebView::OnBeforeContextMenu(CefRefPtr browser, CefRefPtrClear(); } -void CWebView::ResumeCefThread() -{ - { - // It's recommended to unlock a mutex before the cv notifying to avoid a possible pessimization - std::unique_lock lock(m_RenderData.dataMutex); - m_RenderData.cefThreadState = ECefThreadState::Running; - } - - m_RenderData.cefThreadCv.notify_all(); -} diff --git a/Client/cefweb/CWebView.h b/Client/cefweb/CWebView.h index 6bf2374257f..2f96479e847 100644 --- a/Client/cefweb/CWebView.h +++ b/Client/cefweb/CWebView.h @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -43,12 +42,6 @@ namespace WebViewAuth bool HandleInputFocus(CWebView*, CefRefPtr, const bool); } -enum class ECefThreadState -{ - Running = 0, // CEF thread is currently running - Wait // CEF thread is waiting for the main thread -}; - class CWebView : public CWebViewInterface, private CefClient, private CefRenderHandler, @@ -199,7 +192,6 @@ class CWebView : public CWebViewInterface, CefRefPtr model) override; private: - void ResumeCefThread(); void QueueBrowserEvent(const char* name, std::function&& fn); struct FEventTarget @@ -275,11 +267,12 @@ class CWebView : public CWebViewInterface, { bool changed = false; std::mutex dataMutex; - ECefThreadState cefThreadState = ECefThreadState::Running; - std::condition_variable cefThreadCv; - const void* buffer; - int width, height; + // Main frame buffer - we now own this buffer (copied in OnPaint) + std::unique_ptr buffer; + size_t bufferSize = 0; + int width = 0; + int height = 0; CefRenderHandler::RectList dirtyRects; CefRect popupRect; From d728302d716dc3607168795594c707cf725c29a0 Mon Sep 17 00:00:00 2001 From: lopsi <40902730+Lpsd@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:48:22 +0000 Subject: [PATCH 03/12] Optimize dirty rect handling --- Client/cefweb/CWebView.cpp | 124 +++++++++++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 4 deletions(-) diff --git a/Client/cefweb/CWebView.cpp b/Client/cefweb/CWebView.cpp index 48a084da24b..cdd45cd8fc5 100644 --- a/Client/cefweb/CWebView.cpp +++ b/Client/cefweb/CWebView.cpp @@ -15,10 +15,111 @@ #include "CWebDevTools.h" #include "CWebViewAuth.h" // AUTH: IPC validation helpers #include +#include namespace { const int CEF_PIXEL_STRIDE = 4; + + // Threshold for switching from dirty rect updates to full frame copy + // If dirty area exceeds this fraction of total area, full copy is more efficient + constexpr float DIRTY_RECT_THRESHOLD = 0.25f; + + // Calculate total area covered by dirty rects (accounting for overlaps approximately) + size_t CalculateDirtyArea(const CefRenderHandler::RectList& dirtyRects, int frameWidth, int frameHeight) + { + if (dirtyRects.empty()) + return 0; + + // For a single rect, just return its area + if (dirtyRects.size() == 1) + { + const auto& rect = dirtyRects[0]; + return static_cast(rect.width) * static_cast(rect.height); + } + + // For multiple rects, calculate bounding box area as upper bound estimate + // This is faster than precise overlap calculation and gives good results + int minX = frameWidth, minY = frameHeight; + int maxX = 0, maxY = 0; + size_t totalRectArea = 0; + + for (const auto& rect : dirtyRects) + { + if (rect.width <= 0 || rect.height <= 0) + continue; + + minX = std::min(minX, rect.x); + minY = std::min(minY, rect.y); + maxX = std::max(maxX, rect.x + rect.width); + maxY = std::max(maxY, rect.y + rect.height); + totalRectArea += static_cast(rect.width) * static_cast(rect.height); + } + + // Use the smaller of: sum of rect areas, or bounding box area + // This gives a reasonable estimate without expensive overlap calculation + const size_t boundingArea = static_cast(maxX - minX) * static_cast(maxY - minY); + return std::min(totalRectArea, boundingArea); + } + + // Check if two rects are adjacent or overlapping (can be merged) + bool RectsAreAdjacent(const CefRect& a, const CefRect& b, int tolerance = 1) + { + // Check if rects overlap or touch within tolerance + return !(a.x + a.width + tolerance < b.x || + b.x + b.width + tolerance < a.x || + a.y + a.height + tolerance < b.y || + b.y + b.height + tolerance < a.y); + } + + // Merge two rects into their bounding box + CefRect MergeRects(const CefRect& a, const CefRect& b) + { + const int minX = std::min(a.x, b.x); + const int minY = std::min(a.y, b.y); + const int maxX = std::max(a.x + a.width, b.x + b.width); + const int maxY = std::max(a.y + a.height, b.y + b.height); + return CefRect(minX, minY, maxX - minX, maxY - minY); + } + + // Merge adjacent dirty rects to reduce number of copy operations + // Returns optimized list with fewer, larger rects + std::vector MergeAdjacentRects(const CefRenderHandler::RectList& dirtyRects) + { + if (dirtyRects.size() <= 1) + return std::vector(dirtyRects.begin(), dirtyRects.end()); + + std::vector merged(dirtyRects.begin(), dirtyRects.end()); + + // Simple greedy merge - keep merging until no more merges possible + // Limited iterations to prevent excessive processing + constexpr int maxIterations = 3; + for (int iter = 0; iter < maxIterations; ++iter) + { + bool mergedAny = false; + + for (size_t i = 0; i < merged.size() && merged.size() > 1; ++i) + { + for (size_t j = i + 1; j < merged.size(); ++j) + { + if (RectsAreAdjacent(merged[i], merged[j])) + { + merged[i] = MergeRects(merged[i], merged[j]); + merged.erase(merged.begin() + j); + mergedAny = true; + break; + } + } + if (mergedAny) + break; + } + + if (!mergedAny) + break; + } + + return merged; + } } CWebView::CWebView(bool bIsLocal, CWebBrowserItem* pWebBrowserRenderItem, bool bTransparent) @@ -391,8 +492,20 @@ void CWebView::UpdateTexture() m_RenderData.changed = false; const auto& dirtyRects = m_RenderData.dirtyRects; - if (!dirtyRects.empty() && dirtyRects[0].width == m_RenderData.width && - dirtyRects[0].height == m_RenderData.height) + const auto frameArea = static_cast(m_RenderData.width) * static_cast(m_RenderData.height); + + // Determine if we should do full frame copy or partial dirty rect update + // Full copy is more efficient when dirty area exceeds threshold due to fewer memcpy calls + bool doFullCopy = dirtyRects.empty() || + (dirtyRects.size() == 1 && dirtyRects[0].width == m_RenderData.width && dirtyRects[0].height == m_RenderData.height); + + if (!doFullCopy && frameArea > 0) + { + const auto dirtyArea = CalculateDirtyArea(dirtyRects, m_RenderData.width, m_RenderData.height); + doFullCopy = (static_cast(dirtyArea) / static_cast(frameArea)) > DIRTY_RECT_THRESHOLD; + } + + if (doFullCopy) { // Full frame update - copy entire buffer if (destPitch == sourcePitch) [[likely]] @@ -440,7 +553,7 @@ void CWebView::UpdateTexture() } else { - // Partial update using dirty rects + // Partial update using optimized dirty rects if (m_RenderData.height > 0 && static_cast(m_RenderData.height) > SIZE_MAX / static_cast(destPitch)) [[unlikely]] { @@ -450,7 +563,10 @@ void CWebView::UpdateTexture() return; } - for (const auto& rect : dirtyRects) + // Merge adjacent rects to reduce number of copy operations + const auto mergedRects = MergeAdjacentRects(dirtyRects); + + for (const auto& rect : mergedRects) { if (rect.x < 0 || rect.y < 0 || rect.width <= 0 || rect.height <= 0) [[unlikely]] continue; From a84d11c63dd96446ffc0a4164c13279c7a2c7de2 Mon Sep 17 00:00:00 2001 From: lopsi <40902730+Lpsd@users.noreply.github.com> Date: Tue, 13 Jan 2026 19:54:20 +0000 Subject: [PATCH 04/12] Use D3DPOOL_DEFAULT with D3DUSAGE_DYNAMIC --- Client/cefweb/CWebView.cpp | 5 ++-- .../core/Graphics/CRenderItem.WebBrowser.cpp | 24 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Client/cefweb/CWebView.cpp b/Client/cefweb/CWebView.cpp index cdd45cd8fc5..d9c66e3c6c5 100644 --- a/Client/cefweb/CWebView.cpp +++ b/Client/cefweb/CWebView.cpp @@ -449,9 +449,10 @@ void CWebView::UpdateTexture() if (m_RenderData.changed || m_RenderData.popupShown) [[likely]] { - // Lock surface + // Lock surface with D3DLOCK_DISCARD for dynamic textures - tells driver we'll overwrite entire content + // This avoids GPU stalls waiting for previous frame to finish rendering D3DLOCKED_RECT LockedRect; - if (SUCCEEDED(pSurface->LockRect(&LockedRect, nullptr, 0))) + if (SUCCEEDED(pSurface->LockRect(&LockedRect, nullptr, D3DLOCK_DISCARD))) { auto* const destData = static_cast(LockedRect.pBits); const auto* const sourceData = m_RenderData.buffer.get(); diff --git a/Client/core/Graphics/CRenderItem.WebBrowser.cpp b/Client/core/Graphics/CRenderItem.WebBrowser.cpp index bfa4e246907..0ab32bfef72 100644 --- a/Client/core/Graphics/CRenderItem.WebBrowser.cpp +++ b/Client/core/Graphics/CRenderItem.WebBrowser.cpp @@ -57,22 +57,24 @@ bool CWebBrowserItem::IsValid() // // CWebBrowserItem::OnLostDevice // -// Release device stuff +// Release device stuff - D3DPOOL_DEFAULT textures must be released // //////////////////////////////////////////////////////////////// void CWebBrowserItem::OnLostDevice() { + ReleaseUnderlyingData(); } //////////////////////////////////////////////////////////////// // // CWebBrowserItem::OnResetDevice // -// Recreate device stuff +// Recreate device stuff - D3DPOOL_DEFAULT textures must be recreated // //////////////////////////////////////////////////////////////// void CWebBrowserItem::OnResetDevice() { + CreateUnderlyingData(); } //////////////////////////////////////////////////////////////// @@ -87,8 +89,12 @@ void CWebBrowserItem::CreateUnderlyingData() assert(!m_pD3DRenderTargetSurface); assert(!m_pD3DTexture); - // Check if texture is actually created. It can be failed in some conditions(e.g. lack of memory). - if (FAILED(D3DXCreateTexture(m_pDevice, m_uiSizeX, m_uiSizeY, 1, 0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, (IDirect3DTexture9**)&m_pD3DTexture)) || + // Use D3DPOOL_DEFAULT with D3DUSAGE_DYNAMIC for better performance: + // - Dynamic textures are optimized for frequent Lock/Unlock operations + // - D3DLOCK_DISCARD can be used effectively to avoid stalls + // - No system memory copy (unlike D3DPOOL_MANAGED), reducing memory usage + // Note: Must handle device lost/reset as DEFAULT pool textures don't survive resets + if (FAILED(m_pDevice->CreateTexture(m_uiSizeX, m_uiSizeY, 1, D3DUSAGE_DYNAMIC, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, (IDirect3DTexture9**)&m_pD3DTexture, nullptr)) || !m_pD3DTexture) return; @@ -99,15 +105,7 @@ void CWebBrowserItem::CreateUnderlyingData() return; } - // D3DXCreateTexture sets width and height to 1 if the argument value was 0 - // See: https://docs.microsoft.com/en-us/windows/desktop/direct3d9/d3dxcreatetexture - if (m_uiSizeX == 0) - m_uiSizeX = 1; - - if (m_uiSizeY == 0) - m_uiSizeY = 1; - - // Update surface size, although it probably will be unchanged | Todo: Remove this + // Update surface size - dynamic textures may have different dimensions than requested D3DSURFACE_DESC desc; m_pD3DRenderTargetSurface->GetDesc(&desc); m_uiSurfaceSizeX = desc.Width; From ab0bcbf9ff65ed38a4d87e29a6d9c2f3c1cfc472 Mon Sep 17 00:00:00 2001 From: lopsi <40902730+Lpsd@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:09:57 +0000 Subject: [PATCH 05/12] Browser lazy loading --- Client/cefweb/CWebView.cpp | 15 +++++++++++++++ Client/cefweb/CWebView.h | 2 ++ 2 files changed, 17 insertions(+) diff --git a/Client/cefweb/CWebView.cpp b/Client/cefweb/CWebView.cpp index d9c66e3c6c5..e2f8ca4940b 100644 --- a/Client/cefweb/CWebView.cpp +++ b/Client/cefweb/CWebView.cpp @@ -225,6 +225,16 @@ void CWebView::QueueBrowserEvent(const char* name, std::functionGetFPSLimiter()->GetFPSTarget(); @@ -250,6 +260,8 @@ void CWebView::Initialise() windowInfo.external_begin_frame_enabled = true; CefBrowserHost::CreateBrowser(windowInfo, this, "", browserSettings, nullptr, nullptr); + m_bBrowserCreated = true; + return true; } void CWebView::CloseBrowser() @@ -280,6 +292,9 @@ void CWebView::CloseBrowser() bool CWebView::LoadURL(const SString& strURL, bool bFilterEnabled, const SString& strPostData, bool bURLEncoded) { + // Lazy creation: create browser on first use + EnsureBrowserCreated(); + if (!m_pWebView) return false; diff --git a/Client/cefweb/CWebView.h b/Client/cefweb/CWebView.h index 2f96479e847..c466cf0a36d 100644 --- a/Client/cefweb/CWebView.h +++ b/Client/cefweb/CWebView.h @@ -64,6 +64,7 @@ class CWebView : public CWebViewInterface, void SetWebBrowserEvents(CWebBrowserEventsInterface* pInterface); void ClearWebBrowserEvents(CWebBrowserEventsInterface* pInterface); void CloseBrowser(); + bool EnsureBrowserCreated(); // Lazy creation: creates browser on first use CefRefPtr GetCefBrowser() { return m_pWebView; }; bool IsBeingDestroyed() { return m_bBeingDestroyed; } @@ -254,6 +255,7 @@ class CWebView : public CWebViewInterface, bool m_bIsLocal; bool m_bIsRenderingPaused; bool m_bIsTransparent; + bool m_bBrowserCreated = false; // Lazy creation: tracks if CEF browser has been created POINT m_vecMousePosition; bool m_mouseButtonStates[3]; SString m_CurrentTitle; From 7d5c788489bf772d9b3cac3d0e5ae048faa90ea2 Mon Sep 17 00:00:00 2001 From: lopsi <40902730+Lpsd@users.noreply.github.com> Date: Wed, 14 Jan 2026 05:42:30 +0000 Subject: [PATCH 06/12] Video decode acceleration & gpu compositing --- Client/cefweb/CWebApp.cpp | 22 +++++++++++++++++----- Client/core/CClientVariables.cpp | 1 + Client/core/CSettings.cpp | 20 +++++++++++++++++--- Client/core/CSettings.h | 1 + 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/Client/cefweb/CWebApp.cpp b/Client/cefweb/CWebApp.cpp index 29ff60f7c56..2177cbece81 100644 --- a/Client/cefweb/CWebApp.cpp +++ b/Client/cefweb/CWebApp.cpp @@ -63,8 +63,7 @@ namespace // Prevent Chromium from dropping privileges; required for elevated launches (see chromium/3960) commandLine->AppendSwitch("do-not-de-elevate"); - // Must apply essential CEF switches regardless of WebCore availability - commandLine->AppendSwitch("disable-gpu-compositing"); + // Enable external begin frame scheduling for MTA-controlled rendering commandLine->AppendSwitch("enable-begin-frame-scheduling"); // Explicitly block account sign-in to avoid crashes when Google API keys are registered on the system commandLine->AppendSwitchWithValue("allow-browser-signin", "false"); @@ -77,6 +76,7 @@ namespace } bool disableGpu = false; + bool enableVideoAccel = true; if (g_pCore && IsReadablePointer(g_pCore, sizeof(void*))) { auto* cvars = g_pCore->GetCVars(); @@ -85,6 +85,8 @@ namespace bool gpuEnabled = true; cvars->Get("browser_enable_gpu", gpuEnabled); disableGpu = !gpuEnabled; + + cvars->Get("browser_enable_video_acceleration", enableVideoAccel); } } @@ -104,14 +106,24 @@ namespace } if (disableGpu) + { commandLine->AppendSwitch("disable-gpu"); + // Also disable GPU compositing when GPU is disabled + commandLine->AppendSwitch("disable-gpu-compositing"); + } + + // Hardware video decoding - enable when GPU is enabled and video acceleration is requested + if (!disableGpu && enableVideoAccel) + { + commandLine->AppendSwitch("enable-accelerated-video-decode"); + } } } // namespace [[nodiscard]] CefRefPtr CWebApp::HandleError(const SString& strError, unsigned int uiError) { auto stream = CefStreamReader::CreateForData( - (void*)strError.c_str(), + (void*)strError.c_str(), strError.length() ); if (!stream) @@ -131,7 +143,7 @@ void CWebApp::OnBeforeChildProcessLaunch(CefRefPtr command_line) const CefString processType = command_line->GetSwitchValue("type"); ConfigureCommandLineSwitches(command_line, processType); - + // Attach IPC validation code for render processes // This runs in browser process context where g_pCore and webCore are valid // The auth code is generated in CWebCore constructor and passed to subprocesses @@ -325,7 +337,7 @@ CefRefPtr CWebApp::Create(CefRefPtr browser, Cef static constexpr unsigned int CODE_404 = 404; return HandleError(ERROR_404, CODE_404); } - + return CefRefPtr(new CefStreamResourceHandler(mimeType, stream)); } } diff --git a/Client/core/CClientVariables.cpp b/Client/core/CClientVariables.cpp index ea7b3b07bd7..959ec68bae0 100644 --- a/Client/core/CClientVariables.cpp +++ b/Client/core/CClientVariables.cpp @@ -395,6 +395,7 @@ void CClientVariables::LoadDefaults() DEFAULT("discord_rpc_share_data", false); // Consistent Rich Presence data sharing DEFAULT("discord_rpc_share_data_firsttime", false); // Display the user data sharing consent dialog box - for the first time DEFAULT("browser_enable_gpu", true); // Enable GPU in CEF? (allows stuff like WebGL to function) + DEFAULT("browser_enable_video_acceleration", true); // Enable hardware video decoding in CEF? DEFAULT("process_cpu_affinity", true); // Set CPU 0 affinity to improve game performance and fix the known issue in single-threaded games DEFAULT("ask_before_disconnect", true); // Ask before disconnecting from a server DEFAULT("allow_steam_client", false); // Allow connecting with the local Steam client (to set GTA:SA ingame status) diff --git a/Client/core/CSettings.cpp b/Client/core/CSettings.cpp index 676a85c9660..12f3ffbaa82 100644 --- a/Client/core/CSettings.cpp +++ b/Client/core/CSettings.cpp @@ -428,6 +428,7 @@ void CSettings::ResetGuiPointers() m_pGridBrowserWhitelist = NULL; m_pButtonBrowserWhitelistRemove = NULL; m_pCheckBoxBrowserGPUEnabled = NULL; + m_pCheckBoxBrowserVideoAccelEnabled = NULL; } CSettings::CSettings() @@ -1565,6 +1566,10 @@ void CSettings::CreateGUI() m_pCheckBoxBrowserGPUEnabled->SetPosition(CVector2D(browserRightColumnX, vecTemp.fY - 25.0f)); m_pCheckBoxBrowserGPUEnabled->AutoSize(NULL, 20.0f); + m_pCheckBoxBrowserVideoAccelEnabled = reinterpret_cast(pManager->CreateCheckBox(m_pTabBrowser, _("Enable video acceleration"), true)); + m_pCheckBoxBrowserVideoAccelEnabled->SetPosition(CVector2D(browserRightColumnX, vecTemp.fY)); + m_pCheckBoxBrowserVideoAccelEnabled->AutoSize(NULL, 20.0f); + m_pLabelBrowserCustomBlacklist = reinterpret_cast(pManager->CreateLabel(m_pTabBrowser, _("Custom blacklist"))); m_pLabelBrowserCustomBlacklist->SetPosition(CVector2D(vecTemp.fX, vecTemp.fY + 30.0f)); m_pLabelBrowserCustomBlacklist->GetPosition(vecTemp); @@ -2391,7 +2396,7 @@ void CSettings::PopulateResolutionComboBox() if (!m_pComboResolution) return; - + m_pComboResolution->Clear(); numVidModes = gameSettings->GetNumVideoModes(); @@ -2428,7 +2433,7 @@ void CSettings::PopulateResolutionComboBox() break; } } - + if (!bDuplicate) resolutions.push_back(resData); } @@ -4224,6 +4229,8 @@ void CSettings::LoadData() m_pCheckBoxRemoteJavascript->SetSelected(bVar); CVARS_GET("browser_enable_gpu", bVar); m_pCheckBoxBrowserGPUEnabled->SetSelected(bVar); + CVARS_GET("browser_enable_video_acceleration", bVar); + m_pCheckBoxBrowserVideoAccelEnabled->SetSelected(bVar); ReloadBrowserLists(); } @@ -4729,6 +4736,13 @@ void CSettings::SaveData() bool bBrowserGPUSettingChanged = (bBrowserGPUSetting != bBrowserGPUEnabled); CVARS_SET("browser_enable_gpu", bBrowserGPUSetting); + bool bBrowserVideoAccelEnabled = false; + CVARS_GET("browser_enable_video_acceleration", bBrowserVideoAccelEnabled); + + bool bBrowserVideoAccelSetting = m_pCheckBoxBrowserVideoAccelEnabled->GetSelected(); + bool bBrowserVideoAccelSettingChanged = (bBrowserVideoAccelSetting != bBrowserVideoAccelEnabled); + CVARS_SET("browser_enable_video_acceleration", bBrowserVideoAccelSetting); + // Ensure CVARS ranges ok CClientVariables::GetSingleton().ValidateValues(); @@ -4738,7 +4752,7 @@ void CSettings::SaveData() gameSettings->Save(); // Ask to restart? - if (bIsVideoModeChanged || bIsAntiAliasingChanged || bIsCustomizedSAFilesChanged || processsDPIAwareChanged || bBrowserGPUSettingChanged) + if (bIsVideoModeChanged || bIsAntiAliasingChanged || bIsCustomizedSAFilesChanged || processsDPIAwareChanged || bBrowserGPUSettingChanged || bBrowserVideoAccelSettingChanged) ShowRestartQuestion(); else if (CModManager::GetSingleton().IsLoaded() && bBrowserSettingChanged) ShowDisconnectQuestion(); diff --git a/Client/core/CSettings.h b/Client/core/CSettings.h index 075fa761e64..0c541cd49df 100644 --- a/Client/core/CSettings.h +++ b/Client/core/CSettings.h @@ -375,6 +375,7 @@ class CSettings CGUIButton* m_pButtonBrowserWhitelistRemove; CGUIButton* m_pButtonBrowserWhitelistRemoveAll; CGUICheckBox* m_pCheckBoxBrowserGPUEnabled; + CGUICheckBox* m_pCheckBoxBrowserVideoAccelEnabled; bool m_bBrowserListsChanged; bool m_bBrowserListsLoadEnabled; From 0ff9e41d52877de38fa925d346c6521caf7fef38 Mon Sep 17 00:00:00 2001 From: lopsi <40902730+Lpsd@users.noreply.github.com> Date: Wed, 14 Jan 2026 05:46:57 +0000 Subject: [PATCH 07/12] Mouse input throttling & dirty rect optimizations --- Client/cefweb/CWebView.cpp | 73 +++++++++++++++++++++++++++++++++++--- Client/cefweb/CWebView.h | 4 +++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/Client/cefweb/CWebView.cpp b/Client/cefweb/CWebView.cpp index e2f8ca4940b..7add27ee234 100644 --- a/Client/cefweb/CWebView.cpp +++ b/Client/cefweb/CWebView.cpp @@ -699,11 +699,32 @@ void CWebView::InjectMouseMove(int iPosX, int iPosY) if (!m_pWebView) return; + // Throttle mouse move events to reduce excessive CEF repaints + // Allow ~60 mouse updates per second (16ms interval) + constexpr auto MOUSE_THROTTLE_INTERVAL = std::chrono::milliseconds(16); + auto now = std::chrono::steady_clock::now(); + + // Always update the pending position + m_vecPendingMousePosition.x = iPosX; + m_vecPendingMousePosition.y = iPosY; + + // Check if enough time has passed since last mouse move + if (now - m_lastMouseMoveTime < MOUSE_THROTTLE_INTERVAL) + { + // Store as pending - will be sent on next allowed interval or on click + m_bHasPendingMouseMove = true; + return; + } + + // Send the mouse move event + m_lastMouseMoveTime = now; + m_bHasPendingMouseMove = false; + CefMouseEvent mouseEvent; mouseEvent.x = iPosX; mouseEvent.y = iPosY; - // Set modifiers from mouse states (yeah, using enum values as indices isn't best practise, but it's the easiest solution here) + // Set modifiers from mouse states if (m_mouseButtonStates[BROWSER_MOUSEBUTTON_LEFT]) mouseEvent.modifiers |= EVENTFLAG_LEFT_MOUSE_BUTTON; if (m_mouseButtonStates[BROWSER_MOUSEBUTTON_MIDDLE]) @@ -722,6 +743,19 @@ void CWebView::InjectMouseDown(eWebBrowserMouseButton mouseButton, int count) if (!m_pWebView) return; + // Flush any pending mouse move before click to ensure accurate position + if (m_bHasPendingMouseMove) + { + m_vecMousePosition.x = m_vecPendingMousePosition.x; + m_vecMousePosition.y = m_vecPendingMousePosition.y; + m_bHasPendingMouseMove = false; + + CefMouseEvent moveEvent; + moveEvent.x = m_vecMousePosition.x; + moveEvent.y = m_vecMousePosition.y; + m_pWebView->GetHost()->SendMouseMoveEvent(moveEvent, false); + } + CefMouseEvent mouseEvent; mouseEvent.x = m_vecMousePosition.x; mouseEvent.y = m_vecMousePosition.y; @@ -1152,15 +1186,44 @@ void CWebView::OnPaint(CefRefPtr browser, CefRenderHandler::PaintEle } // Allocate or reallocate buffer if size changed - if (!m_RenderData.buffer || m_RenderData.bufferSize != requiredSize) [[unlikely]] + const bool bSizeChanged = !m_RenderData.buffer || m_RenderData.bufferSize != requiredSize; + if (bSizeChanged) [[unlikely]] { m_RenderData.buffer = std::make_unique(requiredSize); m_RenderData.bufferSize = requiredSize; } - // Copy the buffer immediately - with external_begin_frame_enabled, we control timing - // so we copy here rather than storing a pointer and blocking - std::memcpy(m_RenderData.buffer.get(), buffer, requiredSize); + // Copy buffer - with external_begin_frame_enabled, we control timing + // Optimize: only copy dirty regions when buffer size hasn't changed + if (bSizeChanged || dirtyRects.empty()) + { + // Full copy needed for new/resized buffer or if no dirty rects specified + std::memcpy(m_RenderData.buffer.get(), buffer, requiredSize); + } + else + { + // Partial copy - only copy dirty regions + const auto srcData = static_cast(buffer); + auto* dstData = m_RenderData.buffer.get(); + const auto pitch = static_cast(width) * CEF_PIXEL_STRIDE; + + for (const auto& rect : dirtyRects) + { + // Validate rect bounds + if (rect.x < 0 || rect.y < 0 || rect.width <= 0 || rect.height <= 0) [[unlikely]] + continue; + if (rect.x + rect.width > width || rect.y + rect.height > height) [[unlikely]] + continue; + + // Copy each row of the dirty rect + const auto rectPitch = static_cast(rect.width) * CEF_PIXEL_STRIDE; + for (int y = rect.y; y < rect.y + rect.height; ++y) + { + const auto offset = static_cast(y) * pitch + static_cast(rect.x) * CEF_PIXEL_STRIDE; + std::memcpy(&dstData[offset], &srcData[offset], rectPitch); + } + } + } m_RenderData.width = width; m_RenderData.height = height; diff --git a/Client/cefweb/CWebView.h b/Client/cefweb/CWebView.h index c466cf0a36d..ec04e81ca96 100644 --- a/Client/cefweb/CWebView.h +++ b/Client/cefweb/CWebView.h @@ -29,6 +29,7 @@ #include #include #include +#include #define GetNextSibling(hwnd) GetWindow(hwnd, GW_HWNDNEXT) // Re-define the conflicting macro #define GetFirstChild(hwnd) GetTopWindow(hwnd) @@ -257,6 +258,9 @@ class CWebView : public CWebViewInterface, bool m_bIsTransparent; bool m_bBrowserCreated = false; // Lazy creation: tracks if CEF browser has been created POINT m_vecMousePosition; + POINT m_vecPendingMousePosition; // Pending position for throttled mouse move + bool m_bHasPendingMouseMove = false; // Whether there's a pending throttled mouse move + std::chrono::steady_clock::time_point m_lastMouseMoveTime; // For mouse move throttling bool m_mouseButtonStates[3]; SString m_CurrentTitle; float m_fVolume; From eefc173e78f9303b1939b6047f8bdef953fafe35 Mon Sep 17 00:00:00 2001 From: lopsi <40902730+Lpsd@users.noreply.github.com> Date: Wed, 14 Jan 2026 06:46:55 +0000 Subject: [PATCH 08/12] Lazy loading fixes --- Client/cefweb/CWebView.cpp | 25 ++++++++++++++++++++++++- Client/cefweb/CWebView.h | 4 ++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Client/cefweb/CWebView.cpp b/Client/cefweb/CWebView.cpp index 7add27ee234..426f5371362 100644 --- a/Client/cefweb/CWebView.cpp +++ b/Client/cefweb/CWebView.cpp @@ -295,8 +295,15 @@ bool CWebView::LoadURL(const SString& strURL, bool bFilterEnabled, const SString // Lazy creation: create browser on first use EnsureBrowserCreated(); + // If browser isn't ready yet (async creation), store the URL to load when ready if (!m_pWebView) - return false; + { + m_strPendingURL = strURL; + m_bPendingURLFilterEnabled = bFilterEnabled; + m_strPendingPostData = strPostData; + m_bPendingURLEncoded = bURLEncoded; + return true; // Return true - we'll load it when browser is ready + } CefURLParts urlParts; if (strURL.empty() || !CefParseURL(strURL, urlParts)) @@ -1506,6 +1513,22 @@ void CWebView::OnAfterCreated(CefRefPtr browser) // Set web view reference m_pWebView = browser; + // If we have a pending URL from lazy loading, load it now + if (!m_strPendingURL.empty()) + { + SString pendingURL = m_strPendingURL; + bool filterEnabled = m_bPendingURLFilterEnabled; + SString postData = m_strPendingPostData; + bool urlEncoded = m_bPendingURLEncoded; + + // Clear pending state before loading to prevent recursion + m_strPendingURL.clear(); + m_strPendingPostData.clear(); + + // Load the pending URL + LoadURL(pendingURL, filterEnabled, postData, urlEncoded); + } + // Call created event callback QueueBrowserEvent( "OnAfterCreated", diff --git a/Client/cefweb/CWebView.h b/Client/cefweb/CWebView.h index ec04e81ca96..0750566f3f7 100644 --- a/Client/cefweb/CWebView.h +++ b/Client/cefweb/CWebView.h @@ -257,6 +257,10 @@ class CWebView : public CWebViewInterface, bool m_bIsRenderingPaused; bool m_bIsTransparent; bool m_bBrowserCreated = false; // Lazy creation: tracks if CEF browser has been created + SString m_strPendingURL; // Lazy creation: URL to load when browser is ready + bool m_bPendingURLFilterEnabled = true; + SString m_strPendingPostData; + bool m_bPendingURLEncoded = true; POINT m_vecMousePosition; POINT m_vecPendingMousePosition; // Pending position for throttled mouse move bool m_bHasPendingMouseMove = false; // Whether there's a pending throttled mouse move From 701b0a9b3900f3a93bc0692b7ca55bec2613abee Mon Sep 17 00:00:00 2001 From: lopsi <40902730+Lpsd@users.noreply.github.com> Date: Wed, 14 Jan 2026 06:47:02 +0000 Subject: [PATCH 09/12] Remove outdated wine comment --- Client/cefweb/CWebApp.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Client/cefweb/CWebApp.cpp b/Client/cefweb/CWebApp.cpp index 2177cbece81..1d0d4cbfc89 100644 --- a/Client/cefweb/CWebApp.cpp +++ b/Client/cefweb/CWebApp.cpp @@ -100,7 +100,6 @@ namespace else { // In Wine, we generally want to try GPU (DXVK handles it well) - // But disable-gpu-compositing is already set above which is key // If user hasn't explicitly disabled GPU in cvars, let it run } } From 3388945cf101555cab65f39edcc6be12d9f69e02 Mon Sep 17 00:00:00 2001 From: lopsi <40902730+Lpsd@users.noreply.github.com> Date: Wed, 14 Jan 2026 06:50:44 +0000 Subject: [PATCH 10/12] Revert OnPaint dirty rect optimization from 0ff9e41d5 --- Client/cefweb/CWebView.cpp | 40 +++++++++----------------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/Client/cefweb/CWebView.cpp b/Client/cefweb/CWebView.cpp index 426f5371362..22b68028939 100644 --- a/Client/cefweb/CWebView.cpp +++ b/Client/cefweb/CWebView.cpp @@ -1198,39 +1198,17 @@ void CWebView::OnPaint(CefRefPtr browser, CefRenderHandler::PaintEle { m_RenderData.buffer = std::make_unique(requiredSize); m_RenderData.bufferSize = requiredSize; + // Zero-initialize new buffer to avoid garbage pixels in areas not painted yet + std::memset(m_RenderData.buffer.get(), 0, requiredSize); } - // Copy buffer - with external_begin_frame_enabled, we control timing - // Optimize: only copy dirty regions when buffer size hasn't changed - if (bSizeChanged || dirtyRects.empty()) - { - // Full copy needed for new/resized buffer or if no dirty rects specified - std::memcpy(m_RenderData.buffer.get(), buffer, requiredSize); - } - else - { - // Partial copy - only copy dirty regions - const auto srcData = static_cast(buffer); - auto* dstData = m_RenderData.buffer.get(); - const auto pitch = static_cast(width) * CEF_PIXEL_STRIDE; - - for (const auto& rect : dirtyRects) - { - // Validate rect bounds - if (rect.x < 0 || rect.y < 0 || rect.width <= 0 || rect.height <= 0) [[unlikely]] - continue; - if (rect.x + rect.width > width || rect.y + rect.height > height) [[unlikely]] - continue; - - // Copy each row of the dirty rect - const auto rectPitch = static_cast(rect.width) * CEF_PIXEL_STRIDE; - for (int y = rect.y; y < rect.y + rect.height; ++y) - { - const auto offset = static_cast(y) * pitch + static_cast(rect.x) * CEF_PIXEL_STRIDE; - std::memcpy(&dstData[offset], &srcData[offset], rectPitch); - } - } - } + // Always do a full copy from CEF's buffer + // CEF's buffer contains the complete frame state, and dirty rects indicate what changed + // However, we must copy the full buffer because: + // 1. Our intermediate buffer may be stale if frames were skipped + // 2. CEF may combine multiple + // 3. Partial copies can cause rendering artifacts with popups/modals + std::memcpy(m_RenderData.buffer.get(), buffer, requiredSize); m_RenderData.width = width; m_RenderData.height = height; From 2090d12e1be52a632309c97942bfdf4f90e53e01 Mon Sep 17 00:00:00 2001 From: lopsi <40902730+Lpsd@users.noreply.github.com> Date: Wed, 14 Jan 2026 07:06:37 +0000 Subject: [PATCH 11/12] Improve CWebView::UpdateTexture edge cases --- Client/cefweb/CWebView.cpp | 10 ++++++++-- Client/cefweb/CWebView.h | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Client/cefweb/CWebView.cpp b/Client/cefweb/CWebView.cpp index 22b68028939..07f1b6c60a3 100644 --- a/Client/cefweb/CWebView.cpp +++ b/Client/cefweb/CWebView.cpp @@ -392,6 +392,7 @@ void CWebView::SetRenderingPaused(bool bPaused) m_RenderData.dirtyRects.clear(); m_RenderData.dirtyRects.shrink_to_fit(); m_RenderData.popupBuffer.reset(); + m_RenderData.needsFullCopy = true; // Force full copy when unpaused } } } @@ -467,6 +468,7 @@ void CWebView::UpdateTexture() if (m_RenderData.changed && (m_pWebBrowserRenderItem->m_uiSizeX != m_RenderData.width || m_pWebBrowserRenderItem->m_uiSizeY != m_RenderData.height)) { m_RenderData.changed = false; + m_RenderData.needsFullCopy = true; // Force full copy after size change } if (m_RenderData.changed || m_RenderData.popupShown) [[likely]] @@ -518,8 +520,9 @@ void CWebView::UpdateTexture() const auto frameArea = static_cast(m_RenderData.width) * static_cast(m_RenderData.height); // Determine if we should do full frame copy or partial dirty rect update - // Full copy is more efficient when dirty area exceeds threshold due to fewer memcpy calls - bool doFullCopy = dirtyRects.empty() || + // Always do full copy on first update (texture may have garbage data) + // Full copy is also more efficient when dirty area exceeds threshold + bool doFullCopy = m_RenderData.needsFullCopy || dirtyRects.empty() || (dirtyRects.size() == 1 && dirtyRects[0].width == m_RenderData.width && dirtyRects[0].height == m_RenderData.height); if (!doFullCopy && frameArea > 0) @@ -530,6 +533,9 @@ void CWebView::UpdateTexture() if (doFullCopy) { + // Clear the needsFullCopy flag after we do a full copy + m_RenderData.needsFullCopy = false; + // Full frame update - copy entire buffer if (destPitch == sourcePitch) [[likely]] { diff --git a/Client/cefweb/CWebView.h b/Client/cefweb/CWebView.h index 0750566f3f7..c83ffd9d517 100644 --- a/Client/cefweb/CWebView.h +++ b/Client/cefweb/CWebView.h @@ -276,6 +276,7 @@ class CWebView : public CWebViewInterface, struct { bool changed = false; + bool needsFullCopy = true; // Force full copy on first update or after texture reset std::mutex dataMutex; // Main frame buffer - we now own this buffer (copied in OnPaint) From 624f063cc50f762cf42ca3a27bb689abd8e6320a Mon Sep 17 00:00:00 2001 From: lopsi <40902730+Lpsd@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:35:30 +0000 Subject: [PATCH 12/12] Remove dirty rect processing & improve restore Dirty rect 'optimizations' were actually lowering performance, and causing edge case issues with device restoration and partial updates - removed this functionality. Also improved device reset/restore handling (force repaint) --- Client/cefweb/CWebView.cpp | 237 +++--------------- Client/cefweb/CWebView.h | 2 - .../core/Graphics/CRenderItem.WebBrowser.cpp | 1 + Client/sdk/core/CRenderItemManagerInterface.h | 1 + 4 files changed, 42 insertions(+), 199 deletions(-) diff --git a/Client/cefweb/CWebView.cpp b/Client/cefweb/CWebView.cpp index 07f1b6c60a3..016f5689a02 100644 --- a/Client/cefweb/CWebView.cpp +++ b/Client/cefweb/CWebView.cpp @@ -20,106 +20,6 @@ namespace { const int CEF_PIXEL_STRIDE = 4; - - // Threshold for switching from dirty rect updates to full frame copy - // If dirty area exceeds this fraction of total area, full copy is more efficient - constexpr float DIRTY_RECT_THRESHOLD = 0.25f; - - // Calculate total area covered by dirty rects (accounting for overlaps approximately) - size_t CalculateDirtyArea(const CefRenderHandler::RectList& dirtyRects, int frameWidth, int frameHeight) - { - if (dirtyRects.empty()) - return 0; - - // For a single rect, just return its area - if (dirtyRects.size() == 1) - { - const auto& rect = dirtyRects[0]; - return static_cast(rect.width) * static_cast(rect.height); - } - - // For multiple rects, calculate bounding box area as upper bound estimate - // This is faster than precise overlap calculation and gives good results - int minX = frameWidth, minY = frameHeight; - int maxX = 0, maxY = 0; - size_t totalRectArea = 0; - - for (const auto& rect : dirtyRects) - { - if (rect.width <= 0 || rect.height <= 0) - continue; - - minX = std::min(minX, rect.x); - minY = std::min(minY, rect.y); - maxX = std::max(maxX, rect.x + rect.width); - maxY = std::max(maxY, rect.y + rect.height); - totalRectArea += static_cast(rect.width) * static_cast(rect.height); - } - - // Use the smaller of: sum of rect areas, or bounding box area - // This gives a reasonable estimate without expensive overlap calculation - const size_t boundingArea = static_cast(maxX - minX) * static_cast(maxY - minY); - return std::min(totalRectArea, boundingArea); - } - - // Check if two rects are adjacent or overlapping (can be merged) - bool RectsAreAdjacent(const CefRect& a, const CefRect& b, int tolerance = 1) - { - // Check if rects overlap or touch within tolerance - return !(a.x + a.width + tolerance < b.x || - b.x + b.width + tolerance < a.x || - a.y + a.height + tolerance < b.y || - b.y + b.height + tolerance < a.y); - } - - // Merge two rects into their bounding box - CefRect MergeRects(const CefRect& a, const CefRect& b) - { - const int minX = std::min(a.x, b.x); - const int minY = std::min(a.y, b.y); - const int maxX = std::max(a.x + a.width, b.x + b.width); - const int maxY = std::max(a.y + a.height, b.y + b.height); - return CefRect(minX, minY, maxX - minX, maxY - minY); - } - - // Merge adjacent dirty rects to reduce number of copy operations - // Returns optimized list with fewer, larger rects - std::vector MergeAdjacentRects(const CefRenderHandler::RectList& dirtyRects) - { - if (dirtyRects.size() <= 1) - return std::vector(dirtyRects.begin(), dirtyRects.end()); - - std::vector merged(dirtyRects.begin(), dirtyRects.end()); - - // Simple greedy merge - keep merging until no more merges possible - // Limited iterations to prevent excessive processing - constexpr int maxIterations = 3; - for (int iter = 0; iter < maxIterations; ++iter) - { - bool mergedAny = false; - - for (size_t i = 0; i < merged.size() && merged.size() > 1; ++i) - { - for (size_t j = i + 1; j < merged.size(); ++j) - { - if (RectsAreAdjacent(merged[i], merged[j])) - { - merged[i] = MergeRects(merged[i], merged[j]); - merged.erase(merged.begin() + j); - mergedAny = true; - break; - } - } - if (mergedAny) - break; - } - - if (!mergedAny) - break; - } - - return merged; - } } CWebView::CWebView(bool bIsLocal, CWebBrowserItem* pWebBrowserRenderItem, bool bTransparent) @@ -389,10 +289,7 @@ void CWebView::SetRenderingPaused(bool bPaused) m_RenderData.popupShown = false; m_RenderData.buffer.reset(); m_RenderData.bufferSize = 0; - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); m_RenderData.popupBuffer.reset(); - m_RenderData.needsFullCopy = true; // Force full copy when unpaused } } } @@ -468,7 +365,20 @@ void CWebView::UpdateTexture() if (m_RenderData.changed && (m_pWebBrowserRenderItem->m_uiSizeX != m_RenderData.width || m_pWebBrowserRenderItem->m_uiSizeY != m_RenderData.height)) { m_RenderData.changed = false; - m_RenderData.needsFullCopy = true; // Force full copy after size change + } + + // After device reset (minimize/restore), force full copy from our buffer to new texture + if (m_pWebBrowserRenderItem->m_bTextureWasRecreated) + { + m_pWebBrowserRenderItem->m_bTextureWasRecreated = false; + + // If we have valid buffer data matching texture size, trigger full update + if (m_RenderData.buffer && m_RenderData.bufferSize > 0 && + m_RenderData.width == static_cast(m_pWebBrowserRenderItem->m_uiSizeX) && + m_RenderData.height == static_cast(m_pWebBrowserRenderItem->m_uiSizeY)) + { + m_RenderData.changed = true; + } } if (m_RenderData.changed || m_RenderData.popupShown) [[likely]] @@ -516,75 +426,24 @@ void CWebView::UpdateTexture() { m_RenderData.changed = false; - const auto& dirtyRects = m_RenderData.dirtyRects; - const auto frameArea = static_cast(m_RenderData.width) * static_cast(m_RenderData.height); - - // Determine if we should do full frame copy or partial dirty rect update - // Always do full copy on first update (texture may have garbage data) - // Full copy is also more efficient when dirty area exceeds threshold - bool doFullCopy = m_RenderData.needsFullCopy || dirtyRects.empty() || - (dirtyRects.size() == 1 && dirtyRects[0].width == m_RenderData.width && dirtyRects[0].height == m_RenderData.height); - - if (!doFullCopy && frameArea > 0) + // Always do full frame copy since D3DLOCK_DISCARD invalidates entire texture + // Our buffer contains the complete frame from OnPaint's full memcpy + if (destPitch == sourcePitch) [[likely]] { - const auto dirtyArea = CalculateDirtyArea(dirtyRects, m_RenderData.width, m_RenderData.height); - doFullCopy = (static_cast(dirtyArea) / static_cast(frameArea)) > DIRTY_RECT_THRESHOLD; - } - - if (doFullCopy) - { - // Clear the needsFullCopy flag after we do a full copy - m_RenderData.needsFullCopy = false; - - // Full frame update - copy entire buffer - if (destPitch == sourcePitch) [[likely]] - { - if (m_RenderData.height > 0 && - static_cast(m_RenderData.height) > SIZE_MAX / static_cast(destPitch)) [[unlikely]] - { - pSurface->UnlockRect(); - m_RenderData.changed = false; - m_RenderData.popupShown = false; - return; - } - std::memcpy(destData, sourceData, static_cast(destPitch) * static_cast(m_RenderData.height)); - } - else + if (m_RenderData.height > 0 && + static_cast(m_RenderData.height) > SIZE_MAX / static_cast(destPitch)) [[unlikely]] { - // Row-by-row copy when pitches differ - if (destPitch <= 0 || sourcePitch <= 0) [[unlikely]] - { - pSurface->UnlockRect(); - m_RenderData.changed = false; - m_RenderData.popupShown = false; - return; - } - - if (m_RenderData.height > 0 && - (static_cast(m_RenderData.height) > SIZE_MAX / static_cast(destPitch) || - static_cast(m_RenderData.height) > SIZE_MAX / static_cast(sourcePitch))) [[unlikely]] - { - pSurface->UnlockRect(); - m_RenderData.changed = false; - m_RenderData.popupShown = false; - return; - } - - for (int y = 0; y < m_RenderData.height; ++y) - { - const auto sourceIndex = static_cast(y) * static_cast(sourcePitch); - const auto destIndex = static_cast(y) * static_cast(destPitch); - const auto copySize = std::min(static_cast(sourcePitch), static_cast(destPitch)); - - std::memcpy(&destData[destIndex], &sourceData[sourceIndex], copySize); - } + pSurface->UnlockRect(); + m_RenderData.changed = false; + m_RenderData.popupShown = false; + return; } + std::memcpy(destData, sourceData, static_cast(destPitch) * static_cast(m_RenderData.height)); } else { - // Partial update using optimized dirty rects - if (m_RenderData.height > 0 && - static_cast(m_RenderData.height) > SIZE_MAX / static_cast(destPitch)) [[unlikely]] + // Row-by-row copy when pitches differ + if (destPitch <= 0 || sourcePitch <= 0) [[unlikely]] { pSurface->UnlockRect(); m_RenderData.changed = false; @@ -592,33 +451,23 @@ void CWebView::UpdateTexture() return; } - // Merge adjacent rects to reduce number of copy operations - const auto mergedRects = MergeAdjacentRects(dirtyRects); - - for (const auto& rect : mergedRects) + if (m_RenderData.height > 0 && + (static_cast(m_RenderData.height) > SIZE_MAX / static_cast(destPitch) || + static_cast(m_RenderData.height) > SIZE_MAX / static_cast(sourcePitch))) [[unlikely]] { - if (rect.x < 0 || rect.y < 0 || rect.width <= 0 || rect.height <= 0) [[unlikely]] - continue; - - if (rect.x >= m_RenderData.width || rect.y >= m_RenderData.height || - rect.width > m_RenderData.width || rect.height > m_RenderData.height || - rect.x > m_RenderData.width - rect.width || rect.y > m_RenderData.height - rect.height) [[unlikely]] - continue; - - const auto rectEndY = rect.y + rect.height; - - if (static_cast(destPitch) < static_cast(rect.x + rect.width) * CEF_PIXEL_STRIDE) [[unlikely]] - continue; + pSurface->UnlockRect(); + m_RenderData.changed = false; + m_RenderData.popupShown = false; + return; + } - for (int y = rect.y; y < rectEndY; ++y) - { - const auto sourceIndex = static_cast(y) * static_cast(sourcePitch) + - static_cast(rect.x) * CEF_PIXEL_STRIDE; - const auto destIndex = static_cast(y) * static_cast(destPitch) + - static_cast(rect.x) * CEF_PIXEL_STRIDE; + for (int y = 0; y < m_RenderData.height; ++y) + { + const auto sourceIndex = static_cast(y) * static_cast(sourcePitch); + const auto destIndex = static_cast(y) * static_cast(destPitch); + const auto copySize = std::min(static_cast(sourcePitch), static_cast(destPitch)); - std::memcpy(&destData[destIndex], &sourceData[sourceIndex], static_cast(rect.width) * CEF_PIXEL_STRIDE); - } + std::memcpy(&destData[destIndex], &sourceData[sourceIndex], copySize); } } } @@ -672,10 +521,6 @@ void CWebView::UpdateTexture() m_RenderData.changed = false; m_RenderData.popupShown = false; } - - // Clear dirty rects to prevent memory accumulation - m_RenderData.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); } } @@ -1218,8 +1063,6 @@ void CWebView::OnPaint(CefRefPtr browser, CefRenderHandler::PaintEle m_RenderData.width = width; m_RenderData.height = height; - m_RenderData.dirtyRects = dirtyRects; - m_RenderData.dirtyRects.shrink_to_fit(); m_RenderData.changed = true; } diff --git a/Client/cefweb/CWebView.h b/Client/cefweb/CWebView.h index c83ffd9d517..0e6be8ad8b4 100644 --- a/Client/cefweb/CWebView.h +++ b/Client/cefweb/CWebView.h @@ -276,7 +276,6 @@ class CWebView : public CWebViewInterface, struct { bool changed = false; - bool needsFullCopy = true; // Force full copy on first update or after texture reset std::mutex dataMutex; // Main frame buffer - we now own this buffer (copied in OnPaint) @@ -284,7 +283,6 @@ class CWebView : public CWebViewInterface, size_t bufferSize = 0; int width = 0; int height = 0; - CefRenderHandler::RectList dirtyRects; CefRect popupRect; bool popupShown = false; diff --git a/Client/core/Graphics/CRenderItem.WebBrowser.cpp b/Client/core/Graphics/CRenderItem.WebBrowser.cpp index 0ab32bfef72..e765345dbbf 100644 --- a/Client/core/Graphics/CRenderItem.WebBrowser.cpp +++ b/Client/core/Graphics/CRenderItem.WebBrowser.cpp @@ -75,6 +75,7 @@ void CWebBrowserItem::OnLostDevice() void CWebBrowserItem::OnResetDevice() { CreateUnderlyingData(); + m_bTextureWasRecreated = true; // Force full repaint after device reset } //////////////////////////////////////////////////////////////// diff --git a/Client/sdk/core/CRenderItemManagerInterface.h b/Client/sdk/core/CRenderItemManagerInterface.h index c6faf45a0c5..acd6e9f8445 100644 --- a/Client/sdk/core/CRenderItemManagerInterface.h +++ b/Client/sdk/core/CRenderItemManagerInterface.h @@ -563,4 +563,5 @@ class CWebBrowserItem : public CTextureItem virtual void Resize(const CVector2D& size); IDirect3DSurface9* m_pD3DRenderTargetSurface; + bool m_bTextureWasRecreated = false; // Set after device reset to force full repaint };