Code-review follow-ups to the ImGui inspector: - Add public UiRoot.Pick(x,y) over the private HitTestTopDown (honors Z-order + modal exclusivity); StudioWindow uses it instead of a manual UiElement.HitTest with subtracted ScreenPosition. - RenderStack : IDisposable — disposes the GL pieces it owns in one place; StudioWindow OnClosing + Dispose both call _stack?.Dispose(), closing the error-path leak (only UiHost was disposed on the Dispose-without-OnClosing path). - Drop the stale _dt field; OnRender passes its own dt to Tick + BeginFrame. - Fix a stale PanelFbo comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
5.3 KiB
C#
128 lines
5.3 KiB
C#
using System;
|
||
using System.Numerics;
|
||
using AcDream.App.Rendering.Wb;
|
||
using AcDream.App.UI;
|
||
using Silk.NET.OpenGL;
|
||
|
||
namespace AcDream.App.Studio;
|
||
|
||
/// <summary>
|
||
/// Renders a <see cref="UiHost"/> into an off-screen FBO each frame and
|
||
/// returns the color texture handle for display in ImGui.
|
||
///
|
||
/// <para>Pattern lifted verbatim from <see cref="AcDream.App.Rendering.PaperdollViewportRenderer"/>:
|
||
/// RGBA8 color texture + Depth24Stencil8 renderbuffer, lazily (re)created on
|
||
/// size change. The entire 2-D UI pass is sealed in a <see cref="GLStateScope"/>
|
||
/// so it cannot disturb the surrounding ImGui GL state.</para>
|
||
///
|
||
/// <para>FBO origin is bottom-left (GL convention). The caller must flip V when
|
||
/// displaying the texture in ImGui (pass uv0=(0,1), uv1=(1,0) to ImGui.Image)
|
||
/// so the image appears right-side-up in ImGui's top-left coordinate system.</para>
|
||
/// </summary>
|
||
public sealed unsafe class PanelFbo : IDisposable
|
||
{
|
||
private readonly GL _gl;
|
||
|
||
// Off-screen target — lazily (re)created when the requested size changes.
|
||
private uint _fbo;
|
||
private uint _colorTex;
|
||
private uint _depthRbo;
|
||
private int _fbW;
|
||
private int _fbH;
|
||
|
||
public PanelFbo(GL gl)
|
||
{
|
||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Render <paramref name="host"/> (a full <see cref="UiHost"/> draw pass) into a
|
||
/// private FBO at <paramref name="width"/> × <paramref name="height"/> pixels.
|
||
/// Returns the GL color texture handle (0 on failure). The texture is valid until
|
||
/// the next call to <see cref="Render"/> with a different size, or until <see cref="Dispose"/>.
|
||
/// </summary>
|
||
public uint Render(int width, int height, UiHost host)
|
||
{
|
||
if (width <= 0 || height <= 0 || host is null) return 0u;
|
||
|
||
EnsureFramebuffer(width, height);
|
||
if (_fbo == 0) return 0u;
|
||
|
||
// Seal the entire pass: GLStateScope saves + restores every GL state the
|
||
// UI draw touches (viewport, blend, FBO binding, etc.) so ImGui's own state
|
||
// — set up by BeginFrame and expected intact by Render — is untouched.
|
||
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(0.18f, 0.18f, 0.18f, 1f); // opaque dark-grey canvas background (the FBO IS the canvas)
|
||
_gl.ClearDepth(1.0);
|
||
_gl.DepthMask(true);
|
||
_gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
||
|
||
host.Draw(new Vector2(width, height));
|
||
|
||
// FBO stays bound here; GLStateScope.Dispose() restores the previous binding.
|
||
return _colorTex;
|
||
}
|
||
|
||
// ── FBO lifecycle (mirrors PaperdollViewportRenderer.EnsureFramebuffer) ──────
|
||
|
||
private void EnsureFramebuffer(int width, int height)
|
||
{
|
||
if (_fbo != 0 && width == _fbW && height == _fbH) return;
|
||
DeleteFramebuffer();
|
||
|
||
_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, 0);
|
||
|
||
_depthRbo = _gl.GenRenderbuffer();
|
||
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthRbo);
|
||
_gl.RenderbufferStorage(RenderbufferTarget.Renderbuffer,
|
||
InternalFormat.Depth24Stencil8, (uint)width, (uint)height);
|
||
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0);
|
||
|
||
_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);
|
||
|
||
var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
|
||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||
if (status != GLEnum.FramebufferComplete)
|
||
{
|
||
Console.WriteLine($"[studio] PanelFbo incomplete: {status} ({width}x{height})");
|
||
DeleteFramebuffer();
|
||
}
|
||
}
|
||
|
||
private void DeleteFramebuffer()
|
||
{
|
||
if (_fbo != 0) { _gl.DeleteFramebuffer(_fbo); _fbo = 0; }
|
||
if (_colorTex != 0) { _gl.DeleteTexture(_colorTex); _colorTex = 0; }
|
||
if (_depthRbo != 0) { _gl.DeleteRenderbuffer(_depthRbo); _depthRbo = 0; }
|
||
_fbW = _fbH = 0;
|
||
}
|
||
|
||
public void Dispose() => DeleteFramebuffer();
|
||
}
|