Skip to content

Commit d74b66a

Browse files
authored
AtlasEngine: Improve glyph generation performance (#13477)
#13458 added the ability to reuse tiles from our glyph atlas texture so that we stop running out of GPU memory for complex Unicode. This however can result in our glyph generation being a performance issue in edge cases, to the point that the application may feel outright unuseable. CJK glyphs for instance can easily exceed the maximum atlas texture size (twice the window size), but take a significant amount of CPU and GPU time to rasterize and draw, which results in "jelly scrolling" down to ~1 FPS. This PR improves the situation of the latter half by directly drawing glyphs into the texture atlas without an intermediate scratchpad texture. This reduces GPU usage by 96% on my system (33% -> 2%) which improves general render performance by ~100% (15 -> 30 FPS). CPU usage remains the same however, but that's not really something we can do anything about at this time. The atlas texture is already our primary means to reduce the CPU cost after all. ## Validation Steps Performed * Disable V-Sync for OpenConsole in NVIDIA Control Panel * Enable `debugGlyphGenerationPerformance` * Print the entire CJK block U+4E00..U+9FFF * Measure the above GPU usage and FPS improvements ✅ (Alternatively: Just scroll around and judge the "jellyness".)
1 parent 32379c2 commit d74b66a

File tree

3 files changed

+24
-78
lines changed

3 files changed

+24
-78
lines changed

src/renderer/atlas/AtlasEngine.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -929,8 +929,7 @@ void AtlasEngine::_recreateFontDependentResources()
929929
{
930930
// We're likely resizing the atlas anyways and can
931931
// thus also release any of these buffers prematurely.
932-
_r.d2dRenderTarget.reset(); // depends on _r.atlasScratchpad
933-
_r.atlasScratchpad.reset();
932+
_r.d2dRenderTarget.reset(); // depends on _r.atlasBuffer
934933
_r.atlasView.reset();
935934
_r.atlasBuffer.reset();
936935
}
@@ -970,8 +969,6 @@ void AtlasEngine::_recreateFontDependentResources()
970969
_r.strikethroughPos = _api.fontMetrics.strikethroughPos;
971970
_r.lineThickness = _api.fontMetrics.lineThickness;
972971
_r.dpi = _api.dpi;
973-
_r.maxEncounteredCellCount = 0;
974-
_r.scratchpadCellWidth = 0;
975972
}
976973
{
977974
// See AtlasEngine::UpdateFont.
@@ -1446,7 +1443,6 @@ void AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, size_t bufferPos1, si
14461443
const auto it = _r.glyphs.insert(std::move(key), std::move(value));
14471444
valueRef = &it->second;
14481445
_r.glyphQueue.emplace_back(&it->first, &it->second);
1449-
_r.maxEncounteredCellCount = std::max(_r.maxEncounteredCellCount, cellCount);
14501446
}
14511447

14521448
// For some reason MSVC doesn't understand that valueRef is overwritten in the branch above, resulting in:

src/renderer/atlas/AtlasEngine.h

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -857,11 +857,9 @@ namespace Microsoft::Console::Render
857857
void _setShaderResources() const;
858858
void _updateConstantBuffer() const noexcept;
859859
void _adjustAtlasSize();
860-
void _reserveScratchpadSize(u16 minWidth);
861860
void _processGlyphQueue();
862861
void _drawGlyph(const AtlasQueueItem& item) const;
863862
void _drawCursor();
864-
void _copyScratchpadTile(uint32_t scratchpadIndex, u16x2 target, uint32_t copyFlags = 0) const noexcept;
865863

866864
static constexpr bool debugGlyphGenerationPerformance = false;
867865
static constexpr bool debugGeneralPerformance = false || debugGlyphGenerationPerformance;
@@ -906,7 +904,6 @@ namespace Microsoft::Console::Render
906904
// D2D resources
907905
wil::com_ptr<ID3D11Texture2D> atlasBuffer;
908906
wil::com_ptr<ID3D11ShaderResourceView> atlasView;
909-
wil::com_ptr<ID3D11Texture2D> atlasScratchpad;
910907
wil::com_ptr<ID2D1RenderTarget> d2dRenderTarget;
911908
wil::com_ptr<ID2D1Brush> brush;
912909
wil::com_ptr<IDWriteTextFormat> textFormats[2][2];
@@ -921,8 +918,6 @@ namespace Microsoft::Console::Render
921918
u16 strikethroughPos = 0;
922919
u16 lineThickness = 0;
923920
u16 dpi = USER_DEFAULT_SCREEN_DPI; // invalidated by ApiInvalidations::Font, caches _api.dpi
924-
u16 maxEncounteredCellCount = 0;
925-
u16 scratchpadCellWidth = 0;
926921
u16x2 atlasSizeInPixel; // invalidated by ApiInvalidations::Font
927922
TileHashMap glyphs;
928923
TileAllocator tileAllocator;

