Skip to content

refactor(rhi-webgl): rework canvas resolution API, follow display size by default#3037

Open
luo2430 wants to merge 43 commits into
galacean:dev/2.0from
luo2430:dev/2.0
Open

refactor(rhi-webgl): rework canvas resolution API, follow display size by default#3037
luo2430 wants to merge 43 commits into
galacean:dev/2.0from
luo2430:dev/2.0

Conversation

@luo2430

@luo2430 luo2430 commented Jun 17, 2026

Copy link
Copy Markdown

概述

一次 canvas 分辨率 API 的易用性增强:把 auto-resize 能力归位到 WebCanvas,让渲染分辨率默认自动跟随显示尺寸——大多数应用 create({ canvas }) 之后不用写任何 resize 代码。

原始需求是给 WebGLEngine 加 auto-resize opt-in(@luo2430)。经过讨论从第一性演进成这套重构,方向不变、形态更彻底。

主要改动

1. 归属:auto-resize 全部归 WebCanvas,pump 下沉到基类

observer 观察 canvas、算 canvas 尺寸、改 canvas 分辨率——概念上 100% 属于 canvas。原本放 engine 是因为白屏修复需要帧时序(实现依赖绑架了归属)。现在:

  • auto-resize 逻辑全在 WebCanvas
  • 每帧的 resize pump 下沉到基类 Engine.update()this._canvas._pumpPendingResize()),_pumpPendingResize 是基类 Canvas 的默认 no-op、WebCanvas override——WebGLEngine 不再需要 override update()

2. 默认自动跟随显示尺寸(零配置)

// 之前:用户要显式调 + 自己挂 window.resize 监听
engine.canvas.resizeByClientSize();

// 现在:create 后什么都不用写
WebGLEngine.create({ canvas });
  • ResizeObserver 监听 canvas 元素本身 → 接住窗口、CSS、flex/容器、侧边栏/面板等所有来源的尺寸变化(window.resize 只能接住窗口变化)
  • 0×0 安全:resize 延迟到帧循环,canvas 无布局尺寸时跳过——未挂载的 canvas 永不被设成 0×0 buffer
  • example 中的显式 resize 调用相应删除

3. 两个分辨率入口

engine.canvas.setAutoResolution(scale?)    // 自动:跟随显示尺寸
engine.canvas.setResolution(width, height) // 固定:锁死绝对分辨率(隐式退出自动)
  • scalebase 是物理分辨率1 = 满物理分辨率(原生清晰),0.7 = 降 30% 提性能,2 = 超采样。device pixel ratio 自动吸收,用户无需理解 DPR
  • 想固定分辨率:create({ canvas }) 后调 setResolution(w, h)(退出自动,无闪烁)

4. width / height 改只读

分辨率是原子的整体:单独设 width 不动 height 会产生畸变中间态 + 两次 buffer 重建,且裸写 canvas.width 会让引擎缓存尺寸与真实 buffer 分叉。改只读后,所有分辨率变更走 setResolution 一个原子入口。width/height 是"画布分辨率"的分量。

5. 分数 DPR 精度:devicePixelContentBoxSize

clientWidth * dpr 在分数 DPR(Windows 1.25/1.5)下有 ≤1px 舍入误差。改用 ResizeObserver entry 的 devicePixelContentBoxSize(精确物理像素)消除它,Safari 等不支持的浏览器自动降级回 clientWidth * dpr(特性检测,无人退化)。four 大 web3D 引擎(three.js/Babylon/Pixi/PlayCanvas)均未采用此特性。

6. 删 resizeByClientSize

被默认自动 + setAutoResolution 覆盖。

Breaking Changes

