Skip to content

AF Manager class for dynamic animation frame request/cancel#1407

Merged
gkjohnson merged 39 commits intoNASA-AMMOS:masterfrom
phoenixbf:master
Apr 3, 2026
Merged

AF Manager class for dynamic animation frame request/cancel#1407
gkjohnson merged 39 commits intoNASA-AMMOS:masterfrom
phoenixbf:master

Conversation

@phoenixbf
Copy link
Copy Markdown
Contributor

See #1342

Copy link
Copy Markdown
Contributor

@gkjohnson gkjohnson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks! A couple comments but I think this is the right direction

@@ -0,0 +1,43 @@
class AFManager {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets call this FrameScheduler for now.

Comment on lines +11 to +21
setXRSession( xrsession ) {

this.xrsession = xrsession;

}

removeXRSession() {

this.xrsession = null;

}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When changing the system being used for callbacks we will have to cancel all previous callbacks and reregister them with the new function. Eg if the "LRUCache" schedules an unload event it will be waiting until that event fires before scheduling another one. So if the last event is scheduled using the "xrSession" callback and then the session closes, the scheduled callback will never be fired.

So we'll need to keep track of all scheduled callbacks and rAF handles to swap over the scheduled events.

@gkjohnson
Copy link
Copy Markdown
Contributor

@phoenixbf - just wanted to check in to see if you had any more questions on the changes needed here.

@phoenixbf
Copy link
Copy Markdown
Contributor Author

@phoenixbf - just wanted to check in to see if you had any more questions on the changes needed here.

Hi, performing some local testing in order to have a clean rAF tracking system and handling swaps

@phoenixbf
Copy link
Copy Markdown
Contributor Author

I've added management of pending AF.
It basically keeps track of requested AFs via handle as suggested, providing methods to cancel or flush all pending AFs. When entering XR session, it internally flushes and completes all pending, switching to this.xrsession.requestAnimationFrame for new requests.

So far I tried the above logic in both local and online setups without any evident issues, also with mutiple tsets. If this is the right direction, it should be probably instantiated in TilesRendererBase and then referenced by LRUCache, PriorityQueue, etc. during their initializations

@gkjohnson
Copy link
Copy Markdown
Contributor

Thanks the direction of this looks great. I think we can cut down on some code redundancy in the implementation but we can deal with that once everything is working.

If this is the right direction, it should be probably instantiated in TilesRendererBase and then referenced by LRUCache, PriorityQueue, etc. during their initializations

This sounds good to me. Looks like the "LRUCache", "PriorityQueue", the "throttle" function, and "QueryManager" are all places where rAF is used (though the throttle function can maybe be changed to "queueMicrotask")

Copy link
Copy Markdown
Contributor

@gkjohnson gkjohnson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Made a few comments -


setXRSession( xrsession ) {

if ( ! this.framescheduler ) return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should never be "null" so we should be able to remove this check.

this.isLoading = false;

// FrameScheduler referenced by LRUCache, PriorityQueue
this.framescheduler = new FrameScheduler();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets use camel casing to align with the class naming:

this.framescheduler -> this.frameScheduler

Comment thread src/core/renderer/utilities/LRUCache.js Outdated

this._unloadPriorityCallback = null;

this.framescheduler = null;
Copy link
Copy Markdown
Contributor

@gkjohnson gkjohnson Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets initialize these to new FrameScheduler() so the classes are in a usable state when constructed. Then we can remove the null checks, as well. This should help the tests pass, too.

@gkjohnson gkjohnson added this to the v0.4.20 milestone Jan 19, 2026
@gkjohnson
Copy link
Copy Markdown
Contributor

I've just made some of the remaining changes to fix tests, add new tests for FrameScheduler, and address some code styling and other issues. I think this should be ready to merge but if you could test it in a real XR environment that would be great.

@phoenixbf
Copy link
Copy Markdown
Contributor Author

phoenixbf commented Jan 23, 2026

I've just made some of the remaining changes to fix tests, add new tests for FrameScheduler, and address some code styling and other issues. I think this should be ready to merge but if you could test it in a real XR environment that would be great.

Yep, sure. Today I'll carry out additional online tests with a few HMDs

@gkjohnson gkjohnson modified the milestones: v0.4.20, v0.4.21 Feb 1, 2026
@h0ldemslav
Copy link
Copy Markdown

The ImageOverlayPlugin creates a "PriorityQueue" internally so that would need to share a FrameScheduler, as well. The more of these changes that need to be made, though, the more brittle this solution seems. I'm wondering if we should just provide a global FrameScheduler that everything shares by default so these issues aren't encountered.

One other potential case that comes to mind: Is there ever a case where the XR Session is "paused" (as in not rendering or updating) when a separate window is open? It seems like there could be a case where the XR Session has initialized but the session "rAF" has stopped firing and the window "rAF" starts firing again, instead. Is this something you're able to reproduce? I'm wondering how to be resilient to this.

Not sure if I understand the last paragraph. I mean rAF is fired always, even if you have other windows in Meta quest open. What's interesting is that, both window.requestAnimationFrame and xrsession.requestAnimationFrame are fired by the frame scheduler, whether your session has visibilityState "visible" or "visible-blurred".

I made a video to illustrate the problem. Have a look at it: https://drive.google.com/file/d/12ZfhMkAjkJPxaYLlh30wFM-1iiOrX0Fw/view?usp=sharing (cannot attach to GitHub, size exceeds GitHub limits). I'm not good at it but I can make more videos, if you need, feel free to give me concrete instructions.

In the video, you can see how the imagery is not applied at all and how there are very few calls to both rAF, when I was in VR, or better say visibilityState was "visible". Once I press the meta button on the controller, the session visibilityState changes to "visible-blurred" and you can see how many rAF are fired (both window and xrsession). Once I toggle the state (from "visible" to "visible-again"), there are very few rAF calls.

@gkjohnson
Copy link
Copy Markdown
Contributor

gkjohnson commented Mar 17, 2026

@phoenixbf

A FrameScheduler is indeed needed at the moment to handle and delegate to the correct rAF routine. Where do you imagine this happening and how? A singleton?

Yeah lets just make this a singleton for now. I'm open to suggestions but we can manage it like so:

import { FrameScheduler } from '3d-tiles-renderer';

FrameScheduler.requestAnimationFrame( ... );
FrameScehduler.setXRSession( ... )

@h0ldemslav

Not sure if I understand the last paragraph. I mean rAF is fired always, even if you have other windows in Meta quest open. What's interesting is that, both window.requestAnimationFrame and xrsession.requestAnimationFrame are fired by the frame scheduler, whether your session has visibilityState "visible" or "visible-blurred".

The window's "requestAnimationFrame" will have to fire when the "window" is visible so content in just the window, like DOM, can be updated. And the xr session rAF will have to be fired when the XR session is presenting so content can be updated for the XR view. If the XR rAF didn't fire while the background was rendering then it would just freeze and the camera view would not be able to update, either. So what we're seeing are the following situations when the presenting === true (the xr session is running):

xr rAF fires window rAF fires
immersive view true false
window is up with immersive background true true

What I'm wondering is if there is every a situation we can get into where the "presenting" is true (eg sessionstart has fired & sessionend has not) and the window rAF is firing while the xr session rAF is not (this would be a line with "false | true" on the above table). If we think there is or there may be in the future then we should make sure we account for this in our FrameSchduler implementation. Otherwise this will happen again and it's quite a hard issue to understand the track down.

Relatedly, I'd appreciate it if you could make any issue at the meta emulator and link it here so we can more easily understand and test this behavior:

I would also recommend making an issue for Meta's web emulator to request that the rAF callback behavior be made consistent with the real hardware (or better add a toggle so different device / browser behaviors can be emulated).

--

I made a video to illustrate the problem. Have a look at it: https://drive.google.com/file/d/12ZfhMkAjkJPxaYLlh30wFM-1iiOrX0Fw/view?usp=sharing (cannot attach to GitHub, size exceeds GitHub limits)

I can't read any of the text in this video with the compression, unfortunately, so I can't fully understand what its showing.

@h0ldemslav
Copy link
Copy Markdown

The window's "requestAnimationFrame" will have to fire when the "window" is visible so content in just the window, like DOM, can be updated. And the xr session rAF will have to be fired when the XR session is presenting so content can be updated for the XR view. If the XR rAF didn't fire while the background was rendering then it would just freeze and the camera view would not be able to update, either. So what we're seeing are the following situations when the presenting === true (the xr session is running):

I see, now that makes much more sense. Thanks a lot!! Your table basically summarized what I did in the video. Though some window.rAF are called even in the immersive view (perhaps this is some other part of the library). I don't know why Google drive still hasn't provided a better quality video.

Anyway, I'm not sure if one can reproduce a situation where the renderer.xr is presenting, the window.rAF is fired, while xrsession.rAF is not fired. (I can't really think of any good example) If you look at the WebXR specification, 4.3 (links to the highlighted part), they discourage to use window.rAF in the context of xrsession because "the timing of callbacks of window.rAF may not coincide with xrsession.rAF". Though there is some sort of the pattern they suggest using for "transition between these two types of animation loops ".

So, I would rule out the use case where you solely use window.rAF while renderer.xr presenting. I think referring to the WebXR specification is a good justification. However, I'm thinking that maybe the frame scheduler could also use a third option, setTimeout(cb, 16) as a wilcard option that would suffice anyway, though someone has already mentioned in the issues that tiles start flickering if settimeout is used. What do you think?

@gkjohnson
Copy link
Copy Markdown
Contributor

However, I'm thinking that maybe the frame scheduler could also use a third option, setTimeout(cb, 16) as a wilcard option that would suffice anyway

I'm not sure what you mean by this? The classes using frame scheduler need to run every frame and setTimeout is not guaranteed to do so. It may run multiple times per frame or not at all depending on the platform and the framerate.

@gkjohnson gkjohnson modified the milestones: v0.4.23, v0.4.24 Mar 18, 2026
@phoenixbf
Copy link
Copy Markdown
Contributor Author

phoenixbf commented Apr 1, 2026

Hi there,
I've committed a revised FrameScheduler that how behaves as singleton. If the current proposal sounds good, it should be now possible to use this anywhere:

import { FrameScheduler } from '../utilities/FrameScheduler.js';

let fs = new FrameScheduler(); // this now behave as singleton

// do stuff with fs
fs.setXRSession(...);
fs.requestAnimationFrame(...);

@gkjohnson
Copy link
Copy Markdown
Contributor

Thanks @phoenixbf - I'll take a deeper look this weekend and make a few more changes I've had in the back of my mind.

It looks like one of the tests is failing, though. Would you be able to take a look?

@gkjohnson gkjohnson modified the milestones: v0.4.24, v0.4.25 Apr 3, 2026
@gkjohnson
Copy link
Copy Markdown
Contributor

I've made a few changes - primarily changing the class to use all static functions and using it as a singleton everywhere. The class has also been renamed to "Scheduler" and I've removed the "setXRSession" function from "TilesRenderer" so you set it like so:

import { Scheduler } from '3d-tiles-renderer';
Scheduler.setXRSession( session );

I'll merge this but if you could test it and follow up if there are any issues that would be great! Thanks again for your help in tracking down the issue and coming up with a solution.

@gkjohnson gkjohnson merged commit 9975c04 into NASA-AMMOS:master Apr 3, 2026
1 check passed
@gkjohnson gkjohnson modified the milestones: v0.4.25, v0.4.24 Apr 3, 2026
@phoenixbf
Copy link
Copy Markdown
Contributor Author

I've made a few changes - primarily changing the class to use all static functions and using it as a singleton everywhere. The class has also been renamed to "Scheduler" and I've removed the "setXRSession" function from "TilesRenderer" so you set it like so:

import { Scheduler } from '3d-tiles-renderer';
Scheduler.setXRSession( session );

I'll merge this but if you could test it and follow up if there are any issues that would be great! Thanks again for your help in tracking down the issue and coming up with a solution.

Thanks, I'll test these ASAP on my XR equipment

@phoenixbf
Copy link
Copy Markdown
Contributor Author

just to confirm I tested several online tilesets on different equipment (Meta Quest 2, Meta Quest Pro and Hololens 2) without issues

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants