perf(vfx): port retail particle visibility degradation
Resolve DAT-authored particle ranges from the hardware GfxObj, apply retail distance and completed-cell visibility gates, and preserve the exact finite/infinite off-view update semantics. This removes dense-world simulation work without shortening terrain, entity, fog, or streaming distance. Publish doorway-clipped outdoor cells through a focused frame controller, retain effect cell identity for outdoor statics, reject hidden emitters before particle-slot scans, and offer an explicit opt-in Extended particle range. Release build succeeds and all 5,857 tests pass with five intentional skips. Retail-conformance, architecture, and adversarial review cycles are clean; connected Aerlinthe visual/performance gate pending. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
82789eea88
commit
f1ba147ac5
29 changed files with 1810 additions and 125 deletions
|
|
@ -638,21 +638,38 @@ totals ("Particles Rendered", "Particle Systems").
|
|||
Retail did not implement a global particle LRU. Instead, each emitter
|
||||
enforced its `MaxParticles` hard cap and relied on distance-degrade:
|
||||
|
||||
`PhysicsObj.ShouldDrawParticles(degradeDistance)` (ACE
|
||||
`PhysicsObj.cs:1540-1544`):
|
||||
`CPhysicsObj::ShouldDrawParticles` (`0x0050FE60`):
|
||||
|
||||
```csharp
|
||||
public bool ShouldDrawParticles(float degradeDistance) {
|
||||
if (!ExaminationObject) return true;
|
||||
return !(CYpt > degradeDistance || CurCell == null);
|
||||
}
|
||||
```text
|
||||
if ExaminationObject: return true
|
||||
if CYpt > degradeDistance: return false
|
||||
if CurCell == null: return false
|
||||
return CurCell.IsInView()
|
||||
```
|
||||
|
||||
Particles beyond a per-emitter `DegradeDistance` (ACE stubs it to
|
||||
`float.MaxValue`) get `SetNoDraw(true)` — they keep simulating (so the
|
||||
server-side script advances) but aren't rendered. Real retail probably
|
||||
used a per-`GfxObjDegradeInfo` value; for acdream we can set a sensible
|
||||
default (50 m outdoor, 15 m indoor) and override via a PluginAPI hook.
|
||||
The matching x87 `test ah,0x41` admits equal, less-than, and unordered
|
||||
comparisons. Authored negative/NaN/infinite values are preserved; 100 is used
|
||||
only when the hardware particle GfxObj has no degradation table.
|
||||
|
||||
The exact distance is not guessed. `CPhysicsPart::GetMaxDegradeDistance`
|
||||
(`0x0050D510`) returns 100 world units when the hardware particle GfxObj has
|
||||
no degradation table. `GfxObjDegradeInfo::get_max_degrade_distance`
|
||||
(`0x0051E2D0`) selects entry zero for one/two-entry tables and the
|
||||
second-to-last entry for larger tables. `ParticleEmitter::SetInfo`
|
||||
(`0x0051CE90`) stores that value from its first hardware particle part.
|
||||
|
||||
`ParticleEmitter::UpdateParticles` (`0x0051D180`) does more than hide the
|
||||
result. While degraded, an infinite emitter performs no normal particle
|
||||
integration or emission and freezes its live particle ages. A finite emitter
|
||||
ages/kills existing particles, records at most one due emission without making
|
||||
it drawable, and calls the conditional `StopEmitter` duration/count test. It
|
||||
does not stop merely because it is degraded. If that counter-only emission
|
||||
reaches the stop condition, retail's logical `num_particles` can remain
|
||||
nonzero without a drawable `PhysicsPart`; the emitter then remains owned until
|
||||
explicit physics-object teardown. ACE preserves those two update branches but stubs the
|
||||
distance to `float.MaxValue`; retail's named code and matching x86 are the
|
||||
oracle. Full pseudocode and the installed-data fixture are in
|
||||
`docs/research/2026-07-17-retail-particle-visibility-pseudocode.md`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -798,11 +815,11 @@ RenderFrame:
|
|||
7. UI
|
||||
```
|
||||
|
||||
Simulation runs every frame regardless of visibility (matches retail's
|
||||
`ShouldDrawParticles` behavior: hidden emitters still advance their
|
||||
script). This costs a bit of CPU but keeps semantics correct — when an
|
||||
ally buff expires behind a wall, it should still be gone when you see
|
||||
them again.
|
||||
Simulation obeys retail's completed-view and authored-distance gate. Visible
|
||||
emitters run their full update. Degraded infinite emitters freeze in place;
|
||||
finite effects still age/retire and run their authored duration/count stop
|
||||
test, so a time-bounded ally buff effect does not become immortal behind a
|
||||
wall.
|
||||
|
||||
### Silk.NET shader sketch
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue