Skip to content

feat(particle): support continuous Birth sub-emitters - #3071

Open
hhhhkrx wants to merge 100 commits into
galacean:dev/2.0from
hhhhkrx:fix/particle-orbital-linear-order
Open

feat(particle): support continuous Birth sub-emitters#3071
hhhhkrx wants to merge 100 commits into
galacean:dev/2.0from
hhhhkrx:fix/particle-orbital-linear-order

Conversation

@hhhhkrx

@hhhhkrx hhhhkrx commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

背景

Birth 子发射器此前只会在父粒子创建时固定触发一次,无法让目标粒子系统在父粒子的整个生命周期内独立运行自己的发射时间线;父子系统的更新还依赖 Renderer 注册顺序与相机裁剪,父粒子的完整 GPU 轨迹也无法直接传递给子粒子。

本 PR 将 Birth 扩展为连续子发射模式,并补齐父子粒子系统的调度、轨迹传递、速度继承、Ring 生命周期与 Bounds 管理。

功能行为

  • 每个存活父粒子、每个 Birth 槽位分别持有一条目标系统的发射时间线。
  • Birth 使用目标系统自身的 Start DelayDurationLoopRate over TimeBurst 配置。
  • Death 在父粒子生命周期结束时,按 deathEmitCount 一次性产生子粒子。
  • emitProbability 按父粒子、子发射器槽位独立采样。
  • 颜色、尺寸、旋转和速度继承继续由 inheritProperties 控制。
  • 新增 InheritVelocityModule,支持:
    • Initial:粒子出生时采样发射器速度,并按曲线作用于粒子生命周期。
    • Current:粒子存活期间持续使用发射器当前速度。

实现思路

独立的 Birth 发射状态

  • SubEmittersModule 为每个父粒子 Ring 槽位和 Birth 子发射器槽位维护独立 EmissionState
  • 状态只保存发射游标与随机流;EmissionModule 仍是发射配置和 Rate/Burst 算法的唯一所有者。
  • CPU 只生成包含事件时间、数量、父粒子 Ring 索引和继承配置的轻量 Birth/Death Command,由目标 ParticleGenerator 统一创建粒子。
  • State、Command 和内部请求对象均按高水位复用,并随 Ring 扩容、迁移、退休和清空同步维护。

GPU 轨迹 Gather

flowchart LR
  A[父系统 Transform Feedback Buffer] --> B[SubEmitterTrajectoryGather Pass]
  C[CPU Birth / Death Command] --> B
  B --> D[与子粒子槽位对齐的 Spawn State Buffer]
  D --> E[子系统 Transform Feedback 或 Forward Pass]
Loading
  • 父系统 Transform Feedback 持续输出粒子的世界位置和完整轨迹速度。
  • ParticleSubEmitterSpawnState 根据 Command 中的父 Ring 索引与子粒子数量,在 GPU 上复制、展开并重排父轨迹,使其与新生子粒子槽位一一对应。
  • Spawn State Buffer 直接作为子系统 Transform Feedback 或 Forward Pass 的 Attribute 输入。
  • 发射时刻位于两帧之间时,Shader 使用父轨迹速度和时间偏移还原对应的父世界坐标。
  • 当前实现不使用 Fence、getBufferSubData 或任何 GPU→CPU 轨迹回读,CPU 不等待 GPU。

父子系统调度

  • ParticleSystemManager 维护场景内的粒子系统依赖图,并按父系统到子系统的拓扑顺序更新。
  • 每个粒子系统每帧最多更新一次;拓扑只在槽位或组件关系变化时重建。
  • 父系统可见且仍可能向下游发射时,会继续驱动其子发射依赖;独立播放且可见的目标系统也可以作为更新根节点。
  • 配置阶段拒绝直接或间接循环依赖,父子 Renderer 必须属于同一 Scene。

Transform Feedback 与 Bounds

  • 新旧粒子在同一个 Feedback Pass 中更新,避免为同一帧拆分额外 Feedback Draw。
  • Gather 会合并父索引连续、子粒子数量一致且目标槽位连续的范围,并在连续输出区间复用同一 Transform Feedback Scope。
  • 同帧退休的容量可以立即用于新粒子;Ring 扩容、缩容和跨界时同步重映射 Feedback 与 Spawn State。
  • ParticleBounds 独立管理历史发射 Bounds、继承速度位移和退休清理,既保持保守裁剪,也避免一次极端位移永久放大 Bounds。

API 与兼容性

  • 新增 ParticleGenerator.inheritVelocity: InheritVelocityModule
  • 新增 ParticleInheritVelocityMode.Initial / Current
  • SubEmittersModule.addSubEmitter(...) 返回创建的 SubEmitter
  • SubEmitter.emitCount 更名为 deathEmitCount,明确该数量只作用于 Death;这是不兼容 API 变更。

限制

  • SubEmitter 功能依赖 WebGL2;WebGL1 下模块保持停用。
  • 连续 Birth 当前支持 Rate over TimeBurst,不支持 Rate over Distance
  • Rate over Distance 的发射数量依赖 GPU 中的父粒子轨迹;本版本选择保持轨迹纯 GPU 化,不引入 GPU→CPU 回读。
  • InheritVelocityMode.Current 仅在 World Simulation 且 WebGL2 环境下生效。

性能与 GC

  • 无同步或异步 GPU Readback,也没有 CPU/GPU Fence 轮询。
  • 稳态热路径不创建临时对象、MapSet 或排序容器。
  • Birth State、Birth/Death Command、TypedArray 与 GPU Buffer 均池化或按高水位复用。
  • 不需要轨迹反馈的目标系统继续使用公式模拟;Spawn State 按需创建和释放。
  • 场景级调度为一次线性遍历,Gather 仅处理实际产生的子粒子请求。

验证

  • 单测覆盖:逐父粒子 Rate over Time / Burst / Start Delay / Loop、Death、概率、属性和速度继承、时间边界、容量复用、Ring 跨界与迁移、共享目标、多层拓扑、循环检测、裁剪恢复、设备状态恢复、Bounds 与 GPU Gather。
  • E2E 覆盖连续子发射器与 Inherit Velocity 的渲染结果。
  • 当前 HEAD 的 Lint、Windows/macOS/Linux Build、4 组 E2E 与 Codecov 检查均通过。

@hhhhkrx
hhhhkrx requested a review from GuoLei1990 July 17, 2026 15:58
@coderabbitai

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

Particle simulation now uses scene-level scheduling, queued sub-emitter commands, pooled emission runtime state, Birth/Death lifecycle tracking, transform-feedback synchronization, and configurable parent-velocity inheritance. Public particle exports and coverage tests are updated.

Changes

Particle system runtime

Layer / File(s) Summary
Emission sampling and velocity contracts
packages/core/src/particle/modules/*, packages/core/src/particle/enums/*, packages/core/src/particle/index.ts, packages/core/src/particle/ParticleGenerator.ts
Emission cursors use pooled runtime state, and generators expose configurable inherited velocity with deterministic random seeding.
System sub-emitter lifecycle
packages/core/src/particle/modules/SubEmitter.ts, packages/core/src/particle/modules/SubEmittersModule.ts, packages/core/src/particle/ParticleGenerator.ts
Birth and Death sub-emitters track per-particle state, sample parent motion, enqueue commands, and preserve timing and inherited spawn data.
Scene particle scheduling
packages/core/src/ComponentsManager.ts, packages/core/src/Engine.ts, packages/core/src/particle/ParticleSystemManager.ts, packages/core/src/particle/ParticleRenderer.ts
Scene managers register particle renderers and update them in dependency order while passing queued sub-emitter commands.
Feedback preservation and validation
packages/core/src/particle/ParticleTransformFeedbackSimulator.ts, tests/src/core/particle/*
Partial transform-feedback passes preserve untouched slots, with tests covering scheduling, timing, inheritance, resizing, and velocity integration.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Engine
  participant ParticleSystemManager
  participant ParentParticleRenderer
  participant ChildParticleRenderer
  participant ParticleGenerator
  Engine->>ParticleSystemManager: update(deltaTime)
  ParticleSystemManager->>ParentParticleRenderer: update parent particles
  ParentParticleRenderer->>ParticleGenerator: process births and deaths
  ParticleGenerator->>ParticleSystemManager: enqueue child emission command
  ParticleSystemManager->>ChildParticleRenderer: update with queued command
  ChildParticleRenderer->>ParticleGenerator: spawn inherited child particles
Loading

Possibly related PRs

Suggested labels: enhancement, particle

Suggested reviewers: cptbtptpbcptdtptp, guolei1990

Poem

I’m a rabbit watching particles fly,
Through queued little sparks across the sky.
Birth follows birth in ordered streams,
Velocities dance through feedback dreams.
Hop, hop—new trails now bloom! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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 and concisely describes the PR's main change: adding support for continuous Birth sub-emitters.
✨ 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.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.41244% with 135 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.81%. Comparing base (8e11477) to head (3e41a6d).
⚠️ Report is 2 commits behind head on dev/2.0.

Files with missing lines Patch % Lines
packages/core/src/particle/ParticleBounds.ts 94.02% 58 Missing ⚠️
packages/core/src/particle/ParticleGenerator.ts 97.64% 20 Missing and 1 partial ⚠️
...ges/core/src/particle/modules/SubEmittersModule.ts 94.33% 20 Missing ⚠️
.../core/src/particle/ParticleSubEmitterSpawnState.ts 97.04% 8 Missing ⚠️
...src/particle/modules/VelocityOverLifetimeModule.ts 90.90% 5 Missing ⚠️
...ckages/core/src/particle/modules/EmissionModule.ts 97.01% 4 Missing ⚠️
...src/particle/ParticleTransformFeedbackSimulator.ts 96.77% 3 Missing ⚠️
...ore/src/particle/modules/BirthSubEmitterCommand.ts 97.08% 3 Missing ⚠️
...ges/core/src/graphic/TransformFeedbackSimulator.ts 94.59% 2 Missing ⚠️
...ackages/core/src/particle/ParticleSystemManager.ts 98.56% 2 Missing ⚠️
... and 5 more
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #3071      +/-   ##
===========================================
+ Coverage    85.41%   85.81%   +0.39%     
===========================================
  Files          811      821      +10     
  Lines        94654    96793    +2139     
  Branches     11510    11969     +459     
===========================================
+ Hits         80849    83063    +2214     
+ Misses       13715    13639      -76     
- Partials        90       91       +1     
Flag Coverage Δ
unittests 85.81% <96.41%> (+0.39%) ⬆️

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 406c0ca819

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1042 to +1045
if (parentWorldVelocity && this.inheritVelocity.enabled) {
const inheritFactor = this.inheritVelocity.curve.evaluate(
normalizedEmitAge,
this.inheritVelocity._curveRand.random()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor Current inherit-velocity mode

When child.generator.inheritVelocity.mode is set to ParticleInheritVelocityMode.Current, this path still applies the sampled parent velocity only once to the child's initial direction and speed, exactly like Initial; mode is never read elsewhere. Consequently, a living child does not react when its parent velocity subsequently changes, despite the exported enum documenting that behavior for Current. Either implement the per-frame parent-velocity linkage/update or avoid exposing Current as supported.

Useful? React with 👍 / 👎.

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/particle/modules/SubEmitter.ts (1)

45-66: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject the unsupported Death + System configuration.

System processing only handles Birth slots, while event dispatch excludes every System slot. The currently accepted combination therefore never emits.

  • packages/core/src/particle/modules/SubEmitter.ts#L45-L66: validate the type/mode invariant in both setters.
  • packages/core/src/particle/modules/SubEmittersModule.ts#L106-L123: reject the invalid pair during addSubEmitter.
🤖 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 `@packages/core/src/particle/modules/SubEmitter.ts` around lines 45 - 66,
Reject the unsupported Death + System combination. In
packages/core/src/particle/modules/SubEmitter.ts lines 45-66, enforce the
type/mode invariant in both the mode and type setters; in
packages/core/src/particle/modules/SubEmittersModule.ts lines 106-123, apply the
same validation when addSubEmitter accepts a slot so invalid configurations
cannot be registered.
🤖 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 `@packages/core/src/particle/modules/EmissionModule.ts`:
- Around line 295-319: Update _emitByBurst to iterate through the interval from
lastPlayTime to playTime, splitting emission segments at each actual duration
boundary. Handle updates within later cycles instead of limiting the fallback to
the first cycle, and ensure ranges such as 0.1–1.1 stop at 1.0 rather than
processing the next full cycle. Reset state.currentBurstIndex only after
reaching a cycle boundary, preserving _emitBySubBurst for each segment.

In `@packages/core/src/particle/modules/SubEmittersModule.ts`:
- Around line 270-280: In the recreated-state branch of the SubEmittersModule
update, evaluate and assign startDelay before resetting state.emission. Reset
the emission to the parent’s current emission time after applying the delay,
clamping that time to target.duration for non-looping targets, instead of
resetting it to zero; preserve the existing previousParentAge and position
initialization.

In `@packages/core/src/particle/ParticleGenerator.ts`:
- Around line 1042-1069: Update the inherited-velocity handling in the particle
generation path to branch on inheritVelocity.mode: retain the existing one-time
combination for Initial mode, while Current mode must preserve the inheritance
configuration on the particle and apply the parent’s current velocity during its
lifetime. Use the existing particle update/inheritance symbols and avoid baking
Current-mode velocity into the initial direction and speed.
- Around line 709-710: Update the runtime remapping logic around the
migrateCount/runtimeMappings push to also map the wrapped front segment when
firstRetiredElement is greater than firstFreeElement. Preserve both occupied
ranges—[firstRetiredElement, end) and [0, firstFreeElement)—so front-segment
sub-emitter cursors remain mapped during shrink.

In `@packages/core/src/particle/ParticleSystemManager.ts`:
- Around line 55-59: Update ParticleSystemManager.update so commands enqueued
before the scheduler runs, including Birth sub-emitter commands from onUpdate,
remain available for consumption. Replace the unconditional
this._commands.clear() with logic that preserves the existing entry queue and
removes only commands genuinely consumed during processing, while retaining
filtering of destroyed or disabled renderers.

---

Outside diff comments:
In `@packages/core/src/particle/modules/SubEmitter.ts`:
- Around line 45-66: Reject the unsupported Death + System combination. In
packages/core/src/particle/modules/SubEmitter.ts lines 45-66, enforce the
type/mode invariant in both the mode and type setters; in
packages/core/src/particle/modules/SubEmittersModule.ts lines 106-123, apply the
same validation when addSubEmitter accepts a slot so invalid configurations
cannot be registered.
🪄 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: 1c585c35-e3a5-41c4-8466-e90ac922918d

📥 Commits

Reviewing files that changed from the base of the PR and between 9fe48c6 and 406c0ca.

⛔ Files ignored due to path filters (3)
  • e2e/fixtures/originImage/Particle_particleRenderer-sub-emitter.jpg is excluded by !**/*.jpg
  • e2e/fixtures/originImage/Particle_particleRenderer-velocity-orbital-constant.jpg is excluded by !**/*.jpg
  • packages/shader/src/Shaders/Effect/ParticleFeedback.shader is excluded by !**/*.shader
