Repository: microsoft/terminal (language: cpp)

Issue:
Allow for the configuration of font fallback
I am a Chinese developer, sometimes I have to use Chinese, or my Chinese version system will display some prompts in Chinese.  

However, this terminal cannot specify multiple fonts in setting, for example: `"editor.fontFamily": "DejaVu Sans Mono, Noto Sans CJK SC"`(an example from vscode).

I hope this terminal can support multiple fonts setting in the future, thank you.


Discussion / hints:
I don't think you need to specify different fonts. Regardless of any locale you use, Chinese characters can always be displayed properly because there are font substitution, font linking and font fallback behind the scenes
Fonts in the JSON file could be an array, like with CSS, where if a font is not available, or maybe even per glyph - it goes to the next font listed, and falls back.
This will be part of #1790.
Adding the word fontFace in here to make it easier to find this issue.
It would be nice if VS Code and Terminal were more consistent on configuration parameters as well. This is a bigger problem, but this is a good example: vscode uses fontFamily, Terminal uses fontFace as the configuration parameter.
![671](https://user-images.githubusercontent.com/39271166/101343112-d600f880-38c6-11eb-8b60-71f23d42820b.png)

It's been over a year since this issue was opened. Is there any progress in implementing font fallback?
Even for Japanese users like me, this feature is absolutely necessary.

For example, suppose you want to use Cascadia Code in Windows Terminal. For VS Code, you can specify an alternative font to display characters that Cascadia Code doesn't have.
This is not possible with current Windows Terminal, and characters that are not in Cascadia Code are always displayed in the system font (Yu Gothic UI in Japanese). However, the design of the Yu Gothic UI is thin and condensed (see image), there is too much space between the letters, and the weights do not match. At least I find it hard to see on Windows Terminal.

There are not many fonts for coding that contain Japanese characters. If font fallback is possible, you can easily combine alphabet fonts and Japanese fonts, expanding your font choices.
We hope that this feature will be implemented soon. Thank you.
Nope. We'll make sure to update this thread when there is. In the meantime, might I recommend the Subscribe button?
![image](https://user-images.githubusercontent.com/18356694/91237459-5cbb0c80-e700-11ea-9347-b9b1ec2813b1.png)
That way you'll be notified of any updates to this thread, without needlessly pinging everyone on this thread ☺️
Is there any solution to this problem yet ?
My temporary solution is to merge 2 ttf files.
This is sort of a strange use-case for this, but I often access a remote machine that uses Material Design Icons in their zsh prompt. I'd like to be able to render those in WinTerm instead of getting boxes.

Also I work for microsoft and I think it'd be cool to have the windows logo on my prompt.
> This is sort of a strange use-case for this, but I often access a remote machine that uses Material Design Icons in their zsh prompt. I'd like to be able to render those in WinTerm instead of getting boxes.
> 
> Also I work for microsoft and I think it'd be cool to have the windows logo on my prompt.

Go ahead and grab [Nerd Fonts](https://nerdfonts.com/) for yourself.
@yzlnew, there are pretty big issues with using Nerd Fonts. Take a close look and compare the patched version of Cascadia with the original variable TTF one as an example. Letters are taller in the patched font.

This is another use case for allowing multiple fonts at the same time. This way I could use the original variable TTF Cascadia Code as the author indented it and at the same time have a random font patched with Nerd Font as a fallback for the fancy albeit nerdy and mostly useless glyphs from packs like Font Awesome.

@guodong2005, can you elaborate how you merge TTF fonts? Also are we talking about variable or static TTF?
https://github.com/aleigood/Hybrid-Font
> https://github.com/aleigood/Hybrid-Font

Please, stop recommending such "font mixing" approaches. It is a well-known ugly hack, with well-known drawbacks (most notably, glitched display possibly due to wrong kerning etc.; besides, what should have been a n+m problem now becomes n*m). A quick search on GitHub with "hybrid font" gives tens of results, half of which seems abandoned after the first several commits. I think we all agree this is a problem to be solved by either the operating system or the software, and definitely not the end user. And to go a bit off-topic, such "mixed fonts" usually (as is the case in the repository linked above) fail to meet the license requirements of the original fonts, e.g., by not providing a copyright notice (or a license file) in the repository.
> > https://github.com/aleigood/Hybrid-Font
> 
> Please, stop recommending such "font mixing" approaches. It is a well-known ugly hack, with well-known drawbacks (most notably, glitched display possibly due to wrong kerning etc.; besides, what should have been a n+m problem now becomes n*m). A quick search on GitHub with "hybrid font" gives tens of results, half of which seems abandoned after the first several commits. I think we all agree this is a problem to be solved by either the operating system or the software, and definitely not the end user. And to go a bit off-topic, such "mixed fonts" usually (as is the case in the repository linked above) fail to meet the license requirements of the original fonts, e.g., by not providing a copyright notice (or a license file) in the repository.

I agree with you very much. I'm not an expert in making fonts. After requesting Sublime Text support fallback font and waiting for years without results, I finally couldn't bear to spend time making this font and only give it to those who need it. At the same time, I hope terminal can support it as soon as possible.
FWIW, I think this is set up to be pretty easy to solve at this point.
* for the DX engine:
  https://github.com/microsoft/terminal/blob/9edf55de7564912cb8e19ef3a3f88b689be08af7/src/renderer/dx/DxFontInfo.cpp#L10
* For the Atlas engine:
  https://github.com/microsoft/terminal/blob/9edf55de7564912cb8e19ef3a3f88b689be08af7/src/renderer/atlas/AtlasEngine.api.cpp#L414
  
Both engines are using static lists of fonts for fallback. It shouldn't be _too_ hard to:

1. Make those lists a member variable of the engine itself
2. Add support to `Terminal.Settings.Model` for parsing a list of strings as fallback font names	
    - see https://github.com/microsoft/terminal/blob/9edf55de7564912cb8e19ef3a3f88b689be08af7/src/cascadia/TerminalSettingsModel/MTSMSettings.h#L92, which should take care of most of the work
3. Plumb those through to the `TermControl` in the `IControlSettings` interface
4. Have the Control tell the render engine about the fallback list on initialization and on hot-reload 




_**EDIT**_

It was brought to my attention that **I was totally off base here**. Turns out those hardcoded lists are whole-font based fallbacks, when what we really want is a **per-glyph fallback**. That we _don't_ have largely hooked up, so this is a lot harder to do than I originally thought. We'd need to construct a whole array of fonts internal to the render engines, and then when a glyph is missing in a font, check the next font too. Frankly, I'm not even sure that second bit is possible.

I'm gonna take off `Easy Starter`, but if someone wants to get ambitious, please go for it!
@zadjii-msft I am really curious about this issue. I would like to contribute if I could, but I could not even point out which is the involved code file becouse the whole project is too large to read. If you please, could you tell me the involved file name and give me a hint to solve this issue?
In all honesty, the renderer code is probably not my area of expertise. The best I could likely do is point you at `DxEngine` and `AtlasEngine`, which are the two different text renderers in the Terminal. 

@lhecker might be able to point you to a more specific method to look at. 

My uneducated guess is that there's some way to determine if a particular glyph isn't in a font, before we actually render that glyph. Probably some DX API, but that is not something I'm sufficiently experienced with. If we determine that there's nothing for that glyph in the current font, then we should use some list of fonts as "fallback" fonts to look for that glyph. 

I'd personally start working with a hardcoded list of fonts to fallback to. If that works, then we can pretty easily hook up some parsing logic to pass in a list of font names. But that's the easy part. Figuring out the per-glyph fallback is the hard part.
@zadjii-msft I really appreciate your fast reply. I will begin with reading the renderer code. 
> @zadjii-msft I really appreciate your fast reply. I will begin with reading the renderer code.

I really hope you to solve this problem!
This is the only issue that limits me from using Windows Terminal over other solutions now.

> **Note**
> ## Walkthrough

> To do per-glyph (codepoint?) fallback you'd use [`IDWriteFontFallbackBuilder::AddMapping`](https://learn.microsoft.com/en-us/windows/win32/api/dwrite_2/nf-dwrite_2-idwritefontfallbackbuilder-addmapping) and use the resulting custom `IDWriteFontFallback` instead of the current one we get through `IDWriteFactory2::GetSystemFontFallback`. In other words, we can easily add per-codepoint fallback in the future!

From discussion on #14959, specifically this thread: https://github.com/microsoft/terminal/pull/14959#discussion_r1169113851
Why is something as important as this not implemented since 4 yrs, please this is something very barebones on linux if you ask me. I am using a Mono font for my Terminal but i needed to add a secondary font for better glyph/icons support in my prompt which is not possible at the moment
@xd003 

Can you maybe make a gif of how this works in Linux the font fallback thing? 
https://www.screentogif.com/

Having things in Gifs describing the issue helps me.
Maybe it'll help get this solved faster:


@mdtauk @oncess @tsukumijima 

So let me get this straight the normal font is Japanese (tsu) / Chinese (once) and it should fall back to English font if not available?

@Jaswir No, it's preferable to initially set a beautiful monospace font for English console and programming tasks, and then render glyphs that are not implemented in that font, like Japanese hiragana and kanji, using a secondary monospace font that supports Japanese.
This is because, unlike fonts for English alphabets, Japanese fonts have a vast number of glyphs, limiting the options available, especially for free fonts.
Furthermore, the design of the English numerals and letters in Japanese fonts may not necessarily be suited for console environments. In other words, there's a need to "use a well-designed English font for English letters and numerals, and choose a Japanese font that stylistically matches that English font for hiragana and kanji".
While fonts that combine reputable open-source English and Japanese fonts like [HackGen](https://github.com/yuru7/HackGen) are available, creating such a composite font oneself is considerably laborious and generally not realistic.
@tsukumijima 

Okay, so you have two fonts. One for the English alphabet and numbers and one for the Japanese characters? With current terminal you cannot have two fonts? 

PS: I can play around a bit with vs code and the fonts and check the differences


@Jaswir  What does it mean?
English fonts are fonts such as [Source Code Pro](https://fonts.google.com/specimen/Source+Code+Pro), while Japanese fonts (including half-width alphanumeric characters) are [Kosugi](https://fonts.google.com/specimen/Kosugi?preview.text=Hello!%20%E3%81%82%E3%82%89%E3%82%86%E3%82%8B%E7%8F%BE%E5%AE%9F%E3%82%92%E3%80%81%E3%81%99%E3%81%B9%E3%81%A6%E8%87%AA%E5%88%86%E3%81%AE%E6%96%B9%E3%81%B8%E3%81%AD%E3%81%98%E6%9B%B2%E3%81%92%E3%81%9F%E3%81%AE%E3%81%A0%E3%80%82&query=Kosugi), etc.
"Kosugi" is a monospaced Japanese font that also includes alphanumeric characters. However, the alphanumeric characters in "Source Code Pro" look better on the terminal.
Therefore, in this example, I think "Source Code Pro" should be used preferentially, and "Kosugi" should be used for the characters not included in "Source Code Pro".

> @xd003
> 
> Can you maybe make a gif of how this works in Linux the font fallback thing? https://www.screentogif.com/
> 
> Having things in Gifs describing the issue helps me. Maybe it'll help get this solved faster:

Sure i can do that, but i am didn't quite understood if you need the part where this feature is coded in all the terminal emulators on linux or where exactly is the font fallback defined ? if you need the code part, sorry you will have to find it yourself on their respective github repos as i am not familiar with their codebase. i am linking github for the most preferred/famous linux terminals here - [Konsole](https://github.com/KDE/konsole), [Alacritty](https://github.com/alacritty/alacritty), [Kitty](https://github.com/kovidgoyal/kitty), [wezterm](https://github.com/wez/wezterm), [foot](https://codeberg.org/dnkl/foot), [st](https://git.suckless.org/st/). Do note that some of these terminals have windows OS support but their development is majorly focused on catering linux users except maybe just wezterm, it works pretty fine on windows i have heard. 
If you were asking where is the font fallback defined by the user, each of the github repo i have linked has their own wiki section, it is defined in the respective config files used by the terminals. For example, kitty's font section [here](https://sw.kovidgoyal.net/kitty/conf/#fonts) clearly shows how to define font fallback.
Good Luck

You can try using a nerd font, it contains alot of unicode including chinese character!
> You can try using a nerd font, it contains alot of unicode including chinese character!

I guess that is precisley one of the many reasons that this issue exists. We would like to be able to have fallback font options, as to avoid using hacks like nerd fonts that are patch on fonts, sometimes with great results. sometimes with alignment issues.
> > You can try using a nerd font, it contains alot of unicode including chinese character!
> 
> I guess that is precisley one of the many reasons this issue exists. We wan't to be able to have fallback font as to avoid using hacks like nerd fonts that are patching a fonts, sometimes with great results. sometimes with alignment issues.

Moreover there are very limited number of fonts available within the Nerd Fonts apart from the occasional alignment issues, its not really a viable option as people think it to be
+1 to this issue.

Since my choice of font ([Sarasa Gothic](https://github.com/be5invis/Sarasa-Gothic)) does not have a prepatched version with nerd font, I used [font-patcher](https://github.com/ryanoasis/nerd-fonts?tab=readme-ov-file#font-patcher) in the official nerd font repo to patch nerd font in manually.

Docker example:
`docker run --rm -v ./SarasaFixedSlabHC-TTF-1.0.5/:/in:Z -v ./out:/out:Z -e "PN=10" nerdfonts/patcher --adjust-line-height --careful --complete`
(Lower `PN` if you have a weak PC, otherwise it might use up all your RAM and freeze your computer)

Another option I have considered is to use [Wezterm](https://github.com/wez/wezterm), which is cross platform, has tab support and has nerd font as fallback font by default (so you don't have to patch nerd font by yourself).
For the subset of this thread that has been following along because they want the Powerline glyphs in their prompt but don't have a font with those glyphs: #16729 added some support for the Terminal drawing some of those glyphs itself, rather than relying on the font. That's available in [Canary builds](https://github.com/microsoft/terminal/discussions/16121), if folks want to give that a try 😄 

_(understanding though, that this thread is mostly_ not _about powerline glyphs, and there's still a lot of work to do here)_

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 32dd37878d7..28b9b790d7b 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -1155,6 +1155,7 @@ NOCONTEXTHELP
 NOCOPYBITS
 NODUP
 noexcepts
+NOFONT
 NOINTEGRALHEIGHT
 NOINTERFACE
 NOLINKINFO
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index a1070d9b811..98c70398559 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -336,6 +336,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             _renderEngine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
             _renderer->AddRenderEngine(_renderEngine.get());
 
+            // Hook up the warnings callback as early as possible so that we catch everything.
+            _renderEngine->SetWarningCallback([this](HRESULT hr, wil::zwstring_view parameter) {
+                _rendererWarning(hr, parameter);
+            });
+
             // Initialize our font with the renderer
             // We don't have to care about DPI. We'll get a change message immediately if it's not 96
             // and react accordingly.
@@ -371,12 +376,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
             _terminal->CreateFromSettings(*_settings, *_renderer);
 
-            // IMPORTANT! Set this callback up sooner than later. If we do it
-            // after Enable, then it'll be possible to paint the frame once
-            // _before_ the warning handler is set up, and then warnings from
-            // the first paint will be ignored!
-            _renderEngine->SetWarningCallback(std::bind(&ControlCore::_rendererWarning, this, std::placeholders::_1));
-
             // Tell the render engine to notify us when the swap chain changes.
             // We do this after we initially set the swapchain so as to avoid
             // unnecessary callbacks (and locking problems)
@@ -1024,18 +1023,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             LOG_IF_FAILED(_renderEngine->UpdateFont(_desiredFont, _actualFont, featureMap, axesMap));
         }
 
-        // If the actual font isn't what was requested...
-        if (_actualFont.GetFaceName() != _desiredFont.GetFaceName())
-        {
-            // Then warn the user that we picked something because we couldn't find their font.
-            // Format message with user's choice of font and the font that was chosen instead.
-            const winrt::hstring message{ fmt::format(std::wstring_view{ RS_(L"NoticeFontNotFound") },
-                                                      _desiredFont.GetFaceName(),
-                                                      _actualFont.GetFaceName()) };
-            auto noticeArgs = winrt::make<NoticeEventArgs>(NoticeLevel::Warning, message);
-            RaiseNotice.raise(*this, std::move(noticeArgs));
-        }
-
         const auto actualNewSize = _actualFont.GetSize();
         FontSizeChanged.raise(*this, winrt::make<FontSizeChangedArgs>(actualNewSize.width, actualNewSize.height));
     }
@@ -1724,9 +1711,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         }
     }
 
-    void ControlCore::_rendererWarning(const HRESULT hr)
+    void ControlCore::_rendererWarning(const HRESULT hr, wil::zwstring_view parameter)
     {
-        RendererWarning.raise(*this, winrt::make<RendererWarningArgs>(hr));
+        RendererWarning.raise(*this, winrt::make<RendererWarningArgs>(hr, winrt::hstring{ parameter }));
     }
 
     winrt::fire_and_forget ControlCore::_renderEngineSwapChainChanged(const HANDLE sourceHandle)
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 218d1b842eb..7dbf228cd26 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -390,7 +390,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         winrt::Windows::System::DispatcherQueueTimer _midiAudioSkipTimer{ nullptr };
 
 #pragma region RendererCallbacks
-        void _rendererWarning(const HRESULT hr);
+        void _rendererWarning(const HRESULT hr, wil::zwstring_view parameter);
         winrt::fire_and_forget _renderEngineSwapChainChanged(const HANDLE handle);
         void _rendererBackgroundColorChanged();
         void _rendererTabColorChanged();
diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h
index 1c135cdf4f6..2ed2c2b828b 100644
--- a/src/cascadia/TerminalControl/EventArgs.h
+++ b/src/cascadia/TerminalControl/EventArgs.h
@@ -148,12 +148,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     struct RendererWarningArgs : public RendererWarningArgsT<RendererWarningArgs>
     {
     public:
-        RendererWarningArgs(const uint64_t hr) :
-            _Result(hr)
+        RendererWarningArgs(const HRESULT hr, winrt::hstring parameter) :
+            _Result{ hr },
+            _Parameter{ std::move(parameter) }
         {
         }
 
-        WINRT_PROPERTY(uint64_t, Result);
+        WINRT_PROPERTY(HRESULT, Result);
+        WINRT_PROPERTY(winrt::hstring, Parameter);
     };
 
     struct TransparencyChangedEventArgs : public TransparencyChangedEventArgsT<TransparencyChangedEventArgs>
diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl
index 978d69d8e36..92784fc6414 100644
--- a/src/cascadia/TerminalControl/EventArgs.idl
+++ b/src/cascadia/TerminalControl/EventArgs.idl
@@ -77,7 +77,8 @@ namespace Microsoft.Terminal.Control
 
     runtimeclass RendererWarningArgs
     {
-        UInt64 Result { get; };
+        HRESULT Result { get; };
+        String Parameter { get; };
     }
 
     runtimeclass TransparencyChangedEventArgs
diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw
index a604fc912e4..02245a01577 100644
--- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw
@@ -213,6 +213,14 @@ Please either install the missing font or choose another one.</value>
     <value>Renderer encountered an unexpected error: {0}</value>
     <comment>{0} is an error code.</comment>
   </data>
+  <data name="RendererErrorFontNotFound" xml:space="preserve">
+    <value>Unable to find the following fonts: {0}. Please either install them or choose different fonts.</value>
+    <comment>{Locked="{0}"} This is a warning dialog shown when the user selects a font that isn't installed.</comment>
+  </data>
+  <data name="RendererErrorOther" xml:space="preserve">
+    <value>Renderer encountered an unexpected error: {0:#010x} {1}</value>
+    <comment>{Locked="{0:#010x}","{1}"} {0:#010x} is a placeholder for a Windows error code (e.g. 0x88985002). {1} is the corresponding message.</comment>
+  </data>
   <data name="TermControlReadOnly" xml:space="preserve">
     <value>Read-only mode is enabled.</value>
   </data>
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index c8b378672c5..303d6d61482 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -991,36 +991,45 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     // - hr: an  HRESULT describing the warning
     // Return Value:
     // - <none>
-    winrt::fire_and_forget TermControl::_RendererWarning(IInspectable /*sender*/,
-                                                         Control::RendererWarningArgs args)
+    winrt::fire_and_forget TermControl::_RendererWarning(IInspectable /*sender*/, Control::RendererWarningArgs args)
     {
-        const auto hr = static_cast<HRESULT>(args.Result());
-
         auto weakThis{ get_weak() };
         co_await wil::resume_foreground(Dispatcher());
 
-        if (auto control{ weakThis.get() })
+        const auto control = weakThis.get();
+        if (!control)
         {
-            winrt::hstring message;
-            if (HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == hr ||
-                HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND) == hr)
-            {
-                message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderNotFound") },
-                                                      (_focused ? _core.FocusedAppearance() : _core.UnfocusedAppearance()).PixelShaderPath()) };
-            }
-            else if (D2DERR_SHADER_COMPILE_FAILED == hr)
-            {
-                message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderCompileFailed") }) };
-            }
-            else
-            {
-                message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"UnexpectedRendererError") },
-                                                      hr) };
-            }
+            co_return;
+        }
 
-            auto noticeArgs = winrt::make<NoticeEventArgs>(NoticeLevel::Warning, std::move(message));
-            control->RaiseNotice.raise(*control, std::move(noticeArgs));
+        const auto hr = args.Result();
+        const auto parameter = args.Parameter();
+        winrt::hstring message;
+
+        switch (hr)
+        {
+        case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):
+        case HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND):
+            message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderNotFound") }, parameter) };
+            break;
+        case D2DERR_SHADER_COMPILE_FAILED:
+            message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderCompileFailed") }) };
+            break;
+        case DWRITE_E_NOFONT:
+            message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"RendererErrorFontNotFound") }, parameter) };
+            break;
+        default:
+        {
+            wchar_t buf[512];
+            const auto len = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), &buf[0], ARRAYSIZE(buf), nullptr);
+            const std::wstring_view msg{ &buf[0], len };
+            message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"RendererErrorOther") }, hr, msg) };
+            break;
         }