src/renderer/atlas/AtlasEngine.r.cpp

Lines changed: 23 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ using namespace Microsoft::Console::Render;
3131
try
3232
{
3333
_adjustAtlasSize();
34-
_reserveScratchpadSize(_r.maxEncounteredCellCount);
3534
_processGlyphQueue();
3635

3736
if (WI_IsFlagSet(_r.invalidations, RenderInvalidations::Cursor))
@@ -86,6 +85,7 @@ try
8685
}
8786
catch (const wil::ResultException& exception)
8887
{
88+
// TODO: this writes to _api.
8989
return _handleException(exception);
9090
}
9191
CATCH_RETURN()
@@ -159,7 +159,7 @@ void AtlasEngine::_adjustAtlasSize()
159159
desc.ArraySize = 1;
160160
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
161161
desc.SampleDesc = { 1, 0 };
162-
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
162+
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
163163
THROW_IF_FAILED(_r.device->CreateTexture2D(&desc, nullptr, atlasBuffer.addressof()));
164164
THROW_IF_FAILED(_r.device->CreateShaderResourceView(atlasBuffer.get(), nullptr, atlasView.addressof()));
165165
}
@@ -184,38 +184,8 @@ void AtlasEngine::_adjustAtlasSize()
184184
_r.atlasView = std::move(atlasView);
185185
_setShaderResources();
186186

187-
WI_SetFlagIf(_r.invalidations, RenderInvalidations::Cursor, !copyFromExisting);
188-
}
189-
190-
void AtlasEngine::_reserveScratchpadSize(u16 minWidth)
191-
{
192-
if (minWidth <= _r.scratchpadCellWidth)
193-
{
194-
return;
195-
}
196-
197-
// The new size is the greater of ... cells wide:
198-
// * 2
199-
// * minWidth
200-
// * current size * 1.5
201-
const auto newWidth = std::max<UINT>(std::max<UINT>(2, minWidth), _r.scratchpadCellWidth + (_r.scratchpadCellWidth >> 1));
202-
203-
_r.d2dRenderTarget.reset();
204-
_r.atlasScratchpad.reset();
205-
206-
{
207-
D3D11_TEXTURE2D_DESC desc{};
208-
desc.Width = _r.cellSize.x * newWidth;
209-
desc.Height = _r.cellSize.y;
210-
desc.MipLevels = 1;
211-
desc.ArraySize = 1;
212-
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
213-
desc.SampleDesc = { 1, 0 };
214-
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
215-
THROW_IF_FAILED(_r.device->CreateTexture2D(&desc, nullptr, _r.atlasScratchpad.put()));
216-
}
217187
{
218-
const auto surface = _r.atlasScratchpad.query<IDXGISurface>();
188+
const auto surface = _r.atlasBuffer.query<IDXGISurface>();
219189

220190
wil::com_ptr<IDWriteRenderingParams1> renderingParams;
221191
DWrite_GetRenderParams(_sr.dwriteFactory.get(), &_r.gamma, &_r.cleartypeEnhancedContrast, &_r.grayscaleEnhancedContrast, renderingParams.addressof());
@@ -243,8 +213,8 @@ void AtlasEngine::_reserveScratchpadSize(u16 minWidth)
243213
_r.brush = brush.query<ID2D1Brush>();
244214
}
245215

246-
_r.scratchpadCellWidth = _r.maxEncounteredCellCount;
247216
WI_SetAllFlags(_r.invalidations, RenderInvalidations::ConstBuffer);
217+
WI_SetFlagIf(_r.invalidations, RenderInvalidations::Cursor, !copyFromExisting);
248218
}
249219

250220
void AtlasEngine::_processGlyphQueue()
@@ -254,10 +224,12 @@ void AtlasEngine::_processGlyphQueue()
254224
return;
255225
}
256226

227+
_r.d2dRenderTarget->BeginDraw();
257228
for (const auto& pair : _r.glyphQueue)
258229
{
259230
_drawGlyph(pair);
260231
}
232+
THROW_IF_FAILED(_r.d2dRenderTarget->EndDraw());
261233

262234
_r.glyphQueue.clear();
263235
}
@@ -280,7 +252,7 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const
280252
textLayout->SetTypography(_r.typography.get(), { 0, charsLength });
281253
}
282254