major 版本级

  • canvas.width / canvas.height 改为只读(用 setResolution 设分辨率)
  • 移除 resizeByClientSize
  • auto-resize 默认开启(想要固定分辨率:create 后调 setResolution

设计参照

  • create({...}) 配置模式对齐 three.js / Babylon(web 引擎标准)
  • 默认自动跟随对齐 Unity / UE 的"默认就跟随"心智
  • scale(物理分辨率之上的性能倍数)对齐 UE ScreenPercentage
  • setResolution 命名对齐 Unity / UE(SetResolution / SetScreenResolution

验证

  • tsc 全绿(core + rhi-webgl)
  • 全量单测全过、e2e ×4 全绿
  • devicePixelContentBoxSize 精确路径 + 首帧时序 jsdom 不忠实复现,靠规范 + 代码审查(建议真机验一次)

供讨论。 @luo2430 有不同意见或更好想法请直接回复。

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

WebCanvas gains an isOffscreenCanvas() method that centralizes the repeated inline OffscreenCanvas type checks; three existing guards in WebCanvas and two in WebGLGraphicDevice are updated to use it. WebGLEngine receives enableAutoResize(pixelRatio?) and disableAutoResize() methods backed by a ResizeObserver with EngineEventType.Shutdown cleanup. New tests cover both isOffscreenCanvas() and the full auto-resize lifecycle.

Changes

OffscreenCanvas detection and auto-resize

Layer / File(s) Summary
WebCanvas.isOffscreenCanvas() method and usage refactor
packages/rhi-webgl/src/WebCanvas.ts, tests/src/rhi-webgl/WebCanvas.test.ts
Adds isOffscreenCanvas(): boolean to WebCanvas and replaces three inline typeof OffscreenCanvas/instanceof guards (in scale getter, scale setter, and resizeByClientSize) with calls to the new method. A new test suite verifies the return value for HTMLCanvasElement, OffscreenCanvas, and post-construction field replacements.
WebGLGraphicDevice context fallback updated
packages/rhi-webgl/src/WebGLGraphicDevice.ts
Updates WebGLGraphicDevice.init to allow the WebGL2 gl variable to be undefined and replaces both experimental-webgl2 and experimental-webgl fallback gating with (canvas as WebCanvas).isOffscreenCanvas().
WebGLEngine enableAutoResize / disableAutoResize
packages/rhi-webgl/src/WebGLEngine.ts, tests/src/rhi-webgl/WebGLEngine.test.ts, tests/vitest.config.ts
Adds EngineEventType import, a private _resizeObserver field and static cleanup helper, and public enableAutoResize(pixelRatio?) / disableAutoResize() methods. enableAutoResize skips OffscreenCanvas, disconnects any prior observer, registers a new ResizeObserver calling canvas.resizeByClientSize, and registers a Shutdown listener. Tests verify listener counts, observer lifecycle and replacement, resizeByClientSize invocation after style mutation, and cleanup on engine.destroy(). screenshotFailures is set to false in the Vitest Playwright config.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant WebGLEngine
  participant ResizeObserver
  participant WebCanvas

  Caller->>WebGLEngine: enableAutoResize(pixelRatio?)
  WebGLEngine->>ResizeObserver: disconnect() (prior observer, if any)
  WebGLEngine->>ResizeObserver: new ResizeObserver(callback)
  WebGLEngine->>ResizeObserver: observe(canvas element)
  WebGLEngine->>WebGLEngine: on(Shutdown, _cleanupResizeObserver)

  Note over ResizeObserver,WebCanvas: canvas client dimensions change
  ResizeObserver->>WebCanvas: resizeByClientSize(pixelRatio)

  Caller->>WebGLEngine: disableAutoResize()
  WebGLEngine->>WebGLEngine: off(Shutdown, _cleanupResizeObserver)
  WebGLEngine->>ResizeObserver: disconnect()

  Note over WebGLEngine: engine.destroy() triggers Shutdown
  WebGLEngine->>WebGLEngine: _cleanupResizeObserver()
  WebGLEngine->>ResizeObserver: disconnect()
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Poem

🐰 A canvas that knows what it is — how grand!
No more typeof checks scattered by hand.
isOffscreenCanvas() speaks for the whole,
ResizeObserver watches with vigilant soul.
When shutdown arrives, it tidies the nest,
A well-behaved bunny — clean-up's the best! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately reflects the main change: canvas auto-resizing and display-size-based resolution behavior.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@luo2430 luo2430 marked this pull request as draft June 17, 2026 05:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/src/rhi-webgl/WebGLEngine.test.ts`:
- Around line 140-145: The spy on the _cleanupResizeObserver method is created
after enableAutoResize() has already registered the Shutdown listener, meaning
the listener holds a reference to the original function rather than the spied
version. Move the vi.spyOn call for cleanupResizeObserverSpy to before the
enableAutoResize() call to ensure the registered Shutdown listener captures the
spied function reference, allowing the assertion on
cleanupResizeObserverSpy.toHaveBeenCalledTimes(1) to work correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ca7819a5-d724-46f3-ad7d-879ae0800382

📥 Commits

Reviewing files that changed from the base of the PR and between 5d74c1d and 887ac8c.

📒 Files selected for processing (3)
  • packages/core/src/input/InputManager.ts
  • packages/rhi-webgl/src/WebGLEngine.ts
  • tests/src/rhi-webgl/WebGLEngine.test.ts

Comment thread tests/src/rhi-webgl/WebGLEngine.test.ts Outdated
@luo2430 luo2430 marked this pull request as ready for review June 17, 2026 05:59
GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

@luo2430

luo2430 commented Jun 17, 2026

Copy link
Copy Markdown
Author

@GuoLei1990

this指针问题我考虑到了,但不知为何代码经过修改后我的本地测试可以跑通。

image

我会再去处理这个问题,原本以为已经修复了。

GuoLei1990

This comment was marked as outdated.

@cptbtptpbcptdtptp

Copy link
Copy Markdown
Collaborator

这里有一个时序问题需要提一下,根据 W3C 规范,浏览器每帧的渲染管线顺序是:

 Input Events (包括 window resize event) → rAF callbacks(引擎逻辑) → Style/Layout → ResizeObserver callbacks(本次 PR 添加) → Paint

在发生 resize 那帧,引擎绘制 rAF 时使用的还是旧的画布尺寸,随后 ResizeObserver 回调修改画布的尺寸会清空 canvas buffer ,表现就是这一帧会白,但 window 监听 resize 也不可以直接用,因为 window.resize 仅监听浏览器窗口大小变化,所以其他引擎要么完全手动触发,要么轮询检测,避免本帧 resize 与渲染不同步。

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 62.59843% with 95 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.58%. Comparing base (721b42a) to head (ecae895).
⚠️ Report is 3 commits behind head on dev/2.0.

Files with missing lines Patch % Lines
packages/rhi-webgl/src/WebCanvas.ts 75.22% 27 Missing ⚠️
e2e/case/text-typed.ts 0.00% 11 Missing ⚠️
...cleRenderer-emit-mesh-cone-scale-rotation-world.ts 0.00% 6 Missing ⚠️
.../particleRenderer-emit-mesh-cone-scale-rotation.ts 0.00% 5 Missing ⚠️
...rer-emit-mesh-cone-scale-rotation-life-seperate.ts 0.00% 3 Missing ⚠️
...icleRenderer-emit-mesh-cone-scale-rotation-life.ts 0.00% 3 Missing ⚠️
examples/src/paricle-emit-mesh.ts 0.00% 3 Missing ⚠️
.../case/particleRenderer-emit-billboard-stretched.ts 0.00% 2 Missing ⚠️
e2e/case/animator-additive.ts 0.00% 1 Missing ⚠️
e2e/case/animator-blendShape.ts 0.00% 1 Missing ⚠️
... and 33 more
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #3037      +/-   ##
===========================================
+ Coverage    79.37%   79.58%   +0.20%     
===========================================
  Files          903      904       +1     
  Lines       100632   101224     +592     
  Branches     11260    11404     +144     
===========================================
+ Hits         79879    80558     +679     
+ Misses       20569    20482      -87     
  Partials       184      184              
Flag Coverage Δ
unittests 79.58% <62.59%> (+0.20%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@luo2430 luo2430 changed the title feat(rhi-webgl): add auto-resize support via ResizeObserver and refactor input canvas naming refactor(rhi-webgl): extract isOffscreenCanvas helper | feat(rhi-webgl): add WebGLEngine auto-resize support via ResizeObserver | chore(vitest): disable screenshotFailures to avoid taking blank screenshots Jun 18, 2026
@luo2430

luo2430 commented Jun 18, 2026

Copy link
Copy Markdown
Author

@GuoLei1990

代码处理完毕,vitest问题我会去开一个issue,有部分解决方案和提议,但需要验证。另外,isOffscreenCanvas中的typeof OffscreenCanvas !== "undefined"是不是可以直接提成一个全局常量或者静态常量,但项目中似乎没有这种先例,这符合项目规范吗?

GuoLei1990

This comment was marked as outdated.

@luo2430

luo2430 commented Jun 18, 2026

Copy link
Copy Markdown
Author

@GuoLei1990

注释已修改。
可以确定的是isOffscreenCanvas会被设定为一个public api,因为有可以跨包使用的地方。但为了这一处使用而添加一个新的工作区依赖我觉得不值得。

image

GuoLei1990

This comment was marked as outdated.

@luo2430

luo2430 commented Jun 18, 2026

Copy link
Copy Markdown
Author

@cptbtptpbcptdtptp

GuoLei1990

This comment was marked as outdated.

@luo2430

luo2430 commented Jun 21, 2026

Copy link
Copy Markdown
Author

@cptbtptpbcptdtptp 这里有好康的

@cptbtptpbcptdtptp

Copy link
Copy Markdown
Collaborator

@cptbtptpbcptdtptp 这里有好康的

这个是当前 PR 在真实 resize 时的表现
resize

期望应该是这样的(时序导致)
resize1

@luo2430

luo2430 commented Jun 21, 2026

Copy link
Copy Markdown
Author

@cptbtptpbcptdtptp 这个示例能分享一下吗?我用于测试。

@cptbtptpbcptdtptp

Copy link
Copy Markdown
Collaborator

@cptbtptpbcptdtptp 这个示例能分享一下吗?我用于测试。

examples/src/device-restore.ts 这个,设置 enableAutoResize 就可以。

@luo2430

luo2430 commented Jun 21, 2026

Copy link
Copy Markdown
Author

@cptbtptpbcptdtptp 收到

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jun 22, 2026
@luo2430

luo2430 commented Jul 4, 2026

Copy link
Copy Markdown
Author

+1

@GuoLei1990 我根据pr改动修改了文档,需要commit在这吗?

@GuoLei1990

Copy link
Copy Markdown
Member

嗯嗯,可以的,是我遗漏了!PR 拉到最新帮忙适配下吧🙏

- Updated canvas resizing logic in docs from resizeByClientSize to setAutoResolution and setResolution
- Aligned English and Chinese documentation for physics and platform modules
- Removed deprecated method calls from README and example snippets
- Enhanced clarity on device pixel ratio (DPR) handling and resolution strategies
@luo2430

luo2430 commented Jul 4, 2026

Copy link
Copy Markdown
Author

嗯嗯,可以的,是我遗漏了!PR 拉到最新帮忙适配下吧🙏

@GuoLei1990 提交了,但严格来说要设置lint-staged的,不然直接提交报错 Git: �[0;34m→�[0m lint-staged could not find any staged files matching configured tasks ,目前是用 --no-verify 临时跳过的

@GuoLei1990

Copy link
Copy Markdown
Member

嗯嗯,可以的,是我遗漏了!PR 拉到最新帮忙适配下吧🙏

@GuoLei1990 提交了,但严格来说要设置lint-staged的,不然直接提交报错 Git: �[0;34m→�[0m lint-staged could not find any staged files matching configured tasks ,目前是用 --no-verify 临时跳过的

👌

GuoLei1990

This comment was marked as outdated.

@luo2430

luo2430 commented Jul 8, 2026

Copy link
Copy Markdown
Author

@cptbtptpbcptdtptp 帮忙审阅一下

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

整体方向认可:auto-resize 归位 WebCanvas、pump 下沉基类、width/height 只读 + setResolution 原子入口,归属和 API 形态都比旧的清晰。残留清理也很干净(全仓已无 resizeByClientSize / canvas.width = 写入残留),_exitAutoResize 清 pending 的竞态处理、Shutdown 释放时序都是对的。

但默认开启的 auto 路径上有 1 个 blocker(行内 ①)和 2 个兼容性缺口(行内 ②③),建议修复 ① 后再合入。

行内评论索引(① 最重要):

# 位置 问题 验证方式
WebCanvas.ts L95 canvas 无 CSS 尺寸时 auto-resize 布局反馈环,分辨率指数发散至白屏 Chromium 实测复现
WebCanvas.ts L58 new ResizeObserver 无特性检测,小程序 adapter 无此 polyfill,create() 直接崩 adapter 仓库全文检索
WebCanvas.ts L64 默认 content-box 观察,DPR-only 变化(跨屏拖动/zoom)不触发更新 规范推导,建议真机验证
Canvas.ts L35 setResolution 非整数时引擎缓存与真实 buffer 分叉 Chromium 实测
Canvas.ts L62 新增 _ 成员缺 /** @internal */,会泄漏进公开 d.ts tsconfig stripInternal: true
WebCanvas.ts L49 OffscreenCanvas 上显式调 setAutoResolution 静默丢弃 代码事实

其余较轻的点,集中列在这里:

  • e2e 语义变化:31 处 resizeByClientSize(2)setAutoResolution(2) 不等价——前者固定 ×2 与 DPR 无关,后者 ×DPR×2。CI 的 DPR=1 时结果相同,但本地 Retina 跑 e2e 会与 baseline 环境分叉。e2e 要的是确定性,建议改用 setResolution 锁定分辨率。
  • 单测截图 baseline:7 张 png 更新是 create 默认 auto 使 canvas 分辨率跟随 playwright 页面布局所致,可解释;但 baseline 从此绑定 vitest browser 的默认 viewport,建议在 PR 描述注明。
  • create 失败路径泄漏_initialize reject 时不会走 Shutdown,已创建的 ResizeObserver 泄漏。可在 create 的失败分支里调 webCanvas._destroy()
  • README quickstartpackages/galacean/README.md 示例的 canvas 无任何 CSS 尺寸,配合默认 auto 正好落进 ① 的触发条件,修 ① 时记得同步补样式或说明。
  • 无关改动混入:e2e/examples 的 prettier 重排、animator?.play 顺手加 optional chaining、vitest.config.tsscreenshotFailures: false 与主题无关,建议在描述里说明缘由或剥离。

验证方法说明:①④ 是在 Chromium 中 1:1 复刻本 PR WebCanvassetAutoResolution/_pumpPendingResize 逻辑实测复现(数据见行内评论);② 是对 galacean/platform-adapter 仓库的全文检索;③ 是 ResizeObserver 规范行为推导——顺带提醒:headless/CDP 的 deviceScaleFactor 模拟对 devicePixelContentBoxSize 不生效(window.devicePixelRatio 显示 2 但 dpcb 仍按真实 DPR 返回),所以 ①③ 这类问题 headless 环境测不出来,与 PR 描述"建议真机验一次"的判断一致,真机务必验。

Comment thread packages/rhi-webgl/src/WebCanvas.ts Outdated
Comment thread packages/rhi-webgl/src/WebCanvas.ts
Comment thread packages/rhi-webgl/src/WebCanvas.ts Outdated
Comment thread packages/core/src/Canvas.ts
Comment thread packages/core/src/Canvas.ts
}

// OffscreenCanvas has no display size to follow
if (this._isOffscreenCanvas()) return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] OffscreenCanvas 上显式调用被静默丢弃

create 默认调用时静默跳过合理;但用户对 OffscreenCanvas 引擎显式调 setAutoResolution(0.7) 期待生效,现状连 scale 都直接丢弃且无任何反馈。建议 console.warn 一次,或至少在 JSDoc/docs 标注"OffscreenCanvas 仅支持 setResolution"。

@luo2430

luo2430 commented Jul 10, 2026

Copy link
Copy Markdown
Author

@cptbtptpbcptdtptp 先简单加了internal,另外 this._resizeObserver.observe(this._webCanvas, { box: "device-pixel-content-box" }); 在edge中测试与原先并无差异。

@cptbtptpbcptdtptp

Copy link
Copy Markdown
Collaborator

this._resizeObserver.observe(this._webCanvas, { box: "device-pixel-content-box" }); 在edge中测试与原先并无差异。

这个结果符合预期——两种 box 在窗口/布局尺寸变化时行为完全一致,常规 resize 测试看不出差异。差异只出现在 devicePixelRatio 变了、但 CSS 尺寸没变的场景,典型两个:把窗口拖到不同缩放比例的显示器;对固定 px 尺寸的 canvas 做浏览器缩放(Ctrl +/-)。

另外注意:如果测试页的 canvas 是 width:100% 这类布局(examples 模板就是),浏览器缩放会连带改变 clientWidth,两种 box 都触发,必然得出"无差异"——要用固定 px 的 canvas 才能分离出 DPR-only 变化。

60 秒复现(任意页面 DevTools console 粘贴,然后 Ctrl+加号 缩放几次):

const c = document.createElement("canvas");
c.style.cssText = "width:200px;height:100px"; // 固定 CSS px,隔离 DPR-only 变化
document.body.append(c);
new ResizeObserver(() => console.log("content-box fired, dpr =", devicePixelRatio)).observe(c);
const ro = new ResizeObserver((es) => {
  const b = es[0].devicePixelContentBoxSize[0];
  console.log("device-pixel-content-box fired:", b.inlineSize + "x" + b.blockSize, "dpr =", devicePixelRatio);
});
ro.observe(c, { box: "device-pixel-content-box" });

预期:每次缩放只有 device-pixel-content-box 触发并报告新的物理像素,content-box 始终静默。对应到引擎:用户 zoom 或把窗口拖到另一块屏后,现状的 observe 收不到通知,渲染分辨率停留在旧 DPR(模糊或过采样浪费)。

两个小点顺带:

  • d1cda6c@internal 统一漏了 Canvas.ts L7 的 _sizeUpdateFlagManager——它是单星 /* @internal */,不是合法 JSDoc,stripInternal 剥不掉,恰好是唯一真正会泄漏进公开 d.ts 的存量。
  • 行内 ①(无 CSS 尺寸时的布局反馈环,有实测数据)和 ②(小程序无 ResizeObserver polyfill,create() 必崩)影响默认路径可用性,建议合入前修掉;④ 的 round 一行即可。修完我来复验。

GuoLei1990

This comment was marked as outdated.

@luo2430

luo2430 commented Jul 10, 2026

Copy link
Copy Markdown
Author

this._resizeObserver.observe(this._webCanvas, { box: "device-pixel-content-box" }); 在edge中测试与原先并无差异。

这个结果符合预期——两种 box 在窗口/布局尺寸变化时行为完全一致,常规 resize 测试看不出差异。差异只出现在 devicePixelRatio 变了、但 CSS 尺寸没变的场景,典型两个:把窗口拖到不同缩放比例的显示器;对固定 px 尺寸的 canvas 做浏览器缩放(Ctrl +/-)。

另外注意:如果测试页的 canvas 是 width:100% 这类布局(examples 模板就是),浏览器缩放会连带改变 clientWidth,两种 box 都触发,必然得出"无差异"——要用固定 px 的 canvas 才能分离出 DPR-only 变化。

60 秒复现(任意页面 DevTools console 粘贴,然后 Ctrl+加号 缩放几次):

const c = document.createElement("canvas");
c.style.cssText = "width:200px;height:100px"; // 固定 CSS px,隔离 DPR-only 变化
document.body.append(c);
new ResizeObserver(() => console.log("content-box fired, dpr =", devicePixelRatio)).observe(c);
const ro = new ResizeObserver((es) => {
  const b = es[0].devicePixelContentBoxSize[0];
  console.log("device-pixel-content-box fired:", b.inlineSize + "x" + b.blockSize, "dpr =", devicePixelRatio);
});
ro.observe(c, { box: "device-pixel-content-box" });

预期:每次缩放只有 device-pixel-content-box 触发并报告新的物理像素,content-box 始终静默。对应到引擎:用户 zoom 或把窗口拖到另一块屏后,现状的 observe 收不到通知,渲染分辨率停留在旧 DPR(模糊或过采样浪费)。

两个小点顺带:

  • d1cda6c@internal 统一漏了 Canvas.ts L7 的 _sizeUpdateFlagManager——它是单星 /* @internal */,不是合法 JSDoc,stripInternal 剥不掉,恰好是唯一真正会泄漏进公开 d.ts 的存量。
  • 行内 ①(无 CSS 尺寸时的布局反馈环,有实测数据)和 ②(小程序无 ResizeObserver polyfill,create() 必崩)影响默认路径可用性,建议合入前修掉;④ 的 round 一行即可。修完我来复验。

等会儿验证

GuoLei1990

This comment was marked as outdated.

@luo2430

luo2430 commented Jul 10, 2026

Copy link
Copy Markdown
Author

this._resizeObserver.observe(this._webCanvas, { box: "device-pixel-content-box" }); 在edge中测试与原先并无差异。

这个结果符合预期——两种 box 在窗口/布局尺寸变化时行为完全一致,常规 resize 测试看不出差异。差异只出现在 devicePixelRatio 变了、但 CSS 尺寸没变的场景,典型两个:把窗口拖到不同缩放比例的显示器;对固定 px 尺寸的 canvas 做浏览器缩放(Ctrl +/-)。

另外注意:如果测试页的 canvas 是 width:100% 这类布局(examples 模板就是),浏览器缩放会连带改变 clientWidth,两种 box 都触发,必然得出"无差异"——要用固定 px 的 canvas 才能分离出 DPR-only 变化。

60 秒复现(任意页面 DevTools console 粘贴,然后 Ctrl+加号 缩放几次):

const c = document.createElement("canvas");
c.style.cssText = "width:200px;height:100px"; // 固定 CSS px,隔离 DPR-only 变化
document.body.append(c);
new ResizeObserver(() => console.log("content-box fired, dpr =", devicePixelRatio)).observe(c);
const ro = new ResizeObserver((es) => {
  const b = es[0].devicePixelContentBoxSize[0];
  console.log("device-pixel-content-box fired:", b.inlineSize + "x" + b.blockSize, "dpr =", devicePixelRatio);
});
ro.observe(c, { box: "device-pixel-content-box" });

预期:每次缩放只有 device-pixel-content-box 触发并报告新的物理像素,content-box 始终静默。对应到引擎:用户 zoom 或把窗口拖到另一块屏后,现状的 observe 收不到通知,渲染分辨率停留在旧 DPR(模糊或过采样浪费)。

两个小点顺带:

  • d1cda6c@internal 统一漏了 Canvas.ts L7 的 _sizeUpdateFlagManager——它是单星 /* @internal */,不是合法 JSDoc,stripInternal 剥不掉,恰好是唯一真正会泄漏进公开 d.ts 的存量。
  • 行内 ①(无 CSS 尺寸时的布局反馈环,有实测数据)和 ②(小程序无 ResizeObserver polyfill,create() 必崩)影响默认路径可用性,建议合入前修掉;④ 的 round 一行即可。修完我来复验。

edge验证有效,之前无效是因为开启了设备仿真模式

GuoLei1990

This comment was marked as outdated.

@luo2430

luo2430 commented Jul 10, 2026

Copy link
Copy Markdown
Author

@cptbtptpbcptdtptp @GuoLei1990 关键项处理完毕

GuoLei1990

This comment was marked as outdated.

@cptbtptpbcptdtptp

Copy link
Copy Markdown
Collaborator

复验结果(基于 488b5b2):

②③④⑤ 验收通过

  • typeof ResizeObserver 检测 + 一次性 sizing 降级,路径正确;
  • device-pixel-content-box + try/catch 降级,写法干净(也感谢确认 Edge 复现——设备仿真模式正是"模拟环境对 dpcb 不忠实"的又一例,这类问题只能真机验);
  • setResolution 入口 round 后,检查了 _setSize 的全部调用方(构造/pump 两路径/新降级分支),整数化已全覆盖;
  • ⑤ 含 L7 存量单星,d.ts 泄漏面清零;override 因 stripInternal 类型链而删除也合理。

但 ①(布局反馈环)还没有动——_pumpPendingResize 主体与上一版相同,这是行内评论里唯一的 blocker,"关键项处理完毕"还差这一项。

刚在 488b5b2 的新实现上(1:1 含 dpcb observe + fallback)重新实测:真实 DPR=2 屏、默认 setAutoResolution()、无 CSS 尺寸的 <canvas>

canvas.width: 300 → 600 → 1200 → 2400 → 4800 → 9600 → 19200 → 38400 → 76800(9 帧)

CSS width:400px 对照组稳定在 800。观察 box 的切换不影响反馈环成立(布局仍跟随 width 属性)。

修复方向建议(供参考):pump 应用尺寸前记录 clientWidth/clientHeight,应用后再读一次,若被 buffer 尺寸反向带动(说明 canvas 无 CSS 约束、布局在跟随 buffer)→ console.warn 一次并 _exitAutoResize() 停止 auto,保持当前尺寸。同步读 clientWidth 会触发一次 layout,但只发生在尺寸实际变化的帧,成本可接受。同时 packages/galacean/README.md quickstart 的 canvas 示例请同步补上 CSS 尺寸(现状照抄即触发此问题)。

两个小点顺带:

  • ② 的降级分支在 canvas 未挂载或 display:noneclientWidth=0,会把 buffer 设成 0×0(RO 路径有 0 值 guard,降级路径没有)——加个 clientWidth > 0 判断跳过即可;
  • e2e 里 31 处 setAutoResolution(2) 依赖 CI DPR=1 的隐式假设(详见首轮总评),非阻塞,看团队取舍。

… CSS dimensions

When a canvas element has no CSS width/height, setting its buffer width/height
attributes changes the intrinsic layout size, causing a feedback loop where
_resize() inflates the buffer endlessly on each frame.

- Guard ResizeObserver fallback against zero client dimensions
- Detect layout feedback loop by comparing client size before/after _setSize()
- Disable auto-resolution and warn user when loop is detected
@luo2430 luo2430 closed this Jul 10, 2026
@luo2430 luo2430 reopened this Jul 10, 2026
@luo2430

luo2430 commented Jul 10, 2026

Copy link
Copy Markdown
Author

同时 packages/galacean/README.md quickstart 的 canvas 示例请同步补上 CSS 尺寸(现状照抄即触发此问题)。

@cptbtptpbcptdtptp
没理解什么意思,只注意到部分内容格式可以统一还有部分链接过期

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

复审 @ ecae89589e24(线性追加一个 fix commit:修 no-CSS canvas 的 auto-resize 布局反馈环——① 一次性 fallback 加 clientWidth/Height > 0 守卫防 0×0 buffer;② _pumpPendingResize_setSize 前后快照 clientWidth/Height,检测到布局跟随 buffer 变化即 console.warn + _exitAutoResize() 打破指数发散——闭环 co-maintainer 最后一条真门控 [P1]〔无 CSS 约束反馈环,现 thread RESOLVED--approve 维持 gate 清空)

线性推进检测(审前先校准真 tip)

自我上轮 review(488b5b2827ed)以来 HEAD 线性推进一个 commitcompare 488b5b2827ed...ecae89589e24 = status: ahead / ahead_by: 1 / behind_by: 0,非 force-push;gh api commits/ecae89589e24 --jq .parents = [488b5b2827ed] 父即上轮 tip):

  • ecae89589e24 fix(rhi-webgl): prevent infinite buffer size growth when canvas lacks CSS dimensions

git diff = 仅 1 文件 packages/rhi-webgl/src/WebCanvas.ts,+23 / -2因是 fix 前缀,我逐链 trace(非信 commit message)。

逐链核对:布局反馈环检测 —— 闭环 co-maintainer open [P1],修真发散 bug

根因(canvas 是替换元素):无 CSS 宽高时 canvas 的布局尺寸 = width/height 属性。默认 auto 路径成环:pump 读 clientWidth(=当前 buffer)→ 写 canvas.width = clientWidth × dpr × scale_setSize_onSizeChanged)→ 改布局 → 下一次 observer 回调拿到更大的 clientWidth_pendingResize=true → 下帧再放大 → 指数发散

     this._pendingResize = false;
     const scale = this._autoResolutionScale;
+    const prevClientWidth = webCanvas.clientWidth;
+    const prevClientHeight = webCanvas.clientHeight;
     if (this._pendingDevicePixelWidth > 0 && this._pendingDevicePixelHeight > 0) { ... }
     else { ... }
+    if (webCanvas.clientWidth !== prevClientWidth || webCanvas.clientHeight !== prevClientHeight) {
+      console.warn("Canvas layout feedback loop detected: ...");
+      this._exitAutoResize();
+    }

逐场景对抗性 trace(检测逻辑,非信描述)——无正确性 bug,无误报,无危险漏报

  1. 有 CSS 约束的 canvas(常见路径)prevClientWidth = CSS 锁定的显示宽(如 800);_setSizecanvas.width = round(800×dpr×scale),但 CSS 约束布局 → clientWidth 仍 800;检测 clientWidth === prevClientWidth不 warn 不 exit。✓ 无误报——这是关键,正常用户零影响。
  2. 无 CSS 反馈环prevClientWidth = 当前 buffer(如 300)→ _setSize 写 600(dpr=2)→ 无 CSS 布局跟随属性 → clientWidth 变 600 → 检测 600 !== 300warn + _exitAutoResize()。✓ 第一个发散帧就打破,不等指数放大。
  3. _setSize no-op(尺寸未变)Canvas._setSize 有守卫 if (_width !== width),尺寸相同则 _onSizeChanged 不调 → canvas.width 属性不变 → clientWidth 不变 → 不误报。✓
  4. _exitAutoResize() 是否彻底止环disconnect() observer + 置 undefined(无未来回调)+ _pendingResize=false + 清 pending device px。observer 没了,无未来 pump 会置 _pendingResize=true环死。buffer 停在最后一步 _setSize 的尺寸(一步发散),warn 引导用户加 CSS,可接受。✓
  5. 同步 reflow 可靠性:读 clientWidth 强制同步 layout flush(浏览器既有行为),写 canvas.width 属性后读 clientWidth 同步返回新布局尺寸 → 检测在真机可靠。✓
  6. exact 设备像素路径 + 检测交互:检测放在 if/else 之后,同时覆盖 exact 与 fallback 两分支——两条路径都写 canvas.width → 无 CSS 则 clientWidth 变 → 检测触发;有 CSS 则 CSS 锁定 → 不误报。✓
  7. dpr=1 scale=1 边缘(headless e2e):buffer=300 → _setSize(300) no-op → clientWidth 300→300 不变 → 不触发。这不是漏报——此时环处于固定点(300→300),本就无发散;只有 buffer≠clientWidth(dpr>1 或 scale≠1)才发散,那些 case 检测都触发。dpr=1 scale=2:300→_setSize(600)clientWidth 变→触发。✓

fallback 守卫(setAutoResolution 无 observer 一次性路径)—— 防 0×0 buffer

-      const pixelRatio = window.devicePixelRatio * scale;
-      this._setSize(Math.round(webCanvas.clientWidth * pixelRatio), ...);
+      if (webCanvas.clientWidth > 0 && webCanvas.clientHeight > 0) {
+        const pixelRatio = window.devicePixelRatio * scale;
+        this._setSize(Math.round(webCanvas.clientWidth * pixelRatio), ...);
+      }

旧:unmounted(clientWidth=0)时 _setSize(0,0) = 0×0 buffer(坏)。新:守卫后跳过。✓ 该路径本就是「无 ResizeObserver = 一次性快照」(小程序语义),守卫只防 0×0,不新增 retry——与既有一次性语义一致,非回归。✓

设计方向(第一性原理复核)—— 检测→warn→exit 是此模型下的正解

对比替代方案:getComputedStyle 前置查 CSS(更重的 layout-forcing 调用 + auto/"" 跨浏览器语义模糊,更脆);observer-fire 前 baseline 快照(更多状态,当前 post-_setSize 比较更简且等价正确)。Three.js/Babylon setSize 同时写 canvas.width + canvas.style.width(强制 CSS),构造上永不成环,但它们的模型不分离 buffer/CSS;Galacean 本 PR 刻意分离 buffer resolution 与 CSS(setResolutionsetScale),无法强制 CSS——检测是此分离模型下的务实正解,比竞品更防御(主动检测误配置而非静默假设有 CSS)。非 band-aid:它精准处理真实失败模式(误配置 canvas)且用最佳可得信号。

注释合规 + console.warn 选型

  • 两处新 // 多句块(:117-119 记录快照 why、:135-137 检测 why)逐条打开代码验证属实、非 aspirational,解释 why(反馈环机制)非复述代码。末尾句号非 finding:70-71 同款多句 // 块(含句号)在我上轮已 approve 的 488b5b2827ed 即存在,本 commit 新块跟随同一既有文件惯例(项目惯例 > 教条,且我历史已放行该风格)。
  • console.warn 落在真能力降级路径(canvas 误配置 → auto-resize 无法工作 → 已禁用,用户应知情 + 给出可操作指引「设 CSS 宽高后调 setResolution/setAutoResolution」),选型合规(cr 前置检查 #6)。

核心机制未回退(独立复核 @ tip)

  • Canvas.ts @tip:read-only width/height getter、setResolution throw + Math.round:41)、_pumpPendingResize/_destroy /** @internal */:63/65)、_pumpPendingResize 基类 no-op、_setSize 尺寸守卫 + 单次 dispatch 全在
  • WebCanvas.ts @tip:read-only scale getter、setAutoResolution scale throw + OffscreenCanvas 早返回、observer device-pixel-content-box + try/catch + typeof ResizeObserver 守卫 + 一次性 fallback(现加 0×0 守卫)、_pumpPendingResize 0×0 保留 pending + exact 路径 + Safari fallback + 新增反馈环检测_exitAutoResize disconnect + 清 flag、_isOffscreenCanvas internal 全在

CI(e2e 3/4 in_progress 非失败,codecov/patch 是比值 artifact)

tip ecae89589e24build×3〔mac/ubuntu/win〕/ codecov / codecov/project / lint / labeler / e2e 1,2,4/4 SUCCESS;e2e 3/4 in_progress(非失败)。codecov/patch fail = 新增反馈环分支(无 CSS canvas 路径)在真机 headless Chromium e2e(恒有 CSS-sized canvas,不触发反馈环)下不覆盖,是环境特定分支的覆盖率比值 artifact,非测试失败(codecov job 本身 SUCCESS),同历轮判定,不作为 finding。e2e 走真机 resize 路径全绿即实证检测逻辑在有-CSS 常见路径零误报、像素零漂移。

已闭环清单(逐条对 tip 核对,未回退)

  • [P1 by cptbtptpbcptdtptp] 无 CSS 约束 auto-resize 布局反馈环指数发散 → 本 commit ecae89589e24 加快照检测 + _exitAutoResize,thread RESOLVED闭环——co-maintainer 最后一条真门控现已解除。
  • [P1 by cptbtptpbcptdtptp] 小程序 new ResizeObserver ReferenceErrortypeof ResizeObserver 守卫,RESOLVED闭环。
  • [P1 by cptbtptpbcptdtptp] content-box 接不住 DPR-onlydevice-pixel-content-boxRESOLVED闭环。
  • [P2 by cptbtptpbcptdtptp] setResolution 不整数化 / @internal d.ts 泄漏Math.round / 补齐,均 RESOLVED闭环。
  • [P2 我] OffscreenCanvas warn 过度触发 → 早前 force-push 撤除。闭环。
  • [P0] e2e resizeByClientSize 迁移 → 源码 @tip = 0。[P1] this-loss / lint → SUCCESS。[P2] observe 注释/白屏、isOffscreenCanvas internal、DPR 固化、scale throw、devicePixelContentBoxSize 精确设尺、deprecated forwarder 删除、autoResize create 选项删除、docs/README 迁移;[P3] pending 标志/字段位置/注释句号/悬挂引用。均关闭、未回退〔本轮仅动 25 行〕。

仍 open(非我 finding,非阻塞,仅记录不重复展开)

cptbtptpbcptdtptp[P3]〔OffscreenCanvas 上显式调用被静默丢弃〕resolved: falseoutdated: true)——该 reviewer 的独立非阻塞 finding,正常展开中,不由我代述或重复。

结论

approve:本轮线性追加一个 fix commit,修 no-CSS canvas 的 auto-resize 布局反馈环——① 一次性 fallback 加 clientWidth/Height > 0 守卫防 0×0 buffer;② _pumpPendingResize_setSize 前后快照 clientWidth/Height,检测到布局跟随 buffer 变化即 console.warn + _exitAutoResize()在第一个发散帧就打破指数发散——闭环 co-maintainer 最后一条真门控 [P1]〔无 CSS 约束反馈环,现 thread RESOLVED〕。逐 7 场景对抗性 trace 无正确性 bug:有 CSS canvas 检测零误报(关键,正常用户零影响)、无 CSS 首帧即触发、_setSize no-op 不误报(Canvas._setSize 尺寸守卫)、_exitAutoResize 彻底止环、clientWidth 读同步 reflow 真机可靠、检测置于 if/else 后覆盖 exact+fallback 两路、dpr=1 唯一「漏报」是固定点本就无发散的良性 case。设计方向经第一性原理复核是 buffer/CSS 分离模型下的务实正解(对比 getComputedStyle/baseline-snapshot 更简且等价,比 Three.js/Babylon 强制 CSS 更防御地检测误配置)。两处新 // 注释属实合规、末尾句号跟随我上轮已 approve 的既有文件惯例(非 finding)、console.warn 落真降级路径选型合规。核心机制未回退,历史全部闭环项未回退,CI 无失败(e2e 3/4 in_progress 非失败,codecov/patch 是环境特定分支比值 artifact)。我无 P0/P1/P2/新 finding。co-maintainer 全部 P1/P2 现均 RESOLVED,仅剩一条他人 [P3]〔OffscreenCanvas 静默丢弃,非阻塞 + outdated〕不由我重复。因 HEAD 已推过我上轮 approve 的 commit(reviewDecision = REVIEW_REQUIRED),在新 tip ecae89589e24 提交 APPROVE 维持 gate 清空。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants