4444import com .jme3 .material .RenderState ;
4545import com .jme3 .material .Technique ;
4646import com .jme3 .material .TechniqueDef ;
47+ import com .jme3 .math .FastMath ;
4748import com .jme3 .math .Matrix4f ;
4849import com .jme3 .post .SceneProcessor ;
4950import com .jme3 .profile .AppProfiler ;
7475import java .util .function .Function ;
7576import java .util .function .Predicate ;
7677import java .util .function .Supplier ;
78+ import java .util .logging .Level ;
7779import 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