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 at543bc79f, 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 against22aa2edc: 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 <noreply@anthropic.com>
This commit is contained in:
parent
22aa2edc65
commit
eb7e6b4e5c
7 changed files with 173 additions and 169 deletions
|
|
@ -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
|
|||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <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 DRAW inside the pass is still raw GL: <c>WbDrawDispatcher</c> 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 <see cref="GLStateScope"/> is what restores the previous
|
||||
/// framebuffer, viewport and capability state afterwards, exactly as before.</para>
|
||||
/// </summary>
|
||||
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<SyntheticEntityMeshReferenceOwner>
|
||||
_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 :
|
|||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue