Skip to content

Conversation

@leiyue123
Copy link
Collaborator

重构 Web 平台字体渲染机制,将 WebTypeface 相关代码移至 platform/web 目录,实现与 FreeType 的共存支持。优化字形光栅化流程,引入 WebGlyphRasterizer 和 WebAtlasUploadTask 子类,提升 Canvas 直接上传纹理的性能。

Copy link
Collaborator

@Hparty Hparty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Unify upload logic by pushing platform variance into CellDecodeTask

Currently WebAtlasUploadTask maintains a separate directUploadCells list alongside the base class tasks list, and upload() has to execute two independent loops. The asyncSupport() check in addCell is also semantically mismatched — it means "supports multithreading", not "supports direct upload".

Suggested approach

  1. Add a virtual upload(shared_ptr<Texture>, CommandQueue*) method to CellDecodeTask. The base implementation does queue->writeTexture(texture, atlasRect, pixels, rowBytes) as before.

  2. Create DirectUploadCellTask (in src/platform/web/) that holds a shared_ptr<WebImageBuffer> and overrides upload() to call webBuffer->uploadToTexture(texture, offsetX, offsetY).

  3. WebAtlasUploadTask only overrides addCell: for non-async codecs, call codec->makeBuffer(), wrap the result in a DirectUploadCellTask, and push it into the base class tasks list. Otherwise fall through to the base addCell.

  4. AtlasUploadTask::upload becomes a single unified loop with no override needed:

for (auto& task : tasks) {
  task->wait();
  if (!hardwarePixels) {
    task->upload(texture, queue);  // polymorphic dispatch
  }
}
  1. Delete DirectUploadCell struct, directUploadCells member, and WebAtlasUploadTask::upload override.

This eliminates the dual-list / dual-loop pattern, keeps platform-specific logic isolated in WebAtlasUploadTask::addCell + DirectUploadCellTask::upload, and the static_pointer_cast<WebImageBuffer> is contained within src/platform/web/ (consistent with the static_pointer_cast<GLTexture> pattern used elsewhere).

Copy link
Collaborator

@Hparty Hparty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Review Comments

1. uploadToTextureRegion uses gl.RED while uploadToTexture uses gl.ALPHA for alphaOnly

In web/src/tgfx.ts, the two upload functions handle alphaOnly textures differently:

  • uploadToTexture (line 97): gl.ALPHA
  • uploadToTextureRegion (line 119): gl.RED

gl.RED is WebGL2-only. If WebGL1 compatibility is needed, this will fail. Either way, the inconsistency between the two functions for the same scenario looks unintentional.

2. Typo in GlyphRasterizer.h

Line 27: A Rasterizer that rasterizes a give Glyph. → should be a given Glyph.

3. getGlyphCanvas holds canvas objects until upload completes

Unlike readPixels which renders and immediately extracts pixels then calls releaseCanvas2D, getGlyphCanvas returns a canvas held by WebImageBuffer::MakeAdopted until upload finishes. If many new glyphs are added in a single frame, this could exhaust the canvas pool and cause frequent DOM element creation.

4. WebScalerContext::getGlyphCanvas comment says "Returns null"

The comment says "Returns null if the glyph cannot be rendered", but the actual return is emscripten::val::null(). In C++ context, "null" typically means nullptr. Consider clarifying to Returns val::null() if the glyph cannot be rendered.

Copy link
Collaborator

@Hparty Hparty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ScalerContext::asyncSupport() can be replaced with #ifdef

The newly added ScalerContext::asyncSupport() virtual method is only overridden by WebScalerContext (returns false), and its only call site is GlyphRasterizer::Make to decide between GlyphRasterizer and WebGlyphRasterizer. This is effectively a compile-time platform check disguised as a runtime virtual call.