283-
auto options = D2D1_DRAW_TEXT_OPTIONS_NONE;
255+
auto options = D2D1_DRAW_TEXT_OPTIONS_CLIP;
284256
// D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT enables a bunch of internal machinery
285257
// which doesn't have to run if we know we can't use it anyways in the shader.
286258
WI_SetFlagIf(options, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT, coloredGlyph);
@@ -294,31 +266,29 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const
294266
_r.d2dRenderTarget->SetTextAntialiasMode(coloredGlyph ? D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE : D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE);
295267
}
296268

297-
_r.d2dRenderTarget->BeginDraw();
298-
// We could call
299-
// _r.d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED);
300-
// now to reduce the surface that needs to be cleared, but this decreases
301-
// performance by 10% (tested using debugGlyphGenerationPerformance).
302-
_r.d2dRenderTarget->Clear();
303-
_r.d2dRenderTarget->DrawTextLayout({}, textLayout.get(), _r.brush.get(), options);
304-
THROW_IF_FAILED(_r.d2dRenderTarget->EndDraw());
305-
306269
for (u32 i = 0; i < cells; ++i)
307270
{
308-
// Specifying NO_OVERWRITE means that the system can assume that existing references to the surface that
309-
// may be in flight on the GPU will not be affected by the update, so the copy can proceed immediately
310-
// (avoiding either a batch flush or the system maintaining multiple copies of the resource behind the scenes).
311-
//
312-
// Since our shader only draws whatever is in the atlas, and since we don't replace glyph tiles that are in use,
313-
// we can safely (?) tell the GPU that we don't overwrite parts of our atlas that are in use.
314-
_copyScratchpadTile(i, coords[i], D3D11_COPY_NO_OVERWRITE);
271+
const auto coord = coords[i];
272+
273+
D2D1_RECT_F rect;
274+
rect.left = static_cast<float>(coord.x) * static_cast<float>(USER_DEFAULT_SCREEN_DPI) / static_cast<float>(_r.dpi);
275+
rect.top = static_cast<float>(coord.y) * static_cast<float>(USER_DEFAULT_SCREEN_DPI) / static_cast<float>(_r.dpi);
276+
rect.right = rect.left + _r.cellSizeDIP.x;
277+
rect.bottom = rect.top + _r.cellSizeDIP.y;
278+
279+
D2D1_POINT_2F origin;
280+
origin.x = rect.left - i * _r.cellSizeDIP.x;
281+
origin.y = rect.top;
282+
283+
_r.d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED);
284+
_r.d2dRenderTarget->Clear();
285+
_r.d2dRenderTarget->DrawTextLayout(origin, textLayout.get(), _r.brush.get(), options);
286+
_r.d2dRenderTarget->PopAxisAlignedClip();
315287
}
316288
}
317289

318290
void AtlasEngine::_drawCursor()
319291
{
320-
_reserveScratchpadSize(1);
321-
322292
// lineWidth is in D2D's DIPs. For instance if we have a 150-200% zoom scale we want to draw a 2px wide line.
323293
// At 150% scale lineWidth thus needs to be 1.33333... because at a zoom scale of 1.5 this results in a 2px wide line.
324294
const auto lineWidth = std::max(1.0f, static_cast<float>((_r.dpi + USER_DEFAULT_SCREEN_DPI / 2) / USER_DEFAULT_SCREEN_DPI * USER_DEFAULT_SCREEN_DPI) / static_cast<float>(_r.dpi));
@@ -377,19 +347,4 @@ void AtlasEngine::_drawCursor()
377347
}
378348

379349
THROW_IF_FAILED(_r.d2dRenderTarget->EndDraw());
380-
381-
_copyScratchpadTile(0, {});
382-
}
383-
384-
void AtlasEngine::_copyScratchpadTile(uint32_t scratchpadIndex, u16x2 target, uint32_t copyFlags) const noexcept
385-
{
386-
D3D11_BOX box;
387-
box.left = scratchpadIndex * _r.cellSize.x;
388-
box.top = 0;
389-
box.front = 0;
390-
box.right = box.left + _r.cellSize.x;
391-
box.bottom = _r.cellSize.y;
392-
box.back = 1;
393-
#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function '...' which may throw exceptions (f.6).
394-
_r.deviceContext->CopySubresourceRegion1(_r.atlasBuffer.get(), 0, target.x, target.y, 0, _r.atlasScratchpad.get(), 0, &box, copyFlags);
395350
}

0 commit comments

Comments
 (0)