diff --git a/README.md b/README.md index bf3283c1..abcc9074 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,10 @@ Label 一直是项目优化的最难点,因为它完全不能和其它的渲 ![demo3](/docs/static/demo-imgs/demo3.png) +除了以上优化之外,在 v3.x 版本中,还支持了**文本测量值烘焙**和**预加载 Label Canvas 和 `Char` 图集**。 + +这两项优化可大幅降低文本渲染在低端设备上的性能消耗,使游戏界面打开时不再卡顿。 + ### Spine 组件增强 除了 Label 之外,如果你的项目用到了 Spine 组件,那么它大概率会成为项目第二个优化难点。 @@ -96,6 +100,16 @@ Label 一直是项目优化的最难点,因为它完全不能和其它的渲 现在在满足条件的情况下可以**复用 Culling 数据,以减少项目 CPU 的性能消耗**。 +### 性能指示器增强 + +社区版优化了引擎自带的性能指示器,增加了三个重要的性能指标: + +- Label Canvas(Label 组件的 Canvas 数量) +- Char Atlas(Char 字符图集使用情况) +- Dynamic Atlas(动态图集使用情况) + +![demo6](/docs/static/demo-imgs/demo6.png) + ### 多线程支持 **现在,以下引擎的部分增加了多线程支持:** diff --git a/docs/docs/best-practices/new-features.md b/docs/docs/best-practices/new-features.md index 2583a0bc..22cb3aa5 100644 --- a/docs/docs/best-practices/new-features.md +++ b/docs/docs/best-practices/new-features.md @@ -36,3 +36,13 @@ skeletonComponent.setRegionData('Head', 'Head', new sp.RegionData(spriteFrame)); ## 支持 RichText 自定义材质 可前往 [RichText 自定义材质](../user-guide/text-render/text-richtext.md) 文档了解更多详情。 + +## 性能指示器增强 + +优化了引擎自带的性能指示器,增加了三个重要的性能指标: + +- Label Canvas(Label 组件的 Canvas 数量) +- Char Atlas(Char 字符图集使用情况) +- Dynamic Atlas(动态图集使用情况) + +![demo6](/demo-imgs/demo6.png) diff --git a/docs/docs/best-practices/performance-guide.md b/docs/docs/best-practices/performance-guide.md index 226d15a1..9ae3bab4 100644 --- a/docs/docs/best-practices/performance-guide.md +++ b/docs/docs/best-practices/performance-guide.md @@ -84,6 +84,14 @@ cc.dynamicAtlasManager.maxFrameSize = 1024; // 推荐 512、1024 甚至 2048 这是引擎原本的限制,我们未对其进行修复,原因是我们认为 8 张数量已经够多了,8 张都用完的情况大部分是没有合理搭配使用两种缓存模式。 +## 进行文本渲染烘焙 + +在之前,即使没有 Drawcall 高的问题,引擎文本渲染本身的性能消耗也并不低。 + +为此,社区版新增了烘焙的相关接口,你可以通过这些接口将文本渲染的性能提升 10 倍以上! + +可前往 [文本渲染烘焙](../user-guide/text-render/text-baking.md) 文档了解更多详情。 + ## 启用 Spine 合批 Spine 组件现在不仅可以参与动态合图,还能与其他渲染组件进行合批。 diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 51262f1a..730a4035 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -80,6 +80,10 @@ Label 一直是项目优化的最难点,因为它完全不能和其它的渲 ![demo3](/demo-imgs/demo3.png) +除了以上优化之外,在 v3.x 版本中,还支持了**文本测量值烘焙**和**预加载 Label Canvas 和 `Char` 图集**。 + +这两项优化可大幅降低文本渲染在低端设备上的性能消耗,使游戏界面打开时不再卡顿。 + ### Spine 组件增强 除了 Label 之外,如果你的项目用到了 Spine 组件,那么它大概率会成为项目第二个优化难点。 @@ -94,6 +98,16 @@ Label 一直是项目优化的最难点,因为它完全不能和其它的渲 现在在满足条件的情况下可以复用 Culling 数据,以减少项目 CPU 的性能消耗。 +### 性能指示器增强 + +社区版优化了引擎自带的性能指示器,增加了三个重要的性能指标: + +- Label Canvas(Label 组件的 Canvas 数量) +- Char Atlas(Char 字符图集使用情况) +- Dynamic Atlas(动态图集使用情况) + +![demo6](/demo-imgs/demo6.png) + ### 多线程支持 **现在,以下引擎的部分增加了多线程支持:** diff --git a/docs/docs/user-guide/profiler/_category_.json b/docs/docs/user-guide/profiler/_category_.json new file mode 100644 index 00000000..5de7be48 --- /dev/null +++ b/docs/docs/user-guide/profiler/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Profiler", + "position": 7, + "collapsed": true, + "link": { + "type": "doc", + "id": "profiler-intro" + } +} diff --git a/docs/docs/user-guide/profiler/profiler-features.md b/docs/docs/user-guide/profiler/profiler-features.md new file mode 100644 index 00000000..d194b351 --- /dev/null +++ b/docs/docs/user-guide/profiler/profiler-features.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 1 +description: "监测更多性能数据。" +--- + +# 新性能指标 + +通过宏启用后,如下图所示: + +![demo6](/demo-imgs/demo6.png) + +会新增以下几个非常有用的指标: + +**Label Canvas(Label 组件的 Canvas 数量)** + +- 指标:(用量) / (总量) +- 用量:指正在使用的 Canvas 对象数量,一般每个非 CHAR 模式的 Label 都会使用一个 Canvas 对象,直到被销毁。 +- 总量:指引擎内部维护的 Canvas 池中 Canvas 对象的总数量。 + +**Char Atlas(Char 字符图集使用情况)** + +- 指标:(用量) / (缓存数量) / (总量) +- 用量:指正在占用的像素数量,注意这是通过每块区域的像素占比进行估算得出,会存在小数点,比如 2.8 则表示占用了 2.8 张图集大小。 +- 缓存数量:计算方式与用量相同,但包括无引用的字符区域,这意味着随时可能被释放。 +- 总量:指引擎已创建的字符图集数量。 + +**Dynamic Atlas(动态图集使用情况)** + +- 指标:(用量) / (当前数量) / (最大数量) +- 用量:指正在占用的像素数量,注意这是通过每块区域的像素占比进行估算得出,会存在小数点,比如 2.8 则表示占用了 2.8 张图集大小。 +- 当前数量:指引擎已创建的字符图集数量。 +- 最大数量:指当前设置的 `maxAtlasCount` 值。 diff --git a/docs/docs/user-guide/profiler/profiler-intro.mdx b/docs/docs/user-guide/profiler/profiler-intro.mdx new file mode 100644 index 00000000..995c4313 --- /dev/null +++ b/docs/docs/user-guide/profiler/profiler-intro.mdx @@ -0,0 +1,14 @@ +import DocCardList from '@theme/DocCardList'; +import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; + +# Profiler + +社区版对引擎自带的 Profiler 进行了一些改进,默认情况下是禁用的,可通过宏来开启: + +``` +cc.macro.ENABLE_CUSTOM_PROFILER = true; +``` + +你可以阅读下面的文档了解详情: + + diff --git a/docs/docs/user-guide/text-render/assets/after-baking.png b/docs/docs/user-guide/text-render/assets/after-baking.png new file mode 100644 index 00000000..db53c747 Binary files /dev/null and b/docs/docs/user-guide/text-render/assets/after-baking.png differ diff --git a/docs/docs/user-guide/text-render/assets/before-baking.png b/docs/docs/user-guide/text-render/assets/before-baking.png new file mode 100644 index 00000000..7748c7e4 Binary files /dev/null and b/docs/docs/user-guide/text-render/assets/before-baking.png differ diff --git a/docs/docs/user-guide/text-render/text-baking.md b/docs/docs/user-guide/text-render/text-baking.md new file mode 100644 index 00000000..423a426d --- /dev/null +++ b/docs/docs/user-guide/text-render/text-baking.md @@ -0,0 +1,187 @@ +--- +sidebar_position: 4 +description: "详细了解如何通过烘焙提升运行时文本渲染性能。" +--- +# 文本渲染烘焙 + +社区版为引擎增加了文本渲染烘焙的能力。 + +以某个项目的界面打开操作为例,通过分析 CPU Profile 可以看到,当界面节点激活时,绝大部分的耗时都来自于文本渲染函数(_applyFontTexture)。 + +![before baking](./assets/before-baking.png) + +但这个界面的文本量并不多,在低端机上的耗时却有 1.06s。 + +通过文本渲染烘焙优化后,耗时降至 60ms! + +![after baking](./assets/after-baking.png) + +文本渲染烘焙分为三个部分: + +- **预加载 Label Canvas** +- **烘焙文本测量值** +- **预加载 `Char` 图集** + +我们将逐个说明。 + +## 预加载 Label Canvas + +引擎通过离屏 Canvas 渲染文本,内部维护着一个 Canvas 池以作复用。 + +每个非 Char 模式的 Label 都会占用一个 Canvas 对象,直到 Label 销毁才会将 Canvas 对象放回池中。 + +默认情况下,Canvas 池不预先创建对象,而是在使用时进行创建,在部分低端设备上,这会增加文本渲染时的卡顿。 + +并且该 Canvas 池默认最大缓存数量为 32,超过该数量的对象不会放入池中而是直接销毁,这也导致大量使用 Label 的项目可能会因为持续创建 Canvas 而造成卡顿。 + +现在,社区版开放了这部分的接口,可通过 `cc.Label._canvasPool` 访问该对象池。 + +可以调整最大缓存数量: + +```ts +cc.Label._canvasPool.max = 64; +``` + +可以预先缓存 Canvas 对象: + +```ts +cc.Label._canvasPool.cache(Number.MAX_SAFE_INTEGER); +``` + +预先缓存接口需传入要缓存的对象数量,无论如何,实际缓存的对象数量都不会超过设置的最大数量。 + +## 烘焙文本测量值 + +引擎内部在渲染文本前需要先测量字符的宽高,这是通过 Canvas 上的 `measureText` 接口实现的。 + +如果你对性能进行了分析,那么可以看出,该接口占据单次文本渲染总耗时的 80% 以上。 + +引擎会对此接口的结果进行缓存,相同样式的文本会从缓存中读取,避免了部分性能消耗。 + +但默认情况下,该缓存仅会保留最后 100 次的测量值,也就是说,如果所有 Label 使用 Char 模式渲染,在渲染 100 个不同的字符后,缓存就会被逐个移除,测量值又会有大量性能消耗。 + +所以,社区版对这部分接口进行了优化,开放了 `cc.textUtils` 的部分接口来控制测量值缓存。 + +首先,你可以调整缓存容量,容量越大,就能避免更多测量值的性能消耗,但内存占用会上升,请视情况而定: + +```ts +cc.textUtils.measureCache.limit = Number.MAX_SAFE_INTEGER; +``` + +其次,我们开放了操作缓存的接口,意味着你可以烘焙测量值,将测量值数据保存到项目中,完全避免测量值的性能消耗! + +获取与应用缓存对象: + +```ts +// 获取 +const data = cc.textUtils.getMeasureCache(); + +// 应用 +cc.textUtils.applyMeasureCache(data); +``` + +我们推荐的用法是: + +- 在开发测试的版本中收集缓存数据并序列化到项目的 JSON 文件中(因为收集会有性能消耗,所以线上版本不进行收集) +- 在游戏启动前应用缓存数据 + +**注意** + +经过我们的测试,同个字符在微信小游戏平台与 Web 端得出的测量值是一致的。 + +但可能会出现其它不同平台或者设备对同个字符的测量值是不一致的,建议针对这种情况烘焙多个 JSON 文件并分别应用即可。 + +## 预加载 `Char` 图集 + +建议先阅读 [烘焙文本测量值](#烘焙文本测量值),再阅读本节内容。 + +如果你对性能进行了分析,那么可以看出,即使我们烘焙了文本测量值,依旧会有将文本绘制到纹理上的性能消耗,那么这剩下 20% 的性能消耗如果无法避免,能控制它消耗的时机也是很有意义的。 + +所以,社区版提供了 Char 图集的预加载接口,我们可以在特定时机(比如游戏启动时)就加载好会用到的所有字符,避免游戏中的卡顿。 + +引擎的 Char 图集只会在用到时才会创建,所以我们在调用任何接口前需要初始化: + +```ts +cc.Label.LetterAtlases.init(); +``` + +由于引擎并不会持有每个字符信息的缓存,所以需要通过社区版新增的开启缓存接口来保留字符信息: + +```ts +cc.Label._shareAtlas.enableLetterCache = true; +``` + +和烘焙文本测量值一样,我们只推荐在开发测试版本收集缓存,因为保留缓存和收集操作都会有性能消耗。 + +以下两个接口可以获取与应用字符缓存: + +```ts +// 获取 +const data = cc.Label._shareAtlas.getLetterCache(); + +// 应用 +cc.Label._shareAtlas.applyLetterCache(data); +``` + +需要注意的是,我们只是将渲染过的字符信息缓存下来,而不是纹理本身,然后在特定时机调用接口来批量渲染字符到纹理上。 + +并且,这里的缓存信息已经包括了每个字符的测量值,所以如果你的项目只使用 `CHAR` 模式的 Label 组件,那么你不用再单独烘焙文本测量值。 + +## 实际应用 + +你可以参考以下代码,在游戏开发测试版本中将缓存数据上传至一个后端服务器。 + +```ts +// 上报 Label 烘焙信息 +if (!CC_EDITOR && game.TEST) { + cc.director.once(cc.Director.EVENT_AFTER_SCENE_LAUNCH, () => { + // 启用缓存 + cc.Label._shareAtlas.enableLetterCache = true; + + setInterval(() => { + const unCaches = {}; + for (const key in cc.textUtils.measureCache.datas) { + if (!(key in _measureCache)) { + unCaches[key] = cc.textUtils.measureCache.datas[key].value; + _measureCache[key] = unCaches[key]; + console.warn("未收集的测量值:", key, unCaches[key]); + } + } + + if (Object.keys(unCaches).length > 0) { + sendToServer(JSON.stringify(unCaches)); + } + + const unLetters = {}; + for (const key in cc.Label._shareAtlas.letterCache) { + if (!(key in _letterCache)) { + unLetters[key] = cc.Label._shareAtlas.letterCache[key]; + _letterCache[key] = unLetters[key]; + console.warn("未收集的 Char 字符:", key, unLetters[key]); + } + } + + if (Object.keys(unLetters).length > 0) { + sendToServer(JSON.stringify(unLetters)); + } + }, 10000); + }); +} +``` + +然后在每个版本上线前,从后端下载缓存数据到项目中,并用代码读取并在游戏开始前应用: + +```ts +cc.director.once(cc.Director.EVENT_AFTER_SCENE_LAUNCH, () => { + // 提前缓存 Label Canvas + cc.Label._canvasPool.cache(Number.MAX_SAFE_INTEGER); + + // 提高文本测量值缓存容量 + cc.textUtils.measureCache.limit = Number.MAX_SAFE_INTEGER; + cc.textUtils.applyMeasureCache(_measureCache); + + // 烘焙 CHAR 字符 + cc.Label.LetterAtlases.init(); + cc.Label._shareAtlas.applyLetterCache(_letterCache); +}); +``` diff --git a/docs/static/demo-imgs/demo6.png b/docs/static/demo-imgs/demo6.png new file mode 100644 index 00000000..ea629359 Binary files /dev/null and b/docs/static/demo-imgs/demo6.png differ