1793cecTime-domain nonlinear shell cymbal after Samejima (2021)Jeremy Magland 1// Hook owning the render worker. Unlike the sibling cymbal app there is no
2// debounced auto-preview: a cymbal2 render is a full time-domain integration
3// and takes from tens of seconds (preview) to minutes (full quality) in the
4// browser, so renders are explicit. A request in flight can be cancelled,
5// which tears the worker down and builds a fresh one.
7import { useCallback, useEffect, useRef, useState } from 'react';
8import type { Cymbal2Params } from '../params';
9import type { MFile } from '../mfiles';
10import type { ErrorMessage, ProgressMessage, RenderResponse } from '../worker/renderWorker';
12export interface RenderError {
13 message: string;
14 file?: string;
15 line?: number;
16}
18export interface RenderState {
19 result: RenderResponse | null;
20 error: RenderError | null;
21 busy: boolean;
22 stage: string;
23 elapsedS: number;
24}
26export function useRenderer(params: Cymbal2Params, mfiles: MFile[]) {
27 const workerRef = useRef<Worker | null>(null);
28 const nextId = useRef(1);
29 const inFlight = useRef<number | null>(null);
30 const startedAt = useRef(0);
31 const [state, setState] = useState<RenderState>({
32 result: null,
33 error: null,
34 busy: false,
35 stage: '',
36 elapsedS: 0,
37 });
39 const attachHandlers = useCallback((worker: Worker) => {
40 worker.onmessage = (ev: MessageEvent<RenderResponse | ProgressMessage | ErrorMessage>) => {
41 const msg = ev.data;
42 if (msg.type === 'progress') {
65ecc03Run the time loop on the GPU in the web app when WebGPU is availableJeremy Magland 43 const pct = msg.stage === 'integrate' && msg.frac > 0.1
44 ? ` ${Math.round(((msg.frac - 0.1) / 0.85) * 100)}%`
45 : '';
46 setState((s) => ({ ...s, stage: msg.stage + pct }));
48 }
49 inFlight.current = null;
50 if (msg.type === 'error') {
51 setState((s) => ({
52 ...s,
53 error: { message: msg.message, file: msg.file, line: msg.line },
54 busy: false,
55 stage: '',
56 }));
57 return;
58 }
59 setState((s) => ({ ...s, result: msg, error: null, busy: false, stage: '' }));
60 };
61 }, []);
63 const makeWorker = useCallback(() => {
64 const worker = new Worker(new URL('../worker/renderWorker.ts', import.meta.url), {
65 type: 'module',
66 });
67 attachHandlers(worker);
68 workerRef.current = worker;
69 return worker;
70 }, [attachHandlers]);
72 useEffect(() => {
73 const worker = makeWorker();
74 return () => {
75 worker.terminate();
76 workerRef.current = null;
77 inFlight.current = null;
78 };
79 }, [makeWorker]);
81 // elapsed-time ticker while a render is in flight
82 useEffect(() => {
83 if (!state.busy) return;
84 const t = setInterval(
85 () => setState((s) => ({ ...s, elapsedS: (performance.now() - startedAt.current) / 1000 })),
86 250,
87 );
88 return () => clearInterval(t);
89 }, [state.busy]);
91 const render = useCallback(
92 (quality: 'preview' | 'full') => {
93 const worker = workerRef.current;
94 if (!worker || inFlight.current !== null) return;
95 const id = nextId.current++;
96 inFlight.current = id;
97 startedAt.current = performance.now();
98 setState((s) => ({ ...s, busy: true, stage: 'starting', elapsedS: 0 }));
99 worker.postMessage({ type: 'render', id, params, mfiles, quality });
100 },
101 [params, mfiles],
102 );
104 const cancel = useCallback(() => {
105 const worker = workerRef.current;
106 if (worker) worker.terminate();
107 inFlight.current = null;
108 makeWorker();
109 setState((s) => ({ ...s, busy: false, stage: '', elapsedS: 0 }));
110 }, [makeWorker]);
112 const renderPreview = useCallback(() => render('preview'), [render]);
113 const renderFull = useCallback(() => render('full'), [render]);
115 return { ...state, renderPreview, renderFull, cancel };
116}