@plasius/gpu-performance
v0.1.14
Published
Device-negotiated GPU performance governor and quality ladder coordination for Plasius rendering stacks.
Maintainers
Readme
@plasius/gpu-performance
Device-negotiated GPU performance governance for Plasius rendering stacks. The package tracks frame-budget trends, negotiates per-device targets, and orchestrates quality ladders across rendering modules without taking ownership of renderer internals.
Apache-2.0. ESM + CJS builds. TypeScript types included.
Install
npm install @plasius/gpu-performanceThe browser demo is validated against the current stable
@plasius/gpu-shared 1.x public runtime line and consumes its exported harbor
showcase API through the package import surface.
Browser Demo
npm run demoThen open http://localhost:8000/gpu-performance/demo/.
npm run demo now mounts the adaptive governor against the shared 3D harbor
scene through the public @plasius/gpu-shared package surface so quality
changes are visible, while
npm run demo:example keeps the console example path.
What It Solves
- Negotiates frame targets by device mode and refresh characteristics.
- Watches sustained performance trends instead of reacting to one noisy frame.
- Degrades visual and non-authoritative simulation quality before gameplay-safe physics.
- Coordinates quality ladders for renderer, lighting, particles, cloth, XR, and other GPU-heavy systems through adapter contracts.
- Keeps control local to the runtime while exposing telemetry hooks that callers
can route through
@plasius/analyticsfor shared analytics transport, batching, and persistence. - Enforces fail-fast validation, bounded histories, and structured error records to satisfy the package NFR baseline.
Usage
import {
createDeviceProfile,
createGpuPerformanceGovernor,
createQualityLadderAdapter,
createWorkerJobBudgetAdapter,
rayTracingQualityDimensions,
representationBands,
} from "@plasius/gpu-performance";
const device = createDeviceProfile({
deviceClass: "xr-headset",
mode: "immersive-vr",
refreshRateHz: 72,
supportedFrameRates: [72, 90],
supportsFoveation: true,
});
const renderScale = createQualityLadderAdapter({
id: "render-scale",
domain: "resolution",
levels: [
{ id: "50", config: { scale: 0.5 }, estimatedCostMs: 1.2 },
{ id: "67", config: { scale: 0.67 }, estimatedCostMs: 2.1 },
{ id: "80", config: { scale: 0.8 }, estimatedCostMs: 3.3 },
{ id: "100", config: { scale: 1.0 }, estimatedCostMs: 5.4 },
],
initialLevel: "100",
onLevelChange: ({ adjustment }) => {
console.log("render-scale change", adjustment.toLevelId);
},
});
const cloth = createQualityLadderAdapter({
id: "cloth",
domain: "cloth",
authority: "non-authoritative-simulation",
levels: [
{ id: "off", config: { enabled: false, solverIterations: 0 }, estimatedCostMs: 0 },
{ id: "low", config: { enabled: true, solverIterations: 2 }, estimatedCostMs: 0.8 },
{ id: "high", config: { enabled: true, solverIterations: 5 }, estimatedCostMs: 2.2 },
],
initialLevel: "high",
});
const physics = createQualityLadderAdapter({
id: "physics",
domain: "physics",
authority: "authoritative",
importance: "critical",
levels: [
{ id: "fixed", config: { tickHz: 60 }, estimatedCostMs: 2.8 },
],
});
const reflections = createQualityLadderAdapter({
id: "reflections",
domain: "reflections",
representationBand: "far",
qualityDimensions: {
rayTracing: 1,
updateCadence: 1,
temporalReuse: 1,
},
importanceSignals: {
visible: false,
reflectionSignificance: "medium",
},
levels: [
{ id: "reduced", config: { raysPerPixel: 1 }, estimatedCostMs: 0.8 },
{ id: "full", config: { raysPerPixel: 4 }, estimatedCostMs: 2.1 },
],
});
const governor = createGpuPerformanceGovernor({
device,
modules: [renderScale, cloth, reflections, physics],
});
for (const [index, frameTimeMs] of [12.8, 13.1, 18.4, 18.9, 19.2, 18.8, 18.1, 17.9].entries()) {
const decision = governor.recordFrame({
frameId: `frame-${index}`,
frameTimeMs,
});
if (decision.adjustments.length > 0) {
console.log(decision.reason, decision.adjustments);
}
}Ray-tracing-first budget metadata is part of the public contract:
representationBands:near,mid,far,horizonrayTracingQualityDimensions:geometryanimationdeformationshadingshadowsrayTracinglightingSamplesupdateCadencetemporalReuse
That lets modules preserve representation tiers and degrade cheaper cadence, history, and RT-fidelity controls before more expensive geometry or authoritative work.
Wavefront path tracing now has a dedicated adapter surface when a caller needs concrete controls rather than generic metadata:
import {
createWavefrontPathTracingBudgetAdapter,
} from "@plasius/gpu-performance";
const wavefrontBudget = createWavefrontPathTracingBudgetAdapter({
id: "wavefront-path-tracing",
levels: [
{
id: "reduced",
estimatedCostMs: 1.4,
config: {
maxBounceDepth: 3,
samplesPerPixel: 1,
activeRayQueueCapacity: 1024,
explicitLightSamples: 0,
visibilityProbeMode: "disabled",
bvhUpdateCadence: 4,
denoiseMode: "off",
temporalAccumulation: false,
renderScale: 0.7,
},
},
{
id: "full",
estimatedCostMs: 3.9,
config: {
maxBounceDepth: 6,
samplesPerPixel: 4,
activeRayQueueCapacity: 4096,
explicitLightSamples: 2,
visibilityProbeMode: "mis-balanced",
bvhUpdateCadence: 1,
denoiseMode: "spatiotemporal",
temporalAccumulation: true,
renderScale: 1,
},
},
],
});
console.log(wavefrontBudget.getControlSummary().degradeFirst);
console.log(wavefrontBudget.getCurrentBudget().renderScale);The dedicated wavefront adapter names the P0 controls directly:
maxBounceDepthsamplesPerPixelactiveRayQueueCapacityexplicitLightSamplesvisibilityProbeModebvhUpdateCadencedenoiseModetemporalAccumulationrenderScale
Its control summary keeps the active-ray emissive/environment baseline protected while degrading optional variance-reduction work first.
Worker-Job Governance
For @plasius/gpu-* packages that schedule work through @plasius/gpu-worker,
prefer adapting worker job budgets instead of inventing package-local control
loops.
import {
createDeviceProfile,
createGpuPerformanceGovernor,
createWorkerJobBudgetAdapter,
createWorkerJobBudgetManifestGraph,
createWorkerJobBudgetAdaptersFromManifest,
} from "@plasius/gpu-performance";
const device = createDeviceProfile({
deviceClass: "desktop",
mode: "flat",
refreshRateHz: 144,
});
const postProcessingJobs = createWorkerJobBudgetAdapter({
id: "post-processing",
jobType: "post.process",
queueClass: "post-processing",
domain: "post-processing",
levels: [
{
id: "reduced",
config: {
maxDispatchesPerFrame: 1,
maxJobsPerDispatch: 32,
cadenceDivisor: 2,
workgroupScale: 0.5,
},
estimatedCostMs: 0.9,
},
{
id: "full",
config: {
maxDispatchesPerFrame: 2,
maxJobsPerDispatch: 128,
cadenceDivisor: 1,
workgroupScale: 1,
},
estimatedCostMs: 2.8,
},
],
});
const governor = createGpuPerformanceGovernor({
device,
modules: [postProcessingJobs],
});
const decision = governor.recordFrame({ frameTimeMs: 19.2 });
console.log(decision.adjustments, postProcessingJobs.getBudget());If an adopting package already exposes worker manifests, convert those manifests directly into adapters instead of rebuilding the same ladders by hand.
import { createWorkerJobBudgetAdaptersFromManifest } from "@plasius/gpu-performance";
import { getLightingTechniqueWorkerManifest } from "@plasius/gpu-lighting";
const manifest = getLightingTechniqueWorkerManifest("hybrid");
const graph = createWorkerJobBudgetManifestGraph(manifest);
const adapters = createWorkerJobBudgetAdaptersFromManifest(manifest, {
initialLevels: {
"lighting.direct": "medium",
},
selectJob(job) {
return job.performance.importance !== "critical";
},
});
const governor = createGpuPerformanceGovernor({
device,
modules: adapters,
});
console.log(graph.roots);
console.log(graph.priorityLanes);Manifest-driven adapters now preserve scheduler metadata from the source package:
schedulerMode:flatordagpriority: non-negative ready-queue prioritydependencies: upstream job labels that must complete before a job becomes runnabledependents: downstream job labels unlocked by completionroot: whether a job starts runnable
If callers need the manifest normalized into an explicit multi-root DAG before
adapting budgets, use createWorkerJobBudgetManifestGraph(...). The helper
derives:
jobIdsrootstopologicalOrderpriorityLanes- per-job dependency and dependent counts
That means consumers can reuse DAG-aware manifests from packages such as
@plasius/gpu-lighting, @plasius/gpu-particles, and @plasius/gpu-physics
without rebuilding dependency metadata by hand.
When those adapters are registered with the governor, DAG metadata is no longer debug-only. The governor now:
- emits
decision.workerGraphandgetState().workerGraphsummaries for local observability, - protects DAG roots, high-priority jobs, and high fan-out jobs from being degraded before lower-value leaf work,
- keeps that graph reasoning local to the governor without taking ownership of queue execution internals.
API
createDeviceProfile(input)negotiateFrameTarget(options)createQualityLadderAdapter(options)representationBandsrayTracingQualityDimensionsmotionClassescreateWorkerJobBudgetAdapter(options)createWorkerJobBudgetManifestGraph(manifest)createWorkerJobBudgetAdaptersFromManifest(manifest, options?)createGpuPerformanceGovernor(options)createPerformanceGovernor(options)alias
Operational Guarantees
- Invalid configuration fails fast instead of being silently coerced.
- Duplicate
frameIdvalues are ignored for idempotent frame ingestion. - Aborted frame samples are ignored before processing.
- Module and telemetry failures are isolated into structured error records so one broken integration does not cascade into the rest of the runtime.
Analytics Integration
@plasius/gpu-performance does not ship its own analytics client. If governor
events need to be exported beyond local debugging, route them through
@plasius/analytics.
import { createFrontendAnalyticsClient } from "@plasius/analytics";
import { createGpuPerformanceGovernor } from "@plasius/gpu-performance";
const analytics = createFrontendAnalyticsClient({
source: "gpu-performance",
endpoint: "https://analytics.example.com/collect",
});
const governor = createGpuPerformanceGovernor({
device,
modules,
telemetry: {
onDecision(decision) {
if (decision.adjustments.length === 0) {
return;
}
analytics.track({
component: "GpuPerformanceGovernor",
action: "quality_adjustment",
label: decision.pressureLevel,
context: {
cycle: decision.cycle,
targetFrameTimeMs: decision.metrics.targetFrameTimeMs,
averageFrameTimeMs: decision.metrics.averageFrameTimeMs,
dagRootCount: decision.workerGraph?.rootCount ?? 0,
dagMaxPriority: decision.workerGraph?.maxPriority ?? 0,
modules: decision.adjustments.map((adjustment) => adjustment.moduleId),
},
});
},
},
});Design Principles
- WebGPU-first. Non-WebGPU platforms remain unsupported.
- Device-specific frame targets rather than a single global FPS constant.
- Trend-aware adaptation with hysteresis and cooldowns.
- Visual quality is flexible; gameplay-authoritative simulation stays fixed by default.
- Renderer integration happens through adapters, not hidden coupling.
- Worker-job budgets are the preferred actuation surface for current and future
@plasius/gpu-*compute packages. - Worker manifests may carry DAG scheduling metadata so the governor can reason about ordered worker stages without owning queue internals.
- Analytics export goes through
@plasius/analytics; this package only emits local lifecycle hooks.
Demo
Run the console demo locally:
npm run demoSee demo/README.md for details.
Development Checks
npm run lint
npm run typecheck
npm run test:coverage
npm run build
npm run pack:check
npm run zero-threePermanent Zero-Three invariant
@plasius/gpu-performance is renderer-independent GPU budget policy. Three.js,
Three.js subpaths, TSL, @types/three, @react-three/*, and packages whose
dependency graph reaches Three.js are permanently prohibited from source,
manifests, locks, installed dependency graphs, declarations, bundles, npm
tarballs, SBOMs, tests, tooling, and active usage documentation. There is no
compatibility mode, waiver, rollback, or fallback.
Run npm run zero-three:source before dependency installation and npm run
zero-three after building and installing to generate complete package evidence.
The canonical architectural decision is the site ADR
ADR 0168.
Release Automation
GitHub Actions now carries the package delivery path:
- CI runs on pushes and pull requests to enforce lint, typecheck, audit, build, coverage, and package verification.
- CD publishes to npm only through the manual GitHub workflow.
- A scheduled workflow opens monthly npm audit-fix pull requests.
Files
src/device.ts: device profile normalization and frame-target negotiation.src/ladder.ts: generic quality ladder adapters for GPU-facing modules.src/governor.ts: trend-aware control loop and module orchestration.src/worker.ts: worker-job budget adapters and consumer-manifest helpers.src/validation.ts: shared runtime input validation helpers.src/worker.ts: worker-job budget adapters for@plasius/gpu-workerintegrations.tests/*.test.ts: unit coverage for negotiation, adaptation, and recovery.docs/adrs/*: architecture decisions for package scope and control policy.docs/tdrs/*: implementation records for the control loop and adapter model.docs/design/*: design detail for rollout and integrations.
Release integrity
CI keeps the administrative contributor registry outside Git and npm package
artifacts using exact, case-normalised path checks. Repository-owned pull
requests and main execute on GitHub-hosted runners with Node.js 24.18.0 LTS
and explicit cache finalization disabled.
Release preparation lands metadata through the protected branch, waits for
successful exact-commit CI, and then publishes the sealed package through npm
OIDC from the production environment with a pinned npm 11.6.2 client. The release retains and attests the
SBOM and complete Zero-Three evidence; there is no legacy npm token fallback.
