From 2dc1894e1efb6efe12f4a78dc1c97ea0caeb8b60 Mon Sep 17 00:00:00 2001 From: avan Date: Sat, 1 Aug 2026 22:42:21 +0800 Subject: [PATCH] Fix Vulkan/OpenGL attachmentless rendering Some fragment passes used by FFT do not have any color or depth/stencil attachments. Instead, the fragment shader writes the results directly to storage images (on OpenGL: through imageStore). ### Vulkan When SetImage is called and FramebufferParams has no attachments, its virtual size must be updated using the width, height, and layer count of the fragment storage image. Otherwise, the attachmentless framebuffer may retain an incorrect 1x1 extent, causing backgrounds, logos, UI elements, and other rendered content to be missing. If the first draw occurs before the storage-image descriptor is rebound, SetImage cannot yet provide the correct storage-image dimensions. Therefore, when RecreateGraphicsPipelineIfNeeded finds that FramebufferParams has no attachments and still uses the default 1x1 extent, it initializes the framebuffer dimensions from the active viewport. Otherwise, the first Vulkan draw may be restricted to a 1x1 area and render incorrectly. The storage-image extent is therefore used as the authoritative size for FramebufferParams, while the active viewport is used as a fallback when the storage-image extent is not yet available before the first draw. ### OpenGL A storage image is not a framebuffer attachment, so OpenGL cannot derive the framebuffer width and height from it. When the framebuffer has no attachments but a viewport with valid dimensions has already been defined, Pipeline.PreDraw must set the default framebuffer width and height from the active viewport. Without non-zero default width and height values, the attachmentless framebuffer remains incomplete. Drawing with that framebuffer results in InvalidFramebufferOperation, so rasterization and fragment shader execution do not occur even when the storage images are bound correctly. (cherry picked from commit ac6db0fe76cf70b0c94ad466a0debfb9dc865f80) --- src/Ryujinx.Graphics.OpenGL/Framebuffer.cs | 38 ++++++++++++++++++- src/Ryujinx.Graphics.OpenGL/Pipeline.cs | 5 +++ .../FramebufferParams.cs | 23 +++++++++++ src/Ryujinx.Graphics.Vulkan/PipelineBase.cs | 23 +++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs b/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs index 394b8bc76..aad8735bb 100644 --- a/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs +++ b/src/Ryujinx.Graphics.OpenGL/Framebuffer.cs @@ -20,6 +20,27 @@ namespace Ryujinx.Graphics.OpenGL private int _colorsCount; private bool _dualSourceBlend; + public bool HasAttachments + { + get + { + if (_depthStencil != null) + { + return true; + } + + for (int index = 0; index < _colors.Length; index++) + { + if (_colors[index] != null) + { + return true; + } + } + + return false; + } + } + public Framebuffer() { Handle = GL.GenFramebuffer(); @@ -34,6 +55,12 @@ namespace Ryujinx.Graphics.OpenGL return Handle; } + public void SetDefaultSize(int width, int height) + { + GL.FramebufferParameter(FramebufferTarget.Framebuffer, FramebufferDefaultParameter.FramebufferDefaultWidth, Math.Max(1, width)); + GL.FramebufferParameter(FramebufferTarget.Framebuffer, FramebufferDefaultParameter.FramebufferDefaultHeight, Math.Max(1, height)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AttachColor(int index, TextureView color) { @@ -105,13 +132,20 @@ namespace Ryujinx.Graphics.OpenGL _colorsCount = colorsCount; } - private static void SetDrawBuffersImpl(int colorsCount) + private void SetDrawBuffersImpl(int colorsCount) { DrawBuffersEnum[] drawBuffers = new DrawBuffersEnum[colorsCount]; for (int index = 0; index < colorsCount; index++) { - drawBuffers[index] = DrawBuffersEnum.ColorAttachment0 + index; + if (_colors[index] != null) + { + drawBuffers[index] = DrawBuffersEnum.ColorAttachment0 + index; + } + else + { + drawBuffers[index] = DrawBuffersEnum.None; + } } GL.DrawBuffers(colorsCount, drawBuffers); diff --git a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs index 6774f0fba..0de923275 100644 --- a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs +++ b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs @@ -1537,6 +1537,11 @@ namespace Ryujinx.Graphics.OpenGL { DrawCount++; + if (!_framebuffer.HasAttachments && _viewportArray.Length >= 4) + { + _framebuffer.SetDefaultSize((int)_viewportArray[2], (int)_viewportArray[3]); + } + _unit0Texture?.Bind(0); } diff --git a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs index a0d1b50df..e5800ed7d 100644 --- a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs +++ b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs @@ -32,6 +32,29 @@ namespace Ryujinx.Graphics.Vulkan public bool HasDepthStencil { get; private set; } public int ColorAttachmentsCount => AttachmentsCount - (HasDepthStencil ? 1 : 0); + public bool SetVirtualSize(uint width, uint height, uint layers) + { + if (AttachmentsCount != 0) + { + return false; + } + + width = Math.Max(1u, width); + height = Math.Max(1u, height); + layers = Math.Max(1u, layers); + + if (Width == width && Height == height && Layers == layers) + { + return false; + } + + Width = width; + Height = height; + Layers = layers; + + return true; + } + public FramebufferParams(Device device, TextureView view, uint width, uint height) { Format format = view.Info.Format; diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs index aececee61..fcb8826c2 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs @@ -843,6 +843,11 @@ namespace Ryujinx.Graphics.Vulkan public void SetImage(ShaderStage stage, int binding, ITexture image) { _descriptorSetUpdater.SetImage(Cbs, stage, binding, image); + + if (stage == ShaderStage.Fragment && image is TextureView view) + { + FramebufferParams?.SetVirtualSize((uint)view.Width, (uint)view.Height, (uint)view.Layers); + } } public void SetImage(int binding, Auto image) @@ -1607,6 +1612,24 @@ namespace Ryujinx.Graphics.Vulkan private bool RecreateGraphicsPipelineIfNeeded() { + if (FramebufferParams != null && + FramebufferParams.AttachmentsCount == 0 && + FramebufferParams.Width == 1 && + FramebufferParams.Height == 1 && + DynamicState.ViewportsCount != 0) + { + // An attachmentless fragment pass can reach its first draw before the storage + // image descriptor is rebound. At that point the null framebuffer still has + // its constructor fallback of 1x1, even though the guest viewport already + // describes the real render area. Seed the virtual framebuffer from that + // viewport so the first storage-image draw is not clipped to one pixel. + Silk.NET.Vulkan.Viewport viewport = DynamicState.Viewports[0]; + uint width = (uint)Math.Max(1f, Math.Abs(viewport.Width)); + uint height = (uint)Math.Max(1f, Math.Abs(viewport.Height)); + + FramebufferParams.SetVirtualSize(width, height, 1); + } + if (AutoFlush.ShouldFlushDraw(DrawCount)) { Gd.FlushAllCommands();