@clypra/runtime 1.0.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/.releaserc.json +16 -0
- package/package.json +47 -0
- package/src/__tests__/integration.test.ts +345 -0
- package/src/graph/__tests__/builder.test.ts +204 -0
- package/src/graph/__tests__/validator.test.ts +381 -0
- package/src/graph/builder.ts +263 -0
- package/src/graph/index.ts +14 -0
- package/src/graph/types.ts +176 -0
- package/src/graph/validator.ts +208 -0
- package/src/index.ts +28 -0
- package/src/pixi/filters.ts +98 -0
- package/src/pixi/index.ts +11 -0
- package/src/pixi/renderer.ts +375 -0
- package/src/pixi/texture-pool.ts +159 -0
- package/src/pixi/types.ts +58 -0
- package/src/planner/index.ts +10 -0
- package/src/planner/optimizer.ts +247 -0
- package/src/planner/planner.ts +201 -0
- package/src/planner/types.ts +56 -0
- package/src/resources/cache.ts +166 -0
- package/src/resources/index.ts +9 -0
- package/src/resources/manager.ts +184 -0
- package/src/resources/types.ts +29 -0
- package/src/testing/benchmarkRunner.ts +399 -0
- package/src/testing/goldenTests.ts +390 -0
- package/src/validation/effectValidator.ts +571 -0
- package/src/validation/index.ts +9 -0
- package/src/validation/resource-validator.ts +173 -0
- package/src/validation/shader-validator.ts +154 -0
- package/src/validation/types.ts +31 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +18 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clypra/runtime — Frame Graph Optimizer
|
|
3
|
+
*
|
|
4
|
+
* Optimizes frame graphs for better performance.
|
|
5
|
+
* - Merges redundant passes
|
|
6
|
+
* - Reorders passes for better cache locality
|
|
7
|
+
* - Reduces transient resource usage
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FrameGraph, RenderPass, ResourceRequest } from "./types";
|
|
11
|
+
|
|
12
|
+
export interface OptimizationResult {
|
|
13
|
+
optimized: FrameGraph;
|
|
14
|
+
stats: {
|
|
15
|
+
passesRemoved: number;
|
|
16
|
+
resourcesReduced: number;
|
|
17
|
+
estimatedSavingsMs: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* FrameGraphOptimizer - Optimizes frame graphs
|
|
23
|
+
*/
|
|
24
|
+
export class FrameGraphOptimizer {
|
|
25
|
+
/**
|
|
26
|
+
* Optimize a frame graph
|
|
27
|
+
*/
|
|
28
|
+
optimize(frameGraph: FrameGraph): OptimizationResult {
|
|
29
|
+
let optimized = frameGraph;
|
|
30
|
+
let passesRemoved = 0;
|
|
31
|
+
let resourcesReduced = 0;
|
|
32
|
+
|
|
33
|
+
// Merge redundant passes
|
|
34
|
+
const { passes: mergedPasses, removed: passRemoved } = this.mergeRedundantPasses(optimized.passes);
|
|
35
|
+
passesRemoved += passRemoved;
|
|
36
|
+
optimized = { ...optimized, passes: mergedPasses };
|
|
37
|
+
|
|
38
|
+
// Reduce transient resources
|
|
39
|
+
const { resources: reducedResources, reduced: resourceReduced } = this.reduceTransientResources(optimized.resourceRequests, optimized.passes);
|
|
40
|
+
resourcesReduced += resourceReduced;
|
|
41
|
+
optimized = { ...optimized, resourceRequests: reducedResources };
|
|
42
|
+
|
|
43
|
+
// Reorder passes for cache locality
|
|
44
|
+
const reorderedPasses = this.reorderPasses(optimized.passes);
|
|
45
|
+
optimized = { ...optimized, passes: reorderedPasses };
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
optimized,
|
|
49
|
+
stats: {
|
|
50
|
+
passesRemoved,
|
|
51
|
+
resourcesReduced,
|
|
52
|
+
estimatedSavingsMs: this.estimateSavings(passesRemoved, resourcesReduced),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Merge redundant passes
|
|
59
|
+
*/
|
|
60
|
+
private mergeRedundantPasses(passes: readonly RenderPass[]): {
|
|
61
|
+
passes: readonly RenderPass[];
|
|
62
|
+
removed: number;
|
|
63
|
+
} {
|
|
64
|
+
const merged: RenderPass[] = [];
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
let removed = 0;
|
|
67
|
+
|
|
68
|
+
for (const pass of passes) {
|
|
69
|
+
// Create a key based on shader and inputs
|
|
70
|
+
const key = `${pass.shaderId}:${pass.inputs.join(",")}`;
|
|
71
|
+
|
|
72
|
+
if (seen.has(key)) {
|
|
73
|
+
removed++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
seen.add(key);
|
|
78
|
+
merged.push(pass);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { passes: merged, removed };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reduce transient resource allocations
|
|
86
|
+
*/
|
|
87
|
+
private reduceTransientResources(
|
|
88
|
+
resources: readonly ResourceRequest[],
|
|
89
|
+
passes: readonly RenderPass[],
|
|
90
|
+
): {
|
|
91
|
+
resources: readonly ResourceRequest[];
|
|
92
|
+
reduced: number;
|
|
93
|
+
} {
|
|
94
|
+
const reduced: ResourceRequest[] = [];
|
|
95
|
+
const transient: ResourceRequest[] = [];
|
|
96
|
+
let reducedCount = 0;
|
|
97
|
+
|
|
98
|
+
// Separate transient from persistent
|
|
99
|
+
for (const resource of resources) {
|
|
100
|
+
if (resource.transient) {
|
|
101
|
+
transient.push(resource);
|
|
102
|
+
} else {
|
|
103
|
+
reduced.push(resource);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build resource usage map
|
|
108
|
+
const usage = this.buildResourceUsageMap(transient, passes);
|
|
109
|
+
|
|
110
|
+
// Pool transient resources that don't overlap
|
|
111
|
+
const pooled = this.poolResources(transient, usage);
|
|
112
|
+
reducedCount = transient.length - pooled.length;
|
|
113
|
+
|
|
114
|
+
reduced.push(...pooled);
|
|
115
|
+
|
|
116
|
+
return { resources: reduced, reduced: reducedCount };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build resource usage map (which passes use which resources)
|
|
121
|
+
*/
|
|
122
|
+
private buildResourceUsageMap(resources: ResourceRequest[], passes: readonly RenderPass[]): Map<string, number[]> {
|
|
123
|
+
const usage = new Map<string, number[]>();
|
|
124
|
+
|
|
125
|
+
for (const resource of resources) {
|
|
126
|
+
usage.set(resource.id, []);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
passes.forEach((pass, index) => {
|
|
130
|
+
for (const input of pass.inputs) {
|
|
131
|
+
if (usage.has(input)) {
|
|
132
|
+
usage.get(input)!.push(index);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (usage.has(pass.output)) {
|
|
136
|
+
usage.get(pass.output)!.push(index);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return usage;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Pool resources that don't have overlapping usage
|
|
145
|
+
*/
|
|
146
|
+
private poolResources(resources: ResourceRequest[], usage: Map<string, number[]>): ResourceRequest[] {
|
|
147
|
+
const pooled: ResourceRequest[] = [];
|
|
148
|
+
const processed = new Set<string>();
|
|
149
|
+
|
|
150
|
+
for (const resource of resources) {
|
|
151
|
+
if (processed.has(resource.id)) continue;
|
|
152
|
+
|
|
153
|
+
const resourceUsage = usage.get(resource.id) || [];
|
|
154
|
+
|
|
155
|
+
// Try to find another resource with non-overlapping usage
|
|
156
|
+
let found = false;
|
|
157
|
+
for (const other of resources) {
|
|
158
|
+
if (other.id === resource.id || processed.has(other.id)) continue;
|
|
159
|
+
if (other.format !== resource.format) continue;
|
|
160
|
+
if (other.width !== resource.width || other.height !== resource.height) continue;
|
|
161
|
+
|
|
162
|
+
const otherUsage = usage.get(other.id) || [];
|
|
163
|
+
|
|
164
|
+
// Check if usage overlaps
|
|
165
|
+
const overlaps = resourceUsage.some((idx) => otherUsage.includes(idx));
|
|
166
|
+
if (!overlaps) {
|
|
167
|
+
// Can reuse this resource
|
|
168
|
+
processed.add(other.id);
|
|
169
|
+
found = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!found || pooled.length === 0) {
|
|
174
|
+
pooled.push(resource);
|
|
175
|
+
}
|
|
176
|
+
processed.add(resource.id);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return pooled;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Reorder passes for better cache locality
|
|
184
|
+
*/
|
|
185
|
+
private reorderPasses(passes: readonly RenderPass[]): readonly RenderPass[] {
|
|
186
|
+
// Build dependency graph
|
|
187
|
+
const deps = new Map<string, Set<string>>();
|
|
188
|
+
const inDegree = new Map<string, number>();
|
|
189
|
+
|
|
190
|
+
for (const pass of passes) {
|
|
191
|
+
deps.set(pass.id, new Set());
|
|
192
|
+
inDegree.set(pass.id, 0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Calculate dependencies
|
|
196
|
+
for (const pass of passes) {
|
|
197
|
+
for (const input of pass.inputs) {
|
|
198
|
+
// Find pass that outputs this resource
|
|
199
|
+
const producer = passes.find((p) => p.output === input);
|
|
200
|
+
if (producer) {
|
|
201
|
+
deps.get(pass.id)!.add(producer.id);
|
|
202
|
+
inDegree.set(pass.id, inDegree.get(pass.id)! + 1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Topological sort (Kahn's algorithm)
|
|
208
|
+
const sorted: RenderPass[] = [];
|
|
209
|
+
const queue: RenderPass[] = [];
|
|
210
|
+
|
|
211
|
+
// Start with passes that have no dependencies
|
|
212
|
+
for (const pass of passes) {
|
|
213
|
+
if (inDegree.get(pass.id) === 0) {
|
|
214
|
+
queue.push(pass);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
while (queue.length > 0) {
|
|
219
|
+
const pass = queue.shift()!;
|
|
220
|
+
sorted.push(pass);
|
|
221
|
+
|
|
222
|
+
// Update in-degrees
|
|
223
|
+
for (const other of passes) {
|
|
224
|
+
if (deps.get(other.id)!.has(pass.id)) {
|
|
225
|
+
const newDegree = inDegree.get(other.id)! - 1;
|
|
226
|
+
inDegree.set(other.id, newDegree);
|
|
227
|
+
|
|
228
|
+
if (newDegree === 0) {
|
|
229
|
+
queue.push(other);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return sorted;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Estimate performance savings
|
|
240
|
+
*/
|
|
241
|
+
private estimateSavings(passesRemoved: number, resourcesReduced: number): number {
|
|
242
|
+
// Rough estimates:
|
|
243
|
+
// - Each pass costs ~1ms
|
|
244
|
+
// - Each resource allocation costs ~0.5ms
|
|
245
|
+
return passesRemoved * 1.0 + resourcesReduced * 0.5;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clypra/runtime — Frame Graph Planner
|
|
3
|
+
*
|
|
4
|
+
* Converts media processing graphs into executable frame graphs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MediaProcessingGraph, GraphNode } from "../graph/types";
|
|
8
|
+
import { GraphHelper } from "../graph/types";
|
|
9
|
+
import type { FrameGraph, ResourceRequest, RenderPass, PlannerConfig } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* FrameGraphPlanner - Plans frame execution from media processing graphs
|
|
13
|
+
*/
|
|
14
|
+
export class FrameGraphPlanner {
|
|
15
|
+
private config: PlannerConfig;
|
|
16
|
+
|
|
17
|
+
constructor(config: Partial<PlannerConfig> = {}) {
|
|
18
|
+
this.config = {
|
|
19
|
+
targetWidth: config.targetWidth || 1920,
|
|
20
|
+
targetHeight: config.targetHeight || 1080,
|
|
21
|
+
enableOptimizations: config.enableOptimizations ?? true,
|
|
22
|
+
allowHalfResolution: config.allowHalfResolution ?? true,
|
|
23
|
+
maxTransientResources: config.maxTransientResources || 8,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Plan frame execution for a specific frame number
|
|
29
|
+
*/
|
|
30
|
+
plan(graph: MediaProcessingGraph, frameNumber: number, timeMs: number): FrameGraph {
|
|
31
|
+
// Get topologically sorted nodes
|
|
32
|
+
const sortedNodes = GraphHelper.topologicalSort(graph);
|
|
33
|
+
|
|
34
|
+
// Filter active nodes (for now, all nodes are active)
|
|
35
|
+
const activeNodes = this.filterActiveNodes(sortedNodes, frameNumber);
|
|
36
|
+
|
|
37
|
+
// Generate resource requests
|
|
38
|
+
const resourceRequests = this.generateResourceRequests(activeNodes);
|
|
39
|
+
|
|
40
|
+
// Generate render passes
|
|
41
|
+
const passes = this.generateRenderPasses(graph, activeNodes);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
frameNumber,
|
|
45
|
+
timelineTimeMs: timeMs,
|
|
46
|
+
nodes: activeNodes,
|
|
47
|
+
edges: graph.edges,
|
|
48
|
+
resourceRequests,
|
|
49
|
+
passes,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Filter nodes that contribute to the current frame
|
|
55
|
+
*/
|
|
56
|
+
private filterActiveNodes(nodes: readonly GraphNode[], frameNumber: number): readonly GraphNode[] {
|
|
57
|
+
// For now, all nodes are active
|
|
58
|
+
// In the future, we can skip nodes based on temporal properties, conditions, etc.
|
|
59
|
+
return nodes.filter((node) => {
|
|
60
|
+
// Skip nodes that are disabled or culled
|
|
61
|
+
if (node.lifecycle === "Disposed") return false;
|
|
62
|
+
|
|
63
|
+
// Include all other nodes
|
|
64
|
+
return true;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate resource requests for active nodes
|
|
70
|
+
*/
|
|
71
|
+
private generateResourceRequests(nodes: readonly GraphNode[]): readonly ResourceRequest[] {
|
|
72
|
+
const requests: ResourceRequest[] = [];
|
|
73
|
+
let resourceId = 0;
|
|
74
|
+
|
|
75
|
+
for (const node of nodes) {
|
|
76
|
+
// Input resources (from MediaInput nodes)
|
|
77
|
+
if (node.type === "MediaInput") {
|
|
78
|
+
requests.push({
|
|
79
|
+
id: `resource-${resourceId++}`,
|
|
80
|
+
type: "texture",
|
|
81
|
+
width: this.config.targetWidth,
|
|
82
|
+
height: this.config.targetHeight,
|
|
83
|
+
format: "rgba8",
|
|
84
|
+
transient: false,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Output resources
|
|
89
|
+
const outputKeys = Object.keys(node.outputs);
|
|
90
|
+
for (const outputKey of outputKeys) {
|
|
91
|
+
const output = node.outputs[outputKey];
|
|
92
|
+
const format = this.getFormatForType(output.type);
|
|
93
|
+
|
|
94
|
+
requests.push({
|
|
95
|
+
id: `resource-${resourceId++}`,
|
|
96
|
+
type: "texture",
|
|
97
|
+
width: this.config.targetWidth,
|
|
98
|
+
height: this.config.targetHeight,
|
|
99
|
+
format,
|
|
100
|
+
transient: node.type !== "Output", // Output resource is not transient
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Temporary resources for multipass effects
|
|
105
|
+
if (node.requirements.multipass) {
|
|
106
|
+
requests.push({
|
|
107
|
+
id: `resource-${resourceId++}-temp`,
|
|
108
|
+
type: "texture",
|
|
109
|
+
width: this.config.targetWidth,
|
|
110
|
+
height: this.config.targetHeight,
|
|
111
|
+
format: "rgba16f",
|
|
112
|
+
transient: true,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return requests;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Generate render passes from nodes
|
|
122
|
+
*/
|
|
123
|
+
private generateRenderPasses(graph: MediaProcessingGraph, nodes: readonly GraphNode[]): readonly RenderPass[] {
|
|
124
|
+
const passes: RenderPass[] = [];
|
|
125
|
+
|
|
126
|
+
for (const node of nodes) {
|
|
127
|
+
// Skip input and output nodes (they don't have shaders)
|
|
128
|
+
if (node.type === "MediaInput" || node.type === "Output") {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Get input resources
|
|
133
|
+
const inputResourceIds: string[] = [];
|
|
134
|
+
const incomingEdges = GraphHelper.getIncomingEdges(graph, node.id);
|
|
135
|
+
for (const edge of incomingEdges) {
|
|
136
|
+
// Find the resource ID for this edge
|
|
137
|
+
// For now, use a simple naming scheme
|
|
138
|
+
inputResourceIds.push(`${edge.fromNodeId}-${edge.fromPinId}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get output resource
|
|
142
|
+
const outputResourceId = `${node.id}-output`;
|
|
143
|
+
|
|
144
|
+
// Create pass
|
|
145
|
+
const pass: RenderPass = {
|
|
146
|
+
id: `pass-${node.id}`,
|
|
147
|
+
name: node.type,
|
|
148
|
+
shaderId: node.type,
|
|
149
|
+
inputs: inputResourceIds,
|
|
150
|
+
output: outputResourceId,
|
|
151
|
+
uniforms: node.params,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
passes.push(pass);
|
|
155
|
+
|
|
156
|
+
// If multipass, add additional passes
|
|
157
|
+
if (node.requirements.multipass) {
|
|
158
|
+
// Add a second pass (e.g., for blur, we'd do horizontal then vertical)
|
|
159
|
+
passes.push({
|
|
160
|
+
id: `pass-${node.id}-2`,
|
|
161
|
+
name: `${node.type}-pass2`,
|
|
162
|
+
shaderId: `${node.type}-pass2`,
|
|
163
|
+
inputs: [outputResourceId],
|
|
164
|
+
output: `${node.id}-output-final`,
|
|
165
|
+
uniforms: node.params,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return passes;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get texture format based on data type
|
|
175
|
+
*/
|
|
176
|
+
private getFormatForType(type: string): "rgba8" | "rgba16f" | "rgba32f" | "r8" | "depth24" {
|
|
177
|
+
switch (type) {
|
|
178
|
+
case "Depth":
|
|
179
|
+
return "depth24";
|
|
180
|
+
case "Mask":
|
|
181
|
+
return "r8";
|
|
182
|
+
case "Texture":
|
|
183
|
+
default:
|
|
184
|
+
return "rgba8";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Update configuration
|
|
190
|
+
*/
|
|
191
|
+
updateConfig(config: Partial<PlannerConfig>): void {
|
|
192
|
+
this.config = { ...this.config, ...config };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get current configuration
|
|
197
|
+
*/
|
|
198
|
+
getConfig(): PlannerConfig {
|
|
199
|
+
return { ...this.config };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clypra/runtime — Planner Types
|
|
3
|
+
*
|
|
4
|
+
* Defines transient planning constructs: resource requests, frame-isolated graphs, and render passes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { GraphNode, GraphEdge } from "../graph/types";
|
|
8
|
+
|
|
9
|
+
export interface ResourceRequest {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly type: "texture" | "buffer";
|
|
12
|
+
readonly width: number;
|
|
13
|
+
readonly height: number;
|
|
14
|
+
readonly format: "rgba8" | "rgba16f" | "rgba32f" | "r8" | "depth24";
|
|
15
|
+
readonly transient: boolean; // True if it can be reclaimed after the pass completes
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RenderPass {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly shaderId: string;
|
|
22
|
+
readonly inputs: readonly string[]; // Active resource IDs
|
|
23
|
+
readonly output: string; // Target resource ID
|
|
24
|
+
readonly uniforms: Readonly<Record<string, any>>;
|
|
25
|
+
readonly blendMode?: string;
|
|
26
|
+
readonly clearBeforeRender?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A flattened, active graph containing only nodes contributing to a specific frame.
|
|
31
|
+
*/
|
|
32
|
+
export interface FrameGraph {
|
|
33
|
+
readonly frameNumber: number;
|
|
34
|
+
readonly timelineTimeMs: number;
|
|
35
|
+
readonly nodes: readonly GraphNode[];
|
|
36
|
+
readonly edges: readonly GraphEdge[];
|
|
37
|
+
readonly resourceRequests: readonly ResourceRequest[];
|
|
38
|
+
readonly passes: readonly RenderPass[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DependencyGraph {
|
|
42
|
+
readonly executionOrder: readonly string[]; // Topologically sorted list of node IDs
|
|
43
|
+
readonly readDependencies: Readonly<Record<string, readonly string[]>>; // nodeId -> list of input resource IDs
|
|
44
|
+
readonly writeDependencies: Readonly<Record<string, string>>; // nodeId -> output resource ID
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Planner configuration
|
|
49
|
+
*/
|
|
50
|
+
export interface PlannerConfig {
|
|
51
|
+
readonly targetWidth: number;
|
|
52
|
+
readonly targetHeight: number;
|
|
53
|
+
readonly enableOptimizations: boolean;
|
|
54
|
+
readonly allowHalfResolution: boolean;
|
|
55
|
+
readonly maxTransientResources: number;
|
|
56
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clypra/runtime — LRU Cache
|
|
3
|
+
*
|
|
4
|
+
* Generic LRU cache for resource management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface CacheEntry<T> {
|
|
8
|
+
key: string;
|
|
9
|
+
value: T;
|
|
10
|
+
size: number;
|
|
11
|
+
lastUsed: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CacheStats {
|
|
15
|
+
size: number;
|
|
16
|
+
capacity: number;
|
|
17
|
+
hits: number;
|
|
18
|
+
misses: number;
|
|
19
|
+
evictions: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Least Recently Used (LRU) Cache
|
|
24
|
+
*/
|
|
25
|
+
export class LRUCache<T> {
|
|
26
|
+
private cache = new Map<string, CacheEntry<T>>();
|
|
27
|
+
private capacity: number;
|
|
28
|
+
private currentSize = 0;
|
|
29
|
+
private hits = 0;
|
|
30
|
+
private misses = 0;
|
|
31
|
+
private evictions = 0;
|
|
32
|
+
|
|
33
|
+
constructor(capacity: number) {
|
|
34
|
+
this.capacity = capacity;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get a value from the cache
|
|
39
|
+
*/
|
|
40
|
+
get(key: string): T | undefined {
|
|
41
|
+
const entry = this.cache.get(key);
|
|
42
|
+
|
|
43
|
+
if (entry) {
|
|
44
|
+
// Update last used time
|
|
45
|
+
entry.lastUsed = Date.now();
|
|
46
|
+
this.hits++;
|
|
47
|
+
return entry.value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.misses++;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Put a value into the cache
|
|
56
|
+
*/
|
|
57
|
+
put(key: string, value: T, size: number = 1): void {
|
|
58
|
+
// Remove existing entry if present
|
|
59
|
+
if (this.cache.has(key)) {
|
|
60
|
+
const existing = this.cache.get(key)!;
|
|
61
|
+
this.currentSize -= existing.size;
|
|
62
|
+
this.cache.delete(key);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Evict entries if necessary
|
|
66
|
+
while (this.currentSize + size > this.capacity && this.cache.size > 0) {
|
|
67
|
+
this.evictLRU();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Add new entry
|
|
71
|
+
this.cache.set(key, {
|
|
72
|
+
key,
|
|
73
|
+
value,
|
|
74
|
+
size,
|
|
75
|
+
lastUsed: Date.now(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.currentSize += size;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Remove a value from the cache
|
|
83
|
+
*/
|
|
84
|
+
remove(key: string): T | undefined {
|
|
85
|
+
const entry = this.cache.get(key);
|
|
86
|
+
if (!entry) return undefined;
|
|
87
|
+
|
|
88
|
+
this.cache.delete(key);
|
|
89
|
+
this.currentSize -= entry.size;
|
|
90
|
+
return entry.value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if key exists
|
|
95
|
+
*/
|
|
96
|
+
has(key: string): boolean {
|
|
97
|
+
return this.cache.has(key);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Evict least recently used entry
|
|
102
|
+
*/
|
|
103
|
+
private evictLRU(): void {
|
|
104
|
+
let oldestKey: string | null = null;
|
|
105
|
+
let oldestTime = Infinity;
|
|
106
|
+
|
|
107
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
108
|
+
if (entry.lastUsed < oldestTime) {
|
|
109
|
+
oldestTime = entry.lastUsed;
|
|
110
|
+
oldestKey = key;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (oldestKey) {
|
|
115
|
+
const entry = this.cache.get(oldestKey)!;
|
|
116
|
+
this.cache.delete(oldestKey);
|
|
117
|
+
this.currentSize -= entry.size;
|
|
118
|
+
this.evictions++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get cache statistics
|
|
124
|
+
*/
|
|
125
|
+
getStats(): CacheStats {
|
|
126
|
+
return {
|
|
127
|
+
size: this.currentSize,
|
|
128
|
+
capacity: this.capacity,
|
|
129
|
+
hits: this.hits,
|
|
130
|
+
misses: this.misses,
|
|
131
|
+
evictions: this.evictions,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Clear the cache
|
|
137
|
+
*/
|
|
138
|
+
clear(): void {
|
|
139
|
+
this.cache.clear();
|
|
140
|
+
this.currentSize = 0;
|
|
141
|
+
this.hits = 0;
|
|
142
|
+
this.misses = 0;
|
|
143
|
+
this.evictions = 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get all keys
|
|
148
|
+
*/
|
|
149
|
+
keys(): string[] {
|
|
150
|
+
return Array.from(this.cache.keys());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get all values
|
|
155
|
+
*/
|
|
156
|
+
values(): T[] {
|
|
157
|
+
return Array.from(this.cache.values()).map((entry) => entry.value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get cache size
|
|
162
|
+
*/
|
|
163
|
+
get size(): number {
|
|
164
|
+
return this.cache.size;
|
|
165
|
+
}
|
|
166
|
+
}
|