From eb7e6b4e5c5e416f717e610b3f8f5385dcc759b5 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 28 Jul 2026 16:46:01 +0200 Subject: [PATCH] feat(render): Campaign V slice V6k commit 2 - the viewports name their own target, and section 5.4 is discharged V4g's first half, and the V7 blocker section 5.4 named. What moved. PrivateEntityViewportRenderer - the paperdoll and creature-appraisal viewports - stops hand-rolling an FBO, a colour texture and a depth renderbuffer and asks the device for an IGpuRenderTarget. The pass it opens DECLARES that target rather than binding one behind the RHI's back, and the colour attachment is registered into the global texture table through RegisterTexture like any other texture. Render() returns the UiTextureTableHandle the retained UI already speaks instead of a raw GL name. That deletes the V4a pre-approved transitional seam. GlGpuDevice's RegisterExternalColorTexture / TryResolveExternalColorTexture existed so the UI could blit a texture whose owner the RHI knew nothing about; plan section 7.1's final paragraph gave them exactly this slice as their end, and both are gone along with the GlGpuDevice casts in RetailPaperdollFrameView and RetailCreatureAppraisalFrameView. Those two views are now backend-neutral: they decode a handle instead of registering one. Section 5.4, stated precisely, because the answer is not what the section predicts. The divergence it describes - GL's BeginPass refusing to bind framebuffer 0 for a null target - is NOT on the tree and has not been since the V4c revert at 543bc79f, which took that hunk with it. GL's BeginPass binds the declared target today, so the two backends already agree about what Target: null means. What the revert did not undo was the REASON the divergence existed: this renderer bound a framebuffer no pass had declared. It now names its target, and PortalTunnelPresentation - the other renderer section 5.4 names - draws into the active viewport rather than an offscreen buffer, which is the backbuffer, which is what a null target literally means. The obligation is therefore discharged on both halves and V7's second defect is closed. PortalDepthMaskRenderer is NOT ported and is not blocking. Its two-pass punch is built on glStencilFunc/glStencilOp/glStencilMask, and GpuPipelineDescription has no stencil dimension to express them with. That is a pinned-contract question, reported rather than worked around. The section 5.5.7 re-check, which was asked for explicitly and does not come back clean. That note recorded that "the render-target-view-in-table usage from V6c did not fire" and asked that it not be carried forward as accepted. It still does not fire, and now for a reason worth writing down: a Vulkan render-target image is viewed as VK_IMAGE_VIEW_TYPE_2D because that is what an attachment needs, while the texture table's descriptor array is declared sampler2DArray, so registering one is invalid usage rather than a mismatch that samples oddly. It has never fired because the only renderer with an offscreen target is composed on GL alone. VulkanGpuDevice.RegisterTexture now refuses it loudly and names the fix - a second, layered sampled view per render target - so the slice that gives the Vulkan arm a viewport finds a precondition instead of a driver-level fault. Gates. Release build green. App tests 4,109 passed / 3 skipped, unchanged from commit 1. Strict GL offline pixel gate against 22aa2edc: 4.08e-05, 23 differing pixels of 563,200, inside the documented 9-31 band, maximumChannelDelta 48. GL connected repeat gate at 3 runs: 3/3 RENDERED on the desktop witness and 3/3 on the client capture. One offline Vulkan run with VK_LAYER_KHRONOS_validation proven inserted by the loader: zero validation errors, zero warnings. And the surface the automated gates cannot see was checked rather than banked. The offline scene never opens the inventory, so the pixel gate is a tripwire for this change and nothing more - plan section 5.1's debt table has said so since V6d. A connected run that presses ToggleInventoryPanel and captures the result is in artifacts/v6k-paperdoll: the doll renders through the new render target with the correct pose, orientation and alpha, which is the row that table has been carrying since V4c. No divergence-register row: no retail-facing behaviour changes. Co-Authored-By: Claude Fable 5 --- .../LivePresentationComposition.cs | 10 +- .../CreatureAppraisalPresentation.cs | 16 +- .../Rendering/Gpu/Gl/GlGpuDevice.cs | 57 +---- .../Gpu/Vk/VulkanGpuDevice.Resources.cs | 20 ++ .../Rendering/PaperdollFramePresenter.cs | 17 +- .../Rendering/PaperdollViewportRenderer.cs | 6 +- .../PrivateEntityViewportRenderer.cs | 216 ++++++++++-------- 7 files changed, 173 insertions(+), 169 deletions(-) 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) {