Skip to content

[Performance] Batch one: skip the empty Gaussian pass, animation pause flag, lazy error names, unfair lock on component ids - #54

Draft
miogds wants to merge 1 commit into
feature/perf_countersfrom
feature/perf_batch1
Draft

miogds wants to merge 1 commit into
feature/perf_countersfrom
feature/perf_batch1

Conversation

@miogds

@miogds miogds commented Sep 20, 2026 •

Copy link
Copy Markdown
Member

First optimization batch from docs/proposals/PerformanceMeasurementResearch.md, stacked on #53 so the benchmark can measure it. Fork-only; nothing goes upstream until the whole effort is done.

Changes

  • Skip the Gaussian pass when the frame has no splats. With an empty working set the pass still copied the opaque depth, cleared and stored the full-resolution coverage target and ran the tile initialize and post-process stages. On Apple Vision Pro (2048×1984 per eye, two eyes) that was 0.7 to 4.9 ms of GPU time per frame in scenes without a single splat, the single largest fixed cost in the first headset run. gaussianCoverageWritten stays false, so the composite and anti-aliasing passes do not read the stale map. Verified on macOS that the pass and its depth copy appear only in the Gaussian scene.
  • AnimationSystem. The update loop read each entity's pause state through isAnimationComponentPaused(entityId:), which walks the entity's subtree allocating a Set and an array per node with a scene.get per node, every frame, to answer a Bool the loop already holds. It now reads animationComponent.pause; the public API is unchanged.
  • ErrorHandlingSystem. The entity-taking handleError overloads formatted the entity name before the logger's level check. The message is built inside Logger.logError's autoclosure.
  • ComponentPool. The component type-to-id map, read on every scene.get, sits behind an OSAllocatedUnfairLock instead of an NSLock.

Measured on macOS (Mac16,5, 1920×1080 at 120 Hz, release, three runs each side, aggregated by minimum, before the Gaussian change)

Scene update scene graph culling encode mean frame
primitives-1k −12.8 % −14.5 % −14.9 % −6.7 % 0 (display-paced)
primitives-10k −15.1 % −15.8 % −4.5 % −6.1 % −7.0 %
lights-64 −9.6 % −14.1 % −13.4 % −8.6 % 0 (display-paced)
postfx −10.7 % −12.1 % −12.5 % −6.0 % 0 (display-paced)

The light scenes move within the run-to-run noise of a mostly idle machine. Most of that gain is the component-id lock, which every ECS access hits.

What the first Apple Vision Pro run said (baseline in #53, perf/baselines/RealityDevice17_1)

Every scene missed the compositor deadline on almost every frame. The light scenes (animation-16, stadium, gaussian) are GPU-bound at 8.6 to 9.7 ms mean GPU time for 14 to 160 visible instances: the fixed per-frame cost of full-screen passes at two eyes of 2048×1984 (Gaussian pass 3.6 to 4.9 ms, G-buffer + light 3.3 to 4.5 ms even with a dozen draws, pre-composite 1.1, output transform 0.8 to 1.9, post-processing bypass 0.4 to 2.0, environment 0.5 to 1.0, HZB and depth copies about 1). The heavy scenes are CPU-bound: 24 ms of encode for 1264 opaque plus 1407 shadow draws (primitives-1k) and 48 ms for the 10k scene, about 9 µs per draw on the headset CPU, with the eye-dependent graph executed twice. Those numbers are the case for the next items in the note: layered stereo with vertex amplification, foveation, the draw list and the scene-access refactor.

Verified

swift build; animation, camera, component-registry and custom-system core tests; GaussianRenderingTest (all but the PSNR comparison, which needs cv2 locally), GPUPassTimerTests, UpdateXRRenderingSystemTest, WholeRenderGraphValidationTest, EngineStatsMonitorTests; the benchmark runs above.

@miogds
miogds force-pushed the feature/perf_batch1 branch 2 times, most recently from 82b60d2 to 34f813b Compare September 20, 2026 20:22
@miogds miogds changed the title [Performance] Batch one: animation pause flag, lazy error names, unfair lock on component ids [Performance] Batch one: skip the empty Gaussian pass, animation pause flag, lazy error names, unfair lock on component ids Sep 20, 2026
@miogds
miogds force-pushed the feature/perf_batch1 branch from 34f813b to 0e0c10d Compare September 20, 2026 20:42
…ths and component-id lookups

Three small, behaviour-preserving changes from the hot-path survey in
docs/proposals/PerformanceMeasurementResearch.md (C7, C16, C3):

- AnimationSystem: the update loop read each entity's pause state through
  isAnimationComponentPaused(entityId:), which walks the entity's subtree
  allocating a Set and an array per node with a scene.get per node, every
  frame, to answer a Bool the loop already holds. Read
  animationComponent.pause directly; the public API is unchanged.
- ErrorHandlingSystem: the entity-taking handleError overloads formatted
  the entity name before the logger's level check. Build the message inside
  Logger.logError's autoclosure so per-entity call sites in the draw loop
  and culling pay nothing unless the error is logged.
- ComponentPool: the component type-to-id map, read on every scene.get,
  now sits behind an OSAllocatedUnfairLock instead of an NSLock; it only
  changes when a new component type registers.
@miogds
miogds force-pushed the feature/perf_batch1 branch from da8ed5a to ef201da Compare October 16, 2026 14:34
@miogds
miogds force-pushed the feature/perf_counters branch from 4bb0bb5 to 51873ca Compare October 16, 2026 14:34
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.

1 participant