+        }
+
+        auto noticeArgs = winrt::make<NoticeEventArgs>(NoticeLevel::Warning, std::move(message));
+        control->RaiseNotice.raise(*control, std::move(noticeArgs));
     }
 
     void TermControl::_AttachDxgiSwapChainToXaml(HANDLE swapChainHandle)
diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.cpp b/src/cascadia/TerminalSettingsEditor/Appearances.cpp
index 85099de045d..fe9cabc824f 100644
--- a/src/cascadia/TerminalSettingsEditor/Appearances.cpp
+++ b/src/cascadia/TerminalSettingsEditor/Appearances.cpp
@@ -6,8 +6,8 @@
 #include "Appearances.g.cpp"
 #include "AxisKeyValuePair.g.cpp"
 #include "FeatureKeyValuePair.g.cpp"
-#include "EnumEntry.h"
 
+#include "EnumEntry.h"
 #include <LibraryResources.h>
 #include "..\WinRTUtils\inc\Utils.h"
 
@@ -36,28 +36,6 @@ static constexpr std::array<std::wstring_view, 11> DefaultFeatures{
 
 namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
 {
-    bool Font::HasPowerlineCharacters()
-    {
-        if (!_hasPowerlineCharacters.has_value())
-        {
-            try
-            {
-                winrt::com_ptr<IDWriteFont> font;
-                THROW_IF_FAILED(_family->GetFont(0, font.put()));
-                BOOL exists{};
-                // We're actually checking for the "Extended" PowerLine glyph set.
-                // They're more fun.
-                THROW_IF_FAILED(font->HasCharacter(0xE0B6, &exists));
-                _hasPowerlineCharacters = (exists == TRUE);
-            }
-            catch (...)
-            {
-                _hasPowerlineCharacters = false;
-            }
-        }
-        return _hasPowerlineCharacters.value_or(false);
-    }
-
     Windows::Foundation::Collections::IMap<winrt::hstring, winrt::hstring> Font::FontAxesTagsAndNames()
     {
         if (!_fontAxesTagsAndNames)
@@ -370,6 +348,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
             }
         });
 
+        _refreshFontFaceDependents();
         InitializeFontAxesVector();
         InitializeFontFeaturesVector();
 
@@ -382,7 +361,119 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         }
     }
 
