diff --git a/Client/cefweb/CWebApp.cpp b/Client/cefweb/CWebApp.cpp index ed9fd0f55fb..c5f11644308 100644 --- a/Client/cefweb/CWebApp.cpp +++ b/Client/cefweb/CWebApp.cpp @@ -57,8 +57,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"); @@ -71,6 +70,7 @@ namespace } bool disableGpu = false; + bool enableVideoAccel = true; if (g_pCore && IsReadablePointer(g_pCore, sizeof(void*))) { auto* cvars = g_pCore->GetCVars(); @@ -79,6 +79,8 @@ namespace bool gpuEnabled = true; cvars->Get("browser_enable_gpu", gpuEnabled); disableGpu = !gpuEnabled; + + cvars->Get("browser_enable_video_acceleration", enableVideoAccel); } } @@ -92,19 +94,31 @@ 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 } } 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(), strError.length()); + auto stream = CefStreamReader::CreateForData( + (void*)strError.c_str(), + strError.length() + ); if (!stream) return nullptr; return CefRefPtr(new CefStreamResourceHandler(uiError, strError, "text/plain", CefResponse::HeaderMap(), stream)); diff --git a/Client/cefweb/CWebCore.cpp b/Client/cefweb/CWebCore.cpp index 6336bb03cfd..7de6daeeabe 100644 --- a/Client/cefweb/CWebCore.cpp +++ b/Client/cefweb/CWebCore.cpp @@ -440,7 +440,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(); diff --git a/Client/cefweb/CWebView.cpp b/Client/cefweb/CWebView.cpp index c1a64b72297..dab8dee4c64 100644 --- a/Client/cefweb/CWebView.cpp +++ b/Client/cefweb/CWebView.cpp @@ -16,6 +16,7 @@ #include #include "CWebViewAuth.h" // AUTH: IPC validation helpers #include +#include namespace { @@ -62,9 +63,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(); @@ -129,6 +127,16 @@ void CWebView::QueueBrowserEvent(const char* name, std::functionGetFPSLimiter()->GetFPSTarget(); @@ -150,7 +158,12 @@ 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); + m_bBrowserCreated = true; + return true; } void CWebView::CloseBrowser() @@ -158,9 +171,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(); @@ -184,8 +194,18 @@ 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 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)) @@ -269,14 +289,9 @@ 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.dirtyRects.clear(); - m_RenderData.dirtyRects.shrink_to_fit(); + m_RenderData.buffer.reset(); + m_RenderData.bufferSize = 0; m_RenderData.popupBuffer.reset(); - - // Release any waiting CEF thread - m_RenderData.cefThreadState = ECefThreadState::Running; - m_RenderData.cefThreadCv.notify_all(); } } } @@ -319,8 +334,8 @@ 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 && static_cast(SurfaceDesc.Height) <= SIZE_MAX / static_cast(LockedRect.Pitch)) - [[likely]] + 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); std::memset(LockedRect.pBits, 0xFF, memsetSize); @@ -337,11 +352,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; } @@ -349,11 +359,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; } @@ -362,21 +367,32 @@ 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(); + } + + // 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]] { - // 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))) { - // 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 destPitch = LockedRect.Pitch; + auto* const destData = static_cast(LockedRect.pBits); + const auto* const sourceData = m_RenderData.buffer.get(); + const auto destPitch = LockedRect.Pitch; // Validate destination pitch if (destPitch <= 0) [[unlikely]] @@ -384,11 +400,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; } @@ -399,11 +410,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; @@ -414,209 +420,110 @@ 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) + // 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]] { - // 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 - if (destPitch == sourcePitch) [[likely]] + if (m_RenderData.height > 0 && + static_cast(m_RenderData.height) > SIZE_MAX / static_cast(destPitch)) [[unlikely]] { - // Check for integer overflow in size calculation: height * pitch must fit in size_t - 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 - 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 && (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)); - - 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 { - // Check for integer overflow in destination size calculation - 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; - 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) + 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]] { - // 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) + static_cast(rect.x) * CEF_PIXEL_STRIDE; - 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); - } + 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); } } } - // 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 = popupRect.x < 0 || popupRect.y < 0 || popupRect.width <= 0 || popupRect.height <= 0 || - popupRect.x >= renderWidth || popupRect.y >= renderHeight || popupRect.width > renderWidth || - popupRect.height > renderHeight || popupRect.x > renderWidth - popupRect.width || - popupRect.y > renderHeight - popupRect.height; - - // Verify popup buffer exists before accessing it + const auto renderWidth = static_cast(m_pWebBrowserRenderItem->m_uiSizeX); + const auto renderHeight = static_cast(m_pWebBrowserRenderItem->m_uiSizeY); + const auto popupSizeMismatches = + popupRect.x < 0 || popupRect.y < 0 || + popupRect.width <= 0 || popupRect.height <= 0 || + popupRect.x >= renderWidth || popupRect.y >= renderHeight || + popupRect.width > renderWidth || popupRect.height > renderHeight || + popupRect.x > renderWidth - popupRect.width || + popupRect.y > renderHeight - popupRect.height; + 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) + static_cast(popupRect.x) * CEF_PIXEL_STRIDE; + 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 - 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) @@ -652,11 +559,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]) @@ -675,6 +603,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; @@ -798,8 +739,6 @@ void CWebView::Resize(const CVector2D& size) // Send resize event to CEF if (m_pWebView) m_pWebView->GetHost()->WasResized(); - - ResumeCefThread(); } CVector2D CWebView::GetSize() @@ -1068,7 +1007,8 @@ void CWebView::OnPaint(CefRefPtr browser, CefRenderHandler::PaintEle if (popupRect.width > 0 && popupRect.height > 0 && 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) * static_cast(popupRect.height) * CEF_PIXEL_STRIDE; + currentSize = static_cast(popupRect.width) * + static_cast(popupRect.height) * CEF_PIXEL_STRIDE; } // Reallocate if size changed or buffer doesn't exist @@ -1082,7 +1022,6 @@ void CWebView::OnPaint(CefRefPtr browser, CefRenderHandler::PaintEle std::memcpy(m_RenderData.popupBuffer.get(), buffer, requiredSize); - // Popup path doesn't wait, so no need to signal return; } @@ -1090,11 +1029,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; } @@ -1103,45 +1037,37 @@ 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 + const bool bSizeChanged = !m_RenderData.buffer || m_RenderData.bufferSize != requiredSize; + if (bSizeChanged) [[unlikely]] + { + 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); + } + + // 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; - 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; - } } //////////////////////////////////////////////////////////////////// @@ -1404,6 +1330,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", [](CWebBrowserEventsInterface* iface) { iface->Events_OnCreated(); }); } @@ -1524,13 +1466,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 45020abb884..e8c30daea37 100644 --- a/Client/cefweb/CWebView.h +++ b/Client/cefweb/CWebView.h @@ -24,14 +24,14 @@ #include #include #include -#include #include #include #include #include #include -#define GetNextSibling(hwnd) GetWindow(hwnd, GW_HWNDNEXT) // Re-define the conflicting macro -#define GetFirstChild(hwnd) GetTopWindow(hwnd) +#include +#define GetNextSibling(hwnd) GetWindow(hwnd, GW_HWNDNEXT) // Re-define the conflicting macro +#define GetFirstChild(hwnd) GetTopWindow(hwnd) #define MTA_CEF_USERAGENT "Multi Theft Auto: San Andreas Client " MTA_DM_BUILDTAG_LONG @@ -43,12 +43,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, @@ -71,6 +65,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; } @@ -211,7 +206,6 @@ class CWebView : public CWebViewInterface, CefRefPtr model) override; private: - void ResumeCefThread(); void QueueBrowserEvent(const char* name, std::function&& fn); struct FEventTarget @@ -270,29 +264,37 @@ class CWebView : public CWebViewInterface, CefRefPtr m_pWebView; CWebBrowserItem* m_pWebBrowserRenderItem; - std::atomic_bool m_bBeingDestroyed; - bool m_bIsLocal; - bool m_bIsRenderingPaused; - bool m_bIsTransparent; - POINT m_vecMousePosition; - bool m_mouseButtonStates[3]; - SString m_CurrentTitle; - float m_fVolume; - std::map m_Properties; - bool m_bHasInputFocus; - std::set m_AjaxHandlers; + std::atomic_bool m_bBeingDestroyed; + bool m_bIsLocal; + 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 + std::chrono::steady_clock::time_point m_lastMouseMoveTime; // For mouse move throttling + bool m_mouseButtonStates[3]; + SString m_CurrentTitle; + float m_fVolume; + std::map m_Properties; + bool m_bHasInputFocus; + std::set m_AjaxHandlers; std::shared_ptr m_pEventTarget; struct { bool changed = false; std::mutex dataMutex; - ECefThreadState cefThreadState = ECefThreadState::Running; - std::condition_variable cefThreadCv; - const void* buffer; - int width, height; - CefRenderHandler::RectList dirtyRects; + // 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; CefRect popupRect; bool popupShown = false; diff --git a/Client/core/CClientVariables.cpp b/Client/core/CClientVariables.cpp index 65d8e6ce53a..2ae7bedaa24 100644 --- a/Client/core/CClientVariables.cpp +++ b/Client/core/CClientVariables.cpp @@ -373,30 +373,30 @@ void CClientVariables::LoadDefaults() Get("borderless_enable_srgb", legacyEnable); Set("borderless_apply_windowed", legacyEnable); } - DEFAULT("vertical_aim_sensitivity", 0.0015f); // 0.0015f is GTA default setting - DEFAULT("process_priority", 0); // 0-normal 1-above normal 2-high - DEFAULT("process_dpi_aware", false); // Enable DPI awareness in core initialization - DEFAULT("mute_master_when_minimized", 0); // 0-off 1-on - DEFAULT("mute_sfx_when_minimized", 0); // 0-off 1-on - DEFAULT("mute_radio_when_minimized", 0); // 0-off 1-on - DEFAULT("mute_mta_when_minimized", 0); // 0-off 1-on - DEFAULT("mute_voice_when_minimized", 0); // 0-off 1-on - DEFAULT("share_file_cache", 1); // 0-no 1-share client resource file cache with other MTA installs - DEFAULT("show_unsafe_resolutions", 0); // 0-off 1-show resolutions that are higher that the desktop - DEFAULT("fov", 70); // Camera field of view - DEFAULT("browser_remote_websites", true); // Load remote websites? - DEFAULT("browser_remote_javascript", true); // Execute javascript on remote websites? - DEFAULT("filter_duplicate_log_lines", true); // Filter duplicate log lines for debug view and clientscript.log - DEFAULT("always_show_transferbox", false); // Should the transfer box always be visible for downloads? (and ignore scripted control) - DEFAULT("allow_discord_rpc", true); // Enable Discord Rich Presence - 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("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) - DEFAULT("use_mouse_sensitivity_for_aiming", - false); // It uses the horizontal mouse sensitivity for aiming, making the Y-axis sensitivity the same as the X-axis + DEFAULT("vertical_aim_sensitivity", 0.0015f); // 0.0015f is GTA default setting + DEFAULT("process_priority", 0); // 0-normal 1-above normal 2-high + DEFAULT("process_dpi_aware", false); // Enable DPI awareness in core initialization + DEFAULT("mute_master_when_minimized", 0); // 0-off 1-on + DEFAULT("mute_sfx_when_minimized", 0); // 0-off 1-on + DEFAULT("mute_radio_when_minimized", 0); // 0-off 1-on + DEFAULT("mute_mta_when_minimized", 0); // 0-off 1-on + DEFAULT("mute_voice_when_minimized", 0); // 0-off 1-on + DEFAULT("share_file_cache", 1); // 0-no 1-share client resource file cache with other MTA installs + DEFAULT("show_unsafe_resolutions", 0); // 0-off 1-show resolutions that are higher that the desktop + DEFAULT("fov", 70); // Camera field of view + DEFAULT("browser_remote_websites", true); // Load remote websites? + DEFAULT("browser_remote_javascript", true); // Execute javascript on remote websites? + DEFAULT("filter_duplicate_log_lines", true); // Filter duplicate log lines for debug view and clientscript.log + DEFAULT("always_show_transferbox", false); // Should the transfer box always be visible for downloads? (and ignore scripted control) + DEFAULT("allow_discord_rpc", true); // Enable Discord Rich Presence + 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) + DEFAULT("use_mouse_sensitivity_for_aiming", false); // It uses the horizontal mouse sensitivity for aiming, making the Y-axis sensitivity the same as the X-axis if (!Exists("locale")) { diff --git a/Client/core/CSettings.cpp b/Client/core/CSettings.cpp index 6ddf6477052..15f395899a0 100644 --- a/Client/core/CSettings.cpp +++ b/Client/core/CSettings.cpp @@ -423,6 +423,7 @@ void CSettings::ResetGuiPointers() m_pGridBrowserWhitelist = NULL; m_pButtonBrowserWhitelistRemove = NULL; m_pCheckBoxBrowserGPUEnabled = NULL; + m_pCheckBoxBrowserVideoAccelEnabled = NULL; } CSettings::CSettings() @@ -1555,6 +1556,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); @@ -4218,6 +4223,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(); } @@ -4723,6 +4730,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(); @@ -4732,7 +4746,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 23e1297b2a5..8c0e24b5785 100644 --- a/Client/core/CSettings.h +++ b/Client/core/CSettings.h @@ -372,6 +372,7 @@ class CSettings CGUIButton* m_pButtonBrowserWhitelistRemove; CGUIButton* m_pButtonBrowserWhitelistRemoveAll; CGUICheckBox* m_pCheckBoxBrowserGPUEnabled; + CGUICheckBox* m_pCheckBoxBrowserVideoAccelEnabled; bool m_bBrowserListsChanged; bool m_bBrowserListsLoadEnabled; diff --git a/Client/core/Graphics/CRenderItem.WebBrowser.cpp b/Client/core/Graphics/CRenderItem.WebBrowser.cpp index bfa4e246907..e765345dbbf 100644 --- a/Client/core/Graphics/CRenderItem.WebBrowser.cpp +++ b/Client/core/Graphics/CRenderItem.WebBrowser.cpp @@ -57,22 +57,25 @@ 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(); + m_bTextureWasRecreated = true; // Force full repaint after device reset } //////////////////////////////////////////////////////////////// @@ -87,8 +90,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 +106,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; diff --git a/Client/sdk/core/CRenderItemManagerInterface.h b/Client/sdk/core/CRenderItemManagerInterface.h index d2ed814103d..6de606ea551 100644 --- a/Client/sdk/core/CRenderItemManagerInterface.h +++ b/Client/sdk/core/CRenderItemManagerInterface.h @@ -573,4 +573,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 };