/** * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal */ import { RenderableSchema, Values, UnboxedValues, UniformSpec, TextureSpec, DefineSpec, RenderableValues } from '../renderable/schema'; import { WebGLContext } from '../webgl/context'; import { getRegularGrid3dDelta, RegularGrid3d } from '../../mol-math/geometry/common'; import { grid3dTemplate_frag } from '../shader/util/grid3d-template.frag'; import { quad_vert } from '../shader/quad.vert'; import { ShaderCode } from '../shader-code'; import { UUID, ValueCell } from '../../mol-util'; import { objectForEach } from '../../mol-util/object'; import { getUniformGlslType, isUniformValueScalar } from '../webgl/uniform'; import { QuadSchema, QuadValues } from './util'; import { createComputeRenderItem } from '../webgl/render-item'; import { createComputeRenderable } from '../renderable'; import { isLittleEndian } from '../../mol-util/is-little-endian'; import { RuntimeContext } from '../../mol-task'; export function canComputeGrid3dOnGPU(webgl?: WebGLContext) { return !!webgl?.extensions.textureFloat; } export interface Grid3DComputeRenderableSpec { schema: S, // indicate which params are loop bounds for WebGL1 compat loopBounds?: (keyof S)[] utilCode?: string, mainCode: string, returnCode: string, values(params: P, grid: RegularGrid3d): UnboxedValues, cumulative?: { states(params: P): CS[], update(params: P, state: CS, values: Values): void, // call gl.readPixes every 'yieldPeriod' states to split the computation // into multiple parts, if not set, the computation will be synchronous yieldPeriod?: number } } const FrameBufferName = 'grid3d-computable' as const; const Texture0Name = 'grid3d-computable-0' as const; const Texture1Name = 'grid3d-computable-1' as const; const SchemaBase = { ...QuadSchema, uDimensions: UniformSpec('v3'), uMin: UniformSpec('v3'), uDelta: UniformSpec('v3'), uWidth: UniformSpec('f'), uLittleEndian: UniformSpec('b'), }; const CumulativeSumSchema = { tCumulativeSum: TextureSpec('texture', 'rgba', 'ubyte', 'nearest') }; export function createGrid3dComputeRenderable(spec: Grid3DComputeRenderableSpec) { const id = UUID.create22(); const uniforms: string[] = []; objectForEach(spec.schema, (u, k) => { if (u.type === 'define') return; if (u.kind.indexOf('[]') >= 0) throw new Error('array uniforms are not supported'); const isBound = (spec.loopBounds?.indexOf(k) ?? -1) >= 0; if (isBound) uniforms.push(`#ifndef ${k}`); if (u.type === 'uniform') uniforms.push(`uniform ${getUniformGlslType(u.kind as any)} ${k};`); else if (u.type === 'texture') uniforms.push(`uniform sampler2D ${k};`); if (isBound) uniforms.push(`#endif`); }); const code = grid3dTemplate_frag .replace('{UNIFORMS}', uniforms.join('\n')) .replace('{UTILS}', spec.utilCode ?? '') .replace('{MAIN}', spec.mainCode) .replace('{RETURN}', spec.returnCode); const shader = ShaderCode(id, quad_vert, code); return async (ctx: RuntimeContext, webgl: WebGLContext, grid: RegularGrid3d, params: P) => { const schema: RenderableSchema = { ...SchemaBase, ...(spec.cumulative ? CumulativeSumSchema : {}), ...spec.schema, }; if (!webgl.isWebGL2) { if (spec.loopBounds) { for (const b of spec.loopBounds) { (schema as any)[b] = DefineSpec('number'); } } (schema as any)['WEBGL1'] = DefineSpec('boolean'); } if (spec.cumulative) { (schema as any)['CUMULATIVE'] = DefineSpec('boolean'); } if (!webgl.namedFramebuffers[FrameBufferName]) { webgl.namedFramebuffers[FrameBufferName] = webgl.resources.framebuffer(); } const framebuffer = webgl.namedFramebuffers[FrameBufferName]; if (!webgl.namedTextures[Texture0Name]) { webgl.namedTextures[Texture0Name] = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest'); } if (spec.cumulative && !webgl.namedTextures[Texture1Name]) { webgl.namedTextures[Texture1Name] = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest'); } const tex = [webgl.namedTextures[Texture0Name], webgl.namedTextures[Texture1Name]]; const [nx, ny, nz] = grid.dimensions; const uWidth = Math.ceil(Math.sqrt(nx * ny * nz)); const values: UnboxedValues = { uDimensions: grid.dimensions, uMin: grid.box.min, uDelta: getRegularGrid3dDelta(grid), uWidth, uLittleEndian: isLittleEndian(), ...spec.values(params, grid) } as any; if (!webgl.isWebGL2) { (values as any).WEBGL1 = true; } if (spec.cumulative) { (values as any).tCumulativeSum = tex[0]; (values as any).CUMULATIVE = true; } let renderable = webgl.namedComputeRenderables[id]; let cells: RenderableValues; if (renderable) { cells = renderable.values as RenderableValues; objectForEach(values, (c, k) => { const s = schema[k]; if (s?.type === 'value' || s?.type === 'attribute') return; if (!s || !isUniformValueScalar(s.kind as any)) { ValueCell.update(cells[k], c); } else { ValueCell.updateIfChanged(cells[k], c); } }); } else { cells = {} as any; objectForEach(QuadValues, (v, k) => (cells as any)[k] = v); objectForEach(values, (v, k) => (cells as any)[k] = ValueCell.create(v)); renderable = createComputeRenderable(createComputeRenderItem(webgl, 'triangles', shader, schema, cells), cells); } const array = new Uint8Array(uWidth * uWidth * 4); if (spec.cumulative) { const { gl } = webgl; const states = spec.cumulative.states(params); tex[0].define(uWidth, uWidth); tex[1].define(uWidth, uWidth); resetGl(webgl, uWidth); gl.clearColor(0, 0, 0, 0); tex[0].attachFramebuffer(framebuffer, 'color0'); gl.clear(gl.COLOR_BUFFER_BIT); tex[1].attachFramebuffer(framebuffer, 'color0'); gl.clear(gl.COLOR_BUFFER_BIT); if (spec.cumulative.yieldPeriod) { await ctx.update({ message: 'Computing...', isIndeterminate: false, current: 0, max: states.length }); } const yieldPeriod = Math.max(1, spec.cumulative.yieldPeriod ?? 1 | 0); for (let i = 0; i < states.length; i++) { ValueCell.update(cells.tCumulativeSum, tex[(i + 1) % 2]); tex[i % 2].attachFramebuffer(framebuffer, 'color0'); resetGl(webgl, uWidth); spec.cumulative.update(params, states[i], cells as any); renderable.update(); renderable.render(); if (spec.cumulative.yieldPeriod && i !== states.length - 1) { if (i % yieldPeriod === yieldPeriod - 1) { webgl.readPixels(0, 0, 1, 1, array); } if (ctx.shouldUpdate) { await ctx.update({ current: i + 1 }); } } } } else { tex[0].define(uWidth, uWidth); tex[0].attachFramebuffer(framebuffer, 'color0'); framebuffer.bind(); resetGl(webgl, uWidth); renderable.update(); renderable.render(); } webgl.readPixels(0, 0, uWidth, uWidth, array); return new Float32Array(array.buffer, array.byteOffset, nx * ny * nz); }; } function resetGl(webgl: WebGLContext, w: number) { const { gl, state } = webgl; gl.viewport(0, 0, w, w); gl.scissor(0, 0, w, w); state.disable(gl.SCISSOR_TEST); state.disable(gl.BLEND); state.disable(gl.DEPTH_TEST); state.depthMask(false); }