graphics: improve native system font fallback

Initialize SystemFontResolver when enabled and make its lifecycle and font-list
population thread-safe.

Use native codepoint fallback matching through Fontconfig on Linux, BSD, and
Haiku, DirectWrite on Windows, and Core Text on macOS and iOS. Preserve the
cached FreeType scan as a fallback when native matching is unavailable.

Enable system fonts by default for UIApplication and warm the font database in
the background. Allow applications to override the policy and disable the
automatic default in the unit-test runner.

Fix glyph fallback checks to respect the resolver's enabled state and add
coverage for concurrent warm-up and shaped symbol fallback.

Update efsw.
This commit is contained in:
Martín Lucas Golini
2026-08-23 16:59:23 -03:00
parent 89487fec8b
commit 301b7458d2
15 changed files with 756 additions and 1461 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,34 @@
# eepp Superluminal GUI/HTML Optimization Plan - 2026-07-09
## Status (updated 2026-08-23)
Implemented since this plan was written:
### Item 3 — Fast-parse common CSS lengths without allocations: DONE
`StyleSheetLength::fromString()` trims once, scans numeric prefixes directly from a
`std::string_view`, and converts through `String::fromString()` view overloads; the CSS parser
no longer builds number/unit strings or retries parsing by removing characters. Function
expressions retain the existing parser.
- Focused benchmark median for 320,000 mixed values: 13.67 ms → 10.10 ms, a 26.1% reduction.
- Post-change full release suite: 737 passed, one skipped.
### Item 10 — Broaden hot accessor/type-check inlining beyond selector code: DONE (core hierarchy)
Trivial `getType()` / `isType()` implementations for `Node`, `UINode`, `UIWidget`, `UILayout`,
`UIHTMLWidget`, and `UIRichText`, plus `Node::isLayout()`, are now inline.
- `UISceneNode::invalidateLayout()`: 86.1 ms inclusive / 41.5 ms exclusive → 26.3 ms total.
- Base type accessor symbols largely disappeared from the hot function list.
- `getEffectiveWhiteSpaceCollapse()`: 344.0 ms → 162.9 ms inclusive, partly from cheaper type checks.
- Unit-test process CPU time: 20.12 s → 19.88 s (~1.2%, directional only).
Further subclass inlining (table widgets) remains possible but is not an independent priority
here; it can be folded into other items if needed.
All remaining items (1, 2, 49, 11, 12) are **not started**.
## Goal
Use the Superluminal capture from `2026-07-09_19-52-19_eepp-unit_tests` to identify cheap, low-risk optimization work in eepp's GUI, text, and HTML compatibility layers. This plan intentionally ignores unit-test-specific hotspots such as `EE::Graphics::Image::diff()` and focuses on code paths likely to matter in real applications and `ecode`.
@@ -58,7 +87,6 @@ Approximate aggregate timings from the capture:
| White-space inheritance lookup | `getEffectiveWhiteSpaceCollapse`: ~406 ms inclusive, 3235 calls |
| Text width measurement | `Text::updateWidthCache`: ~142 ms inclusive, 916 calls |
| Font glyph/kerning lookup | `FontTrueType::getGlyphByIndex`: ~338 ms inclusive, 2189 calls; `getKerning`: ~72 ms inclusive, 525 calls |
| CSS length parsing | `StyleSheetLength::fromString`: ~44.7 ms exclusive / ~106.9 ms inclusive, 872 calls |
| CSS style selection | `StyleSheet::getElementStyles`: ~154 ms inclusive; `StyleSheetSelector::select`: ~116 ms inclusive; `StyleSheetSelectorRule::matches`: ~55 ms inclusive |
| HTML load/style reload | `UIHTML_KittyHomeSmallDoesNotHang` branch: `loadLayoutFromString` ~199 ms, recursive `reloadStyle` ~101 ms, `updateDirtyLayouts` ~185 ms |
@@ -121,58 +149,6 @@ Validation:
- Existing `UIHTML.*white*`, `UIRichText.*`, and text-transform tests.
- Add a nested span test with inherited `white-space`, local `white-space-collapse`, and nested text-transform override.
### 3. Fast-parse common CSS lengths without allocations
**Status: Implemented and measured**
`StyleSheetLength::fromString()` now trims once, scans scalar numeric prefixes directly from a
`std::string_view`, and converts the numeric subview through `String::fromString()`. The core string
API exposes `std::string_view` numeric overloads while retaining exact `std::string` forwarding
overloads for source and ABI compatibility with the implicitly constructible `EE::String` type.
The CSS parser no longer builds separate number/unit strings or retries parsing by removing
characters. Function expressions retain the existing parser, and position keywords map directly
to percentage lengths without recursive string construction.
Validation includes signed values, leading-dot decimals, scientific notation, surrounding
whitespace, position keywords, unitless and unknown units, `pxAsDp`, all existing CSS function
tests, and a dedicated release benchmark.
- Final focused benchmark median for 320,000 mixed values: 13.67 ms to 10.10 ms, a 26.1%
reduction.
- Before numeric conversion was centralized in `String`, the full-suite capture measured 105.4 ms
inclusive / 44.6 ms exclusive to 46.5 ms / 12.4 ms. A future full capture should refresh those
aggregate numbers for the final implementation.
- Post-change full release suite: 737 passed, one skipped.
Comparison capture:
```text
/tmp/eepp-unit-tests-length-fast-path-2026-07-10.linux
```
Files:
```text
src/eepp/ui/css/stylesheetlength.cpp
include/eepp/ui/css/stylesheetlength.hpp
```
Current hot section: `src/eepp/ui/css/stylesheetlength.cpp:391`.
`StyleSheetLength::fromString()` allocates `num`, allocates `unit` via `substr`, lower-hashes the whole string before knowing whether it is a keyword, and may repeatedly call `String::fromString()` while popping characters.
Proposed cheap path:
- First scan a trimmed `std::string_view`.
- If the first non-sign character is numeric or `.`, parse numeric prefix directly and pass the unit suffix as `std::string_view`.
- Add `unitFromString(std::string_view)` and avoid constructing `unit`.
- Only call `String::hashToLower()` for non-numeric keyword candidates.
- Keep the existing function-parser path for `calc()`, `min()`, `max()`, and `clamp()`.
Validation:
- CSS length parser tests for signed values, decimals, percentages, unitless `0`, `pxAsDp`, keywords, and function strings.
### 4. Avoid duplicate content-offset and size work in `UIRichText::rebuildRichText()`
Files:
@@ -345,57 +321,6 @@ Validation:
- Existing layout loading tests.
- HTML fixture tests involving inherited color/font/white-space and immediate layout after load.
### 10. Broaden hot accessor/type-check inlining beyond selector code
**Status: Core hierarchy implemented and measured**
The trivial `getType()` / `isType()` implementations for `Node`, `UINode`, `UIWidget`,
`UILayout`, `UIHTMLWidget`, and `UIRichText` are now inline, along with `Node::isLayout()`.
In the comparable full-suite captures:
- `UISceneNode::invalidateLayout()` decreased from 86.1 ms inclusive / 41.5 ms exclusive to
26.3 ms total.
- Base type accessor symbols largely disappeared from the hot function list.
- `getEffectiveWhiteSpaceCollapse()` decreased from 344.0 ms to 162.9 ms inclusive, partly
because its repeated type checks became cheaper.
- Unit-test process CPU time decreased from 20.12 s to 19.88 s, approximately 1.2%. Treat this
whole-process result as directional because the full suite contains rendering and image-diff
noise.
Further subclass inlining remains possible, especially table widgets, but the next independent
high-value target is CSS length parsing.
Files:
```text
include/eepp/scene/node.hpp
include/eepp/ui/uiwidget.hpp
include/eepp/ui/uinode.hpp
include/eepp/ui/uilayout.hpp
```
The selector optimization plan already lists some inlining work. This capture shows the same issue in rich-text and layout code:
```text
Node::getParent: ~37.7 ms exclusive
Node::getType: ~36.1 ms exclusive
UIWidget::isType: ~37.6 ms exclusive
UINode::isType: ~26.9 ms exclusive
UIHTMLWidget::isType / getType: ~32.0 ms combined
```
Proposed cheap path:
- Execute the accessor-inline phase from the CSS selector plan, but validate impact on rich-text/layout too.
- Include `getType()`/`isType()` candidates for `UINode`, `UIWidget`, `UILayout`, `UIHTMLWidget`, `UIRichText`, and table element subclasses where definitions are trivial.
- Keep ABI/ODR constraints in mind; remove matching out-of-line definitions where required.
Validation:
- Full build and unit test smoke.
- `git diff --check` and one focused HTML/rich-text filter.
### 11. Keep CSS selector optimization in the existing dedicated plan
Files:
@@ -415,11 +340,11 @@ StyleSheetSelector::select: ~116 ms inclusive
StyleSheetSelectorRule::matches: ~55 ms inclusive
```
Do not duplicate that work here. When executing this plan, treat selector indexing, sibling combinator correctness, and class/hash matching as owned by `eepp_css_selector_optimization_plan.md`.
Do not duplicate that work here. When executing this plan, treat selector instrumentation and any further selector work as owned by `eepp_css_selector_optimization_plan.md` (its Phases 13 and 58 are done; Phase 4 instrumentation plus optional Phases 910 remain).
Small complementary task:
- If a style reload batching change is implemented first, rerun the same capture or filtered HTML tests before starting selector indexing. Batching may reduce selector call volume and change the measured priority.
- If a style reload batching change is implemented first, rerun the same capture or filtered HTML tests before starting further selector work. Batching may reduce selector call volume and change the measured priority.
### 12. Add a repeatable profiling harness for these focused cases
@@ -451,12 +376,12 @@ The goal is not new product code, but a repeatable before/after measurement path
## Suggested Execution Order
1. Fast local wins: CSS length parsing, white-space collapse reuse, duplicate content-offset/size work.
1. Fast local wins: white-space collapse reuse, duplicate content-offset/size work.
2. Dirty layout invalidation coalescing.
3. RichText update-layout de-duplication and buffer reuse.
4. Text/glyph/kerning metrics helper.
5. Style reload batching.
6. Existing CSS selector optimization plan.
6. Remaining items of the CSS selector optimization plan (Phase 4 instrumentation first).
## Non-Goals For This Pass

View File

@@ -1,280 +0,0 @@
# HTML Replaced Elements and CSS Auto-Margin Plan
Status: proposed follow-up after the Asahi Linux async inline-image regression fix, 2026-08-02.
## Goal
Replace formatting-role inference with an explicit CSS used-value model for margins and make
`UIHTMLImage` a real `UIHTMLWidget` replaced element rather than a native `UIImage` with an HTML
flag.
The end state should provide:
- CSS `display`, positioning, float, intrinsic sizing, and used-margin behavior from
`UIHTMLWidget` for `<img>`;
- reusable image loading, drawable ownership, intrinsic dimensions, aspect ratio, and painting
shared with native `UIImage` through composition;
- formatting-context-specific auto-margin resolution based on the generated CSS box, not widget
type or `SizePolicy` heuristics;
- deterministic behavior across synchronous and asynchronous stylesheet/image loading;
- no image-specific exceptions in `BlockLayouter`, `UIRichText`, or `FlexLayouter`.
## Current State and Problem
`UIHTMLImage` currently derives from `UIImage`. It adds HTML identity, `alt`, and fallback text,
but it does not inherit the CSS layout state owned by `UIHTMLWidget`:
- `CSSDisplay` and blockification;
- `CSSPosition`, float, and clear;
- box sizing and CSS used-size helpers;
- layouter selection and formatting-context participation;
- normal-flow and out-of-flow classification;
- baseline behavior shared by other HTML boxes.
As a result, HTML layout infers an image's formatting role from combinations of:
- `UI_HTML_ELEMENT`;
- `SizePolicy`;
- concrete widget type;
- parent layout context.
That inference caused the Asahi regression. An inline `<img>` with `margin-left:auto` and
`margin-right:auto` was passed to the generic native block auto-margin calculator. During async
SVG loading, stale parent width was converted into large image margins, the inline anchor expanded
to include them, and repeated layout fed the expanded width back into the next pass.
The immediate fix centralizes formatting-context used margins, but still classifies some boxes
indirectly. This plan replaces that compatibility layer with explicit CSS box semantics.
## Standards Baseline
Implement against CSS used values, keeping these cases distinct:
1. Inline non-replaced and inline replaced boxes: horizontal `auto` margins use `0`.
2. Normal-flow inline-block boxes, replaced or non-replaced: horizontal `auto` margins use `0`.
3. Floats, replaced or non-replaced: horizontal `auto` margins use `0`.
4. Normal-flow block boxes: solve horizontal margins and width using the block constraint
equation; two auto margins center a definite-width box.
5. Absolutely/fixed positioned boxes: solve margins together with `left`, `right`, and `width`;
auto margins are not unconditionally zero.
6. Flex and grid items: use their module-specific auto-margin algorithms, not block formatting
rules.
7. Vertical auto margins: preserve the rules of the applicable formatting model. Do not fold them
into a horizontal-only shortcut.
Primary references:
- CSS 2.2 section 10.3, Calculating widths and margins.
- CSS 2.2 sections 10.3.1 through 10.3.10 for normal flow, replaced boxes, floats,
inline-blocks, and positioned boxes.
- CSS 2.2 section 10.6 for heights and vertical margins.
- CSS Display Level 3 for inner/outer display roles and blockification.
- CSS Flexbox section 8.1 and CSS Grid alignment rules for auto margins on flex/grid items.
- HTML rendering rules and replaced-element behavior for `img`.
## Design Direction
### Separate CSS box semantics from image mechanics
Change the inheritance direction to:
```text
UIWidget
├── UIImage native eepp image widget
└── UIHTMLWidget
└── UIHTMLImage HTML replaced element
```
Do not copy `UIImage` wholesale into `UIHTMLImage`. Extract reusable image mechanics into a
non-widget component or small set of helpers, tentatively named `UIImageContent`:
```text
UIImageContent
├── DrawablePtr and resource-change connection
├── local/remote/deferred source loading
├── intrinsic pixel size and aspect ratio
├── scale mode, tint, destination-size calculation
├── sprite scheduling support
└── drawable painting
```
Both `UIImage` and `UIHTMLImage` own an `UIImageContent` instance and provide narrow host callbacks
for scene/resource access, size changes, invalidation, and main-thread delivery.
The component must not own layout policy, CSS display, margins, padding, borders, position, or
parent geometry. Those remain widget responsibilities.
### Represent formatting role explicitly
Introduce a small formatting-role/used-value input derived from computed CSS state after
blockification, for example:
```cpp
enum class CSSFormattingRole {
Inline,
InlineBlock,
NormalFlowBlock,
Float,
Absolute,
Fixed,
FlexItem,
GridItem,
Table
};
```
The exact enum is an implementation decision; avoid exposing redundant public API if existing
computed display/position state can produce the same result cheaply. The important invariant is
that used-margin resolution receives an explicit role and never guesses from `SizePolicy` or
concrete widget class.
Keep role derivation centralized near `UILayouterManager` / `UIHTMLWidget`, including CSS
blockification. Do not derive it separately in RichText, block, flex, grid, and positioned layout.
### Separate computed, resolved, and used margins
Preserve three concepts:
- specified/computed margin value, including the `auto` bit;
- resolved length for non-auto values, including percentages;
- used margin for the current formatting context and containing block.
Do not mutate the stored resolved margin merely to obtain a used value for one layout pass. Return
a stack-local used-margin structure instead. This prevents stale block auto margins from being
reused when display, position, float, or parent formatting context changes asynchronously.
Suggested API shape:
```cpp
struct CSSUsedMargins {
Rectf value;
Uint8 autoSides;
};
CSSUsedMargins resolveUsedMargins(
const UIHTMLWidget& box,
CSSFormattingRole role,
const CSSContainingBlockMetrics& containingBlock );
```
The final API can be smaller, but it must not call the generic native `UIWidget::calculateAutoMargin()`
for HTML formatting.
## Implementation Stages
### Stage 1 - Lock Down Current Behavior
- Keep the real Asahi WebView fixture test with a thread pool and weighted native host layout.
- Assert the logo image is 130px, its inline anchor matches that width, and document overflow stays
within a reasonable viewport-derived bound.
- Add focused tests for:
- inline `<img style="margin: auto">`;
- inline-block non-replaced element with horizontal auto margins;
- fixed-width normal-flow block with horizontal auto margins;
- block-level replaced image with horizontal auto margins;
- floated image with auto margins;
- absolutely positioned replaced and non-replaced boxes for the relevant inset combinations;
- flex and grid items with auto margins;
- display changes before and after deferred CSS/image completion.
- Where practical, compare numeric invariants against a browser reference.
### Stage 2 - Introduce Formatting-Role-Aware Used Margins
- Add one centralized role derivation function based on computed display, position, float, and
parent flex/grid state.
- Introduce a non-mutating used-margin resolver.
- Route `BlockLayouter`, `UIRichText`, positioned layout, flex, and grid through the appropriate
role-specific solver.
- Keep flex/grid distribution in their existing layouters; the shared resolver should identify and
defer those roles rather than reimplementing their algorithms.
- Remove `UIHTMLWidget::getFormattingContextLayoutPixelsMargin()` once all callers use the explicit
role API.
- Audit `UIWidget::calculateAutoMargin()` callers and retain it only for native eepp layout.
### Stage 3 - Extract Reusable Image Content
- Inventory every `UIImage` responsibility and split it into:
- image resource/content behavior suitable for reuse;
- native widget layout and alignment behavior that remains in `UIImage`.
- Create `UIImageContent` or equivalent without adding a second scene/widget hierarchy.
- Move drawable loading, async lifetime guarding, resource-change subscription, destination-size
calculation, tint/scale settings, and drawing helpers behind the component.
- Preserve the current shared `DrawablePtr` ownership and scene-scoped resource resolution.
- Keep async callbacks generation/lifetime guarded and main-thread UI mutation explicit.
- Add unit tests for the component through both native and HTML hosts; avoid exposing internals
solely for testing.
### Stage 4 - Rebase `UIHTMLImage` on `UIHTMLWidget`
- Change `UIHTMLImage` to inherit from `UIHTMLWidget` and own shared image content.
- Implement it as a replaced element with:
- intrinsic width, height, and ratio from the decoded drawable/SVG;
- CSS width/height/min/max/box-sizing integration;
- default inline outer display and atomic inline participation;
- block, inline-block, float, flex/grid item, and positioned behavior from computed CSS;
- baseline fallback at the replaced element's bottom margin edge as required by the supported
inline formatting model;
- `alt` fallback sizing, painting, and accessibility/hit-box behavior;
- async intrinsic-size invalidation that dirties the correct ancestors exactly once.
- Ensure CSS `display` changes exchange layouters correctly and do not retain stale geometry.
- Keep the public `UIHTMLImage` API source-compatible where reasonable (`getDrawable`, `setAlt`,
`getAlt`, source properties), but do not preserve inheritance-based `UIImage*` conversion.
### Stage 5 - Migrate Callers and Remove Compatibility Inference
- Update callers that assume `UIHTMLImage` is a `UIImage`.
- Replace casts and shared behavior with explicit `UIHTMLImage` or image-content APIs.
- Audit type queries, inspector property reporting, serialization, widget creation, and tests.
- Remove `UI_HTML_ELEMENT` branches in `UIImage` that only exist to emulate HTML pixel/intrinsic
behavior.
- Remove image-specific checks and `SizePolicy` heuristics from RichText and layouters.
- Validate that `UISvg` remains a native image widget unless/until inline `<svg>` receives its own
explicit HTML/SVG replaced-element wrapper; do not accidentally fold that larger migration into
this work.
### Stage 6 - Conformance and Performance Validation
- Run focused HTML layout suites after each stage, then the full unit-test suite.
- Exercise synchronous and asynchronous image loading, repeated navigation, viewport resize,
device-pixel-ratio changes, and deferred stylesheets.
- Add a stress test that repeatedly loads/destroys image-heavy WebViews under ASan.
- Verify no layout oscillation: count dirty-layout iterations and assert convergence for the Asahi
fixture.
- Compare release-build layout/rebuild counters before and after the refactor.
- Audit every introduced allocation, string copy, shared ownership handoff, callback capture, and
per-frame branch. Image loading may allocate; steady-state layout and drawing must not add new
heap work.
## Compatibility and Migration Risks
- `UIHTMLImage*` will no longer convert to `UIImage*`. Search the complete repository and document
any external API compatibility impact before landing the inheritance change.
- Native `UIImage` alignment semantics are not CSS `object-position` or inline formatting
semantics. Keep those concepts separate during extraction.
- Replaced sizing must distinguish CSS pixels from physical raster pixels, especially for SVG and
HiDPI rendering.
- Async drawable completion must not apply geometry from an obsolete navigation or destroyed
document scene.
- `alt` text is not merely native image fallback drawing; its intrinsic sizing and line
participation need an explicit supported behavior and tests.
- Absolute-position auto margins require inset-aware equation solving. Treating all out-of-flow
auto margins as zero would be another compatibility shortcut.
- Tables, flex items, and grid items have formatting-model-specific used-size and auto-margin rules;
do not force them through the normal block equation.
## Exit Criteria
This follow-up is complete when:
- `UIHTMLImage` derives from `UIHTMLWidget`, not `UIImage`;
- native and HTML images share image mechanics without duplicated resource/drawing code;
- HTML used margins are resolved from explicit formatting roles without mutating stored margins;
- inline, block, float, positioned, flex, and grid auto-margin tests match supported CSS behavior;
- the Asahi async fixture converges with a 130px logo/anchor and bounded document overflow;
- deferred style/image completion and viewport resizing cannot feed stale margins or positions back
into intrinsic sizing;
- no `UI_HTML_ELEMENT` or `SizePolicy` heuristic remains as the source of an image's CSS display
role;
- focused and full test suites pass under the required wrapper, and the allocation/performance
audit finds no new steady-state heap work.

View File

@@ -110,6 +110,10 @@ class EE_API SystemFontResolver {
FontDesc getFallbackForCodepoint( Uint32 codepoint, FontWeight weight, bool italic );
/** Populate and cache the system font database without copying the resulting font list.
* Safe to call from a worker thread after enabling the resolver. */
void warmUp() const;
bool fontContainsCodepoint( const std::string& path, Uint32 codepoint );
void invalidateCache();
@@ -144,11 +148,13 @@ class EE_API SystemFontResolver {
void populateGenericFallbacks() const;
FontDesc matchFallbackForCodepoint( Uint32 codepoint, FontWeight weight, bool italic ) const;
static int scoreMatch( const FontQuery& query, const FontDesc& candidate );
mutable System::Mutex mMutex;
mutable std::vector<FontDesc> mFontList;
mutable bool mFontListPopulated{ false };
mutable std::atomic<bool> mFontListPopulated{ false };
mutable std::atomic<bool> mFontListLoading{ false };
static Uint64 makeCacheKey( const std::string& normFamily, FontWeight weight,
@@ -166,7 +172,7 @@ class EE_API SystemFontResolver {
};
mutable std::vector<GenericEntry> mGenericFallbacks;
static bool sEnabled;
static std::atomic<bool> sEnabled;
};
}} // namespace EE::Graphics

View File

@@ -4,6 +4,7 @@
#include <eepp/graphics/font.hpp>
#include <eepp/window/window.hpp>
#include <memory>
#include <optional>
using namespace EE::Window;
@@ -12,6 +13,10 @@ namespace EE { namespace UI {
class UISceneNode;
namespace Private {
class UIApplicationSystemFontState;
}
class EE_API UIApplication {
public:
struct EE_API Settings {
@@ -50,6 +55,9 @@ class EE_API UIApplication {
FontHinting fontHinting{ FontHinting::Full };
//! The antialiasing policy applied to fonts owned by the default and UI resource scopes.
FontAntialiasing fontAntialiasing{ FontAntialiasing::Grayscale };
//! Enables system font fallback and warms the system font list on a background thread. If
//! not set, UIApplication::systemFontsEnabledByDefault() is used.
std::optional<bool> enableSystemFonts;
};
UIApplication( const WindowSettings& windowSettings, const Settings& appSettings = Settings(),
@@ -74,6 +82,12 @@ class EE_API UIApplication {
bool showMemoryManagerResult() const;
//! Controls the system-font fallback policy used by Settings::enableSystemFonts when unset.
//! Enabled by default. Test runners can disable it before constructing any UIApplication.
static void setSystemFontsEnabledByDefault( bool enabled );
static bool systemFontsEnabledByDefault();
String::HashType getStyleSheetDefaultMarker() const { return mStyleSheetMarker; }
protected:
@@ -82,6 +96,7 @@ class EE_API UIApplication {
String::HashType mStyleSheetMarker{ 0 };
bool mDidRun{ false };
bool mShowMemoryManagerResult{ false };
std::unique_ptr<Private::UIApplicationSystemFontState> mSystemFontState;
};
}} // namespace EE::UI

View File

@@ -742,7 +742,7 @@ Glyph FontTrueType::getGlyph( Uint32 codePoint, unsigned int characterSize, bool
}
}
if ( 0 == idx && mEnableSystemFallback && SystemFontResolver::existsSingleton() ) {
if ( 0 == idx && mEnableSystemFallback && SystemFontResolver::isEnabled() ) {
FontDesc fallbackDesc = SystemFontResolver::instance()->getFallbackForCodepoint(
codePoint, FontWeight::Normal, false );
if ( !fallbackDesc.path.empty() ) {
@@ -892,7 +892,7 @@ GlyphDrawable* FontTrueType::getGlyphDrawable( Uint32 codePoint, unsigned int ch
glyphIndex = getGlyphIndex( codePoint );
}
if ( 0 == glyphIndex && mEnableSystemFallback && SystemFontResolver::existsSingleton() ) {
if ( 0 == glyphIndex && mEnableSystemFallback && SystemFontResolver::isEnabled() ) {
FontDesc fallbackDesc = SystemFontResolver::instance()->getFallbackForCodepoint(
codePoint, FontWeight::Normal, false );
if ( !fallbackDesc.path.empty() ) {
@@ -1301,8 +1301,7 @@ Float FontTrueType::getGlyphAdvance( Uint32 codePoint, unsigned int characterSiz
}
}
}
if ( index == 0 && mEnableSystemFallback && mFontService &&
SystemFontResolver::existsSingleton() ) {
if ( index == 0 && mEnableSystemFallback && mFontService && SystemFontResolver::isEnabled() ) {
FontDesc fallbackDesc = SystemFontResolver::instance()->getFallbackForCodepoint(
codePoint, FontWeight::Normal, false );
FontTrueType* fallbackFont =

View File

@@ -23,6 +23,7 @@
#endif
#include <dwrite.h>
#include <dwrite_1.h>
#include <dwrite_2.h>
#include <windows.h>
#include <wrl/client.h>
#pragma comment( lib, "dwrite.lib" )
@@ -36,14 +37,6 @@
#include <pugixml/pugixml.hpp>
#endif
#if EE_PLATFORM == EE_PLATFORM_HAIKU
#include <Directory.h>
#include <Entry.h>
#include <Font.h>
#include <Path.h>
#include <String.h>
#endif
#include <ft2build.h>
#include FT_FREETYPE_H
#include <ft2build.h>
@@ -210,23 +203,31 @@ bool fontHasSvgTable( const std::string& path ) {
namespace EE { namespace Graphics {
static void destroyNativeFontResolverState();
SINGLETON_DECLARE_IMPLEMENTATION( SystemFontResolver )
bool SystemFontResolver::sEnabled = false;
std::atomic<bool> SystemFontResolver::sEnabled{ false };
SystemFontResolver::SystemFontResolver() {}
SystemFontResolver::~SystemFontResolver() {
sEnabled.store( false, std::memory_order_release );
invalidateCache();
destroyNativeFontResolverState();
destroyFTState();
}
void SystemFontResolver::setEnabled( bool enabled ) {
sEnabled = enabled;
// Enabling establishes the invariant used by hot glyph lookup paths: an enabled resolver always
// has an initialized singleton. Font enumeration remains lazy and can be warmed on a worker.
if ( enabled )
instance();
sEnabled.store( enabled, std::memory_order_release );
}
bool SystemFontResolver::isEnabled() {
return sEnabled;
return sEnabled.load( std::memory_order_acquire );
}
void SystemFontResolver::invalidateCache() {
@@ -237,7 +238,7 @@ void SystemFontResolver::invalidateCache() {
mCodepointFallbackCache.clear();
mFontList.clear();
mGenericFallbacks.clear();
mFontListPopulated = false;
mFontListPopulated.store( false, std::memory_order_release );
}
clearFTProbeCache();
}
@@ -259,17 +260,23 @@ GenericFamily SystemFontResolver::genericFamilyFromName( const std::string& name
}
void SystemFontResolver::ensureFontListPopulated() const {
if ( !mFontListPopulated ) {
if ( !mFontListPopulated.load( std::memory_order_acquire ) ) {
Lock lock( mMutex );
if ( !mFontListPopulated ) {
AtomicBoolScopedOp op( mFontListLoading );
if ( !mFontListPopulated.load( std::memory_order_relaxed ) ) {
AtomicBoolScopedOp op( mFontListLoading, true );
populateFontList();
populateGenericFallbacks();
mFontListPopulated = true;
mFontListPopulated.store( true, std::memory_order_release );
}
}
}
void SystemFontResolver::warmUp() const {
Clock c;
ensureFontListPopulated();
Log::info( "SystemFontResolver::warmUp took: %s", c.getElapsedTime().toString() );
}
std::vector<FontDesc> SystemFontResolver::enumerate() {
ensureFontListPopulated();
Lock lock( mMutex );
@@ -474,7 +481,6 @@ FontDesc SystemFontResolver::getFallbackForCodepoint( Uint32 codepoint, FontWeig
ensureFontListPopulated();
const bool isEmoji = Font::isEmojiCodePoint( codepoint );
std::vector<FontDesc> snapshot;
{
Lock lock( mMutex );
@@ -495,8 +501,19 @@ FontDesc SystemFontResolver::getFallbackForCodepoint( Uint32 codepoint, FontWeig
}
}
}
}
snapshot = mFontList;
FontDesc nativeMatch = matchFallbackForCodepoint( codepoint, weight, italic );
if ( !nativeMatch.path.empty() && !( isEmoji && fontHasSvgTable( nativeMatch.path ) ) ) {
Lock lock( mMutex );
mCodepointFallbackCache[codepoint] = nativeMatch.path;
return nativeMatch;
}
static thread_local std::vector<FontDesc> snapshot;
{
Lock lock( mMutex );
snapshot.assign( mFontList.begin(), mFontList.end() );
}
for ( const auto& desc : snapshot ) {
@@ -602,14 +619,164 @@ static std::string wideToUtf8( const WCHAR* wstr ) {
}
static IDWriteFactory* getDWriteFactory() {
static IDWriteFactory* sFactory = nullptr;
static Microsoft::WRL::ComPtr<IDWriteFactory> sFactory;
if ( !sFactory ) {
HRESULT hr = DWriteCreateFactory( DWRITE_FACTORY_TYPE_SHARED, __uuidof( IDWriteFactory ),
reinterpret_cast<IUnknown**>( &sFactory ) );
reinterpret_cast<IUnknown**>( sFactory.GetAddressOf() ) );
if ( FAILED( hr ) )
return nullptr;
}
return sFactory;
return sFactory.Get();
}
class CodepointAnalysisSource final : public IDWriteTextAnalysisSource {
public:
explicit CodepointAnalysisSource( Uint32 codepoint ) {
if ( codepoint <= 0xFFFF ) {
mText[0] = static_cast<WCHAR>( codepoint );
mLength = 1;
} else {
codepoint -= 0x10000;
mText[0] = static_cast<WCHAR>( 0xD800 + ( codepoint >> 10 ) );
mText[1] = static_cast<WCHAR>( 0xDC00 + ( codepoint & 0x3FF ) );
mLength = 2;
}
}
UINT32 length() const { return mLength; }
HRESULT STDMETHODCALLTYPE QueryInterface( REFIID iid, void** object ) override {
if ( !object )
return E_POINTER;
if ( iid == __uuidof( IUnknown ) || iid == __uuidof( IDWriteTextAnalysisSource ) ) {
*object = this;
return S_OK;
}
*object = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override { return 1; }
ULONG STDMETHODCALLTYPE Release() override { return 1; }
HRESULT STDMETHODCALLTYPE GetTextAtPosition( UINT32 position, const WCHAR** text,
UINT32* length ) override {
if ( !text || !length )
return E_POINTER;
if ( position >= mLength ) {
*text = nullptr;
*length = 0;
} else {
*text = mText + position;
*length = mLength - position;
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE GetTextBeforePosition( UINT32 position, const WCHAR** text,
UINT32* length ) override {
if ( !text || !length )
return E_POINTER;
if ( position == 0 || position > mLength ) {
*text = nullptr;
*length = 0;
} else {
*text = mText;
*length = position;
}
return S_OK;
}
DWRITE_READING_DIRECTION STDMETHODCALLTYPE GetParagraphReadingDirection() override {
return DWRITE_READING_DIRECTION_LEFT_TO_RIGHT;
}
HRESULT STDMETHODCALLTYPE GetLocaleName( UINT32 position, UINT32* length,
const WCHAR** localeName ) override {
if ( !length || !localeName )
return E_POINTER;
*length = position < mLength ? mLength - position : 0;
*localeName = L"en-us";
return S_OK;
}
HRESULT STDMETHODCALLTYPE GetNumberSubstitution(
UINT32 position, UINT32* length, IDWriteNumberSubstitution** substitution ) override {
if ( !length || !substitution )
return E_POINTER;
*length = position < mLength ? mLength - position : 0;
*substitution = nullptr;
return S_OK;
}
private:
WCHAR mText[2]{};
UINT32 mLength{ 0 };
};
static FontDesc fontDescFromDWriteFont( IDWriteFont* font, FontWeight weight, bool italic ) {
using Microsoft::WRL::ComPtr;
FontDesc desc;
if ( !font )
return desc;
ComPtr<IDWriteFontFamily> family;
ComPtr<IDWriteLocalizedStrings> familyNames;
if ( FAILED( font->GetFontFamily( &family ) ) || !family ||
FAILED( family->GetFamilyNames( &familyNames ) ) || !familyNames )
return desc;
UINT32 nameLength = 0;
if ( FAILED( familyNames->GetStringLength( 0, &nameLength ) ) )
return desc;
std::wstring familyName( nameLength + 1, L'\0' );
if ( FAILED( familyNames->GetString( 0, &familyName[0], nameLength + 1 ) ) )
return desc;
ComPtr<IDWriteFontFace> face;
if ( FAILED( font->CreateFontFace( &face ) ) || !face )
return desc;
UINT32 fileCount = 0;
if ( FAILED( face->GetFiles( &fileCount, nullptr ) ) || fileCount == 0 )
return desc;
std::vector<IDWriteFontFile*> rawFiles( fileCount, nullptr );
if ( FAILED( face->GetFiles( &fileCount, rawFiles.data() ) ) )
return desc;
for ( UINT32 i = 1; i < fileCount; ++i )
rawFiles[i]->Release();
ComPtr<IDWriteFontFile> fontFile;
fontFile.Attach( rawFiles[0] );
ComPtr<IDWriteFontFileLoader> loader;
ComPtr<IDWriteLocalFontFileLoader> localLoader;
if ( FAILED( fontFile->GetLoader( &loader ) ) || !loader ||
FAILED( loader.As( &localLoader ) ) || !localLoader )
return desc;
const void* key = nullptr;
UINT32 keySize = 0;
if ( FAILED( fontFile->GetReferenceKey( &key, &keySize ) ) )
return desc;
UINT32 pathLength = 0;
if ( FAILED( localLoader->GetFilePathLengthFromKey( key, keySize, &pathLength ) ) )
return desc;
std::wstring path( pathLength + 1, L'\0' );
if ( FAILED( localLoader->GetFilePathFromKey( key, keySize, &path[0], pathLength + 1 ) ) )
return desc;
desc.family = wideToUtf8( familyName.c_str() );
desc.path = wideToUtf8( path.c_str() );
desc.faceIndex = face->GetIndex();
desc.weight = weight;
desc.stretch = static_cast<FontStretch>( static_cast<Uint8>( font->GetStretch() ) );
desc.italic = italic;
desc.monospace = false;
ComPtr<IDWriteFont1> font1;
if ( SUCCEEDED( font->QueryInterface( __uuidof( IDWriteFont1 ), &font1 ) ) && font1 ) {
DWRITE_PANOSE panose;
font1->GetPanose( &panose );
const BYTE* raw = reinterpret_cast<const BYTE*>( &panose );
desc.monospace = raw[0] == 2 && raw[3] == 9;
}
return desc;
}
void SystemFontResolver::populateFontList() const {
@@ -800,6 +967,56 @@ static FontStretch ctWidthToFontStretch( CGFloat width ) {
return FontStretch::UltraExpanded;
}
static FontDesc fontDescFromCTFont( CTFontRef font, FontWeight weight, bool italic ) {
FontDesc desc;
if ( !font )
return desc;
CTFontDescriptorRef descriptor = CTFontCopyFontDescriptor( font );
if ( !descriptor )
return desc;
CFURLRef url =
static_cast<CFURLRef>( CTFontDescriptorCopyAttribute( descriptor, kCTFontURLAttribute ) );
CFRelease( descriptor );
if ( !url )
return desc;
CFStringRef pathRef = CFURLCopyFileSystemPath( url, kCFURLPOSIXPathStyle );
CFRelease( url );
desc.path = cfStringToStd( pathRef );
if ( pathRef )
CFRelease( pathRef );
if ( desc.path.empty() )
return {};
CFStringRef familyRef = CTFontCopyFamilyName( font );
desc.family = cfStringToStd( familyRef );
if ( familyRef )
CFRelease( familyRef );
CFStringRef postScriptNameRef = CTFontCopyPostScriptName( font );
std::string postScriptName = cfStringToStd( postScriptNameRef );
if ( postScriptNameRef )
CFRelease( postScriptNameRef );
std::shared_ptr<FreeTypeState> ftState = getFTState();
if ( !ftState || !ftState->findFaceIndex( desc.path, postScriptName, desc.faceIndex ) )
return {};
CGFloat widthValue = 0.0;
CFDictionaryRef traits = CTFontCopyTraits( font );
if ( traits ) {
CFNumberRef widthNumber =
static_cast<CFNumberRef>( CFDictionaryGetValue( traits, kCTFontWidthTrait ) );
if ( widthNumber )
CFNumberGetValue( widthNumber, kCFNumberCGFloatType, &widthValue );
CFRelease( traits );
}
desc.weight = weight;
desc.stretch = ctWidthToFontStretch( widthValue );
desc.italic = italic;
desc.monospace = ( CTFontGetSymbolicTraits( font ) & kCTFontMonoSpaceTrait ) != 0;
return desc;
}
void SystemFontResolver::populateFontList() const {
CFArrayRef descriptors = CTFontManagerCopyAvailableFontFamilyNames();
if ( !descriptors )
@@ -930,9 +1147,10 @@ void SystemFontResolver::populateFontList() const {
}
// =====================================================================
// Platform: Linux / FreeBSD (Fontconfig — dynamically loaded)
// Platform: Linux / FreeBSD / Haiku (Fontconfig — dynamically loaded)
// =====================================================================
#elif EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD
#elif EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \
EE_PLATFORM == EE_PLATFORM_HAIKU
struct FcLib {
void* handle{ nullptr };
@@ -943,6 +1161,7 @@ struct FcLib {
struct FcConfig;
struct FcPattern;
struct FcCharSet;
struct FcObjectSet;
struct FcFontSet {
int nfont;
@@ -990,6 +1209,13 @@ struct FcLib {
void ( *FontSetDestroy )( FcFontSet* );
FcResult ( *PatternGetString )( const FcPattern*, const char*, int, FcChar8** );
FcResult ( *PatternGetInteger )( const FcPattern*, const char*, int, int* );
FcCharSet* ( *CharSetCreate )( void );
void ( *CharSetDestroy )( FcCharSet* );
FcBool ( *CharSetAddChar )( FcCharSet*, Uint32 );
FcBool ( *PatternAddCharSet )( FcPattern*, const char*, const FcCharSet* );
FcBool ( *ConfigSubstitute )( FcConfig*, FcPattern*, int );
void ( *DefaultSubstitute )( FcPattern* );
FcPattern* ( *FontMatch )( FcConfig*, FcPattern*, FcResult* );
bool load() {
handle = Sys::loadObject( "libfontconfig.so.1" );
@@ -1017,10 +1243,24 @@ struct FcLib {
(decltype( PatternGetString ))Sys::loadFunction( handle, "FcPatternGetString" );
PatternGetInteger =
(decltype( PatternGetInteger ))Sys::loadFunction( handle, "FcPatternGetInteger" );
CharSetCreate = (decltype( CharSetCreate ))Sys::loadFunction( handle, "FcCharSetCreate" );
CharSetDestroy =
(decltype( CharSetDestroy ))Sys::loadFunction( handle, "FcCharSetDestroy" );
CharSetAddChar =
(decltype( CharSetAddChar ))Sys::loadFunction( handle, "FcCharSetAddChar" );
PatternAddCharSet =
(decltype( PatternAddCharSet ))Sys::loadFunction( handle, "FcPatternAddCharSet" );
ConfigSubstitute =
(decltype( ConfigSubstitute ))Sys::loadFunction( handle, "FcConfigSubstitute" );
DefaultSubstitute =
(decltype( DefaultSubstitute ))Sys::loadFunction( handle, "FcDefaultSubstitute" );
FontMatch = (decltype( FontMatch ))Sys::loadFunction( handle, "FcFontMatch" );
return InitLoadConfigAndFonts && ConfigDestroy && Fini && PatternCreate &&
ObjectSetCreate && ObjectSetAdd && FontList && PatternDestroy && ObjectSetDestroy &&
FontSetDestroy && PatternGetString && PatternGetInteger;
FontSetDestroy && PatternGetString && PatternGetInteger && CharSetCreate &&
CharSetDestroy && CharSetAddChar && PatternAddCharSet && ConfigSubstitute &&
DefaultSubstitute && FontMatch;
}
void finish( FcConfig* config ) {
@@ -1037,6 +1277,37 @@ struct FcLib {
}
};
struct FontconfigState {
FcLib fc;
FcLib::FcConfig* config{ nullptr };
Mutex mutex;
FontconfigState() {
if ( fc.load() )
config = fc.InitLoadConfigAndFonts();
}
~FontconfigState() {
Lock lock( mutex );
if ( fc.handle ) {
fc.finish( config );
fc.unload();
}
}
bool ready() const { return config != nullptr; }
};
Mutex sFontconfigStateMutex;
std::shared_ptr<FontconfigState> sFontconfigState;
static std::shared_ptr<FontconfigState> getFontconfigState() {
Lock lock( sFontconfigStateMutex );
if ( !sFontconfigState )
sFontconfigState = std::make_shared<FontconfigState>();
return sFontconfigState;
}
#define FC_W( v ) FcLib::FC_WEIGHT_##v
#define FC_S( v ) FcLib::FC_SLANT_##v
#define FC_WI( v ) FcLib::FC_WIDTH_##v
@@ -1082,18 +1353,14 @@ static FontStretch fcWidthToFontStretch( int fcWidth ) {
}
void SystemFontResolver::populateFontList() const {
FcLib fc;
if ( !fc.load() ) {
populateFontListFallback();
return;
}
FcLib::FcConfig* config = fc.InitLoadConfigAndFonts();
if ( !config ) {
fc.finish( nullptr );
fc.unload();
std::shared_ptr<FontconfigState> state = getFontconfigState();
if ( !state || !state->ready() ) {
populateFontListFallback();
return;
}
Lock stateLock( state->mutex );
FcLib& fc = state->fc;
FcLib::FcConfig* config = state->config;
FcLib::FcPattern* pattern = fc.PatternCreate();
FcLib::FcObjectSet* os = fc.ObjectSetCreate();
@@ -1102,8 +1369,6 @@ void SystemFontResolver::populateFontList() const {
fc.PatternDestroy( pattern );
if ( os )
fc.ObjectSetDestroy( os );
fc.finish( config );
fc.unload();
populateFontListFallback();
return;
}
@@ -1115,8 +1380,6 @@ void SystemFontResolver::populateFontList() const {
if ( !objectSetOk ) {
fc.PatternDestroy( pattern );
fc.ObjectSetDestroy( os );
fc.finish( config );
fc.unload();
populateFontListFallback();
return;
}
@@ -1127,8 +1390,6 @@ void SystemFontResolver::populateFontList() const {
fc.ObjectSetDestroy( os );
if ( !fontSet ) {
fc.finish( config );
fc.unload();
populateFontListFallback();
return;
}
@@ -1172,8 +1433,6 @@ void SystemFontResolver::populateFontList() const {
}
fc.FontSetDestroy( fontSet );
fc.finish( config );
fc.unload();
}
// =====================================================================
@@ -1245,15 +1504,6 @@ void SystemFontResolver::populateFontList() const {
}
}
// =====================================================================
// Platform: Haiku (Fallback)
// =====================================================================
#elif EE_PLATFORM == EE_PLATFORM_HAIKU
void SystemFontResolver::populateFontList() const {
return populateFontListFallback();
}
// =====================================================================
// Platform: Emscripten / Unknown (no system font access)
// =====================================================================
@@ -1263,6 +1513,144 @@ void SystemFontResolver::populateFontList() const {}
#endif
FontDesc SystemFontResolver::matchFallbackForCodepoint( Uint32 codepoint, FontWeight weight,
bool italic ) const {
#if EE_PLATFORM == EE_PLATFORM_WIN
using Microsoft::WRL::ComPtr;
IDWriteFactory* factory = getDWriteFactory();
if ( !factory )
return {};
ComPtr<IDWriteFactory2> factory2;
if ( FAILED( factory->QueryInterface( __uuidof( IDWriteFactory2 ),
reinterpret_cast<void**>( factory2.GetAddressOf() ) ) ) ||
!factory2 )
return {};
ComPtr<IDWriteFontFallback> fallback;
if ( FAILED( factory2->GetSystemFontFallback( &fallback ) ) || !fallback )
return {};
CodepointAnalysisSource source( codepoint );
UINT32 mappedLength = 0;
FLOAT scale = 1.f;
ComPtr<IDWriteFont> mappedFont;
HRESULT result =
fallback->MapCharacters( &source, 0, source.length(), nullptr, nullptr,
static_cast<DWRITE_FONT_WEIGHT>( static_cast<Uint16>( weight ) ),
italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL, &mappedLength, &mappedFont, &scale );
if ( FAILED( result ) || !mappedFont || mappedLength < source.length() )
return {};
return fontDescFromDWriteFont( mappedFont.Get(), weight, italic );
#elif EE_PLATFORM == EE_PLATFORM_MACOS || EE_PLATFORM == EE_PLATFORM_IOS
if ( codepoint > 0x10FFFF || ( codepoint >= 0xD800 && codepoint <= 0xDFFF ) )
return {};
UniChar characters[2];
CFIndex length = 1;
if ( codepoint <= 0xFFFF ) {
characters[0] = static_cast<UniChar>( codepoint );
} else {
codepoint -= 0x10000;
characters[0] = static_cast<UniChar>( 0xD800 + ( codepoint >> 10 ) );
characters[1] = static_cast<UniChar>( 0xDC00 + ( codepoint & 0x3FF ) );
length = 2;
}
CFStringRef text = CFStringCreateWithCharacters( kCFAllocatorDefault, characters, length );
if ( !text )
return {};
CTFontRef baseFont = CTFontCreateWithName( CFSTR( "Helvetica" ), 12.0, nullptr );
if ( !baseFont ) {
CFRelease( text );
return {};
}
CTFontSymbolicTraits desiredTraits = 0;
if ( weight >= FontWeight::Bold )
desiredTraits |= kCTFontBoldTrait;
if ( italic )
desiredTraits |= kCTFontItalicTrait;
CTFontRef styledFont = CTFontCreateCopyWithSymbolicTraits(
baseFont, 0.0, nullptr, desiredTraits, kCTFontBoldTrait | kCTFontItalicTrait );
CTFontRef fallbackFont =
CTFontCreateForString( styledFont ? styledFont : baseFont, text, CFRangeMake( 0, length ) );
FontDesc desc = fontDescFromCTFont( fallbackFont, weight, italic );
if ( fallbackFont )
CFRelease( fallbackFont );
if ( styledFont )
CFRelease( styledFont );
CFRelease( baseFont );
CFRelease( text );
return desc;
#elif EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \
EE_PLATFORM == EE_PLATFORM_HAIKU
// Fontconfig caches charset coverage, so ask it to match the codepoint instead of opening every
// font file with FreeType. Its dynamically loaded API uses process-global internals.
Lock lock( mMutex );
std::shared_ptr<FontconfigState> state = getFontconfigState();
if ( !state || !state->ready() )
return {};
Lock stateLock( state->mutex );
FcLib& fc = state->fc;
FcLib::FcConfig* config = state->config;
FcLib::FcPattern* pattern = fc.PatternCreate();
FcLib::FcCharSet* charset = pattern ? fc.CharSetCreate() : nullptr;
FontDesc desc;
if ( charset && fc.CharSetAddChar( charset, codepoint ) &&
fc.PatternAddCharSet( pattern, "charset", charset ) &&
fc.ConfigSubstitute( config, pattern, 0 ) ) {
fc.DefaultSubstitute( pattern );
FcLib::FcResult matchResult{};
FcLib::FcPattern* match = fc.FontMatch( config, pattern, &matchResult );
if ( match && matchResult == FcLib::FcResultMatch ) {
FcLib::FcChar8* family = nullptr;
FcLib::FcChar8* file = nullptr;
if ( fc.PatternGetString( match, "family", 0, &family ) == FcLib::FcResultMatch &&
family && fc.PatternGetString( match, "file", 0, &file ) == FcLib::FcResultMatch &&
file ) {
int fcIndex = 0;
int fcWidth = FC_WI( NORMAL );
int fcSpacing = FcLib::FC_PROPORTIONAL;
fc.PatternGetInteger( match, "index", 0, &fcIndex );
fc.PatternGetInteger( match, "width", 0, &fcWidth );
fc.PatternGetInteger( match, "spacing", 0, &fcSpacing );
desc.family = reinterpret_cast<const char*>( family );
desc.path = reinterpret_cast<const char*>( file );
desc.faceIndex = fcIndex >= 0 ? static_cast<Uint32>( fcIndex & 0xFFFF ) : 0;
desc.weight = weight;
desc.stretch = fcWidthToFontStretch( fcWidth );
desc.italic = italic;
desc.monospace = fcSpacing == FcLib::FC_MONO;
}
}
if ( match )
fc.PatternDestroy( match );
}
if ( charset )
fc.CharSetDestroy( charset );
if ( pattern )
fc.PatternDestroy( pattern );
return desc;
#else
(void)codepoint;
(void)weight;
(void)italic;
return {};
#endif
}
static void destroyNativeFontResolverState() {
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \
EE_PLATFORM == EE_PLATFORM_HAIKU
std::shared_ptr<FontconfigState> state;
{
Lock lock( sFontconfigStateMutex );
state = std::move( sFontconfigState );
}
state.reset();
#endif
}
void SystemFontResolver::populateFontListFallback() const {
// Added Haiku font paths so testing this fallback on Haiku actually finds files
static const char* fontDirs[] = { "/usr/share/fonts",

View File

@@ -1,8 +1,10 @@
#include <eepp/graphics/fontfamily.hpp>
#include <eepp/graphics/fontservice.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/systemfontresolver.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/thread.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uitheme.hpp>
@@ -11,6 +13,7 @@
#include <eepp/window/engine.hpp>
#include <eepp/window/input.hpp>
#include <atomic>
#include <iostream>
using namespace EE::Graphics;
@@ -19,8 +22,31 @@ using namespace EE::Scene;
namespace EE { namespace UI {
namespace Private {
class UIApplicationSystemFontState {
public:
UIApplicationSystemFontState() :
warmUpThread( [] { SystemFontResolver::instance()->warmUp(); } ) {
warmUpThread.launch();
}
Thread warmUpThread;
};
} // namespace Private
static std::atomic<bool> sSystemFontsEnabledByDefault{ true };
UIApplication::UIApplication( const WindowSettings& windowSettings, const Settings& appSettings,
const ContextSettings& contextSettings ) {
const bool enableSystemFonts = appSettings.enableSystemFonts.value_or(
sSystemFontsEnabledByDefault.load( std::memory_order_acquire ) );
if ( enableSystemFonts ) {
SystemFontResolver::setEnabled( true );
mSystemFontState = std::make_unique<Private::UIApplicationSystemFontState>();
}
DisplayManager* displayManager = Engine::instance()->getDisplayManager();
displayManager->enableScreenSaver();
displayManager->enableMouseFocusClickThrough();
@@ -113,6 +139,7 @@ UIApplication::UIApplication( const WindowSettings& windowSettings, const Settin
}
UIApplication::~UIApplication() {
mSystemFontState.reset();
Engine::destroySingleton();
if ( mShowMemoryManagerResult )
MemoryManager::showResults();
@@ -167,4 +194,12 @@ bool UIApplication::showMemoryManagerResult() const {
return mShowMemoryManagerResult;
}
void UIApplication::setSystemFontsEnabledByDefault( bool enabled ) {
sSystemFontsEnabledByDefault.store( enabled, std::memory_order_release );
}
bool UIApplication::systemFontsEnabledByDefault() {
return sSystemFontsEnabledByDefault.load( std::memory_order_acquire );
}
}} // namespace EE::UI

View File

@@ -8,7 +8,6 @@ EE_MAIN_FUNC int main( int argc, char** argv ) {
std::shared_ptr<ThreadPool> threadPool(
ThreadPool::createShared( eemax<int>( 4, Sys::getCPUCount() ) ) );
Http::setThreadPool( threadPool );
SystemFontResolver::setEnabled( true );
args::ArgumentParser parser( "eepp HTML Example" );
args::HelpFlag help( parser, "help", "Display this help menu", { 'h', "help" } );

View File

@@ -13,6 +13,7 @@
#include <eepp/graphics/renderer/renderergl.hpp>
#include <eepp/graphics/resourcescope.hpp>
#include <eepp/graphics/richtext.hpp>
#include <eepp/graphics/systemfontresolver.hpp>
#include <eepp/graphics/text.hpp>
#include <eepp/graphics/texturefactory.hpp>
#include <eepp/scene/scenemanager.hpp>
@@ -350,6 +351,37 @@ UTEST( FontRendering, fontFeaturesStringConversion ) {
}
#ifdef EE_TEXT_SHAPER_ENABLED
UTEST( FontRendering, shapedTextUsesSystemFallbackForCommonSymbols ) {
UIApplication app(
WindowSettings( 320, 240, "eepp - Shaped System Font Fallback Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
ResourceScope& scope = *app.getUI()->getResourceScope();
FontTrueTypePtr font = FontTrueType::New( "SystemFallbackSymbols-Regular", scope );
ASSERT_TRUE(
font->loadFromFile( Sys::getProcessPath() + "../assets/fonts/NotoSans-Regular.ttf" ) );
SystemFontResolver::setEnabled( true );
SystemFontResolver::instance()->warmUp();
BoolScopedOp shaperEnabled( Text::TextShaperEnabled, true );
BoolScopedOp shaperOptimizations( Text::TextShaperOptimizations, false );
const String symbols( "⬢ ⑂ ⟲" );
TextLayout::Cache layout =
TextLayout::layout( symbols, font.get(), 24, Text::Regular, 4, 0, {}, 0 );
ASSERT_EQ( 1u, layout->paragraphs.size() );
ASSERT_EQ( symbols.size(), layout->paragraphs.front().shapedGlyphs.size() );
for ( const ShapedGlyph& glyph : layout->paragraphs.front().shapedGlyphs ) {
const Uint32 codepoint = symbols[glyph.stringIndex];
if ( codepoint == ' ' )
continue;
EXPECT_NE( 0u, glyph.glyphIndex );
EXPECT_NE( font.get(), glyph.font );
}
SystemFontResolver::setEnabled( false );
}
UTEST( FontRendering, latinOpenTypeFeaturesAreExplicitAndCachedByTextHints ) {
UIApplication app(
WindowSettings( 320, 240, "eepp - Latin Ligatures Test", WindowStyle::Default,

View File

@@ -2,10 +2,12 @@
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/ui/uiapplication.hpp>
UTEST_STATE();
EE_MAIN_FUNC int main( int argc, char* argv[] ) {
EE::System::FileSystem::changeWorkingDirectory( EE::System::Sys::getProcessPath() );
EE::UI::UIApplication::setSystemFontsEnabledByDefault( false );
return utest_main( argc, argv );
}

View File

@@ -5,6 +5,7 @@
#include <eepp/graphics/systemfontresolver.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/system/thread.hpp>
#if EE_PLATFORM == EE_PLATFORM_LINUX
#include <dirent.h>
@@ -35,7 +36,13 @@ static std::size_t getOpenFileDescriptorCount() {
#endif
UTEST( SystemFontResolver, singletonLifecycle ) {
SystemFontResolver::destroySingleton();
SystemFontResolver::setEnabled( false );
UTEST_PRINT_STEP( "Enabling creates singleton" );
SystemFontResolver::setEnabled( true );
EXPECT_TRUE( SystemFontResolver::existsSingleton() != nullptr );
UTEST_PRINT_STEP( "Create singleton" );
auto* resolver = SystemFontResolver::createSingleton();
EXPECT_TRUE( resolver != nullptr );
@@ -54,6 +61,43 @@ UTEST( SystemFontResolver, singletonLifecycle ) {
SystemFontResolver::setEnabled( false );
}
UTEST( SystemFontResolver, workerWarmUp ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
Thread warmUpThread( [resolver] { resolver->warmUp(); } );
warmUpThread.launch();
warmUpThread.wait();
EXPECT_FALSE( resolver->isLoading() );
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \
EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_MACOS || \
EE_PLATFORM == EE_PLATFORM_IOS || EE_PLATFORM == EE_PLATFORM_HAIKU
EXPECT_FALSE( resolver->enumerate().empty() );
#endif
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, fallbackWaitsForConcurrentWarmUp ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
resolver->invalidateCache();
Thread warmUpThread( [resolver] { resolver->warmUp(); } );
warmUpThread.launch();
FontDesc fallback = resolver->getFallbackForCodepoint( 'A', FontWeight::Normal, false );
warmUpThread.wait();
EXPECT_FALSE( resolver->isLoading() );
EXPECT_FALSE( fallback.path.empty() );
EXPECT_FALSE( resolver->enumerate().empty() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, genericFamilyFromName ) {
SystemFontResolver::setEnabled( true );
EXPECT_EQ( GenericFamily::Serif, SystemFontResolver::genericFamilyFromName( "serif" ) );
@@ -83,8 +127,9 @@ UTEST( SystemFontResolver, enumerate ) {
const auto& fonts = resolver->enumerate();
UTEST_PRINT_INFO( String::format( "Enumerated %zu system fonts", fonts.size() ).c_str() );
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD
EXPECT_TRUE_MSG( fonts.size() > 0, "Fontconfig should find fonts on Linux/BSD" );
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \
EE_PLATFORM == EE_PLATFORM_HAIKU
EXPECT_TRUE_MSG( fonts.size() > 0, "Fontconfig should find fonts on Linux/BSD/Haiku" );
#elif EE_PLATFORM == EE_PLATFORM_WIN
EXPECT_TRUE_MSG( fonts.size() > 0, "DirectWrite should find fonts on Windows" );
#elif EE_PLATFORM == EE_PLATFORM_MACOS || EE_PLATFORM == EE_PLATFORM_IOS
@@ -147,7 +192,8 @@ UTEST( SystemFontResolver, findVerdana ) {
}
#endif
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \
EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_MACOS
EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_MACOS || \
EE_PLATFORM == EE_PLATFORM_HAIKU
if ( resolver->enumerate().size() > 0 ) {
if ( !desc.path.empty() ) {
EXPECT_STDSTREQ( "Verdana", desc.family );

View File

@@ -1044,7 +1044,10 @@ App::App( const size_t& jobs, const std::vector<std::string>& args ) :
ThreadPool::createShared( jobs > 0 ? jobs : eemax<int>( 4, Sys::getCPUCount() ) ) ),
mDateTimeController( std::make_unique<DateTimeController>( this ) ),
mFontPickerController( std::make_unique<FontPickerController>( this ) ),
mSettingsActions( std::make_unique<SettingsActions>( this ) ) {}
mSettingsActions( std::make_unique<SettingsActions>( this ) ) {
if ( SystemFontResolver::isEnabled() )
mThreadPool->run( [] { SystemFontResolver::instance()->warmUp(); } );
}
App::~App() {
appInstance = nullptr;
@@ -5379,6 +5382,8 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
return EXIT_FAILURE;
}
SystemFontResolver::setEnabled( true );
if ( convertLangPath && !convertLangPath.Get().empty() ) {
Sys::windowAttachConsole();
IOStreamFile sfile( convertLangPath.Get() );

View File

@@ -243,6 +243,8 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
return EXIT_FAILURE;
}
SystemFontResolver::setEnabled( true );
DisplayManager* displayManager = Engine::instance()->getDisplayManager();
Display* currentDisplay = displayManager->getDisplayIndex( 0 );
@@ -273,6 +275,13 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
return EXIT_SUCCESS;
}
std::unique_ptr<Thread> systemFontWarmUp;
if ( SystemFontResolver::isEnabled() ) {
systemFontWarmUp =
std::make_unique<Thread>( [] { SystemFontResolver::instance()->warmUp(); } );
systemFontWarmUp->launch();
}
displayManager->enableScreenSaver();
displayManager->enableMouseFocusClickThrough();
displayManager->disableBypassCompositor();
@@ -349,6 +358,7 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
win->showMessageBox( EE::Window::Window::MessageBoxType::Error, "eterm",
"Operating System not supported." );
terminal.reset();
systemFontWarmUp.reset();
Engine::destroySingleton();
MemoryManager::showResults();
return EXIT_FAILURE;
@@ -473,6 +483,7 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
}
terminal.reset();
systemFontWarmUp.reset();
Engine::destroySingleton();