Pass through soft fonts over conpty (#13965)

This PR introduces a mechanism for passing through downloadable soft
fonts to the conpty client, so that we can support DRCS (Dynamically
Redefinable Character Sets) in Windows Terminal.

Soft fonts were first implemented in conhost (with the GDI renderer) in
PR #10011, and were implemented in the DX renderer in PR #13362.

The way this works is by passing through the `DECDLD` sequence
containing the font definition, but with the character set ID patched to
use a hardcoded value (this is to make sure it's not going to override
the default character set). At the same time we send through an `SCS`
sequence to map this character set into the G1 table so we can easily
activate it.

We still need to process the `DECDLD` sequence locally, though, since
the initial character set mapping take place on the host side. This gets
the DRCS characters into our buffer as PUA Unicode characters. Then when
the VT engine needs to output these characters, it masks them with `7F`
to map them back to ASCII, and outputs an `SO` control to activate the
soft font in the conpty client.

## Validation Steps Performed

I've manually tested with a number of soft fonts and applications that
make use of soft fonts. But if you're testing with the VT320 fonts from
the vt100.net collection, note that you'll need to enable the ISO-2022
coding system first, since they use 8-bit C1 controls.
This commit is contained in:
James Holderness
2022-09-13 01:21:06 +01:00
committed by GitHub
parent f2b361c146
commit 704458ee0e
6 changed files with 103 additions and 11 deletions

View File

@@ -29,7 +29,7 @@ Xterm256Engine::Xterm256Engine(_In_ wil::unique_hfile hPipe,
[[nodiscard]] HRESULT Xterm256Engine::UpdateDrawingBrushes(const TextAttribute& textAttributes, [[nodiscard]] HRESULT Xterm256Engine::UpdateDrawingBrushes(const TextAttribute& textAttributes,
const RenderSettings& /*renderSettings*/, const RenderSettings& /*renderSettings*/,
const gsl::not_null<IRenderData*> pData, const gsl::not_null<IRenderData*> pData,
const bool /*usingSoftFont*/, const bool usingSoftFont,
const bool isSettingDefaultBrushes) noexcept const bool isSettingDefaultBrushes) noexcept
{ {
RETURN_HR_IF(S_FALSE, _passthrough && isSettingDefaultBrushes); RETURN_HR_IF(S_FALSE, _passthrough && isSettingDefaultBrushes);
@@ -38,6 +38,17 @@ Xterm256Engine::Xterm256Engine(_In_ wil::unique_hfile hPipe,
RETURN_IF_FAILED(_UpdateHyperlinkAttr(textAttributes, pData)); RETURN_IF_FAILED(_UpdateHyperlinkAttr(textAttributes, pData));
// If we're using a soft font, it should have already been mapped into the
// G1 table, so we just need to switch between G0 and G1 when turning the
// soft font on and off. We don't want to do this when setting the default
// brushes, though, because that could result in an unnecessary G0 switch
// at the start of every frame.
if (usingSoftFont != _usingSoftFont && !isSettingDefaultBrushes)
{
RETURN_IF_FAILED(_Write(usingSoftFont ? "\x0E" : "\x0F"));
_usingSoftFont = usingSoftFont;
}
// Only do extended attributes in xterm-256color, as to not break telnet.exe. // Only do extended attributes in xterm-256color, as to not break telnet.exe.
return _UpdateExtendedAttrs(textAttributes); return _UpdateExtendedAttrs(textAttributes);
} }

View File

@@ -535,8 +535,17 @@ using namespace Microsoft::Console::Types;
// Move the cursor to the start of this run. // Move the cursor to the start of this run.
RETURN_IF_FAILED(_MoveCursor(coord)); RETURN_IF_FAILED(_MoveCursor(coord));
// Write the actual text string // Write the actual text string. If we're using a soft font, the character
// set should have already been selected, so we just need to map our internal
// representation back to ASCII (handled by the _WriteTerminalDrcs method).
if (_usingSoftFont) [[unlikely]]
{
RETURN_IF_FAILED(VtEngine::_WriteTerminalDrcs({ _bufferLine.data(), cchActual }));
}
else
{
RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8({ _bufferLine.data(), cchActual })); RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8({ _bufferLine.data(), cchActual }));
}
// GH#4415, GH#5181 // GH#4415, GH#5181
// If the renderer told us that this was a wrapped line, then mark // If the renderer told us that this was a wrapped line, then mark

View File

@@ -31,6 +31,7 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe,
_hFile(std::move(pipe)), _hFile(std::move(pipe)),
_usingLineRenditions(false), _usingLineRenditions(false),
_stopUsingLineRenditions(false), _stopUsingLineRenditions(false),
_usingSoftFont(false),
_lastTextAttributes(INVALID_COLOR, INVALID_COLOR), _lastTextAttributes(INVALID_COLOR, INVALID_COLOR),
_lastViewport(initialViewport), _lastViewport(initialViewport),
_pool(til::pmr::get_default_resource()), _pool(til::pmr::get_default_resource()),
@@ -209,6 +210,30 @@ CATCH_RETURN();
return _Write(needed); return _Write(needed);
} }
// Method Description:
// - Writes a wstring to the tty when the characters are from the DRCS soft font.
// It is assumed that the character set has already been designated in the
// client terminal, so we just need to re-map our internal representation
// of the characters into ASCII.
// Arguments:
// - wstr - wstring of text to be written
// Return Value:
// - S_OK or suitable HRESULT error from writing pipe.
[[nodiscard]] HRESULT VtEngine::_WriteTerminalDrcs(const std::wstring_view wstr) noexcept
{
std::string needed;
needed.reserve(wstr.size());
for (const auto& wch : wstr)
{
// Our DRCS characters use the range U+EF20 to U+EF7F from the Unicode
// Private Use Area. To map them back to ASCII we just mask with 7F.
needed.push_back(wch & 0x7F);
}
return _Write(needed);
}
// Method Description: // Method Description:
// - This method will update the active font on the current device context // - This method will update the active font on the current device context
// Does nothing for vt, the font is handed by the terminal. // Does nothing for vt, the font is handed by the terminal.

View File

@@ -100,6 +100,7 @@ namespace Microsoft::Console::Render
bool _usingLineRenditions; bool _usingLineRenditions;
bool _stopUsingLineRenditions; bool _stopUsingLineRenditions;
bool _usingSoftFont;
TextAttribute _lastTextAttributes; TextAttribute _lastTextAttributes;
std::function<void(bool)> _pfnSetLookingForDSR; std::function<void(bool)> _pfnSetLookingForDSR;
@@ -225,6 +226,7 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT _WriteTerminalUtf8(const std::wstring_view str) noexcept; [[nodiscard]] HRESULT _WriteTerminalUtf8(const std::wstring_view str) noexcept;
[[nodiscard]] HRESULT _WriteTerminalAscii(const std::wstring_view str) noexcept; [[nodiscard]] HRESULT _WriteTerminalAscii(const std::wstring_view str) noexcept;
[[nodiscard]] HRESULT _WriteTerminalDrcs(const std::wstring_view str) noexcept;
[[nodiscard]] virtual HRESULT _DoUpdateTitle(const std::wstring_view newTitle) noexcept override; [[nodiscard]] virtual HRESULT _DoUpdateTitle(const std::wstring_view newTitle) noexcept override;

View File

@@ -2585,14 +2585,6 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber,
const VTParameter cellHeight, const VTParameter cellHeight,
const DispatchTypes::DrcsCharsetSize charsetSize) const DispatchTypes::DrcsCharsetSize charsetSize)
{ {
// If we're a conpty, we're just going to ignore the operation for now.
// There's no point in trying to pass it through without also being able
// to pass through the character set designations.
if (_api.IsConsolePty())
{
return nullptr;
}
// The font buffer is created on demand. // The font buffer is created on demand.
if (!_fontBuffer) if (!_fontBuffer)
{ {
@@ -2612,7 +2604,19 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber,
return nullptr; return nullptr;
} }
// If we're a conpty, we create a special passthrough handler that will
// forward the DECDLD sequence to the conpty terminal with a hardcoded ID.
// That ID is also pre-mapped into the G1 table, so the VT engine can just
// switch to G1 when it needs to output any DRCS characters. But note that
// we still need to process the DECDLD sequence locally, so the character
// set translation is correctly handled on the host side.
const auto conptyPassthrough = _api.IsConsolePty() ? _CreateDrcsPassthroughHandler(charsetSize) : nullptr;
return [=](const auto ch) { return [=](const auto ch) {
if (conptyPassthrough)
{
conptyPassthrough(ch);
}
// We pass the data string straight through to the font buffer class // We pass the data string straight through to the font buffer class
// until we receive an ESC, indicating the end of the string. At that // until we receive an ESC, indicating the end of the string. At that
// point we can finalize the buffer, and if valid, update the renderer // point we can finalize the buffer, and if valid, update the renderer
@@ -2643,6 +2647,46 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber,
}; };
} }
// Routine Description:
// - Helper method to create a string handler that can be used to pass through
// DECDLD sequences when in conpty mode. This patches the original sequence
// with a hardcoded character set ID, and pre-maps that ID into the G1 table.
// Arguments:
// - <none>
// Return value:
// - a function to receive the data or nullptr if the initial flush fails
ITermDispatch::StringHandler AdaptDispatch::_CreateDrcsPassthroughHandler(const DispatchTypes::DrcsCharsetSize charsetSize)
{
const auto defaultPassthrough = _CreatePassthroughHandler();
if (defaultPassthrough)
{
auto& engine = _api.GetStateMachine().Engine();
return [=, &engine, gotId = false](const auto ch) mutable {
// The character set ID is contained in the first characters of the
// sequence, so we just ignore that initial content until we receive
// a "final" character (i.e. in range 30 to 7E). At that point we
// pass through a hardcoded ID of "@".
if (!gotId)
{
if (ch >= 0x30 && ch <= 0x7E)
{
gotId = true;
defaultPassthrough('@');
}
}
else if (!defaultPassthrough(ch))
{
// Once the DECDLD sequence is finished, we also output an SCS
// sequence to map the character set into the G1 table.
const auto charset96 = charsetSize == DispatchTypes::DrcsCharsetSize::Size96;
engine.ActionPassThroughString(charset96 ? L"\033-@" : L"\033)@");
}
return true;
};
}
return nullptr;
}
// Method Description: // Method Description:
// - DECRSTS - Restores the terminal state from a stream of data previously // - DECRSTS - Restores the terminal state from a stream of data previously
// saved with a DECRQTSR query. // saved with a DECRQTSR query.

View File

@@ -209,6 +209,7 @@ namespace Microsoft::Console::VirtualTerminal
void _ReportSGRSetting() const; void _ReportSGRSetting() const;
void _ReportDECSTBMSetting(); void _ReportDECSTBMSetting();
StringHandler _CreateDrcsPassthroughHandler(const DispatchTypes::DrcsCharsetSize charsetSize);
StringHandler _CreatePassthroughHandler(); StringHandler _CreatePassthroughHandler();
std::vector<bool> _tabStopColumns; std::vector<bool> _tabStopColumns;