Since WebScalerContext only exists on Web, this can be a simple #ifdef TGFX_BUILD_FOR_WEB branch in GlyphRasterizer::Make, consistent with how AtlasUploadTask::Make already handles the WebAtlasUploadTask split. This avoids adding an asyncSupport() method to ScalerContext whose semantics are vague and only serve one platform.

Copy link
Collaborator

@Hparty Hparty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestions

1. GlyphRasterizer members exposed as protected — consider using getters

All members (scalerContext, glyphID, fauxBold, stroke, glyphOffset) were changed from private to protected for WebGlyphRasterizer access. However glyphOffset is not used by WebGlyphRasterizer at all. Consider providing protected getter methods instead of exposing raw member variables, which gives better control over access.

2. uploadToTexture and uploadToTextureRegion are nearly identical

In web/src/tgfx.ts, both functions share the same logic (bind texture → set premultiply → choose format by alphaOnly → texSubImage2D → reset premultiply). The only difference is the offset parameters. uploadToTexture could simply call uploadToTextureRegion(GL, source, textureID, 0, 0, alphaOnly) to eliminate duplication.

3. Stroke handling in WebGlyphRasterizer::onMakeBuffer

GlyphRasterizer stores stroke as a Stroke object (via std::optional or direct member), while WebScalerContext::getGlyphCanvas takes const Stroke*. Please verify that the null/no-stroke case is handled correctly when passing the member to getGlyphCanvas — i.e., that a default-constructed Stroke doesn't accidentally produce stroke rendering when fill was intended.

@leiyue123
Copy link
Collaborator Author

Suggestion: Unify upload logic by pushing platform variance into CellDecodeTask

Currently WebAtlasUploadTask maintains a separate directUploadCells list alongside the base class tasks list, and upload() has to execute two independent loops. The asyncSupport() check in addCell is also semantically mismatched — it means "supports multithreading", not "supports direct upload".

Suggested approach

  1. Add a virtual upload(shared_ptr<Texture>, CommandQueue*) method to CellDecodeTask. The base implementation does queue->writeTexture(texture, atlasRect, pixels, rowBytes) as before.
  2. Create DirectUploadCellTask (in src/platform/web/) that holds a shared_ptr<WebImageBuffer> and overrides upload() to call webBuffer->uploadToTexture(texture, offsetX, offsetY).
  3. WebAtlasUploadTask only overrides addCell: for non-async codecs, call codec->makeBuffer(), wrap the result in a DirectUploadCellTask, and push it into the base class tasks list. Otherwise fall through to the base addCell.
  4. AtlasUploadTask::upload becomes a single unified loop with no override needed:
for (auto& task : tasks) {
  task->wait();
  if (!hardwarePixels) {
    task->upload(texture, queue);  // polymorphic dispatch
  }
}
  1. Delete DirectUploadCell struct, directUploadCells member, and WebAtlasUploadTask::upload override.

This eliminates the dual-list / dual-loop pattern, keeps platform-specific logic isolated in WebAtlasUploadTask::addCell + DirectUploadCellTask::upload, and the static_pointer_cast<WebImageBuffer> is contained within src/platform/web/ (consistent with the static_pointer_cast<GLTexture> pattern used elsewhere).

  1. CellDecodeTask 继承自 Task(异步任务),本质是不同的
    CellDecodeTask 是一个 异步解码任务(继承 Task,有 onExecute/onCancel,通过 Task::Run 提交线程池)。它的核心职责是:

异步解码 codec → 写入 dstPixels
upload 时 wait() 等待完成,然后把像素 writeTexture 到 GPU
而 Web 的直传 cell 根本不需要异步解码——它持有的是一个已就绪的 WebImageBuffer(Canvas 元素),上传时直接调 uploadToTexture。如果硬套进 Task 框架:

DirectUploadCellTask 的 onExecute 什么都不做(不需要解码)
Task::Run 白白提交一个空任务到线程池
wait() 白白等待一个已完成的任务
这是为了形式统一而引入无意义的开销和语义混乱。

  1. CellDecodeTask 定义在 .cpp 文件内部
    CellDecodeTask 是 AtlasUploadTask.cpp 的 内部实现类(没有暴露在头文件中)。要让 platform/web/ 下的代码继承它,必须把它移到头文件里暴露出来。这会:

扩大 CellDecodeTask 的可见性(原本是实现细节)
让平台代码依赖基础模块的内部类

@leiyue123
Copy link
Collaborator Author

ScalerContext::asyncSupport() can be replaced with #ifdef

The newly added ScalerContext::asyncSupport() virtual method is only overridden by WebScalerContext (returns false), and its only call site is GlyphRasterizer::Make to decide between GlyphRasterizer and WebGlyphRasterizer. This is effectively a compile-time platform check disguised as a runtime virtual call.

Since WebScalerContext only exists on Web, this can be a simple #ifdef TGFX_BUILD_FOR_WEB branch in GlyphRasterizer::Make, consistent with how AtlasUploadTask::Make already handles the WebAtlasUploadTask split. This avoids adding an asyncSupport() method to ScalerContext whose semantics are vague and only serve one platform.

在 Web 多线程构建中(TGFX_BUILD_FOR_WEB + TGFX_USE_FREETYPE 同时定义):

FTTypeface → FTScalerContext → asyncSupport() 返回 true → 走标准 GlyphRasterizer(异步解码)
WebTypeface → WebScalerContext → asyncSupport() 返回 false → 走 WebGlyphRasterizer(Canvas 直传)
这是 同一编译产物中,运行时根据字体类型走不同路径。#ifdef 只能做二选一,无法处理这种共存场景。

@leiyue123
Copy link
Collaborator Author

Additional Review Comments

1. uploadToTextureRegion uses gl.RED while uploadToTexture uses gl.ALPHA for alphaOnly

In web/src/tgfx.ts, the two upload functions handle alphaOnly textures differently:

  • uploadToTexture (line 97): gl.ALPHA
  • uploadToTextureRegion (line 119): gl.RED

gl.RED is WebGL2-only. If WebGL1 compatibility is needed, this will fail. Either way, the inconsistency between the two functions for the same scenario looks unintentional.

2. Typo in GlyphRasterizer.h

Line 27: A Rasterizer that rasterizes a give Glyph. → should be a given Glyph.

3. getGlyphCanvas holds canvas objects until upload completes

Unlike readPixels which renders and immediately extracts pixels then calls releaseCanvas2D, getGlyphCanvas returns a canvas held by WebImageBuffer::MakeAdopted until upload finishes. If many new glyphs are added in a single frame, this could exhaust the canvas pool and cause frequent DOM element creation.

4. WebScalerContext::getGlyphCanvas comment says "Returns null"

The comment says "Returns null if the glyph cannot be rendered", but the actual return is emscripten::val::null(). In C++ context, "null" typically means nullptr. Consider clarifying to Returns val::null() if the glyph cannot be rendered.

1、这个不一致不是 bug,而是两个函数服务于不同场景——一个上传到新建的 RGBA 纹理,另一个上传到已有的 Atlas 纹理(可能是 R8 格式)。
2、已修复。
3、确实会同时持有多个 canvas,首屏时可能超出池大小。但这是 有意的性能权衡:
省掉了 getImageData()——这是 Canvas API 中最重的操作之一,需要 GPU→CPU 像素读回
省掉了像素数据的 texSubImage2D(pixels)——改为 texSubImage2D(canvas) 让浏览器内部做 GPU→GPU 拷贝
多创建几个 OffscreenCanvas 的代价远小于多次 getImageData 的代价
4、emscripten::val::null() 在 emscripten 里就是 JS 的 null,写 "Returns null" 是对语义的自然描述——"返回空值"。API 注释应该描述语义而非类型细节,写 val::null() 反而把注释变成了实现细节的复读。
况且函数签名已经是 emscripten::val,读者看到返回类型就知道不可能是 nullptr(那是指针语义),不会混淆。项目中其他返回 emscripten::val 的地方如果有类似注释,也应该保持风格一致。

