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