@jgengine/shell 0.25.0 → 0.26.0

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/Shell3dPresentation.js +29 -11
  3. package/dist/devtools/FrameTelemetry.d.ts +12 -0
  4. package/dist/devtools/FrameTelemetry.js +68 -0
  5. package/dist/devtools/agentBridge.js +2 -0
  6. package/dist/devtools/devtoolsReports.d.ts +2 -0
  7. package/dist/devtools/devtoolsReports.js +3 -0
  8. package/dist/devtools/frameSampler.d.ts +127 -0
  9. package/dist/devtools/frameSampler.js +345 -0
  10. package/dist/environment/AuthoredDaylight.d.ts +28 -0
  11. package/dist/environment/AuthoredDaylight.js +42 -0
  12. package/dist/environment/Daylight.d.ts +55 -7
  13. package/dist/environment/Daylight.js +126 -29
  14. package/dist/environment/daylightCycle.d.ts +26 -4
  15. package/dist/environment/daylightCycle.js +36 -5
  16. package/dist/postfx/PostProcessing.d.ts +7 -1
  17. package/dist/postfx/PostProcessing.js +28 -2
  18. package/dist/postfx/autoExposure.d.ts +49 -0
  19. package/dist/postfx/autoExposure.js +189 -0
  20. package/dist/postfx/gradeShader.js +43 -4
  21. package/dist/presentationRecovery3d.js +2 -1
  22. package/dist/render/CascadedShadows.d.ts +3 -1
  23. package/dist/render/CascadedShadows.js +9 -1
  24. package/dist/render/EnvironmentLighting.d.ts +10 -1
  25. package/dist/render/EnvironmentLighting.js +11 -3
  26. package/dist/render/IrradianceVolumeLighting.d.ts +18 -0
  27. package/dist/render/IrradianceVolumeLighting.js +35 -0
  28. package/dist/render/SceneLighting.d.ts +14 -1
  29. package/dist/render/SceneLighting.js +29 -8
  30. package/dist/render/authoredGrade.d.ts +11 -0
  31. package/dist/render/authoredGrade.js +26 -0
  32. package/dist/render/presentationLook.d.ts +15 -0
  33. package/dist/render/presentationLook.js +18 -0
  34. package/dist/render/stableTextureSet.d.ts +9 -0
  35. package/dist/render/stableTextureSet.js +24 -0
  36. package/dist/render/triplanarMaterial.d.ts +52 -0
  37. package/dist/render/triplanarMaterial.js +169 -0
  38. package/dist/settings/appliedSettings.js +16 -13
  39. package/package.json +4 -4
  40. package/skills/jgengine/api.md +59 -6
  41. package/skills/jgengine/capabilities.md +4 -0
  42. package/skills/jgengine-ui/api.md +82 -22
  43. package/skills/jgengine-ui/capabilities.md +10 -0
  44. package/skills/jgengine-ui/reference-primitives.md +4 -2
  45. package/skills/jgengine-world/SKILL.md +3 -1
  46. package/skills/jgengine-world/api.md +107 -3
  47. package/skills/jgengine-world/capabilities.md +22 -2
  48. package/skills/jgengine-world/recipes/npc-senses.md +8 -2
