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); } } }