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:
Leonard Hecker 2024-07-23 21:39:12 +02:00 committed by GitHub
parent 6372baa0d3
commit 75f7ae4bec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 327 additions and 68 deletions

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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)
{

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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
}

View File

@ -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{};

View File

@ -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;
}
};

View File

@ -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()));

View File

@ -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++)