-    double AppearanceViewModel::LineHeight() const noexcept
+    winrt::hstring AppearanceViewModel::FontFace() const
+    {
+        return _appearance.SourceProfile().FontInfo().FontFace();
+    }
+
+    void AppearanceViewModel::FontFace(const winrt::hstring& value)
+    {
+        const auto fontInfo = _appearance.SourceProfile().FontInfo();
+        if (fontInfo.FontFace() == value)
+        {
+            return;
+        }
+
+        fontInfo.FontFace(value);
+        _refreshFontFaceDependents();
+
+        _NotifyChanges(L"HasFontFace", L"FontFace");
+    }
+
+    bool AppearanceViewModel::HasFontFace() const
+    {
+        return _appearance.SourceProfile().FontInfo().HasFontFace();
+    }
+
+    void AppearanceViewModel::ClearFontFace()
+    {
+        const auto fontInfo = _appearance.SourceProfile().FontInfo();
+        const auto hadValue = fontInfo.HasFontFace();
+
+        fontInfo.ClearFontFace();
+        _refreshFontFaceDependents();
+
+        if (hadValue)
+        {
+            _NotifyChanges(L"HasFontFace", L"FontFace");
+        }
+    }
+
+    Model::FontConfig AppearanceViewModel::FontFaceOverrideSource() const
+    {
+        return _appearance.SourceProfile().FontInfo().FontFaceOverrideSource();
+    }
+
+    void AppearanceViewModel::_refreshFontFaceDependents()
+    {
+        wil::com_ptr<IDWriteFactory> factory;
+        THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(factory), reinterpret_cast<::IUnknown**>(factory.addressof())));
+
+        wil::com_ptr<IDWriteFontCollection> fontCollection;
+        THROW_IF_FAILED(factory->GetSystemFontCollection(fontCollection.addressof(), FALSE));
+
+        const auto fontFace = FontFace();
+        std::wstring primaryFontName;
+        std::wstring missingFonts;
+        std::wstring proportionalFonts;
+        BOOL hasPowerlineCharacters = FALSE;
+
+        til::iterate_font_families(fontFace, [&](wil::zwstring_view name) {
+            std::wstring* accumulator = nullptr;
+
+            UINT32 index = 0;
+            BOOL exists = FALSE;
+            THROW_IF_FAILED(fontCollection->FindFamilyName(name.c_str(), &index, &exists));
+
+            // Look ma, no goto!
+            do
+            {
+                if (!exists)
+                {
+                    accumulator = &missingFonts;
+                    break;
+                }
+
+                if (primaryFontName.empty())
+                {
+                    primaryFontName = name;
+                }
+
+                wil::com_ptr<IDWriteFontFamily> fontFamily;
+                THROW_IF_FAILED(fontCollection->GetFontFamily(index, fontFamily.addressof()));
+
+                wil::com_ptr<IDWriteFont> font;
+                THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, font.addressof()));
+
+                if (!font.query<IDWriteFont1>()->IsMonospacedFont())
+                {
+                    accumulator = &proportionalFonts;
+                }
+
+                // We're actually checking for the "Extended" PowerLine glyph set.
+                // They're more fun.
+                BOOL hasE0B6 = FALSE;
+                std::ignore = font->HasCharacter(0xE0B6, &hasE0B6);
+                hasPowerlineCharacters |= hasE0B6;
+            } while (false);
+
+            if (accumulator)
+            {
+                if (!accumulator->empty())
+                {
+                    accumulator->append(L", ");
+                }
+                accumulator->append(name);
+            }
+        });
+
+        _primaryFontName = std::move(primaryFontName);
+        MissingFontFaces(winrt::hstring{ missingFonts });
+        ProportionalFontFaces(winrt::hstring{ proportionalFonts });
+        HasPowerlineCharacters(hasPowerlineCharacters);
+    }
+
+    double AppearanceViewModel::LineHeight() const
     {
         const auto fontInfo = _appearance.SourceProfile().FontInfo();
         const auto cellHeight = fontInfo.CellHeight();
@@ -526,7 +617,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
 
         // find one axis that does not already exist, and add that
         // if there are no more possible axes to add, the button is disabled so there shouldn't be a way to get here
-        const auto possibleAxesTagsAndNames = ProfileViewModel::FindFontWithLocalizedName(FontFace()).FontAxesTagsAndNames();
+        const auto possibleAxesTagsAndNames = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontAxesTagsAndNames();
         for (const auto tagAndName : possibleAxesTagsAndNames)
         {
             if (!fontAxesMap.HasKey(tagAndName.Key()))
@@ -567,7 +658,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         _FontAxesVector.Clear();
         if (const auto fontAxesMap = _appearance.SourceProfile().FontInfo().FontAxes())
         {
-            const auto fontAxesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(FontFace()).FontAxesTagsAndNames();
+            const auto fontAxesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontAxesTagsAndNames();
             for (const auto axis : fontAxesMap)
             {
                 // only show the axes that the font supports
@@ -585,14 +676,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     // - Determines whether the currently selected font has any variable font axes
     bool AppearanceViewModel::AreFontAxesAvailable()
     {
-        return ProfileViewModel::FindFontWithLocalizedName(FontFace()).FontAxesTagsAndNames().Size() > 0;
+        return ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontAxesTagsAndNames().Size() > 0;
     }
 
     // Method Description:
     // - Determines whether the currently selected font has any variable font axes that have not already been set
     bool AppearanceViewModel::CanFontAxesBeAdded()
     {
-        if (const auto fontAxesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(FontFace()).FontAxesTagsAndNames(); fontAxesTagToNameMap.Size() > 0)
+        if (const auto fontAxesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontAxesTagsAndNames(); fontAxesTagToNameMap.Size() > 0)
         {
             if (const auto fontAxesMap = _appearance.SourceProfile().FontInfo().FontAxes())
             {
@@ -641,7 +732,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
 
         // find one feature that does not already exist, and add that
         // if there are no more possible features to add, the button is disabled so there shouldn't be a way to get here
-        const auto possibleFeaturesTagsAndNames = ProfileViewModel::FindFontWithLocalizedName(FontFace()).FontFeaturesTagsAndNames();
+        const auto possibleFeaturesTagsAndNames = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontFeaturesTagsAndNames();
         for (const auto tagAndName : possibleFeaturesTagsAndNames)
         {
             const auto featureKey = tagAndName.Key();
@@ -684,7 +775,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         _FontFeaturesVector.Clear();
         if (const auto fontFeaturesMap = _appearance.SourceProfile().FontInfo().FontFeatures())
         {
-            const auto fontFeaturesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(FontFace()).FontFeaturesTagsAndNames();
+            const auto fontFeaturesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontFeaturesTagsAndNames();
             for (const auto feature : fontFeaturesMap)
             {
                 const auto featureKey = feature.Key();
@@ -703,14 +794,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     // - Determines whether the currently selected font has any font features
     bool AppearanceViewModel::AreFontFeaturesAvailable()
     {
-        return ProfileViewModel::FindFontWithLocalizedName(FontFace()).FontFeaturesTagsAndNames().Size() > 0;
+        return ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontFeaturesTagsAndNames().Size() > 0;
     }
 
     // Method Description:
     // - Determines whether the currently selected font has any font features that have not already been set
     bool AppearanceViewModel::CanFontFeaturesBeAdded()
     {
-        if (const auto fontFeaturesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(FontFace()).FontFeaturesTagsAndNames(); fontFeaturesTagToNameMap.Size() > 0)
+        if (const auto fontFeaturesTagToNameMap = ProfileViewModel::FindFontWithLocalizedName(_primaryFontName).FontFeaturesTagsAndNames(); fontFeaturesTagToNameMap.Size() > 0)
         {
             if (const auto fontFeaturesMap = _appearance.SourceProfile().FontInfo().FontFeatures())
             {
@@ -775,9 +866,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
 
     DependencyProperty Appearances::_AppearanceProperty{ nullptr };
 
-    Appearances::Appearances() :
-        _ShowAllFonts{ false },
-        _ShowProportionalFontWarning{ false }
+    Appearances::Appearances()
     {
         InitializeComponent();
 
@@ -848,61 +937,58 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         INITIALIZE_BINDABLE_ENUM_SETTING(IntenseTextStyle, IntenseTextStyle, winrt::Microsoft::Terminal::Settings::Model::IntenseStyle, L"Appearance_IntenseTextStyle", L"Content");
     }
 
-    // Method Description:
-    // - Searches through our list of monospace fonts to determine if the settings model's current font face is a monospace font
-    bool Appearances::UsingMonospaceFont() const noexcept
+    IObservableVector<Editor::Font> Appearances::FilteredFontList()
     {
-        auto result{ false };
-        const auto currentFont{ Appearance().FontFace() };
-        for (const auto& font : ProfileViewModel::MonospaceFontList())
+        if (!_filteredFonts)
         {
-            if (font.LocalizedName() == currentFont)
-            {
-                result = true;
-            }
+            _updateFilteredFontList();
         }
-        return result;
+        return _filteredFonts;
     }
 
     // Method Description:
     // - Determines whether we should show the list of all the fonts, or we should just show monospace fonts
     bool Appearances::ShowAllFonts() const noexcept
     {
-        // - _ShowAllFonts is directly bound to the checkbox. So this is the user set value.
-        // - If we are not using a monospace font, show all of the fonts so that the ComboBox is still properly bound
-        return _ShowAllFonts || !UsingMonospaceFont();
+        return _ShowAllFonts;
     }
 
-    void Appearances::ShowAllFonts(const bool& value)
+    void Appearances::ShowAllFonts(const bool value)
     {
         if (_ShowAllFonts != value)
         {
             _ShowAllFonts = value;
+            _filteredFonts = nullptr;
             PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"ShowAllFonts" });
+            PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"FilteredFontList" });
         }
     }
 
-    IInspectable Appearances::CurrentFontFace() const
+    void Appearances::FontFaceBox_GotFocus(const Windows::Foundation::IInspectable& sender, const RoutedEventArgs&)
     {
-        const auto& appearanceVM{ Appearance() };
-        const auto appearanceFontFace{ appearanceVM.FontFace() };
-        return box_value(ProfileViewModel::FindFontWithLocalizedName(appearanceFontFace));
+        _updateFontNameFilter({});
+        sender.as<AutoSuggestBox>().IsSuggestionListOpen(true);
     }
 
-    void Appearances::FontFace_SelectionChanged(const IInspectable& /*sender*/, const SelectionChangedEventArgs& e)
+    void Appearances::FontFaceBox_LostFocus(const IInspectable& sender, const RoutedEventArgs&)
     {
-        // NOTE: We need to hook up a selection changed event handler here instead of directly binding to the appearance view model.
-        //       A two way binding to the view model causes an infinite loop because both combo boxes keep fighting over which one's right.
-        const auto selectedItem{ e.AddedItems().GetAt(0) };
-        const auto newFontFace{ unbox_value<Editor::Font>(selectedItem) };
-        Appearance().FontFace(newFontFace.LocalizedName());
-        if (!UsingMonospaceFont())
+        const auto appearance = Appearance();
+        const auto fontSpec = sender.as<AutoSuggestBox>().Text();
+
+        if (fontSpec.empty())
         {
-            ShowProportionalFontWarning(true);
+            appearance.ClearFontFace();
         }
         else
         {
-            ShowProportionalFontWarning(false);
+            appearance.FontFace(fontSpec);
+        }
+
+        // TODO: Any use of FindFontWithLocalizedName is broken and requires refactoring in time for version 1.21.
+        const auto newFontFace = ProfileViewModel::FindFontWithLocalizedName(fontSpec);
+        if (!newFontFace)
+        {
+            return;
         }
 
         _FontAxesNames.Clear();
@@ -948,6 +1034,89 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         }
     }
 
+    void Appearances::FontFaceBox_SuggestionChosen(const AutoSuggestBox& sender, const AutoSuggestBoxSuggestionChosenEventArgs& args)
+    {
+        const auto font = unbox_value<Editor::Font>(args.SelectedItem());
+        const auto fontName = font.Name();
+        auto fontSpec = sender.Text();
+
+        const std::wstring_view fontSpecView{ fontSpec };
+        if (const auto idx = fontSpecView.rfind(L','); idx != std::wstring_view::npos)
+        {
+            const auto prefix = fontSpecView.substr(0, idx);
+            const auto suffix = std::wstring_view{ fontName };
+            fontSpec = winrt::hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), prefix, suffix) };
+        }
+        else
+        {
+            fontSpec = fontName;
+        }
+
+        sender.Text(fontSpec);
+    }
+
+    void Appearances::FontFaceBox_TextChanged(const AutoSuggestBox& sender, const AutoSuggestBoxTextChangedEventArgs& args)
+    {
+        if (args.Reason() != AutoSuggestionBoxTextChangeReason::UserInput)
+        {
+            return;
+        }
+
+        const auto fontSpec = sender.Text();
+        std::wstring_view filter{ fontSpec };
+
+        // Find the last font name in the font, spec, list.
+        if (const auto idx = filter.rfind(L','); idx != std::wstring_view::npos)
+        {
+            filter = filter.substr(idx + 1);
+        }
+
+        filter = til::trim(filter, L' ');
+        _updateFontNameFilter(filter);
+    }
+
+    void Appearances::_updateFontNameFilter(std::wstring_view filter)
+    {
+        if (_fontNameFilter != filter)
+        {
+            _filteredFonts = nullptr;
+            _fontNameFilter = filter;
+            PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"FilteredFontList" });
+        }
+    }
+
+    void Appearances::_updateFilteredFontList()
+    {
+        _filteredFonts = _ShowAllFonts ? ProfileViewModel::CompleteFontList() : ProfileViewModel::MonospaceFontList();
+
+        if (_fontNameFilter.empty())
+        {
+            return;
+        }
+
+        std::vector<Editor::Font> filtered;
+        filtered.reserve(_filteredFonts.Size());
+
+        for (const auto& font : _filteredFonts)
+        {
+            const auto name = font.Name();
+            bool match = til::contains_linguistic_insensitive(name, _fontNameFilter);
+
+            if (!match)
+            {
+                const auto localizedName = font.LocalizedName();
+                match = localizedName != name && til::contains_linguistic_insensitive(localizedName, _fontNameFilter);
+            }
+
+            if (match)
+            {
+                filtered.emplace_back(font);
+            }
+        }
+
+        _filteredFonts = winrt::single_threaded_observable_vector(std::move(filtered));
+    }
+
     void Appearances::_ViewModelChanged(const DependencyObject& d, const DependencyPropertyChangedEventArgs& /*args*/)
     {
         const auto& obj{ d.as<Editor::Appearances>() };
@@ -956,21 +1125,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
 
     void Appearances::_UpdateWithNewViewModel()
     {
-        if (Appearance())
+        if (const auto appearance = Appearance())
         {
-            const auto& biAlignmentVal{ static_cast<int32_t>(Appearance().BackgroundImageAlignment()) };
+            const auto& biAlignmentVal{ static_cast<int32_t>(appearance.BackgroundImageAlignment()) };
             for (const auto& biButton : _BIAlignmentButtons)
             {
                 biButton.IsChecked(biButton.Tag().as<int32_t>() == biAlignmentVal);
             }
 
             FontAxesCVS().Source(Appearance().FontAxesVector());
-            Appearance().AreFontAxesAvailable() ? FontAxesContainer().HelpText(RS_(L"Profile_FontAxesAvailable/Text")) : FontAxesContainer().HelpText(RS_(L"Profile_FontAxesUnavailable/Text"));
+            FontAxesContainer().HelpText(appearance.AreFontAxesAvailable() ? RS_(L"Profile_FontAxesAvailable/Text") : RS_(L"Profile_FontAxesUnavailable/Text"));
 
             FontFeaturesCVS().Source(Appearance().FontFeaturesVector());
-            Appearance().AreFontFeaturesAvailable() ? FontFeaturesContainer().HelpText(RS_(L"Profile_FontFeaturesAvailable/Text")) : FontFeaturesContainer().HelpText(RS_(L"Profile_FontFeaturesUnavailable/Text"));
+            FontFeaturesContainer().HelpText(appearance.AreFontFeaturesAvailable() ? RS_(L"Profile_FontFeaturesAvailable/Text") : RS_(L"Profile_FontFeaturesUnavailable/Text"));
 
-            _ViewModelChangedRevoker = Appearance().PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) {
+            _ViewModelChangedRevoker = appearance.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) {
                 const auto settingName{ args.PropertyName() };
                 if (settingName == L"CursorShape")
                 {
@@ -987,24 +1156,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
                 }
                 else if (settingName == L"BackgroundImageAlignment")
                 {
-                    _UpdateBIAlignmentControl(static_cast<int32_t>(Appearance().BackgroundImageAlignment()));
+                    _UpdateBIAlignmentControl(static_cast<int32_t>(appearance.BackgroundImageAlignment()));
                 }
                 else if (settingName == L"FontWeight")
                 {
                     PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentFontWeight" });
                     PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"IsCustomFontWeight" });
                 }
-                else if (settingName == L"FontFace" || settingName == L"CurrentFontList")
-                {
-                    // notify listener that all font face related values might have changed
-                    if (!UsingMonospaceFont())
-                    {
-                        _ShowAllFonts = true;
-                    }
-                    PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentFontFace" });
-                    PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"ShowAllFonts" });
-                    PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"UsingMonospaceFont" });
-                }
                 else if (settingName == L"IntenseTextStyle")
                 {
                     PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentIntenseTextStyle" });
@@ -1040,12 +1198,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
             PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"IsVintageCursor" });
             PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentColorScheme" });
             PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentBackgroundImageStretchMode" });
-            _UpdateBIAlignmentControl(static_cast<int32_t>(Appearance().BackgroundImageAlignment()));
+            _UpdateBIAlignmentControl(static_cast<int32_t>(appearance.BackgroundImageAlignment()));
             PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentFontWeight" });
             PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"IsCustomFontWeight" });
-            PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentFontFace" });
             PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"ShowAllFonts" });
-            PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"UsingMonospaceFont" });
             PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentIntenseTextStyle" });
             PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"CurrentAdjustIndistinguishableColors" });
             PropertyChanged.raise(*this, PropertyChangedEventArgs{ L"ShowProportionalFontWarning" });
diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.h b/src/cascadia/TerminalSettingsEditor/Appearances.h
index 72270cd78d7..d01819b24b5 100644
--- a/src/cascadia/TerminalSettingsEditor/Appearances.h
+++ b/src/cascadia/TerminalSettingsEditor/Appearances.h
@@ -31,15 +31,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     struct Font : FontT<Font>
     {
     public:
-        Font(winrt::hstring name, winrt::hstring localizedName, IDWriteFontFamily* family) :
+        Font(winrt::hstring name, winrt::hstring localizedName, wil::com_ptr<IDWriteFontFamily> family) :
             _Name{ std::move(name) },
-            _LocalizedName{ std::move(localizedName) }
+            _LocalizedName{ std::move(localizedName) },
+            _family{ std::move(family) }
         {
-            _family.copy_from(family);
         }
 
         hstring ToString() { return _LocalizedName; }
-        bool HasPowerlineCharacters();
         Windows::Foundation::Collections::IMap<winrt::hstring, winrt::hstring> FontAxesTagsAndNames();
         Windows::Foundation::Collections::IMap<winrt::hstring, winrt::hstring> FontFeaturesTagsAndNames();
 
@@ -47,14 +46,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         WINRT_PROPERTY(hstring, LocalizedName);
 
     private:
-        winrt::com_ptr<IDWriteFontFamily> _family;
-        std::optional<bool> _hasPowerlineCharacters;
-
         winrt::hstring _tagToString(DWRITE_FONT_AXIS_TAG tag);
         winrt::hstring _tagToString(DWRITE_FONT_FEATURE_TAG tag);
 
         Windows::Foundation::Collections::IMap<winrt::hstring, winrt::hstring> _fontAxesTagsAndNames;
         Windows::Foundation::Collections::IMap<winrt::hstring, winrt::hstring> _fontFeaturesTagsAndNames;
+        wil::com_ptr<IDWriteFontFamily> _family;
     };
 
     struct AxisKeyValuePair : AxisKeyValuePairT<AxisKeyValuePair>, ViewModelHelper<AxisKeyValuePair>
@@ -108,11 +105,18 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     public:
         AppearanceViewModel(const Model::AppearanceConfig& appearance);
 
-        double LineHeight() const noexcept;
+        winrt::hstring FontFace() const;
+        void FontFace(const winrt::hstring& value);
+        bool HasFontFace() const;
+        void ClearFontFace();
+        Model::FontConfig FontFaceOverrideSource() const;
+
+        double LineHeight() const;
         void LineHeight(const double value);
         bool HasLineHeight() const;
         void ClearLineHeight();
         Model::FontConfig LineHeightOverrideSource() const;
+
         void SetFontWeightFromDouble(double fontWeight);
         void SetBackgroundImageOpacityFromPercentageValue(double percentageValue);
         void SetBackgroundImagePath(winrt::hstring path);
@@ -139,13 +143,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         bool CanFontFeaturesBeAdded();
 
         WINRT_PROPERTY(bool, IsDefault, false);
+        WINRT_PROPERTY(bool, HasPowerlineCharacters, false);
 
         // These settings are not defined in AppearanceConfig, so we grab them
         // from the source profile itself. The reason we still want them in the
         // AppearanceViewModel is so we can continue to have the 'Text' grouping
         // we currently have in xaml, since that grouping has some settings that
         // are defined in AppearanceConfig and some that are not.
-        OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), FontFace);
         OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), FontSize);
         OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), FontWeight);
         OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), FontAxes);
@@ -165,37 +169,43 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         OBSERVABLE_PROJECTED_SETTING(_appearance, IntenseTextStyle);
         OBSERVABLE_PROJECTED_SETTING(_appearance, AdjustIndistinguishableColors);
         WINRT_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::ColorSchemeViewModel>, SchemesList, _propertyChangedHandlers, nullptr);
+        WINRT_OBSERVABLE_PROPERTY(winrt::hstring, MissingFontFaces, _propertyChangedHandlers);
+        WINRT_OBSERVABLE_PROPERTY(winrt::hstring, ProportionalFontFaces, _propertyChangedHandlers);
         WINRT_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::AxisKeyValuePair>, FontAxesVector, _propertyChangedHandlers, nullptr);
         WINRT_OBSERVABLE_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::FeatureKeyValuePair>, FontFeaturesVector, _propertyChangedHandlers, nullptr);
 
     private:
-        Model::AppearanceConfig _appearance;
-        winrt::hstring _lastBgImagePath;
-
+        void _refreshFontFaceDependents();
         Editor::AxisKeyValuePair _CreateAxisKeyValuePairHelper(winrt::hstring axisKey, float axisValue, const Windows::Foundation::Collections::IMap<winrt::hstring, float>& baseMap, const Windows::Foundation::Collections::IMap<winrt::hstring, winrt::hstring>& tagToNameMap);
         Editor::FeatureKeyValuePair _CreateFeatureKeyValuePairHelper(winrt::hstring axisKey, uint32_t axisValue, const Windows::Foundation::Collections::IMap<winrt::hstring, uint32_t>& baseMap, const Windows::Foundation::Collections::IMap<winrt::hstring, winrt::hstring>& tagToNameMap);
-
         bool _IsDefaultFeature(winrt::hstring featureTag);
