Put each body's photograph on it one frame at a time, so entering the Sun's system no longer stalls

buildMarker gave every photographed body its map at once. A texture is copied to the GPU in the
first frame that draws it, and the 28 maps arrive within about 40 ms of each other, so that frame
copied some 20 megapixels of JPEG (seven maps at 2048x1024) through copyExternalImageToTexture: a
second long task of 135-162 ms about 1.25 s after entering, measured here four times on the
committed renderer (reviewers measured 160-210 against 85-100 without the 18 new maps). de34fff's
"adds no long task" was measured before those maps landed.

A photographed body now starts in its kind's flat colour, as a derived one does, and its texture
waits in a queue; each update() puts the first one that has loaded on its body. The copies are
spread one a frame, and all 38 bodies have their maps within half a second of the first. In the
running app, five fresh entries into the Sun's system at 1600x1000 left one long task of 52-66 ms
or none at all ([66], [52], [62], [], [56] ms, where the committed renderer gave [62, 149], [56,
135], [74, 162], [78, 162]).

Control: putting every loaded photograph on in one frame fails "puts them on their bodies once
loaded, one a frame".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 19:48:31 +02:00
co-authored by Claude Opus 5.5
parent 6c0626ca38
commit 2d4b8a98d5
2 changed files with 61 additions and 8 deletions
@@ -8,6 +8,7 @@ import { orientationAt } from '../../shared/astro/rotational-elements';
import { BodyRecord, RotationalElements } from '../../shared/models/body.model';
import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
import { SystemOrbitsRenderer } from './system-orbits-renderer';
import { bodyTexturePath, loadCachedTexture } from '../../shared/rendering/texture-catalog';
/** The clock's UT date that names a TDB one: TT - UT, which moves by under a second a year, earlier. */
const utOf = (jdTdb: number): number => jdTdb - ttMinusUtSeconds(jdTdb) / 86400;
@@ -448,6 +449,32 @@ describe('rotation without IAU elements', () => {
});
});
describe('photographs', () => {
it('puts them on their bodies once loaded, one a frame, so the GPU is not handed every map at once', () => {
// Ids no other test here draws, since the loaded textures are shared through the cache.
const ids = ['ganymede', 'callisto'];
const records: BodyRecord[] = ids.map((id, index) => ({
id, systemStarId: 0, name: id, kind: 'planet', radiusKm: 2500, orbitSource: 'test',
orbit: { semiMajorAxisAu: 1 + index, eccentricity: 0, inclinationDeg: 0, longitudeOfAscendingNodeDeg: 0, argumentOfPeriapsisDeg: 0, meanAnomalyAtEpochDeg: 0, epochJd: DEFAULT_EPOCH_JD },
rates: keplerRates(1 + index, GM_SUN_AU3_PER_DAY2)
}));
const renderer = new SystemOrbitsRenderer(records, []);
const maps = (): Array<THREE.Texture | null> => renderer.members.map((member) => ((member.marker as THREE.Mesh).material as THREE.MeshStandardMaterial).map);
renderer.update(DEFAULT_EPOCH_JD);
expect(maps()).toEqual([null, null]); // not loaded yet: jsdom never loads an image
for (const id of ids) {
loadCachedTexture(bodyTexturePath(id)!).image = { width: 2, height: 1 };
}
renderer.update(DEFAULT_EPOCH_JD);
expect(maps().filter(Boolean)).toHaveLength(1);
renderer.update(DEFAULT_EPOCH_JD);
expect(maps()).toEqual(ids.map((id) => loadCachedTexture(bodyTexturePath(id)!)));
renderer.dispose();
});
});
describe('derived surfaces', () => {
const maps = (renderer: SystemOrbitsRenderer): Array<THREE.Texture | null> =>
renderer.members.map((member) => ((member.marker as THREE.Mesh).material as THREE.MeshStandardMaterial).map);