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>
420 lines
14 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|