1/**
2 * A compiled solver session together with the selection it currently has
3 * applied.
4 *
5 * Which changes are cheap and which are not is a property of the solver, not
6 * of any front end: parameters are a uniform upload, a geometry change
7 * re-evaluates the surface, and a model change recompiles everything, since
8 * the model's step is compiled into the GPU pipelines. Both the page and the
9 * command line need that distinction — a walk through the parameter space
10 * spends its whole time on the cheap side of it — so it lives here rather
11 * than in either of them.
12 */
13import { ModelSession } from '../mgpu/session.ts';
14import { mModelByKey, type MModel, type Params } from '../mgpu/registry.ts';
15import { mGeometryByKey } from '../geom/registry.ts';
16import type { CacheSpec } from './spec.ts';
18/** Cap on GPU dispatches per submission (watchdog safety; see turing-surface). */
19const DISPATCH_BUDGET = 1000;
21export interface SolverEvents {
22 /** A model change costs a recompile — a second or two on a real GPU. */
23 onCompiling?(model: MModel): void;
24 /**
25 * The surface has changed (a new session, or a new geometry in the running
26 * one), so anything drawing it must be rebuilt. Awaited, so a caller that
27 * rebuilds a mesh finishes before the session is used.
28 */
29 onSurface?(): Promise<void> | void;
30}
32export class SolverSession {
33 session: ModelSession | null = null;
34 /** The model the session is compiled for. */
35 model: MModel;
36 /**
37 * Steps per GPU submission, sized on every compile so one submission stays
38 * under the dispatch budget however expensive niter has made a step.
39 */
40 stepsPerSubmit = 4;
42 #modelKey = '';
43 #geomKey = '';
44 #geomParams: Params = {};
46 constructor(
47 readonly device: GPUDevice,
48 /** Render grid fineness; 1 (the default) allocates no display plan. */
49 readonly oversample = 1,
50 private readonly events: SolverEvents = {},
51 ) {
52 this.model = mModelByKey('schnakenberg')!;
53 }
55 /** The session, or a thrown error rather than a silent no-op. */
56 get live(): ModelSession {
57 if (!this.session) throw new Error('no solver session');
58 return this.session;
59 }
61 /**
62 * Bring the session in line with a spec, doing the least work that will do:
63 * a uniform upload for parameters, a surface re-evaluation for a geometry
64 * change, a full recompile for a model change.
65 */
66 async apply(spec: CacheSpec): Promise<void> {
67 if (!this.session || spec.model !== this.#modelKey) {
68 await this.#rebuild(spec);
69 return;
70 }
71 this.session.setParams(spec.params);
72 const geomChanged =
73 spec.geometry !== this.#geomKey ||
74 JSON.stringify(spec.geometryParams) !== JSON.stringify(this.#geomParams);
75 if (!geomChanged) return;
76 await this.session.setGeometry(mGeometryByKey(spec.geometry)!, spec.geometryParams);
77 this.#geomKey = spec.geometry;
78 this.#geomParams = { ...spec.geometryParams };
79 await this.events.onSurface?.();
80 }
82 async #rebuild(spec: CacheSpec): Promise<void> {
83 const nextModel = mModelByKey(spec.model)!;
84 this.session?.destroy();
85 this.session = null;
86 this.#modelKey = '';
87 this.events.onCompiling?.(nextModel);
88 this.session = await ModelSession.create({
89 device: this.device,
90 model: nextModel,
91 params: spec.params,
92 lmax: spec.lmax,
93 oversample: this.oversample,
94 geometry: mGeometryByKey(spec.geometry)!,
95 geometryParams: spec.geometryParams,
96 niter: spec.niter,
97 lam3: spec.lam3,
98 });
99 this.model = nextModel;
100 this.#modelKey = spec.model;
101 this.#geomKey = spec.geometry;
102 this.#geomParams = { ...spec.geometryParams };
103 const opsPerStep = Math.max(1, this.session.describe().step.length);
104 this.stepsPerSubmit = Math.max(1, Math.floor(DISPATCH_BUDGET / opsPerStep));
105 await this.events.onSurface?.();
106 }
108 destroy(): void {
109 this.session?.destroy();
110 this.session = null;
111 this.#modelKey = '';
112 }
113}