GUI.Control._moveToProjectedPosition is endlessly called even on a still scene

Hi.

I’m not sure if it’s bug or as designed. But it seem just a bit not ok.

I tried to put some custom logic there and was forced to implement cachedPosition check.
(again, like with the old bug about rectangle invalidation)

Check out dev console here:

https://playground.babylonjs.com/#DEZ2PE#2

Apparently, the method is called from advancedDynamicTexture._checkUpdate, along with all the recalculations of world matrices and stuff. And it’s called before it’s own dirty check.

It seems relatively easy to dirty-check all positions of camera and linked meshes.

Is it intentionally skipped?

Ok,

here is my actuall playground:

https://playground.babylonjs.com/#X9DCCM#11

If I remove my override of _moveToProjectedPosition with the cache check, fps drops to 10.

cc @georgie / @amoebachant

Hi @qwiglydee

I took a look at this today, and I don’t think that _moveToProjectedPosition() should bail out immediately if the projectedPosition param is unchanged, because it also relies on this._currentMeasuret to calculate the newLeft and newTop, so it can’t know if the left and top will change until it has done the measure work.

Thanks for providing the playground! I first tried commenting out your override for _moveToProjectedPosition() and did see the framerate drop after rapidly moving the scene around for a while, and it stayed low even after I stopped moving the camera.

Stepping through the code, I noticed that this bail out was not happening even long after motion had stopped:

image

I then set a breakpoint in AdvancedDynamicTexture._checkUpdate() before the loop through linkedControls, so I could be sure I’m just looking at the first one that had changed left and top values for the frame.

Then I enabled a breakpoint in _moveToProjectedPosition after the newLeft === oldLeft check and saw the value changed a lot, in my current case 419 to 358. Then I disabled the _moveToProjectedPosition breakpoint and continued to the next frame (when my AdvancedDynamicTexture._checkUpdate() breakpoint hit again). Then reenabling my _moveToProjectedPosition breakpoint and running, I see this time it’s going from 358 back to 419.

If I comment out the call to snapToEdges() in the playground, I don’t see this happen and the framerate stays at 60. I think something in the math in snapToEdges() is causing the controls to jitter, which is causing them to be marked as dirty each frame and rerendered. I wasn’t able to track down the exact cause of that jitter though.

I did notice that snapToEdges() is allocating many Vector3 objects every time it’s called (which is per callout per frame). That will cause a lot of garbage collection pressure, and the GC will start kicking in and causing frames to drop. I’d recommend creating the Vector3 objects you need ahead of time just once and reusing them each time snapToEdges is called.

I hope that helps you find the problem - let me know how it goes!

Thanks!

Thanks for elaboration!

Ok, I see now that the _moveToProjected is itself a source of dirtyness of position, and it relies on whatever layout logic is implemented via _processMeasures

Anyway, the scheme of gui alignment is stll unclear obscure to me, and it’s not obvious where, when and how I can change position, so it won’t break everything else.
My snapToEdge depend on sizes of host (parentMeasure) and current control (i assume it’s currentMeasure) and also padding. The changes of the measures seem somewhat untrackable.
I thought that _computeAlignment is just the right place, but i’m not sure now.

The algorithm itself should be stable, because it’s based on pure math magic, although it uses floats for pixels and that might break some equality checks.

I just missed the fact that linkOffset is already included in left/top by built-in _moveToProjected, so my snapToEdge produces infinite loop of dirtyness and jitter.

I should investigate the underhood of the gui more scrutinous.

the _moveToProjected is itself a source of dirtyness of position, and it relies on whatever layout logic is implemented via _processMeasures

But the logic is applied before control is repositioned, so all the processMeasures, layout, computeAlignment and whatever else, is applied to an old position, but then again to the new position.

That’s confusing a lot.

Another thing is that the moveToProjected endlessly calls processMeasures along with all the alignment stuff.

Here’s the code from moveToProjected:

        const parentMeasure = this.parent?._currentMeasure;
        if (parentMeasure) {
            this._processMeasures(parentMeasure, this._host.getContext());
        }

For a linked control, parent is the root container, and it always exists and has currentMeasure, initialized to Measure.Empty(). So, the if just doesn’t work for linked controls.

Here is updated playground:

https://playground.babylonjs.com/#DEZ2PE#6

It shows that _processMeasure / _additionalProcessing called twice with empty parent measure from the moveToProjected, and then later from _layout with proper measure.
But actualy, it’s endlessly called every frame.

In my another playground it’s even worse, because the control is a rectangle with a child textblock with autosizing, so there are 5 initial calls with different parents and sizes.

And that looks more like a bug.

Also, this code of Control::_processMeasures triggers dirty observers every time the moveProjected checks measures:

Apparently, it is supposed to notify that _currentMeasure is changed.
But it triggers even if it’s the same.

Probably, this is a cause of another bug of infinite relayout when a line is connected to a linked control.

Just wanted to let you know I’m still digging into your latest info here…

Hi @qwiglydee

I tried running this playground you sent:https://playground.babylonjs.com/#DEZ2PE#6

But I’m not seeing any double calls. When I clear the console, click in the scene, then tap the spacebar, I see:

Which looks like a moveToProjected() call, then processMeasure(), then layout(), and finally draw().

Am I missing something? I appreciate your detailed investigation - could you narrow down what bad behavior you’re seeing that seems to be a bug?

Thanks!

Yes, after they initialized it makes just 1 call per change.

But when it’s restarted, there are 2 initial calls with uninitialized parent.
Both came from moveToProjected. (the debug logging is post-call)

Upd.
I mean 1 and 2 logged calls when isDirty is set or layout returns true.
Of course, all the processMeasure stuff is called endlessly every frame without changing dirtyness.

Got it, thanks for clarifying! I’m glad that the extra call only happens on the first frame. This is good feedback for us to consider for further optimizations, I really appreciate you looking into it and providing all this great feedback!

Have you seen my other post with alternative implementation of linking?
Maybe, there’s something you can take from there.

Yes I did - thanks for those ideas! I’m headed out for the holidays, but we’ll definitely consider your suggestions when we are working on improvements to the GUI code. Thanks again!