@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,375 @@
1
+ /**
2
+ * @clypra/runtime — Pixi Renderer
3
+ *
4
+ * Main renderer that executes frame graphs using Pixi.js.
5
+ */
6
+
7
+ import * as PIXI from "pixi.js";
8
+ import type { FrameGraph, RenderPass } from "../planner/types";
9
+ import type { RendererConfig, RenderResult, RenderStats } from "./types";
10
+ import { TexturePool } from "./texture-pool";
11
+ import { createFilter, updateFilterUniforms } from "./filters";
12
+
13
+ /**
14
+ * Pixi.js Renderer
15
+ *
16
+ * Executes frame graphs and manages GPU resources.
17
+ */
18
+ export class PixiRenderer {
19
+ private app: PIXI.Application | null = null;
20
+ private initialized = false;
21
+ private texturePool: TexturePool;
22
+ private resources = new Map<string, PIXI.Texture>();
23
+ private filters = new Map<string, PIXI.Filter>();
24
+ private canvasElement?: HTMLCanvasElement;
25
+
26
+ constructor() {
27
+ this.texturePool = new TexturePool(20);
28
+ }
29
+
30
+ /**
31
+ * Initialize the renderer
32
+ */
33
+ async initialize(config: RendererConfig = {}): Promise<void> {
34
+ if (this.initialized) {
35
+ throw new Error("PixiRenderer already initialized");
36
+ }
37
+
38
+ this.canvasElement = config.canvas;
39
+
40
+ const width = config.width ?? config.canvas?.clientWidth ?? 1920;
41
+ const height = config.height ?? config.canvas?.clientHeight ?? 1080;
42
+
43
+ this.app = new PIXI.Application();
44
+ await this.app.init({
45
+ canvas: config.canvas,
46
+ width,
47
+ height,
48
+ backgroundColor: config.backgroundColor ?? 0x0e0e12,
49
+ antialias: config.antialias ?? true,
50
+ preference: "webgl",
51
+ resolution: config.resolution ?? (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1),
52
+ autoDensity: true,
53
+ preserveDrawingBuffer: config.preserveDrawingBuffer ?? true,
54
+ });
55
+
56
+ this.initialized = true;
57
+ }
58
+
59
+ /**
60
+ * Render a frame graph
61
+ */
62
+ async render(frameGraph: FrameGraph): Promise<RenderResult> {
63
+ if (!this.initialized || !this.app) {
64
+ throw new Error("PixiRenderer not initialized");
65
+ }
66
+
67
+ const startTime = performance.now();
68
+
69
+ // Allocate resources
70
+ for (const resource of frameGraph.resourceRequests) {
71
+ if (!this.resources.has(resource.id)) {
72
+ this.allocateResource(resource.id, resource.width, resource.height);
73
+ }
74
+ }
75
+
76
+ // Execute passes
77
+ let totalGpuTime = 0;
78
+ for (const pass of frameGraph.passes) {
79
+ const passStart = performance.now();
80
+ await this.executePass(pass);
81
+ totalGpuTime += performance.now() - passStart;
82
+ }
83
+
84
+ // Release transient resources
85
+ for (const resource of frameGraph.resourceRequests) {
86
+ if (resource.transient) {
87
+ this.releaseResource(resource.id);
88
+ }
89
+ }
90
+
91
+ const totalCpuTime = performance.now() - startTime - totalGpuTime;
92
+
93
+ // Get output texture
94
+ const outputTexture = this.resources.get("output") || this.resources.values().next().value;
95
+
96
+ if (!outputTexture) {
97
+ throw new Error("No output texture available");
98
+ }
99
+
100
+ const stats: RenderStats = {
101
+ passCount: frameGraph.passes.length,
102
+ totalGpuTime,
103
+ totalCpuTime,
104
+ resourceCount: this.resources.size,
105
+ textureMemory: this.calculateTextureMemory(),
106
+ };
107
+
108
+ return {
109
+ outputTexture,
110
+ stats,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Execute a single render pass
116
+ */
117
+ private async executePass(pass: RenderPass): Promise<void> {
118
+ if (!this.app) return;
119
+
120
+ // Get input and output textures
121
+ const inputTexture = pass.inputs.length > 0 ? this.resources.get(pass.inputs[0]) : null;
122
+ const outputTexture = this.resources.get(pass.output);
123
+
124
+ if (!outputTexture) {
125
+ throw new Error(`Output resource not found: ${pass.output}`);
126
+ }
127
+
128
+ // Handle copy/blit passes
129
+ if (pass.shaderId === "copy" || pass.shaderId === "blit" || pass.shaderId === "blit-source") {
130
+ if (inputTexture) {
131
+ this.blitTexture(inputTexture, outputTexture as PIXI.RenderTexture, pass.clearBeforeRender);
132
+ }
133
+ return;
134
+ }
135
+
136
+ if (!inputTexture) {
137
+ console.warn(`No input texture for pass: ${pass.id}`);
138
+ return;
139
+ }
140
+
141
+ // Create sprite with input texture
142
+ const target = outputTexture as PIXI.RenderTexture;
143
+ const sprite = new PIXI.Sprite(inputTexture);
144
+ sprite.width = target.width;
145
+ sprite.height = target.height;
146
+
147
+ // Get or create filter
148
+ let filter: PIXI.Filter;
149
+ let disposeFilter = false;
150
+
151
+ if (this.filters.has(pass.shaderId)) {
152
+ filter = this.filters.get(pass.shaderId)!;
153
+ updateFilterUniforms(filter, pass.uniforms, pass.shaderId);
154
+ } else {
155
+ filter = createFilter(pass.shaderId, pass.uniforms);
156
+
157
+ // Cache simple filters
158
+ if (["brightness", "contrast", "saturation", "copy", "blit"].includes(pass.shaderId)) {
159
+ this.filters.set(pass.shaderId, filter);
160
+ } else {
161
+ disposeFilter = true;
162
+ }
163
+ }
164
+
165
+ // Apply filter and render
166
+ sprite.filters = [filter];
167
+ this.app.renderer.render({
168
+ container: sprite,
169
+ target,
170
+ clear: pass.clearBeforeRender ?? true,
171
+ });
172
+ sprite.filters = null;
173
+
174
+ // Clean up if needed
175
+ if (disposeFilter) {
176
+ filter.destroy();
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Blit texture to another
182
+ */
183
+ private blitTexture(source: PIXI.Texture, target: PIXI.RenderTexture, clear: boolean = true): void {
184
+ if (!this.app) return;
185
+
186
+ const sprite = new PIXI.Sprite(source);
187
+ sprite.width = target.width;
188
+ sprite.height = target.height;
189
+
190
+ this.app.renderer.render({
191
+ container: sprite,
192
+ target,
193
+ clear,
194
+ });
195
+ }
196
+
197
+ /**
198
+ * Allocate a texture resource
199
+ */
200
+ private allocateResource(id: string, width: number, height: number): void {
201
+ if (this.resources.has(id)) {
202
+ throw new Error(`Resource "${id}" already allocated`);
203
+ }
204
+
205
+ const texture = PIXI.RenderTexture.create({ width, height });
206
+ this.resources.set(id, texture);
207
+ }
208
+
209
+ /**
210
+ * Release a texture resource
211
+ */
212
+ private releaseResource(id: string): void {
213
+ const texture = this.resources.get(id);
214
+ if (!texture) return;
215
+
216
+ // Return to pool if it's a render texture
217
+ if (texture instanceof PIXI.RenderTexture) {
218
+ this.texturePool.release(texture, {
219
+ width: texture.width,
220
+ height: texture.height,
221
+ format: "rgba8",
222
+ });
223
+ }
224
+
225
+ this.resources.delete(id);
226
+ }
227
+
228
+ /**
229
+ * Upload source image to resources
230
+ */
231
+ uploadSourceImage(sourceImage: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement, resourceIds: readonly string[]): void {
232
+ if (!this.app) return;
233
+
234
+ const sourceTexture = PIXI.Texture.from(sourceImage);
235
+
236
+ for (const resourceId of resourceIds) {
237
+ const texture = this.resources.get(resourceId);
238
+ if (!texture || !(texture instanceof PIXI.RenderTexture)) continue;
239
+
240
+ // Contain-fit scaling
241
+ const fitScale = Math.min(texture.width / sourceTexture.width, texture.height / sourceTexture.height);
242
+
243
+ const sprite = new PIXI.Sprite(sourceTexture);
244
+ sprite.scale.set(fitScale);
245
+ sprite.position.set((texture.width - sourceTexture.width * fitScale) / 2, (texture.height - sourceTexture.height * fitScale) / 2);
246
+
247
+ this.app.renderer.render({ container: sprite, target: texture });
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Present a resource to the canvas
253
+ */
254
+ present(resourceId: string): void {
255
+ if (!this.app) return;
256
+
257
+ const texture = this.resources.get(resourceId);
258
+ if (!texture) return;
259
+
260
+ const screenW = this.app.renderer.screen.width;
261
+ const screenH = this.app.renderer.screen.height;
262
+ const scale = Math.min(screenW / texture.width, screenH / texture.height);
263
+
264
+ const sprite = new PIXI.Sprite(texture);
265
+ sprite.scale.set(scale);
266
+ sprite.position.set((screenW - texture.width * scale) / 2, (screenH - texture.height * scale) / 2);
267
+
268
+ this.app.stage.removeChildren();
269
+ this.app.stage.addChild(sprite);
270
+ this.app.renderer.render(this.app.stage);
271
+ }
272
+
273
+ /**
274
+ * Read pixels from a resource
275
+ */
276
+ async readPixels(resourceId: string): Promise<Uint8Array> {
277
+ if (!this.app) {
278
+ throw new Error("PixiRenderer not initialized");
279
+ }
280
+
281
+ const texture = this.resources.get(resourceId);
282
+ if (!texture) {
283
+ throw new Error(`Resource "${resourceId}" not found`);
284
+ }
285
+
286
+ const pixels = this.app.renderer.extract.pixels(texture);
287
+ return new Uint8Array(pixels.pixels);
288
+ }
289
+
290
+ /**
291
+ * Get a resource texture
292
+ */
293
+ getResource(id: string): PIXI.Texture | undefined {
294
+ return this.resources.get(id);
295
+ }
296
+
297
+ /**
298
+ * Resize the renderer
299
+ */
300
+ resize(width: number, height: number): void {
301
+ if (!this.app) return;
302
+ this.app.renderer.resize(width, height);
303
+ }
304
+
305
+ /**
306
+ * Get logical dimensions
307
+ */
308
+ get width(): number {
309
+ return this.app?.renderer.width ?? 0;
310
+ }
311
+
312
+ get height(): number {
313
+ return this.app?.renderer.height ?? 0;
314
+ }
315
+
316
+ /**
317
+ * Calculate total texture memory
318
+ */
319
+ private calculateTextureMemory(): number {
320
+ let total = 0;
321
+ for (const texture of this.resources.values()) {
322
+ total += texture.width * texture.height * 4; // RGBA8
323
+ }
324
+ return total;
325
+ }
326
+
327
+ /**
328
+ * Clear all resources
329
+ */
330
+ clearResources(): void {
331
+ for (const id of [...this.resources.keys()]) {
332
+ this.releaseResource(id);
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Get texture pool stats
338
+ */
339
+ getPoolStats() {
340
+ return this.texturePool.getStats();
341
+ }
342
+
343
+ /**
344
+ * Dispose the renderer
345
+ */
346
+ dispose(): void {
347
+ if (!this.initialized) return;
348
+
349
+ // Clear resources
350
+ for (const texture of this.resources.values()) {
351
+ if (texture instanceof PIXI.RenderTexture) {
352
+ texture.destroy(true);
353
+ }
354
+ }
355
+ this.resources.clear();
356
+
357
+ // Clear filters
358
+ for (const filter of this.filters.values()) {
359
+ filter.destroy();
360
+ }
361
+ this.filters.clear();
362
+
363
+ // Clear texture pool
364
+ this.texturePool.dispose();
365
+
366
+ // Destroy app
367
+ if (this.app) {
368
+ this.app.destroy(true);
369
+ this.app = null;
370
+ }
371
+
372
+ this.initialized = false;
373
+ this.canvasElement = undefined;
374
+ }
375
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @clypra/runtime — Texture Pool
3
+ *
4
+ * LRU texture pooling for efficient resource management.
5
+ */
6
+
7
+ import * as PIXI from "pixi.js";
8
+ import type { PixiResourceDescriptor, TexturePoolStats } from "./types";
9
+
10
+ interface PooledTexture {
11
+ texture: PIXI.RenderTexture;
12
+ descriptor: PixiResourceDescriptor;
13
+ lastUsed: number;
14
+ }
15
+
16
+ /**
17
+ * Texture pool with LRU eviction
18
+ */
19
+ export class TexturePool {
20
+ private pool: Map<string, PooledTexture> = new Map();
21
+ private maxSize: number;
22
+ private hits = 0;
23
+ private misses = 0;
24
+
25
+ constructor(maxSize: number = 20) {
26
+ this.maxSize = maxSize;
27
+ }
28
+
29
+ /**
30
+ * Acquire a texture from the pool or create a new one
31
+ */
32
+ acquire(descriptor: PixiResourceDescriptor): PIXI.RenderTexture {
33
+ const key = this.descriptorKey(descriptor);
34
+ const pooled = this.pool.get(key);
35
+
36
+ if (pooled) {
37
+ // Hit: Reuse existing texture
38
+ this.pool.delete(key);
39
+ this.hits++;
40
+ return pooled.texture;
41
+ }
42
+
43
+ // Miss: Create new texture
44
+ this.misses++;
45
+ const texture = PIXI.RenderTexture.create({
46
+ width: descriptor.width,
47
+ height: descriptor.height,
48
+ });
49
+
50
+ return texture;
51
+ }
52
+
53
+ /**
54
+ * Release a texture back to the pool
55
+ */
56
+ release(texture: PIXI.RenderTexture, descriptor: PixiResourceDescriptor): void {
57
+ const key = this.descriptorKey(descriptor);
58
+
59
+ // Check if pool is full
60
+ if (this.pool.size >= this.maxSize) {
61
+ this.evictLRU();
62
+ }
63
+
64
+ this.pool.set(key, {
65
+ texture,
66
+ descriptor,
67
+ lastUsed: Date.now(),
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Evict least recently used texture
73
+ */
74
+ private evictLRU(): void {
75
+ let oldestKey: string | null = null;
76
+ let oldestTime = Infinity;
77
+
78
+ for (const [key, pooled] of this.pool.entries()) {
79
+ if (pooled.lastUsed < oldestTime) {
80
+ oldestTime = pooled.lastUsed;
81
+ oldestKey = key;
82
+ }
83
+ }
84
+
85
+ if (oldestKey) {
86
+ const pooled = this.pool.get(oldestKey);
87
+ if (pooled) {
88
+ pooled.texture.destroy(true);
89
+ this.pool.delete(oldestKey);
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Generate key for descriptor
96
+ */
97
+ private descriptorKey(descriptor: PixiResourceDescriptor): string {
98
+ return `${descriptor.width}x${descriptor.height}:${descriptor.format}`;
99
+ }
100
+
101
+ /**
102
+ * Get pool statistics
103
+ */
104
+ getStats(): TexturePoolStats {
105
+ let totalMemory = 0;
106
+
107
+ for (const pooled of this.pool.values()) {
108
+ const bytesPerPixel = this.getBytesPerPixel(pooled.descriptor.format);
109
+ totalMemory += pooled.descriptor.width * pooled.descriptor.height * bytesPerPixel;
110
+ }
111
+
112
+ return {
113
+ allocated: this.pool.size,
114
+ available: this.maxSize - this.pool.size,
115
+ totalMemory,
116
+ hits: this.hits,
117
+ misses: this.misses,
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Get bytes per pixel for format
123
+ */
124
+ private getBytesPerPixel(format: string): number {
125
+ switch (format) {
126
+ case "rgba8":
127
+ return 4;
128
+ case "rgba16f":
129
+ return 8;
130
+ case "rgba32f":
131
+ return 16;
132
+ case "r8":
133
+ return 1;
134
+ case "depth24":
135
+ return 3;
136
+ default:
137
+ return 4;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Clear the pool
143
+ */
144
+ clear(): void {
145
+ for (const pooled of this.pool.values()) {
146
+ pooled.texture.destroy(true);
147
+ }
148
+ this.pool.clear();
149
+ this.hits = 0;
150
+ this.misses = 0;
151
+ }
152
+
153
+ /**
154
+ * Dispose the pool
155
+ */
156
+ dispose(): void {
157
+ this.clear();
158
+ }
159
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @clypra/runtime — Pixi Backend Types
3
+ */
4
+
5
+ import type { FrameGraph } from "../planner/types";
6
+ import type * as PIXI from "pixi.js";
7
+
8
+ /**
9
+ * Render backend configuration
10
+ */
11
+ export interface RendererConfig {
12
+ canvas?: HTMLCanvasElement;
13
+ width?: number;
14
+ height?: number;
15
+ backgroundColor?: number;
16
+ antialias?: boolean;
17
+ resolution?: number;
18
+ preserveDrawingBuffer?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Render result
23
+ */
24
+ export interface RenderResult {
25
+ outputTexture: PIXI.Texture;
26
+ stats: RenderStats;
27
+ }
28
+
29
+ /**
30
+ * Rendering statistics
31
+ */
32
+ export interface RenderStats {
33
+ passCount: number;
34
+ totalGpuTime: number;
35
+ totalCpuTime: number;
36
+ resourceCount: number;
37
+ textureMemory: number;
38
+ }
39
+
40
+ /**
41
+ * Texture pool statistics
42
+ */
43
+ export interface TexturePoolStats {
44
+ allocated: number;
45
+ available: number;
46
+ totalMemory: number;
47
+ hits: number;
48
+ misses: number;
49
+ }
50
+
51
+ /**
52
+ * Pixi-specific resource descriptor (simpler than full ResourceDescriptor)
53
+ */
54
+ export interface PixiResourceDescriptor {
55
+ width: number;
56
+ height: number;
57
+ format: "rgba8" | "rgba16f" | "rgba32f" | "r8" | "depth24";
58
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @clypra/runtime — Frame Graph Planner
3
+ *
4
+ * Converts media processing graphs into executable frame graphs.
5
+ * Handles resource allocation, pass optimization, and execution ordering.
6
+ */
7
+
8
+ export * from "./types";
9
+ export * from "./planner";
10
+ export * from "./optimizer";