@leiyue123
Copy link
Collaborator Author

Additional Suggestions

1. GlyphRasterizer members exposed as protected — consider using getters

All members (scalerContext, glyphID, fauxBold, stroke, glyphOffset) were changed from private to protected for WebGlyphRasterizer access. However glyphOffset is not used by WebGlyphRasterizer at all. Consider providing protected getter methods instead of exposing raw member variables, which gives better control over access.

2. uploadToTexture and uploadToTextureRegion are nearly identical

In web/src/tgfx.ts, both functions share the same logic (bind texture → set premultiply → choose format by alphaOnly → texSubImage2D → reset premultiply). The only difference is the offset parameters. uploadToTexture could simply call uploadToTextureRegion(GL, source, textureID, 0, 0, alphaOnly) to eliminate duplication.

3. Stroke handling in WebGlyphRasterizer::onMakeBuffer

GlyphRasterizer stores stroke as a Stroke object (via std::optional or direct member), while WebScalerContext::getGlyphCanvas takes const Stroke*. Please verify that the null/no-stroke case is handled correctly when passing the member to getGlyphCanvas — i.e., that a default-constructed Stroke doesn't accidentally produce stroke rendering when fill was intended.

1、把 glyphOffset 留在 private,其余四个保持 protected 即可。不需要加 getter。
2、alphaOnly 的格式不同:前者用 gl.ALPHA,后者用 gl.RED——之前已分析过,这是因为目标纹理不同(RGBA vs R8 atlas 纹理)
3、stroke 本身就是 Stroke* 指针,不是 std::optional 也不是值对象,null 就是 fill,不存在"默认构造意外描边"的问题。

virtual void upload(std::shared_ptr<Texture> texture, CommandQueue* queue) = 0;

virtual void cancel() {
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CellUploadTask 不需要 cancel() 接口。cancel() 的唯一目的是在 AtlasUploadTask 析构时阻止线程池对已释放的 dstPixels 执行写入,这是 AsyncCellUploadTask 自身的资源管理问题,应该由它自己的析构函数负责:

~AsyncCellUploadTask() override {
  Task::cancel();
}

这样 AtlasUploadTask::~AtlasUploadTask 里的 cancel 循环也可以删掉,让 cellTasks 自然析构即可。DirectCellUploadTask 没有异步操作,析构时自然释放资源,不需要任何额外处理。

CellUploadTask 就只保留一个纯粹的 upload() 接口,职责更干净。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

std::shared_ptr<TextureView> WebImageBuffer::onMakeTexture(Context* context, bool) const {
auto textureView = TextureView::MakeRGBA(context, width(), height(), nullptr, 0);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onMakeTexture 始终创建 RGBA 纹理,但现在 _alphaOnly 可以为 true(通过 MakeAdopted(canvas, isAlphaOnly()))。后续 uploadToTexture 会按 _alphaOnly 选择 gl.RED 格式上传,向 RGBA 纹理写入 gl.RED 数据会格式不匹配。

当前 glyph 直传走 atlas 路径不经过 onMakeTexture,暂时不会触发。但建议按 _alphaOnly 选择纹理格式,避免未来踩坑:

std::shared_ptr<TextureView> textureView = nullptr;
if (_alphaOnly) {
  textureView = TextureView::MakeAlpha(context, width(), height(), nullptr, 0);
} else {
  textureView = TextureView::MakeRGBA(context, width(), height(), nullptr, 0);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Hparty
Hparty previously approved these changes Feb 11, 2026
Copy link
Collaborator

@Hparty Hparty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Hparty Hparty merged commit e102f7c into main Feb 11, 2026
9 checks passed
@Hparty Hparty deleted the feature/leiyue123_web_typeface branch February 11, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants