Skip to content

Commit

Permalink
Pass through soft fonts over conpty (#13965)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
j4james authored Sep 13, 2022
1 parent f2b361c commit 704458e
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 11 deletions.
13 changes: 12 additions & 1 deletion src/renderer/vt/Xterm256Engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Xterm256Engine::Xterm256Engine(_In_ wil::unique_hfile hPipe,
[[nodiscard]] HRESULT Xterm256Engine::UpdateDrawingBrushes(const TextAttribute& textAttributes,
const RenderSettings& /*renderSettings*/,
const gsl::not_null<IRenderData*> pData,
const bool /*usingSoftFont*/,
const bool usingSoftFont,
const bool isSettingDefaultBrushes) noexcept
{
RETURN_HR_IF(S_FALSE, _passthrough && isSettingDefaultBrushes);
Expand All @@ -38,6 +38,17 @@ Xterm256Engine::Xterm256Engine(_In_ wil::unique_hfile hPipe,

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.
return _UpdateExtendedAttrs(textAttributes);
}
Expand Down
13 changes: 11 additions & 2 deletions src/renderer/vt/paint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -535,8 +535,17 @@ using namespace Microsoft::Console::Types;
// Move the cursor to the start of this run.
RETURN_IF_FAILED(_MoveCursor(coord));

// Write the actual text string
RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8({ _bufferLine.data(), cchActual }));
// 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 }));
}

// GH#4415, GH#5181
// If the renderer told us that this was a wrapped line, then mark
Expand Down
25 changes: 25 additions & 0 deletions src/renderer/vt/state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe,
_hFile(std::move(pipe)),
_usingLineRenditions(false),
_stopUsingLineRenditions(false),
_usingSoftFont(false),
_lastTextAttributes(INVALID_COLOR, INVALID_COLOR),
_lastViewport(initialViewport),
_pool(til::pmr::get_default_resource()),
Expand Down Expand Up @@ -209,6 +210,30 @@ CATCH_RETURN();
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:
// - This method will update the active font on the current device context
// Does nothing for vt, the font is handed by the terminal.
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/vt/vtrenderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ namespace Microsoft::Console::Render

bool _usingLineRenditions;
bool _stopUsingLineRenditions;
bool _usingSoftFont;
TextAttribute _lastTextAttributes;

std::function<void(bool)> _pfnSetLookingForDSR;
Expand Down Expand Up @@ -225,6 +226,7 @@ namespace Microsoft::Console::Render

[[nodiscard]] HRESULT _WriteTerminalUtf8(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;

Expand Down
60 changes: 52 additions & 8 deletions src/terminal/adapter/adaptDispatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2585,14 +2585,6 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber,
const VTParameter cellHeight,
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.
if (!_fontBuffer)
{
Expand All @@ -2612,7 +2604,19 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber,
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) {
if (conptyPassthrough)
{
conptyPassthrough(ch);
}
// 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
// point we can finalize the buffer, and if valid, update the renderer
Expand Down Expand Up @@ -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:
// - DECRSTS - Restores the terminal state from a stream of data previously
// saved with a DECRQTSR query.
Expand Down
1 change: 1 addition & 0 deletions src/terminal/adapter/adaptDispatch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ namespace Microsoft::Console::VirtualTerminal
void _ReportSGRSetting() const;
void _ReportDECSTBMSetting();

StringHandler _CreateDrcsPassthroughHandler(const DispatchTypes::DrcsCharsetSize charsetSize);
StringHandler _CreatePassthroughHandler();

std::vector<bool> _tabStopColumns;
Expand Down

0 comments on commit 704458e

Please sign in to comment.