@motion-core/motion-gpu 0.1.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -0
  3. package/dist/FragCanvas.svelte +511 -0
  4. package/dist/FragCanvas.svelte.d.ts +26 -0
  5. package/dist/MotionGPUErrorOverlay.svelte +394 -0
  6. package/dist/MotionGPUErrorOverlay.svelte.d.ts +7 -0
  7. package/dist/Portal.svelte +46 -0
  8. package/dist/Portal.svelte.d.ts +8 -0
  9. package/dist/advanced-scheduler.d.ts +44 -0
  10. package/dist/advanced-scheduler.js +58 -0
  11. package/dist/advanced.d.ts +14 -0
  12. package/dist/advanced.js +9 -0
  13. package/dist/core/error-diagnostics.d.ts +40 -0
  14. package/dist/core/error-diagnostics.js +111 -0
  15. package/dist/core/error-report.d.ts +67 -0
  16. package/dist/core/error-report.js +190 -0
  17. package/dist/core/material-preprocess.d.ts +63 -0
  18. package/dist/core/material-preprocess.js +166 -0
  19. package/dist/core/material.d.ts +157 -0
  20. package/dist/core/material.js +358 -0
  21. package/dist/core/recompile-policy.d.ts +27 -0
  22. package/dist/core/recompile-policy.js +15 -0
  23. package/dist/core/render-graph.d.ts +55 -0
  24. package/dist/core/render-graph.js +73 -0
  25. package/dist/core/render-targets.d.ts +39 -0
  26. package/dist/core/render-targets.js +63 -0
  27. package/dist/core/renderer.d.ts +9 -0
  28. package/dist/core/renderer.js +1097 -0
  29. package/dist/core/shader.d.ts +42 -0
  30. package/dist/core/shader.js +196 -0
  31. package/dist/core/texture-loader.d.ts +129 -0
  32. package/dist/core/texture-loader.js +295 -0
  33. package/dist/core/textures.d.ts +114 -0
  34. package/dist/core/textures.js +136 -0
  35. package/dist/core/types.d.ts +523 -0
  36. package/dist/core/types.js +4 -0
  37. package/dist/core/uniforms.d.ts +48 -0
  38. package/dist/core/uniforms.js +222 -0
  39. package/dist/current-writable.d.ts +31 -0
  40. package/dist/current-writable.js +27 -0
  41. package/dist/frame-context.d.ts +287 -0
  42. package/dist/frame-context.js +731 -0
  43. package/dist/index.d.ts +17 -0
  44. package/dist/index.js +11 -0
  45. package/dist/motiongpu-context.d.ts +77 -0
  46. package/dist/motiongpu-context.js +26 -0
  47. package/dist/passes/BlitPass.d.ts +32 -0
  48. package/dist/passes/BlitPass.js +158 -0
  49. package/dist/passes/CopyPass.d.ts +25 -0
  50. package/dist/passes/CopyPass.js +53 -0
  51. package/dist/passes/ShaderPass.d.ts +40 -0
  52. package/dist/passes/ShaderPass.js +182 -0
  53. package/dist/passes/index.d.ts +3 -0
  54. package/dist/passes/index.js +3 -0
  55. package/dist/use-motiongpu-user-context.d.ts +35 -0
  56. package/dist/use-motiongpu-user-context.js +74 -0
  57. package/dist/use-texture.d.ts +35 -0
  58. package/dist/use-texture.js +147 -0
  59. package/package.json +94 -0
@@ -0,0 +1,42 @@
1
+ import type { MaterialLineMap, MaterialSourceLocation } from './material-preprocess';
2
+ import type { UniformLayout } from './types';
3
+ /**
4
+ * 1-based map from generated WGSL lines to original material source lines.
5
+ */
6
+ export type ShaderLineMap = Array<MaterialSourceLocation | null>;
7
+ /**
8
+ * Result of shader source generation with line mapping metadata.
9
+ */
10
+ export interface BuiltShaderSource {
11
+ /**
12
+ * Full WGSL source code.
13
+ */
14
+ code: string;
15
+ /**
16
+ * 1-based generated-line map to material source locations.
17
+ */
18
+ lineMap: ShaderLineMap;
19
+ }
20
+ /**
21
+ * Assembles complete WGSL shader source used by the fullscreen renderer pipeline.
22
+ *
23
+ * @param fragmentWgsl - User fragment shader code containing `frag(uv: vec2f) -> vec4f`.
24
+ * @param uniformLayout - Resolved uniform layout.
25
+ * @param textureKeys - Sorted texture keys.
26
+ * @param options - Shader build options.
27
+ * @returns Complete WGSL source for vertex + fragment stages.
28
+ */
29
+ export declare function buildShaderSource(fragmentWgsl: string, uniformLayout: UniformLayout, textureKeys?: string[], options?: {
30
+ convertLinearToSrgb?: boolean;
31
+ }): string;
32
+ /**
33
+ * Assembles complete WGSL shader source with material-source line mapping metadata.
34
+ */
35
+ export declare function buildShaderSourceWithMap(fragmentWgsl: string, uniformLayout: UniformLayout, textureKeys?: string[], options?: {
36
+ convertLinearToSrgb?: boolean;
37
+ fragmentLineMap?: MaterialLineMap;
38
+ }): BuiltShaderSource;
39
+ /**
40
+ * Converts source location metadata to user-facing diagnostics label.
41
+ */
42
+ export declare function formatShaderSourceLocation(location: MaterialSourceLocation | null): string | null;
@@ -0,0 +1,196 @@
1
+ import { assertUniformName } from './uniforms';
2
+ /**
3
+ * Fallback uniform field used when no custom uniforms are provided.
4
+ */
5
+ const DEFAULT_UNIFORM_FIELD = 'motiongpu_unused: vec4f,';
6
+ /**
7
+ * Builds WGSL struct fields for user uniforms.
8
+ */
9
+ function buildUniformStruct(layout) {
10
+ if (layout.entries.length === 0) {
11
+ return DEFAULT_UNIFORM_FIELD;
12
+ }
13
+ return layout.entries
14
+ .map((entry) => {
15
+ assertUniformName(entry.name);
16
+ return `${entry.name}: ${entry.type},`;
17
+ })
18
+ .join('\n\t');
19
+ }
20
+ /**
21
+ * Builds a numeric expression that references one uniform value to keep bindings alive.
22
+ */
23
+ function getKeepAliveExpression(layout) {
24
+ if (layout.entries.length === 0) {
25
+ return 'motiongpuUniforms.motiongpu_unused.x';
26
+ }
27
+ const [firstEntry] = layout.entries;
28
+ if (!firstEntry) {
29
+ return 'motiongpuUniforms.motiongpu_unused.x';
30
+ }
31
+ if (firstEntry.type === 'f32') {
32
+ return `motiongpuUniforms.${firstEntry.name}`;
33
+ }
34
+ if (firstEntry.type === 'mat4x4f') {
35
+ return `motiongpuUniforms.${firstEntry.name}[0].x`;
36
+ }
37
+ return `motiongpuUniforms.${firstEntry.name}.x`;
38
+ }
39
+ /**
40
+ * Builds texture sampler/texture binding declarations.
41
+ */
42
+ function buildTextureBindings(textureKeys) {
43
+ if (textureKeys.length === 0) {
44
+ return '';
45
+ }
46
+ const declarations = [];
47
+ for (let index = 0; index < textureKeys.length; index += 1) {
48
+ const key = textureKeys[index];
49
+ if (key === undefined) {
50
+ continue;
51
+ }
52
+ assertUniformName(key);
53
+ const binding = 2 + index * 2;
54
+ declarations.push(`@group(0) @binding(${binding}) var ${key}Sampler: sampler;`);
55
+ declarations.push(`@group(0) @binding(${binding + 1}) var ${key}: texture_2d<f32>;`);
56
+ }
57
+ return declarations.join('\n');
58
+ }
59
+ /**
60
+ * Optionally returns helper WGSL for linear-to-sRGB conversion.
61
+ */
62
+ function buildColorTransformHelpers(enableSrgbTransform) {
63
+ if (!enableSrgbTransform) {
64
+ return '';
65
+ }
66
+ return `
67
+ fn motiongpuLinearToSrgb(linearColor: vec3f) -> vec3f {
68
+ let cutoff = vec3f(0.0031308);
69
+ let lower = linearColor * 12.92;
70
+ let higher = vec3f(1.055) * pow(linearColor, vec3f(1.0 / 2.4)) - vec3f(0.055);
71
+ return select(lower, higher, linearColor > cutoff);
72
+ }
73
+ `;
74
+ }
75
+ /**
76
+ * Builds fragment output code with optional color-space conversion.
77
+ */
78
+ function buildFragmentOutput(keepAliveExpression, enableSrgbTransform) {
79
+ if (enableSrgbTransform) {
80
+ return `
81
+ let fragColor = frag(in.uv);
82
+ let motiongpuKeepAlive = ${keepAliveExpression};
83
+ let motiongpuLinear = vec4f(fragColor.rgb + motiongpuKeepAlive * 0.0, fragColor.a);
84
+ let motiongpuSrgb = motiongpuLinearToSrgb(max(motiongpuLinear.rgb, vec3f(0.0)));
85
+ return vec4f(motiongpuSrgb, motiongpuLinear.a);
86
+ `;
87
+ }
88
+ return `
89
+ let fragColor = frag(in.uv);
90
+ let motiongpuKeepAlive = ${keepAliveExpression};
91
+ return vec4f(fragColor.rgb + motiongpuKeepAlive * 0.0, fragColor.a);
92
+ `;
93
+ }
94
+ /**
95
+ * Assembles complete WGSL shader source used by the fullscreen renderer pipeline.
96
+ *
97
+ * @param fragmentWgsl - User fragment shader code containing `frag(uv: vec2f) -> vec4f`.
98
+ * @param uniformLayout - Resolved uniform layout.
99
+ * @param textureKeys - Sorted texture keys.
100
+ * @param options - Shader build options.
101
+ * @returns Complete WGSL source for vertex + fragment stages.
102
+ */
103
+ export function buildShaderSource(fragmentWgsl, uniformLayout, textureKeys = [], options) {
104
+ const uniformFields = buildUniformStruct(uniformLayout);
105
+ const keepAliveExpression = getKeepAliveExpression(uniformLayout);
106
+ const textureBindings = buildTextureBindings(textureKeys);
107
+ const enableSrgbTransform = options?.convertLinearToSrgb ?? false;
108
+ const colorTransformHelpers = buildColorTransformHelpers(enableSrgbTransform);
109
+ const fragmentOutput = buildFragmentOutput(keepAliveExpression, enableSrgbTransform);
110
+ return `
111
+ struct MotionGPUFrame {
112
+ time: f32,
113
+ delta: f32,
114
+ resolution: vec2f,
115
+ };
116
+
117
+ struct MotionGPUUniforms {
118
+ ${uniformFields}
119
+ };
120
+
121
+ @group(0) @binding(0) var<uniform> motiongpuFrame: MotionGPUFrame;
122
+ @group(0) @binding(1) var<uniform> motiongpuUniforms: MotionGPUUniforms;
123
+ ${textureBindings}
124
+ ${colorTransformHelpers}
125
+
126
+ struct MotionGPUVertexOut {
127
+ @builtin(position) position: vec4f,
128
+ @location(0) uv: vec2f,
129
+ };
130
+
131
+ @vertex
132
+ fn motiongpuVertex(@builtin(vertex_index) index: u32) -> MotionGPUVertexOut {
133
+ var positions = array<vec2f, 3>(
134
+ vec2f(-1.0, -3.0),
135
+ vec2f(-1.0, 1.0),
136
+ vec2f(3.0, 1.0)
137
+ );
138
+
139
+ let position = positions[index];
140
+ var out: MotionGPUVertexOut;
141
+ out.position = vec4f(position, 0.0, 1.0);
142
+ out.uv = (position + vec2f(1.0, 1.0)) * 0.5;
143
+ return out;
144
+ }
145
+
146
+ ${fragmentWgsl}
147
+
148
+ @fragment
149
+ fn motiongpuFragment(in: MotionGPUVertexOut) -> @location(0) vec4f {
150
+ ${fragmentOutput}
151
+ }
152
+ `;
153
+ }
154
+ /**
155
+ * Assembles complete WGSL shader source with material-source line mapping metadata.
156
+ */
157
+ export function buildShaderSourceWithMap(fragmentWgsl, uniformLayout, textureKeys = [], options) {
158
+ const code = buildShaderSource(fragmentWgsl, uniformLayout, textureKeys, options);
159
+ const fragmentStartIndex = code.indexOf(fragmentWgsl);
160
+ const lineCount = code.split('\n').length;
161
+ const lineMap = new Array(lineCount + 1).fill(null);
162
+ if (fragmentStartIndex === -1) {
163
+ return {
164
+ code,
165
+ lineMap
166
+ };
167
+ }
168
+ const fragmentStartLine = code.slice(0, fragmentStartIndex).split('\n').length;
169
+ const fragmentLineCount = fragmentWgsl.split('\n').length;
170
+ for (let line = 0; line < fragmentLineCount; line += 1) {
171
+ const generatedLine = fragmentStartLine + line;
172
+ lineMap[generatedLine] = options?.fragmentLineMap?.[line + 1] ?? {
173
+ kind: 'fragment',
174
+ line: line + 1
175
+ };
176
+ }
177
+ return {
178
+ code,
179
+ lineMap
180
+ };
181
+ }
182
+ /**
183
+ * Converts source location metadata to user-facing diagnostics label.
184
+ */
185
+ export function formatShaderSourceLocation(location) {
186
+ if (!location) {
187
+ return null;
188
+ }
189
+ if (location.kind === 'fragment') {
190
+ return `fragment line ${location.line}`;
191
+ }
192
+ if (location.kind === 'include') {
193
+ return `include <${location.include}> line ${location.line}`;
194
+ }
195
+ return `define "${location.define}" line ${location.line}`;
196
+ }
@@ -0,0 +1,129 @@
1
+ import type { TextureUpdateMode } from './types';
2
+ /**
3
+ * Options controlling bitmap decode behavior.
4
+ */
5
+ export interface TextureDecodeOptions {
6
+ /**
7
+ * Controls color-space conversion during decode.
8
+ */
9
+ colorSpaceConversion?: 'default' | 'none';
10
+ /**
11
+ * Controls alpha premultiplication during decode.
12
+ */
13
+ premultiplyAlpha?: 'default' | 'none' | 'premultiply';
14
+ /**
15
+ * Controls bitmap orientation during decode.
16
+ */
17
+ imageOrientation?: 'none' | 'flipY';
18
+ }
19
+ /**
20
+ * Options controlling URL-based texture loading and decode behavior.
21
+ */
22
+ export interface TextureLoadOptions {
23
+ /**
24
+ * Desired texture color space.
25
+ */
26
+ colorSpace?: 'srgb' | 'linear';
27
+ /**
28
+ * Fetch options forwarded to `fetch`.
29
+ */
30
+ requestInit?: RequestInit;
31
+ /**
32
+ * Decode options forwarded to `createImageBitmap`.
33
+ */
34
+ decode?: TextureDecodeOptions;
35
+ /**
36
+ * Optional cancellation signal for this request.
37
+ */
38
+ signal?: AbortSignal;
39
+ /**
40
+ * Optional runtime update strategy metadata attached to loaded textures.
41
+ */
42
+ update?: TextureUpdateMode;
43
+ /**
44
+ * Optional runtime flip-y metadata attached to loaded textures.
45
+ */
46
+ flipY?: boolean;
47
+ /**
48
+ * Optional runtime premultiplied-alpha metadata attached to loaded textures.
49
+ */
50
+ premultipliedAlpha?: boolean;
51
+ /**
52
+ * Optional runtime mipmap metadata attached to loaded textures.
53
+ */
54
+ generateMipmaps?: boolean;
55
+ }
56
+ /**
57
+ * Loaded texture payload returned by URL loaders.
58
+ */
59
+ export interface LoadedTexture {
60
+ /**
61
+ * Source URL.
62
+ */
63
+ url: string;
64
+ /**
65
+ * Decoded bitmap source.
66
+ */
67
+ source: ImageBitmap;
68
+ /**
69
+ * Bitmap width in pixels.
70
+ */
71
+ width: number;
72
+ /**
73
+ * Bitmap height in pixels.
74
+ */
75
+ height: number;
76
+ /**
77
+ * Effective color space.
78
+ */
79
+ colorSpace: 'srgb' | 'linear';
80
+ /**
81
+ * Effective runtime update strategy.
82
+ */
83
+ update?: TextureUpdateMode;
84
+ /**
85
+ * Effective runtime flip-y metadata.
86
+ */
87
+ flipY?: boolean;
88
+ /**
89
+ * Effective runtime premultiplied-alpha metadata.
90
+ */
91
+ premultipliedAlpha?: boolean;
92
+ /**
93
+ * Effective runtime mipmap metadata.
94
+ */
95
+ generateMipmaps?: boolean;
96
+ /**
97
+ * Releases bitmap resources.
98
+ */
99
+ dispose: () => void;
100
+ }
101
+ /**
102
+ * Checks whether error represents abort cancellation.
103
+ */
104
+ export declare function isAbortError(error: unknown): boolean;
105
+ /**
106
+ * Builds deterministic resource cache key from full URL IO config.
107
+ */
108
+ export declare function buildTextureResourceCacheKey(url: string, options?: TextureLoadOptions): string;
109
+ /**
110
+ * Clears the internal texture resource cache.
111
+ */
112
+ export declare function clearTextureBlobCache(): void;
113
+ /**
114
+ * Loads a single texture from URL and converts it to an `ImageBitmap`.
115
+ *
116
+ * @param url - Texture URL.
117
+ * @param options - Loading options.
118
+ * @returns Loaded texture object.
119
+ * @throws {Error} When runtime does not support `createImageBitmap` or request fails.
120
+ */
121
+ export declare function loadTextureFromUrl(url: string, options?: TextureLoadOptions): Promise<LoadedTexture>;
122
+ /**
123
+ * Loads many textures in parallel from URLs.
124
+ *
125
+ * @param urls - Texture URLs.
126
+ * @param options - Shared loading options.
127
+ * @returns Promise resolving to loaded textures in input order.
128
+ */
129
+ export declare function loadTexturesFromUrls(urls: string[], options?: TextureLoadOptions): Promise<LoadedTexture[]>;
@@ -0,0 +1,295 @@
1
+ const resourceCache = new Map();
2
+ function createAbortError() {
3
+ try {
4
+ return new DOMException('Texture request was aborted', 'AbortError');
5
+ }
6
+ catch {
7
+ const error = new Error('Texture request was aborted');
8
+ error.name = 'AbortError';
9
+ return error;
10
+ }
11
+ }
12
+ /**
13
+ * Checks whether error represents abort cancellation.
14
+ */
15
+ export function isAbortError(error) {
16
+ return (error instanceof Error &&
17
+ (error.name === 'AbortError' || error.message.toLowerCase().includes('aborted')));
18
+ }
19
+ function toBodyFingerprint(body) {
20
+ if (body == null) {
21
+ return null;
22
+ }
23
+ if (typeof body === 'string') {
24
+ return `string:${body}`;
25
+ }
26
+ if (body instanceof URLSearchParams) {
27
+ return `urlsearchparams:${body.toString()}`;
28
+ }
29
+ if (typeof FormData !== 'undefined' && body instanceof FormData) {
30
+ const entries = Array.from(body.entries()).map(([key, value]) => `${key}:${String(value)}`);
31
+ return `formdata:${entries.join('&')}`;
32
+ }
33
+ if (body instanceof Blob) {
34
+ return `blob:${body.type}:${body.size}`;
35
+ }
36
+ if (body instanceof ArrayBuffer) {
37
+ return `arraybuffer:${body.byteLength}`;
38
+ }
39
+ if (ArrayBuffer.isView(body)) {
40
+ return `view:${body.byteLength}`;
41
+ }
42
+ return `opaque:${Object.prototype.toString.call(body)}`;
43
+ }
44
+ function normalizeRequestInit(requestInit) {
45
+ if (!requestInit) {
46
+ return {};
47
+ }
48
+ const headers = new Headers(requestInit.headers);
49
+ const headerEntries = Array.from(headers.entries()).sort(([a], [b]) => a.localeCompare(b));
50
+ const normalized = {};
51
+ normalized.method = (requestInit.method ?? 'GET').toUpperCase();
52
+ normalized.mode = requestInit.mode ?? null;
53
+ normalized.cache = requestInit.cache ?? null;
54
+ normalized.credentials = requestInit.credentials ?? null;
55
+ normalized.redirect = requestInit.redirect ?? null;
56
+ normalized.referrer = requestInit.referrer ?? null;
57
+ normalized.referrerPolicy = requestInit.referrerPolicy ?? null;
58
+ normalized.integrity = requestInit.integrity ?? null;
59
+ normalized.keepalive = requestInit.keepalive ?? false;
60
+ normalized.priority = requestInit.priority ?? null;
61
+ normalized.headers = headerEntries;
62
+ normalized.body = toBodyFingerprint(requestInit.body);
63
+ return normalized;
64
+ }
65
+ function normalizeTextureLoadOptions(options) {
66
+ const colorSpace = options.colorSpace ?? 'srgb';
67
+ const normalized = {
68
+ colorSpace,
69
+ decode: {
70
+ colorSpaceConversion: options.decode?.colorSpaceConversion ?? (colorSpace === 'linear' ? 'none' : 'default'),
71
+ premultiplyAlpha: options.decode?.premultiplyAlpha ?? 'default',
72
+ imageOrientation: options.decode?.imageOrientation ?? 'none'
73
+ }
74
+ };
75
+ if (options.requestInit !== undefined) {
76
+ normalized.requestInit = options.requestInit;
77
+ }
78
+ if (options.signal !== undefined) {
79
+ normalized.signal = options.signal;
80
+ }
81
+ if (options.update !== undefined) {
82
+ normalized.update = options.update;
83
+ }
84
+ if (options.flipY !== undefined) {
85
+ normalized.flipY = options.flipY;
86
+ }
87
+ if (options.premultipliedAlpha !== undefined) {
88
+ normalized.premultipliedAlpha = options.premultipliedAlpha;
89
+ }
90
+ if (options.generateMipmaps !== undefined) {
91
+ normalized.generateMipmaps = options.generateMipmaps;
92
+ }
93
+ return normalized;
94
+ }
95
+ /**
96
+ * Builds deterministic resource cache key from full URL IO config.
97
+ */
98
+ export function buildTextureResourceCacheKey(url, options = {}) {
99
+ const normalized = normalizeTextureLoadOptions(options);
100
+ return JSON.stringify({
101
+ url,
102
+ colorSpace: normalized.colorSpace,
103
+ requestInit: normalizeRequestInit(normalized.requestInit),
104
+ decode: normalized.decode
105
+ });
106
+ }
107
+ /**
108
+ * Clears the internal texture resource cache.
109
+ */
110
+ export function clearTextureBlobCache() {
111
+ for (const entry of resourceCache.values()) {
112
+ if (!entry.settled) {
113
+ entry.controller.abort();
114
+ }
115
+ }
116
+ resourceCache.clear();
117
+ }
118
+ function acquireTextureBlob(url, options) {
119
+ const key = buildTextureResourceCacheKey(url, options);
120
+ const existing = resourceCache.get(key);
121
+ if (existing) {
122
+ existing.refs += 1;
123
+ let released = false;
124
+ return {
125
+ entry: existing,
126
+ release: () => {
127
+ if (released) {
128
+ return;
129
+ }
130
+ released = true;
131
+ existing.refs = Math.max(0, existing.refs - 1);
132
+ if (existing.refs === 0) {
133
+ if (!existing.settled) {
134
+ existing.controller.abort();
135
+ }
136
+ resourceCache.delete(key);
137
+ }
138
+ }
139
+ };
140
+ }
141
+ const normalized = normalizeTextureLoadOptions(options);
142
+ const controller = new AbortController();
143
+ const requestInit = {
144
+ ...(normalized.requestInit ?? {}),
145
+ signal: controller.signal
146
+ };
147
+ const entry = {
148
+ key,
149
+ refs: 1,
150
+ controller,
151
+ settled: false,
152
+ blobPromise: fetch(url, requestInit)
153
+ .then(async (response) => {
154
+ if (!response.ok) {
155
+ throw new Error(`Texture request failed (${response.status}) for ${url}`);
156
+ }
157
+ return response.blob();
158
+ })
159
+ .then((blob) => {
160
+ entry.settled = true;
161
+ return blob;
162
+ })
163
+ .catch((error) => {
164
+ resourceCache.delete(key);
165
+ throw error;
166
+ })
167
+ };
168
+ resourceCache.set(key, entry);
169
+ let released = false;
170
+ return {
171
+ entry,
172
+ release: () => {
173
+ if (released) {
174
+ return;
175
+ }
176
+ released = true;
177
+ entry.refs = Math.max(0, entry.refs - 1);
178
+ if (entry.refs === 0) {
179
+ if (!entry.settled) {
180
+ entry.controller.abort();
181
+ }
182
+ resourceCache.delete(key);
183
+ }
184
+ }
185
+ };
186
+ }
187
+ async function awaitWithAbort(promise, signal) {
188
+ if (!signal) {
189
+ return promise;
190
+ }
191
+ if (signal.aborted) {
192
+ throw createAbortError();
193
+ }
194
+ return new Promise((resolve, reject) => {
195
+ const onAbort = () => {
196
+ reject(createAbortError());
197
+ };
198
+ signal.addEventListener('abort', onAbort, { once: true });
199
+ promise.then(resolve, reject).finally(() => {
200
+ signal.removeEventListener('abort', onAbort);
201
+ });
202
+ });
203
+ }
204
+ /**
205
+ * Loads a single texture from URL and converts it to an `ImageBitmap`.
206
+ *
207
+ * @param url - Texture URL.
208
+ * @param options - Loading options.
209
+ * @returns Loaded texture object.
210
+ * @throws {Error} When runtime does not support `createImageBitmap` or request fails.
211
+ */
212
+ export async function loadTextureFromUrl(url, options = {}) {
213
+ if (typeof createImageBitmap !== 'function') {
214
+ throw new Error('createImageBitmap is not available in this runtime');
215
+ }
216
+ const normalized = normalizeTextureLoadOptions(options);
217
+ const { entry, release } = acquireTextureBlob(url, options);
218
+ let bitmap = null;
219
+ try {
220
+ const blob = await awaitWithAbort(entry.blobPromise, normalized.signal);
221
+ const bitmapOptions = {
222
+ colorSpaceConversion: normalized.decode.colorSpaceConversion,
223
+ premultiplyAlpha: normalized.decode.premultiplyAlpha,
224
+ imageOrientation: normalized.decode.imageOrientation
225
+ };
226
+ const allDefaults = bitmapOptions.colorSpaceConversion === 'default' &&
227
+ bitmapOptions.premultiplyAlpha === 'default' &&
228
+ bitmapOptions.imageOrientation === 'none';
229
+ bitmap = allDefaults
230
+ ? await createImageBitmap(blob)
231
+ : await createImageBitmap(blob, bitmapOptions);
232
+ if (normalized.signal?.aborted) {
233
+ bitmap.close();
234
+ throw createAbortError();
235
+ }
236
+ const loaded = {
237
+ url,
238
+ source: bitmap,
239
+ width: bitmap.width,
240
+ height: bitmap.height,
241
+ colorSpace: normalized.colorSpace,
242
+ dispose: () => {
243
+ bitmap?.close();
244
+ }
245
+ };
246
+ if (normalized.update !== undefined) {
247
+ loaded.update = normalized.update;
248
+ }
249
+ if (normalized.flipY !== undefined) {
250
+ loaded.flipY = normalized.flipY;
251
+ }
252
+ if (normalized.premultipliedAlpha !== undefined) {
253
+ loaded.premultipliedAlpha = normalized.premultipliedAlpha;
254
+ }
255
+ if (normalized.generateMipmaps !== undefined) {
256
+ loaded.generateMipmaps = normalized.generateMipmaps;
257
+ }
258
+ return loaded;
259
+ }
260
+ catch (error) {
261
+ if (bitmap) {
262
+ bitmap.close();
263
+ }
264
+ throw error;
265
+ }
266
+ finally {
267
+ release();
268
+ }
269
+ }
270
+ /**
271
+ * Loads many textures in parallel from URLs.
272
+ *
273
+ * @param urls - Texture URLs.
274
+ * @param options - Shared loading options.
275
+ * @returns Promise resolving to loaded textures in input order.
276
+ */
277
+ export async function loadTexturesFromUrls(urls, options = {}) {
278
+ const settled = await Promise.allSettled(urls.map((url) => loadTextureFromUrl(url, options)));
279
+ const loaded = [];
280
+ let firstError = null;
281
+ for (const entry of settled) {
282
+ if (entry.status === 'fulfilled') {
283
+ loaded.push(entry.value);
284
+ continue;
285
+ }
286
+ firstError ??= entry.reason;
287
+ }
288
+ if (firstError) {
289
+ for (const texture of loaded) {
290
+ texture.dispose();
291
+ }
292
+ throw firstError;
293
+ }
294
+ return loaded;
295
+ }