acdream/src/AcDream.App/Rendering/PaperdollFramePresenter.cs
Erik 8a7a0837e1 feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1
already removed ImGui/Studio/DevTools. This step deletes the GL
rendering backend itself: every Gpu/Gl/** implementation, the Wb
ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/
BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache,
RenderBootstrap, and RenderFrameGlStateController.

GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/
OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses
its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and
RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone —
there is nothing left to select between. The five world-draw dual-arm
renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer,
ParticleRenderer, SkyRenderer) and the composition roots
(WorldRenderComposition, HostInputCameraComposition,
LivePresentationComposition, FrameRootComposition) collapse to their
RHI-only arm. GL-only diagnostic properties with a live external reader
(DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op
rather than disappearing, since the reader is out of this commit's
scope.

A few GL-flavored mechanisms turned out to be backend-neutral once
isolated: GlConstructionCleanupLedger is renamed
ResourceConstructionCleanupLedger (exception-chain walking has nothing
to do with GL), and GlfwNativePlatformProbe moved out of the otherwise
GL-only GraphicalCapabilityRecord.cs into
GraphicalWindowBackendSelection.cs before the rest of that file was
deleted.

Test files with no surviving subject are deleted outright
(GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests,
PortalDepthShaderParityTests, TextureCacheBindlessTests,
TextRendererFailureSafetyTests, ClipFrameUploadTests, every
Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests);
others get their dead GL-only members trimmed while their live
assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now
reads GpuBindingModel.StorageClipRegions, the same binding index under
its new backend-neutral name; GpuResourceRetirementTransactionTests
drops its OpenGLGraphicsDevice-subclassing test double and the two GL
queue tests it existed for). EnvCellRendererTests' construction helper
now builds a real ObjectMeshManager via VulkanMeshPipelineDevice
instead of passing null through a null-forgiving operator, since the
RHI constructor never tolerated a null mesh manager and the old GL
constructor (which did) is gone.

Deferred to the next two steps, deliberately not touched here: the
Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl
(WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale
csproj comment (the package itself is still load-bearing —
TextureFormat and friends are used well beyond the deleted
ManagedGLUniformBuffer), and the CI/gate scripts.

Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors.
Tests: full-solution `dotnet test` green across every project
(App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all
others 100%); the 2 App.Tests names that flake under full-suite
parallel execution (#250-family, documented pre-existing) pass in
isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 02:19:53 +02:00

407 lines
13 KiB
C#

using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Rendering.Gpu;
using AcDream.App.UI;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter;
namespace AcDream.App.Rendering;
internal interface IPaperdollDollRenderer
{
void SetDoll(WorldEntity? doll);
uint Render(int width, int height);
}
internal interface IPaperdollFrameView
{
bool TryGetVisibleSize(out int width, out int height);
void SetTextureHandle(uint textureHandle);
}
internal interface IPaperdollInventoryVisibility
{
bool IsVisible { get; }
}
internal interface IPaperdollDollFactory
{
bool TryBuild(out WorldEntity? doll);
}
internal interface IPaperdollEntityLookup
{
bool TryGet(uint serverGuid, out WorldEntity player);
}
internal interface IPaperdollPoseApplicator
{
void Apply(WorldEntity doll, uint setupId);
}
/// <summary>
/// Owns paperdoll dirty/rebuild state and the private render-target
/// presentation edge. The renderer remains a borrowed resource disposed by
/// the existing window shutdown transaction.
/// </summary>
internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
{
private readonly IPaperdollDollRenderer _renderer;
private readonly IPaperdollFrameView _view;
private readonly IPaperdollDollFactory _factory;
private WorldEntity? _doll;
private bool _dirty = true;
public PaperdollFramePresenter(
IPaperdollDollRenderer renderer,
IPaperdollFrameView view,
IPaperdollDollFactory factory)
{
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
_view = view ?? throw new ArgumentNullException(nameof(view));
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
internal bool IsDirty => _dirty;
public void MarkDirty() => _dirty = true;
public void Render()
{
if (!_view.TryGetVisibleSize(out int width, out int height))
return;
if (_dirty)
{
if (_factory.TryBuild(out WorldEntity? doll))
{
// Same-generation CreateObject refreshes can repeat the exact
// player ObjDesc at a portal boundary. Retail redresses its
// private inventory object in place; releasing and reacquiring
// an identical synthetic owner briefly blanks the viewport and
// churns its texture composites.
if (!HasEquivalentAppearance(_doll, doll))
{
_renderer.SetDoll(doll);
_doll = doll;
}
_dirty = false;
}
else
{
// gmPaperDollUI::RedressCreature @ 0x004A3BC0 leaves its
// private m_pInventoryObject intact when the SmartBox player
// is temporarily unavailable. Keep the successful doll and
// retry this dirty redress on the next visible frame.
}
}
_view.SetTextureHandle(_renderer.Render(width, height));
}
/// <summary>
/// Clears the private object only at the owning character-session
/// boundary, matching gmPaperDollUI's private-object lifetime.
/// </summary>
public void ResetSession()
{
_renderer.SetDoll(null);
_doll = null;
_dirty = true;
}
private static bool HasEquivalentAppearance(
WorldEntity? current,
WorldEntity? candidate)
{
if (current is null || candidate is null)
return ReferenceEquals(current, candidate);
if (current.SourceGfxObjOrSetupId != candidate.SourceGfxObjOrSetupId
|| current.Scale != candidate.Scale
|| current.HiddenPartsMask != candidate.HiddenPartsMask
|| current.MeshRefs.Count != candidate.MeshRefs.Count
|| current.PartOverrides.Count != candidate.PartOverrides.Count)
{
return false;
}
for (int i = 0; i < current.MeshRefs.Count; i++)
{
MeshRef left = current.MeshRefs[i];
MeshRef right = candidate.MeshRefs[i];
if (left.GfxObjId != right.GfxObjId
|| left.PartTransform != right.PartTransform
|| !DictionaryEquals(
left.SurfaceOverrides,
right.SurfaceOverrides))
{
return false;
}
}
for (int i = 0; i < current.PartOverrides.Count; i++)
{
if (current.PartOverrides[i] != candidate.PartOverrides[i])
return false;
}
PaletteOverride? leftPalette = current.PaletteOverride;
PaletteOverride? rightPalette = candidate.PaletteOverride;
if (leftPalette is null || rightPalette is null)
return leftPalette is null && rightPalette is null;
if (leftPalette.BasePaletteId != rightPalette.BasePaletteId
|| leftPalette.SubPalettes.Count != rightPalette.SubPalettes.Count)
{
return false;
}
for (int i = 0; i < leftPalette.SubPalettes.Count; i++)
{
if (leftPalette.SubPalettes[i] != rightPalette.SubPalettes[i])
return false;
}
return true;
}
private static bool DictionaryEquals(
IReadOnlyDictionary<uint, uint>? left,
IReadOnlyDictionary<uint, uint>? right)
{
if (left is null || right is null)
return left is null && right is null;
if (left.Count != right.Count)
return false;
foreach ((uint key, uint value) in left)
{
if (!right.TryGetValue(key, out uint rightValue)
|| rightValue != value)
{
return false;
}
}
return true;
}
}
/// <summary>Retained-UI visibility and texture publication for the doll view.</summary>
internal sealed class RetailPaperdollFrameView : IPaperdollFrameView
{
private readonly UiViewport _viewport;
private readonly IPaperdollInventoryVisibility _inventory;
public RetailPaperdollFrameView(
UiViewport viewport,
IPaperdollInventoryVisibility inventory)
{
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
}
public bool TryGetVisibleSize(out int width, out int height)
{
width = 0;
height = 0;
if (!_viewport.Visible || !_inventory.IsVisible)
{
return false;
}
width = (int)_viewport.Width;
height = (int)_viewport.Height;
return true;
}
/// <summary>
/// Campaign V slice V6k: the renderer already hands out a
/// <see cref="UiTextureTableHandle"/>, so this decodes rather than registers.
/// The §7.1 external-texture seam it used to call is deleted with V4g.
/// </summary>
public void SetTextureHandle(uint textureHandle) =>
_viewport.TextureSlot = UiTextureTableHandle.ToSlot(textureHandle);
}
/// <summary>Narrow visibility adapter for the paperdoll's inventory host.</summary>
internal sealed class PaperdollInventoryVisibility : IPaperdollInventoryVisibility
{
private readonly UiElement _inventoryFrame;
public PaperdollInventoryVisibility(UiElement inventoryFrame)
{
_inventoryFrame = inventoryFrame
?? throw new ArgumentNullException(nameof(inventoryFrame));
}
public bool IsVisible => _inventoryFrame.Visible;
}
/// <summary>Canonical live-entity lookup used by the paperdoll factory.</summary>
internal sealed class LivePaperdollEntityLookup : IPaperdollEntityLookup
{
private readonly LiveEntityRuntime _liveEntities;
public LivePaperdollEntityLookup(LiveEntityRuntime liveEntities)
{
_liveEntities = liveEntities
?? throw new ArgumentNullException(nameof(liveEntities));
}
public bool TryGet(uint serverGuid, out WorldEntity player) =>
_liveEntities.TryGetWorldEntity(serverGuid, out player);
}
/// <summary>
/// Builds the static retail paperdoll clone from the canonical live player
/// projection and applies the DAT-defined held pose.
/// </summary>
internal sealed class RetailPaperdollDollFactory : IPaperdollDollFactory
{
private readonly IPaperdollEntityLookup _entities;
private readonly ILocalPlayerIdentitySource _identity;
private readonly IPaperdollPoseApplicator _pose;
public RetailPaperdollDollFactory(
IPaperdollEntityLookup entities,
ILocalPlayerIdentitySource identity,
IPaperdollPoseApplicator pose)
{
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
_pose = pose ?? throw new ArgumentNullException(nameof(pose));
}
public bool TryBuild(out WorldEntity? doll)
{
doll = null;
if (!_entities.TryGet(_identity.ServerGuid, out WorldEntity player)
|| player.MeshRefs.Count == 0)
{
return false;
}
uint? basePalette = null;
List<(uint, byte, byte)>? subPalettes = null;
if (player.PaletteOverride is { } palette)
{
basePalette = palette.BasePaletteId;
subPalettes = new List<(uint, byte, byte)>();
foreach (var range in palette.SubPalettes)
{
subPalettes.Add((
range.SubPaletteId,
range.Offset,
range.Length));
}
}
List<(byte, uint)>? partOverrides = null;
if (player.PartOverrides.Count > 0)
{
partOverrides = new List<(byte, uint)>(player.PartOverrides.Count);
foreach (var part in player.PartOverrides)
partOverrides.Add((part.PartIndex, part.GfxObjId));
}
doll = DollEntityBuilder.Build(
player.SourceGfxObjOrSetupId,
new List<MeshRef>(player.MeshRefs),
basePalette,
subPalettes,
partOverrides);
_pose.Apply(doll, player.SourceGfxObjOrSetupId);
return true;
}
}
/// <summary>Applies retail's DAT-defined settled paperdoll stance.</summary>
internal sealed class RetailPaperdollPoseApplicator : IPaperdollPoseApplicator
{
private readonly IDatReaderWriter _dats;
private readonly IAnimationLoader _animations;
private readonly object _datLock;
public RetailPaperdollPoseApplicator(
IDatReaderWriter dats,
IAnimationLoader animations,
object datLock)
{
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
_animations = animations ?? throw new ArgumentNullException(nameof(animations));
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
}
/// <summary>
/// Retail <c>gmPaperDollUI</c> resolves its held pose with
/// <c>DBCache::GetDIDFromEnumStatic(0x10000005, 7)</c>. The master map
/// therefore resolves key 7 to a sub-map, then key 0x10000005 to the
/// Animation DID.
/// </summary>
private uint ResolvePoseDid()
{
uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId;
if (masterDid == 0
|| !_dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumIDMap>(
masterDid,
out var master)
|| !master.ClientEnumToID.TryGetValue(7u, out uint subDid)
|| !_dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumIDMap>(
subDid,
out var sub))
{
return 0u;
}
return sub.ClientEnumToID.TryGetValue(0x10000005u, out uint did)
? did
: 0u;
}
public void Apply(WorldEntity doll, uint setupId)
{
DatReaderWriter.DBObjs.Animation? animation;
DatReaderWriter.DBObjs.Setup? setup;
lock (_datLock)
{
uint poseDid = ResolvePoseDid();
if ((poseDid >> 24) != 0x03u)
return;
animation = _animations.LoadAnimation(poseDid);
setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(setupId);
}
if (animation is null || setup is null || animation.PartFrames.Count == 0)
return;
// RedressCreature @ 0x004A3C22 installs the pose with zero frame rate
// and holds the settled final frame.
var frame = animation.PartFrames[^1];
var reposed = new List<MeshRef>(doll.MeshRefs.Count);
for (int index = 0; index < doll.MeshRefs.Count; index++)
{
Vector3 scale = index < setup.DefaultScale.Count
? setup.DefaultScale[index]
: Vector3.One;
Vector3 origin = Vector3.Zero;
Quaternion orientation = Quaternion.Identity;
if (index < frame.Frames.Count)
{
origin = frame.Frames[index].Origin;
orientation = frame.Frames[index].Orientation;
}
Matrix4x4 transform = Matrix4x4.CreateScale(scale)
* Matrix4x4.CreateFromQuaternion(orientation)
* Matrix4x4.CreateTranslation(origin);
MeshRef source = doll.MeshRefs[index];
reposed.Add(new MeshRef(source.GfxObjId, transform)
{
SurfaceOverrides = source.SurfaceOverrides,
});
}
doll.MeshRefs = reposed;
}
}