Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118import { Effect, Schema } from 'effect'import { FieldKey, LutId } from './brands'import { numField, strField } from './layers/fields'import type { Layer, LayerType } from './layers/schemas'import type { LayerEntry } from './layers/registry'import type { LutCube } from './luts/cube'import { generateChainSource } from './shaders/chain-source'import type { ChainLayerInfo, ChainPass, ChainShader } from './shaders/chain-source'
export class GpuError extends Schema.TaggedErrorClass<GpuError>()('GpuError', { cause: Schema.optional(Schema.Unknown), message: Schema.String,}) {}
/** * Pack one pass's layer parameter values into a flat Float32Array * following the slot layout returned by the assembler. */function packUniforms(chain: readonly Layer[], pass: ChainPass): Float32Array { const buf = new Float32Array(pass.uniforms.length) const visibleLayers = chain.filter((l) => l.visible)
for (const slot of pass.uniforms) { const layer = visibleLayers[slot.layerIndex] if (layer) { // Schema-validated layers always carry the field as a number; skip // anything else rather than coerce garbage. const value = numField(layer, slot.field) if (!Number.isNaN(value)) { buf[slot.offset] = value } } }
return buf}
/** * Everything the GPU backend needs to render one frame: the assembled * chain passes, the packed uniforms, the source image, and a frame * counter (seeds `u_frame` for animated bodies like grain). * * The engine stops at building the request — WebGPU execution, canvas * presentation, and readback are the frontend's concern (it owns the * device and the canvas). */export interface RenderRequest { readonly shader: ChainShader /** Packed uniforms, one Float32Array per pass (aligned with shader.passes). */ readonly uniforms: readonly Float32Array[] readonly srcBitmap: ImageBitmap readonly frame: number /** * Cubes referenced by LUT layers in the chain, keyed by lutId. The GPU * backend uploads each cube to a 3D texture once and caches it. */ readonly luts: ReadonlyMap<LutId, LutCube>}
/** * Build a render request for the given ordered chain of adjustment * layers. An empty chain (or all layers hidden) is valid — the * assembler emits a passthrough shader, so the request presents the * source image unchanged. */export const createRenderRequest = Effect.fn('createRenderRequest')(function* ( chain: readonly Layer[], registry: Record<LayerType, LayerEntry>, srcBitmap: ImageBitmap, frame: number, luts: ReadonlyMap<LutId, LutCube>,) { const chainLayers: ChainLayerInfo[] = [] for (const l of chain) { if (!l.visible) continue
const entry = registry[l.type] if (!entry) { return yield* Effect.fail(new GpuError({ message: `Unknown layer type: ${l.type}` })) }
// LUT layers carry a cube reference: resolve the id through the // LUT map the caller provided. The engine stays pure — it never // fetches or parses cubes, and an unresolvable id is a hard error. if (l.type === 'lut') { const lutId = strField(l, FieldKey('lutId')) if (lutId === '') { return yield* Effect.fail(new GpuError({ message: 'LUT layer is missing a lutId' })) } const id = LutId(lutId) const cube = luts.get(id) if (!cube) { return yield* Effect.fail(new GpuError({ message: `Unknown LUT: ${id}` })) } chainLayers.push({ body: entry.body, fieldKeys: Object.keys(entry.fields).map(FieldKey), lut: { id, size: cube.size }, type: l.type, }) } else { chainLayers.push({ body: entry.body, fieldKeys: Object.keys(entry.fields).map(FieldKey), type: l.type, }) } }
const shader = yield* generateChainSource(chainLayers).pipe( Effect.mapError((cause) => new GpuError({ cause, message: 'Shader generation failed' })), )
const uniforms = shader.passes.map((pass) => packUniforms(chain, pass))
return { frame, luts, shader, srcBitmap, uniforms }})