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;
///
/// Camera contract for retail CreatureMode-style UI viewports. The
/// renderer supplies the authored viewport aspect before each draw and uses
/// when it publishes the private scene lighting UBO.
///
internal interface IPrivateEntityViewportCamera : ICamera
{
Vector3 Eye { get; }
}
///
/// 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.
///
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 _animatedIds;
private readonly string _diagnosticName;
private readonly List
_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,
IReadOnlyDictionary?)[]
{
(
PrivateLandblockId,
new Vector3(-1024f),
new Vector3(1024f),
entities,
null),
};
_dispatcher.Draw(
_camera,
entries,
frustum: null,
neverCullLandblockId: PrivateLandblockId,
visibleCellIds: null,
animatedEntityIds: _animatedIds);
return _colorTex;
}
///
/// Both retail paperdoll and creature examination call
/// UIElement_Viewport::SetLight(DISTANT_LIGHT, 2, (0.3,1.9,0.65)).
///
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? 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 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? 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);
}
}
}