mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-10 18:43:54 -06:00
AtlasEngine: Implement sixels (#17581)
* Add a revision to `ImageSlice` so that the renderers can use it to cache them as bitmaps across frames. * Hooked up the revision tracking to AtlasEngine to cache the slices into `Buffer`s so we can own them into the `Present`. * Hooked up those snapshots to BackendD3D with a straightforward hashmap -> atlas-rect logic. Just like rendering text. * Hooked up BackendD2D with a bad, but simple & direct drawing logic. * Bonus: Modify `ImageSlice` to be returned as a raw pointers as this helps performance slightly. (Trivial type == good.) * Bonus: Fixed the `_debugShowDirty` code (disabled by default). ## Validation Steps Performed * `mpv --really-quiet --vo=sixel foo.mp4` looks good ✅ * Scroll up down & observe dirty rects ✅
This commit is contained in:
parent
6372baa0d3
commit
75f7ae4bec
@ -7,11 +7,27 @@
|
||||
#include "Row.hpp"
|
||||
#include "textBuffer.hpp"
|
||||
|
||||
static std::atomic<uint64_t> s_revision{ 0 };
|
||||
|
||||
ImageSlice::ImageSlice(const til::size cellSize) noexcept :
|
||||
_cellSize{ cellSize }
|
||||
{
|
||||
}
|
||||
|
||||
void ImageSlice::BumpRevision() noexcept
|
||||
{
|
||||
// Avoid setting the revision to 0. This allows the renderer to use 0 as a sentinel value.
|
||||
do
|
||||
{
|
||||
_revision = s_revision.fetch_add(1, std::memory_order_relaxed);
|
||||
} while (_revision == 0);
|
||||
}
|
||||
|
||||
uint64_t ImageSlice::Revision() const noexcept
|
||||
{
|
||||
return _revision;
|
||||
}
|
||||
|
||||
til::size ImageSlice::CellSize() const noexcept
|
||||
{
|
||||
return _cellSize;
|
||||
@ -108,9 +124,8 @@ void ImageSlice::CopyBlock(const TextBuffer& srcBuffer, const til::rect srcRect,
|
||||
|
||||
void ImageSlice::CopyRow(const ROW& srcRow, ROW& dstRow)
|
||||
{
|
||||
const auto& srcSlice = srcRow.GetImageSlice();
|
||||
auto& dstSlice = dstRow.GetMutableImageSlice();
|
||||
dstSlice = srcSlice ? std::make_unique<ImageSlice>(*srcSlice) : nullptr;
|
||||
const auto srcSlice = srcRow.GetImageSlice();
|
||||
dstRow.SetImageSlice(srcSlice ? std::make_unique<ImageSlice>(*srcSlice) : nullptr);
|
||||
}
|
||||
|
||||
void ImageSlice::CopyCells(const ROW& srcRow, const til::CoordType srcColumn, ROW& dstRow, const til::CoordType dstColumnBegin, const til::CoordType dstColumnEnd)
|
||||
@ -119,24 +134,25 @@ void ImageSlice::CopyCells(const ROW& srcRow, const til::CoordType srcColumn, RO
|
||||
// a blank image into the destination, which is the same thing as an erase.
|
||||
// Also if the line renditions are different, there's no meaningful way to
|
||||
// copy the image content, so we also just treat that as an erase.
|
||||
const auto& srcSlice = srcRow.GetImageSlice();
|
||||
const auto srcSlice = srcRow.GetImageSlice();
|
||||
if (!srcSlice || srcRow.GetLineRendition() != dstRow.GetLineRendition()) [[likely]]
|
||||
{
|
||||
ImageSlice::EraseCells(dstRow, dstColumnBegin, dstColumnEnd);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto& dstSlice = dstRow.GetMutableImageSlice();
|
||||
auto dstSlice = dstRow.GetMutableImageSlice();
|
||||
if (!dstSlice)
|
||||
{
|
||||
dstSlice = std::make_unique<ImageSlice>(srcSlice->CellSize());
|
||||
dstSlice = dstRow.SetImageSlice(std::make_unique<ImageSlice>(srcSlice->CellSize()));
|
||||
__assume(dstSlice != nullptr);
|
||||
}
|
||||
const auto scale = srcRow.GetLineRendition() != LineRendition::SingleWidth ? 1 : 0;
|
||||
if (dstSlice->_copyCells(*srcSlice, srcColumn << scale, dstColumnBegin << scale, dstColumnEnd << scale))
|
||||
{
|
||||
// If _copyCells returns true, that means the destination was
|
||||
// completely erased, so we can delete this slice.
|
||||
dstSlice = nullptr;
|
||||
dstRow.SetImageSlice(nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -203,7 +219,7 @@ void ImageSlice::EraseCells(TextBuffer& buffer, const til::point at, const size_
|
||||
|
||||
void ImageSlice::EraseCells(ROW& row, const til::CoordType columnBegin, const til::CoordType columnEnd)
|
||||
{
|
||||
auto& imageSlice = row.GetMutableImageSlice();
|
||||
const auto imageSlice = row.GetMutableImageSlice();
|
||||
if (imageSlice) [[unlikely]]
|
||||
{
|
||||
const auto scale = row.GetLineRendition() != LineRendition::SingleWidth ? 1 : 0;
|
||||
@ -211,7 +227,7 @@ void ImageSlice::EraseCells(ROW& row, const til::CoordType columnBegin, const ti
|
||||
{
|
||||
// If _eraseCells returns true, that means the image was
|
||||
// completely erased, so we can delete this slice.
|
||||
imageSlice = nullptr;
|
||||
row.SetImageSlice(nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,9 @@ public:
|
||||
ImageSlice(const ImageSlice& rhs) = default;
|
||||
ImageSlice(const til::size cellSize) noexcept;
|
||||
|
||||
void BumpRevision() noexcept;
|
||||
uint64_t Revision() const noexcept;
|
||||
|
||||
til::size CellSize() const noexcept;
|
||||
til::CoordType ColumnOffset() const noexcept;
|
||||
til::CoordType PixelWidth() const noexcept;
|
||||
@ -45,6 +48,7 @@ private:
|
||||
bool _copyCells(const ImageSlice& srcSlice, const til::CoordType srcColumn, const til::CoordType dstColumnBegin, const til::CoordType dstColumnEnd);
|
||||
bool _eraseCells(const til::CoordType columnBegin, const til::CoordType columnEnd);
|
||||
|
||||
uint64_t _revision = 0;
|
||||
til::size _cellSize;
|
||||
std::vector<RGBQUAD> _pixelBuffer;
|
||||
til::CoordType _columnBegin = 0;
|
||||
|
||||
@ -965,14 +965,26 @@ std::vector<uint16_t> ROW::GetHyperlinks() const
|
||||
return ids;
|
||||
}
|
||||
|
||||
const ImageSlice::Pointer& ROW::GetImageSlice() const noexcept
|
||||
ImageSlice* ROW::SetImageSlice(ImageSlice::Pointer imageSlice) noexcept
|
||||
{
|
||||
return _imageSlice;
|
||||
_imageSlice = std::move(imageSlice);
|
||||
return GetMutableImageSlice();
|
||||
}
|
||||
|
||||
ImageSlice::Pointer& ROW::GetMutableImageSlice() noexcept
|
||||
const ImageSlice* ROW::GetImageSlice() const noexcept
|
||||
{
|
||||
return _imageSlice;
|
||||
return _imageSlice.get();
|
||||
}
|
||||
|
||||
ImageSlice* ROW::GetMutableImageSlice() noexcept
|
||||
{
|
||||
const auto ptr = _imageSlice.get();
|
||||
if (!ptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
ptr->BumpRevision();
|
||||
return ptr;
|
||||
}
|
||||
|
||||
uint16_t ROW::size() const noexcept
|
||||
|
||||
@ -152,8 +152,9 @@ public:
|
||||
const til::small_rle<TextAttribute, uint16_t, 1>& Attributes() const noexcept;
|
||||
TextAttribute GetAttrByColumn(til::CoordType column) const;
|
||||
std::vector<uint16_t> GetHyperlinks() const;
|
||||
const ImageSlice::Pointer& GetImageSlice() const noexcept;
|
||||
ImageSlice::Pointer& GetMutableImageSlice() noexcept;
|
||||
ImageSlice* SetImageSlice(ImageSlice::Pointer imageSlice) noexcept;
|
||||
const ImageSlice* GetImageSlice() const noexcept;
|
||||
ImageSlice* GetMutableImageSlice() noexcept;
|
||||
uint16_t size() const noexcept;
|
||||
til::CoordType GetLastNonSpaceColumn() const noexcept;
|
||||
til::CoordType MeasureLeft() const noexcept;
|
||||
@ -299,8 +300,6 @@ private:
|
||||
til::small_rle<TextAttribute, uint16_t, 1> _attr;
|
||||
// The width of the row in visual columns.
|
||||
uint16_t _columnCount = 0;
|
||||
// Stores any image content covering the row.
|
||||
ImageSlice::Pointer _imageSlice;
|
||||
// Stores double-width/height (DECSWL/DECDWL/DECDHL) attributes.
|
||||
LineRendition _lineRendition = LineRendition::SingleWidth;
|
||||
// Occurs when the user runs out of text in a given row and we're forced to wrap the cursor to the next line
|
||||
@ -309,6 +308,9 @@ private:
|
||||
bool _doubleBytePadded = false;
|
||||
|
||||
std::optional<ScrollbarData> _promptData = std::nullopt;
|
||||
|
||||
// Stores any image content covering the row.
|
||||
ImageSlice::Pointer _imageSlice;
|
||||
};
|
||||
|
||||
#ifdef UNIT_TESTING
|
||||
|
||||
@ -918,7 +918,7 @@ void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition, cons
|
||||
// If the line rendition has changed, the row can no longer be wrapped.
|
||||
row.SetWrapForced(false);
|
||||
// And all image content on the row is removed.
|
||||
row.GetMutableImageSlice().reset();
|
||||
row.SetImageSlice(nullptr);
|
||||
// And if it's no longer single width, the right half of the row should be erased.
|
||||
if (lineRendition != LineRendition::SingleWidth)
|
||||
{
|
||||
|
||||
@ -129,7 +129,8 @@ try
|
||||
};
|
||||
_p.invalidatedRows = _api.invalidatedRows;
|
||||
_p.cursorRect = {};
|
||||
_p.scrollOffset = _api.scrollOffset;
|
||||
_p.scrollOffsetX = _api.viewportOffset.x;
|
||||
_p.scrollDeltaY = _api.scrollOffset;
|
||||
|
||||
// This if condition serves 2 purposes:
|
||||
// * By setting top/bottom to the full height we ensure that we call Present() without
|
||||
@ -148,7 +149,7 @@ try
|
||||
_p.MarkAllAsDirty();
|
||||
#endif
|
||||
|
||||
if (const auto offset = _p.scrollOffset)
|
||||
if (const auto offset = _p.scrollDeltaY)
|
||||
{
|
||||
if (offset < 0)
|
||||
{
|
||||
@ -256,6 +257,14 @@ try
|
||||
{
|
||||
_flushBufferLine();
|
||||
|
||||
for (const auto r : _p.rows)
|
||||
{
|
||||
if (r->bitmap.revision != 0 && !r->bitmap.active)
|
||||
{
|
||||
r->bitmap = {};
|
||||
}
|
||||
}
|
||||
|
||||
// PaintCursor() is only called when the cursor is visible, but we need to invalidate the cursor area
|
||||
// even if it isn't. Otherwise a transition from a visible to an invisible cursor wouldn't be rendered.
|
||||
if (const auto r = _api.invalidatedCursorArea; r.non_empty())
|
||||
@ -520,10 +529,49 @@ try
|
||||
}
|
||||
CATCH_RETURN()
|
||||
|
||||
[[nodiscard]] HRESULT AtlasEngine::PaintImageSlice(const ImageSlice& /*imageSlice*/, const til::CoordType /*targetRow*/, const til::CoordType /*viewportLeft*/) noexcept
|
||||
[[nodiscard]] HRESULT AtlasEngine::PaintImageSlice(const ImageSlice& imageSlice, const til::CoordType targetRow, const til::CoordType viewportLeft) noexcept
|
||||
try
|
||||
{
|
||||
return S_FALSE;
|
||||
const auto y = clamp<til::CoordType>(targetRow, 0, _p.s->viewportCellCount.y - 1);
|
||||
const auto row = _p.rows[y];
|
||||
const auto revision = imageSlice.Revision();
|
||||
const auto srcWidth = std::max(0, imageSlice.PixelWidth());
|
||||
const auto srcCellSize = imageSlice.CellSize();
|
||||
auto& b = row->bitmap;
|
||||
|
||||
// If this row's ImageSlice has changed we need to update our snapshot.
|
||||
// Theoretically another _p.rows[y]->bitmap may have this particular revision already,
|
||||
// but that can only happen if we're scrolling _and_ the entire viewport was invalidated.
|
||||
if (b.revision != revision)
|
||||
{
|
||||
const auto srcHeight = std::max(0, srcCellSize.height);
|
||||
const auto pixels = imageSlice.Pixels();
|
||||
const auto expectedSize = gsl::narrow_cast<size_t>(srcWidth) * gsl::narrow_cast<size_t>(srcHeight);
|
||||
|
||||
// Sanity check.
|
||||
if (pixels.size() != expectedSize)
|
||||
{
|
||||
assert(false);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
if (b.source.size() != pixels.size())
|
||||
{
|
||||
b.source = Buffer<u32, 32>{ pixels.size() };
|
||||
}
|
||||
|
||||
memcpy(b.source.data(), pixels.data(), pixels.size_bytes());
|
||||
b.revision = revision;
|
||||
b.sourceSize.x = srcWidth;
|
||||
b.sourceSize.y = srcHeight;
|
||||
}
|
||||
|
||||
b.targetOffset = (imageSlice.ColumnOffset() - viewportLeft);
|
||||
b.targetWidth = srcWidth / srcCellSize.width;
|
||||
b.active = true;
|
||||
return S_OK;
|
||||
}
|
||||
CATCH_RETURN()
|
||||
|
||||
[[nodiscard]] HRESULT AtlasEngine::PaintSelection(const til::rect& rect) noexcept
|
||||
try
|
||||
|
||||
@ -471,9 +471,9 @@ void AtlasEngine::_present()
|
||||
params.DirtyRectsCount = 1;
|
||||
params.pDirtyRects = &dirtyRect;
|
||||
|
||||
if (_p.scrollOffset)
|
||||
if (_p.scrollDeltaY)
|
||||
{
|
||||
const auto offsetInPx = _p.scrollOffset * _p.s->font->cellSize.y;
|
||||
const auto offsetInPx = _p.scrollDeltaY * _p.s->font->cellSize.y;
|
||||
const auto width = _p.s->targetSize.x;
|
||||
// We don't use targetSize.y here, because "height" refers to the bottom coordinate of the last text row
|
||||
// in the buffer. We then add the "offsetInPx" (which is negative when scrolling text upwards) and thus
|
||||
|
||||
@ -292,6 +292,11 @@ void BackendD2D::_drawText(RenderingPayload& p)
|
||||
_drawTextResetLineRendition(row);
|
||||
}
|
||||
|
||||
if (row->bitmap.revision != 0)
|
||||
{
|
||||
_drawBitmap(p, row, y);
|
||||
}
|
||||
|
||||
if (p.invalidatedRows.contains(y))
|
||||
{
|
||||
dirtyTop = std::min(dirtyTop, row->dirtyTop);
|
||||
@ -745,6 +750,39 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro
|
||||
}
|
||||
}
|
||||
|
||||
void BackendD2D::_drawBitmap(const RenderingPayload& p, const ShapedRow* row, u16 y) const
|
||||
{
|
||||
const auto& b = row->bitmap;
|
||||
|
||||
// TODO: This could use some caching logic like BackendD3D.
|
||||
const D2D1_SIZE_U size{
|
||||
gsl::narrow_cast<UINT32>(b.sourceSize.x),
|
||||
gsl::narrow_cast<UINT32>(b.sourceSize.y),
|
||||
};
|
||||
const D2D1_BITMAP_PROPERTIES bitmapProperties{
|
||||
.pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED },
|
||||
.dpiX = static_cast<f32>(p.s->font->dpi),
|
||||
.dpiY = static_cast<f32>(p.s->font->dpi),
|
||||
};
|
||||
wil::com_ptr<ID2D1Bitmap> bitmap;
|
||||
THROW_IF_FAILED(_renderTarget->CreateBitmap(size, b.source.data(), static_cast<UINT32>(b.sourceSize.x) * 4, &bitmapProperties, bitmap.addressof()));
|
||||
|
||||
const i32 cellWidth = p.s->font->cellSize.x;
|
||||
const i32 cellHeight = p.s->font->cellSize.y;
|
||||
const auto left = (b.targetOffset - p.scrollOffsetX) * cellWidth;
|
||||
const auto right = left + b.targetWidth * cellWidth;
|
||||
const auto top = y * cellHeight;
|
||||
const auto bottom = left + cellHeight;
|
||||
|
||||
const D2D1_RECT_F rectF{
|
||||
static_cast<f32>(left),
|
||||
static_cast<f32>(top),
|
||||
static_cast<f32>(right),
|
||||
static_cast<f32>(bottom),
|
||||
};
|
||||
_renderTarget->DrawBitmap(bitmap.get(), &rectF, 1, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR);
|
||||
}
|
||||
|
||||
void BackendD2D::_drawCursorPart1(const RenderingPayload& p)
|
||||
{
|
||||
if (p.cursorRect.empty())
|
||||
@ -893,23 +931,25 @@ void BackendD2D::_drawSelection(const RenderingPayload& p)
|
||||
#if ATLAS_DEBUG_SHOW_DIRTY
|
||||
void BackendD2D::_debugShowDirty(const RenderingPayload& p)
|
||||
{
|
||||
if (p.dirtyRectInPx.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_presentRects[_presentRectsPos] = p.dirtyRectInPx;
|
||||
_presentRectsPos = (_presentRectsPos + 1) % std::size(_presentRects);
|
||||
|
||||
for (size_t i = 0; i < std::size(_presentRects); ++i)
|
||||
{
|
||||
const auto& rect = _presentRects[(_presentRectsPos + i) % std::size(_presentRects)];
|
||||
if (rect.non_empty())
|
||||
{
|
||||
const D2D1_RECT_F rectF{
|
||||
static_cast<f32>(rect.left),
|
||||
static_cast<f32>(rect.top),
|
||||
static_cast<f32>(rect.right),
|
||||
static_cast<f32>(rect.bottom),
|
||||
};
|
||||
const auto color = til::colorbrewer::pastel1[i] | 0x1f000000;
|
||||
_fillRectangle(rectF, color);
|
||||
}
|
||||
const D2D1_RECT_F rectF{
|
||||
static_cast<f32>(rect.left),
|
||||
static_cast<f32>(rect.top),
|
||||
static_cast<f32>(rect.right),
|
||||
static_cast<f32>(rect.bottom),
|
||||
};
|
||||
const auto color = til::colorbrewer::pastel1[i] | 0x1f000000;
|
||||
_fillRectangle(rectF, color);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -923,9 +963,12 @@ void BackendD2D::_debugDumpRenderTarget(const RenderingPayload& p)
|
||||
std::filesystem::create_directories(_dumpRenderTargetBasePath);
|
||||
}
|
||||
|
||||
wil::com_ptr<ID3D11Texture2D> buffer;
|
||||
THROW_IF_FAILED(p.swapChain.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(buffer.addressof())));
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
swprintf_s(path, L"%s\\%u_%08zu.png", &_dumpRenderTargetBasePath[0], GetCurrentProcessId(), _dumpRenderTargetCounter);
|
||||
SaveTextureToPNG(_deviceContext.get(), _swapChainManager.GetBuffer().get(), p.s->font->dpi, &path[0]);
|
||||
WIC::SaveTextureToPNG(p.deviceContext.get(), buffer.get(), p.s->font->dpi, &path[0]);
|
||||
_dumpRenderTargetCounter++;
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <til/flat_set.h>
|
||||
|
||||
#include "Backend.h"
|
||||
#include "BuiltinGlyphs.h"
|
||||
|
||||
@ -26,6 +28,7 @@ namespace Microsoft::Console::Render::Atlas
|
||||
ATLAS_ATTR_COLD void _drawTextResetLineRendition(const ShapedRow* row) const noexcept;
|
||||
ATLAS_ATTR_COLD f32r _getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 baselineX, f32 baselineY);
|
||||
ATLAS_ATTR_COLD void _drawGridlineRow(const RenderingPayload& p, const ShapedRow* row, u16 y);
|
||||
ATLAS_ATTR_COLD void _drawBitmap(const RenderingPayload& p, const ShapedRow* row, u16 y) const;
|
||||
void _drawCursorPart1(const RenderingPayload& p);
|
||||
void _drawCursorPart2(const RenderingPayload& p);
|
||||
static void _drawCursor(const RenderingPayload& p, ID2D1RenderTarget* renderTarget, D2D1_RECT_F rect, ID2D1Brush* brush) noexcept;
|
||||
|
||||
@ -740,7 +740,7 @@ void BackendD3D::_d2dEndDrawing()
|
||||
}
|
||||
}
|
||||
|
||||
void BackendD3D::_resetGlyphAtlas(const RenderingPayload& p)
|
||||
void BackendD3D::_resetGlyphAtlas(const RenderingPayload& p, u32 minWidth, u32 minHeight)
|
||||
{
|
||||
// The index returned by _BitScanReverse is undefined when the input is 0. We can simultaneously guard
|
||||
// against that and avoid unreasonably small textures, by clamping the min. texture size to `minArea`.
|
||||
@ -757,10 +757,8 @@ void BackendD3D::_resetGlyphAtlas(const RenderingPayload& p)
|
||||
// It's hard to say what the max. size of the cache should be. Optimally I think we should use as much
|
||||
// memory as is available, but the rendering code in this project is a big mess and so integrating
|
||||
// memory pressure feedback (RegisterVideoMemoryBudgetChangeNotificationEvent) is rather difficult.
|
||||
// As an alternative I'm using 1.25x the size of the swap chain. The 1.25x is there to avoid situations, where
|
||||
// we're locked into a state, where on every render pass we're starting with a half full atlas, drawing once,
|
||||
// filling it with the remaining half and drawing again, requiring two rendering passes on each frame.
|
||||
const auto maxAreaByFont = targetArea + targetArea / 4;
|
||||
// As an alternative I'm using 2x the size of the swap chain. This fits a screen full of glyphs and sixels.
|
||||
const auto maxAreaByFont = 2 * targetArea;
|
||||
|
||||
auto area = std::min(maxAreaByFont, std::max(minAreaByFont, minAreaByGrowth));
|
||||
area = clamp(area, minArea, maxArea);
|
||||
@ -771,8 +769,21 @@ void BackendD3D::_resetGlyphAtlas(const RenderingPayload& p)
|
||||
// every time you resize the window by a pixel. Instead it only grows/shrinks by a factor of 2.
|
||||
unsigned long index;
|
||||
_BitScanReverse(&index, area - 1);
|
||||
const auto u = static_cast<u16>(1u << ((index + 2) / 2));
|
||||
const auto v = static_cast<u16>(1u << ((index + 1) / 2));
|
||||
auto u = static_cast<u16>(1u << ((index + 2) / 2));
|
||||
auto v = static_cast<u16>(1u << ((index + 1) / 2));
|
||||
|
||||
// However, if we're asked for a specific minimum size, round up the u/v to the next power of 2 of the given size.
|
||||
// Because u/v cannot ever be less than sqrt(minArea), the _BitScanReverse() calls below cannot fail.
|
||||
if (u < minWidth)
|
||||
{
|
||||
_BitScanReverse(&index, minWidth - 1);
|
||||
u = 1u << (index + 1);
|
||||
}
|
||||
if (v < minHeight)
|
||||
{
|
||||
_BitScanReverse(&index, minHeight - 1);
|
||||
v = 1u << (index + 1);
|
||||
}
|
||||
|
||||
if (u != _rectPacker.width || v != _rectPacker.height)
|
||||
{
|
||||
@ -796,6 +807,7 @@ void BackendD3D::_resetGlyphAtlas(const RenderingPayload& p)
|
||||
{
|
||||
glyphs.clear();
|
||||
}
|
||||
_glyphAtlasBitmaps.clear();
|
||||
|
||||
_d2dBeginDrawing();
|
||||
_d2dRenderTarget->Clear();
|
||||
@ -1088,7 +1100,7 @@ void BackendD3D::_drawText(RenderingPayload& p)
|
||||
{
|
||||
if (_fontChangedResetGlyphAtlas)
|
||||
{
|
||||
_resetGlyphAtlas(p);
|
||||
_resetGlyphAtlas(p, 0, 0);
|
||||
}
|
||||
|
||||
til::CoordType dirtyTop = til::CoordTypeMax;
|
||||
@ -1189,6 +1201,11 @@ void BackendD3D::_drawText(RenderingPayload& p)
|
||||
_drawGridlines(p, y);
|
||||
}
|
||||
|
||||
if (row->bitmap.revision != 0)
|
||||
{
|
||||
_drawBitmap(p, row, y);
|
||||
}
|
||||
|
||||
if (p.invalidatedRows.contains(y))
|
||||
{
|
||||
dirtyTop = std::min(dirtyTop, row->dirtyTop);
|
||||
@ -1685,7 +1702,7 @@ void BackendD3D::_drawGlyphAtlasAllocate(const RenderingPayload& p, stbrp_rect&
|
||||
|
||||
_d2dEndDrawing();
|
||||
_flushQuads(p);
|
||||
_resetGlyphAtlas(p);
|
||||
_resetGlyphAtlas(p, rect.w, rect.h);
|
||||
|
||||
if (!stbrp_pack_rects(&_rectPacker, &rect, 1))
|
||||
{
|
||||
@ -1851,6 +1868,58 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
|
||||
}
|
||||
}
|
||||
|
||||
void BackendD3D::_drawBitmap(const RenderingPayload& p, const ShapedRow* row, u16 y)
|
||||
{
|
||||
const auto& b = row->bitmap;
|
||||
auto ab = _glyphAtlasBitmaps.lookup(b.revision);
|
||||
if (!ab)
|
||||
{
|
||||
stbrp_rect rect{
|
||||
.w = p.s->font->cellSize.x * b.targetWidth,
|
||||
.h = p.s->font->cellSize.y,
|
||||
};
|
||||
_drawGlyphAtlasAllocate(p, rect);
|
||||
_d2dBeginDrawing();
|
||||
|
||||
const D2D1_SIZE_U size{
|
||||
static_cast<UINT32>(b.sourceSize.x),
|
||||
static_cast<UINT32>(b.sourceSize.y),
|
||||
};
|
||||
const D2D1_BITMAP_PROPERTIES bitmapProperties{
|
||||
.pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED },
|
||||
.dpiX = static_cast<f32>(p.s->font->dpi),
|
||||
.dpiY = static_cast<f32>(p.s->font->dpi),
|
||||
};
|
||||
wil::com_ptr<ID2D1Bitmap> bitmap;
|
||||
THROW_IF_FAILED(_d2dRenderTarget->CreateBitmap(size, b.source.data(), static_cast<UINT32>(b.sourceSize.x) * 4, &bitmapProperties, bitmap.addressof()));
|
||||
|
||||
const D2D1_RECT_F rectF{
|
||||
static_cast<f32>(rect.x),
|
||||
static_cast<f32>(rect.y),
|
||||
static_cast<f32>(rect.x + rect.w),
|
||||
static_cast<f32>(rect.y + rect.h),
|
||||
};
|
||||
_d2dRenderTarget->DrawBitmap(bitmap.get(), &rectF, 1, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR);
|
||||
|
||||
ab = _glyphAtlasBitmaps.insert(b.revision).first;
|
||||
ab->size.x = static_cast<u16>(rect.w);
|
||||
ab->size.y = static_cast<u16>(rect.h);
|
||||
ab->texcoord.x = static_cast<u16>(rect.x);
|
||||
ab->texcoord.y = static_cast<u16>(rect.y);
|
||||
}
|
||||
|
||||
const auto left = p.s->font->cellSize.x * (b.targetOffset - p.scrollOffsetX);
|
||||
const auto top = p.s->font->cellSize.y * y;
|
||||
|
||||
_appendQuad() = {
|
||||
.shadingType = static_cast<u16>(ShadingType::TextPassthrough),
|
||||
.renditionScale = { 1, 1 },
|
||||
.position = { static_cast<i16>(left), static_cast<i16>(top) },
|
||||
.size = ab->size,
|
||||
.texcoord = ab->texcoord,
|
||||
};
|
||||
}
|
||||
|
||||
void BackendD3D::_drawCursorBackground(const RenderingPayload& p)
|
||||
{
|
||||
_cursorRects.clear();
|
||||
@ -2221,27 +2290,29 @@ void BackendD3D::_drawSelection(const RenderingPayload& p)
|
||||
void BackendD3D::_debugShowDirty(const RenderingPayload& p)
|
||||
{
|
||||
#if ATLAS_DEBUG_SHOW_DIRTY
|
||||
if (p.dirtyRectInPx.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_presentRects[_presentRectsPos] = p.dirtyRectInPx;
|
||||
_presentRectsPos = (_presentRectsPos + 1) % std::size(_presentRects);
|
||||
|
||||
for (size_t i = 0; i < std::size(_presentRects); ++i)
|
||||
{
|
||||
const auto& rect = _presentRects[(_presentRectsPos + i) % std::size(_presentRects)];
|
||||
if (rect.non_empty())
|
||||
{
|
||||
_appendQuad() = {
|
||||
.shadingType = static_cast<u16>(ShadingType::Selection),
|
||||
.position = {
|
||||
static_cast<i16>(rect.left),
|
||||
static_cast<i16>(rect.top),
|
||||
},
|
||||
.size = {
|
||||
static_cast<u16>(rect.right - rect.left),
|
||||
static_cast<u16>(rect.bottom - rect.top),
|
||||
},
|
||||
.color = til::colorbrewer::pastel1[i] | 0x1f000000,
|
||||
};
|
||||
}
|
||||
_appendQuad() = {
|
||||
.shadingType = static_cast<u16>(ShadingType::Selection),
|
||||
.position = {
|
||||
static_cast<i16>(rect.left),
|
||||
static_cast<i16>(rect.top),
|
||||
},
|
||||
.size = {
|
||||
static_cast<u16>(rect.right - rect.left),
|
||||
static_cast<u16>(rect.bottom - rect.top),
|
||||
},
|
||||
.color = til::colorbrewer::pastel1[i] | 0x1f000000,
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@ -2255,9 +2326,12 @@ void BackendD3D::_debugDumpRenderTarget(const RenderingPayload& p)
|
||||
std::filesystem::create_directories(_dumpRenderTargetBasePath);
|
||||
}
|
||||
|
||||
wil::com_ptr<ID3D11Texture2D> buffer;
|
||||
THROW_IF_FAILED(p.swapChain.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(buffer.addressof())));
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
swprintf_s(path, L"%s\\%u_%08zu.png", &_dumpRenderTargetBasePath[0], GetCurrentProcessId(), _dumpRenderTargetCounter);
|
||||
SaveTextureToPNG(p.deviceContext.get(), _swapChainManager.GetBuffer().get(), p.s->font->dpi, &path[0]);
|
||||
WIC::SaveTextureToPNG(p.deviceContext.get(), buffer.get(), p.s->font->dpi, &path[0]);
|
||||
_dumpRenderTargetCounter++;
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -180,6 +180,41 @@ namespace Microsoft::Console::Render::Atlas
|
||||
}
|
||||
};
|
||||
|
||||
struct AtlasBitmap
|
||||
{
|
||||
u64 key;
|
||||
u16x2 size;
|
||||
u16x2 texcoord;
|
||||
};
|
||||
|
||||
struct AtlasBitmapHashTrait
|
||||
{
|
||||
static bool occupied(const AtlasBitmap& entry) noexcept
|
||||
{
|
||||
return entry.key != 0;
|
||||
}
|
||||
|
||||
static constexpr size_t hash(const u64 key) noexcept
|
||||
{
|
||||
return til::flat_set_hash_integer(gsl::narrow_cast<size_t>(key));
|
||||
}
|
||||
|
||||
static size_t hash(const AtlasBitmap& entry) noexcept
|
||||
{
|
||||
return hash(entry.key);
|
||||
}
|
||||
|
||||
static bool equals(const AtlasBitmap& entry, const u64 key) noexcept
|
||||
{
|
||||
return entry.key == key;
|
||||
}
|
||||
|
||||
static void assign(AtlasBitmap& entry, u64 key) noexcept
|
||||
{
|
||||
entry.key = key;
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
struct CursorRect
|
||||
{
|
||||
@ -202,7 +237,7 @@ namespace Microsoft::Console::Render::Atlas
|
||||
void _debugDumpRenderTarget(const RenderingPayload& p);
|
||||
void _d2dBeginDrawing() noexcept;
|
||||
void _d2dEndDrawing();
|
||||
ATLAS_ATTR_COLD void _resetGlyphAtlas(const RenderingPayload& p);
|
||||
ATLAS_ATTR_COLD void _resetGlyphAtlas(const RenderingPayload& p, u32 minWidth, u32 minHeight);
|
||||
ATLAS_ATTR_COLD void _resizeGlyphAtlas(const RenderingPayload& p, u16 u, u16 v);
|
||||
static bool _checkMacTypeVersion(const RenderingPayload& p);
|
||||
QuadInstance& _getLastQuad() noexcept;
|
||||
@ -220,7 +255,8 @@ namespace Microsoft::Console::Render::Atlas
|
||||
void _drawGlyphAtlasAllocate(const RenderingPayload& p, stbrp_rect& rect);
|
||||
static AtlasGlyphEntry* _drawGlyphAllocateEntry(const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u32 glyphIndex);
|
||||
static void _splitDoubleHeightGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, AtlasGlyphEntry* glyphEntry);
|
||||
void _drawGridlines(const RenderingPayload& p, u16 y);
|
||||
ATLAS_ATTR_COLD void _drawGridlines(const RenderingPayload& p, u16 y);
|
||||
ATLAS_ATTR_COLD void _drawBitmap(const RenderingPayload& p, const ShapedRow* row, u16 y);
|
||||
void _drawCursorBackground(const RenderingPayload& p);
|
||||
ATLAS_ATTR_COLD void _drawCursorForeground();
|
||||
ATLAS_ATTR_COLD size_t _drawCursorForegroundSlowPath(const CursorRect& c, size_t offset);
|
||||
@ -260,6 +296,7 @@ namespace Microsoft::Console::Render::Atlas
|
||||
wil::com_ptr<ID3D11Texture2D> _glyphAtlas;
|
||||
wil::com_ptr<ID3D11ShaderResourceView> _glyphAtlasView;
|
||||
til::linear_flat_set<AtlasFontFaceEntry, AtlasFontFaceEntryHashTrait> _glyphAtlasMap;
|
||||
til::linear_flat_set<AtlasBitmap, AtlasBitmapHashTrait> _glyphAtlasBitmaps;
|
||||
AtlasFontFaceEntry _builtinGlyphs;
|
||||
Buffer<stbrp_node> _rectPackerData;
|
||||
stbrp_context _rectPacker{};
|
||||
|
||||
@ -448,6 +448,21 @@ namespace Microsoft::Console::Render::Atlas
|
||||
u16 to = 0;
|
||||
};
|
||||
|
||||
struct Bitmap
|
||||
{
|
||||
// Matches ImageSlice::Revision(). A revision of 0 means the bitmap is empty.
|
||||
u64 revision = 0;
|
||||
// The source RGBA data. Its size matches sourceSize exactly.
|
||||
Buffer<u32, 32> source;
|
||||
i32x2 sourceSize{};
|
||||
// Horizontal offset and width of the bitmap after scaling it (in columns).
|
||||
// The height is always the cell height.
|
||||
i32 targetOffset = 0;
|
||||
i32 targetWidth = 0;
|
||||
// This is used to track unused bitmaps, so that we can free them up.
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
struct ShapedRow
|
||||
{
|
||||
void Clear(u16 y, u16 cellHeight) noexcept
|
||||
@ -457,6 +472,7 @@ namespace Microsoft::Console::Render::Atlas
|
||||
glyphAdvances.clear();
|
||||
glyphOffsets.clear();
|
||||
colors.clear();
|
||||
bitmap.active = false;
|
||||
gridLineRanges.clear();
|
||||
lineRendition = LineRendition::SingleWidth;
|
||||
selectionFrom = 0;
|
||||
@ -477,6 +493,7 @@ namespace Microsoft::Console::Render::Atlas
|
||||
// Same size as glyphIndices.
|
||||
std::vector<u32> colors;
|
||||
|
||||
Bitmap bitmap;
|
||||
std::vector<GridLineRange> gridLineRanges;
|
||||
LineRendition lineRendition = LineRendition::SingleWidth;
|
||||
u16 selectionFrom = 0;
|
||||
@ -565,14 +582,16 @@ namespace Microsoft::Console::Render::Atlas
|
||||
i32r dirtyRectInPx{};
|
||||
// In rows.
|
||||
range<u16> invalidatedRows{};
|
||||
// In columns.
|
||||
i32 scrollOffsetX = 0;
|
||||
// In pixel.
|
||||
i16 scrollOffset = 0;
|
||||
i16 scrollDeltaY = 0;
|
||||
|
||||
void MarkAllAsDirty() noexcept
|
||||
{
|
||||
dirtyRectInPx = { 0, 0, s->targetSize.x, s->targetSize.y };
|
||||
invalidatedRows = { 0, s->viewportCellCount.y };
|
||||
scrollOffset = 0;
|
||||
scrollDeltaY = 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -837,7 +837,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
|
||||
_PaintBufferOutputHelper(pEngine, it, screenPosition, lineWrapped);
|
||||
|
||||
// Paint any image content on top of the text.
|
||||
const auto& imageSlice = buffer.GetRowByOffset(row).GetImageSlice();
|
||||
const auto imageSlice = buffer.GetRowByOffset(row).GetImageSlice();
|
||||
if (imageSlice) [[unlikely]]
|
||||
{
|
||||
LOG_IF_FAILED(pEngine->PaintImageSlice(*imageSlice, screenPosition.y, view.Left()));
|
||||
|
||||
@ -780,10 +780,11 @@ void SixelParser::_maybeFlushImageBuffer(const bool endOfSequence)
|
||||
if (rowOffset >= 0)
|
||||
{
|
||||
auto& dstRow = page.Buffer().GetMutableRowByOffset(rowOffset);
|
||||
auto& dstSlice = dstRow.GetMutableImageSlice();
|
||||
auto dstSlice = dstRow.GetMutableImageSlice();
|
||||
if (!dstSlice)
|
||||
{
|
||||
dstSlice = std::make_unique<ImageSlice>(_cellSize);
|
||||
dstSlice = dstRow.SetImageSlice(std::make_unique<ImageSlice>(_cellSize));
|
||||
__assume(dstSlice != nullptr);
|
||||
}
|
||||
auto dstIterator = dstSlice->MutablePixels(columnBegin, columnEnd);
|
||||
for (auto pixelRow = 0; pixelRow < _cellSize.height; pixelRow++)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user