Add unknown blend state and BufferBlendOverride handling from modern Iris (#1408)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-02-21 15:27:22 -08:00
1 parent ad123d0bd5
commit 0325a96bc2
8 files changed
+224 -21

No files matched your search

@@ -1788,6 +1788,7 @@ public class GLStateManager {
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
}
BlendModeStorage.flushDeferredBlend();
GL11.glDrawElements(mode, indices);
}
@@ -1795,6 +1796,7 @@ public class GLStateManager {
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
}
BlendModeStorage.flushDeferredBlend();
GL11.glDrawElements(mode, indices);
}
@@ -1802,6 +1804,7 @@ public class GLStateManager {
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
}
BlendModeStorage.flushDeferredBlend();
GL11.glDrawElements(mode, indices);
}
@@ -1809,6 +1812,7 @@ public class GLStateManager {
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
}
BlendModeStorage.flushDeferredBlend();
GL11.glDrawElements(mode, count, type, indices);
}
@@ -1824,6 +1828,7 @@ public class GLStateManager {
return;
}
}
BlendModeStorage.flushDeferredBlend();
GL11.glDrawElements(mode, indices_count, type, indices_buffer_offset);
}
@@ -1850,6 +1855,7 @@ public class GLStateManager {
}
return;
}
BlendModeStorage.flushDeferredBlend();
GL11.glDrawArrays(mode, first, count);
}
@@ -347,10 +347,12 @@ public class RenderSystem {
public static void disableBufferBlend(int buffer) {
GL30.glDisablei(GL11.GL_BLEND, buffer);
GLStateManager.getBlendMode().setUnknownState();
}
public static void enableBufferBlend(int buffer) {
GL30.glEnablei(GL11.GL_BLEND, buffer);
GLStateManager.getBlendMode().setUnknownState();
}
public static void blendFuncSeparatei(int buffer, int srcRGB, int dstRGB, int srcAlpha, int dstAlpha) {
@@ -8,11 +8,16 @@ public class BooleanState implements ISettableState<BooleanState> {
protected final int glCap;
@Getter protected boolean enabled;
protected boolean stateUnknown;
public BooleanState(int glCap) {
this.glCap = glCap;
}
public void setUnknownState() {
stateUnknown = true;
}
public void disable() {
this.setEnabled(false);
}
@@ -23,7 +28,8 @@ public class BooleanState implements ISettableState<BooleanState> {
public void setEnabled(boolean enabled) {
final boolean bypass = GLStateManager.shouldBypassCache();
if (bypass || enabled != this.enabled || (this.glCap == GL11.GL_BLEND && GLStateManager.vendorIsAMD() && GLStateManager.isPoppingAttributes())) {
if (stateUnknown || bypass || enabled != this.enabled || (this.glCap == GL11.GL_BLEND && GLStateManager.vendorIsAMD() && GLStateManager.isPoppingAttributes())) {
stateUnknown = false;
if (!bypass) {
this.enabled = enabled;
}
@@ -1,11 +1,14 @@
package net.coderbot.iris.celeritas;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Ints;
import lombok.Getter;
import lombok.experimental.Accessors;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.blending.BlendModeOverride;
import net.coderbot.iris.gl.blending.BufferBlendOverride;
import net.coderbot.iris.gl.framebuffer.GlFramebuffer;
import net.coderbot.iris.shaderpack.loading.ProgramId;
import net.coderbot.iris.gl.program.ProgramImages;
import net.coderbot.iris.gl.program.ProgramSamplers;
import net.coderbot.iris.gl.program.ProgramUniforms;
@@ -19,7 +22,10 @@ import net.coderbot.iris.shaderpack.ProgramSource;
import net.coderbot.iris.uniforms.custom.CustomUniforms;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@@ -43,6 +49,7 @@ public class CeleritasTerrainPipeline {
private final EnumMap<PatchShaderType, Optional<String>> sources = new EnumMap<>(PatchShaderType.class);
private GlFramebuffer framebuffer;
private BlendModeOverride blendModeOverride;
private List<BufferBlendOverride> bufferBlendOverrides;
private float alphaReference;
private PassInfo() {
@@ -91,21 +98,41 @@ public class CeleritasTerrainPipeline {
PassInfo passInfo = new PassInfo();
passInfoMap.put(pass, passInfo);
// Set up framebuffer
// Set up framebuffer, blend mode, and buffer blend overrides
final ProgramId programId = switch (pass) {
case GBUFFER_TRANSLUCENT -> ProgramId.Water;
case SHADOW, SHADOW_CUTOUT -> ProgramId.Shadow;
default -> ProgramId.Terrain;
};
if (pass == IrisTerrainPass.SHADOW || pass == IrisTerrainPass.SHADOW_CUTOUT) {
passInfo.framebuffer = shadowFramebuffer;
passInfo.blendModeOverride = programId.getBlendModeOverride();
passInfo.bufferBlendOverrides = Collections.emptyList();
} else if (renderTargets != null) {
ImmutableSet<Integer> flipped = pass == IrisTerrainPass.GBUFFER_TRANSLUCENT ? flippedAfterTranslucent : flippedAfterPrepare;
Optional<ProgramSource> programSource = gbufferProgramSource.get(pass);
programSource.ifPresentOrElse(source -> passInfo.framebuffer = renderTargets.createGbufferFramebuffer(flipped, source.getDirectives().getDrawBuffers()), () -> passInfo.framebuffer = renderTargets.createGbufferFramebuffer(flipped, new int[] {0}));
}
programSource.ifPresentOrElse(source -> {
passInfo.framebuffer = renderTargets.createGbufferFramebuffer(flipped, source.getDirectives().getDrawBuffers());
passInfo.blendModeOverride = source.getDirectives().getBlendModeOverride().orElse(programId.getBlendModeOverride());
// Set blend mode override
passInfo.blendModeOverride = switch (pass) {
case GBUFFER_TRANSLUCENT -> null; // Default blending
default -> BlendModeOverride.OFF;
};
passInfo.bufferBlendOverrides = new ArrayList<>();
source.getDirectives().getBufferBlendOverrides().forEach(information -> {
int index = Ints.indexOf(source.getDirectives().getDrawBuffers(), information.getIndex());
if (index > -1) {
passInfo.bufferBlendOverrides.add(new BufferBlendOverride(index, information.getBlendMode()));
}
});
}, () -> {
passInfo.framebuffer = renderTargets.createGbufferFramebuffer(flipped, new int[] {0});
passInfo.blendModeOverride = programId.getBlendModeOverride();
passInfo.bufferBlendOverrides = Collections.emptyList();
});
} else {
passInfo.blendModeOverride = programId.getBlendModeOverride();
passInfo.bufferBlendOverrides = Collections.emptyList();
}
// Set alpha reference
passInfo.alphaReference = switch (pass) {
@@ -2,6 +2,7 @@ package net.coderbot.iris.celeritas;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.blending.BlendModeOverride;
import net.coderbot.iris.gl.blending.BufferBlendOverride;
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
import net.coderbot.iris.pipeline.transform.PatchShaderType;
import net.coderbot.iris.shadows.ShadowRenderingState;
@@ -15,6 +16,7 @@ import org.embeddedt.embeddium.impl.render.chunk.terrain.TerrainRenderPass;
import org.jetbrains.annotations.Nullable;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@@ -82,11 +84,11 @@ public class IrisCeleritasChunkProgramOverrides {
builder.bindAttribute(attr.getName(), attrIndex++);
}
// Get blend from PassInfo
final CeleritasTerrainPipeline.PassInfo passInfo = pipeline.getPassInfo(pass);
final BlendModeOverride blendOverride = passInfo.blendModeOverride();
final List<BufferBlendOverride> bufferOverrides = passInfo.bufferBlendOverrides();
return builder.link(context -> new IrisCeleritasChunkShaderInterface(((GlObject) context).handle(), context, pipeline, pass.isShadow(), blendOverride, pipeline.getCustomUniforms()));
return builder.link(context -> new IrisCeleritasChunkShaderInterface(((GlObject) context).handle(), context, pipeline, pass.isShadow(), blendOverride, bufferOverrides, pipeline.getCustomUniforms()));
} finally {
vertShader.delete();
if (geomShader != null) geomShader.delete();
@@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.mixins.interfaces.EntityRendererAccessor;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.blending.BlendModeOverride;
import net.coderbot.iris.gl.blending.BufferBlendOverride;
import net.coderbot.iris.gl.framebuffer.GlFramebuffer;
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
import net.coderbot.iris.samplers.IrisSamplers;
@@ -16,6 +17,7 @@ import net.coderbot.iris.gl.program.ProgramSamplers;
import net.coderbot.iris.gl.program.ProgramUniforms;
import net.coderbot.iris.shadows.ShadowRenderingState;
import net.coderbot.iris.uniforms.custom.CustomUniforms;
import java.util.List;
import org.embeddedt.embeddium.impl.gl.shader.ShaderBindingContext;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformFloat3v;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformMatrix3f;
@@ -51,13 +53,15 @@ public class IrisCeleritasChunkShaderInterface implements ChunkShaderInterface {
// Rendering state
private final BlendModeOverride blendModeOverride;
private final List<BufferBlendOverride> bufferBlendOverrides;
private final boolean hasOverrides;
// Stored matrices for inverse and normal matrix computation
private final Matrix4f projectionMatrixInverse = new Matrix4f();
private final Matrix4f modelViewMatrixInverse = new Matrix4f();
private final Matrix3f normalMatrix = new Matrix3f();
public IrisCeleritasChunkShaderInterface(int handle, ShaderBindingContext context, CeleritasTerrainPipeline pipeline, boolean isShadowPass, BlendModeOverride blendModeOverride, CustomUniforms customUniforms) {
public IrisCeleritasChunkShaderInterface(int handle, ShaderBindingContext context, CeleritasTerrainPipeline pipeline, boolean isShadowPass, BlendModeOverride blendModeOverride, List<BufferBlendOverride> bufferBlendOverrides, CustomUniforms customUniforms) {
this.uniformModelViewMatrix = context.bindUniformIfPresent("iris_ModelViewMatrix", GlUniformMatrix4f::new);
this.uniformModelViewMatrixInverse = context.bindUniformIfPresent("iris_ModelViewMatrixInverse", GlUniformMatrix4f::new);
this.uniformProjectionMatrix = context.bindUniformIfPresent("iris_ProjectionMatrix", GlUniformMatrix4f::new);
@@ -66,6 +70,8 @@ public class IrisCeleritasChunkShaderInterface implements ChunkShaderInterface {
this.uniformRegionOffset = context.bindUniformIfPresent("u_RegionOffset", GlUniformFloat3v::new);
this.blendModeOverride = blendModeOverride;
this.bufferBlendOverrides = bufferBlendOverrides;
this.hasOverrides = bufferBlendOverrides != null && !bufferBlendOverrides.isEmpty();
this.customUniforms = customUniforms;
final ProgramUniforms.Builder builder = pipeline.initUniforms(handle);
@@ -94,6 +100,10 @@ public class IrisCeleritasChunkShaderInterface implements ChunkShaderInterface {
blendModeOverride.apply();
}
if (hasOverrides) {
bufferBlendOverrides.forEach(BufferBlendOverride::apply);
}
if (irisProgramUniforms != null) {
irisProgramUniforms.update();
}
@@ -109,7 +119,7 @@ public class IrisCeleritasChunkShaderInterface implements ChunkShaderInterface {
@Override
public void restoreState() {
if (blendModeOverride != null) {
if (blendModeOverride != null || hasOverrides) {
BlendModeOverride.restore();
}
@@ -4,29 +4,41 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.states.BlendState;
import lombok.Getter;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL14;
public class BlendModeStorage {
private static final int MAX_DRAW_BUFFERS = 8;
private static boolean originalBlendEnable;
private static final BlendState originalBlend = new BlendState();
@Getter private static boolean blendLocked;
@Getter private static boolean hasDeferredChanges;
private static final int[] bufferOverrideIndices = new int[MAX_DRAW_BUFFERS];
private static final boolean[] bufferOverrideIsDisable = new boolean[MAX_DRAW_BUFFERS];
private static final BlendState[] bufferOverrideStates = new BlendState[MAX_DRAW_BUFFERS];
private static int bufferOverrideCount;
static {
for (int i = 0; i < MAX_DRAW_BUFFERS; i++) {
bufferOverrideStates[i] = new BlendState();
}
}
public static void overrideBlend(BlendState override) {
if (!blendLocked) {
// Only save the previous state if the blend mode wasn't already locked
originalBlendEnable = GL11.glGetBoolean(GL11.GL_BLEND);
originalBlendEnable = GLStateManager.getBlendMode().isEnabled();
originalBlend.set(GLStateManager.getBlendState());
}
bufferOverrideCount = 0;
hasDeferredChanges = false;
blendLocked = false;
if (override == null) {
GL11.glDisable(GL11.GL_BLEND);
GLStateManager.disableBlend();
} else {
GL11.glEnable(GL11.GL_BLEND);
GL14.glBlendFuncSeparate(override.getSrcRgb(), override.getDstRgb(), override.getSrcAlpha(), override.getDstAlpha());
GLStateManager.enableBlend();
GLStateManager.tryBlendFuncSeparate(override.getSrcRgb(), override.getDstRgb(), override.getSrcAlpha(), override.getDstAlpha());
}
blendLocked = true;
@@ -35,7 +47,7 @@ public class BlendModeStorage {
public static void overrideBufferBlend(int index, BlendState override) {
if (!blendLocked) {
// Only save the previous state if the blend mode wasn't already locked
originalBlendEnable = GL11.glGetBoolean(GL11.GL_BLEND);
originalBlendEnable = GLStateManager.getBlendMode().isEnabled();
originalBlend.set(GLStateManager.getBlendState());
}
@@ -46,15 +58,53 @@ public class BlendModeStorage {
RenderSystem.blendFuncSeparatei(index, override.getSrcRgb(), override.getDstRgb(), override.getSrcAlpha(), override.getDstAlpha());
}
if (bufferOverrideCount < MAX_DRAW_BUFFERS) {
final int slot = bufferOverrideCount++;
bufferOverrideIndices[slot] = index;
bufferOverrideIsDisable[slot] = (override == null);
if (override != null) {
bufferOverrideStates[slot].setAll(override.getSrcRgb(), override.getDstRgb(), override.getSrcAlpha(), override.getDstAlpha());
}
}
blendLocked = true;
}
public static void deferBlendModeToggle(boolean enabled) {
originalBlendEnable = enabled;
hasDeferredChanges = true;
}
public static void deferBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) {
originalBlend.setAll(srcRgb, dstRgb, srcAlpha, dstAlpha);
hasDeferredChanges = true;
}
public static void flushDeferredBlend() {
if (!blendLocked || !hasDeferredChanges) return;
hasDeferredChanges = false;
blendLocked = false;
if (originalBlendEnable) {
GLStateManager.enableBlend();
} else {
GLStateManager.disableBlend();
}
GLStateManager.tryBlendFuncSeparate(
originalBlend.getSrcRgb(), originalBlend.getDstRgb(),
originalBlend.getSrcAlpha(), originalBlend.getDstAlpha());
blendLocked = true;
for (int i = 0; i < bufferOverrideCount; i++) {
final int idx = bufferOverrideIndices[i];
if (bufferOverrideIsDisable[i]) {
RenderSystem.disableBufferBlend(idx);
} else {
final BlendState s = bufferOverrideStates[i];
RenderSystem.enableBufferBlend(idx);
RenderSystem.blendFuncSeparatei(idx, s.getSrcRgb(), s.getDstRgb(), s.getSrcAlpha(), s.getDstAlpha());
}
}
}
public static void restoreBlend() {
@@ -63,6 +113,8 @@ public class BlendModeStorage {
}
blendLocked = false;
hasDeferredChanges = false;
bufferOverrideCount = 0;
if (originalBlendEnable) {
GLStateManager.enableBlend();
@@ -0,0 +1,98 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.AngelicaExtension;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
import static com.gtnewhorizons.angelica.util.GLSMUtil.resetGLState;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests for {@link com.gtnewhorizons.angelica.glsm.states.BooleanState#setUnknownState()}.
* Verifies that per-buffer blend operations (glEnablei/glDisablei) properly invalidate
* the GLSM blend cache so subsequent global blend enable/disable calls are not skipped.
*/
@ExtendWith(AngelicaExtension.class)
class GLSM_BlendUnknownState_UnitTest {
@BeforeEach
void setUp() {
resetGLState();
}
@AfterEach
void tearDown() {
resetGLState();
}
@Test
void testSetUnknownStateForcesCacheBypass() {
// Enable blend via GLSM — both cache and GL should agree
GLStateManager.enableBlend();
assertTrue(GL11.glIsEnabled(GL11.GL_BLEND), "GL blend should be enabled");
assertTrue(GLStateManager.getBlendMode().isEnabled(), "GLSM blend cache should be enabled");
// Simulate a per-buffer disable that bypasses the cache (as glDisablei does)
GL30.glDisablei(GL11.GL_BLEND, 0);
GLStateManager.getBlendMode().setUnknownState();
// Cache still says enabled, but GL state for buffer 0 is now disabled
assertTrue(GLStateManager.getBlendMode().isEnabled(), "GLSM cache should still say enabled");
// Re-enable blend via GLSM — without setUnknownState, this would be a no-op
// because the cache thinks blend is already enabled
GLStateManager.enableBlend();
// Per GL spec, glEnable(GL_BLEND) enables blend for ALL draw buffers,
// overriding any per-buffer disables
assertTrue(GL11.glIsEnabled(GL11.GL_BLEND), "GL blend should be re-enabled after setUnknownState");
}
@Test
void testDisableBufferBlendInvalidatesCache() {
GLStateManager.enableBlend();
assertTrue(GL11.glIsEnabled(GL11.GL_BLEND));
// Use RenderSystem which should call setUnknownState internally
RenderSystem.disableBufferBlend(0);
// Now re-enable via GLSM — should issue the GL call
GLStateManager.enableBlend();
assertTrue(GL11.glIsEnabled(GL11.GL_BLEND), "Blend should be re-enabled after disableBufferBlend");
}
@Test
void testEnableBufferBlendInvalidatesCache() {
// Start with blend disabled
GLStateManager.disableBlend();
assertFalse(GL11.glIsEnabled(GL11.GL_BLEND));
// Per-buffer enable bypasses cache
RenderSystem.enableBufferBlend(0);
// Disable via GLSM — should issue the GL call despite cache saying disabled
GLStateManager.disableBlend();
assertFalse(GL11.glIsEnabled(GL11.GL_BLEND), "Blend should be disabled after enableBufferBlend + disableBlend");
}
@Test
void testUnknownStateClearedAfterSetEnabled() {
GLStateManager.enableBlend();
// Mark unknown
GLStateManager.getBlendMode().setUnknownState();
// First setEnabled clears the unknown flag
GLStateManager.enableBlend();
// Second setEnabled should use normal caching (no redundant GL call)
// Verify state is consistent
assertTrue(GL11.glIsEnabled(GL11.GL_BLEND));
assertTrue(GLStateManager.getBlendMode().isEnabled());
}
}