@origingame/engine 0.4.0 → 0.5.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.
- package/dist/web/origin-engine.d.ts +8 -5
- package/dist/web/origin-engine.js +39 -19
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +6 -5
- package/dist/web-publish/origin-engine-lanes.js +49 -29
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +8 -5
- package/dist/web-publish/origin-engine.js +39 -19
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +6 -5
- package/dist/web-publish-2d/origin-engine-lanes.js +49 -29
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +8 -5
- package/dist/web-publish-2d/origin-engine.js +39 -19
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +6 -5
- package/knowledge/looks/catalogue.json +123 -2
- package/knowledge/package.json +1 -1
- package/knowledge/scaffold/origin-engine.json +2 -2
- package/knowledge/scaffold/template/package.json +1 -1
- package/knowledge/scaffold/template/project.ogt +1 -1
- package/knowledge/scripts/validate.ts +69 -6
- package/knowledge/skills/og-audio/SKILL.md +31 -5
- package/knowledge/skills/og-build/SKILL.md +44 -4
- package/knowledge/skills/og-core/SKILL.md +4 -3
- package/knowledge/skills/og-feel/SKILL.md +9 -1
- package/knowledge/skills/og-game/SKILL.md +199 -16
- package/knowledge/skills/og-platform-sdk/SKILL.md +6 -0
- package/knowledge/skills/og-taste/SKILL.md +7 -0
- package/knowledge/skills/og-ui/SKILL.md +24 -0
- package/knowledge/wiki/ai.md +20 -20
- package/knowledge/wiki/anim.md +526 -60
- package/knowledge/wiki/atmosphere.md +13 -13
- package/knowledge/wiki/audio.md +160 -20
- package/knowledge/wiki/camera.md +13 -13
- package/knowledge/wiki/commands.md +11 -11
- package/knowledge/wiki/debug.md +13 -13
- package/knowledge/wiki/game.md +8 -8
- package/knowledge/wiki/i18n.md +7 -7
- package/knowledge/wiki/index.md +25 -1
- package/knowledge/wiki/input.md +47 -47
- package/knowledge/wiki/load.md +76 -50
- package/knowledge/wiki/math.md +102 -102
- package/knowledge/wiki/nav.md +34 -34
- package/knowledge/wiki/net.md +10 -10
- package/knowledge/wiki/path.md +14 -14
- package/knowledge/wiki/physics.md +111 -100
- package/knowledge/wiki/platform.md +44 -44
- package/knowledge/wiki/probe.md +86 -86
- package/knowledge/wiki/reflect.md +17 -17
- package/knowledge/wiki/render.md +245 -176
- package/knowledge/wiki/rng.md +14 -14
- package/knowledge/wiki/shapes.md +26 -26
- package/knowledge/wiki/stream.md +15 -15
- package/knowledge/wiki/system.md +8 -8
- package/knowledge/wiki/systems.md +2 -2
- package/knowledge/wiki/terrain.md +19 -19
- package/knowledge/wiki/tilemap.md +13 -13
- package/knowledge/wiki/time.md +4 -4
- package/knowledge/wiki/timeline.md +14 -14
- package/knowledge/wiki/transform.md +8 -8
- package/knowledge/wiki/tween.md +17 -17
- package/knowledge/wiki/ui.md +243 -243
- package/knowledge/wiki/video.md +5 -5
- package/knowledge/wiki/world.md +36 -36
- package/package.json +4 -4
- package/schema/api.schema.json +639 -11
- package/shell/page.ts +4 -0
- package/src/generated/abi.ts +3 -0
- package/src/index.ts +11 -0
- package/src/node/device.ts +5 -2
- package/src/node/polish.ts +34 -15
- package/src/node/run.ts +23 -6
- package/src/runtime.ts +1 -0
- package/src/web/gl-state.ts +907 -0
- package/src/web/worker.ts +6 -0
- package/types/bindings.json +823 -349
- package/types/index.d.ts +650 -15
package/shell/page.ts
CHANGED
|
@@ -600,6 +600,10 @@ export async function play(): Promise<PlayHandle> {
|
|
|
600
600
|
pausedByPlayer = m.paused;
|
|
601
601
|
if (m.started && phase === "gate") start();
|
|
602
602
|
};
|
|
603
|
+
// The usual case with a real Player: it answered the shell's hello while the engine loaded,
|
|
604
|
+
// and the handler above was not installed yet (the first one only released og.platform).
|
|
605
|
+
// What that hello says applies now: its rooms, and a Play link's room through them.
|
|
606
|
+
if (!page && player.hosted) playerEvents.hello({ started: player.started, paused: player.paused, capabilities: player.capabilities });
|
|
603
607
|
playerEvents.start = () => {
|
|
604
608
|
byPlayer = true;
|
|
605
609
|
if (phase === "gate") start();
|
package/src/generated/abi.ts
CHANGED
|
@@ -397,6 +397,8 @@ export interface HostAbi<A, V> {
|
|
|
397
397
|
og_anim_humanoid(world: number, desc: string | undefined): string;
|
|
398
398
|
/** `og.anim.retarget` */
|
|
399
399
|
og_anim_retarget(world: number, desc: string | undefined): string;
|
|
400
|
+
/** `og.anim.mirror` */
|
|
401
|
+
og_anim_mirror(world: number, desc: string | undefined): string;
|
|
400
402
|
/** `og.anim.rig2dInfo` */
|
|
401
403
|
og_anim_rig2d_info(world: number, rig: number): string;
|
|
402
404
|
/** `og.anim.skeleton2dBoxes` */
|
|
@@ -978,6 +980,7 @@ export const SYMBOLS: readonly string[] = [
|
|
|
978
980
|
"og_anim_graph",
|
|
979
981
|
"og_anim_humanoid",
|
|
980
982
|
"og_anim_retarget",
|
|
983
|
+
"og_anim_mirror",
|
|
981
984
|
"og_anim_rig2d_info",
|
|
982
985
|
"og_anim_skeleton2d_boxes",
|
|
983
986
|
"og_tween_to",
|
package/src/index.ts
CHANGED
|
@@ -72,6 +72,7 @@ export const LineMode = ((value = "strip" as Api.LineMode, options) => spec("Lin
|
|
|
72
72
|
export const DofQuality = ((value = "low" as Api.DofQuality, options) => spec("DofQuality", value, options)) as typeof Api.DofQuality;
|
|
73
73
|
export const ReflectionProbeMode = ((value = "baked" as Api.ReflectionProbeMode, options) => spec("ReflectionProbeMode", value, options)) as typeof Api.ReflectionProbeMode;
|
|
74
74
|
export const SpriteBlend = ((value = "alpha" as Api.SpriteBlend, options) => spec("SpriteBlend", value, options)) as typeof Api.SpriteBlend;
|
|
75
|
+
export const SpriteFacing = ((value = "none" as Api.SpriteFacing, options) => spec("SpriteFacing", value, options)) as typeof Api.SpriteFacing;
|
|
75
76
|
export const StripBlend = ((value = "additive" as Api.StripBlend, options) => spec("StripBlend", value, options)) as typeof Api.StripBlend;
|
|
76
77
|
export const Palette = ((value = 0 as Api.Palette, options) => spec("Palette", value, options)) as typeof Api.Palette;
|
|
77
78
|
export const DitherPattern = ((value = "none" as Api.DitherPattern, options) => spec("DitherPattern", value, options)) as typeof Api.DitherPattern;
|
|
@@ -79,6 +80,11 @@ export const StyliseSpace = ((value = "screen" as Api.StyliseSpace, options) =>
|
|
|
79
80
|
export const TextureArray = ((value = 0 as Api.TextureArray, options) => spec("TextureArray", value, options)) as typeof Api.TextureArray;
|
|
80
81
|
export const Asset = ((value = 0 as Api.Asset, options) => spec("Asset", value, options)) as typeof Api.Asset;
|
|
81
82
|
export const Sound = ((value = 0 as Api.Sound, options) => spec("Sound", value, options)) as typeof Api.Sound;
|
|
83
|
+
export const Font = ((value = 0 as Api.Font, options) => spec("Font", value, options)) as typeof Api.Font;
|
|
84
|
+
export const Style = ((value = 0 as Api.Style, options) => spec("Style", value, options)) as typeof Api.Style;
|
|
85
|
+
export const Look = ((value = 0 as Api.Look, options) => spec("Look", value, options)) as typeof Api.Look;
|
|
86
|
+
export const Shader = ((value = 0 as Api.Shader, options) => spec("Shader", value, options)) as typeof Api.Shader;
|
|
87
|
+
export const Rig = ((value = 0 as Api.Rig, options) => spec("Rig", value, options)) as typeof Api.Rig;
|
|
82
88
|
export const SkeletonAsset = ((value = 0 as Api.SkeletonAsset, options) => spec("SkeletonAsset", value, options)) as typeof Api.SkeletonAsset;
|
|
83
89
|
export const ClipAsset = ((value = 0 as Api.ClipAsset, options) => spec("ClipAsset", value, options)) as typeof Api.ClipAsset;
|
|
84
90
|
export const GraphAsset = ((value = 0 as Api.GraphAsset, options) => spec("GraphAsset", value, options)) as typeof Api.GraphAsset;
|
|
@@ -99,6 +105,10 @@ export const BlendshapeSource = ((value = "component" as Api.BlendshapeSource, o
|
|
|
99
105
|
export const IkSolver = ((value = "twoBone" as Api.IkSolver, options) => spec("IkSolver", value, options)) as typeof Api.IkSolver;
|
|
100
106
|
export const IkSpace = ((value = "world" as Api.IkSpace, options) => spec("IkSpace", value, options)) as typeof Api.IkSpace;
|
|
101
107
|
export const AudioBus = ((value = "master" as Api.AudioBus, options) => spec("AudioBus", value, options)) as typeof Api.AudioBus;
|
|
108
|
+
export const PanningModel = ((value = "equalpower" as Api.PanningModel, options) => spec("PanningModel", value, options)) as typeof Api.PanningModel;
|
|
109
|
+
export const AudioResampler = ((value = "linear" as Api.AudioResampler, options) => spec("AudioResampler", value, options)) as typeof Api.AudioResampler;
|
|
110
|
+
export const SpringSpace = ((value = "world" as Api.SpringSpace, options) => spec("SpringSpace", value, options)) as typeof Api.SpringSpace;
|
|
111
|
+
export const SpringColliderShape = ((value = "sphere" as Api.SpringColliderShape, options) => spec("SpringColliderShape", value, options)) as typeof Api.SpringColliderShape;
|
|
102
112
|
export const Navigation = ((value = 0 as Api.Navigation, options) => spec("Navigation", value, options)) as typeof Api.Navigation;
|
|
103
113
|
export const Behaviour = ((value = 0 as Api.Behaviour, options) => spec("Behaviour", value, options)) as typeof Api.Behaviour;
|
|
104
114
|
|
|
@@ -147,6 +157,7 @@ export const SOUND_STOPPED: typeof Api.SOUND_STOPPED = 1;
|
|
|
147
157
|
export const SOUND_REMOVED: typeof Api.SOUND_REMOVED = 2;
|
|
148
158
|
export const SOUND_REPLACED: typeof Api.SOUND_REPLACED = 3;
|
|
149
159
|
export const SOUND_CULLED: typeof Api.SOUND_CULLED = 4;
|
|
160
|
+
export const SOUND_RETRIGGERED: typeof Api.SOUND_RETRIGGERED = 5;
|
|
150
161
|
export const NAV_IDLE: typeof Api.NAV_IDLE = 0;
|
|
151
162
|
export const NAV_MOVING: typeof Api.NAV_MOVING = 1;
|
|
152
163
|
export const NAV_ARRIVED: typeof Api.NAV_ARRIVED = 2;
|
package/src/node/device.ts
CHANGED
|
@@ -514,7 +514,7 @@ async function connectAndroid(wanted: string | null, port: number, scheme: strin
|
|
|
514
514
|
|
|
515
515
|
const ELEMENT = "element-6066-11e4-a52e-4f735466cecf";
|
|
516
516
|
const IPHONE_FIX =
|
|
517
|
-
"on the iPhone turn on Settings > Apps > Safari > Advanced > Web Inspector and Remote Automation; on the Mac run `safaridriver --enable` once and pair the phone in Xcode (Window > Devices and Simulators)";
|
|
517
|
+
"on the iPhone turn on Settings > Apps > Safari > Advanced > Web Inspector and Remote Automation; on the Mac turn on Safari > Develop > Allow Remote Automation, run `safaridriver --enable` once and pair the phone in Xcode (Window > Devices and Simulators)";
|
|
518
518
|
|
|
519
519
|
/** Safari on an iPhone over WebDriver (safaridriver). */
|
|
520
520
|
async function connectIphone(udid: string | null): Promise<Driver> {
|
|
@@ -568,7 +568,10 @@ async function connectIphone(udid: string | null): Promise<Driver> {
|
|
|
568
568
|
);
|
|
569
569
|
session = s.sessionId;
|
|
570
570
|
} catch (e) {
|
|
571
|
-
|
|
571
|
+
// The whole answer: safaridriver names the device it found and why it
|
|
572
|
+
// could not use it on the lines after the first.
|
|
573
|
+
const why = (e as Error).message.split("\n").map((l) => l.trim()).filter(Boolean).join("; ");
|
|
574
|
+
return refuse(`safaridriver could not open Safari on the iPhone (${why}): ${IPHONE_FIX}`);
|
|
572
575
|
}
|
|
573
576
|
await call("POST", `/session/${session}/timeouts`, { script: 120_000, pageLoad: 120_000 });
|
|
574
577
|
return {
|
package/src/node/polish.ts
CHANGED
|
@@ -611,7 +611,8 @@ interface PlaytestMeasures {
|
|
|
611
611
|
hud: { gameplayShots: number; worstShare: number; worstCentre: number };
|
|
612
612
|
ui: { screenChanges: number; withMotion: number; presses: number; pressesWithSound: number; motionTicks: number };
|
|
613
613
|
latency: { inputs: number; answered: number; medianTicks: number; maxTicks: number; baselineShare: number; sound: number };
|
|
614
|
-
|
|
614
|
+
/** Per signal, the moments that had it; `whole`, the moments with three of the four (hit stop, shake, floating text, a sound: og-feel §7's rule); `credit`, each moment's min(3, its signals) summed, what the score reads. */
|
|
615
|
+
damage: { moments: number; hitStop: number; shake: number; floating: number; sound: number; flash: number; particles: number; whole: number; credit: number; facts: string[] };
|
|
615
616
|
idle: { share: number };
|
|
616
617
|
particles: { peakSystems: number; bursts: number };
|
|
617
618
|
/** `rendered` false: no renderer (tier `none`), so render and frame time were not measured. */
|
|
@@ -757,22 +758,39 @@ function measure(rec: RunRecord, frameDir: string, frames: number, relTo: string
|
|
|
757
758
|
const tick = rec.facts[i]!.tick;
|
|
758
759
|
if (hit && (moments.length === 0 || tick - moments.at(-1)! > 3)) moments.push(tick);
|
|
759
760
|
}
|
|
760
|
-
const dmg = { moments: moments.length, hitStop: 0, shake: 0, floating: 0, sound: 0, flash: 0, particles: 0, facts: [...factKeys].slice(0, 6) };
|
|
761
|
+
const dmg = { moments: moments.length, hitStop: 0, shake: 0, floating: 0, sound: 0, flash: 0, particles: 0, whole: 0, credit: 0, facts: [...factKeys].slice(0, 6) };
|
|
762
|
+
// The four signals an impact is held to, in the order a finding names them: three of four make it whole.
|
|
763
|
+
const FOUR = ["hit stop", "shake", "floating text", "a sound"] as const;
|
|
764
|
+
const FIX: Record<(typeof FOUR)[number], string> = { "hit stop": "og-feel §3 (hit stop by impact weight)", shake: "og-feel §4 (trauma, not offsets)", "floating text": "og-feel §7 (floating text)", "a sound": "og-audio §1 (every event sounds; a hit's sound on its tick)" };
|
|
765
|
+
const lacking = new Map<(typeof FOUR)[number], number>(FOUR.map((k) => [k, 0]));
|
|
766
|
+
let short = 0;
|
|
761
767
|
for (const t of moments) {
|
|
762
768
|
const i = tickIndex.get(t) ?? -1;
|
|
763
|
-
|
|
764
|
-
const w = sig.slice(Math.max(1, i - 3), Math.min(s.length, i + 9));
|
|
765
|
-
const held = s.slice(Math.max(0, i - 3), Math.min(s.length, i + 9)).some((p) => p.hitStop > 0 || p.scale < 1);
|
|
766
|
-
|
|
767
|
-
if (
|
|
768
|
-
if (
|
|
769
|
-
if (
|
|
769
|
+
// A moment the record cannot place is short of all four.
|
|
770
|
+
const w = i < 0 ? [] : sig.slice(Math.max(1, i - 3), Math.min(s.length, i + 9));
|
|
771
|
+
const held = i >= 0 && s.slice(Math.max(0, i - 3), Math.min(s.length, i + 9)).some((p) => p.hitStop > 0 || p.scale < 1);
|
|
772
|
+
const has = { "hit stop": held || w.some((x) => x.hitStop), shake: w.some((x) => x.shake), "floating text": w.some((x) => x.floating), "a sound": w.some((x) => x.sfx) };
|
|
773
|
+
if (has["hit stop"]) dmg.hitStop++;
|
|
774
|
+
if (has.shake) dmg.shake++;
|
|
775
|
+
if (has["floating text"]) dmg.floating++;
|
|
776
|
+
if (has["a sound"]) dmg.sound++;
|
|
770
777
|
if (w.some((x) => x.tween)) dmg.flash++;
|
|
771
778
|
if (w.some((x) => x.particles)) dmg.particles++;
|
|
779
|
+
const n = FOUR.filter((k) => has[k]).length;
|
|
780
|
+
dmg.credit += Math.min(3, n);
|
|
781
|
+
if (n >= 3) {
|
|
782
|
+
dmg.whole++;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
// Short of three: each signal it lacks is owed. A whole moment owes none,
|
|
786
|
+
// so a party game's bump with hit stop, shake and a sound is not asked
|
|
787
|
+
// for a floating number (og-feel §7: damage and score numbers).
|
|
788
|
+
short++;
|
|
789
|
+
for (const k of FOUR) if (!has[k]) lacking.set(k, lacking.get(k)! + 1);
|
|
772
790
|
}
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
791
|
+
for (const k of FOUR) {
|
|
792
|
+
const n = lacking.get(k)!;
|
|
793
|
+
if (n > 0) findings.push({ measure: "hit feedback", where: rec.playtest, detail: `${n} of ${dmg.moments} damage moments (${dmg.facts.join(", ")}) had no ${k} within 8 ticks, and fewer than three of hit stop, shake, floating text and a sound (${short} short)`, fix: FIX[k] });
|
|
776
794
|
}
|
|
777
795
|
|
|
778
796
|
// Idle frames, particles, frame time.
|
|
@@ -844,7 +862,8 @@ function scores(ms: PlaytestMeasures[], missingKeys: number, hasLocales: boolean
|
|
|
844
862
|
const lat = ms.filter((m) => m.latency.inputs > 0);
|
|
845
863
|
const latency = lat.length === 0 ? null : Math.round(lat.reduce((n, m) => n + (m.latency.answered / m.latency.inputs) * (m.latency.medianTicks <= 1 ? 100 : m.latency.medianTicks <= 3 ? 70 : m.latency.medianTicks <= 6 ? 40 : 0), 0) / lat.length);
|
|
846
864
|
const dm = sum((m) => m.damage.moments);
|
|
847
|
-
|
|
865
|
+
// Per impact: each moment scores min(3, its signals) of 3, so three of four is 100 and no moment's lack hides behind another's signal.
|
|
866
|
+
const hitFeedback = dm === 0 ? null : Math.round((100 * sum((m) => m.damage.credit)) / (3 * dm));
|
|
848
867
|
const gameplay = ms.filter((m) => m.hud.gameplayShots > 0);
|
|
849
868
|
const hud = gameplay.length === 0 ? null : Math.min(...gameplay.map((m) => Math.min(band(m.hud.worstShare, 15, 45), band(m.hud.worstCentre, 5, 30))));
|
|
850
869
|
const timed = ms.filter((m) => m.frame.rendered);
|
|
@@ -894,7 +913,7 @@ function markdown(project: string, ms: PlaytestMeasures[], sc: Scores, findings:
|
|
|
894
913
|
L.push(`| Locale keys | ${show(sc.locale)} | string tables without missing keys (og cook) |`);
|
|
895
914
|
L.push(`| **Feel** | **${show(sc.feel)}** | mean of the feel rows |`);
|
|
896
915
|
L.push(`| Input latency | ${show(sc.latency)} | inputs answered, by the median ticks to the answer (0–1: 100) |`);
|
|
897
|
-
L.push(`| Hit feedback | ${show(sc.hitFeedback)} | damage
|
|
916
|
+
L.push(`| Hit feedback | ${show(sc.hitFeedback)} | each damage moment with three of hit stop, shake, floating text and a sound (every moment so: 100) |`);
|
|
898
917
|
L.push(`| Idle frames | ${show(sc.idle)} | ticks where nothing new is seen or heard (10 %: 100, 60 %: 0) |`);
|
|
899
918
|
L.push(`| Frame time | ${sc.frameTime === null && ms.length > 0 && ms.every((m) => !m.frame.rendered) ? "n/a (no renderer)" : show(sc.frameTime)} | p95 step + render (8.3 ms: 100, 33 ms: 0), worst playtest${ms.some((m) => !m.frame.rendered) ? "; no renderer: not measured" : ""} |`);
|
|
900
919
|
L.push(`| Sound on input | ${show(sc.inputSound)} | inputs answered by a sound within 6 ticks |`, "");
|
|
@@ -920,7 +939,7 @@ function markdown(project: string, ms: PlaytestMeasures[], sc: Scores, findings:
|
|
|
920
939
|
L.push(`| HUD | ${m.hud.gameplayShots} gameplay frame(s), up to ${m.hud.worstShare} % of the surface, ${m.hud.worstCentre} % of the centre |`);
|
|
921
940
|
L.push(`| UI | ${m.ui.screenChanges} screen change(s), ${m.ui.withMotion} with motion; ${m.ui.motionTicks} ticks of UI motion; ${m.ui.presses} press(es), ${m.ui.pressesWithSound} with a ui-bus sound |`);
|
|
922
941
|
L.push(`| Inputs | ${m.latency.inputs}, ${m.latency.answered} answered, median ${m.latency.medianTicks} ticks, max ${m.latency.maxTicks}; ${m.latency.sound} with a sound; ${m.latency.baselineShare} % of input-free ticks also change something |`);
|
|
923
|
-
L.push(`| Damage | ${m.damage.moments} moment(s)${m.damage.facts.length ? ` from ${m.damage.facts.map((f) => `\`${f}\``).join(", ")}` : ""}: hit stop ${m.damage.hitStop}, shake ${m.damage.shake}, floating text ${m.damage.floating}, sound ${m.damage.sound}, tween ${m.damage.flash}, particles ${m.damage.particles} |`);
|
|
942
|
+
L.push(`| Damage | ${m.damage.moments} moment(s)${m.damage.facts.length ? ` from ${m.damage.facts.map((f) => `\`${f}\``).join(", ")}` : ""}: hit stop ${m.damage.hitStop}, shake ${m.damage.shake}, floating text ${m.damage.floating}, sound ${m.damage.sound} (three of four: ${m.damage.whole}), tween ${m.damage.flash}, particles ${m.damage.particles} |`);
|
|
924
943
|
L.push(`| Idle | ${m.idle.share} % of ticks |`);
|
|
925
944
|
L.push(`| Particles | peak ${m.particles.peakSystems} system(s), ${m.particles.bursts} burst(s) |`);
|
|
926
945
|
if (m.frame.rendered) L.push(`| Frame (${m.frame.tier}) | step ${m.frame.stepMedianMs} ms median, ${m.frame.stepP95Ms} p95; render ${m.frame.renderMedianMs} median, ${m.frame.renderP95Ms} p95; frame p95 ${m.frame.frameP95Ms}, max ${m.frame.frameMaxMs} |`);
|
package/src/node/run.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { loadNativeHost, native } from "./index.ts";
|
|
|
44
44
|
import { openAudio, type AudioSink } from "./audio.ts";
|
|
45
45
|
import { nativePlatform } from "./platform.ts";
|
|
46
46
|
import { Trace, unixMs, type TraceDocument } from "../trace.ts";
|
|
47
|
-
import { beginBundle, boundHost, createEngine, fixedClock, type Engine, type GameToken } from "../runtime.ts";
|
|
47
|
+
import { beginBundle, boundHost, createEngine, fixedClock, type Engine, type GameToken, type Result } from "../runtime.ts";
|
|
48
48
|
|
|
49
49
|
const argv = process.argv.slice(2);
|
|
50
50
|
let entry = "";
|
|
@@ -298,7 +298,7 @@ if (!probing) {
|
|
|
298
298
|
if (!advanced.ok) fail(advanced.reason);
|
|
299
299
|
sink.pump();
|
|
300
300
|
} else {
|
|
301
|
-
let pendingReload: { mtimeMs: number } | null = null;
|
|
301
|
+
let pendingReload: { mtimeMs: number; importMs: number } | null = null;
|
|
302
302
|
// Imports in flight: a reload swaps only once the last one has declared
|
|
303
303
|
// everything (a second write of the bundle starts another import, whose
|
|
304
304
|
// beginBundle clears the declarations the first one made).
|
|
@@ -313,9 +313,10 @@ if (!probing) {
|
|
|
313
313
|
const mtimeMs = statSync(watchFile!).mtimeMs;
|
|
314
314
|
importing++;
|
|
315
315
|
try {
|
|
316
|
+
const started = performance.now();
|
|
316
317
|
beginBundle();
|
|
317
318
|
await importGame(watchFile!);
|
|
318
|
-
pendingReload = { mtimeMs };
|
|
319
|
+
pendingReload = { mtimeMs, importMs: performance.now() - started };
|
|
319
320
|
} catch (e) {
|
|
320
321
|
process.stderr.write(`WARN reload: the bundle failed to import (${e instanceof Error ? e.message : String(e)}); the old one keeps running\n`);
|
|
321
322
|
} finally {
|
|
@@ -328,13 +329,29 @@ if (!probing) {
|
|
|
328
329
|
let last = performance.now();
|
|
329
330
|
for (;;) {
|
|
330
331
|
if (pendingReload && importing === 0) {
|
|
331
|
-
const { mtimeMs } = pendingReload as { mtimeMs: number };
|
|
332
|
+
const { mtimeMs, importMs } = pendingReload as { mtimeMs: number; importMs: number };
|
|
332
333
|
pendingReload = null;
|
|
334
|
+
const restoring = performance.now();
|
|
333
335
|
const report = engine.probe.reload() as { ms: number; tick: number } | null;
|
|
336
|
+
const restored = performance.now();
|
|
334
337
|
const first = engine.frame(0);
|
|
335
|
-
const
|
|
338
|
+
const submitted = performance.now();
|
|
339
|
+
let rendered = false;
|
|
340
|
+
if (first.rendered) {
|
|
341
|
+
// Measure the completed first frame, without rendering and encoding
|
|
342
|
+
// an unused second frame through probe.capture.
|
|
343
|
+
const waited: Result<void> = JSON.parse(extras.og_node_wait_gpu(engine.id));
|
|
344
|
+
if (!waited.ok) fail(`reload's first frame: ${waited.reason}`);
|
|
345
|
+
rendered = true;
|
|
346
|
+
}
|
|
347
|
+
const timings = {
|
|
348
|
+
importMs,
|
|
349
|
+
reloadMs: restored - restoring,
|
|
350
|
+
frameMs: submitted - restored,
|
|
351
|
+
gpuWaitMs: performance.now() - submitted,
|
|
352
|
+
};
|
|
336
353
|
const latencyMs = Date.now() - mtimeMs;
|
|
337
|
-
process.stdout.write(JSON.stringify({ event: "reload", ok: report !== null, report, tick: first.tick,
|
|
354
|
+
process.stdout.write(JSON.stringify({ event: "reload", ok: report !== null && first.ok, report, tick: first.tick, rendered, timings, latencyMs: +latencyMs.toFixed(1) }) + "\n");
|
|
338
355
|
drain();
|
|
339
356
|
}
|
|
340
357
|
const elapsed = realtime ? performance.now() - last : tickMs;
|
package/src/runtime.ts
CHANGED
|
@@ -1470,6 +1470,7 @@ export const anim = Object.freeze({
|
|
|
1470
1470
|
graph: (world: WorldImpl, desc?: unknown) => parse(abi().og_anim_graph(world.id, json(desc))),
|
|
1471
1471
|
humanoid: (world: WorldImpl, desc?: unknown) => parse(abi().og_anim_humanoid(world.id, json(desc))),
|
|
1472
1472
|
retarget: (world: WorldImpl, desc?: unknown) => parse(abi().og_anim_retarget(world.id, json(desc))),
|
|
1473
|
+
mirror: (world: WorldImpl, desc?: unknown) => parse(abi().og_anim_mirror(world.id, json(desc))),
|
|
1473
1474
|
rig2dInfo: (world: WorldImpl, rig: number) => parse(abi().og_anim_rig2d_info(world.id, rig)),
|
|
1474
1475
|
skeleton2dBoxes: (world: WorldImpl, entity: number) => parse(abi().og_anim_skeleton2d_boxes(world.id, entity)),
|
|
1475
1476
|
// One lookup for a flipbook's or a 2D rig's clips (an index, -1 when
|