Rewrite AtlasEngine to allow arbitrary overhangs (#14959)

This is practically a from scratch rewrite of AtlasEngine.

The initial approach used a very classic monospace text renderer, where
the viewport is subdivided into cells and each cell is assigned one
glyph texture, just like how real terminals used to work.
While we knew that it would have problems with overly large glyphs,
like those found in less often used languages, we didn't expect the
absolutely massive number of fonts that this approach would break.
For one, the assumption that monospace fonts are actually mostly
monospace has turned out to be a complete lie and we can't force users
to use better designed fonts. But more importantly, we can't just
design an entire Unicode fallback font collection from scratch where
every major glyph is monospace either. This is especially problematic
for vertical overhangs which are extremely difficult to handle in a
way that outperforms the much simpler alternative approach:
Just implementing a bog-standard, modern, quad-based text renderer.

Such an approach is both, less code and runs faster due to a less
complex CPU-side. The text shaping engine (in our case DirectWrite)
has to resolve text into glyph indices anyways, so using them directly
for text rendering allows reduces the effort of turning it back into
text ranges and hashing those. It's memory overhead is also reduced,
because we can now break up long ligatures into their individual glyphs.
Especially on AMD APUs I found this approach to run much faster.

A list of issues I think are either obsolete (and could be closed)
or resolved with this PR in combination with #14255:

Closes #6864
Closes #6974
Closes #8993
Closes #9940
Closes #10128
Closes #12537
Closes #13064
Closes #13527
Closes #13662
Closes #13700
Closes #13989
Closes #14022
Closes #14057
Closes #14094
Closes #14098
Closes #14117
Closes #14533
Closes #14877

## PR Checklist
* Enabling software rendering enables D2D mode 
* Both D2D and D3D:
  * Background appears correctly 
  * Text appears correctly
    * Cascadia Code Regular 
    * Cascadia Code Bold 
    * Cascadia Code Italic 
    * Cascadia Code block chars leave (almost) no gaps 
    * Terminus TTF at 13.5pt leaves no gaps between block chars 
    * ``"`u{e0b2}`u{e0b0}"`` in Fira Code Nerd Font forms a square 
  * Cursor appears correctly
    * Legacy small/medium/large 
    * Vertical bar 
    * Underscore 
    * Empty box 
    * Full box 
    * Double underscore 
  * Changing the cursor color works 
  * Selection appears correctly 
  * Scrolling in various manners always renders correctly 
  * Changing the text antialising mode works 
  * Semi-transparent backgrounds work 
  * Scroll-zooming the font size works 
  * Double-size characters work 
  * Resizing while text is printing works 
  * DWM `+Heatmap_ShowDirtyRegions` shows that only the cursor
    region is dirty when it's blinking 
* D2D
  * Margins are filled with background color 
    They're filled with the neighboring's cell background color for
    convenience, as D2D doesn't support `D3D11_TEXTURE_ADDRESS_BORDER`
* D3D
  * Margins are filled with background color 
  * Soft fonts work 
  * Custom shaders enable continous redraw if time constant is used 
  * Retro shader appears correctly 
  * Resizing while a custom shader is running works 
This commit is contained in:
Leonard Hecker 2023-04-26 14:02:51 +02:00 коммит произвёл GitHub
Родитель 405fb51201
Коммит 2e3d5e658e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
34 изменённых файлов: 6248 добавлений и 3691 удалений

1
.github/actions/spelling/allow/apis.txt поставляемый
Просмотреть файл

@ -214,6 +214,7 @@ userenv
USEROBJECTFLAGS
Viewbox
virtualalloc
vsnwprintf
wcsstr
wcstoui
WDJ

1
.github/actions/spelling/expect/expect.txt поставляемый
Просмотреть файл

@ -1784,6 +1784,7 @@ STARTWPARMS
STARTWPARMSA
STARTWPARMSW
Statusline
stb
stdafx
STDAPI
stdc

Просмотреть файл

@ -276,6 +276,31 @@ OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
```
## stb
**Source**: [https://github.com/nothings/stb](https://github.com/nothings/stb)
### License
```
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## ConEmu
**Source**: [https://github.com/Maximus5/ConEmu](https://github.com/Maximus5/ConEmu)

37
oss/stb/LICENSE Normal file
Просмотреть файл

@ -0,0 +1,37 @@
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2017 Sean Barrett
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Просмотреть файл

@ -0,0 +1,4 @@
### Notes for Future Maintainers
Search for files prefixed with `stb_` in this project.
At the time of writing, the only file being used is `stb_rect_pack.h`.

15
oss/stb/cgmanifest.json Normal file
Просмотреть файл

@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/component-detection-manifest.json",
"Registrations": [
{
"component": {
"type": "git",
"git": {
"repositoryUrl": "https://github.com/nothings/stb",
"commitHash": "5736b15f7ea0ffb08dd38af21067c314d6a3aae9"
}
}
}
],
"Version": 1
}

623
oss/stb/stb_rect_pack.h Normal file
Просмотреть файл

@ -0,0 +1,623 @@
// stb_rect_pack.h - v1.01 - public domain - rectangle packing
// Sean Barrett 2014
//
// Useful for e.g. packing rectangular textures into an atlas.
// Does not do rotation.
//
// Before #including,
//
// #define STB_RECT_PACK_IMPLEMENTATION
//
// in the file that you want to have the implementation.
//
// Not necessarily the awesomest packing method, but better than
// the totally naive one in stb_truetype (which is primarily what
// this is meant to replace).
//
// Has only had a few tests run, may have issues.
//
// More docs to come.
//
// No memory allocations; uses qsort() and assert() from stdlib.
// Can override those by defining STBRP_SORT and STBRP_ASSERT.
//
// This library currently uses the Skyline Bottom-Left algorithm.
//
// Please note: better rectangle packers are welcome! Please
// implement them to the same API, but with a different init
// function.
//
// Credits
//
// Library
// Sean Barrett
// Minor features
// Martins Mozeiko
// github:IntellectualKitty
//
// Bugfixes / warning fixes
// Jeremy Jaussaud
// Fabian Giesen
//
// Version history:
//
// 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section
// 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles
// 0.99 (2019-02-07) warning fixes
// 0.11 (2017-03-03) return packing success/fail result
// 0.10 (2016-10-25) remove cast-away-const to avoid warnings
// 0.09 (2016-08-27) fix compiler warnings
// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0)
// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0)
// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort
// 0.05: added STBRP_ASSERT to allow replacing assert
// 0.04: fixed minor bug in STBRP_LARGE_RECTS support
// 0.01: initial release
//
// LICENSE
//
// See end of file for license information.
//////////////////////////////////////////////////////////////////////////////
//
// INCLUDE SECTION
//
#ifndef STB_INCLUDE_STB_RECT_PACK_H
#define STB_INCLUDE_STB_RECT_PACK_H
#define STB_RECT_PACK_VERSION 1
#ifdef STBRP_STATIC
#define STBRP_DEF static
#else
#define STBRP_DEF extern
#endif
#ifdef __cplusplus
extern "C" {
#endif
typedef struct stbrp_context stbrp_context;
typedef struct stbrp_node stbrp_node;
typedef struct stbrp_rect stbrp_rect;
typedef int stbrp_coord;
#define STBRP__MAXVAL 0x7fffffff
// Mostly for internal use, but this is the maximum supported coordinate value.
STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects);
// Assign packed locations to rectangles. The rectangles are of type
// 'stbrp_rect' defined below, stored in the array 'rects', and there
// are 'num_rects' many of them.
//
// Rectangles which are successfully packed have the 'was_packed' flag
// set to a non-zero value and 'x' and 'y' store the minimum location
// on each axis (i.e. bottom-left in cartesian coordinates, top-left
// if you imagine y increasing downwards). Rectangles which do not fit
// have the 'was_packed' flag set to 0.
//
// You should not try to access the 'rects' array from another thread
// while this function is running, as the function temporarily reorders
// the array while it executes.
//
// To pack into another rectangle, you need to call stbrp_init_target
// again. To continue packing into the same rectangle, you can call
// this function again. Calling this multiple times with multiple rect
// arrays will probably produce worse packing results than calling it
// a single time with the full rectangle array, but the option is
// available.
//
// The function returns 1 if all of the rectangles were successfully
// packed and 0 otherwise.
struct stbrp_rect
{
// reserved for your use:
int id;
// input:
stbrp_coord w, h;
// output:
stbrp_coord x, y;
int was_packed; // non-zero if valid packing
}; // 16 bytes, nominally
STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes);
// Initialize a rectangle packer to:
// pack a rectangle that is 'width' by 'height' in dimensions
// using temporary storage provided by the array 'nodes', which is 'num_nodes' long
//
// You must call this function every time you start packing into a new target.
//
// There is no "shutdown" function. The 'nodes' memory must stay valid for
// the following stbrp_pack_rects() call (or calls), but can be freed after
// the call (or calls) finish.
//
// Note: to guarantee best results, either:
// 1. make sure 'num_nodes' >= 'width'
// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1'
//
// If you don't do either of the above things, widths will be quantized to multiples
// of small integers to guarantee the algorithm doesn't run out of temporary storage.
//
// If you do #2, then the non-quantized algorithm will be used, but the algorithm
// may run out of temporary storage and be unable to pack some rectangles.
STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem);
// Optionally call this function after init but before doing any packing to
// change the handling of the out-of-temp-memory scenario, described above.
// If you call init again, this will be reset to the default (false).
STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic);
// Optionally select which packing heuristic the library should use. Different
// heuristics will produce better/worse results for different data sets.
// If you call init again, this will be reset to the default.
enum
{
STBRP_HEURISTIC_Skyline_default=0,
STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default,
STBRP_HEURISTIC_Skyline_BF_sortHeight
};
//////////////////////////////////////////////////////////////////////////////
//
// the details of the following structures don't matter to you, but they must
// be visible so you can handle the memory allocations for them
struct stbrp_node
{
stbrp_coord x,y;
stbrp_node *next;
};
struct stbrp_context
{
int width;
int height;
int align;
int init_mode;
int heuristic;
int num_nodes;
stbrp_node *active_head;
stbrp_node *free_head;
stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2'
};
#ifdef __cplusplus
}
#endif
#endif
//////////////////////////////////////////////////////////////////////////////
//
// IMPLEMENTATION SECTION
//
#ifdef STB_RECT_PACK_IMPLEMENTATION
#ifndef STBRP_SORT
#include <stdlib.h>
#define STBRP_SORT qsort
#endif
#ifndef STBRP_ASSERT
#include <assert.h>
#define STBRP_ASSERT assert
#endif
#ifdef _MSC_VER
#define STBRP__NOTUSED(v) (void)(v)
#define STBRP__CDECL __cdecl
#else
#define STBRP__NOTUSED(v) (void)sizeof(v)
#define STBRP__CDECL
#endif
enum
{
STBRP__INIT_skyline = 1
};
STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic)
{
switch (context->init_mode) {
case STBRP__INIT_skyline:
STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight);
context->heuristic = heuristic;
break;
default:
STBRP_ASSERT(0);
}
}
STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem)
{
if (allow_out_of_mem)
// if it's ok to run out of memory, then don't bother aligning them;
// this gives better packing, but may fail due to OOM (even though
// the rectangles easily fit). @TODO a smarter approach would be to only
// quantize once we've hit OOM, then we could get rid of this parameter.
context->align = 1;
else {
// if it's not ok to run out of memory, then quantize the widths
// so that num_nodes is always enough nodes.
//
// I.e. num_nodes * align >= width
// align >= width / num_nodes
// align = ceil(width/num_nodes)
context->align = (context->width + context->num_nodes-1) / context->num_nodes;
}
}
STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes)
{
int i;
for (i=0; i < num_nodes-1; ++i)
nodes[i].next = &nodes[i+1];
nodes[i].next = NULL;
context->init_mode = STBRP__INIT_skyline;
context->heuristic = STBRP_HEURISTIC_Skyline_default;
context->free_head = &nodes[0];
context->active_head = &context->extra[0];
context->width = width;
context->height = height;
context->num_nodes = num_nodes;
stbrp_setup_allow_out_of_mem(context, 0);
// node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly)
context->extra[0].x = 0;
context->extra[0].y = 0;
context->extra[0].next = &context->extra[1];
context->extra[1].x = (stbrp_coord) width;
context->extra[1].y = (1<<30);
context->extra[1].next = NULL;
}
// find minimum y position if it starts at x1
static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste)
{
stbrp_node *node = first;
int x1 = x0 + width;
int min_y, visited_width, waste_area;
STBRP__NOTUSED(c);
STBRP_ASSERT(first->x <= x0);
#if 0
// skip in case we're past the node
while (node->next->x <= x0)
++node;
#else
STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency
#endif
STBRP_ASSERT(node->x <= x0);
min_y = 0;
waste_area = 0;
visited_width = 0;
while (node->x < x1) {
if (node->y > min_y) {
// raise min_y higher.
// we've accounted for all waste up to min_y,
// but we'll now add more waste for everything we've visted
waste_area += visited_width * (node->y - min_y);
min_y = node->y;
// the first time through, visited_width might be reduced
if (node->x < x0)
visited_width += node->next->x - x0;
else
visited_width += node->next->x - node->x;
} else {
// add waste area
int under_width = node->next->x - node->x;
if (under_width + visited_width > width)
under_width = width - visited_width;
waste_area += under_width * (min_y - node->y);
visited_width += under_width;
}
node = node->next;
}
*pwaste = waste_area;
return min_y;
}
typedef struct
{
int x,y;
stbrp_node **prev_link;
} stbrp__findresult;
static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height)
{
int best_waste = (1<<30), best_x, best_y = (1 << 30);
stbrp__findresult fr;
stbrp_node **prev, *node, *tail, **best = NULL;
// align to multiple of c->align
width = (width + c->align - 1);
width -= width % c->align;
STBRP_ASSERT(width % c->align == 0);
// if it can't possibly fit, bail immediately
if (width > c->width || height > c->height) {
fr.prev_link = NULL;
fr.x = fr.y = 0;
return fr;
}
node = c->active_head;
prev = &c->active_head;
while (node->x + width <= c->width) {
int y,waste;
y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste);
if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL
// bottom left
if (y < best_y) {
best_y = y;
best = prev;
}
} else {
// best-fit
if (y + height <= c->height) {
// can only use it if it first vertically
if (y < best_y || (y == best_y && waste < best_waste)) {
best_y = y;
best_waste = waste;
best = prev;
}
}
}
prev = &node->next;
node = node->next;
}
best_x = (best == NULL) ? 0 : (*best)->x;
// if doing best-fit (BF), we also have to try aligning right edge to each node position
//
// e.g, if fitting
//
// ____________________
// |____________________|
//
// into
//
// | |
// | ____________|
// |____________|
//
// then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned
//
// This makes BF take about 2x the time
if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) {
tail = c->active_head;
node = c->active_head;
prev = &c->active_head;
// find first node that's admissible
while (tail->x < width)
tail = tail->next;
while (tail) {
int xpos = tail->x - width;
int y,waste;
STBRP_ASSERT(xpos >= 0);
// find the left position that matches this
while (node->next->x <= xpos) {
prev = &node->next;
node = node->next;
}
STBRP_ASSERT(node->next->x > xpos && node->x <= xpos);
y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste);
if (y + height <= c->height) {
if (y <= best_y) {
if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) {
best_x = xpos;
STBRP_ASSERT(y <= best_y);
best_y = y;
best_waste = waste;
best = prev;
}
}
}
tail = tail->next;
}
}
fr.prev_link = best;
fr.x = best_x;
fr.y = best_y;
return fr;
}
static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height)
{
// find best position according to heuristic
stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height);
stbrp_node *node, *cur;
// bail if:
// 1. it failed
// 2. the best node doesn't fit (we don't always check this)
// 3. we're out of memory
if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) {
res.prev_link = NULL;
return res;
}
// on success, create new node
node = context->free_head;
node->x = (stbrp_coord) res.x;
node->y = (stbrp_coord) (res.y + height);
context->free_head = node->next;
// insert the new node into the right starting point, and
// let 'cur' point to the remaining nodes needing to be
// stiched back in
cur = *res.prev_link;
if (cur->x < res.x) {
// preserve the existing one, so start testing with the next one
stbrp_node *next = cur->next;
cur->next = node;
cur = next;
} else {
*res.prev_link = node;
}
// from here, traverse cur and free the nodes, until we get to one
// that shouldn't be freed
while (cur->next && cur->next->x <= res.x + width) {
stbrp_node *next = cur->next;
// move the current node to the free list
cur->next = context->free_head;
context->free_head = cur;
cur = next;
}
// stitch the list back in
node->next = cur;
if (cur->x < res.x + width)
cur->x = (stbrp_coord) (res.x + width);
#ifdef _DEBUG
cur = context->active_head;
while (cur->x < context->width) {
STBRP_ASSERT(cur->x < cur->next->x);
cur = cur->next;
}
STBRP_ASSERT(cur->next == NULL);
{
int count=0;
cur = context->active_head;
while (cur) {
cur = cur->next;
++count;
}
cur = context->free_head;
while (cur) {
cur = cur->next;
++count;
}
STBRP_ASSERT(count == context->num_nodes+2);
}
#endif
return res;
}
static int STBRP__CDECL rect_height_compare(const void *a, const void *b)
{
const stbrp_rect *p = (const stbrp_rect *) a;
const stbrp_rect *q = (const stbrp_rect *) b;
if (p->h > q->h)
return -1;
if (p->h < q->h)
return 1;
return (p->w > q->w) ? -1 : (p->w < q->w);
}
static int STBRP__CDECL rect_original_order(const void *a, const void *b)
{
const stbrp_rect *p = (const stbrp_rect *) a;
const stbrp_rect *q = (const stbrp_rect *) b;
return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed);
}
STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects)
{
int i, all_rects_packed = 1;
// we use the 'was_packed' field internally to allow sorting/unsorting
for (i=0; i < num_rects; ++i) {
rects[i].was_packed = i;
}
// sort according to heuristic
STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare);
for (i=0; i < num_rects; ++i) {
if (rects[i].w == 0 || rects[i].h == 0) {
rects[i].x = rects[i].y = 0; // empty rect needs no space
} else {
stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h);
if (fr.prev_link) {
rects[i].x = (stbrp_coord) fr.x;
rects[i].y = (stbrp_coord) fr.y;
} else {
rects[i].x = rects[i].y = STBRP__MAXVAL;
}
}
}
// unsort
STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order);
// set was_packed flags and all_rects_packed status
for (i=0; i < num_rects; ++i) {
rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL);
if (!rects[i].was_packed)
all_rects_packed = 0;
}
// return the all_rects_packed status
return all_rects_packed;
}
#endif
/*
------------------------------------------------------------------------------
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2017 Sean Barrett
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
*/

