fix: complete retail parity stability pass
This commit is contained in:
parent
d3df4cb20a
commit
f7aa8e0eb7
131 changed files with 7765 additions and 1190 deletions
|
|
@ -109,10 +109,17 @@ public static class InteriorEntityPartition
|
|||
HashSet<uint> visibleCells,
|
||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||
IReadOnlyList<WorldEntity> Entities,
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries)
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries,
|
||||
FrustumPlanes? frustum = null,
|
||||
uint neverCullLandblockId = 0u)
|
||||
{
|
||||
var result = new Result();
|
||||
Partition(result, visibleCells, landblockEntries);
|
||||
Partition(
|
||||
result,
|
||||
visibleCells,
|
||||
landblockEntries,
|
||||
frustum,
|
||||
neverCullLandblockId);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -131,11 +138,23 @@ public static class InteriorEntityPartition
|
|||
HashSet<uint> visibleCells,
|
||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||
IReadOnlyList<WorldEntity> Entities,
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries)
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries,
|
||||
FrustumPlanes? frustum = null,
|
||||
uint neverCullLandblockId = 0u)
|
||||
{
|
||||
result.ClearForReuse();
|
||||
foreach (var entry in landblockEntries)
|
||||
{
|
||||
if (!IsLandblockVisible(
|
||||
entry.LandblockId,
|
||||
entry.AabbMin,
|
||||
entry.AabbMax,
|
||||
frustum,
|
||||
neverCullLandblockId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var e in entry.Entities)
|
||||
{
|
||||
if (e.MeshRefs.Count == 0) continue;
|
||||
|
|
@ -176,11 +195,18 @@ public static class InteriorEntityPartition
|
|||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||
IReadOnlyList<WorldEntity> Entities,
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries,
|
||||
IObserver? observer)
|
||||
IObserver? observer,
|
||||
FrustumPlanes? frustum = null,
|
||||
uint neverCullLandblockId = 0u)
|
||||
{
|
||||
if (observer is null)
|
||||
{
|
||||
Partition(result, visibleCells, landblockEntries);
|
||||
Partition(
|
||||
result,
|
||||
visibleCells,
|
||||
landblockEntries,
|
||||
frustum,
|
||||
neverCullLandblockId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -190,6 +216,16 @@ public static class InteriorEntityPartition
|
|||
result.ClearForReuse();
|
||||
foreach (var entry in landblockEntries)
|
||||
{
|
||||
if (!IsLandblockVisible(
|
||||
entry.LandblockId,
|
||||
entry.AabbMin,
|
||||
entry.AabbMax,
|
||||
frustum,
|
||||
neverCullLandblockId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var e in entry.Entities)
|
||||
{
|
||||
if (e.MeshRefs.Count == 0) continue;
|
||||
|
|
@ -247,4 +283,14 @@ public static class InteriorEntityPartition
|
|||
|
||||
/// <inheritdoc cref="IsIndoorCellId(uint)"/>
|
||||
public static bool IsIndoorCellId(uint? cellId) => cellId is uint c && IsIndoorCellId(c);
|
||||
|
||||
private static bool IsLandblockVisible(
|
||||
uint landblockId,
|
||||
Vector3 aabbMin,
|
||||
Vector3 aabbMax,
|
||||
FrustumPlanes? frustum,
|
||||
uint neverCullLandblockId) =>
|
||||
frustum is null
|
||||
|| landblockId == neverCullLandblockId
|
||||
|| FrustumCuller.IsAabbVisible(frustum.Value, aabbMin, aabbMax);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,16 +159,12 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
continue;
|
||||
if (span > 0 && legacyAdvanceSeconds > 0f)
|
||||
{
|
||||
animation.CurrFrame += legacyAdvanceSeconds * animation.Framerate;
|
||||
if (animation.CurrFrame > animation.HighFrame)
|
||||
{
|
||||
float over = animation.CurrFrame - animation.LowFrame;
|
||||
animation.CurrFrame = animation.LowFrame + (over % (span + 1));
|
||||
}
|
||||
else if (animation.CurrFrame < animation.LowFrame)
|
||||
{
|
||||
animation.CurrFrame = animation.LowFrame;
|
||||
}
|
||||
animation.CurrFrame = RetailAnimationCyclePlayback.Advance(
|
||||
animation.CurrFrame,
|
||||
animation.LowFrame,
|
||||
animation.HighFrame,
|
||||
animation.Framerate,
|
||||
legacyAdvanceSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -279,33 +275,14 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
return false;
|
||||
}
|
||||
|
||||
int frameIndex = (int)Math.Floor(animation.CurrFrame);
|
||||
if (frameIndex < animation.LowFrame
|
||||
|| frameIndex > animation.HighFrame
|
||||
|| frameIndex >= animation.Animation.PartFrames.Count)
|
||||
{
|
||||
frameIndex = animation.LowFrame;
|
||||
}
|
||||
int nextIndex = frameIndex + 1;
|
||||
if (nextIndex > animation.HighFrame
|
||||
|| nextIndex >= animation.Animation.PartFrames.Count)
|
||||
{
|
||||
nextIndex = animation.LowFrame;
|
||||
}
|
||||
float t = Math.Clamp(animation.CurrFrame - frameIndex, 0f, 1f);
|
||||
var frames = animation.Animation.PartFrames[frameIndex].Frames;
|
||||
var nextFrames = animation.Animation.PartFrames[nextIndex].Frames;
|
||||
if (partIndex < frames.Count)
|
||||
{
|
||||
var first = frames[partIndex];
|
||||
var next = partIndex < nextFrames.Count ? nextFrames[partIndex] : first;
|
||||
origin = Vector3.Lerp(first.Origin, next.Origin, t);
|
||||
orientation = Quaternion.Slerp(first.Orientation, next.Orientation, t);
|
||||
return true;
|
||||
}
|
||||
origin = default;
|
||||
orientation = default;
|
||||
return false;
|
||||
return RetailAnimationCyclePlayback.TryInterpolatePart(
|
||||
animation.Animation,
|
||||
animation.CurrFrame,
|
||||
animation.LowFrame,
|
||||
animation.HighFrame,
|
||||
partIndex,
|
||||
out origin,
|
||||
out orientation);
|
||||
}
|
||||
|
||||
private static void EnsureRetainedPoses(LiveEntityAnimationState animation)
|
||||
|
|
|
|||
|
|
@ -239,7 +239,9 @@ public sealed class RetailPViewRenderer
|
|||
_partitionResult,
|
||||
prepareCells,
|
||||
ctx.LandblockEntries,
|
||||
_partitionObserver);
|
||||
_partitionObserver,
|
||||
ctx.Frustum,
|
||||
ctx.PlayerLandblockId ?? 0u);
|
||||
partition = _partitionResult;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Meshing;
|
||||
|
|
@ -52,15 +53,17 @@ public sealed partial class SkyRenderer : IDisposable
|
|||
// Lazily-built GPU resources per sky-GfxObj.
|
||||
private readonly Dictionary<uint, List<SubMeshGpu>> _gpuByGfxObj = new();
|
||||
|
||||
// When did we start running — used to accumulate TexVelocityX/Y over
|
||||
// real time (independent of the day-fraction clock).
|
||||
private readonly DateTime _startedAt = DateTime.UtcNow;
|
||||
// Retail advances animated texture coordinates from Timer::cur_time
|
||||
// deltas in CPhysics::UseTime. Stopwatch is the matching monotonic clock:
|
||||
// unlike wall time, OS clock synchronization cannot make rain/cloud UVs
|
||||
// jump forward or backward.
|
||||
private readonly long _animationStartedAtTimestamp = Stopwatch.GetTimestamp();
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V7: pins the sky's scroll phase to a fixed number of
|
||||
/// seconds instead of reading the wall clock, so two launches agree.
|
||||
/// seconds instead of advancing the live animation clock, so two launches agree.
|
||||
/// <c>null</c> — the default, and what every ordinary run gets — keeps the
|
||||
/// wall clock.
|
||||
/// monotonic real-elapsed-time clock.
|
||||
///
|
||||
/// <para><b>Why the sky needs its own pin when the world clock is already
|
||||
/// pinnable.</b> Two independent clocks drive this renderer. The Dereth clock
|
||||
|
|
@ -239,7 +242,9 @@ public sealed partial class SkyRenderer : IDisposable
|
|||
var replaces = PickReplaces(group, dayFraction);
|
||||
|
||||
float secondsSinceStart = AnimationPhaseSecondsOverride
|
||||
?? (float)(DateTime.UtcNow - _startedAt).TotalSeconds;
|
||||
?? ElapsedAnimationSeconds(
|
||||
_animationStartedAtTimestamp,
|
||||
Stopwatch.GetTimestamp());
|
||||
|
||||
for (int i = 0; i < group.SkyObjects.Count; i++)
|
||||
{
|
||||
|
|
@ -454,6 +459,9 @@ public sealed partial class SkyRenderer : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
internal static float ElapsedAnimationSeconds(long startTimestamp, long currentTimestamp)
|
||||
=> (float)Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp).TotalSeconds;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6e: the table slot for one (texture, wrap-mode) pair,
|
||||
/// interning a resident bindless handle on first use.
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ namespace AcDream.App.Rendering;
|
|||
/// </summary>
|
||||
internal sealed class SkyPesFrameController
|
||||
{
|
||||
private static readonly Matrix4x4[] IdentityPartPose =
|
||||
[Matrix4x4.Identity];
|
||||
|
||||
private readonly record struct SkyPesKey(
|
||||
int ObjectIndex,
|
||||
uint GfxObjId,
|
||||
|
|
@ -122,12 +125,23 @@ internal sealed class SkyPesFrameController
|
|||
? ParticleRenderPass.SkyPostScene
|
||||
: ParticleRenderPass.SkyPreScene;
|
||||
_particles.SetEntityRenderPass(ownerId, renderPass);
|
||||
// The sky cell follows the viewer. Keep the script dispatch
|
||||
// anchor on that same current-frame pose: SoundTweaked hooks in
|
||||
// the Rainy carriers are ordinary world sounds, and a stale
|
||||
// creation-time anchor falls beyond retail's audible radius as
|
||||
// soon as login/teleport/movement displaces the camera.
|
||||
_scripts.SetOwnerAnchor(ownerId, cameraWorldPosition);
|
||||
Quaternion rotation = Rotation(skyObject, dayFraction);
|
||||
_poses.Publish(
|
||||
ownerId,
|
||||
Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* Matrix4x4.CreateTranslation(cameraWorldPosition),
|
||||
Array.Empty<Matrix4x4>(),
|
||||
// Dereth's scripted sky carriers (including lightning Setup
|
||||
// 0x02000BA6) are one-part dummy anchors whose default part-0
|
||||
// frame is identity. Their CreateParticle hooks target part
|
||||
// 0, not the -1 root sentinel, so a root-only synthetic pose
|
||||
// makes a live carrier look pose-less to ParticleHookSink.
|
||||
IdentityPartPose,
|
||||
cellId: 0u);
|
||||
|
||||
if (_active.Contains(key) || _missing.Contains(key))
|
||||
|
|
|
|||
|
|
@ -264,11 +264,8 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
{
|
||||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||
int groupIndex = drawRange.GroupIndex;
|
||||
var cullMode = (CullMode)(groupIndex % 4);
|
||||
// Phase A8 visual-gate evidence: cell meshes use CullMode.Landblock
|
||||
// uniformly, but the room surfaces need to be visible from inside.
|
||||
// Render cell polys double-sided, exactly as the GL arm does.
|
||||
if (cullMode == CullMode.Landblock) cullMode = CullMode.None;
|
||||
CullMode cullMode = ResolveRetailCellShellCullMode(
|
||||
(CullMode)(groupIndex % 4));
|
||||
|
||||
bool isAdditive = groupIndex >= 4;
|
||||
IGpuPipeline rangeBasePipeline = isAdditive
|
||||
|
|
@ -361,9 +358,8 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
|
||||
{
|
||||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||
var cullMode = (CullMode)(drawRange.GroupIndex % 4);
|
||||
if (cullMode == CullMode.Landblock)
|
||||
cullMode = CullMode.None;
|
||||
CullMode cullMode = ResolveRetailCellShellCullMode(
|
||||
(CullMode)(drawRange.GroupIndex % 4));
|
||||
SetCullMode(encoder, cullMode);
|
||||
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
|
|
@ -417,6 +413,25 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a CellStruct polygon's DAT <c>sides_type</c> to the render
|
||||
/// state used by retail's constructed EnvCell mesh. The similarly named
|
||||
/// <see cref="CullMode"/> values on <c>Polygon.SidesType</c> are not GPU
|
||||
/// cull states: 0 emits the positive face, 1 emits that face twice with
|
||||
/// reversed indices, and 2 emits the positive and negative surface.
|
||||
/// <c>D3DPolyRender::ConstructMesh @ 0x0059DFA0</c> performs that geometry
|
||||
/// expansion, then every subset is drawn with <c>D3DCULL_CW</c> through
|
||||
/// <c>RenderMeshSubset @ 0x0059CA10</c>. <see cref="MeshExtractor"/>
|
||||
/// already performs the identical expansion, so every shell batch must
|
||||
/// cull clockwise here. Returning <see cref="CullMode.None"/> for DAT 0
|
||||
/// was #178's Phase-A8 double-sided stopgap.
|
||||
/// </summary>
|
||||
internal static CullMode ResolveRetailCellShellCullMode(CullMode sidesType)
|
||||
{
|
||||
_ = sidesType;
|
||||
return CullMode.Clockwise;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reserves this frame's ring, copies into it, and binds the slice. A
|
||||
/// logically empty section still reserves one element so the bound range is
|
||||
|
|
|
|||
|
|
@ -462,6 +462,7 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
|
|||
private readonly SceneLightingUboBinding? _lightingUbo;
|
||||
private readonly IWorldRenderRangeSource _ranges;
|
||||
private readonly SkyPesFrameController? _skyPes;
|
||||
private readonly Func<bool> _persistentDaylight;
|
||||
private readonly HashSet<uint> _visibleCells = [];
|
||||
private bool _visibleCellsValid;
|
||||
|
||||
|
|
@ -473,7 +474,8 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
|
|||
EnvCellRenderer? environmentCells,
|
||||
SceneLightingUboBinding? lightingUbo,
|
||||
IWorldRenderRangeSource ranges,
|
||||
SkyPesFrameController? skyPes)
|
||||
SkyPesFrameController? skyPes,
|
||||
Func<bool>? persistentDaylight = null)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_worldTime = worldTime ?? throw new ArgumentNullException(nameof(worldTime));
|
||||
|
|
@ -483,6 +485,7 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
|
|||
_lightingUbo = lightingUbo;
|
||||
_ranges = ranges ?? throw new ArgumentNullException(nameof(ranges));
|
||||
_skyPes = skyPes;
|
||||
_persistentDaylight = persistentDaylight ?? (static () => false);
|
||||
}
|
||||
|
||||
public void Prepare(
|
||||
|
|
@ -500,7 +503,14 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
|
|||
activeDayGroup,
|
||||
camera.Position);
|
||||
|
||||
UpdateSunFromSky(foundation.Sky, roots.PlayerInsideCell);
|
||||
// LScape::set_landscape_lighting @0x005054D0 keeps the live sky/fog
|
||||
// clock but, when PersistentAtDay is set, asks the active region for
|
||||
// lighting at exactly 0.5 (noon). Do not pin WorldTime.DayFraction:
|
||||
// clouds, celestial objects, fog, and scripts must keep advancing.
|
||||
SkyKeyframe landscapeLighting = _persistentDaylight()
|
||||
? _worldTime.SkyAtDayFraction(0.5f)
|
||||
: foundation.Sky;
|
||||
UpdateSunFromSky(landscapeLighting, roots.PlayerInsideCell);
|
||||
_lighting.UpdateViewerLight(roots.PlayerViewPosition);
|
||||
_lighting.Tick(camera.Position);
|
||||
_lighting.BuildPointLightSnapshot(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue