diff --git a/src/buffer/out/ImageSlice.cpp b/src/buffer/out/ImageSlice.cpp index 37aacb31ce2..4950682ee89 100644 --- a/src/buffer/out/ImageSlice.cpp +++ b/src/buffer/out/ImageSlice.cpp @@ -7,11 +7,27 @@ #include "Row.hpp" #include "textBuffer.hpp" +static std::atomic 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(*srcSlice) : nullptr; + const auto srcSlice = srcRow.GetImageSlice(); + dstRow.SetImageSlice(srcSlice ? std::make_unique(*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(srcSlice->CellSize()); + dstSlice = dstRow.SetImageSlice(std::make_unique(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); } } } diff --git a/src/buffer/out/ImageSlice.hpp b/src/buffer/out/ImageSlice.hpp index c17277851bb..87244abe78b 100644 --- a/src/buffer/out/ImageSlice.hpp +++ b/src/buffer/out/ImageSlice.hpp @@ -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; @@ -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 _pixelBuffer; til::CoordType _columnBegin = 0; diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index 7861af50d59..7a46215b6fc 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -965,14 +965,26 @@ std::vector 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 diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp index f4462f0488d..d2c19036baf 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -152,8 +152,9 @@ class ROW final const til::small_rle& Attributes() const noexcept; TextAttribute GetAttrByColumn(til::CoordType column) const; std::vector 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 @@ class ROW final til::small_rle _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 @@ class ROW final bool _doubleBytePadded = false; std::optional _promptData = std::nullopt; + + // Stores any image content covering the row. + ImageSlice::Pointer _imageSlice; }; #ifdef UNIT_TESTING diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 553365e4f5e..40e3405dcd4 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -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) { diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index 8464bef58a3..277d3818dc2 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -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(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(srcWidth) * gsl::narrow_cast(srcHeight); + + // Sanity check. + if (pixels.size() != expectedSize) + { + assert(false); + return S_OK; + } + + if (b.source.size() != pixels.size()) + { + b.source = Buffer{ 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 diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index ca665228345..2df96f0c620 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -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 diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index 36db637e360..00ceb28f22d 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -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(b.sourceSize.x), + gsl::narrow_cast(b.sourceSize.y), + }; + const D2D1_BITMAP_PROPERTIES bitmapProperties{ + .pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }, + .dpiX = static_cast(p.s->font->dpi), + .dpiY = static_cast(p.s->font->dpi), + }; + wil::com_ptr bitmap; + THROW_IF_FAILED(_renderTarget->CreateBitmap(size, b.source.data(), static_cast(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(left), + static_cast(top), + static_cast(right), + static_cast(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(rect.left), - static_cast(rect.top), - static_cast(rect.right), - static_cast(rect.bottom), - }; - const auto color = til::colorbrewer::pastel1[i] | 0x1f000000; - _fillRectangle(rectF, color); - } + const D2D1_RECT_F rectF{ + static_cast(rect.left), + static_cast(rect.top), + static_cast(rect.right), + static_cast(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 buffer; + THROW_IF_FAILED(p.swapChain.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(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 diff --git a/src/renderer/atlas/BackendD2D.h b/src/renderer/atlas/BackendD2D.h index 4206390ea52..c5f7dfbe7d2 100644 --- a/src/renderer/atlas/BackendD2D.h +++ b/src/renderer/atlas/BackendD2D.h @@ -3,6 +3,8 @@ #pragma once +#include + #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; diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index 33cb3147355..2e02e342863 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -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(1u << ((index + 2) / 2)); - const auto v = static_cast(1u << ((index + 1) / 2)); + auto u = static_cast(1u << ((index + 2) / 2)); + auto v = static_cast(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(b.sourceSize.x), + static_cast(b.sourceSize.y), + }; + const D2D1_BITMAP_PROPERTIES bitmapProperties{ + .pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }, + .dpiX = static_cast(p.s->font->dpi), + .dpiY = static_cast(p.s->font->dpi), + }; + wil::com_ptr bitmap; + THROW_IF_FAILED(_d2dRenderTarget->CreateBitmap(size, b.source.data(), static_cast(b.sourceSize.x) * 4, &bitmapProperties, bitmap.addressof())); + + const D2D1_RECT_F rectF{ + static_cast(rect.x), + static_cast(rect.y), + static_cast(rect.x + rect.w), + static_cast(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(rect.w); + ab->size.y = static_cast(rect.h); + ab->texcoord.x = static_cast(rect.x); + ab->texcoord.y = static_cast(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(ShadingType::TextPassthrough), + .renditionScale = { 1, 1 }, + .position = { static_cast(left), static_cast(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(ShadingType::Selection), - .position = { - static_cast(rect.left), - static_cast(rect.top), - }, - .size = { - static_cast(rect.right - rect.left), - static_cast(rect.bottom - rect.top), - }, - .color = til::colorbrewer::pastel1[i] | 0x1f000000, - }; - } + _appendQuad() = { + .shadingType = static_cast(ShadingType::Selection), + .position = { + static_cast(rect.left), + static_cast(rect.top), + }, + .size = { + static_cast(rect.right - rect.left), + static_cast(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 buffer; + THROW_IF_FAILED(p.swapChain.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(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 } diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h index d2712bb453f..cb1bfcccea4 100644 --- a/src/renderer/atlas/BackendD3D.h +++ b/src/renderer/atlas/BackendD3D.h @@ -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(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 _glyphAtlas; wil::com_ptr _glyphAtlasView; til::linear_flat_set _glyphAtlasMap; + til::linear_flat_set _glyphAtlasBitmaps; AtlasFontFaceEntry _builtinGlyphs; Buffer _rectPackerData; stbrp_context _rectPacker{}; diff --git a/src/renderer/atlas/common.h b/src/renderer/atlas/common.h index 270dd35f9dd..56cc1fce139 100644 --- a/src/renderer/atlas/common.h +++ b/src/renderer/atlas/common.h @@ -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 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 colors; + Bitmap bitmap; std::vector gridLineRanges; LineRendition lineRendition = LineRendition::SingleWidth; u16 selectionFrom = 0; @@ -565,14 +582,16 @@ namespace Microsoft::Console::Render::Atlas i32r dirtyRectInPx{}; // In rows. range 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; } }; diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 8e00beac1b3..511563a0e1c 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -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())); diff --git a/src/terminal/adapter/SixelParser.cpp b/src/terminal/adapter/SixelParser.cpp index baf37f75eec..f9dbdf6603a 100644 --- a/src/terminal/adapter/SixelParser.cpp +++ b/src/terminal/adapter/SixelParser.cpp @@ -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(_cellSize); + dstSlice = dstRow.SetImageSlice(std::make_unique(_cellSize)); + __assume(dstSlice != nullptr); } auto dstIterator = dstSlice->MutablePixels(columnBegin, columnEnd); for (auto pixelRow = 0; pixelRow < _cellSize.height; pixelRow++)