Add support for line renditions in the DX renderer (#13102)
This PR adds support for the VT line rendition attributes in the DirectX renderer, which allows for double-width and double-height line renditions. Line renditions were first implemented in conhost (with the GDI renderer) in PR #8664. Supporting them in the DX renderer now is a small step towards #11595. The DX implementation is very similar to the GDI one. When a particular line rendition is requested, we create a transform that is applied to the render target. And in the case of double-height renditions, we also initialize some clipping offsets to allow for the fact that we only render half of a line at a time. One additional complication exists when drawing the cursor, which requires a two part process where it first renders to a command list, and then draw the command list in a second step. We need to temporarily reset the transform in that first stage otherwise it ends up being applied twice. I've manually tested the renderer in conhost by setting the `UseDx` registry entry and confirmed that it passes the _Vttest_ double-size tests as well as several of my own tests. I've also checked that the renderer can now handle horizontal scrolling, which is a feature we get for free with the transforms.
This commit is contained in:
parent
e1086de512
commit
0154da5d33
|
@ -344,6 +344,7 @@ try
|
|||
Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> brush;
|
||||
Microsoft::WRL::ComPtr<ID2D1Image> originalTarget;
|
||||
Microsoft::WRL::ComPtr<ID2D1CommandList> commandList;
|
||||
D2D1::Matrix3x2F originalTransform;
|
||||
if (!fInvert)
|
||||
{
|
||||
// Make sure to make the cursor opaque
|
||||
|
@ -407,6 +408,9 @@ try
|
|||
RETURN_IF_FAILED(d2dContext->CreateCommandList(&commandList));
|
||||
d2dContext->GetTarget(&originalTarget);
|
||||
d2dContext->SetTarget(commandList.Get());
|
||||
// We use an identity transform here to avoid the active transform being applied twice.
|
||||
d2dContext->GetTransform(&originalTransform);
|
||||
d2dContext->SetTransform(D2D1::Matrix3x2F::Identity());
|
||||
RETURN_IF_FAILED(d2dContext->CreateSolidColorBrush(COLOR_WHITE, &brush));
|
||||
}
|
||||
}
|
||||
|
@ -449,6 +453,7 @@ try
|
|||
// so now we draw that command list using MASK_INVERT over the existing image
|
||||
RETURN_IF_FAILED(commandList->Close());
|
||||
d2dContext->SetTarget(originalTarget.Get());
|
||||
d2dContext->SetTransform(originalTransform);
|
||||
d2dContext->DrawImage(commandList.Get(), D2D1_INTERPOLATION_MODE_LINEAR, D2D1_COMPOSITE_MODE_MASK_INVERT);
|
||||
}
|
||||
|
||||
|
@ -501,8 +506,8 @@ CATCH_RETURN()
|
|||
|
||||
// Determine clip rectangle
|
||||
D2D1_RECT_F clipRect;
|
||||
clipRect.top = origin.y;
|
||||
clipRect.bottom = clipRect.top + drawingContext->cellSize.height;
|
||||
clipRect.top = origin.y + drawingContext->topClipOffset;
|
||||
clipRect.bottom = origin.y + drawingContext->cellSize.height - drawingContext->bottomClipOffset;
|
||||
clipRect.left = 0;
|
||||
clipRect.right = drawingContext->targetSize.width;
|
||||
|
||||
|
|
|
@ -32,7 +32,9 @@ namespace Microsoft::Console::Render
|
|||
cellSize(cellSize),
|
||||
targetSize(targetSize),
|
||||
cursorInfo(cursorInfo),
|
||||
options(options)
|
||||
options(options),
|
||||
topClipOffset(0),
|
||||
bottomClipOffset(0)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -48,6 +50,8 @@ namespace Microsoft::Console::Render
|
|||
D2D_SIZE_F targetSize;
|
||||
std::optional<CursorOptions> cursorInfo;
|
||||
D2D1_DRAW_TEXT_OPTIONS options;
|
||||
FLOAT topClipOffset;
|
||||
FLOAT bottomClipOffset;
|
||||
};
|
||||
|
||||
// Helper to choose which Direct2D method to use when drawing the cursor rectangle
|
||||
|
|
|
@ -49,6 +49,14 @@ D3D11_INPUT_ELEMENT_DESC _shaderInputLayout[] = {
|
|||
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
bool operator==(const D2D1::Matrix3x2F& lhs, const D2D1::Matrix3x2F& rhs) noexcept
|
||||
{
|
||||
return ::memcmp(&lhs.m[0][0], &rhs.m[0][0], sizeof(lhs.m)) == 0;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma hdrstop
|
||||
|
||||
using namespace Microsoft::Console::Render;
|
||||
|
@ -77,6 +85,8 @@ DxEngine::DxEngine() :
|
|||
_foregroundColor{ 0 },
|
||||
_backgroundColor{ 0 },
|
||||
_selectionBackground{},
|
||||
_currentLineRendition{ LineRendition::SingleWidth },
|
||||
_currentLineTransform{ D2D1::Matrix3x2F::Identity() },
|
||||
_haveDeviceResources{ false },
|
||||
_swapChainHandle{ INVALID_HANDLE_VALUE },
|
||||
_swapChainDesc{ 0 },
|
||||
|
@ -2283,3 +2293,79 @@ void DxEngine::UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept
|
|||
_drawingContext->cursorInfo = info.cursorInfo;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// Routine Description
|
||||
// - Resets the world transform to the identity matrix.
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - S_OK if successful. S_FALSE if already reset. E_FAIL if there was an error.
|
||||
[[nodiscard]] HRESULT DxEngine::ResetLineTransform() noexcept
|
||||
{
|
||||
// Return early if the current transform is already the identity matrix.
|
||||
RETURN_HR_IF(S_FALSE, _currentLineTransform.IsIdentity());
|
||||
// Reset the active transform to the identity matrix.
|
||||
_drawingContext->renderTarget->SetTransform(D2D1::Matrix3x2F::Identity());
|
||||
// Reset the clipping offsets.
|
||||
_drawingContext->topClipOffset = 0;
|
||||
_drawingContext->bottomClipOffset = 0;
|
||||
// Reset the current state.
|
||||
_currentLineTransform = D2D1::Matrix3x2F::Identity();
|
||||
_currentLineRendition = LineRendition::SingleWidth;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// Routine Description
|
||||
// - Applies an appropriate transform for the given line rendition and viewport offset.
|
||||
// Arguments:
|
||||
// - lineRendition - The line rendition specifying the scaling of the line.
|
||||
// - targetRow - The row on which the line is expected to be rendered.
|
||||
// - viewportLeft - The left offset of the current viewport.
|
||||
// Return Value:
|
||||
// - S_OK if successful. S_FALSE if already set. E_FAIL if there was an error.
|
||||
[[nodiscard]] HRESULT DxEngine::PrepareLineTransform(const LineRendition lineRendition,
|
||||
const size_t targetRow,
|
||||
const size_t viewportLeft) noexcept
|
||||
{
|
||||
auto lineTransform = D2D1::Matrix3x2F{ 0, 0, 0, 0, 0, 0 };
|
||||
const auto fontSize = _fontRenderData->GlyphCell();
|
||||
// The X delta is to account for the horizontal viewport offset.
|
||||
lineTransform.dx = viewportLeft ? -1.0f * viewportLeft * fontSize.width : 0.0f;
|
||||
switch (lineRendition)
|
||||
{
|
||||
case LineRendition::SingleWidth:
|
||||
lineTransform.m11 = 1; // single width
|
||||
lineTransform.m22 = 1; // single height
|
||||
break;
|
||||
case LineRendition::DoubleWidth:
|
||||
lineTransform.m11 = 2; // double width
|
||||
lineTransform.m22 = 1; // single height
|
||||
break;
|
||||
case LineRendition::DoubleHeightTop:
|
||||
lineTransform.m11 = 2; // double width
|
||||
lineTransform.m22 = 2; // double height
|
||||
// The Y delta is to negate the offset caused by the scaled height.
|
||||
lineTransform.dy = -1.0f * targetRow * fontSize.height;
|
||||
break;
|
||||
case LineRendition::DoubleHeightBottom:
|
||||
lineTransform.m11 = 2; // double width
|
||||
lineTransform.m22 = 2; // double height
|
||||
// The Y delta is to negate the offset caused by the scaled height.
|
||||
// An extra row is added because we need the bottom half of the line.
|
||||
lineTransform.dy = -1.0f * (targetRow + 1) * fontSize.height;
|
||||
break;
|
||||
}
|
||||
// Return early if the new matrix is the same as the current transform.
|
||||
RETURN_HR_IF(S_FALSE, _currentLineRendition == lineRendition && _currentLineTransform == lineTransform);
|
||||
// Set the active transform with the new matrix.
|
||||
_drawingContext->renderTarget->SetTransform(lineTransform);
|
||||
// If the line rendition is double height, we need to adjust the top or bottom
|
||||
// of the clipping rect to clip half the height of the rendered characters.
|
||||
const auto halfHeight = _drawingContext->cellSize.height / 2;
|
||||
_drawingContext->topClipOffset = lineRendition == LineRendition::DoubleHeightBottom ? halfHeight : 0;
|
||||
_drawingContext->bottomClipOffset = lineRendition == LineRendition::DoubleHeightTop ? halfHeight : 0;
|
||||
// Save the current state.
|
||||
_currentLineTransform = lineTransform;
|
||||
_currentLineRendition = lineRendition;
|
||||
return S_OK;
|
||||
}
|
||||
|
|
|
@ -93,6 +93,11 @@ namespace Microsoft::Console::Render
|
|||
|
||||
[[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override;
|
||||
|
||||
[[nodiscard]] HRESULT ResetLineTransform() noexcept override;
|
||||
[[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition,
|
||||
const size_t targetRow,
|
||||
const size_t viewportLeft) noexcept override;
|
||||
|
||||
[[nodiscard]] HRESULT PaintBackground() noexcept override;
|
||||
[[nodiscard]] HRESULT PaintBufferLine(const gsl::span<const Cluster> clusters,
|
||||
const COORD coord,
|
||||
|
@ -167,6 +172,9 @@ namespace Microsoft::Console::Render
|
|||
D2D1_COLOR_F _backgroundColor;
|
||||
D2D1_COLOR_F _selectionBackground;
|
||||
|
||||
LineRendition _currentLineRendition;
|
||||
D2D1::Matrix3x2F _currentLineTransform;
|
||||
|
||||
uint16_t _hyperlinkHoveredId;
|
||||
|
||||
bool _firstFrame;
|
||||
|
|
Loading…
Reference in New Issue