Skip to content

AtlasEngine: Implement sixels #17581

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
54 changes: 50 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 = _p.s->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 @@ -144,7 +145,7 @@ try
_p.MarkAllAsDirty();
}

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

auto buffer = Buffer<u32, 32>{ pixels.size() };
memcpy(buffer.data(), pixels.data(), pixels.size_bytes());

b.revision = revision;
b.source = std::move(buffer);
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 @@ -282,6 +282,11 @@ void BackendD2D::_drawText(RenderingPayload& p)

_flushBuiltinGlyphs();

if (row->bitmap.revision != 0)
{
_drawBitmap(p, row, y);
}

if (!row->gridLineRanges.empty())
{
_drawGridlineRow(p, row, y);
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea I was curious why this wasn't just exactly the same

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot the exact reason, but something about building the hashmap was annoying so I decided to do that in a follow-up PR. The D2D renderer isn't particularly fast anyway, so I suspect the additional overhead here won't make it that much worse either (for now).

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
Loading