diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index fb10392c..97757dee 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -810,6 +810,8 @@ internal sealed class LivePresentationCompositionPhase "paperdoll viewport", () => new PaperdollViewportRenderer( gl, + host.GpuDevice, + host.GpuFrameLifetime, paperdollDispatcher, foundation.SceneLighting!, foundation.TextureCache, @@ -828,8 +830,7 @@ internal sealed class LivePresentationCompositionPhase paperdollLease.Resource, new RetailPaperdollFrameView( viewport, - new PaperdollInventoryVisibility(inventoryFrame), - host.GpuDevice), + new PaperdollInventoryVisibility(inventoryFrame)), new RetailPaperdollDollFactory( new LivePaperdollEntityLookup(liveEntities), d.PlayerIdentity, @@ -855,6 +856,8 @@ internal sealed class LivePresentationCompositionPhase "creature appraisal viewport", () => new CreatureAppraisalViewportRenderer( gl, + host.GpuDevice, + host.GpuFrameLifetime, appraisalDispatcher, foundation.SceneLighting!, foundation.TextureCache, @@ -878,8 +881,7 @@ internal sealed class LivePresentationCompositionPhase new RetailCreatureAppraisalFrameView( creatureViewport, examinationFrame, - appraisalController, - host.GpuDevice), + appraisalController), new RetailCreatureAppraisalCloneFactory( new LiveCreatureAppraisalEntityLookup(liveEntities))); } diff --git a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs index 8e5ed5fa..83180620 100644 --- a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs +++ b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs @@ -116,23 +116,15 @@ internal sealed class RetailCreatureAppraisalFrameView : private readonly UiViewport _viewport; private readonly UiElement _windowFrame; private readonly AppraisalUiController _controller; - private readonly GlGpuDevice _gpuDevice; public RetailCreatureAppraisalFrameView( UiViewport viewport, UiElement windowFrame, - AppraisalUiController controller, - IGpuDevice gpuDevice) + AppraisalUiController controller) { _viewport = viewport ?? throw new ArgumentNullException(nameof(viewport)); _windowFrame = windowFrame ?? throw new ArgumentNullException(nameof(windowFrame)); _controller = controller ?? throw new ArgumentNullException(nameof(controller)); - // The paperdoll/appraisal viewport texture escape hatch (campaign doc - // §7.1) is GL-only; see GlGpuDevice.RegisterExternalColorTexture. - _gpuDevice = gpuDevice as GlGpuDevice - ?? throw new ArgumentException( - "RetailCreatureAppraisalFrameView's viewport-texture registration is GL-only.", - nameof(gpuDevice)); } public bool TryGetVisibleTarget( @@ -159,7 +151,7 @@ internal sealed class RetailCreatureAppraisalFrameView : } public void SetTextureHandle(uint textureHandle) => - _viewport.TextureSlot = _gpuDevice.RegisterExternalColorTexture(textureHandle); + _viewport.TextureSlot = UiTextureTableHandle.ToSlot(textureHandle); private static bool IsEffectivelyVisible(UiElement element) { @@ -386,6 +378,8 @@ internal sealed class CreatureAppraisalViewportRenderer : public CreatureAppraisalViewportRenderer( GL gl, + AcDream.App.Rendering.Gpu.IGpuDevice device, + ICurrentGpuFrameSource frames, WbDrawDispatcher dispatcher, SceneLightingUboBinding lightUbo, IEntityTextureLifetime textureLifetime, @@ -393,6 +387,8 @@ internal sealed class CreatureAppraisalViewportRenderer : { _renderer = new PrivateEntityViewportRenderer( gl, + device, + frames, dispatcher, lightUbo, textureLifetime, diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs index ae25fd5c..38deb719 100644 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs +++ b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs @@ -399,55 +399,14 @@ internal sealed class GlGpuDevice : IGpuDevice _renderState.Reset(); } - // ── V4a pre-approved transitional seam (campaign doc §7.1, final paragraph) ── - // - // The paperdoll and creature-appraisal viewport textures are produced by - // PaperdollViewportRenderer / PrivateEntityViewportRenderer, both still raw - // GL until V4g. UiViewport (ported this slice) needs a GpuTextureSlot for - // whatever texture they hand it so it can draw through the same seam every - // other ported UI texture uses, without those renderers themselves porting - // early. This registers an EXTERNALLY-OWNED GL texture name into the - // device's texture table without taking ownership of its GL lifetime: this - // device never deletes it, and the owning renderer keeps recreating it on - // resize exactly as before. Idempotent by GL name so calling this every - // frame with the same still-live texture does not churn the table. - // - // Deleted at V4g, when PaperdollViewportRenderer / PrivateEntityViewportRenderer - // port onto IGpuDevice and can call RegisterTexture directly instead. - private readonly Dictionary _externalColorTextureSlotsByGlName = new(); - private readonly Dictionary _externalColorTextureGlNamesBySlot = new(); - - internal GpuTextureSlot RegisterExternalColorTexture(uint glTextureName) - { - ThrowIfDisposed(); - if (glTextureName == 0) - return GpuTextureSlot.Unassigned; - if (_externalColorTextureSlotsByGlName.TryGetValue(glTextureName, out GpuTextureSlot existing)) - return existing; - - ulong handle = _bindless.GetResidentHandle(glTextureName); - uint slotIndex = _textureSlotAllocator.Allocate(); - WriteHandle(slotIndex, handle); - var slot = new GpuTextureSlot(slotIndex); - _externalColorTextureSlotsByGlName[glTextureName] = slot; - _externalColorTextureGlNamesBySlot[slotIndex] = glTextureName; - return slot; - } - - /// - /// Resolves a slot produced by - /// back to its raw GL texture name, for the still-classic texture-unit - /// binding draw path (TextRenderer.DrawSprite(uint texture, ...)). - /// - internal bool TryResolveExternalColorTexture(GpuTextureSlot slot, out uint glTextureName) - { - if (!slot.IsAssigned) - { - glTextureName = 0; - return false; - } - return _externalColorTextureGlNamesBySlot.TryGetValue(slot.Index, out glTextureName); - } + // Campaign V slice V6k deleted the V4a pre-approved transitional seam + // (RegisterExternalColorTexture / TryResolveExternalColorTexture, campaign + // doc §7.1's final paragraph). It existed so the retained UI could blit the + // paperdoll and creature-appraisal viewport textures while their renderer + // still owned a hand-rolled FBO the RHI knew nothing about. That renderer now + // creates an IGpuRenderTarget and registers its colour attachment through + // RegisterTexture like anything else, so the escape hatch has no caller — + // exactly the end §7.1 wrote for it. // ── V4t transitional seam: the world texture stack's entry to this table ── // diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuDevice.Resources.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuDevice.Resources.cs index 3137c11d..d4de2ea7 100644 --- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuDevice.Resources.cs +++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuDevice.Resources.cs @@ -258,6 +258,26 @@ internal sealed unsafe partial class VulkanGpuDevice if (sampler is not VulkanGpuSampler vulkanSampler) throw new ArgumentException("The Vulkan backend can only register a Vulkan sampler.", nameof(sampler)); + // Campaign V slice V6k, the §5.5.7 re-check. A render-target image is + // viewed as VK_IMAGE_VIEW_TYPE_2D because that is what an ATTACHMENT + // needs; the texture table's descriptor array is declared + // sampler2DArray, so writing that view into it is invalid usage rather + // than a mismatch that samples oddly. §5.5.7 recorded that this "did not + // fire" and asked that it be re-checked rather than accepted, and the + // reason it still does not fire is that the one renderer with an + // offscreen target — PrivateEntityViewportRenderer — is composed on GL + // only. Making the precondition loud here is what stops that from being + // discovered as a driver-level fault the first time it is composed; + // serving it needs a SECOND, layered view per render target, which + // belongs to the slice that gives the Vulkan arm a viewport. + if (VulkanTextureFormatMapping.IsRenderTarget(vulkanTexture.Format)) + { + throw new NotSupportedException( + $"Render target '{vulkanTexture.Name}' cannot be registered into the texture table: " + + "its attachment view is 2-D and the table's descriptor array is sampler2DArray. " + + "A layered sampled view per render target is the fix (campaign plan §5.5.7)."); + } + return TextureTable.Register(vulkanTexture.View, vulkanSampler.Handle); } diff --git a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs index ec0efd0f..b22e7424 100644 --- a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs +++ b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs @@ -193,21 +193,13 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView { private readonly UiViewport _viewport; private readonly IPaperdollInventoryVisibility _inventory; - private readonly GlGpuDevice _gpuDevice; public RetailPaperdollFrameView( UiViewport viewport, - IPaperdollInventoryVisibility inventory, - IGpuDevice gpuDevice) + IPaperdollInventoryVisibility inventory) { _viewport = viewport ?? throw new ArgumentNullException(nameof(viewport)); _inventory = inventory ?? throw new ArgumentNullException(nameof(inventory)); - // The paperdoll/appraisal viewport texture escape hatch (campaign doc - // §7.1) is GL-only; see GlGpuDevice.RegisterExternalColorTexture. - _gpuDevice = gpuDevice as GlGpuDevice - ?? throw new ArgumentException( - "RetailPaperdollFrameView's viewport-texture registration is GL-only.", - nameof(gpuDevice)); } public bool TryGetVisibleSize(out int width, out int height) @@ -224,8 +216,13 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView return true; } + /// + /// Campaign V slice V6k: the renderer already hands out a + /// , so this decodes rather than registers. + /// The §7.1 external-texture seam it used to call is deleted with V4g. + /// public void SetTextureHandle(uint textureHandle) => - _viewport.TextureSlot = _gpuDevice.RegisterExternalColorTexture(textureHandle); + _viewport.TextureSlot = UiTextureTableHandle.ToSlot(textureHandle); } /// Narrow visibility adapter for the paperdoll's inventory host. diff --git a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs index b01395d3..6cf61973 100644 --- a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs +++ b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs @@ -17,8 +17,10 @@ public sealed class PaperdollViewportRenderer : { private readonly PrivateEntityViewportRenderer _renderer; - public PaperdollViewportRenderer( + internal PaperdollViewportRenderer( GL gl, + AcDream.App.Rendering.Gpu.IGpuDevice device, + ICurrentGpuFrameSource frames, WbDrawDispatcher dispatcher, SceneLightingUboBinding lightUbo, IEntityTextureLifetime textureLifetime, @@ -26,6 +28,8 @@ public sealed class PaperdollViewportRenderer : { _renderer = new PrivateEntityViewportRenderer( gl, + device, + frames, dispatcher, lightUbo, textureLifetime, diff --git a/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs b/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs index a42ebaf1..34dbbcfd 100644 --- a/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs +++ b/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs @@ -1,4 +1,5 @@ using System.Numerics; +using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Wb; using AcDream.App.UI; using AcDream.Core.Lighting; @@ -19,8 +20,32 @@ internal interface IPrivateEntityViewportCamera : ICamera /// /// 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. +/// 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 DRAW inside the pass is still raw GL: WbDrawDispatcher keeps +/// its GL arm through to V10 (§5.5.6), so this renderer opens an RHI pass and +/// then lets a raw-GL renderer record into the framebuffer that pass bound. The +/// surrounding is what restores the previous +/// framebuffer, viewport and capability state afterwards, exactly as before. /// internal sealed unsafe class PrivateEntityViewportRenderer : IUiViewportRenderer, @@ -29,6 +54,8 @@ internal sealed unsafe class PrivateEntityViewportRenderer : private const uint PrivateLandblockId = 0u; private readonly GL _gl; + private readonly IGpuDevice _device; + private readonly ICurrentGpuFrameSource _frames; private readonly WbDrawDispatcher _dispatcher; private readonly SceneLightingUboBinding _lightUbo; private readonly FixedEntityTextureOwnerLease _textureOwnerLease; @@ -39,9 +66,9 @@ internal sealed unsafe class PrivateEntityViewportRenderer : private readonly List _retiringMeshReferences = []; - private uint _fbo; - private uint _colorTex; - private uint _depthRbo; + private IGpuRenderTarget? _target; + private IGpuSampler? _sampler; + private GpuTextureSlot _slot = GpuTextureSlot.Unassigned; private int _fbW; private int _fbH; private WorldEntity? _entity; @@ -49,6 +76,8 @@ internal sealed unsafe class PrivateEntityViewportRenderer : public PrivateEntityViewportRenderer( GL gl, + IGpuDevice device, + ICurrentGpuFrameSource frames, WbDrawDispatcher dispatcher, SceneLightingUboBinding lightUbo, IEntityTextureLifetime textureLifetime, @@ -60,6 +89,8 @@ internal sealed unsafe class PrivateEntityViewportRenderer : if (renderId == 0u) throw new ArgumentOutOfRangeException(nameof(renderId)); _gl = gl ?? throw new ArgumentNullException(nameof(gl)); + _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 @@ -135,26 +166,52 @@ internal sealed unsafe class PrivateEntityViewportRenderer : } } + /// + /// 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; - EnsureFramebuffer(width, height); - if (_fbo == 0u) + EnsureRenderTarget(width, height); + if (_target is null) 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); + IGpuFrame frame = _frames.CurrentFrame + ?? throw new InvalidOperationException( + $"The {_diagnosticName} requires an open IGpuFrame (see GpuDeviceFrameLifetime)."); + using var scope = new GLStateScope(_gl); + // The pass's clear is not scissored on either backend, but GL's glClear + // is: a doorway slice earlier in the frame can have left the scissor on, + // and this target is not confined to it. + _gl.Disable(EnableCap.ScissorTest); + + 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, + }); + + // GL's BeginPass deliberately does not touch viewport or scissor, and + // the renderer that draws inside this pass is still raw GL, so the + // remaining state is set here exactly as it always was. + _gl.Viewport(0, 0, (uint)width, (uint)height); _gl.Enable(EnableCap.DepthTest); _gl.DepthFunc(DepthFunction.Less); _gl.Enable(EnableCap.CullFace); @@ -184,7 +241,7 @@ internal sealed unsafe class PrivateEntityViewportRenderer : neverCullLandblockId: PrivateLandblockId, visibleCellIds: null, animatedEntityIds: _animatedIds); - return _colorTex; + return UiTextureTableHandle.FromSlot(_slot); } /// @@ -210,95 +267,64 @@ internal sealed unsafe class PrivateEntityViewportRenderer : }); } - private void EnsureFramebuffer(int width, int height) + private void EnsureRenderTarget(int width, int height) { - if (_fbo != 0u && width == _fbW && height == _fbH) + if (_target is not null && width == _fbW && height == _fbH) return; - DeleteFramebuffer(); + ReleaseRenderTarget(); - _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) + 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}] framebuffer incomplete: {status} ({width}x{height})"); - DeleteFramebuffer(); + $"[{_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 DeleteFramebuffer() + private void ReleaseRenderTarget() { - if (_fbo != 0u) + if (_slot.IsAssigned) { - _gl.DeleteFramebuffer(_fbo); - _fbo = 0u; - } - if (_colorTex != 0u) - { - _gl.DeleteTexture(_colorTex); - _colorTex = 0u; - } - if (_depthRbo != 0u) - { - _gl.DeleteRenderbuffer(_depthRbo); - _depthRbo = 0u; + _device.ReleaseTextureSlot(_slot); + _slot = GpuTextureSlot.Unassigned; } + _sampler = null; + _target?.Dispose(); + _target = null; _fbW = 0; _fbH = 0; } @@ -331,7 +357,7 @@ internal sealed unsafe class PrivateEntityViewportRenderer : } try { - DeleteFramebuffer(); + ReleaseRenderTarget(); } catch (Exception error) {