Просмотреть файл

@ -16,9 +16,14 @@ Author(s):
#include "../inc/IConsoleWindow.hpp"
namespace Microsoft::Console::Render
namespace Microsoft::Console::Render::Atlas
{
class AtlasEngine;
}
namespace Microsoft::Console::Render
{
using AtlasEngine = Atlas::AtlasEngine;
class DxEngine;
class GdiEngine;
}

Просмотреть файл

@ -4,6 +4,7 @@
#include "pch.h"
#include "AtlasEngine.h"
#include "Backend.h"
#include "../base/FontCache.h"
// #### NOTE ####
@ -20,7 +21,7 @@
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2).
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::Render::Atlas;
// Like gsl::narrow but returns a HRESULT.
#pragma warning(push)
@ -34,7 +35,7 @@ constexpr HRESULT api_narrow(U val, T& out) noexcept
#pragma warning(pop)
template<typename T, typename U>
constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2<T>& out) noexcept
constexpr HRESULT vec2_narrow(U x, U y, vec2<T>& out) noexcept
{
return api_narrow(x, out.x) | api_narrow(y, out.y);
}
@ -46,8 +47,8 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2<T>& out) noexcept
//assert(psrRegion->top < psrRegion->bottom && psrRegion->top >= 0 && psrRegion->bottom <= _api.cellCount.y);
// BeginPaint() protects against invalid out of bounds numbers.
_api.invalidatedRows.x = std::min(_api.invalidatedRows.x, gsl::narrow_cast<u16>(psrRegion->top));
_api.invalidatedRows.y = std::max(_api.invalidatedRows.y, gsl::narrow_cast<u16>(psrRegion->bottom));
_api.invalidatedRows.start = std::min(_api.invalidatedRows.start, gsl::narrow_cast<u16>(psrRegion->top));
_api.invalidatedRows.end = std::max(_api.invalidatedRows.end, gsl::narrow_cast<u16>(psrRegion->bottom));
return S_OK;
}
@ -71,8 +72,8 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2<T>& out) noexcept
[[nodiscard]] HRESULT AtlasEngine::InvalidateSystem(const til::rect* const prcDirtyClient) noexcept
{
const auto top = prcDirtyClient->top / _api.fontMetrics.cellSize.y;
const auto bottom = prcDirtyClient->bottom / _api.fontMetrics.cellSize.y;
const auto top = prcDirtyClient->top / _api.s->font->cellSize.y;
const auto bottom = prcDirtyClient->bottom / _api.s->font->cellSize.y;
// BeginPaint() protects against invalid out of bounds numbers.
til::rect rect;
@ -88,8 +89,8 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2<T>& out) noexcept
// BeginPaint() protects against invalid out of bounds numbers.
// TODO: rect can contain invalid out of bounds coordinates when the selection is being
// dragged outside of the viewport (and the window begins scrolling automatically).
_api.invalidatedRows.x = gsl::narrow_cast<u16>(std::min<int>(_api.invalidatedRows.x, std::max<int>(0, rect.top)));
_api.invalidatedRows.y = gsl::narrow_cast<u16>(std::max<int>(_api.invalidatedRows.y, std::max<int>(0, rect.bottom)));
_api.invalidatedRows.start = gsl::narrow_cast<u16>(std::min<int>(_api.invalidatedRows.start, std::max<int>(0, rect.top)));
_api.invalidatedRows.end = gsl::narrow_cast<u16>(std::max<int>(_api.invalidatedRows.end, std::max<int>(0, rect.bottom)));
}
return S_OK;
}
@ -117,13 +118,13 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2<T>& out) noexcept
if (delta < 0)
{
_api.invalidatedRows.x = gsl::narrow_cast<u16>(clamp<int>(_api.invalidatedRows.x + delta, u16min, u16max));
_api.invalidatedRows.y = _api.cellCount.y;
_api.invalidatedRows.start = gsl::narrow_cast<u16>(clamp<int>(_api.invalidatedRows.start + delta, u16min, u16max));
_api.invalidatedRows.end = _api.s->cellCount.y;
}
else
{
_api.invalidatedRows.x = 0;
_api.invalidatedRows.y = gsl::narrow_cast<u16>(clamp<int>(_api.invalidatedRows.y + delta, u16min, u16max));
_api.invalidatedRows.start = 0;
_api.invalidatedRows.end = gsl::narrow_cast<u16>(clamp<int>(_api.invalidatedRows.end + delta, u16min, u16max));
}
}
@ -145,7 +146,7 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2<T>& out) noexcept
[[nodiscard]] HRESULT AtlasEngine::InvalidateTitle(const std::wstring_view proposedTitle) noexcept
{
WI_SetFlag(_api.invalidations, ApiInvalidations::Title);
_api.invalidatedTitle = true;
return S_OK;
}
@ -161,6 +162,9 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2<T>& out) noexcept
[[nodiscard]] HRESULT AtlasEngine::UpdateSoftFont(const std::span<const uint16_t> bitPattern, const til::size cellSize, const size_t centeringHint) noexcept
{
const auto softFont = _api.s.write()->font.write();
softFont->softFontPattern = std::vector(bitPattern.begin(), bitPattern.end());
softFont->softFontCellSize = cellSize;
return S_OK;
}
@ -169,10 +173,9 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2<T>& out) noexcept
u16 newDPI;
RETURN_IF_FAILED(api_narrow(dpi, newDPI));
if (_api.dpi != newDPI)
if (_api.s->font->dpi != newDPI)
{
_api.dpi = newDPI;
WI_SetFlag(_api.invalidations, ApiInvalidations::Font);
_api.s.write()->font.write()->dpi = newDPI;
}
return S_OK;
@ -184,10 +187,9 @@ constexpr HRESULT vec2_narrow(U x, U y, AtlasEngine::vec2<T>& out) noexcept
gsl::narrow_cast<u16>(std::max(1, srNewViewport.right - srNewViewport.left + 1)),
gsl::narrow_cast<u16>(std::max(1, srNewViewport.bottom - srNewViewport.top + 1)),
};
if (_api.cellCount != cellCount)
if (_api.s->cellCount != cellCount)
{
_api.cellCount = cellCount;
WI_SetFlag(_api.invalidations, ApiInvalidations::Size);
_api.s.write()->cellCount = cellCount;
}
return S_OK;
}
@ -263,8 +265,8 @@ CATCH_RETURN()
[[nodiscard]] HRESULT AtlasEngine::GetFontSize(_Out_ til::size* pFontSize) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, pFontSize);
pFontSize->width = _api.fontMetrics.cellSize.x;
pFontSize->height = _api.fontMetrics.cellSize.y;
pFontSize->width = _api.s->font->cellSize.x;
pFontSize->height = _api.s->font->cellSize.y;
return S_OK;
}
@ -272,13 +274,25 @@ CATCH_RETURN()
{
RETURN_HR_IF_NULL(E_INVALIDARG, pResult);
wil::com_ptr<IDWriteTextLayout> textLayout;
RETURN_IF_FAILED(_sr.dwriteFactory->CreateTextLayout(glyph.data(), gsl::narrow_cast<uint32_t>(glyph.size()), _getTextFormat(false, false), FLT_MAX, FLT_MAX, textLayout.addressof()));
wil::com_ptr<IDWriteTextFormat> textFormat;
RETURN_IF_FAILED(_p.dwriteFactory->CreateTextFormat(
/* fontFamilyName */ _api.s->font->fontName.c_str(),
/* fontCollection */ _api.s->font->fontCollection.get(),
/* fontWeight */ static_cast<DWRITE_FONT_WEIGHT>(_api.s->font->fontWeight),
/* fontStyle */ DWRITE_FONT_STYLE_NORMAL,
/* fontStretch */ DWRITE_FONT_STRETCH_NORMAL,
/* fontSize */ _api.s->font->fontSize,
/* localeName */ L"",
/* textFormat */ textFormat.put()));
DWRITE_TEXT_METRICS metrics;
wil::com_ptr<IDWriteTextLayout> textLayout;
RETURN_IF_FAILED(_p.dwriteFactory->CreateTextLayout(glyph.data(), gsl::narrow_cast<uint32_t>(glyph.size()), textFormat.get(), FLT_MAX, FLT_MAX, textLayout.addressof()));
DWRITE_TEXT_METRICS metrics{};
RETURN_IF_FAILED(textLayout->GetMetrics(&metrics));
*pResult = static_cast<unsigned int>(std::ceilf(metrics.width)) > _api.fontMetrics.cellSize.x;
const auto minWidth = (_api.s->font->cellSize.x * 1.2f);
*pResult = metrics.width > minWidth;
return S_OK;
}
@ -298,47 +312,46 @@ HRESULT AtlasEngine::Enable() noexcept
[[nodiscard]] std::wstring_view AtlasEngine::GetPixelShaderPath() noexcept
{
return _api.customPixelShaderPath;
return _api.s->misc->customPixelShaderPath;
}
[[nodiscard]] bool AtlasEngine::GetRetroTerminalEffect() const noexcept
{
return _api.useRetroTerminalEffect;
return _api.s->misc->useRetroTerminalEffect;
}
[[nodiscard]] float AtlasEngine::GetScaling() const noexcept
{
return static_cast<float>(_api.dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
return static_cast<f32>(_api.s->font->dpi) / static_cast<f32>(USER_DEFAULT_SCREEN_DPI);
}
[[nodiscard]] Microsoft::Console::Types::Viewport AtlasEngine::GetViewportInCharacters(const Types::Viewport& viewInPixels) const noexcept
{
assert(_api.fontMetrics.cellSize.x != 0);
assert(_api.fontMetrics.cellSize.y != 0);
return Types::Viewport::FromDimensions(viewInPixels.Origin(), { viewInPixels.Width() / _api.fontMetrics.cellSize.x, viewInPixels.Height() / _api.fontMetrics.cellSize.y });
assert(_api.s->font->cellSize.x != 0);
assert(_api.s->font->cellSize.y != 0);
return Types::Viewport::FromDimensions(viewInPixels.Origin(), { viewInPixels.Width() / _api.s->font->cellSize.x, viewInPixels.Height() / _api.s->font->cellSize.y });
}
[[nodiscard]] Microsoft::Console::Types::Viewport AtlasEngine::GetViewportInPixels(const Types::Viewport& viewInCharacters) const noexcept
{
assert(_api.fontMetrics.cellSize.x != 0);
assert(_api.fontMetrics.cellSize.y != 0);
return Types::Viewport::FromDimensions(viewInCharacters.Origin(), { viewInCharacters.Width() * _api.fontMetrics.cellSize.x, viewInCharacters.Height() * _api.fontMetrics.cellSize.y });
assert(_api.s->font->cellSize.x != 0);
assert(_api.s->font->cellSize.y != 0);
return Types::Viewport::FromDimensions(viewInCharacters.Origin(), { viewInCharacters.Width() * _api.s->font->cellSize.x, viewInCharacters.Height() * _api.s->font->cellSize.y });
}
void AtlasEngine::SetAntialiasingMode(const D2D1_TEXT_ANTIALIAS_MODE antialiasingMode) noexcept
{
const auto mode = gsl::narrow_cast<u8>(antialiasingMode);
const auto mode = static_cast<AntialiasingMode>(antialiasingMode);
if (_api.antialiasingMode != mode)
{
_api.antialiasingMode = mode;
_resolveTransparencySettings();
WI_SetFlag(_api.invalidations, ApiInvalidations::Font);
}
}
void AtlasEngine::SetCallback(std::function<void(HANDLE)> pfn) noexcept
{
_api.swapChainChangedCallback = std::move(pfn);
_p.swapChainChangedCallback = std::move(pfn);
}
void AtlasEngine::EnableTransparentBackground(const bool isTransparent) noexcept
@ -347,7 +360,6 @@ void AtlasEngine::EnableTransparentBackground(const bool isTransparent) noexcept
{
_api.enableTransparentBackground = isTransparent;
_resolveTransparencySettings();
WI_SetFlag(_api.invalidations, ApiInvalidations::SwapChain);
}
}
@ -357,56 +369,53 @@ void AtlasEngine::SetForceFullRepaintRendering(bool enable) noexcept
[[nodiscard]] HRESULT AtlasEngine::SetHwnd(const HWND hwnd) noexcept
{
if (_api.hwnd != hwnd)
if (_api.s->target->hwnd != hwnd)
{
_api.hwnd = hwnd;
WI_SetFlag(_api.invalidations, ApiInvalidations::SwapChain);
_api.s.write()->target.write()->hwnd = hwnd;
}
return S_OK;
}
void AtlasEngine::SetPixelShaderPath(std::wstring_view value) noexcept
try
{
if (_api.customPixelShaderPath != value)
if (_api.s->misc->customPixelShaderPath != value)
{
_api.customPixelShaderPath = value;
_api.s.write()->misc.write()->customPixelShaderPath = value;
_resolveTransparencySettings();
WI_SetFlag(_api.invalidations, ApiInvalidations::Device);
}
}
CATCH_LOG()
void AtlasEngine::SetRetroTerminalEffect(bool enable) noexcept
{
if (_api.useRetroTerminalEffect != enable)
if (_api.s->misc->useRetroTerminalEffect != enable)
{
_api.useRetroTerminalEffect = enable;
_api.s.write()->misc.write()->useRetroTerminalEffect = enable;
_resolveTransparencySettings();
WI_SetFlag(_api.invalidations, ApiInvalidations::Device);
}
}
void AtlasEngine::SetSelectionBackground(const COLORREF color, const float alpha) noexcept
{
const u32 selectionColor = (color & 0xffffff) | gsl::narrow_cast<u32>(std::lroundf(alpha * 255.0f)) << 24;
if (_api.selectionColor != selectionColor)
const u32 selectionColor = (color & 0xffffff) | gsl::narrow_cast<u32>(lrintf(alpha * 255.0f)) << 24;
if (_api.s->misc->selectionColor != selectionColor)
{
_api.selectionColor = selectionColor;
WI_SetFlag(_api.invalidations, ApiInvalidations::Settings);
_api.s.write()->misc.write()->selectionColor = selectionColor;
}
}
void AtlasEngine::SetSoftwareRendering(bool enable) noexcept
{
if (_api.useSoftwareRendering != enable)
if (_api.s->target->useSoftwareRendering != enable)
{
_api.useSoftwareRendering = enable;
WI_SetFlag(_api.invalidations, ApiInvalidations::Device);
_api.s.write()->target.write()->useSoftwareRendering = enable;
}
}
void AtlasEngine::SetWarningCallback(std::function<void(HRESULT)> pfn) noexcept
{
_api.warningCallback = std::move(pfn);
_p.warningCallback = std::move(pfn);
}
[[nodiscard]] HRESULT AtlasEngine::SetWindowSize(const til::size pixels) noexcept
@ -418,10 +427,9 @@ void AtlasEngine::SetWarningCallback(std::function<void(HRESULT)> pfn) noexcept
// When Win+D is pressed, `TriggerRedrawCursor` is called and a render pass is initiated.
// As conhost is in the background, GetClientRect will return {0,0} and we'll get called with {0,0}.
// This isn't a valid value for _api.sizeInPixel and would crash _recreateSizeDependentResources().
if (_api.sizeInPixel != newSize && newSize != u16x2{})
if (_api.s->targetSize != newSize && newSize != u16x2{})
{
_api.sizeInPixel = newSize;
WI_SetFlag(_api.invalidations, ApiInvalidations::Size);
_api.s.write()->targetSize = newSize;
}
return S_OK;
@ -467,10 +475,18 @@ void AtlasEngine::_resolveTransparencySettings() noexcept
// If the user asks for ClearType, but also for a transparent background
// (which our ClearType shader doesn't simultaneously support)
// then we need to sneakily force the renderer to grayscale AA.
_api.realizedAntialiasingMode = _api.enableTransparentBackground && _api.antialiasingMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE ? D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE : _api.antialiasingMode;
// An opaque background allows us to use true "independent" flips. See AtlasEngine::_createSwapChain().
// We can't enable them if custom shaders are specified, because it's unknown, whether they support opaque inputs.
_api.backgroundOpaqueMixin = _api.enableTransparentBackground || !_api.customPixelShaderPath.empty() || _api.useRetroTerminalEffect ? 0x00000000 : 0xff000000;
const auto antialiasingMode = _api.enableTransparentBackground && _api.antialiasingMode == AntialiasingMode::ClearType ? AntialiasingMode::Grayscale : _api.antialiasingMode;
const bool enableTransparentBackground = _api.enableTransparentBackground || !_api.s->misc->customPixelShaderPath.empty() || _api.s->misc->useRetroTerminalEffect;
if (antialiasingMode != _api.s->font->antialiasingMode || enableTransparentBackground != _api.s->target->enableTransparentBackground)
{
const auto s = _api.s.write();
s->font.write()->antialiasingMode = antialiasingMode;
// An opaque background allows us to use true "independent" flips. See AtlasEngine::_createSwapChain().
// We can't enable them if custom shaders are specified, because it's unknown, whether they support opaque inputs.
s->target.write()->enableTransparentBackground = enableTransparentBackground;
_api.backgroundOpaqueMixin = enableTransparentBackground ? 0x00000000 : 0xff000000;
}
}
void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes)
@ -486,9 +502,9 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo
// Gsub is for GetGlyphs() and Gpos for GetGlyphPlacements().
//
// GH#10774: Apparently specifying all of the features is just redundant.
fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES, 1 });
fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_LIGATURES, 1 });
fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_ALTERNATES, 1 });
fontFeatures.emplace_back(DWRITE_FONT_FEATURE_TAG_STANDARD_LIGATURES, 1);
fontFeatures.emplace_back(DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_LIGATURES, 1);
fontFeatures.emplace_back(DWRITE_FONT_FEATURE_TAG_CONTEXTUAL_ALTERNATES, 1);
for (const auto& p : features)
{
@ -507,7 +523,7 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo
fontFeatures[2].parameter = p.second;
break;
default:
fontFeatures.emplace_back(DWRITE_FONT_FEATURE{ tag, p.second });
fontFeatures.emplace_back(tag, p.second);
break;
}
}
@ -521,9 +537,9 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo
// AtlasEngine::_recreateFontDependentResources() relies on these fields to
// exist in this particular order in order to create appropriate default axes.
fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WEIGHT, -1.0f });
fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_ITALIC, -1.0f });
fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_SLANT, -1.0f });
fontAxisValues.emplace_back(DWRITE_FONT_AXIS_TAG_WEIGHT, -1.0f);
fontAxisValues.emplace_back(DWRITE_FONT_AXIS_TAG_ITALIC, -1.0f);
fontAxisValues.emplace_back(DWRITE_FONT_AXIS_TAG_SLANT, -1.0f);
for (const auto& p : axes)
{
@ -542,27 +558,20 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo
fontAxisValues[2].value = p.second;
break;
default:
fontAxisValues.emplace_back(DWRITE_FONT_AXIS_VALUE{ tag, p.second });
fontAxisValues.emplace_back(tag, p.second);
break;
}
}
}
}
const auto previousCellSize = _api.fontMetrics.cellSize;
_resolveFontMetrics(faceName, fontInfoDesired, fontInfo, &_api.fontMetrics);
_api.fontFeatures = std::move(fontFeatures);
_api.fontAxisValues = std::move(fontAxisValues);
WI_SetFlag(_api.invalidations, ApiInvalidations::Font);
if (previousCellSize != _api.fontMetrics.cellSize)
{
WI_SetFlag(_api.invalidations, ApiInvalidations::Size);
}
const auto font = _api.s.write()->font.write();
_resolveFontMetrics(faceName, fontInfoDesired, fontInfo, font);
font->fontFeatures = std::move(fontFeatures);
font->fontAxisValues = std::move(fontAxisValues);
}
void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontMetrics* fontMetrics) const
void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontSettings* fontMetrics) const
{
const auto requestedFamily = fontInfoDesired.GetFamily();
auto requestedWeight = fontInfoDesired.GetWeight();
@ -603,40 +612,42 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
wil::com_ptr<IDWriteFontFace> fontFace;
THROW_IF_FAILED(font->CreateFontFace(fontFace.addressof()));
DWRITE_FONT_METRICS metrics;
DWRITE_FONT_METRICS metrics{};
fontFace->GetMetrics(&metrics);
// According to Wikipedia:
// > One em was traditionally defined as the width of the capital 'M' in the current typeface and point size,
// > because the 'M' was commonly cast the full-width of the square blocks [...] which are used in printing presses.
// Even today M is often the widest character in a font that supports ASCII.
// In the future a more robust solution could be written, until then this simple solution works for most cases.
static constexpr u32 codePoint = L'M';
u16 glyphIndex;
THROW_IF_FAILED(fontFace->GetGlyphIndicesW(&codePoint, 1, &glyphIndex));
DWRITE_GLYPH_METRICS glyphMetrics;
THROW_IF_FAILED(fontFace->GetDesignGlyphMetrics(&glyphIndex, 1, &glyphMetrics));
// Point sizes are commonly treated at a 72 DPI scale
// (including by OpenType), whereas DirectWrite uses 96 DPI.
// Since we want the height in px we multiply by the display's DPI.
const auto fontSizeInDIP = fontSize / 72.0f * 96.0f;
const auto fontSizeInPx = fontSize / 72.0f * _api.dpi;
const auto dpi = static_cast<f32>(_api.s->font->dpi);
const auto fontSizeInPx = fontSize / 72.0f * dpi;
const auto designUnitsPerPx = fontSizeInPx / static_cast<float>(metrics.designUnitsPerEm);
const auto ascent = static_cast<float>(metrics.ascent) * designUnitsPerPx;
const auto descent = static_cast<float>(metrics.descent) * designUnitsPerPx;
const auto lineGap = static_cast<float>(metrics.lineGap) * designUnitsPerPx;
const auto underlinePosition = static_cast<float>(-metrics.underlinePosition) * designUnitsPerPx;
const auto underlineThickness = static_cast<float>(metrics.underlineThickness) * designUnitsPerPx;
const auto strikethroughPosition = static_cast<float>(-metrics.strikethroughPosition) * designUnitsPerPx;
const auto strikethroughThickness = static_cast<float>(metrics.strikethroughThickness) * designUnitsPerPx;
const auto advanceWidth = static_cast<float>(glyphMetrics.advanceWidth) * designUnitsPerPx;
const auto designUnitsPerPx = fontSizeInPx / static_cast<f32>(metrics.designUnitsPerEm);
const auto ascent = static_cast<f32>(metrics.ascent) * designUnitsPerPx;
const auto descent = static_cast<f32>(metrics.descent) * designUnitsPerPx;
const auto lineGap = static_cast<f32>(metrics.lineGap) * designUnitsPerPx;
const auto underlinePosition = static_cast<f32>(-metrics.underlinePosition) * designUnitsPerPx;
const auto underlineThickness = static_cast<f32>(metrics.underlineThickness) * designUnitsPerPx;
const auto strikethroughPosition = static_cast<f32>(-metrics.strikethroughPosition) * designUnitsPerPx;
const auto strikethroughThickness = static_cast<f32>(metrics.strikethroughThickness) * designUnitsPerPx;
const auto advanceHeight = ascent + descent + lineGap;
auto adjustedWidth = std::roundf(fontInfoDesired.GetCellWidth().Resolve(advanceWidth, _api.dpi, fontSizeInPx, advanceWidth));
auto adjustedHeight = std::roundf(fontInfoDesired.GetCellHeight().Resolve(advanceHeight, _api.dpi, fontSizeInPx, advanceWidth));
// We use the same character to determine the advance width as CSS for its "ch" unit ("0").
// According to the CSS spec, if it's impossible to determine the advance width,
// it must be assumed to be 0.5em wide. em in CSS refers to the computed font-size.
auto advanceWidth = 0.5f * fontSizeInPx;
{
static constexpr u32 codePoint = '0';
u16 glyphIndex;
if (SUCCEEDED(fontFace->GetGlyphIndicesW(&codePoint, 1, &glyphIndex)))
{
DWRITE_GLYPH_METRICS glyphMetrics{};
THROW_IF_FAILED(fontFace->GetDesignGlyphMetrics(&glyphIndex, 1, &glyphMetrics, FALSE));
advanceWidth = static_cast<f32>(glyphMetrics.advanceWidth) * designUnitsPerPx;
}
}
auto adjustedWidth = std::roundf(fontInfoDesired.GetCellWidth().Resolve(advanceWidth, dpi, fontSizeInPx, advanceWidth));
auto adjustedHeight = std::roundf(fontInfoDesired.GetCellHeight().Resolve(advanceHeight, dpi, fontSizeInPx, advanceWidth));
// Protection against bad user values in GetCellWidth/Y.
// AtlasEngine fails hard with 0 cell sizes.
@ -667,13 +678,13 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
doubleUnderlinePosTop = std::max(doubleUnderlinePosTop, baseline + thinLineWidth);
// 5. The gap is only the distance _between_ the lines, but we need the distance from the
// top border of the top and bottom lines, which includes an additional line width.
const auto doubleUnderlineGap = std::max(1.0f, std::roundf(1.2f / 72.0f * _api.dpi));
const auto doubleUnderlineGap = std::max(1.0f, std::roundf(1.2f / 72.0f * dpi));
doubleUnderlinePosBottom = std::max(doubleUnderlinePosBottom, doubleUnderlinePosTop + doubleUnderlineGap + thinLineWidth);
// Our cells can't overlap each other so we additionally clamp the bottom line to be inside the cell boundaries.
doubleUnderlinePosBottom = std::min(doubleUnderlinePosBottom, adjustedHeight - thinLineWidth);
const auto cellWidth = gsl::narrow<u16>(std::lroundf(adjustedWidth));
const auto cellHeight = gsl::narrow<u16>(std::lroundf(adjustedHeight));
const auto cellWidth = gsl::narrow<u16>(lrintf(adjustedWidth));
const auto cellHeight = gsl::narrow<u16>(lrintf(adjustedHeight));
{
til::size coordSize;
@ -685,7 +696,7 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
// The coordSizeUnscaled parameter to SetFromEngine is used for API functions like GetConsoleFontSize.
// Since clients expect that settings the font height to Y yields back a font height of Y,
// we're scaling the X relative/proportional to the actual cellWidth/cellHeight ratio.
requestedSize.width = gsl::narrow_cast<til::CoordType>(std::lroundf(fontSize / cellHeight * cellWidth));
requestedSize.width = gsl::narrow_cast<til::CoordType>(lrintf(fontSize / cellHeight * cellWidth));
}
fontInfo.SetFromEngine(requestedFaceName, requestedFamily, requestedWeight, false, coordSize, requestedSize);
@ -695,13 +706,20 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
{
std::wstring fontName{ requestedFaceName };
const auto fontWeightU16 = gsl::narrow_cast<u16>(requestedWeight);
const auto underlinePosU16 = gsl::narrow_cast<u16>(underlinePos);
const auto underlineWidthU16 = gsl::narrow_cast<u16>(underlineWidth);
const auto strikethroughPosU16 = gsl::narrow_cast<u16>(strikethroughPos);
const auto strikethroughWidthU16 = gsl::narrow_cast<u16>(strikethroughWidth);
const auto doubleUnderlinePosTopU16 = gsl::narrow_cast<u16>(doubleUnderlinePosTop);
const auto doubleUnderlinePosBottomU16 = gsl::narrow_cast<u16>(doubleUnderlinePosBottom);
const auto thinLineWidthU16 = gsl::narrow_cast<u16>(thinLineWidth);
const auto advanceWidthU16 = gsl::narrow_cast<u16>(lrintf(advanceWidth));
const auto baselineU16 = gsl::narrow_cast<u16>(lrintf(baseline));
const auto descenderU16 = gsl::narrow_cast<u16>(cellHeight - baselineU16);
const auto thinLineWidthU16 = gsl::narrow_cast<u16>(lrintf(thinLineWidth));
const auto gridBottomPositionU16 = gsl::narrow_cast<u16>(cellHeight - thinLineWidth);
const auto gridRightPositionU16 = gsl::narrow_cast<u16>(cellWidth - thinLineWidth);
const auto underlinePosU16 = gsl::narrow_cast<u16>(lrintf(underlinePos));
const auto underlineWidthU16 = gsl::narrow_cast<u16>(lrintf(underlineWidth));
const auto strikethroughPosU16 = gsl::narrow_cast<u16>(lrintf(strikethroughPos));
const auto strikethroughWidthU16 = gsl::narrow_cast<u16>(lrintf(strikethroughWidth));
const auto doubleUnderlinePosTopU16 = gsl::narrow_cast<u16>(lrintf(doubleUnderlinePosTop));
const auto doubleUnderlinePosBottomU16 = gsl::narrow_cast<u16>(lrintf(doubleUnderlinePosBottom));
// NOTE: From this point onward no early returns or throwing code should exist,
// as we might cause _api to be in an inconsistent state otherwise.
@ -709,16 +727,23 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
fontMetrics->fontCollection = std::move(fontCollection);
fontMetrics->fontFamily = std::move(fontFamily);
fontMetrics->fontName = std::move(fontName);
fontMetrics->fontSizeInDIP = fontSizeInDIP;
fontMetrics->baselineInDIP = baseline / static_cast<float>(_api.dpi) * 96.0f;
fontMetrics->advanceScale = cellWidth / advanceWidth;
fontMetrics->fontSize = fontSizeInPx;
fontMetrics->cellSize = { cellWidth, cellHeight };
fontMetrics->fontWeight = fontWeightU16;
fontMetrics->underlinePos = underlinePosU16;
fontMetrics->underlineWidth = underlineWidthU16;
fontMetrics->strikethroughPos = strikethroughPosU16;
fontMetrics->strikethroughWidth = strikethroughWidthU16;
fontMetrics->doubleUnderlinePos = { doubleUnderlinePosTopU16, doubleUnderlinePosBottomU16 };
fontMetrics->advanceWidth = advanceWidthU16;
fontMetrics->baseline = baselineU16;
fontMetrics->descender = descenderU16;
fontMetrics->thinLineWidth = thinLineWidthU16;
fontMetrics->gridTop = { 0, thinLineWidthU16 };
fontMetrics->gridBottom = { gridBottomPositionU16, thinLineWidthU16 };
fontMetrics->gridLeft = { 0, thinLineWidthU16 };
fontMetrics->gridRight = { gridRightPositionU16, thinLineWidthU16 };
fontMetrics->underline = { underlinePosU16, underlineWidthU16 };
fontMetrics->strikethrough = { strikethroughPosU16, strikethroughWidthU16 };
fontMetrics->doubleUnderline[0] = { doubleUnderlinePosTopU16, thinLineWidthU16 };
fontMetrics->doubleUnderline[1] = { doubleUnderlinePosBottomU16, thinLineWidthU16 };
fontMetrics->overline = { 0, underlineWidthU16 };
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "Backend.h"
#include <dwmapi.h>
TIL_FAST_MATH_BEGIN
// Disable a bunch of warnings which get in the way of writing performant code.
#pragma warning(disable : 26429) // Symbol 'data' is never tested for nullness, it can be marked as not_null (f.23).
#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4).
#pragma warning(disable : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...].
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2).
using namespace Microsoft::Console::Render::Atlas;
void Microsoft::Console::Render::Atlas::GlyphRunAccumulateBounds(const ID2D1DeviceContext* d2dRenderTarget, D2D1_POINT_2F baselineOrigin, const DWRITE_GLYPH_RUN* glyphRun, D2D1_RECT_F& bounds)
{
D2D1_RECT_F rect{};
THROW_IF_FAILED(d2dRenderTarget->GetGlyphRunWorldBounds(baselineOrigin, glyphRun, DWRITE_MEASURING_MODE_NATURAL, &rect));
if (rect.top < rect.bottom)
{
bounds.left = std::min(bounds.left, rect.left);
bounds.top = std::min(bounds.top, rect.top);
bounds.right = std::max(bounds.right, rect.right);
bounds.bottom = std::max(bounds.bottom, rect.bottom);
}
}
wil::com_ptr<IDWriteColorGlyphRunEnumerator1> Microsoft::Console::Render::Atlas::TranslateColorGlyphRun(IDWriteFactory4* dwriteFactory4, D2D_POINT_2F baselineOrigin, const DWRITE_GLYPH_RUN* glyphRun) noexcept
{
static constexpr auto formats =
DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE |
DWRITE_GLYPH_IMAGE_FORMATS_CFF |
DWRITE_GLYPH_IMAGE_FORMATS_COLR |
DWRITE_GLYPH_IMAGE_FORMATS_SVG |
DWRITE_GLYPH_IMAGE_FORMATS_PNG |
DWRITE_GLYPH_IMAGE_FORMATS_JPEG |
DWRITE_GLYPH_IMAGE_FORMATS_TIFF |
DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8;
wil::com_ptr<IDWriteColorGlyphRunEnumerator1> enumerator;
if (dwriteFactory4)
{
std::ignore = dwriteFactory4->TranslateColorGlyphRun(baselineOrigin, glyphRun, nullptr, formats, DWRITE_MEASURING_MODE_NATURAL, nullptr, 0, enumerator.addressof());
}
return enumerator;
}
bool Microsoft::Console::Render::Atlas::ColorGlyphRunMoveNext(IDWriteColorGlyphRunEnumerator1* enumerator)
{
BOOL hasRun;
THROW_IF_FAILED(enumerator->MoveNext(&hasRun));
return hasRun;
}
const DWRITE_COLOR_GLYPH_RUN1* Microsoft::Console::Render::Atlas::ColorGlyphRunGetCurrentRun(IDWriteColorGlyphRunEnumerator1* enumerator)
{
const DWRITE_COLOR_GLYPH_RUN1* colorGlyphRun = nullptr;
THROW_IF_FAILED(enumerator->GetCurrentRun(&colorGlyphRun));
return colorGlyphRun;
}
void Microsoft::Console::Render::Atlas::ColorGlyphRunAccumulateBounds(const ID2D1DeviceContext* d2dRenderTarget, const DWRITE_COLOR_GLYPH_RUN1* colorGlyphRun, D2D1_RECT_F& bounds)
{
const D2D1_POINT_2F baselineOrigin{ colorGlyphRun->baselineOriginX, colorGlyphRun->baselineOriginY };
GlyphRunAccumulateBounds(d2dRenderTarget, baselineOrigin, &colorGlyphRun->glyphRun, bounds);
}
void Microsoft::Console::Render::Atlas::ColorGlyphRunDraw(ID2D1DeviceContext4* d2dRenderTarget4, ID2D1SolidColorBrush* emojiBrush, ID2D1SolidColorBrush* foregroundBrush, const DWRITE_COLOR_GLYPH_RUN1* colorGlyphRun) noexcept
{
ID2D1Brush* runBrush = nullptr;
if (colorGlyphRun->paletteIndex == /*DWRITE_NO_PALETTE_INDEX*/ 0xffff)
{
runBrush = foregroundBrush;
}
else
{
emojiBrush->SetColor(&colorGlyphRun->runColor);
runBrush = emojiBrush;
}
const D2D1_POINT_2F baselineOrigin{ colorGlyphRun->baselineOriginX, colorGlyphRun->baselineOriginY };
switch (colorGlyphRun->glyphImageFormat)
{
case DWRITE_GLYPH_IMAGE_FORMATS_NONE:
break;
case DWRITE_GLYPH_IMAGE_FORMATS_PNG:
case DWRITE_GLYPH_IMAGE_FORMATS_JPEG:
case DWRITE_GLYPH_IMAGE_FORMATS_TIFF:
case DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8:
d2dRenderTarget4->DrawColorBitmapGlyphRun(colorGlyphRun->glyphImageFormat, baselineOrigin, &colorGlyphRun->glyphRun, colorGlyphRun->measuringMode, D2D1_COLOR_BITMAP_GLYPH_SNAP_OPTION_DEFAULT);
break;
case DWRITE_GLYPH_IMAGE_FORMATS_SVG:
d2dRenderTarget4->DrawSvgGlyphRun(baselineOrigin, &colorGlyphRun->glyphRun, runBrush, nullptr, 0, colorGlyphRun->measuringMode);
break;
default:
d2dRenderTarget4->DrawGlyphRun(baselineOrigin, &colorGlyphRun->glyphRun, colorGlyphRun->glyphRunDescription, runBrush, colorGlyphRun->measuringMode);
break;
}
}
TIL_FAST_MATH_END

Просмотреть файл

@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "common.h"
namespace Microsoft::Console::Render::Atlas
{
// If set to 1, this will cause the entire viewport to be invalidated at all times.
// Helpful for benchmarking our text shaping code based on DirectWrite.
#define ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION 0
// Redraw at display refresh rate at all times. This helps with shader debugging.
#define ATLAS_DEBUG_CONTINUOUS_REDRAW 0
// Disables the use of DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT.
// This helps with benchmarking the application as it'll run beyond display refresh rate.
#define ATLAS_DEBUG_DISABLE_FRAME_LATENCY_WAITABLE_OBJECT 0
// Forces the use of Direct2D for text rendering (= BackendD2D).
#define ATLAS_DEBUG_FORCE_D2D_MODE 0
// Adds an artificial delay before every render pass. In milliseconds.
#define ATLAS_DEBUG_RENDER_DELAY 0
// Shows the dirty rects as given to IDXGISwapChain2::Present1() during each frame.
#define ATLAS_DEBUG_SHOW_DIRTY 0
// Dumps the contents of the swap chain on each render pass into the given directory as PNG.
// I highly recommend setting ATLAS_DEBUG_RENDER_DELAY to 250 or similar if this is used.
#define ATLAS_DEBUG_DUMP_RENDER_TARGET 0
#define ATLAS_DEBUG_DUMP_RENDER_TARGET_PATH LR"(%USERPROFILE%\Downloads\AtlasEngine)"
template<typename T = D2D1_COLOR_F>
constexpr T colorFromU32(u32 rgba)
{
const auto r = static_cast<f32>((rgba >> 0) & 0xff) / 255.0f;
const auto g = static_cast<f32>((rgba >> 8) & 0xff) / 255.0f;
const auto b = static_cast<f32>((rgba >> 16) & 0xff) / 255.0f;
const auto a = static_cast<f32>((rgba >> 24) & 0xff) / 255.0f;
return { r, g, b, a };
}
template<typename T = D2D1_COLOR_F>
constexpr T colorFromU32Premultiply(u32 rgba)
{
const auto r = static_cast<f32>((rgba >> 0) & 0xff) / 255.0f;
const auto g = static_cast<f32>((rgba >> 8) & 0xff) / 255.0f;
const auto b = static_cast<f32>((rgba >> 16) & 0xff) / 255.0f;
const auto a = static_cast<f32>((rgba >> 24) & 0xff) / 255.0f;
return { r * a, g * a, b * a, a };
}
constexpr u32 u32ColorPremultiply(u32 rgba)
{
auto rb = rgba & 0x00ff00ff;
auto g = rgba & 0x0000ff00;
const auto a = rgba & 0xff000000;
const auto m = rgba >> 24;
rb = (rb * m / 0xff) & 0x00ff00ff;
g = (g * m / 0xff) & 0x0000ff00;
return rb | g | a;
}
// MSVC STL (version 22000) implements std::clamp<T>(T, T, T) in terms of the generic
// std::clamp<T, Predicate>(T, T, T, Predicate) with std::less{} as the argument,
// which introduces branching. While not perfect, this is still better than std::clamp.
template<typename T>
constexpr T clamp(T val, T min, T max)
{
return val < min ? min : (max < val ? max : val);
}
inline constexpr D2D1_RECT_F GlyphRunEmptyBounds{ 1e38f, 1e38f, -1e38f, -1e38f };
void GlyphRunAccumulateBounds(const ID2D1DeviceContext* d2dRenderTarget, D2D1_POINT_2F baselineOrigin, const DWRITE_GLYPH_RUN* glyphRun, D2D1_RECT_F& bounds);
wil::com_ptr<IDWriteColorGlyphRunEnumerator1> TranslateColorGlyphRun(IDWriteFactory4* dwriteFactory4, D2D_POINT_2F baselineOrigin, const DWRITE_GLYPH_RUN* glyphRun) noexcept;
bool ColorGlyphRunMoveNext(IDWriteColorGlyphRunEnumerator1* enumerator);
const DWRITE_COLOR_GLYPH_RUN1* ColorGlyphRunGetCurrentRun(IDWriteColorGlyphRunEnumerator1* enumerator);
void ColorGlyphRunAccumulateBounds(const ID2D1DeviceContext* d2dRenderTarget, const DWRITE_COLOR_GLYPH_RUN1* colorGlyphRun, D2D1_RECT_F& bounds);
void ColorGlyphRunDraw(ID2D1DeviceContext4* d2dRenderTarget4, ID2D1SolidColorBrush* emojiBrush, ID2D1SolidColorBrush* foregroundBrush, const DWRITE_COLOR_GLYPH_RUN1* colorGlyphRun) noexcept;
}

Просмотреть файл

@ -0,0 +1,684 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "BackendD2D.h"
#if ATLAS_DEBUG_SHOW_DIRTY
#include "colorbrewer.h"
#endif
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
#include "wic.h"
#endif
TIL_FAST_MATH_BEGIN
// Disable a bunch of warnings which get in the way of writing performant code.
#pragma warning(disable : 26429) // Symbol 'data' is never tested for nullness, it can be marked as not_null (f.23).
#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4).
#pragma warning(disable : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...].
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2).
using namespace Microsoft::Console::Render::Atlas;
void BackendD2D::ReleaseResources() noexcept
{
_renderTarget.reset();
_renderTarget4.reset();
// Ensure _handleSettingsUpdate() is called so that _renderTarget gets recreated.
_generation = {};
}
void BackendD2D::Render(RenderingPayload& p)
{
if (_generation != p.s.generation())
{
_handleSettingsUpdate(p);
}
_renderTarget->BeginDraw();
#if ATLAS_DEBUG_SHOW_DIRTY || ATLAS_DEBUG_DUMP_RENDER_TARGET
// Invalidating the render target helps with spotting Present1() bugs.
_renderTarget->Clear();
#endif
_drawBackground(p);
_drawCursorPart1(p);
_drawText(p);
_drawCursorPart2(p);
_drawSelection(p);
#if ATLAS_DEBUG_SHOW_DIRTY
_debugShowDirty(p);
#endif
THROW_IF_FAILED(_renderTarget->EndDraw());
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
_debugDumpRenderTarget(p);
#endif
}
bool BackendD2D::RequiresContinuousRedraw() noexcept
{
return false;
}
void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p)
{
const auto renderTargetChanged = !_renderTarget;
const auto fontChanged = _fontGeneration != p.s->font.generation();
const auto cursorChanged = _cursorGeneration != p.s->cursor.generation();
const auto cellCountChanged = _cellCount != p.s->cellCount;
if (renderTargetChanged)
{
{
wil::com_ptr<ID3D11Texture2D> buffer;
THROW_IF_FAILED(p.swapChain.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(buffer.addressof())));
const D2D1_RENDER_TARGET_PROPERTIES props{
.type = D2D1_RENDER_TARGET_TYPE_DEFAULT,
.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),
};
// ID2D1RenderTarget and ID2D1DeviceContext are the same and I'm tired of pretending they're not.
THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(buffer.query<IDXGISurface>().get(), &props, reinterpret_cast<ID2D1RenderTarget**>(_renderTarget.addressof())));
_renderTarget.query_to(_renderTarget4.addressof());
_renderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS);
_renderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
}
{
static constexpr D2D1_COLOR_F color{};
THROW_IF_FAILED(_renderTarget->CreateSolidColorBrush(&color, nullptr, _emojiBrush.put()));
THROW_IF_FAILED(_renderTarget->CreateSolidColorBrush(&color, nullptr, _brush.put()));
_brushColor = 0;
}
}
if (!_dottedStrokeStyle)
{
static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM };
static constexpr FLOAT dashes[2]{ 1, 1 };
THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dottedStrokeStyle.addressof()));
}
if (renderTargetChanged || fontChanged)
{
const auto dpi = static_cast<f32>(p.s->font->dpi);
_renderTarget->SetDpi(dpi, dpi);
_renderTarget->SetTextAntialiasMode(static_cast<D2D1_TEXT_ANTIALIAS_MODE>(p.s->font->antialiasingMode));
}
if (renderTargetChanged || fontChanged || cellCountChanged)
{
const D2D1_BITMAP_PROPERTIES props{
.pixelFormat = { DXGI_FORMAT_R8G8B8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED },
.dpiX = static_cast<f32>(p.s->font->dpi),
.dpiY = static_cast<f32>(p.s->font->dpi),
};
const D2D1_SIZE_U size{
p.s->cellCount.x,
p.s->cellCount.y,
};
const D2D1_MATRIX_3X2_F transform{
.m11 = static_cast<f32>(p.s->font->cellSize.x),
.m22 = static_cast<f32>(p.s->font->cellSize.y),
};
THROW_IF_FAILED(_renderTarget->CreateBitmap(size, nullptr, 0, &props, _backgroundBitmap.put()));
THROW_IF_FAILED(_renderTarget->CreateBitmapBrush(_backgroundBitmap.get(), _backgroundBrush.put()));
_backgroundBrush->SetInterpolationMode(D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR);
_backgroundBrush->SetExtendModeX(D2D1_EXTEND_MODE_MIRROR);
_backgroundBrush->SetExtendModeY(D2D1_EXTEND_MODE_MIRROR);
_backgroundBrush->SetTransform(&transform);
_backgroundBitmapGeneration = {};
}
if (fontChanged || cursorChanged)
{
_cursorBitmap.reset();
_cursorBitmapSize = {};
}
_generation = p.s.generation();
_fontGeneration = p.s->font.generation();
_cursorGeneration = p.s->cursor.generation();
_cellCount = p.s->cellCount;
}
void BackendD2D::_drawBackground(const RenderingPayload& p) noexcept
{
if (_backgroundBitmapGeneration != p.colorBitmapGenerations[0])
{
_backgroundBitmap->CopyFromMemory(nullptr, p.backgroundBitmap.data(), gsl::narrow_cast<UINT32>(p.colorBitmapRowStride * sizeof(u32)));
_backgroundBitmapGeneration = p.colorBitmapGenerations[0];
}
// If the terminal was 120x30 cells and 1200x600 pixels large, this would draw the
// background by upscaling a 120x30 pixel bitmap to fill the entire render target.
const D2D1_RECT_F rect{ 0, 0, static_cast<f32>(p.s->targetSize.x), static_cast<f32>(p.s->targetSize.y) };
_renderTarget->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_COPY);
_renderTarget->FillRectangle(&rect, _backgroundBrush.get());
_renderTarget->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_SOURCE_OVER);
}
void BackendD2D::_drawText(RenderingPayload& p)
{
til::CoordType dirtyTop = til::CoordTypeMax;
til::CoordType dirtyBottom = til::CoordTypeMin;
// It is possible to create a "_foregroundBrush" similar to how the `_backgroundBrush` is created and
// use that as the brush for text rendering below. That way we wouldn't have to search `row->colors` for color
// changes and could draw entire lines of text in a single call. Unfortunately Direct2D is not particularly
// smart if you do this and chooses to draw the given text into a way too small offscreen texture first and
// then blends it on the screen with the given bitmap brush. While this roughly doubles the performance
// when drawing lots of colors, the extra latency drops performance by >10x when drawing fewer colors.
// Since fewer colors are more common, I've chosen to go with regular solid-color brushes.
u16 y = 0;
for (const auto row : p.rows)
{
auto baselineX = 0.0f;
auto baselineY = static_cast<f32>(p.s->font->cellSize.y * y + p.s->font->baseline);
if (row->lineRendition != LineRendition::SingleWidth)
{
baselineY = _drawTextPrepareLineRendition(p, row, baselineY);
}
for (const auto& m : row->mappings)
{
const auto colorsBegin = row->colors.begin();
auto it = colorsBegin + m.glyphsFrom;
const auto end = colorsBegin + m.glyphsTo;
while (it != end)
{
const auto beg = it;
const auto off = it - colorsBegin;
const auto fg = *it;
while (++it != end && *it == fg)
{
}
const auto count = it - beg;
const auto brush = _brushWithColor(fg);
const DWRITE_GLYPH_RUN glyphRun{
.fontFace = m.fontFace.get(),
.fontEmSize = p.s->font->fontSize,
.glyphCount = gsl::narrow_cast<UINT32>(count),
.glyphIndices = &row->glyphIndices[off],
.glyphAdvances = &row->glyphAdvances[off],
.glyphOffsets = &row->glyphOffsets[off],
};
const D2D1_POINT_2F baselineOrigin{
baselineX,
baselineY,
};
if (glyphRun.fontFace)
{
D2D1_RECT_F bounds = GlyphRunEmptyBounds;
if (const auto enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun))
{
while (ColorGlyphRunMoveNext(enumerator.get()))
{
const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get());
ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun);
ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds);
}
}
else
{
_renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL);
GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds);
}
if (bounds.top < bounds.bottom)
{
// Since we used SetUnitMode(D2D1_UNIT_MODE_PIXELS), bounds.top/bottom is in pixels already and requires no conversion/rounding.
if (row->lineRendition != LineRendition::DoubleHeightTop)
{
row->dirtyBottom = std::max(row->dirtyBottom, static_cast<i32>(lrintf(bounds.bottom)));
}
if (row->lineRendition != LineRendition::DoubleHeightBottom)
{
row->dirtyTop = std::min(row->dirtyTop, static_cast<i32>(lrintf(bounds.top)));
}
}
}
for (UINT32 i = 0; i < glyphRun.glyphCount; ++i)
{
baselineX += glyphRun.glyphAdvances[i];
}
}
}
if (!row->gridLineRanges.empty())
{
_drawGridlineRow(p, row, y);
}
if (row->lineRendition != LineRendition::SingleWidth)
{
_drawTextResetLineRendition(row);
}
if (p.invalidatedRows.contains(y))
{
dirtyTop = std::min(dirtyTop, row->dirtyTop);
dirtyBottom = std::max(dirtyBottom, row->dirtyBottom);
}
++y;
}
if (dirtyTop < dirtyBottom)
{
p.dirtyRectInPx.top = std::min(p.dirtyRectInPx.top, dirtyTop);
p.dirtyRectInPx.bottom = std::max(p.dirtyRectInPx.bottom, dirtyBottom);
}
}
f32 BackendD2D::_drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept
{
const auto lineRendition = row->lineRendition;
D2D1_MATRIX_3X2_F transform{
.m11 = 2.0f,
.m22 = 1.0f,
};
if (lineRendition >= LineRendition::DoubleHeightTop)
{
D2D1_RECT_F clipRect{ 0, 0, static_cast<f32>(p.s->targetSize.x), static_cast<f32>(p.s->targetSize.y) };
transform.m22 = 2.0f;
transform.dy = -1.0f * (baselineY + p.s->font->descender);
// If you print the top half of a double height row (DECDHL), the expectation is that only
// the top half is visible, which requires us to keep the clip rect at the bottom of the row.
// (Vice versa for the bottom half of a double height row.)
if (lineRendition == LineRendition::DoubleHeightTop)
{
const auto delta = static_cast<f32>(p.s->font->cellSize.y);
baselineY += delta;
transform.dy -= delta;
clipRect.bottom = static_cast<f32>(row->dirtyBottom);
}
else
{
clipRect.top = static_cast<f32>(row->dirtyTop);
}
_renderTarget->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED);
}
_renderTarget->SetTransform(&transform);
return baselineY;
}
void BackendD2D::_drawTextResetLineRendition(const ShapedRow* row) const noexcept
{
static constexpr D2D1_MATRIX_3X2_F identity{ .m11 = 1, .m22 = 1 };
_renderTarget->SetTransform(&identity);
if (row->lineRendition >= LineRendition::DoubleHeightTop)
{
_renderTarget->PopAxisAlignedClip();
}
}
// Returns the theoretical/design design size of the given `DWRITE_GLYPH_RUN`, relative the the given baseline origin.
// This algorithm replicates what DirectWrite does internally to provide `IDWriteTextLayout::GetMetrics`.
f32r BackendD2D::_getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 baselineX, f32 baselineY)
{
DWRITE_FONT_METRICS fontMetrics;
glyphRun.fontFace->GetMetrics(&fontMetrics);
if (glyphRun.glyphCount > _glyphMetrics.size())
{
// Growth factor 1.5x.
auto size = _glyphMetrics.size();
size = size + (size >> 1);
size = std::max<size_t>(size, glyphRun.glyphCount);
// Overflow check.
Expects(size > _glyphMetrics.size());
_glyphMetrics = Buffer<DWRITE_GLYPH_METRICS>{ size };
}
glyphRun.fontFace->GetDesignGlyphMetrics(glyphRun.glyphIndices, glyphRun.glyphCount, _glyphMetrics.data(), false);
const f32 fontScale = glyphRun.fontEmSize / fontMetrics.designUnitsPerEm;
f32r accumulatedBounds{
baselineX,
baselineY,
baselineX,
baselineY,
};
for (uint32_t i = 0; i < glyphRun.glyphCount; ++i)
{
const auto& glyphMetrics = _glyphMetrics[i];
const auto glyphAdvance = glyphRun.glyphAdvances ? glyphRun.glyphAdvances[i] : glyphMetrics.advanceWidth * fontScale;
const auto left = static_cast<f32>(glyphMetrics.leftSideBearing) * fontScale;
const auto top = static_cast<f32>(glyphMetrics.topSideBearing - glyphMetrics.verticalOriginY) * fontScale;
const auto right = static_cast<f32>(gsl::narrow_cast<INT32>(glyphMetrics.advanceWidth) - glyphMetrics.rightSideBearing) * fontScale;
const auto bottom = static_cast<f32>(gsl::narrow_cast<INT32>(glyphMetrics.advanceHeight) - glyphMetrics.bottomSideBearing - glyphMetrics.verticalOriginY) * fontScale;
if (left < right && top < bottom)
{
auto glyphX = baselineX;
auto glyphY = baselineY;
if (glyphRun.glyphOffsets)
{
glyphX += glyphRun.glyphOffsets[i].advanceOffset;
glyphY -= glyphRun.glyphOffsets[i].ascenderOffset;
}
accumulatedBounds.left = std::min(accumulatedBounds.left, left + glyphX);
accumulatedBounds.top = std::min(accumulatedBounds.top, top + glyphY);
accumulatedBounds.right = std::max(accumulatedBounds.right, right + glyphX);
accumulatedBounds.bottom = std::max(accumulatedBounds.bottom, bottom + glyphY);
}
baselineX += glyphAdvance;
}
return accumulatedBounds;
}
void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* row, u16 y)
{
const auto widthShift = gsl::narrow_cast<u8>(row->lineRendition != LineRendition::SingleWidth);
const auto cellSize = p.s->font->cellSize;
const auto rowTop = gsl::narrow_cast<i16>(cellSize.y * y);
const auto rowBottom = gsl::narrow_cast<i16>(rowTop + cellSize.y);
const auto textCellCenter = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : rowTop;
const auto appendVerticalLines = [&](const GridLineRange& r, FontDecorationPosition pos) {
const auto from = r.from >> widthShift;
const auto to = r.to >> widthShift;
auto posX = from * cellSize.x + pos.position;
const auto end = to * cellSize.x;
D2D1_POINT_2F point0{ 0, static_cast<f32>(textCellCenter) };
D2D1_POINT_2F point1{ 0, static_cast<f32>(textCellCenter + cellSize.y) };
const auto brush = _brushWithColor(r.color);
const f32 w = pos.height;
const f32 hw = w * 0.5f;
for (; posX < end; posX += cellSize.x)
{
const auto centerX = posX + hw;
point0.x = centerX;
point1.x = centerX;
_renderTarget->DrawLine(point0, point1, brush, w, nullptr);
}
};
const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ID2D1StrokeStyle* strokeStyle) {
const auto from = r.from >> widthShift;
const auto to = r.to >> widthShift;
const auto brush = _brushWithColor(r.color);
const f32 w = pos.height;
const f32 centerY = textCellCenter + pos.position + w * 0.5f;
const D2D1_POINT_2F point0{ static_cast<f32>(from * cellSize.x), centerY };
const D2D1_POINT_2F point1{ static_cast<f32>(to * cellSize.x), centerY };
_renderTarget->DrawLine(point0, point1, brush, w, strokeStyle);
};
for (const auto& r : row->gridLineRanges)
{
// AtlasEngine.cpp shouldn't add any gridlines if they don't do anything.
assert(r.lines.any());
if (r.lines.test(GridLines::Left))
{
appendVerticalLines(r, p.s->font->gridLeft);
}
if (r.lines.test(GridLines::Right))
{
appendVerticalLines(r, p.s->font->gridRight);
}
if (r.lines.test(GridLines::Top))
{
appendHorizontalLine(r, p.s->font->gridTop, nullptr);
}
if (r.lines.test(GridLines::Bottom))
{
appendHorizontalLine(r, p.s->font->gridBottom, nullptr);
}
if (r.lines.test(GridLines::Underline))
{
appendHorizontalLine(r, p.s->font->underline, nullptr);
}
if (r.lines.test(GridLines::HyperlinkUnderline))
{
appendHorizontalLine(r, p.s->font->underline, _dottedStrokeStyle.get());
}
if (r.lines.test(GridLines::DoubleUnderline))
{
for (const auto pos : p.s->font->doubleUnderline)
{
appendHorizontalLine(r, pos, nullptr);
}
}
if (r.lines.test(GridLines::Strikethrough))
{
appendHorizontalLine(r, p.s->font->strikethrough, nullptr);
}
}
}
void BackendD2D::_drawCursorPart1(const RenderingPayload& p)
{
if (p.cursorRect.empty())
{
return;
}
const auto cursorColor = p.s->cursor->cursorColor;
if (cursorColor != 0xffffffff)
{
const D2D1_RECT_F rect{
static_cast<f32>(p.cursorRect.left * p.s->font->cellSize.x),
static_cast<f32>(p.cursorRect.top * p.s->font->cellSize.y),
static_cast<f32>(p.cursorRect.right * p.s->font->cellSize.x),
static_cast<f32>(p.cursorRect.bottom * p.s->font->cellSize.y),
};
const auto brush = _brushWithColor(cursorColor);
_drawCursor(p, _renderTarget.get(), rect, brush);
}
}
void BackendD2D::_drawCursorPart2(const RenderingPayload& p)
{
if (p.cursorRect.empty())
{
return;
}
if (p.s->cursor->cursorColor == 0xffffffff)
{
const auto cursorSize = p.cursorRect.size();
if (cursorSize != _cursorBitmapSize)
{
_resizeCursorBitmap(p, cursorSize);
}
const D2D1_POINT_2F target{
static_cast<f32>(p.cursorRect.left * p.s->font->cellSize.x),
static_cast<f32>(p.cursorRect.top * p.s->font->cellSize.y),
};
_renderTarget->DrawImage(_cursorBitmap.get(), &target, nullptr, D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR, D2D1_COMPOSITE_MODE_MASK_INVERT);
}
}
void BackendD2D::_resizeCursorBitmap(const RenderingPayload& p, const til::size newSize)
{
const til::size newSizeInPx{
newSize.width * p.s->font->cellSize.x,
newSize.height * p.s->font->cellSize.y,
};
// CreateCompatibleRenderTarget is a terrific API and does not adopt _any_ of the settings of the
// parent render target (like the AA mode or D2D1_UNIT_MODE_PIXELS). Not sure who came up with that,
// but fact is that we need to set both sizes to override the DPI and fake D2D1_UNIT_MODE_PIXELS.
const D2D1_SIZE_F sizeF{ static_cast<f32>(newSizeInPx.width), static_cast<f32>(newSizeInPx.height) };
const D2D1_SIZE_U sizeU{ gsl::narrow_cast<UINT32>(newSizeInPx.width), gsl::narrow_cast<UINT32>(newSizeInPx.height) };
wil::com_ptr<ID2D1BitmapRenderTarget> cursorRenderTarget;
_renderTarget->CreateCompatibleRenderTarget(&sizeF, &sizeU, nullptr, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, cursorRenderTarget.addressof());
cursorRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
cursorRenderTarget->BeginDraw();
{
const D2D1_RECT_F rect{ 0, 0, sizeF.width, sizeF.height };
const auto brush = _brushWithColor(0xffffffff);
_drawCursor(p, cursorRenderTarget.get(), rect, brush);
}
THROW_IF_FAILED(cursorRenderTarget->EndDraw());
cursorRenderTarget->GetBitmap(_cursorBitmap.put());
_cursorBitmapSize = newSize;
}
void BackendD2D::_drawCursor(const RenderingPayload& p, ID2D1RenderTarget* renderTarget, D2D1_RECT_F rect, ID2D1Brush* brush) noexcept
{
switch (static_cast<CursorType>(p.s->cursor->cursorType))
{
case CursorType::Legacy:
{
const auto height = p.s->cursor->heightPercentage / 100.0f;
rect.top = roundf((rect.top - rect.bottom) * height + rect.bottom);
renderTarget->FillRectangle(&rect, brush);
break;
}
case CursorType::VerticalBar:
rect.right = rect.left + p.s->font->thinLineWidth;
renderTarget->FillRectangle(&rect, brush);
break;
case CursorType::Underscore:
rect.top += p.s->font->underline.position;
rect.bottom = rect.top + p.s->font->underline.height;
renderTarget->FillRectangle(&rect, brush);
break;
case CursorType::EmptyBox:
{
const auto w = static_cast<f32>(p.s->font->thinLineWidth);
const auto wh = w / 2.0f;
rect.left += wh;
rect.top += wh;
rect.right -= wh;
rect.bottom -= wh;
renderTarget->DrawRectangle(&rect, brush, w, nullptr);
break;
}
case CursorType::FullBox:
renderTarget->FillRectangle(&rect, brush);
break;
case CursorType::DoubleUnderscore:
{
auto rect2 = rect;
rect2.top = rect.top + p.s->font->doubleUnderline[0].position;
rect2.bottom = rect2.top + p.s->font->thinLineWidth;
renderTarget->FillRectangle(&rect2, brush);
rect.top = rect.top + p.s->font->doubleUnderline[1].position;
rect.bottom = rect.top + p.s->font->thinLineWidth;
renderTarget->FillRectangle(&rect, brush);
break;
}
default:
break;
}
}
void BackendD2D::_drawSelection(const RenderingPayload& p)
{
u16 y = 0;
for (const auto& row : p.rows)
{
if (row->selectionTo > row->selectionFrom)
{
const D2D1_RECT_F rect{
static_cast<f32>(p.s->font->cellSize.x * row->selectionFrom),
static_cast<f32>(p.s->font->cellSize.y * y),
static_cast<f32>(p.s->font->cellSize.x * row->selectionTo),
static_cast<f32>(p.s->font->cellSize.y * (y + 1)),
};
_fillRectangle(rect, p.s->misc->selectionColor);
}
y++;
}
}
#if ATLAS_DEBUG_SHOW_DIRTY
void BackendD2D::_debugShowDirty(const RenderingPayload& p)
{
_presentRects[_presentRectsPos] = p.dirtyRectInPx;
_presentRectsPos = (_presentRectsPos + 1) % std::size(_presentRects);
for (size_t i = 0; i < std::size(_presentRects); ++i)
{
if (const auto& rect = _presentRects[i])
{
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 = colorbrewer::pastel1[i] | 0x1f000000;
_fillRectangle(rectF, color);
}
}
}
#endif
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
void BackendD2D::_debugDumpRenderTarget(const RenderingPayload& p)
{
if (_dumpRenderTargetCounter == 0)
{
ExpandEnvironmentStringsW(ATLAS_DEBUG_DUMP_RENDER_TARGET_PATH, &_dumpRenderTargetBasePath[0], gsl::narrow_cast<DWORD>(std::size(_dumpRenderTargetBasePath)));
std::filesystem::create_directories(_dumpRenderTargetBasePath);
}
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]);
_dumpRenderTargetCounter++;
}
#endif
ID2D1SolidColorBrush* BackendD2D::_brushWithColor(u32 color)
{
if (_brushColor != color)
{
_brushWithColorUpdate(color);
}
return _brush.get();
}
ID2D1SolidColorBrush* BackendD2D::_brushWithColorUpdate(u32 color)
{
const auto d2dColor = colorFromU32(color);
_brush->SetColor(&d2dColor);
_brushColor = color;
return _brush.get();
}
void BackendD2D::_fillRectangle(const D2D1_RECT_F& rect, u32 color)
{
const auto brush = _brushWithColor(color);
_renderTarget->FillRectangle(&rect, brush);
}
TIL_FAST_MATH_END

