11package funkin .modding .modchart .backend .graphics .renderers ;
22
3+ import haxe .ds .ObjectMap ;
4+
35using flixel .util .FlxColorTransformUtil ;
46
57typedef HoldSegmentOutput = {
@@ -41,6 +43,35 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
4143 var _uvtCacheVals : Array <openfl. Vector <Float >> = [];
4244 var _uvtCacheSubs : Int = - 1 ;
4345
46+ // Per-hold pools to avoid per-frame allocations of geometry/color buffers.
47+ var _holdVerticesPool : ObjectMap <FlxSprite , openfl. Vector <Float >> = new ObjectMap <FlxSprite , openfl. Vector <Float >>();
48+ var _holdColorsPool : ObjectMap <FlxSprite , NativeVector <ColorTransform >> = new ObjectMap <FlxSprite , NativeVector <ColorTransform >>();
49+ var _holdPoolSubs : ObjectMap <FlxSprite , Int > = new ObjectMap <FlxSprite , Int >();
50+
51+ inline private function _getPooledVertices (item : FlxSprite , subs : Int ): openfl. Vector < Float > {
52+ final oldSubs = _holdPoolSubs .get (item );
53+ var verts = _holdVerticesPool .get (item );
54+ if (verts == null || oldSubs == null || oldSubs != subs || verts .length != subs * 8 ) {
55+ verts = new openfl. Vector <Float >(subs * 8 , true );
56+ _holdVerticesPool .set (item , verts );
57+ _holdPoolSubs .set (item , subs );
58+ }
59+ return verts ;
60+ }
61+
62+ inline private function _getPooledColors (item : FlxSprite , subs : Int ): NativeVector <ColorTransform > {
63+ final oldSubs = _holdPoolSubs .get (item );
64+ var cols = _holdColorsPool .get (item );
65+ if (cols == null || oldSubs == null || oldSubs != subs || cols .length != subs ) {
66+ cols = new NativeVector <ColorTransform >(subs );
67+ for (i in 0 ... subs )
68+ cols [i ] = new ColorTransform ();
69+ _holdColorsPool .set (item , cols );
70+ _holdPoolSubs .set (item , subs );
71+ }
72+ return cols ;
73+ }
74+
4475 inline private function _getCachedUVT (item : FlxSprite , subs : Int ): openfl. Vector < Float > {
4576 if (subs != _uvtCacheSubs ) {
4677 // Subdivision count changed (settings) — invalidate entire cache
@@ -115,7 +146,7 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
115146
116147 final size = hold .frame .frame .width * hold .scale .x * .5 ;
117148
118- var origin : ModifierOutput = parent .modifiers .getPath (basePos . clone ( ), params );
149+ var origin : ModifierOutput = parent .modifiers .getPath (copyVec3 ( basePos , _pathInputA ), params );
119150 var curPoint = new Vector3 (origin .pos .x , origin .pos .y , 0 );
120151 final depth = (origin .pos .z - 1 ) * 1000 ;
121152 final worldX = origin .rawX ;
@@ -128,7 +159,7 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
128159 if (Config .OPTIMIZE_HOLDS ) {
129160 unit = __holdUnitUp ; // reuse static up-vector, no allocation
130161 } else {
131- var next = parent .modifiers .getPath (basePos . clone ( ), params , 1 , false , true );
162+ var next = parent .modifiers .getPath (copyVec3 ( basePos , _pathInputB ), params , 1 , false , true );
132163 next .pos .z = 0 ;
133164
134165 // normalized points difference (from 0-1)
@@ -224,6 +255,24 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
224255 final _holdArrowBuf : ArrowData = {hitTime : 0 , distance : 0 , sourceTime : 0 , lane : 0 , player : 0 , hitten : false , isTapArrow : false , straightHolds : false };
225256 /** Pre-allocated ArrowData buffer for parentData (rotate path) to avoid per-hold heap allocation. */
226257 final _parentDataBuf : ArrowData = {hitTime : 0 , distance : 0 , sourceTime : 0 , lane : 0 , player : 0 , hitten : false , isTapArrow : false , straightHolds : false };
258+ /** Reused path input vectors to avoid allocating basePos.clone() for every getPath call. */
259+ final _pathInputA : Vector3 = new Vector3 ();
260+ final _pathInputB : Vector3 = new Vector3 ();
261+
262+ // Cached hold metadata used by getArrowParams() in the subdivision loop.
263+ var __cachedHoldPlayer : Int = 0 ;
264+ var __cachedHoldLane : Int = 0 ;
265+ var __cachedHoldHitTime : Float = 0 ;
266+ var __cachedHoldParentTime : Float = 0 ;
267+ var __cachedHoldHitten : Bool = false ;
268+ var __cachedSongPos : Float = 0 ;
269+
270+ inline private function copyVec3 (from : Vector3 , into : Vector3 ): Vector3 {
271+ into .x = from .x ;
272+ into .y = from .y ;
273+ into .z = from .z ;
274+ return into ;
275+ }
227276
228277 override public function prepare (item : FlxSprite ): Null <DrawCommand > {
229278 if (item == null || item .graphic == null || item .frame == null ) {
@@ -245,14 +294,25 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
245294
246295 final player = Adapter .instance .getPlayerFromArrow (item );
247296 final lane = Adapter .instance .getLaneFromArrow (item );
297+ final hitten = Adapter .instance .arrowHit (item );
298+ final holdHitTime = Adapter .instance .getTimeFromArrow (item );
299+ final holdParentTime = Adapter .instance .getHoldParentTime (item );
300+ final songPosNow = Adapter .instance .getSongPosition ();
301+
302+ __cachedHoldPlayer = player ;
303+ __cachedHoldLane = lane ;
304+ __cachedHoldHitTime = holdHitTime ;
305+ __cachedHoldParentTime = holdParentTime ;
306+ __cachedHoldHitten = hitten ;
307+ __cachedSongPos = songPosNow ;
248308
249309 basePos = ModchartUtil .getHalfPos ();
250310 basePos .x + = Adapter .instance .getDefaultReceptorX (lane , player );
251311 basePos .y + = Adapter .instance .getDefaultReceptorY (lane , player );
252312
253- // build directly as openfl.Vector to avoid conversion at render time
254- var vertices = new openfl. Vector < Float >( 8 * HOLD_SUBDIVISIONS , true );
255- var transfTotal = new NativeVector < ColorTransform >( HOLD_SUBDIVISIONS );
313+ // Reuse per-hold buffers to avoid per-frame heap churn.
314+ var vertices = _getPooledVertices ( item , HOLD_SUBDIVISIONS );
315+ var transfTotal = _getPooledColors ( item , HOLD_SUBDIVISIONS );
256316 var tID = 0 ;
257317
258318 var lastData : ArrowData = null ;
@@ -272,19 +332,19 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
272332 __rotateY = canUseLast ? __lastRY : (__lastRY = parent .getPercent (' holdRotateY' , player ));
273333 __rotateZ = canUseLast ? __lastRZ : (__lastRZ = parent .getPercent (' holdRotateZ' , player ));
274334
275- var parentTime = Adapter . instance . getHoldParentTime ( item ) ;
335+ var parentTime = holdParentTime ;
276336 _parentDataBuf .hitTime = parentTime ;
277337 // this fixed the clipping gaps
278- _parentDataBuf .distance = Math .max (0 , parentTime - Adapter . instance . getSongPosition () );
338+ _parentDataBuf .distance = Math .max (0 , parentTime - songPosNow );
279339 _parentDataBuf .sourceTime = parentTime ;
280340 _parentDataBuf .lane = lane ;
281341 _parentDataBuf .player = player ;
282- _parentDataBuf .hitten = Adapter . instance . arrowHit ( item ) ;
342+ _parentDataBuf .hitten = hitten ;
283343 _parentDataBuf .isTapArrow = true ;
284344 _parentDataBuf .straightHolds = __straightHolds > 0 ;
285345 final parentData = _parentDataBuf ;
286346 if (__rotateX != 0 || __rotateY != 0 || __rotateZ != 0 ) {
287- __parentOutput = parent .modifiers .getPath (basePos . clone ( ), parentData );
347+ __parentOutput = parent .modifiers .getPath (copyVec3 ( basePos , _pathInputA ), parentData );
288348 }
289349
290350 var vertPointer = 0 ;
@@ -345,10 +405,19 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
345405 final negGlow = 1 - out1 .visuals .glow ;
346406 final absGlow = out1 .visuals .glow * 255 ;
347407
348- var ctr : ColorTransform ;
349-
350- transfTotal [tID ++ ] = ctr = new ColorTransform (negGlow , negGlow , negGlow , out1 .visuals .alpha * item .alpha ,
351- Math .round (out1 .visuals .glowR * absGlow ), Math .round (out1 .visuals .glowG * absGlow ), Math .round (out1 .visuals .glowB * absGlow ));
408+ var ctr = transfTotal [tID ++ ];
409+ if (ctr == null ) {
410+ ctr = new ColorTransform ();
411+ transfTotal [tID - 1 ] = ctr ;
412+ }
413+ ctr .redMultiplier = negGlow ;
414+ ctr .greenMultiplier = negGlow ;
415+ ctr .blueMultiplier = negGlow ;
416+ ctr .alphaMultiplier = out1 .visuals .alpha * item .alpha ;
417+ ctr .redOffset = Math .round (out1 .visuals .glowR * absGlow );
418+ ctr .greenOffset = Math .round (out1 .visuals .glowG * absGlow );
419+ ctr .blueOffset = Math .round (out1 .visuals .glowB * absGlow );
420+ ctr .alphaOffset = 0 ;
352421
353422 if (ctr .hasRGBMultipliers () || ctr .alphaMultiplier != 1 )
354423 hasC = true ;
@@ -383,24 +452,29 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
383452 }
384453
385454 inline private function getArrowParams (arrow : FlxSprite , posOff : Float = 0 ): ArrowData {
386- final player = Adapter .instance .getPlayerFromArrow (arrow );
387- final lane = Adapter .instance .getLaneFromArrow (arrow );
388-
389455 final timeC2 = flixel. FlxG .height * 0.25 * __centered2 ;
390- final hitTime = Adapter . instance . getTimeFromArrow ( arrow ) ;
456+ final hitTime = __cachedHoldHitTime ;
391457
392- var pos = (hitTime - Adapter . instance . getSongPosition () ) + posOff ;
458+ var pos = (hitTime - __cachedSongPos ) + posOff ;
393459 pos + = timeC2 ;
394460
395461 // Reuse _holdArrowBuf to avoid a heap allocation per segment.
396462 _holdArrowBuf .hitTime = hitTime + posOff + timeC2 ;
397463 _holdArrowBuf .distance = pos ;
398- _holdArrowBuf .sourceTime = Adapter . instance . getHoldParentTime ( arrow ) ;
399- _holdArrowBuf .lane = lane ;
400- _holdArrowBuf .player = player ;
401- _holdArrowBuf .hitten = Adapter . instance . arrowHit ( arrow ) ;
464+ _holdArrowBuf .sourceTime = __cachedHoldParentTime ;
465+ _holdArrowBuf .lane = __cachedHoldLane ;
466+ _holdArrowBuf .player = __cachedHoldPlayer ;
467+ _holdArrowBuf .hitten = __cachedHoldHitten ;
402468 _holdArrowBuf .isTapArrow = true ;
403469 _holdArrowBuf .straightHolds = __straightHolds > 0 ;
404470 return _holdArrowBuf ;
405471 }
472+
473+ override function dispose () {
474+ _uvtCacheKeys = [];
475+ _uvtCacheVals = [];
476+ _holdVerticesPool = new ObjectMap <FlxSprite , openfl. Vector <Float >>();
477+ _holdColorsPool = new ObjectMap <FlxSprite , NativeVector <ColorTransform >>();
478+ _holdPoolSubs = new ObjectMap <FlxSprite , Int >();
479+ }
406480}
0 commit comments