config(vfx): default to extended particle range

Honor the requested non-dev-UI behavior by making the 2x DAT-authored particle cutoff the application default while keeping Retail as the exact opt-down setting. Update migration tests and the divergence/roadmap documentation accordingly.

Release build succeeds and all 5,857 tests pass with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 15:32:57 +02:00
parent f1ba147ac5
commit 48a118db91
7 changed files with 18 additions and 16 deletions

View file

@ -214,7 +214,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| ~~AP-114~~ | **RETIRED 2026-07-14 (protection-effect corrective gate)** — the particle renderer no longer replaces every authored GfxObj with one bounding-box quad. Retail `Always2D` classification preserves mode-1/no-degrade full meshes through the modern shared mesh buffer and leaves only other degrade modes on the billboard path; stable emitter handles balance mesh ownership. | `src/AcDream.App/Rendering/ParticleRenderer.cs`; `RetailParticleGeometryClassifier.cs`; `particle_mesh.vert/.frag` | — | — | `CPhysicsPart::Draw @ 0x0050D7A0`; `CPhysicsPart::Always2D @ 0x0050D8A0`; `ParticleEmitter::SetInfo @ 0x0051CE90`; `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md` | | ~~AP-114~~ | **RETIRED 2026-07-14 (protection-effect corrective gate)** — the particle renderer no longer replaces every authored GfxObj with one bounding-box quad. Retail `Always2D` classification preserves mode-1/no-degrade full meshes through the modern shared mesh buffer and leaves only other degrade modes on the billboard path; stable emitter handles balance mesh ownership. | `src/AcDream.App/Rendering/ParticleRenderer.cs`; `RetailParticleGeometryClassifier.cs`; `particle_mesh.vert/.frag` | — | — | `CPhysicsPart::Draw @ 0x0050D7A0`; `CPhysicsPart::Always2D @ 0x0050D8A0`; `ParticleEmitter::SetInfo @ 0x0051CE90`; `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md` |
| AP-115 | The DAT-authored portal-space viewport and its animation `SoundTweakedHook` are live, but the separate `ClientUISystem` enter/exit sound enums and centered repeating `"In Portal Space - Please Wait..."` display string are not yet presented. | `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; teleport event switch in `GameWindow.cs` | acdream has no retained cross-stack centered display-string owner or ClientUISystem sound-table-enum resolver yet; routing the line into chat or inventing direct wave IDs would be less faithful | Portal travel has the correct animated wormhole, timing, direct viewport switch, view-plane transitions, and animation-authored sound, but lacks retail's short UI cue sounds and center notice | `gmSmartBoxUI::BeginTeleportAnimation @ 0x004D6300`; `gmSmartBoxUI::UseTime @ 0x004D6E30` | | AP-115 | The DAT-authored portal-space viewport and its animation `SoundTweakedHook` are live, but the separate `ClientUISystem` enter/exit sound enums and centered repeating `"In Portal Space - Please Wait..."` display string are not yet presented. | `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; teleport event switch in `GameWindow.cs` | acdream has no retained cross-stack centered display-string owner or ClientUISystem sound-table-enum resolver yet; routing the line into chat or inventing direct wave IDs would be less faithful | Portal travel has the correct animated wormhole, timing, direct viewport switch, view-plane transitions, and animation-authored sound, but lacks retail's short UI cue sounds and center notice | `gmSmartBoxUI::BeginTeleportAnimation @ 0x004D6300`; `gmSmartBoxUI::UseTime @ 0x004D6E30` |
| AP-116 | Optional Display setting `Particle Range = Extended` multiplies DAT-authored particle degradation distances by 2; default `Retail` uses exact values | `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs`; `src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs`; `src/AcDream.Core/Vfx/ParticleSystem.cs` | Explicit opt-in for players who value distant VFX; it changes no terrain, scenery, entity, fog, or streaming distance and is reversible live | Extended can roughly enlarge the active particle area and reduce the CPU gain from MP2; distant VFX remain visible beyond retail's authored cutoff | `CPhysicsPart::GetMaxDegradeDistance @ 0x0050D510`; `GfxObjDegradeInfo::get_max_degrade_distance @ 0x0051E2D0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` | | AP-116 | Default `Particle Range = Extended` multiplies DAT-authored particle degradation distances by 2; the `Retail` option restores exact values | `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs`; `src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs`; `src/AcDream.Core/Vfx/ParticleSystem.cs` | User explicitly requested doubled range as the normal non-dev-UI behavior; it changes no terrain, scenery, entity, fog, or streaming distance, and remains reversible through settings | The default roughly enlarges the active particle area and reduces the CPU gain from MP2; distant VFX remain visible beyond retail's authored cutoff | `CPhysicsPart::GetMaxDegradeDistance @ 0x0050D510`; `GfxObjDegradeInfo::get_max_degrade_distance @ 0x0051E2D0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
| AP-117 | Outdoor particle `CLandCell::IsInView` state is reconstructed with the modern landscape renderer's per-cell frustum plus active doorway clip-plane/scissor-AABB tests; retail `LScape::landcell_check` uses `Render::get_clip_height` + `Render::block_check` on terrain-cell corner intervals | `src/AcDream.App/Rendering/TerrainModernRenderer.cs` (`CollectVisibleCells`) | The mandatory modern renderer batches terrain by landblock and has no retail `ViewIntervalType` product. Publishing cell visibility from the exact landscape draw slices preserves ownership/order and removes the former object-survivor dependency without adding a second view pipeline | At a terrain cell grazing a frustum or doorway boundary, the conservative AABB test may freeze or resume particles on a slightly different frame than retail; whole regions outside the active doorway slice are rejected, and authored distance, login/portal fail-closed behavior, and indoor PView cells remain exact | `LScape::landcell_check @ 0x005050A0`; `CLandCell::IsInView @ 0x00532CB0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` | | AP-117 | Outdoor particle `CLandCell::IsInView` state is reconstructed with the modern landscape renderer's per-cell frustum plus active doorway clip-plane/scissor-AABB tests; retail `LScape::landcell_check` uses `Render::get_clip_height` + `Render::block_check` on terrain-cell corner intervals | `src/AcDream.App/Rendering/TerrainModernRenderer.cs` (`CollectVisibleCells`) | The mandatory modern renderer batches terrain by landblock and has no retail `ViewIntervalType` product. Publishing cell visibility from the exact landscape draw slices preserves ownership/order and removes the former object-survivor dependency without adding a second view pipeline | At a terrain cell grazing a frustum or doorway boundary, the conservative AABB test may freeze or resume particles on a slightly different frame than retail; whole regions outside the active doorway slice are rejected, and authored distance, login/portal fail-closed behavior, and indoor PView cells remain exact | `LScape::landcell_check @ 0x005050A0`; `CLandCell::IsInView @ 0x00532CB0`; `CPhysicsObj::ShouldDrawParticles @ 0x0050FE60` |
## 4. Temporary stopgap (TS) — 33 active rows (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-44 narrowed same day — NPC UP unified onto the interp queue, gate retained for orientation) ## 4. Temporary stopgap (TS) — 33 active rows (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-44 narrowed same day — NPC UP unified onto the interp queue, gate retained for orientation)

View file

@ -1332,7 +1332,7 @@ hitch and follows.
| **MP-Alloc (safe batch)** | Reuse per-frame buffers: anim pose, particle draw-list, interior partition, animatedIds/drawableCells sets | ✅ SHIPPED + USER-GATED 2026-07-05 — dense-town frame-time spikes 2087ms → 610ms, alloc spikes (3075MB single-frame) eliminated, gen2 GC 511/window → ~0; user confirms FPS steady. 4 commits `b8c05e2b`..`91afea24` | | **MP-Alloc (safe batch)** | Reuse per-frame buffers: anim pose, particle draw-list, interior partition, animatedIds/drawableCells sets | ✅ SHIPPED + USER-GATED 2026-07-05 — dense-town frame-time spikes 2087ms → 610ms, alloc spikes (3075MB single-frame) eliminated, gen2 GC 511/window → ~0; user confirms FPS steady. 4 commits `b8c05e2b`..`91afea24` |
| MP-Alloc (hard sites) | EnvCell settled-camera rebuild gate + physics `Transition` pooling — the residual steady ~1.6MB/frame | ⚪ OPTIONAL follow-up — would lower the median too, but the wild-swing complaint is already resolved; each needs its own careful gate (batch correctness / physics faithfulness) | | MP-Alloc (hard sites) | EnvCell settled-camera rebuild gate + physics `Transition` pooling — the residual steady ~1.6MB/frame | ⚪ OPTIONAL follow-up — would lower the median too, but the wild-swing complaint is already resolved; each needs its own careful gate (batch correctness / physics faithfulness) |
| MP1c | Streaming cutover to pak + hitch gate (vs the 211 ms baseline) | ⚪ after MP-Alloc | | MP1c | Streaming cutover to pak + hitch gate (vs the 211 ms baseline) | ⚪ after MP-Alloc |
| MP2 | Retail particle distance/cell-view degradation: DAT-authored range, exact infinite/finite off-view update branches, emitter-first render scan, optional Extended range | ✅ SHIPPED 2026-07-17 — Aerlinthe hotspot `0x32000223` resolves to retail 64m; landscape/entity streaming distance unchanged | | MP2 | Retail particle distance/cell-view degradation: DAT-authored range, exact infinite/finite off-view update branches, emitter-first render scan, user-requested 2× default range | ✅ SHIPPED 2026-07-17 — Aerlinthe hotspot `0x32000223` resolves to retail 64m and defaults to 128m; landscape/entity streaming distance unchanged |
| MP3 | Arch ECS render world + delta submission (the 300-FPS lever) | ⚪ — note: does NOT fix the steady-state GC churn (that's MP-Alloc); MP3 is the draw-submission throughput lever | | MP3 | Arch ECS render world + delta submission (the 300-FPS lever) | ⚪ — note: does NOT fix the steady-state GC churn (that's MP-Alloc); MP3 is the draw-submission throughput lever |
| MP4 | Zero-alloc frame loop + flat physics data (residual, post-MP-Alloc) | ⚪ hard-queued behind M1.5 #137 | | MP4 | Zero-alloc frame loop + flat physics data (residual, post-MP-Alloc) | ⚪ hard-queued behind M1.5 #137 |
| MP5 | Job-system parallelism | ⚪ stretch, evidence-gated | | MP5 | Job-system parallelism | ⚪ stretch, evidence-gated |

View file

@ -640,7 +640,8 @@ include dungeons.
stop bookkeeping, and emitter-first render rejection. The landscape renderer stop bookkeeping, and emitter-first render rejection. The landscape renderer
publishes outdoor landcells independently of surviving object meshes. publishes outdoor landcells independently of surviving object meshes.
Terrain/entity streaming distance is Terrain/entity streaming distance is
unchanged; an explicit Extended setting doubles only particle range. The unchanged; the user-requested default Extended setting doubles only particle
range, while Retail remains available as the exact cutoff. The
final two-client visual gate is still pending. final two-client visual gate is still pending.
- **L.1c local attack receive path (implemented and live-gated 2026-07-15)** - **L.1c local attack receive path (implemented and live-gated 2026-07-15)**
local non-autonomous mt-0 UpdateMotion now uses retail's wholesale interpreted local non-autonomous mt-0 UpdateMotion now uses retail's wholesale interpreted

View file

@ -172,5 +172,6 @@ does not change landscape, scenery, entity, or fog distance.
Login and portal-space frames publish an empty world view; explicit Login and portal-space frames publish an empty world view; explicit
examination and dedicated-pass policies are the only bypasses. examination and dedicated-pass policies are the only bypasses.
- The renderer filters an emitter before scanning its particle slots. - The renderer filters an emitter before scanning its particle slots.
- Default mode uses authored retail distances. The optional Extended setting is - The user-requested default Extended mode doubles authored retail distances.
an explicit user-facing adaptation and is recorded in the divergence register. Exact Retail remains selectable; the default adaptation is recorded in the
divergence register.

View file

@ -5,8 +5,8 @@ namespace AcDream.UI.Abstractions.Panels.Settings;
/// <summary> /// <summary>
/// Particle visibility distance policy. Retail uses the GfxObj-authored /// Particle visibility distance policy. Retail uses the GfxObj-authored
/// distance exactly; Extended is an explicit acdream adaptation for players /// distance exactly; Extended is the acdream default requested for longer
/// who prefer longer-lived distant effects with the corresponding CPU cost. /// distant-effect visibility, with the corresponding CPU cost.
/// </summary> /// </summary>
public enum ParticleRange public enum ParticleRange
{ {
@ -49,7 +49,7 @@ public sealed record DisplaySettings(
Gamma: 1.0f, Gamma: 1.0f,
ShowFps: false, ShowFps: false,
Quality: QualityPreset.High, Quality: QualityPreset.High,
ParticleRange: ParticleRange.Retail); ParticleRange: ParticleRange.Extended);
/// <summary>16:9 resolution presets offered in the dropdown.</summary> /// <summary>16:9 resolution presets offered in the dropdown.</summary>
public static IReadOnlyList<string> AvailableResolutions { get; } = new[] public static IReadOnlyList<string> AvailableResolutions { get; } = new[]

View file

@ -238,7 +238,7 @@ public sealed class SettingsPanel : IPanel
int particleRangeIndex = (int)d.ParticleRange; int particleRangeIndex = (int)d.ParticleRange;
if (particleRangeIndex < 0 || particleRangeIndex >= s_particleRangeNames.Length) if (particleRangeIndex < 0 || particleRangeIndex >= s_particleRangeNames.Length)
particleRangeIndex = (int)ParticleRange.Retail; particleRangeIndex = (int)DisplaySettings.Default.ParticleRange;
if (renderer.Combo("Particle Range", ref particleRangeIndex, s_particleRangeNames)) if (renderer.Combo("Particle Range", ref particleRangeIndex, s_particleRangeNames))
_vm.SetDisplay(d with { ParticleRange = (ParticleRange)particleRangeIndex }); _vm.SetDisplay(d with { ParticleRange = (ParticleRange)particleRangeIndex });
@ -248,8 +248,8 @@ public sealed class SettingsPanel : IPanel
+ "preview live as you drag; Cancel reverts to the saved value. " + "preview live as you drag; Cancel reverts to the saved value. "
+ "Quality preset applies streaming radius, anisotropic, and A2C " + "Quality preset applies streaming radius, anisotropic, and A2C "
+ "immediately on Save; MSAA sample count requires a restart. " + "immediately on Save; MSAA sample count requires a restart. "
+ "Particle Range defaults to retail-authored distances; Extended " + "Particle Range defaults to Extended, which doubles authored "
+ "doubles effect range and costs additional CPU time."); + "effect distance; Retail restores the exact client cutoff.");
} }
/// <summary> /// <summary>

View file

@ -84,11 +84,11 @@ public sealed class SettingsStoreTests : System.IDisposable
Assert.Equal(DisplaySettings.Default.Fullscreen, loaded.Fullscreen); Assert.Equal(DisplaySettings.Default.Fullscreen, loaded.Fullscreen);
Assert.Equal(DisplaySettings.Default.VSync, loaded.VSync); Assert.Equal(DisplaySettings.Default.VSync, loaded.VSync);
Assert.Equal(DisplaySettings.Default.FieldOfView, loaded.FieldOfView); Assert.Equal(DisplaySettings.Default.FieldOfView, loaded.FieldOfView);
Assert.Equal(ParticleRange.Retail, loaded.ParticleRange); Assert.Equal(ParticleRange.Extended, loaded.ParticleRange);
} }
[Fact] [Fact]
public void LoadDisplay_invalid_particle_range_falls_back_to_retail() public void LoadDisplay_invalid_particle_range_falls_back_to_default()
{ {
File.WriteAllText(_tempPath, """ File.WriteAllText(_tempPath, """
{ {
@ -99,11 +99,11 @@ public sealed class SettingsStoreTests : System.IDisposable
var loaded = new SettingsStore(_tempPath).LoadDisplay(); var loaded = new SettingsStore(_tempPath).LoadDisplay();
Assert.Equal(ParticleRange.Retail, loaded.ParticleRange); Assert.Equal(ParticleRange.Extended, loaded.ParticleRange);
} }
[Fact] [Fact]
public void LoadDisplay_numeric_undefined_particle_range_falls_back_to_retail() public void LoadDisplay_numeric_undefined_particle_range_falls_back_to_default()
{ {
File.WriteAllText(_tempPath, """ File.WriteAllText(_tempPath, """
{ {
@ -114,7 +114,7 @@ public sealed class SettingsStoreTests : System.IDisposable
var loaded = new SettingsStore(_tempPath).LoadDisplay(); var loaded = new SettingsStore(_tempPath).LoadDisplay();
Assert.Equal(ParticleRange.Retail, loaded.ParticleRange); Assert.Equal(ParticleRange.Extended, loaded.ParticleRange);
} }
[Fact] [Fact]