@aura3d/debug 1.0.6
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/README.md +17 -0
- package/dist/AnimationInspector.d.ts +25 -0
- package/dist/AnimationInspector.d.ts.map +1 -0
- package/dist/AnimationInspector.js +46 -0
- package/dist/AnimationInspector.js.map +1 -0
- package/dist/ChromeTraceExporter.d.ts +35 -0
- package/dist/ChromeTraceExporter.d.ts.map +1 -0
- package/dist/ChromeTraceExporter.js +110 -0
- package/dist/ChromeTraceExporter.js.map +1 -0
- package/dist/DebugLineCanvasRenderer.d.ts +24 -0
- package/dist/DebugLineCanvasRenderer.d.ts.map +1 -0
- package/dist/DebugLineCanvasRenderer.js +58 -0
- package/dist/DebugLineCanvasRenderer.js.map +1 -0
- package/dist/DebugOverlay.d.ts +18 -0
- package/dist/DebugOverlay.d.ts.map +1 -0
- package/dist/DebugOverlay.js +19 -0
- package/dist/DebugOverlay.js.map +1 -0
- package/dist/DrawCallTracker.d.ts +22 -0
- package/dist/DrawCallTracker.d.ts.map +1 -0
- package/dist/DrawCallTracker.js +28 -0
- package/dist/DrawCallTracker.js.map +1 -0
- package/dist/ECSInspector.d.ts +16 -0
- package/dist/ECSInspector.d.ts.map +1 -0
- package/dist/ECSInspector.js +12 -0
- package/dist/ECSInspector.js.map +1 -0
- package/dist/GPUProfiler.d.ts +22 -0
- package/dist/GPUProfiler.d.ts.map +1 -0
- package/dist/GPUProfiler.js +32 -0
- package/dist/GPUProfiler.js.map +1 -0
- package/dist/MaterialDiagnostics.d.ts +17 -0
- package/dist/MaterialDiagnostics.d.ts.map +1 -0
- package/dist/MaterialDiagnostics.js +43 -0
- package/dist/MaterialDiagnostics.js.map +1 -0
- package/dist/PhysicsDebugAdapter.d.ts +29 -0
- package/dist/PhysicsDebugAdapter.d.ts.map +1 -0
- package/dist/PhysicsDebugAdapter.js +57 -0
- package/dist/PhysicsDebugAdapter.js.map +1 -0
- package/dist/Profiler.d.ts +24 -0
- package/dist/Profiler.d.ts.map +1 -0
- package/dist/Profiler.js +56 -0
- package/dist/Profiler.js.map +1 -0
- package/dist/RenderStateInspector.d.ts +19 -0
- package/dist/RenderStateInspector.d.ts.map +1 -0
- package/dist/RenderStateInspector.js +33 -0
- package/dist/RenderStateInspector.js.map +1 -0
- package/dist/ReportExporter.d.ts +12 -0
- package/dist/ReportExporter.d.ts.map +1 -0
- package/dist/ReportExporter.js +27 -0
- package/dist/ReportExporter.js.map +1 -0
- package/dist/ResourceTracker.d.ts +23 -0
- package/dist/ResourceTracker.d.ts.map +1 -0
- package/dist/ResourceTracker.js +41 -0
- package/dist/ResourceTracker.js.map +1 -0
- package/dist/SceneHelpers.d.ts +46 -0
- package/dist/SceneHelpers.d.ts.map +1 -0
- package/dist/SceneHelpers.js +137 -0
- package/dist/SceneHelpers.js.map +1 -0
- package/dist/ShaderDiagnostics.d.ts +20 -0
- package/dist/ShaderDiagnostics.d.ts.map +1 -0
- package/dist/ShaderDiagnostics.js +49 -0
- package/dist/ShaderDiagnostics.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/package.json +18 -0
- package/src/AnimationInspector.ts +71 -0
- package/src/ChromeTraceExporter.ts +158 -0
- package/src/DebugLineCanvasRenderer.ts +96 -0
- package/src/DebugOverlay.ts +35 -0
- package/src/DrawCallTracker.ts +46 -0
- package/src/ECSInspector.ts +23 -0
- package/src/GPUProfiler.ts +48 -0
- package/src/MaterialDiagnostics.ts +59 -0
- package/src/PhysicsDebugAdapter.ts +86 -0
- package/src/Profiler.ts +77 -0
- package/src/RenderStateInspector.ts +45 -0
- package/src/ReportExporter.ts +32 -0
- package/src/ResourceTracker.ts +59 -0
- package/src/SceneHelpers.ts +188 -0
- package/src/ShaderDiagnostics.ts +64 -0
- package/src/index.ts +46 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { PhysicsDebugDraw, type DebugLine, type PhysicsWorld } from "@aura3d/physics";
|
|
2
|
+
|
|
3
|
+
export type PhysicsDebugSnapshot = {
|
|
4
|
+
readonly bodyCount: number;
|
|
5
|
+
readonly colliderCount: number;
|
|
6
|
+
readonly contactCount: number;
|
|
7
|
+
readonly lines: readonly DebugLine[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type PhysicsStackEvidence = {
|
|
11
|
+
readonly bodyCount: number;
|
|
12
|
+
readonly dynamicBodyCount: number;
|
|
13
|
+
readonly contactCount: number;
|
|
14
|
+
readonly lineCount: number;
|
|
15
|
+
readonly minY: number;
|
|
16
|
+
readonly maxY: number;
|
|
17
|
+
readonly settledCount: number;
|
|
18
|
+
readonly bodyPositions: readonly {
|
|
19
|
+
readonly id: number;
|
|
20
|
+
readonly type: string;
|
|
21
|
+
readonly position: readonly [number, number, number];
|
|
22
|
+
}[];
|
|
23
|
+
readonly stableHash: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class PhysicsDebugAdapter {
|
|
27
|
+
private readonly draw = new PhysicsDebugDraw();
|
|
28
|
+
|
|
29
|
+
buildLines(world: Pick<PhysicsWorld, "colliders" | "getBody">): readonly DebugLine[] {
|
|
30
|
+
return this.draw.buildLines(world);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
snapshot(world: Pick<PhysicsWorld, "colliders" | "bodies" | "getBody" | "snapshot">): PhysicsDebugSnapshot {
|
|
34
|
+
return {
|
|
35
|
+
bodyCount: world.bodies().length,
|
|
36
|
+
colliderCount: world.colliders().length,
|
|
37
|
+
contactCount: world.snapshot().contacts.length,
|
|
38
|
+
lines: this.buildLines(world)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
stackEvidence(world: Pick<PhysicsWorld, "colliders" | "bodies" | "getBody" | "snapshot">, velocityEpsilon = 0.05): PhysicsStackEvidence {
|
|
43
|
+
const snapshot = world.snapshot();
|
|
44
|
+
const lines = this.buildLines(world);
|
|
45
|
+
const bodyPositions = snapshot.bodies.map((body) => ({
|
|
46
|
+
id: body.id,
|
|
47
|
+
type: body.type,
|
|
48
|
+
position: roundVec3(body.position)
|
|
49
|
+
}));
|
|
50
|
+
const dynamicBodies = snapshot.bodies.filter((body) => body.type === "dynamic");
|
|
51
|
+
const settledCount = dynamicBodies.filter((body) => Math.hypot(body.velocity[0], body.velocity[1], body.velocity[2]) <= velocityEpsilon).length;
|
|
52
|
+
const ys = snapshot.bodies.map((body) => body.position[1]);
|
|
53
|
+
const evidenceWithoutHash = {
|
|
54
|
+
bodyCount: snapshot.stats.bodies,
|
|
55
|
+
dynamicBodyCount: dynamicBodies.length,
|
|
56
|
+
contactCount: snapshot.contacts.length,
|
|
57
|
+
lineCount: lines.length,
|
|
58
|
+
minY: roundNumber(ys.length > 0 ? Math.min(...ys) : 0),
|
|
59
|
+
maxY: roundNumber(ys.length > 0 ? Math.max(...ys) : 0),
|
|
60
|
+
settledCount,
|
|
61
|
+
bodyPositions
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
...evidenceWithoutHash,
|
|
65
|
+
stableHash: stableHash(evidenceWithoutHash)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function roundNumber(value: number, places = 6): number {
|
|
71
|
+
return Number(value.toFixed(places));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function roundVec3(value: readonly [number, number, number]): [number, number, number] {
|
|
75
|
+
return [roundNumber(value[0]), roundNumber(value[1]), roundNumber(value[2])];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function stableHash(value: unknown): string {
|
|
79
|
+
const input = JSON.stringify(value);
|
|
80
|
+
let hash = 2166136261;
|
|
81
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
82
|
+
hash ^= input.charCodeAt(index);
|
|
83
|
+
hash = Math.imul(hash, 16777619);
|
|
84
|
+
}
|
|
85
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
86
|
+
}
|
package/src/Profiler.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface ProfilerMarker {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly startMs: number;
|
|
4
|
+
readonly endMs: number;
|
|
5
|
+
readonly durationMs: number;
|
|
6
|
+
readonly depth: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ProfilerSnapshot {
|
|
10
|
+
readonly markerCount: number;
|
|
11
|
+
readonly totalDurationMs: number;
|
|
12
|
+
readonly markers: readonly ProfilerMarker[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ActiveMarker {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly startMs: number;
|
|
18
|
+
readonly depth: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Profiler {
|
|
22
|
+
private readonly active: ActiveMarker[] = [];
|
|
23
|
+
private readonly markers: ProfilerMarker[] = [];
|
|
24
|
+
|
|
25
|
+
constructor(private readonly now: () => number = () => performance.now()) {}
|
|
26
|
+
|
|
27
|
+
begin(name: string): void {
|
|
28
|
+
const trimmed = name.trim();
|
|
29
|
+
if (trimmed.length === 0) {
|
|
30
|
+
throw new Error("Profiler marker name is required");
|
|
31
|
+
}
|
|
32
|
+
this.active.push({ name: trimmed, startMs: this.now(), depth: this.active.length });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
end(name?: string): ProfilerMarker {
|
|
36
|
+
const active = this.active.pop();
|
|
37
|
+
if (!active) {
|
|
38
|
+
throw new Error("Cannot end profiler marker because no marker is active");
|
|
39
|
+
}
|
|
40
|
+
if (name !== undefined && active.name !== name) {
|
|
41
|
+
throw new Error(`Profiler marker mismatch: expected ${active.name}, received ${name}`);
|
|
42
|
+
}
|
|
43
|
+
const endMs = this.now();
|
|
44
|
+
const marker: ProfilerMarker = {
|
|
45
|
+
name: active.name,
|
|
46
|
+
startMs: active.startMs,
|
|
47
|
+
endMs,
|
|
48
|
+
durationMs: endMs - active.startMs,
|
|
49
|
+
depth: active.depth
|
|
50
|
+
};
|
|
51
|
+
this.markers.push(marker);
|
|
52
|
+
return marker;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
measure<T>(name: string, fn: () => T): T {
|
|
56
|
+
this.begin(name);
|
|
57
|
+
try {
|
|
58
|
+
return fn();
|
|
59
|
+
} finally {
|
|
60
|
+
this.end(name);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
snapshot(): ProfilerSnapshot {
|
|
65
|
+
const markers = [...this.markers];
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
markerCount: markers.length,
|
|
68
|
+
totalDurationMs: markers.reduce((total, marker) => total + marker.durationMs, 0),
|
|
69
|
+
markers
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
reset(): void {
|
|
74
|
+
this.active.length = 0;
|
|
75
|
+
this.markers.length = 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type RenderStateValue = string | number | boolean | null;
|
|
2
|
+
export type RenderStateSnapshot = ReadonlyMap<string, RenderStateValue>;
|
|
3
|
+
|
|
4
|
+
export interface RenderStateDiff {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
readonly before: RenderStateValue | undefined;
|
|
7
|
+
readonly after: RenderStateValue | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class RenderStateInspector {
|
|
11
|
+
capture(source: { captureState(): RenderStateSnapshot }): RenderStateSnapshot {
|
|
12
|
+
return new Map(source.captureState());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
diff(before: RenderStateSnapshot, after: RenderStateSnapshot): readonly RenderStateDiff[] {
|
|
16
|
+
const keys = new Set([...before.keys(), ...after.keys()]);
|
|
17
|
+
const diffs: RenderStateDiff[] = [];
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
const beforeValue = before.get(key);
|
|
20
|
+
const afterValue = after.get(key);
|
|
21
|
+
if (beforeValue !== afterValue) {
|
|
22
|
+
diffs.push({ key, before: beforeValue, after: afterValue });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return diffs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
assertNoLeak(before: RenderStateSnapshot, after: RenderStateSnapshot, ignoredKeys: readonly string[] = []): void {
|
|
29
|
+
const ignored = new Set(ignoredKeys);
|
|
30
|
+
const leaks = this.diff(before, after).filter((diff) => !ignored.has(diff.key));
|
|
31
|
+
if (leaks.length > 0) {
|
|
32
|
+
throw new RenderStateLeakError("Render state leak detected", leaks);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class RenderStateLeakError extends Error {
|
|
38
|
+
constructor(
|
|
39
|
+
message: string,
|
|
40
|
+
public readonly leaks: readonly RenderStateDiff[]
|
|
41
|
+
) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "RenderStateLeakError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface DebugReport {
|
|
2
|
+
readonly generatedAt: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly data: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class ReportExporter {
|
|
8
|
+
constructor(private readonly now: () => Date = () => new Date()) {}
|
|
9
|
+
|
|
10
|
+
create(name: string, data: unknown): DebugReport {
|
|
11
|
+
const trimmed = name.trim();
|
|
12
|
+
if (trimmed.length === 0) {
|
|
13
|
+
throw new Error("Report name is required");
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
generatedAt: this.now().toISOString(),
|
|
17
|
+
name: trimmed,
|
|
18
|
+
data
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toJson(report: DebugReport): string {
|
|
23
|
+
return `${JSON.stringify(report, sortKeys, 2)}\n`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sortKeys(_key: string, value: unknown): unknown {
|
|
28
|
+
if (value === null || Array.isArray(value) || typeof value !== "object") {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
return Object.fromEntries(Object.entries(value as Record<string, unknown>).sort(([left], [right]) => left.localeCompare(right)));
|
|
32
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface TrackedResource {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly type: string;
|
|
4
|
+
readonly disposed: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ResourceLeakReport {
|
|
8
|
+
readonly total: number;
|
|
9
|
+
readonly leaked: number;
|
|
10
|
+
readonly resources: readonly TrackedResource[];
|
|
11
|
+
readonly leaks: readonly TrackedResource[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ResourceTracker {
|
|
15
|
+
private readonly resources = new Map<string, TrackedResource>();
|
|
16
|
+
|
|
17
|
+
track(id: string, type: string): void {
|
|
18
|
+
if (this.resources.has(id)) {
|
|
19
|
+
throw new Error(`Resource already tracked: ${id}`);
|
|
20
|
+
}
|
|
21
|
+
this.resources.set(id, { id, type, disposed: false });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
dispose(id: string): void {
|
|
25
|
+
const resource = this.resources.get(id);
|
|
26
|
+
if (!resource) {
|
|
27
|
+
throw new Error(`Unknown resource: ${id}`);
|
|
28
|
+
}
|
|
29
|
+
this.resources.set(id, { ...resource, disposed: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
report(): ResourceLeakReport {
|
|
33
|
+
const resources = [...this.resources.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
34
|
+
const leaks = resources.filter((resource) => !resource.disposed);
|
|
35
|
+
return {
|
|
36
|
+
total: resources.length,
|
|
37
|
+
leaked: leaks.length,
|
|
38
|
+
resources,
|
|
39
|
+
leaks
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
assertNoLeaks(): void {
|
|
44
|
+
const report = this.report();
|
|
45
|
+
if (report.leaked > 0) {
|
|
46
|
+
throw new ResourceLeakError("Resource leaks detected", report);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class ResourceLeakError extends Error {
|
|
52
|
+
constructor(
|
|
53
|
+
message: string,
|
|
54
|
+
public readonly report: ResourceLeakReport
|
|
55
|
+
) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "ResourceLeakError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { DebugRenderLine } from "./DebugLineCanvasRenderer.js";
|
|
2
|
+
|
|
3
|
+
export type DebugVec3 = readonly [number, number, number];
|
|
4
|
+
export type DebugColor = readonly [number, number, number, number];
|
|
5
|
+
|
|
6
|
+
export interface AxesHelperOptions {
|
|
7
|
+
readonly size?: number;
|
|
8
|
+
readonly origin?: DebugVec3;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GridHelperOptions {
|
|
12
|
+
readonly size?: number;
|
|
13
|
+
readonly divisions?: number;
|
|
14
|
+
readonly y?: number;
|
|
15
|
+
readonly color?: DebugColor;
|
|
16
|
+
readonly centerColor?: DebugColor;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BoundsHelperOptions {
|
|
20
|
+
readonly min: DebugVec3;
|
|
21
|
+
readonly max: DebugVec3;
|
|
22
|
+
readonly color?: DebugColor;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CameraFrustumHelperOptions {
|
|
26
|
+
readonly nearHalfWidth: number;
|
|
27
|
+
readonly nearHalfHeight: number;
|
|
28
|
+
readonly farHalfWidth: number;
|
|
29
|
+
readonly farHalfHeight: number;
|
|
30
|
+
readonly nearZ?: number;
|
|
31
|
+
readonly farZ?: number;
|
|
32
|
+
readonly color?: DebugColor;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DirectionalLightHelperOptions {
|
|
36
|
+
readonly direction: DebugVec3;
|
|
37
|
+
readonly origin?: DebugVec3;
|
|
38
|
+
readonly length?: number;
|
|
39
|
+
readonly color?: DebugColor;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SkeletonHelperJoint {
|
|
43
|
+
readonly id: string;
|
|
44
|
+
readonly parentId?: string;
|
|
45
|
+
readonly position: DebugVec3;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const AXIS_X: DebugColor = [1, 0.18, 0.14, 1];
|
|
49
|
+
const AXIS_Y: DebugColor = [0.18, 0.86, 0.28, 1];
|
|
50
|
+
const AXIS_Z: DebugColor = [0.2, 0.48, 1, 1];
|
|
51
|
+
const DEFAULT_GRID: DebugColor = [0.38, 0.46, 0.58, 1];
|
|
52
|
+
const DEFAULT_CENTER: DebugColor = [0.72, 0.78, 0.86, 1];
|
|
53
|
+
const DEFAULT_HELPER: DebugColor = [1, 0.84, 0.28, 1];
|
|
54
|
+
|
|
55
|
+
export function buildAxesHelper(options: AxesHelperOptions = {}): readonly DebugRenderLine[] {
|
|
56
|
+
const size = positive(options.size ?? 1, "Axes helper size");
|
|
57
|
+
const origin = options.origin ?? [0, 0, 0];
|
|
58
|
+
return [
|
|
59
|
+
{ from: origin, to: [origin[0] + size, origin[1], origin[2]], color: AXIS_X },
|
|
60
|
+
{ from: origin, to: [origin[0], origin[1] + size, origin[2]], color: AXIS_Y },
|
|
61
|
+
{ from: origin, to: [origin[0], origin[1], origin[2] + size], color: AXIS_Z }
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildGridHelper(options: GridHelperOptions = {}): readonly DebugRenderLine[] {
|
|
66
|
+
const size = positive(options.size ?? 10, "Grid helper size");
|
|
67
|
+
const divisions = integerAtLeast(options.divisions ?? 10, 1, "Grid helper divisions");
|
|
68
|
+
const y = finite(options.y ?? 0, "Grid helper y");
|
|
69
|
+
const half = size / 2;
|
|
70
|
+
const step = size / divisions;
|
|
71
|
+
const lines: DebugRenderLine[] = [];
|
|
72
|
+
for (let index = 0; index <= divisions; index += 1) {
|
|
73
|
+
const offset = -half + index * step;
|
|
74
|
+
const isCenter = Math.abs(offset) < 1e-8;
|
|
75
|
+
const color = isCenter ? options.centerColor ?? DEFAULT_CENTER : options.color ?? DEFAULT_GRID;
|
|
76
|
+
lines.push({ from: [-half, y, offset], to: [half, y, offset], color });
|
|
77
|
+
lines.push({ from: [offset, y, -half], to: [offset, y, half], color });
|
|
78
|
+
}
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function buildBoundsHelper(options: BoundsHelperOptions): readonly DebugRenderLine[] {
|
|
83
|
+
validateVec3(options.min, "Bounds helper min");
|
|
84
|
+
validateVec3(options.max, "Bounds helper max");
|
|
85
|
+
const [minX, minY, minZ] = options.min;
|
|
86
|
+
const [maxX, maxY, maxZ] = options.max;
|
|
87
|
+
if (maxX < minX || maxY < minY || maxZ < minZ) {
|
|
88
|
+
throw new RangeError("Bounds helper max must be greater than or equal to min.");
|
|
89
|
+
}
|
|
90
|
+
const color = options.color ?? DEFAULT_HELPER;
|
|
91
|
+
const p: readonly DebugVec3[] = [
|
|
92
|
+
[minX, minY, minZ], [maxX, minY, minZ], [maxX, maxY, minZ], [minX, maxY, minZ],
|
|
93
|
+
[minX, minY, maxZ], [maxX, minY, maxZ], [maxX, maxY, maxZ], [minX, maxY, maxZ]
|
|
94
|
+
];
|
|
95
|
+
return edges([
|
|
96
|
+
[0, 1], [1, 2], [2, 3], [3, 0],
|
|
97
|
+
[4, 5], [5, 6], [6, 7], [7, 4],
|
|
98
|
+
[0, 4], [1, 5], [2, 6], [3, 7]
|
|
99
|
+
], p, color);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function buildCameraFrustumHelper(options: CameraFrustumHelperOptions): readonly DebugRenderLine[] {
|
|
103
|
+
const nearHalfWidth = positive(options.nearHalfWidth, "Camera frustum nearHalfWidth");
|
|
104
|
+
const nearHalfHeight = positive(options.nearHalfHeight, "Camera frustum nearHalfHeight");
|
|
105
|
+
const farHalfWidth = positive(options.farHalfWidth, "Camera frustum farHalfWidth");
|
|
106
|
+
const farHalfHeight = positive(options.farHalfHeight, "Camera frustum farHalfHeight");
|
|
107
|
+
const nearZ = finite(options.nearZ ?? -0.1, "Camera frustum nearZ");
|
|
108
|
+
const farZ = finite(options.farZ ?? -1, "Camera frustum farZ");
|
|
109
|
+
if (Math.abs(nearZ - farZ) < 1e-8) {
|
|
110
|
+
throw new RangeError("Camera frustum nearZ and farZ must be distinct.");
|
|
111
|
+
}
|
|
112
|
+
const p: readonly DebugVec3[] = [
|
|
113
|
+
[-nearHalfWidth, -nearHalfHeight, nearZ],
|
|
114
|
+
[nearHalfWidth, -nearHalfHeight, nearZ],
|
|
115
|
+
[nearHalfWidth, nearHalfHeight, nearZ],
|
|
116
|
+
[-nearHalfWidth, nearHalfHeight, nearZ],
|
|
117
|
+
[-farHalfWidth, -farHalfHeight, farZ],
|
|
118
|
+
[farHalfWidth, -farHalfHeight, farZ],
|
|
119
|
+
[farHalfWidth, farHalfHeight, farZ],
|
|
120
|
+
[-farHalfWidth, farHalfHeight, farZ]
|
|
121
|
+
];
|
|
122
|
+
return edges([
|
|
123
|
+
[0, 1], [1, 2], [2, 3], [3, 0],
|
|
124
|
+
[4, 5], [5, 6], [6, 7], [7, 4],
|
|
125
|
+
[0, 4], [1, 5], [2, 6], [3, 7]
|
|
126
|
+
], p, options.color ?? DEFAULT_HELPER);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function buildDirectionalLightHelper(options: DirectionalLightHelperOptions): readonly DebugRenderLine[] {
|
|
130
|
+
const origin = options.origin ?? [0, 0, 0];
|
|
131
|
+
const length = positive(options.length ?? 1, "Directional light helper length");
|
|
132
|
+
const direction = normalize(options.direction, "Directional light helper direction");
|
|
133
|
+
const to: DebugVec3 = [
|
|
134
|
+
origin[0] + direction[0] * length,
|
|
135
|
+
origin[1] + direction[1] * length,
|
|
136
|
+
origin[2] + direction[2] * length
|
|
137
|
+
];
|
|
138
|
+
return [{ from: origin, to, color: options.color ?? DEFAULT_HELPER }];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function buildSkeletonHelper(joints: readonly SkeletonHelperJoint[], color: DebugColor = DEFAULT_HELPER): readonly DebugRenderLine[] {
|
|
142
|
+
const byId = new Map<string, SkeletonHelperJoint>();
|
|
143
|
+
for (const joint of joints) {
|
|
144
|
+
if (!joint.id) throw new Error("Skeleton helper joints require stable ids.");
|
|
145
|
+
validateVec3(joint.position, `Skeleton helper joint ${joint.id}`);
|
|
146
|
+
byId.set(joint.id, joint);
|
|
147
|
+
}
|
|
148
|
+
const lines: DebugRenderLine[] = [];
|
|
149
|
+
for (const joint of joints) {
|
|
150
|
+
if (!joint.parentId) continue;
|
|
151
|
+
const parent = byId.get(joint.parentId);
|
|
152
|
+
if (!parent) continue;
|
|
153
|
+
lines.push({ from: parent.position, to: joint.position, color });
|
|
154
|
+
}
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function edges(indices: readonly (readonly [number, number])[], points: readonly DebugVec3[], color: DebugColor): readonly DebugRenderLine[] {
|
|
159
|
+
return indices.map(([from, to]) => ({ from: points[from]!, to: points[to]!, color }));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function positive(value: number, label: string): number {
|
|
163
|
+
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${label} must be finite and positive.`);
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function finite(value: number, label: string): number {
|
|
168
|
+
if (!Number.isFinite(value)) throw new RangeError(`${label} must be finite.`);
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function integerAtLeast(value: number, minimum: number, label: string): number {
|
|
173
|
+
if (!Number.isInteger(value) || value < minimum) throw new RangeError(`${label} must be an integer >= ${minimum}.`);
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function validateVec3(value: readonly number[], label: string): void {
|
|
178
|
+
if (value.length !== 3 || value.some((component) => !Number.isFinite(component))) {
|
|
179
|
+
throw new RangeError(`${label} must contain three finite values.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalize(value: DebugVec3, label: string): DebugVec3 {
|
|
184
|
+
validateVec3(value, label);
|
|
185
|
+
const length = Math.hypot(value[0], value[1], value[2]);
|
|
186
|
+
if (length <= 1e-8) throw new RangeError(`${label} must be non-zero.`);
|
|
187
|
+
return [value[0] / length, value[1] / length, value[2] / length];
|
|
188
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type RenderShaderProgram, type ShaderSources } from "@aura3d/rendering";
|
|
2
|
+
|
|
3
|
+
export interface ShaderDiagnosticReport {
|
|
4
|
+
readonly label: string;
|
|
5
|
+
readonly marker: string;
|
|
6
|
+
readonly markerPresentInVertex: boolean;
|
|
7
|
+
readonly markerPresentInFragment: boolean;
|
|
8
|
+
readonly attributes: readonly string[];
|
|
9
|
+
readonly uniforms: readonly string[];
|
|
10
|
+
readonly compileLog: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ShaderDiagnostics {
|
|
14
|
+
inspectSources(sources: ShaderSources, compileLog: string | null = null): ShaderDiagnosticReport {
|
|
15
|
+
return {
|
|
16
|
+
label: sources.label,
|
|
17
|
+
marker: sources.marker,
|
|
18
|
+
markerPresentInVertex: sources.vertex.includes(sources.marker),
|
|
19
|
+
markerPresentInFragment: sources.fragment.includes(sources.marker),
|
|
20
|
+
attributes: extractNames(/\b(?:in|attribute)\s+\w+\s+(\w+)\s*;/g, `${sources.vertex}\n${sources.fragment}`),
|
|
21
|
+
uniforms: extractNames(/\buniform\s+\w+\s+(\w+)(?:\s*\[[^\]]+\])?\s*;/g, `${sources.vertex}\n${sources.fragment}`),
|
|
22
|
+
compileLog
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
inspectProgram(program: RenderShaderProgram, compileLog: string | null = null): ShaderDiagnosticReport {
|
|
27
|
+
return {
|
|
28
|
+
label: program.label,
|
|
29
|
+
marker: program.marker,
|
|
30
|
+
markerPresentInVertex: true,
|
|
31
|
+
markerPresentInFragment: true,
|
|
32
|
+
attributes: [...program.reflection.attributes.keys()],
|
|
33
|
+
uniforms: [...program.reflection.uniforms],
|
|
34
|
+
compileLog
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
assertMarker(report: ShaderDiagnosticReport): void {
|
|
39
|
+
if (!report.markerPresentInVertex || !report.markerPresentInFragment) {
|
|
40
|
+
throw new ShaderDiagnosticError("Shader marker mismatch", report);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class ShaderDiagnosticError extends Error {
|
|
46
|
+
constructor(
|
|
47
|
+
message: string,
|
|
48
|
+
public readonly report: ShaderDiagnosticReport
|
|
49
|
+
) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "ShaderDiagnosticError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractNames(pattern: RegExp, source: string): readonly string[] {
|
|
56
|
+
const names = new Set<string>();
|
|
57
|
+
let match: RegExpExecArray | null;
|
|
58
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
59
|
+
if (match[1]) {
|
|
60
|
+
names.add(match[1]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [...names];
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export { DrawCallTracker } from "./DrawCallTracker.js";
|
|
2
|
+
export type { DrawCallRecord, DrawCallSnapshot } from "./DrawCallTracker.js";
|
|
3
|
+
export { RenderStateInspector, RenderStateLeakError } from "./RenderStateInspector.js";
|
|
4
|
+
export type { RenderStateDiff, RenderStateSnapshot, RenderStateValue } from "./RenderStateInspector.js";
|
|
5
|
+
export { ShaderDiagnosticError, ShaderDiagnostics } from "./ShaderDiagnostics.js";
|
|
6
|
+
export type { ShaderDiagnosticReport } from "./ShaderDiagnostics.js";
|
|
7
|
+
export { MaterialDiagnosticError, MaterialDiagnostics } from "./MaterialDiagnostics.js";
|
|
8
|
+
export type { MaterialDiagnosticReport } from "./MaterialDiagnostics.js";
|
|
9
|
+
export { PhysicsDebugAdapter } from "./PhysicsDebugAdapter.js";
|
|
10
|
+
export type { PhysicsDebugSnapshot, PhysicsStackEvidence } from "./PhysicsDebugAdapter.js";
|
|
11
|
+
export { AnimationInspector } from "./AnimationInspector.js";
|
|
12
|
+
export type { AnimationDebugSnapshot, AnimationVisualEvidence, SkeletonDebugSnapshot } from "./AnimationInspector.js";
|
|
13
|
+
export { Profiler } from "./Profiler.js";
|
|
14
|
+
export type { ProfilerMarker, ProfilerSnapshot } from "./Profiler.js";
|
|
15
|
+
export { GPUProfiler } from "./GPUProfiler.js";
|
|
16
|
+
export type { GPUProfilerSnapshot, GPUSample, GPUProfilerTimer } from "./GPUProfiler.js";
|
|
17
|
+
export { ChromeTraceExporter } from "./ChromeTraceExporter.js";
|
|
18
|
+
export type { ChromeTrace, ChromeTraceEvent, ChromeTraceExportOptions } from "./ChromeTraceExporter.js";
|
|
19
|
+
export { ResourceLeakError, ResourceTracker } from "./ResourceTracker.js";
|
|
20
|
+
export type { ResourceLeakReport, TrackedResource } from "./ResourceTracker.js";
|
|
21
|
+
export { ECSInspector } from "./ECSInspector.js";
|
|
22
|
+
export type { ECSInspectorSnapshot, ECSWorldLike } from "./ECSInspector.js";
|
|
23
|
+
export { DebugOverlay } from "./DebugOverlay.js";
|
|
24
|
+
export type { DebugOverlayRow, DebugOverlaySection, DebugOverlaySnapshot } from "./DebugOverlay.js";
|
|
25
|
+
export { DebugLineCanvasRenderer } from "./DebugLineCanvasRenderer.js";
|
|
26
|
+
export type { DebugLineCanvasRendererOptions, DebugLineCanvasRenderResult, DebugRenderLine } from "./DebugLineCanvasRenderer.js";
|
|
27
|
+
export {
|
|
28
|
+
buildAxesHelper,
|
|
29
|
+
buildBoundsHelper,
|
|
30
|
+
buildCameraFrustumHelper,
|
|
31
|
+
buildDirectionalLightHelper,
|
|
32
|
+
buildGridHelper,
|
|
33
|
+
buildSkeletonHelper
|
|
34
|
+
} from "./SceneHelpers.js";
|
|
35
|
+
export type {
|
|
36
|
+
AxesHelperOptions,
|
|
37
|
+
BoundsHelperOptions,
|
|
38
|
+
CameraFrustumHelperOptions,
|
|
39
|
+
DebugColor,
|
|
40
|
+
DebugVec3,
|
|
41
|
+
DirectionalLightHelperOptions,
|
|
42
|
+
GridHelperOptions,
|
|
43
|
+
SkeletonHelperJoint
|
|
44
|
+
} from "./SceneHelpers.js";
|
|
45
|
+
export { ReportExporter } from "./ReportExporter.js";
|
|
46
|
+
export type { DebugReport } from "./ReportExporter.js";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noImplicitAny": true,
|
|
9
|
+
"strictNullChecks": true,
|
|
10
|
+
"noImplicitReturns": true,
|
|
11
|
+
"noFallthroughCasesInSwitch": true,
|
|
12
|
+
"useUnknownInCatchVariables": true,
|
|
13
|
+
"exactOptionalPropertyTypes": false,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"noEmit": true,
|
|
18
|
+
"paths": {
|
|
19
|
+
"@aura3d/rendering": ["../rendering/src/index.ts"],
|
|
20
|
+
"@aura3d/scene": ["../scene/src/index.ts"],
|
|
21
|
+
"@aura3d/math": ["../math/src/index.ts"],
|
|
22
|
+
"@aura3d/core": ["../core/src/index.ts"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"src/DrawCallTracker.ts",
|
|
27
|
+
"src/RenderStateInspector.ts",
|
|
28
|
+
"src/ShaderDiagnostics.ts",
|
|
29
|
+
"src/MaterialDiagnostics.ts",
|
|
30
|
+
"src/index.ts"
|
|
31
|
+
]
|
|
32
|
+
}
|