Skip to content

Fix crash when a graphics pipeline is bound with no active render pass#871

Open
Chaos02 wants to merge 1 commit into
xCollateral:devfrom
Chaos02:fix/tab-out
Open

Fix crash when a graphics pipeline is bound with no active render pass#871
Chaos02 wants to merge 1 commit into
xCollateral:devfrom
Chaos02:fix/tab-out

Conversation

@Chaos02

@Chaos02 Chaos02 commented Jul 16, 2026

Copy link
Copy Markdown

Fixes an intermittent NullPointerException that kills the game when the window is
occluded, minimized, or the title bar is being dragged.

Symptom

java.lang.NullPointerException: Cannot invoke "...RenderPass.getId()" because "state.renderPass" is null
	at net.vulkanmod.vulkan.shader.GraphicsPipeline.createGraphicsPipeline(GraphicsPipeline.java:192)
	at ...Object2LongOpenHashMap.computeIfAbsent (getHandle:58)
	at net.vulkanmod.vulkan.Renderer.bindGraphicsPipeline(Renderer.java:638)
	at net.minecraft.class_5944.bindPipeline(ShaderInstance)

Repro

In-game, open chat, then tab out or minimize the window. Crashes intermittently.

It is a race between three conditions: the window stops having a valid render target,
the GUI (chat) keeps rendering anyway, and a GUI shader is used whose pipeline is not
yet in the cache.

Cause

The GUI still renders while no render pass is bound. bindGraphicsPipeline builds a
PipelineState from the current (null) boundRenderPass; the first use of an
uncached GUI shader then reaches createGraphicsPipeline, which dereferences
state.renderPass.getId() and throws. A cached pipeline survives this path, which is
why the crash only shows up when a not-yet-compiled shader happens to be used during
the occluded frames.

Fix

Skip the bind when no render pass is bound. Without a pass there is no valid target to
record a draw into, so there is nothing useful to do in this method anyway.

This covers the whole path to the NPE:

  • createGraphicsPipeline has two call sites.
  • Site 2, getHandle, is only reached from Renderer.bindGraphicsPipeline, which this
    guard covers.
  • Site 1, the eager compile in the GraphicsPipeline constructor, is gated on
    Pipeline.Builder.renderPass.

Notes

One unrelated observation found while tracing this, left alone here: PipelineState.DEFAULT
carries a null renderPass, so if the constructor's eager-compile path were ever wired up it
would hit the same dereference on non-DYNAMIC_RENDERING devices. Not reachable today; happy
to open a separate issue if that is worth tracking.

Verification

Repro steps above against the patched build: no crash across repeated tab-outs.

Copilot AI review requested due to automatic review settings July 16, 2026 10:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses two window-occlusion/minimize edge cases in VulkanMod’s frame/render lifecycle: a crash caused by attempting to bind/compile graphics pipelines without an active render pass, and a “clear-color flash” caused by presenting frames that were only cleared (but not fully rendered) while occluded.

Changes:

  • Skip Renderer.bindGraphicsPipeline(...) when no render pass is currently bound (prevents NPE during pipeline handle creation).
  • Change the main swapchain render pass color attachment load-op to VK_ATTACHMENT_LOAD_OP_LOAD to preserve prior contents on GUI-only / occluded frames.
  • Stop clearing the color attachment every tick (clear depth only) to avoid presenting the global clear color on occluded frames.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/main/java/net/vulkanmod/vulkan/Renderer.java Adds an early-out in bindGraphicsPipeline when no render pass is bound to prevent a null render-pass pipeline-state crash.
src/main/java/net/vulkanmod/vulkan/pass/DefaultMainPass.java Switches the main pass color load-op to LOAD so GUI-only frames can draw over prior contents instead of a freshly-cleared color.
src/main/java/net/vulkanmod/mixin/render/frame/MinecraftMixin.java Changes per-tick clears to depth-only to pair with the render-pass LOAD and eliminate the clear-color flash.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +27 to +30
// Don't clear COLOR each frame — the world/GUI overwrites it on normal frames,
// and preserving it lets the main render pass's LOAD keep the last good image
// (fixes the tab-out clear-color flash). Depth is still cleared.
Renderer.clearAttachments(GL11.GL_DEPTH_BUFFER_BIT);
Comment on lines +72 to +77
// Main swapchain pass LOADs the previously-presented frame so GUI-only / occluded
// frames (where the world render is skipped) composite over the last good image
// instead of showing the stale clear color (fixes the tab-out flash). The color
// attachment's initialLayout is COLOR_ATTACHMENT_OPTIMAL (not UNDEFINED) and begin()
// transitions the image to that layout preserving contents, so LOAD retains the
// prior frame. On normal frames the world/sky fully overwrites color, so this is a no-op.
Comment on lines +640 to +641
if (boundRenderPass == null)
return;
@Chaos02

Chaos02 commented Jul 16, 2026

Copy link
Copy Markdown
Author

Related PRs from the same effort (getting a VulkanMod based 1.21.1 pack working). Both are independent of this one, which is purely the tab-out path:

Rendering the GUI while no render pass is bound (window occluded,
minimized, or the title bar being dragged) makes bindGraphicsPipeline
build a PipelineState from a null render pass, which then NPEs in
GraphicsPipeline.createGraphicsPipeline at state.renderPass.getId().

Skip the bind when nothing is bound. Without a render pass there is no
valid target to record a draw into, so there is nothing useful to do
here anyway. getHandle is only reached from bindGraphicsPipeline, and
the eager compile in the GraphicsPipeline constructor is gated on
builder.renderPass, so this covers the whole path to the NPE.
@Chaos02 Chaos02 changed the title Fix tab-out crash and clear-color flash Fix crash when a graphics pipeline is bound with no active render pass Jul 17, 2026
@Chaos02

Chaos02 commented Jul 17, 2026

Copy link
Copy Markdown
Author

I am withdrawing the flash half of this PR. It is wrong, and the verification I claimed for it
does not hold up. Detail below, since the reasoning matters more than the retraction.

The stated rationale is factually wrong. The PR body claims LOAD_OP_LOAD on the main
swapchain pass "retains the previously presented frame". It does not. With N swapchain images,
LOAD on a swapchain image yields what that image held N frames ago, not what was presented
last frame. In game this is immediately visible once the change is actually live: the panorama,
the resource-pack MOTD splash and the mod-update indicator all jitter back and forth through N
stale states while only slowly advancing. It is every animated GUI element, not just the
background I was aiming at.

It also reads undefined memory after a swapchain recreate. SwapChain.createSwapChain
builds fresh VulkanImages on each recreate, so their contents are UNDEFINED on first use,
and LOAD then loads undefined. Window drag and the GNOME overview both trigger recreation.
This would explain the black flashes I now see once the change is live, and the cadence fits
(first tap clean, drag brings on black and jitter, every subsequent tap has it), though I
have not yet finished isolating those artifacts against a stock build.

The "Verified in-game on MC 1.21.1" claim is not supportable, and I should not have made
it.
What I actually tested was a backport of this change in a companion mod on 1.21.1, not
this patch against dev, and on 1.21.1 neither half of the flash fix ever executed:

  • A runtime trace of every Renderer.clearAttachments call on 1.21.1 shows zero
    DEPTH|COLOR (0x4100) clears. The preFrameOps frame-start DEPTH|COLOR clear that the
    clear-strip targets does not exist in the shipped 1.21.1 version, so the strip never
    matched anything. The only COLOR clear on 1.21.1 is a COLOR-only 0x4000 from
    LoadingOverlay.render, which the strip deliberately passes through.
  • The DefaultMainPass LOAD was shadowed: the test profile had Beryl installed, which
    replaces DefaultMainPass with its own main pass, so the patched class was not the one in
    use.

So the test that "verified" this exercised neither the clear-strip nor the LOAD. The
pause-menu flash improvement I signed off on most likely came from the null-renderPass guard
in the other commit, not from this change. I extrapolated from reading dev and assuming
1.21.1 matched, and the in-game evidence I leaned on was contaminated.

The crash guard is unaffected by any of this. Rather than close this PR and scatter the
history across a new number, I have force-pushed it to carry only that guard, and retitled
and rewritten the body to match. The flash commit is gone from the branch; this PR is now
just the crash fix.

Sorry for the round trip.

@Chaos02

Chaos02 commented Jul 17, 2026

Copy link
Copy Markdown
Author

Separate finding from the same tracing session, flagging it here since Beryl has no issue
tracker and this is relevant to any main-pass-level change in VulkanMod.

Beryl (0.2.0-alpha) replaces DefaultMainPass with net.beryl.render.ShaderMainPass, which
issues its own full-screen DEPTH|COLOR clear every frame from inside beginFrame. Traced
call, present on every frame:

Renderer.clearAttachments mask=0x4100 clearColor=[0.1176, 0.1176, 0.1176, 1.0]
	caller=net.beryl.render.ShaderMainPass.begin:57
	<- Renderer.beginMainRenderPass:308
	<- Renderer.beginFrame:289

The practical consequence for review here: any fix made at the DefaultMainPass level is
inert for Beryl users
, because DefaultMainPass is not the class in use for them. That is
what invalidated my own verification of this PR, and it may be worth keeping in mind for
main-pass changes generally.

One related data point, since it points at Beryl rather than VulkanMod: A/B-ing Beryl off
makes the launch/menu flash I was originally chasing disappear.

Beryl is closed-source so I only have stack frames here, and I am not speculating past them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants