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