@plasius/gpu-shared 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4619,6 +4619,40 @@ function normalizeDependencyUnlockSample(sample) {
4619
4619
  frameId: sample.frameId === void 0 ? void 0 : assertIdentifier4("dependencyUnlock.frameId", sample.frameId)
4620
4620
  };
4621
4621
  }
4622
+ function normalizePipelinePhaseSample(sample) {
4623
+ if (sample.signal !== void 0 && !isAbortSignalLike2(sample.signal)) {
4624
+ throw new Error("pipelinePhase.signal must be an AbortSignal when provided.");
4625
+ }
4626
+ const snapshotAgeFrames = readNonNegativeNumber2(
4627
+ "pipelinePhase.snapshotAgeFrames",
4628
+ sample.snapshotAgeFrames
4629
+ );
4630
+ if (snapshotAgeFrames !== void 0 && !Number.isInteger(snapshotAgeFrames)) {
4631
+ throw new Error(
4632
+ "pipelinePhase.snapshotAgeFrames must be an integer greater than or equal to zero."
4633
+ );
4634
+ }
4635
+ return {
4636
+ owner: assertIdentifier4("pipelinePhase.owner", sample.owner),
4637
+ pipeline: assertEnumValue4(
4638
+ "pipelinePhase.pipeline",
4639
+ sample.pipeline,
4640
+ gpuPipelinePhases
4641
+ ),
4642
+ stage: assertIdentifier4("pipelinePhase.stage", sample.stage),
4643
+ frameId: sample.frameId === void 0 ? void 0 : assertIdentifier4("pipelinePhase.frameId", sample.frameId),
4644
+ durationMs: readNonNegativeNumber2(
4645
+ "pipelinePhase.durationMs",
4646
+ sample.durationMs
4647
+ ),
4648
+ snapshotFrameId: sample.snapshotFrameId === void 0 ? void 0 : assertIdentifier4("pipelinePhase.snapshotFrameId", sample.snapshotFrameId),
4649
+ snapshotAgeFrames,
4650
+ snapshotAgeMs: readNonNegativeNumber2(
4651
+ "pipelinePhase.snapshotAgeMs",
4652
+ sample.snapshotAgeMs
4653
+ )
4654
+ };
4655
+ }
4622
4656
  function createGpuDebugSession(options = {}) {
4623
4657
  const settings = {
4624
4658
  enabled: options.enabled ?? DEFAULT_OPTIONS.enabled,
@@ -4638,6 +4672,10 @@ function createGpuDebugSession(options = {}) {
4638
4672
  options.maxRetainedDependencyUnlockSamples,
4639
4673
  DEFAULT_OPTIONS.maxRetainedDependencyUnlockSamples
4640
4674
  ),
4675
+ maxRetainedPipelinePhaseSamples: clampCount(
4676
+ options.maxRetainedPipelinePhaseSamples,
4677
+ DEFAULT_OPTIONS.maxRetainedPipelinePhaseSamples
4678
+ ),
4641
4679
  maxRetainedFrameSamples: clampCount(
4642
4680
  options.maxRetainedFrameSamples,
4643
4681
  DEFAULT_OPTIONS.maxRetainedFrameSamples
@@ -4655,6 +4693,7 @@ function createGpuDebugSession(options = {}) {
4655
4693
  const readyLaneSamples = [];
4656
4694
  const dispatchSamples = [];
4657
4695
  const dependencyUnlockSamples = [];
4696
+ const pipelinePhaseSamples = [];
4658
4697
  const frameSamples = [];
4659
4698
  let peakTrackedBytes = 0;
4660
4699
  const totalTrackedBytes = () => [...allocations.values()].reduce((total, allocation) => total + allocation.sizeBytes, 0);
@@ -4806,6 +4845,66 @@ function createGpuDebugSession(options = {}) {
4806
4845
  )
4807
4846
  };
4808
4847
  };
4848
+ const buildPipelineSnapshot = () => {
4849
+ const durations = pipelinePhaseSamples.map((sample) => sample.durationMs).filter((value) => value !== void 0);
4850
+ const snapshotAgeMsValues = pipelinePhaseSamples.map((sample) => sample.snapshotAgeMs).filter((value) => value !== void 0);
4851
+ const snapshotAgeFrameValues = pipelinePhaseSamples.map((sample) => sample.snapshotAgeFrames).filter((value) => value !== void 0);
4852
+ const byPipeline = /* @__PURE__ */ new Map();
4853
+ for (const sample of pipelinePhaseSamples) {
4854
+ const bucket = byPipeline.get(sample.pipeline) ?? {
4855
+ pipeline: sample.pipeline,
4856
+ sampleCount: 0,
4857
+ totalDurationMs: 0,
4858
+ durationValues: [],
4859
+ snapshotAgeMsValues: [],
4860
+ snapshotAgeFramesValues: []
4861
+ };
4862
+ bucket.sampleCount += 1;
4863
+ bucket.totalDurationMs += sample.durationMs ?? 0;
4864
+ if (sample.durationMs !== void 0) {
4865
+ bucket.durationValues.push(sample.durationMs);
4866
+ }
4867
+ if (sample.snapshotAgeMs !== void 0) {
4868
+ bucket.snapshotAgeMsValues.push(sample.snapshotAgeMs);
4869
+ }
4870
+ if (sample.snapshotAgeFrames !== void 0) {
4871
+ bucket.snapshotAgeFramesValues.push(sample.snapshotAgeFrames);
4872
+ }
4873
+ byPipeline.set(sample.pipeline, bucket);
4874
+ }
4875
+ const hottestStages = pipelinePhaseSamples.map((sample) => ({
4876
+ owner: sample.owner,
4877
+ pipeline: sample.pipeline,
4878
+ stage: sample.stage,
4879
+ frameId: sample.frameId,
4880
+ durationMs: sample.durationMs,
4881
+ snapshotFrameId: sample.snapshotFrameId,
4882
+ snapshotAgeFrames: sample.snapshotAgeFrames,
4883
+ snapshotAgeMs: sample.snapshotAgeMs
4884
+ })).sort((left, right) => {
4885
+ const leftScore = left.durationMs ?? left.snapshotAgeMs ?? left.snapshotAgeFrames ?? 0;
4886
+ const rightScore = right.durationMs ?? right.snapshotAgeMs ?? right.snapshotAgeFrames ?? 0;
4887
+ return rightScore - leftScore;
4888
+ }).slice(0, 5);
4889
+ return {
4890
+ sampleCount: pipelinePhaseSamples.length,
4891
+ totalDurationMs: durations.reduce((total, value) => total + value, 0),
4892
+ averageDurationMs: average(durations),
4893
+ averageSnapshotAgeMs: average(snapshotAgeMsValues),
4894
+ maxSnapshotAgeMs: snapshotAgeMsValues.length > 0 ? Math.max(...snapshotAgeMsValues) : void 0,
4895
+ maxSnapshotAgeFrames: snapshotAgeFrameValues.length > 0 ? Math.max(...snapshotAgeFrameValues) : void 0,
4896
+ byPipeline: [...byPipeline.values()].map((bucket) => ({
4897
+ pipeline: bucket.pipeline,
4898
+ sampleCount: bucket.sampleCount,
4899
+ totalDurationMs: bucket.totalDurationMs,
4900
+ averageDurationMs: average(bucket.durationValues),
4901
+ averageSnapshotAgeMs: average(bucket.snapshotAgeMsValues),
4902
+ maxSnapshotAgeMs: bucket.snapshotAgeMsValues.length > 0 ? Math.max(...bucket.snapshotAgeMsValues) : void 0,
4903
+ maxSnapshotAgeFrames: bucket.snapshotAgeFramesValues.length > 0 ? Math.max(...bucket.snapshotAgeFramesValues) : void 0
4904
+ })).sort((left, right) => right.totalDurationMs - left.totalDurationMs),
4905
+ hottestStages
4906
+ };
4907
+ };
4809
4908
  return {
4810
4909
  isEnabled() {
4811
4910
  return enabled;
@@ -4876,6 +4975,17 @@ function createGpuDebugSession(options = {}) {
4876
4975
  );
4877
4976
  return true;
4878
4977
  },
4978
+ recordPipelinePhase(sample) {
4979
+ if (!enabled || sample.signal?.aborted === true) {
4980
+ return false;
4981
+ }
4982
+ pipelinePhaseSamples.push(normalizePipelinePhaseSample(sample));
4983
+ trimHistory(
4984
+ pipelinePhaseSamples,
4985
+ settings.maxRetainedPipelinePhaseSamples
4986
+ );
4987
+ return true;
4988
+ },
4879
4989
  recordFrame(sample) {
4880
4990
  if (!enabled || sample.signal?.aborted === true) {
4881
4991
  return false;
@@ -4917,6 +5027,7 @@ function createGpuDebugSession(options = {}) {
4917
5027
  averageGpuBusyMs: average(gpuBusyTimes)
4918
5028
  },
4919
5029
  dag: buildDagSnapshot(),
5030
+ pipeline: buildPipelineSnapshot(),
4920
5031
  limitations: LIMITATIONS
4921
5032
  };
4922
5033
  return snapshot;
@@ -4928,12 +5039,13 @@ function createGpuDebugSession(options = {}) {
4928
5039
  readyLaneSamples.splice(0, readyLaneSamples.length);
4929
5040
  dispatchSamples.splice(0, dispatchSamples.length);
4930
5041
  dependencyUnlockSamples.splice(0, dependencyUnlockSamples.length);
5042
+ pipelinePhaseSamples.splice(0, pipelinePhaseSamples.length);
4931
5043
  frameSamples.splice(0, frameSamples.length);
4932
5044
  peakTrackedBytes = 0;
4933
5045
  }
4934
5046
  };
4935
5047
  }
4936
- var IDENTIFIER_PATTERN2, gpuDebugQueueClasses, gpuResourceCategories, DEFAULT_OPTIONS, LIMITATIONS;
5048
+ var IDENTIFIER_PATTERN2, gpuDebugQueueClasses, gpuResourceCategories, gpuPipelinePhases, DEFAULT_OPTIONS, LIMITATIONS;
4937
5049
  var init_dist5 = __esm({
4938
5050
  "node_modules/@plasius/gpu-debug/dist/index.js"() {
4939
5051
  IDENTIFIER_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/u;
@@ -4953,12 +5065,19 @@ var init_dist5 = __esm({
4953
5065
  "pipeline",
4954
5066
  "custom"
4955
5067
  ]);
5068
+ gpuPipelinePhases = Object.freeze([
5069
+ "simulation",
5070
+ "secondary-simulation",
5071
+ "scene-preparation",
5072
+ "render"
5073
+ ]);
4956
5074
  DEFAULT_OPTIONS = Object.freeze({
4957
5075
  enabled: false,
4958
5076
  maxRetainedDispatches: 240,
4959
5077
  maxRetainedQueueSamples: 240,
4960
5078
  maxRetainedReadyLaneSamples: 240,
4961
5079
  maxRetainedDependencyUnlockSamples: 240,
5080
+ maxRetainedPipelinePhaseSamples: 240,
4962
5081
  maxRetainedFrameSamples: 240,
4963
5082
  maxTrackedAllocations: 512
4964
5083
  });
@@ -4966,7 +5085,8 @@ var init_dist5 = __esm({
4966
5085
  "Tracked memory reflects only allocations reported to this debug session.",
4967
5086
  "Portable WebGPU does not expose authoritative live GPU core-count or total-memory counters.",
4968
5087
  "Hardware hints are optional caller-supplied metadata and may be platform-specific.",
4969
- "Ready-lane and dependency-unlock diagnostics are caller-reported integration samples, not automatic WebGPU counters."
5088
+ "Ready-lane and dependency-unlock diagnostics are caller-reported integration samples, not automatic WebGPU counters.",
5089
+ "Pipeline phase and snapshot-lag diagnostics are caller-reported integration samples, not automatic WebGPU counters."
4970
5090
  ]);
4971
5091
  }
4972
5092
  });
@@ -6642,7 +6762,145 @@ function buildDemoDom(root, options) {
6642
6762
  sceneNotes: root.querySelector("#sceneNotes")
6643
6763
  };
6644
6764
  }
6645
- function buildClothSurface(model, state, meshDetail) {
6765
+ function buildSceneSnapshot(state, shipModel) {
6766
+ return Object.freeze({
6767
+ focus: state.focus,
6768
+ frame: state.frame,
6769
+ time: state.time,
6770
+ stress: state.stress,
6771
+ collisions: state.contactCount,
6772
+ collisionCount: state.collisionCount,
6773
+ collisionFlash: state.collisionFlash,
6774
+ sprays: Object.freeze(
6775
+ state.sprays.map(
6776
+ (spray) => Object.freeze({
6777
+ life: spray.life,
6778
+ position: Object.freeze({ ...spray.position }),
6779
+ velocity: Object.freeze({ ...spray.velocity })
6780
+ })
6781
+ )
6782
+ ),
6783
+ ships: Object.freeze(
6784
+ state.ships.map(
6785
+ (ship) => Object.freeze({
6786
+ id: ship.id,
6787
+ position: Object.freeze({ ...ship.position }),
6788
+ velocity: Object.freeze({ ...ship.velocity }),
6789
+ rotationY: ship.rotationY,
6790
+ angularVelocity: ship.angularVelocity,
6791
+ tint: Object.freeze({ ...ship.tint })
6792
+ })
6793
+ )
6794
+ ),
6795
+ waveImpulses: Object.freeze(
6796
+ state.waveImpulses.map(
6797
+ (impulse) => Object.freeze({
6798
+ x: impulse.x,
6799
+ z: impulse.z,
6800
+ strength: impulse.strength,
6801
+ radius: impulse.radius,
6802
+ life: impulse.life
6803
+ })
6804
+ )
6805
+ ),
6806
+ physics: Object.freeze({
6807
+ profile: state.physics.profile,
6808
+ plan: state.physics.plan,
6809
+ manifest: state.physics.manifest,
6810
+ snapshot: state.physics.snapshot,
6811
+ shipPhysics: shipModel?.physics ?? null
6812
+ })
6813
+ });
6814
+ }
6815
+ function resolveSceneDescription(state, options, shipModel) {
6816
+ const scene = buildSceneSnapshot(state, shipModel);
6817
+ if (typeof options.describeState !== "function") {
6818
+ return { scene, description: null };
6819
+ }
6820
+ const description = options.describeState(state.packageState, scene) ?? null;
6821
+ return { scene, description };
6822
+ }
6823
+ function updatePackageState(state, options, shipModel, dt) {
6824
+ if (typeof options.updateState !== "function") {
6825
+ return;
6826
+ }
6827
+ const scene = buildSceneSnapshot(state, shipModel);
6828
+ const nextState = options.updateState(state.packageState, scene, dt);
6829
+ if (typeof nextState !== "undefined") {
6830
+ state.packageState = nextState;
6831
+ }
6832
+ }
6833
+ function normalizeColorOverride(color, fallback) {
6834
+ if (!color || typeof color !== "object") {
6835
+ return fallback;
6836
+ }
6837
+ return {
6838
+ r: typeof color.r === "number" ? color.r : fallback.r,
6839
+ g: typeof color.g === "number" ? color.g : fallback.g,
6840
+ b: typeof color.b === "number" ? color.b : fallback.b
6841
+ };
6842
+ }
6843
+ function readVisualNumber(value, fallback) {
6844
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
6845
+ }
6846
+ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
6847
+ const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
6848
+ const defaults = {
6849
+ skyTop: premiumShadows ? "#f0f7fb" : "#e8f1f7",
6850
+ skyMid: premiumShadows ? "#c7d9e5" : "#b9ceda",
6851
+ skyBottom: premiumShadows ? "#84a7bd" : "#7b9bb0",
6852
+ seaTop: premiumShadows ? "#235064" : "#264c5f",
6853
+ seaMid: premiumShadows ? "#153e53" : "#173d4f",
6854
+ seaBottom: "#0b2433",
6855
+ sunCore: "rgba(255, 244, 210, 0.9)",
6856
+ reflectionStrength: lightingSnapshot.currentLevel.config.reflectionStrength,
6857
+ shadowAccent: lightingSnapshot.currentLevel.config.shadowStrength,
6858
+ waveAmplitude: 1,
6859
+ waveDirection: { x: 0.86, z: 0.34 },
6860
+ wavePhaseSpeed: 1,
6861
+ wakeStrength: 0.24,
6862
+ wakeLength: 15,
6863
+ collisionRippleStrength: 0.34,
6864
+ waterNear: { r: 0.12, g: 0.36, b: 0.46 },
6865
+ waterFar: { r: 0.28, g: 0.56, b: 0.68 },
6866
+ harborWall: { r: 0.48, g: 0.4, b: 0.32 },
6867
+ harborDeck: { r: 0.5, g: 0.34, b: 0.22 },
6868
+ harborTower: { r: 0.34, g: 0.32, b: 0.36 },
6869
+ flagColor: { r: 0.76, g: 0.24, b: 0.18 },
6870
+ flagMotion: 1
6871
+ };
6872
+ return {
6873
+ skyTop: typeof customVisuals.skyTop === "string" ? customVisuals.skyTop : defaults.skyTop,
6874
+ skyMid: typeof customVisuals.skyMid === "string" ? customVisuals.skyMid : defaults.skyMid,
6875
+ skyBottom: typeof customVisuals.skyBottom === "string" ? customVisuals.skyBottom : defaults.skyBottom,
6876
+ seaTop: typeof customVisuals.seaTop === "string" ? customVisuals.seaTop : defaults.seaTop,
6877
+ seaMid: typeof customVisuals.seaMid === "string" ? customVisuals.seaMid : defaults.seaMid,
6878
+ seaBottom: typeof customVisuals.seaBottom === "string" ? customVisuals.seaBottom : defaults.seaBottom,
6879
+ sunCore: typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.sunCore,
6880
+ reflectionStrength: readVisualNumber(
6881
+ customVisuals.reflectionStrength,
6882
+ defaults.reflectionStrength
6883
+ ),
6884
+ shadowAccent: readVisualNumber(customVisuals.shadowAccent, defaults.shadowAccent),
6885
+ waveAmplitude: readVisualNumber(customVisuals.waveAmplitude, defaults.waveAmplitude),
6886
+ waveDirection: customVisuals.waveDirection && typeof customVisuals.waveDirection.x === "number" && typeof customVisuals.waveDirection.z === "number" ? { x: customVisuals.waveDirection.x, z: customVisuals.waveDirection.z } : defaults.waveDirection,
6887
+ wavePhaseSpeed: readVisualNumber(customVisuals.wavePhaseSpeed, defaults.wavePhaseSpeed),
6888
+ wakeStrength: readVisualNumber(customVisuals.wakeStrength, defaults.wakeStrength),
6889
+ wakeLength: readVisualNumber(customVisuals.wakeLength, defaults.wakeLength),
6890
+ collisionRippleStrength: readVisualNumber(
6891
+ customVisuals.collisionRippleStrength,
6892
+ defaults.collisionRippleStrength
6893
+ ),
6894
+ waterNear: normalizeColorOverride(customVisuals.waterNear, defaults.waterNear),
6895
+ waterFar: normalizeColorOverride(customVisuals.waterFar, defaults.waterFar),
6896
+ harborWall: normalizeColorOverride(customVisuals.harborWall, defaults.harborWall),
6897
+ harborDeck: normalizeColorOverride(customVisuals.harborDeck, defaults.harborDeck),
6898
+ harborTower: normalizeColorOverride(customVisuals.harborTower, defaults.harborTower),
6899
+ flagColor: normalizeColorOverride(customVisuals.flagColor, defaults.flagColor),
6900
+ flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion)
6901
+ };
6902
+ }
6903
+ function buildClothSurface(model, state, meshDetail, visuals) {
6646
6904
  const clothPlan = createClothRepresentationPlan({
6647
6905
  garmentId: "shore-flag",
6648
6906
  kind: state.focus === "cloth" ? "flag" : clothGarmentKinds[0],
@@ -6668,12 +6926,12 @@ function buildClothSurface(model, state, meshDetail) {
6668
6926
  for (let column = 0; column < cols; column += 1) {
6669
6927
  const u = column / (cols - 1);
6670
6928
  const v = row / (rows - 1);
6671
- const gust = Math.sin(time * 1.9 + v * 3.2 + u * 2.1) * continuity.broadMotionFloor;
6672
- const wrinkle = Math.sin(time * 4.4 + u * 9.2 + v * 5.6) * continuity.wrinkleFloor * 0.22;
6929
+ const gust = Math.sin(time * 1.9 + v * 3.2 + u * 2.1) * continuity.broadMotionFloor * visuals.flagMotion;
6930
+ const wrinkle = Math.sin(time * 4.4 + u * 9.2 + v * 5.6) * continuity.wrinkleFloor * 0.22 * Math.max(0.55, visuals.flagMotion);
6673
6931
  const x = origin.x + u * 1.8 + gust * 0.55 * (u * 0.9);
6674
6932
  const y = origin.y - height * v + wrinkle * 0.2;
6675
6933
  const z = origin.z + width * u + gust * 0.72 * (u * 0.85);
6676
- const flap = Math.cos(time * 2.7 + u * 7.4 + v * 3.8) * continuity.broadMotionFloor * 0.28;
6934
+ const flap = Math.cos(time * 2.7 + u * 7.4 + v * 3.8) * continuity.broadMotionFloor * 0.28 * visuals.flagMotion;
6677
6935
  positions.push(vec3(x + flap, y, z));
6678
6936
  }
6679
6937
  }
@@ -6691,15 +6949,74 @@ function buildClothSurface(model, state, meshDetail) {
6691
6949
  band,
6692
6950
  representation,
6693
6951
  continuity,
6952
+ color: visuals.flagColor,
6694
6953
  positions,
6695
6954
  indices,
6696
6955
  grid: { rows, cols }
6697
6956
  };
6698
6957
  }
6699
- function sampleWave(x, z, time) {
6700
- return Math.sin(x * 0.18 + time * 1.2) * 0.55 + Math.cos(z * 0.12 + time * 0.9) * 0.35 + Math.sin((x + z) * 0.08 + time * 1.6) * 0.22;
6958
+ function resolveWaveDirection(state) {
6959
+ const direction = state.demoVisuals?.waveDirection;
6960
+ if (direction && typeof direction === "object" && typeof direction.x === "number" && typeof direction.z === "number") {
6961
+ return normalizeVec3(vec3(direction.x, 0, direction.z));
6962
+ }
6963
+ return normalizeVec3(vec3(0.86, 0, 0.34));
6701
6964
  }
6702
- function buildWaterBands(state, fluidDetail) {
6965
+ function sampleShipWake(state, x, z, time) {
6966
+ const wakeStrength = readVisualNumber(state.demoVisuals?.wakeStrength, 0.24);
6967
+ const wakeLength = readVisualNumber(state.demoVisuals?.wakeLength, 15);
6968
+ let total = 0;
6969
+ for (const ship of state.ships) {
6970
+ const speed = Math.hypot(ship.velocity.x, ship.velocity.z);
6971
+ if (speed <= 0.05) {
6972
+ continue;
6973
+ }
6974
+ const direction = normalizeVec3(vec3(ship.velocity.x, 0, ship.velocity.z));
6975
+ const behind = scaleVec3(direction, -1);
6976
+ const lateral = vec3(-direction.z, 0, direction.x);
6977
+ const delta = vec3(x - ship.position.x, 0, z - ship.position.z);
6978
+ const along = dotVec3(delta, behind);
6979
+ if (along < 0 || along > wakeLength) {
6980
+ continue;
6981
+ }
6982
+ const cross = Math.abs(dotVec3(delta, lateral));
6983
+ const width = 0.9 + along * 0.2;
6984
+ if (cross > width * 3.2) {
6985
+ continue;
6986
+ }
6987
+ const envelope = Math.exp(-along * 0.14) * Math.exp(-(cross * cross / Math.max(0.4, width * width * 2.4)));
6988
+ total += Math.sin(along * 1.6 - time * 4.2) * speed * wakeStrength * envelope;
6989
+ }
6990
+ return total;
6991
+ }
6992
+ function sampleWaveImpulses(state, x, z, time) {
6993
+ const rippleStrength = readVisualNumber(state.demoVisuals?.collisionRippleStrength, 0.34);
6994
+ let total = 0;
6995
+ for (const impulse of state.waveImpulses) {
6996
+ const dx = x - impulse.x;
6997
+ const dz = z - impulse.z;
6998
+ const distance = Math.hypot(dx, dz);
6999
+ const radius = impulse.radius + (1 - impulse.life) * 4.8;
7000
+ if (distance > radius * 2.8) {
7001
+ continue;
7002
+ }
7003
+ const phase = distance * 1.8 - (1 - impulse.life) * 10 - time * 0.4;
7004
+ const envelope = Math.exp(-distance / Math.max(0.1, radius)) * impulse.life;
7005
+ total += Math.sin(phase) * impulse.strength * rippleStrength * envelope * 0.18;
7006
+ }
7007
+ return total;
7008
+ }
7009
+ function sampleWave(state, x, z, time) {
7010
+ const direction = resolveWaveDirection(state);
7011
+ const lateral = vec3(-direction.z, 0, direction.x);
7012
+ const along = x * direction.x + z * direction.z;
7013
+ const cross = x * lateral.x + z * lateral.z;
7014
+ const phaseSpeed = readVisualNumber(state.demoVisuals?.wavePhaseSpeed, 1);
7015
+ const amplitude = readVisualNumber(state.demoVisuals?.waveAmplitude, 1);
7016
+ const base = Math.sin(along * 0.22 - time * 1.12 * phaseSpeed) * 0.42 + Math.cos(along * 0.11 + cross * 0.07 - time * 0.78 * phaseSpeed) * 0.26 + Math.sin(cross * 0.19 - time * 1.34 * phaseSpeed) * 0.16;
7017
+ return base * amplitude + sampleShipWake(state, x, z, time) + sampleWaveImpulses(state, x, z, time);
7018
+ }
7019
+ function buildWaterBands(state, fluidDetail, visuals) {
6703
7020
  const fluidPlan = createFluidRepresentationPlan({
6704
7021
  fluidBodyId: "harbor",
6705
7022
  kind: state.focus === "fluid" ? "ocean" : fluidBodyKinds[0],
@@ -6732,7 +7049,7 @@ function buildWaterBands(state, fluidDetail) {
6732
7049
  const v = row / (rows - 1);
6733
7050
  const x = originX + bandSpec.width * u;
6734
7051
  const z = originZ + bandSpec.depth * v;
6735
- const y = bandSpec.y + sampleWave(x, z, state.time) * continuity.amplitudeFloor * (bandSpec.band === "near" ? 0.9 : bandSpec.band === "mid" ? 0.55 : 0.3);
7052
+ const y = bandSpec.y + sampleWave(state, x, z, state.time) * continuity.amplitudeFloor * (bandSpec.band === "near" ? 0.9 : bandSpec.band === "mid" ? 0.55 : 0.3);
6736
7053
  positions.push(vec3(x, y, z));
6737
7054
  }
6738
7055
  }
@@ -6753,7 +7070,15 @@ function buildWaterBands(state, fluidDetail) {
6753
7070
  cols,
6754
7071
  positions,
6755
7072
  indices,
6756
- color: bandSpec.band === "near" ? { r: 0.12, g: 0.36, b: 0.46 } : bandSpec.band === "mid" ? { r: 0.15, g: 0.42, b: 0.54 } : bandSpec.band === "far" ? { r: 0.22, g: 0.48, b: 0.6 } : { r: 0.34, g: 0.58, b: 0.7 }
7073
+ color: bandSpec.band === "near" ? visuals.waterNear : bandSpec.band === "mid" ? {
7074
+ r: mix(visuals.waterNear.r, visuals.waterFar.r, 0.4),
7075
+ g: mix(visuals.waterNear.g, visuals.waterFar.g, 0.4),
7076
+ b: mix(visuals.waterNear.b, visuals.waterFar.b, 0.4)
7077
+ } : bandSpec.band === "far" ? visuals.waterFar : {
7078
+ r: mix(visuals.waterFar.r, 0.76, 0.2),
7079
+ g: mix(visuals.waterFar.g, 0.78, 0.2),
7080
+ b: mix(visuals.waterFar.b, 0.82, 0.2)
7081
+ }
6757
7082
  });
6758
7083
  }
6759
7084
  return { fluidPlan, bandMeshes };
@@ -6792,6 +7117,9 @@ function createSceneState(options) {
6792
7117
  clothDetail,
6793
7118
  lightingDetail,
6794
7119
  debugSession,
7120
+ packageState: void 0,
7121
+ demoDescription: null,
7122
+ demoVisuals: null,
6795
7123
  time: 0,
6796
7124
  lastTimeMs: null,
6797
7125
  paused: false,
@@ -6819,7 +7147,9 @@ function createSceneState(options) {
6819
7147
  }
6820
7148
  ],
6821
7149
  sprays: [],
7150
+ waveImpulses: [],
6822
7151
  frame: 0,
7152
+ contactCount: 0,
6823
7153
  collisionCount: 0,
6824
7154
  collisionFlash: 0,
6825
7155
  physics: {
@@ -6834,24 +7164,24 @@ function createSceneState(options) {
6834
7164
  function setListContent(element, values) {
6835
7165
  element.innerHTML = values.map((value) => `<li>${value}</li>`).join("");
6836
7166
  }
6837
- function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength) {
7167
+ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength, visuals) {
6838
7168
  const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
6839
7169
  const sky = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.5);
6840
- sky.addColorStop(0, premiumShadows ? "#f0f7fb" : "#e8f1f7");
6841
- sky.addColorStop(0.6, premiumShadows ? "#c7d9e5" : "#b9ceda");
6842
- sky.addColorStop(1, premiumShadows ? "#84a7bd" : "#7b9bb0");
7170
+ sky.addColorStop(0, visuals.skyTop);
7171
+ sky.addColorStop(0.6, visuals.skyMid);
7172
+ sky.addColorStop(1, visuals.skyBottom);
6843
7173
  ctx.fillStyle = sky;
6844
7174
  ctx.fillRect(0, 0, canvas.width, canvas.height);
6845
7175
  const shoreline = ctx.createLinearGradient(0, canvas.height * 0.45, 0, canvas.height);
6846
- shoreline.addColorStop(0, premiumShadows ? "#235064" : "#264c5f");
6847
- shoreline.addColorStop(0.52, premiumShadows ? "#153e53" : "#173d4f");
6848
- shoreline.addColorStop(1, "#0b2433");
7176
+ shoreline.addColorStop(0, visuals.seaTop);
7177
+ shoreline.addColorStop(0.52, visuals.seaMid);
7178
+ shoreline.addColorStop(1, visuals.seaBottom);
6849
7179
  ctx.fillStyle = shoreline;
6850
7180
  ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
6851
7181
  const sunX = mix(canvas.width * 0.16, canvas.width * 0.84, (Math.sin(state.time * 0.12) + 1) * 0.5);
6852
7182
  const sunY = canvas.height * 0.14 + Math.cos(state.time * 0.12) * 22;
6853
7183
  const sun = ctx.createRadialGradient(sunX, sunY, 10, sunX, sunY, 90);
6854
- sun.addColorStop(0, "rgba(255, 244, 210, 0.9)");
7184
+ sun.addColorStop(0, visuals.sunCore);
6855
7185
  sun.addColorStop(1, "rgba(255, 244, 210, 0)");
6856
7186
  ctx.fillStyle = sun;
6857
7187
  ctx.beginPath();
@@ -6927,27 +7257,27 @@ function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, opt
6927
7257
  ctx.fill();
6928
7258
  ctx.restore();
6929
7259
  }
6930
- function pushHarborGeometry(camera, viewport, triangles) {
7260
+ function pushHarborGeometry(camera, viewport, triangles, visuals) {
6931
7261
  const harborObjects = [
6932
7262
  {
6933
7263
  position: vec3(-8.2, 1.1, -0.9),
6934
7264
  rotationY: -0.16,
6935
7265
  scale: { x: 5.4, y: 2.4, z: 4.2 },
6936
- color: { r: 0.48, g: 0.4, b: 0.32 },
7266
+ color: visuals.harborWall,
6937
7267
  accent: 0.06
6938
7268
  },
6939
7269
  {
6940
7270
  position: vec3(-5.7, 0.45, 1.4),
6941
7271
  rotationY: -0.08,
6942
7272
  scale: { x: 6.8, y: 0.3, z: 2.1 },
6943
- color: { r: 0.5, g: 0.34, b: 0.22 },
7273
+ color: visuals.harborDeck,
6944
7274
  accent: 0.04
6945
7275
  },
6946
7276
  {
6947
7277
  position: vec3(-10.4, 0.28, 0.8),
6948
7278
  rotationY: 0.22,
6949
7279
  scale: { x: 1.2, y: 0.9, z: 1.2 },
6950
- color: { r: 0.34, g: 0.32, b: 0.36 },
7280
+ color: visuals.harborTower,
6951
7281
  accent: 0.02
6952
7282
  }
6953
7283
  ];
@@ -7027,7 +7357,7 @@ function renderClothAccent(ctx, cloth, camera, viewport) {
7027
7357
  cloth.grid.rows * cloth.grid.cols - 1,
7028
7358
  (cloth.grid.rows - 1) * cloth.grid.cols
7029
7359
  ];
7030
- ctx.fillStyle = "rgba(164, 44, 28, 0.95)";
7360
+ ctx.fillStyle = colorToRgba(cloth.color, 0.95);
7031
7361
  for (const index of borderIndices) {
7032
7362
  const point = projected[index];
7033
7363
  if (!point) {
@@ -7088,12 +7418,13 @@ function updateShips(state, dt, shipModel) {
7088
7418
  const physics = shipModel.physics;
7089
7419
  const halfExtents = physics.halfExtents ?? [1.35, 0.95, 3.9];
7090
7420
  let collided = false;
7421
+ state.contactCount = 0;
7091
7422
  for (const ship of state.ships) {
7092
7423
  ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
7093
7424
  ship.rotationY += ship.angularVelocity * dt;
7094
7425
  ship.velocity = scaleVec3(ship.velocity, 1 - (physics.linearDamping ?? 0.04) * dt);
7095
7426
  ship.angularVelocity *= 1 - (physics.angularDamping ?? 0.08) * dt;
7096
- ship.position.y = sampleWave(ship.position.x, ship.position.z, state.time) * 0.22 + (physics.waterline ?? 0.42);
7427
+ ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.22 + (physics.waterline ?? 0.42);
7097
7428
  if (Math.abs(ship.position.x) > 10) {
7098
7429
  ship.velocity.x *= -1;
7099
7430
  ship.angularVelocity *= -1;
@@ -7120,11 +7451,25 @@ function updateShips(state, dt, shipModel) {
7120
7451
  b.angularVelocity -= 0.55;
7121
7452
  const contactPoint = vec3((a.position.x + b.position.x) * 0.5, (a.position.y + b.position.y) * 0.5 + 0.1, (a.position.z + b.position.z) * 0.5);
7122
7453
  spawnSpray(state, contactPoint, Math.abs(dx) + Math.abs(dz));
7454
+ state.waveImpulses.push({
7455
+ x: contactPoint.x,
7456
+ z: contactPoint.z,
7457
+ strength: Math.min(1.4, 0.2 + (Math.abs(dx) + Math.abs(dz)) * 0.18),
7458
+ radius: 0.8,
7459
+ life: 1
7460
+ });
7123
7461
  state.collisionCount += 1;
7462
+ state.contactCount = 1;
7124
7463
  collided = true;
7125
7464
  }
7126
7465
  state.collisionFlash = collided ? 1 : Math.max(0, state.collisionFlash - dt * 1.8);
7127
7466
  }
7467
+ function updateWaveImpulses(state, dt) {
7468
+ state.waveImpulses = state.waveImpulses.map((impulse) => ({
7469
+ ...impulse,
7470
+ life: impulse.life - dt * 0.55
7471
+ })).filter((impulse) => impulse.life > 0);
7472
+ }
7128
7473
  function updateSpray(state, dt) {
7129
7474
  state.sprays = state.sprays.map((particle) => {
7130
7475
  const nextVelocity = vec3(particle.velocity.x, particle.velocity.y - 4.2 * dt, particle.velocity.z);
@@ -7238,7 +7583,7 @@ function renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDi
7238
7583
  vec3(bounds.min[0], keelY, bounds.max[2])
7239
7584
  ].map((point) => transformPoint(point, transform));
7240
7585
  renderProjectedShadow(ctx, hullCorners, camera, viewport, lightDir, {
7241
- planeY: sampleWave(ship.position.x, ship.position.z, state.time) * 0.24 - 0.03,
7586
+ planeY: sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.24 - 0.03,
7242
7587
  alpha: 0.08 + shadowStrength * 0.2,
7243
7588
  blur: 14 + shadowStrength * 24
7244
7589
  });
@@ -7267,12 +7612,30 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7267
7612
  const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
7268
7613
  const lightDir = normalizeVec3(vec3(-0.45, 0.85, -0.24));
7269
7614
  const lightingSnapshot = state.lightingDetail.getSnapshot();
7270
- const reflectionStrength = lightingSnapshot.currentLevel.config.reflectionStrength;
7271
- const shadowStrength = lightingSnapshot.currentLevel.config.shadowStrength;
7272
- drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength);
7615
+ const visuals = resolveVisualConfig(
7616
+ nearLighting,
7617
+ lightingSnapshot,
7618
+ state.demoDescription?.visuals
7619
+ );
7620
+ state.demoVisuals = visuals;
7621
+ const reflectionStrength = visuals.reflectionStrength;
7622
+ const shadowStrength = visuals.shadowAccent;
7623
+ drawSkyAndShore(
7624
+ ctx,
7625
+ canvas,
7626
+ state,
7627
+ nearLighting,
7628
+ reflectionStrength,
7629
+ shadowStrength,
7630
+ visuals
7631
+ );
7273
7632
  const triangles = [];
7274
- pushHarborGeometry(camera, viewport, triangles);
7275
- const water = buildWaterBands(state, state.fluidDetail.getSnapshot().currentLevel.config);
7633
+ pushHarborGeometry(camera, viewport, triangles, visuals);
7634
+ const water = buildWaterBands(
7635
+ state,
7636
+ state.fluidDetail.getSnapshot().currentLevel.config,
7637
+ visuals
7638
+ );
7276
7639
  for (const bandMesh of water.bandMeshes) {
7277
7640
  const bandAccent = bandMesh.band === "near" ? 0.06 : bandMesh.band === "mid" ? 0.04 : 0;
7278
7641
  for (let index = 0; index < bandMesh.indices.length; index += 3) {
@@ -7294,7 +7657,12 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7294
7657
  });
7295
7658
  }
7296
7659
  }
7297
- const cloth = buildClothSurface(state, state, state.clothDetail.getSnapshot().currentLevel.config);
7660
+ const cloth = buildClothSurface(
7661
+ state,
7662
+ state,
7663
+ state.clothDetail.getSnapshot().currentLevel.config,
7664
+ visuals
7665
+ );
7298
7666
  for (let index = 0; index < cloth.indices.length; index += 3) {
7299
7667
  const a = cloth.positions[cloth.indices[index]];
7300
7668
  const b = cloth.positions[cloth.indices[index + 1]];
@@ -7309,7 +7677,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7309
7677
  depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
7310
7678
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
7311
7679
  normal,
7312
- baseColor: { r: 0.76, g: 0.24, b: 0.18 },
7680
+ baseColor: cloth.color,
7313
7681
  accent: cloth.band === "near" ? 0.1 : 0.04
7314
7682
  });
7315
7683
  }
@@ -7372,15 +7740,26 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7372
7740
  "The ships collide on GLTF-derived hull volumes while cloth and fluid remain downstream visual consumers.",
7373
7741
  "Near-field lighting keeps the ray-traced-primary shadow impression so the collision read stays crisp."
7374
7742
  ] : SCENE_NOTES;
7375
- setListContent(dom.sceneMetrics, sceneMetrics);
7376
- setListContent(dom.qualityMetrics, qualityMetrics);
7377
- setListContent(dom.debugMetrics, debugMetrics);
7378
- setListContent(dom.sceneNotes, sceneNotes);
7379
- dom.status.textContent = `3D scene live \xB7 ${state.lastDecision.metrics.fps.toFixed(1)} FPS`;
7380
- dom.details.textContent = state.focus === "physics" ? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver; GLTF ships collide on ${shipModel.physics.shape ?? "box"} volumes while visual follow-up remains downstream.` : `GLTF ships are colliding with ${shipModel.physics.shape ?? "box"} physics volumes; cloth and fluid remain continuous while the governor pressure is ${state.lastDecision.pressureLevel}.`;
7743
+ const custom = state.demoDescription ?? null;
7744
+ setListContent(
7745
+ dom.sceneMetrics,
7746
+ Array.isArray(custom?.sceneMetrics) ? custom.sceneMetrics : sceneMetrics
7747
+ );
7748
+ setListContent(
7749
+ dom.qualityMetrics,
7750
+ Array.isArray(custom?.qualityMetrics) ? custom.qualityMetrics : qualityMetrics
7751
+ );
7752
+ setListContent(
7753
+ dom.debugMetrics,
7754
+ Array.isArray(custom?.debugMetrics) ? custom.debugMetrics : debugMetrics
7755
+ );
7756
+ setListContent(dom.sceneNotes, Array.isArray(custom?.notes) ? custom.notes : sceneNotes);
7757
+ dom.status.textContent = typeof custom?.status === "string" ? custom.status : `3D scene live \xB7 ${state.lastDecision.metrics.fps.toFixed(1)} FPS`;
7758
+ dom.details.textContent = typeof custom?.details === "string" ? custom.details : state.focus === "physics" ? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver; GLTF ships collide on ${shipModel.physics.shape ?? "box"} volumes while visual follow-up remains downstream.` : `GLTF ships are colliding with ${shipModel.physics.shape ?? "box"} physics volumes; cloth and fluid remain continuous while the governor pressure is ${state.lastDecision.pressureLevel}.`;
7381
7759
  }
7382
7760
  function updateSceneState(state, dt, shipModel) {
7383
7761
  updateShips(state, dt, shipModel);
7762
+ updateWaveImpulses(state, dt);
7384
7763
  updateSpray(state, dt);
7385
7764
  updatePhysicsSnapshot(state, shipModel);
7386
7765
  }
@@ -7399,13 +7778,15 @@ function syncTextState(state, shipModel) {
7399
7778
  })),
7400
7779
  shipPhysics: shipModel.physics,
7401
7780
  sprays: state.sprays.length,
7781
+ waveImpulses: state.waveImpulses.length,
7402
7782
  pressure: state.lastDecision?.pressureLevel ?? "stable",
7403
7783
  physics: {
7404
7784
  profile: state.physics.profile,
7405
7785
  snapshotStageId: state.physics.plan.snapshotStageId,
7406
7786
  workerJobCount: state.physics.manifest.jobs.length,
7407
7787
  snapshot: state.physics.snapshot
7408
- }
7788
+ },
7789
+ package: state.demoDescription?.textState ?? null
7409
7790
  };
7410
7791
  window.render_game_to_text = () => JSON.stringify(snapshot);
7411
7792
  window.advanceTime = (ms) => {
@@ -7431,8 +7812,10 @@ async function mountGpuShowcase(options = {}) {
7431
7812
  const state = createSceneState({ focus });
7432
7813
  const shipModel = await loadGltfModel(resolveShowcaseAssetUrl());
7433
7814
  state.shipModel = shipModel;
7815
+ state.packageState = typeof options.createState === "function" ? options.createState() : void 0;
7434
7816
  updatePhysicsSnapshot(state, shipModel);
7435
7817
  state.lastDecision = recordTelemetry(state, 16.4);
7818
+ state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7436
7819
  syncTextState(state, shipModel);
7437
7820
  const ctx = dom.canvas.getContext("2d");
7438
7821
  const renderFrame = (nowMs) => {
@@ -7445,9 +7828,11 @@ async function mountGpuShowcase(options = {}) {
7445
7828
  state.time += dt;
7446
7829
  state.frame += 1;
7447
7830
  updateSceneState(state, dt, shipModel);
7831
+ updatePackageState(state, options, shipModel, dt);
7448
7832
  const syntheticFrame = 14.2 + state.sprays.length * 0.1 + (state.stress ? 6.4 : 0);
7449
7833
  state.lastDecision = recordTelemetry(state, syntheticFrame);
7450
7834
  }
7835
+ state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7451
7836
  renderScene(ctx, dom.canvas, state, shipModel, dom);
7452
7837
  syncTextState(state, shipModel);
7453
7838
  requestAnimationFrame(renderFrame);
@@ -7487,6 +7872,7 @@ function updatePhysicsSnapshot(state, shipModel) {
7487
7872
  contactCount: state.collisionFlash > 0.02 ? 1 : 0,
7488
7873
  metadata: {
7489
7874
  collisionCount: state.collisionCount,
7875
+ contactCount: state.contactCount,
7490
7876
  snapshotStageId: state.physics.plan.snapshotStageId,
7491
7877
  rigidBodyShape: shipModel.physics.shape ?? "box"
7492
7878
  }