Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
383 lines
12 KiB
C#
383 lines
12 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.UI;
|
|
using AcDream.Core.Lighting;
|
|
using AcDream.Core.World;
|
|
using Silk.NET.OpenGL;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// Camera contract for retail <c>CreatureMode</c>-style UI viewports. The
|
|
/// renderer supplies the authored viewport aspect before each draw and uses
|
|
/// <see cref="Eye"/> when it publishes the private scene lighting UBO.
|
|
/// </summary>
|
|
internal interface IPrivateEntityViewportCamera : ICamera
|
|
{
|
|
Vector3 Eye { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shared render-to-texture implementation for the private 3-D creature
|
|
/// viewports used by paperdoll and examination UI. Each instance owns one FBO,
|
|
/// one synthetic render identity, and one balanced texture-owner lease.
|
|
/// </summary>
|
|
internal sealed unsafe class PrivateEntityViewportRenderer :
|
|
IUiViewportRenderer,
|
|
IDisposable
|
|
{
|
|
private const uint PrivateLandblockId = 0u;
|
|
|
|
private readonly GL _gl;
|
|
private readonly WbDrawDispatcher _dispatcher;
|
|
private readonly SceneLightingUboBinding _lightUbo;
|
|
private readonly FixedEntityTextureOwnerLease _textureOwnerLease;
|
|
private readonly IWbMeshAdapter _meshAdapter;
|
|
private readonly IPrivateEntityViewportCamera _camera;
|
|
private readonly HashSet<uint> _animatedIds;
|
|
private readonly string _diagnosticName;
|
|
private readonly List<SyntheticEntityMeshReferenceOwner>
|
|
_retiringMeshReferences = [];
|
|
|
|
private uint _fbo;
|
|
private uint _colorTex;
|
|
private uint _depthRbo;
|
|
private int _fbW;
|
|
private int _fbH;
|
|
private WorldEntity? _entity;
|
|
private SyntheticEntityMeshReferenceOwner? _meshReferences;
|
|
|
|
public PrivateEntityViewportRenderer(
|
|
GL gl,
|
|
WbDrawDispatcher dispatcher,
|
|
SceneLightingUboBinding lightUbo,
|
|
IEntityTextureLifetime textureLifetime,
|
|
IWbMeshAdapter meshAdapter,
|
|
uint renderId,
|
|
IPrivateEntityViewportCamera camera,
|
|
string diagnosticName)
|
|
{
|
|
if (renderId == 0u)
|
|
throw new ArgumentOutOfRangeException(nameof(renderId));
|
|
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
|
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
|
_lightUbo = lightUbo ?? throw new ArgumentNullException(nameof(lightUbo));
|
|
_meshAdapter = meshAdapter
|
|
?? throw new ArgumentNullException(nameof(meshAdapter));
|
|
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
|
_diagnosticName = string.IsNullOrWhiteSpace(diagnosticName)
|
|
? "creature viewport"
|
|
: diagnosticName;
|
|
_animatedIds = [renderId];
|
|
_textureOwnerLease = new FixedEntityTextureOwnerLease(
|
|
textureLifetime ?? throw new ArgumentNullException(nameof(textureLifetime)),
|
|
renderId);
|
|
}
|
|
|
|
public void SetEntity(WorldEntity? entity)
|
|
{
|
|
ReleaseRetiringMeshReferences();
|
|
|
|
if (ReferenceEquals(_entity, entity))
|
|
return;
|
|
|
|
SyntheticEntityMeshReferenceOwner? replacement = null;
|
|
if (entity is not null)
|
|
{
|
|
replacement = new SyntheticEntityMeshReferenceOwner(
|
|
_meshAdapter,
|
|
CollectMeshIds(entity));
|
|
replacement.Acquire();
|
|
}
|
|
|
|
SyntheticEntityMeshReferenceOwner? previous = _meshReferences;
|
|
try
|
|
{
|
|
_textureOwnerLease.Replace(entity is not null);
|
|
}
|
|
catch (Exception textureFailure)
|
|
{
|
|
if (replacement is null)
|
|
throw;
|
|
|
|
try
|
|
{
|
|
replacement.Dispose();
|
|
}
|
|
catch (Exception rollbackFailure)
|
|
{
|
|
throw new AggregateException(
|
|
$"The {_diagnosticName} texture-owner replacement failed "
|
|
+ "and the replacement mesh-owner rollback did not converge.",
|
|
textureFailure,
|
|
rollbackFailure);
|
|
}
|
|
|
|
System.Runtime.ExceptionServices.ExceptionDispatchInfo
|
|
.Capture(textureFailure)
|
|
.Throw();
|
|
}
|
|
|
|
_meshReferences = replacement;
|
|
_entity = entity;
|
|
|
|
if (previous is not null)
|
|
{
|
|
try
|
|
{
|
|
previous.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
_retiringMeshReferences.Add(previous);
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
public uint Render(int width, int height)
|
|
{
|
|
WorldEntity? entity = _entity;
|
|
if (entity is null || entity.MeshRefs.Count == 0 || width <= 0 || height <= 0)
|
|
return 0u;
|
|
|
|
EnsureFramebuffer(width, height);
|
|
if (_fbo == 0u)
|
|
return 0u;
|
|
_camera.Aspect = width / (float)height;
|
|
|
|
using var scope = new GLStateScope(_gl);
|
|
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo);
|
|
_gl.Viewport(0, 0, (uint)width, (uint)height);
|
|
_gl.Disable(EnableCap.ScissorTest);
|
|
_gl.ClearColor(0f, 0f, 0f, 0f);
|
|
_gl.ClearDepth(1.0);
|
|
_gl.DepthMask(true);
|
|
_gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
|
|
|
_gl.Enable(EnableCap.DepthTest);
|
|
_gl.DepthFunc(DepthFunction.Less);
|
|
_gl.Enable(EnableCap.CullFace);
|
|
_gl.CullFace(TriangleFace.Back);
|
|
_gl.FrontFace(FrontFaceDirection.Ccw);
|
|
_gl.Disable(EnableCap.Blend);
|
|
|
|
UploadCreatureLight();
|
|
|
|
WorldEntity[] entities = [entity];
|
|
var entries =
|
|
new (uint, Vector3, Vector3, IReadOnlyList<WorldEntity>,
|
|
IReadOnlyDictionary<uint, WorldEntity>?)[]
|
|
{
|
|
(
|
|
PrivateLandblockId,
|
|
new Vector3(-1024f),
|
|
new Vector3(1024f),
|
|
entities,
|
|
null),
|
|
};
|
|
|
|
_dispatcher.Draw(
|
|
_camera,
|
|
entries,
|
|
frustum: null,
|
|
neverCullLandblockId: PrivateLandblockId,
|
|
visibleCellIds: null,
|
|
animatedEntityIds: _animatedIds);
|
|
return _colorTex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Both retail paperdoll and creature examination call
|
|
/// <c>UIElement_Viewport::SetLight(DISTANT_LIGHT, 2, (0.3,1.9,0.65))</c>.
|
|
/// </summary>
|
|
private void UploadCreatureLight()
|
|
{
|
|
Vector3 direction = Vector3.Normalize(new Vector3(0.3f, 1.9f, 0.65f));
|
|
_lightUbo.Upload(new SceneLightingUbo
|
|
{
|
|
Light0 = new UboLight
|
|
{
|
|
PosAndKind = Vector4.Zero,
|
|
DirAndRange = new Vector4(direction, 1e9f),
|
|
ColorAndIntensity = new Vector4(1f, 1f, 1f, 2f),
|
|
ConeAngleEtc = Vector4.Zero,
|
|
},
|
|
CellAmbient = new Vector4(0.3f, 0.3f, 0.3f, 1f),
|
|
FogParams = new Vector4(1e9f, 1e9f, 0f, 0f),
|
|
FogColor = Vector4.Zero,
|
|
CameraAndTime = new Vector4(_camera.Eye, 0f),
|
|
});
|
|
}
|
|
|
|
private void EnsureFramebuffer(int width, int height)
|
|
{
|
|
if (_fbo != 0u && width == _fbW && height == _fbH)
|
|
return;
|
|
DeleteFramebuffer();
|
|
|
|
_fbW = width;
|
|
_fbH = height;
|
|
_colorTex = _gl.GenTexture();
|
|
_gl.BindTexture(TextureTarget.Texture2D, _colorTex);
|
|
_gl.TexImage2D(
|
|
TextureTarget.Texture2D,
|
|
0,
|
|
InternalFormat.Rgba8,
|
|
(uint)width,
|
|
(uint)height,
|
|
0,
|
|
PixelFormat.Rgba,
|
|
PixelType.UnsignedByte,
|
|
(void*)0);
|
|
_gl.TexParameter(
|
|
TextureTarget.Texture2D,
|
|
TextureParameterName.TextureMinFilter,
|
|
(int)TextureMinFilter.Linear);
|
|
_gl.TexParameter(
|
|
TextureTarget.Texture2D,
|
|
TextureParameterName.TextureMagFilter,
|
|
(int)TextureMinFilter.Linear);
|
|
_gl.TexParameter(
|
|
TextureTarget.Texture2D,
|
|
TextureParameterName.TextureWrapS,
|
|
(int)TextureWrapMode.ClampToEdge);
|
|
_gl.TexParameter(
|
|
TextureTarget.Texture2D,
|
|
TextureParameterName.TextureWrapT,
|
|
(int)TextureWrapMode.ClampToEdge);
|
|
_gl.BindTexture(TextureTarget.Texture2D, 0u);
|
|
|
|
_depthRbo = _gl.GenRenderbuffer();
|
|
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthRbo);
|
|
_gl.RenderbufferStorage(
|
|
RenderbufferTarget.Renderbuffer,
|
|
InternalFormat.Depth24Stencil8,
|
|
(uint)width,
|
|
(uint)height);
|
|
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0u);
|
|
|
|
_fbo = _gl.GenFramebuffer();
|
|
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo);
|
|
_gl.FramebufferTexture2D(
|
|
FramebufferTarget.Framebuffer,
|
|
FramebufferAttachment.ColorAttachment0,
|
|
TextureTarget.Texture2D,
|
|
_colorTex,
|
|
0);
|
|
_gl.FramebufferRenderbuffer(
|
|
FramebufferTarget.Framebuffer,
|
|
FramebufferAttachment.DepthStencilAttachment,
|
|
RenderbufferTarget.Renderbuffer,
|
|
_depthRbo);
|
|
|
|
GLEnum status = _gl.CheckFramebufferStatus(
|
|
FramebufferTarget.Framebuffer);
|
|
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0u);
|
|
if (status != GLEnum.FramebufferComplete)
|
|
{
|
|
Console.WriteLine(
|
|
$"[{_diagnosticName}] framebuffer incomplete: {status} ({width}x{height})");
|
|
DeleteFramebuffer();
|
|
}
|
|
}
|
|
|
|
private void DeleteFramebuffer()
|
|
{
|
|
if (_fbo != 0u)
|
|
{
|
|
_gl.DeleteFramebuffer(_fbo);
|
|
_fbo = 0u;
|
|
}
|
|
if (_colorTex != 0u)
|
|
{
|
|
_gl.DeleteTexture(_colorTex);
|
|
_colorTex = 0u;
|
|
}
|
|
if (_depthRbo != 0u)
|
|
{
|
|
_gl.DeleteRenderbuffer(_depthRbo);
|
|
_depthRbo = 0u;
|
|
}
|
|
_fbW = 0;
|
|
_fbH = 0;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_entity = null;
|
|
if (_meshReferences is { } current)
|
|
{
|
|
_meshReferences = null;
|
|
_retiringMeshReferences.Add(current);
|
|
}
|
|
|
|
List<Exception>? failures = null;
|
|
try
|
|
{
|
|
_textureOwnerLease.Dispose();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= []).Add(error);
|
|
}
|
|
try
|
|
{
|
|
ReleaseRetiringMeshReferences();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= []).Add(error);
|
|
}
|
|
try
|
|
{
|
|
DeleteFramebuffer();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= []).Add(error);
|
|
}
|
|
|
|
if (failures is not null)
|
|
{
|
|
throw new AggregateException(
|
|
$"The {_diagnosticName} resources did not fully release.",
|
|
failures);
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<ulong> CollectMeshIds(WorldEntity entity)
|
|
{
|
|
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
|
yield return entity.MeshRefs[i].GfxObjId;
|
|
for (int i = 0; i < entity.PartOverrides.Count; i++)
|
|
yield return entity.PartOverrides[i].GfxObjId;
|
|
}
|
|
|
|
private void ReleaseRetiringMeshReferences()
|
|
{
|
|
List<Exception>? failures = null;
|
|
for (int i = _retiringMeshReferences.Count - 1; i >= 0; i--)
|
|
{
|
|
SyntheticEntityMeshReferenceOwner owner =
|
|
_retiringMeshReferences[i];
|
|
try
|
|
{
|
|
owner.Dispose();
|
|
if (owner.IsDisposed)
|
|
_retiringMeshReferences.RemoveAt(i);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= []).Add(error);
|
|
}
|
|
}
|
|
|
|
if (failures is not null)
|
|
{
|
|
throw new AggregateException(
|
|
$"One or more {_diagnosticName} mesh owners remain pending.",
|
|
failures);
|
|
}
|
|
}
|
|
}
|