Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
74aed37
AFManager class for dynamic animation frame request/cancel
phoenixbf Dec 13, 2025
59e1230
Merge branch 'NASA-AMMOS:master' into master
phoenixbf Dec 13, 2025
eafb6b7
spacing
phoenixbf Dec 13, 2025
e4b313f
spacing
phoenixbf Dec 13, 2025
ecffd60
rename class
phoenixbf Dec 15, 2025
a7fb47b
Merge branch 'NASA-AMMOS:master' into master
phoenixbf Jan 10, 2026
0cf8ae5
Add pending AF management
phoenixbf Jan 10, 2026
3fa5aae
minor comment
phoenixbf Jan 10, 2026
6079607
Merge branch 'NASA-AMMOS:master' into master
phoenixbf Jan 18, 2026
f6eb5eb
spacing
phoenixbf Jan 18, 2026
d78ab44
FrameScheduler referenced by LRUCache,PriorityQueue, throttle and Que…
phoenixbf Jan 19, 2026
f8ab031
camel casing; intialization in LRU, PriorityQueue and QueryManager
phoenixbf Jan 20, 2026
edeab4b
Merge branch 'NASA-AMMOS:master' into master
phoenixbf Jan 22, 2026
743dc61
- replace "removeXRSession" with "setXRSession( null )"
gkjohnson Jan 22, 2026
c3c0051
Add tests for FrameScheduler
gkjohnson Jan 22, 2026
72e0260
small cleanup
gkjohnson Jan 22, 2026
ca0a22f
comment
gkjohnson Jan 22, 2026
c65fbc7
variable rename
gkjohnson Jan 22, 2026
8182160
Merge branch 'NASA-AMMOS:master' into master
phoenixbf Feb 3, 2026
b7abfd1
Merge branch 'NASA-AMMOS:master' into master
phoenixbf Feb 7, 2026
106a8c1
Merge branch 'NASA-AMMOS:master' into master
phoenixbf Feb 8, 2026
3e50a7c
Merge branch 'NASA-AMMOS:master' into master
phoenixbf Feb 14, 2026
aebada3
Merge branch 'NASA-AMMOS:master' into master
phoenixbf Feb 24, 2026
bc0e4e7
Merge branch 'NASA-AMMOS:master' into master
phoenixbf Feb 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions example/three/vr.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ function handleCamera() {

xrSession = renderer.xr.getSession();

tiles.setXRSession( xrSession );

}

} else {
Expand All @@ -246,6 +248,8 @@ function handleCamera() {

xrSession = null;

tiles.setXRSession( null );

}

}
Expand Down
16 changes: 15 additions & 1 deletion src/core/renderer/tiles/TilesRendererBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { runTraversal } from './traverseFunctions.js';
import { UNLOADED, QUEUED, LOADING, PARSING, LOADED, FAILED } from '../constants.js';
import { throttle } from '../utilities/throttle.js';
import { traverseSet } from '../utilities/TraversalUtils.js';
import { FrameScheduler } from '../utilities/FrameScheduler.js';

const PLUGIN_REGISTERED = Symbol( 'PLUGIN_REGISTERED' );
const regionErrorTarget = {
Expand Down Expand Up @@ -194,19 +195,25 @@ export class TilesRendererBase {
this.cachedSinceLoadComplete = new Set();
this.isLoading = false;

const frameScheduler = new FrameScheduler();

const lruCache = new LRUCache();
lruCache.unloadPriorityCallback = lruPriorityCallback;
lruCache.frameScheduler = frameScheduler;

const downloadQueue = new PriorityQueue();
downloadQueue.maxJobs = 25;
downloadQueue.priorityCallback = defaultPriorityCallback;
downloadQueue.frameScheduler = frameScheduler;

const parseQueue = new PriorityQueue();
parseQueue.maxJobs = 5;
parseQueue.priorityCallback = defaultPriorityCallback;
parseQueue.frameScheduler = frameScheduler;

const processNodeQueue = new PriorityQueue();
processNodeQueue.maxJobs = 25;
processNodeQueue.frameScheduler = frameScheduler;
processNodeQueue.priorityCallback = ( a, b ) => {

const aParent = a.parent;
Expand Down Expand Up @@ -241,6 +248,7 @@ export class TilesRendererBase {
this.downloadQueue = downloadQueue;
this.parseQueue = parseQueue;
this.processNodeQueue = processNodeQueue;
this.frameScheduler = frameScheduler;
this.stats = {
inCacheSinceLoad: 0,
inCache: 0,
Expand All @@ -265,7 +273,7 @@ export class TilesRendererBase {

this.dispatchEvent( { type: 'needs-update' } );

} );
}, this.frameScheduler );

// options
this.errorTarget = 16.0;
Expand All @@ -278,6 +286,12 @@ export class TilesRendererBase {

}

setXRSession( xrsession ) {

this.frameScheduler.setXRSession( xrsession );

}

// Plugins
registerPlugin( plugin ) {

Expand Down
89 changes: 89 additions & 0 deletions src/core/renderer/utilities/FrameScheduler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
class FrameScheduler {

constructor() {

// XR session
this.session = null;

// Pending AFs
this.pending = new Map();

}

// Set active XR session
// To be called when entering XR
setXRSession( session ) {

// call "flush" before assigning xr session to ensure callbacks are
// cancelled on the previous handle
if ( session !== this.session ) {

this.flushPending();
this.session = session;

}

}

// Request animation frame (defer to XR session if active)
requestAnimationFrame( cb ) {

const { session, pending } = this;
let handle;

const func = () => {

pending.delete( handle );
cb();

};

if ( ! session ) {

handle = requestAnimationFrame( func );

} else {

handle = session.requestAnimationFrame( func );

}

pending.set( handle, cb );

return handle;

}

// Cancel animation frame via handle (defer to XR session if active)
cancelAnimationFrame( handle ) {

const { pending, session } = this;
pending.delete( handle );

if ( ! session ) {

cancelAnimationFrame( handle );

} else {

session.cancelAnimationFrame( handle );

}

}

// Flush and complete pending AFs (defer to XR session if active)
flushPending() {

this.pending.forEach( ( cb, handle ) => {

cb();
this.cancelAnimationFrame( handle );

} );

}

}

export { FrameScheduler };
8 changes: 6 additions & 2 deletions src/core/renderer/utilities/LRUCache.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FrameScheduler } from './FrameScheduler.js';

const GIGABYTE_BYTES = 2 ** 30;

class LRUCache {
Expand Down Expand Up @@ -56,6 +58,8 @@ class LRUCache {

this._unloadPriorityCallback = null;

this.frameScheduler = new FrameScheduler();

const itemSet = this.itemSet;
this.defaultPriorityCallback = item => itemSet.get( item );

Expand Down Expand Up @@ -346,15 +350,15 @@ class LRUCache {

if ( needsRerun ) {

this.unloadingHandle = requestAnimationFrame( () => this.scheduleUnload() );
this.unloadingHandle = this.frameScheduler.requestAnimationFrame( () => this.scheduleUnload() );

}

}

scheduleUnload() {

cancelAnimationFrame( this.unloadingHandle );
this.frameScheduler.cancelAnimationFrame( this.unloadingHandle );

if ( ! this.scheduled ) {

Expand Down
6 changes: 5 additions & 1 deletion src/core/renderer/utilities/PriorityQueue.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FrameScheduler } from './FrameScheduler.js';

export class PriorityQueueItemRemovedError extends Error {

constructor() {
Expand Down Expand Up @@ -31,10 +33,12 @@ export class PriorityQueue {

this.priorityCallback = null;

this.frameScheduler = new FrameScheduler();

// Customizable scheduling callback. Default using requestAnimationFrame()
this.schedulingCallback = func => {

requestAnimationFrame( func );
this.frameScheduler.requestAnimationFrame( func );

};

Expand Down
6 changes: 3 additions & 3 deletions src/core/renderer/utilities/throttle.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// function that rate limits the amount of time a function can be called to once
// per frame, initially queuing a new call for the next frame.
export function throttle( callback ) {
export function throttle( callback, frameScheduler ) {

let handle = null;
return () => {

if ( handle === null ) {
if ( handle === null && frameScheduler ) {

handle = requestAnimationFrame( () => {
handle = frameScheduler.requestAnimationFrame( () => {

handle = null;
callback();
Expand Down
6 changes: 5 additions & 1 deletion src/r3f/utilities/QueryManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from 'three';
import { SceneObserver } from './SceneObserver.js';
import { Ellipsoid } from '3d-tiles-renderer/three';
import { FrameScheduler } from '../../core/renderer/utilities/FrameScheduler.js';

const _raycaster = /* @__PURE__ */ new Raycaster();
const _line0 = /* @__PURE__ */ new Line3();
Expand Down Expand Up @@ -43,6 +44,9 @@ export class QueryManager extends EventDispatcher {
// cameras for sorting
this.cameras = new Set();

// frame scheduler
this.frameScheduler = new FrameScheduler();

// register to mark items as dirty
const queueAll = ( () => {

Expand Down Expand Up @@ -201,7 +205,7 @@ export class QueryManager extends EventDispatcher {
if ( this.autoRun && ! this.scheduled ) {

this.scheduled = true;
requestAnimationFrame( () => {
this.frameScheduler.requestAnimationFrame( () => {

this.scheduled = false;
this._runJobs();
Expand Down
97 changes: 97 additions & 0 deletions test/core/FrameScheduler.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { FrameScheduler } from '../../src/core/renderer/utilities/FrameScheduler.js';

const nextFrame = async () => new Promise( resolve => requestAnimationFrame( resolve ) );
describe( 'FrameScheduler', () => {

let scheduler;
beforeEach( () => {

scheduler = new FrameScheduler();

} );

it( 'should fire callbacks.', async () => {

let called = false;
scheduler.requestAnimationFrame( () => called = true );
expect( called ).toBe( false );

await nextFrame();
expect( called ).toBe( true );

} );

it( 'should flush callbacks.', () => {

let called = false;
scheduler.requestAnimationFrame( () => called = true );
scheduler.flushPending();

expect( called ).toBe( true );
expect( scheduler.pending.size ).toBe( 0 );

} );

it( 'should allow for cancelling callbacks.', async () => {

let called = false;
const handle = scheduler.requestAnimationFrame( () => called = true );
expect( called ).toBe( false );

scheduler.cancelAnimationFrame( handle );
expect( called ).toBe( false );

await nextFrame();
expect( called ).toBe( false );

} );

it( 'should flush callbacks when setting xr session.', () => {

let called = false;
scheduler.requestAnimationFrame( () => called = true );
scheduler.setXRSession( {} );
expect( called ).toBe( true );

} );

it( 'should use the xr session rAF if set.', async () => {

let called = false;
let cancelledHandle = null;
scheduler.setXRSession( {
requestAnimationFrame: () => called = true,
cancelAnimationFrame: handle => cancelledHandle = handle,
} );

const handle = scheduler.requestAnimationFrame( () => {} );
expect( called ).toBe( true );

scheduler.cancelAnimationFrame( handle );
expect( cancelledHandle ).toBe( handle );

} );

it( 'should not flush callbacks when setting the same session.', () => {

let called = false;
let xrSession = {
requestAnimationFrame,
cancelAnimationFrame,
};

scheduler.requestAnimationFrame( () => called = true );
scheduler.setXRSession( null );
expect( called ).toBe( false );

scheduler.setXRSession( xrSession );
expect( called ).toBe( true );

called = false;
scheduler.requestAnimationFrame( () => called = true );
scheduler.setXRSession( xrSession );
expect( called ).toBe( false );

} );

} );
Loading