fix: stagger particle lifecycle respawn
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-05 10:36:07 +08:00
parent c7acefcb21
commit 7929052c58
5 changed files with 46 additions and 12 deletions

View File

@@ -0,0 +1,16 @@
# STEP-055 Android 真实 WeatherV2 风场综合可视化 v5
状态:**IMPLEMENTED / WAITING_ANDROID_VALIDATION**
Starting HEAD:`c7acefcb21840bc1db736a5f061ba39b18ba2c36`
## 生命周期错峰小修复
- `ParticleLifecycleSeconds` 从 `3600` 改为 `14400` simulation seconds;`VisualSimulationSecondsPerRealSecond=1200` 未变,因此基础生命周期为 12 real seconds。
- 首次 State seed 的 age 改为 `particleHash(v_uv + vec2(71.19, 29.47)) * maxAge`,不再统一为零。
- 每个 slot 的 `maxAge = base * mix(0.75, 1.25, particleHash(v_uv + vec2(17.31, 83.17)))`;普通 respawn 仍重置 age=0,initial seed 保留错峰 age。
- `ParticleLifecycle` 日志使用 `configuredSlots`,并标示 `initialAge=HASH_STAGGERED`、`maxAge=HASH_JITTERED_0.75_1.25` 与 `readback=disabled`。
State Float、Trail UByte、previous→current segment、view-space Trail、time-based fade、canonical Field、Timeline、Runtime Window、WeatherColorLayer 和 REAL WEATHER UI 均未修改。
shader generator、Linux OpenGL 精确编译、既有非 Jest style layer 精确编译和 `git diff --check` 均通过;未执行 Android 或 Jest。

View File

@@ -196,12 +196,16 @@ void main() {
// Formal particles use GPU-resident lifecycle state. Each slot keeps
// its age in B; A=0 marks this frame as a respawn so Trail does not
// connect the retired position to the new randomized one.
bool respawn = u_mask.w > 0.5;
float age = respawn ? 0.0 : previous.b + u_pass.y;
bool initialSeed = u_mask.w > 0.5;
float lifecycleHash = particleHash(v_uv + vec2(17.31, 83.17));
float maxAge = u_marker.z * mix(0.75, 1.25, lifecycleHash);
float initialAgeHash = particleHash(v_uv + vec2(71.19, 29.47));
bool respawn = initialSeed;
float age = initialSeed ? initialAgeHash * maxAge : previous.b + u_pass.y;
vec2 position = previous.rg;
vec2 velocity;
bool valid = !respawn && fieldVelocityAt(position, velocity);
if (!valid || age >= u_marker.z) {
if (!valid || age >= maxAge) {
respawn = true;
}
@@ -235,7 +239,9 @@ void main() {
// frame; alpha still prevents an invalid visible segment.
position = respawnSeed(v_uv, previous.rg, 3.0);
}
age = 0.0;
if (!initialSeed) {
age = 0.0;
}
}
fragColor = vec4(position, age, respawn ? 0.0 : 1.0);
return;

View File

@@ -100,12 +100,16 @@ void main() {
// Formal particles use GPU-resident lifecycle state. Each slot keeps
// its age in B; A=0 marks this frame as a respawn so Trail does not
// connect the retired position to the new randomized one.
bool respawn = u_mask.w > 0.5;
float age = respawn ? 0.0 : previous.b + u_pass.y;
bool initialSeed = u_mask.w > 0.5;
float lifecycleHash = particleHash(v_uv + vec2(17.31, 83.17));
float maxAge = u_marker.z * mix(0.75, 1.25, lifecycleHash);
float initialAgeHash = particleHash(v_uv + vec2(71.19, 29.47));
bool respawn = initialSeed;
float age = initialSeed ? initialAgeHash * maxAge : previous.b + u_pass.y;
vec2 position = previous.rg;
vec2 velocity;
bool valid = !respawn && fieldVelocityAt(position, velocity);
if (!valid || age >= u_marker.z) {
if (!valid || age >= maxAge) {
respawn = true;
}
@@ -139,7 +143,9 @@ void main() {
// frame; alpha still prevents an invalid visible segment.
position = respawnSeed(v_uv, previous.rg, 3.0);
}
age = 0.0;
if (!initialSeed) {
age = 0.0;
}
}
fragColor = vec4(position, age, respawn ? 0.0 : 1.0);
return;

View File

@@ -53,7 +53,7 @@ constexpr float EarthCircumferenceMeters = 40075016.68557849f;
// moves about 100 m (roughly 0.65 px at z9), while a 30 m/s sample remains
// below a four-pixel segment at that zoom.
constexpr float VisualSimulationSecondsPerRealSecond = 1200.0f;
constexpr float ParticleLifecycleSeconds = 3600.0f;
constexpr float ParticleLifecycleSeconds = 14400.0f;
constexpr float TrailFadeReferenceFramesPerSecond = 60.0f;
std::atomic_uint64_t nextGpuGeneration{0};
@@ -463,9 +463,12 @@ public:
if (resources->formalDisplay && now - resources->lastLifecycleLog >= std::chrono::seconds(1)) {
resources->lastLifecycleLog = now;
Log::Info(Event::Render,
"ParticleLifecycle: activeSlots=" + std::to_string(resources->particleCount) +
" respawn=GPU_HASH_ON_INVALID_OR_AGE_EXPIRED lifecycleSeconds=" +
std::to_string(ParticleLifecycleSeconds) +
"ParticleLifecycle: configuredSlots=" + std::to_string(resources->particleCount) +
" initialAge=HASH_STAGGERED maxAge=HASH_JITTERED_0.75_1.25 "
"lifecycleBaseSimulationSeconds=" + std::to_string(ParticleLifecycleSeconds) +
" lifecycleBaseRealSeconds=" +
std::to_string(ParticleLifecycleSeconds / VisualSimulationSecondsPerRealSecond) +
" respawn=GPU_HASH_ON_INVALID_OR_AGE_EXPIRED"
" fieldId=" + resources->field.fieldID + " simulationFrame=" +
std::to_string(resources->committedFrame) + " readback=disabled");
}

View File

@@ -429,3 +429,6 @@
## STEP-055 生命周期、真实轨迹与 view-space Trail 修复
真实 Weather 路径改为 GPU age/哈希 respawn,StateRead→StateWrite 真实位移 segment,且 Trail 仅在当前 camera view 内累积。相机变化只重建 Trail;`trailFade` 改为 wall-time 指数衰减。visual clock 调整为 1200 simulation seconds/real second。canonical、Timeline、Runtime、WeatherColorLayer 未改,仍待 Android 综合验收。
## STEP-055 生命周期错峰小修复
粒子基础生命周期调整为 14400 simulation seconds(12 real seconds),首次 seed 与 maxAge 都由稳定 hash 错峰,消除同步生命周期到期。日志改用 configuredSlots 并说明无 GPU readback;其他粒子视觉与数据链语义未改。