1/**
2 * Backend abstraction over the spherical harmonic transform, mirroring the
3 * websph "porting boundary": the solver only ever needs coeffs->vals,
4 * vals->coeffs, and the grid. Spectral layout is the SHTNS convention used
5 * by shtns-webgpu (see src/sht/layout.ts): complex interleaved [re, im],
6 * m >= 0 only, m-major ordering, orthonormal + Condon-Shortley.
7 */
8import { ShtPlan, requestShtDevice } from '../sht/sht.ts';
9import { ShtReference } from '../sht/reference.ts';
10import type { ShtConfig } from '../sht/layout.ts';
12export interface ShtBackend {
13 readonly cfg: ShtConfig;
14 readonly nlm: number;
15 /** cos(colatitude), length nlat, decreasing (north to south). */
16 readonly cosTheta: Float64Array;
17 readonly kind: 'webgpu' | 'cpu';
18 synth(qlm: Float64Array): Promise<Float32Array | Float64Array>;
19 analys(spat: Float64Array): Promise<Float32Array | Float64Array>;
20 destroy(): void;
21}
23/** fp32 WebGPU backend (fast path). */
24export class GpuBackend implements ShtBackend {
25 readonly kind = 'webgpu';
26 readonly cfg: ShtConfig;
27 readonly nlm: number;
28 readonly cosTheta: Float64Array;
29 #plan: ShtPlan;
30 #qlm32: Float32Array;
31 #spat32: Float32Array;
33 private constructor(plan: ShtPlan) {
34 this.#plan = plan;
35 this.cfg = plan.cfg;
36 this.nlm = plan.nlm;
37 this.cosTheta = plan.cosTheta;
38 this.#qlm32 = new Float32Array(2 * plan.nlm);
39 this.#spat32 = new Float32Array(plan.cfg.nlat * plan.cfg.nphi);
40 }
42 static async create(device: GPUDevice, cfg: ShtConfig): Promise<GpuBackend> {
43 return new GpuBackend(await ShtPlan.create(device, cfg));
44 }
46 synth(qlm: Float64Array): Promise<Float32Array> {
47 this.#qlm32.set(qlm);
48 return this.#plan.synth(this.#qlm32);
49 }
51 analys(spat: Float64Array): Promise<Float32Array> {
52 this.#spat32.set(spat);
53 return this.#plan.analys(this.#spat32);
54 }
56 destroy(): void {
57 this.#plan.destroy();
58 }
59}
61/** Best-effort human-readable adapter name, so it is clear which GPU (or
62 * software rasterizer) is actually running the transforms. */
63export async function describeAdapter(device: GPUDevice): Promise<string> {
64 const fmt = (info: GPUAdapterInfo | undefined): string => {
65 if (!info) return '';
66 const parts = [info.description, info.device, info.vendor].filter(
67 (s): s is string => !!s && s.length > 0,
68 );
69 const name = parts[0] ?? '';
70 return info.architecture && !name.includes(info.architecture)
71 ? `${name} (${info.architecture})`.trim()
72 : name;
73 };
74 const own = fmt((device as GPUDevice & { adapterInfo?: GPUAdapterInfo }).adapterInfo);
75 if (own) return own;
76 try {
77 const adapter = await navigator.gpu.requestAdapter();
78 return fmt(adapter?.info);
79 } catch {
80 return '';
81 }
82}
84/** f64 CPU backend by direct summation (slow; tests and no-WebGPU fallback). */
85export class CpuBackend implements ShtBackend {
86 readonly kind = 'cpu';
87 readonly cfg: ShtConfig;
88 readonly nlm: number;
89 readonly cosTheta: Float64Array;
90 #ref: ShtReference;
92 constructor(cfg: ShtConfig) {
93 this.#ref = new ShtReference(cfg);
94 this.cfg = cfg;
95 this.nlm = this.#ref.nlm;
96 this.cosTheta = this.#ref.ct;
97 }
99 synth(qlm: Float64Array): Promise<Float64Array> {
100 return Promise.resolve(this.#ref.synth(qlm));
101 }
103 analys(spat: Float64Array): Promise<Float64Array> {
104 return Promise.resolve(this.#ref.analys(spat));
105 }
107 destroy(): void {}
108}
110export { requestShtDevice };