Skip to content

Commit 3df0161

Browse files
Copilotriccardobl
andauthored
Auto-scale singlePassLightBatchSize exponentially up to hardware-clamped maximum (jMonkeyEngine#2649)
* Initial plan * Auto-scale singlePassLightBatchSize exponentially up to 16 in RenderManager Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> Agent-Logs-Url: https://github.com/jMonkeyEngine/jmonkeyengine/sessions/24e4cfb9-5410-45e8-a112-3bcab1bece4a * Address review: add setMaxSinglePassLightBatchSize, pin size in set, use FastMath.nearestPowerOfTwo Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> Agent-Logs-Url: https://github.com/jMonkeyEngine/jmonkeyengine/sessions/0fb649f6-9ff3-4f61-99a2-9adfd5d4a981 * increase default max single pass light batch limit to 32 * Clamp maxSinglePassLightBatchSize by FragmentUniformVectors; init via constructor Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> Agent-Logs-Url: https://github.com/jMonkeyEngine/jmonkeyengine/sessions/7f45fdc6-5fdc-4d0d-8618-5874f95dbf29 * Log warning when maxSinglePassLightBatchSize is clamped below 16 by hardware Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> Agent-Logs-Url: https://github.com/jMonkeyEngine/jmonkeyengine/sessions/80c83fa1-19e7-4e6f-a032-3d5fe7be1ff7 * Guard auto-scaling against forced-technique/forced-material rendering passes Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> Agent-Logs-Url: https://github.com/jMonkeyEngine/jmonkeyengine/sessions/7639aa59-5a5c-4583-9703-ce7dfa4b8786 * Add MAX_DEFAULT constant; guard autoscaling to SinglePass/SinglePassAndImageBased techniques only Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> Agent-Logs-Url: https://github.com/jMonkeyEngine/jmonkeyengine/sessions/5ba0a4db-bee3-4a97-9535-3b22c7dd92ea * clean and improve logic * resize after first render, to let the material logic select the technique internally --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> Co-authored-by: Riccardo Balbo <riccardo0blb@gmail.com> Co-authored-by: Riccardo Balbo <os@rblb.it>
1 parent 912fba5 commit 3df0161

1 file changed

Lines changed: 106 additions & 3 deletions

File tree

jme3-core/src/main/java/com/jme3/renderer/RenderManager.java

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import com.jme3.material.RenderState;
4545
import com.jme3.material.Technique;
4646
import com.jme3.material.TechniqueDef;
47+
import com.jme3.math.FastMath;
4748
import com.jme3.math.Matrix4f;
4849
import com.jme3.post.SceneProcessor;
4950
import com.jme3.profile.AppProfiler;
@@ -74,6 +75,7 @@
7475
import java.util.function.Function;
7576
import java.util.function.Predicate;
7677
import java.util.function.Supplier;
78+
import java.util.logging.Level;
7779
import java.util.logging.Logger;
7880

7981
/**
@@ -86,6 +88,21 @@ public class RenderManager {
8688

8789
private static final Logger logger = Logger.getLogger(RenderManager.class.getName());
8890

91+
/**
92+
* Number of vec4 fragment uniform vectors consumed per light in g_LightData (lightColor, lightData1,
93+
* lightData2/spotDir).
94+
*/
95+
private static final int VEC4_UNIFORMS_PER_LIGHT = 3;
96+
/**
97+
* Fraction of the total fragment uniform budget reserved for shader parameters other than g_LightData
98+
* (material params, transforms, etc.). A value of 4 means one quarter is reserved.
99+
*/
100+
private static final int RESERVED_UNIFORM_FRACTION = 4;
101+
/**
102+
* Hard upper limit for reserved uniform budget
103+
*/
104+
private static final int RESERVED_UNIFORMS_MAX = 16;
105+
89106
private final Renderer renderer;
90107
private final UniformBindingManager uniformBindingManager = new UniformBindingManager();
91108
private final ArrayList<ViewPort> preViewPorts = new ArrayList<>();
@@ -108,6 +125,7 @@ public class RenderManager {
108125
private LightFilter lightFilter = new DefaultLightFilter();
109126
private TechniqueDef.LightMode preferredLightMode = TechniqueDef.LightMode.MultiPass;
110127
private int singlePassLightBatchSize = 1;
128+
private int maxSinglePassLightBatchSize = 16;
111129
private final MatParamOverride boundDrawBufferId = new MatParamOverride(VarType.Int, "BoundDrawBuffer", 0);
112130
private Predicate<Geometry> renderFilter;
113131

@@ -123,6 +141,7 @@ public RenderManager(Renderer renderer) {
123141
this.forcedOverrides.add(boundDrawBufferId);
124142
// register default pipeline context
125143
contexts.put(PipelineContext.class, new DefaultPipelineContext());
144+
setMaxSinglePassLightBatchSize(maxSinglePassLightBatchSize);
126145
}
127146

128147
/**
@@ -761,6 +780,20 @@ public void renderGeometry(Geometry geom) {
761780
renderGeometry(geom, lightList);
762781
}
763782

783+
/**
784+
* Auto-scale singlePassLightBatchSize exponentially (powers of 2) up to maxSinglePassLightBatchSize only
785+
* when a tecnique needs it
786+
*
787+
* @param tech
788+
* @param nLights
789+
*/
790+
private void maybeResizeLightBatch(TechniqueDef tech, int nLights) {
791+
boolean isSPL = tech.getLightMode() == TechniqueDef.LightMode.SinglePass || tech.getLightMode() == TechniqueDef.LightMode.SinglePassAndImageBased;
792+
if (isSPL && nLights > singlePassLightBatchSize && singlePassLightBatchSize < maxSinglePassLightBatchSize) {
793+
singlePassLightBatchSize = Math.min(FastMath.nearestPowerOfTwo(nLights), maxSinglePassLightBatchSize);
794+
}
795+
}
796+
764797
/**
765798
* Renders a single {@link Geometry} with a specific list of lights.
766799
* This method applies the world transform, handles forced materials and techniques,
@@ -811,8 +844,12 @@ public void renderGeometry(Geometry geom, LightList lightList) {
811844
//forcing forced technique renderState
812845
forcedRenderState = geom.getMaterial().getActiveTechnique().getDef().getForcedRenderState();
813846
}
847+
814848
// use geometry's material
815849
material.render(geom, lightList, this);
850+
851+
// resize light batch if needed before rendering
852+
maybeResizeLightBatch(geom.getMaterial().getActiveTechnique().getDef(), lightList.size());
816853
material.selectTechnique(previousTechniqueName, this);
817854

818855
//restoring forcedRenderState
@@ -824,12 +861,19 @@ public void renderGeometry(Geometry geom, LightList lightList) {
824861
} else if (forcedMaterial != null) {
825862
// use forced material
826863
forcedMaterial.render(geom, lightList, this);
864+
865+
// resize light batch if needed before rendering
866+
maybeResizeLightBatch(forcedMaterial.getActiveTechnique().getDef(), lightList.size());
827867
}
828868
} else if (forcedMaterial != null) {
829869
// use forced material
830870
forcedMaterial.render(geom, lightList, this);
871+
// resize light batch if needed before rendering
872+
maybeResizeLightBatch(forcedMaterial.getActiveTechnique().getDef(), lightList.size());
831873
} else {
832874
material.render(geom, lightList, this);
875+
// resize light batch if needed before rendering
876+
maybeResizeLightBatch(geom.getMaterial().getActiveTechnique().getDef(), lightList.size());
833877
}
834878
this.renderer.popDebugGroup();
835879
}
@@ -1056,20 +1100,79 @@ public TechniqueDef.LightMode getPreferredLightMode() {
10561100
/**
10571101
* Returns the number of lights used for each pass when the light mode is single pass.
10581102
*
1103+
* <p>
1104+
* This value is automatically scaled up (in powers of two, up to
1105+
* {@link #getMaxSinglePassLightBatchSize()}) during rendering whenever a geometry has more lights than
1106+
* the current batch size.
1107+
*
10591108
* @return the number of lights.
10601109
*/
10611110
public int getSinglePassLightBatchSize() {
10621111
return singlePassLightBatchSize;
10631112
}
10641113

10651114
/**
1066-
* Sets the number of lights to use for each pass when the light mode is single pass.
1115+
* Sets the number of lights to use for each pass when the light mode is single pass, and simultaneously
1116+
* sets the maximum batch size to the same value.
1117+
*
1118+
* <p>
1119+
* This effectively pins the batch size and disables the automatic scaling, which is useful when you know
1120+
* in advance how many lights your scene uses.
1121+
*
1122+
* <p>
1123+
* To set only the upper limit while still allowing automatic scaling, use
1124+
* {@link #setMaxSinglePassLightBatchSize(int)} instead.
10671125
*
1068-
* @param singlePassLightBatchSize the number of lights.
1126+
* @param singlePassLightBatchSize the number of lights (minimum 1).
10691127
*/
10701128
public void setSinglePassLightBatchSize(int singlePassLightBatchSize) {
1071-
// Ensure the batch size is no less than 1
10721129
this.singlePassLightBatchSize = Math.max(singlePassLightBatchSize, 1);
1130+
this.maxSinglePassLightBatchSize = this.singlePassLightBatchSize;
1131+
}
1132+
1133+
/**
1134+
* Returns the maximum number of lights allowed in a single pass batch.
1135+
*
1136+
* <p>
1137+
* The batch size will never be auto-scaled beyond this value.
1138+
*
1139+
* @return the maximum single pass light batch size.
1140+
*/
1141+
public int getMaxSinglePassLightBatchSize() {
1142+
return maxSinglePassLightBatchSize;
1143+
}
1144+
1145+
/**
1146+
* Sets the maximum number of lights allowed in a single pass batch.
1147+
*
1148+
* <p>
1149+
* The requested value is clamped to a hardware-safe upper bound.
1150+
*
1151+
* <p>
1152+
* If the current {@link #getSinglePassLightBatchSize() batch size} exceeds the new maximum, it is clamped
1153+
* down to the new maximum. Otherwise the current batch size is left unchanged and will continue to
1154+
* auto-scale up to the new limit.
1155+
*
1156+
* @param maxSinglePassLightBatchSize the maximum number of lights (minimum 1).
1157+
*/
1158+
public void setMaxSinglePassLightBatchSize(int maxSinglePassLightBatchSize) {
1159+
this.maxSinglePassLightBatchSize = Math.max(maxSinglePassLightBatchSize, 1);
1160+
// Clamp to a hardware-safe value.
1161+
Integer fragUniformVecs = renderer.getLimits().get(Limits.FragmentUniformVectors);
1162+
if (fragUniformVecs != null && fragUniformVecs > 0) {
1163+
int reservedUniforms = Math.min(Math.max(fragUniformVecs / RESERVED_UNIFORM_FRACTION, 1), RESERVED_UNIFORMS_MAX);
1164+
int maxBatchForHardware = Math.max((fragUniformVecs - reservedUniforms) / VEC4_UNIFORMS_PER_LIGHT, 1);
1165+
if (this.maxSinglePassLightBatchSize > 16 && maxBatchForHardware < 16) {
1166+
logger.log(Level.WARNING,
1167+
"setMaxSinglePassLightBatchSize({0}) was requested but hardware only supports"
1168+
+ " {1} lights per pass (FragmentUniformVectors={2}); clamping to {1}.",
1169+
new Object[] { maxSinglePassLightBatchSize, maxBatchForHardware, fragUniformVecs });
1170+
}
1171+
this.maxSinglePassLightBatchSize = Math.min(this.maxSinglePassLightBatchSize, maxBatchForHardware);
1172+
}
1173+
if (singlePassLightBatchSize > this.maxSinglePassLightBatchSize) {
1174+
singlePassLightBatchSize = this.maxSinglePassLightBatchSize;
1175+
}
10731176
}
10741177

10751178
/**

0 commit comments

Comments
 (0)