Просмотреть файл

@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <til/flat_set.h>
#include "Backend.h"
namespace Microsoft::Console::Render::Atlas
{
struct BackendD2D : IBackend
{
void ReleaseResources() noexcept override;
void Render(RenderingPayload& payload) override;
bool RequiresContinuousRedraw() noexcept override;
private:
ATLAS_ATTR_COLD void _handleSettingsUpdate(const RenderingPayload& p);
void _drawBackground(const RenderingPayload& p) noexcept;
void _drawText(RenderingPayload& p);
ATLAS_ATTR_COLD f32 _drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept;
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);
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;
void _resizeCursorBitmap(const RenderingPayload& p, til::size newSize);
void _drawSelection(const RenderingPayload& p);
void _debugShowDirty(const RenderingPayload& p);
void _debugDumpRenderTarget(const RenderingPayload& p);
ID2D1SolidColorBrush* _brushWithColor(u32 color);
ATLAS_ATTR_COLD ID2D1SolidColorBrush* _brushWithColorUpdate(u32 color);
void _fillRectangle(const D2D1_RECT_F& rect, u32 color);
wil::com_ptr<ID2D1DeviceContext> _renderTarget;
wil::com_ptr<ID2D1DeviceContext4> _renderTarget4; // Optional. Supported since Windows 10 14393.
wil::com_ptr<ID2D1StrokeStyle> _dottedStrokeStyle;
wil::com_ptr<ID2D1Bitmap> _backgroundBitmap;
wil::com_ptr<ID2D1BitmapBrush> _backgroundBrush;
til::generation_t _backgroundBitmapGeneration;
wil::com_ptr<ID2D1Bitmap> _cursorBitmap;
til::size _cursorBitmapSize; // in columns/rows
wil::com_ptr<ID2D1SolidColorBrush> _emojiBrush;
wil::com_ptr<ID2D1SolidColorBrush> _brush;
u32 _brushColor = 0;
Buffer<DWRITE_GLYPH_METRICS> _glyphMetrics;
til::generation_t _generation;
til::generation_t _fontGeneration;
til::generation_t _cursorGeneration;
u16x2 _cellCount{};
#if ATLAS_DEBUG_SHOW_DIRTY
til::rect _presentRects[9]{};
size_t _presentRectsPos = 0;
#endif
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
wchar_t _dumpRenderTargetBasePath[MAX_PATH]{};
size_t _dumpRenderTargetCounter = 0;
#endif
};
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,311 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <stb_rect_pack.h>
#include <til/flat_set.h>
#include <til/small_vector.h>
#include "Backend.h"
namespace Microsoft::Console::Render::Atlas
{
struct BackendD3D : IBackend
{
BackendD3D(const RenderingPayload& p);
void ReleaseResources() noexcept override;
void Render(RenderingPayload& payload) override;
bool RequiresContinuousRedraw() noexcept override;
// NOTE: D3D constant buffers sizes must be a multiple of 16 bytes.
struct alignas(16) VSConstBuffer
{
// WARNING: Modify this carefully after understanding how HLSL struct packing works. The gist is:
// * Minimum alignment is 4 bytes
// * Members cannot straddle 16 byte boundaries
// This means a structure like {u32; u32; u32; u32x2} would require
// padding so that it is {u32; u32; u32; <4 byte padding>; u32x2}.
// * bool will probably not work the way you want it to,
// because HLSL uses 32-bit bools and C++ doesn't.
alignas(sizeof(f32x2)) f32x2 positionScale;
#pragma warning(suppress : 4324) // 'VSConstBuffer': structure was padded due to alignment specifier
};
// WARNING: Same rules as for VSConstBuffer above apply.
struct alignas(16) PSConstBuffer
{
alignas(sizeof(f32x4)) f32x4 backgroundColor;
alignas(sizeof(f32x2)) f32x2 cellSize;
alignas(sizeof(f32x2)) f32x2 cellCount;
alignas(sizeof(f32x4)) f32 gammaRatios[4]{};
alignas(sizeof(f32)) f32 enhancedContrast = 0;
alignas(sizeof(f32)) f32 underlineWidth = 0;
#pragma warning(suppress : 4324) // 'PSConstBuffer': structure was padded due to alignment specifier
};
// WARNING: Same rules as for VSConstBuffer above apply.
struct alignas(16) CustomConstBuffer
{
alignas(sizeof(f32)) f32 time = 0;
alignas(sizeof(f32)) f32 scale = 0;
alignas(sizeof(f32x2)) f32x2 resolution;
alignas(sizeof(f32x4)) f32x4 background;
#pragma warning(suppress : 4324) // 'CustomConstBuffer': structure was padded due to alignment specifier
};
enum class ShadingType : u32
{
Default = 0,
Background = 0,
// This block of values will be used for the TextDrawingFirst/Last range and need to stay together.
// This is used to quickly check if an instance is related to a "text drawing primitive".
TextGrayscale = 1,
TextClearType = 2,
TextPassthrough = 3,
DottedLine = 4,
DottedLineWide = 5,
// All items starting here will be drawing as a solid RGBA color
SolidLine = 6,
Cursor = 7,
Selection = 8,
TextDrawingFirst = TextGrayscale,
TextDrawingLast = SolidLine,
};
// NOTE: Don't initialize any members in this struct. This ensures that no
// zero-initialization needs to occur when we allocate large buffers of this object.
struct QuadInstance
{
// `position` might clip outside of the bounds of the viewport and so it needs to be a
// signed coordinate. i16x2 is used as the size of the instance buffer made the largest
// impact on performance and power draw. If (when?) displays with >32k resolution make their
// appearance in the future, this should be changed to f32x2. But if you do so, please change
// all other occurrences of i16x2 positions/offsets throughout the class to keep it consistent.
alignas(u32) ShadingType shadingType;
alignas(u32) i16x2 position;
alignas(u32) u16x2 size;
alignas(u32) u16x2 texcoord;
alignas(u32) u32 color;
};
struct alignas(u32) AtlasGlyphEntryData
{
u16 shadingType;
u16 overlapSplit;
i16x2 offset;
u16x2 size;
u16x2 texcoord;
constexpr ShadingType GetShadingType() const noexcept
{
return static_cast<ShadingType>(shadingType);
}
};
// NOTE: Don't initialize any members in this struct. This ensures that no
// zero-initialization needs to occur when we allocate large buffers of this object.
struct AtlasGlyphEntry
{
u16 glyphIndex;
// All data in QuadInstance is u32-aligned anyways, so this simultaneously serves as padding.
u16 _occupied;
AtlasGlyphEntryData data;
constexpr bool operator==(u16 key) const noexcept
{
return glyphIndex == key;
}
constexpr operator bool() const noexcept
{
return _occupied != 0;
}
constexpr AtlasGlyphEntry& operator=(u16 key) noexcept
{
glyphIndex = key;
_occupied = 1;
return *this;
}
};
// This exists so that we can look up a AtlasFontFaceEntry without AddRef()/Release()ing fontFace first.
struct AtlasFontFaceKey
{
IDWriteFontFace2* fontFace;
LineRendition lineRendition;
};
struct AtlasFontFaceEntryInner
{
// BODGY: At the time of writing IDWriteFontFallback::MapCharacters returns the same IDWriteFontFace instance
// for the same font face variant as long as someone is holding a reference to the instance (see ActiveFaceCache).
// This allows us to hash the value of the pointer as if it was uniquely identifying the font face variant.
wil::com_ptr<IDWriteFontFace2> fontFace;
LineRendition lineRendition = LineRendition::SingleWidth;
til::linear_flat_set<AtlasGlyphEntry> glyphs;
};
struct AtlasFontFaceEntry
{
// This being a heap allocated allows us to insert into `glyphs` in `_splitDoubleHeightGlyph`
// (which might resize the hashmap!), while the caller `_drawText` is holding onto `glyphs`.
// If it wasn't heap allocated, all pointers into `linear_flat_set` would be invalidated.
std::unique_ptr<AtlasFontFaceEntryInner> inner;
bool operator==(const AtlasFontFaceKey& key) const noexcept
{
const auto& i = *inner;
return i.fontFace.get() == key.fontFace && i.lineRendition == key.lineRendition;
}
operator bool() const noexcept
{
return static_cast<bool>(inner);
}
AtlasFontFaceEntry& operator=(const AtlasFontFaceKey& key)
{
inner = std::make_unique<AtlasFontFaceEntryInner>();
auto& i = *inner;
i.fontFace = key.fontFace;
i.lineRendition = key.lineRendition;
return *this;
}
};
private:
struct CursorRect
{
i16x2 position;
u16x2 size;
u32 background;
u32 foreground;
};
ATLAS_ATTR_COLD void _handleSettingsUpdate(const RenderingPayload& p);
void _updateFontDependents(const RenderingPayload& p);
void _d2dRenderTargetUpdateFontSettings(const RenderingPayload& p) const noexcept;
void _recreateCustomShader(const RenderingPayload& p);
void _recreateCustomRenderTargetView(const RenderingPayload& p);
void _recreateBackgroundColorBitmap(const RenderingPayload& p);
void _recreateConstBuffer(const RenderingPayload& p) const;
void _setupDeviceContextState(const RenderingPayload& p);
void _debugUpdateShaders(const RenderingPayload& p) noexcept;
void _debugShowDirty(const RenderingPayload& p);
void _debugDumpRenderTarget(const RenderingPayload& p);
void _d2dBeginDrawing() noexcept;
void _d2dEndDrawing();
ATLAS_ATTR_COLD void _resetGlyphAtlas(const RenderingPayload& p);
ATLAS_ATTR_COLD void _resizeGlyphAtlas(const RenderingPayload& p, u16 u, u16 v);
QuadInstance& _getLastQuad() noexcept;
QuadInstance& _appendQuad();
ATLAS_ATTR_COLD void _bumpInstancesSize();
void _flushQuads(const RenderingPayload& p);
ATLAS_ATTR_COLD void _recreateInstanceBuffers(const RenderingPayload& p);
void _drawBackground(const RenderingPayload& p);
void _uploadBackgroundBitmap(const RenderingPayload& p);
void _drawText(RenderingPayload& p);
ATLAS_ATTR_COLD void _drawTextOverlapSplit(const RenderingPayload& p, u16 y);
ATLAS_ATTR_COLD [[nodiscard]] bool _drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
bool _drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
void _drawGlyphPrepareRetry(const RenderingPayload& p);
void _splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
void _drawGridlines(const RenderingPayload& p, u16 y);
void _drawCursorBackground(const RenderingPayload& p);
ATLAS_ATTR_COLD void _drawCursorForeground(const RenderingPayload& p);
ATLAS_ATTR_COLD size_t _drawCursorForegroundSlowPath(const RenderingPayload& p, const CursorRect& c, size_t offset);
void _drawSelection(const RenderingPayload& p);
void _executeCustomShader(RenderingPayload& p);
wil::com_ptr<ID3D11RenderTargetView> _renderTargetView;
wil::com_ptr<ID3D11InputLayout> _inputLayout;
wil::com_ptr<ID3D11VertexShader> _vertexShader;
wil::com_ptr<ID3D11PixelShader> _pixelShader;
wil::com_ptr<ID3D11BlendState> _blendState;
wil::com_ptr<ID3D11Buffer> _vsConstantBuffer;
wil::com_ptr<ID3D11Buffer> _psConstantBuffer;
wil::com_ptr<ID3D11Buffer> _vertexBuffer;
wil::com_ptr<ID3D11Buffer> _indexBuffer;
wil::com_ptr<ID3D11Buffer> _instanceBuffer;
size_t _instanceBufferCapacity = 0;
Buffer<QuadInstance, 32> _instances;
size_t _instancesCount = 0;
wil::com_ptr<ID3D11RenderTargetView> _customRenderTargetView;
wil::com_ptr<ID3D11Texture2D> _customOffscreenTexture;
wil::com_ptr<ID3D11ShaderResourceView> _customOffscreenTextureView;
wil::com_ptr<ID3D11VertexShader> _customVertexShader;
wil::com_ptr<ID3D11PixelShader> _customPixelShader;
wil::com_ptr<ID3D11Buffer> _customShaderConstantBuffer;
wil::com_ptr<ID3D11SamplerState> _customShaderSamplerState;
std::chrono::steady_clock::time_point _customShaderStartTime;
wil::com_ptr<ID3D11Texture2D> _backgroundBitmap;
wil::com_ptr<ID3D11ShaderResourceView> _backgroundBitmapView;
til::generation_t _backgroundBitmapGeneration;
wil::com_ptr<ID3D11Texture2D> _glyphAtlas;
wil::com_ptr<ID3D11ShaderResourceView> _glyphAtlasView;
til::linear_flat_set<AtlasFontFaceEntry> _glyphAtlasMap;
Buffer<stbrp_node> _rectPackerData;
stbrp_context _rectPacker{};
til::CoordType _ligatureOverhangTriggerLeft = 0;
til::CoordType _ligatureOverhangTriggerRight = 0;
wil::com_ptr<ID2D1DeviceContext> _d2dRenderTarget;
wil::com_ptr<ID2D1DeviceContext4> _d2dRenderTarget4; // Optional. Supported since Windows 10 14393.
wil::com_ptr<ID2D1SolidColorBrush> _emojiBrush;
wil::com_ptr<ID2D1SolidColorBrush> _brush;
wil::com_ptr<ID2D1Bitmap1> _softFontBitmap;
bool _d2dBeganDrawing = false;
bool _fontChangedResetGlyphAtlas = false;
float _gamma = 0;
float _cleartypeEnhancedContrast = 0;
float _grayscaleEnhancedContrast = 0;
wil::com_ptr<IDWriteRenderingParams1> _textRenderingParams;
til::generation_t _generation;
til::generation_t _fontGeneration;
til::generation_t _miscGeneration;
u16x2 _targetSize{};
u16x2 _cellCount{};
ShadingType _textShadingType = ShadingType::Default;
// An empty-box cursor spanning a wide glyph that has different
// background colors on each side results in 6 lines being drawn.
til::small_vector<CursorRect, 6> _cursorRects;
// The bounding rect of _cursorRects in pixels.
til::rect _cursorPosition;
bool _requiresContinuousRedraw = false;
#if ATLAS_DEBUG_SHOW_DIRTY
til::rect _presentRects[9]{};
size_t _presentRectsPos = 0;
#endif
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
wchar_t _dumpRenderTargetBasePath[MAX_PATH]{};
size_t _dumpRenderTargetCounter = 0;
#endif
#if ATLAS_DEBUG_COLORIZE_GLYPH_ATLAS
size_t _colorizeGlyphAtlasCounter = 0;
#endif
#ifndef NDEBUG
std::filesystem::path _sourceDirectory;
wil::unique_folder_change_reader_nothrow _sourceCodeWatcher;
std::atomic<int64_t> _sourceCodeInvalidationTime{ INT64_MAX };
#endif
};
}