📒 Files selected for processing (17)
  • packages/core/src/ComponentsManager.ts
  • packages/core/src/Engine.ts
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/ParticleRenderer.ts
  • packages/core/src/particle/ParticleSystemManager.ts
  • packages/core/src/particle/ParticleTransformFeedbackSimulator.ts
  • packages/core/src/particle/enums/ParticleInheritVelocityMode.ts
  • packages/core/src/particle/enums/ParticleRandomSubSeeds.ts
  • packages/core/src/particle/enums/ParticleSubEmitterMode.ts
  • packages/core/src/particle/index.ts
  • packages/core/src/particle/modules/EmissionModule.ts
  • packages/core/src/particle/modules/EmissionRuntimeState.ts
  • packages/core/src/particle/modules/InheritVelocityModule.ts
  • packages/core/src/particle/modules/SubEmitter.ts
  • packages/core/src/particle/modules/SubEmittersModule.ts
  • tests/src/core/particle/SubEmitter.test.ts
  • tests/src/core/particle/VelocityOverLifetime.test.ts

Comment thread packages/core/src/particle/modules/EmissionModule.ts Outdated
Comment thread packages/core/src/particle/modules/SubEmittersModule.ts Outdated
Comment thread packages/core/src/particle/ParticleGenerator.ts Outdated
Comment thread packages/core/src/particle/ParticleGenerator.ts Outdated
Comment thread packages/core/src/particle/ParticleSystemManager.ts Outdated
GuoLei1990

This comment was marked as outdated.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/particle/modules/SubEmittersModule.ts (1)

333-333: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize the end of a non-looping emission to 1.

At sample.time === duration, modulo returns 0, causing end-of-duration curves such as inherited velocity to evaluate at their beginning.

Proposed fix
-            const emissionNormalizedTime = duration > 0 ? (sample.time % duration) / duration : 0;
+            const emissionNormalizedTime =
+              duration > 0
+                ? target.main.isLoop
+                  ? (sample.time % duration) / duration
+                  : Math.min(sample.time / duration, 1)
+                : 0;
🤖 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 `@packages/core/src/particle/modules/SubEmittersModule.ts` at line 333, Update
the emissionNormalizedTime calculation in the SubEmittersModule sampling logic
so a non-looping emission at sample.time === duration normalizes to 1 instead of
wrapping to 0; preserve modulo-based looping behavior for times within or beyond
looping emissions.
🤖 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.

Outside diff comments:
In `@packages/core/src/particle/modules/SubEmittersModule.ts`:
- Line 333: Update the emissionNormalizedTime calculation in the
SubEmittersModule sampling logic so a non-looping emission at sample.time ===
duration normalizes to 1 instead of wrapping to 0; preserve modulo-based looping
behavior for times within or beyond looping emissions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9d5e48ed-ad5f-4fe5-ad4e-71f7a502df84

📥 Commits

Reviewing files that changed from the base of the PR and between d30870d and af9f5de.

⛔ Files ignored due to path filters (1)
  • e2e/fixtures/originImage/Particle_particleRenderer-velocity-orbital-constant.jpg is excluded by !**/*.jpg
📒 Files selected for processing (8)
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/ParticleRenderer.ts
  • packages/core/src/particle/ParticleSystemManager.ts
  • packages/core/src/particle/index.ts
  • packages/core/src/particle/modules/EmissionRuntimeState.ts
  • packages/core/src/particle/modules/SubEmitter.ts
  • packages/core/src/particle/modules/SubEmittersModule.ts
  • tests/src/core/particle/SubEmitter.test.ts
💤 Files with no reviewable changes (1)
  • packages/core/src/particle/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/core/src/particle/ParticleSystemManager.ts
  • packages/core/src/particle/modules/EmissionRuntimeState.ts
  • packages/core/src/particle/ParticleRenderer.ts
  • packages/core/src/particle/ParticleGenerator.ts

GuoLei1990

This comment was marked as outdated.

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

♻️ Duplicate comments (2)
packages/core/src/particle/ParticleGenerator.ts (2)

1041-1045: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Implement Current mode instead of always baking inherited velocity at spawn.

This logic evaluates the inherit curve once and bakes the result into the particle's initial direction and speed. Current mode should not be baked here, as it is designed to continuously follow the parent's velocity during the particle's lifetime.

Gate this initialization step on this.inheritVelocity.mode === ParticleInheritVelocityMode.Initial.

🐛 Proposed fix
-    if (parentWorldVelocity && this.inheritVelocity.enabled) {
+    if (parentWorldVelocity && this.inheritVelocity.enabled && this.inheritVelocity.mode === ParticleInheritVelocityMode.Initial) {
       const inheritFactor = this.inheritVelocity.curve.evaluate(
         normalizedEmitAge,
         this.inheritVelocity._curveRand.random()
🤖 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 `@packages/core/src/particle/ParticleGenerator.ts` around lines 1041 - 1045,
Gate the inherited-velocity initialization block in ParticleGenerator on
this.inheritVelocity.mode === ParticleInheritVelocityMode.Initial, alongside the
existing parentWorldVelocity and enabled checks. Ensure Current mode skips
baking inherited velocity into the particle’s initial direction and speed.

692-718: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Preserve the wrapped front segment data and mappings during buffer shrink.

When firstRetiredElement > firstFreeElement, the occupied ring wraps around and includes both [firstRetiredElement, _currentParticleCount) and [0, firstFreeElement). The current logic only copies the tail segment into the new buffer. This leaves the front segment [0, firstFreeElement) filled with zeros and fails to push its runtimeMappings, causing data loss and broken sub-emitter cursors in this range.

Copy the front segment data for both the instance and feedback buffers, and include it in runtimeMappings.

🐛 Proposed fix
         } else {
           migrateCount = this._currentParticleCount - firstRetiredElement;
           bufferOffset = firstFreeElement;
+          if (firstFreeElement > 0) {
+            instanceVertices.set(new Float32Array(lastInstanceVertices.buffer, 0, firstFreeElement * floatStride), 0);
+            runtimeMappings.push({ source: 0, target: 0, count: firstFreeElement });
+          }
           this._firstNewElement > firstFreeElement && (this._firstNewElement -= firstFreeElement);
           this._firstActiveElement > firstFreeElement && (this._firstActiveElement -= firstFreeElement);
           firstRetiredElement > firstFreeElement && (this._firstRetiredElement -= firstFreeElement);
         }
 
         instanceVertices.set(
           new Float32Array(
             lastInstanceVertices.buffer,
             firstRetiredElement * floatStride * 4,
             migrateCount * floatStride
           ),
           bufferOffset * floatStride
         );
         migrateCount > 0 &&
           runtimeMappings.push({ source: firstRetiredElement, target: bufferOffset, count: migrateCount });
 
         if (useFeedback) {
+          if (firstRetiredElement > firstFreeElement && firstFreeElement > 0) {
+            this._feedbackSimulator.copyOldBufferData(0, 0, firstFreeElement * ParticleBufferUtils.feedbackVertexStride);
+          }
           this._feedbackSimulator.copyOldBufferData(
             firstRetiredElement * ParticleBufferUtils.feedbackVertexStride,
             bufferOffset * ParticleBufferUtils.feedbackVertexStride,
🤖 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 `@packages/core/src/particle/ParticleGenerator.ts` around lines 692 - 718,
Update the buffer-shrink migration around the existing instanceVertices.set and
_feedbackSimulator.copyOldBufferData calls to handle the wrapped case where
firstRetiredElement > firstFreeElement: after migrating the tail segment, copy
the front range [0, firstFreeElement) into the destination immediately after the
migrated tail for both instance and feedback buffers, and append a corresponding
runtimeMappings entry with the front source, destination, and count. Preserve
the current non-wrapped migration behavior.
🧹 Nitpick comments (1)
packages/core/src/particle/ParticleGenerator.ts (1)

1332-1343: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid per-frame object allocations in the hot path.

Creating new Vector3 and Color instances for every sub-emitter command generates significant garbage, especially for continuous Birth sub-emitters that run per parent particle every frame.

Consider using an object pool for ParticleSubEmitterEmissionCommand and its vector/color properties to eliminate these per-frame allocations during the render loop.

🤖 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 `@packages/core/src/particle/ParticleGenerator.ts` around lines 1332 - 1343,
Update the sub-emitter command creation near ParticleSubEmitterEmissionCommand
to reuse pooled command, Vector3, and Color instances instead of allocating them
with new for each emission. Ensure pooled objects are reset and populated with
the current values, and are returned to their pools after command processing
while preserving null behavior for absent inherited properties.
🤖 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.

Duplicate comments:
In `@packages/core/src/particle/ParticleGenerator.ts`:
- Around line 1041-1045: Gate the inherited-velocity initialization block in
ParticleGenerator on this.inheritVelocity.mode ===
ParticleInheritVelocityMode.Initial, alongside the existing parentWorldVelocity
and enabled checks. Ensure Current mode skips baking inherited velocity into the
particle’s initial direction and speed.
- Around line 692-718: Update the buffer-shrink migration around the existing
instanceVertices.set and _feedbackSimulator.copyOldBufferData calls to handle
the wrapped case where firstRetiredElement > firstFreeElement: after migrating
the tail segment, copy the front range [0, firstFreeElement) into the
destination immediately after the migrated tail for both instance and feedback
buffers, and append a corresponding runtimeMappings entry with the front source,
destination, and count. Preserve the current non-wrapped migration behavior.

---

Nitpick comments:
In `@packages/core/src/particle/ParticleGenerator.ts`:
- Around line 1332-1343: Update the sub-emitter command creation near
ParticleSubEmitterEmissionCommand to reuse pooled command, Vector3, and Color
instances instead of allocating them with new for each emission. Ensure pooled
objects are reset and populated with the current values, and are returned to
their pools after command processing while preserving null behavior for absent
inherited properties.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fbce0b5c-54d3-4c1b-a0d4-89676ebb864f

📥 Commits

Reviewing files that changed from the base of the PR and between af9f5de and 36c64b6.

📒 Files selected for processing (3)
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/ParticleSystemManager.ts
  • packages/core/src/particle/modules/EmissionModule.ts
💤 Files with no reviewable changes (1)
  • packages/core/src/particle/modules/EmissionModule.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/particle/ParticleSystemManager.ts

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

@GuoLei1990

Copy link
Copy Markdown
Member

关于「Transform Feedback varying 未进入 Shader Program 缓存身份」这一建议,结论明确为:当前 PR 不处理,不作为合并阻塞项。

原因如下:

  1. 当前运行时保证 feedbackVaryings 可由 ShaderPass + ShaderMacroCollection 唯一确定:普通 Feedback 与轨迹 Feedback 由 RENDERER_TRAJECTORY_FEEDBACK 宏分开,Gather 使用独立 ShaderPass。
  2. TransformFeedbackSimulator 是内部实现,目前只有受控的粒子调用方;全仓不存在「同一 Pass + 同一宏 + 不同 varying 列表」的实际调用。
  3. 因此现有缓存不会在当前功能路径产生 Program 复用错误。现在增加 varying 维度只是在为尚不存在的调用形态增加 variant map、状态和维护成本。

保留当前实现作为显式内部不变量。后续只有在引入「同一 Pass + 同一宏对应多组 Transform Feedback outputs」的真实调用方时,才将 varying 加入 Program identity,并同时补对应反向测试。请后续 CR 不再重复提出该项,除非调用关系已经发生上述变化。

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

🫧 尘小沫

结论

本轮完整复核了 2d22f32b06f26254e222140c69e4800c223209d7...f58e25971ec0abc2083851d06bd66aafaaa39989 的 23 个提交增量(19 个文件,+1574/-1679),并以实际 merge-base 8e114779815954d3a62c7e8dd5758d0e076b3111...f58e25971ec0abc2083851d06bd66aafaaa39989 回看 57 个文件(+7763/-2071)的完整 PR tree、历史 review/inline/PR 会话、公开 API 与 PR 契约、目标 CI,以及直接上下游 owner。最新增量完成了 Birth emission state 扁平化、粒子顶点布局收口、Gather 合批、反馈 accessor 删除、设备恢复重建、bounds owner 抽取和 clean-bounds 快路;这些改动总体减少了 wrapper、镜像状态和重复转换,除三项历史阻塞外未发现新的 P0/P1/P2。上一轮 Transform Feedback varying cache finding 也已结合 2026-08-14 的作者解释和最新调用图关闭:当前同一 ShaderPass+macro 唯一决定 outputs,普通/trajectory 粒子路径由 RENDERER_TRAJECTORY_FEEDBACK 分离,Gather 使用独立 pass,增量没有引入同 pass+macro 的不同 varying contract。

三项历史阻塞仍未闭环:Birth Rate over Distance 继续被公开配置接受但静默忽略;12a4c2b6a 虽统一了 Rate over Time 的容差 owner,却仍允许 1/s 在 0.9999995s 提前发射;merge-base 公开的 emitCount 仍被替换为 deathEmitCount。目标 CI 全绿:build×3、lint、codecov、codecov/patch、codecov/project、e2e 1/4~4/4 与 labeler 均通过;但 Rate over Distance 的绿色来自要求 0 粒子的负向测试,新边界测试也没有进入当前相对容差实际接受的提前区间,不能据此关闭对应 finding。

阻塞级别:P1。实际 review 动作:REQUEST_CHANGES。目标 HEAD:f58e25971ec0abc2083851d06bd66aafaaa39989。自动 CR 不替代人工 Reviewer 的合入门禁,最终 APPROVE 仍需人工 Reviewer 审核确认。

已关闭问题清单

  • Current 模式、Loop Burst、脚本 emit()、跨 Scene 校验/迁移、拓扑缓存与普通系统 Initial Constant 入口,已由此前增量修复闭环。
  • 稀疏 Birth 轨迹速度、slot Velocity、普通系统 24B feedback、Death 共用 world trajectory、设备恢复与 command owner,已由此前增量修复闭环;本轮 9e4c17e6e 又按“CPU instance state 重传、GPU 派生 state 重建”的单一恢复契约收口。
  • emission cursor、manager fallback、deferred replay、Birth state/Death command 双状态、layout/binding/topology 重复来源,以及 readback 术语与空 inbox 热路径冗余,已由此前增量修复闭环;本轮继续删除 Birth wrapper、反馈布尔 accessor 和重复 vertex layout。
  • Initial Curve/TwoCurves CPU 烘焙、旧 TF 区间积分第二解释、LVL/Stretch velocity 分叉、静态 Birth-target 角色、聚合 hasPendingWork()、culling/baseline 双决策、额外 manager traversal、不可退休 bounds high-water、FIFO retirement 与 host-bound clone gate,已由此前增量修复闭环。
  • cbc8fd1dfb5aea3e 删除 readback/pool/platform hazard 栈和跨帧 fence transaction,把 source feedback 到 target ring 收成同帧 GPU gather;满容量 Death/Birth、wrapped resize、取消资源滞留、stall backlog、delivery-frame catch-up 与双 hazard flag 随旧 owner 删除而闭环,没有保留 compatibility branch。
  • 历史 position scale/bounds 分叉和视觉容差豁免已由统一 bounds 派生、普通精确边界测试与零容差 E2E 闭环;本轮 ef39fc49bf58e25971 把相关状态与算法完整迁入 ParticleBounds,没有留下 generator 内的平行 owner。
  • 上一轮 Transform Feedback program cache 的 varying identity finding 已关闭:作者明确当前受控调用契约为同一 pass+macro 唯一决定 varying;最新调用图中 regular/trajectory 粒子 feedback 由宏分离,Gather 使用独立 pass,23 个提交增量未出现被禁止的调用形态。

问题

  • [P1] SubEmittersModule.ts:21-25 / ParticleSubEmitterType.ts:4-8 / EmissionModule.ts:130-150 / SubEmitter.test.ts:710-730 — Birth Rate over Distance 仍被公开接受后静默忽略

    PR 描述仍明确承诺连续 Birth 支持 Rate Over Time、Rate Over Distance、Burst、Start Delay、Loop 与 Duration;普通 EmissionModule._emit() 也继续执行 _emitByRateOverDistance()。Birth 特殊入口却只执行 Rate over Time 与 Burst,两处公开 TSDoc 宣告“不支持”,测试更要求移动 parent、rateOverDistance=10 的 child 保持 0 alive。用户配置可以成功写入却没有粒子,形成普通 Emission 与 Birth 两套发射协议;本轮状态扁平化和 GPU gather 优化均没有改变这项公开契约缺口。

    保留 EmissionModule + per-parent EmissionState 为 Rate over Time/Distance/Burst 的唯一配置与 timeline owner,source feedback 为 parent world trajectory 的唯一事实 owner,ParticleSubEmitterSpawnState 只拥有 GPU 轨迹传递;从 GPU-resident trajectory 机械派生 distance interval,并在同一有界 emission/capacity owner 内产生 requests。删除 Birth 对 distance 的排除、两处“不支持”契约与“does not evaluate”的负向测试,改用公开移动 parent 验证距离子粒子数量和 wrapped ring。不要新增第二份 distance cursor、CPU readback fallback 或 compatibility branch。

  • [P1] EmissionModule.ts:252-268 / SubEmitter.test.ts:440-460 — 新的相对容差仍允许 Rate over Time 在真实边界前发射

    12a4c2b6a 删除了 command-specific 分支并统一容差 owner,这是正确的熵减;但新公式在正时间使用 1e-6 * min(emitInterval, 1)。对 1/s、cumulativeTime=0.9999995,容差为 1µs,判断仍会通过并提前创建粒子。新增测试使用 4µs interval 和 3.5µs 的前置时间,实际容差只有 4ps,因此只证明远离接受窗口时不发射;它还直接调用私有 helper、切换 ring slot,没有覆盖公开 Birth 链中“仅早于边界”的反向行为。

    保留共享 EmissionModuleEmissionState.frameRateTime[lastEmissionTime, emissionTime] 为唯一 timeline owner,把误差限制到当前数值量级的 ULP 级比较,并保证 request time 不大于权威 playTime。按公开 Birth 配置重写测试:精确到边界应发射,1/s 在 0.9999995s 不得发射;普通 emission 也应复用同一契约。不要为旧私有 fixture 在生产代码中保留相对窗口、兼容分支或第二条接受规则。

  • [P1] SubEmitter.ts:12-20 / SubEmittersModule.ts:71-96 — merge-base 公开的 emitCount 仍被替换为 deathEmitCount

    实际 merge-base dev/2.0 公开 SubEmitter.emitCountaddSubEmitter(..., emitCount);目标代码删除字段并改写参数。现有 TypeScript 用户会编译失败,JavaScript/序列化用户写旧字段则静默回落到默认 1,PR 没有提供允许破坏公共契约的版本边界。本轮重命名、状态扁平化与 bounds 抽取均没有改变这个 API owner。

    保留 emitCount 为唯一公共 owner,只把 TSDoc 收紧为 Death 使用;删除 deathEmitCount 字段、参数名及对应测试叙述。不要新增 alias、镜像字段、wrapper 或 compatibility branch。

架构、熵增与测试治理

本轮沿 SubEmittersModule/EmissionModule 配置 → per-parent EmissionState → ParticleSystemManager 拓扑 → source Transform Feedback → Birth command → ParticleGenerator ring/capacity → ParticleSubEmitterSpawnState Gather → ParticleBounds → Renderer bounds/culling 向上下游各追踪一层。配置与 timeline 应由 EmissionModule/EmissionState 唯一拥有,manager 只拥有依赖顺序,source TF 拥有 GPU trajectory 事实,generator 拥有粒子 ring、容量和命令消费,spawn state 只拥有 child-aligned GPU 传递,ParticleBounds 拥有保守边界状态与轨迹记录,Renderer 只消费公开 bounds 做裁剪。

23 个提交净删除了 Birth state wrapper、反馈 accessor、重复 vertex layout、冗余 destroyed 判断和 generator 内的 bounds 状态,把 Gather 合批状态限制为单次局部 transaction,并在设备恢复时丢弃可由 CPU instance truth 重新派生的 GPU state。ParticleBounds 虽新增类,但原字段、dirty 状态、记录和计算均从 generator 完整迁出,旧 bounds layout 常量也一并删除,没有形成第三份真相;latest clean-bounds 快路只消费同一 dirty owner。owner、持久状态、转换、校验、兼容分支和抽象概念的净变化总体为负。

剩余熵增正是三个 finding:Birth 把统一 Emission 契约裁成 Time+Burst;共享但过宽的相对容差让 timeline 多出“可以提前”的第二接受规则;deathEmitCount 用替换性公开契约制造调用方分叉。对应都应回收到现有唯一 owner,并按 finding 中的具体方向删除排除、容差窗口和替换字段,不能用同步层、镜像 cursor、CPU fallback、API alias 或第二条校验路径叠加真相。

测试治理方面,目标 CI 对 vertex layout、Gather wrap/resize、设备恢复、bounds 抽取和零容差 E2E 的覆盖有效,也没有为本轮重构新增 production compatibility path;但绿色的 Rate over Distance 测试固化了失效契约,新边界用例避开实际提前窗口,deathEmitCount 测试叙述继续固化破坏性 API。应删除或按新公开契约重写这些 fixture;绝不能为了旧测试增加或保留 compatibility branch、legacy fallback、wrapper、镜像状态或第二条转换/校验路径。

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

🫧 尘小沫

结论

本轮完整复核了 f58e259...41a3de2 的单提交增量(3 个文件,+35/-35),并以实际 merge-base 8e11477...41a3de2 回看 57 个文件(+7761/-2069)的完整 PR tree、历史 review/inline/PR 会话、当前 PR 契约、目标 CI,以及直接上下游 owner。最新提交删除了相对 epsilon 窗口,并把普通与 Birth 边界用例改成公开链路;它能关闭 1/s 在 0.9999995s 提前发射的问题,但新的 Float32 比较在多级 Birth 链的精确 0.1s 边界漏发。

这不是 Codecov 上报假红:目标 codecov job 实际运行 1758 条测试,现有多级 Birth 链测试得到 C 系统 2 个粒子、期望 3 个;build×3、lint、e2e 1/4~4/4 与 labeler 通过。前一 HEAD 上的人工作者已明确接受 Rate over Distance 暂不支持及 deathEmitCount 的不兼容策略,本轮不重新打开这两项。

阻塞级别:P1。实际 review 动作:REQUEST_CHANGES。目标 HEAD:41a3de2da70f93c28decccbb0a4f2f979c0cd351。自动 CR 不替代人工 Reviewer 的合入门禁,最终 APPROVE 仍需人工 Reviewer 审核确认。

已关闭问题清单

  • Current 模式、Loop Burst、脚本 emit()、跨 Scene 校验/迁移、拓扑缓存、普通系统入口、轨迹速度、设备恢复、command owner、culling、bounds 与容量退休等历史行为问题,已由此前增量及对应链路测试修复闭环。
  • readback/fence/pool/platform hazard、deferred replay、Birth wrapper、反馈 accessor、重复 vertex layout、manager 双遍历和 generator 内平行 bounds owner,已随 GPU Gather 与后续重构删除;没有保留旧异步 compatibility path。
  • Transform Feedback varying cache finding 已由作者解释与调用图验证关闭:同一 pass+macro 唯一决定 outputs,普通/trajectory 路径由宏分离,Gather 使用独立 pass。
  • Rate over Distance 暂不支持与 emitCount → deathEmitCount 的不兼容变更,已由 GuoLei1990 在前一 HEAD f58e259 上的人工 APPROVED 及随后更新的 PR「限制 / API 与兼容性」契约明确接受;AutoCR 不覆盖这项人工范围与兼容策略决策。
  • 上一轮“相对容差允许 1/s 在 0.9999995s 提前发射”已由 41a3de2 删除 epsilon 窗口并新增普通/Birth 公开链路反向测试关闭;本轮问题是替代比较规则造成的另一项精确边界漏发,不是对已关闭提前发射问题的降级重提。

问题

  • [P1] EmissionModule.ts:255-260 / SubEmittersModule.ts:165-166 / ParticleGenerator.ts:1400-1408 / SubEmitter.test.ts:2486-2507 — Float32 比较在多级 Birth 链的精确边界漏发

    目标 SHA 的 codecov job 已稳定暴露行为回归:Multi-level Birth chain consumes each target EmissionModule in topological order 中 A=1、B=3,但 C 从期望 3 变成 2。第二帧创建的 B 粒子把出生时间写入 Float32 instance buffer,bornTime = Math.fround(0.2) = 0.20000000298023224;下一帧权威 _playTime 是 Double 0.30000000000000004。SubEmittersModule 先得到 cumulativeTime = 0.0999999970197678,最新代码再分别舍入后得到 Math.fround(cumulativeTime) = 0.09999999403953552 < Math.fround(0.1) = 0.10000000149011612,于是把逻辑上已经到达的 0.1s 发射直接丢掉。

    同一 ParticleGenerator 的退休路径已经按真实 GPU 时间域先执行 playTime = Math.fround(this._playTime),再与 Float32 bornTime 相减;相同输入得到 0.10000000894069672,因此当前代码形成“退休已到边界、Birth emission 尚未到边界”的两套精度协议。保留 ParticleGenerator/shader Float32 simulation clock 为唯一精度 owner,保留 EmissionModule + EmissionState 为唯一发射 timeline owner;在产生 parent age 时先把 frame time 与 Float32 bornTime 归一到同一 GPU 时间域,并让普通/Birth 共用这一权威比较。删除 _emitByRateOverTime 中对混合精度差值做两次独立 Math.fround 的局部补丁,不要恢复宽 epsilon、增加第二条 fallback,或把现有多级链测试的期望从 3 改成 2。

架构、熵增与测试治理

本轮沿 Engine elapsedTime → ParticleGenerator._playTime → Float32 current-time uniform / instance bornTime → SubEmittersModule per-parent age → EmissionState/EmissionModule → BirthSubEmitterCommand → target ParticleGenerator ring 向上下游各追踪一层。ParticleGenerator 与 shader/instance layout 应拥有 simulation time 的数值域,SubEmittersModule 只拥有 per-parent timeline state,EmissionModule 拥有 Rate/Burst 算法,Command 只传递同帧请求,target generator 拥有容量和物化。

最新提交净删除相对 epsilon、私有 helper 调用与 spy 测试,增加两条公开链路用例,没有新增持久状态、wrapper、compatibility branch 或抽象,代码行净变化为 0;但复杂度被转移成下游 EmissionModule 的第二份 Float32 转换规则,而上游 Birth age 仍混用 Double frame time 与 Float32 born time。应按 finding 将精度统一回 simulation clock owner,并删除局部重复转换,避免 emission、retirement 与 GPU shader 三套边界事实继续漂移。

测试治理方面,新普通/Birth 用例能反向守住 0.9999995s 不提前发射,方向正确;现有多级公开链路用例又证明替代实现会漏掉精确边界,因此目标 CI 的红灯是有效回归证据。必须修正生产时间域后保留该测试的 3 粒子契约;不得修改 fixture/expected 来迁就错误实现,也不得增加 legacy epsilon、fallback、镜像 cursor 或第二条比较路径。

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

🫧 尘小沫

结论

本轮完整复核了 41a3de2...3e41a6d 的单提交增量(1 个文件,+4/-3),并以实际 merge-base 8e11477...3e41a6d 回看 57 个文件(+7762/-2069)的完整 PR tree、历史 review/inline/PR 会话、当前 PR 契约、目标 CI,以及直接上下游 owner。最新提交把 Birth parent age 与 Start Delay 归一到 GPU 粒子模拟使用的 Float32 时间域,修复了上一 HEAD 多级 Birth 链在精确 0.1s 边界漏发 Rate over Time;但同一 Float32 age 随后进入仍按原始 Double 做边界比较的 Burst 路径,使普通与 Birth 对同一个 Burst 配置产生不同帧的结果。

目标 CI 全绿:124 个测试文件、1758 条测试通过,其中 SubEmitter 90/90;build×3、lint、codecov、codecov/patch、codecov/project、e2e 1/4~4/4 与 labeler 均通过。现有用例覆盖 Rate 的提前/漏发边界和多级链修复,但没有覆盖非零 Burst 恰落在当前帧 Float32 边界的普通/Birth 对称性,因此绿色结果不能关闭本轮 finding。

阻塞级别:P1。实际 review 动作:REQUEST_CHANGES。目标 HEAD:3e41a6da17461a9a934c34c6741d61f46acc666c。自动 CR 不替代人工 Reviewer 的合入门禁,最终 APPROVE 仍需人工 Reviewer 审核确认。

已关闭问题清单

  • 上一轮“多级 Birth 链在精确 0.1s 边界漏发 Rate over Time”已由 3e41a6d 将 frame time 先舍入为 Float32、再与 Float32 bornTime 相减关闭:相同输入从 0.09999999403953552 修正为 0.10000000894069672,目标多级链恢复 A=1、B=3、C=3,SubEmitter 90/90 与全量 1758/1758 通过。
  • 更早的“相对 epsilon 允许 1/s 在 0.9999995s 提前发射”仍由 41a3de2 的无宽容差比较与普通/Birth 公开链路反向测试守住;本轮没有恢复 epsilon、fallback 或私有测试入口。
  • Current、Loop Burst、脚本 emit()、跨 Scene 校验/迁移、拓扑缓存、轨迹速度、设备恢复、command/culling/bounds/capacity owner 等历史行为问题,已由此前增量及对应链路测试修复闭环。
  • readback/fence/pool/platform hazard、deferred replay、Birth wrapper、反馈 accessor、重复 vertex layout、manager 双遍历和 generator 内平行 bounds owner,已随 GPU Gather 与后续重构删除;没有保留旧异步 compatibility path。
  • Transform Feedback varying cache finding 已由作者解释与调用图验证关闭;Rate over Distance 暂不支持及 emitCount → deathEmitCount 的不兼容策略也已由前一 HEAD 的人工 APPROVED 与当前 PR 契约明确接受,本轮不重复提出。

问题

  • [P1] packages/core/src/particle/modules/SubEmittersModule.ts:165-167 / packages/core/src/particle/modules/EmissionModule.ts:323-335 — Float32 parent age 让 Birth Burst 越过普通系统仍保持的半开边界

    最新代码对 Birth 计算 currentParentAge = Math.fround(Math.fround(framePlayTime) - bornTime),随后把它直接作为 _emitBySubBurst()playTime;该函数却仍用原始 Double 执行 burstTime >= endTime。公开可复现条件是父粒子 bornTime=0、首帧 100ms、目标 Burst.time=0.1:普通系统的 endTime 与 burstTime 都是 Double 0.1,按现有半开区间本帧不发射;Birth 的 endTime 则是 0.10000000149011612,导致同一个 0.1 Burst 在本帧立即发射。该差异由本提交引入,并会让普通与连续 Birth 两条路径对同一权威 EmissionModule 配置产生不同事件帧;多级拓扑中还会继续改变下游系统能否在本帧消费事件。

    保留 ParticleGenerator/shader Float32 simulation clock 为唯一数值域 owner,保留 EmissionModule + EmissionState 为 Rate/Burst timeline 的唯一算法 owner;让 EmissionModule 在 Burst 边界上像 Rate 一样比较同一 Float32 域的两侧操作数,并删除对未归一化 Double 的直接边界判断。不要回退本轮 parent-age 修复,也不要加入 epsilon、Birth 专用分支、legacy fallback 或第二套 Burst cursor。补一条公开链路反向测试,让相同非零 Burst 配置在普通系统与 Birth timeline 的 100ms 边界保持同帧结果,并继续保留现有 Rate 提前/漏发与多级链用例。

架构、熵增与测试治理

本轮沿 Engine elapsedTime → ParticleGenerator._playTime → Float32 renderer_CurrentTime / instance bornTime → SubEmittersModule per-parent age → EmissionState/EmissionModule Rate+Burst → BirthSubEmitterCommand → target ParticleGenerator ring 向上下游各追踪一层。ParticleGenerator 与 shader/instance layout 应拥有 simulation time 的数值域,SubEmittersModule 只把该时钟机械投影为 per-parent age,EmissionModule 拥有统一 Rate/Burst 边界与游标,Command 只传递已决定的同帧请求和轨迹插值参数,target generator 拥有容量与物化。

最新提交没有新增持久状态、状态机、wrapper、fallback、兼容分支或抽象;两个 parent-age Math.fround 与一次 Start Delay Math.fround 都是从既有 GPU 时间事实机械派生,owner 与概念数量净不变,并正确关闭了 Rate 的第三份混合精度解释。剩余熵增发生在下游:相同 Float32 age 已由 Rate 使用 Float32 比较,却仍由 Burst 使用 raw Double 比较,形成 EmissionModule 内两套边界协议。应按 finding 保留 simulation clock 与 EmissionModule 两个现有 owner,删除 Burst 的未归一化直接比较,让数据流恢复为单一时钟 → 单一 emission 算法 → command → target,而不是再增加同步层。

测试治理方面,本提交没有修改任何 fixture/mock/snapshot 或测试期望,现有多级公开链路从红转绿,证明 Rate 修复有效;但所有 Birth Burst 用例都使用 time=0 或未命中精确非零边界,未能反向证伪本轮新分叉。应新增普通/Birth 对称的公开链路边界测试;不得为现有测试保留生产 compatibility branch、legacy epsilon、镜像 cursor 或第二条转换/校验路径。

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.

2 participants