fix(refboard): cap loaded textures at 60 and prioritize by distance
When zoomed out, all items fall within the viewport so the culling system tried to load everything at once. Now: - Max 60 textures loaded at any time (sorted by distance to center) - Max 5 new texture loads per tick (prevents spike) - Items over budget or far away are unloaded immediately
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { Application } from 'pixi.js';
|
import { Application } from 'pixi.js';
|
||||||
import { Viewport } from 'pixi-viewport';
|
import { Viewport } from 'pixi-viewport';
|
||||||
import { SceneManager, getItemWorldBounds } from './SceneManager';
|
import { SceneManager, getItemWorldBounds, type SceneItem } from './SceneManager';
|
||||||
import { TextureManager } from './TextureManager';
|
import { TextureManager } from './TextureManager';
|
||||||
import { ImageSprite } from './sprites/ImageSprite';
|
import { ImageSprite } from './sprites/ImageSprite';
|
||||||
import { SpringManager } from './spring';
|
import { SpringManager } from './spring';
|
||||||
@@ -167,9 +167,10 @@ const PixiCanvas = forwardRef<PixiCanvasHandle, PixiCanvasProps>(
|
|||||||
|
|
||||||
// -- Visibility culling ticker (runs every 200ms, not every frame) --
|
// -- Visibility culling ticker (runs every 200ms, not every frame) --
|
||||||
// Images: load textures when near viewport, unload when far away.
|
// Images: load textures when near viewport, unload when far away.
|
||||||
// Uses preload margin (1 screen) and larger unload margin (2 screens)
|
// Uses preload margin and hysteresis. Caps concurrent loaded textures
|
||||||
// to avoid thrashing during panning.
|
// to prevent GPU OOM when zoomed out (all items visible at once).
|
||||||
|
|
||||||
|
const MAX_LOADED_TEXTURES = 60; // GPU budget — only this many images loaded at once
|
||||||
let lastCullCheck = 0;
|
let lastCullCheck = 0;
|
||||||
app.ticker.add((ticker) => {
|
app.ticker.add((ticker) => {
|
||||||
lastCullCheck += ticker.deltaMS;
|
lastCullCheck += ticker.deltaMS;
|
||||||
@@ -177,34 +178,27 @@ const PixiCanvas = forwardRef<PixiCanvasHandle, PixiCanvasProps>(
|
|||||||
lastCullCheck = 0;
|
lastCullCheck = 0;
|
||||||
|
|
||||||
const bounds = viewport.getVisibleBounds();
|
const bounds = viewport.getVisibleBounds();
|
||||||
// Preload margin: 1x screen size beyond viewport edges
|
|
||||||
const loadMargin = Math.max(bounds.width, bounds.height);
|
const loadMargin = Math.max(bounds.width, bounds.height);
|
||||||
// Unload margin: 2x screen size — hysteresis prevents thrash
|
|
||||||
const unloadMargin = loadMargin * 2;
|
const unloadMargin = loadMargin * 2;
|
||||||
|
const vcx = bounds.x + bounds.width / 2;
|
||||||
|
const vcy = bounds.y + bounds.height / 2;
|
||||||
|
|
||||||
|
// Collect image items with distance from viewport center
|
||||||
|
const imageItems: { item: SceneItem; sprite: ImageSprite; dist: number; near: boolean }[] = [];
|
||||||
|
|
||||||
for (const item of scene.getAllItems()) {
|
for (const item of scene.getAllItems()) {
|
||||||
const { x: ix, y: iy, w: iw, h: ih } = getItemWorldBounds(item);
|
const { x: ix, y: iy, w: iw, h: ih } = getItemWorldBounds(item);
|
||||||
|
|
||||||
if (item.type === 'image' && item.displayObject instanceof ImageSprite) {
|
if (item.type === 'image' && item.displayObject instanceof ImageSprite) {
|
||||||
const nearViewport =
|
const near =
|
||||||
ix + iw > bounds.x - loadMargin &&
|
ix + iw > bounds.x - loadMargin &&
|
||||||
ix < bounds.x + bounds.width + loadMargin &&
|
ix < bounds.x + bounds.width + loadMargin &&
|
||||||
iy + ih > bounds.y - loadMargin &&
|
iy + ih > bounds.y - loadMargin &&
|
||||||
iy < bounds.y + bounds.height + loadMargin;
|
iy < bounds.y + bounds.height + loadMargin;
|
||||||
|
const cx = ix + iw / 2;
|
||||||
if (nearViewport && !item.displayObject.loaded) {
|
const cy = iy + ih / 2;
|
||||||
item.displayObject.loadTexture();
|
const dist = (cx - vcx) ** 2 + (cy - vcy) ** 2;
|
||||||
} else if (!nearViewport) {
|
imageItems.push({ item, sprite: item.displayObject, dist, near });
|
||||||
// Only unload if well outside viewport (hysteresis)
|
|
||||||
const farFromViewport =
|
|
||||||
ix + iw < bounds.x - unloadMargin ||
|
|
||||||
ix > bounds.x + bounds.width + unloadMargin ||
|
|
||||||
iy + ih < bounds.y - unloadMargin ||
|
|
||||||
iy > bounds.y + bounds.height + unloadMargin;
|
|
||||||
if (farFromViewport && item.displayObject.loaded) {
|
|
||||||
item.displayObject.unloadTexture();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === 'video' && 'onVisibilityChange' in item.displayObject) {
|
if (item.type === 'video' && 'onVisibilityChange' in item.displayObject) {
|
||||||
@@ -216,6 +210,32 @@ const PixiCanvas = forwardRef<PixiCanvasHandle, PixiCanvasProps>(
|
|||||||
(item.displayObject as any).onVisibilityChange(inView);
|
(item.displayObject as any).onVisibilityChange(inView);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort by distance — closest to viewport center first
|
||||||
|
imageItems.sort((a, b) => a.dist - b.dist);
|
||||||
|
|
||||||
|
// Determine which should be loaded: nearest items up to budget
|
||||||
|
const nearItems = imageItems.filter(i => i.near);
|
||||||
|
const shouldBeLoaded = new Set(
|
||||||
|
nearItems.slice(0, MAX_LOADED_TEXTURES).map(i => i.item.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load nearest unloaded items (cap at 5 per tick to avoid spike)
|
||||||
|
let loadsThisTick = 0;
|
||||||
|
for (const { item, sprite } of imageItems) {
|
||||||
|
if (shouldBeLoaded.has(item.id) && !sprite.loaded && loadsThisTick < 5) {
|
||||||
|
sprite.loadTexture();
|
||||||
|
loadsThisTick++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unload items that are either far away or over budget
|
||||||
|
for (const { item, sprite, near } of imageItems) {
|
||||||
|
if (!sprite.loaded) continue;
|
||||||
|
if (!near || !shouldBeLoaded.has(item.id)) {
|
||||||
|
sprite.unloadTexture();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- Store refs ------------------------------------------------------
|
// -- Store refs ------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user