Implement DECSET 2026 - Synchronized Output (#18826)

This commit is contained in:
Leonard Hecker 2025-04-23 19:15:51 +02:00 committed by GitHub
parent 68d9e0d038
commit 773a4b9198
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 91 additions and 3 deletions

View File

@ -4,8 +4,6 @@
#include "precomp.h"
#include "renderer.hpp"
#pragma hdrstop
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::Types;
@ -90,6 +88,11 @@ IRenderData* Renderer::GetRenderData() const noexcept
_pData->UnlockConsole();
});
if (_isSynchronizingOutput)
{
_synchronizeWithOutput();
}
// Last chance check if anything scrolled without an explicit invalidate notification since the last frame.
_CheckViewportAndScroll();
@ -183,6 +186,71 @@ void Renderer::NotifyPaintFrame() noexcept
_thread.NotifyPaint();
}
// NOTE: You must be holding the console lock when calling this function.
void Renderer::SynchronizedOutputBegin() noexcept
{
// Kick the render thread into calling `_synchronizeWithOutput()`.
_isSynchronizingOutput = true;
}
// NOTE: You must be holding the console lock when calling this function.
void Renderer::SynchronizedOutputEnd() noexcept
{
// Unblock `_synchronizeWithOutput()` from the `WaitOnAddress` call.
_isSynchronizingOutput = false;
WakeByAddressSingle(&_isSynchronizingOutput);
// It's crucial to give the render thread at least a chance to gain the lock.
// Otherwise, a VT application could continuously spam DECSET 2026 (Synchronized Output) and
// essentially drop our renderer to 10 FPS, because `_isSynchronizingOutput` is always true.
//
// Obviously calling LockConsole/UnlockConsole here is an awful, ugly hack,
// since there's no guarantee that this is the same lock as the one the VT parser uses.
// But the alternative is Denial-Of-Service of the render thread.
//
// Note that this causes raw throughput of DECSET 2026 to be comparatively low, but that's fine.
// Apps that use DECSET 2026 don't produce that sequence continuously, but rather at a fixed rate.
_pData->UnlockConsole();
_pData->LockConsole();
}
void Renderer::_synchronizeWithOutput() noexcept
{
constexpr DWORD timeout = 100;
UINT64 start = 0;
DWORD elapsed = 0;
bool wrong = false;
QueryUnbiasedInterruptTime(&start);
// Wait for `_isSynchronizingOutput` to be set to false or for a timeout to occur.
while (true)
{
// We can't call a blocking function while holding the console lock, so release it temporarily.
_pData->UnlockConsole();
const auto ok = WaitOnAddress(&_isSynchronizingOutput, &wrong, sizeof(_isSynchronizingOutput), timeout - elapsed);
_pData->LockConsole();
if (!ok || !_isSynchronizingOutput)
{
break;
}
UINT64 now;
QueryUnbiasedInterruptTime(&now);
elapsed = static_cast<DWORD>((now - start) / 10000);
if (elapsed >= timeout)
{
break;
}
}
// If a timeout occurred, `_isSynchronizingOutput` may still be true.
// Set it to false now to skip calling `_synchronizeWithOutput()` on the next frame.
_isSynchronizingOutput = false;
}
// Routine Description:
// - Called when the system has requested we redraw a portion of the console.
// Arguments:

View File

@ -35,6 +35,8 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT PaintFrame();
void NotifyPaintFrame() noexcept;
void SynchronizedOutputBegin() noexcept;
void SynchronizedOutputEnd() noexcept;
void TriggerSystemRedraw(const til::rect* const prcDirtyClient);
void TriggerRedraw(const Microsoft::Console::Types::Viewport& region);
void TriggerRedraw(const til::point* const pcoord);
@ -91,6 +93,7 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT _PaintFrame() noexcept;
[[nodiscard]] HRESULT _PaintFrameForEngine(_In_ IRenderEngine* const pEngine) noexcept;
void _synchronizeWithOutput() noexcept;
bool _CheckViewportAndScroll();
[[nodiscard]] HRESULT _PaintBackground(_In_ IRenderEngine* const pEngine);
void _PaintBufferOutput(_In_ IRenderEngine* const pEngine);
@ -124,6 +127,7 @@ namespace Microsoft::Console::Render
std::function<void()> _pfnBackgroundColorChanged;
std::function<void()> _pfnFrameColorChanged;
std::function<void()> _pfnRendererEnteredErrorState;
bool _isSynchronizingOutput = false;
bool _forceUpdateViewport = false;
til::point_span _lastSelectionPaintSpan{};

View File

@ -34,7 +34,7 @@ DWORD WINAPI RenderThread::_ThreadProc()
// Between waiting on _hEvent and calling PaintFrame() there should be a minimal delay,
// so that a key press progresses to a drawing operation as quickly as possible.
// As such, we wait for the renderer to complete _before_ waiting on _hEvent.
// As such, we wait for the renderer to complete _before_ waiting on `_redraw`.
renderer->WaitUntilCanRender();
_redraw.wait();
@ -78,6 +78,8 @@ void RenderThread::EnablePainting() noexcept
}
}
// This function is meant to only be called by `Renderer`. You should use `TriggerTeardown()` instead,
// even if you plan to call `EnablePainting()` later, because that ensures proper synchronization.
void RenderThread::DisablePainting() noexcept
{
_enable.ResetEvent();

View File

@ -544,6 +544,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
ALTERNATE_SCROLL = DECPrivateMode(1007),
ASB_AlternateScreenBuffer = DECPrivateMode(1049),
XTERM_BracketedPasteMode = DECPrivateMode(2004),
SO_SynchronizedOutput = DECPrivateMode(2026),
GCM_GraphemeClusterMode = DECPrivateMode(2027),
W32IM_Win32InputMode = DECPrivateMode(9001),
};

View File

@ -1914,6 +1914,19 @@ void AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
case DispatchTypes::ModeParams::XTERM_BracketedPasteMode:
_api.SetSystemMode(ITerminalApi::Mode::BracketedPaste, enable);
break;
case DispatchTypes::ModeParams::SO_SynchronizedOutput:
if (_renderer)
{
if (enable)
{
_renderer->SynchronizedOutputBegin();
}
else
{
_renderer->SynchronizedOutputEnd();
}
}
break;
case DispatchTypes::ModeParams::GCM_GraphemeClusterMode:
break;
case DispatchTypes::ModeParams::W32IM_Win32InputMode: