using System.Numerics; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Wb; using AcDream.App.UI; using AcDream.Core.Lighting; using AcDream.Core.World; 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 /// , one synthetic render identity, and one /// balanced texture-owner lease. /// /// Campaign V slice V6k (V4g's first half). The target used to be a /// hand-rolled FBO, colour texture and depth renderbuffer, and the resulting GL /// texture name was published to the retained UI through /// GlGpuDevice.RegisterExternalColorTexture — the pre-approved /// transitional seam plan §7.1's final paragraph created, whose stated end was /// exactly this slice. Both are gone: the device creates the target, the pass /// DECLARES it, and the colour attachment is registered into the global texture /// table like any other texture. /// /// Declaring the target is what discharges §5.4. That section's divergence /// — GL's BeginPass not binding framebuffer 0 for a null target, so this /// renderer's own FBO survived — was reverted with V4c and is not on the tree; /// what remained was the reason it had existed, namely that this renderer bound /// a framebuffer the RHI knew nothing about. It now names its target, so /// nothing depends on inheritance and the GL and Vulkan backends agree about /// what Target: null means. /// /// The GL arm this renderer used to open an RHI pass and delegate a /// raw-GL WbDrawDispatcher into (through V10, §5.5.6) was deleted at /// Campaign V slice V11: WbDrawDispatcher now records into the pass /// this renderer publishes on both call sites the same way. /// internal sealed class PrivateEntityViewportRenderer : IUiViewportRenderer, IDisposable { private const uint PrivateLandblockId = 0u; private readonly IGpuDevice _device; private readonly ICurrentGpuFrameSource _frames; /// /// The world pass scope this renderer's own pass publishes into for the /// span of the draw. WbDrawDispatcher borrows its pass from the /// scope rather than opening one, so this renderer — which opens a pass of /// its own to draw into an offscreen target — has to publish it there. /// /// The raw-GL arm this used to be optional for (the GL dispatcher /// recorded against whatever framebuffer was already bound, needing no /// publication) was deleted at Campaign V slice V11. /// private readonly IWorldPassScope _scope; 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 IGpuRenderTarget? _target; private IGpuSampler? _sampler; private GpuTextureSlot _slot = GpuTextureSlot.Unassigned; private int _fbW; private int _fbH; private WorldEntity? _entity; private SyntheticEntityMeshReferenceOwner? _meshReferences; public PrivateEntityViewportRenderer( IWorldPassScope scope, IGpuDevice device, ICurrentGpuFrameSource frames, WbDrawDispatcher dispatcher, SceneLightingUboBinding lightUbo, IEntityTextureLifetime textureLifetime, IWbMeshAdapter meshAdapter, uint renderId, IPrivateEntityViewportCamera camera, string diagnosticName) { if (renderId == 0u) throw new ArgumentOutOfRangeException(nameof(renderId)); _scope = scope ?? throw new ArgumentNullException( nameof(scope), "The viewport must publish a world pass scope to draw into."); _device = device ?? throw new ArgumentNullException(nameof(device)); _frames = frames ?? throw new ArgumentNullException(nameof(frames)); _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); } /// /// A GL framebuffer's colour texture used to sample bottom-up; that arm was /// deleted at Campaign V slice V11, and a Vulkan render target's does not. /// See . /// public bool TextureIsBottomUp => false; 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; } } } /// /// Renders the entity and returns the the /// retained UI blits — a one-based index into the device's global texture /// table, not a GL texture name. Zero means nothing was rendered. /// public uint Render(int width, int height) { WorldEntity? entity = _entity; if (entity is null || entity.MeshRefs.Count == 0 || width <= 0 || height <= 0) return 0u; EnsureRenderTarget(width, height); if (_target is null) return 0u; _camera.Aspect = width / (float)height; IGpuFrame frame = _frames.CurrentFrame ?? throw new InvalidOperationException( $"The {_diagnosticName} requires an open IGpuFrame (see GpuDeviceFrameLifetime)."); using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription { Name = _diagnosticName, Color = new GpuColorAttachment( Target: _target, Load: GpuLoadOp.Clear, Store: GpuStoreOp.Store, ClearColor: Vector4.Zero), Depth = new GpuDepthAttachment( Load: GpuLoadOp.Clear, Store: GpuStoreOp.DontCare, ClearDepth: 1f, ClearStencil: 0), SampleCount = 1, }); // The dispatcher borrows its pass from the scope, so this pass has to // BE the scope's for the span of the draw. Published after the world // phase has closed its own, which is why it does not nest; the // sections it resets are republished by UploadCreatureLight // immediately below, which is the private lighting this view wants // rather than the world's. using IDisposable publication = _scope.Publish(encoder); 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 UiTextureTableHandle.FromSlot(_slot); } /// /// 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 EnsureRenderTarget(int width, int height) { if (_target is not null && width == _fbW && height == _fbH) return; ReleaseRenderTarget(); IGpuRenderTarget target; try { target = _device.CreateRenderTarget(new GpuRenderTargetDescription( _diagnosticName, width, height, GpuTextureFormat.Rgba8UnormRenderTarget, // Depth24Stencil8, as the hand-rolled renderbuffer was: nothing // samples it, and the stencil aspect keeps the attachment shape // the depth/stencil renderers already expect. GpuTextureFormat.Depth24Stencil8, SampleCount: 1)); } catch (Exception failure) { Console.WriteLine( $"[{_diagnosticName}] render target unavailable ({width}x{height}): {failure.Message}"); return; } try { // The retained UI blits this attachment as an ordinary table entry. // Linear/clamped is the filtering the hand-rolled colour texture set // on itself before the §7.1 seam registered it. _sampler = _device.CreateSampler(GpuSamplerDescription.WorldClamp); _slot = _device.RegisterTexture(target.ColorTexture, _sampler); } catch { target.Dispose(); _sampler = null; _slot = GpuTextureSlot.Unassigned; throw; } _target = target; _fbW = width; _fbH = height; } private void ReleaseRenderTarget() { if (_slot.IsAssigned) { _device.ReleaseTextureSlot(_slot); _slot = GpuTextureSlot.Unassigned; } _sampler = null; _target?.Dispose(); _target = null; _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 { ReleaseRenderTarget(); } 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); } } }