Skip to content

fix(particle): make size-over-lifetime TwoCurves random mode work#3060

Merged
GuoLei1990 merged 9 commits into
dev/2.0from
fix/particle-sol-random-two
Jul 23, 2026
Merged

fix(particle): make size-over-lifetime TwoCurves random mode work#3060
GuoLei1990 merged 9 commits into
dev/2.0from
fix/particle-sol-random-two

Conversation

@cptbtptpbcptdtptp

@cptbtptpbcptdtptp cptbtptpbcptdtptp commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Problem

Size-over-lifetime's TwoCurves (random between two curves) mode has never worked since it was introduced in #1682, due to two stacked bugs:

  1. Operator precedence bug in SizeOverLifetimeModule._updateShaderData: isRandomCurveMode || separateAxes ? A : B parses as (isRandomCurveMode || separateAxes) ? A : B, so isCurveMode is always false when the mode is TwoCurves. Neither RENDERER_SOL_CURVE_MODE nor RENDERER_SOL_IS_RANDOM_TWO ever gets enabled — the module is entirely inert in TwoCurves mode (even the max curve is ignored), and the shader's random-two branch (mix(min, max, a_Random0.z) in SizeOverLifetime.glsl) was unreachable dead code.

  2. Missing per-particle random: instance-buffer slot 21 (a_Random0.z) was reserved for the SOL random factor in Add new particle renderer #1682 (left as a commented-out placeholder in _addNewParticle), and both SizeOverLifetime.glsl and NoiseModule.glsl sample it. Since feat(particle): add NoiseModule for simplex noise turbulence #2953 the CPU write is gated on noise.enabled only, so with noise disabled the factor is always 0 and size would stick to the min curve. The sub-emitter inherit-size CPU path (ParticleGenerator._getParticleColorAndSize) reads the same slot and was equally affected.

  3. Mesh render mode: SOL entirely dead (found in review, fixed in the second commit): computeParticleSizeMesh was gated on four macros (RENDERER_SOL_CURVE / RENDERER_SOL_RANDOM_CURVES / RENDERER_SOL_CURVE_SEPARATE / RENDERER_SOL_RANDOM_CURVES_SEPARATE) that no TS code has ever defined, and its random branch referenced a non-existent uniform u_SOLSizeGradientMax. Mesh particles ignored size-over-lifetime completely — plain Curve mode included — since Add new particle renderer #1682; the other half of the same port leftover.

Fix

  • Fix the parenthesization so TwoCurves enables the curve-mode + random-two macros and uploads both min and max curves.
  • Add _sizeRand to SizeOverLifetimeModule — the ParticleRandomSubSeeds.SizeOverLifetime sub-seed already existed unused — plus _isRandomMode() / _resetRandomSeed(), registered in _resetGlobalRandSeed.
  • Write slot 21 when SOL is in random mode and noise is disabled. Noise keeps precedence when both are enabled: the instance vertex layout is full (stride 168 B = 42 floats, all four a_Random0 components taken), so the two modules share the slot per the existing shader layout; keeping noise first preserves the existing noise random sequence. This sharing is an explicit layout-constrained trade-off, not final semantics — decoupling (a dedicated random slot) is left to a follow-up layout extension (see discussion below).
  • Rewrite the mesh branch of SizeOverLifetime.glsl onto the same macro set as the billboard path (RENDERER_SOL_CURVE_MODE / IS_RANDOM_TWO / IS_SEPARATE), extended to three axes. Pure shader-side fix — the TS upload side already provided all six curve uniforms.

