From b6d867c87832721961a998c8c68c014d4d759d5a Mon Sep 17 00:00:00 2001 From: Adam Abeshouse Date: Tue, 1 Jun 2021 17:54:18 -0400 Subject: [PATCH 01/20] Dont delete and recreate buffers unnecessarily -> memory and time savings Signed-off-by: Adam Abeshouse --- src/js/oncoprintwebglcellview.ts | 98 +++++++++++++++++++++++--------- 1 file changed, 71 insertions(+), 27 deletions(-) diff --git a/src/js/oncoprintwebglcellview.ts b/src/js/oncoprintwebglcellview.ts index 9715f03..91800cf 100644 --- a/src/js/oncoprintwebglcellview.ts +++ b/src/js/oncoprintwebglcellview.ts @@ -92,6 +92,12 @@ export default class OncoprintWebGLCellView { private vertex_position_buffer:TrackProp = {}; private vertex_color_buffer:TrackProp = {}; private vertex_column_buffer:TrackProp = {}; + private is_buffer_empty:TrackProp<{ + position:boolean; + color:boolean; + column:boolean; + color_texture:boolean; + }> = {}; private color_texture:TrackProp<{texture: WebGLTexture, size:number}> = {}; private id_to_first_vertex_index:TrackProp> = {}; // index of first vertex corresponding to given id for given track, e.g. 0, 3, 6, ... @@ -734,17 +740,10 @@ export default class OncoprintWebGLCellView { tracks_to_clear = [track_id]; } for (let i=0; i Date: Tue, 8 Jun 2021 11:24:03 -0400 Subject: [PATCH 02/20] make release rendering not refire at 0 Signed-off-by: Adam Abeshouse --- src/js/oncoprint.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/js/oncoprint.ts b/src/js/oncoprint.ts index a42fa56..336fe5b 100644 --- a/src/js/oncoprint.ts +++ b/src/js/oncoprint.ts @@ -1175,17 +1175,19 @@ export default class Oncoprint { if(this.webgl_unavailable || this.destroyed) { return; } - this.model.rendering_suppressed_depth -= 1; - this.model.rendering_suppressed_depth = Math.max(0, this.model.rendering_suppressed_depth); - if (this.model.rendering_suppressed_depth === 0) { - this.label_view.releaseRendering(this.model, this.getCellViewHeight); - this.header_view.releaseRendering(this.model); - this.cell_view.releaseRendering(this.model); - this.track_options_view.releaseRendering(this.model, this.getCellViewHeight); - this.track_info_view.releaseRendering(this.model, this.getCellViewHeight); - this.legend_view.releaseRendering(this.model); - this.minimap_view.releaseRendering(this.model, this.cell_view); - this.resizeAndOrganizeAfterTimeout(onComplete); + if (this.model.rendering_suppressed_depth > 0) { + this.model.rendering_suppressed_depth -= 1; + this.model.rendering_suppressed_depth = Math.max(0, this.model.rendering_suppressed_depth); + if (this.model.rendering_suppressed_depth === 0) { + this.label_view.releaseRendering(this.model, this.getCellViewHeight); + this.header_view.releaseRendering(this.model); + this.cell_view.releaseRendering(this.model); + this.track_options_view.releaseRendering(this.model, this.getCellViewHeight); + this.track_info_view.releaseRendering(this.model, this.getCellViewHeight); + this.legend_view.releaseRendering(this.model); + this.minimap_view.releaseRendering(this.model, this.cell_view); + this.resizeAndOrganizeAfterTimeout(onComplete); + } } } From 6719e08db024392d1ab38c0fa6e6d0e3da75ddb8 Mon Sep 17 00:00:00 2001 From: Adam Abeshouse Date: Thu, 3 Jun 2021 12:39:41 -0400 Subject: [PATCH 03/20] Use new universal shapes system to optimize shapes that are drawn for every cell (e.g. default gray rect) Signed-off-by: Adam Abeshouse --- src/js/oncoprintmodel.ts | 47 ++- src/js/oncoprintruleset.ts | 44 +-- src/js/oncoprintwebglcellview.ts | 335 ++++++++++---------- src/js/shaders.ts | 106 +++++++ src/js/utils.ts | 13 + src/test/gradientCategoricalRuleset.spec.ts | 2 +- 6 files changed, 347 insertions(+), 200 deletions(-) create mode 100644 src/js/shaders.ts diff --git a/src/js/oncoprintmodel.ts b/src/js/oncoprintmodel.ts index 003639c..0bd8f0f 100644 --- a/src/js/oncoprintmodel.ts +++ b/src/js/oncoprintmodel.ts @@ -6,7 +6,7 @@ import CachedProperty from './CachedProperty'; import {hclusterColumns, hclusterTracks} from './clustering'; import $ from 'jquery'; import * as BucketSort from "./bucketsort"; -import {cloneShallow, doesCellIntersectPixel, ifndef} from "./utils"; +import {cloneShallow, doesCellIntersectPixel, ifndef, z_comparator} from "./utils"; import _ from "lodash"; import {RuleSet, RuleSetParams, RuleWithId} from "./oncoprintruleset"; import {InitParams} from "./oncoprint"; @@ -828,7 +828,36 @@ export default class OncoprintModel { } }; - public getIdentifiedShapeListList(track_id:TrackId, use_base_size:boolean, sort_by_z:boolean):IdentifiedShapeList[] { + public getAlwaysShapes( + track_id:TrackId, + use_base_size:boolean, + sort_by_z:boolean + ):ComputedShapeParams[] { + const universalRule = this.getRuleSet(track_id).getUniversalRule(); + if (!universalRule) { + return []; + } + const spacing = this.getTrackHasColumnSpacing(track_id); + const width = this.getCellWidth(use_base_size) + (!spacing ? this.getCellPadding(use_base_size, true) : 0); + const height = this.getCellHeight(track_id, use_base_size); + const shapes = universalRule.rule.apply( + {}, // a universal rule does not rely on anything specific to the data + width, + height + ) + + if (sort_by_z) { + shapes.sort(z_comparator); + } + + return shapes; + } + + public getSpecificShapesForData( + track_id:TrackId, + use_base_size:boolean, + sort_by_z:boolean + ):IdentifiedShapeList[] { const active_rules = {}; const data = this.getTrackData(track_id); const id_key = this.getTrackDataIdKey(track_id); @@ -840,18 +869,6 @@ export default class OncoprintModel { this.setTrackActiveRules(track_id, active_rules); - - function z_comparator(shapeA:ComputedShapeParams, shapeB:ComputedShapeParams) { - const zA = shapeA.z; - const zB = shapeB.z; - if (zA < zB) { - return -1; - } else if (zA > zB) { - return 1; - } else { - return 0; - } - } return shapes.map(function(shape_list:ComputedShapeParams[], index:number) { if (sort_by_z) { shape_list.sort(z_comparator); @@ -866,7 +883,7 @@ export default class OncoprintModel { public getActiveRules(rule_set_id:RuleSetId) { const rule_set_active_rules = this.rule_set_active_rules[rule_set_id]; if (rule_set_active_rules) { - return this.rule_sets[rule_set_id].getRulesWithId().filter(function(rule_with_id:RuleWithId) { + return this.rule_sets[rule_set_id].getSpecificRulesForDatum().filter(function(rule_with_id:RuleWithId) { return !!rule_set_active_rules[rule_with_id.id]; }); } else { diff --git a/src/js/oncoprintruleset.ts b/src/js/oncoprintruleset.ts index f4ed917..8533345 100644 --- a/src/js/oncoprintruleset.ts +++ b/src/js/oncoprintruleset.ts @@ -311,6 +311,7 @@ export class RuleSet { public exclude_from_legend?:boolean; protected active_rule_ids:ActiveRules; protected rules_with_id:RuleWithId[]; + protected universal_rule?:RuleWithId; constructor(params:Omit) { /* params: @@ -323,7 +324,6 @@ export class RuleSet { this.exclude_from_legend = params.exclude_from_legend; this.active_rule_ids = {}; this.rules_with_id = []; - } public getLegendLabel() { @@ -349,6 +349,10 @@ export class RuleSet { return rule_id; } + public setUniversalRule(r:RuleWithId) { + this.universal_rule = r; + } + public removeRule(rule_id:RuleId) { var index = -1; for (let i = 0; i < this.rules_with_id.length; i++) { @@ -399,10 +403,14 @@ export class RuleSet { return shapes; } - public getRulesWithId(datum?:Datum):RuleWithId[] { + public getSpecificRulesForDatum(datum?:Datum):RuleWithId[] { throw "Not implemented on base class"; } + public getUniversalRule() { + return this.universal_rule; + } + public apply(data:Datum[], cell_width:number, cell_height:number, out_active_rules?:ActiveRules|undefined, data_id_key?:string&keyof Datum, important_ids?:ColumnProp) { // Returns a list of lists of concrete shapes, in the same order as data // optional parameter important_ids determines which ids count towards active rules (optional parameter data_id_key @@ -411,7 +419,7 @@ export class RuleSet { for (var i = 0; i < data.length; i++) { var datum = data[i]; var should_mark_active = !important_ids || !!important_ids[datum[data_id_key!]]; - var rules = this.getRulesWithId(datum); + var rules = this.getSpecificRulesForDatum(datum); if (typeof out_active_rules !== 'undefined' && should_mark_active) { for (let j = 0; j < rules.length; j++) { out_active_rules[rules[j].id] = true; @@ -419,6 +427,10 @@ export class RuleSet { } ret.push(this.applyRulesToDatum(rules, data[i], cell_width, cell_height)); } + // mark universal rule as active + if (this.getUniversalRule()) { + out_active_rules[this.getUniversalRule().id] = true; + } return ret; } } @@ -426,15 +438,13 @@ export class RuleSet { class LookupRuleSet extends RuleSet { private lookup_map_by_key_and_value:{[key:string]:{[value:string]:RuleWithId}} = {}; private lookup_map_by_key:{[key:string]:RuleWithId} = {}; - private universal_rules:RuleWithId[] = []; private rule_id_to_conditions:{[ruleId:number]:{ key:string, value:string }[] } = {}; - public getRulesWithId(datum?:Datum) { + public getSpecificRulesForDatum(datum?:Datum) { if (typeof datum === 'undefined') { return this.rules_with_id; } let ret:RuleWithId[] = []; - ret = ret.concat(this.universal_rules); for (var key in datum) { if (typeof datum[key] !== 'undefined' && datum.hasOwnProperty(key)) { var key_rule = this.lookup_map_by_key[key]; @@ -452,7 +462,7 @@ class LookupRuleSet extends RuleSet { private indexRuleForLookup(condition_key:string, condition_value:string, rule_with_id:RuleWithId) { if (condition_key === null) { - this.universal_rules.push(rule_with_id); + this.setUniversalRule(rule_with_id); } else { if (condition_value === null) { this.lookup_map_by_key[condition_key] = rule_with_id; @@ -483,16 +493,8 @@ class LookupRuleSet extends RuleSet { while (this.rule_id_to_conditions[rule_id].length > 0) { var condition = this.rule_id_to_conditions[rule_id].pop(); if (condition.key === null) { - var index = -1; - for (var i = 0; i < this.universal_rules.length; i++) { - if (this.universal_rules[i].id === rule_id) { - index = i; - break; - } - } - if (index > -1) { - this.universal_rules.splice(index, 1); - } + // universal rule + this.universal_rule = undefined; } else { if (condition.value === null) { delete this.lookup_map_by_key[condition.key]; @@ -526,7 +528,7 @@ class ConditionRuleSet extends RuleSet { } } - public getRulesWithId(datum?:Datum) { + public getSpecificRulesForDatum(datum?:Datum) { if (typeof datum === 'undefined') { return this.rules_with_id; } @@ -1093,9 +1095,9 @@ class GradientCategoricalRuleSet extends RuleSet { } // RuleSet API - public getRulesWithId(datum?:Datum) { - const categoricalRules = this.categoricalRuleSet.getRulesWithId(datum); - const gradientRules = this.gradientRuleSet.getRulesWithId(datum); + public getSpecificRulesForDatum(datum?:Datum) { + const categoricalRules = this.categoricalRuleSet.getSpecificRulesForDatum(datum); + const gradientRules = this.gradientRuleSet.getSpecificRulesForDatum(datum); const rules = categoricalRules.concat(gradientRules); return rules; } diff --git a/src/js/oncoprintwebglcellview.ts b/src/js/oncoprintwebglcellview.ts index 91800cf..05c763d 100644 --- a/src/js/oncoprintwebglcellview.ts +++ b/src/js/oncoprintwebglcellview.ts @@ -3,7 +3,7 @@ import svgfactory from './svgfactory'; import makeSvgElement from './makesvgelement'; import shapeToVertexes from './oncoprintshapetovertexes'; import CachedProperty from './CachedProperty'; -import {Shape} from './oncoprintshape'; +import {ComputedShapeParams, Shape} from './oncoprintshape'; import $ from 'jquery'; import OncoprintModel, { ColumnId, ColumnLabel, @@ -18,7 +18,10 @@ import {arrayFindIndex, ifndef, sgndiff} from "./utils"; import MouseUpEvent = JQuery.MouseUpEvent; import MouseMoveEvent = JQuery.MouseMoveEvent; import {CellClickCallback, CellMouseOverCallback} from "./oncoprint"; -import {OMath} from "./polyfill"; +import { + getFragmentShaderSource, getVertexShaderSource +} from "./shaders"; +import _ from 'lodash'; type ColorBankIndex = number; // index into vertex bank (e.g. 0, 4, 8, ...) type ColorBank = number[]; // flat list of color: [c0,c0,c0,c0,v1,v1,v1,c1,c1,c1,c1,...] @@ -58,6 +61,12 @@ export type OncoprintTrackBuffer = WebGLBuffer & { numItems:number; }; // TODO: handle this differently, considered an anti-pattern https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html +export type OncoprintVertexTrackBuffer = OncoprintTrackBuffer & { + // the always shapes vertexes start at index itemSize*numItems, and go on for alwaysShapesItemSIze*alwaysShapesNumItems indexes + alwaysShapesItemSize:number; + alwaysShapesNumItems:number; +} + const COLUMN_LABEL_ANGLE = 65; const COLUMN_LABEL_MARGIN = 30; @@ -74,6 +83,7 @@ export default class OncoprintWebGLCellView { private mouseMoveHandler:(evt:MouseMoveEvent)=>void; private ctx:OncoprintWebGLContext|null; + private ext:ANGLE_instanced_arrays | null; private overlay_ctx:CanvasRenderingContext2D|null; private column_label_ctx:CanvasRenderingContext2D|null; private mvMatrix:any; @@ -87,11 +97,17 @@ export default class OncoprintWebGLCellView { private rendering_suppressed = false; private identified_shape_list_list:TrackProp = {}; - public vertex_data:TrackProp<{ pos_array:number[], col_array:ColorBankIndex[], col_bank:ColorBank}> = {}; + public vertex_data:TrackProp<{ + pos_array:number[], + col_array:ColorBankIndex[], + col_bank:ColorBank, + always_shapes_start_index:number + }> = {}; public vertex_column_array:TrackProp = {}; - private vertex_position_buffer:TrackProp = {}; - private vertex_color_buffer:TrackProp = {}; + private vertex_position_buffer:TrackProp = {}; + private vertex_color_buffer:TrackProp = {}; private vertex_column_buffer:TrackProp = {}; + private simple_count_buffer:OncoprintTrackBuffer | null = null; private is_buffer_empty:TrackProp<{ position:boolean; color:boolean; @@ -101,6 +117,10 @@ export default class OncoprintWebGLCellView { private color_texture:TrackProp<{texture: WebGLTexture, size:number}> = {}; private id_to_first_vertex_index:TrackProp> = {}; // index of first vertex corresponding to given id for given track, e.g. 0, 3, 6, ... + // NEW + private always_shapes:TrackProp = {}; + private always_vertex_data:TrackProp<{ pos_array: number[], col_array:ColorBankIndex[], col_bank:ColorBank}> = {}; + constructor( private $container:JQuery, private $canvas:JQuery, @@ -272,6 +292,7 @@ export default class OncoprintWebGLCellView { parent_node.insertBefore(new_canvas, parent_node.childNodes[0]); // keep on bottom since we need overlays to not be hidden this.$canvas = $(new_canvas); this.ctx = null; + this.ext = null; } private getWebGLCanvasContext() { @@ -387,6 +408,9 @@ export default class OncoprintWebGLCellView { private getWebGLContextAndSetUpMatrices() { this.ctx = this.getWebGLCanvasContext(); + if (this.ctx) { + this.ext = this.ctx.getExtension('ANGLE_instanced_arrays'); + } (function initializeMatrices(self) { const mvMatrix = gl_matrix.mat4.create(); gl_matrix.mat4.lookAt(mvMatrix, [0, 0, 1], [0, 0, 0], [0, 1, 0]); @@ -405,107 +429,8 @@ export default class OncoprintWebGLCellView { private setUpShaders(model:OncoprintModel) { const columnsRightAfterGapsSize = this.getColumnIndexesAfterAGap(model).length; - const vertex_shader_source = ` - precision highp float; - attribute float aPosVertex; - attribute float aColVertex; - attribute float aVertexOncoprintColumn; - - uniform float gapSize; - - uniform float columnsRightAfterGaps[${columnsRightAfterGapsSize}]; // sorted in ascending order - - uniform float columnWidth; - uniform float scrollX; - uniform float zoomX; - uniform float scrollY; - uniform float zoomY; - uniform mat4 uMVMatrix; - uniform mat4 uPMatrix; - uniform float offsetY; - uniform float supersamplingRatio; - uniform float positionBitPackBase; - uniform float texSize; - varying float texCoord; - - vec3 getUnpackedPositionVec3() { - float pos0 = floor(aPosVertex / (positionBitPackBase * positionBitPackBase)); - float pos0Contr = pos0 * positionBitPackBase * positionBitPackBase; - float pos1 = floor((aPosVertex - pos0Contr)/positionBitPackBase); - float pos1Contr = pos1 * positionBitPackBase; - float pos2 = aPosVertex - pos0Contr - pos1Contr; - return vec3(pos0, pos1, pos2); - } - - float getGapOffset() { - // first do binary search to compute the number of gaps before this column, G(c) - // G(c) = the index in columnsRightAfterGaps of the first entry thats greater than c - - int lower_incl = 0; - int upper_excl = ${columnsRightAfterGapsSize}; - int numGaps = 0; - - for (int loopDummyVar = 0; loopDummyVar == 0; loopDummyVar += 0) { - if (lower_incl >= upper_excl) { - break; - } - - int middle = (lower_incl + upper_excl)/2; - if (columnsRightAfterGaps[middle] < aVertexOncoprintColumn) { - // G(c) > middle - lower_incl = middle + 1; - } else if (columnsRightAfterGaps[middle] == aVertexOncoprintColumn) { - // G(c) = middle + 1 - numGaps = middle + 1; - break; - } else { - // columnsRightAfterGaps[middle] > column, so G(c) <= middle - if (middle == 0) { - // 0 <= G(c) <= 0 -> G(c) = 0 - numGaps = 0; - break; - } else if (columnsRightAfterGaps[middle-1] < aVertexOncoprintColumn) { - // G(c) = middle - numGaps = middle; - break; - } else { - // columnsRightAfterGaps[middle-1] >= column, so G(c) <= middle-1 - upper_excl = middle; - } - } - } - - // multiply it by the gap size to get the total offset - return float(numGaps)*gapSize; - } - - void main(void) { - gl_Position = vec4(getUnpackedPositionVec3(), 1.0); - gl_Position[0] += aVertexOncoprintColumn*columnWidth; - gl_Position *= vec4(zoomX, zoomY, 1.0, 1.0); - - // gaps should not be affected by zoom: - gl_Position[0] += getGapOffset(); - - // offsetY is given zoomed: - gl_Position[1] += offsetY; - - gl_Position -= vec4(scrollX, scrollY, 0.0, 0.0); - gl_Position[0] *= supersamplingRatio; - gl_Position[1] *= supersamplingRatio; - gl_Position = uPMatrix * uMVMatrix * gl_Position; - - texCoord = (aColVertex + 0.5) / texSize; - }`; - const fragment_shader_source = ` - precision mediump float; - varying float texCoord; - uniform sampler2D uSampler; - void main(void) { - gl_FragColor = texture2D(uSampler, vec2(texCoord, 0.5)); - }`; - const vertex_shader = this.createShader(vertex_shader_source, 'VERTEX_SHADER'); - const fragment_shader = this.createShader(fragment_shader_source, 'FRAGMENT_SHADER'); + const vertex_shader = this.createShader(getVertexShaderSource(columnsRightAfterGapsSize), 'VERTEX_SHADER'); + const fragment_shader = this.createShader(getFragmentShaderSource(), 'FRAGMENT_SHADER'); const shader_program = this.createShaderProgram(vertex_shader, fragment_shader) as OncoprintShaderProgram; shader_program.vertexPositionAttribute = this.ctx.getAttribLocation(shader_program, 'aPosVertex'); @@ -610,35 +535,62 @@ export default class OncoprintWebGLCellView { const first_index = this.id_to_first_vertex_index[track_id][horz_first_id_in_window]; const first_index_out = horz_first_id_after_window === null ? buffers.position.numItems : this.id_to_first_vertex_index[track_id][horz_first_id_after_window]; - this.ctx.useProgram(this.shader_program); - this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.position); - this.ctx.vertexAttribPointer(this.shader_program.vertexPositionAttribute, buffers.position.itemSize, this.ctx.FLOAT, false, 0, 0); - this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.color); - this.ctx.vertexAttribPointer(this.shader_program.vertexColorAttribute, buffers.color.itemSize, this.ctx.FLOAT, false, 0, 0); - - this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.column); - this.ctx.vertexAttribPointer(this.shader_program.vertexOncoprintColumnAttribute, buffers.column.itemSize, this.ctx.FLOAT, false, 0, 0); - - this.ctx.activeTexture(this.ctx.TEXTURE0); - this.ctx.bindTexture(this.ctx.TEXTURE_2D, buffers.color_tex.texture); - this.ctx.uniform1i(this.shader_program.samplerUniform, 0); - this.ctx.uniform1f(this.shader_program.texSizeUniform, buffers.color_tex.size); + for (const forSpecificShapes of [false,true]) { + const shader_program = this.shader_program; + this.ctx.useProgram(shader_program); + + if (forSpecificShapes) { + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.position); + this.ctx.vertexAttribPointer(shader_program.vertexPositionAttribute, buffers.position.itemSize, this.ctx.FLOAT, false, 0, 0); + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.color); + this.ctx.vertexAttribPointer(shader_program.vertexColorAttribute, buffers.color.itemSize, this.ctx.FLOAT, false, 0, 0); + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.column); + this.ctx.vertexAttribPointer(shader_program.vertexOncoprintColumnAttribute, buffers.column.itemSize, this.ctx.FLOAT, false, 0, 0); + // make sure to set divisor 0, otherwise the track will only use the first item in the column buffer + this.ext.vertexAttribDivisorANGLE(shader_program.vertexOncoprintColumnAttribute, 0); + } else { + // set up for drawArraysInstanced + const universalShapesStart = buffers.position.numItems*buffers.position.itemSize; + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.position); + this.ctx.vertexAttribPointer(shader_program.vertexPositionAttribute, buffers.position.itemSize, this.ctx.FLOAT, false, 0, 4*universalShapesStart); - this.ctx.uniform1fv(this.shader_program.columnsRightAfterGapsUniform, this.getColumnIndexesAfterAGap(model)); // need min size of 1 - this.ctx.uniform1f(this.shader_program.gapSizeUniform, model.getGapSize()); + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.color); + this.ctx.vertexAttribPointer(shader_program.vertexColorAttribute, buffers.color.itemSize, this.ctx.FLOAT, false, 0, 4*universalShapesStart); - this.ctx.uniformMatrix4fv(this.shader_program.pMatrixUniform, false, this.pMatrix); - this.ctx.uniformMatrix4fv(this.shader_program.mvMatrixUniform, false, this.mvMatrix); - this.ctx.uniform1f(this.shader_program.columnWidthUniform, model.getCellWidth(true) + model.getCellPadding(true)); - this.ctx.uniform1f(this.shader_program.scrollXUniform, scroll_x); - this.ctx.uniform1f(this.shader_program.scrollYUniform, scroll_y); - this.ctx.uniform1f(this.shader_program.zoomXUniform, zoom_x); - this.ctx.uniform1f(this.shader_program.zoomYUniform, zoom_y); - this.ctx.uniform1f(this.shader_program.offsetYUniform, cell_top); - this.ctx.uniform1f(this.shader_program.supersamplingRatioUniform, this.supersampling_ratio); - this.ctx.uniform1f(this.shader_program.positionBitPackBaseUniform, this.position_bit_pack_base); + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.simple_count_buffer); + this.ctx.vertexAttribPointer(shader_program.vertexOncoprintColumnAttribute, 1, this.ctx.FLOAT, false, 0, 4*horz_first_id_in_window_index); + this.ext.vertexAttribDivisorANGLE(shader_program.vertexOncoprintColumnAttribute, 1); + } - this.ctx.drawArrays(this.ctx.TRIANGLES, first_index, first_index_out - first_index); + this.ctx.activeTexture(this.ctx.TEXTURE0); + this.ctx.bindTexture(this.ctx.TEXTURE_2D, buffers.color_tex.texture); + this.ctx.uniform1i(shader_program.samplerUniform, 0); + this.ctx.uniform1f(shader_program.texSizeUniform, buffers.color_tex.size); + + this.ctx.uniform1fv(shader_program.columnsRightAfterGapsUniform, this.getColumnIndexesAfterAGap(model)); // need min size of 1 + this.ctx.uniform1f(shader_program.gapSizeUniform, model.getGapSize()); + + this.ctx.uniformMatrix4fv(shader_program.pMatrixUniform, false, this.pMatrix); + this.ctx.uniformMatrix4fv(shader_program.mvMatrixUniform, false, this.mvMatrix); + this.ctx.uniform1f(shader_program.columnWidthUniform, model.getCellWidth(true) + model.getCellPadding(true)); + this.ctx.uniform1f(shader_program.scrollXUniform, scroll_x); + this.ctx.uniform1f(shader_program.scrollYUniform, scroll_y); + this.ctx.uniform1f(shader_program.zoomXUniform, zoom_x); + this.ctx.uniform1f(shader_program.zoomYUniform, zoom_y); + this.ctx.uniform1f(shader_program.offsetYUniform, cell_top); + this.ctx.uniform1f(shader_program.supersamplingRatioUniform, this.supersampling_ratio); + this.ctx.uniform1f(shader_program.positionBitPackBaseUniform, this.position_bit_pack_base); + if (forSpecificShapes) { + this.ctx.drawArrays(this.ctx.TRIANGLES, first_index, first_index_out - first_index); + } else { + this.ext.drawArraysInstancedANGLE( + this.ctx.TRIANGLES, + 0, + buffers.position.alwaysShapesItemSize*buffers.position.alwaysShapesNumItems, + horz_first_id_after_window_index - horz_first_id_in_window_index + ); + } + } } this.renderColumnLabels(model, id_order.slice(horz_first_id_in_window_index, horz_first_id_after_window_index === -1 ? undefined : horz_first_id_after_window_index)); @@ -732,6 +684,18 @@ export default class OncoprintWebGLCellView { return { radius }; } + private ensureSimpleCountBuffer(model:OncoprintModel) { + const numColumns = model.getIdOrder().length; + if (!this.simple_count_buffer || this.simple_count_buffer.numItems !== numColumns) { + const buffer = this.ctx.createBuffer() as OncoprintTrackBuffer; + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffer); + this.ctx.bufferData(this.ctx.ARRAY_BUFFER, new Float32Array(_.range(0, numColumns)), this.ctx.STATIC_DRAW); + buffer.itemSize = 1; + buffer.numItems = numColumns; + this.simple_count_buffer = buffer; + } + } + private clearTrackPositionAndColorBuffers(model:OncoprintModel, track_id?:TrackId) { let tracks_to_clear; if (typeof track_id === 'undefined') { @@ -796,6 +760,13 @@ export default class OncoprintWebGLCellView { } } + private deleteSimpleCountBuffer(model:OncoprintModel) { + if (this.simple_count_buffer) { + this.ctx.deleteBuffer(this.simple_count_buffer); + this.simple_count_buffer = null; + } + } + private getTrackBuffers(track_id:TrackId) { this.is_buffer_empty[track_id] = this.is_buffer_empty[track_id] || { position: true, @@ -805,25 +776,31 @@ export default class OncoprintWebGLCellView { }; if (this.is_buffer_empty[track_id].position) { - const pos_buffer = this.vertex_position_buffer[track_id] || this.ctx.createBuffer() as OncoprintTrackBuffer; + const pos_buffer = this.vertex_position_buffer[track_id] || this.ctx.createBuffer() as OncoprintVertexTrackBuffer; const pos_array = this.vertex_data[track_id].pos_array; + const always_shapes_start_index = this.vertex_data[track_id].always_shapes_start_index; this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, pos_buffer); this.ctx.bufferData(this.ctx.ARRAY_BUFFER, new Float32Array(pos_array), this.ctx.STATIC_DRAW); pos_buffer.itemSize = 1; - pos_buffer.numItems = pos_array.length / pos_buffer.itemSize; + pos_buffer.numItems = always_shapes_start_index / pos_buffer.itemSize; + pos_buffer.alwaysShapesItemSize = 1; + pos_buffer.alwaysShapesNumItems = (pos_array.length - always_shapes_start_index) / pos_buffer.alwaysShapesItemSize; this.vertex_position_buffer[track_id] = pos_buffer; } if (this.is_buffer_empty[track_id].color) { - const col_buffer = this.vertex_color_buffer[track_id] || this.ctx.createBuffer() as OncoprintTrackBuffer; + const col_buffer = this.vertex_color_buffer[track_id] || this.ctx.createBuffer() as OncoprintVertexTrackBuffer; const col_array = this.vertex_data[track_id].col_array; + const always_shapes_start_index = this.vertex_data[track_id].always_shapes_start_index; this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, col_buffer); this.ctx.bufferData(this.ctx.ARRAY_BUFFER, new Float32Array(col_array), this.ctx.STATIC_DRAW); col_buffer.itemSize = 1; - col_buffer.numItems = col_array.length / col_buffer.itemSize; + col_buffer.numItems = always_shapes_start_index / col_buffer.itemSize; + col_buffer.alwaysShapesItemSize = 1; + col_buffer.alwaysShapesNumItems = (col_array.length - always_shapes_start_index) / col_buffer.alwaysShapesItemSize; this.vertex_color_buffer[track_id] = col_buffer; } @@ -893,6 +870,10 @@ export default class OncoprintWebGLCellView { if (this.rendering_suppressed) { return; } + + // check simple count buffer whenever we recompute vertexes + this.ensureSimpleCountBuffer(model); + const identified_shape_list_list = this.identified_shape_list_list[track_id]; const id_to_index = model.getIdToIndexMap(); identified_shape_list_list.sort(function(a, b) { @@ -918,6 +899,31 @@ export default class OncoprintWebGLCellView { const vertexifiedShapes:{[shapeHash:string]:{position:number[], color:number[]}} = {}; + function addShapeVertexes(_shape:ComputedShapeParams, zindex:number) { + const hash = Shape.hashComputedShape(_shape, zindex); + if (!vertexifiedShapes.hasOwnProperty(hash)) { + vertexifiedShapes[hash] = {position:[], color:[]}; + const position = vertexifiedShapes[hash].position; + const color = vertexifiedShapes[hash].color; + shapeToVertexes(_shape, zindex, function(pos:PositionVertex, col:ColorVertex) { + pos = pos.map(Math.round) as PositionVertex; + col = col.map(function(x) { return Math.round(x*255);}) as ColorVertex; + + position.push(packPos(pos)); + + const col_hash = hashVector(col); + let col_index = color_bank_index[col_hash]; + if (typeof col_index === "undefined") { + col_index = color_vertexes.length; + color_vertexes.push(col); + color_bank_index[col_hash] = col_index; + } + color.push(col_index); + }); + } + vertex_pos_array.push.apply(vertex_pos_array, vertexifiedShapes[hash].position); + vertex_col_array.push.apply(vertex_col_array, vertexifiedShapes[hash].color); + } for (let i = 0; i < identified_shape_list_list.length; i++) { const shape_list = identified_shape_list_list[i].shape_list; @@ -927,31 +933,20 @@ export default class OncoprintWebGLCellView { for (let j = 0; j < shape_list.length; j++) { const shape = shape_list[j]; - const hash = Shape.hashComputedShape(shape, j); - if (!vertexifiedShapes.hasOwnProperty(hash)) { - vertexifiedShapes[hash] = {position:[], color:[]}; - const position = vertexifiedShapes[hash].position; - const color = vertexifiedShapes[hash].color; - shapeToVertexes(shape, j, function(pos:PositionVertex, col:ColorVertex) { - pos = pos.map(Math.round) as PositionVertex; - col = col.map(function(x) { return Math.round(x*255);}) as ColorVertex; - - position.push(packPos(pos)); - - const col_hash = hashVector(col); - let col_index = color_bank_index[col_hash]; - if (typeof col_index === "undefined") { - col_index = color_vertexes.length; - color_vertexes.push(col); - color_bank_index[col_hash] = col_index; - } - color.push(col_index); - }); - } - vertex_pos_array.push.apply(vertex_pos_array, vertexifiedShapes[hash].position); - vertex_col_array.push.apply(vertex_col_array, vertexifiedShapes[hash].color); + addShapeVertexes(shape, j); + } + } + + // record start index of always shapes + const always_shapes_start_index = vertex_pos_array.length; + const always_shapes = this.always_shapes[track_id]; + if (always_shapes) { + for (let j = 0; j < always_shapes.length; j++) { + const shape = always_shapes[j]; + addShapeVertexes(shape, j); } } + const color_bank:ColorBank = color_vertexes.reduce(function(arr, next) { return arr.concat(next); }, []); // minimum color bank to avoid webGL texture errors if (color_bank.length === 0) { @@ -960,7 +955,8 @@ export default class OncoprintWebGLCellView { this.vertex_data[track_id] = { pos_array: vertex_pos_array, col_array: vertex_col_array, - col_bank: color_bank + col_bank: color_bank, + always_shapes_start_index }; this.id_to_first_vertex_index[track_id] = id_to_first_vertex_index; @@ -971,14 +967,18 @@ export default class OncoprintWebGLCellView { if (this.rendering_suppressed) { return; } - this.identified_shape_list_list[track_id] = model.getIdentifiedShapeListList(track_id, true, true); + this.identified_shape_list_list[track_id] = model.getSpecificShapesForData(track_id, true, true); + this.always_shapes[track_id] = model.getAlwaysShapes(track_id, true, true); } private refreshCanvas(model:OncoprintModel) { - this.deleteBuffers(model);// whenever you get a new context, you have to get new buffers + // whenever you get a new context, you have to get new buffers + this.deleteBuffers(model); + this.deleteSimpleCountBuffer(model); this.getNewCanvas(); this.getWebGLContextAndSetUpMatrices(); this.setUpShaders(model); + this.ensureSimpleCountBuffer(model); } private highlightCell(model:OncoprintModel, track_id:TrackId, uid:ColumnId) { @@ -1070,6 +1070,8 @@ export default class OncoprintWebGLCellView { delete this.vertex_column_array[track_id]; delete this.id_to_first_vertex_index[track_id]; delete this.is_buffer_empty[track_id]; + delete this.always_shapes[track_id]; + delete this.always_vertex_data[track_id]; if (!this.rendering_suppressed) { this.renderAllTracks(model); @@ -1375,7 +1377,8 @@ export default class OncoprintWebGLCellView { for (let i=0; i