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/material/MatParamUniformBuffer.java b/jme3-core/src/main/java/com/jme3/material/MatParamUniformBuffer.java
new file mode 100644
index 0000000000..f4170b12d1
--- /dev/null
+++ b/jme3-core/src/main/java/com/jme3/material/MatParamUniformBuffer.java
@@ -0,0 +1,664 @@
+/*
+ * 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;
+ private ByteBuffer tempBuffer;
+
+ /**
+ * 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 = getTempBuffer(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;
+ }
+
+ 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.
+ *
+ * @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) {
+ if (current == null || next == null) {
+ return current == next;
+ }
+ ByteBuffer a = current.duplicate();
+ ByteBuffer b = next.duplicate();
+ a.clear();
+ b.clear();
+ return a.equals(b);
+ }
+
+ /**
+ * 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 0c4317a307..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,50 +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;
- 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++;
- } 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 {
@@ -916,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/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/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/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/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-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java
index 82bf96a428..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
@@ -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;
@@ -72,6 +73,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;
@@ -409,7 +411,7 @@ private void loadCapabilitiesCommon() {
caps.add(Caps.IntegerIndexBuffer);
}
- if (hasAnyExtension("GL_OES_texture_buffer", "GL_EXT_texture_buffer")
+ if (hasAnyExtension("GL_OES_texture_buffer", "GL_EXT_texture_buffer")
|| caps.contains(Caps.OpenGL31)
|| caps.contains(Caps.OpenGLES32)
) {
@@ -702,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
@@ -728,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)) {
@@ -831,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);
@@ -858,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);
@@ -1618,7 +1634,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) {
@@ -1631,39 +1646,20 @@ protected void updateShaderBufferBlock(final Shader shader, final ShaderBufferBl
int usage = resolveUsageHint(bufferObject.getAccessHint(), bufferObject.getNatureHint());
if (usage == -1) return; // cpu only
- bindProgram(shader);
-
- final int shaderId = shader.getId();
-
- int bindingPoint = bufferObject.getBinding();
+ int bindingPoint = bufferBlock.getBinding();
+ if (bindingPoint < 0) {
+ // Binding not yet resolved — skip until resolveBufferBlockBindings runs
+ bufferBlock.clearUpdateNeeded();
+ return;
+ }
switch (bufferType) {
case UniformBufferObject: {
- setUniformBufferObject(bindingPoint, bufferObject); // rebind buffer if needed
- if (bufferBlock.isUpdateNeeded()) {
- int blockIndex = bufferBlock.getLocation();
- if (blockIndex < 0) {
- blockIndex = 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: {
@@ -1674,6 +1670,186 @@ 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 = getProgramResourceIndex(shaderId, GL4.GL_SHADER_STORAGE_BLOCK, block.getName());
+ } else {
+ blockIndex = getUniformBlockIndex(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 = queryUniformBlockBinding(shaderId, blockIndex);
+ }
+ block.setBinding(binding);
+ }
+
+ 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);
+ int binding = block.getBinding();
+ 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 (binding < 0 || !usedBindings.add(binding)) {
+ // Collision within the same namespace — find a free binding point
+ if (bufferType == BufferType.ShaderStorageBufferObject) {
+ 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 (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);
+ }
+ }
+ }
+
+ 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()));
+ }
+
+ /**
+ * Queries the binding point of a shader storage block using
+ * glGetProgramResourceiv with GL_BUFFER_BINDING.
+ *
+ * @param program the shader program id.
+ * @param blockIndex the block index within the program.
+ * @return the binding point assigned to the block.
+ */
+ private int queryShaderStorageBlockBinding(int program, int blockIndex) {
+ intBuf16.clear();
+ intBuf16.put(GL4.GL_BUFFER_BINDING).flip();
+ intBuf1.clear();
+ 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);
+ }
+
protected void updateShaderUniforms(Shader shader) {
ListMap uniforms = shader.getUniformMap();
for (int i = 0; i < uniforms.size(); i++) {
@@ -1691,11 +1867,27 @@ protected void updateShaderUniforms(Shader shader) {
*/
protected void updateShaderBufferBlocks(final Shader shader) {
final ListMap bufferBlocks = shader.getBufferBlockMap();
+ if (hasUnresolvedBufferBlockBindings(bufferBlocks)) {
+ resolveBufferBlockBindings(shader);
+ }
+
for (int i = 0; i < bufferBlocks.size(); i++) {
updateShaderBufferBlock(shader, bufferBlocks.getValue(i));
}
}
+ 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++) {
@@ -1721,6 +1913,50 @@ 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+");
+ 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,
+ * 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) {
+ 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<>();
+ 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) {
@@ -1761,7 +1997,7 @@ public void updateShaderSourceData(ShaderSource source) {
}
stringBuf.append(version);
-
+
if (version >= 150) {
if(gles3) {
stringBuf.append(" es");
@@ -1795,7 +2031,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();
@@ -3113,14 +3353,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);
@@ -3133,13 +3374,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);
@@ -3148,6 +3390,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)
*/
@@ -3252,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) {
@@ -3289,29 +3543,124 @@ public void updateBufferData(VertexBuffer vb) {
}
int usage = convertUsage(vb.getUsage());
- vb.getData().rewind();
+ 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) {
@@ -3363,6 +3712,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());
@@ -3389,7 +3781,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() });
}
@@ -3466,13 +3859,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..cdc2aea5ad 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,245 @@ 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);
+ 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);
+ 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);
+ IntBuffer copySource = ib.duplicate();
+ copySource.position(0);
+ copySource.limit(ib.limit());
+ result.asIntBuffer().put(copySource);
+ 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 +1041,7 @@ public void convertToHalf() {
halfData.putShort(half);
}
this.data = halfData;
+ this.dataBytes = null;
setUpdateNeeded();
dataSizeChanged = true;
}
@@ -773,10 +1061,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 +1076,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 +1085,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 +1093,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 +1126,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 +1152,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 +1176,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 +1256,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 +1265,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 +1274,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 +1282,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 +1291,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 +1373,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 +1403,7 @@ public String toString() {
public void resetObject() {
// assert this.id != -1;
this.id = -1;
+ unsetRegions();
setUpdateNeeded();
}
@@ -1104,9 +1414,7 @@ public void deleteObject(Object rendererObject) {
@Override
protected void deleteNativeBuffers() {
- if (data != null) {
- BufferUtils.destroyDirectBuffer(data);
- }
+ super.deleteNativeBuffers();
}
@Override
@@ -1131,6 +1439,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 +1477,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 +1485,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..cf8e43d167 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;
}
/**
@@ -309,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);
@@ -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/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/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/shader/bufferobject/BufferObject.java b/jme3-core/src/main/java/com/jme3/shader/bufferobject/BufferObject.java
index 6823d8f5fb..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,67 +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;
}
+ 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;
}
/**
@@ -234,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
*
@@ -242,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();
}
@@ -269,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
@@ -337,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
@@ -358,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/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/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 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 extends Struct> 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/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-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/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/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));
+ }
+}
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 7f33494421..949ab23c15 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();
@@ -54,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 = "";
@@ -77,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;
@@ -214,5 +250,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.getByteData().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.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.getByteData();
+ 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.getByteData().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());
}
}
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/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..ecef6c27ac
--- /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 = 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);
+ 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/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);
+}
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);
+}
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/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-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/LwjglGL.java b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java
index f449d1c6b1..a9d5afb9d5 100644
--- a/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java
+++ b/jme3-lwjgl3/src/main/java/com/jme3/renderer/lwjgl/LwjglGL.java
@@ -698,5 +698,15 @@ public void glBindBufferBase(final int target, final int index, final int buffer
public void glUniformBlockBinding(final int program, final int uniformBlockIndex, final int uniformBlockBinding) {
GL31.glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding);
}
-
+
+ @Override
+ public int glGetActiveUniformBlocki(final int program, final int uniformBlockIndex, final int pname) {
+ return GL31.glGetActiveUniformBlocki(program, uniformBlockIndex, pname);
+ }
+
+ @Override
+ public void glGetProgramResourceiv(final int program, final int programInterface, final int index, IntBuffer props, IntBuffer length, IntBuffer params) {
+ GL43.glGetProgramResourceiv(program, programInterface, index, props, length, params);
+ }
+
}
diff --git a/jme3-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);
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/main/resources/TestSSBOBinding/SSBOBinding.vert b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOBinding.vert
new file mode 100644
index 0000000000..6bdec22867
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/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/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOBinding0OnSecond.frag b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOBinding0OnSecond.frag
new file mode 100644
index 0000000000..b01fba8ff4
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/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/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOBinding0OnSecond.j3md b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOBinding0OnSecond.j3md
new file mode 100644
index 0000000000..890a44e44c
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/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/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOCollision.frag b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOCollision.frag
new file mode 100644
index 0000000000..a9e4df49fb
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/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/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOCollision.j3md b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOCollision.j3md
new file mode 100644
index 0000000000..5f4d606033
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/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/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOExplicitBindings.frag b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOExplicitBindings.frag
new file mode 100644
index 0000000000..5faa4813f5
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/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/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOExplicitBindings.j3md b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOExplicitBindings.j3md
new file mode 100644
index 0000000000..b42681c8ed
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/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/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOMixedBindings.frag b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOMixedBindings.frag
new file mode 100644
index 0000000000..bc301b48de
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/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/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOMixedBindings.j3md b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBOMixedBindings.j3md
new file mode 100644
index 0000000000..3f9e5ce953
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/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/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBONoBindings.frag b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBONoBindings.frag
new file mode 100644
index 0000000000..4b02c572be
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/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/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBONoBindings.j3md b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/TestSSBOBinding/SSBONoBindings.j3md
new file mode 100644
index 0000000000..72407a5aaf
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/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/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Binding0OnSecond_f1.png b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Binding0OnSecond_f1.png
new file mode 100644
index 0000000000..dd3c768e9b
Binary files /dev/null and b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Binding0OnSecond_f1.png differ
diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Collision_f1.png b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Collision_f1.png
new file mode 100644
index 0000000000..dd3c768e9b
Binary files /dev/null and b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_Collision_f1.png differ
diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_ExplicitBindings_f1.png b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_ExplicitBindings_f1.png
new file mode 100644
index 0000000000..dd3c768e9b
Binary files /dev/null and b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_ExplicitBindings_f1.png differ
diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_MixedBindings_f1.png b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_MixedBindings_f1.png
new file mode 100644
index 0000000000..dd3c768e9b
Binary files /dev/null and b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_MixedBindings_f1.png differ
diff --git a/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_NoBindings_f1.png b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_NoBindings_f1.png
new file mode 100644
index 0000000000..dd3c768e9b
Binary files /dev/null and b/jme3-screenshot-tests/jme3-screenshot-tests-desktop/src/main/resources/org.jmonkeyengine.screenshottests.ssbo.TestSSBOBinding.testSSBOBinding_NoBindings_f1.png differ
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/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/ssbo/ScenarioSSBOBinding.java b/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/ssbo/ScenarioSSBOBinding.java
new file mode 100644
index 0000000000..c911243529
--- /dev/null
+++ b/jme3-screenshot-tests/jme3-screenshot-tests-shared/src/main/java/org/jmonkeyengine/screenshottests/scenarios/ssbo/ScenarioSSBOBinding.java
@@ -0,0 +1,100 @@
+/*
+ * 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.scenarios.ssbo;
+
+import static org.jmonkeyengine.screenshottests.testframework.ScreenshotTestBase.screenshotTest;
+
+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 java.nio.ByteBuffer;
+import org.jmonkeyengine.screenshottests.testframework.ScreenshotTest;
+
+/**
+ * Tests SSBO binding point resolution with different layout(binding=N) combinations.
+ */
+public final class ScenarioSSBOBinding {
+
+ private ScenarioSSBOBinding() {
+ }
+
+ public static ScreenshotTest testSSBOBinding(String matDefPath) {
+ return 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() {
+ }
+ }).setFramesToTakeScreenshotsOn(1);
+ }
+
+ private static BufferObject createColorBuffer(float r, float g, float b, float a) {
+ BufferObject bo = new BufferObject();
+ ByteBuffer buf = BufferUtils.createByteBuffer(16);
+ buf.putFloat(r).putFloat(g).putFloat(b).putFloat(a);
+ buf.flip();
+ bo.setData(buf);
+ return bo;
+ }
+}