@@ -0,0 +1,24 @@
1
+ import { useRef } from "react";
2
+ /** Whether two texture sets hold the same roles and texture instances. @internal */
3
+ export function sameTextureSet(a, b) {
4
+ if (a === b)
5
+ return true;
6
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
7
+ return false;
8
+ const keys = Object.keys(a);
9
+ if (keys.length !== Object.keys(b).length)
10
+ return false;
11
+ return keys.every((key) => a[key] === b[key]);
12
+ }
13
+ /**
14
+ * Keep one texture-set identity while its roles and texture instances are unchanged. The drei
15
+ * loader hands back a fresh keyed object per render, and materials built from it rebuilt and
16
+ * recompiled their shaders every frame.
17
+ * @internal
18
+ */
19
+ export function useStableTextureSet(textures) {
20
+ const stable = useRef(textures);
21
+ if (!sameTextureSet(stable.current, textures))
22
+ stable.current = textures;
23
+ return stable.current;
24
+ }
@@ -0,0 +1,52 @@
1
+ import type { MaterialAsset } from "@jgengine/core/material/materialAsset";
2
+ import * as THREE from "three";
3
+ /** Inputs for {@link applyTriplanarProjection}. */
4
+ export interface TriplanarProjectionOptions {
5
+ /** Metres one texture tile covers in world space. Must be finite and positive. */
6
+ tileMetres: number;
7
+ /** Blend sharpness between the three projections; 4 keeps corners soft, 8 is decisive. */
8
+ sharpness?: number;
9
+ /** Normal strength applied to each projection's tangent-space perturbation. */
10
+ normalScale?: number;
11
+ }
12
+ /**
13
+ * The GLSL the triplanar path injects, split by chunk. Exposed so a test can prove the patch
14
+ * samples by world position instead of `vMapUv`, and so a reader can audit the projection.
15
+ * @internal
16
+ */
17
+ export declare function triplanarChunks(): {
18
+ vertexPars: string;
19
+ vertexNormal: string;
20
+ vertexPosition: string;
21
+ fragmentPars: string;
22
+ color: string;
23
+ roughness: string;
24
+ normal: string;
25
+ ao: string;
26
+ };
27
+ /**
28
+ * Projects a standard material's maps from world position instead of mesh UVs, so world geometry
29
+ * without a `uv` attribute still renders a textured PBR surface. Instanced meshes project in their
30
+ * instanced world space. Skinned meshes keep the bind-pose normal for the blend weights, which is
31
+ * close enough for the static world surfaces this path exists for.
32
+ */
33
+ export declare function applyTriplanarProjection(material: THREE.MeshStandardMaterial, options: TriplanarProjectionOptions): THREE.MeshStandardMaterial;
34
+ /**
35
+ * Render a serializable material asset — one built by `libraryMaterialAsset`, or authored in the
36
+ * editor — with world-space triplanar projection, so world geometry needs no UVs. Tile size
37
+ * defaults to the asset's own `physicalSize`, which the shared library sets per surface.
38
+ * @capability material-triplanar render a material asset on world geometry without UVs
39
+ */
40
+ export declare function useTriplanarAssetMaterial(asset: MaterialAsset, options?: {
41
+ tileMetres?: number;
42
+ sharpness?: number;
43
+ normalScale?: number;
44
+ }): THREE.MeshStandardMaterial;
45
+ /** Mounts {@link useTriplanarAssetMaterial} on a mesh; the R3F renderer owns disposal. */
46
+ export declare function TriplanarAssetMaterial({ asset, ...options }: {
47
+ asset: MaterialAsset;
48
+ } & {
49
+ tileMetres?: number;
50
+ sharpness?: number;
51
+ normalScale?: number;
52
+ }): import("react").JSX.Element;
@@ -0,0 +1,169 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import * as THREE from "three";
4
+ import { useMaterialAssetMaterial } from "./materialAsset.js";
5
+ const VERTEX_PARS = `varying vec3 vJgTriPos;
6
+ varying vec3 vJgTriNormal;`;
7
+ const VERTEX_WORLD_NORMAL = `vJgTriNormal = normalize(mat3(modelMatrix) * objectNormal);`;
8
+ const VERTEX_WORLD_POSITION = `vec4 jgTriWorld = vec4(transformed, 1.0);
9
+ #ifdef USE_INSTANCING
10
+ jgTriWorld = instanceMatrix * jgTriWorld;
11
+ #endif
12
+ vJgTriPos = (modelMatrix * jgTriWorld).xyz;`;
13
+ const FRAGMENT_PARS = `varying vec3 vJgTriPos;
14
+ varying vec3 vJgTriNormal;
15
+ uniform float uJgTriTile;
16
+ uniform float uJgTriSharp;
17
+ uniform float uJgTriNormalScale;
18
+ vec3 jgTriWeights() {
19
+ vec3 w = pow(abs(normalize(vJgTriNormal)), vec3(uJgTriSharp));
20
+ return w / max(dot(w, vec3(1.0)), 1e-4);
21
+ }
22
+ vec3 jgTriSample(sampler2D tex, vec3 w) {
23
+ vec3 p = vJgTriPos / uJgTriTile;
24
+ return texture2D(tex, p.zy).rgb * w.x + texture2D(tex, p.xz).rgb * w.y + texture2D(tex, p.xy).rgb * w.z;
25
+ }`;
26
+ const FRAGMENT_COLOR = `#ifdef USE_MAP
27
+ {
28
+ vec3 jgW = jgTriWeights();
29
+ vec3 jgP = vJgTriPos / uJgTriTile;
30
+ diffuseColor *= texture2D(map, jgP.zy) * jgW.x + texture2D(map, jgP.xz) * jgW.y + texture2D(map, jgP.xy) * jgW.z;
31
+ }
32
+ #endif`;
33
+ const FRAGMENT_ROUGHNESS = `float roughnessFactor = roughness;
34
+ #ifdef USE_ROUGHNESSMAP
35
+ roughnessFactor *= jgTriSample(roughnessMap, jgTriWeights()).g;
36
+ #endif`;
37
+ // Each projection perturbs the world normal along its own tangent frame: an X-facing projection
38
+ // uses +Z as U and +Y as V, a Y-facing one +X/+Z, a Z-facing one +X/+Y.
39
+ const FRAGMENT_NORMAL = `#ifdef USE_NORMALMAP
40
+ {
41
+ vec3 jgW = jgTriWeights();
42
+ vec3 jgP = vJgTriPos / uJgTriTile;
43
+ vec3 jgNx = texture2D(normalMap, jgP.zy).xyz * 2.0 - 1.0;
44
+ vec3 jgNy = texture2D(normalMap, jgP.xz).xyz * 2.0 - 1.0;
45
+ vec3 jgNz = texture2D(normalMap, jgP.xy).xyz * 2.0 - 1.0;
46
+ jgNx.xy *= uJgTriNormalScale;
47
+ jgNy.xy *= uJgTriNormalScale;
48
+ jgNz.xy *= uJgTriNormalScale;
49
+ vec3 jgPerturb = vec3(jgNx.z, jgNx.y, jgNx.x) * jgW.x
50
+ + vec3(jgNy.x, jgNy.z, jgNy.y) * jgW.y
51
+ + vec3(jgNz.x, jgNz.y, jgNz.z) * jgW.z;
52
+ normal = normalize((viewMatrix * vec4(normalize(vJgTriNormal + jgPerturb), 0.0)).xyz);
53
+ }
54
+ #endif`;
55
+ const FRAGMENT_AO = `#ifdef USE_AOMAP
56
+ {
57
+ float jgAo = jgTriSample(aoMap, jgTriWeights()).r;
58
+ float ambientOcclusion = ( jgAo - 1.0 ) * aoMapIntensity + 1.0;
59
+ reflectedLight.indirectDiffuse *= ambientOcclusion;
60
+ #if defined( USE_CLEARCOAT )
61
+ clearcoatSpecularIndirect *= ambientOcclusion;
62
+ #endif
63
+ #if defined( USE_SHEEN )
64
+ sheenSpecularIndirect *= ambientOcclusion;
65
+ #endif
66
+ #if defined( USE_ENVMAP ) && defined( STANDARD )
67
+ float dotNV = saturate( dot( geometryNormal, geometryViewDir ) );
68
+ reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );
69
+ #endif
70
+ }
71
+ #endif`;
72
+ /**
73
+ * The GLSL the triplanar path injects, split by chunk. Exposed so a test can prove the patch
74
+ * samples by world position instead of `vMapUv`, and so a reader can audit the projection.
75
+ * @internal
76
+ */
77
+ export function triplanarChunks() {
78
+ return {
79
+ vertexPars: VERTEX_PARS,
80
+ vertexNormal: VERTEX_WORLD_NORMAL,
81
+ vertexPosition: VERTEX_WORLD_POSITION,
82
+ fragmentPars: FRAGMENT_PARS,
83
+ color: FRAGMENT_COLOR,
84
+ roughness: FRAGMENT_ROUGHNESS,
85
+ normal: FRAGMENT_NORMAL,
86
+ ao: FRAGMENT_AO,
87
+ };
88
+ }
89
+ function assertPatchable(material) {
90
+ const vertex = THREE.ShaderLib.physical.vertexShader;
91
+ const fragment = THREE.ShaderLib.physical.fragmentShader;
92
+ const required = [
93
+ [vertex, "#include <beginnormal_vertex>"],
94
+ [vertex, "#include <worldpos_vertex>"],
95
+ [fragment, "#include <map_fragment>"],
96
+ [fragment, "#include <roughnessmap_fragment>"],
97
+ [fragment, "#include <normal_fragment_maps>"],
98
+ [fragment, "#include <aomap_fragment>"],
99
+ ];
100
+ for (const [source, marker] of required) {
101
+ if (!source.includes(marker))
102
+ throw new Error(`Three.js r${THREE.REVISION} physical shader lacks ${marker} for the triplanar world projection`);
103
+ }
104
+ if (!material.isMeshStandardMaterial)
105
+ throw new Error("Triplanar projection requires a standard or physical material");
106
+ }
107
+ /**
108
+ * Projects a standard material's maps from world position instead of mesh UVs, so world geometry
109
+ * without a `uv` attribute still renders a textured PBR surface. Instanced meshes project in their
110
+ * instanced world space. Skinned meshes keep the bind-pose normal for the blend weights, which is
111
+ * close enough for the static world surfaces this path exists for.
112
+ */
113
+ export function applyTriplanarProjection(material, options) {
114
+ const tile = options.tileMetres;
115
+ if (!Number.isFinite(tile) || tile <= 0)
116
+ throw new Error("Triplanar tileMetres must be finite and positive");
117
+ const sharpness = options.sharpness ?? 8;
118
+ const normalScale = options.normalScale ?? 1;
119
+ if (!Number.isFinite(sharpness) || sharpness <= 0)
120
+ throw new Error("Triplanar sharpness must be finite and positive");
121
+ if (!Number.isFinite(normalScale) || normalScale < 0)
122
+ throw new Error("Triplanar normalScale must be finite and non-negative");
123
+ if (material.userData["jgTriplanar"] !== undefined)
124
+ return material;
125
+ assertPatchable(material);
126
+ const uniforms = {
127
+ uJgTriTile: { value: tile },
128
+ uJgTriSharp: { value: sharpness },
129
+ uJgTriNormalScale: { value: normalScale },
130
+ };
131
+ material.userData["jgTriplanar"] = uniforms;
132
+ const previousCompile = material.onBeforeCompile;
133
+ const previousKey = material.customProgramCacheKey;
134
+ material.onBeforeCompile = (shader, renderer) => {
135
+ previousCompile.call(material, shader, renderer);
136
+ Object.assign(shader.uniforms, uniforms);
137
+ shader.vertexShader = shader.vertexShader
138
+ .replace("#include <common>", `#include <common>\n${VERTEX_PARS}`)
139
+ .replace("#include <beginnormal_vertex>", `#include <beginnormal_vertex>\n${VERTEX_WORLD_NORMAL}`)
140
+ .replace("#include <worldpos_vertex>", `#include <worldpos_vertex>\n${VERTEX_WORLD_POSITION}`);
141
+ shader.fragmentShader = shader.fragmentShader
142
+ .replace("#include <common>", `#include <common>\n${FRAGMENT_PARS}`)
143
+ .replace("#include <map_fragment>", FRAGMENT_COLOR)
144
+ .replace("#include <roughnessmap_fragment>", FRAGMENT_ROUGHNESS)
145
+ .replace("#include <normal_fragment_maps>", FRAGMENT_NORMAL)
146
+ .replace("#include <aomap_fragment>", FRAGMENT_AO);
147
+ };
148
+ material.customProgramCacheKey = () => `${previousKey.call(material)}|jg-triplanar:v1`;
149
+ material.needsUpdate = true;
150
+ return material;
151
+ }
152
+ /**
153
+ * Render a serializable material asset — one built by `libraryMaterialAsset`, or authored in the
154
+ * editor — with world-space triplanar projection, so world geometry needs no UVs. Tile size
155
+ * defaults to the asset's own `physicalSize`, which the shared library sets per surface.
156
+ * @capability material-triplanar render a material asset on world geometry without UVs
157
+ */
158
+ export function useTriplanarAssetMaterial(asset, options = {}) {
159
+ const material = useMaterialAssetMaterial(asset);
160
+ const tile = options.tileMetres ?? asset.textures?.color?.physicalSize?.[0] ?? asset.textures?.normal?.physicalSize?.[0] ?? 1;
161
+ const sharpness = options.sharpness ?? 8;
162
+ const normalScale = options.normalScale ?? 1;
163
+ return useMemo(() => applyTriplanarProjection(material, { tileMetres: tile, sharpness, normalScale }), [material, tile, sharpness, normalScale]);
164
+ }
165
+ /** Mounts {@link useTriplanarAssetMaterial} on a mesh; the R3F renderer owns disposal. */
166
+ export function TriplanarAssetMaterial({ asset, ...options }) {
167
+ const material = useTriplanarAssetMaterial(asset, options);
168
+ return _jsx("primitive", { object: material, attach: "material" });
169
+ }
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
2
2
  import { busVolumeSettingId, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
3
3
  import { readGraphicsSettings } from "@jgengine/core/settings/graphicsSettings";
4
4
  import { readFrameRateLimit } from "@jgengine/core/settings/frameRateLimit";
@@ -29,18 +29,21 @@ export function useSettingsRevision(store) {
29
29
  return useSyncExternalStore(subscribe, getVersion, getVersion);
30
30
  }
31
31
  export function useGraphicsSettings(store, shadowsDefault, overrides) {
32
- useSettingsRevision(store);
33
- const { quality, profile } = readGraphicsSettings(store, overrides);
34
- const rawUiScale = store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE);
35
- return {
36
- shadows: store.get(SETTING_IDS.graphicsShadows, shadowsDefault),
37
- dpr: profile.renderScale,
38
- uiScale: Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, rawUiScale)),
39
- quality,
40
- profile,
41
- frameRateLimit: readFrameRateLimit(store),
42
- showFps: store.get(SETTING_IDS.graphicsShowFps, false),
43
- };
32
+ const rev = useSettingsRevision(store);
33
+ // Effects rebuild on identity change; a fresh object each render recompiled the post chain every frame.
34
+ return useMemo(() => {
35
+ const { quality, profile } = readGraphicsSettings(store, overrides);
36
+ const rawUiScale = store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE);
37
+ return {
38
+ shadows: store.get(SETTING_IDS.graphicsShadows, shadowsDefault),
39
+ dpr: profile.renderScale,
40
+ uiScale: Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, rawUiScale)),
41
+ quality,
42
+ profile,
43
+ frameRateLimit: readFrameRateLimit(store),
44
+ showFps: store.get(SETTING_IDS.graphicsShowFps, false),
45
+ };
46
+ }, [store, rev, shadowsDefault, overrides]);
44
47
  }
45
48
  export function AudioSettingsBridge({ store, engine, buses, }) {
46
49
  const rev = useSettingsRevision(store);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/shell",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Game player shell for JGengine: React Three Fiber canvas, orbit camera, input tracking, HUD mounting, GameUiPreview, and a demo game. Consumers supply a GameRegistry.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -35,9 +35,9 @@
35
35
  "test": "bun test src"
36
36
  },
37
37
  "dependencies": {
38
- "@jgengine/core": "^0.25.0",
39
- "@jgengine/react": "^0.25.0",
40
- "@jgengine/ws": "^0.25.0"
38
+ "@jgengine/core": "^0.26.0",
39
+ "@jgengine/react": "^0.26.0",
40
+ "@jgengine/ws": "^0.26.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@react-three/drei": "^10.0.0",
@@ -6,7 +6,7 @@
6
6
 
7
7
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
8
8
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
9
- - `VERSION` (const): const VERSION: "0.25.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.26.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
10
10
 
11
11
  ## @jgengine/core/authoring
12
12
 
@@ -82,25 +82,42 @@
82
82
  - `Devtools` (interface): interface Devtools — Top-level devtools facade aggregating frame, profile, render, logs, latency, controls, discover, overrides, and probes.
83
83
  - `DevtoolsControl` (interface): interface DevtoolsControl — Registered tunable control with its resolved schema metadata and read/write/reset accessors.
84
84
  - `DevtoolsControlKind` (type): type DevtoolsControlKind = | "slider" | "toggle" | "color" | "select" | "text" | "vec2" | "vec3" | "vec4" | "interval" | "angle" | "enum" — ⚠ undocumented
85
- - `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, and formatted message.
85
+ - `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, owning system tag, and formatted message.
86
86
  - `DevtoolsLogLevel` (type): type DevtoolsLogLevel = "log" | "info" | "warn" | "error" — Severity level for a captured devtools log entry.
87
+ - `DevtoolsLogs` (interface): interface DevtoolsLogs — Bounded log ring with per-system tags, a warning/error ring, and lifetime counts.
87
88
  - `DevtoolsOverrides` (interface): interface DevtoolsOverrides { version: number; enabled: string[]; values: Record<string, unknown>; schemas?: Record<string, {kind: DevtoolsControlKind; schemaVersion?: number}> } — ⚠ undocumented
88
89
  - `DevtoolsSnapshot` (interface): interface DevtoolsSnapshot — Serializable point-in-time snapshot of devtools state: frame, render, latency, logs, controls, and discovered fields.
89
90
  - `DiscoveredEntry` (interface): interface DiscoveredEntry — A control auto-discovered by scanning an object/table, with its binding metadata and current reader.
90
91
  - `DiscoverySkip` (interface): interface DiscoverySkip { readonly path: string; readonly reason: string } — ⚠ undocumented
91
92
  - `FrameRecordSample` (interface): interface FrameRecordSample — Per-frame timing input recorded by callers: total frame time, sim time, and optional named phase durations.
92
93
  - `FrameStats` (interface): interface FrameStats — Aggregated frame-timing statistics over the recent sampling window, including per-phase breakdown.
93
- - `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — Frame duration in milliseconds above which a frame is recorded as a long-frame spike (~30fps budget).
94
+ - `FrameWindow` (interface): interface FrameWindow — Frame timings over the measured window, as the player sees them.
95
+ - `GlReport` (interface): interface GlReport — GPU-object creation counts. `total` is since the page loaded, the named fields since warmup.
96
+ - `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — The one perf record shape. `bun run perf` (CDP, out of process) and the in-page telemetry report (`window.__JG.report()`, the dev-server route, `bun run debug`) both produce exactly this object, so a budget check and a diff read either one.
94
97
  - `LatencyStats` (interface): interface LatencyStats — Aggregated latency statistics (last/avg/min/max) over recorded latency samples.
98
+ - `LogBusCounts` (interface): interface LogBusCounts — Lifetime totals per level, plus how many lines the ring dropped.
99
+ - `LogBusEntry` (interface): interface LogBusEntry — One captured line: when, how bad, which system said it, and what it said.
100
+ - `LogBusSnapshot` (interface): interface LogBusSnapshot — Read-out of the log bus: newest-window entries, the warning/error ring, totals, and evictions.
95
101
  - `LongFrameEvent` (interface): interface LongFrameEvent — Recorded spike event for a frame that exceeded the long-frame threshold, with culprit attribution.
96
102
  - `NormalizedColor` (interface): interface NormalizedColor { readonly hex: string; readonly rgb: string; readonly alpha: number; readonly hasAlpha: boolean } — ⚠ undocumented
97
103
  - `OVERRIDES_FORMAT_VERSION` (const): const OVERRIDES_FORMAT_VERSION: 1 — ⚠ undocumented
98
104
  - `OverrideApplyDiagnostic` (interface): interface OverrideApplyDiagnostic { readonly id: string; readonly reason: string } — ⚠ undocumented
99
105
  - `OverrideApplyResult` (interface): interface OverrideApplyResult — Outcome of applying a devtools overrides payload: counts of applied and skipped entries plus diagnostics.
106
+ - `PerfQuality` (type): type PerfQuality = "full" | "draft" — Graphics tier the record was measured at.
107
+ - `PerfReport` (interface): interface PerfReport — One measured window of one game: the record `bun run perf`, `window.__JG.report()` and `bun run debug` print.
100
108
  - `PhaseStats` (interface): interface PhaseStats — Timing summary for a single named profiling phase across the sampling window.
109
+ - `ProfileEntry` (interface): interface ProfileEntry — One sampled stack frame reduced to a readable key and its self time.
110
+ - `ProfileReport` (interface): interface ProfileReport — CPU profile summary; only the CDP harness can produce one.
111
+ - `RenderReport` (interface): interface RenderReport — Renderer-side counts for the measured window; `null` where the source could not read them.
101
112
  - `RenderSample` (interface): interface RenderSample — Snapshot of renderer resource counts (draw calls, triangles, geometries, textures) for a frame.
113
+ - `ReportLogLine` (interface): interface ReportLogLine — One log line as it appears in the report.
114
+ - `ReportLogs` (interface): interface ReportLogs — Bounded log read-out carried by the report: warn/error lines survive even a flood of info logs.
115
+ - `ReportTelemetry` (interface): interface ReportTelemetry — Who produced the report, and how much producing it cost.
102
116
  - `ScanFieldMeta` (interface): interface ScanFieldMeta extends TunableOptions { kind?: DevtoolsControlKind } — ⚠ undocumented
103
117
  - `ScanMeta` (type): type ScanMeta = Readonly<Record<string, ScanFieldMeta>> — ⚠ undocumented
118
+ - `SceneNodeSource` (type): type SceneNodeSource = "scene-graph" | "heap-object3d" | "unavailable" — Where the scene-node count came from: the live scene graph, a heap walk, or nowhere.
119
+ - `SystemProbe` (interface): interface SystemProbe — What one system reports about itself: totals it keeps anyway, named timings, and one state line.
120
+ - `SystemProbeReading` (interface): interface SystemProbeReading — One system's probe read out on demand; missing sections stay empty rather than absent.
104
121
  - `Tunable` (interface): interface Tunable<T> — Typed handle to a registered control, exposing its live value and subscribe/set/reset operations.
105
122
  - `TunableAccessor` (interface): interface TunableAccessor — Get/set accessor pair plus initial value used to bind a discovered field to a control.
106
123
  - `TunableChoice` (interface): interface TunableChoice<T = unknown> { readonly value: T; readonly label?: string } — ⚠ undocumented
@@ -139,9 +156,20 @@
139
156
  ## @jgengine/core/devtools/frame
140
157
 
141
158
  - `FrameModule` (interface): interface FrameModule — Frame subsystem exposing the frame-timing, profiling, and render-sample facades.
142
- - `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — Frame duration in milliseconds above which a frame is recorded as a long-frame spike (~30fps budget).
143
159
  - `createFrameModule` (const): const createFrameModule: (deps: { readProbes: () => Record<string, unknown>; }) => FrameModule — Create the frame subsystem that records frame/sim/phase timings, computes stats, and captures long-frame events.
144
160
 
161
+ ## @jgengine/core/devtools/logBus
162
+
163
+ - `LOG_BUS_CAPACITY` (const): const LOG_BUS_CAPACITY: 200 — Default measured window for the log ring; older lines are evicted, never grown.
164
+ - `LOG_BUS_MESSAGE_MAX` (const): const LOG_BUS_MESSAGE_MAX: 400 — Longest kept message; a longer one is cut once, at write time.
165
+ - `LOG_BUS_SEVERE_CAPACITY` (const): const LOG_BUS_SEVERE_CAPACITY: 64 — Warnings and errors also land in their own ring, so an error is never pushed out by info traffic.
166
+ - `LogBus` (interface): interface LogBus — A fixed-capacity ring of log lines tagged by owning system. `write` reuses a preallocated slot — no array growth, no splice, no per-write object — so a hot path can call it and only the message string (already built by the caller) costs an allocation.
167
+ - `LogBusCounts` (interface): interface LogBusCounts — Lifetime totals per level, plus how many lines the ring dropped.
168
+ - `LogBusEntry` (interface): interface LogBusEntry — One captured line: when, how bad, which system said it, and what it said.
169
+ - `LogBusOptions` (interface): interface LogBusOptions — Tunables for the log ring: capacity, the severity ring's capacity, and the clock.
170
+ - `LogBusSnapshot` (interface): interface LogBusSnapshot — Read-out of the log bus: newest-window entries, the warning/error ring, totals, and evictions.
171
+ - `createLogBus` (function): function createLogBus(options: LogBusOptions = {}): LogBus — Create the bounded log bus the devtools log facade writes through.
172
+
145
173
  ## @jgengine/core/devtools/logs
146
174
 
147
175
  - `LogsModule` (interface): interface LogsModule — Logs subsystem exposing the log-capture and latency-tracking facades.
@@ -153,6 +181,27 @@
153
181
  - `OverridesModule` (interface): interface OverridesModule — Overrides subsystem exposing the export/apply facade for persisting and restoring control values.
154
182
  - `createOverridesModule` (const): const createOverridesModule: (deps: { signal: ChangeSignal; controlRecords: Map<string, ControlRecord>; writeControl: (record: ControlRecord, raw: unknown) => boolean; discoveredRecords: Map<string, DiscoveredRecord>; enableDiscovered: (id: string) => void; }) => OverridesModule — Create the overrides subsystem that serializes changed control values and re-applies a saved overrides payload.
155
183
 
184
+ ## @jgengine/core/devtools/report
185
+
186
+ - `FrameWindow` (interface): interface FrameWindow — Frame timings over the measured window, as the player sees them.
187
+ - `GlReport` (interface): interface GlReport — GPU-object creation counts. `total` is since the page loaded, the named fields since warmup.
188
+ - `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — The one perf record shape. `bun run perf` (CDP, out of process) and the in-page telemetry report (`window.__JG.report()`, the dev-server route, `bun run debug`) both produce exactly this object, so a budget check and a diff read either one.
189
+ - `PerfQuality` (type): type PerfQuality = "full" | "draft" — Graphics tier the record was measured at.
190
+ - `PerfReport` (interface): interface PerfReport — One measured window of one game: the record `bun run perf`, `window.__JG.report()` and `bun run debug` print.
191
+ - `ProfileEntry` (interface): interface ProfileEntry — One sampled stack frame reduced to a readable key and its self time.
192
+ - `ProfileReport` (interface): interface ProfileReport — CPU profile summary; only the CDP harness can produce one.
193
+ - `RenderReport` (interface): interface RenderReport — Renderer-side counts for the measured window; `null` where the source could not read them.
194
+ - `ReportLogLine` (interface): interface ReportLogLine — One log line as it appears in the report.
195
+ - `ReportLogs` (interface): interface ReportLogs — Bounded log read-out carried by the report: warn/error lines survive even a flood of info logs.
196
+ - `ReportTelemetry` (interface): interface ReportTelemetry — Who produced the report, and how much producing it cost.
197
+ - `SceneNodeSource` (type): type SceneNodeSource = "scene-graph" | "heap-object3d" | "unavailable" — Where the scene-node count came from: the live scene graph, a heap walk, or nowhere.
198
+
199
+ ## @jgengine/core/devtools/reportEndpoint
200
+
201
+ - `REPORT_ENDPOINT_PATH` (const): const REPORT_ENDPOINT_PATH: "/__jgengine/report" — The dev-only route a running game pushes its in-page report to, and that `bun run debug <game>` reads back. One process: the page gathers, the dev server holds the newest record per game, the CLI fetches it. Nothing here ships in a production build — the Vite plugin mounts it under `apply: "serve"`.
202
+ - `ReportPushBody` (interface): interface ReportPushBody — Body the page POSTs: the game it measured and the record itself.
203
+ - `ReportRouteError` (interface): interface ReportRouteError — Body sent when there is nothing to return yet.
204
+
156
205
  ## @jgengine/core/devtools/rewriteTunables
157
206
 
158
207
  - `TunableDelta` (interface): interface TunableDelta — One changed tunable to write back into game source: which table, key path, and new value.
@@ -203,8 +252,10 @@
203
252
 
204
253
  - `Devtools` (interface): interface Devtools — Top-level devtools facade aggregating frame, profile, render, logs, latency, controls, discover, overrides, and probes.
205
254
  - `DevtoolsControl` (interface): interface DevtoolsControl — Registered tunable control with its resolved schema metadata and read/write/reset accessors.
206
- - `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, and formatted message.
255
+ - `DevtoolsLatency` (interface): interface DevtoolsLatency — Latency samples recorded by instrumented transport methods.
256
+ - `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, owning system tag, and formatted message.
207
257
  - `DevtoolsLogLevel` (type): type DevtoolsLogLevel = "log" | "info" | "warn" | "error" — Severity level for a captured devtools log entry.
258
+ - `DevtoolsLogs` (interface): interface DevtoolsLogs — Bounded log ring with per-system tags, a warning/error ring, and lifetime counts.
208
259
  - `DevtoolsSnapshot` (interface): interface DevtoolsSnapshot — Serializable point-in-time snapshot of devtools state: frame, render, latency, logs, controls, and discovered fields.
209
260
  - `DiscoveredEntry` (interface): interface DiscoveredEntry — A control auto-discovered by scanning an object/table, with its binding metadata and current reader.
210
261
  - `FrameRecordSample` (interface): interface FrameRecordSample — Per-frame timing input recorded by callers: total frame time, sim time, and optional named phase durations.
@@ -214,6 +265,8 @@
214
265
  - `OverrideApplyResult` (interface): interface OverrideApplyResult — Outcome of applying a devtools overrides payload: counts of applied and skipped entries plus diagnostics.
215
266
  - `PhaseStats` (interface): interface PhaseStats — Timing summary for a single named profiling phase across the sampling window.
216
267
  - `RenderSample` (interface): interface RenderSample — Snapshot of renderer resource counts (draw calls, triangles, geometries, textures) for a frame.
268
+ - `SystemProbe` (interface): interface SystemProbe — What one system reports about itself: totals it keeps anyway, named timings, and one state line.
269
+ - `SystemProbeReading` (interface): interface SystemProbeReading — One system's probe read out on demand; missing sections stay empty rather than absent.
217
270
  - `Tunable` (interface): interface Tunable<T> — Typed handle to a registered control, exposing its live value and subscribe/set/reset operations.
218
271
  - `TunableAccessor` (interface): interface TunableAccessor — Get/set accessor pair plus initial value used to bind a discovered field to a control.
219
272
 
@@ -232,7 +285,7 @@
232
285
 
233
286
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
234
287
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
235
- - `VERSION` (const): const VERSION: "0.25.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
288
+ - `VERSION` (const): const VERSION: "0.26.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
236
289
 
237
290
  ## @jgengine/core/runtime/adapter
238
291
 
@@ -48,6 +48,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
48
48
 
49
49
  - `defineSystem` (function) · `import { defineSystem } from "@jgengine/shell/gameKit"`
50
50
 
51
+ ## devtools-log-bus — a bounded, per-system-tagged log ring a system writes to and a report reads
52
+
53
+ - `LogBus` (interface) · `import { LogBus } from "@jgengine/core/devtools/logBus"`
54
+
51
55
  ## editor-catalogs — Export typed gameplay catalogs for in-editor tuning.
52
56
 
53
57
  - `EditorCatalogDefinition` (interface) · `import { EditorCatalogDefinition } from "@jgengine/shell/gameKit"`