Budget the jump-link layer in pixels of line, nearest the view's centre first

With the drawn set following the view, the layer at 8 pc cost the integrated Radeon 503 ms a frame
at 30 pc from the Sun. Measured, the cost follows the length of line on screen (about 10 ms per
million pixels near the Sun), not the number of links: 100 000 links were 12 ms at the opening
view, 25 000 were 61 ms at 30 pc. So the budget is a length: a million pixels, turned into parsecs
at the depth the view is centred on, spent on the links nearest that centre by their nearer end.
Orbiting with links at 8 pc on the iGPU: 12-18 ms p50 at every pose measured, no long tasks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
2026-09-17 18:23:55 +02:00
co-authored by Claude Opus 5
parent 45d5209433
commit bd5e9d4b0d
7 changed files with 161 additions and 20 deletions
@@ -39,6 +39,7 @@ import { SystemObjectCardComponent } from './system-object-card.component';
import { RoutingClient } from './routing-client';
import { colorIndexToRgb, FOCUS_RADIUS_PC, StarFieldRenderer, starRenderBudgetFromUrl, VIEW_MARGIN } from './star-field-renderer';
import { BrightnessIndex, brightestWithin, brightnessIndex } from '../../shared/astro/brightest';
import { LinkBudget } from '../../shared/astro/jump-links';
import { StarNeighbourhood } from '../../shared/astro/star-neighbourhood';
import { MAX_JUMP_RANGE_PC } from '../hud/routes-panel.component';
import { HostStarRings } from './host-star-rings';
@@ -76,6 +77,22 @@ const LABEL_REACH_NDC = 0.3;
* emits per pixel; and how often at most a view on the move gets a graph for its new drawn stars.
*/
const JUMP_LINK_REBUILD_DELAY_MS = 250;
/**
* How much jump-link line the layer draws, in pixels of length on screen: about a million, measured
* where lines are longest.
*
* What a graph costs to draw is its length on screen, not its number of links: every pixel of it is
* blended over whatever is already there. On the Ryzen 7700X's integrated Radeon, standing in for an
* entry-level laptop, at 1920 × 1080 with the range at 8 pc:
* - near the Sun, about 10 ms a frame per million pixels. At 30 pc from it, 25 000 links were 4.8
* million pixels and 60 ms; 5 000 were 0.9 million and 18 ms, about 55 frames a second;
* - at the opening view, where the links are short, 100 000 links were 1.8 million pixels and 12 ms.
*
* So a count could not serve both: the budget is a length, turned into parsecs at the depth the view
* is centred on, and spent on the links nearest that centre. The RTX 4080 draws every graph in the
* same 6 ms, but the budget is the same everywhere, like the stars'.
*/
const JUMP_LINK_PIXEL_BUDGET = 1_000_000;
/**
* How far in or out the plan view may be zoomed from the extent its distance frames. Under a
@@ -1553,7 +1570,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
}
this.drawnJumpRangePc = rangePc;
this.linkedStars = drawn;
void this.routing.links(rangePc, drawn).then(
void this.routing.links(rangePc, drawn, this.jumpLinkBudget()).then(
(segments) => {
if (this.drawnJumpRangePc === rangePc) {
this.jumpLinks?.setSegments(segments);
@@ -1570,6 +1587,20 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
);
}
/**
* How much of the graph to draw: the links nearest the view's centre, up to the length that
* `JUMP_LINK_PIXEL_BUDGET` pixels of line make at that depth. None without a canvas to measure.
*/
private jumpLinkBudget(): LinkBudget | undefined {
const heightPx = this.canvasRef().nativeElement.clientHeight;
if (heightPx === 0) {
return undefined;
}
const centre = this.controls?.target ?? GALAXY_OVERVIEW_TARGET;
const halfHeight = this.engine.visibleHalfHeight(this.engine.getCamera().position.distanceTo(centre));
return { centre: { x: centre.x, y: centre.y, z: centre.z }, lengthPc: (JUMP_LINK_PIXEL_BUDGET * 2 * halfHeight) / heightPx };
}
/** A pinned body wins over a hovered one, so the card does not change under the pointer. */
private refreshObjectCard(): void {
const id = this.pinnedBodyId ?? this.hoveredBodyId;