Просмотреть файл

@ -1,10 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "DWriteTextAnalysis.h"
#pragma warning(disable : 4100) // '...': unreferenced formal parameter
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::Render::Atlas;
TextAnalysisSource::TextAnalysisSource(const wchar_t* _text, const UINT32 _textLength) noexcept :
_text{ _text },
@ -151,7 +154,8 @@ HRESULT TextAnalysisSink::QueryInterface(const IID& riid, void** ppvObject) noex
HRESULT __stdcall TextAnalysisSink::SetScriptAnalysis(UINT32 textPosition, UINT32 textLength, const DWRITE_SCRIPT_ANALYSIS* scriptAnalysis) noexcept
try
{
_results.emplace_back(TextAnalysisSinkResult{ textPosition, textLength, scriptAnalysis->script, static_cast<UINT8>(scriptAnalysis->shapes), 0 });
__assume(scriptAnalysis != nullptr);
_results.emplace_back(textPosition, textLength, *scriptAnalysis);
return S_OK;
}
CATCH_RETURN()

Просмотреть файл

@ -3,21 +3,10 @@
#pragma once
namespace Microsoft::Console::Render
#include "common.h"
namespace Microsoft::Console::Render::Atlas
{
struct TextAnalysisSinkResult
{
uint32_t textPosition = 0;
uint32_t textLength = 0;
// These 2 fields represent DWRITE_SCRIPT_ANALYSIS.
// Not using DWRITE_SCRIPT_ANALYSIS drops the struct size from 20 down to 12 bytes.
uint16_t script = 0;
uint8_t shapes = 0;
uint8_t bidiLevel = 0;
};
struct TextAnalysisSource final : IDWriteTextAnalysisSource
{
TextAnalysisSource(const wchar_t* _text, const UINT32 _textLength) noexcept;

Просмотреть файл

@ -0,0 +1,120 @@
# AtlasEngine
## General architecture overview
```mermaid
graph TD
RenderThread["RenderThread (base/thread.cpp)\n<small>calls Renderer::PaintFrame() x times per sec</small>"]
Renderer["Renderer (base/renderer.cpp)\n<small>breaks the text buffer down into GDI-oriented graphics\nprimitives (#quot;change brush to color X#quot;, #quot;draw string Y#quot;, ...)</small>"]
RenderEngineBase[/"RenderEngineBase\n(base/RenderEngineBase.cpp)\n<small>abstracts 24 LOC 👻</small>"\]
GdiEngine["GdiEngine (gdi/...)"]
DxEngine["DxEngine (dx/...)"]
subgraph AtlasEngine["AtlasEngine (atlas/...)"]
AtlasEngine.cpp["AtlasEngine.cpp\n<small>Implements IRenderEngine text rendering API\nbreaks GDI graphics primitives down into DWRITE_GLYPH_RUNs</small>"]
AtlasEngine.api.cpp["AtlasEngine.api.cpp\n<small>Implements the parts run inside the console\nlock (many IRenderEngine setters)<small>"]
AtlasEngine.r.cpp["AtlasEngine.r.cpp\n<small>Implements the parts run\noutside of the console lock<small>"]
Backend.cpp["Backend.cpp\n<small>Implements common functionality/helpers</small>"]
BackendD2D.cpp["BackendD2D.cpp\n<small>Pure Direct2D text renderer (for low latency\nremote desktop and older/no GPUs)</small>"]
BackendD3D.cpp["BackendD3D.cpp\n<small>Custom, performant text renderer\nwith our own glyph cache</small>"]
end
RenderThread --> Renderer
Renderer -->|owns| RenderThread
Renderer -.-> RenderEngineBase
%% Mermaid.js has no support for backwards arrow at the moment
RenderEngineBase <-.->|extends| GdiEngine
RenderEngineBase <-.->|extends| DxEngine
Renderer ----> AtlasEngine
AtlasEngine.cpp <--> AtlasEngine.api.cpp
AtlasEngine.cpp <--> AtlasEngine.r.cpp
AtlasEngine.r.cpp --> BackendD2D.cpp
AtlasEngine.r.cpp --> BackendD3D.cpp
BackendD2D.cpp -.- Backend.cpp
BackendD3D.cpp -.- Backend.cpp
```
As you can see, breaking the text buffer down into GDI-style primitives just to rebuild them into DirectWrite ones, is pretty wasteful. It's also incredibly bug prone. It would be beneficial if the TextBuffer and rendering settings were given directly to AtlasEngine so it can do its own bidding.
## BackendD3D
The primary entrypoint for rendering is `IBackend::Render` and `BackendD3D` implements it via the following functions, by calling them one by one in the order listed here.
### `_handleSettingsUpdate`
```mermaid
graph TD
Render --> _handleSettingsUpdate
_handleSettingsUpdate -->|font changes| _updateFontDependents --> _d2dRenderTargetUpdateFontSettings
_handleSettingsUpdate -->|misc changes| _recreateCustomShader
_handleSettingsUpdate --->|misc changes| _recreateCustomRenderTargetView
_handleSettingsUpdate ---->|size changes| _recreateBackgroundColorBitmap
_handleSettingsUpdate -----> _recreateConstBuffer
_handleSettingsUpdate ------> _setupDeviceContextState
```
### `_drawBackground`
```mermaid
graph TD
Render --> _drawBackground
_drawBackground --> _uploadBackgroundBitmap
```
### `_drawCursorPart1` / `_drawCursorPart2`
```mermaid
graph TD
Render --> _drawCursorPart1["_drawCursorPart1\nruns before _drawText\ndraws cursors that are behind the text"]
Render --> _drawCursorPart2["_drawCursorPart2\nruns after _drawText\ndraws inverted cursors"]
_drawCursorPart1 -.->|_cursorRects| _drawCursorPart2
```
### `_drawText`
```mermaid
graph TD
Render --> _drawText
_drawText --> foreachRow(("for each row"))
foreachRow --> foreachRow
foreachRow --> foreachFont(("for each font face"))
foreachFont --> foreachFont
foreachFont --> foreachGlyph(("for each glyph"))
foreachGlyph --> foreachGlyph
foreachGlyph --> _glyphAtlasMap[("font/glyph-pair lookup in\nglyph cache hashmap")]
_glyphAtlasMap --> drawGlyph
drawGlyph --> _appendQuad["_appendQuad\n<small>stages the glyph for later drawing</small>"]
_glyphAtlasMap --> _appendQuad
subgraph drawGlyph["if glyph is missing"]
_drawGlyph["_drawGlyph\n<small>(defers to _drawSoftFontGlyph for soft fonts)</small>"]
_drawGlyph -.->|if glpyh cache is full| _drawGlyphPrepareRetry
_drawGlyphPrepareRetry --> _flushQuads["_flushQuads\n<small>draws the current state\ninto the render target</small>"]
_flushQuads --> _recreateInstanceBuffers["_recreateInstanceBuffers\n<small>allocates a GPU buffer\nfor our glyph instances</small>"]
_drawGlyphPrepareRetry --> _resetGlyphAtlas["_resetGlyphAtlas\n<small>clears the glyph texture</small>"]
_resetGlyphAtlas --> _resizeGlyphAtlas["_resizeGlyphAtlas\n<small>resizes the glyph texture if it's still small</small>"]
_drawGlyph -.->|if it's a DECDHL glyph| _splitDoubleHeightGlyph["_splitDoubleHeightGlyph\n<small>DECDHL glyphs are split up into their\ntop/bottom halves to emulate clip rects</small>"]
end
foreachGlyph -.-> _drawTextOverlapSplit["_drawTextOverlapSplit\n<small>splits overly wide glyphs up into smaller chunks to support\nforeground color changes within the ligature</small>"]
foreachRow -.->|if gridlines exist| _drawGridlineRow["_drawGridlineRow\n<small>draws underlines, etc.</small>"]
```
### `_drawSelection`
```mermaid
graph TD
Render --> _drawSelection
```
### `_handleSettingsUpdate`
```mermaid
graph TD
Render --> _executeCustomShader
```

Просмотреть файл

@ -13,25 +13,35 @@
<ItemGroup>
<ClCompile Include="AtlasEngine.api.cpp" />
<ClCompile Include="AtlasEngine.r.cpp" />
<ClCompile Include="Backend.cpp" />
<ClCompile Include="BackendD2D.cpp" />
<ClCompile Include="BackendD3D.cpp" />
<ClCompile Include="dwrite.cpp" />
<ClCompile Include="DWriteTextAnalysis.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="AtlasEngine.cpp" />
<ClCompile Include="stb_rect_pack.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="Backend.h" />
<ClInclude Include="BackendD2D.h" />
<ClInclude Include="BackendD3D.h" />
<ClInclude Include="colorbrewer.h" />
<ClInclude Include="common.h" />
<ClInclude Include="dwrite.h" />
<ClInclude Include="DWriteTextAnalysis.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="AtlasEngine.h" />
<ClInclude Include="wic.h" />
</ItemGroup>
<ItemGroup>
<FxCompile Include="custom_shader_ps.hlsl">
<ShaderType>Pixel</ShaderType>
<ShaderModel>4.0</ShaderModel>
<AllResourcesBound>true</AllResourcesBound>
<VariableName>custom_shader_ps</VariableName>
<VariableName>%(Filename)</VariableName>
<ObjectFileOutput />
<HeaderFileOutput>$(OutDir)$(ProjectName)\%(Filename).h</HeaderFileOutput>
<TreatWarningAsError>true</TreatWarningAsError>
@ -42,7 +52,7 @@
<ShaderType>Vertex</ShaderType>
<ShaderModel>4.0</ShaderModel>
<AllResourcesBound>true</AllResourcesBound>
<VariableName>custom_shader_vs</VariableName>
<VariableName>%(Filename)</VariableName>
<ObjectFileOutput />
<HeaderFileOutput>$(OutDir)$(ProjectName)\%(Filename).h</HeaderFileOutput>
<TreatWarningAsError>true</TreatWarningAsError>
@ -52,11 +62,14 @@
<FxCompile Include="dwrite.hlsl">
<ExcludedFromBuild>true</ExcludedFromBuild>
</FxCompile>
<FxCompile Include="shader_common.hlsl">
<ExcludedFromBuild>true</ExcludedFromBuild>
</FxCompile>
<FxCompile Include="shader_ps.hlsl">
<ShaderType>Pixel</ShaderType>
<ShaderModel>4.0</ShaderModel>
<AllResourcesBound>true</AllResourcesBound>
<VariableName>shader_ps</VariableName>
<VariableName>%(Filename)</VariableName>
<ObjectFileOutput />
<HeaderFileOutput>$(OutDir)$(ProjectName)\%(Filename).h</HeaderFileOutput>
<TreatWarningAsError>true</TreatWarningAsError>
@ -67,7 +80,7 @@
<ShaderType>Vertex</ShaderType>
<ShaderModel>4.0</ShaderModel>
<AllResourcesBound>true</AllResourcesBound>
<VariableName>shader_vs</VariableName>
<VariableName>%(Filename)</VariableName>
<ObjectFileOutput />
<HeaderFileOutput>$(OutDir)$(ProjectName)\%(Filename).h</HeaderFileOutput>
<TreatWarningAsError>true</TreatWarningAsError>
@ -75,12 +88,15 @@
<AdditionalOptions Condition="'$(Configuration)'=='Release'">/O3 /Qstrip_debug /Qstrip_reflect %(AdditionalOptions)</AdditionalOptions>
</FxCompile>
</ItemGroup>
<ItemGroup>
<None Include="README.md" />
</ItemGroup>
<Import Project="$(SolutionDir)src\common.build.post.props" />
<Import Project="$(SolutionDir)src\common.nugetversions.targets" />
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<AdditionalIncludeDirectories>$(OutDir)$(ProjectName)\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(SolutionDir)\oss\stb;$(OutDir)$(ProjectName);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
</Project>

Просмотреть файл

@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
namespace Microsoft::Console::Render::Atlas::colorbrewer
{
// The following list of colors is only used as a debug aid and not part of the final product.
// They're licensed under:
//
// Apache-Style Software License for ColorBrewer software and ColorBrewer Color Schemes
//
// Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State University.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
//
inline constexpr u32 pastel1[]{
0xfbb4ae,
0xb3cde3,
0xccebc5,
0xdecbe4,
0xfed9a6,
0xffffcc,
0xe5d8bd,
0xfddaec,
0xf2f2f2,
};
}

557
src/renderer/atlas/common.h Normal file
Просмотреть файл

@ -0,0 +1,557 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <til/generational.h>
#include "../../renderer/inc/IRenderEngine.hpp"
namespace Microsoft::Console::Render::Atlas
{
#define ATLAS_FLAG_OPS(type, underlying) \
constexpr type operator~(type v) noexcept \
{ \
return static_cast<type>(~static_cast<underlying>(v)); \
} \
constexpr type operator|(type lhs, type rhs) noexcept \
{ \
return static_cast<type>(static_cast<underlying>(lhs) | static_cast<underlying>(rhs)); \
} \
constexpr type operator&(type lhs, type rhs) noexcept \
{ \
return static_cast<type>(static_cast<underlying>(lhs) & static_cast<underlying>(rhs)); \
} \
constexpr type operator^(type lhs, type rhs) noexcept \
{ \
return static_cast<type>(static_cast<underlying>(lhs) ^ static_cast<underlying>(rhs)); \
} \
constexpr void operator|=(type& lhs, type rhs) noexcept \
{ \
lhs = lhs | rhs; \
} \
constexpr void operator&=(type& lhs, type rhs) noexcept \
{ \
lhs = lhs & rhs; \
} \
constexpr void operator^=(type& lhs, type rhs) noexcept \
{ \
lhs = lhs ^ rhs; \
}
#define ATLAS_POD_OPS(type) \
constexpr bool operator==(const type& rhs) const noexcept \
{ \
return __builtin_memcmp(this, &rhs, sizeof(rhs)) == 0; \
} \
\
constexpr bool operator!=(const type& rhs) const noexcept \
{ \
return !(*this == rhs); \
}
// My best effort of replicating __attribute__((cold)) from gcc/clang.
#define ATLAS_ATTR_COLD __declspec(noinline)
template<typename T>
struct vec2
{
// These members aren't zero-initialized to make these trivial types,
// and allow the compiler to quickly memset() allocations, etc.
T x;
T y;
ATLAS_POD_OPS(vec2)
};
template<typename T>
struct vec4
{
// These members aren't zero-initialized to make these trivial types,
// and allow the compiler to quickly memset() allocations, etc.
T x;
T y;
T z;
T w;
ATLAS_POD_OPS(vec4)
};
template<typename T>
struct rect
{
// These members aren't zero-initialized to make these trivial types,
// and allow the compiler to quickly memset() allocations, etc.
T left;
T top;
T right;
T bottom;
ATLAS_POD_OPS(rect)
constexpr bool empty() const noexcept
{
return left >= right || top >= bottom;
}
constexpr bool non_empty() const noexcept
{
return left < right && top < bottom;
}
};
template<typename T>
struct range
{
T start;
T end;
ATLAS_POD_OPS(range)
constexpr bool empty() const noexcept
{
return start >= end;
}
constexpr bool non_empty() const noexcept
{
return start < end;
}
constexpr bool contains(T v) const noexcept
{
return v >= start && v < end;
}
};
using u8 = uint8_t;
using u16 = uint16_t;
using u16x2 = vec2<u16>;
using u16r = rect<u16>;
using i16 = int16_t;
using i16x2 = vec2<i16>;
using i16x4 = vec4<i16>;
using i16r = rect<i16>;
using u32 = uint32_t;
using u32x2 = vec2<u32>;
using u32x4 = vec4<u32>;
using u32r = rect<u32>;
using i32 = int32_t;
using i32x2 = vec2<i32>;
using i32x4 = vec4<i32>;
using i32r = rect<i32>;
using f32 = float;
using f32x2 = vec2<f32>;
using f32x4 = vec4<f32>;
using f32r = rect<f32>;
// I wrote `Buffer` instead of using `std::vector`, because I want to convey that these things
// explicitly _don't_ hold resizeable contents, but rather plain content of a fixed size.
// For instance I didn't want a resizeable vector with a `push_back` method for my fixed-size
// viewport arrays - that doesn't make sense after all. `Buffer` also doesn't initialize
// contents to zero, allowing rapid creation/destruction and you can easily specify a custom
// (over-)alignment which can improve rendering perf by up to ~20% over `std::vector`.
template<typename T, size_t Alignment = alignof(T)>
struct Buffer
{
constexpr Buffer() noexcept = default;
explicit Buffer(size_t size) :
_data{ allocate(size) },
_size{ size }
{
std::uninitialized_default_construct_n(_data, size);
}
Buffer(const T* data, size_t size) :
_data{ allocate(size) },
_size{ size }
{
// Changing the constructor arguments to accept std::span might
// be a good future extension, but not to improve security here.
// You can trivially construct std::span's from invalid ranges.
// Until then the raw-pointer style is more practical.
#pragma warning(suppress : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...].
std::uninitialized_copy_n(data, size, _data);
}
~Buffer()
{
destroy();
}
Buffer(const Buffer& other) = delete;
Buffer& operator=(const Buffer& other) = delete;
Buffer(Buffer&& other) noexcept :
_data{ std::exchange(other._data, nullptr) },
_size{ std::exchange(other._size, 0) }
{
}
Buffer& operator=(Buffer&& other) noexcept
{
destroy();
_data = std::exchange(other._data, nullptr);
_size = std::exchange(other._size, 0);
return *this;
}
explicit operator bool() const noexcept
{
return _data != nullptr;
}
T& operator[](size_t index) noexcept
{
assert(index < _size);
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
return _data[index];
}
const T& operator[](size_t index) const noexcept
{
assert(index < _size);
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
return _data[index];
}
T* data() noexcept
{
return _data;
}
const T* data() const noexcept
{
return _data;
}
size_t size() const noexcept
{
return _size;
}
T* begin() noexcept
{
return _data;
}
const T* begin() const noexcept
{
return _data;
}
T* end() noexcept
{
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
return _data + _size;
}
const T* end() const noexcept
{
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
return _data + _size;
}
private:
// These two functions don't need to use scoped objects or standard allocators,
// since this class is in fact an scoped allocator object itself.
#pragma warning(push)
#pragma warning(disable : 26402) // Return a scoped object instead of a heap-allocated if it has a move constructor (r.3).
#pragma warning(disable : 26409) // Avoid calling new and delete explicitly, use std::make_unique<T> instead (r.11).
static T* allocate(size_t size)
{
if (!size)
{
return nullptr;
}
if constexpr (Alignment <= __STDCPP_DEFAULT_NEW_ALIGNMENT__)
{
return static_cast<T*>(::operator new(size * sizeof(T)));
}
else
{
return static_cast<T*>(::operator new(size * sizeof(T), static_cast<std::align_val_t>(Alignment)));
}
}
static void deallocate(T* data) noexcept
{
if constexpr (Alignment <= __STDCPP_DEFAULT_NEW_ALIGNMENT__)
{
::operator delete(data);
}
else
{
::operator delete(data, static_cast<std::align_val_t>(Alignment));
}
}
#pragma warning(pop)
void destroy() noexcept
{
std::destroy_n(_data, _size);
deallocate(_data);
}
T* _data = nullptr;
size_t _size = 0;
};
struct TextAnalysisSinkResult
{
uint32_t textPosition;
uint32_t textLength;
DWRITE_SCRIPT_ANALYSIS analysis;
};
struct TargetSettings
{
HWND hwnd = nullptr;
bool enableTransparentBackground = false;
bool useSoftwareRendering = false;
};
enum class AntialiasingMode : u8
{
ClearType = D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE,
Grayscale = D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE,
Aliased = D2D1_TEXT_ANTIALIAS_MODE_ALIASED,
};
inline constexpr auto DefaultAntialiasingMode = AntialiasingMode::ClearType;
struct FontDecorationPosition
{
u16 position = 0;
u16 height = 0;
};
struct FontSettings
{
wil::com_ptr<IDWriteFontCollection> fontCollection;
wil::com_ptr<IDWriteFontFamily> fontFamily;
std::wstring fontName;
std::vector<DWRITE_FONT_FEATURE> fontFeatures;
std::vector<DWRITE_FONT_AXIS_VALUE> fontAxisValues;
f32 fontSize = 0;
u16x2 cellSize;
u16 fontWeight = 0;
u16 advanceWidth = 0;
u16 baseline = 0;
u16 descender = 0;
u16 thinLineWidth = 0;
FontDecorationPosition gridTop;
FontDecorationPosition gridBottom;
FontDecorationPosition gridLeft;
FontDecorationPosition gridRight;
FontDecorationPosition underline;
FontDecorationPosition strikethrough;
FontDecorationPosition doubleUnderline[2];
FontDecorationPosition overline;
u16 dpi = 96;
AntialiasingMode antialiasingMode = DefaultAntialiasingMode;
std::vector<uint16_t> softFontPattern;
til::size softFontCellSize;
};
struct CursorSettings
{
ATLAS_POD_OPS(CursorSettings)
u32 cursorColor = 0xffffffff;
u16 cursorType = 0;
u16 heightPercentage = 20;
};
struct MiscellaneousSettings
{
u32 backgroundColor = 0;
u32 selectionColor = 0x7fffffff;
std::wstring customPixelShaderPath;
bool useRetroTerminalEffect = false;
};
struct Settings
{
til::generational<TargetSettings> target;
til::generational<FontSettings> font;
til::generational<CursorSettings> cursor;
til::generational<MiscellaneousSettings> misc;
u16x2 targetSize{};
u16x2 cellCount{};
};
using GenerationalSettings = til::generational<Settings>;
inline GenerationalSettings DirtyGenerationalSettings() noexcept
{
return GenerationalSettings{
til::generation_t{ 1 },
til::generational<TargetSettings>{ til::generation_t{ 1 } },
til::generational<FontSettings>{ til::generation_t{ 1 } },
til::generational<CursorSettings>{ til::generation_t{ 1 } },
til::generational<MiscellaneousSettings>{ til::generation_t{ 1 } },
};
}
enum class FontRelevantAttributes : u8
{
None = 0,
Bold = 0b01,
Italic = 0b10,
};
ATLAS_FLAG_OPS(FontRelevantAttributes, u8)
struct FontMapping
{
wil::com_ptr<IDWriteFontFace2> fontFace;
u32 glyphsFrom = 0;
u32 glyphsTo = 0;
};
struct GridLineRange
{
GridLineSet lines;
u32 color = 0;
u16 from = 0;
u16 to = 0;
};
struct ShapedRow
{
void clear(u16 y, u16 cellHeight) noexcept
{
mappings.clear();
glyphIndices.clear();
glyphAdvances.clear();
glyphOffsets.clear();
colors.clear();
gridLineRanges.clear();
lineRendition = LineRendition::SingleWidth;
selectionFrom = 0;
selectionTo = 0;
dirtyTop = y * cellHeight;
dirtyBottom = dirtyTop + cellHeight;
}
std::vector<FontMapping> mappings;
std::vector<u16> glyphIndices;
std::vector<f32> glyphAdvances; // same size as glyphIndices
std::vector<DWRITE_GLYPH_OFFSET> glyphOffsets; // same size as glyphIndices
std::vector<u32> colors; // same size as glyphIndices
std::vector<GridLineRange> gridLineRanges;
LineRendition lineRendition = LineRendition::SingleWidth;
u16 selectionFrom = 0;
u16 selectionTo = 0;
til::CoordType dirtyTop = 0;
til::CoordType dirtyBottom = 0;
};
struct RenderingPayload
{
//// Parameters which are constant across backends.
wil::com_ptr<ID2D1Factory> d2dFactory;
wil::com_ptr<IDWriteFactory2> dwriteFactory;
wil::com_ptr<IDWriteFactory4> dwriteFactory4; // optional, might be nullptr
wil::com_ptr<IDWriteFontFallback> systemFontFallback;
wil::com_ptr<IDWriteFontFallback1> systemFontFallback1; // optional, might be nullptr
wil::com_ptr<IDWriteTextAnalyzer1> textAnalyzer;
wil::com_ptr<IDWriteRenderingParams1> renderingParams;
std::function<void(HRESULT)> warningCallback;
std::function<void(HANDLE)> swapChainChangedCallback;
//// Parameters which are constant for the existence of the backend.
struct
{
wil::com_ptr<IDXGIFactory2> factory;
wil::com_ptr<IDXGIAdapter1> adapter;
LUID adapterLuid{};
UINT adapterFlags = 0;
} dxgi;
struct
{
wil::com_ptr<IDXGISwapChain2> swapChain;
wil::unique_handle handle;
wil::unique_handle frameLatencyWaitableObject;
til::generation_t generation;
til::generation_t targetGeneration;
til::generation_t fontGeneration;
u16x2 targetSize{};
bool waitForPresentation = false;
} swapChain;
wil::com_ptr<ID3D11Device2> device;
wil::com_ptr<ID3D11DeviceContext2> deviceContext;
//// Parameters which change seldom.
GenerationalSettings s;
//// Parameters which change every frame.
// This is the backing buffer for `rows`.
Buffer<ShapedRow> unorderedRows;
// This is used as a scratch buffer during scrolling.
Buffer<ShapedRow*> rowsScratch;
// This contains the rows in the right order from row 0 to N.
// They get rotated around when we scroll the buffer. Technically
// we could also implement scrolling by using a circular array.
Buffer<ShapedRow*> rows;
// This contains two viewport-sized bitmaps back to back, sort of like a Texture2DArray.
// The first NxM (for instance 120x30 pixel) chunk contains background colors and the
// second chunk contains foreground colors. The distance in u32 items between the start
// and the begin of the foreground bitmap is equal to colorBitmapDepthStride.
//
// The background part is in premultiplied alpha, whereas the foreground part is in straight
// alpha. This is mostly because of Direct2D being annoying, as the former is the only thing
// it supports for bitmaps, whereas the latter is the only thing it supports for text.
// Since we implement Direct2D's text blending algorithm, we're equally dependent on
// straight alpha for BackendD3D, as straight alpha is used in the pixel shader there.
Buffer<u32, 32> colorBitmap;
// This exists as a convenience access to colorBitmap and
// contains a view into the background color bitmap.
std::span<u32> backgroundBitmap;
// This exists as a convenience access to colorBitmap and
// contains a view into the foreground color bitmap.
std::span<u32> foregroundBitmap;
// This stride of the colorBitmap is a "count" of u32 and not in bytes.
size_t colorBitmapRowStride = 0;
// FYI depth refers to the `colorBitmapRowStride * height` size of each bitmap contained
// in colorBitmap. colorBitmap contains 2 bitmaps (background and foreground colors).
size_t colorBitmapDepthStride = 0;
// A generation of 1 ensures that the backends redraw the background on the first Present().
// The 1st entry in this array corresponds to the background and the 2nd to the foreground bitmap.
std::array<til::generation_t, 2> colorBitmapGenerations{ 1, 1 };
// In columns/rows.
til::rect cursorRect;
// In pixel.
til::rect dirtyRectInPx;
// In rows.
range<u16> invalidatedRows{};
// In pixel.
i16 scrollOffset = 0;
void MarkAllAsDirty() noexcept
{
dirtyRectInPx = { 0, 0, s->targetSize.x, s->targetSize.y };
invalidatedRows = { 0, s->cellCount.y };
scrollOffset = 0;
}
};
struct IBackend
{
virtual ~IBackend() = default;
virtual void ReleaseResources() noexcept = 0;
virtual void Render(RenderingPayload& payload) = 0;
virtual bool RequiresContinuousRedraw() noexcept = 0;
};
}

Просмотреть файл

@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// The original retro pixel shader
Texture2D shaderTexture;
SamplerState samplerState;

Просмотреть файл

@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
struct VS_OUTPUT
{
float4 pos : SV_POSITION;

Просмотреть файл

@ -17,8 +17,8 @@
#include <unordered_set>
#include <vector>
#include <d2d1_1.h>
#include <d3d11_1.h>
#include <d2d1_3.h>
#include <d3d11_2.h>
#include <d3dcompiler.h>
#include <dwrite_3.h>
#include <dcomp.h>

Просмотреть файл

@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// clang-format off
#define SHADING_TYPE_TEXT_BACKGROUND 0
#define SHADING_TYPE_TEXT_GRAYSCALE 1
#define SHADING_TYPE_TEXT_CLEARTYPE 2
#define SHADING_TYPE_TEXT_PASSTHROUGH 3
#define SHADING_TYPE_DOTTED_LINE 4
#define SHADING_TYPE_DOTTED_LINE_WIDE 5
// clang-format on
struct VSData
{
float2 vertex : SV_Position;
uint shadingType : shadingType;
int2 position : position;
uint2 size : size;
uint2 texcoord : texcoord;
float4 color : color;
};
struct PSData
{
float4 position : SV_Position;
float2 texcoord : texcoord;
nointerpolation uint shadingType : shadingType;
nointerpolation float4 color : color;
};
float4 premultiplyColor(float4 color)
{
color.rgb *= color.a;
return color;
}
float4 alphaBlendPremultiplied(float4 bottom, float4 top)
{
bottom *= 1 - top.a;
return bottom + top;
}
float4 decodeRGBA(uint i)
{
return (i >> uint4(0, 8, 16, 24) & 0xff) / 255.0f;
}

Просмотреть файл

@ -2,200 +2,99 @@
// Licensed under the MIT license.
#include "dwrite.hlsl"
#define INVALID_COLOR 0xffffffff
// These flags are shared with AtlasEngine::CellFlags.
//
// clang-format off
#define CellFlags_None 0x00000000
#define CellFlags_Inlined 0x00000001
#define CellFlags_ColoredGlyph 0x00000002
#define CellFlags_Cursor 0x00000008
#define CellFlags_Selected 0x00000010
#define CellFlags_BorderLeft 0x00000020
#define CellFlags_BorderTop 0x00000040
#define CellFlags_BorderRight 0x00000080
#define CellFlags_BorderBottom 0x00000100
#define CellFlags_Underline 0x00000200
#define CellFlags_UnderlineDotted 0x00000400
#define CellFlags_UnderlineDouble 0x00000800
#define CellFlags_Strikethrough 0x00001000
// clang-format on
// According to Nvidia's "Understanding Structured Buffer Performance" guide
// one should aim for structures with sizes divisible by 128 bits (16 bytes).
// This prevents elements from spanning cache lines.
struct Cell
{
uint glyphPos;
uint flags;
uint2 color; // x: foreground, y: background
};
#include "shader_common.hlsl"
cbuffer ConstBuffer : register(b0)
{
float4 viewport;
float4 backgroundColor;
float2 cellSize;
float2 cellCount;
float4 gammaRatios;
float enhancedContrast;
uint cellCountX;
uint2 cellSize;
uint underlinePos;
uint underlineWidth;
uint strikethroughPos;
uint strikethroughWidth;
uint2 doubleUnderlinePos;
uint thinLineWidth;
uint backgroundColor;
uint cursorColor;
uint selectionColor;
uint useClearType;
float underlineWidth;
}
Texture2D<float4> background : register(t0);
Texture2D<float4> glyphAtlas : register(t1);
struct Output
{
float4 color;
float4 weights;
};
StructuredBuffer<Cell> cells : register(t0);
Texture2D<float4> glyphs : register(t1);
float4 decodeRGBA(uint i)
{
float4 c = (i >> uint4(0, 8, 16, 24) & 0xff) / 255.0f;
// Convert to premultiplied alpha for simpler alpha blending.
c.rgb *= c.a;
return c;
}
uint2 decodeU16x2(uint i)
{
return uint2(i & 0xffff, i >> 16);
}
float4 alphaBlendPremultiplied(float4 bottom, float4 top)
{
bottom *= 1 - top.a;
return bottom + top;
}
// clang-format off
float4 main(float4 pos: SV_Position): SV_Target
Output main(PSData data) : SV_Target
// clang-format on
{
// We need to fill the entire render target with pixels, but only our "viewport"
// has cells we want to draw. The rest gets treated with the background color.
[branch] if (any(pos.xy < viewport.xy || pos.xy >= viewport.zw))
float4 color;
float4 weights;
switch (data.shadingType)
{
return decodeRGBA(backgroundColor);
case SHADING_TYPE_TEXT_BACKGROUND:
{
const float2 cell = data.position.xy / cellSize;
color = all(cell < cellCount) ? background[cell] : backgroundColor;
weights = float4(1, 1, 1, 1);
break;
}
case SHADING_TYPE_TEXT_GRAYSCALE:
{
// These are independent of the glyph texture and could be moved to the vertex shader or CPU side of things.
const float4 foreground = premultiplyColor(data.color);
const float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb);
const float intensity = DWrite_CalcColorIntensity(data.color.rgb);
// These aren't.
const float4 glyph = glyphAtlas[data.texcoord];
const float contrasted = DWrite_EnhanceContrast(glyph.a, blendEnhancedContrast);
const float alphaCorrected = DWrite_ApplyAlphaCorrection(contrasted, intensity, gammaRatios);
color = alphaCorrected * foreground;
weights = color.aaaa;
break;
}
case SHADING_TYPE_TEXT_CLEARTYPE:
{
// These are independent of the glyph texture and could be moved to the vertex shader or CPU side of things.
const float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb);
// These aren't.
const float4 glyph = glyphAtlas[data.texcoord];
const float3 contrasted = DWrite_EnhanceContrast3(glyph.rgb, blendEnhancedContrast);
const float3 alphaCorrected = DWrite_ApplyAlphaCorrection3(contrasted, data.color.rgb, gammaRatios);
weights = float4(alphaCorrected * data.color.a, 1);
color = weights * data.color;
break;
}
case SHADING_TYPE_TEXT_PASSTHROUGH:
{
color = glyphAtlas[data.texcoord];
weights = color.aaaa;
break;
}
case SHADING_TYPE_DOTTED_LINE:
{
const bool on = frac(data.position.x / (2.0f * underlineWidth)) < 0.5f;
color = on * premultiplyColor(data.color);
weights = color.aaaa;
break;
}
case SHADING_TYPE_DOTTED_LINE_WIDE:
{
const bool on = frac(data.position.x / (4.0f * underlineWidth)) < 0.5f;
color = on * premultiplyColor(data.color);
weights = color.aaaa;
break;
}
default:
{
color = premultiplyColor(data.color);
weights = color.aaaa;
break;
}
}
uint2 viewportPos = pos.xy - viewport.xy;
uint2 cellIndex = viewportPos / cellSize;
uint2 cellPos = viewportPos % cellSize;
Cell cell = cells[cellIndex.y * cellCountX + cellIndex.x];
// Layer 0:
// The cell's background color
float4 color = decodeRGBA(cell.color.y);
float4 fg = decodeRGBA(cell.color.x);
// Layer 1 (optional):
// Colored cursors are drawn "in between" the background color and the text of a cell.
[branch] if (cell.flags & CellFlags_Cursor)
{
[branch] if (cursorColor != INVALID_COLOR)
{
// The cursor texture is stored at the top-left-most glyph cell.
// Cursor pixels are either entirely transparent or opaque.
// --> We can just use .a as a mask to flip cursor pixels on or off.
color = alphaBlendPremultiplied(color, decodeRGBA(cursorColor) * glyphs[cellPos].a);
}
else if (glyphs[cellPos].a != 0)
{
// Make sure the cursor is always readable (see gh-3647)
// If we imagine the two colors to be in 0-255 instead of 0-1,
// this effectively XORs them with 63. This avoids a situation
// where a gray background color (0.5) gets inverted to the
// same gray making the cursor invisible.
float2x4 colors = { color, fg };
float2x4 ip; // integral part
float2x4 frac = modf(colors * (255.0f / 64.0f), ip);
colors = (3.0f - ip + frac) * (64.0f / 255.0f);
color = float4(colors[0].rgb, 1);
fg = float4(colors[1].rgb, 1);
}
}
// Layer 2:
// Step 1: The cell's glyph, potentially drawn in the foreground color
{
float4 glyph = glyphs[decodeU16x2(cell.glyphPos) + cellPos];
[branch] if (cell.flags & CellFlags_ColoredGlyph)
{
color = alphaBlendPremultiplied(color, glyph);
}
else
{
float3 foregroundStraight = DWrite_UnpremultiplyColor(fg);
float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, foregroundStraight);
[branch] if (useClearType)
{
// See DWrite_ClearTypeBlend
float3 contrasted = DWrite_EnhanceContrast3(glyph.rgb, blendEnhancedContrast);
float3 alphaCorrected = DWrite_ApplyAlphaCorrection3(contrasted, foregroundStraight, gammaRatios);
color = float4(lerp(color.rgb, foregroundStraight, alphaCorrected * fg.a), 1.0f);
}
else
{
// See DWrite_GrayscaleBlend
float intensity = DWrite_CalcColorIntensity(foregroundStraight);
float contrasted = DWrite_EnhanceContrast(glyph.a, blendEnhancedContrast);
float4 alphaCorrected = DWrite_ApplyAlphaCorrection(contrasted, intensity, gammaRatios);
color = alphaBlendPremultiplied(color, alphaCorrected * fg);
}
}
}
// Step 2: Lines
{
// What a nice coincidence that we have exactly 8 flags to handle right now!
// `mask` will mask away any positive results from checks we don't want.
// (I.e. even if we're in an underline, it doesn't matter if we don't want an underline.)
bool2x4 mask = {
cell.flags & CellFlags_BorderLeft,
cell.flags & CellFlags_BorderTop,
cell.flags & CellFlags_BorderRight,
cell.flags & CellFlags_BorderBottom,
cell.flags & CellFlags_Underline,
cell.flags & CellFlags_UnderlineDotted,
cell.flags & CellFlags_UnderlineDouble,
cell.flags & CellFlags_Strikethrough,
};
// The following <lineWidth checks rely on underflow turning the
// uint into a way larger number than any reasonable lineWidth.
// That way we don't need to write `y >= lo && y < hi`.
bool2x4 checks = {
// These 2 expand to 4 bools, because cellPos is a
// uint2 vector which results in a bool2 result each.
cellPos < thinLineWidth,
(cellSize - cellPos) <= thinLineWidth,
// These 4 are 4 regular bools.
(cellPos.y - underlinePos) < underlineWidth,
(cellPos.y - underlinePos) < underlineWidth && (viewportPos.x / underlineWidth & 3) == 0,
any((cellPos.y - doubleUnderlinePos) < thinLineWidth),
(cellPos.y - strikethroughPos) < strikethroughWidth,
};
[flatten] if (any(mask && checks))
{
color = alphaBlendPremultiplied(color, fg);
}
}
// Layer 4:
// The current selection is drawn semi-transparent on top.
[branch] if (cell.flags & CellFlags_Selected)
{
color = alphaBlendPremultiplied(color, decodeRGBA(selectionColor));
}
return color;
Output output;
output.color = color;
output.weights = weights;
return output;
}

Просмотреть файл

@ -1,17 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "shader_common.hlsl"
cbuffer ConstBuffer : register(b0)
{
float2 positionScale;
}
// clang-format off
float4 main(uint id: SV_VERTEXID): SV_POSITION
PSData main(VSData data)
// clang-format on
{
// The algorithm below is a fast way to generate a full screen triangle,
// published by Bill Bilodeau "Vertex Shader Tricks" at GDC14.
// It covers the entire viewport and is faster for the GPU than a quad/rectangle.
return float4(
float(id / 2) * 4.0 - 1.0,
float(id % 2) * 4.0 - 1.0,
0.0,
1.0
);
PSData output;
output.color = data.color;
output.shadingType = data.shadingType;
// positionScale is expected to be float2(2.0f / sizeInPixel.x, -2.0f / sizeInPixel.y). Together with the
// addition below this will transform our "position" from pixel into normalized device coordinate (NDC) space.
output.position.xy = (data.position + data.vertex.xy * data.size) * positionScale + float2(-1.0f, 1.0f);
output.position.zw = float2(0, 1);
output.texcoord = data.texcoord + data.vertex.xy * data.size;
return output;
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
// AtlasEngine doesn't use stb_rect_pack efficiently right now and packs rectangles one by one,
// because this simplifies the text rendering implementation quite a bit. On the flip side however,
// this allows us to skip sorting rectangles, because sorting arrays of size 1 is pointless.
#pragma warning(disable : 4505) // '...': unreferenced function with internal linkage has been removed
#define STBRP_SORT(_Base, _NumOfElements, _SizeOfElements, _CompareFunction) \
assert(_NumOfElements == 1)
#define STB_RECT_PACK_IMPLEMENTATION
#include "stb_rect_pack.h"

57
src/renderer/atlas/wic.h Normal file
Просмотреть файл

@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <wincodec.h>
inline void SaveTextureToPNG(ID3D11DeviceContext* deviceContext, ID3D11Resource* source, double dpi, const wchar_t* fileName)
{
__assume(deviceContext != nullptr);
__assume(source != nullptr);
wil::com_ptr<ID3D11Texture2D> texture;
THROW_IF_FAILED(source->QueryInterface(IID_PPV_ARGS(texture.addressof())));
wil::com_ptr<ID3D11Device> d3dDevice;
deviceContext->GetDevice(d3dDevice.addressof());
D3D11_TEXTURE2D_DESC desc{};
texture->GetDesc(&desc);
desc.BindFlags = 0;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
desc.Usage = D3D11_USAGE_STAGING;
wil::com_ptr<ID3D11Texture2D> staging;
THROW_IF_FAILED(d3dDevice->CreateTexture2D(&desc, nullptr, staging.put()));
deviceContext->CopyResource(staging.get(), source);
static const auto coUninitialize = wil::CoInitializeEx();
static const auto wicFactory = wil::CoCreateInstance<IWICImagingFactory2>(CLSID_WICImagingFactory2);
wil::com_ptr<IWICStream> stream;
THROW_IF_FAILED(wicFactory->CreateStream(stream.addressof()));
THROW_IF_FAILED(stream->InitializeFromFilename(fileName, GENERIC_WRITE));
wil::com_ptr<IWICBitmapEncoder> encoder;
THROW_IF_FAILED(wicFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.addressof()));
THROW_IF_FAILED(encoder->Initialize(stream.get(), WICBitmapEncoderNoCache));
wil::com_ptr<IWICBitmapFrameEncode> frame;
wil::com_ptr<IPropertyBag2> props;
THROW_IF_FAILED(encoder->CreateNewFrame(frame.addressof(), props.addressof()));
THROW_IF_FAILED(frame->Initialize(props.get()));
THROW_IF_FAILED(frame->SetSize(desc.Width, desc.Height));
THROW_IF_FAILED(frame->SetResolution(dpi, dpi));
auto pixelFormat = GUID_WICPixelFormat32bppBGRA;
THROW_IF_FAILED(frame->SetPixelFormat(&pixelFormat));
D3D11_MAPPED_SUBRESOURCE mapped;
THROW_IF_FAILED(deviceContext->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped));
THROW_IF_FAILED(frame->WritePixels(desc.Height, mapped.RowPitch, mapped.RowPitch * desc.Height, static_cast<BYTE*>(mapped.pData)));
deviceContext->Unmap(staging.get(), 0);
THROW_IF_FAILED(frame->Commit());
THROW_IF_FAILED(encoder->Commit());
}

