@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.
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @clypra/runtime — Resource Management
3
+ *
4
+ * Manages texture and buffer resources with LRU caching.
5
+ */
6
+
7
+ export * from "./types";
8
+ export * from "./manager";
9
+ export * from "./cache";