diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 980da4bb60..be7350f11a 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -152,12 +152,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation _renderer->SetBackgroundColorChangedCallback([this]() { _rendererBackgroundColorChanged(); }); _renderer->SetFrameColorChangedCallback([this]() { _rendererTabColorChanged(); }); - _renderer->SetRendererEnteredErrorStateCallback([this]() { RendererEnteredErrorState.raise(nullptr, nullptr); }); + _renderer->SetRendererEnteredErrorStateCallback([this]() { _rendererEnteredErrorState(); }); } UpdateSettings(settings, unfocusedAppearance); } + void ControlCore::_rendererEnteredErrorState() + { + // The first time the renderer fails out (after all of its own retries), switch it to D2D and WARP + // and force it to try again. If it _still_ fails, we can let it halt. + if (_renderFailures++ == 0) + { + const auto lock = _terminal->LockForWriting(); + _renderEngine->SetGraphicsAPI(parseGraphicsAPI(GraphicsAPI::Direct2D)); + _renderEngine->SetSoftwareRendering(true); + _renderer->EnablePainting(); + return; + } + RendererEnteredErrorState.raise(nullptr, nullptr); + } + void ControlCore::_setupDispatcherAndCallbacks() { // Get our dispatcher. If we're hosted in-proc with XAML, this will get @@ -917,6 +932,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _renderEngine->SetSoftwareRendering(_settings.SoftwareRendering()); // Inform the renderer of our opacity _renderEngine->EnableTransparentBackground(_isBackgroundTransparent()); + _renderFailures = 0; // We may have changed the engine; reset the failure counter. // Trigger a redraw to repaint the window background and tab colors. _renderer->TriggerRedrawAll(true, true); @@ -1983,6 +1999,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { // The lock must be held, because it calls into IRenderData which is shared state. const auto lock = _terminal->LockForWriting(); + _renderFailures = 0; _renderer->EnablePainting(); } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index d793d88b58..112848dc6e 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -344,6 +344,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation safe_void_coroutine _renderEngineSwapChainChanged(const HANDLE handle); void _rendererBackgroundColorChanged(); void _rendererTabColorChanged(); + void _rendererEnteredErrorState(); #pragma endregion void _raiseReadOnlyWarning(); @@ -398,6 +399,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation float _panelWidth{ 0 }; float _panelHeight{ 0 }; float _compositionScale{ 0 }; + uint8_t _renderFailures{ 0 }; bool _forceCursorVisible = false; // Audio stuff. diff --git a/src/features.xml b/src/features.xml index ba54205e3b..4788697703 100644 --- a/src/features.xml +++ b/src/features.xml @@ -68,6 +68,13 @@ + + Feature_AtlasEngineLoudErrors + Atlas Engine can optionally support signaling every presentation failure to its consumer. For now, we only want that to happen in non-Release builds. + AlwaysEnabled + + + Feature_NearbyFontLoading Controls whether fonts in the same directory as the binary are used during rendering. Disabled for conhost so that it doesn't iterate the entire system32 directory. diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index 2df96f0c62..805e14c1e7 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -61,13 +61,18 @@ catch (const wil::ResultException& exception) return E_PENDING; } - if (_p.warningCallback) + if constexpr (Feature_AtlasEngineLoudErrors::IsEnabled()) { - try + // We may fail to present repeatedly, e.g. if there's a short-term device failure. + // We should not bombard the consumer with repeated warning callbacks (where they may present a dialog to the user). + if (_p.warningCallback) { - _p.warningCallback(hr, {}); + try + { + _p.warningCallback(hr, {}); + } + CATCH_LOG() } - CATCH_LOG() } _b.reset(); diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index e32bff3d25..2c0d6e8162 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -12,9 +12,10 @@ using namespace Microsoft::Console::Types; using PointTree = interval_tree::IntervalTree; static constexpr TimerRepr TimerReprMax = std::numeric_limits::max(); -static constexpr DWORD maxRetriesForRenderEngine = 3; -// The renderer will wait this number of milliseconds * how many tries have elapsed before trying again. -static constexpr DWORD renderBackoffBaseTimeMilliseconds = 150; +// We want there to be five retry periods; after the last one, we will mark the render as failed. +static constexpr unsigned int maxRetriesForRenderEngine = 5; +// The renderer will wait this number of milliseconds * 2^tries before trying again. +static constexpr DWORD renderBackoffBaseTimeMilliseconds = 100; // Routine Description: // - Creates a new renderer controller for a console. @@ -326,40 +327,44 @@ DWORD Renderer::_timerToMillis(TimerRepr t) noexcept // - HRESULT S_OK, GDI error, Safe Math error, or state/argument errors. [[nodiscard]] HRESULT Renderer::PaintFrame() { - auto tries = maxRetriesForRenderEngine; - while (tries > 0) + HRESULT hr{ S_FALSE }; + // Attempt zero doesn't count as a retry. We should try maxRetries + 1 times. + for (unsigned int attempt = 0u; attempt <= maxRetriesForRenderEngine; ++attempt) { + if (attempt > 0) [[unlikely]] + { + // Add a bit of backoff. + // Sleep 100, 200, 400, 600, 800ms, 1600ms before failing out and disabling the renderer. + Sleep(renderBackoffBaseTimeMilliseconds * (1 << (attempt - 1))); + } + // BODGY: Optimally we would want to retry per engine, but that causes different // problems (intermittent inconsistent states between text renderer and UIA output, // not being able to lock the cursor location, etc.). - const auto hr = _PaintFrame(); + hr = _PaintFrame(); if (SUCCEEDED(hr)) { break; } LOG_HR_IF(hr, hr != E_PENDING); - - if (--tries == 0) - { - // Stop trying. - _disablePainting(); - if (_pfnRendererEnteredErrorState) - { - _pfnRendererEnteredErrorState(); - } - // If there's no callback, we still don't want to FAIL_FAST: the renderer going black - // isn't near as bad as the entire application aborting. We're a component. We shouldn't - // abort applications that host us. - return S_FALSE; - } - - // Add a bit of backoff. - // Sleep 150ms, 300ms, 450ms before failing out and disabling the renderer. - Sleep(renderBackoffBaseTimeMilliseconds * (maxRetriesForRenderEngine - tries)); } - return S_OK; + if (FAILED(hr)) + { + // Stop trying. + _disablePainting(); + if (_pfnRendererEnteredErrorState) + { + _pfnRendererEnteredErrorState(); + } + // If there's no callback, we still don't want to FAIL_FAST: the renderer going black + // isn't near as bad as the entire application aborting. We're a component. We shouldn't + // abort applications that host us. + hr = S_FALSE; + } + + return hr; } [[nodiscard]] HRESULT Renderer::_PaintFrame() noexcept