+
+        Model::AppearanceConfig _appearance;
+        winrt::hstring _lastBgImagePath;
+        std::wstring _primaryFontName;
     };
 
     struct Appearances : AppearancesT<Appearances>
     {
     public:
-        Appearances();
+        static winrt::hstring FontAxisName(const winrt::hstring& key);
+        static winrt::hstring FontFeatureName(const winrt::hstring& key);
 
-        // font face
-        Windows::Foundation::IInspectable CurrentFontFace() const;
+        Appearances();
 
         // CursorShape visibility logic
         bool IsVintageCursor() const;
 
-        bool UsingMonospaceFont() const noexcept;
+        Windows::Foundation::Collections::IObservableVector<Editor::Font> FilteredFontList();
         bool ShowAllFonts() const noexcept;
-        void ShowAllFonts(const bool& value);
+        void ShowAllFonts(bool value);
 
+        void FontFaceBox_GotFocus(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
+        void FontFaceBox_LostFocus(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
+        void FontFaceBox_SuggestionChosen(const winrt::Windows::UI::Xaml::Controls::AutoSuggestBox&, const winrt::Windows::UI::Xaml::Controls::AutoSuggestBoxSuggestionChosenEventArgs&);
+        void FontFaceBox_TextChanged(const winrt::Windows::UI::Xaml::Controls::AutoSuggestBox&, const winrt::Windows::UI::Xaml::Controls::AutoSuggestBoxTextChangedEventArgs&);
         fire_and_forget BackgroundImage_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
         void BIAlignment_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
-        void FontFace_SelectionChanged(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& e);
         void DeleteAxisKeyValuePair_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
         void AddNewAxisKeyValuePair_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
         void DeleteFeatureKeyValuePair_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
@@ -219,21 +229,23 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         GETSET_BINDABLE_ENUM_SETTING(BackgroundImageStretchMode, Windows::UI::Xaml::Media::Stretch, Appearance().BackgroundImageStretchMode);
 
         GETSET_BINDABLE_ENUM_SETTING(IntenseTextStyle, Microsoft::Terminal::Settings::Model::IntenseStyle, Appearance().IntenseTextStyle);
-        WINRT_OBSERVABLE_PROPERTY(bool, ShowProportionalFontWarning, PropertyChanged.raise, nullptr);
 
     private:
-        bool _ShowAllFonts;
-        void _UpdateBIAlignmentControl(const int32_t val);
+        Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ViewModelChangedRevoker;
         std::array<Windows::UI::Xaml::Controls::Primitives::ToggleButton, 9> _BIAlignmentButtons;
-
         Windows::Foundation::Collections::IMap<uint16_t, Microsoft::Terminal::Settings::Editor::EnumEntry> _FontWeightMap;
         Editor::EnumEntry _CustomFontWeight{ nullptr };
-
+        Windows::Foundation::Collections::IObservableVector<Editor::Font> _filteredFonts;
         Windows::Foundation::Collections::IObservableVector<winrt::hstring> _FontAxesNames;
         Windows::Foundation::Collections::IObservableVector<winrt::hstring> _FontFeaturesNames;
+        std::wstring _fontNameFilter;
+        bool _ShowAllFonts = false;
 
-        Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ViewModelChangedRevoker;
         static void _ViewModelChanged(const Windows::UI::Xaml::DependencyObject& d, const Windows::UI::Xaml::DependencyPropertyChangedEventArgs& e);
+
+        void _updateFontNameFilter(std::wstring_view filter);
+        void _updateFilteredFontList();
+        void _UpdateBIAlignmentControl(const int32_t val);
         void _UpdateWithNewViewModel();
     };
 };
diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.idl b/src/cascadia/TerminalSettingsEditor/Appearances.idl
index 45f07acaa44..88fb2c6f898 100644
--- a/src/cascadia/TerminalSettingsEditor/Appearances.idl
+++ b/src/cascadia/TerminalSettingsEditor/Appearances.idl
@@ -21,7 +21,6 @@ namespace Microsoft.Terminal.Settings.Editor
     {
         String Name { get; };
         String LocalizedName { get; };
-        Boolean HasPowerlineCharacters { get; };
         Windows.Foundation.Collections.IMap<String, String> FontAxesTagsAndNames { get; };
         Windows.Foundation.Collections.IMap<String, String> FontFeaturesTagsAndNames { get; };
     }
@@ -59,6 +58,10 @@ namespace Microsoft.Terminal.Settings.Editor
         ColorSchemeViewModel CurrentColorScheme;
         Windows.Foundation.Collections.IObservableVector<ColorSchemeViewModel> SchemesList;
 
+        String MissingFontFaces { get; };
+        String ProportionalFontFaces { get; };
+        Boolean HasPowerlineCharacters { get; };
+
         void AddNewAxisKeyValuePair();
         void DeleteAxisKeyValuePair(String key);
         void InitializeFontAxesVector();
@@ -103,9 +106,8 @@ namespace Microsoft.Terminal.Settings.Editor
         IHostedInWindow WindowRoot;
         static Windows.UI.Xaml.DependencyProperty AppearanceProperty { get; };
 
-        Boolean UsingMonospaceFont { get; };
+        Windows.Foundation.Collections.IObservableVector<Font> FilteredFontList { get; };
         Boolean ShowAllFonts;
-        Boolean ShowProportionalFontWarning;
 
         IInspectable CurrentCursorShape;
         Boolean IsVintageCursor { get; };
@@ -121,8 +123,6 @@ namespace Microsoft.Terminal.Settings.Editor
         Boolean IsCustomFontWeight { get; };
         Windows.Foundation.Collections.IObservableVector<Microsoft.Terminal.Settings.Editor.EnumEntry> FontWeightList { get; };
 
-        IInspectable CurrentFontFace { get; };
-
         IInspectable CurrentIntenseTextStyle;
         Windows.Foundation.Collections.IObservableVector<Microsoft.Terminal.Settings.Editor.EnumEntry> IntenseTextStyleList { get; };
     }
diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.xaml b/src/cascadia/TerminalSettingsEditor/Appearances.xaml
index 35236fc509b..7d065dc4c92 100644
--- a/src/cascadia/TerminalSettingsEditor/Appearances.xaml
+++ b/src/cascadia/TerminalSettingsEditor/Appearances.xaml
@@ -183,7 +183,6 @@
             </local:SettingContainer>
 
             <!--  Font Face  -->
-
             <local:SettingContainer x:Uid="Profile_FontFace"
                                     ClearSettingValue="{x:Bind Appearance.ClearFontFace}"
                                     HasSettingValue="{x:Bind Appearance.HasFontFace, Mode=OneWay}"
@@ -191,35 +190,29 @@
                                     Visibility="{x:Bind Appearance.IsDefault, Mode=OneWay}">
 
                 <StackPanel Margin="0,8,0,0">
-                    <!--
-                        Binding the ItemsSource to a separate variable that switches between the
-                        two font lists causes a crash within the ComboBox code.
-                        As a workaround, introduce two ComboBox controls and only display one at a time.
-                    -->
-                    <ComboBox x:Uid="Profile_FontFaceBox"
-                              ItemTemplate="{StaticResource FontFaceComboBoxItemTemplate}"
-                              ItemsSource="{x:Bind SourceProfile.MonospaceFontList, Mode=OneWay}"
-                              SelectedItem="{x:Bind CurrentFontFace, Mode=OneWay}"
-                              SelectionChanged="FontFace_SelectionChanged"
-                              Style="{StaticResource ComboBoxSettingStyle}"
-                              Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(ShowAllFonts), Mode=OneWay}" />
-                    <ComboBox x:Uid="Profile_FontFaceBox"
-                              ItemTemplate="{StaticResource FontFaceComboBoxItemTemplate}"
-                              ItemsSource="{x:Bind SourceProfile.CompleteFontList, Mode=OneWay}"
-                              SelectedItem="{x:Bind CurrentFontFace, Mode=OneWay}"
-                              SelectionChanged="FontFace_SelectionChanged"
-                              Style="{StaticResource ComboBoxSettingStyle}"
-                              Visibility="{x:Bind ShowAllFonts, Mode=OneWay}" />
+                    <AutoSuggestBox x:Name="_fontFaceBox"
+                                    x:Uid="Profile_FontFaceBox"
+                                    GotFocus="FontFaceBox_GotFocus"
+                                    ItemTemplate="{StaticResource FontFaceComboBoxItemTemplate}"
+                                    ItemsSource="{x:Bind FilteredFontList, Mode=OneWay}"
+                                    LostFocus="FontFaceBox_LostFocus"
+                                    SuggestionChosen="FontFaceBox_SuggestionChosen"
+                                    Text="{x:Bind Appearance.FontFace, Mode=OneWay}"
+                                    TextBoxStyle="{StaticResource TextBoxSettingStyle}"
+                                    TextChanged="FontFaceBox_TextChanged" />
                     <CheckBox x:Name="ShowAllFontsCheckbox"
                               x:Uid="Profile_FontFaceShowAllFonts"
-                              IsChecked="{x:Bind ShowAllFonts, Mode=TwoWay}"
-                              IsEnabled="{x:Bind UsingMonospaceFont, Mode=OneWay}" />
+                              IsChecked="{x:Bind ShowAllFonts, Mode=TwoWay}" />
                 </StackPanel>
             </local:SettingContainer>
-            <muxc:InfoBar x:Uid="Profile_FontFace_ProportionalFontWarning"
-                          IsOpen="{x:Bind ShowProportionalFontWarning, Mode=OneWay}"
-                          Severity="Warning" />
-
+            <local:SettingContainer x:Uid="Profile_MissingFontFaces"
+                                    HelpText="{x:Bind Appearance.MissingFontFaces, Mode=OneWay}"
+                                    Style="{StaticResource SettingContainerErrorStyle}"
+                                    Visibility="{x:Bind mtu:Converters.StringNotEmptyToVisibility(Appearance.MissingFontFaces), Mode=OneWay}" />
+            <local:SettingContainer x:Uid="Profile_ProportionalFontFaces"
+                                    HelpText="{x:Bind Appearance.ProportionalFontFaces, Mode=OneWay}"
+                                    Style="{StaticResource SettingContainerWarningStyle}"
+                                    Visibility="{x:Bind mtu:Converters.StringNotEmptyToVisibility(Appearance.ProportionalFontFaces), Mode=OneWay}" />
 
             <!--  Font Size  -->
             <local:SettingContainer x:Uid="Profile_FontSize"
diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp
index aa6585e6a14..5295d3042bb 100644
--- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp
+++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp
@@ -28,16 +28,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         _Profile = args.Profile();
         _windowRoot = args.WindowRoot();
 
-        // generate the font list, if we don't have one
-        if (_Profile.CompleteFontList() || !_Profile.MonospaceFontList())
-        {
-            ProfileViewModel::UpdateFontList();
-        }
-
         if (!_previewControl)
         {
             const auto settings = _Profile.TermSettings();
-            _previewConnection->DisplayPowerlineGlyphs(_looksLikePowerlineFont());
+            _previewConnection->DisplayPowerlineGlyphs(_Profile.DefaultAppearance().HasPowerlineCharacters());
             _previewControl = Control::TermControl(settings, settings, *_previewConnection);
             _previewControl.IsEnabled(false);
             _previewControl.AllowFocusWhenDisabled(false);
@@ -70,25 +64,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         _Profile.DeleteUnfocusedAppearance();
     }
 
-    bool Profiles_Appearance::_looksLikePowerlineFont() const
-    {
-        if (_Profile && _Profile.DefaultAppearance())
-        {
-            if (const auto fontName = _Profile.DefaultAppearance().FontFace(); !fontName.empty())
-            {
-                if (const auto font = ProfileViewModel::FindFontWithLocalizedName(fontName))
-                {
-                    return font.HasPowerlineCharacters();
-                }
-            }
-        }
-        return false;
-    }
-
     void Profiles_Appearance::_onProfilePropertyChanged(const IInspectable&, const PropertyChangedEventArgs&) const
     {
         const auto settings = _Profile.TermSettings();
-        _previewConnection->DisplayPowerlineGlyphs(_looksLikePowerlineFont());
+        _previewConnection->DisplayPowerlineGlyphs(_Profile.DefaultAppearance().HasPowerlineCharacters());
         _previewControl.UpdateControlSettings(settings, settings);
     }
 }
diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h
index ad4a779e0cc..ab9ebf66fb0 100644
--- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h
+++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h
@@ -27,7 +27,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
 
     private:
         void _onProfilePropertyChanged(const IInspectable&, const PropertyChangedEventArgs&) const;
-        bool _looksLikePowerlineFont() const;
 
         winrt::com_ptr<PreviewConnection> _previewConnection{ nullptr };
         Microsoft::Terminal::Control::TermControl _previewControl{ nullptr };
diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
index e8995079ead..d046ac12df7 100644
--- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
@@ -137,11 +137,9 @@
   </data>
   <data name="ColorScheme_InboxSchemeDuplicate.Header" xml:space="preserve">
     <value>This color scheme is part of the built-in settings or an installed extension</value>
-    <comment></comment>
   </data>
   <data name="ColorScheme_InboxSchemeDuplicate.HelpText" xml:space="preserve">
     <value>To make changes to this color scheme, you must make a copy of it.</value>
-    <comment></comment>
   </data>
   <data name="ColorScheme_DuplicateButton.Content" xml:space="preserve">
     <value>Make a copy</value>
@@ -1810,12 +1808,12 @@
     <value>Learn more.</value>
     <comment>A hyperlink displayed near Settings_PortableModeNote.Text that the user can follow for more information.</comment>
   </data>
-	<data name="Profile_FontFace_ProportionalFontWarning.Title" xml:space="preserve">
-    <value>Warning:</value>
-    <comment>Title for the warning info bar used when a non monospace font face is chosen to indicate that there may be visual artifacts</comment>
+  <data name="Profile_MissingFontFaces.Header" xml:space="preserve">
+    <value>Missing fonts:</value>
+    <comment>This is a label that is followed by a list of missing fonts.</comment>
   </data>
-	<data name="Profile_FontFace_ProportionalFontWarning.Message" xml:space="preserve">
-    <value>Choosing a non-monospaced font will likely result in visual artifacts. Use at your own discretion.</value>
-    <comment>Warning info bar used when a non monospace font face is chosen to indicate that there may be visual artifacts</comment>
+  <data name="Profile_ProportionalFontFaces.Header" xml:space="preserve">
+    <value>Non-monospace fonts:</value>
+    <comment>This is a label that is followed by a list of proportional fonts.</comment>
   </data>
