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>
194 lines
7.8 KiB
C#
194 lines
7.8 KiB
C#
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
using AcDream.Core.Content;
|
|
using DatReaderWriter.Enums;
|
|
|
|
namespace AcDream.Core.Meshing;
|
|
|
|
/// <summary>
|
|
/// Resolve a base GfxObj id to its retail "close-detail" mesh by walking
|
|
/// the <c>DIDDegrade</c> table to <c>Degrades[0]</c>.
|
|
///
|
|
/// <para>
|
|
/// <b>Why this exists (Issue #47).</b> Many AC GfxObjs — most notably
|
|
/// humanoid body parts — store the LOW-detail mesh as the GfxObj that
|
|
/// the Setup or AnimPartChange references. The high-detail mesh used
|
|
/// for close/player rendering is reached indirectly: the base GfxObj's
|
|
/// <c>HasDIDDegrade</c> flag is set, <c>DIDDegrade</c> points at a
|
|
/// <see cref="GfxObjDegradeInfo"/>, and <see cref="GfxObjInfo.Id"/> at
|
|
/// <c>Degrades[0]</c> is the close-detail variant. Drawing the base
|
|
/// GfxObj id directly produces the LOD-3 mesh — visibly bulky and
|
|
/// detail-less — which is exactly what acdream and ACViewer were both
|
|
/// rendering for humanoid body parts before this fix.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// <b>Concrete example.</b> The Aluvian Male upper-arm GfxObj
|
|
/// <c>0x01000055</c> is a 14-vertex / 17-poly low-detail stub. Its
|
|
/// degrade table <c>0x110006D0</c> points at <c>0x01001795</c>, the
|
|
/// 32-vertex / 60-poly close-detail mesh that carries the bicep /
|
|
/// deltoid / shoulder geometry retail draws on the player. Same story
|
|
/// for the lower arm <c>0x01000056 → 0x0100178F</c> and matching
|
|
/// heritage variants (<c>0x010004BF → 0x010017A8</c>,
|
|
/// <c>0x010004BD → 0x010017A7</c>, <c>0x010004B7 → 0x0100179A</c>).
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// <b>Retail flow (named-retail decomp).</b>
|
|
/// <list type="bullet">
|
|
/// <item>
|
|
/// <c>acclient!CPhysicsPart::LoadGfxObjArray</c> at <c>0x0050DCF0</c>
|
|
/// loads the base GfxObj solely to discover <c>DIDDegrade</c>; if
|
|
/// a <see cref="GfxObjDegradeInfo"/> exists, retail loads each entry
|
|
/// in <c>Degrades</c> into the part's render array.
|
|
/// </item>
|
|
/// <item>
|
|
/// <c>acclient!CPhysicsPart::UpdateViewerDistance</c> at
|
|
/// <c>0x0050E030</c> picks <c>deg_level</c> per part by distance.
|
|
/// For close / player rendering <c>deg_level == 0</c>.
|
|
/// </item>
|
|
/// <item>
|
|
/// <c>acclient!CPhysicsPart::Draw</c> at <c>0x0050D7A0</c>
|
|
/// draws <c>gfxobj[deg_level]</c>.
|
|
/// </item>
|
|
/// </list>
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// We don't yet have distance-based LOD plumbing, so this resolver
|
|
/// always returns slot 0 (the close-detail mesh). That's correct for
|
|
/// player + nearby NPC rendering; far-distance LOD is a future concern.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class GfxObjDegradeResolver
|
|
{
|
|
/// <summary>
|
|
/// DatCollection-backed convenience overload. Production callers use
|
|
/// this; tests use the callback overload below for easy fakes.
|
|
/// </summary>
|
|
public static bool TryResolveCloseGfxObj(
|
|
IDatObjectSource dats,
|
|
uint gfxObjId,
|
|
out uint resolvedId,
|
|
out GfxObj? resolvedGfxObj)
|
|
=> TryResolveCloseGfxObj(
|
|
id => dats.Get<GfxObj>(id),
|
|
id => dats.Get<GfxObjDegradeInfo>(id),
|
|
gfxObjId,
|
|
out resolvedId,
|
|
out resolvedGfxObj);
|
|
|
|
/// <summary>
|
|
/// Loader-callback overload. Returns the close-detail GfxObj id and
|
|
/// loaded object when a degrade table is present, otherwise the
|
|
/// base id and base GfxObj.
|
|
/// </summary>
|
|
/// <param name="getGfxObj">
|
|
/// Lookup for a GfxObj by id. May return null when not found.
|
|
/// </param>
|
|
/// <param name="getDegradeInfo">
|
|
/// Lookup for a GfxObjDegradeInfo by id. May return null.
|
|
/// </param>
|
|
/// <param name="gfxObjId">Base GfxObj id (post-AnimPartChange).</param>
|
|
/// <param name="resolvedId">
|
|
/// The id to actually render. Same as <paramref name="gfxObjId"/>
|
|
/// when no degrade table exists; <c>Degrades[0].Id</c> when it does.
|
|
/// </param>
|
|
/// <param name="resolvedGfxObj">
|
|
/// The loaded GfxObj for <paramref name="resolvedId"/>, cached so
|
|
/// callers don't have to re-read.
|
|
/// </param>
|
|
/// <returns>
|
|
/// <c>true</c> if a usable GfxObj was resolved (either base or
|
|
/// degrade slot 0 loaded). <c>false</c> only when the base GfxObj
|
|
/// itself was missing — caller should drop this part.
|
|
/// </returns>
|
|
public static bool TryResolveCloseGfxObj(
|
|
Func<uint, GfxObj?> getGfxObj,
|
|
Func<uint, GfxObjDegradeInfo?> getDegradeInfo,
|
|
uint gfxObjId,
|
|
out uint resolvedId,
|
|
out GfxObj? resolvedGfxObj)
|
|
{
|
|
var gfxObj = getGfxObj(gfxObjId);
|
|
if (gfxObj is null)
|
|
{
|
|
resolvedId = gfxObjId;
|
|
resolvedGfxObj = null;
|
|
return false;
|
|
}
|
|
|
|
// Default: base mesh stays selected unless the degrade table
|
|
// resolves cleanly. Every fallback below leaves these set.
|
|
resolvedId = gfxObjId;
|
|
resolvedGfxObj = gfxObj;
|
|
|
|
if (!gfxObj.Flags.HasFlag(GfxObjFlags.HasDIDDegrade) || gfxObj.DIDDegrade == 0)
|
|
return true;
|
|
|
|
var degradeInfo = getDegradeInfo(gfxObj.DIDDegrade);
|
|
if (degradeInfo is null || degradeInfo.Degrades.Count == 0)
|
|
return true;
|
|
|
|
uint closeId = (uint)degradeInfo.Degrades[0].Id;
|
|
if (closeId == 0)
|
|
return true;
|
|
|
|
var closeGfxObj = getGfxObj(closeId);
|
|
if (closeGfxObj is null)
|
|
return true;
|
|
|
|
resolvedId = closeId;
|
|
resolvedGfxObj = closeGfxObj;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// True when a GfxObj is an EDITOR-ONLY placement marker that retail's distance-based
|
|
/// degrade hides at any runtime distance. Such a marker's closest degrade slot is visible
|
|
/// ONLY at distance 0 (<c>Degrades[0].MaxDist == 0</c>) and the table degrades to GfxObj
|
|
/// id 0 (= nothing) at real distance. Retail
|
|
/// (<c>CPhysicsPart::UpdateViewerDistance</c> 0x0050E030 → <c>Draw</c> 0x0050D7A0 picks
|
|
/// <c>gfxobj[deg_level]</c> by viewer distance) therefore never draws it in the live
|
|
/// client — only WorldBuilder shows it at the editor origin. acdream has no per-frame
|
|
/// distance-LOD (the resolver above always returns slot 0), so without this check it
|
|
/// renders the marker mesh forever — the #136 dungeon "red/green cone" (Setup 0x02000C39
|
|
/// / GfxObj 0x010028CA, whose degrade table 0x11000118 is {slot0 Id=mesh MaxDist=0,
|
|
/// slot1 Id=0 MaxDist=FLT_MAX}). Callers that hydrate static geometry (always viewed at
|
|
/// distance > 0) skip such GfxObjs.
|
|
/// </summary>
|
|
public static bool IsRuntimeHiddenMarker(IDatObjectSource dats, uint gfxObjId)
|
|
=> IsRuntimeHiddenMarker(
|
|
id => dats.Get<GfxObj>(id),
|
|
id => dats.Get<GfxObjDegradeInfo>(id),
|
|
gfxObjId);
|
|
|
|
/// <summary>Loader-callback overload of <see cref="IsRuntimeHiddenMarker(DatCollection, uint)"/>.</summary>
|
|
public static bool IsRuntimeHiddenMarker(
|
|
Func<uint, GfxObj?> getGfxObj,
|
|
Func<uint, GfxObjDegradeInfo?> getDegradeInfo,
|
|
uint gfxObjId)
|
|
{
|
|
var gfxObj = getGfxObj(gfxObjId);
|
|
if (gfxObj is null
|
|
|| !gfxObj.Flags.HasFlag(GfxObjFlags.HasDIDDegrade)
|
|
|| gfxObj.DIDDegrade == 0)
|
|
return false;
|
|
|
|
var info = getDegradeInfo(gfxObj.DIDDegrade);
|
|
if (info is null || info.Degrades.Count == 0)
|
|
return false;
|
|
|
|
// Closest slot visible only at distance exactly 0 = editor-only placement marker.
|
|
bool firstSlotEditorOnly = info.Degrades[0].MaxDist == 0f;
|
|
if (!firstSlotEditorOnly)
|
|
return false;
|
|
|
|
// ...and the table degrades to NOTHING (id 0) at real distance — confirms it
|
|
// becomes invisible at runtime rather than LOD-swapping to a real mesh.
|
|
foreach (var d in info.Degrades)
|
|
if ((uint)d.Id == 0u)
|
|
return true;
|
|
return false;
|
|
}
|
|
}
|