chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
45
web/app/src/engine-client/gpu-compute-relay.ts
Normal file
45
web/app/src/engine-client/gpu-compute-relay.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { COMPUTE_LIMITS } from '../../../protocol/gpu-compute';
|
||||
export type ComputeEndpoint = EventTarget & { postMessage(message: unknown): void; terminate(): void };
|
||||
export class RemoteComputeEndpoint extends EventTarget implements ComputeEndpoint {
|
||||
private readonly id = crypto.randomUUID();
|
||||
private closed = false;
|
||||
private readonly receive = (event: MessageEvent) => {
|
||||
const data = event.data; if(this.closed || data?.id !== this.id)return;
|
||||
if(data.type === 'ready')this.dispatchEvent(new MessageEvent('message', { data: { type: 'gpu-compute-ready' } })); else if(data.type === 'error')this.dispatchEvent(new ErrorEvent('error', { message: String(data.message) }));
|
||||
};
|
||||
constructor(private readonly port: MessagePort) { super(); port.addEventListener('message', this.receive); port.start(); port.postMessage({ type: 'prepare', id: this.id }); }
|
||||
postMessage(message: unknown): void { if(this.closed)throw new Error('GPU compute endpoint closed'); this.port.postMessage({ type: 'dispatch', id: this.id, request: message }); }
|
||||
terminate(): void { if(this.closed)return; this.closed = true; this.port.postMessage({ type: 'close', id: this.id }); this.port.removeEventListener('message', this.receive); }
|
||||
}
|
||||
type PendingCompute = { control: SharedArrayBuffer; error: SharedArrayBuffer };
|
||||
type ComputeEntry = { worker: Worker; ready: boolean; pending: Set<PendingCompute> };
|
||||
export function attachPageComputeRelay(engine: Worker): void {
|
||||
const { port1, port2 } = new MessageChannel(), entries = new Map<string, ComputeEntry>(); let closed = false;
|
||||
const fail = (pending: PendingCompute, message: string) => {
|
||||
const control = new Int32Array(pending.control), error = new Uint8Array(pending.error); if(Atomics.load(control, 0) !== 0)return;
|
||||
const bytes = new TextEncoder().encode(message).subarray(0, error.length); error.set(bytes); Atomics.store(control, 1, bytes.length); Atomics.compareExchange(control, 0, 0, -1); Atomics.notify(control, 0);
|
||||
};
|
||||
const close = (id: string, message: string) => { const entry = entries.get(id); if(!entry)return; entries.delete(id); for(const pending of entry.pending)fail(pending, message); entry.worker.terminate(); };
|
||||
port1.onmessage = ({ data }) => {
|
||||
if(closed || typeof data?.id !== 'string')return; const id = data.id;
|
||||
if(data.type === 'prepare'){
|
||||
if(entries.has(id)){port1.postMessage({ type: 'error', id, message: 'GPU compute endpoint already exists' }); return;}
|
||||
try {
|
||||
const worker = new Worker(new URL('../workers/gpu-compute.worker.ts', import.meta.url), { type: 'module' }), entry: ComputeEntry = { worker, ready: false, pending: new Set() }; entries.set(id, entry);
|
||||
worker.onmessage = event => { if(closed || entries.get(id) !== entry)return; if(event.data?.type === 'gpu-compute-ready'){entry.ready = true; port1.postMessage({ type: 'ready', id });} };
|
||||
worker.onerror = event => { event.preventDefault(); if(entries.get(id) !== entry)return; const message = event.message || 'GPU compute worker failed'; close(id, message); port1.postMessage({ type: 'error', id, message }); };
|
||||
} catch(error) { close(id, String(error)); port1.postMessage({ type: 'error', id, message: String(error) }); }
|
||||
} else if(data.type === 'close'){close(id, 'GPU compute endpoint closed');
|
||||
} else if(data.type === 'dispatch'){
|
||||
const entry = entries.get(id), request = data.request;
|
||||
const pending: PendingCompute | null = request?.control instanceof SharedArrayBuffer && request.control.byteLength === 8 && request.error instanceof SharedArrayBuffer && request.error.byteLength === COMPUTE_LIMITS.errorBytes ? request : null;
|
||||
if(!entry?.ready){if(pending)fail(pending, 'GPU compute relay not ready'); return;}
|
||||
for(const item of entry.pending)if(Atomics.load(new Int32Array(item.control), 0) !== 0)entry.pending.delete(item); if(pending)entry.pending.add(pending);
|
||||
try{entry.worker.postMessage(request);}catch(error){close(id, String(error)); port1.postMessage({ type: 'error', id, message: String(error) });}
|
||||
}
|
||||
};
|
||||
const terminate = engine.terminate.bind(engine);
|
||||
engine.terminate = () => { if(!closed){closed = true; for(const id of [...entries.keys()])close(id, 'Engine compute relay disposed'); port1.close();} terminate(); };
|
||||
port1.start();
|
||||
try{engine.postMessage({ type: 'native-gpu-compute-port', port: port2 }, [port2]);}catch(error){engine.terminate(); throw error;}
|
||||
}
|
||||
Reference in New Issue
Block a user