diff --git a/Client/cefweb/CWebApp.cpp b/Client/cefweb/CWebApp.cpp index 29ff60f7c56..1d0d4cbfc89 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); } } @@ -98,20 +100,29 @@ 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(), + (void*)strError.c_str(), strError.length() ); if (!stream) @@ -131,7 +142,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 +336,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/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..016f5689a02 100644 --- a/Client/cefweb/CWebView.cpp +++ b/Client/cefweb/CWebView.cpp @@ -13,9 +13,9 @@ #include #include #include "CWebDevTools.h" -#include #include "CWebViewAuth.h" // AUTH: IPC validation helpers #include +#include namespace { @@ -62,9 +62,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(); @@ -128,6 +125,16 @@ void CWebView::QueueBrowserEvent(const char* name, std::functionGetFPSLimiter()->GetFPSTarget(); @@ -149,7 +156,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() @@ -157,9 +169,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(); @@ -183,8 +192,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)) @@ -268,14 +287,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(); } } } @@ -293,7 +307,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 +318,7 @@ void CWebView::ClearTexture() { if (!m_pWebBrowserRenderItem) [[unlikely]] return; - + auto* const pD3DSurface = m_pWebBrowserRenderItem->m_pD3DRenderTargetSurface; if (!pD3DSurface) [[unlikely]] return; @@ -318,7 +332,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 +350,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 +357,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,20 +365,31 @@ 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* const sourceData = m_RenderData.buffer.get(); const auto destPitch = LockedRect.Pitch; // Validate destination pitch @@ -383,14 +398,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 +408,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,147 +418,65 @@ 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]] - { - // 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 + if (m_RenderData.height > 0 && + static_cast(m_RenderData.height) > SIZE_MAX / static_cast(destPitch)) [[unlikely]] { - // 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 = + const auto popupSizeMismatches = popupRect.x < 0 || popupRect.y < 0 || popupRect.width <= 0 || popupRect.height <= 0 || popupRect.x >= renderWidth || popupRect.y >= renderHeight || @@ -561,71 +484,44 @@ 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 - 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) @@ -661,11 +557,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]) @@ -684,6 +601,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; @@ -775,7 +705,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 +726,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 +993,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 +1003,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 +1015,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 +1025,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 +1033,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; - } } //////////////////////////////////////////////////////////////////// @@ -1426,6 +1340,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", @@ -1554,13 +1484,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..0e6be8ad8b4 100644 --- a/Client/cefweb/CWebView.h +++ b/Client/cefweb/CWebView.h @@ -24,12 +24,12 @@ #include #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) @@ -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; } @@ -199,7 +194,6 @@ class CWebView : public CWebViewInterface, CefRefPtr model) override; private: - void ResumeCefThread(); void QueueBrowserEvent(const char* name, std::function&& fn); struct FEventTarget @@ -262,7 +256,15 @@ 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 + 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; @@ -275,12 +277,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; - 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 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; 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 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 };