acdream/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs
Erik 8a7a0837e1 feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1
already removed ImGui/Studio/DevTools. This step deletes the GL
rendering backend itself: every Gpu/Gl/** implementation, the Wb
ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/
BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache,
RenderBootstrap, and RenderFrameGlStateController.

GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/
OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses
its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and
RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone —
there is nothing left to select between. The five world-draw dual-arm
renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer,
ParticleRenderer, SkyRenderer) and the composition roots
(WorldRenderComposition, HostInputCameraComposition,
LivePresentationComposition, FrameRootComposition) collapse to their
RHI-only arm. GL-only diagnostic properties with a live external reader
(DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op
rather than disappearing, since the reader is out of this commit's
scope.

A few GL-flavored mechanisms turned out to be backend-neutral once
isolated: GlConstructionCleanupLedger is renamed
ResourceConstructionCleanupLedger (exception-chain walking has nothing
to do with GL), and GlfwNativePlatformProbe moved out of the otherwise
GL-only GraphicalCapabilityRecord.cs into
GraphicalWindowBackendSelection.cs before the rest of that file was
deleted.

Test files with no surviving subject are deleted outright
(GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests,
PortalDepthShaderParityTests, TextureCacheBindlessTests,
TextRendererFailureSafetyTests, ClipFrameUploadTests, every
Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests);
others get their dead GL-only members trimmed while their live
assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now
reads GpuBindingModel.StorageClipRegions, the same binding index under
its new backend-neutral name; GpuResourceRetirementTransactionTests
drops its OpenGLGraphicsDevice-subclassing test double and the two GL
queue tests it existed for). EnvCellRendererTests' construction helper
now builds a real ObjectMeshManager via VulkanMeshPipelineDevice
instead of passing null through a null-forgiving operator, since the
RHI constructor never tolerated a null mesh manager and the old GL
constructor (which did) is gone.

Deferred to the next two steps, deliberately not touched here: the
Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl
(WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale
csproj comment (the package itself is still load-bearing —
TextureFormat and friends are used well beyond the deleted
ManagedGLUniformBuffer), and the CI/gate scripts.

Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors.
Tests: full-solution `dotnet test` green across every project
(App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all
others 100%); the 2 App.Tests names that flake under full-suite
parallel execution (#250-family, documented pre-existing) pass in
isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 02:19:53 +02:00

420 lines
14 KiB
C#

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;
/// <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
/// <see cref="IGpuRenderTarget"/>, one synthetic render identity, and one
/// balanced texture-owner lease.
///
/// <para><b>Campaign V slice V6k (V4g's first half).</b> 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
/// <c>GlGpuDevice.RegisterExternalColorTexture</c> — 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.</para>
///
/// <para>Declaring the target is what discharges §5.4. That section's divergence
/// — GL's <c>BeginPass</c> 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 <c>Target: null</c> means.</para>
///
/// <para>The GL arm this renderer used to open an RHI pass and delegate a
/// raw-GL <c>WbDrawDispatcher</c> into (through V10, §5.5.6) was deleted at
/// Campaign V slice V11: <c>WbDrawDispatcher</c> now records into the pass
/// this renderer publishes on both call sites the same way.</para>
/// </summary>
internal sealed class PrivateEntityViewportRenderer :
IUiViewportRenderer,
IDisposable
{
private const uint PrivateLandblockId = 0u;
private readonly IGpuDevice _device;
private readonly ICurrentGpuFrameSource _frames;
/// <summary>
/// The world pass scope this renderer's own pass publishes into for the
/// span of the draw. <c>WbDrawDispatcher</c> 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.
///
/// <para>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.</para>
/// </summary>
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<uint> _animatedIds;
private readonly string _diagnosticName;
private readonly List<SyntheticEntityMeshReferenceOwner>
_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);
}
/// <summary>
/// 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 <see cref="IUiViewportRenderer.TextureIsBottomUp"/>.
/// </summary>
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;
}
}
}
/// <summary>
/// Renders the entity and returns the <see cref="UiTextureTableHandle"/> 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.
/// </summary>
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<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 UiTextureTableHandle.FromSlot(_slot);
}
/// <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 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<Exception>? 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<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);
}
}
}