Tests

  • tests/src/core/particle/SizeOverLifetime.test.ts with 6 cases covering the TS side (macro enabling, min/max curve upload, slot-21 writes): TwoCurves macros + upload, single-Curve regression, per-particle random written to a_Random0.z, slot untouched in non-random modes, noise precedence on the shared slot (same seed with/without SOL random yields the identical noise value), and mesh + separateAxes three-axis TwoCurves.
  • Red/green verified: with the first commit's source fix reverted, exactly the two targeted cases fail. Full particle suite passes.
  • e2e baseline particleRenderer-emit-mesh-size-over-lifetime: mesh render mode + separateAxes three-axis TwoCurves with a fixed seed — per-particle random size variation is baked into the image, so the mesh shader path (computeParticleSizeMesh) is guarded end-to-end across all three macros (RENDERER_SOL_CURVE_MODE / IS_RANDOM_TWO / IS_SEPARATE); unit tests assert TS-side state only, and before the fix this scene rendered uniform startSize cuboids. No remote assets, deterministic across runs. (An earlier revision hung on CI — not a shader issue: the case called engine.canvas.resizeByClientSize, which refactor(rhi-webgl): rework canvas resolution API, follow display size by default #3037 removed on dev/2.0 while PR CI builds the merge; dropped in 88bc982.) All pre-existing e2e cases use single-Curve SOL on billboard/stretched modes only, so no existing baselines are affected.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved particle size-over-lifetime random sizing to remain consistent after resets.
    • Corrected per-particle shared randomness handling so size-over-lifetime uses the proper random value when noise is disabled, while noise still takes precedence when enabled.
    • Refined size-over-lifetime shader mode selection for more reliable curve vs random behavior.
  • Tests
    • Added comprehensive Vitest coverage for size-over-lifetime shader macros, instance random-slot behavior, noise precedence, and mesh render mode with separate axes.

Two stacked bugs since #1682 left SOL's random-between-two-curves mode
entirely inert: an operator-precedence bug in _updateShaderData kept
RENDERER_SOL_CURVE_MODE / RENDERER_SOL_IS_RANDOM_TWO from ever being
enabled, and the per-particle random factor in instance slot 21
(a_Random0.z) was never written unless the noise module happened to be
enabled (#2953 gated the shared slot's write on noise.enabled only).

Fix the parenthesization, give SizeOverLifetimeModule its own Rand
(using the already-reserved SizeOverLifetime sub-seed), and write the
shared slot when SOL needs it; noise keeps precedence when both are on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 2, 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

This PR adds SizeOverLifetime random-seed handling, updates shader curve-mode selection, writes SizeOverLifetime random values to shared particle slot 21 when noise is disabled, and adds deterministic unit and end-to-end coverage.

Changes

SizeOverLifetime random seed integration

Layer / File(s) Summary
Random seed and mode detection
packages/core/src/particle/modules/SizeOverLifetimeModule.ts
Adds _sizeRand, _isRandomMode(), and _resetRandomSeed(), and refactors shader curve-mode selection.
Generator wiring
packages/core/src/particle/ParticleGenerator.ts
Resets the SizeOverLifetime seed and writes its random value to shared slot 21 when noise is disabled and random mode is active.
Shader and particle-buffer tests
tests/src/core/particle/SizeOverLifetime.test.ts
Tests shader macros, curve uploads, random-slot values, noise precedence, and separate-axis mesh rendering.
End-to-end mesh rendering coverage
e2e/case/particleRenderer-emit-mesh-size-over-lifetime.ts, e2e/config.ts, codecov.yml
Adds a mesh particle scenario, registers screenshot comparison settings, and excludes E2E files from coverage reporting.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ParticleGenerator
  participant SizeOverLifetimeModule
  participant ShaderData
  participant E2ERunner

  ParticleGenerator->>SizeOverLifetimeModule: reset random seed
  ParticleGenerator->>SizeOverLifetimeModule: request random size value
  SizeOverLifetimeModule-->>ParticleGenerator: random value
  ParticleGenerator->>ParticleGenerator: write shared slot 21
  SizeOverLifetimeModule->>ShaderData: upload curve data and macros
  E2ERunner->>ParticleGenerator: create mesh particle scenario
Loading

Possibly related PRs

  • galacean/engine#2982: Both PRs update ParticleGenerator._resetGlobalRandSeed with module-specific random-seed resets.
  • galacean/engine#3004: Both PRs extend module RNG handling through the particle generator seed-reset path.
  • galacean/engine#3071: Both PRs add an additional particle-module RNG domain to ParticleGenerator._resetGlobalRandSeed.

Suggested labels: bug, shader

Suggested reviewers: hhhhkrx, guolei1990

Poem

A rabbit hops through seeds so fine,
Size now dances in random time. 🐇
Noise steps back when SOL takes lead,
Slot twenty-one gets what it needs.
Tests confirm the values true—
Hop, hop, hooray, the module’s new!

🚥 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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main fix: size-over-lifetime TwoCurves random mode now works.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/particle-sol-random-two

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.

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.41%. Comparing base (9fe48c6) to head (d1b5cfc).

Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #3060      +/-   ##
===========================================
+ Coverage    79.82%   85.41%   +5.59%     
===========================================
  Files          904      810      -94     
  Lines       101297    94753    -6544     
  Branches     11423    11344      -79     
===========================================
+ Hits         80865    80938      +73     
+ Misses       20250    13727    -6523     
+ Partials       182       88      -94     
Flag Coverage Δ
unittests 85.41% <100.00%> (+5.59%) ⬆️

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.

@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.

🧹 Nitpick comments (2)
tests/src/core/particle/SizeOverLifetime.test.ts (2)

77-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for separateAxes + TwoCurves combination.

_isRandomMode() has a distinct branch requiring all three axes to be TwoCurves when separateAxes is true, but no test exercises separateAxes = true with random curves on x/y/z. This branch is part of the fixed precedence logic and would benefit from direct coverage.

🤖 Prompt for 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.

In `@tests/src/core/particle/SizeOverLifetime.test.ts` around lines 77 - 189, Add
a test in SizeOverLifetime.test.ts that covers the _isRandomMode branch for
separateAxes=true with all three axes set to TwoCurves. Reuse
createParticleRenderer, ParticleCompositeCurve, and the sizeOverLifetime setup,
then enable separateAxes on the SOL module and assign random TwoCurves to x/y/z
so the renderer exercises the distinct precedence path. Verify the expected
SOL_CURVE_MODE_MACRO and SOL_RANDOM_TWO_MACRO behavior and that the random slot
upload occurs as intended, similar to the existing TwoCurves and noise
precedence tests.

20-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid hardcoding the particle instance stride here.
ParticleBufferUtils isn’t part of the public API, so the test shouldn’t mirror 42 by hand; use a shared test helper or another source of the instance layout instead.

  • Add a separateAxes = true case so _isRandomMode() covers the all-axes branch.
🤖 Prompt for 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.

In `@tests/src/core/particle/SizeOverLifetime.test.ts` around lines 20 - 21, The
SizeOverLifetime test is hardcoding the particle instance stride and should
instead derive it from a shared test helper or the instance layout used by
particle buffers, so update the setup around FLOAT_STRIDE to avoid mirroring
ParticleBufferUtils internals. Also extend the _isRandomMode() coverage in
SizeOverLifetime by adding a separateAxes = true case so the all-axes branch is
exercised.
🤖 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.

Nitpick comments:
In `@tests/src/core/particle/SizeOverLifetime.test.ts`:
- Around line 77-189: Add a test in SizeOverLifetime.test.ts that covers the
_isRandomMode branch for separateAxes=true with all three axes set to TwoCurves.
Reuse createParticleRenderer, ParticleCompositeCurve, and the sizeOverLifetime
setup, then enable separateAxes on the SOL module and assign random TwoCurves to
x/y/z so the renderer exercises the distinct precedence path. Verify the
expected SOL_CURVE_MODE_MACRO and SOL_RANDOM_TWO_MACRO behavior and that the
random slot upload occurs as intended, similar to the existing TwoCurves and
noise precedence tests.
- Around line 20-21: The SizeOverLifetime test is hardcoding the particle
instance stride and should instead derive it from a shared test helper or the
instance layout used by particle buffers, so update the setup around
FLOAT_STRIDE to avoid mirroring ParticleBufferUtils internals. Also extend the
_isRandomMode() coverage in SizeOverLifetime by adding a separateAxes = true
case so the all-axes branch is exercised.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f245d376-8f8f-41a4-bdca-ab213fa6d856

📥 Commits

Reviewing files that changed from the base of the PR and between 721b42a and 2ce412e.

📒 Files selected for processing (3)
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/modules/SizeOverLifetimeModule.ts
  • tests/src/core/particle/SizeOverLifetime.test.ts

GuoLei1990

This comment was marked as outdated.

@hhhhkrx

hhhhkrx commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

建议这里不要让 SizeOverLifetime 的 random 和 Noise 的 random 共用同一个 a_Random0.z

现在的实现里,Noise 和 SOL TwoCurves 都读 attributes.a_Random0.z,并且两个模块同时开启时 slot 里写的是 noise random。这会导致 SOL 的曲线插值因子和 noise strength 的随机因子完全相关,两个本应独立的模块在视觉分布上被耦合了。

从 Unity / Unreal 的语义看,更常见的是“同一个粒子有可复现 seed”,但不同属性/模块从 seed 派生各自的随机值;只有用户显式创建共享 random attribute 时,多个属性才共用同一个随机因子。这里的共用更像是 instance buffer slot 不够时的实现取舍,不太适合作为默认行为。

建议优先考虑:

  1. 给 SOL TwoCurves 使用独立 random slot;或
  2. 从 particle seed / module sub-seed 派生一个独立 deterministic random,避免占用新的 attribute;或
  3. 如果现阶段必须复用这个 slot,至少在代码/PR 描述里明确这是临时取舍,并补测试覆盖 “Noise + SOL 同开时 SOL 会使用 noise random” 这个耦合行为。

这个点不影响本 PR 修复 “noise 关闭时 SOL random 恒为 0” 的方向,但我建议不要把共用 random 固化成最终设计。

computeParticleSizeMesh was gated on four macros (RENDERER_SOL_CURVE,
RENDERER_SOL_RANDOM_CURVES, RENDERER_SOL_CURVE_SEPARATE,
RENDERER_SOL_RANDOM_CURVES_SEPARATE) that no TS module ever enables —
the random branch even referenced a nonexistent uniform
(u_SOLSizeGradientMax) — so the module was entirely inert for mesh-mode
particles. Rewrite it against the macro set the billboard path and
SizeOverLifetimeModule already use, extended to all three axes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

CR 结论

逐条核对了 diff 与既有评审意见。billboard 路径的修复本身验证无误:宏四态枚举正确、slot 21 写入条件与宏启用条件一致(enabled && _isRandomMode())、curveMin 裸调与 Force/Noise/Rotation/Velocity 等模块的既有 GIGO 约定一致、与 #3049 无 instance-slot 交叠、_resetGlobalRandSeed 各子种子独立顺序无关。

新发现:P2 — mesh 渲染模式下 SOL 整体失效(已修,3447163a7)

computeParticleSizeMesh(SizeOverLifetime.glsl)gate 在 RENDERER_SOL_CURVE / RENDERER_SOL_RANDOM_CURVES / RENDERER_SOL_CURVE_SEPARATE / RENDERER_SOL_RANDOM_CURVES_SEPARATE 四个宏上——全仓 TS 侧从未定义过任何一个(SizeOverLifetimeModule 只启用 RENDERER_SOL_CURVE_MODE / IS_SEPARATE / IS_RANDOM_TWO),random 分支还引用了不存在的 uniform u_SOLSizeGradientMax。即 mesh 粒子的 SOL 自 #1682 起完全 inert(不止 TwoCurves,单 Curve 也一样)——与本 PR 修的 billboard 优先级 bug 是同一次移植的另一半遗留。base 里 _evaluateOverLifetime 的 CPU inherit-size 路径按 "shader gates on RENDERER_SOL_CURVE_MODE" 假设工作,mesh 死宏还造成 CPU/GPU 不一致。

修复:按 billboard 路径的宏体系重写 mesh 分支(扩展到三轴)。uniform 声明块与 TS 上传侧本来就齐备(X/Y/Z min/max),纯 shader 侧修复。

Baseline 影响:9 个 mesh e2e case 虽解构了 sizeOverLifetime 变量但没有一个启用它,修复不影响任何现有 e2e baseline。

验证:

  • 新增用例 mesh + separateAxes + 三轴 TwoCurves:宏(CURVE_MODE/IS_RANDOM_TWO/IS_SEPARATE)、三轴 min/max 曲线上传、slot 21 写入全断言通过;粒子套件 157/157 过。
  • 真机像素级验证(examples + WebGL readPixels,双系统对比):SOL 关的对照组粒子横截面恒定 33px;SOL TwoCurves 组尺寸随生命周期 7px→77px 增长且个体间随机抖动,shader 编译零错误。修复前该组与对照组行为相同(恒定)。

对既有意见的处理

  • GuoLei1990 / CodeRabbit 的 P3(缺 separateAxes+TwoCurves 覆盖):新增的 mesh 用例即 separateAxes=true + 三轴 TwoCurves,_isRandomMode() 的 separate 分支已获直接覆盖。
  • CodeRabbit 的 stride 硬编码(Trivial):维持现状——ParticleBufferUtils.instanceVertexFloatStride 非公开 API,42 与 stride 168B 的注释已互为印证,引入访问私有工具类的 helper 收益低于成本。

GuoLei1990

This comment was marked as outdated.

The mesh-path SOL shader rewrite (3447163) had no automated guard:
unit tests only assert TS-side macro/upload/slot state, and no e2e case
combined mesh render mode with an enabled sizeOverLifetime module. This
case pins computeParticleSizeMesh end-to-end: separateAxes TwoCurves on
all three axes with a fixed seed, so per-particle random size variation
is baked into the baseline — before the fix every cuboid rendered at
constant startSize.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cptbtptpbcptdtptp

Copy link
Copy Markdown
Collaborator Author

@hhhhkrx 感谢指出,这个耦合确实存在,你的三条建议逐一回应:

现状与约束:a_Random0 四个分量当前全部有主——.x = gravity、.y = colorOverLifetime、.z = noise/SOL、.w = rotationOverLifetime(instance stride 168 B = 42 floats)。给 SOL 加独立随机必须新增 attribute 并动整个 instance 布局(CPU 写入、ParticleBufferUtils 全部 offset、shader 声明),这超出了本 bugfix 的范围,所以本 PR 沿用了既有槽位并让 noise 优先(保住 #2953 以来的 noise 随机序列不变)。

方案 2(shader 内从 seed 派生) 我们评估过:GPU 端需要浮点 hash,跨设备精度差异会导致分布不一致,且 sub-emitter inherit-size 的 CPU 路径(_evaluateOverLifetime 读同一 slot)必须复刻同一派生逻辑才能保持 CPU/GPU 一致,复杂度不低,不适合塞进本修复。

方案 3(文档化 + 测试覆盖) 首个提交已包含:_addNewParticle slot 21 写入处的注释、PR 描述里的取舍说明,以及专门钉住该行为的用例 "noise random keeps precedence on the shared slot when both modules are enabled"(同种子下 noise-only 与 noise+SOL 两个 generator 的 slot 值断言一致——既锁"SOL 用的是 noise random",也锁"SOL 不扰动 noise 序列")。

同意不把共用固化为最终设计:这是槽位布局约束下的显式取舍,不是语义终态。独立 random slot(方案 1)留作后续扩展 instance 布局时的独立 PR 处理。

@cptbtptpbcptdtptp

cptbtptpbcptdtptp commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator Author

补充:mesh + SOL 的 e2e baseline(8e9560c88bc982)

回应 @GuoLei1990 的 P2(mesh 路径 shader 修复无自动化守护、e2e 无 mesh+SOL 组合):

新增 particleRenderer-emit-mesh-size-over-lifetime 用例——mesh 渲染模式 + separateAxes 三轴 TwoCurves + 固定种子,单次 burst 16 个 cuboid,粒子间尺寸/轴比随机差异直接烘进基线。修复前(mesh 死宏)该画面是清一色等大的 startSize 方块,与基线像素差巨大——computeParticleSizeMesh 的全部三个宏(RENDERER_SOL_CURVE_MODE / IS_RANDOM_TWO / IS_SEPARATE)的 shader 执行由此进入像素回归网。用例不加载远程资源(纯色 ParticleMaterial),消除网络波动。

排障记录(88bc982,供后人参考):该用例首版在 CI 曾 3×180s 静默超时(黑屏、无 console 报错),一度误归因为 SwiftShader 对特定宏组合的编译问题;经带打点的 probe 定位(#3072,已关闭),真正原因是:PR CI 构建的是与 dev/2.0 的 merge 产物,base 上 #3037 已移除 engine.canvas.resizeByClientSize(并同步更新了 78 个既有 e2e case),而本分支新写的 case 照旧惯例调用了它——then 链头抛 TypeError 进 unhandled rejection,截图按钮永不出现;本地分支自身构建仍含该 API,故本地全绿。删掉该调用后,完整的 separateAxes 三轴 TwoCurves 用例在 CI 正常运行,与 shader 变体完全无关。给同样从较旧分叉点开 PR 的同学提个醒:新增 e2e 请按 dev/2.0 当前惯例书写。

本地验证:用例连续两次运行像素级 match,Particle 分类 26/26 通过(新用例阈值 0.005 取既有 mesh 用例区间中位)。

另:codecov/patch 在纯 e2e 提交上假红(vitest 覆盖收集把 e2e/**/*.ts 以 0% 计入 report,无源码行稀释时 97%→35%),已加 codecov.yml ignore e2e/** 根修(94dc4fe)。

PR 描述中的测试措辞已按建议校准:单测覆盖 TS 侧宏/上传/slot 写入,shader 执行由本 e2e baseline 守护。

cptbtptpbcptdtptp and others added 2 commits July 19, 2026 21:16
Vitest's coverage collection sweeps e2e/**/*.ts into the report at 0%
(they only run under Playwright, never under the unit-test runner), so
any e2e-only commit craters codecov's patch metric — this PR's baseline
commit dropped it from 97% to 35% with zero source changes. Ignore the
directory at the codecov layer; unit coverage semantics are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The RENDERER_SOL_IS_SEPARATE + RENDERER_SOL_IS_RANDOM_TWO macro combo
hangs SwiftShader's shader compiler (evaluateParticleCurve inlined at
10 call sites in one program), so the case never reached initScreenshot
on GPU-less CI runners while passing on real GPUs. Non-separate
TwoCurves keeps the mesh path and the per-particle random mix under
pixel guard; the separate combo stays covered by unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…call

The earlier CI hangs had nothing to do with shader compilation: PR CI
builds the merge with dev/2.0, where #3037 removed
engine.canvas.resizeByClientSize (updating all existing e2e cases), so
any newly added case still calling it died in an unhandled rejection
before initScreenshot — silently, since the rejection never hits the
console. Locally the branch's own build still had the API, which is why
every variant passed here. With the call dropped, the full
separateAxes three-axis TwoCurves case runs fine on CI, so restore it
(macro coverage: CURVE_MODE + IS_RANDOM_TWO + IS_SEPARATE) along with
its original baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

总结

自上轮 review(tip 3447163a7)以来的增量是纯测试 / CI 基础设施packages/ 源码(.ts / .glsl)零改动——compare 3447163a7...88bc9825d 只有 4 个文件:新增 e2e/case/particleRenderer-emit-mesh-size-over-lifetime.ts + e2e/config.ts 注册 + baseline 图 + codecov.yml。这个 commit 直接闭环我上轮唯一的 [P2](mesh shader 修复无自动化守护、无 mesh+SOL 组合 e2e)。逐条核对后无 P0/P1/P2/P3 新问题,可合

已关闭问题清单(本轮全部闭环)

  • [P2 我上轮] mesh shader 修复无回归守护 → ✅ 已修:新 e2e 用例 mesh 渲染 + separateAxes 三轴 TwoCurves + 固定种子。逐链核实这是真反向证伪守护,非空壳:
    • sizeX/Y/Z 各用 new ParticleCompositeCurve(curveMin, curveMax)(两 ParticleCurve 参)→ 构造器落 mode = TwoCurvesParticleCompositeCurve.ts:154-157);separateAxes = true + 三轴全 TwoCurves → _isRandomMode() 返回 true(SizeOverLifetimeModule.ts:181-185)→ isCurveMode true → 三宏 RENDERER_SOL_CURVE_MODE / IS_SEPARATE / IS_RANDOM_TWO 全启用,正是重写后 computeParticleSizeMesh 依赖的宏组合。
    • noise 未启用 + _isRandomMode() true → slot 21(a_Random0.z)写入 _sizeRand.random() → mesh shader mix(min,max,a_Random0.z) 产出逐粒子尺寸差异。修复前 mesh 分支挂死宏 → 16 个 cuboid 全渲染成等大 startSize(0.7),与 baseline 像素差巨大。revert 任一 commit 的 fix(TS 侧或 .glsl mesh 重写)都会改变基线图 → 真守护成立
  • [P2 我上轮] PR 描述"覆盖 mesh separateAxes"措辞 → ✅ 已校准:现为"单测覆盖 TS 侧状态,mesh shader 路径由 e2e baseline 端到端守护"。
  • [P3 我首轮] 缺 separateAxes 三轴 TwoCurves 用例 → ✅ 已覆盖(单测 + 本 e2e)。
  • [hhhhkrx] SOL/Noise 共用 a_Random0.z 耦合 → ✅ 作者解释成立(已闭环):instance 布局已满(168B=42 floats,四分量全有主),独立 slot 需动整个布局超出本 bugfix scope;noise 优先已文档化 + 测试钉住;明示为布局约束下的显式取舍非语义终态。按已合理解释处理,不再重提。

核对要点

  • e2e 确定性useAutoRandomSeed = false 固定种子,SphereShape 发射随之确定;作者报连续 2× 像素级 match,CI e2e 4/4 全绿。threshold:0 + diffPercentage:0.005 取既有 mesh 用例区间,零色差容忍 + 薄 buffer,与仓库惯例一致。
  • CI-hang 根因已消除:final 用例无 resizeByClientSize 调用(#3037 已在 dev/2.0 移除该 API 而 PR CI 构建 merge 产物,旧版调用致 unhandled rejection 静默超时)。churn 两 commit(12c2cfca8 drop separateAxes → 88bc9825d restore + drop canvas API)净成预期三轴版,无残留。
  • codecov.yml ignore e2e/** 是正确根修,非掩盖覆盖率:e2e 是截图场景用例,从不被单测覆盖,把 e2e/**/*.ts 以 0% 计入 report 会稀释 patch coverage(无源码行时 97%→35%);只排除 e2e、不碰任何 packages/ 源码,codecov 现全绿。
  • CI 全绿(build 3/3 + e2e 4/4 + codecov 全项 + lint)。

简化建议

无。本轮是纯回归网补齐,是净收敛。

@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.

🫧 尘小沫

总结

自上轮 review(tip 88bc9825d)以来,1e5480c73 只是把最新 dev/2.0 合回分支;PR 自有净增量是 b1000bb3b 的谓词复用重构。逐链核对后,SOL 的 mode 判定、slot 21 写入、shader 宏/曲线上传、sub-emitter CPU 求值与 bounds 均保持原语义,先前三个 bugfix 和 mesh e2e 守护没有回归;CI 13/13 全绿。无 P0/P1,存在一项可在本 PR 内直接收敛的 P2 冗余。

已关闭问题清单

  • mesh shader 无端到端守护:88bc9825d 的 mesh + separateAxes 三轴 TwoCurves e2e 已闭环,本轮仍有效。
  • separateAxes 三轴 TwoCurves 单测缺口与 PR 描述措辞:均已闭环,本轮不重提。
  • Noise/SOL 共用 a_Random0.z:instance 布局约束、noise 优先和测试契约均已解释并验证成立,本轮不重提。

问题

  • [P2] packages/core/src/particle/modules/ParticleCompositeCurve.ts:287 — 新增的 _isRandomCurveMode() 只是 mode === ParticleCurveMode.TwoCurves 的单消费者包装;而同一个类型已经以 mode 持有权威事实,并有 _isCurveMode() / _isRandomMode() 两个通用分类。现在又新增第三个可机械推导的分类,并在 ParticleCurve.test.ts 复制四格真值表,增加了一个需要随 enum 演进同步的 owner,却没有减少 SizeOverLifetime 的模块级组合逻辑。建议保留 ParticleCompositeCurve.mode 为权威 owner,删除本次新增 helper 及四条对应断言,让 SizeOverLifetimeModule._isRandomCurveMode() 在唯一消费点直接判断各轴 mode === TwoCurves(或由已有两个通用谓词机械组合)。删除后数据流仍是 curve mode → SOL 跨轴聚合 → slot/macro/shader,行为与现有 6 条 SOL 单测及 e2e 完全等价。

简化建议

除上述删除外无新增建议。SOL 模块级 helper 仍有两个真实消费者(shader 状态与新粒子 slot 写入),应保留;应删除的是 ParticleCompositeCurve 上这层仅服务它的额外包装。

@GuoLei1990
GuoLei1990 merged commit 05bdfd9 into dev/2.0 Jul 23, 2026
12 checks passed
@GuoLei1990
GuoLei1990 deleted the fix/particle-sol-random-two branch July 24, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants