From b1cbcc7ca6f1fba704ead99d2c49e4666240aee2 Mon Sep 17 00:00:00 2001 From: zzuegg Date: Sat, 21 Mar 2026 00:34:08 +0100 Subject: [PATCH 01/15] fix: correct SSBO/UBO binding point resolution in material system The material system was using wrong binding points for shader storage and uniform buffer objects, causing data to be bound to incorrect slots. - GLRenderer.updateShaderBufferBlock() now queries the actual binding from the compiled shader (glGetActiveUniformBlocki for UBOs, glGetProgramResourceiv for SSBOs) and respects layout(binding=N). Falls back to blockIndex when no explicit binding is declared. - Remove premature renderer.setShaderStorageBufferObject/ setUniformBufferObject calls from Material.updateShaderMaterialParameter that used a sequential counter as binding point. - Cache binding on ShaderBufferBlock (per-shader) instead of BufferObject (per-buffer), since the same buffer can be bound at different points in different shaders. - Add glGetActiveUniformBlocki to GL3 and glGetProgramResourceiv + GL_BUFFER_BINDING to GL4 with implementations in both LWJGL backends. - BufferObject.initializeEmpty() now calls setUpdateNeeded() so the GPU buffer gets allocated on first use. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../main/java/com/jme3/material/Material.java | 6 +- .../java/com/jme3/renderer/opengl/GL3.java | 16 ++++ .../java/com/jme3/renderer/opengl/GL4.java | 24 +++++- .../com/jme3/renderer/opengl/GLRenderer.java | 80 +++++++++++++------ .../com/jme3/shader/ShaderBufferBlock.java | 24 ++++++ .../shader/bufferobject/BufferObject.java | 1 + .../java/com/jme3/renderer/lwjgl/LwjglGL.java | 10 +++ .../java/com/jme3/renderer/lwjgl/LwjglGL.java | 12 ++- 8 files changed, 142 insertions(+), 31 deletions(-) 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..341ab0e593 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) { @@ -1473,35 +1472,52 @@ protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBl final int shaderId = shader.getId(); - int bindingPoint = bufferObject.getBinding(); + // Resolve the block index (location) from the compiled shader + int blockIndex = bufferBlock.getLocation(); + if (blockIndex < 0) { + if (bufferType == BufferType.ShaderStorageBufferObject) { + blockIndex = gl4.glGetProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName()); + } else { + blockIndex = gl3.glGetUniformBlockIndex(shaderId, bufferBlock.getName()); + } + bufferBlock.setLocation(blockIndex); + } + + // Block not found in the shader — skip silently + if (blockIndex < 0 || blockIndex == NativeObject.INVALID_ID) { + bufferBlock.clearUpdateNeeded(); + return; + } + + // Resolve the binding point for this block. First try to read the + // binding declared in the shader (layout(binding=N)). If the query + // returns 0 the shader may not have an explicit binding, so fall + // back to blockIndex to avoid multiple blocks colliding on point 0. + int bindingPoint = bufferBlock.getBinding(); + if (bindingPoint < 0) { + if (bufferType == BufferType.ShaderStorageBufferObject) { + bindingPoint = queryShaderStorageBlockBinding(shaderId, blockIndex); + } else { + bindingPoint = gl3.glGetActiveUniformBlocki(shaderId, blockIndex, GL3.GL_UNIFORM_BLOCK_BINDING); + } + if (bindingPoint == 0) { + bindingPoint = blockIndex; + } + if (bufferType == BufferType.ShaderStorageBufferObject) { + gl4.glShaderStorageBlockBinding(shaderId, blockIndex, bindingPoint); + } else { + gl3.glUniformBlockBinding(shaderId, blockIndex, bindingPoint); + } + bufferBlock.setBinding(bindingPoint); + } 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 +1528,22 @@ protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBl bufferBlock.clearUpdateNeeded(); } + /** + * 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) { + IntBuffer props = BufferUtils.createIntBuffer(1); + props.put(GL4.GL_BUFFER_BINDING).flip(); + IntBuffer params = BufferUtils.createIntBuffer(1); + gl4.glGetProgramResourceiv(program, GL4.GL_SHADER_STORAGE_BLOCK, blockIndex, props, null, params); + return params.get(0); + } + protected void updateShaderUniforms(Shader shader) { ListMap uniforms = shader.getUniformMap(); for (int i = 0; i < uniforms.size(); i++) { 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..bac97ff244 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(); } 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); + } + } From 0a76df2ea5116bad7add7dd84c3a91f1ce669f35 Mon Sep 17 00:00:00 2001 From: zzuegg Date: Sat, 21 Mar 2026 09:10:32 +0100 Subject: [PATCH 02/15] test: add SSBO binding screenshot tests with red test for collision bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 5 parameterized screenshot tests that verify SSBO binding point resolution through the material system. Each test creates 3 SSBOs (red, green, blue) and checks that the shader output is white. - NoBindings: no explicit layout(binding=N) on any block - ExplicitBindings: all blocks have explicit non-zero bindings - Binding0OnSecond: second block has layout(binding=0) - MixedBindings: mix of explicit non-zero and implicit bindings - Collision (KNOWN_TO_FAIL): unbound block at blockIndex=1 collides with another block's explicit layout(binding=1), producing magenta instead of white — demonstrates the binding point ambiguity bug Also fixes BufferObject.setData() which was destroying the input parameter instead of this.data and was not calling setUpdateNeeded(). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../shader/bufferobject/BufferObject.java | 5 +- .../screenshottests/ssbo/TestSSBOBinding.java | 131 ++++++++++++++++++ .../TestSSBOBinding/SSBOBinding.vert | 9 ++ .../TestSSBOBinding/SSBOBinding0OnSecond.frag | 23 +++ .../TestSSBOBinding/SSBOBinding0OnSecond.j3md | 17 +++ .../TestSSBOBinding/SSBOCollision.frag | 23 +++ .../TestSSBOBinding/SSBOCollision.j3md | 17 +++ .../TestSSBOBinding/SSBOExplicitBindings.frag | 20 +++ .../TestSSBOBinding/SSBOExplicitBindings.j3md | 17 +++ .../TestSSBOBinding/SSBOMixedBindings.frag | 21 +++ .../TestSSBOBinding/SSBOMixedBindings.j3md | 17 +++ .../TestSSBOBinding/SSBONoBindings.frag | 21 +++ .../TestSSBOBinding/SSBONoBindings.j3md | 17 +++ ...ng.testSSBOBinding_Binding0OnSecond_f1.png | Bin 0 -> 1684 bytes ...BOBinding.testSSBOBinding_Collision_f1.png | Bin 0 -> 1684 bytes ...ng.testSSBOBinding_ExplicitBindings_f1.png | Bin 0 -> 1684 bytes ...nding.testSSBOBinding_MixedBindings_f1.png | Bin 0 -> 1684 bytes ...OBinding.testSSBOBinding_NoBindings_f1.png | Bin 0 -> 1684 bytes 18 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java create mode 100644 jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding.vert create mode 100644 jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding0OnSecond.frag create mode 100644 jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOBinding0OnSecond.j3md create mode 100644 jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOCollision.frag create mode 100644 jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOCollision.j3md create mode 100644 jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOExplicitBindings.frag create mode 100644 jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOExplicitBindings.j3md create mode 100644 jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOMixedBindings.frag create mode 100644 jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBOMixedBindings.j3md create mode 100644 jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBONoBindings.frag create mode 100644 jme3-screenshot-tests/src/test/resources/TestSSBOBinding/SSBONoBindings.j3md create mode 100644 jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Binding0OnSecond_f1.png create mode 100644 jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Collision_f1.png create mode 100644 jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_ExplicitBindings_f1.png create mode 100644 jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_MixedBindings_f1.png create mode 100644 jme3-screenshot-tests/src/test/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_NoBindings_f1.png 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 bac97ff244..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 @@ -168,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-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..8facd7bac7 --- /dev/null +++ b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java @@ -0,0 +1,131 @@ +/* + * 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("Binding0OnSecond", "TestSSBOBinding/SSBOBinding0OnSecond.j3md", TestType.MUST_PASS), + Arguments.of("MixedBindings", "TestSSBOBinding/SSBOMixedBindings.j3md", TestType.MUST_PASS), + Arguments.of("Collision", "TestSSBOBinding/SSBOCollision.j3md", TestType.KNOWN_TO_FAIL) + ); + } + + @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 0000000000000000000000000000000000000000..dd3c768e9bda3699ca3d05dfd0acd825487b5ebb GIT binary patch literal 1684 zcmeAS@N?(olHy`uVBq!ia0y~yVEh8a6FAs_A`zP>9b;f%mGX3P45_&F_NpN-0|U?D z4WHESUb>+o?KE!*!-49FKp|NMgA#@V7Z@3w*+B@-hYCUX2qi#1TnV+c0IeJVTT%TM z%Y+mjwDc^hwYXw|+Cfa5t!S~p2}>XJObobPI`FY_;DRc{KxY<|@BwmA_%zF+cx;(I yv2i3o!Reoz3GZx+cy4nP2`O=(pC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dd3c768e9bda3699ca3d05dfd0acd825487b5ebb GIT binary patch literal 1684 zcmeAS@N?(olHy`uVBq!ia0y~yVEh8a6FAs_A`zP>9b;f%mGX3P45_&F_NpN-0|U?D z4WHESUb>+o?KE!*!-49FKp|NMgA#@V7Z@3w*+B@-hYCUX2qi#1TnV+c0IeJVTT%TM z%Y+mjwDc^hwYXw|+Cfa5t!S~p2}>XJObobPI`FY_;DRc{KxY<|@BwmA_%zF+cx;(I yv2i3o!Reoz3GZx+cy4nP2`O=(pC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dd3c768e9bda3699ca3d05dfd0acd825487b5ebb GIT binary patch literal 1684 zcmeAS@N?(olHy`uVBq!ia0y~yVEh8a6FAs_A`zP>9b;f%mGX3P45_&F_NpN-0|U?D z4WHESUb>+o?KE!*!-49FKp|NMgA#@V7Z@3w*+B@-hYCUX2qi#1TnV+c0IeJVTT%TM z%Y+mjwDc^hwYXw|+Cfa5t!S~p2}>XJObobPI`FY_;DRc{KxY<|@BwmA_%zF+cx;(I yv2i3o!Reoz3GZx+cy4nP2`O=(pC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dd3c768e9bda3699ca3d05dfd0acd825487b5ebb GIT binary patch literal 1684 zcmeAS@N?(olHy`uVBq!ia0y~yVEh8a6FAs_A`zP>9b;f%mGX3P45_&F_NpN-0|U?D z4WHESUb>+o?KE!*!-49FKp|NMgA#@V7Z@3w*+B@-hYCUX2qi#1TnV+c0IeJVTT%TM z%Y+mjwDc^hwYXw|+Cfa5t!S~p2}>XJObobPI`FY_;DRc{KxY<|@BwmA_%zF+cx;(I yv2i3o!Reoz3GZx+cy4nP2`O=(pC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dd3c768e9bda3699ca3d05dfd0acd825487b5ebb GIT binary patch literal 1684 zcmeAS@N?(olHy`uVBq!ia0y~yVEh8a6FAs_A`zP>9b;f%mGX3P45_&F_NpN-0|U?D z4WHESUb>+o?KE!*!-49FKp|NMgA#@V7Z@3w*+B@-hYCUX2qi#1TnV+c0IeJVTT%TM z%Y+mjwDc^hwYXw|+Cfa5t!S~p2}>XJObobPI`FY_;DRc{KxY<|@BwmA_%zF+cx;(I yv2i3o!Reoz3GZx+cy4nP2`O=(pC literal 0 HcmV?d00001 From 775d76987e004277ad087e281f4a9bdb38e730d1 Mon Sep 17 00:00:00 2001 From: zzuegg Date: Sat, 21 Mar 2026 09:21:39 +0100 Subject: [PATCH 03/15] fix(renderer): resolve SSBO/UBO binding collisions via two-pass detection Replaces the per-block binding resolution with a two-pass approach in resolveBufferBlockBindings() that runs once per shader program: Pass 1: query all block bindings from the compiled shader. Pass 2: detect duplicate binding points and reassign colliding blocks to unique free binding points. This fixes the case where an unbound block's default binding (0) collides with another block's explicit layout(binding=N). The collision test now passes as MUST_PASS instead of KNOWN_TO_FAIL. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/jme3/renderer/opengl/GLRenderer.java | 123 ++++++++++++------ .../screenshottests/ssbo/TestSSBOBinding.java | 2 +- 2 files changed, 85 insertions(+), 40 deletions(-) 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 341ab0e593..37244fd238 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 @@ -1468,47 +1468,11 @@ 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(); - - // Resolve the block index (location) from the compiled shader - int blockIndex = bufferBlock.getLocation(); - if (blockIndex < 0) { - if (bufferType == BufferType.ShaderStorageBufferObject) { - blockIndex = gl4.glGetProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName()); - } else { - blockIndex = gl3.glGetUniformBlockIndex(shaderId, bufferBlock.getName()); - } - bufferBlock.setLocation(blockIndex); - } - - // Block not found in the shader — skip silently - if (blockIndex < 0 || blockIndex == NativeObject.INVALID_ID) { - bufferBlock.clearUpdateNeeded(); - return; - } - - // Resolve the binding point for this block. First try to read the - // binding declared in the shader (layout(binding=N)). If the query - // returns 0 the shader may not have an explicit binding, so fall - // back to blockIndex to avoid multiple blocks colliding on point 0. int bindingPoint = bufferBlock.getBinding(); if (bindingPoint < 0) { - if (bufferType == BufferType.ShaderStorageBufferObject) { - bindingPoint = queryShaderStorageBlockBinding(shaderId, blockIndex); - } else { - bindingPoint = gl3.glGetActiveUniformBlocki(shaderId, blockIndex, GL3.GL_UNIFORM_BLOCK_BINDING); - } - if (bindingPoint == 0) { - bindingPoint = blockIndex; - } - if (bufferType == BufferType.ShaderStorageBufferObject) { - gl4.glShaderStorageBlockBinding(shaderId, blockIndex, bindingPoint); - } else { - gl3.glUniformBlockBinding(shaderId, blockIndex, bindingPoint); - } - bufferBlock.setBinding(bindingPoint); + // Binding not yet resolved — skip until resolveBufferBlockBindings runs + bufferBlock.clearUpdateNeeded(); + return; } switch (bufferType) { @@ -1528,6 +1492,82 @@ 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 + Set usedBindings = new HashSet<>(); + int nextFree = 0; + + for (int i = 0; i < bufferBlocks.size(); i++) { + ShaderBufferBlock block = bufferBlocks.getValue(i); + int binding = block.getBinding(); + if (binding < 0) continue; + + if (!usedBindings.add(binding)) { + // Collision — find a free binding point + while (usedBindings.contains(nextFree)) { + nextFree++; + } + binding = nextFree; + usedBindings.add(binding); + block.setBinding(binding); + } + + // Set the binding on the shader program + int blockIndex = block.getLocation(); + BufferType bufferType = block.getType(); + 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. @@ -1561,6 +1601,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)); } 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 index 8facd7bac7..21b675c937 100644 --- 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 @@ -74,7 +74,7 @@ private static Stream testParameters() { Arguments.of("ExplicitBindings", "TestSSBOBinding/SSBOExplicitBindings.j3md", TestType.MUST_PASS), Arguments.of("Binding0OnSecond", "TestSSBOBinding/SSBOBinding0OnSecond.j3md", TestType.MUST_PASS), Arguments.of("MixedBindings", "TestSSBOBinding/SSBOMixedBindings.j3md", TestType.MUST_PASS), - Arguments.of("Collision", "TestSSBOBinding/SSBOCollision.j3md", TestType.KNOWN_TO_FAIL) + Arguments.of("Collision", "TestSSBOBinding/SSBOCollision.j3md", TestType.MUST_PASS) ); } From d706616c20c9a02e2dace40acdf281acd279ff6b Mon Sep 17 00:00:00 2001 From: zzuegg Date: Sat, 21 Mar 2026 09:24:16 +0100 Subject: [PATCH 04/15] fix(renderer): track UBO and SSBO binding namespaces separately UBOs and SSBOs use separate GL binding namespaces (GL_UNIFORM_BUFFER vs GL_SHADER_STORAGE_BUFFER), so a UBO at binding 0 does not collide with an SSBO at binding 0. Track them independently in the collision detection pass to avoid unnecessary reassignments. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/jme3/renderer/opengl/GLRenderer.java | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) 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 37244fd238..e7c8171b9b 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 @@ -1538,28 +1538,41 @@ private void resolveBufferBlockBindings(final Shader shader) { block.setBinding(binding); } - // Pass 2: detect and resolve collisions - Set usedBindings = new HashSet<>(); - int nextFree = 0; + // 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 — find a free binding point - while (usedBindings.contains(nextFree)) { - nextFree++; + // 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; } - binding = nextFree; usedBindings.add(binding); block.setBinding(binding); } // Set the binding on the shader program int blockIndex = block.getLocation(); - BufferType bufferType = block.getType(); if (bufferType == BufferType.ShaderStorageBufferObject) { gl4.glShaderStorageBlockBinding(shaderId, blockIndex, binding); } else { From efbb28086757c8b5526fbd7c9d49ce56d446206f Mon Sep 17 00:00:00 2001 From: zzuegg Date: Sat, 21 Mar 2026 09:26:53 +0100 Subject: [PATCH 05/15] fix(renderer): error on layout(binding=0) on non-first buffer block Adds compile-time validation that rejects shaders using layout(binding=0) on a buffer block that is not the first declared block. The GL query cannot distinguish explicit binding=0 from the default, so this case would silently produce incorrect bindings. The error message tells the user to use a non-zero binding or declare the block first in the shader. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/jme3/renderer/opengl/GLRenderer.java | 45 ++++++++++++++++++- .../screenshottests/ssbo/TestSSBOBinding.java | 1 - 2 files changed, 44 insertions(+), 2 deletions(-) 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 e7c8171b9b..f8661ce3e0 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 @@ -1649,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) { @@ -1713,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-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java b/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java index 21b675c937..97338fb5db 100644 --- 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 @@ -72,7 +72,6 @@ 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("Binding0OnSecond", "TestSSBOBinding/SSBOBinding0OnSecond.j3md", TestType.MUST_PASS), Arguments.of("MixedBindings", "TestSSBOBinding/SSBOMixedBindings.j3md", TestType.MUST_PASS), Arguments.of("Collision", "TestSSBOBinding/SSBOCollision.j3md", TestType.MUST_PASS) ); From 3f5c986b1492053ffdf41fa7909b26335659589d Mon Sep 17 00:00:00 2001 From: zzuegg Date: Sat, 21 Mar 2026 10:39:23 +0100 Subject: [PATCH 06/15] refactor(renderer): reuse existing IntBuffer fields in queryShaderStorageBlockBinding Use the renderer's intBuf1 and intBuf16 instance fields instead of allocating new IntBuffers on each call. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../main/java/com/jme3/renderer/opengl/GLRenderer.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 f8661ce3e0..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 @@ -1590,11 +1590,11 @@ private void resolveBufferBlockBindings(final Shader shader) { * @return the binding point assigned to the block. */ private int queryShaderStorageBlockBinding(int program, int blockIndex) { - IntBuffer props = BufferUtils.createIntBuffer(1); - props.put(GL4.GL_BUFFER_BINDING).flip(); - IntBuffer params = BufferUtils.createIntBuffer(1); - gl4.glGetProgramResourceiv(program, GL4.GL_SHADER_STORAGE_BLOCK, blockIndex, props, null, params); - return params.get(0); + 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) { From c47f325f0fb7860a10819e3ac71a7cccb290b276 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 9 Jun 2026 21:20:10 +0200 Subject: [PATCH 07/15] glGetActiveUniformBlocki and glGetProgramResourceiv methods for OpenGL ES backends --- .../com/jme3/renderer/android/AndroidGL.java | 12 +++++++++ .../java/com/jme3/renderer/opengl/GLExt.java | 26 +++++++++++++++++++ .../java/com/jme3/renderer/ios/IosGL.java | 11 ++++++++ .../com/jme3/renderer/lwjgl/LwjglGLExt.java | 10 +++++++ .../com/jme3/renderer/lwjgl/LwjglGLES.java | 10 +++++++ .../com/jme3/renderer/lwjgl/LwjglGLExt.java | 10 +++++++ 6 files changed, 79 insertions(+) diff --git a/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java b/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java index 896520c32e..5ab310a4a4 100644 --- a/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java +++ b/jme3-android/src/main/java/com/jme3/renderer/android/AndroidGL.java @@ -706,11 +706,23 @@ public void glUniformBlockBinding(int program, int uniformBlockIndex, int unifor GLES30.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetActiveUniformBlocki(int program, int uniformBlockIndex, int pname) { + IntBuffer buff = (IntBuffer) tmpBuff.clear(); + GLES30.glGetActiveUniformBlockiv(program, uniformBlockIndex, pname, buff); + return buff.get(0); + } + @Override public int glGetProgramResourceIndex(int program, int programInterface, String name) { throw new UnsupportedOperationException("Shader storage buffer objects require OpenGL ES 3.1"); } + @Override + public void glGetProgramResourceiv(int program, int programInterface, int index, IntBuffer props, IntBuffer length, IntBuffer params) { + throw new UnsupportedOperationException("Shader storage buffer objects require OpenGL ES 3.1"); + } + @Override public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { throw new UnsupportedOperationException("Shader storage buffer objects require OpenGL ES 3.1"); diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java index 2db094c437..663af63250 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLExt.java @@ -293,6 +293,18 @@ public default void glUniformBlockBinding(int program, int uniformBlockIndex, in throw new UnsupportedOperationException("Uniform buffer objects are not supported"); } + /** + * 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 parameter to query + * @return the queried parameter value + */ + public default int glGetActiveUniformBlocki(int program, int uniformBlockIndex, int pname) { + throw new UnsupportedOperationException("Uniform buffer objects are not supported"); + } + /** * Retrieves the index of a named program resource. * @@ -305,6 +317,20 @@ public default int glGetProgramResourceIndex(int program, int programInterface, throw new UnsupportedOperationException("Shader storage buffer objects are not supported"); } + /** + * Retrieves values for properties of an active program resource. + * + * @param program the name of a program object + * @param programInterface the program interface containing the resource + * @param index the active resource index + * @param props properties to query + * @param length receives the number of values written to params + * @param params receives the queried values + */ + public default void glGetProgramResourceiv(int program, int programInterface, int index, IntBuffer props, IntBuffer length, IntBuffer params) { + throw new UnsupportedOperationException("Shader storage buffer objects are not supported"); + } + /** * Assigns a shader storage block to a binding point. * diff --git a/jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java b/jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java index 751ade8479..e0086a4494 100644 --- a/jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java +++ b/jme3-ios/src/main/java/com/jme3/renderer/ios/IosGL.java @@ -872,11 +872,22 @@ public void glUniformBlockBinding(int program, int uniformBlockIndex, int unifor GLES.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetActiveUniformBlocki(int program, int uniformBlockIndex, int pname) { + GLES.glGetActiveUniformBlockiv(program, uniformBlockIndex, pname, tempArray); + return tempArray[0]; + } + @Override public int glGetProgramResourceIndex(int program, int programInterface, String name) { throw new UnsupportedOperationException("Shader storage buffer objects require OpenGL ES 3.1"); } + @Override + public void glGetProgramResourceiv(int program, int programInterface, int index, IntBuffer props, IntBuffer length, IntBuffer params) { + throw new UnsupportedOperationException("Shader storage buffer objects require OpenGL ES 3.1"); + } + @Override public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { throw new UnsupportedOperationException("Shader storage buffer objects require OpenGL ES 3.1"); diff --git a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java index 4d132618cd..53205e9a15 100644 --- a/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java +++ b/jme3-lwjgl/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -89,11 +89,21 @@ public void glUniformBlockBinding(int program, int uniformBlockIndex, int unifor ARBUniformBufferObject.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetActiveUniformBlocki(int program, int uniformBlockIndex, int pname) { + return ARBUniformBufferObject.glGetActiveUniformBlocki(program, uniformBlockIndex, pname); + } + @Override public int glGetProgramResourceIndex(int program, int programInterface, String name) { return ARBProgramInterfaceQuery.glGetProgramResourceIndex(program, programInterface, name); } + @Override + public void glGetProgramResourceiv(int program, int programInterface, int index, IntBuffer props, IntBuffer length, IntBuffer params) { + ARBProgramInterfaceQuery.glGetProgramResource(program, programInterface, index, props, length, params); + } + @Override public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { ARBShaderStorageBufferObject.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLES.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLES.java index a5c21f0676..347bdb8b19 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLES.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLES.java @@ -627,11 +627,21 @@ public void glUniformBlockBinding(int program, int uniformBlockIndex, int unifor GLES30.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetActiveUniformBlocki(int program, int uniformBlockIndex, int pname) { + return GLES30.glGetActiveUniformBlocki(program, uniformBlockIndex, pname); + } + @Override public int glGetProgramResourceIndex(int program, int programInterface, String name) { throw new UnsupportedOperationException("Shader storage buffer objects require OpenGL ES 3.1"); } + @Override + public void glGetProgramResourceiv(int program, int programInterface, int index, IntBuffer props, IntBuffer length, IntBuffer params) { + throw new UnsupportedOperationException("Shader storage buffer objects require OpenGL ES 3.1"); + } + @Override public void glShaderStorageBlockBinding(int program, int storageBlockIndex, int storageBlockBinding) { throw new UnsupportedOperationException("Shader storage buffer objects require OpenGL ES 3.1"); diff --git a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java index 83836aa3da..34bfbd518b 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGLExt.java @@ -103,11 +103,21 @@ public void glUniformBlockBinding(final int program, final int uniformBlockIndex ARBUniformBufferObject.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); } + @Override + public int glGetActiveUniformBlocki(final int program, final int uniformBlockIndex, final int pname) { + return ARBUniformBufferObject.glGetActiveUniformBlocki(program, uniformBlockIndex, pname); + } + @Override public int glGetProgramResourceIndex(final int program, final int programInterface, final String name) { return ARBProgramInterfaceQuery.glGetProgramResourceIndex(program, programInterface, name); } + @Override + public void glGetProgramResourceiv(final int program, final int programInterface, final int index, IntBuffer props, IntBuffer length, IntBuffer params) { + ARBProgramInterfaceQuery.glGetProgramResourceiv(program, programInterface, index, props, length, params); + } + @Override public void glShaderStorageBlockBinding(final int program, final int storageBlockIndex, final int storageBlockBinding) { ARBShaderStorageBufferObject.glShaderStorageBlockBinding(program, storageBlockIndex, storageBlockBinding); From 7fa1af700d12e6c5f14c502b4297faa8b18db890 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 9 Jun 2026 22:22:16 +0200 Subject: [PATCH 08/15] add std430 layout --- .../bufferobject/layout/BufferLayout.java | 10 + .../bufferobject/layout/Std140Layout.java | 5 + .../bufferobject/layout/Std430Layout.java | 222 ++++++++++++++++++ .../util/struct/StructStd430BufferObject.java | 106 +++++++++ .../com/jme3/util/struct/StructUtils.java | 13 +- .../test/java/com/jme3/util/StructTest.java | 96 ++++++++ 6 files changed, 449 insertions(+), 3 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/Std430Layout.java create mode 100644 jme3-core/src/main/java/com/jme3/util/struct/StructStd430BufferObject.java diff --git a/jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/BufferLayout.java b/jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/BufferLayout.java index 265aa8f563..396047c26b 100644 --- a/jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/BufferLayout.java +++ b/jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/BufferLayout.java @@ -127,6 +127,16 @@ public int align(int pos, int basicAlignment) { return pos==0?pos:FastMath.alignToPowerOfTwo(pos, basicAlignment); } + /** + * Returns the alignment used for the end of a structure. + * + * @param maxMemberAlignment largest member alignment in the structure + * @return structure alignment + */ + public int getStructureAlignment(int maxMemberAlignment) { + return maxMemberAlignment; + } + /** * Serialize an object accordingly with the std140 layout and write the * result to a BufferObject diff --git a/jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/Std140Layout.java b/jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/Std140Layout.java index 62aa54e0fb..cd4ea303cc 100644 --- a/jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/Std140Layout.java +++ b/jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/Std140Layout.java @@ -607,6 +607,11 @@ public void write(BufferLayout serializer, ByteBuffer bbf, Matrix4f[] objs) { } + @Override + public int getStructureAlignment(int maxMemberAlignment) { + return Math.max(16, maxMemberAlignment); + } + @Override public String getId() { return "std140"; diff --git a/jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/Std430Layout.java b/jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/Std430Layout.java new file mode 100644 index 0000000000..3a40790964 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/shader/bufferobject/layout/Std430Layout.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2026 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 com.jme3.shader.bufferobject.layout; + +import com.jme3.math.ColorRGBA; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; +import com.jme3.math.Vector4f; +import java.nio.ByteBuffer; + +/** + * Serializer that respects the Std430 layout. + */ +public class Std430Layout extends Std140Layout { + + public Std430Layout() { + registerSerializer(new ObjectSerializer(Integer[].class) { + @Override + public int length(BufferLayout serializer, Integer[] obj) { + return 4 * obj.length; + } + + @Override + public int basicAlignment(BufferLayout serializer, Integer[] obj) { + return 4; + } + + @Override + public void write(BufferLayout serializer, ByteBuffer bbf, Integer[] obj) { + for (int i : obj) { + bbf.putInt(i); + } + } + }); + + registerSerializer(new ObjectSerializer(Float[].class) { + @Override + public int length(BufferLayout serializer, Float[] obj) { + return 4 * obj.length; + } + + @Override + public int basicAlignment(BufferLayout serializer, Float[] obj) { + return 4; + } + + @Override + public void write(BufferLayout serializer, ByteBuffer bbf, Float[] obj) { + for (float i : obj) { + bbf.putFloat(i); + } + } + }); + + registerSerializer(new ObjectSerializer(Boolean[].class) { + @Override + public int length(BufferLayout serializer, Boolean[] obj) { + return 4 * obj.length; + } + + @Override + public int basicAlignment(BufferLayout serializer, Boolean[] obj) { + return 4; + } + + @Override + public void write(BufferLayout serializer, ByteBuffer bbf, Boolean[] obj) { + for (boolean i : obj) { + bbf.putInt(i ? 1 : 0); + } + } + }); + + registerSerializer(new ObjectSerializer(Vector2f[].class) { + @Override + public int length(BufferLayout serializer, Vector2f[] obj) { + return 8 * obj.length; + } + + @Override + public int basicAlignment(BufferLayout serializer, Vector2f[] obj) { + return 8; + } + + @Override + public void write(BufferLayout serializer, ByteBuffer bbf, Vector2f[] obj) { + for (Vector2f i : obj) { + bbf.putFloat(i.x); + bbf.putFloat(i.y); + } + } + }); + + registerSerializer(new ObjectSerializer(Vector3f[].class) { + @Override + public int length(BufferLayout serializer, Vector3f[] obj) { + return 16 * obj.length; + } + + @Override + public int basicAlignment(BufferLayout serializer, Vector3f[] obj) { + return 16; + } + + @Override + public void write(BufferLayout serializer, ByteBuffer bbf, Vector3f[] obj) { + for (Vector3f i : obj) { + bbf.putFloat(i.x); + bbf.putFloat(i.y); + bbf.putFloat(i.z); + bbf.putInt(0); + } + } + }); + + registerSerializer(new ObjectSerializer(Vector4f[].class) { + @Override + public int length(BufferLayout serializer, Vector4f[] obj) { + return 16 * obj.length; + } + + @Override + public int basicAlignment(BufferLayout serializer, Vector4f[] obj) { + return 16; + } + + @Override + public void write(BufferLayout serializer, ByteBuffer bbf, Vector4f[] obj) { + for (Vector4f i : obj) { + bbf.putFloat(i.x); + bbf.putFloat(i.y); + bbf.putFloat(i.z); + bbf.putFloat(i.w); + } + } + }); + + registerSerializer(new ObjectSerializer(ColorRGBA[].class) { + @Override + public int length(BufferLayout serializer, ColorRGBA[] obj) { + return 16 * obj.length; + } + + @Override + public int basicAlignment(BufferLayout serializer, ColorRGBA[] obj) { + return 16; + } + + @Override + public void write(BufferLayout serializer, ByteBuffer bbf, ColorRGBA[] obj) { + for (ColorRGBA i : obj) { + bbf.putFloat(i.r); + bbf.putFloat(i.g); + bbf.putFloat(i.b); + bbf.putFloat(i.a); + } + } + }); + + registerSerializer(new ObjectSerializer(Quaternion[].class) { + @Override + public int length(BufferLayout serializer, Quaternion[] obj) { + return 16 * obj.length; + } + + @Override + public int basicAlignment(BufferLayout serializer, Quaternion[] obj) { + return 16; + } + + @Override + public void write(BufferLayout serializer, ByteBuffer bbf, Quaternion[] obj) { + for (Quaternion i : obj) { + bbf.putFloat(i.getX()); + bbf.putFloat(i.getY()); + bbf.putFloat(i.getZ()); + bbf.putFloat(i.getW()); + } + } + }); + } + + @Override + public int getStructureAlignment(int maxMemberAlignment) { + return maxMemberAlignment; + } + + @Override + public String getId() { + return "std430"; + } +} diff --git a/jme3-core/src/main/java/com/jme3/util/struct/StructStd430BufferObject.java b/jme3-core/src/main/java/com/jme3/util/struct/StructStd430BufferObject.java new file mode 100644 index 0000000000..a22c9a4a34 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/util/struct/StructStd430BufferObject.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026 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 com.jme3.util.struct; + +import com.jme3.export.InputCapsule; +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.OutputCapsule; +import com.jme3.shader.bufferobject.layout.Std430Layout; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +/** + * A BufferObject containing a struct serialized with Std430 layout. + */ +public class StructStd430BufferObject extends com.jme3.shader.bufferobject.BufferObject { + private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(StructStd430BufferObject.class.getName()); + private transient Class rootStruct; + private transient List> resolvedFields; + private final Std430Layout std430 = new Std430Layout(); + + public StructStd430BufferObject() { + } + + public StructStd430BufferObject(Struct str) { + update(str); + } + + private void loadLayout(Struct struct) { + ArrayList classFields = new ArrayList(); + resolvedFields = StructUtils.getFields(struct, classFields); + for (Field field : classFields) { + if (!Modifier.isFinal(field.getModifiers())) { + throw new RuntimeException("Can't load layout for " + struct + " every field must be final"); + } + } + rootStruct = struct.getClass(); + StructUtils.setBufferLayout(resolvedFields, std430, this); + } + + public void update(Struct struct) { + boolean forceUpdate = false; + if (rootStruct != struct.getClass()) { + if (logger.isLoggable(java.util.logging.Level.FINE)) { + logger.log(java.util.logging.Level.FINE, "Change in layout {0} =/= {1} ", new Object[]{rootStruct, struct.getClass()}); + } + loadLayout(struct); + forceUpdate = true; + } + StructUtils.updateBufferData(resolvedFields, forceUpdate, std430, this); + } + + @Override + public void write(JmeExporter ex) throws IOException { + super.write(ex); + OutputCapsule oc = ex.getCapsule(this); + oc.write(rootStruct.getName(), "rootClass", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + super.read(im); + InputCapsule ic = im.getCapsule(this); + try { + String rootClass = ic.readString("rootClass", null); + if (rootClass == null) throw new Exception("rootClass is undefined"); + Class rootStructClass = Class.forName(rootClass).asSubclass(Struct.class); + Struct rootStruct = rootStructClass.newInstance(); + loadLayout(rootStruct); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/util/struct/StructUtils.java b/jme3-core/src/main/java/com/jme3/util/struct/StructUtils.java index 707788cd29..34411440a7 100644 --- a/jme3-core/src/main/java/com/jme3/util/struct/StructUtils.java +++ b/jme3-core/src/main/java/com/jme3/util/struct/StructUtils.java @@ -131,7 +131,7 @@ private static List> getFields(Struct struct, int depth, ArrayLis } else { so.setDepth(depth); - so.setGroup(struct.hashCode()); + so.setGroup(System.identityHashCode(struct)); expandedStructFields.add(so); } } @@ -156,8 +156,13 @@ public static BufferObject setStd140BufferLayout(List> fields, St // List // fieldList) // { + return setBufferLayout(fields, serializer, out); + } + + public static BufferObject setBufferLayout(List> fields, BufferLayout serializer, BufferObject out) { int pos = -1; + int structMaxAlignment = 1; List regions = new ArrayList(); @@ -167,12 +172,14 @@ public static BufferObject setStd140BufferLayout(List> fields, St int basicAlignment = serializer.getBasicAlignment(v); int length = serializer.estimateSize(v); + structMaxAlignment = Math.max(structMaxAlignment, basicAlignment); int start = serializer.align(pos + 1, basicAlignment); int end = start + length - 1; if ((i == fields.size() - 1) || f.getGroup()!= fields.get(i + 1).getGroup()){// > fields.get(i + 1).getDepth()) { - end = (serializer.align(end, 16)) - 1; + end = (serializer.align(end + 1, serializer.getStructureAlignment(structMaxAlignment))) - 1; + structMaxAlignment = 1; } BufferRegion r = new BufferRegion(start, end); @@ -214,4 +221,4 @@ public static void updateBufferData(List> fields, boolean forceUp if (updateNeeded) out.setUpdateNeeded(false); } -} \ No newline at end of file +} diff --git a/jme3-core/src/test/java/com/jme3/util/StructTest.java b/jme3-core/src/test/java/com/jme3/util/StructTest.java index 7f33494421..4752842657 100644 --- a/jme3-core/src/test/java/com/jme3/util/StructTest.java +++ b/jme3-core/src/test/java/com/jme3/util/StructTest.java @@ -4,13 +4,20 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.nio.ByteBuffer; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.export.binary.BinaryImporter; import com.jme3.shader.bufferobject.BufferObject; import com.jme3.shader.bufferobject.BufferRegion; import com.jme3.shader.bufferobject.DirtyRegionsIterator; import com.jme3.shader.bufferobject.layout.Std140Layout; +import com.jme3.shader.bufferobject.layout.Std430Layout; import com.jme3.util.struct.Struct; +import com.jme3.util.struct.StructStd430BufferObject; import com.jme3.util.struct.StructField; import com.jme3.util.struct.StructUtils; import com.jme3.util.struct.fields.*; @@ -33,6 +40,32 @@ static class TestStruct implements Struct { public final BooleanField boolField6 = new BooleanField(6, "boolField6", true); } + static class PackedArrayStruct implements Struct { + public final FloatArrayField values = new FloatArrayField(0, "values", new Float[] { 1f, 2f, 3f }); + public final FloatField tail = new FloatField(1, "tail", 4f); + } + + public static class SerializablePackedArrayStruct implements Struct { + public final FloatArrayField values = new FloatArrayField(0, "values", new Float[] { 1f, 2f, 3f }); + public final FloatField tail = new FloatField(1, "tail", 4f); + } + + static class ConstantHashSubStruct implements Struct { + public final IntField intField = new IntField(0, "intField", 100); + public final FloatField floatField = new FloatField(1, "floatField", 100f); + + @Override + public int hashCode() { + return 1; + } + } + + static class ConstantHashStructArray implements Struct { + public final SubStructArrayField structs = + new SubStructArrayField<>(0, "structs", + new ConstantHashSubStruct[] { new ConstantHashSubStruct(), new ConstantHashSubStruct() }); + } + @Test public void testFieldsExtraction() { TestStruct test = new TestStruct(); @@ -214,5 +247,68 @@ public void testStd140PartialUpdate() { + } + + @Test + public void testStd430ArrayPacking() { + PackedArrayStruct test = new PackedArrayStruct(); + java.util.List> fields = StructUtils.getFields(test); + + BufferObject std140Bo = new BufferObject(); + StructUtils.setBufferLayout(fields, new Std140Layout(), std140Bo); + assertEquals(64, std140Bo.getData().limit()); + assertEquals(0, std140Bo.getRegion(0).getStart()); + assertEquals(47, std140Bo.getRegion(0).getEnd()); + assertEquals(48, std140Bo.getRegion(1).getStart()); + assertEquals(63, std140Bo.getRegion(1).getEnd()); + + BufferObject std430Bo = new BufferObject(); + StructUtils.setBufferLayout(fields, new Std430Layout(), std430Bo); + assertEquals(16, std430Bo.getData().limit()); + assertEquals(0, std430Bo.getRegion(0).getStart()); + assertEquals(11, std430Bo.getRegion(0).getEnd()); + assertEquals(12, std430Bo.getRegion(1).getStart()); + assertEquals(15, std430Bo.getRegion(1).getEnd()); + + StructUtils.updateBufferData(fields, false, new Std430Layout(), std430Bo); + ByteBuffer data = std430Bo.getData(); + assertEquals(1f, data.getFloat(0)); + assertEquals(2f, data.getFloat(4)); + assertEquals(3f, data.getFloat(8)); + assertEquals(4f, data.getFloat(12)); + } + + @Test + public void testStd430BufferObjectSerializationRestoresLayout() throws IOException { + StructStd430BufferObject bo = new StructStd430BufferObject(new SerializablePackedArrayStruct()); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + BinaryExporter.getInstance().save(bo, output); + StructStd430BufferObject copy = (StructStd430BufferObject) BinaryImporter.getInstance() + .load(new ByteArrayInputStream(output.toByteArray())); + + assertEquals(16, copy.getData().limit()); + assertEquals(0, copy.getRegion(0).getStart()); + assertEquals(11, copy.getRegion(0).getEnd()); + assertEquals(12, copy.getRegion(1).getStart()); + assertEquals(15, copy.getRegion(1).getEnd()); + } + + @Test + public void testStructArrayLayoutDoesNotDependOnHashCode() { + ConstantHashStructArray test = new ConstantHashStructArray(); + java.util.List> fields = StructUtils.getFields(test); + + BufferObject bo = new BufferObject(); + StructUtils.setBufferLayout(fields, new Std140Layout(), bo); + + assertEquals(0, bo.getRegion(0).getStart()); + assertEquals(3, bo.getRegion(0).getEnd()); + assertEquals(4, bo.getRegion(1).getStart()); + assertEquals(15, bo.getRegion(1).getEnd()); + assertEquals(16, bo.getRegion(2).getStart()); + assertEquals(19, bo.getRegion(2).getEnd()); + assertEquals(20, bo.getRegion(3).getStart()); + assertEquals(31, bo.getRegion(3).getEnd()); } } From 45021650dca0e6de5ed261847da99a30da8b542f Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 9 Jun 2026 22:23:12 +0200 Subject: [PATCH 09/15] preserve byte order when copying ByteBuffer --- .../main/java/com/jme3/util/BufferUtils.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/util/BufferUtils.java b/jme3-core/src/main/java/com/jme3/util/BufferUtils.java index 8ed6d9eb78..18381bdac5 100644 --- a/jme3-core/src/main/java/com/jme3/util/BufferUtils.java +++ b/jme3-core/src/main/java/com/jme3/util/BufferUtils.java @@ -1055,14 +1055,15 @@ public static ByteBuffer clone(ByteBuffer buf) { buf.rewind(); ByteBuffer copy; - if (isDirect(buf)) { - copy = createByteBuffer(buf.limit()); - } else { - copy = ByteBuffer.allocate(buf.limit()); - } - copy.put(buf); - - return copy; + if (isDirect(buf)) { + copy = createByteBuffer(buf.limit()); + } else { + copy = ByteBuffer.allocate(buf.limit()); + } + copy.order(buf.order()); + copy.put(buf); + + return copy; } //// -- GENERAL SHORT ROUTINES -- //// From 68c9358422d9989a904166e121fc52ae8335fe97 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 10 Jun 2026 15:39:05 +0200 Subject: [PATCH 10/15] refactor VertexBuffer to use BufferObject, implement partial VertexBuffer update and support custom named VertexBuffers --- .../com/jme3/renderer/opengl/GLRenderer.java | 117 ++++- .../src/main/java/com/jme3/scene/Mesh.java | 92 +++- .../java/com/jme3/scene/VertexBuffer.java | 335 +++++++++++++- .../src/main/java/com/jme3/shader/Shader.java | 18 + .../shader/bufferobject/BufferObject.java | 188 +++++++- .../shader/bufferobject/BufferRegion.java | 6 +- .../bufferobject/DirtyRegionsIterator.java | 11 +- .../java/com/jme3/scene/VertexBufferTest.java | 422 ++++++++++++++++++ .../java/com/jme3/scene/mesh/MeshTest.java | 31 ++ .../DirtyRegionsIteratorTest.java | 245 +++++++++- .../test/java/com/jme3/util/StructTest.java | 19 +- .../jme3test/scene/TestNamedVertexBuffer.java | 99 ++++ .../TestVertexBufferParticleBenchmark.java | 172 +++++++ .../vertexbuffer/NamedVertexBuffer.frag | 9 + .../vertexbuffer/NamedVertexBuffer.j3md | 11 + .../vertexbuffer/NamedVertexBuffer.vert | 13 + 16 files changed, 1717 insertions(+), 71 deletions(-) create mode 100644 jme3-core/src/test/java/com/jme3/scene/VertexBufferTest.java create mode 100644 jme3-examples/src/main/java/jme3test/scene/TestNamedVertexBuffer.java create mode 100644 jme3-examples/src/main/java/jme3test/stress/TestVertexBufferParticleBenchmark.java create mode 100644 jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.frag create mode 100644 jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.j3md create mode 100644 jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.vert 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 79011a4aac..7344f24013 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 @@ -72,6 +72,7 @@ import jme3tools.shader.ShaderDebug; import java.lang.ref.WeakReference; +import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; @@ -3445,29 +3446,125 @@ public void updateBufferData(VertexBuffer vb) { } int usage = convertUsage(vb.getUsage()); - vb.getData().rewind(); + Buffer data = vb.getData(); + data.rewind(); + if (created || vb.hasDataSizeChanged() || !vb.hasRegions()) { + updateVertexBufferData(target, vb, data, usage); + vb.unsetRegions(); + } else { + DirtyRegionsIterator it = vb.getDirtyRegions(); + BufferRegion reg; + int bufferSize = getVertexBufferSizeBytes(vb); + while ((reg = it.next()) != null) { + if (reg.getStart() == 0 && reg.length() == bufferSize) { + updateVertexBufferData(target, vb, data, usage); + break; + } + updateVertexBufferSubData(target, vb, data, reg); + reg.clearDirty(); + } + vb.unsetRegions(); + } + + vb.clearUpdateNeeded(); + } + + private void updateVertexBufferData(int target, VertexBuffer vb, Buffer data, int usage) { + data.rewind(); switch (vb.getFormat()) { case Byte: case UnsignedByte: - gl.glBufferData(target, (ByteBuffer) vb.getData(), usage); + case Half: + gl.glBufferData(target, (ByteBuffer) data, usage); break; case Short: case UnsignedShort: - gl.glBufferData(target, (ShortBuffer) vb.getData(), usage); + gl.glBufferData(target, (ShortBuffer) data, usage); break; case Int: case UnsignedInt: - glext.glBufferData(target, (IntBuffer) vb.getData(), usage); + glext.glBufferData(target, (IntBuffer) data, usage); break; case Float: - gl.glBufferData(target, (FloatBuffer) vb.getData(), usage); + gl.glBufferData(target, (FloatBuffer) data, usage); break; default: throw new UnsupportedOperationException("Unknown buffer format."); } + } - vb.clearUpdateNeeded(); + private void updateVertexBufferSubData(int target, VertexBuffer vb, Buffer data, BufferRegion reg) { + Buffer slice = sliceVertexBuffer(vb, data, reg.getStart(), reg.length()); + switch (vb.getFormat()) { + case Byte: + case UnsignedByte: + case Half: + gl.glBufferSubData(target, reg.getStart(), (ByteBuffer) slice); + break; + case Short: + case UnsignedShort: + gl.glBufferSubData(target, reg.getStart(), (ShortBuffer) slice); + break; + case Int: + case UnsignedInt: + glext.glBufferSubData(target, reg.getStart(), (IntBuffer) slice); + break; + case Float: + gl.glBufferSubData(target, reg.getStart(), (FloatBuffer) slice); + break; + default: + throw new UnsupportedOperationException("Unknown buffer format."); + } + } + + private Buffer sliceVertexBuffer(VertexBuffer vb, Buffer data, int byteOffset, int byteLength) { + int componentSize = vb.getFormat().getComponentSize(); + if (byteOffset % componentSize != 0 || byteLength % componentSize != 0) { + throw new IllegalArgumentException("Dirty vertex buffer range is not aligned to " + vb.getFormat()); + } + int start = byteOffset / componentSize; + int end = start + byteLength / componentSize; + switch (vb.getFormat()) { + case Byte: + case UnsignedByte: + case Half: { + ByteBuffer view = ((ByteBuffer) data).duplicate(); + view.position(byteOffset); + view.limit(byteOffset + byteLength); + return view.slice().order(((ByteBuffer) data).order()); + } + case Short: + case UnsignedShort: { + ShortBuffer view = ((ShortBuffer) data).duplicate(); + view.position(start); + view.limit(end); + return view.slice(); + } + case Int: + case UnsignedInt: { + IntBuffer view = ((IntBuffer) data).duplicate(); + view.position(start); + view.limit(end); + return view.slice(); + } + case Float: { + FloatBuffer view = ((FloatBuffer) data).duplicate(); + view.position(start); + view.limit(end); + return view.slice(); + } + default: + throw new UnsupportedOperationException("Unknown buffer format."); + } + } + + private int getVertexBufferSizeBytes(VertexBuffer vb) { + Buffer data = vb.getData(); + if (data instanceof ByteBuffer) { + return data.limit(); + } + return data.limit() * vb.getFormat().getComponentSize(); } private int resolveUsageHint(BufferObject.AccessHint ah, BufferObject.NatureHint nh) { @@ -3545,7 +3642,8 @@ private void updateBufferData(int type, BufferObject bo) { while ((reg = it.next()) != null) { gl.glBindBuffer(type, bufferId); if (reg.isFullBufferRegion()) { - ByteBuffer bbf = bo.getData(); + ByteBuffer bbf = bo.getByteData().duplicate(); + bbf.clear(); if (logger.isLoggable(java.util.logging.Level.FINER)) { logger.log(java.util.logging.Level.FINER, "Update full buffer {0} with {1} bytes", new Object[] { bo, bbf.remaining() }); } @@ -3622,13 +3720,14 @@ public void setVertexAttrib(VertexBuffer vb, VertexBuffer idb) { throw new IllegalStateException("Cannot render mesh without shader bound"); } - Attribute attrib = context.boundShader.getAttribute(vb.getBufferType()); + String attributeName = vb.getShaderAttributeName(); + Attribute attrib = context.boundShader.getAttribute(attributeName); int loc = attrib.getLocation(); if (loc == -1) { return; // not defined } if (loc == -2) { - loc = gl.glGetAttribLocation(context.boundShaderProgram, "in" + vb.getBufferType().name()); + loc = gl.glGetAttribLocation(context.boundShaderProgram, attributeName); // not really the name of it in the shader (inPosition) but // the internal name of the enum (Position). diff --git a/jme3-core/src/main/java/com/jme3/scene/Mesh.java b/jme3-core/src/main/java/com/jme3/scene/Mesh.java index e4ab6393b3..c66a43296f 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Mesh.java +++ b/jme3-core/src/main/java/com/jme3/scene/Mesh.java @@ -49,6 +49,8 @@ import java.io.IOException; import java.nio.*; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; /** * Mesh is used to store rendering data. @@ -175,6 +177,7 @@ public boolean isListMode() { private SafeArrayList buffersList = new SafeArrayList<>(VertexBuffer.class); private IntMap buffers = new IntMap<>(); + private HashMap customBuffers = new HashMap<>(); private VertexBuffer[] lodLevels; private float pointSize = DEFAULT_POINT_SIZE; @@ -215,6 +218,7 @@ public Mesh clone() { clone.meshBound = meshBound.clone(); clone.collisionTree = collisionTree != null ? collisionTree : null; clone.buffers = buffers.clone(); + clone.customBuffers = new HashMap<>(customBuffers); clone.buffersList = new SafeArrayList<>(VertexBuffer.class, buffersList); clone.vertexArrayID = DEFAULT_VERTEX_ARRAY_ID; if (elementLengths != null) { @@ -246,10 +250,15 @@ public Mesh deepClone() { clone.collisionTree = DEFAULT_COLLISION_TREE; // it will get re-generated in any case clone.buffers = new IntMap<>(); + clone.customBuffers = new HashMap<>(); clone.buffersList = new SafeArrayList<>(VertexBuffer.class); for (VertexBuffer vb : buffersList.getArray()) { VertexBuffer bufClone = vb.clone(); - clone.buffers.put(vb.getBufferType().ordinal(), bufClone); + if (bufClone.getBufferType() == Type.Custom) { + clone.customBuffers.put(bufClone.getShaderAttributeName(), bufClone); + } else { + clone.buffers.put(vb.getBufferType().ordinal(), bufClone); + } clone.buffersList.add(bufClone); } @@ -331,6 +340,7 @@ public void cloneFields(Cloner cloner, Object original) { this.meshBound = cloner.clone(meshBound); this.buffersList = cloner.clone(buffersList); this.buffers = cloner.clone(buffers); + this.customBuffers = cloner.clone(customBuffers); this.lodLevels = cloner.clone(lodLevels); this.elementLengths = cloner.clone(elementLengths); this.modeStart = cloner.clone(modeStart); @@ -752,6 +762,11 @@ public void setInterleaved() { } break; case Half: + ByteBuffer hb = (ByteBuffer) vb.getData(); + for (int comp = 0; comp < vb.components; comp++) { + dataBuf.putShort(hb.getShort()); + } + break; case Short: case UnsignedShort: ShortBuffer sb = (ShortBuffer) vb.getData(); @@ -1065,6 +1080,16 @@ public int collideWith(Collidable other, * @throws IllegalArgumentException If the buffer type is already set */ public void setBuffer(VertexBuffer vb) { + if (vb.getBufferType() == Type.Custom) { + String attributeName = vb.getShaderAttributeName(); + if (customBuffers.containsKey(attributeName)) { + throw new IllegalArgumentException("Custom buffer attribute already set: " + attributeName); + } + customBuffers.put(attributeName, vb); + buffersList.add(vb); + updateCounts(); + return; + } if (buffers.containsKey(vb.getBufferType().ordinal())) { throw new IllegalArgumentException("Buffer type already set: " + vb.getBufferType()); } @@ -1082,6 +1107,9 @@ public void setBuffer(VertexBuffer vb) { * @param type The buffer type to remove */ public void clearBuffer(VertexBuffer.Type type) { + if (type == Type.Custom) { + throw new IllegalArgumentException("Use clearBuffer(String) for custom vertex buffers"); + } VertexBuffer vb = buffers.remove(type.ordinal()); if (vb != null) { buffersList.remove(vb); @@ -1089,6 +1117,19 @@ public void clearBuffer(VertexBuffer.Type type) { } } + /** + * Unsets the custom vertex buffer bound to the given shader attribute name. + * + * @param attributeName the shader attribute name + */ + public void clearBuffer(String attributeName) { + VertexBuffer vb = customBuffers.remove(attributeName); + if (vb != null) { + buffersList.remove(vb); + updateCounts(); + } + } + /** * Creates a {@link VertexBuffer} for the mesh or modifies * the existing one per the parameters given. @@ -1102,6 +1143,9 @@ public void clearBuffer(VertexBuffer.Type type) { * incompatible with the parameters given. */ public void setBuffer(Type type, int components, Format format, Buffer buf) { + if (type == Type.Custom) { + throw new IllegalArgumentException("Use setBuffer(String, int, Format, Buffer) for custom vertex buffers"); + } VertexBuffer vb = buffers.get(type.ordinal()); if (vb == null) { vb = new VertexBuffer(type); @@ -1117,6 +1161,32 @@ public void setBuffer(Type type, int components, Format format, Buffer buf) { } } + /** + * Creates or updates a custom vertex buffer bound to the specified shader + * attribute name. + * + * @param attributeName the exact attribute name in the shader + * @param components Number of components + * @param format Data format + * @param buf The buffer data + */ + public void setBuffer(String attributeName, int components, Format format, Buffer buf) { + VertexBuffer vb = customBuffers.get(attributeName); + if (vb == null) { + vb = new VertexBuffer(Type.Custom); + vb.setAttributeName(attributeName); + vb.setupData(Usage.Dynamic, components, format, buf); + setBuffer(vb); + } else { + if (vb.getNumComponents() != components || vb.getFormat() != format) { + throw new UnsupportedOperationException("The custom buffer already set " + + "is incompatible with the given parameters"); + } + vb.updateData(buf); + updateCounts(); + } + } + /** * Set a floating point {@link VertexBuffer} on the mesh. * @@ -1168,9 +1238,23 @@ public void setBuffer(Type type, int components, short[] buf) { * @return the VertexBuffer data, or null if not set */ public VertexBuffer getBuffer(Type type) { + if (type == Type.Custom) { + throw new IllegalArgumentException("Use getBuffer(String) for custom vertex buffers"); + } return buffers.get(type.ordinal()); } + /** + * Get the custom {@link VertexBuffer} stored on this mesh with the given + * shader attribute name. + * + * @param attributeName the shader attribute name + * @return the VertexBuffer data, or null if not set + */ + public VertexBuffer getBuffer(String attributeName) { + return customBuffers.get(attributeName); + } + /** * Get the {@link VertexBuffer} data stored on this mesh in float * format. @@ -1663,6 +1747,7 @@ public void write(JmeExporter ex) throws IOException { } out.writeIntSavableMap(buffers, "buffers", null); + out.writeStringSavableMap(customBuffers, "customBuffers", null); //restoring Hw skinning buffers. if (hwBoneIndex != null) { @@ -1700,6 +1785,11 @@ public void read(JmeImporter im) throws IOException { for (Entry entry : buffers) { buffersList.add(entry.getValue()); } + Map readCustomBuffers = (Map) in.readStringSavableMap("customBuffers", null); + if (readCustomBuffers != null) { + customBuffers.putAll(readCustomBuffers); + buffersList.addAll(customBuffers.values()); + } //creating hw animation buffers empty so that they are put in the cache if (isAnimated()) { diff --git a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java index ddb34f43ee..050137d678 100644 --- a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java +++ b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java @@ -34,6 +34,7 @@ import com.jme3.export.*; import com.jme3.math.FastMath; import com.jme3.renderer.Renderer; +import com.jme3.shader.bufferobject.BufferObject; import com.jme3.util.BufferUtils; import com.jme3.util.NativeObject; import java.io.IOException; @@ -53,7 +54,7 @@ * For a 3D vector, a single component is one of the dimensions, X, Y or Z. * */ -public class VertexBuffer extends NativeObject implements Savable, Cloneable { +public class VertexBuffer extends BufferObject implements Savable, Cloneable { /** * Type of buffer. Specifies the actual attribute it defines. */ @@ -226,6 +227,11 @@ public enum Type { MorphTarget11, MorphTarget12, MorphTarget13, + /** + * Application-defined vertex attribute. Custom buffers must provide + * an explicit shader attribute name with {@link #setAttributeName(String)}. + */ + Custom, } /** @@ -325,14 +331,14 @@ public int getComponentSize() { * derived from components * format.getComponentSize() */ protected transient int componentsLength = 0; - protected Buffer data = null; protected Usage usage; protected Type bufType; protected Format format; protected boolean normalized = false; protected int instanceSpan = 0; protected transient boolean dataSizeChanged = false; - protected String name; + protected transient ByteBuffer dataBytes; + protected String attributeName; /** * Creates an empty, uninitialized buffer. @@ -403,7 +409,8 @@ public boolean invariant() { throw new AssertionError(); } else if (data instanceof ShortBuffer && format != Format.Short && format != Format.UnsignedShort) { throw new AssertionError(); - } else if (data instanceof ByteBuffer && format != Format.Byte && format != Format.UnsignedByte) { + } else if (data instanceof ByteBuffer && format != Format.Byte && format != Format.UnsignedByte + && format != Format.Half) { throw new AssertionError(); } return true; @@ -458,6 +465,7 @@ public void setStride(int stride) { * * @return A native buffer, in the specified {@link Format format}. */ + @SuppressWarnings("unchecked") public Buffer getData() { return data; } @@ -484,7 +492,8 @@ public Buffer getDataReadOnly() { // does not have an asReadOnlyBuffer() method. Buffer result; if (data instanceof ByteBuffer) { - result = ((ByteBuffer) data).asReadOnlyBuffer(); + ByteBuffer source = (ByteBuffer) data; + result = source.asReadOnlyBuffer().order(source.order()); } else if (data instanceof FloatBuffer) { result = ((FloatBuffer) data).asReadOnlyBuffer(); } else if (data instanceof ShortBuffer) { @@ -520,6 +529,7 @@ public void setUsage(Usage usage) { // throw new UnsupportedOperationException("Data has already been sent. Cannot set usage."); this.usage = usage; + unsetRegions(); this.setUpdateNeeded(); } @@ -589,6 +599,45 @@ public Type getBufferType() { return bufType; } + /** + * Sets the shader attribute name used to bind this vertex buffer. + * Setting an explicit attribute name turns this buffer into a custom + * vertex attribute. + * + * @param attributeName the exact attribute name in the shader + */ + public void setAttributeName(String attributeName) { + if (attributeName == null || attributeName.isEmpty()) { + throw new IllegalArgumentException("Attribute name cannot be null or empty"); + } + this.attributeName = attributeName; + this.bufType = Type.Custom; + } + + /** + * Returns the explicit shader attribute name, or null for built-in types. + * + * @return the custom attribute name + */ + public String getAttributeName() { + return attributeName; + } + + /** + * Returns the shader attribute name used by the renderer. + * + * @return shader attribute name + */ + public String getShaderAttributeName() { + if (attributeName != null) { + return attributeName; + } + if (bufType == Type.Custom) { + throw new IllegalStateException("Custom vertex buffers require an attribute name"); + } + return "in" + bufType.name(); + } + /** * @return The {@link Format format}, or data type of the data. */ @@ -666,11 +715,13 @@ public void setupData(Usage usage, int components, Format format, Buffer data) { } this.data = data; + this.dataBytes = null; this.components = components; this.usage = usage; this.format = format; this.componentsLength = components * format.getComponentSize(); this.lastLimit = data.limit(); + unsetRegions(); setUpdateNeeded(); } @@ -707,9 +758,242 @@ public void updateData(Buffer data) { } this.data = data; + this.dataBytes = null; + unsetRegions(); + setUpdateNeeded(); + } + + /** + * Updates this vertex buffer from byte-addressable data while preserving + * the existing vertex layout metadata. + *

+ * This method converts the supplied bytes into the buffer type implied by + * {@link #getFormat()}, which may allocate and copy data. Prefer + * {@link #updateData(Buffer)} for vertex-buffer data. + * + * @param data the new byte-addressable data + */ + @Deprecated + @Override + public void setByteData(ByteBuffer data) { + if (data == null) { + updateData(null); + ownsData = true; + return; + } + ByteBuffer source = data.duplicate().order(data.order()); + updateData(createTypedDataCopy(source)); + ownsData = true; + } + + /** + * Sets data from a byte buffer. + *

+ * This method converts the supplied bytes into the buffer type implied by + * {@link #getFormat()}, which may allocate and copy data. Prefer + * {@link #updateData(Buffer)} for vertex-buffer data. + * + * @param data the new byte-addressable data + */ + @Deprecated + @Override + public void setData(ByteBuffer data) { + setByteData(data); + } + + /** + * Sets byte-addressable vertex data without copying it. + *

+ * This is only valid for byte-backed vertex formats. For typed vertex + * formats, use {@link #updateData(Buffer)} with the matching typed buffer. + * Prefer {@link #updateData(Buffer)} for vertex-buffer data, including + * byte-backed vertex data, because it is the regular VertexBuffer API for + * installing caller-owned backing storage. + *

+ * This installs the passed buffer as the current backing buffer, but it is + * not a permanent synchronization contract. Later operations may replace + * the backing buffer with a new internal buffer, after which mutations to + * the original pointer buffer are no longer reflected by this vertex buffer. + * + * @param data ByteBuffer to use directly + */ + @Deprecated + @Override + public void setByteDataPointer(ByteBuffer data) { + if (data != null && format != Format.Byte && format != Format.UnsignedByte && format != Format.Half) { + throw new UnsupportedOperationException("Use updateData(Buffer) for non-byte vertex buffer data"); + } + if (data != null && !data.isDirect()) { + throw new IllegalArgumentException("VertexBuffer data must be direct."); + } + if (data == null) { + updateData(null); + ownsData = false; + return; + } + if (this.data.getClass() != data.getClass() || data.limit() != lastLimit) { + dataSizeChanged = true; + lastLimit = data.limit(); + } + this.data = data; + ownsData = false; + this.dataBytes = null; + unsetRegions(); setUpdateNeeded(); } + private Buffer createTypedDataCopy(ByteBuffer source) { + int componentSize = format.getComponentSize(); + if (source.remaining() % componentSize != 0) { + throw new IllegalArgumentException("Byte data is not aligned to " + format); + } + switch (format) { + case Byte: + case UnsignedByte: + case Half: { + ByteBuffer copy = BufferUtils.createByteBuffer(source.remaining()); + copy.order(source.order()); + copy.put(source); + copy.clear(); + return copy; + } + case Short: + case UnsignedShort: { + ShortBuffer view = source.asShortBuffer(); + ShortBuffer copy = BufferUtils.createShortBuffer(view.remaining()); + copy.put(view); + copy.clear(); + return copy; + } + case Int: + case UnsignedInt: { + IntBuffer view = source.asIntBuffer(); + IntBuffer copy = BufferUtils.createIntBuffer(view.remaining()); + copy.put(view); + copy.clear(); + return copy; + } + case Float: { + FloatBuffer view = source.asFloatBuffer(); + FloatBuffer copy = BufferUtils.createFloatBuffer(view.remaining()); + copy.put(view); + copy.clear(); + return copy; + } + default: + throw new UnsupportedOperationException("Unrecognized buffer format: " + format); + } + } + + /** + * Returns byte-addressable data for this vertex buffer. + *

+ * For byte-backed vertex buffers, this returns the underlying data. For + * typed vertex buffers, this method creates and caches a byte copy. Prefer + * {@link #getData()} when working with vertex-buffer data directly. + * Do not mutate the returned buffer's position or limit directly; use + * {@link ByteBuffer#duplicate()} first when cursor state needs to change. + * + * @return byte-backed vertex data + */ + @Deprecated + @Override + public ByteBuffer getByteData() { + if (data == null) { + throw new IllegalStateException("VertexBuffer data has not been initialized"); + } + if (data instanceof ByteBuffer) { + return super.getByteData(); + } + if (dataBytes == null) { + dataBytes = createByteDataCopy(data); + } + return dataBytes; + } + + private ByteBuffer createByteDataCopy(Buffer source) { + if (source instanceof FloatBuffer) { + FloatBuffer fb = (FloatBuffer) source; + ByteBuffer result = BufferUtils.createByteBuffer(fb.limit() * Float.BYTES); + for (int i = 0; i < fb.limit(); i++) { + result.putFloat(fb.get(i)); + } + result.clear(); + return result; + } else if (source instanceof ShortBuffer) { + ShortBuffer sb = (ShortBuffer) source; + ByteBuffer result = BufferUtils.createByteBuffer(sb.limit() * Short.BYTES); + for (int i = 0; i < sb.limit(); i++) { + result.putShort(sb.get(i)); + } + result.clear(); + return result; + } else if (source instanceof IntBuffer) { + IntBuffer ib = (IntBuffer) source; + ByteBuffer result = BufferUtils.createByteBuffer(ib.limit() * Integer.BYTES); + for (int i = 0; i < ib.limit(); i++) { + result.putInt(ib.get(i)); + } + result.clear(); + return result; + } + throw new UnsupportedOperationException("Use getData() for unsupported vertex buffer data type: " + source); + } + + /** + * Marks a byte range in this vertex buffer for partial GPU upload. + * + * @param byteOffset first dirty byte + * @param byteLength number of dirty bytes + */ + public void markBytesDirty(int byteOffset, int byteLength) { + if (byteLength <= 0) { + return; + } + if (data == null) { + throw new IllegalStateException("VertexBuffer data has not been initialized"); + } + if (byteOffset < 0) { + throw new IllegalArgumentException("Byte offset cannot be negative"); + } + int componentSize = format.getComponentSize(); + if (byteOffset % componentSize != 0 || byteLength % componentSize != 0) { + throw new IllegalArgumentException("Dirty range is not aligned to " + format); + } + int byteEnd = byteOffset + byteLength; + if (byteEnd < byteOffset || byteEnd > getDataSizeBytes()) { + throw new IllegalArgumentException("Dirty range exceeds vertex buffer data"); + } + addDirtyRegion(byteOffset, byteLength); + } + + /** + * Marks a range of vertex elements for partial GPU upload. + * + * @param firstElement first dirty element + * @param elementCount number of dirty elements + */ + public void markElementsDirty(int firstElement, int elementCount) { + if (firstElement < 0) { + throw new IllegalArgumentException("First element cannot be negative"); + } + if (elementCount <= 0) { + return; + } + int elementEnd = firstElement + elementCount; + if (elementEnd < firstElement || elementEnd > getNumElements()) { + throw new IllegalArgumentException("Dirty element range exceeds vertex buffer data"); + } + markBytesDirty(firstElement * componentsLength, elementCount * componentsLength); + } + + private int getDataSizeBytes() { + if (data instanceof ByteBuffer) { + return data.limit(); + } + return data.limit() * format.getComponentSize(); + } + /** * Returns true if the data size of the VertexBuffer has changed. * Internal use only. @@ -754,6 +1038,7 @@ public void convertToHalf() { halfData.putShort(half); } this.data = halfData; + this.dataBytes = null; setUpdateNeeded(); dataSizeChanged = true; } @@ -773,10 +1058,13 @@ public void compact(int numElements) { case UnsignedByte: case Half: ByteBuffer bbuf = (ByteBuffer) data; - bbuf.limit(total); - ByteBuffer bnewBuf = BufferUtils.createByteBuffer(total); + int byteTotal = total * format.getComponentSize(); + bbuf.limit(byteTotal); + ByteBuffer bnewBuf = BufferUtils.createByteBuffer(byteTotal); + bnewBuf.order(bbuf.order()); bnewBuf.put(bbuf); data = bnewBuf; + dataBytes = null; break; case Short: case UnsignedShort: @@ -785,6 +1073,7 @@ public void compact(int numElements) { ShortBuffer snewBuf = BufferUtils.createShortBuffer(total); snewBuf.put(sbuf); data = snewBuf; + dataBytes = null; break; case Int: case UnsignedInt: @@ -793,6 +1082,7 @@ public void compact(int numElements) { IntBuffer inewBuf = BufferUtils.createIntBuffer(total); inewBuf.put(ibuf); data = inewBuf; + dataBytes = null; break; case Float: FloatBuffer fbuf = (FloatBuffer) data; @@ -800,11 +1090,13 @@ public void compact(int numElements) { FloatBuffer fnewBuf = BufferUtils.createFloatBuffer(total); fnewBuf.put(fbuf); data = fnewBuf; + dataBytes = null; break; default: throw new UnsupportedOperationException("Unrecognized buffer format: " + format); } data.clear(); + unsetRegions(); setUpdateNeeded(); dataSizeChanged = true; } @@ -831,9 +1123,12 @@ public void setElementComponent(int elementIndex, int componentIndex, Object val data.clear(); switch (format) { + case Half: + ByteBuffer hbin = (ByteBuffer) data; + hbin.putShort(inPos + elementPos, (Short) val); + break; case Byte: case UnsignedByte: - case Half: ByteBuffer bin = (ByteBuffer) data; bin.put(inPos + elementPos, (Byte) val); break; @@ -854,6 +1149,8 @@ public void setElementComponent(int elementIndex, int componentIndex, Object val default: throw new UnsupportedOperationException("Unrecognized buffer format: " + format); } + dataBytes = null; + markBytesDirty((elementIndex * components + componentIndex) * format.getComponentSize(), format.getComponentSize()); } /** @@ -876,9 +1173,11 @@ public Object getElementComponent(int elementIndex, int componentIndex) { Buffer srcData = getDataReadOnly(); switch (format) { + case Half: + ByteBuffer hbin = (ByteBuffer) srcData; + return hbin.getShort(inPos + elementPos); case Byte: case UnsignedByte: - case Half: ByteBuffer bin = (ByteBuffer) srcData; return bin.get(inPos + elementPos); case Short: @@ -954,6 +1253,7 @@ public void copyElements(int inIndex, VertexBuffer outVb, int outIndex, int len) bin.position(inPos).limit(inPos + elementSz * len); bout.position(outPos).limit(outPos + elementSz * len); bout.put(bin); + outVb.dataBytes = null; break; case Short: case UnsignedShort: @@ -962,6 +1262,7 @@ public void copyElements(int inIndex, VertexBuffer outVb, int outIndex, int len) sin.position(inPos).limit(inPos + elementSz * len); sout.position(outPos).limit(outPos + elementSz * len); sout.put(sin); + outVb.dataBytes = null; break; case Int: case UnsignedInt: @@ -970,6 +1271,7 @@ public void copyElements(int inIndex, VertexBuffer outVb, int outIndex, int len) iin.position(inPos).limit(inPos + elementSz * len); iout.position(outPos).limit(outPos + elementSz * len); iout.put(iin); + outVb.dataBytes = null; break; case Float: FloatBuffer fin = (FloatBuffer) srcData; @@ -977,6 +1279,7 @@ public void copyElements(int inIndex, VertexBuffer outVb, int outIndex, int len) fin.position(inPos).limit(inPos + elementSz * len); fout.position(outPos).limit(outPos + elementSz * len); fout.put(fin); + outVb.dataBytes = null; break; default: throw new UnsupportedOperationException("Unrecognized buffer format: " + format); @@ -985,6 +1288,7 @@ public void copyElements(int inIndex, VertexBuffer outVb, int outIndex, int len) // Clear the output buffer to rewind it and reset its // limit from where we shortened it above. outVb.data.clear(); + outVb.markElementsDirty(outIndex, len); } /** @@ -1066,11 +1370,13 @@ public VertexBuffer clone(Type overrideType) { // reading thread during cloning (and vice versa) since this is // a purely read-only operation. vb.data = BufferUtils.clone(getDataReadOnly()); + vb.dataBytes = null; vb.format = format; vb.handleRef = new Object(); vb.id = -1; vb.normalized = normalized; vb.instanceSpan = instanceSpan; + vb.attributeName = overrideType == Type.Custom ? attributeName : null; vb.offset = offset; vb.stride = stride; vb.updateNeeded = true; @@ -1094,6 +1400,7 @@ public String toString() { public void resetObject() { // assert this.id != -1; this.id = -1; + unsetRegions(); setUpdateNeeded(); } @@ -1104,9 +1411,7 @@ public void deleteObject(Object rendererObject) { @Override protected void deleteNativeBuffers() { - if (data != null) { - BufferUtils.destroyDirectBuffer(data); - } + super.deleteNativeBuffers(); } @Override @@ -1131,6 +1436,7 @@ public void write(JmeExporter ex) throws IOException { oc.write(stride, "stride", 0); oc.write(instanceSpan, "instanceSpan", 0); oc.write(name, "name", null); + oc.write(attributeName, "attributeName", null); String dataName = "data" + format.name(); Buffer roData = getDataReadOnly(); @@ -1168,6 +1474,7 @@ public void read(JmeImporter im) throws IOException { stride = ic.readInt("stride", 0); instanceSpan = ic.readInt("instanceSpan", 0); name = ic.readString("name", null); + attributeName = ic.readString("attributeName", null); componentsLength = components * format.getComponentSize(); @@ -1175,19 +1482,23 @@ public void read(JmeImporter im) throws IOException { switch (format) { case Float: data = ic.readFloatBuffer(dataName, null); + dataBytes = null; break; case Short: case UnsignedShort: data = ic.readShortBuffer(dataName, null); + dataBytes = null; break; case UnsignedByte: case Byte: case Half: data = ic.readByteBuffer(dataName, null); + dataBytes = null; break; case Int: case UnsignedInt: data = ic.readIntBuffer(dataName, null); + dataBytes = null; break; default: throw new IOException("Unsupported import buffer format: " + format); diff --git a/jme3-core/src/main/java/com/jme3/shader/Shader.java b/jme3-core/src/main/java/com/jme3/shader/Shader.java index eb2f778d39..c066671d47 100644 --- a/jme3-core/src/main/java/com/jme3/shader/Shader.java +++ b/jme3-core/src/main/java/com/jme3/shader/Shader.java @@ -68,6 +68,7 @@ public final class Shader extends NativeObject { * Maps attribute name to the location of the attribute in the shader. */ private final IntMap attribs; + private final ListMap namedAttribs; /** * Type of shader. The shader will control the pipeline of its type. @@ -232,6 +233,7 @@ public Shader() { uniforms = new ListMap<>(); bufferBlocks = new ListMap<>(); attribs = new IntMap<>(); + namedAttribs = new ListMap<>(); boundUniforms = new ArrayList<>(); } @@ -254,6 +256,7 @@ protected Shader(Shader s) { bufferBlocks = null; boundUniforms = null; attribs = null; + namedAttribs = null; } /** @@ -346,6 +349,16 @@ public Attribute getAttribute(VertexBuffer.Type attribType){ return attrib; } + public Attribute getAttribute(String attribName){ + Attribute attrib = namedAttribs.get(attribName); + if (attrib == null){ + attrib = new Attribute(); + attrib.name = attribName; + namedAttribs.put(attribName, attrib); + } + return attrib; + } + public ListMap getUniformMap(){ return uniforms; } @@ -429,6 +442,11 @@ public void resetLocations() { entry.getValue().location = ShaderVariable.LOC_UNKNOWN; } } + if (namedAttribs != null) { + for (Attribute attrib : namedAttribs.values()) { + attrib.location = ShaderVariable.LOC_UNKNOWN; + } + } } @Override 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 143b1a0071..59b1eee3dd 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 @@ -32,7 +32,9 @@ package com.jme3.shader.bufferobject; import java.io.IOException; +import java.nio.Buffer; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; @@ -102,9 +104,10 @@ public static enum NatureHint { private transient int binding = -1; protected transient DirtyRegionsIterator dirtyRegionsIterator; - protected ByteBuffer data = null; + protected Buffer data = null; + protected transient boolean ownsData = true; protected ArrayList regions = new ArrayList(); - private String name; + protected String name; public BufferObject() { super(); @@ -152,70 +155,151 @@ public int getBinding() { * @param length expected length of the buffer object */ public void initializeEmpty(int length) { - if (data != null) { + if (data != null && ownsData) { BufferUtils.destroyDirectBuffer(data); } this.data = BufferUtils.createByteBuffer(length); + ownsData = true; setUpdateNeeded(); } /** * Transfer remaining bytes of passed buffer to the internal buffer of this buffer object - * + * * @param data ByteBuffer containing the data to pass */ - public void setData(ByteBuffer data) { + public void setByteData(ByteBuffer data) { if (data == null) { if (this.data != null) { - BufferUtils.destroyDirectBuffer(this.data); + if (ownsData) { + BufferUtils.destroyDirectBuffer(this.data); + } this.data = null; - setUpdateNeeded(); } + ownsData = true; + setUpdateNeeded(); return; } - ByteBuffer source = data == this.data ? data.duplicate() : data; - ByteBuffer oldData = this.data; + ByteBuffer source = data.duplicate().order(data.order()); + ByteBuffer oldData = (ByteBuffer) this.data; + boolean oldOwnsData = ownsData; - this.data = BufferUtils.createByteBuffer(source.limit() - source.position()); - this.data.put(source); + this.data = BufferUtils.createByteBuffer(source.remaining()); + ((ByteBuffer) this.data).order(source.order()); + ((ByteBuffer) this.data).put(source); + ((ByteBuffer) this.data).clear(); + ownsData = true; - if (oldData != null) { + if (oldData != null && oldOwnsData) { BufferUtils.destroyDirectBuffer(oldData); } setUpdateNeeded(); } + /** + * Sets byte-addressable data for this buffer object. + * + * @param data ByteBuffer containing the data to pass + */ + public void setData(ByteBuffer data) { + setByteData(data); + } + + /** + * Sets byte-addressable data from pointer storage without copying it. + *

+ * The passed buffer is installed as the current internal backing buffer. + * It remains owned by the caller and will not be destroyed by this buffer + * object. Use {@link #setByteData(ByteBuffer)} when this object should own + * a mutable copy. + *

+ * This is not a permanent synchronization contract. Later operations may + * replace the internal backing buffer with object-owned storage, for + * example when layout regions require a larger mutable buffer. After that + * replacement, mutations to the original pointer buffer are no longer + * reflected by this buffer object. + *

+ * Read-only buffers are allowed, but their limit must already cover all + * layout regions because this object cannot resize read-only referenced + * storage in place. + * + * @param data ByteBuffer to use directly + */ + public void setByteDataPointer(ByteBuffer data) { + if (data != null && !data.isDirect()) { + throw new IllegalArgumentException("BufferObject data must be direct."); + } + if (this.data != null && this.data != data && ownsData) { + BufferUtils.destroyDirectBuffer(this.data); + } + this.data = data; + ownsData = false; + setUpdateNeeded(); + } + /** - * Rewind and return buffer data - * - * @return + * Return buffer data. + * + * @return buffer data + */ + @SuppressWarnings("unchecked") + public T getData() { + return (T) getByteData(); + } + + /** + * Return byte-addressable buffer data. + *

+ * The returned buffer is the internal backing buffer. Do not mutate its + * position or limit directly. Use {@link ByteBuffer#duplicate()} before + * changing cursor state for reads, writes, or uploads. + *

+ * When layout regions exist, this method ensures the backing buffer can + * cover the last region. If the current backing buffer is read-only and too + * small, this method throws because it cannot resize the referenced storage + * without replacing it. Provide a correctly sized buffer to + * {@link #setByteDataPointer(ByteBuffer)} or use {@link #setByteData(ByteBuffer)} + * to let this object own a mutable copy. + * + * @return byte buffer data */ - public ByteBuffer getData() { + public ByteBuffer getByteData() { if (regions.size() == 0) { if (data == null) data = BufferUtils.createByteBuffer(0); } else { int regionsEnd = regions.get(regions.size() - 1).getEnd(); if (data == null) { data = BufferUtils.createByteBuffer(regionsEnd + 1); + ownsData = true; } else if (data.limit() <= regionsEnd) { + if (data.isReadOnly()) { + throw new IllegalStateException("Read-only BufferObject data is too small for its regions. " + + "Provide a direct ByteBuffer whose limit covers the last region, or use setByteData() " + + "so the BufferObject can own a mutable copy."); + } // new buffer ByteBuffer newData = BufferUtils.createByteBuffer(regionsEnd + 1); + newData.order(((ByteBuffer) data).order()); // copy old buffer in new buffer - if (newData.limit() < data.limit()) data.limit(newData.limit()); - newData.put(data); + ByteBuffer oldData = ((ByteBuffer) data).duplicate(); + oldData.clear(); + if (newData.limit() < oldData.limit()) oldData.limit(newData.limit()); + newData.put(oldData); // destroy old buffer - BufferUtils.destroyDirectBuffer(data); + if (ownsData) { + BufferUtils.destroyDirectBuffer(data); + } data = newData; + ownsData = true; } } - data.rewind(); - return data; + return (ByteBuffer) data; } /** @@ -237,6 +321,39 @@ public void unsetRegions() { regions.trimToSize(); } + /** + * Returns true when this buffer has explicit dirty/layout regions. + * + * @return true if regions are defined + */ + public boolean hasRegions() { + return !regions.isEmpty(); + } + + /** + * Adds a byte range that needs to be uploaded. + * + * @param start byte offset of the first dirty byte + * @param length number of dirty bytes + */ + public void addDirtyRegion(int start, int length) { + if (start < 0) { + throw new IllegalArgumentException("Region start cannot be negative"); + } + if (length <= 0) { + return; + } + BufferRegion region = new BufferRegion(start, start + length - 1); + region.bo = this; + region.markDirty(); + int insertIndex = 0; + while (insertIndex < regions.size() && regions.get(insertIndex).getStart() <= start) { + insertIndex++; + } + regions.add(insertIndex, region); + updateNeeded = true; + } + /** * Add a region at the end of the layout * @@ -245,6 +362,7 @@ public void unsetRegions() { public void setRegions(List lr) { regions.clear(); regions.addAll(lr); + regions.sort((a, b) -> Integer.compare(a.getStart(), b.getStart())); regions.trimToSize(); setUpdateNeeded(); } @@ -272,12 +390,13 @@ public void markAllRegionsDirty() { @Override public void resetObject() { this.id = -1; + setUpdateNeeded(); } @Override protected void deleteNativeBuffers() { super.deleteNativeBuffers(); - if (data != null) BufferUtils.destroyDirectBuffer(data); + if (data != null && ownsData) BufferUtils.destroyDirectBuffer(data); } @Override @@ -340,7 +459,12 @@ public void write(JmeExporter ex) throws IOException { oc.write(accessHint.ordinal(), "accessHint", 0); oc.write(natureHint.ordinal(), "natureHint", 0); oc.writeSavableArrayList(regions, "regions", null); - oc.write(data, "data", null); + ByteBuffer writeData = null; + if (data != null) { + writeData = ((ByteBuffer) data).duplicate(); + writeData.clear(); + } + oc.write(writeData, "data", null); } @Override @@ -361,7 +485,23 @@ public void read(JmeImporter im) throws IOException { public BufferObject clone() { BufferObject clone = (BufferObject) super.clone(); clone.binding = -1; - clone.data = BufferUtils.clone(data); + if (data instanceof ByteBuffer) { + ByteOrder order = ((ByteBuffer) data).order(); + ByteBuffer cloneSource = ((ByteBuffer) data).duplicate(); + cloneSource.order(order); + cloneSource.clear(); + clone.data = BufferUtils.clone(cloneSource); + ((ByteBuffer) clone.data).order(order); + clone.data.clear(); + clone.ownsData = true; + } else if (data != null) { + clone.data = BufferUtils.clone(data); + clone.data.clear(); + clone.ownsData = true; + } else { + clone.data = null; + clone.ownsData = true; + } clone.regions = new ArrayList(); assert clone.regions != regions; for (BufferRegion r : regions) { diff --git a/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferRegion.java b/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferRegion.java index b9e95c1189..6e8eb23790 100644 --- a/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferRegion.java +++ b/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferRegion.java @@ -65,12 +65,12 @@ public BufferRegion() { } /** - * Rewind and get a ByteBuffer pointing to this region of the main buffer - * + * Get a ByteBuffer slice pointing to this region of the main buffer. + * * @return ByteBuffer */ public ByteBuffer getData() { - ByteBuffer source = bo.getData(); + ByteBuffer source = bo.getByteData(); assert end < source.capacity() : "Can't set limit at " + end + " on capacity " + source.capacity(); ByteBuffer view = source.duplicate(); diff --git a/jme3-core/src/main/java/com/jme3/shader/bufferobject/DirtyRegionsIterator.java b/jme3-core/src/main/java/com/jme3/shader/bufferobject/DirtyRegionsIterator.java index efdfc5f2b9..960efec46c 100644 --- a/jme3-core/src/main/java/com/jme3/shader/bufferobject/DirtyRegionsIterator.java +++ b/jme3-core/src/main/java/com/jme3/shader/bufferobject/DirtyRegionsIterator.java @@ -48,7 +48,7 @@ private static class DirtyRegion extends BufferRegion { @Override public ByteBuffer getData() { - ByteBuffer source = bo.getData(); + ByteBuffer source = bo.getByteData(); ByteBuffer view = source.duplicate(); view.position(start); view.limit(end + 1); @@ -78,7 +78,12 @@ public boolean hasNext() { if (bufferObject.regions.size() == 0) { return pos == 0 && bufferObject.isUpdateNeeded(); } - return pos < bufferObject.regions.size(); + for (int i = pos; i < bufferObject.regions.size(); i++) { + if (bufferObject.regions.get(i).isDirty()) { + return true; + } + } + return false; } public BufferRegion next() { @@ -89,7 +94,7 @@ public BufferRegion next() { if (bufferObject.regions.size() == 0) { if (!bufferObject.isUpdateNeeded()) return null; dirtyRegion.fullBufferRegion = true; - dirtyRegion.end = bufferObject.getData().limit() - 1; + dirtyRegion.end = bufferObject.getByteData().limit() - 1; dirtyRegion.start = 0; pos = 1; return dirtyRegion; diff --git a/jme3-core/src/test/java/com/jme3/scene/VertexBufferTest.java b/jme3-core/src/test/java/com/jme3/scene/VertexBufferTest.java new file mode 100644 index 0000000000..7c4a6b51f3 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/scene/VertexBufferTest.java @@ -0,0 +1,422 @@ +/* + * Copyright (c) 2026 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 com.jme3.scene; + +import com.jme3.shader.bufferobject.BufferRegion; +import com.jme3.util.BufferUtils; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class VertexBufferTest { + + @Test + public void testCustomAttributeBuffer() { + Mesh mesh = new Mesh(); + FloatBuffer data = BufferUtils.createFloatBuffer(0f, 0f, 0f); + + mesh.setBuffer("inCustomData", 3, VertexBuffer.Format.Float, data); + + VertexBuffer vb = mesh.getBuffer("inCustomData"); + assertSame(vb, mesh.getBufferList().get(0)); + assertEquals(VertexBuffer.Type.Custom, vb.getBufferType()); + assertEquals("inCustomData", vb.getAttributeName()); + assertEquals("inCustomData", vb.getShaderAttributeName()); + assertThrows(IllegalArgumentException.class, () -> mesh.getBuffer(VertexBuffer.Type.Custom)); + } + + @Test + public void testNamedVertexBufferUsage() { + Mesh mesh = new Mesh(); + + mesh.setBuffer(VertexBuffer.Type.Position, 3, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(0f, 0f, 0f, 1f, 0f, 0f)); + mesh.setBuffer("inWeight", 1, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(0.25f, 0.75f)); + + VertexBuffer weights = mesh.getBuffer("inWeight"); + assertEquals(VertexBuffer.Type.Custom, weights.getBufferType()); + assertEquals("inWeight", weights.getShaderAttributeName()); + assertEquals(2, weights.getNumElements()); + + mesh.setBuffer("inWeight", 1, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(1f, 0f)); + + assertSame(weights, mesh.getBuffer("inWeight")); + assertEquals(1f, ((FloatBuffer) weights.getData()).get(0), 0f); + assertTrue(weights.isUpdateNeeded()); + + Mesh clone = mesh.deepClone(); + VertexBuffer clonedWeights = clone.getBuffer("inWeight"); + assertEquals(VertexBuffer.Type.Custom, clonedWeights.getBufferType()); + assertEquals("inWeight", clonedWeights.getShaderAttributeName()); + assertEquals(1f, ((FloatBuffer) clonedWeights.getData()).get(0), 0f); + + mesh.clearBuffer("inWeight"); + assertNull(mesh.getBuffer("inWeight")); + } + + @Test + public void testCloneWithOverrideTypeClearsCustomAttributeName() { + VertexBuffer custom = new VertexBuffer(VertexBuffer.Type.Custom); + custom.setAttributeName("inCustomData"); + custom.setupData(VertexBuffer.Usage.Dynamic, 3, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(0f, 0f, 0f)); + + VertexBuffer position = custom.clone(VertexBuffer.Type.Position); + + assertEquals(VertexBuffer.Type.Position, position.getBufferType()); + assertNull(position.getAttributeName()); + assertEquals("inPosition", position.getShaderAttributeName()); + } + + @Test + public void testMarkElementsDirtyUsesByteRanges() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Position); + vb.setupData(VertexBuffer.Usage.Dynamic, 3, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(0f, 0f, 0f, 1f, 1f, 1f, 2f, 2f, 2f)); + vb.clearUpdateNeeded(); + + vb.markElementsDirty(1, 1); + + BufferRegion region = vb.getDirtyRegions().next(); + assertTrue(vb.isUpdateNeeded()); + assertEquals(12, region.getStart()); + assertEquals(23, region.getEnd()); + assertEquals(12, region.length()); + } + + @Test + public void testDirtyRangesAreValidatedBeforeRendererUpload() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Position); + vb.setupData(VertexBuffer.Usage.Dynamic, 3, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(0f, 0f, 0f, 1f, 1f, 1f)); + + assertThrows(IllegalArgumentException.class, () -> vb.markBytesDirty(2, 4)); + assertThrows(IllegalArgumentException.class, () -> vb.markBytesDirty(24, 4)); + assertThrows(IllegalArgumentException.class, () -> vb.markElementsDirty(2, 1)); + } + + @Test + public void testTypedVertexBuffersCreateCachedByteData() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Position); + vb.setupData(VertexBuffer.Usage.Dynamic, 3, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(1f, 2f, 3f)); + + ByteBuffer first = vb.getByteData(); + first.position(first.limit()); + ByteBuffer second = vb.getByteData(); + + assertSame(first, second); + assertEquals(second.limit(), second.position()); + assertEquals(3 * Float.BYTES, second.limit()); + ByteBuffer read = second.duplicate(); + read.order(second.order()); + read.clear(); + assertEquals(1f, read.getFloat(), 0f); + assertEquals(2f, read.getFloat(), 0f); + assertEquals(3f, read.getFloat(), 0f); + } + + @Test + public void testTypedByteDataSupportsShortAndIntBuffers() { + VertexBuffer shorts = new VertexBuffer(VertexBuffer.Type.BoneIndex); + ShortBuffer shortData = BufferUtils.createShortBuffer(new short[] {1, 2, 3}); + shorts.setupData(VertexBuffer.Usage.Dynamic, 3, VertexBuffer.Format.Short, shortData); + ByteBuffer shortBytes = shorts.getByteData(); + + assertEquals(3 * Short.BYTES, shortBytes.limit()); + assertEquals((short) 1, shortBytes.getShort()); + assertEquals((short) 2, shortBytes.getShort()); + assertEquals((short) 3, shortBytes.getShort()); + + VertexBuffer ints = new VertexBuffer(VertexBuffer.Type.Index); + IntBuffer intData = BufferUtils.createIntBuffer(new int[] {4, 5, 6}); + ints.setupData(VertexBuffer.Usage.Dynamic, 3, VertexBuffer.Format.Int, intData); + ByteBuffer intBytes = ints.getByteData(); + + assertEquals(3 * Integer.BYTES, intBytes.limit()); + assertEquals(4, intBytes.getInt()); + assertEquals(5, intBytes.getInt()); + assertEquals(6, intBytes.getInt()); + } + + @Test + public void testTypedByteDataCacheIsInvalidatedWhenDataChanges() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Position); + vb.setupData(VertexBuffer.Usage.Dynamic, 1, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(1f, 2f)); + + ByteBuffer first = vb.getByteData(); + + vb.updateData(BufferUtils.createFloatBuffer(3f, 4f)); + ByteBuffer second = vb.getByteData(); + + assertTrue(first != second); + assertEquals(3f, second.getFloat(), 0f); + assertEquals(4f, second.getFloat(), 0f); + } + + @Test + public void testCopyElementsMarksOutputDirtyAndInvalidatesByteCache() { + VertexBuffer source = new VertexBuffer(VertexBuffer.Type.Position); + source.setupData(VertexBuffer.Usage.Dynamic, 3, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(1f, 2f, 3f, 4f, 5f, 6f)); + VertexBuffer target = new VertexBuffer(VertexBuffer.Type.Position); + target.setupData(VertexBuffer.Usage.Dynamic, 3, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(0f, 0f, 0f, 0f, 0f, 0f)); + + ByteBuffer cachedBytes = target.getByteData(); + target.clearUpdateNeeded(); + + source.copyElements(1, target, 0, 1); + + assertTrue(target.isUpdateNeeded()); + BufferRegion region = target.getDirtyRegions().next(); + assertEquals(0, region.getStart()); + assertEquals(11, region.getEnd()); + ByteBuffer updatedBytes = target.getByteData(); + assertTrue(cachedBytes != updatedBytes); + assertEquals(4f, updatedBytes.getFloat(0), 0f); + assertEquals(5f, updatedBytes.getFloat(Float.BYTES), 0f); + assertEquals(6f, updatedBytes.getFloat(2 * Float.BYTES), 0f); + } + + @Test + public void testSetByteDataConvertsByteDataToTypedVertexData() { + VertexBuffer floats = new VertexBuffer(VertexBuffer.Type.Position); + floats.setupData(VertexBuffer.Usage.Dynamic, 2, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(0f, 0f)); + ByteBuffer floatBytes = ByteBuffer.allocateDirect(2 * Float.BYTES).order(ByteOrder.LITTLE_ENDIAN); + floatBytes.putFloat(1.5f); + floatBytes.putFloat(2.5f); + floatBytes.flip(); + + floats.setByteData(floatBytes); + + assertEquals(0, floatBytes.position()); + FloatBuffer floatData = (FloatBuffer) floats.getData(); + assertEquals(2, floatData.limit()); + assertEquals(1.5f, floatData.get(0), 0f); + assertEquals(2.5f, floatData.get(1), 0f); + + VertexBuffer shorts = new VertexBuffer(VertexBuffer.Type.BoneIndex); + shorts.setupData(VertexBuffer.Usage.Dynamic, 2, VertexBuffer.Format.Short, + BufferUtils.createShortBuffer(new short[] {0, 0})); + ByteBuffer shortBytes = ByteBuffer.allocateDirect(2 * Short.BYTES).order(ByteOrder.LITTLE_ENDIAN); + shortBytes.putShort((short) 3); + shortBytes.putShort((short) 4); + shortBytes.flip(); + + shorts.setByteData(shortBytes); + + ShortBuffer shortData = (ShortBuffer) shorts.getData(); + assertEquals((short) 3, shortData.get(0)); + assertEquals((short) 4, shortData.get(1)); + + VertexBuffer ints = new VertexBuffer(VertexBuffer.Type.Index); + ints.setupData(VertexBuffer.Usage.Dynamic, 2, VertexBuffer.Format.Int, + BufferUtils.createIntBuffer(new int[] {0, 0})); + ByteBuffer intBytes = ByteBuffer.allocateDirect(2 * Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN); + intBytes.putInt(5); + intBytes.putInt(6); + intBytes.flip(); + + ints.setByteData(intBytes); + + IntBuffer intData = (IntBuffer) ints.getData(); + assertEquals(5, intData.get(0)); + assertEquals(6, intData.get(1)); + } + + @Test + public void testSetByteDataRejectsMisalignedByteData() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Position); + vb.setupData(VertexBuffer.Usage.Dynamic, 1, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(0f)); + + assertThrows(IllegalArgumentException.class, () -> vb.setByteData(BufferUtils.createByteBuffer(3))); + } + + @Test + public void testSetDataDelegatesToByteDataConversion() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Position); + vb.setupData(VertexBuffer.Usage.Dynamic, 1, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(0f)); + ByteBuffer bytes = ByteBuffer.allocateDirect(Float.BYTES); + bytes.putFloat(7f); + bytes.flip(); + + vb.setData(bytes); + + assertEquals(7f, ((FloatBuffer) vb.getData()).get(0), 0f); + } + + @Test + public void testUninitializedVertexBufferRejectsByteDataAccess() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Position); + + assertThrows(IllegalStateException.class, () -> vb.getByteData()); + } + + @Test + public void testByteBackedVertexBufferSetDataPreservesLayoutMetadata() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.BoneIndex); + vb.setupData(VertexBuffer.Usage.Dynamic, 4, VertexBuffer.Format.UnsignedByte, + BufferUtils.createByteBuffer(new byte[] {0, 0, 0, 0})); + vb.clearUpdateNeeded(); + + ByteBuffer source = BufferUtils.createByteBuffer(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}); + vb.setByteData(source); + + assertEquals(VertexBuffer.Format.UnsignedByte, vb.getFormat()); + assertEquals(4, vb.getNumComponents()); + assertEquals(2, vb.getNumElements()); + assertTrue(vb.hasDataSizeChanged()); + assertEquals(8, vb.getByteData().limit()); + } + + @Test + public void testByteBackedVertexBufferSetDataPreservesByteOrder() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.BoneIndex); + vb.setupData(VertexBuffer.Usage.Dynamic, 4, VertexBuffer.Format.UnsignedByte, + BufferUtils.createByteBuffer(new byte[] {0, 0, 0, 0})); + + ByteBuffer source = ByteBuffer.allocateDirect(4).order(ByteOrder.LITTLE_ENDIAN); + source.putInt(0x11223344); + source.flip(); + + vb.setByteData(source); + + assertEquals(ByteOrder.LITTLE_ENDIAN, vb.getByteData().order()); + assertEquals(0x11223344, vb.getByteData().getInt()); + } + + @Test + public void testByteBackedVertexBufferCanUseByteDataPointer() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.BoneIndex); + vb.setupData(VertexBuffer.Usage.Dynamic, 4, VertexBuffer.Format.UnsignedByte, + BufferUtils.createByteBuffer(new byte[] {0, 0, 0, 0})); + ByteBuffer source = BufferUtils.createByteBuffer(new byte[] {1, 2, 3, 4}); + + vb.setByteDataPointer(source); + + assertSame(source, vb.getData()); + source.put(0, (byte) 9); + assertEquals((byte) 9, vb.getByteData().get()); + } + + @Test + public void testTypedVertexBufferRejectsByteDataPointer() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Position); + vb.setupData(VertexBuffer.Usage.Dynamic, 1, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(0f)); + + assertThrows(UnsupportedOperationException.class, + () -> vb.setByteDataPointer(BufferUtils.createByteBuffer(Float.BYTES))); + } + + @Test + public void testByteDataPointerRejectsHeapVertexBuffers() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.BoneIndex); + vb.setupData(VertexBuffer.Usage.Dynamic, 4, VertexBuffer.Format.UnsignedByte, + BufferUtils.createByteBuffer(new byte[] {0, 0, 0, 0})); + + assertThrows(IllegalArgumentException.class, () -> vb.setByteDataPointer(ByteBuffer.allocate(4))); + } + + @Test + public void testByteDataPointerAllowsReadOnlyVertexBuffers() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.BoneIndex); + vb.setupData(VertexBuffer.Usage.Dynamic, 4, VertexBuffer.Format.UnsignedByte, + BufferUtils.createByteBuffer(new byte[] {0, 0, 0, 0})); + ByteBuffer source = ByteBuffer.allocateDirect(4); + source.put(new byte[] {1, 2, 3, 4}); + source.clear(); + + vb.setByteDataPointer(source.asReadOnlyBuffer()); + + assertTrue(vb.getByteData().isReadOnly()); + assertEquals((byte) 1, vb.getByteData().get(0)); + } + + @Test + public void testHalfBuffersUseTwoBytesPerComponent() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.TexCoord); + ByteBuffer data = BufferUtils.createByteBuffer(6); + data.putShort((short) 0); + data.putShort((short) 0); + data.putShort((short) 0); + data.clear(); + + vb.setupData(VertexBuffer.Usage.Dynamic, 1, VertexBuffer.Format.Half, data); + assertTrue(vb.invariant()); + + vb.clearUpdateNeeded(); + vb.setElementComponent(1, 0, (short) 0x3c00); + + assertEquals((short) 0x3c00, vb.getElementComponent(1, 0)); + BufferRegion region = vb.getDirtyRegions().next(); + assertEquals(2, region.getStart()); + assertEquals(3, region.getEnd()); + + vb.compact(2); + assertEquals(4, vb.getData().limit()); + } + + @Test + public void testByteBackedCompactPreservesByteOrder() { + VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.TexCoord); + ByteBuffer data = ByteBuffer.allocateDirect(6).order(ByteOrder.LITTLE_ENDIAN); + data.putShort((short) 0x0102); + data.putShort((short) 0x0304); + data.putShort((short) 0x0506); + data.clear(); + + vb.setupData(VertexBuffer.Usage.Dynamic, 1, VertexBuffer.Format.Half, data); + vb.compact(2); + + ByteBuffer compacted = (ByteBuffer) vb.getData(); + assertEquals(ByteOrder.LITTLE_ENDIAN, compacted.order()); + assertEquals((short) 0x0102, compacted.getShort(0)); + assertEquals((short) 0x0304, compacted.getShort(2)); + } +} diff --git a/jme3-core/src/test/java/com/jme3/scene/mesh/MeshTest.java b/jme3-core/src/test/java/com/jme3/scene/mesh/MeshTest.java index 030f545f2b..a7c72497ee 100644 --- a/jme3-core/src/test/java/com/jme3/scene/mesh/MeshTest.java +++ b/jme3-core/src/test/java/com/jme3/scene/mesh/MeshTest.java @@ -33,10 +33,15 @@ package com.jme3.scene.mesh; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.nio.ByteBuffer; import org.junit.jupiter.api.Test; import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer; +import com.jme3.util.BufferUtils; /** * Tests selected methods of the Mesh class. @@ -54,4 +59,30 @@ public void testVertexCountOfEmptyMesh() { assertEquals(-1, mesh.getVertexCount()); } + + /** + * Tests interleaving half-float vertex buffers, which are byte-backed. + */ + @Test + @SuppressWarnings("deprecation") + public void testInterleavedHalfBuffer() { + final Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, VertexBuffer.Format.Float, + BufferUtils.createFloatBuffer(0f, 0f, 0f, 1f, 1f, 1f)); + + ByteBuffer halfTexCoords = BufferUtils.createByteBuffer(8); + halfTexCoords.putShort((short) 0); + halfTexCoords.putShort((short) 0); + halfTexCoords.putShort((short) 0x3c00); + halfTexCoords.putShort((short) 0x3c00); + halfTexCoords.clear(); + mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, VertexBuffer.Format.Half, halfTexCoords); + + mesh.setInterleaved(); + + VertexBuffer interleaved = mesh.getBuffer(VertexBuffer.Type.InterleavedData); + assertNotNull(interleaved); + assertEquals(VertexBuffer.Format.UnsignedByte, interleaved.getFormat()); + assertEquals(32, interleaved.getData().limit()); + } } diff --git a/jme3-core/src/test/java/com/jme3/shader/bufferobject/DirtyRegionsIteratorTest.java b/jme3-core/src/test/java/com/jme3/shader/bufferobject/DirtyRegionsIteratorTest.java index a4b4c394bb..ee3bb5ae55 100644 --- a/jme3-core/src/test/java/com/jme3/shader/bufferobject/DirtyRegionsIteratorTest.java +++ b/jme3-core/src/test/java/com/jme3/shader/bufferobject/DirtyRegionsIteratorTest.java @@ -3,6 +3,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.ByteBuffer; @@ -56,12 +58,13 @@ public void testMergedDirtyRegionSliceUsesInclusiveEnd() { @Test public void testMergedDirtyRegionGetDataUsesZeroBasedSliceAndPreservesOrder() { BufferObject bo = new BufferObject(); + ByteBuffer source = ByteBuffer.allocateDirect(12).order(ByteOrder.LITTLE_ENDIAN); + bo.setByteDataPointer(source); bo.setRegions(Arrays.asList( new BufferRegion(0, 3), new BufferRegion(4, 7), new BufferRegion(8, 11))); - bo.getData().order(ByteOrder.LITTLE_ENDIAN); bo.getRegion(0).markDirty(); bo.getRegion(1).markDirty(); bo.getRegion(2).clearDirty(); @@ -75,6 +78,40 @@ public void testMergedDirtyRegionGetDataUsesZeroBasedSliceAndPreservesOrder() { assertEquals(ByteOrder.LITTLE_ENDIAN, data.order()); } + @Test + public void testAddedDirtyRegionsAreKeptSorted() { + BufferObject bo = new BufferObject(); + bo.initializeEmpty(16); + bo.clearUpdateNeeded(); + + bo.addDirtyRegion(8, 4); + bo.addDirtyRegion(0, 4); + + BufferRegion region = bo.getDirtyRegions().next(); + assertNotNull(region); + assertEquals(0, region.getStart()); + assertEquals(11, region.getEnd()); + assertEquals(12, region.getData().remaining()); + } + + @Test + public void testSetRegionsSortsRanges() { + BufferObject bo = new BufferObject(); + bo.initializeEmpty(16); + bo.setRegions(Arrays.asList( + new BufferRegion(8, 11), + new BufferRegion(0, 3))); + bo.getRegion(0).markDirty(); + bo.getRegion(1).markDirty(); + bo.setUpdateNeeded(false); + + BufferRegion region = bo.getDirtyRegions().next(); + assertNotNull(region); + assertEquals(0, region.getStart()); + assertEquals(11, region.getEnd()); + assertEquals(12, region.getData().remaining()); + } + @Test public void testNoRegionsHasNextContract() { BufferObject bo = new BufferObject(); @@ -90,6 +127,43 @@ public void testNoRegionsHasNextContract() { assertEquals(4, region.getData().remaining()); } + @Test + public void testRegionHasNextSkipsCleanRegions() { + BufferObject bo = new BufferObject(); + bo.initializeEmpty(12); + bo.setRegions(Arrays.asList( + new BufferRegion(0, 3), + new BufferRegion(4, 7), + new BufferRegion(8, 11))); + bo.getRegion(0).clearDirty(); + bo.getRegion(1).clearDirty(); + bo.getRegion(2).markDirty(); + bo.setUpdateNeeded(false); + + DirtyRegionsIterator iterator = bo.getDirtyRegions(); + assertTrue(iterator.hasNext()); + BufferRegion region = iterator.next(); + assertEquals(8, region.getStart()); + assertEquals(11, region.getEnd()); + region.clearDirty(); + assertFalse(iterator.hasNext()); + } + + @Test + public void testRegionHasNextReturnsFalseWhenAllRegionsClean() { + BufferObject bo = new BufferObject(); + bo.initializeEmpty(8); + bo.setRegions(Arrays.asList( + new BufferRegion(0, 3), + new BufferRegion(4, 7))); + bo.getRegion(0).clearDirty(); + bo.getRegion(1).clearDirty(); + bo.setUpdateNeeded(false); + + DirtyRegionsIterator iterator = bo.getDirtyRegions(); + assertFalse(iterator.hasNext()); + } + @Test public void testSetDataHandlesSelfAlias() { BufferObject bo = new BufferObject(); @@ -99,12 +173,12 @@ public void testSetDataHandlesSelfAlias() { source.put((byte) 0x33); source.put((byte) 0x44); source.flip(); - bo.setData(source); + bo.setByteData(source); - ByteBuffer sameBuffer = bo.getData(); - bo.setData(sameBuffer); + ByteBuffer sameBuffer = bo.getByteData(); + bo.setByteData(sameBuffer); - ByteBuffer result = bo.getData(); + ByteBuffer result = bo.getByteData(); assertEquals(4, result.remaining()); assertEquals((byte) 0x11, result.get()); assertEquals((byte) 0x22, result.get()); @@ -112,25 +186,174 @@ public void testSetDataHandlesSelfAlias() { assertEquals((byte) 0x44, result.get()); } + @Test + public void testSetDataPreservesByteOrder() { + BufferObject bo = new BufferObject(); + ByteBuffer source = ByteBuffer.allocateDirect(4).order(ByteOrder.LITTLE_ENDIAN); + source.putInt(0x11223344); + source.flip(); + + bo.setByteData(source); + + assertEquals(ByteOrder.LITTLE_ENDIAN, bo.getByteData().order()); + assertEquals(0x11223344, bo.getByteData().getInt()); + } + + @Test + public void testSetDataDoesNotAdvanceSourcePosition() { + BufferObject bo = new BufferObject(); + ByteBuffer source = ByteBuffer.allocateDirect(4); + source.put(new byte[]{1, 2, 3, 4}); + source.flip(); + + bo.setByteData(source); + + assertEquals(0, source.position()); + assertEquals(4, source.limit()); + assertEquals(4, bo.getByteData().remaining()); + } + + @Test + public void testGetByteDataDoesNotRewindBackingBuffer() { + BufferObject bo = new BufferObject(); + ByteBuffer source = ByteBuffer.allocateDirect(4); + source.put(new byte[]{1, 2, 3, 4}); + source.flip(); + bo.setByteData(source); + + ByteBuffer data = bo.getByteData(); + data.position(2); + + assertSame(data, bo.getByteData()); + assertEquals(2, bo.getByteData().position()); + } + + @Test + public void testRegionResizeCopiesWholeBufferAndPreservesOrder() { + BufferObject bo = new BufferObject(); + ByteBuffer source = ByteBuffer.allocateDirect(4).order(ByteOrder.LITTLE_ENDIAN); + source.putInt(0x11223344); + source.flip(); + bo.setByteData(source); + + bo.setRegions(Arrays.asList(new BufferRegion(0, 7))); + ByteBuffer resized = bo.getByteData(); + + assertEquals(ByteOrder.LITTLE_ENDIAN, resized.order()); + assertEquals(8, resized.limit()); + assertEquals(0x11223344, resized.getInt(0)); + } + + @Test + public void testClonePreservesByteOrder() { + BufferObject bo = new BufferObject(); + ByteBuffer source = ByteBuffer.allocateDirect(4).order(ByteOrder.LITTLE_ENDIAN); + source.putInt(0x11223344); + source.flip(); + bo.setByteData(source); + + BufferObject clone = bo.clone(); + + assertEquals(ByteOrder.LITTLE_ENDIAN, clone.getByteData().order()); + assertEquals(0x11223344, clone.getByteData().getInt()); + } + @Test public void testSetDataNullClearsBuffer() { BufferObject bo = new BufferObject(); ByteBuffer source = ByteBuffer.allocateDirect(4); source.put(new byte[]{1, 2, 3, 4}); source.flip(); - bo.setData(source); - assertEquals(4, bo.getData().remaining()); + bo.setByteData(source); + assertEquals(4, bo.getByteData().remaining()); - bo.setData(null); + bo.setByteData(null); // getData() auto-allocates an empty buffer when internal data is null - assertEquals(0, bo.getData().remaining()); + assertEquals(0, bo.getByteData().remaining()); } @Test public void testSetDataNullOnEmptyBufferObject() { BufferObject bo = new BufferObject(); // Should not throw when internal buffer is already null - bo.setData(null); - assertEquals(0, bo.getData().remaining()); + bo.setByteData(null); + assertEquals(0, bo.getByteData().remaining()); + } + + @Test + public void testSetDataDelegatesToByteData() { + BufferObject bo = new BufferObject(); + ByteBuffer source = ByteBuffer.allocateDirect(4); + source.put(new byte[]{1, 2, 3, 4}); + source.flip(); + + bo.setData(source); + + assertEquals(4, bo.getByteData().remaining()); + assertEquals(0, source.position()); + } + + @Test + public void testSetByteDataPointerDoesNotCopy() { + BufferObject bo = new BufferObject(); + ByteBuffer source = ByteBuffer.allocateDirect(4).order(ByteOrder.LITTLE_ENDIAN); + source.putInt(0x11223344); + source.clear(); + + bo.setByteDataPointer(source); + + assertSame(source, bo.getByteData()); + source.putInt(0, 0x55667788); + assertEquals(0x55667788, bo.getByteData().getInt()); + } + + @Test + public void testSetByteDataPointerRejectsHeapBuffers() { + BufferObject bo = new BufferObject(); + + assertThrows(IllegalArgumentException.class, () -> bo.setByteDataPointer(ByteBuffer.allocate(4))); + } + + @Test + public void testSetByteDataPointerAllowsReadOnlyBuffers() { + BufferObject bo = new BufferObject(); + ByteBuffer source = ByteBuffer.allocateDirect(4); + source.put(new byte[]{1, 2, 3, 4}); + source.clear(); + + bo.setByteDataPointer(source.asReadOnlyBuffer()); + + assertTrue(bo.getByteData().isReadOnly()); + assertEquals((byte) 1, bo.getByteData().get(0)); + } + + @Test + public void testByteDataPointerIsNotDestroyedWhenReplaced() { + BufferObject bo = new BufferObject(); + ByteBuffer external = ByteBuffer.allocateDirect(4); + external.putInt(0x11223344); + external.clear(); + bo.setByteDataPointer(external); + + ByteBuffer ownedReplacement = ByteBuffer.allocateDirect(4); + ownedReplacement.putInt(0x55667788); + ownedReplacement.flip(); + bo.setByteData(ownedReplacement); + + external.putInt(0, 0x01020304); + assertEquals(0x01020304, external.getInt(0)); + assertEquals(0x55667788, bo.getByteData().getInt(0)); + } + + @Test + public void testReadOnlyReferenceMustCoverRegions() { + BufferObject bo = new BufferObject(); + ByteBuffer source = ByteBuffer.allocateDirect(4); + source.put(new byte[]{1, 2, 3, 4}); + source.clear(); + bo.setByteDataPointer(source.asReadOnlyBuffer()); + bo.setRegions(Arrays.asList(new BufferRegion(0, 7))); + + assertThrows(IllegalStateException.class, bo::getByteData); } } diff --git a/jme3-core/src/test/java/com/jme3/util/StructTest.java b/jme3-core/src/test/java/com/jme3/util/StructTest.java index 4752842657..949ab23c15 100644 --- a/jme3-core/src/test/java/com/jme3/util/StructTest.java +++ b/jme3-core/src/test/java/com/jme3/util/StructTest.java @@ -87,11 +87,14 @@ public void testStd140Serialization() { Std140Layout layout = new Std140Layout(); BufferObject bo = new BufferObject(); StructUtils.setStd140BufferLayout(fields, layout, bo); - System.out.println(bo.getData().getInt()); + ByteBuffer debugData = bo.getByteData().duplicate(); + debugData.clear(); + System.out.println(debugData.getInt()); StructUtils.updateBufferData(fields, false, layout, bo); - ByteBuffer bbf = bo.getData(); + ByteBuffer bbf = bo.getByteData().duplicate(); + bbf.clear(); String expectedData = "100 0 0 0 0 0 -56 66 0 0 0 0 0 0 0 0 0 0 -56 66 0 0 0 0 0 0 0 0 0 0 0 0 0 0 72 67 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -106 67 0 0 0 0 0 0 0 0 0 0 0 0 100 0 0 0 0 0 -56 66 0 0 0 0 0 0 0 0 100 0 0 0 0 0 -56 66 0 0 0 0 0 0 0 0 100 0 0 0 0 0 -56 66 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "; String actualData = ""; @@ -110,9 +113,9 @@ public void testStd140PartialUpdate() { java.util.List> fields = StructUtils.getFields(test); StructUtils.setStd140BufferLayout(fields, layout, bo); - int bolength = bo.getData().limit(); + int bolength = bo.getByteData().limit(); assertEquals(128, bolength); - assertEquals(128, bo.getData().capacity()); + assertEquals(128, bo.getByteData().capacity()); int nUpdated; @@ -256,7 +259,7 @@ public void testStd430ArrayPacking() { BufferObject std140Bo = new BufferObject(); StructUtils.setBufferLayout(fields, new Std140Layout(), std140Bo); - assertEquals(64, std140Bo.getData().limit()); + assertEquals(64, std140Bo.getByteData().limit()); assertEquals(0, std140Bo.getRegion(0).getStart()); assertEquals(47, std140Bo.getRegion(0).getEnd()); assertEquals(48, std140Bo.getRegion(1).getStart()); @@ -264,14 +267,14 @@ public void testStd430ArrayPacking() { BufferObject std430Bo = new BufferObject(); StructUtils.setBufferLayout(fields, new Std430Layout(), std430Bo); - assertEquals(16, std430Bo.getData().limit()); + assertEquals(16, std430Bo.getByteData().limit()); assertEquals(0, std430Bo.getRegion(0).getStart()); assertEquals(11, std430Bo.getRegion(0).getEnd()); assertEquals(12, std430Bo.getRegion(1).getStart()); assertEquals(15, std430Bo.getRegion(1).getEnd()); StructUtils.updateBufferData(fields, false, new Std430Layout(), std430Bo); - ByteBuffer data = std430Bo.getData(); + ByteBuffer data = std430Bo.getByteData(); assertEquals(1f, data.getFloat(0)); assertEquals(2f, data.getFloat(4)); assertEquals(3f, data.getFloat(8)); @@ -287,7 +290,7 @@ public void testStd430BufferObjectSerializationRestoresLayout() throws IOExcepti StructStd430BufferObject copy = (StructStd430BufferObject) BinaryImporter.getInstance() .load(new ByteArrayInputStream(output.toByteArray())); - assertEquals(16, copy.getData().limit()); + assertEquals(16, copy.getByteData().limit()); assertEquals(0, copy.getRegion(0).getStart()); assertEquals(11, copy.getRegion(0).getEnd()); assertEquals(12, copy.getRegion(1).getStart()); diff --git a/jme3-examples/src/main/java/jme3test/scene/TestNamedVertexBuffer.java b/jme3-examples/src/main/java/jme3test/scene/TestNamedVertexBuffer.java new file mode 100644 index 0000000000..9fe645ea46 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/scene/TestNamedVertexBuffer.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2026 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 jme3test.scene; + +import com.jme3.app.SimpleApplication; +import com.jme3.material.Material; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer; +import com.jme3.system.AppSettings; +import com.jme3.util.BufferUtils; + +import java.nio.FloatBuffer; + +/** + * Demonstrates a named vertex buffer consumed by a shader attribute. + */ +public class TestNamedVertexBuffer extends SimpleApplication { + + private static final String HEAT_ATTRIBUTE = "inHeat"; + + private VertexBuffer heatBuffer; + private FloatBuffer heatData; + private float time; + + public static void main(String[] args) { + TestNamedVertexBuffer app = new TestNamedVertexBuffer(); + AppSettings settings = new AppSettings(true); + settings.setTitle("Named VertexBuffer Shader Attribute"); + app.setSettings(settings); + app.start(); + } + + @Override + public void simpleInitApp() { + flyCam.setEnabled(false); + cam.setLocation(new Vector3f(0f, 0f, 4f)); + cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + + Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer( + -1.4f, -1.0f, 0f, + 1.4f, -1.0f, 0f, + 1.4f, 1.0f, 0f, + -1.4f, 1.0f, 0f)); + mesh.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createShortBuffer( + (short) 0, (short) 1, (short) 2, + (short) 0, (short) 2, (short) 3)); + + heatData = BufferUtils.createFloatBuffer(0f, 0.35f, 1f, 0.65f); + mesh.setBuffer(HEAT_ATTRIBUTE, 1, VertexBuffer.Format.Float, heatData); + heatBuffer = mesh.getBuffer(HEAT_ATTRIBUTE); + mesh.updateBound(); + + Geometry geometry = new Geometry("Named vertex buffer quad", mesh); + geometry.setMaterial(new Material(assetManager, "jme3test/vertexbuffer/NamedVertexBuffer.j3md")); + rootNode.attachChild(geometry); + } + + @Override + public void simpleUpdate(float tpf) { + time += tpf; + for (int i = 0; i < heatData.limit(); i++) { + heatData.put(i, 0.5f + 0.5f * FastMath.sin(time + i * FastMath.HALF_PI)); + } + heatBuffer.markElementsDirty(0, heatData.limit()); + } +} diff --git a/jme3-examples/src/main/java/jme3test/stress/TestVertexBufferParticleBenchmark.java b/jme3-examples/src/main/java/jme3test/stress/TestVertexBufferParticleBenchmark.java new file mode 100644 index 0000000000..726ac270e8 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/stress/TestVertexBufferParticleBenchmark.java @@ -0,0 +1,172 @@ +package jme3test.stress; + +import com.jme3.app.SimpleApplication; +import com.jme3.bounding.BoundingBox; +import com.jme3.font.BitmapText; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer; +import com.jme3.system.AppSettings; +import com.jme3.util.BufferUtils; +import java.nio.FloatBuffer; + +public class TestVertexBufferParticleBenchmark extends SimpleApplication implements ActionListener { + + private static final int DEFAULT_PARTICLES = 250_000; + private static final int DEFAULT_MOVING_PARTICLES = 25_000; + + private final int particleCount = Integer.getInteger("particle.count", DEFAULT_PARTICLES); + private final int movingCount = Math.min(Integer.getInteger("particle.moving", DEFAULT_MOVING_PARTICLES), particleCount); + private final float[] x = new float[particleCount]; + private final float[] y = new float[particleCount]; + private final float[] vx = new float[particleCount]; + private final float[] vy = new float[particleCount]; + + private Mesh mesh; + private VertexBuffer positionBuffer; + private FloatBuffer positions; + private BitmapText hud; + private boolean partialUpdates = Boolean.parseBoolean(System.getProperty("particle.partial", "true")); + private int movingStart; + private float statsTime; + private int statsFrames; + private float fps; + + public static void main(String[] args) { + TestVertexBufferParticleBenchmark app = new TestVertexBufferParticleBenchmark(); + AppSettings settings = new AppSettings(true); + settings.setTitle("Partial VertexBuffer Particle Benchmark"); + settings.setResolution(1280, 720); + app.setSettings(settings); + app.setShowSettings(false); + app.start(); + } + + @Override + public void simpleInitApp() { + flyCam.setMoveSpeed(200f); + cam.setLocation(new Vector3f(0f, 0f, 260f)); + initParticles(); + initMesh(); + initHud(); + + inputManager.addMapping("TogglePartial", new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addListener(this, "TogglePartial"); + } + + @Override + public void simpleUpdate(float tpf) { + updateParticles(tpf); + updateStats(tpf); + } + + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed && "TogglePartial".equals(name)) { + partialUpdates = !partialUpdates; + } + } + + private void initParticles() { + int columns = (int) Math.ceil(Math.sqrt(particleCount)); + float spacing = 0.45f; + float half = columns * spacing * 0.5f; + + for (int i = 0; i < particleCount; i++) { + x[i] = (i % columns) * spacing - half; + y[i] = (i / columns) * spacing - half; + vx[i] = 15f + (i % 37) * 0.17f; + vy[i] = 10f + (i % 53) * 0.13f; + } + } + + private void initMesh() { + positions = BufferUtils.createFloatBuffer(particleCount * 3); + for (int i = 0; i < particleCount; i++) { + putPosition(i); + } + + mesh = new Mesh(); + mesh.setMode(Mesh.Mode.Points); + positionBuffer = new VertexBuffer(VertexBuffer.Type.Position); + positionBuffer.setupData(VertexBuffer.Usage.Stream, 3, VertexBuffer.Format.Float, positions); + mesh.setBuffer(positionBuffer); + mesh.setBound(new BoundingBox(Vector3f.ZERO, 140f, 140f, 20f)); + mesh.updateCounts(); + + Geometry geometry = new Geometry("particles", mesh); + Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + material.setColor("Color", ColorRGBA.Cyan); + geometry.setMaterial(material); + geometry.setQueueBucket(RenderQueue.Bucket.Opaque); + rootNode.attachChild(geometry); + } + + private void initHud() { + guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt"); + hud = new BitmapText(guiFont); + hud.setSize(18f); + hud.setLocalTranslation(12f, settings.getHeight() - 12f, 0f); + guiNode.attachChild(hud); + } + + private void updateParticles(float tpf) { + int start = movingStart; + for (int n = 0; n < movingCount; n++) { + int i = (start + n) % particleCount; + x[i] += vx[i] * tpf; + y[i] += vy[i] * tpf; + + if (x[i] > 120f || x[i] < -120f) { + vx[i] = -vx[i]; + } + if (y[i] > 120f || y[i] < -120f) { + vy[i] = -vy[i]; + } + putPosition(i); + } + + if (partialUpdates) { + int firstLen = Math.min(movingCount, particleCount - start); + positionBuffer.markElementsDirty(start, firstLen); + if (firstLen < movingCount) { + positionBuffer.markElementsDirty(0, movingCount - firstLen); + } + } else { + positionBuffer.updateData(positions); + } + + movingStart = (movingStart + movingCount) % particleCount; + } + + private void putPosition(int particle) { + int offset = particle * 3; + positions.put(offset, x[particle]); + positions.put(offset + 1, y[particle]); + positions.put(offset + 2, 0f); + } + + private void updateStats(float tpf) { + statsTime += tpf; + statsFrames++; + if (statsTime >= 0.5f) { + fps = statsFrames / statsTime; + statsTime = 0f; + statsFrames = 0; + } + + hud.setText("Particles: " + particleCount + + " Moving/frame: " + movingCount + + " Upload: " + (partialUpdates ? "partial dirty regions" : "full updateData") + + " FPS: " + FastMath.floor(fps) + + "\nSPACE toggles partial/full. Set -Dparticle.count, -Dparticle.moving, -Dparticle.partial."); + } +} diff --git a/jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.frag b/jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.frag new file mode 100644 index 0000000000..498d288524 --- /dev/null +++ b/jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.frag @@ -0,0 +1,9 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +varying float heat; + +void main() { + vec3 cold = vec3(0.05, 0.25, 1.0); + vec3 hot = vec3(1.0, 0.15, 0.02); + gl_FragColor = vec4(mix(cold, hot, clamp(heat, 0.0, 1.0)), 1.0); +} diff --git a/jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.j3md b/jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.j3md new file mode 100644 index 0000000000..bc5e998810 --- /dev/null +++ b/jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.j3md @@ -0,0 +1,11 @@ +MaterialDef NamedVertexBuffer { + + Technique { + VertexShader GLSL150 GLSL100: jme3test/vertexbuffer/NamedVertexBuffer.vert + FragmentShader GLSL150 GLSL100: jme3test/vertexbuffer/NamedVertexBuffer.frag + + WorldParameters { + WorldViewProjectionMatrix + } + } +} diff --git a/jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.vert b/jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.vert new file mode 100644 index 0000000000..7da16d553c --- /dev/null +++ b/jme3-examples/src/main/resources/jme3test/vertexbuffer/NamedVertexBuffer.vert @@ -0,0 +1,13 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform mat4 g_WorldViewProjectionMatrix; + +attribute vec3 inPosition; +attribute float inHeat; + +varying float heat; + +void main() { + heat = inHeat; + gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); +} From 7c90a1cb0b02960f268bd2c9df8cc14efc6d10a0 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 10 Jun 2026 16:18:40 +0200 Subject: [PATCH 11/15] fix bufferobject bindings and reserve engine binds --- .../main/java/com/jme3/renderer/Limits.java | 8 + .../java/com/jme3/renderer/RenderContext.java | 30 ++- .../main/java/com/jme3/renderer/Renderer.java | 16 ++ .../com/jme3/renderer/opengl/GLRenderer.java | 222 ++++++++++++++---- .../src/main/java/com/jme3/shader/Shader.java | 2 +- .../bufferobject/BufferBindingPoints.java | 111 +++++++++ .../java/com/jme3/system/NullRenderer.java | 10 + .../GLRendererBufferBlockBindingTest.java | 170 ++++++++++++++ .../bufferobject/BufferBindingPointsTest.java | 69 ++++++ 9 files changed, 582 insertions(+), 56 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferBindingPoints.java create mode 100644 jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererBufferBlockBindingTest.java create mode 100644 jme3-core/src/test/java/com/jme3/shader/bufferobject/BufferBindingPointsTest.java diff --git a/jme3-core/src/main/java/com/jme3/renderer/Limits.java b/jme3-core/src/main/java/com/jme3/renderer/Limits.java index 4e97843566..0b4badf81c 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Limits.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Limits.java @@ -100,6 +100,10 @@ public enum Limits { TextureAnisotropy, // UBO + /** + * Maximum number of UBO binding points. + */ + UniformBufferObjectMaxBindings, /** * Maximum number of UBOs that may be accessed by a vertex shader. */ @@ -118,6 +122,10 @@ public enum Limits { UniformBufferObjectMaxBlockSize, // SSBO + /** + * Maximum number of SSBO binding points. + */ + ShaderStorageBufferObjectMaxBindings, /** * Maximum size of an SSBO. */ diff --git a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java index 15a202a6a5..0559a44d3c 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java @@ -55,11 +55,6 @@ private static WeakReference[] newWeakReferenceArray(int size) { */ public static final int maxTextureUnits = 16; - /** - * Number of buffer object units that JME supports. - */ - public static final int maxBufferObjectUnits = 8; - /** * Criteria for culling faces. * @@ -274,12 +269,29 @@ private static WeakReference[] newWeakReferenceArray(int size) { /** - * Current bound buffer object IDs for each buffer object unit. + * Current bound uniform buffer objects for each UBO binding point. + * + * @see Renderer#setUniformBufferObject(int, com.jme3.shader.bufferobject.BufferObject) + */ + public WeakReference[] boundUniformBuffers = newWeakReferenceArray(0); + + /** + * Current bound shader storage buffer objects for each SSBO binding point. + * + * @see Renderer#setShaderStorageBufferObject(int, com.jme3.shader.bufferobject.BufferObject) + */ + public WeakReference[] boundShaderStorageBuffers = newWeakReferenceArray(0); + + /** + * Resizes buffer object binding caches from runtime GL limits. * - * @see Renderer#setUniformBufferObject(int, com.jme3.shader.BufferObject) - * @see Renderer#setShaderStorageBufferObject(int, com.jme3.shader.BufferObject) + * @param uniformBufferBindings number of UBO binding points + * @param shaderStorageBufferBindings number of SSBO binding points */ - public final WeakReference[] boundBO = newWeakReferenceArray(maxBufferObjectUnits); + public void resetBufferObjectBindings(int uniformBufferBindings, int shaderStorageBufferBindings) { + boundUniformBuffers = newWeakReferenceArray(Math.max(0, uniformBufferBindings)); + boundShaderStorageBuffers = newWeakReferenceArray(Math.max(0, shaderStorageBufferBindings)); + } /** * IDList for texture units. diff --git a/jme3-core/src/main/java/com/jme3/renderer/Renderer.java b/jme3-core/src/main/java/com/jme3/renderer/Renderer.java index 7daec000c1..ebcac73660 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/Renderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/Renderer.java @@ -326,6 +326,22 @@ public void setTexture(int unit, Texture tex) * @param bo the buffer object to upload. */ public void updateUniformBufferObjectData(BufferObject bo); + + /** + * Reads data from a GPU shader storage buffer object. + * + * @param bo the buffer object to read from + * @param store destination buffer + */ + public void readShaderStorageBufferObjectData(BufferObject bo, ByteBuffer store); + + /** + * Reads data from a GPU uniform buffer object. + * + * @param bo the buffer object to read from + * @param store destination buffer + */ + public void readUniformBufferObjectData(BufferObject bo, ByteBuffer store); /** * Deletes a vertex buffer from the GPU. 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 7344f24013..eb70389c08 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 @@ -50,6 +50,7 @@ import com.jme3.system.JmeSystem; import com.jme3.system.Platform; import com.jme3.shader.ShaderBufferBlock.BufferType; +import com.jme3.shader.bufferobject.BufferBindingPoints; import com.jme3.shader.bufferobject.BufferObject; import com.jme3.shader.bufferobject.BufferRegion; import com.jme3.shader.bufferobject.DirtyRegionsIterator; @@ -703,6 +704,8 @@ && hasExtension("GL_ARB_half_float_pixel")) if (hasExtension("GL_ARB_shader_storage_buffer_object") || caps.contains(Caps.OpenGL43) || caps.contains(Caps.OpenGLES31)) { caps.add(Caps.ShaderStorageBufferObject); + limits.put(Limits.ShaderStorageBufferObjectMaxBindings, + getInteger(GL4.GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS)); limits.put(Limits.ShaderStorageBufferObjectMaxBlockSize, getInteger(GL4.GL_MAX_SHADER_STORAGE_BLOCK_SIZE)); // Commented out until we support ComputeShaders and the ComputeShader Cap @@ -729,6 +732,8 @@ && hasExtension("GL_ARB_half_float_pixel")) || caps.contains(Caps.WebGL) || (caps.contains(Caps.OpenGLES30) && JmeSystem.getPlatform().getOs() != Platform.Os.iOS)) { caps.add(Caps.UniformBufferObject); + limits.put(Limits.UniformBufferObjectMaxBindings, + getInteger(GL3.GL_MAX_UNIFORM_BUFFER_BINDINGS)); limits.put(Limits.UniformBufferObjectMaxBlockSize, getInteger(GL3.GL_MAX_UNIFORM_BLOCK_SIZE)); if (caps.contains(Caps.GeometryShader)) { @@ -832,6 +837,13 @@ private void bindUniformBlock(int program, int uniformBlockIndex, int uniformBlo } } + private int queryUniformBlockBinding(int program, int uniformBlockIndex) { + if (gl3 != null) { + return gl3.glGetActiveUniformBlocki(program, uniformBlockIndex, GL3.GL_UNIFORM_BLOCK_BINDING); + } + return glext.glGetActiveUniformBlocki(program, uniformBlockIndex, GL3.GL_UNIFORM_BLOCK_BINDING); + } + private int getProgramResourceIndex(int program, int programInterface, String name) { if (gl4 != null) { return gl4.glGetProgramResourceIndex(program, programInterface, name); @@ -859,6 +871,9 @@ private void bindShaderStorageBlock(int program, int storageBlockIndex, int stor @Override public void initialize() { loadCapabilities(); + context.resetBufferObjectBindings( + limits.getOrDefault(Limits.UniformBufferObjectMaxBindings, 0), + limits.getOrDefault(Limits.ShaderStorageBufferObjectMaxBindings, 0)); // Initialize default state.. gl.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1); @@ -1631,9 +1646,6 @@ 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 = bufferBlock.getBinding(); if (bindingPoint < 0) { // Binding not yet resolved — skip until resolveBufferBlockBindings runs @@ -1643,31 +1655,11 @@ protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBl switch (bufferType) { case UniformBufferObject: { - setUniformBufferObject(bindingPoint, bufferObject); // rebind buffer if needed - if (bufferBlock.isUpdateNeeded()) { - int blockIndex = bufferBlock.getLocation(); - if (blockIndex < 0) { - blockIndex = getUniformBlockIndex(shaderId, bufferBlock.getName()); - bufferBlock.setLocation(blockIndex); - } - if (bufferBlock.getLocation() != NativeObject.INVALID_ID) { - bindUniformBlock(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 = getProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, bufferBlock.getName()); - bufferBlock.setLocation(blockIndex); - } - if (bufferBlock.getLocation() != NativeObject.INVALID_ID) { - bindShaderStorageBlock(shaderId, bufferBlock.getLocation(), bindingPoint); - } - } + setShaderStorageBufferObject(bindingPoint, bufferObject); break; } default: { @@ -1703,9 +1695,9 @@ private void resolveBufferBlockBindings(final Shader shader) { int blockIndex = block.getLocation(); if (blockIndex < 0) { if (bufferType == BufferType.ShaderStorageBufferObject) { - blockIndex = gl4.glGetProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, block.getName()); + blockIndex = getProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, block.getName()); } else { - blockIndex = gl3.glGetUniformBlockIndex(shaderId, block.getName()); + blockIndex = getUniformBlockIndex(shaderId, block.getName()); } block.setLocation(blockIndex); } @@ -1719,17 +1711,44 @@ private void resolveBufferBlockBindings(final Shader shader) { if (bufferType == BufferType.ShaderStorageBufferObject) { binding = queryShaderStorageBlockBinding(shaderId, blockIndex); } else { - binding = gl3.glGetActiveUniformBlocki(shaderId, blockIndex, GL3.GL_UNIFORM_BLOCK_BINDING); + binding = queryUniformBlockBinding(shaderId, blockIndex); } block.setBinding(binding); } - // Pass 2: detect and resolve collisions. + int maxUboBindings = limits.getOrDefault(Limits.UniformBufferObjectMaxBindings, 0); + int maxSsboBindings = limits.getOrDefault(Limits.ShaderStorageBufferObjectMaxBindings, 0); + resolveBufferBlockBindingCollisions(bufferBlocks, maxUboBindings, maxSsboBindings); + + // Set the binding on the shader program. + 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(); + int blockIndex = block.getLocation(); + if (blockIndex < 0 || blockIndex == NativeObject.INVALID_ID) { + continue; + } + + if (bufferType == BufferType.ShaderStorageBufferObject) { + bindShaderStorageBlock(shaderId, blockIndex, binding); + } else { + bindUniformBlock(shaderId, blockIndex, binding); + } + } + } + + static void resolveBufferBlockBindingCollisions(final ListMap bufferBlocks, + int maxUboBindings, int maxSsboBindings) { // 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; + int userUboBindingCount = BufferBindingPoints.getUserBindingCount(maxUboBindings); + int userSsboBindingCount = maxSsboBindings; for (int i = 0; i < bufferBlocks.size(); i++) { ShaderBufferBlock block = bufferBlocks.getValue(i); @@ -1737,34 +1756,78 @@ private void resolveBufferBlockBindings(final Shader shader) { if (binding < 0) continue; BufferType bufferType = block.getType(); + boolean engineBinding = assignEngineBufferBlockBinding(block, maxUboBindings); + if (engineBinding) { + binding = block.getBinding(); + } Set usedBindings; + int userBindingCount; if (bufferType == BufferType.ShaderStorageBufferObject) { usedBindings = usedSsboBindings; + userBindingCount = userSsboBindingCount; } else { usedBindings = usedUboBindings; + userBindingCount = userUboBindingCount; + } + + if (bufferType == BufferType.UniformBufferObject + && !engineBinding + && BufferBindingPoints.isEngineReserved(maxUboBindings, binding)) { + binding = -1; + } + + if (!engineBinding && binding >= userBindingCount) { + binding = -1; } - if (!usedBindings.add(binding)) { + if (binding < 0 || !usedBindings.add(binding)) { // Collision within the same namespace — find a free binding point if (bufferType == BufferType.ShaderStorageBufferObject) { - while (usedBindings.contains(nextFreeSsbo)) nextFreeSsbo++; + while (nextFreeSsbo < userBindingCount && usedBindings.contains(nextFreeSsbo)) nextFreeSsbo++; + if (nextFreeSsbo >= userBindingCount) { + throw new RendererException("No free user SSBO binding point for block " + block.getName()); + } binding = nextFreeSsbo; } else { - while (usedBindings.contains(nextFreeUbo)) nextFreeUbo++; + while (nextFreeUbo < userBindingCount && usedBindings.contains(nextFreeUbo)) nextFreeUbo++; + if (nextFreeUbo >= userBindingCount) { + throw new RendererException("No free user UBO binding point for block " + block.getName()); + } 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); - } + private static boolean assignEngineBufferBlockBinding(ShaderBufferBlock block, int maxUboBindings) { + if (block.getType() != BufferType.UniformBufferObject) { + return false; + } + + int binding = -1; + if (isEngineBufferBlock(block, BufferBindingPoints.MAT_PARAMS_BLOCK_NAME)) { + binding = BufferBindingPoints.getEngineBinding(maxUboBindings, BufferBindingPoints.EngineBinding.MatParams); + } else if (isEngineBufferBlock(block, BufferBindingPoints.FRAME_BLOCK_NAME)) { + binding = BufferBindingPoints.getEngineBinding(maxUboBindings, BufferBindingPoints.EngineBinding.Frame); + } else if (isEngineBufferBlock(block, BufferBindingPoints.OBJECT_BLOCK_NAME)) { + binding = BufferBindingPoints.getEngineBinding(maxUboBindings, BufferBindingPoints.EngineBinding.Object); + } else if (isEngineBufferBlock(block, BufferBindingPoints.LIGHTS_BLOCK_NAME)) { + binding = BufferBindingPoints.getEngineBinding(maxUboBindings, BufferBindingPoints.EngineBinding.Lights); } + + if (binding >= 0) { + block.setBinding(binding); + return true; + } + return false; + } + + private static boolean isEngineBufferBlock(ShaderBufferBlock block, String engineName) { + BufferObject bufferObject = block.getBufferObject(); + return engineName.equals(block.getName()) + || (bufferObject != null && engineName.equals(bufferObject.getName())); } /** @@ -1779,7 +1842,11 @@ 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); + if (gl4 != null) { + gl4.glGetProgramResourceiv(program, GL4.GL_SHADER_STORAGE_BLOCK, blockIndex, intBuf16, null, intBuf1); + } else { + glext.glGetProgramResourceiv(program, GL4.GL_SHADER_STORAGE_BLOCK, blockIndex, intBuf16, null, intBuf1); + } return intBuf1.get(0); } @@ -1800,8 +1867,7 @@ 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) { + if (hasUnresolvedBufferBlockBindings(bufferBlocks)) { resolveBufferBlockBindings(shader); } @@ -1810,6 +1876,18 @@ protected void updateShaderBufferBlocks(final Shader shader) { } } + static boolean hasUnresolvedBufferBlockBindings(final ListMap bufferBlocks) { + for (int i = 0; i < bufferBlocks.size(); i++) { + ShaderBufferBlock block = bufferBlocks.getValue(i); + if (block.getType() != null + && block.getLocation() != NativeObject.INVALID_ID + && block.getBinding() < 0) { + return true; + } + } + return false; + } + protected void resetUniformLocations(Shader shader) { ListMap uniforms = shader.getUniformMap(); for (int i = 0; i < uniforms.size(); i++) { @@ -3270,14 +3348,15 @@ public void setTextureImage(int unit, TextureImage tex) throws TextureUnitExcept @Override public void setUniformBufferObject(int bindingPoint, BufferObject bufferObject) { + validateBufferBindingPoint(bindingPoint, context.boundUniformBuffers.length, "uniform buffer"); if (bufferObject.isUpdateNeeded()) { updateUniformBufferObjectData(bufferObject); } - if (context.boundBO[bindingPoint] == null || context.boundBO[bindingPoint].get() != bufferObject) { + if (context.boundUniformBuffers[bindingPoint] == null || context.boundUniformBuffers[bindingPoint].get() != bufferObject) { bindUniformBufferBase(bindingPoint, bufferObject.getId()); bufferObject.setBinding(bindingPoint); - context.boundBO[bindingPoint] = bufferObject.getWeakRef(); + context.boundUniformBuffers[bindingPoint] = bufferObject.getWeakRef(); } bufferObject.setBinding(bindingPoint); @@ -3290,13 +3369,14 @@ public void setUniformBufferObject(int bindingPoint, BufferObject bufferObject) @Override public void setShaderStorageBufferObject(int bindingPoint, BufferObject bufferObject) { + validateBufferBindingPoint(bindingPoint, context.boundShaderStorageBuffers.length, "shader storage buffer"); if (bufferObject.isUpdateNeeded()) { updateShaderStorageBufferObjectData(bufferObject); } - if (context.boundBO[bindingPoint] == null || context.boundBO[bindingPoint].get() != bufferObject) { + if (context.boundShaderStorageBuffers[bindingPoint] == null || context.boundShaderStorageBuffers[bindingPoint].get() != bufferObject) { bindShaderStorageBufferBase(bindingPoint, bufferObject.getId()); bufferObject.setBinding(bindingPoint); - context.boundBO[bindingPoint] = bufferObject.getWeakRef(); + context.boundShaderStorageBuffers[bindingPoint] = bufferObject.getWeakRef(); } bufferObject.setBinding(bindingPoint); @@ -3305,6 +3385,13 @@ public void setShaderStorageBufferObject(int bindingPoint, BufferObject bufferOb } } + private void validateBufferBindingPoint(int bindingPoint, int bindingCount, String targetName) { + if (bindingPoint < 0 || bindingPoint >= bindingCount) { + throw new RendererException("Invalid " + targetName + " binding point " + bindingPoint + + ". Available binding points: " + bindingCount); + } + } + /** * @deprecated Use modifyTexture(Texture2D dest, Image src, int destX, int destY, int srcX, int srcY, int areaW, int areaH) */ @@ -3616,6 +3703,49 @@ public void updateUniformBufferObjectData(BufferObject bo) { updateBufferData(GL4.GL_UNIFORM_BUFFER, bo); } + @Override + public void readShaderStorageBufferObjectData(BufferObject bo, ByteBuffer store) { + if (!caps.contains(Caps.ShaderStorageBufferObject)) throw new IllegalArgumentException("The current video hardware doesn't support shader storage buffer objects "); + readBufferData(GL4.GL_SHADER_STORAGE_BUFFER, bo, store); + } + + @Override + public void readUniformBufferObjectData(BufferObject bo, ByteBuffer store) { + if (!caps.contains(Caps.UniformBufferObject)) throw new IllegalArgumentException("The current video hardware doesn't support uniform buffer objects"); + readBufferData(GL4.GL_UNIFORM_BUFFER, bo, store); + } + + private void readBufferData(int type, BufferObject bo, ByteBuffer store) { + if (store == null) { + throw new IllegalArgumentException("Store buffer cannot be null"); + } + if (bo.isUpdateNeeded()) { + updateBufferData(type, bo); + } + if (bo.getId() == -1) { + return; + } + int readLength = prepareBufferReadbackStore(bo, store); + if (readLength == 0) { + return; + } + gl.glBindBuffer(type, bo.getId()); + gl.glGetBufferSubData(type, 0, store); + gl.glBindBuffer(type, 0); + store.rewind(); + } + + static int prepareBufferReadbackStore(BufferObject bo, ByteBuffer store) { + if (store == null) { + throw new IllegalArgumentException("Store buffer cannot be null"); + } + + int readLength = Math.min(store.capacity(), bo.getByteData().limit()); + store.clear(); + store.limit(readLength); + return readLength; + } + private void updateBufferData(int type, BufferObject bo) { int bufferId = bo.getId(); int usage = resolveUsageHint(bo.getAccessHint(), bo.getNatureHint()); diff --git a/jme3-core/src/main/java/com/jme3/shader/Shader.java b/jme3-core/src/main/java/com/jme3/shader/Shader.java index c066671d47..cf8e43d167 100644 --- a/jme3-core/src/main/java/com/jme3/shader/Shader.java +++ b/jme3-core/src/main/java/com/jme3/shader/Shader.java @@ -312,7 +312,7 @@ public Uniform getUniform(String name){ */ public ShaderBufferBlock getBufferBlock(final String name) { - assert name.startsWith("m_"); + assert name != null; ShaderBufferBlock block = bufferBlocks.get(name); diff --git a/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferBindingPoints.java b/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferBindingPoints.java new file mode 100644 index 0000000000..bfc1c3c85b --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferBindingPoints.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2026 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 com.jme3.shader.bufferobject; + +/** + * Shared binding-point policy for engine-owned buffer objects. + */ +public final class BufferBindingPoints { + + public static final String MAT_PARAMS_BLOCK_NAME = "m_MatParams"; + public static final String FRAME_BLOCK_NAME = "g_Frame"; + public static final String OBJECT_BLOCK_NAME = "g_Object"; + public static final String LIGHTS_BLOCK_NAME = "g_Lights"; + + public static final String MAT_PARAMS_DEFINE = "JME_UBO_MAT_PARAMS"; + public static final String FRAME_DEFINE = "JME_UBO_FRAME"; + public static final String OBJECT_DEFINE = "JME_UBO_OBJECT"; + public static final String LIGHTS_DEFINE = "JME_UBO_LIGHTS"; + + public static final int ENGINE_RESERVED_BINDINGS = 7; + + private BufferBindingPoints() { + } + + public enum EngineBinding { + Lights(1), + Data1(2), + Data2(3), + Data3(4), + Frame(5), + Object(6), + MatParams(7); + + private final int offsetFromEnd; + + EngineBinding(int offsetFromEnd) { + this.offsetFromEnd = offsetFromEnd; + } + } + + /** + * Returns the first binding point available to users. Engine-owned bindings + * are allocated from the end of the available range. + * + * @param maxBindings runtime binding-point limit + * @return number of user binding points + */ + public static int getUserBindingCount(int maxBindings) { + if (maxBindings < ENGINE_RESERVED_BINDINGS) { + return Math.max(0, maxBindings); + } + return maxBindings - ENGINE_RESERVED_BINDINGS; + } + + /** + * Returns the binding point reserved for an engine-owned block. + * + * @param maxBindings runtime binding-point limit + * @param binding engine binding + * @return binding point, or -1 if the limit cannot reserve engine slots + */ + public static int getEngineBinding(int maxBindings, EngineBinding binding) { + if (maxBindings < ENGINE_RESERVED_BINDINGS) { + return -1; + } + return maxBindings - binding.offsetFromEnd; + } + + /** + * Returns true if the binding point belongs to the engine-reserved range. + * + * @param maxBindings runtime binding-point limit + * @param bindingPoint binding point to test + * @return true if reserved for engine use + */ + public static boolean isEngineReserved(int maxBindings, int bindingPoint) { + if (maxBindings < ENGINE_RESERVED_BINDINGS) { + return false; + } + return bindingPoint >= getUserBindingCount(maxBindings) && bindingPoint < maxBindings; + } +} diff --git a/jme3-core/src/main/java/com/jme3/system/NullRenderer.java b/jme3-core/src/main/java/com/jme3/system/NullRenderer.java index 162f347206..5714c2837c 100644 --- a/jme3-core/src/main/java/com/jme3/system/NullRenderer.java +++ b/jme3-core/src/main/java/com/jme3/system/NullRenderer.java @@ -314,6 +314,16 @@ public void updateUniformBufferObjectData(BufferObject bo) { } + @Override + public void readShaderStorageBufferObjectData(BufferObject bo, ByteBuffer store) { + + } + + @Override + public void readUniformBufferObjectData(BufferObject bo, ByteBuffer store) { + + } + @Override public void setShaderStorageBufferObject(int bindingPoint, BufferObject bufferObject) { diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererBufferBlockBindingTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererBufferBlockBindingTest.java new file mode 100644 index 0000000000..0d24e5abb5 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GLRendererBufferBlockBindingTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2026 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 HOLDER 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 com.jme3.renderer.opengl; + +import com.jme3.shader.ShaderBufferBlock; +import com.jme3.shader.ShaderBufferBlock.BufferType; +import com.jme3.shader.bufferobject.BufferBindingPoints; +import com.jme3.shader.bufferobject.BufferObject; +import com.jme3.util.ListMap; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GLRendererBufferBlockBindingTest { + + @Test + public void testDetectsUnresolvedBlockAfterResolvedBlock() { + ListMap blocks = new ListMap<>(); + blocks.put("Resolved", block("Resolved", BufferType.UniformBufferObject, 0, 0)); + blocks.put("Unresolved", block("Unresolved", BufferType.UniformBufferObject, 1, -1)); + + assertTrue(GLRenderer.hasUnresolvedBufferBlockBindings(blocks)); + } + + @Test + public void testIgnoresUnconfiguredBlocksDuringUnresolvedScan() { + ListMap blocks = new ListMap<>(); + ShaderBufferBlock block = new ShaderBufferBlock(); + block.setName("Unconfigured"); + block.setLocation(0); + blocks.put(block.getName(), block); + + assertFalse(GLRenderer.hasUnresolvedBufferBlockBindings(blocks)); + } + + @Test + public void testUboEngineBindingsUseReservedRange() { + ListMap blocks = new ListMap<>(); + blocks.put(BufferBindingPoints.MAT_PARAMS_BLOCK_NAME, + block(BufferBindingPoints.MAT_PARAMS_BLOCK_NAME, BufferType.UniformBufferObject, 0, 0)); + blocks.put("UserBlock", block("UserBlock", BufferType.UniformBufferObject, 1, 35)); + + GLRenderer.resolveBufferBlockBindingCollisions(blocks, 36, 36); + + assertEquals(29, blocks.get(BufferBindingPoints.MAT_PARAMS_BLOCK_NAME).getBinding()); + assertEquals(0, blocks.get("UserBlock").getBinding()); + } + + @Test + public void testSsboCanUseFullBindingRange() { + ListMap blocks = new ListMap<>(); + blocks.put("Data", block("Data", BufferType.ShaderStorageBufferObject, 0, 35)); + + GLRenderer.resolveBufferBlockBindingCollisions(blocks, 36, 36); + + assertEquals(35, blocks.get("Data").getBinding()); + } + + @Test + public void testMatParamInstanceBlockUsesEngineBinding() { + ListMap blocks = new ListMap<>(); + ShaderBufferBlock block = block("MatParams", BufferType.UniformBufferObject, 0, 0); + block.getBufferObject().setName(BufferBindingPoints.MAT_PARAMS_BLOCK_NAME); + blocks.put("MatParams", block); + + GLRenderer.resolveBufferBlockBindingCollisions(blocks, 36, 36); + + assertEquals(29, block.getBinding()); + } + + @Test + public void testUboAndSsboBindingsAreSeparateNamespaces() { + ListMap blocks = new ListMap<>(); + blocks.put("Params", block("Params", BufferType.UniformBufferObject, 0, 1)); + blocks.put("Data", block("Data", BufferType.ShaderStorageBufferObject, 0, 1)); + + GLRenderer.resolveBufferBlockBindingCollisions(blocks, 36, 36); + + assertEquals(1, blocks.get("Params").getBinding()); + assertEquals(1, blocks.get("Data").getBinding()); + } + + @Test + public void testOutOfRangeUboBindingIsReassigned() { + ListMap blocks = new ListMap<>(); + blocks.put("Params", block("Params", BufferType.UniformBufferObject, 0, 99)); + + GLRenderer.resolveBufferBlockBindingCollisions(blocks, 36, 36); + + assertEquals(0, blocks.get("Params").getBinding()); + } + + @Test + public void testOutOfRangeSsboBindingIsReassigned() { + ListMap blocks = new ListMap<>(); + blocks.put("Data", block("Data", BufferType.ShaderStorageBufferObject, 0, 99)); + + GLRenderer.resolveBufferBlockBindingCollisions(blocks, 36, 36); + + assertEquals(0, blocks.get("Data").getBinding()); + } + + @Test + public void testReadbackStoreIsClampedToBufferSize() { + BufferObject bufferObject = new BufferObject(); + bufferObject.initializeEmpty(8); + ByteBuffer store = ByteBuffer.allocateDirect(16); + + int readLength = GLRenderer.prepareBufferReadbackStore(bufferObject, store); + + assertEquals(8, readLength); + assertEquals(0, store.position()); + assertEquals(8, store.limit()); + } + + @Test + public void testReadbackStoreCanBeSmallerThanBuffer() { + BufferObject bufferObject = new BufferObject(); + bufferObject.initializeEmpty(16); + ByteBuffer store = ByteBuffer.allocateDirect(8); + + int readLength = GLRenderer.prepareBufferReadbackStore(bufferObject, store); + + assertEquals(8, readLength); + assertEquals(0, store.position()); + assertEquals(8, store.limit()); + } + + private static ShaderBufferBlock block(String name, BufferType type, int location, int binding) { + ShaderBufferBlock block = new ShaderBufferBlock(); + block.setName(name); + block.setLocation(location); + block.setBinding(binding); + block.setBufferObject(type, new BufferObject()); + return block; + } +} diff --git a/jme3-core/src/test/java/com/jme3/shader/bufferobject/BufferBindingPointsTest.java b/jme3-core/src/test/java/com/jme3/shader/bufferobject/BufferBindingPointsTest.java new file mode 100644 index 0000000000..d7c9ab3c5d --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/shader/bufferobject/BufferBindingPointsTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 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 com.jme3.shader.bufferobject; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BufferBindingPointsTest { + + @Test + public void testEngineBindingsUseEndOfRange() { + assertEquals(29, BufferBindingPoints.getUserBindingCount(36)); + assertEquals(35, BufferBindingPoints.getEngineBinding(36, BufferBindingPoints.EngineBinding.Lights)); + assertEquals(34, BufferBindingPoints.getEngineBinding(36, BufferBindingPoints.EngineBinding.Data1)); + assertEquals(33, BufferBindingPoints.getEngineBinding(36, BufferBindingPoints.EngineBinding.Data2)); + assertEquals(32, BufferBindingPoints.getEngineBinding(36, BufferBindingPoints.EngineBinding.Data3)); + assertEquals(31, BufferBindingPoints.getEngineBinding(36, BufferBindingPoints.EngineBinding.Frame)); + assertEquals(30, BufferBindingPoints.getEngineBinding(36, BufferBindingPoints.EngineBinding.Object)); + assertEquals(29, BufferBindingPoints.getEngineBinding(36, BufferBindingPoints.EngineBinding.MatParams)); + } + + @Test + public void testReservedRangeDetection() { + assertFalse(BufferBindingPoints.isEngineReserved(36, 28)); + assertTrue(BufferBindingPoints.isEngineReserved(36, 29)); + assertTrue(BufferBindingPoints.isEngineReserved(36, 35)); + assertFalse(BufferBindingPoints.isEngineReserved(36, 36)); + } + + @Test + public void testInsufficientBindings() { + assertEquals(3, BufferBindingPoints.getUserBindingCount(3)); + assertEquals(-1, BufferBindingPoints.getEngineBinding(3, BufferBindingPoints.EngineBinding.MatParams)); + assertFalse(BufferBindingPoints.isEngineReserved(3, 0)); + assertFalse(BufferBindingPoints.isEngineReserved(3, 2)); + } +} From 2cd34dbb7ab556f085defe25dbceaf5b72f983f9 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 10 Jun 2026 19:14:08 +0200 Subject: [PATCH 12/15] support UBO based materials --- .../jme3/material/MatParamUniformBuffer.java | 659 ++++++++++++++++++ .../main/java/com/jme3/material/Material.java | 140 ++-- .../material/MatParamUniformBufferTest.java | 164 +++++ .../material/TestMatParamUniformBuffer.java | 85 +++ .../jme3test/matparamubo/MatParamUBO.frag | 13 + .../jme3test/matparamubo/MatParamUBO.j3md | 15 + .../jme3test/matparamubo/MatParamUBO.vert | 13 + 7 files changed, 1029 insertions(+), 60 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/material/MatParamUniformBuffer.java create mode 100644 jme3-core/src/test/java/com/jme3/material/MatParamUniformBufferTest.java create mode 100644 jme3-examples/src/main/java/jme3test/material/TestMatParamUniformBuffer.java create mode 100644 jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.frag create mode 100644 jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.j3md create mode 100644 jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.vert diff --git a/jme3-core/src/main/java/com/jme3/material/MatParamUniformBuffer.java b/jme3-core/src/main/java/com/jme3/material/MatParamUniformBuffer.java new file mode 100644 index 0000000000..8cfddc4f15 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/material/MatParamUniformBuffer.java @@ -0,0 +1,659 @@ +/* + * Copyright (c) 2009-2026 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 HOLDER 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 com.jme3.material; + +import com.jme3.math.Matrix3f; +import com.jme3.math.Matrix4f; +import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; +import com.jme3.math.Vector4f; +import com.jme3.shader.Shader; +import com.jme3.shader.ShaderBufferBlock; +import com.jme3.shader.VarType; +import com.jme3.shader.bufferobject.BufferBindingPoints; +import com.jme3.shader.bufferobject.BufferObject; +import com.jme3.shader.bufferobject.layout.Std140Layout; +import com.jme3.util.BufferUtils; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Builds the engine-owned {@code m_MatParams} UBO from ordinary material + * parameters. + *

+ * Materials still expose parameters through the normal {@link MatParam} API. + * This helper detects whether the active shader declares a compatible + * {@code layout(std140) uniform m_MatParams} block and, when it does, packs + * matching material parameters into a single {@link BufferObject}. Parameters + * that are not present in the block continue through the regular uniform path + * in {@link Material}. + *

+ * This is intentionally limited to simple std140 declarations. If the parser + * cannot prove the layout from the shader source, the helper stays inactive so + * that material updates fall back to individual uniforms. + */ +final class MatParamUniformBuffer { + + private static final Std140Layout STD140 = new Std140Layout(); + private static final Pattern COMMENT_PATTERN = Pattern.compile("(?s)/\\*.*?\\*/|//.*?(?=\\R|$)"); + private static final Pattern BLOCK_PATTERN = Pattern.compile( + "(?s)(?:layout\\s*\\(([^)]*)\\)\\s*)?uniform\\s+(\\w+)\\s*\\{(.*?)\\}\\s*(\\w+)?\\s*;"); + private static final Pattern MEMBER_PATTERN = Pattern.compile( + "(?s)(?:layout\\s*\\([^)]*\\)\\s*)?(?:highp\\s+|mediump\\s+|lowp\\s+)?(float|int|bool|vec2|vec3|vec4|mat3|mat4)\\s+(.+)"); + private static final Pattern DECLARATOR_PATTERN = Pattern.compile("(\\w+)\\s*(?:\\[\\s*(\\d+)\\s*\\])?\\s*"); + + private final BufferObject bufferObject = new BufferObject(); + private Layout layout; + private Shader shader; + private Object[] values; + private boolean[] set; + + /** + * Creates the reusable material-parameter UBO owned by one material + * instance. + */ + MatParamUniformBuffer() { + bufferObject.setName(BufferBindingPoints.MAT_PARAMS_BLOCK_NAME); + bufferObject.setAccessHint(BufferObject.AccessHint.Dynamic); + bufferObject.setNatureHint(BufferObject.NatureHint.Draw); + } + + /** + * Starts collecting values for the specified shader. + *

+ * The parsed layout is cached per shader instance. When the same shader is + * used again, only the collected values are reset for the next material + * update. + * + * @param shader the active shader being updated + */ + void begin(Shader shader) { + if (this.shader != shader) { + this.shader = shader; + this.layout = parseLayout(shader); + if (layout != null) { + this.values = new Object[layout.members.size()]; + this.set = new boolean[layout.members.size()]; + } else { + this.values = null; + this.set = null; + } + } else if (set != null) { + for (int i = 0; i < set.length; i++) { + set[i] = false; + values[i] = null; + } + } + } + + /** + * Tests whether the active shader contains a supported material-parameter + * UBO declaration. + * + * @return true if matching parameters can be written into the UBO + */ + boolean isActive() { + return layout != null; + } + + /** + * Attempts to store a material parameter in the collected UBO data. + * + * @param param the material parameter or override to store + * @param override true when {@code param} comes from an override list + * @return true if the parameter belongs to the UBO and was consumed + */ + boolean set(MatParam param, boolean override) { + if (layout == null || param.getValue() == null) { + return false; + } + + Member member = layout.membersByParamName.get(param.getName()); + if (member == null || !member.accepts(param.getVarType())) { + return false; + } + + int index = member.index; + if (!override && set[index]) { + // Overrides are applied before material values and must keep + // ownership when both target the same UBO member. + return true; + } + + Object converted = member.convert(param.getValue()); + if (converted == null) { + return false; + } + + values[index] = converted; + set[index] = true; + return true; + } + + /** + * Clears a UBO-backed parameter by writing its std140 zero value. + * + * @param param the disabled override that targets a material parameter + * @return true if the parameter belongs to the UBO and was consumed + */ + boolean clear(MatParam param) { + if (layout == null) { + return false; + } + + Member member = layout.membersByParamName.get(param.getName()); + if (member == null || !member.accepts(param.getVarType())) { + return false; + } + + values[member.index] = member.zeroValue(); + set[member.index] = true; + return true; + } + + /** + * Writes all collected values into the backing buffer object and attaches + * it to the shader block. + *

+ * Members that were not explicitly supplied this frame are written as + * zeroes. This mirrors the behavior of clearing ordinary uniforms when an + * override removes a value and avoids leaking stale UBO data from a + * previous material update. + * + * @param shader the active shader being updated + */ + void finish(Shader shader) { + if (layout == null) { + return; + } + + ByteBuffer data = BufferUtils.createByteBuffer(layout.size); + for (Member member : layout.members) { + Object value = set[member.index] ? values[member.index] : member.zeroValue(); + data.position(member.offset); + STD140.write(data, value); + } + data.clear(); + + // Avoid dirtying the BufferObject when the packed byte content did not + // change; renderer-side uploads can then stay skipped. + if (!contentEquals(bufferObject.getByteData(), data)) { + bufferObject.setData(data); + } + + ShaderBufferBlock block = shader.getBufferBlock(layout.blockName); + block.setBufferObject(ShaderBufferBlock.BufferType.UniformBufferObject, bufferObject); + } + + /** + * Returns the backing buffer object. + * + * @return the UBO populated by this helper + */ + BufferObject getBufferObject() { + return bufferObject; + } + + /** + * Finds a supported material-parameter UBO declaration in the shader. + * + * @param shader the shader to inspect + * @return the parsed layout, or null if none of the shader sources contains + * a supported declaration + */ + static Layout parseLayout(Shader shader) { + for (Shader.ShaderSource source : shader.getSources()) { + Layout layout = parseLayout(source.getSource()); + if (layout != null) { + return layout; + } + } + return null; + } + + /** + * Parses a GLSL source string for a supported {@code m_MatParams} block. + * + * @param source GLSL source code + * @return the parsed layout, or null if the source does not declare a + * supported std140 material-parameter block + */ + static Layout parseLayout(String source) { + if (source == null) { + return null; + } + + Matcher blockMatcher = BLOCK_PATTERN.matcher(stripComments(source)); + while (blockMatcher.find()) { + String layoutQualifier = blockMatcher.group(1); + String blockName = blockMatcher.group(2); + String body = blockMatcher.group(3); + String instanceName = blockMatcher.group(4); + + if (!BufferBindingPoints.MAT_PARAMS_BLOCK_NAME.equals(blockName) + && !BufferBindingPoints.MAT_PARAMS_BLOCK_NAME.equals(instanceName)) { + continue; + } + if (layoutQualifier == null || !layoutQualifier.toLowerCase(Locale.ROOT).contains("std140")) { + continue; + } + + return parseMembers(blockName, body); + } + return null; + } + + /** + * Parses supported block members and computes their std140 offsets. + */ + private static Layout parseMembers(String blockName, String body) { + if (body.indexOf('#') >= 0) { + // Preprocessor-dependent declarations cannot be sized reliably from + // the raw source available here. + return null; + } + + ArrayList members = new ArrayList<>(); + int offset = 0; + int maxAlignment = 0; + + for (String statement : body.split(";")) { + statement = statement.trim(); + if (statement.isEmpty()) { + continue; + } + if (statement.startsWith("layout")) { + // Explicit member layouts, such as layout(offset = N), are not + // interpreted here. Falling back avoids silently using the + // wrong offset. + return null; + } + + Matcher memberMatcher = MEMBER_PATTERN.matcher(statement); + if (!memberMatcher.matches()) { + return null; + } + + String glslType = memberMatcher.group(1); + String declarations = memberMatcher.group(2); + for (String declaration : declarations.split(",")) { + Matcher declaratorMatcher = DECLARATOR_PATTERN.matcher(declaration.trim()); + if (!declaratorMatcher.matches()) { + return null; + } + + String memberName = declaratorMatcher.group(1); + String arrayLength = declaratorMatcher.group(2); + Member member = Member.create(members.size(), memberName, glslType, + arrayLength == null ? 0 : Integer.parseInt(arrayLength)); + if (member == null) { + return null; + } + + // Compute the same offsets that Std140Layout.write() expects. + int alignment = STD140.getBasicAlignment(member.zeroValue()); + offset = STD140.align(offset, alignment); + member.offset = offset; + offset += STD140.estimateSize(member.zeroValue()); + maxAlignment = Math.max(maxAlignment, alignment); + members.add(member); + } + } + + if (members.isEmpty()) { + return null; + } + + int size = STD140.align(offset, STD140.getStructureAlignment(maxAlignment)); + return new Layout(blockName, members, size); + } + + /** + * Compares full buffer contents without mutating the caller-visible + * position or limit of either buffer. + */ + private static boolean contentEquals(ByteBuffer current, ByteBuffer next) { + ByteBuffer a = current.duplicate(); + ByteBuffer b = next.duplicate(); + a.clear(); + b.clear(); + if (a.remaining() != b.remaining()) { + return false; + } + for (int i = 0; i < a.remaining(); i++) { + if (a.get(i) != b.get(i)) { + return false; + } + } + return true; + } + + /** + * Removes GLSL block and line comments before applying the simple + * declaration parser. + */ + private static String stripComments(String source) { + return COMMENT_PATTERN.matcher(source).replaceAll(""); + } + + /** + * Parsed std140 layout for the material-parameter block in one shader. + */ + static final class Layout { + final String blockName; + final ArrayList members; + final Map membersByParamName = new HashMap<>(); + final int size; + + /** + * Creates a parsed block layout. + * + * @param blockName the actual GLSL block name to bind + * @param members ordered block members with computed offsets + * @param size total std140 block size in bytes + */ + Layout(String blockName, ArrayList members, int size) { + this.blockName = blockName; + this.members = members; + this.size = size; + for (Member member : members) { + membersByParamName.put(member.paramName, member); + } + } + + /** + * Finds a block member by material-parameter name. + * + * @param paramName unprefixed material parameter name + * @return the matching member, or null if the parameter is not stored in + * this block + */ + Member getMember(String paramName) { + return membersByParamName.get(paramName); + } + } + + /** + * One scalar, vector, matrix, or array entry in the parsed block. + */ + static final class Member { + final int index; + final String name; + final String paramName; + final String glslType; + final int arrayLength; + final Object zeroValue; + final VarType varType; + int offset; + + /** + * Creates a parsed block member. + */ + private Member(int index, String name, String glslType, int arrayLength, Object zeroValue, VarType varType) { + this.index = index; + this.name = name; + this.paramName = name.startsWith("m_") ? name.substring(2) : name; + this.glslType = glslType; + this.arrayLength = arrayLength; + this.zeroValue = zeroValue; + this.varType = varType; + } + + /** + * Creates a member for a supported GLSL type. + * + * @param index declaration order within the block + * @param name GLSL member name + * @param glslType GLSL type token + * @param arrayLength array length, or zero for non-array members + * @return a member definition, or null for unsupported GLSL types + */ + static Member create(int index, String name, String glslType, int arrayLength) { + boolean array = arrayLength > 0; + switch (glslType) { + case "float": + return new Member(index, name, glslType, arrayLength, + array ? new Float[arrayLength] : Float.valueOf(0), array ? VarType.FloatArray : VarType.Float); + case "int": + return new Member(index, name, glslType, arrayLength, + array ? new Integer[arrayLength] : Integer.valueOf(0), array ? VarType.IntArray : VarType.Int); + case "bool": + return new Member(index, name, glslType, arrayLength, + array ? new Boolean[arrayLength] : Boolean.FALSE, array ? null : VarType.Boolean); + case "vec2": + return new Member(index, name, glslType, arrayLength, + array ? new Vector2f[arrayLength] : new Vector2f(), array ? VarType.Vector2Array : VarType.Vector2); + case "vec3": + return new Member(index, name, glslType, arrayLength, + array ? new Vector3f[arrayLength] : new Vector3f(), array ? VarType.Vector3Array : VarType.Vector3); + case "vec4": + return new Member(index, name, glslType, arrayLength, + array ? new Vector4f[arrayLength] : new Vector4f(), array ? VarType.Vector4Array : VarType.Vector4); + case "mat3": + return new Member(index, name, glslType, arrayLength, + array ? new Matrix3f[arrayLength] : new Matrix3f(Matrix3f.ZERO), array ? VarType.Matrix3Array : VarType.Matrix3); + case "mat4": + return new Member(index, name, glslType, arrayLength, + array ? new Matrix4f[arrayLength] : new Matrix4f(Matrix4f.ZERO), array ? VarType.Matrix4Array : VarType.Matrix4); + default: + return null; + } + } + + /** + * Tests whether a material parameter can supply this member. + * + * @param type material parameter type + * @return true if the VarType exactly matches the parsed GLSL member + */ + boolean accepts(VarType type) { + return type == varType; + } + + /** + * Creates a zero value suitable for writing this member with the std140 + * serializer. + * + * @return a scalar zero or a fresh zero-filled array + */ + Object zeroValue() { + if (!zeroValue.getClass().isArray()) { + return zeroValue; + } + switch (glslType) { + case "float": + return filledFloatArray(); + case "int": + return filledIntArray(); + case "bool": + return filledBoolArray(); + case "vec2": + return filledVector2Array(); + case "vec3": + return filledVector3Array(); + case "vec4": + return filledVector4Array(); + case "mat3": + return filledMatrix3Array(); + case "mat4": + return filledMatrix4Array(); + default: + return zeroValue; + } + } + + /** + * Converts material values to the exact object shape expected by + * {@link Std140Layout}. Array values are copied and padded with zeroes + * so the backing buffer always receives the declared GLSL array length. + * + * @param value material parameter value + * @return converted value, or null if this member cannot convert the + * supplied Java value + */ + Object convert(Object value) { + if (arrayLength == 0) { + return value; + } + switch (glslType) { + case "float": + return copyFloatArray(value); + case "int": + return copyIntArray(value); + case "vec2": + return copyVector2Array((Vector2f[]) value); + case "vec3": + return copyVector3Array((Vector3f[]) value); + case "vec4": + return copyVector4Array((Vector4f[]) value); + case "mat3": + return copyMatrix3Array((Matrix3f[]) value); + case "mat4": + return copyMatrix4Array((Matrix4f[]) value); + default: + return null; + } + } + + private Float[] copyFloatArray(Object value) { + Float[] result = filledFloatArray(); + if (value instanceof float[]) { + float[] source = (float[]) value; + for (int i = 0; i < Math.min(source.length, result.length); i++) { + result[i] = source[i]; + } + } else { + Float[] source = (Float[]) value; + System.arraycopy(source, 0, result, 0, Math.min(source.length, result.length)); + } + return result; + } + + private Integer[] copyIntArray(Object value) { + Integer[] result = filledIntArray(); + if (value instanceof int[]) { + int[] source = (int[]) value; + for (int i = 0; i < Math.min(source.length, result.length); i++) { + result[i] = source[i]; + } + } else { + Integer[] source = (Integer[]) value; + System.arraycopy(source, 0, result, 0, Math.min(source.length, result.length)); + } + return result; + } + + private Float[] filledFloatArray() { + Float[] result = new Float[arrayLength]; + for (int i = 0; i < result.length; i++) result[i] = 0f; + return result; + } + + private Integer[] filledIntArray() { + Integer[] result = new Integer[arrayLength]; + for (int i = 0; i < result.length; i++) result[i] = 0; + return result; + } + + private Boolean[] filledBoolArray() { + Boolean[] result = new Boolean[arrayLength]; + for (int i = 0; i < result.length; i++) result[i] = false; + return result; + } + + private Vector2f[] filledVector2Array() { + Vector2f[] result = new Vector2f[arrayLength]; + for (int i = 0; i < result.length; i++) result[i] = new Vector2f(); + return result; + } + + private Vector3f[] filledVector3Array() { + Vector3f[] result = new Vector3f[arrayLength]; + for (int i = 0; i < result.length; i++) result[i] = new Vector3f(); + return result; + } + + private Vector4f[] filledVector4Array() { + Vector4f[] result = new Vector4f[arrayLength]; + for (int i = 0; i < result.length; i++) result[i] = new Vector4f(); + return result; + } + + private Matrix3f[] filledMatrix3Array() { + Matrix3f[] result = new Matrix3f[arrayLength]; + for (int i = 0; i < result.length; i++) result[i] = new Matrix3f(Matrix3f.ZERO); + return result; + } + + private Matrix4f[] filledMatrix4Array() { + Matrix4f[] result = new Matrix4f[arrayLength]; + for (int i = 0; i < result.length; i++) result[i] = new Matrix4f(Matrix4f.ZERO); + return result; + } + + private Vector2f[] copyVector2Array(Vector2f[] source) { + Vector2f[] result = filledVector2Array(); + System.arraycopy(source, 0, result, 0, Math.min(source.length, result.length)); + return result; + } + + private Vector3f[] copyVector3Array(Vector3f[] source) { + Vector3f[] result = filledVector3Array(); + System.arraycopy(source, 0, result, 0, Math.min(source.length, result.length)); + return result; + } + + private Vector4f[] copyVector4Array(Vector4f[] source) { + Vector4f[] result = filledVector4Array(); + System.arraycopy(source, 0, result, 0, Math.min(source.length, result.length)); + return result; + } + + private Matrix3f[] copyMatrix3Array(Matrix3f[] source) { + Matrix3f[] result = filledMatrix3Array(); + System.arraycopy(source, 0, result, 0, Math.min(source.length, result.length)); + return result; + } + + private Matrix4f[] copyMatrix4Array(Matrix4f[] source) { + Matrix4f[] result = filledMatrix4Array(); + System.arraycopy(source, 0, result, 0, Math.min(source.length, result.length)); + return result; + } + } +} 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 7488bb6b98..082bed7e56 100644 --- a/jme3-core/src/main/java/com/jme3/material/Material.java +++ b/jme3-core/src/main/java/com/jme3/material/Material.java @@ -97,9 +97,10 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable { private HashMap techniques = new HashMap<>(); private RenderState additionalState = null; private final RenderState mergedRenderState = new RenderState(); - private boolean transparent = false; - private boolean receivesShadows = false; - private int sortingId = -1; + private boolean transparent = false; + private boolean receivesShadows = false; + private int sortingId = -1; + private transient MatParamUniformBuffer matParamUniformBuffer; /** * Manages and tracks texture and buffer binding units for rendering. @@ -242,13 +243,14 @@ public Material clone() { mat.technique = null; mat.techniques = new HashMap(); - mat.paramValues = new ListMap(); - for (int i = 0; i < paramValues.size(); i++) { - Map.Entry entry = paramValues.getEntry(i); - mat.paramValues.put(entry.getKey(), entry.getValue().clone()); - } - - mat.sortingId = -1; + mat.paramValues = new ListMap(); + for (int i = 0; i < paramValues.size(); i++) { + Map.Entry entry = paramValues.getEntry(i); + mat.paramValues.put(entry.getKey(), entry.getValue().clone()); + } + mat.matParamUniformBuffer = null; + + mat.sortingId = -1; return mat; } catch (CloneNotSupportedException ex) { @@ -819,8 +821,8 @@ public void selectTechnique(String name, final RenderManager renderManager) { TechniqueDef lastTech = null; float weight = 0; - for (TechniqueDef techDef : techDefs) { - if (rendererCaps.containsAll(techDef.getRequiredCaps())) { + for (TechniqueDef techDef : techDefs) { + if (rendererCaps.containsAll(techDef.getRequiredCaps())) { float techWeight = techDef.getWeight() + (techDef.getLightMode() == renderManager.getPreferredLightMode() ? 10f : 0); if (techWeight > weight) { tech = new Technique(this, techDef); @@ -849,46 +851,61 @@ public void selectTechnique(String name, final RenderManager renderManager) { technique = tech; tech.notifyTechniqueSwitched(); - // shader was changed - sortingId = -1; - } - - private void applyOverrides(Renderer renderer, Shader shader, SafeArrayList overrides, BindUnits bindUnits) { - for (MatParamOverride override : overrides.getArray()) { - VarType type = override.getVarType(); - + // shader was changed + sortingId = -1; + } + + private MatParamUniformBuffer getMatParamUniformBuffer() { + if (matParamUniformBuffer == null) { + matParamUniformBuffer = new MatParamUniformBuffer(); + } + return matParamUniformBuffer; + } + + private void applyOverrides(Renderer renderer, Shader shader, SafeArrayList overrides, + BindUnits bindUnits, MatParamUniformBuffer matParamBuffer) { + for (MatParamOverride override : overrides.getArray()) { + VarType type = override.getVarType(); + MatParam paramDef = def.getMaterialParam(override.getName()); if (paramDef == null || paramDef.getVarType() != type || !override.isEnabled()) { continue; } - - Uniform uniform = shader.getUniform(override.getPrefixedName()); - - if (override.getValue() != null) { - updateShaderMaterialParameter(renderer, type, shader, override, bindUnits, true); - } else { - uniform.clearValue(); - } - } - } - - private void updateShaderMaterialParameter(Renderer renderer, VarType type, Shader shader, MatParam param, BindUnits unit, boolean override) { - if (type == VarType.UniformBufferObject || type == VarType.ShaderStorageBufferObject) { - ShaderBufferBlock bufferBlock = shader.getBufferBlock(param.getPrefixedName()); - BufferObject bufferObject = (BufferObject) param.getValue(); + + if (override.getValue() != null) { + updateShaderMaterialParameter(renderer, type, shader, override, bindUnits, true, matParamBuffer); + } else if (matParamBuffer.clear(override)) { + // The override intentionally clears a parameter stored in the UBO. + } else { + Uniform uniform = shader.getUniform(override.getPrefixedName()); + uniform.clearValue(); + } + } + } + + private void updateShaderMaterialParameter(Renderer renderer, VarType type, Shader shader, MatParam param, + BindUnits unit, boolean override, + MatParamUniformBuffer matParamBuffer) { + if (type == VarType.UniformBufferObject || type == VarType.ShaderStorageBufferObject) { + ShaderBufferBlock bufferBlock = shader.getBufferBlock(param.getPrefixedName()); + BufferObject bufferObject = (BufferObject) param.getValue(); ShaderBufferBlock.BufferType btype; if (type == VarType.ShaderStorageBufferObject) { btype = ShaderBufferBlock.BufferType.ShaderStorageBufferObject; } else { btype = ShaderBufferBlock.BufferType.UniformBufferObject; - } - bufferBlock.setBufferObject(btype, bufferObject); - } else { - Uniform uniform = shader.getUniform(param.getPrefixedName()); - if (!override && uniform.isSetByCurrentMaterial()) - return; + } + bufferBlock.setBufferObject(btype, bufferObject); + } else { + if (matParamBuffer.set(param, override)) { + return; + } + + Uniform uniform = shader.getUniform(param.getPrefixedName()); + if (!override && uniform.isSetByCurrentMaterial()) + return; if (type.isTextureType() || type.isImageType()) { try { @@ -912,25 +929,28 @@ private void updateShaderMaterialParameter(Renderer renderer, VarType type, Shad private BindUnits updateShaderMaterialParameters(Renderer renderer, Shader shader, SafeArrayList worldOverrides, SafeArrayList forcedOverrides) { - - bindUnits.textureUnit = 0; - bindUnits.bufferUnit = 0; - - if (worldOverrides != null) { - applyOverrides(renderer, shader, worldOverrides, bindUnits); - } - if (forcedOverrides != null) { - applyOverrides(renderer, shader, forcedOverrides, bindUnits); - } - - for (int i = 0; i < paramValues.size(); i++) { - MatParam param = paramValues.getValue(i); - VarType type = param.getVarType(); - updateShaderMaterialParameter(renderer, type, shader, param, bindUnits, false); - } - - // TODO: HACKY HACK remove this when texture unit is handled by the uniform. - return bindUnits; + + bindUnits.textureUnit = 0; + bindUnits.bufferUnit = 0; + MatParamUniformBuffer matParamBuffer = getMatParamUniformBuffer(); + matParamBuffer.begin(shader); + + if (worldOverrides != null) { + applyOverrides(renderer, shader, worldOverrides, bindUnits, matParamBuffer); + } + if (forcedOverrides != null) { + applyOverrides(renderer, shader, forcedOverrides, bindUnits, matParamBuffer); + } + + for (int i = 0; i < paramValues.size(); i++) { + MatParam param = paramValues.getValue(i); + VarType type = param.getVarType(); + updateShaderMaterialParameter(renderer, type, shader, param, bindUnits, false, matParamBuffer); + } + matParamBuffer.finish(shader); + + // TODO: HACKY HACK remove this when texture unit is handled by the uniform. + return bindUnits; } private void updateRenderState(Geometry geometry, RenderManager renderManager, Renderer renderer, TechniqueDef techniqueDef) { diff --git a/jme3-core/src/test/java/com/jme3/material/MatParamUniformBufferTest.java b/jme3-core/src/test/java/com/jme3/material/MatParamUniformBufferTest.java new file mode 100644 index 0000000000..ec48af555a --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/material/MatParamUniformBufferTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2009-2026 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 HOLDER 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 com.jme3.material; + +import com.jme3.math.ColorRGBA; +import com.jme3.shader.Shader; +import com.jme3.shader.ShaderBufferBlock; +import com.jme3.shader.VarType; +import com.jme3.shader.bufferobject.BufferBindingPoints; +import com.jme3.shader.bufferobject.BufferObject; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MatParamUniformBufferTest { + + @Test + public void parsesStd140MatParamBlock() { + MatParamUniformBuffer.Layout layout = MatParamUniformBuffer.parseLayout(shaderSource()); + + assertNotNull(layout); + assertEquals(BufferBindingPoints.MAT_PARAMS_BLOCK_NAME, layout.blockName); + assertEquals(48, layout.size); + assertEquals(0, layout.getMember("Color").offset); + assertEquals(16, layout.getMember("Roughness").offset); + assertEquals(32, layout.getMember("Weights").offset); + } + + @Test + public void parsesInstanceNamedMatParamBlock() { + MatParamUniformBuffer.Layout layout = MatParamUniformBuffer.parseLayout("#version 330\n" + + "layout(std140) uniform MatParams {\n" + + " vec4 Color;\n" + + "} m_MatParams;\n"); + + assertNotNull(layout); + assertEquals("MatParams", layout.blockName); + assertEquals(0, layout.getMember("Color").offset); + } + + @Test + public void writesInstanceNamedMatParamBlock() { + String source = "#version 330\n" + + "layout(std140) uniform MatParams {\n" + + " vec4 Color;\n" + + "} m_MatParams;\n"; + Shader shader = new Shader(); + shader.addSource(Shader.ShaderType.Fragment, "mat-param-instance-test.frag", source, null, "GLSL330"); + + MatParamUniformBuffer buffer = new MatParamUniformBuffer(); + buffer.begin(shader); + assertTrue(buffer.set(new MatParam(VarType.Vector4, "Color", new ColorRGBA(1f, 0f, 0f, 1f)), false)); + buffer.finish(shader); + + ShaderBufferBlock block = shader.getBufferBlock("MatParams"); + assertSame(buffer.getBufferObject(), block.getBufferObject()); + assertEquals(BufferBindingPoints.MAT_PARAMS_BLOCK_NAME, block.getBufferObject().getName()); + } + + @Test + public void ignoresBlocksWithUnsupportedMembers() { + MatParamUniformBuffer.Layout layout = MatParamUniformBuffer.parseLayout("#version 330\n" + + "layout(std140) uniform m_MatParams {\n" + + " vec4 Color;\n" + + " sampler2D Unsupported;\n" + + " float Roughness;\n" + + "};\n"); + + assertNull(layout); + } + + @Test + public void ignoresBlocksWithUnsupportedArrayDeclarators() { + MatParamUniformBuffer.Layout layout = MatParamUniformBuffer.parseLayout("#version 330\n" + + "#define WEIGHT_COUNT 4\n" + + "layout(std140) uniform m_MatParams {\n" + + " float Weights[WEIGHT_COUNT];\n" + + "};\n"); + + assertNull(layout); + } + + @Test + public void ignoresBlocksWithExplicitMemberLayout() { + MatParamUniformBuffer.Layout layout = MatParamUniformBuffer.parseLayout("#version 330\n" + + "layout(std140) uniform m_MatParams {\n" + + " layout(offset = 32) vec4 Color;\n" + + "};\n"); + + assertNull(layout); + } + + @Test + public void writesOnlyBlockMembersToBufferObject() { + Shader shader = new Shader(); + shader.addSource(Shader.ShaderType.Fragment, "mat-param-test.frag", shaderSource(), null, "GLSL330"); + + MatParamUniformBuffer buffer = new MatParamUniformBuffer(); + buffer.begin(shader); + + assertTrue(buffer.set(new MatParam(VarType.Vector4, "Color", new ColorRGBA(1f, 0.5f, 0.25f, 1f)), false)); + assertTrue(buffer.set(new MatParam(VarType.Float, "Roughness", 0.75f), false)); + assertFalse(buffer.set(new MatParam(VarType.Float, "Outside", 1f), false)); + buffer.finish(shader); + + ShaderBufferBlock block = shader.getBufferBlock(BufferBindingPoints.MAT_PARAMS_BLOCK_NAME); + BufferObject bo = block.getBufferObject(); + assertSame(buffer.getBufferObject(), bo); + assertEquals(ShaderBufferBlock.BufferType.UniformBufferObject, block.getType()); + + ByteBuffer data = bo.getByteData(); + assertEquals(1f, data.getFloat(0), 0.0001f); + assertEquals(0.5f, data.getFloat(4), 0.0001f); + assertEquals(0.25f, data.getFloat(8), 0.0001f); + assertEquals(1f, data.getFloat(12), 0.0001f); + assertEquals(0.75f, data.getFloat(16), 0.0001f); + } + + private static String shaderSource() { + return "#version 330\n" + + "layout(std140) uniform m_MatParams {\n" + + " vec4 Color;\n" + + " float Roughness;\n" + + " float Weights[1];\n" + + "};\n" + + "void main() {}\n"; + } +} diff --git a/jme3-examples/src/main/java/jme3test/material/TestMatParamUniformBuffer.java b/jme3-examples/src/main/java/jme3test/material/TestMatParamUniformBuffer.java new file mode 100644 index 0000000000..5a7fa5cc49 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/material/TestMatParamUniformBuffer.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2009-2026 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 HOLDER 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 jme3test.material; + +import com.jme3.app.SimpleApplication; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer; +import com.jme3.util.BufferUtils; + +/** + * Demonstrates automatic material-parameter UBO packing and custom vertex attributes. + */ +public class TestMatParamUniformBuffer extends SimpleApplication { + + public static void main(String[] args) { + TestMatParamUniformBuffer app = new TestMatParamUniformBuffer(); + app.start(); + } + + @Override + public void simpleInitApp() { + flyCam.setEnabled(false); + viewPort.setBackgroundColor(ColorRGBA.DarkGray); + + Material material = new Material(assetManager, "jme3test/matparamubo/MatParamUBO.j3md"); + material.setColor("Color", new ColorRGBA(0.1f, 0.7f, 1f, 1f)); + material.setFloat("Offset", 0.15f); + + Geometry geometry = new Geometry("MatParam UBO Quad", createGradientQuad()); + geometry.setMaterial(material); + rootNode.attachChild(geometry); + + cam.setLocation(new Vector3f(0f, 0f, 2.5f)); + cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + } + + private static Mesh createGradientQuad() { + Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer( + -1f, -1f, 0f, + 1f, -1f, 0f, + 1f, 1f, 0f, + -1f, 1f, 0f)); + mesh.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createShortBuffer( + (short) 0, (short) 1, (short) 2, + (short) 0, (short) 2, (short) 3)); + mesh.setBuffer("inGradient", 1, VertexBuffer.Format.Float, BufferUtils.createFloatBuffer( + 0f, 0.35f, 1f, 0.65f)); + mesh.updateBound(); + return mesh; + } +} diff --git a/jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.frag b/jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.frag new file mode 100644 index 0000000000..ee50313c9b --- /dev/null +++ b/jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.frag @@ -0,0 +1,13 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" +layout(std140) uniform m_MatParams { + vec4 Color; +}; + +uniform float m_Offset; + +in float gradient; + +void main() { + float intensity = clamp(gradient + m_Offset, 0.0, 1.0); + outFragColor = vec4(Color.rgb * intensity, Color.a); +} diff --git a/jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.j3md b/jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.j3md new file mode 100644 index 0000000000..31da9a5b63 --- /dev/null +++ b/jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.j3md @@ -0,0 +1,15 @@ +MaterialDef MatParamUBO { + MaterialParameters { + Vector4 Color + Float Offset + } + + Technique { + VertexShader GLSL300 GLSL150 : jme3test/matparamubo/MatParamUBO.vert + FragmentShader GLSL300 GLSL150 : jme3test/matparamubo/MatParamUBO.frag + + WorldParameters { + WorldViewProjectionMatrix + } + } +} diff --git a/jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.vert b/jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.vert new file mode 100644 index 0000000000..9107ae6c74 --- /dev/null +++ b/jme3-examples/src/main/resources/jme3test/matparamubo/MatParamUBO.vert @@ -0,0 +1,13 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +in vec3 inPosition; +in float inGradient; + +uniform mat4 g_WorldViewProjectionMatrix; + +out float gradient; + +void main() { + gradient = inGradient; + gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); +} From 8572ea451bf938210e1eee79dda4c198b7594285 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 10 Jun 2026 20:11:17 +0200 Subject: [PATCH 13/15] Use generic BufferObject path in SdsmFitter SSBO setup --- .../jme3/renderer/opengl/ComputeShader.java | 17 +++++- .../main/java/com/jme3/shadow/SdsmFitter.java | 59 +++++++++++++------ 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/ComputeShader.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/ComputeShader.java index 04173b0eea..270d8df957 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/ComputeShader.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/ComputeShader.java @@ -36,6 +36,7 @@ import com.jme3.math.Vector3f; import com.jme3.math.Vector4f; import com.jme3.renderer.RendererException; +import com.jme3.shader.bufferobject.BufferObject; import com.jme3.util.BufferUtils; import com.jme3.util.NativeObject; @@ -199,6 +200,20 @@ public void bindShaderStorageBuffer(int location, ShaderStorageBufferObject ssbo gl.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, location, ssbo.getId()); } + /** + * Binds a renderer-managed shader storage buffer. + * The buffer must have been uploaded through the renderer before calling this method. + * + * @param location shader storage buffer binding point + * @param bufferObject uploaded buffer object + */ + public void bindShaderStorageBuffer(int location, BufferObject bufferObject) { + if (bufferObject.getId() == NativeObject.INVALID_ID) { + throw new RendererException("BufferObject must be uploaded before binding it to a compute shader"); + } + gl.glBindBufferBase(GL4.GL_SHADER_STORAGE_BUFFER, location, bufferObject.getId()); + } + @Override public void resetObject() { id = INVALID_ID; @@ -223,4 +238,4 @@ public long getUniqueId() { //Note this is the same type of ID as a regular shader. return ((long)OBJTYPE_SHADER << 32) | (0xffffffffL & (long)id); } -} \ No newline at end of file +} diff --git a/jme3-core/src/main/java/com/jme3/shadow/SdsmFitter.java b/jme3-core/src/main/java/com/jme3/shadow/SdsmFitter.java index 6bdfb6d109..0219e1191c 100644 --- a/jme3-core/src/main/java/com/jme3/shadow/SdsmFitter.java +++ b/jme3-core/src/main/java/com/jme3/shadow/SdsmFitter.java @@ -40,9 +40,12 @@ import com.jme3.renderer.opengl.ComputeShader; import com.jme3.renderer.opengl.GL4; import com.jme3.renderer.opengl.GLFence; -import com.jme3.renderer.opengl.ShaderStorageBufferObject; +import com.jme3.shader.bufferobject.BufferObject; import com.jme3.texture.Texture; +import com.jme3.util.BufferUtils; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; @@ -228,16 +231,14 @@ public String toString() { * Internal holder for in-flight fit operations. */ private class SdsmResultHolder { - ShaderStorageBufferObject minMaxDepthSsbo; - ShaderStorageBufferObject fitFrustumSsbo; + BufferObject minMaxDepthSsbo; + BufferObject fitFrustumSsbo; FitParameters parameters; GLFence fence; SdsmResultHolder() { - this.minMaxDepthSsbo = new ShaderStorageBufferObject(gl4); - renderer.registerNativeObject(this.minMaxDepthSsbo); - this.fitFrustumSsbo = new ShaderStorageBufferObject(gl4); - renderer.registerNativeObject(this.fitFrustumSsbo); + this.minMaxDepthSsbo = createStorageBuffer("SDSM MinMax Depth"); + this.fitFrustumSsbo = createStorageBuffer("SDSM Fit Frustum"); } boolean isReady(boolean wait) { @@ -258,8 +259,7 @@ SplitFitResult extract() { } private SplitFit extractFit() { - if(fitFrustumSsbo.isUpdateNeeded()){ return null; } - int[] uintFit = fitFrustumSsbo.read(32); + int[] uintFit = readIntStorageBuffer(fitFrustumSsbo, 32); float[] fitResult = new float[32]; for(int i=0;i Date: Wed, 10 Jun 2026 20:11:43 +0200 Subject: [PATCH 14/15] update tests and screenshots --- .../TestVertexBufferParticleBenchmark.java | 4 +- .../bin/main/TestSSBOBinding/SSBOBinding.vert | 9 +++ .../TestSSBOBinding/SSBOBinding0OnSecond.frag | 23 ++++++ .../TestSSBOBinding/SSBOBinding0OnSecond.j3md | 17 +++++ .../main/TestSSBOBinding/SSBOCollision.frag | 23 ++++++ .../main/TestSSBOBinding/SSBOCollision.j3md | 17 +++++ .../TestSSBOBinding/SSBOExplicitBindings.frag | 20 ++++++ .../TestSSBOBinding/SSBOExplicitBindings.j3md | 17 +++++ .../TestSSBOBinding/SSBOMixedBindings.frag | 21 ++++++ .../TestSSBOBinding/SSBOMixedBindings.j3md | 17 +++++ .../main/TestSSBOBinding/SSBONoBindings.frag | 21 ++++++ .../main/TestSSBOBinding/SSBONoBindings.j3md | 17 +++++ .../screenshottests/ssbo/TestSSBOBinding.java | 70 +++++++++++++++++++ .../scenarios/ssbo/ScenarioSSBOBinding.java} | 64 +++++------------ 14 files changed, 291 insertions(+), 49 deletions(-) create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOBinding.vert create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOBinding0OnSecond.frag create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOBinding0OnSecond.j3md create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOCollision.frag create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOCollision.j3md create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOExplicitBindings.frag create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOExplicitBindings.j3md create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOMixedBindings.frag create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOMixedBindings.j3md create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBONoBindings.frag create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBONoBindings.j3md create mode 100644 jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java rename jme3-screenshot-tests/{src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java => jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/ssbo/ScenarioSSBOBinding.java} (59%) diff --git a/jme3-examples/src/main/java/jme3test/stress/TestVertexBufferParticleBenchmark.java b/jme3-examples/src/main/java/jme3test/stress/TestVertexBufferParticleBenchmark.java index 726ac270e8..ecef6c27ac 100644 --- a/jme3-examples/src/main/java/jme3test/stress/TestVertexBufferParticleBenchmark.java +++ b/jme3-examples/src/main/java/jme3test/stress/TestVertexBufferParticleBenchmark.java @@ -20,8 +20,8 @@ public class TestVertexBufferParticleBenchmark extends SimpleApplication implements ActionListener { - private static final int DEFAULT_PARTICLES = 250_000; - private static final int DEFAULT_MOVING_PARTICLES = 25_000; + private static final int DEFAULT_PARTICLES = 2_000_000; + private static final int DEFAULT_MOVING_PARTICLES = 200_000; private final int particleCount = Integer.getInteger("particle.count", DEFAULT_PARTICLES); private final int movingCount = Math.min(Integer.getInteger("particle.moving", DEFAULT_MOVING_PARTICLES), particleCount); diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOBinding.vert b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOBinding.vert new file mode 100644 index 0000000000..6bdec22867 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/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/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOBinding0OnSecond.frag b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOBinding0OnSecond.frag new file mode 100644 index 0000000000..b01fba8ff4 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/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/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOBinding0OnSecond.j3md b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOBinding0OnSecond.j3md new file mode 100644 index 0000000000..890a44e44c --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/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/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOCollision.frag b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOCollision.frag new file mode 100644 index 0000000000..a9e4df49fb --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/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/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOCollision.j3md b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOCollision.j3md new file mode 100644 index 0000000000..5f4d606033 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/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/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOExplicitBindings.frag b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOExplicitBindings.frag new file mode 100644 index 0000000000..5faa4813f5 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/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/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOExplicitBindings.j3md b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOExplicitBindings.j3md new file mode 100644 index 0000000000..b42681c8ed --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/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/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOMixedBindings.frag b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOMixedBindings.frag new file mode 100644 index 0000000000..bc301b48de --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/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/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOMixedBindings.j3md b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBOMixedBindings.j3md new file mode 100644 index 0000000000..3f9e5ce953 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/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/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBONoBindings.frag b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBONoBindings.frag new file mode 100644 index 0000000000..4b02c572be --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/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/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBONoBindings.j3md b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/TestSSBOBinding/SSBONoBindings.j3md new file mode 100644 index 0000000000..72407a5aaf --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/bin/main/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/jme3-screenshot-tests-desktop/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java new file mode 100644 index 0000000000..becdc782c9 --- /dev/null +++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java @@ -0,0 +1,70 @@ +/* + * 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 java.util.stream.Stream; +import org.jmonkeyengine.screenshottests.scenarios.ssbo.ScenarioSSBOBinding; +import org.jmonkeyengine.screenshottests.testframework.TestType; +import org.jmonkeyengine.screenshottests.testframework.desktop.DesktopRunner; +import org.jmonkeyengine.screenshottests.testframework.desktop.ScreenshotTestDesktopBase; +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; + +/** + * Tests that SSBO binding points are correctly resolved when buffers are set via the material system. + */ +@SuppressWarnings("OptionalGetWithoutIsPresent") +public class TestSSBOBinding extends ScreenshotTestDesktopBase { + + 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; + + ScenarioSSBOBinding.testSSBOBinding(matDefPath) + .setBaseImageFileName(imageName) + .setTestType(testType) + .run(new DesktopRunner()); + } +} diff --git a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java b/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/ssbo/ScenarioSSBOBinding.java similarity index 59% rename from jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java rename to jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/ssbo/ScenarioSSBOBinding.java index 97338fb5db..c911243529 100644 --- a/jme3-screenshot-tests/src/test/java/org/jmonkeyengine/screenshottests/ssbo/TestSSBOBinding.java +++ b/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/ssbo/ScenarioSSBOBinding.java @@ -29,7 +29,9 @@ * 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; +package org.jmonkeyengine.screenshottests.scenarios.ssbo; + +import static org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase.screenshotTest; import com.jme3.app.Application; import com.jme3.app.SimpleApplication; @@ -41,49 +43,19 @@ 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; +import org.jmonkeyengine.screenshottests.testframework.ScreenshotTest; /** - * 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. + * Tests SSBO binding point resolution with different layout(binding=N) combinations. */ -@SuppressWarnings("OptionalGetWithoutIsPresent") -public class TestSSBOBinding extends ScreenshotTestBase { +public final class ScenarioSSBOBinding { - 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) - ); + private ScenarioSSBOBinding() { } - @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() { + public static ScreenshotTest testSSBOBinding(String matDefPath) { + return screenshotTest(new BaseAppState() { @Override protected void initialize(Application app) { SimpleApplication simpleApp = (SimpleApplication) app; @@ -93,7 +65,6 @@ protected void initialize(Application app) { 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)); @@ -105,23 +76,22 @@ protected void initialize(Application app) { } @Override - protected void cleanup(Application app) {} + protected void cleanup(Application app) { + } @Override - protected void onEnable() {} + protected void onEnable() { + } @Override - protected void onDisable() {} - }) - .setBaseImageFileName(imageName) - .setTestType(testType) - .setFramesToTakeScreenshotsOn(1) - .run(); + protected void onDisable() { + } + }).setFramesToTakeScreenshotsOn(1); } private static BufferObject createColorBuffer(float r, float g, float b, float a) { BufferObject bo = new BufferObject(); - ByteBuffer buf = BufferUtils.createByteBuffer(16); // vec4 = 4 floats + ByteBuffer buf = BufferUtils.createByteBuffer(16); buf.putFloat(r).putFloat(g).putFloat(b).putFloat(a); buf.flip(); bo.setData(buf); From 8f9a9b4465c6dda3116fc292000c892ba8bb4ec0 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 10 Jun 2026 22:56:59 +0200 Subject: [PATCH 15/15] apply gemini suggestions --- .../jme3/material/MatParamUniformBuffer.java | 25 +++++++++++-------- .../com/jme3/renderer/opengl/GLRenderer.java | 15 ++++++++--- .../java/com/jme3/scene/VertexBuffer.java | 21 +++++++++------- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/material/MatParamUniformBuffer.java b/jme3-core/src/main/java/com/jme3/material/MatParamUniformBuffer.java index 8cfddc4f15..f4170b12d1 100644 --- a/jme3-core/src/main/java/com/jme3/material/MatParamUniformBuffer.java +++ b/jme3-core/src/main/java/com/jme3/material/MatParamUniformBuffer.java @@ -82,6 +82,7 @@ final class MatParamUniformBuffer { private Shader shader; private Object[] values; private boolean[] set; + private ByteBuffer tempBuffer; /** * Creates the reusable material-parameter UBO owned by one material @@ -202,7 +203,7 @@ void finish(Shader shader) { return; } - ByteBuffer data = BufferUtils.createByteBuffer(layout.size); + ByteBuffer data = getTempBuffer(layout.size); for (Member member : layout.members) { Object value = set[member.index] ? values[member.index] : member.zeroValue(); data.position(member.offset); @@ -229,6 +230,15 @@ BufferObject getBufferObject() { return bufferObject; } + private ByteBuffer getTempBuffer(int size) { + if (tempBuffer == null || tempBuffer.capacity() < size) { + tempBuffer = BufferUtils.createByteBuffer(size); + } + tempBuffer.clear(); + tempBuffer.limit(size); + return tempBuffer; + } + /** * Finds a supported material-parameter UBO declaration in the shader. * @@ -348,19 +358,14 @@ private static Layout parseMembers(String blockName, String body) { * position or limit of either buffer. */ private static boolean contentEquals(ByteBuffer current, ByteBuffer next) { + if (current == null || next == null) { + return current == next; + } ByteBuffer a = current.duplicate(); ByteBuffer b = next.duplicate(); a.clear(); b.clear(); - if (a.remaining() != b.remaining()) { - return false; - } - for (int i = 0; i < a.remaining(); i++) { - if (a.get(i) != b.get(i)) { - return false; - } - } - return true; + return a.equals(b); } /** 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 eb70389c08..1e0e10aa6c 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 @@ -1918,6 +1918,7 @@ public int convertShaderType(ShaderType type) { private static final Pattern BINDING_ZERO_PATTERN = Pattern.compile( "layout\\s*\\([^)]*binding\\s*=\\s*0[^)]*\\)\\s*(buffer|uniform)\\s+\\w+"); + private static final Pattern GLSL_COMMENT_PATTERN = Pattern.compile("(?s)/\\*.*?\\*/|//.*?(?=\\R|$)"); /** * Checks that layout(binding=0) is not used on a non-first buffer block, @@ -1928,8 +1929,12 @@ public int convertShaderType(ShaderType type) { * @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); + if (source == null) { + return; + } + String cleanSource = GLSL_COMMENT_PATTERN.matcher(source).replaceAll(""); + Matcher allBlocks = BUFFER_BLOCK_PATTERN.matcher(cleanSource); + Matcher binding0Blocks = BINDING_ZERO_PATTERN.matcher(cleanSource); // Find positions of all buffer/uniform block declarations List allPositions = new ArrayList<>(); @@ -3496,6 +3501,11 @@ private int convertFormat(Format format) { @Override public void updateBufferData(VertexBuffer vb) { + Buffer data = vb.getData(); + if (data == null) { + return; + } + int bufId = vb.getId(); boolean created = false; if (bufId == -1) { @@ -3533,7 +3543,6 @@ public void updateBufferData(VertexBuffer vb) { } int usage = convertUsage(vb.getUsage()); - Buffer data = vb.getData(); data.rewind(); if (created || vb.hasDataSizeChanged() || !vb.hasRegions()) { diff --git a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java index 050137d678..cdc2aea5ad 100644 --- a/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java +++ b/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java @@ -915,25 +915,28 @@ private ByteBuffer createByteDataCopy(Buffer source) { if (source instanceof FloatBuffer) { FloatBuffer fb = (FloatBuffer) source; ByteBuffer result = BufferUtils.createByteBuffer(fb.limit() * Float.BYTES); - for (int i = 0; i < fb.limit(); i++) { - result.putFloat(fb.get(i)); - } + FloatBuffer copySource = fb.duplicate(); + copySource.position(0); + copySource.limit(fb.limit()); + result.asFloatBuffer().put(copySource); result.clear(); return result; } else if (source instanceof ShortBuffer) { ShortBuffer sb = (ShortBuffer) source; ByteBuffer result = BufferUtils.createByteBuffer(sb.limit() * Short.BYTES); - for (int i = 0; i < sb.limit(); i++) { - result.putShort(sb.get(i)); - } + ShortBuffer copySource = sb.duplicate(); + copySource.position(0); + copySource.limit(sb.limit()); + result.asShortBuffer().put(copySource); result.clear(); return result; } else if (source instanceof IntBuffer) { IntBuffer ib = (IntBuffer) source; ByteBuffer result = BufferUtils.createByteBuffer(ib.limit() * Integer.BYTES); - for (int i = 0; i < ib.limit(); i++) { - result.putInt(ib.get(i)); - } + IntBuffer copySource = ib.duplicate(); + copySource.position(0); + copySource.limit(ib.limit()); + result.asIntBuffer().put(copySource); result.clear(); return result; }