Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
240 lines
8.7 KiB
C#
240 lines
8.7 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Numerics;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.Lib.IO;
|
|
using AcDream.Core.Content;
|
|
using DatParticleEmitter = DatReaderWriter.DBObjs.ParticleEmitter;
|
|
using DatGfxObj = DatReaderWriter.DBObjs.GfxObj;
|
|
using DatGfxObjDegradeInfo = DatReaderWriter.DBObjs.GfxObjDegradeInfo;
|
|
using DatEmitterType = DatReaderWriter.Enums.EmitterType;
|
|
using DatParticleType = DatReaderWriter.Enums.ParticleType;
|
|
using DatGfxObjFlags = DatReaderWriter.Enums.GfxObjFlags;
|
|
|
|
namespace AcDream.Core.Vfx;
|
|
|
|
/// <summary>
|
|
/// Resolves retail <c>ParticleEmitterInfo</c> dat records
|
|
/// (<c>0x32xxxxxx</c>) into acdream runtime descriptors.
|
|
/// </summary>
|
|
public sealed class EmitterDescRegistry
|
|
{
|
|
private readonly Func<uint, DatParticleEmitter?>? _resolver;
|
|
private readonly Func<DatParticleEmitter, float?>? _degradeDistanceResolver;
|
|
private readonly ConcurrentDictionary<uint, EmitterDesc> _byId = new();
|
|
|
|
public EmitterDescRegistry()
|
|
: this((Func<uint, DatParticleEmitter?>?)null)
|
|
{
|
|
}
|
|
|
|
public EmitterDescRegistry(IDatObjectSource dats)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dats);
|
|
_resolver = id => SafeGet<DatParticleEmitter>(dats, id);
|
|
_degradeDistanceResolver = emitter => ResolveMaxDegradeDistance(dats, emitter);
|
|
}
|
|
|
|
public EmitterDescRegistry(Func<uint, DatParticleEmitter?>? resolver)
|
|
{
|
|
_resolver = resolver;
|
|
}
|
|
|
|
public void Register(EmitterDesc desc)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(desc);
|
|
_byId[desc.DatId] = desc;
|
|
}
|
|
|
|
public EmitterDesc Get(uint emitterId)
|
|
{
|
|
if (TryGet(emitterId, out EmitterDesc? desc))
|
|
return desc;
|
|
throw new KeyNotFoundException(
|
|
$"ParticleEmitterInfo 0x{emitterId:X8} was not found.");
|
|
}
|
|
|
|
public bool TryGet(uint emitterId, out EmitterDesc desc)
|
|
{
|
|
if (_byId.TryGetValue(emitterId, out desc!))
|
|
return true;
|
|
if (_resolver is not null)
|
|
{
|
|
var dat = _resolver(emitterId);
|
|
if (dat is not null)
|
|
{
|
|
float? resolvedDistance = _degradeDistanceResolver?.Invoke(dat);
|
|
if (_degradeDistanceResolver is not null && !resolvedDistance.HasValue)
|
|
{
|
|
// ParticleEmitter::SetInfo (0x0051CE90) fails when the
|
|
// authored hardware GfxObj cannot be loaded. It never
|
|
// substitutes the software GfxObj.
|
|
desc = null!;
|
|
return false;
|
|
}
|
|
float maxDegradeDistance = resolvedDistance
|
|
?? RetailParticleDegradeDistance.DefaultDistance;
|
|
desc = FromDat(emitterId, dat, maxDegradeDistance);
|
|
_byId[emitterId] = desc;
|
|
return true;
|
|
}
|
|
}
|
|
desc = null!;
|
|
return false;
|
|
}
|
|
|
|
public int Count => _byId.Count;
|
|
|
|
public static EmitterDesc FromDat(
|
|
uint emitterId,
|
|
DatParticleEmitter dat,
|
|
float maxDegradeDistance = RetailParticleDegradeDistance.DefaultDistance)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dat);
|
|
|
|
float birthrate = MathF.Max(0f, (float)dat.Birthrate);
|
|
float lifespan = MathF.Max(0f, (float)dat.Lifespan);
|
|
float lifespanRand = MathF.Abs((float)dat.LifespanRand);
|
|
float lifetimeMin = MathF.Max(0f, lifespan - lifespanRand);
|
|
float lifetimeMax = MathF.Max(lifetimeMin, lifespan + lifespanRand);
|
|
|
|
// ParticleEmitterInfo has no "additive" field; retail derives blend
|
|
// state from the particle GfxObj surface material.
|
|
var flags = EmitterFlags.Billboard | EmitterFlags.FaceCamera;
|
|
if (dat.IsParentLocal)
|
|
flags |= EmitterFlags.AttachLocal;
|
|
|
|
// ParticleEmitterInfo stores translucency, not opacity. Retail feeds
|
|
// StartTrans/FinalTrans to PhysicsPart::SetTranslucency; the GL path
|
|
// uses the complement as source alpha.
|
|
float startOpacity = 1f - Math.Clamp((float)dat.StartTrans, 0f, 1f);
|
|
float endOpacity = 1f - Math.Clamp((float)dat.FinalTrans, 0f, 1f);
|
|
|
|
return new EmitterDesc
|
|
{
|
|
MaxDegradeDistance = maxDegradeDistance,
|
|
DatId = emitterId,
|
|
Type = MapParticleType(dat.ParticleType),
|
|
EmitterKind = MapEmitterKind(dat.EmitterType),
|
|
Flags = flags,
|
|
GfxObjId = dat.GfxObjId.DataId,
|
|
HwGfxObjId = dat.HwGfxObjId.DataId,
|
|
Birthrate = birthrate,
|
|
EmitRate = dat.EmitterType == DatEmitterType.BirthratePerSec && birthrate > 0f
|
|
? 1f / birthrate
|
|
: 0f,
|
|
MaxParticles = Math.Max(1, dat.MaxParticles),
|
|
InitialParticles = Math.Max(0, dat.InitialParticles),
|
|
TotalParticles = Math.Max(0, dat.TotalParticles),
|
|
TotalDuration = MathF.Max(0f, (float)dat.TotalSeconds),
|
|
Lifespan = lifespan,
|
|
LifespanRand = lifespanRand,
|
|
LifetimeMin = lifetimeMin,
|
|
LifetimeMax = lifetimeMax,
|
|
OffsetDir = dat.OffsetDir,
|
|
MinOffset = dat.MinOffset,
|
|
MaxOffset = dat.MaxOffset,
|
|
SpawnDiskRadius = dat.MaxOffset,
|
|
InitialVelocity = dat.A,
|
|
Gravity = dat.B,
|
|
A = dat.A,
|
|
MinA = dat.MinA,
|
|
MaxA = dat.MaxA,
|
|
B = dat.B,
|
|
MinB = dat.MinB,
|
|
MaxB = dat.MaxB,
|
|
C = dat.C,
|
|
MinC = dat.MinC,
|
|
MaxC = dat.MaxC,
|
|
StartSize = dat.StartScale,
|
|
EndSize = dat.FinalScale,
|
|
ScaleRand = dat.ScaleRand,
|
|
StartAlpha = startOpacity,
|
|
EndAlpha = endOpacity,
|
|
TransRand = dat.TransRand,
|
|
};
|
|
}
|
|
|
|
private static float? ResolveMaxDegradeDistance(
|
|
IDatObjectSource dats,
|
|
DatParticleEmitter emitter)
|
|
{
|
|
uint gfxObjId = GetRetailHardwareGfxObjId(emitter);
|
|
if (gfxObjId == 0)
|
|
return null;
|
|
|
|
DatGfxObj? gfx = SafeGet<DatGfxObj>(dats, gfxObjId);
|
|
if (gfx is null)
|
|
return null;
|
|
if (!gfx.Flags.HasFlag(DatGfxObjFlags.HasDIDDegrade)
|
|
|| gfx.DIDDegrade == 0)
|
|
{
|
|
return RetailParticleDegradeDistance.DefaultDistance;
|
|
}
|
|
|
|
DatGfxObjDegradeInfo? degrade = SafeGet<DatGfxObjDegradeInfo>(dats, gfx.DIDDegrade);
|
|
return RetailParticleDegradeDistance.FromEntries(degrade?.Degrades);
|
|
}
|
|
|
|
internal static uint GetRetailHardwareGfxObjId(DatParticleEmitter emitter)
|
|
=> emitter.HwGfxObjId.DataId;
|
|
|
|
private static T? SafeGet<T>(IDatObjectSource dats, uint id) where T : class, IDBObj
|
|
{
|
|
if (dats is null)
|
|
return null;
|
|
try
|
|
{
|
|
return dats.Get<T>(id);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static ParticleEmitterKind MapEmitterKind(DatEmitterType type) => type switch
|
|
{
|
|
DatEmitterType.BirthratePerSec => ParticleEmitterKind.BirthratePerSec,
|
|
DatEmitterType.BirthratePerMeter => ParticleEmitterKind.BirthratePerMeter,
|
|
_ => ParticleEmitterKind.Unknown,
|
|
};
|
|
|
|
private static ParticleType MapParticleType(DatParticleType type) => type switch
|
|
{
|
|
DatParticleType.Still => ParticleType.Still,
|
|
DatParticleType.LocalVelocity => ParticleType.LocalVelocity,
|
|
DatParticleType.ParabolicLVGA => ParticleType.ParabolicLVGA,
|
|
DatParticleType.ParabolicLVGAGR => ParticleType.ParabolicLVGAGR,
|
|
DatParticleType.Swarm => ParticleType.Swarm,
|
|
DatParticleType.Explode => ParticleType.Explode,
|
|
DatParticleType.Implode => ParticleType.Implode,
|
|
DatParticleType.ParabolicLVLA => ParticleType.ParabolicLVLA,
|
|
DatParticleType.ParabolicLVLALR => ParticleType.ParabolicLVLALR,
|
|
DatParticleType.ParabolicGVGA => ParticleType.ParabolicGVGA,
|
|
DatParticleType.ParabolicGVGAGR => ParticleType.ParabolicGVGAGR,
|
|
DatParticleType.GlobalVelocity => ParticleType.GlobalVelocity,
|
|
_ => ParticleType.Unknown,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exact retail maximum-distance selection for a particle GfxObj.
|
|
/// <c>CPhysicsPart::GetMaxDegradeDistance</c> (0x0050D510) supplies the
|
|
/// 100-unit default; <c>GfxObjDegradeInfo::get_max_degrade_distance</c>
|
|
/// (0x0051E2D0) selects entry zero for one/two-entry tables and the
|
|
/// second-to-last entry for larger tables.
|
|
/// </summary>
|
|
public static class RetailParticleDegradeDistance
|
|
{
|
|
public const float DefaultDistance = 100f;
|
|
|
|
public static float FromEntries(IReadOnlyList<DatReaderWriter.Types.GfxObjInfo>? entries)
|
|
{
|
|
if (entries is null || entries.Count == 0)
|
|
return DefaultDistance;
|
|
|
|
int index = entries.Count <= 2 ? 0 : entries.Count - 2;
|
|
return entries[index].MaxDist;
|
|
}
|
|
}
|