diff --git a/jme3-core/src/main/java/com/jme3/material/Material.java b/jme3-core/src/main/java/com/jme3/material/Material.java
index 0c4317a307..7488bb6b98 100644
--- a/jme3-core/src/main/java/com/jme3/material/Material.java
+++ b/jme3-core/src/main/java/com/jme3/material/Material.java
@@ -881,14 +881,10 @@ private void updateShaderMaterialParameter(Renderer renderer, VarType type, Shad
ShaderBufferBlock.BufferType btype;
if (type == VarType.ShaderStorageBufferObject) {
btype = ShaderBufferBlock.BufferType.ShaderStorageBufferObject;
- bufferBlock.setBufferObject(btype, bufferObject);
- renderer.setShaderStorageBufferObject(unit.bufferUnit, bufferObject); // TODO: probably not needed
} else {
btype = ShaderBufferBlock.BufferType.UniformBufferObject;
- bufferBlock.setBufferObject(btype, bufferObject);
- renderer.setUniformBufferObject(unit.bufferUnit, bufferObject); // TODO: probably not needed
}
- unit.bufferUnit++;
+ bufferBlock.setBufferObject(btype, bufferObject);
} else {
Uniform uniform = shader.getUniform(param.getPrefixedName());
if (!override && uniform.isSetByCurrentMaterial())
diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java
index cf8aeb790f..3dc203d41c 100644
--- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java
+++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL3.java
@@ -227,4 +227,20 @@ public interface GL3 extends GL2 {
* uniformBlockIndex within program.
*/
public void glUniformBlockBinding(int program, int uniformBlockIndex, int uniformBlockBinding);
+
+ /**
+ *
Reference Page
+ *
+ * Queries information about an active uniform block.
+ *
+ * @param program the name of a program containing the uniform block.
+ * @param uniformBlockIndex the index of the uniform block within program.
+ * @param pname the name of the parameter to query. One of:
+ * {@link #GL_UNIFORM_BLOCK_BINDING}
+ * {@link #GL_UNIFORM_BLOCK_DATA_SIZE}
+ * {@link #GL_UNIFORM_BLOCK_NAME_LENGTH}
+ * {@link #GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS}
+ * @return the value of the queried parameter.
+ */
+ public int glGetActiveUniformBlocki(int program, int uniformBlockIndex, int pname);
}
diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java
index 5f734efcdf..a84e4ef079 100644
--- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java
+++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GL4.java
@@ -31,6 +31,8 @@
*/
package com.jme3.renderer.opengl;
+import java.nio.IntBuffer;
+
/**
* GL functions only available on vanilla desktop OpenGL 4.0.
*
@@ -77,6 +79,11 @@ public interface GL4 extends GL3 {
public static final int GL_SHADER_STORAGE_BUFFER = 0x90D2;
public static final int GL_SHADER_STORAGE_BLOCK = 0x92E6;
+ /**
+ * Accepted by the {@code props} parameter of GetProgramResourceiv.
+ */
+ public static final int GL_BUFFER_BINDING = 0x9302;
+
/**
* Accepted by the <pname> parameter of GetIntegerv, GetBooleanv,
* GetInteger64v, GetFloatv, and GetDoublev:
@@ -124,7 +131,22 @@ public interface GL4 extends GL3 {
* @param storageBlockBinding The index storage block binding to associate with the specified storage block.
*/
public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding);
-
+
+ /**
+ * Reference Page
+ *
+ * Retrieves values for multiple properties of a single active resource within a program object.
+ *
+ * @param program the name of a program object whose resources to query.
+ * @param programInterface a token identifying the interface within program containing the resource named name.
+ * @param index the active resource index.
+ * @param props an array of properties to query.
+ * @param length an array that will receive the number of values written to params.
+ * @param params an array that will receive the property values.
+ */
+ public void glGetProgramResourceiv(int program, int programInterface, int index, IntBuffer props, IntBuffer length, IntBuffer params);
+
+
/**
* Binds a single level of a texture to an image unit for the purpose of reading
* and writing it from shaders.
diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
index f4ae6fe0e1..5ad8ae63e3 100644
--- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
+++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
@@ -1456,7 +1456,6 @@ protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBl
final BufferObject bufferObject = bufferBlock.getBufferObject();
final BufferType bufferType = bufferBlock.getType();
-
if (bufferObject.isUpdateNeeded()) {
if (bufferType == BufferType.ShaderStorageBufferObject) {
@@ -1469,39 +1468,20 @@ protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBl
int usage = resolveUsageHint(bufferObject.getAccessHint(), bufferObject.getNatureHint());
if (usage == -1) return; // cpu only
- bindProgram(shader);
-
- final int shaderId = shader.getId();
-
- int bindingPoint = bufferObject.getBinding();
+ int bindingPoint = bufferBlock.getBinding();
+ if (bindingPoint < 0) {
+ // Binding not yet resolved — skip until resolveBufferBlockBindings runs
+ bufferBlock.clearUpdateNeeded();
+ return;
+ }
switch (bufferType) {
case UniformBufferObject: {
- setUniformBufferObject(bindingPoint, bufferObject); // rebind buffer if needed
- if (bufferBlock.isUpdateNeeded()) {
- int blockIndex = bufferBlock.getLocation();
- if (blockIndex < 0) {
- blockIndex = gl3.glGetUniformBlockIndex(shaderId, bufferBlock.getName());
- bufferBlock.setLocation(blockIndex);
- }
- if (bufferBlock.getLocation() != NativeObject.INVALID_ID) {
- gl3.glUniformBlockBinding(shaderId, bufferBlock.getLocation(), bindingPoint);
- }
- }
+ setUniformBufferObject(bindingPoint, bufferObject);
break;
}
case ShaderStorageBufferObject: {
- setShaderStorageBufferObject(bindingPoint, bufferObject); // rebind buffer if needed
- if (bufferBlock.isUpdateNeeded() ) {
- int blockIndex = bufferBlock.getLocation();
- if (blockIndex < 0) {
- blockIndex = gl4.glGetProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName());
- bufferBlock.setLocation(blockIndex);
- }
- if (bufferBlock.getLocation() != NativeObject.INVALID_ID) {
- gl4.glShaderStorageBlockBinding(shaderId, bufferBlock.getLocation(), bindingPoint);
- }
- }
+ setShaderStorageBufferObject(bindingPoint, bufferObject);
break;
}
default: {
@@ -1512,6 +1492,111 @@ protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBl
bufferBlock.clearUpdateNeeded();
}
+ /**
+ * Resolves binding points for all buffer blocks in a shader. Runs once
+ * per shader program. Queries the binding from the compiled shader,
+ * detects collisions, and reassigns duplicates to unique binding points.
+ *
+ * @param shader the shader whose buffer blocks to resolve.
+ */
+ private void resolveBufferBlockBindings(final Shader shader) {
+ final ListMap bufferBlocks = shader.getBufferBlockMap();
+ final int shaderId = shader.getId();
+
+ bindProgram(shader);
+
+ // Pass 1: resolve block indices and query bindings from the compiled shader
+ for (int i = 0; i < bufferBlocks.size(); i++) {
+ ShaderBufferBlock block = bufferBlocks.getValue(i);
+ if (block.getBinding() >= 0) continue; // already resolved
+
+ BufferType bufferType = block.getType();
+ if (bufferType == null) continue; // not yet configured
+
+ // Resolve block index (location)
+ int blockIndex = block.getLocation();
+ if (blockIndex < 0) {
+ if (bufferType == BufferType.ShaderStorageBufferObject) {
+ blockIndex = gl4.glGetProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, block.getName());
+ } else {
+ blockIndex = gl3.glGetUniformBlockIndex(shaderId, block.getName());
+ }
+ block.setLocation(blockIndex);
+ }
+
+ if (blockIndex < 0 || blockIndex == NativeObject.INVALID_ID) {
+ continue;
+ }
+
+ // Query the binding declared in the shader
+ int binding;
+ if (bufferType == BufferType.ShaderStorageBufferObject) {
+ binding = queryShaderStorageBlockBinding(shaderId, blockIndex);
+ } else {
+ binding = gl3.glGetActiveUniformBlocki(shaderId, blockIndex, GL3.GL_UNIFORM_BLOCK_BINDING);
+ }
+ block.setBinding(binding);
+ }
+
+ // Pass 2: detect and resolve collisions.
+ // UBOs and SSBOs use separate GL binding namespaces, so track them independently.
+ Set usedUboBindings = new HashSet<>();
+ Set usedSsboBindings = new HashSet<>();
+ int nextFreeUbo = 0;
+ int nextFreeSsbo = 0;
+
+ for (int i = 0; i < bufferBlocks.size(); i++) {
+ ShaderBufferBlock block = bufferBlocks.getValue(i);
+ int binding = block.getBinding();
+ if (binding < 0) continue;
+
+ BufferType bufferType = block.getType();
+ Set usedBindings;
+ if (bufferType == BufferType.ShaderStorageBufferObject) {
+ usedBindings = usedSsboBindings;
+ } else {
+ usedBindings = usedUboBindings;
+ }
+
+ if (!usedBindings.add(binding)) {
+ // Collision within the same namespace — find a free binding point
+ if (bufferType == BufferType.ShaderStorageBufferObject) {
+ while (usedBindings.contains(nextFreeSsbo)) nextFreeSsbo++;
+ binding = nextFreeSsbo;
+ } else {
+ while (usedBindings.contains(nextFreeUbo)) nextFreeUbo++;
+ binding = nextFreeUbo;
+ }
+ usedBindings.add(binding);
+ block.setBinding(binding);
+ }
+
+ // Set the binding on the shader program
+ int blockIndex = block.getLocation();
+ if (bufferType == BufferType.ShaderStorageBufferObject) {
+ gl4.glShaderStorageBlockBinding(shaderId, blockIndex, binding);
+ } else {
+ gl3.glUniformBlockBinding(shaderId, blockIndex, binding);
+ }
+ }
+ }
+
+ /**
+ * Queries the binding point of a shader storage block using
+ * glGetProgramResourceiv with GL_BUFFER_BINDING.
+ *
+ * @param program the shader program id.
+ * @param blockIndex the block index within the program.
+ * @return the binding point assigned to the block.
+ */
+ private int queryShaderStorageBlockBinding(int program, int blockIndex) {
+ intBuf16.clear();
+ intBuf16.put(GL4.GL_BUFFER_BINDING).flip();
+ intBuf1.clear();
+ gl4.glGetProgramResourceiv(program, GL4.GL_SHADER_STORAGE_BLOCK, blockIndex, intBuf16, null, intBuf1);
+ return intBuf1.get(0);
+ }
+
protected void updateShaderUniforms(Shader shader) {
ListMap uniforms = shader.getUniformMap();
for (int i = 0; i < uniforms.size(); i++) {
@@ -1529,6 +1614,11 @@ protected void updateShaderUniforms(Shader shader) {
*/
protected void updateShaderBufferBlocks(final Shader shader) {
final ListMap bufferBlocks = shader.getBufferBlockMap();
+ // Resolve binding points once per shader, detecting and fixing collisions
+ if (bufferBlocks.size() > 0 && bufferBlocks.getValue(0).getBinding() < 0) {
+ resolveBufferBlockBindings(shader);
+ }
+
for (int i = 0; i < bufferBlocks.size(); i++) {
updateShaderBufferBlock(shader, bufferBlocks.getValue(i));
}
@@ -1559,6 +1649,45 @@ public int convertShaderType(ShaderType type) {
}
}
+ private static final Pattern BUFFER_BLOCK_PATTERN = Pattern.compile(
+ "layout\\s*\\([^)]*\\)\\s*(buffer|uniform)\\s+\\w+");
+
+ private static final Pattern BINDING_ZERO_PATTERN = Pattern.compile(
+ "layout\\s*\\([^)]*binding\\s*=\\s*0[^)]*\\)\\s*(buffer|uniform)\\s+\\w+");
+
+ /**
+ * Checks that layout(binding=0) is not used on a non-first buffer block,
+ * since the GL query cannot distinguish explicit binding=0 from the
+ * default, making collision detection unreliable for that case.
+ *
+ * @param source the GLSL source code.
+ * @param sourceName the name of the shader source for error messages.
+ */
+ private void validateBufferBlockBindings(String source, String sourceName) {
+ Matcher allBlocks = BUFFER_BLOCK_PATTERN.matcher(source);
+ Matcher binding0Blocks = BINDING_ZERO_PATTERN.matcher(source);
+
+ // Find positions of all buffer/uniform block declarations
+ List allPositions = new ArrayList<>();
+ while (allBlocks.find()) {
+ allPositions.add(allBlocks.start());
+ }
+
+ if (allPositions.size() < 2) return; // single block, no ambiguity possible
+
+ int firstBlockPos = allPositions.get(0);
+
+ while (binding0Blocks.find()) {
+ if (binding0Blocks.start() != firstBlockPos) {
+ throw new RendererException(
+ "Shader '" + sourceName + "' uses layout(binding=0) on a non-first "
+ + "buffer block. This is ambiguous because the GL query cannot "
+ + "distinguish explicit binding=0 from the default. Use a non-zero "
+ + "binding or declare this block first in the shader.");
+ }
+ }
+ }
+
public void updateShaderSourceData(ShaderSource source) {
int id = source.getId();
if (id == -1) {
@@ -1623,7 +1752,11 @@ public void updateShaderSourceData(ShaderSource source) {
stringBuf.append("#define ").append(source.getType().name().toUpperCase()).append("_SHADER 1\n");
stringBuf.append(source.getDefines());
- stringBuf.append(source.getSource());
+
+ String sourceCode = source.getSource();
+ validateBufferBlockBindings(sourceCode, source.getName());
+
+ stringBuf.append(sourceCode);
intBuf1.clear();
diff --git a/jme3-core/src/main/java/com/jme3/shader/ShaderBufferBlock.java b/jme3-core/src/main/java/com/jme3/shader/ShaderBufferBlock.java
index 20d2061420..95695d784b 100644
--- a/jme3-core/src/main/java/com/jme3/shader/ShaderBufferBlock.java
+++ b/jme3-core/src/main/java/com/jme3/shader/ShaderBufferBlock.java
@@ -53,6 +53,11 @@ public static enum BufferType {
protected WeakReference bufferObjectRef;
protected BufferType type;
+ /**
+ * The binding point assigned to this block, or -1 if not yet assigned.
+ */
+ protected int binding = -1;
+
/**
* Set the new buffer object.
*
@@ -90,11 +95,30 @@ public void clearUpdateNeeded(){
updateNeeded = false;
}
+ /**
+ * Get the binding point assigned to this block.
+ *
+ * @return the binding point, or -1 if not yet assigned.
+ */
+ public int getBinding() {
+ return binding;
+ }
+
+ /**
+ * Set the binding point for this block.
+ *
+ * @param binding the binding point.
+ */
+ public void setBinding(int binding) {
+ this.binding = binding;
+ }
+
/**
* Reset this storage block.
*/
public void reset() {
location = -1;
+ binding = -1;
updateNeeded = true;
}
diff --git a/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferObject.java b/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferObject.java
index cb7a87b89d..f028f4cd1f 100644
--- a/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferObject.java
+++ b/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferObject.java
@@ -158,6 +158,7 @@ public void initializeEmpty(int length) {
BufferUtils.destroyDirectBuffer(data);
}
this.data = BufferUtils.createByteBuffer(length);
+ setUpdateNeeded();
}
@@ -167,11 +168,12 @@ public void initializeEmpty(int length) {
* @param data ByteBuffer containing the data to pass
*/
public void setData(ByteBuffer data) {
- if (data != null) {
- BufferUtils.destroyDirectBuffer(data);
+ if (this.data != null) {
+ BufferUtils.destroyDirectBuffer(this.data);
}
this.data = BufferUtils.createByteBuffer(data.limit() - data.position());
this.data.put(data);
+ setUpdateNeeded();
}
diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java
index 82e5a40394..8aeebedc78 100644
--- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java
+++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java
@@ -660,4 +660,14 @@ public void glBindBufferBase(final int target, final int index, final int buffer
public void glUniformBlockBinding(final int program, final int uniformBlockIndex, final int uniformBlockBinding) {
GL31.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding);
}
+
+ @Override
+ public int glGetActiveUniformBlocki(final int program, final int uniformBlockIndex, final int pname) {
+ return GL31.glGetActiveUniformBlocki(program, uniformBlockIndex, pname);
+ }
+
+ @Override
+ public void glGetProgramResourceiv(final int program, final int programInterface, final int index, IntBuffer props, IntBuffer length, IntBuffer params) {
+ GL43.glGetProgramResource(program, programInterface, index, props, length, params);
+ }
}
diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java
index f449d1c6b1..a9d5afb9d5 100644
--- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java
+++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java
@@ -698,5 +698,15 @@ public void glBindBufferBase(final int target, final int index, final int buffer
public void glUniformBlockBinding(final int program, final int uniformBlockIndex, final int uniformBlockBinding) {
GL31.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding);
}
-
+
+ @Override
+ public int glGetActiveUniformBlocki(final int program, final int uniformBlockIndex, final int pname) {
+ return GL31.glGetActiveUniformBlocki(program, uniformBlockIndex, pname);
+ }
+
+ @Override
+ public void glGetProgramResourceiv(final int program, final int programInterface, final int index, IntBuffer props, IntBuffer length, IntBuffer params) {
+ GL43.glGetProgramResourceiv(program, programInterface, index, props, length, params);
+ }
+
}
diff --git a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java
new file mode 100644
index 0000000000..97338fb5db
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2025 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.jmonkeyengine.screenshottests.ssbo;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Quad;
+import com.jme3.shader.bufferobject.BufferObject;
+import com.jme3.util.BufferUtils;
+import org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase;
+import org.jmonkeyengine.screenshottests.testframework.TestType;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.nio.ByteBuffer;
+import java.util.stream.Stream;
+
+/**
+ * Tests that SSBO binding points are correctly resolved when buffers are
+ * set via the material system. Each test variant uses a different combination
+ * of layout(binding=N) qualifiers in the fragment shader.
+ *
+ * Three SSBOs are created, each containing a vec4 color:
+ *
+ * - RedBlock: (1, 0, 0, 0)
+ * - GreenBlock: (0, 1, 0, 0)
+ * - BlueBlock: (0, 0, 1, 0)
+ *
+ * The shader reads redColor.r, greenColor.g, blueColor.b and outputs them
+ * as a single color. If all bindings are correct, the result is white.
+ */
+@SuppressWarnings("OptionalGetWithoutIsPresent")
+public class TestSSBOBinding extends ScreenshotTestBase {
+
+ private static Stream testParameters() {
+ return Stream.of(
+ Arguments.of("NoBindings", "TestSSBOBinding/SSBONoBindings.j3md", TestType.MUST_PASS),
+ Arguments.of("ExplicitBindings", "TestSSBOBinding/SSBOExplicitBindings.j3md", TestType.MUST_PASS),
+ Arguments.of("MixedBindings", "TestSSBOBinding/SSBOMixedBindings.j3md", TestType.MUST_PASS),
+ Arguments.of("Collision", "TestSSBOBinding/SSBOCollision.j3md", TestType.MUST_PASS)
+ );
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("testParameters")
+ public void testSSBOBinding(String testName, String matDefPath, TestType testType, TestInfo testInfo) {
+ String imageName = testInfo.getTestClass().get().getName() + "."
+ + testInfo.getTestMethod().get().getName() + "_" + testName;
+
+ screenshotTest(new BaseAppState() {
+ @Override
+ protected void initialize(Application app) {
+ SimpleApplication simpleApp = (SimpleApplication) app;
+
+ simpleApp.getCamera().setLocation(new Vector3f(0, 0, 1));
+ simpleApp.getCamera().lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
+ simpleApp.getViewPort().setBackgroundColor(ColorRGBA.Black);
+
+ Material mat = new Material(simpleApp.getAssetManager(), matDefPath);
+
+ mat.setShaderStorageBufferObject("RedBlock", createColorBuffer(1f, 0f, 0f, 0f));
+ mat.setShaderStorageBufferObject("GreenBlock", createColorBuffer(0f, 1f, 0f, 0f));
+ mat.setShaderStorageBufferObject("BlueBlock", createColorBuffer(0f, 0f, 1f, 0f));
+
+ Geometry quad = new Geometry("FullScreenQuad", new Quad(2, 2));
+ quad.setLocalTranslation(-1, -1, 0);
+ quad.setMaterial(mat);
+ simpleApp.getRootNode().attachChild(quad);
+ }
+
+ @Override
+ protected void cleanup(Application app) {}
+
+ @Override
+ protected void onEnable() {}
+
+ @Override
+ protected void onDisable() {}
+ })
+ .setBaseImageFileName(imageName)
+ .setTestType(testType)
+ .setFramesToTakeScreenshotsOn(1)
+ .run();
+ }
+
+ private static BufferObject createColorBuffer(float r, float g, float b, float a) {
+ BufferObject bo = new BufferObject();
+ ByteBuffer buf = BufferUtils.createByteBuffer(16); // vec4 = 4 floats
+ buf.putFloat(r).putFloat(g).putFloat(b).putFloat(a);
+ buf.flip();
+ bo.setData(buf);
+ return bo;
+ }
+}
diff --git a/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding.vert b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding.vert
new file mode 100644
index 0000000000..6bdec22867
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding.vert
@@ -0,0 +1,9 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+#import "Common/ShaderLib/Instancing.glsllib"
+
+in vec3 inPosition;
+
+void main(){
+ vec4 modelSpacePos = vec4(inPosition, 1.0);
+ gl_Position = TransformWorldViewProjection(modelSpacePos);
+}
diff --git a/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding0OnSecond.frag b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding0OnSecond.frag
new file mode 100644
index 0000000000..b01fba8ff4
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding0OnSecond.frag
@@ -0,0 +1,23 @@
+// Test: second block has explicit layout(binding=0).
+// This exposes the ambiguity: query returns 0 for both the first block
+// (no binding, default 0) and the second block (explicit binding=0).
+// The fix reassigns binding=0 to blockIndex when blockIndex != 0,
+// which incorrectly overrides the explicit binding=0 on GreenBlock.
+
+layout(std430) buffer m_RedBlock {
+ vec4 redColor;
+};
+
+layout(std430, binding=0) buffer m_GreenBlock {
+ vec4 greenColor;
+};
+
+layout(std430) buffer m_BlueBlock {
+ vec4 blueColor;
+};
+
+out vec4 fragColor;
+
+void main(){
+ fragColor = vec4(redColor.r, greenColor.g, blueColor.b, 1.0);
+}
diff --git a/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding0OnSecond.j3md b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding0OnSecond.j3md
new file mode 100644
index 0000000000..890a44e44c
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding0OnSecond.j3md
@@ -0,0 +1,17 @@
+MaterialDef SSBOBinding0OnSecond {
+
+ MaterialParameters {
+ ShaderStorageBufferObject RedBlock
+ ShaderStorageBufferObject GreenBlock
+ ShaderStorageBufferObject BlueBlock
+ }
+
+ Technique {
+ VertexShader GLSL430 : TestSSBOBinding/SSBOBinding.vert
+ FragmentShader GLSL430 : TestSSBOBinding/SSBOBinding0OnSecond.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ }
+ }
+}
diff --git a/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOCollision.frag b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOCollision.frag
new file mode 100644
index 0000000000..a9e4df49fb
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOCollision.frag
@@ -0,0 +1,23 @@
+// Test: collision scenario — demonstrates the binding bug.
+// GreenBlock has no binding (blockIndex=1, query=0, reassigned to 1).
+// BlueBlock has explicit binding=1 (query=1, kept at 1).
+// Both end up at binding point 1: the last buffer bound wins,
+// so GreenBlock reads BlueBlock's data and green is lost.
+
+layout(std430) buffer m_RedBlock {
+ vec4 redColor;
+};
+
+layout(std430) buffer m_GreenBlock {
+ vec4 greenColor;
+};
+
+layout(std430, binding=1) buffer m_BlueBlock {
+ vec4 blueColor;
+};
+
+out vec4 fragColor;
+
+void main(){
+ fragColor = vec4(redColor.r, greenColor.g, blueColor.b, 1.0);
+}
diff --git a/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOCollision.j3md b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOCollision.j3md
new file mode 100644
index 0000000000..5f4d606033
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOCollision.j3md
@@ -0,0 +1,17 @@
+MaterialDef SSBOCollision {
+
+ MaterialParameters {
+ ShaderStorageBufferObject RedBlock
+ ShaderStorageBufferObject GreenBlock
+ ShaderStorageBufferObject BlueBlock
+ }
+
+ Technique {
+ VertexShader GLSL430 : TestSSBOBinding/SSBOBinding.vert
+ FragmentShader GLSL430 : TestSSBOBinding/SSBOCollision.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ }
+ }
+}
diff --git a/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOExplicitBindings.frag b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOExplicitBindings.frag
new file mode 100644
index 0000000000..5faa4813f5
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOExplicitBindings.frag
@@ -0,0 +1,20 @@
+// Test: all blocks have explicit non-zero bindings.
+// Query returns non-zero for all, so all bindings are respected as-is.
+
+layout(std430, binding=1) buffer m_RedBlock {
+ vec4 redColor;
+};
+
+layout(std430, binding=2) buffer m_GreenBlock {
+ vec4 greenColor;
+};
+
+layout(std430, binding=3) buffer m_BlueBlock {
+ vec4 blueColor;
+};
+
+out vec4 fragColor;
+
+void main(){
+ fragColor = vec4(redColor.r, greenColor.g, blueColor.b, 1.0);
+}
diff --git a/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOExplicitBindings.j3md b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOExplicitBindings.j3md
new file mode 100644
index 0000000000..b42681c8ed
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOExplicitBindings.j3md
@@ -0,0 +1,17 @@
+MaterialDef SSBOExplicitBindings {
+
+ MaterialParameters {
+ ShaderStorageBufferObject RedBlock
+ ShaderStorageBufferObject GreenBlock
+ ShaderStorageBufferObject BlueBlock
+ }
+
+ Technique {
+ VertexShader GLSL430 : TestSSBOBinding/SSBOBinding.vert
+ FragmentShader GLSL430 : TestSSBOBinding/SSBOExplicitBindings.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ }
+ }
+}
diff --git a/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOMixedBindings.frag b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOMixedBindings.frag
new file mode 100644
index 0000000000..bc301b48de
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOMixedBindings.frag
@@ -0,0 +1,21 @@
+// Test: mixed explicit and implicit bindings, all non-zero explicit.
+// RedBlock has binding=1, GreenBlock has binding=2, BlueBlock has none.
+// Non-zero queries are respected; BlueBlock gets assigned its blockIndex.
+
+layout(std430, binding=1) buffer m_RedBlock {
+ vec4 redColor;
+};
+
+layout(std430, binding=2) buffer m_GreenBlock {
+ vec4 greenColor;
+};
+
+layout(std430) buffer m_BlueBlock {
+ vec4 blueColor;
+};
+
+out vec4 fragColor;
+
+void main(){
+ fragColor = vec4(redColor.r, greenColor.g, blueColor.b, 1.0);
+}
diff --git a/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOMixedBindings.j3md b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOMixedBindings.j3md
new file mode 100644
index 0000000000..3f9e5ce953
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOMixedBindings.j3md
@@ -0,0 +1,17 @@
+MaterialDef SSBOMixedBindings {
+
+ MaterialParameters {
+ ShaderStorageBufferObject RedBlock
+ ShaderStorageBufferObject GreenBlock
+ ShaderStorageBufferObject BlueBlock
+ }
+
+ Technique {
+ VertexShader GLSL430 : TestSSBOBinding/SSBOBinding.vert
+ FragmentShader GLSL430 : TestSSBOBinding/SSBOMixedBindings.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ }
+ }
+}
diff --git a/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBONoBindings.frag b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBONoBindings.frag
new file mode 100644
index 0000000000..4b02c572be
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBONoBindings.frag
@@ -0,0 +1,21 @@
+// Test: no explicit binding on any block.
+// All blocks get unique blockIndex values, query returns 0 for all,
+// and each is assigned its blockIndex as binding point. No collisions.
+
+layout(std430) buffer m_RedBlock {
+ vec4 redColor;
+};
+
+layout(std430) buffer m_GreenBlock {
+ vec4 greenColor;
+};
+
+layout(std430) buffer m_BlueBlock {
+ vec4 blueColor;
+};
+
+out vec4 fragColor;
+
+void main(){
+ fragColor = vec4(redColor.r, greenColor.g, blueColor.b, 1.0);
+}
diff --git a/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBONoBindings.j3md b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBONoBindings.j3md
new file mode 100644
index 0000000000..72407a5aaf
--- /dev/null
+++ b/jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBONoBindings.j3md
@@ -0,0 +1,17 @@
+MaterialDef SSBONoBindings {
+
+ MaterialParameters {
+ ShaderStorageBufferObject RedBlock
+ ShaderStorageBufferObject GreenBlock
+ ShaderStorageBufferObject BlueBlock
+ }
+
+ Technique {
+ VertexShader GLSL430 : TestSSBOBinding/SSBOBinding.vert
+ FragmentShader GLSL430 : TestSSBOBinding/SSBONoBindings.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ }
+ }
+}
diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Binding0OnSecond_f1.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Binding0OnSecond_f1.png
new file mode 100644
index 0000000000..dd3c768e9b
Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Binding0OnSecond_f1.png differ
diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Collision_f1.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Collision_f1.png
new file mode 100644
index 0000000000..dd3c768e9b
Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Collision_f1.png differ
diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_ExplicitBindings_f1.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_ExplicitBindings_f1.png
new file mode 100644
index 0000000000..dd3c768e9b
Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_ExplicitBindings_f1.png differ
diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_MixedBindings_f1.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_MixedBindings_f1.png
new file mode 100644
index 0000000000..dd3c768e9b
Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_MixedBindings_f1.png differ
diff --git a/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_NoBindings_f1.png b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_NoBindings_f1.png
new file mode 100644
index 0000000000..dd3c768e9b
Binary files /dev/null and b/jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_NoBindings_f1.png differ