-</root>
+</root>
\ No newline at end of file
diff --git a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h
index e306233ab63..61597935390 100644
--- a/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h
+++ b/src/cascadia/TerminalSettingsEditor/ViewModelHelpers.h
@@ -39,20 +39,21 @@ struct ViewModelHelper
 
 #define _BASE_OBSERVABLE_PROJECTED_SETTING(target, name) \
 public:                                                  \
-    auto name() const noexcept                           \
+    auto name() const                                    \
     {                                                    \
         return target.name();                            \
     };                                                   \
     template<typename T>                                 \
     void name(const T& value)                            \
     {                                                    \
-        if (name() != value)                             \
+        const auto t = target;                           \
+        if (t.name() != value)                           \
         {                                                \
-            target.name(value);                          \
+            t.name(value);                               \
             _NotifyChanges(L"Has" #name, L## #name);     \
         }                                                \
     }                                                    \
-    bool Has##name()                                     \
+    bool Has##name() const                               \
     {                                                    \
         return target.Has##name();                       \
     }
@@ -63,14 +64,15 @@ public:                                                  \
     _BASE_OBSERVABLE_PROJECTED_SETTING(target, name) \
     void Clear##name()                               \
     {                                                \
-        const auto hadValue{ target.Has##name() };   \
-        target.Clear##name();                        \
+        const auto t = target;                       \
+        const auto hadValue{ t.Has##name() };        \
+        t.Clear##name();                             \
         if (hadValue)                                \
         {                                            \
             _NotifyChanges(L"Has" #name, L## #name); \
         }                                            \
     }                                                \
-    auto name##OverrideSource()                      \
+    auto name##OverrideSource() const                \
     {                                                \
         return target.name##OverrideSource();        \
     }
diff --git a/src/cascadia/UIHelpers/Converters.cpp b/src/cascadia/UIHelpers/Converters.cpp
index 997f0e5ec7d..ce5123995d4 100644
--- a/src/cascadia/UIHelpers/Converters.cpp
+++ b/src/cascadia/UIHelpers/Converters.cpp
@@ -40,6 +40,11 @@ namespace winrt::Microsoft::Terminal::UI::implementation
         return expected != actual;
     }
 
+    bool Converters::StringNotEmpty(const winrt::hstring& value)
+    {
+        return !value.empty();
+    }
+
     winrt::Windows::UI::Xaml::Visibility Converters::StringNotEmptyToVisibility(const winrt::hstring& value)
     {
         return value.empty() ? winrt::Windows::UI::Xaml::Visibility::Collapsed : winrt::Windows::UI::Xaml::Visibility::Visible;
diff --git a/src/cascadia/UIHelpers/Converters.h b/src/cascadia/UIHelpers/Converters.h
index 6fda669605b..af8bcecb458 100644
--- a/src/cascadia/UIHelpers/Converters.h
+++ b/src/cascadia/UIHelpers/Converters.h
@@ -20,6 +20,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
 
         // Strings
         static bool StringsAreNotEqual(const winrt::hstring& expected, const winrt::hstring& actual);
+        static bool StringNotEmpty(const winrt::hstring& value);
         static winrt::Windows::UI::Xaml::Visibility StringNotEmptyToVisibility(const winrt::hstring& value);
         static winrt::hstring StringOrEmptyIfPlaceholder(const winrt::hstring& placeholder, const winrt::hstring& value);
 
diff --git a/src/cascadia/UIHelpers/Converters.idl b/src/cascadia/UIHelpers/Converters.idl
index 2c9ef90e39d..baf8a97af65 100644
--- a/src/cascadia/UIHelpers/Converters.idl
+++ b/src/cascadia/UIHelpers/Converters.idl
@@ -18,6 +18,7 @@ namespace Microsoft.Terminal.UI
 
         // Strings
         static Boolean StringsAreNotEqual(String expected, String actual);
+        static Boolean StringNotEmpty(String value);
         static Windows.UI.Xaml.Visibility StringNotEmptyToVisibility(String value);
         static String StringOrEmptyIfPlaceholder(String placeholder, String value);
 
diff --git a/src/inc/til/string.h b/src/inc/til/string.h
index e76639000cf..fcb6905e282 100644
--- a/src/inc/til/string.h
+++ b/src/inc/til/string.h
@@ -202,6 +202,26 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
         return to_ulong<>(str, base);
     }
 
+    // Implement to_int in terms of to_ulong by negating its result. to_ulong does not expect
+    // to be passed signed numbers and will return an error accordingly. That error when
+    // compared against -1 evaluates to true. We account for that by returning to_int_error if to_ulong
+    // returns an error.
+    constexpr int to_int(const std::wstring_view& str, unsigned long base = 0) noexcept
+    {
+        auto result = to_ulong_error;
+        const auto signPosition = str.find(L"-");
+        const bool hasSign = signPosition != std::wstring_view::npos;
+        result = hasSign ? to_ulong(str.substr(signPosition + 1), base) : to_ulong(str, base);
+
+        // Check that result is valid and will fit in an int.
+        if (result == to_ulong_error || (result > INT_MAX))
+        {
+            return to_int_error;
+        }
+
+        return hasSign ? result * -1 : result;
+    }
+
     // Just like std::tolower, but without annoying locales.
     template<typename T>
     constexpr T tolower_ascii(T c)
@@ -376,6 +396,81 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
         return { beg, end };
     }
 
+    // Splits a font-family list into individual font-families. It loosely follows the CSS spec for font-family.
+    // It splits by comma, handles quotes and simple escape characters, and it cleans whitespace.
+    //
+    // This is not the right place to put this, because it's highly specialized towards font-family names.
+    // But this code is needed both, in our renderer and in our settings UI. At the time I couldn't find a better place for it.
+    void iterate_font_families(const std::wstring_view& families, auto&& callback)
+    {
+        std::wstring family;
+        bool escape = false;
+        bool delayedSpace = false;
+        wchar_t stringType = 0;
+
+        for (const auto ch : families)
+        {
+            if (!escape)
+            {
+                switch (ch)
+                {
+                case ' ':
+                    if (stringType)
+                    {
+                        // Spaces are treated literally inside strings.
+                        break;
+                    }
+                    delayedSpace = !family.empty();
+                    continue;
+                case '"':
+                case '\'':
+                    if (stringType && stringType != ch)
+                    {
+                        // Single quotes inside double quotes are treated literally and vice versa.
+                        break;
+                    }
+                    stringType = stringType == ch ? 0 : ch;
+                    continue;
+                case ',':
+                    if (stringType)
+                    {
+                        // Commas are treated literally inside strings.
+                        break;
+                    }
+                    if (!family.empty())
+                    {
+                        callback(std::move(family));
+                        family.clear();
+                        delayedSpace = false;
+                    }
+                    continue;
+                case '\\':
+                    escape = true;
+                    continue;
+                default:
+                    break;
+                }
+            }
+
+            // The `delayedSpace` logic automatically takes care for us to
+            // strip leading and trailing spaces and deduplicate them too.
+            if (delayedSpace)
+            {
+                delayedSpace = false;
+                family.push_back(L' ');
+            }
+
+            family.push_back(ch);
+            escape = false;
+        }
+
+        // Just like the comma handler above.
+        if (!stringType && !family.empty())
+        {
+            callback(std::move(family));
+        }
+    }
+
     // This function is appropriate for case-insensitive equivalence testing of file paths and other "system" strings.
     // Similar to memcmp, this returns <0, 0 or >0.
     inline int compare_ordinal_insensitive(const std::wstring_view& lhs, const std::wstring_view& rhs) noexcept
@@ -426,24 +521,4 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
 #pragma warning(suppress : 26477) // Use 'nullptr' rather than 0 or NULL (es.47).
         return FindNLSStringEx(LOCALE_NAME_USER_DEFAULT, LINGUISTIC_IGNORECASE, str.data(), strLen, needle.data(), needleLen, nullptr, nullptr, nullptr, 0) != -1;
     }
-
-    // Implement to_int in terms of to_ulong by negating its result. to_ulong does not expect
-    // to be passed signed numbers and will return an error accordingly. That error when
-    // compared against -1 evaluates to true. We account for that by returning to_int_error if to_ulong
-    // returns an error.
-    constexpr int to_int(const std::wstring_view& str, unsigned long base = 0) noexcept
-    {
-        auto result = to_ulong_error;
-        const auto signPosition = str.find(L"-");
-        const bool hasSign = signPosition != std::wstring_view::npos;
-        result = hasSign ? to_ulong(str.substr(signPosition + 1), base) : to_ulong(str, base);
-
-        // Check that result is valid and will fit in an int.
-        if (result == to_ulong_error || (result > INT_MAX))
-        {
-            return to_int_error;
-        }
-
-        return hasSign ? result * -1 : result;
-    }
 }
diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp
index f739022e8a4..c031f67a154 100644
--- a/src/renderer/atlas/AtlasEngine.api.cpp
+++ b/src/renderer/atlas/AtlasEngine.api.cpp
@@ -264,7 +264,7 @@ try
     }
 #endif
 
-    _resolveFontMetrics(nullptr, fontInfoDesired, fontInfo);
+    _resolveFontMetrics(fontInfoDesired, fontInfo);
     return S_OK;
 }
 CATCH_RETURN()
@@ -443,7 +443,7 @@ void AtlasEngine::SetGraphicsAPI(GraphicsAPI graphicsAPI) noexcept
     }
 }
 
-void AtlasEngine::SetWarningCallback(std::function<void(HRESULT)> pfn) noexcept
+void AtlasEngine::SetWarningCallback(std::function<void(HRESULT, wil::zwstring_view)> pfn) noexcept
 {
     _p.warningCallback = std::move(pfn);
 }
@@ -472,31 +472,39 @@ void AtlasEngine::SetWarningCallback(std::function<void(HRESULT)> pfn) noexcept
 
 [[nodiscard]] HRESULT AtlasEngine::UpdateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes) noexcept
 {
-    try
-    {
-        _updateFont(fontInfoDesired.GetFaceName().c_str(), fontInfoDesired, fontInfo, features, axes);
-        return S_OK;
-    }
-    CATCH_LOG();
-
+    // We're currently faced with a font caching bug that we're unable to reproduce locally. See GH#9375.
+    // But it occurs often enough and has no proper workarounds, so we're forced to fix it.
+    //
+    // Our leading theory is: When an app package has a <uap7:Extension Category="windows.sharedFonts">
+    // (like Windows Terminal with Cascadia Mono/Code) and it gets updated, the system locks the file somehow.
+    // DirectWrite still has some information about the font cached though, so it thinks that it still exists,
+    // but using the font causes it to error out because it can't access it. This fact became apparent in
+    // commit 9e86c98 (PR #16196), because it showed that it's definitely not due to FindFamilyName() failing.
+    //
+    // The workaround is to catch the exception and retry it with our nearby fonts manually loaded in.
     if constexpr (Feature_NearbyFontLoading::IsEnabled())
     {
         try
         {
-            // _resolveFontMetrics() checks `_api.s->font->fontCollection` for a pre-existing font collection,
-            // before falling back to using the system font collection. This way we can inject our custom one. See GH#9375.
-            // Doing it this way is a bit hacky, but it does have the benefit that we can cache a font collection
-            // instance across font changes, like when zooming the font size rapidly using the scroll wheel.
-            _api.s.write()->font.write()->fontCollection = FontCache::GetCached();
-            _updateFont(fontInfoDesired.GetFaceName().c_str(), fontInfoDesired, fontInfo, features, axes);
+            _updateFont(fontInfoDesired, fontInfo, features, axes);
             return S_OK;
         }
         CATCH_LOG();
+
+        // _resolveFontMetrics() checks `_api.s->font->fontCollection` for a pre-existing font collection,
+        // before falling back to using the system font collection. This way we can inject our custom one.
+        // Doing it this way is a bit hacky, but it does have the benefit that we can cache a font collection
+        // instance across font changes, like when zooming the font size rapidly using the scroll wheel.
+        try
+        {
+            _api.s.write()->font.write()->fontCollection = FontCache::GetCached();
+        }
+        CATCH_LOG();
     }
 
     try
     {
-        _updateFont(nullptr, fontInfoDesired, fontInfo, features, axes);
+        _updateFont(fontInfoDesired, fontInfo, features, axes);
         return S_OK;
     }
     CATCH_RETURN();
@@ -528,7 +536,7 @@ void AtlasEngine::_resolveTransparencySettings() noexcept
     }
 }
 
-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)
+void AtlasEngine::_updateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes)
 {
     std::vector<DWRITE_FONT_FEATURE> fontFeatures;
     if (!features.empty())
@@ -605,22 +613,19 @@ void AtlasEngine::_updateFont(const wchar_t* faceName, const FontInfoDesired& fo
     }
 
     const auto font = _api.s.write()->font.write();
-    _resolveFontMetrics(faceName, fontInfoDesired, fontInfo, font);
+    _resolveFontMetrics(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, FontSettings* fontMetrics) const
+void AtlasEngine::_resolveFontMetrics(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontSettings* fontMetrics) const
 {
+    const auto& faceName = fontInfoDesired.GetFaceName();
     const auto requestedFamily = fontInfoDesired.GetFamily();
     auto requestedWeight = fontInfoDesired.GetWeight();
     auto fontSize = fontInfoDesired.GetFontSize();
     auto requestedSize = fontInfoDesired.GetEngineSize();
 
-    if (!requestedFaceName)
-    {
-        requestedFaceName = L"Consolas";
-    }
     if (!requestedSize.height)
     {
         fontSize = 12.0f;
@@ -641,22 +646,88 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
         THROW_IF_FAILED(_p.dwriteFactory->GetSystemFontCollection(fontCollection.addressof(), FALSE));
     }
 
-    u32 index = 0;
-    BOOL exists = false;
-    THROW_IF_FAILED(fontCollection->FindFamilyName(requestedFaceName, &index, &exists));
-    THROW_HR_IF(DWRITE_E_NOFONT, !exists);
+    std::wstring primaryFontName;
+    std::wstring missingFontNames;
+    wil::com_ptr<IDWriteFontFamily> primaryFontFamily;
+    wil::com_ptr<IDWriteFontFallbackBuilder> fontFallbackBuilder;
+
+    // Resolves a comma-separated font list similar to CSS' font-family property. The first font in the list
+    // that can be resolved successfully will be the primary font which dictates the cell size among others.
+    // All remaining fonts are "secondary" fonts used for font fallback.
+    til::iterate_font_families(faceName, [&](std::wstring&& fontName) {
+        u32 index = 0;
+        BOOL exists = false;
+        THROW_IF_FAILED(fontCollection->FindFamilyName(fontName.c_str(), &index, &exists));
+
+        if (!exists)
+        {
+            if (!missingFontNames.empty())
+            {
+                missingFontNames.append(L", ");
+            }
+            missingFontNames.append(fontName);
+            return;
+        }
 
-    wil::com_ptr<IDWriteFontFamily> fontFamily;
-    THROW_IF_FAILED(fontCollection->GetFontFamily(index, fontFamily.addressof()));
+        if (!primaryFontFamily)
+        {
+            primaryFontName = std::move(fontName);
+            THROW_IF_FAILED(fontCollection->GetFontFamily(index, primaryFontFamily.addressof()));
+        }
+        else
+        {
+            if (!fontFallbackBuilder)
+            {
+                THROW_IF_FAILED(_p.dwriteFactory->CreateFontFallbackBuilder(fontFallbackBuilder.addressof()));
+            }
+
+            static constexpr DWRITE_UNICODE_RANGE fullRange{ 0, 0x10FFFF };
+            auto fontNamePtr = fontName.c_str();
+            THROW_IF_FAILED(fontFallbackBuilder->AddMapping(
+                /* ranges                 */ &fullRange,
+                /* rangesCount            */ 1,
+                /* targetFamilyNames      */ &fontNamePtr,
+                /* targetFamilyNamesCount */ 1,
+                /* fontCollection         */ fontCollection.get(),
+                /* localeName             */ nullptr,
+                /* baseFamilyName         */ nullptr,
+                /* scale                  */ 1.0f));
+        }
+    });
+
+    if (!missingFontNames.empty() && _p.warningCallback)
+    {
+        _p.warningCallback(DWRITE_E_NOFONT, missingFontNames);
+    }
+
+    // Fall back to Consolas if no font was found or specified.
+    if (!primaryFontFamily)
+    {
+        primaryFontName = L"Consolas";
+
+        u32 index = 0;
+        BOOL exists = false;
+        THROW_IF_FAILED(fontCollection->FindFamilyName(primaryFontName.c_str(), &index, &exists));
+        THROW_HR_IF(DWRITE_E_NOFONT, !exists);
+
+        THROW_IF_FAILED(fontCollection->GetFontFamily(index, primaryFontFamily.addressof()));
+    }
+
+    auto fontFallback = _api.systemFontFallback;
+    if (fontFallbackBuilder)
+    {
+        THROW_IF_FAILED(fontFallbackBuilder->AddMappings(_api.systemFontFallback.get()));
+        THROW_IF_FAILED(fontFallbackBuilder->CreateFontFallback(fontFallback.put()));
+    }
 
-    wil::com_ptr<IDWriteFont> font;
-    THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(static_cast<DWRITE_FONT_WEIGHT>(requestedWeight), DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, font.addressof()));
+    wil::com_ptr<IDWriteFont> primaryFont;
+    THROW_IF_FAILED(primaryFontFamily->GetFirstMatchingFont(static_cast<DWRITE_FONT_WEIGHT>(requestedWeight), DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, primaryFont.addressof()));
 
-    wil::com_ptr<IDWriteFontFace> fontFace;
-    THROW_IF_FAILED(font->CreateFontFace(fontFace.addressof()));
+    wil::com_ptr<IDWriteFontFace> primaryFontFace;
+    THROW_IF_FAILED(primaryFont->CreateFontFace(primaryFontFace.addressof()));
 
     DWRITE_FONT_METRICS metrics{};
-    fontFace->GetMetrics(&metrics);
+    primaryFontFace->GetMetrics(&metrics);
 
     // Point sizes are commonly treated at a 72 DPI scale
     // (including by OpenType), whereas DirectWrite uses 96 DPI.
@@ -682,12 +753,12 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
         static constexpr u32 codePoint = '0';
 
         u16 glyphIndex;
-        THROW_IF_FAILED(fontFace->GetGlyphIndicesW(&codePoint, 1, &glyphIndex));
+        THROW_IF_FAILED(primaryFontFace->GetGlyphIndicesW(&codePoint, 1, &glyphIndex));
 
         if (glyphIndex)
         {
             DWRITE_GLYPH_METRICS glyphMetrics{};
-            THROW_IF_FAILED(fontFace->GetDesignGlyphMetrics(&glyphIndex, 1, &glyphMetrics, FALSE));
+            THROW_IF_FAILED(primaryFontFace->GetDesignGlyphMetrics(&glyphIndex, 1, &glyphMetrics, FALSE));
             advanceWidth = static_cast<f32>(glyphMetrics.advanceWidth) * designUnitsPerPx;
         }
     }
@@ -746,12 +817,11 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
             requestedSize.width = gsl::narrow_cast<til::CoordType>(lrintf(fontSize / cellHeight * cellWidth));
         }
 
-        fontInfo.SetFromEngine(requestedFaceName, requestedFamily, requestedWeight, false, coordSize, requestedSize);
+        fontInfo.SetFromEngine(primaryFontName, requestedFamily, requestedWeight, false, coordSize, requestedSize);
     }
 
     if (fontMetrics)
     {
-        std::wstring fontName{ requestedFaceName };
         const auto fontWeightU16 = gsl::narrow_cast<u16>(requestedWeight);
         const auto advanceWidthU16 = gsl::narrow_cast<u16>(lrintf(advanceWidth));
         const auto baselineU16 = gsl::narrow_cast<u16>(lrintf(baseline));
@@ -773,8 +843,9 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
         // as we might cause _api to be in an inconsistent state otherwise.
 
         fontMetrics->fontCollection = std::move(fontCollection);
-        fontMetrics->fontFamily = std::move(fontFamily);
-        fontMetrics->fontName = std::move(fontName);
+        fontMetrics->fontFallback = std::move(fontFallback);
+        fontMetrics->fontFallback.try_query_to(fontMetrics->fontFallback1.put());
+        fontMetrics->fontName = std::move(primaryFontName);
         fontMetrics->fontSize = fontSizeInPx;
         fontMetrics->cellSize = { cellWidth, cellHeight };
         fontMetrics->fontWeight = fontWeightU16;
diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp
index e05e5b033eb..8cfbe995a14 100644
--- a/src/renderer/atlas/AtlasEngine.cpp
+++ b/src/renderer/atlas/AtlasEngine.cpp
@@ -41,8 +41,7 @@ AtlasEngine::AtlasEngine()
     THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(_p.dwriteFactory), reinterpret_cast<::IUnknown**>(_p.dwriteFactory.addressof())));
     _p.dwriteFactory4 = _p.dwriteFactory.try_query<IDWriteFactory4>();
 
-    THROW_IF_FAILED(_p.dwriteFactory->GetSystemFontFallback(_p.systemFontFallback.addressof()));
-    _p.systemFontFallback1 = _p.systemFontFallback.try_query<IDWriteFontFallback1>();
+    THROW_IF_FAILED(_p.dwriteFactory->GetSystemFontFallback(_api.systemFontFallback.addressof()));
 
     wil::com_ptr<IDWriteTextAnalyzer> textAnalyzer;
     THROW_IF_FAILED(_p.dwriteFactory->CreateTextAnalyzer(textAnalyzer.addressof()));
@@ -575,7 +574,7 @@ void AtlasEngine::_recreateFontDependentResources()
     {
         wchar_t localeName[LOCALE_NAME_MAX_LENGTH];
 
-        if (FAILED(GetUserDefaultLocaleName(&localeName[0], LOCALE_NAME_MAX_LENGTH)))
+        if (!GetUserDefaultLocaleName(&localeName[0], LOCALE_NAME_MAX_LENGTH))
         {
             memcpy(&localeName[0], L"en-US", 12);
         }
@@ -841,7 +840,7 @@ void AtlasEngine::_mapCharacters(const wchar_t* text, const u32 textLength, u32*
 
     if (textFormatAxis)
     {
-        THROW_IF_FAILED(_p.systemFontFallback1->MapCharacters(
+        THROW_IF_FAILED(_p.s->font->fontFallback1->MapCharacters(
             /* analysisSource     */ &analysisSource,
             /* textPosition       */ 0,
             /* textLength         */ textLength,
@@ -859,7 +858,7 @@ void AtlasEngine::_mapCharacters(const wchar_t* text, const u32 textLength, u32*
         const auto baseStyle = WI_IsFlagSet(_api.attributes, FontRelevantAttributes::Italic) ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL;
         wil::com_ptr<IDWriteFont> font;
 
-        THROW_IF_FAILED(_p.systemFontFallback->MapCharacters(
+        THROW_IF_FAILED(_p.s->font->fontFallback->MapCharacters(
             /* analysisSource     */ &analysisSource,
             /* textPosition       */ 0,
             /* textLength         */ textLength,
diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h
index b75a2a5cdce..5c9f954ae2f 100644
--- a/src/renderer/atlas/AtlasEngine.h
+++ b/src/renderer/atlas/AtlasEngine.h
@@ -76,7 +76,7 @@ namespace Microsoft::Console::Render::Atlas
         void SetSoftwareRendering(bool enable) noexcept;
         void SetDisablePartialInvalidation(bool enable) noexcept;
         void SetGraphicsAPI(GraphicsAPI graphicsAPI) noexcept;
-        void SetWarningCallback(std::function<void(HRESULT)> pfn) noexcept;
+        void SetWarningCallback(std::function<void(HRESULT, wil::zwstring_view)> pfn) noexcept;
         [[nodiscard]] HRESULT SetWindowSize(til::size pixels) noexcept;
         [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes) noexcept;
 
@@ -94,8 +94,8 @@ namespace Microsoft::Console::Render::Atlas
 
         // AtlasEngine.api.cpp
         void _resolveTransparencySettings() noexcept;
-        void _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);
-        void _resolveFontMetrics(const wchar_t* faceName, const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontSettings* fontMetrics = nullptr) const;
+        void _updateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map<std::wstring_view, uint32_t>& features, const std::unordered_map<std::wstring_view, float>& axes);
+        void _resolveFontMetrics(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontSettings* fontMetrics = nullptr) const;
 
         // AtlasEngine.r.cpp
         ATLAS_ATTR_COLD void _recreateAdapter();
@@ -156,6 +156,7 @@ namespace Microsoft::Console::Render::Atlas
             Buffer<f32> glyphAdvances;
             Buffer<DWRITE_GLYPH_OFFSET> glyphOffsets;
 
+            wil::com_ptr<IDWriteFontFallback> systemFontFallback;
             wil::com_ptr<IDWriteFontFace2> replacementCharacterFontFace;
             u16 replacementCharacterGlyphIndex = 0;
             bool replacementCharacterLookedUp = false;
diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp
index ac87c246dc9..f0a448d491b 100644
--- a/src/renderer/atlas/AtlasEngine.r.cpp
+++ b/src/renderer/atlas/AtlasEngine.r.cpp
@@ -65,7 +65,7 @@ catch (const wil::ResultException& exception)
     {
         try
         {
-            _p.warningCallback(hr);
+            _p.warningCallback(hr, {});
         }
         CATCH_LOG()
     }
diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp
index 19e98c6557e..6d7a86c5e4d 100644
--- a/src/renderer/atlas/BackendD3D.cpp
+++ b/src/renderer/atlas/BackendD3D.cpp
@@ -432,7 +432,7 @@ void BackendD3D::_recreateCustomShader(const RenderingPayload& p)
             }
             if (p.warningCallback)
             {
-                p.warningCallback(D2DERR_SHADER_COMPILE_FAILED);
+                p.warningCallback(D2DERR_SHADER_COMPILE_FAILED, p.s->misc->customPixelShaderPath);
             }
         }
 
@@ -448,7 +448,7 @@ void BackendD3D::_recreateCustomShader(const RenderingPayload& p)
                 _customPixelShader.reset();
                 if (p.warningCallback)
                 {
-                    p.warningCallback(D2DERR_SHADER_COMPILE_FAILED);
+                    p.warningCallback(D2DERR_SHADER_COMPILE_FAILED, p.s->misc->customPixelShaderImagePath);
                 }
             }
         }
@@ -1439,7 +1439,7 @@ BackendD3D::AtlasGlyphEntry* BackendD3D::_drawBuiltinGlyph(const RenderingPayloa
 
     if (BuiltinGlyphs::IsSoftFontChar(glyphIndex))
     {
-        _drawSoftFontGlyph(p, r, glyphIndex);
+        shadingType = _drawSoftFontGlyph(p, r, glyphIndex);
     }
     else
     {
@@ -1465,22 +1465,17 @@ BackendD3D::AtlasGlyphEntry* BackendD3D::_drawBuiltinGlyph(const RenderingPayloa
     return glyphEntry;
 }
 
-void BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const D2D1_RECT_F& rect, u32 glyphIndex)
+BackendD3D::ShadingType BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const D2D1_RECT_F& rect, u32 glyphIndex)
 {
-    _d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED);
-    const auto restoreD2D = wil::scope_exit([&]() {
-        _d2dRenderTarget->PopAxisAlignedClip();
-    });
-
     const auto width = static_cast<size_t>(p.s->font->softFontCellSize.width);
     const auto height = static_cast<size_t>(p.s->font->softFontCellSize.height);
     const auto softFontIndex = glyphIndex - 0xEF20u;
     const auto data = til::clamp_slice_len(p.s->font->softFontPattern, height * softFontIndex, height);
 
+    // This happens if someone wrote a U+EF2x character (by accident), but we don't even have soft fonts enabled yet.
     if (data.empty() || data.size() != height)
     {
-        _d2dRenderTarget->Clear();
-        return;
+        return ShadingType::Default;
     }
 
     if (!_softFontBitmap)
@@ -1517,8 +1512,14 @@ void BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const D2D1_RECT_F
         THROW_IF_FAILED(_softFontBitmap->CopyFromMemory(nullptr, bitmapData.data(), pitch));
     }
 
+    _d2dRenderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED);
+    const auto restoreD2D = wil::scope_exit([&]() {
+        _d2dRenderTarget->PopAxisAlignedClip();
+    });
+
     const auto interpolation = p.s->font->antialiasingMode == AntialiasingMode::Aliased ? D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR : D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC;
     _d2dRenderTarget->DrawBitmap(_softFontBitmap.get(), &rect, 1, interpolation, nullptr, nullptr);
+    return ShadingType::TextGrayscale;
 }
 
 void BackendD3D::_drawGlyphAtlasAllocate(const RenderingPayload& p, stbrp_rect& rect)
diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h
index 5e8055aecd0..25833b831dc 100644
--- a/src/renderer/atlas/BackendD3D.h
+++ b/src/renderer/atlas/BackendD3D.h
@@ -215,7 +215,7 @@ namespace Microsoft::Console::Render::Atlas
         ATLAS_ATTR_COLD void _drawTextOverlapSplit(const RenderingPayload& p, u16 y);
         [[nodiscard]] ATLAS_ATTR_COLD AtlasGlyphEntry* _drawGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u32 glyphIndex);
         AtlasGlyphEntry* _drawBuiltinGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u32 glyphIndex);
-        void _drawSoftFontGlyph(const RenderingPayload& p, const D2D1_RECT_F& rect, u32 glyphIndex);
+        ShadingType _drawSoftFontGlyph(const RenderingPayload& p, const D2D1_RECT_F& rect, u32 glyphIndex);
         void _drawGlyphAtlasAllocate(const RenderingPayload& p, stbrp_rect& rect);
         static AtlasGlyphEntry* _drawGlyphAllocateEntry(const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u32 glyphIndex);
         static void _splitDoubleHeightGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, AtlasGlyphEntry* glyphEntry);
diff --git a/src/renderer/atlas/common.h b/src/renderer/atlas/common.h
index 2e937baf04f..ad9bf2587be 100644
--- a/src/renderer/atlas/common.h
+++ b/src/renderer/atlas/common.h
@@ -345,7 +345,8 @@ namespace Microsoft::Console::Render::Atlas
     struct FontSettings
     {
         wil::com_ptr<IDWriteFontCollection> fontCollection;
-        wil::com_ptr<IDWriteFontFamily> fontFamily;
+        wil::com_ptr<IDWriteFontFallback> fontFallback;
+        wil::com_ptr<IDWriteFontFallback1> fontFallback1; // optional, might be nullptr
         std::wstring fontName;
         std::vector<DWRITE_FONT_FEATURE> fontFeatures;
         std::vector<DWRITE_FONT_AXIS_VALUE> fontAxisValues;
@@ -488,10 +489,8 @@ namespace Microsoft::Console::Render::Atlas
         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;
-        std::function<void(HRESULT)> warningCallback;
+        std::function<void(HRESULT, wil::zwstring_view)> warningCallback;
         std::function<void(HANDLE)> swapChainChangedCallback;
 
         //// Parameters which are constant for the existence of the backend.
diff --git a/src/til/ut_til/string.cpp b/src/til/ut_til/string.cpp
index 26fbf92ea47..2ef656a04d7 100644
--- a/src/til/ut_til/string.cpp
+++ b/src/til/ut_til/string.cpp
@@ -7,6 +7,25 @@ using namespace WEX::Common;
 using namespace WEX::Logging;
 using namespace WEX::TestExecution;
 
+template<>
+class WEX::TestExecution::VerifyOutputTraits<std::vector<std::wstring>>
+{
+public:
+    static WEX::Common::NoThrowString ToString(const std::vector<std::wstring>& vec)
+    {
+        WEX::Common::NoThrowString str;
+        str.Append(L"{ ");
+        for (size_t i = 0; i < vec.size(); ++i)
+        {
+            str.Append(i == 0 ? L"\"" : L", \"");
+            str.Append(vec[i].c_str());
+            str.Append(L"\"");
+        }
+        str.Append(L" }");
+        return str;
+    }
+};
+
 class StringTests
 {
     TEST_CLASS(StringTests);
@@ -199,4 +218,24 @@ class StringTests
         VERIFY_IS_TRUE(til::is_legal_path(LR"(C:\Users\Documents and Settings\Users\;\Why not)"));
         VERIFY_IS_FALSE(til::is_legal_path(LR"(C:\Users\Documents and Settings\"Quote-un-quote users")"));
     }
+
+    TEST_METHOD(IterateFontFamilies)
+    {
+        static constexpr auto expected = [](auto&&... args) {
+            return std::vector<std::wstring>{ std::forward<decltype(args)>(args)... };
+        };
+        static constexpr auto actual = [](std::wstring_view families) {
+            std::vector<std::wstring> split;
+            til::iterate_font_families(families, [&](std::wstring&& str) {
+                split.emplace_back(std::move(str));
+            });
+            return split;
+        };
+
+        VERIFY_ARE_EQUAL(expected(L"foo", L" b  a  r ", LR"(b"az)"), actual(LR"(  foo  ," b  a  r ",b\"az)"));
+        VERIFY_ARE_EQUAL(expected(LR"(foo, bar)"), actual(LR"("foo, bar")"));
+        VERIFY_ARE_EQUAL(expected(LR"("foo")", LR"('bar')"), actual(LR"('"foo"', "'bar'")"));
+        VERIFY_ARE_EQUAL(expected(LR"("foo")", LR"('bar')"), actual(LR"("\"foo\"", '\'bar\'')"));
+        VERIFY_ARE_EQUAL(expected(L"foo"), actual(LR"(,,,,foo,,,,)"));
+    }
 };
diff --git a/tools/Lock-CascadiaFont.ps1 b/tools/Lock-CascadiaFont.ps1
new file mode 100644
index 00000000000..f1a1e384b26
--- /dev/null
+++ b/tools/Lock-CascadiaFont.ps1
@@ -0,0 +1,76 @@
+# This script is a failed attempt to lock the Cascadia Mono/Code font files in order to reproduce an issue with the font
+# cache service, where it says a font exists, but then fails to use it (see GH#9375). The script doesn't work because
+# for some reason DirectWrite is still able to fully use the fonts. It's left here for reference.
+
+#Requires -RunAsAdministrator
+
+Add-Type -TypeDefinition @"
+using System;
+using System.Runtime.InteropServices;
+
+public class Win32LockFile {
+    public const uint LOCKFILE_FAIL_IMMEDIATELY = 0x00000001;
+    public const uint LOCKFILE_EXCLUSIVE_LOCK = 0x00000002;
+
+    [DllImport("kernel32.dll")]
+    public static extern bool LockFileEx(IntPtr hFile, uint dwFlags, uint dwReserved, uint nNumberOfBytesToLockLow, uint nNumberOfBytesToLockHigh, ref OVERLAPPED lpOverlapped);
+
+    [StructLayout(LayoutKind.Sequential)]
+    public struct OVERLAPPED {
+        public uint Internal;
+        public uint InternalHigh;
+        public uint Offset;
+        public uint OffsetHigh;
+        public IntPtr hEvent;
+    }
+}
+"@
+
+function Lock-File {
+    param(
+        [Parameter(Mandatory=$true)]
+        [string]$Path
+    )
+
+    $file = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
+    $overlapped = New-Object Win32LockFile+OVERLAPPED
+    $result = [Win32LockFile]::LockFileEx(
+        $file.SafeFileHandle.DangerousGetHandle(), # hFile
+        [Win32LockFile]::LOCKFILE_EXCLUSIVE_LOCK,  # dwFlags
+        0,                                         # dwReserved
+        [UInt32]::MaxValue,                        # nNumberOfBytesToLockLow
+        [UInt32]::MaxValue,                        # nNumberOfBytesToLockHigh
+        [ref]$overlapped                           # lpOverlapped
+    )
+
+    if (-not $result) {
+        throw "Failed to lock file"
+    }
+
+    return $file
+}
+
+$fonts = Get-ItemProperty "HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Fonts\*"
+    | ForEach-Object { $_.PSobject.Properties }
+    | Where-Object { $_.Name.StartsWith("Cascadia") }
+    | ForEach-Object { $_.Value }
+
+$fonts += @("CascadiaCode.ttf", "CascadiaCodeItalic.ttf", "CascadiaMono.ttf", "CascadiaMonoItalic.ttf")
+    | ForEach-Object { "C:\Windows\Fonts\$_" }
+    | Where-Object { Test-Path $_ }
+
+try {
+    $handles = $fonts | ForEach-Object {
+        try {
+            Lock-File $_
+        }
+        catch {
+            Write-Error $_
+        }
+    }
+    Restart-Service FontCache
+    Write-Host "Press any key to unlock the font files..."
+    $null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
+} finally {
+    $handles | Where-Object { $_ } | ForEach-Object { $_.Close() }
+}


Test patch: