import { fail } from "./errors.ts"; /** Reserve all currently available slots atomically, never hold a partial group while waiting. */ export class RenderCapacity { #available: number; #closed = false; readonly #waiters = new Set<() => void>(); constructor(readonly capacity: number) { if (!Number.isSafeInteger(capacity) || capacity < 1) fail("INVALID_RENDER_OPTION", "渲染容量必须是正整数"); this.#available = capacity; } async acquire(requested: number, signal?: AbortSignal): Promise<{ count: number; release: (count?: number) => void }> { if (!Number.isSafeInteger(requested) || requested < 1) fail("INVALID_RENDER_OPTION", "申请容量必须是正整数"); while (true) { if (signal?.aborted) fail("RENDER_CANCELLED", "等待渲染资源时已取消"); if (this.#closed) fail("TIMELINE_INSTANCE_CLOSED", "渲染资源池已关闭"); if (this.#available > 0) break; await new Promise((resolve) => { const wake = () => { this.#waiters.delete(wake); signal?.removeEventListener("abort", wake); resolve(); }; this.#waiters.add(wake); signal?.addEventListener("abort", wake, { once: true }); }); } const count = Math.min(requested, this.#available); this.#available -= count; let held = count; return { count, release: (amount = held) => { if (!Number.isSafeInteger(amount) || amount < 0) fail("INVALID_RENDER_OPTION", "归还容量必须是非负整数"); const released = Math.min(held, amount); held -= released; this.#available += released; if (released > 0) for (const wake of [...this.#waiters]) wake(); } }; } close(): void { this.#closed = true; for (const wake of [...this.#waiters]) wake(); } } /** Keep each sampler on an absolute range so independent encoders can run together. */ export async function runFrameShards( frameCount: number, parallelism: number, samplers: number, sample: (frame: number, worker: number, signal: AbortSignal) => Promise, signal?: AbortSignal, ): Promise { if (!Number.isSafeInteger(frameCount) || frameCount < 0 || !Number.isSafeInteger(parallelism) || parallelism < 1 || !Number.isSafeInteger(samplers) || samplers < 1) { fail("INVALID_RENDER_OPTION", "采样帧数必须是非负整数,并发数必须是正整数"); } const count = Math.min(frameCount, Math.max(parallelism, samplers)); const size = Math.floor(frameCount / count); const extra = frameCount % count; await runRenderPool(count, samplers, async (shard, worker, workerSignal) => { const start = shard * size + Math.min(shard, extra); const end = start + size + (shard < extra ? 1 : 0); for (let frame = start; frame < end; frame++) { if (workerSignal.aborted) fail("RENDER_CANCELLED", "逐帧采样已取消"); await sample(frame, worker, workerSignal); } }, signal); } /** Stop assigning work on failure, cancel peers, and drain before releasing resources. */ export async function runRenderPool( count: number, concurrency: number, task: (index: number, worker: number, signal: AbortSignal) => Promise, signal?: AbortSignal, ): Promise { if (!Number.isSafeInteger(count) || count < 0 || !Number.isSafeInteger(concurrency) || concurrency < 1) { fail("INVALID_RENDER_OPTION", "渲染任务数量必须是非负整数,并发数必须是正整数"); } const controller = new AbortController(); const abort = () => controller.abort(); signal?.addEventListener("abort", abort, { once: true }); if (signal?.aborted) abort(); let cursor = 0; let failed = false; let failure: unknown; try { await Promise.all(Array.from({ length: Math.min(count, concurrency) }, async (_, worker) => { while (!controller.signal.aborted) { const index = cursor++; if (index >= count) return; try { await task(index, worker, controller.signal); } catch (error) { if (!failed) { failed = true; failure = error; } controller.abort(); } } })); if (failed) throw failure; if (signal?.aborted) fail("RENDER_CANCELLED", "渲染已取消"); } finally { signal?.removeEventListener("abort", abort); } }