Skip to content

Commit

Permalink
AtlasEngine: Implement sixels (#17581)
Browse files Browse the repository at this point in the history
* 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 ✅
  • Loading branch information
lhecker authored Jul 23, 2024
1 parent 6372baa commit 75f7ae4
Show file tree
Hide file tree
Showing 14 changed files with 327 additions and 68 deletions.
34 changes: 25 additions & 9 deletions src/buffer/out/ImageSlice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -203,15 +219,15 @@ 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;
if (imageSlice->_eraseCells(columnBegin << scale, columnEnd << scale))
{
// If _eraseCells returns true, that means the image was
// completely erased, so we can delete this slice.
imageSlice = nullptr;
row.SetImageSlice(nullptr);
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/buffer/out/ImageSlice.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class ImageSlice
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;
Expand All @@ -45,6 +48,7 @@ class ImageSlice
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;
Expand Down
20 changes: 16 additions & 4 deletions src/buffer/out/Row.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions src/buffer/out/Row.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ class ROW final
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;
Expand Down Expand Up @@ -299,8 +300,6 @@ class ROW final
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
Expand All @@ -309,6 +308,9 @@ class ROW final
bool _doubleBytePadded = false;

std::optional<ScrollbarData> _promptData = std::nullopt;

// Stores any image content covering the row.
ImageSlice::Pointer _imageSlice;
};

#ifdef UNIT_TESTING
Expand Down
2 changes: 1 addition & 1 deletion src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
56 changes: 52 additions & 4 deletions src/renderer/atlas/AtlasEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -148,7 +149,7 @@ try
_p.MarkAllAsDirty();
#endif

if (const auto offset = _p.scrollOffset)
if (const auto offset = _p.scrollDeltaY)
{
if (offset < 0)
{
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/atlas/AtlasEngine.r.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 55 additions & 12 deletions src/renderer/atlas/BackendD2D.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/atlas/BackendD2D.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#pragma once

#include <til/flat_set.h>

#include "Backend.h"
#include "BuiltinGlyphs.h"

Expand All @@ -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;
Expand Down
Loading

0 comments on commit 75f7ae4

Please sign in to comment.