@genex-ai/cli-demo 0.70.0-dev.182 → 0.71.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 (37) hide show
  1. package/dist/index.js +8 -354
  2. package/package.json +1 -2
  3. package/templates/controllers/character/vrm/vrm-loader.ts +11 -74
  4. package/templates/skills/genex-ai-menu/SKILL.md +1 -7
  5. package/templates/skills/genex-ai-skybox/SKILL.md +4 -15
  6. package/templates/skills/genex-ai-texture/SKILL.md +1 -1
  7. package/templates/skills/genex-ai-video/SKILL.md +1 -1
  8. package/templates/skills/genex-explore/SKILL.md +1 -1
  9. package/templates/skills/genex-getting-started/SKILL.md +2 -2
  10. package/templates/skills/genex-threejs-bloom/SKILL.md +1 -4
  11. package/templates/skills/genex-threejs-bloom/references/bloom.md +1 -1
  12. package/templates/skills/genex-threejs-camera-direction/SKILL.md +8 -48
  13. package/templates/skills/genex-threejs-camera-direction/references/camera-rigs.md +0 -62
  14. package/templates/skills/genex-threejs-embed-auth/SKILL.md +1 -4
  15. package/templates/skills/genex-threejs-game-feel/SKILL.md +1 -4
  16. package/templates/skills/genex-threejs-game-ui/SKILL.md +8 -42
  17. package/templates/skills/genex-threejs-image-pipeline/SKILL.md +0 -5
  18. package/templates/skills/genex-threejs-image-pipeline/references/image-pipeline.md +1 -1
  19. package/templates/skills/genex-threejs-lighting-design/SKILL.md +1 -5
  20. package/templates/skills/genex-threejs-multiplayer/SKILL.md +1 -7
  21. package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +3 -6
  22. package/templates/skills/genex-threejs-physics-rapier/references/colliders-from-assets.md +0 -1
  23. package/templates/skills/genex-threejs-screen-space-ambient-occlusion/references/ambient-occlusion.md +1 -1
  24. package/templates/skills/genex-threejs-shadow-systems/SKILL.md +0 -6
  25. package/templates/skills/genex-threejs-shadow-systems/references/shadow-systems.md +1 -1
  26. package/templates/skills/genex-threejs-skill-router/SKILL.md +5 -26
  27. package/templates/skills/genex-threejs-skill-router/references/routing-map.md +7 -21
  28. package/templates/skills/genex-threejs-spectral-ocean/references/spectral-ocean.md +1 -1
  29. package/templates/skills/genex-threejs-touch-controls/SKILL.md +0 -11
  30. package/templates/skills/genex-threejs-visual-validation/SKILL.md +12 -29
  31. package/templates/skills/genex-threejs-water-optics/references/water-optics.md +1 -1
  32. package/templates/skills/genex-updates/SKILL.md +1 -1
  33. package/templates/controllers/quality/governor.ts +0 -147
  34. package/templates/controllers/quality/pick-asset.ts +0 -57
  35. package/templates/controllers/quality/tier.ts +0 -170
  36. package/templates/skills/genex-threejs-adaptive-quality/SKILL.md +0 -141
  37. package/templates/skills/genex-threejs-adaptive-quality/references/adaptive-quality.md +0 -105
@@ -75,10 +75,6 @@ Pick what matches the game; most games need exactly one or two of these.
75
75
  lives on the stick; the static circle is fine for slower games. On the
76
76
  bundled character controller, pass it through instead — see
77
77
  `$genex-threejs-character-controller` (`joystick: { x: joy.x, y: joy.y }`).
78
- Touch axes obey the same screen-direction contract as WASD and the mouse
79
- (`$genex-threejs-camera-direction`): stick-right must move the player
80
- screen-right, drag-right must turn the view right — a flipped feel is a sign
81
- bug in the mapping, never a device quirk.
82
78
  - **Camera look → drag zone on the right half.** Default `DragZone()` is
83
79
  exactly that; per frame `const { dx, dy } = look.consumeDelta()` then apply
84
80
  to yaw/pitch with the same sensitivity scale as the mouse path. Games on the
@@ -153,13 +149,6 @@ z-order stays: drag zone (5) under joystick/buttons (10) under pause menus.
153
149
  canvas. If the game also needs taps there (tap to shoot), read them from the
154
150
  zone (`onChange` + a small-movement threshold) or shrink the zone.
155
151
 
156
- ## Input is half of phone-playable
157
-
158
- This skill is INPUT only. A phone that can steer a game that then runs out of
159
- GPU memory still loses the player — the rendering half (device tiers, DPR and
160
- shadow budgets, the runtime governor, per-tier asset rungs) lives in
161
- `$genex-threejs-adaptive-quality` and is wired at boot for every game.
162
-
163
152
  ## When you skip
164
153
 
165
154
  Say it in one plain line — *"skipped touch controls: the game is
@@ -72,17 +72,9 @@ everything twice.
72
72
  skill's "Self-testing a draft" section) — so you see the game, not the
73
73
  sign-in gate.
74
74
  2. Press each documented control once (keys, pointer); assert a **visible
75
- response in its labeled direction** this is the input-direction pass, and
76
- it is part of THIS check, not an extra testing loop. Hold `KeyD`/ArrowRight
77
- ~0.5s and screenshot-diff: the controlled thing moves or turns toward
78
- screen-RIGHT (for a pan camera the viewport slides right — the terrain
79
- streams LEFT); `KeyA`/ArrowLeft mirrors it. Keyboard synthesis needs no
80
- pointer lock, so this works headless. For drag rigs, drag right AND drag up:
81
- both axes must follow the one stated convention (grab-the-world or
82
- move-the-camera — never one each). A response in the WRONG direction is a
83
- fail, not a note: two shipped games passed "controls respond" while D
84
- steered screen-left. For an animated character, capture idle, walk, run,
85
- crouch-idle, crouch-move, and jump. Inspect shoulders, elbows, wrists, and hands as well
75
+ response** to every onethe player moves, the camera turns, the button
76
+ fires. For an animated character, capture idle, walk, run, crouch-idle,
77
+ crouch-move, and jump. Inspect shoulders, elbows, wrists, and hands as well
86
78
  as the feet: a fully bound non-T-pose can still be stylistically broken.
87
79
  Reject shrugging palms-up poses, permanently raised elbows, or a gait whose
88
80
  upper-body style contradicts the requested character. “No T-pose” is not
@@ -112,19 +104,16 @@ everything twice.
112
104
  floating above detached wheels means the model was box-fit against the
113
105
  preset's wheelbase (the vehicle skill's "Custom generated bodies" rules
114
106
  fix it).
115
- 6. The cursor pass every game, per the lock-or-tool rule: during play the OS
116
- cursor is either the gameplay tool (cursor-core confirm the explicit
117
- `pointerLockAim: false` opt-out, cursor stays) or it is LOCKED away; a
118
- non-cursor-core game with the arrow parked over the action is a defect, even
119
- a keyboard-only racer. Aim games additionally: click the canvas and assert
120
- the pointer locks (cursor gone, mouse-RIGHT turns the view RIGHT and
121
- mouse-UP looks UP — name BOTH axes; yaw-only evidence has let inverted pitch
122
- ship); press Esc and assert the "click to aim/resume" cue appears. The cue
123
- should be the bundled `createAimCue` helper (or an equivalent
107
+ 6. Aim games get one extra pass (MANDATORY bucket — first-person or mouse-aimed):
108
+ state which pointer bucket the game chose; click the canvas and assert the
109
+ pointer locks (cursor gone, mouse-RIGHT turns the view RIGHT check the axis,
110
+ not just that it moves); press Esc and assert the "click to aim/resume" cue
111
+ appears. The cue should be the bundled `createAimCue` helper (or an equivalent
124
112
  `onAimChange`-driven overlay) — a MANDATORY-bucket game with no unlocked cue
125
- fails. Headless caveat: `requestPointerLock` throws in headless Chromium
126
- assert the wiring and the unlocked cue in a screenshot, and say plainly that
127
- the lock itself needs one manual click (do the both-axes look check there).
113
+ fails. Also confirm a cursor-core game opted OUT (`pointerLockAim: false`) so it
114
+ isn't grabbing the cursor. Headless caveat: `requestPointerLock` throws in
115
+ headless Chromium assert the wiring and the unlocked cue in a screenshot, and
116
+ say plainly that the lock itself needs one manual click.
128
117
  7. **Ask the scene the three things the screenshot cannot answer** (below). Run it
129
118
  once, in the same browser you already have open.
130
119
 
@@ -290,12 +279,6 @@ visual-system work — the sequence above.
290
279
 
291
280
  - a MANDATORY-bucket aim game never requests pointer lock, or locks with no
292
281
  visible unlocked cue;
293
- - a movement key or look axis whose on-screen direction contradicts its label
294
- (D turning the vehicle screen-left, mouse-up looking down with no invert
295
- option);
296
- - drag-pan axes that mix conventions (one axis grab-the-world, the other
297
- move-the-camera);
298
- - a non-cursor-core game that leaves the OS cursor visible during play;
299
282
  - approval relies on a single frame;
300
283
  - post-processing cannot be disabled per pass;
301
284
  - random seeds are not reproducible;
@@ -2,7 +2,7 @@
2
2
 
3
3
  Use this reference for bounded or analytic water with shared displacement and normals, derivative-filtered detail, analytic reflection, heuristic refraction, absorption, and crest foam. Use `$genex-threejs-spectral-ocean` for stochastic FFT seas.
4
4
 
5
- > **Renderer note:** this reference assumes `WebGPURenderer` + TSL node materials. Check the project's actual renderer first — the Genex scaffold ships vanilla WebGL three.js. On WebGL, adapt the technique with standard materials / `EffectComposer` passes or pick a simpler alternative; never switch renderers mid-project. Either way the effect obeys the device tier (`$genex-threejs-adaptive-quality`): expensive passes are desktop-tier, and on WebGPU the per-target MSAA sample count is a runtime knob the governor may drive.
5
+ > **Renderer note:** this reference assumes `WebGPURenderer` + TSL node materials. Check the project's actual renderer first — the Genex scaffold ships vanilla WebGL three.js. On WebGL, adapt the technique with standard materials / `EffectComposer` passes or pick a simpler alternative; never switch renderers mid-project.
6
6
 
7
7
  ## Contents
8
8
 
@@ -38,7 +38,7 @@ update, so update immediately.)
38
38
  Run exactly the command the nudge printed, from the game project root:
39
39
 
40
40
  ```bash
41
- npm i -D @genex-ai/cli-demo@dev # the genex CLI (a dev dependency)
41
+ npm i -D @genex-ai/cli-demo@latest # the genex CLI (a dev dependency)
42
42
  npm i @genex-ai/embed-sdk@latest # identity/saves SDK (ships inside the game)
43
43
  npm i @genex-ai/multiplayer@latest # multiplayer SDK (only if the game uses it)
44
44
  ```
@@ -1,147 +0,0 @@
1
- // Genex adaptive-quality: the runtime governor (mobile-readiness program).
2
- // Tiers pick the START; the governor owns the RUNTIME knobs forever — phones
3
- // thermally throttle after minutes, so a game that benched fine at 0:30
4
- // degrades at 8:00. Feed it a frame time every frame; wire the callbacks to
5
- // the runtime-changeable knobs only (DPR, post passes, draw distance, frame
6
- // cap — NEVER context-creation flags like antialias, those are fixed).
7
- //
8
- // Step-down ladder on sustained slowness: DPR ×0.8 → post off → draw distance
9
- // down → 30fps cap. Step-up is slow (hysteresis) and a step that failed twice
10
- // is never re-attempted. It also self-reports renderer.info counts to
11
- // window.__GENEX_QUALITY__ — the crash watchdog attaches them to its beacons,
12
- // which is how the field data that calibrates the whole program gets its
13
- // memory signal.
14
- import type { QualityTier } from './tier.ts';
15
-
16
- export interface GovernorCallbacks {
17
- /** Apply a DPR multiplier (1 = tier cap). E.g. renderer.setPixelRatio(Math.min(realDpr, tier.dprCap * m)). */
18
- setDprScale?: (multiplier: number) => void;
19
- /** Toggle the post stack between the tier's level and 'off'. */
20
- setPostEnabled?: (enabled: boolean) => void;
21
- /** Apply a draw-distance multiplier (1 = tier scale). */
22
- setDrawDistanceScale?: (multiplier: number) => void;
23
- /** Apply a frame cap (0 = uncapped). Pace to a STABLE 30 over a stuttery 45. */
24
- setFrameCap?: (fps: number) => void;
25
- }
26
-
27
- interface RendererInfoLike {
28
- info?: { memory?: { textures?: number; geometries?: number } };
29
- }
30
-
31
- const SLOW_WINDOW_MS = 4000; // sustained, not spikes — shader compiles must not trigger steps
32
- const RECOVER_WINDOW_MS = 20000; // step up slowly (hysteresis)
33
- const MEM_REPORT_MS = 5000;
34
-
35
- export class QualityGovernor {
36
- private tier: QualityTier;
37
- private steps: Array<{ apply: () => void; revert: () => void; failures: number; pendingRecovery?: boolean }>;
38
- private applied = 0;
39
- private slowSince: number | null = null;
40
- private goodSince: number | null = null;
41
- private lastMemReport = 0;
42
- private paused = false;
43
-
44
- constructor(tier: QualityTier, callbacks: GovernorCallbacks, renderer?: RendererInfoLike) {
45
- this.tier = tier;
46
- this.rendererRef = renderer;
47
- const c = callbacks;
48
- this.steps = [
49
- {
50
- apply: () => c.setDprScale?.(0.8),
51
- revert: () => c.setDprScale?.(1),
52
- failures: 0,
53
- },
54
- {
55
- apply: () => c.setPostEnabled?.(false),
56
- revert: () => c.setPostEnabled?.(true),
57
- failures: 0,
58
- },
59
- {
60
- apply: () => c.setDrawDistanceScale?.(0.6),
61
- revert: () => c.setDrawDistanceScale?.(1),
62
- failures: 0,
63
- },
64
- {
65
- apply: () => c.setFrameCap?.(30),
66
- revert: () => c.setFrameCap?.(this.tier.frameCap),
67
- failures: 0,
68
- },
69
- ];
70
- // Visibility lifecycle: a backgrounded game must stop burning GPU/audio on
71
- // exactly the thermally-constrained device class. The game's loop should
72
- // also pause itself; the governor at minimum stops judging frames.
73
- try {
74
- document.addEventListener('visibilitychange', () => {
75
- this.paused = document.visibilityState !== 'visible';
76
- this.slowSince = null;
77
- this.goodSince = null;
78
- });
79
- } catch {
80
- /* non-DOM context */
81
- }
82
- }
83
-
84
- private rendererRef?: RendererInfoLike;
85
-
86
- /** Call once per frame with the frame delta in ms (performance.now() based). */
87
- frame(deltaMs: number): void {
88
- if (this.paused) return;
89
- const now = performance.now();
90
- const budgetMs = 1000 / Math.min(this.tier.frameCap, 60) + 8; // ~24ms at 30, ~25ms at 60
91
-
92
- if (deltaMs > budgetMs) {
93
- this.goodSince = null;
94
- if (this.slowSince == null) this.slowSince = now;
95
- else if (now - this.slowSince > SLOW_WINDOW_MS) {
96
- this.stepDown();
97
- this.slowSince = null;
98
- }
99
- } else {
100
- this.slowSince = null;
101
- if (this.goodSince == null) this.goodSince = now;
102
- else if (now - this.goodSince > RECOVER_WINDOW_MS) {
103
- this.stepUp();
104
- this.goodSince = null;
105
- }
106
- }
107
-
108
- // Memory self-report for the crash watchdog's beacons (field calibration).
109
- if (now - this.lastMemReport > MEM_REPORT_MS) {
110
- this.lastMemReport = now;
111
- try {
112
- const mem = this.rendererRef?.info?.memory;
113
- if (mem) {
114
- (window as Window & { __GENEX_QUALITY__?: { tex?: number; geo?: number } }).__GENEX_QUALITY__ =
115
- { tex: mem.textures ?? 0, geo: mem.geometries ?? 0 };
116
- }
117
- } catch {
118
- /* observational only */
119
- }
120
- }
121
- }
122
-
123
- private stepDown(): void {
124
- if (this.applied >= this.steps.length) return;
125
- const step = this.steps[this.applied]!;
126
- // A re-application of a rung whose recovery is still pending means the
127
- // revert did NOT hold — that's the failure being counted, never the
128
- // successful revert itself.
129
- if (step.pendingRecovery) {
130
- step.pendingRecovery = false;
131
- step.failures++;
132
- }
133
- step.apply();
134
- this.applied++;
135
- }
136
-
137
- private stepUp(): void {
138
- if (this.applied === 0) return;
139
- const step = this.steps[this.applied - 1]!;
140
- // Never re-attempt a knob whose recovery failed twice (reverting it put
141
- // the game back over budget both times) — it stays applied for the session.
142
- if (step.failures >= 2) return;
143
- step.pendingRecovery = true;
144
- step.revert();
145
- this.applied--;
146
- }
147
- }
@@ -1,57 +0,0 @@
1
- // Genex adaptive-quality: per-tier asset variant selection (mobile-readiness
2
- // program, Option A+D). Generated assets ship with GUARANTEED downscale rungs
3
- // in R2 — sibling roles like "skybox-equirect@2048" — so phones can load a
4
- // ~11 MB sky instead of the 8192x4096 original (~178 MB decoded). Desktop
5
- // always loads the bare URL: the ladder exists so phones stop dying, never to
6
- // make desktops uglier.
7
- //
8
- // FALLBACK CONTRACT: a rung can be missing (remixed old URLs, an environment
9
- // whose backfill hasn't run). loadTextureWithFallback retries the bare URL on
10
- // a rung failure, so the worst case is today's behavior — never a broken boot.
11
- import type { QualityTier } from './tier.ts';
12
-
13
- const GENEX_GENERATIONS_RE = /^https:\/\/assets\.genex\.technology\/generations\/[^/]+\/[^/@]+$/;
14
-
15
- /** Rung widths by role family — mirrors the server's ladder (store.ts MOBILE_RUNGS). */
16
- function rungWidthFor(url: string, tier: QualityTier): number | null {
17
- if (tier.name !== 'phone' && tier.name !== 'phone-low') return null;
18
- const role = url.split('/').pop() ?? '';
19
- if (role === 'skybox-equirect') return tier.name === 'phone-low' ? 2048 : 4096;
20
- if (role === 'texture-basecolor' || role === 'image-main' || /^image-alt-\d+$/.test(role)) {
21
- return tier.name === 'phone-low' ? 1024 : 2048;
22
- }
23
- return null;
24
- }
25
-
26
- /**
27
- * Resolve the URL a THIS-tier device should load. Non-generated URLs and
28
- * desktop tiers pass through untouched; phone tiers get the rung sibling.
29
- */
30
- export function pickAsset(url: string, tier: QualityTier): string {
31
- if (!GENEX_GENERATIONS_RE.test(url)) return url;
32
- const width = rungWidthFor(url, tier);
33
- return width ? `${url}@${width}` : url;
34
- }
35
-
36
- /**
37
- * Load an image URL through the ladder with the bare-URL fallback. Use it for
38
- * generated skyboxes/textures instead of raw TextureLoader.loadAsync:
39
- *
40
- * const texture = await loadTextureWithFallback(
41
- * SKYBOX_URL, tier, (u) => new THREE.TextureLoader().loadAsync(u),
42
- * );
43
- */
44
- export async function loadTextureWithFallback<T>(
45
- url: string,
46
- tier: QualityTier,
47
- load: (resolvedUrl: string) => Promise<T>,
48
- ): Promise<T> {
49
- const picked = pickAsset(url, tier);
50
- if (picked === url) return load(url);
51
- try {
52
- return await load(picked);
53
- } catch {
54
- // Missing rung (old asset, un-backfilled env) — degrade to the original.
55
- return load(url);
56
- }
57
- }
@@ -1,170 +0,0 @@
1
- // Genex adaptive-quality: device-tier detection (mobile-readiness program).
2
- // Boot-conservative by design — on phones you can recover from ugly, you
3
- // cannot recover from a jetsam kill, and boot is exactly when iOS kills. The
4
- // governor (governor.ts) steps quality UP after smooth seconds, so a
5
- // too-cautious start costs moments of softness, never a crash.
6
- //
7
- // Detection honesty: no single signal is trustworthy. iOS masks the WebGL
8
- // renderer string to "Apple GPU" (screen+DPR+OS-version is the real Apple
9
- // signal); Android exposes detailed renderer strings (Adreno/Mali) worth a
10
- // small lookup; the runtime governor measures actual frame times and corrects
11
- // both directions. All heuristics here only pick the STARTING tier.
12
-
13
- export type TierName = 'phone-low' | 'phone' | 'desktop' | 'desktop-high';
14
-
15
- export interface QualityTier {
16
- name: TierName;
17
- /** Cap for renderer.setPixelRatio(Math.min(devicePixelRatio, dprCap)). */
18
- dprCap: number;
19
- /** WebGL context antialias (context-creation-FIXED — cannot change live). */
20
- antialias: boolean;
21
- /** Directional/spot shadow map size (0 = shadows off). */
22
- shadowMapSize: number;
23
- /** 'off' = tone mapping only; 'light' = +FXAA/vignette; 'full' = the named stack. */
24
- postLevel: 'off' | 'light' | 'full';
25
- /** Multiplier for particle counts / scatter density. */
26
- particleScale: number;
27
- /** Multiplier for draw distance / fog far. */
28
- drawDistanceScale: number;
29
- /** Frame target — phones pace to a STABLE 30 over a stuttery 45. */
30
- frameCap: number;
31
- /** Max remote players fully animated/drawn (multiplayer); rest billboard. */
32
- remoteAvatarCap: number;
33
- }
34
-
35
- export const TIERS: Record<TierName, QualityTier> = {
36
- 'phone-low': {
37
- name: 'phone-low',
38
- dprCap: 1,
39
- antialias: false,
40
- shadowMapSize: 512,
41
- postLevel: 'off',
42
- particleScale: 0.25,
43
- drawDistanceScale: 0.5,
44
- frameCap: 30,
45
- remoteAvatarCap: 4,
46
- },
47
- phone: {
48
- name: 'phone',
49
- dprCap: 1.5,
50
- antialias: false,
51
- shadowMapSize: 1024,
52
- postLevel: 'light',
53
- particleScale: 0.5,
54
- drawDistanceScale: 0.75,
55
- frameCap: 60,
56
- remoteAvatarCap: 8,
57
- },
58
- desktop: {
59
- name: 'desktop',
60
- dprCap: 2,
61
- antialias: true,
62
- shadowMapSize: 2048,
63
- postLevel: 'full',
64
- particleScale: 1,
65
- drawDistanceScale: 1,
66
- frameCap: 60,
67
- remoteAvatarCap: 64,
68
- },
69
- 'desktop-high': {
70
- name: 'desktop-high',
71
- dprCap: 2,
72
- antialias: true,
73
- shadowMapSize: 2048,
74
- postLevel: 'full',
75
- particleScale: 1,
76
- drawDistanceScale: 1,
77
- frameCap: 240,
78
- remoteAvatarCap: 64,
79
- },
80
- };
81
-
82
- /** Quality setting persisted PER DEVICE (localStorage) — quality is a property
83
- * of the phone, not the player, so it deliberately does not ride account
84
- * state. 'auto' = heuristics + governor. */
85
- const SETTING_KEY = 'genex:quality';
86
- export type QualitySetting = 'auto' | 'low' | 'medium' | 'high';
87
-
88
- export function getQualitySetting(): QualitySetting {
89
- try {
90
- const v = localStorage.getItem(SETTING_KEY);
91
- if (v === 'low' || v === 'medium' || v === 'high') return v;
92
- } catch {
93
- /* storage blocked */
94
- }
95
- return 'auto';
96
- }
97
-
98
- export function setQualitySetting(v: QualitySetting): void {
99
- try {
100
- localStorage.setItem(SETTING_KEY, v);
101
- } catch {
102
- /* storage blocked */
103
- }
104
- }
105
-
106
- function isTouchDevice(): boolean {
107
- try {
108
- const coarse = matchMedia('(pointer: coarse)').matches;
109
- const touch = navigator.maxTouchPoints > 1;
110
- const nav = navigator as Navigator & { userAgentData?: { mobile?: boolean } };
111
- const mobileUA = nav.userAgentData
112
- ? !!nav.userAgentData.mobile
113
- : /Mobi|Android/i.test(navigator.userAgent);
114
- return (touch && coarse) || mobileUA;
115
- } catch {
116
- return false;
117
- }
118
- }
119
-
120
- /** Android GPU model → strong-enough-for-'phone' (vs 'phone-low'). Tiny,
121
- * vendored on purpose (no detect-gpu dependency): renderer strings are only
122
- * meaningful on Android/desktop anyway, and the governor corrects mistakes. */
123
- const STRONG_ANDROID_GPU = /Adreno \(TM\) [67]\d\d|Adreno \(TM\) 8|Mali-G7[18]|Mali-G[89]\d|Immortalis|Xclipse/i;
124
-
125
- function androidGpuLooksStrong(): boolean {
126
- try {
127
- const canvas = document.createElement('canvas');
128
- const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
129
- if (!gl) return false;
130
- const info = gl.getExtension('WEBGL_debug_renderer_info');
131
- const renderer = info ? String(gl.getParameter(info.UNMASKED_RENDERER_WEBGL)) : '';
132
- const ext = gl.getExtension('WEBGL_lose_context');
133
- if (ext) ext.loseContext(); // free the probe context immediately
134
- return STRONG_ANDROID_GPU.test(renderer);
135
- } catch {
136
- return false;
137
- }
138
- }
139
-
140
- /**
141
- * Pick the STARTING tier. Manual setting wins; 'auto' uses heuristics:
142
- * - non-touch → desktop
143
- * - touch: old OS or small memory signals → phone-low; strong/new → phone
144
- * The kit may have clamped devicePixelRatio — prefer its pristine stash.
145
- */
146
- export function detectTier(): QualityTier {
147
- const setting = getQualitySetting();
148
- if (setting === 'low') return TIERS['phone-low'];
149
- if (setting === 'medium') return TIERS.phone;
150
- if (setting === 'high') return TIERS.desktop;
151
-
152
- if (!isTouchDevice()) return TIERS.desktop;
153
-
154
- const ua = navigator.userAgent;
155
- const kit = (window as Window & { __GENEX_KIT__?: { realDpr?: number } }).__GENEX_KIT__;
156
- const dpr = kit?.realDpr ?? window.devicePixelRatio;
157
- const isIOS = /iPhone|iPad|iPod/.test(ua) || (navigator.maxTouchPoints > 1 && /Mac/.test(ua));
158
-
159
- if (isIOS) {
160
- // iOS version floors device age (iOS 17 ⇒ XS/2018+; screen+DPR refine).
161
- const m = ua.match(/OS (\d+)_/);
162
- const major = m ? Number(m[1]) : 0;
163
- if (major >= 17) return TIERS.phone;
164
- return TIERS['phone-low'];
165
- }
166
- const am = ua.match(/Android (\d+)/);
167
- const androidMajor = am ? Number(am[1]) : 0;
168
- if (androidMajor >= 13 && (dpr >= 2.5 || androidGpuLooksStrong())) return TIERS.phone;
169
- return TIERS['phone-low'];
170
- }
@@ -1,141 +0,0 @@
1
- ---
2
- name: genex-threejs-adaptive-quality
3
- description: Make a Genex Three.js game phone-survivable with the vendored adaptive-quality kit — device-tier detection, tier-budgeted renderer settings, a runtime governor that steps quality down before phones run out of memory, per-tier asset rungs for generated skyboxes/textures, and a Quality picker in settings. Load for every game at boot wiring time, and whenever a game is heavy, crashes on phones, or gets flagged desktop-only.
4
- ---
5
-
6
- # Genex Three.js Adaptive Quality
7
-
8
- Phones enforce a hard GPU-memory ceiling desktops don't have: iOS silently
9
- kills the page when a game allocates too much, and the kill arrives at BOOT —
10
- exactly when a skybox, models, and the post stack all decode at once. This
11
- skill wires the vendored quality kit so the game boots conservatively on
12
- phones, steps quality UP when the device proves smooth, and never gets uglier
13
- on desktop. **You can recover from ugly; you cannot recover from a killed
14
- page.**
15
-
16
- This is a completion gate like the post stack: every game wires the tier at
17
- boot before it is called done. It costs three lines, not a testing burden —
18
- you still verify on desktop only.
19
-
20
- ## Install
21
-
22
- ```bash
23
- npx genex controller quality
24
- ```
25
-
26
- Installs `src/controllers/quality/{tier.ts, governor.ts, pick-asset.ts}` —
27
- game-owned code, edit freely.
28
-
29
- ## Wire the tier at boot (before renderer construction)
30
-
31
- ```ts
32
- import { detectTier } from "./controllers/quality/tier.ts";
33
- import { QualityGovernor } from "./controllers/quality/governor.ts";
34
-
35
- const tier = detectTier(); // phone-low | phone | desktop; manual Quality setting wins
36
- const renderer = new THREE.WebGLRenderer({ antialias: tier.antialias });
37
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, tier.dprCap));
38
- ```
39
-
40
- The tier owns every budget decision: `dprCap` (1.5 on phones — the single
41
- biggest framebuffer lever), `antialias` (off on phones; it is fixed at context
42
- creation and can never change live), `shadowMapSize` (1024 phone / 2048
43
- desktop), `postLevel` (`'light'` = tone mapping + cheap passes on phones;
44
- `'full'` = the named stack on desktop), `particleScale`, `drawDistanceScale`,
45
- `frameCap`, and `remoteAvatarCap` for multiplayer. The exact ladder and which
46
- knobs may change at runtime vs load time vs never: [references/adaptive-quality.md](references/adaptive-quality.md).
47
-
48
- ## Wire the governor (runtime — phones throttle over minutes)
49
-
50
- ```ts
51
- const governor = new QualityGovernor(tier, {
52
- setDprScale: (m) => renderer.setPixelRatio(Math.min(window.devicePixelRatio, tier.dprCap * m)),
53
- setPostEnabled: (on) => (composerEnabled = on),
54
- setDrawDistanceScale: (m) => (scene.fog!.far = baseFogFar * tier.drawDistanceScale * m),
55
- }, renderer);
56
-
57
- // In the render loop, with the same performance.now() delta the loop already computes:
58
- governor.frame(deltaMs);
59
- ```
60
-
61
- Sustained slow frames step down (DPR ×0.8 → post off → draw distance →
62
- 30 fps cap); twenty smooth seconds step back up; a knob that failed twice
63
- stays down for the session. It keeps governing forever — thermal throttling
64
- arrives at minute eight, not second thirty. It also pauses judgment when the
65
- tab is hidden and publishes memory counts the platform's crash telemetry
66
- reads; pause your own loop and audio on `visibilitychange` too.
67
-
68
- ## Generated assets: load through the rungs
69
-
70
- Generated skyboxes are 8192×4096 — about 178 MB decoded, over half a phone's
71
- whole budget in one texture. Every generated image asset ships with downscale
72
- rungs; phones must load through them:
73
-
74
- ```ts
75
- import { detectTier } from "./controllers/quality/tier.ts";
76
- import { loadTextureWithFallback } from "./controllers/quality/pick-asset.ts";
77
-
78
- const texture = await loadTextureWithFallback(SKYBOX_URL, tier, (u) =>
79
- new THREE.TextureLoader().loadAsync(u),
80
- );
81
- ```
82
-
83
- Desktop loads the original, phones the `@2048` rung (~11 MB), and a missing
84
- rung falls back to the original — never a broken boot. The `$genex-ai-skybox`
85
- and `$genex-ai-texture` skills show the wiring in place.
86
-
87
- ## Quality picker in settings
88
-
89
- The pause/settings screen (see `$genex-threejs-game-ui`) always carries a
90
- Quality entry: **Auto / Low / Medium / High**, wired to
91
- `setQualitySetting(...)` from `tier.ts` + a reload or re-tier. Persisted
92
- per-device in localStorage on purpose — quality is a property of the phone,
93
- not the player's account. Default Auto.
94
-
95
- ## Budgets that ride the tier (not separate rules)
96
-
97
- - Shadows: `tier.shadowMapSize`, `shadow.autoUpdate = false` for static scenes,
98
- at most 2 cascades on phones (`$genex-threejs-shadow-systems`).
99
- - Post: phone floor is a BUILT tone-mapping/output pass (`postLevel: 'light'`
100
- adds FXAA/vignette); SSAO, volumetrics, and DoF are desktop-tier only
101
- (`$genex-threejs-skill-router` owns the floor wording).
102
- - Particles/scatter: multiply counts by `tier.particleScale`; render heavy
103
- transparency at half resolution and upsample.
104
- - Animation: distant mixers update at 1/2–1/4 rate; multiplayer remotes above
105
- `tier.remoteAvatarCap` billboard instead of animating
106
- (`$genex-threejs-multiplayer`).
107
- - Physics: phone tiers prefer hull/cuboid colliders for props — trimesh only
108
- where gameplay demands it (`$genex-threejs-physics-rapier`).
109
- - Shaders: default `mediump` (iOS exception: float-texture sampling needs
110
- `highp sampler2D`); precompile with `renderer.compileAsync(scene, camera)`
111
- during the loader screen so first-frame jank doesn't read as a stall; skip
112
- max anisotropy on phones.
113
- - Disposal: level swaps traverse the outgoing scene and dispose geometry,
114
- materials, AND textures (three never frees them for you); watch
115
- `renderer.info.memory` while testing — if textures/geometries climb across
116
- swaps, you leak toward the kill.
117
-
118
- ## WebGPU games
119
-
120
- The scaffold ships WebGL and stays the default; if this project already uses
121
- `WebGPURenderer`, keep it (never switch renderers mid-project). Context loss
122
- differs: WebGL fires `webglcontextlost` events; WebGPU exposes a
123
- `device.lost` promise — attach a handler that pauses the loop and rebuilds,
124
- mirroring the scaffold's WebGL pattern. All tier knobs apply identically
125
- except `antialias` (WebGPU MSAA is per-render-target and CAN change at
126
- runtime).
127
-
128
- ## Failure conditions
129
-
130
- - Renderer constructed before `detectTier()` → context-creation knobs
131
- (antialias) are locked wrong for the session. Tier first, renderer second.
132
- - Governor wired to context-creation flags → no-op at best. Runtime knobs
133
- only: DPR, post toggles, distances, frame cap.
134
- - Skybox loaded with a bare `TextureLoader.loadAsync(SKYBOX_URL)` on a game
135
- that targets phones → ~178 MB decoded; route it through
136
- `loadTextureWithFallback`.
137
- - Quality stepping on every spike → shader compiles read as slowness. The
138
- governor requires SUSTAINED slow windows; do not shorten them.
139
- - Testing quality tiers by resizing the desktop window → tiers key off touch +
140
- OS, not viewport. Trust desktop verification plus the preflight report
141
- `genex preview` prints.