Просмотреть файл

@ -149,6 +149,8 @@ int main()
};
static constexpr VTAttributeTest vtAttributeTests[]{
{ L"ANSI escape SGR:", 0 },
{ L"bold", 1 },
{ L"faint", 2 },
{ L"italic", 3 },
{ L"underline", 4 },
{ L"reverse", 7 },
@ -172,11 +174,43 @@ int main()
{
printUTF16(
L"\x1B[3;5HDECDWL Double Width \U0001FAE0 A\u0353\u0353 B\u036F\u036F"
L"\x1B[4;5H\x1b#6DECDWL Double Width \U0001FAE0 A\u0353\u0353 B\u036F\u036F"
L"\x1B[8;5HDECDHL Double Height \U0001F642\U0001F6C1 A\u0353\u0353 B\u036F\u036F X\u0353\u0353 Y\u036F\u036F"
L"\x1B[9;5H\x1b#3DECDHL Double Height Top \U0001F642 A\u0353\u0353 B\u036F\u036F"
L"\x1B[10;5H\x1b#4DECDHL Double Height Bottom \U0001F6C1 X\u0353\u0353 Y\u036F\u036F");
L"\x1B[3;5HDECDWL Double Width \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F"
L"\x1B[4;3H\x1b#6DECDWL Double Width \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F"
L"\x1B[7;5HDECDHL Double Height \U0001F952\U0001F6C1 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F"
L"\x1B[8;3H\x1b#3DECDHL Double Height Top \U0001F952 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m"
L"\x1B[9;3H\x1b#4DECDHL Double Height Bottom \U0001F6C1 \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F"
L"\x1B[13;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m"
L"\x1B[15;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"
L"\x1B[17;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m"
L"\x1B[19;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"
L"\x1B[21;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m"
L"\x1B[22;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m"
L"\x1B[24;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"
L"\x1B[25;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m");
static constexpr WORD attributes[]{
FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_HORIZONTAL,
FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_HORIZONTAL,
FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_LVERTICAL,
FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_LVERTICAL,
FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_RVERTICAL,
FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_RVERTICAL,
FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_UNDERSCORE,
FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_UNDERSCORE,
};
DWORD numberOfAttrsWritten;
DWORD offset = 0;
for (const auto r : { 12, 14, 16, 18, 20, 21, 23, 24 })
{
COORD coord;
coord.X = r > 14 ? 2 : 4;
coord.X += offset ? 2 : 0;
coord.Y = static_cast<SHORT>(r);
WriteConsoleOutputAttribute(outputHandle, &attributes[offset], 4, coord, &numberOfAttrsWritten);
offset = (offset + 4) & 7;
}
wait();
clear();

Просмотреть файл

@ -107,10 +107,9 @@
</Type>
<Type Name="til::generational&lt;*&gt;">
<DisplayString>{{ gen={_generation._value}, {_value} }}</DisplayString>
<Expand>
<ExpandedItem>_value</ExpandedItem>
</Expand>
<AlternativeType Name="$T1" />
<SmartPointer Usage="Minimal">&amp;_value</SmartPointer>
<DisplayString>{{ generation={_generation._value} }}</DisplayString>
</Type>
<Type Name="til::linear_flat_set&lt;*,*&gt;">
@ -122,4 +121,32 @@
</ArrayItems>
</Expand>
</Type>
<Type Name="Microsoft::Console::Render::Atlas::Buffer&lt;*&gt;">
<DisplayString>{{ size={_size} }}</DisplayString>
<Expand>
<Item Name="[size]" ExcludeView="simple">_size</Item>
<ArrayItems>
<Size>_size</Size>
<ValuePointer>_data</ValuePointer>
</ArrayItems>
</Expand>
</Type>
<Type Name="Microsoft::Console::Render::Atlas::ShapedRow">
<DisplayString>{{ dirtyTop={dirtyTop}, dirtyBottom={dirtyBottom} }}</DisplayString>
</Type>
<Type Name="Microsoft::Console::Render::Atlas::BackendD3D::AtlasGlyphEntry">
<DisplayString Condition="!_occupied">(empty)</DisplayString>
<DisplayString Condition="_occupied">{glyphIndex}</DisplayString>
</Type>
<Type Name="Microsoft::Console::Render::Atlas::BackendD3D::AtlasFontFaceEntry">
<DisplayString Condition="!_occupied">(empty)</DisplayString>
<DisplayString Condition="_occupied">{(void*)fontFace.m_ptr}, {lineRendition}</DisplayString>
<Expand>
<ExpandedItem>glyphs</ExpandedItem>
</Expand>
</Type>
</AutoVisualizer>