From b961504b549593d030894f2ed8ea24adeaa1b435 Mon Sep 17 00:00:00 2001 From: "A.Olokhtonov" Date: Sun, 24 Nov 2024 19:30:25 +0300 Subject: [PATCH] Separate stroke rendering into function. Reuse said function for static and dynamic draw. --- client/config.js | 1 - client/index.js | 1 + client/wasm/lod.c | 3 +- client/webgl_draw.js | 300 ++++++++++++++++++--------------------- client/webgl_geometry.js | 45 ++++++ client/webgl_shaders.js | 5 +- 6 files changed, 191 insertions(+), 164 deletions(-) diff --git a/client/config.js b/client/config.js index 37986f3..d734aed 100644 --- a/client/config.js +++ b/client/config.js @@ -22,7 +22,6 @@ const config = { initial_dynamic_bytes: 4096, initial_wasm_bytes: 4096, stroke_texture_size: 1024, // means no more than 1024^2 = 1M strokes in total (this is a LOT. HMH blackboard has like 80K) - dynamic_stroke_texture_size: 128, // means no more than 128^2 = 16K dynamic strokes at once ui_texture_size: 16, bvh_fullnode_depth: 6, pattern_fadeout_min: 0.3, diff --git a/client/index.js b/client/index.js index f04815c..c649328 100644 --- a/client/index.js +++ b/client/index.js @@ -232,6 +232,7 @@ async function main() { 'dynamic_instance_points': tv_create(Float32Array, 4096), 'dynamic_instance_pressure': tv_create(Uint8Array, 4096), 'dynamic_instance_ids': tv_create(Uint32Array, 4096), + 'dynamic_instance_batches': tv_create(Uint32Array, 4096), 'stroke_data': serializer_create(config.initial_static_bytes), 'dynamic_stroke_data': serializer_create(config.initial_static_bytes), diff --git a/client/wasm/lod.c b/client/wasm/lod.c index 4978b38..c3ae222 100644 --- a/client/wasm/lod.c +++ b/client/wasm/lod.c @@ -324,9 +324,8 @@ do_lod(int *clipped_indices, int clipped_count, float zoom, // Compute recommended LOD level, add to current batch or start new batch int lod; - // The LOD levels have been picked manually based on "COM" (Careful Observation Method) float perceptual_width = width[stroke_index] * zoom; - // The LOD levels have been picked manually based on "COM" (Careful Observation Method) + // The LOD levels have been picked manually based on "COM" (Careful Observation Method) @lod if (perceptual_width < 1.9f) { lod = 0; } else if (perceptual_width < 4.56f) { diff --git a/client/webgl_draw.js b/client/webgl_draw.js index af49392..b10fba8 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -96,6 +96,119 @@ function draw_html(state) { } +function draw_strokes(state, width, height, programs, gl, lod_levels, segment_count, + total_lod_floats, total_lod_indices, + batches_tv, + points_tv, + ids_tv, + pressures_tv, + vbo, + ebo, + stroke_texture, + stroke_data, + stroke_count, +) { + const pr = programs['main']; + + // Last pair (lod unused) to have a proper from;to + tv_add2(batches_tv, segment_count); + tv_add2(batches_tv, -1); + + gl.clear(gl.DEPTH_BUFFER_BIT); // draw strokes above the images + gl.useProgram(pr.program); + + const total_size = points_tv.size * 4 + + ids_tv.size * 4 + + round_to_pow2(pressures_tv.size, 4) + + total_lod_floats * 4; + + gl.bindBuffer(gl.ARRAY_BUFFER, vbo); + gl.bufferData(gl.ARRAY_BUFFER, total_size, gl.STREAM_DRAW); + + // Segment points, segment stroke ids, segment pressures + gl.bufferSubData(gl.ARRAY_BUFFER, 0, tv_data(points_tv)); + gl.bufferSubData(gl.ARRAY_BUFFER, points_tv.size * 4, tv_data(ids_tv)); + gl.bufferSubData(gl.ARRAY_BUFFER, points_tv.size * 4 + ids_tv.size * 4, tv_data(pressures_tv)); + + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, total_lod_indices * 4, gl.STREAM_DRAW); + + // Upload all variants of LOD vertices/indices + const base_lod_points_offset = points_tv.size * 4 + ids_tv.size * 4 + round_to_pow2(pressures_tv.size, 4); + + for (const level of lod_levels) { + gl.bufferSubData(gl.ARRAY_BUFFER, base_lod_points_offset + level.vertices_offset, tv_data(level.data.points)); + gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, level.indices_offset, tv_data(level.data.indices)); + } + + // Per-stroke data (base width, color) + gl.bindTexture(gl.TEXTURE_2D, stroke_texture); + upload_square_rgba16ui_texture(gl, stroke_data, config.stroke_texture_size); + + gl.uniform2f(pr.locations['u_res'], width, height); + gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom); + gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); + gl.uniform1i(pr.locations['u_stroke_count'], stroke_count); + gl.uniform1i(pr.locations['u_debug_mode'], state.debug.red); + gl.uniform1i(pr.locations['u_stroke_data'], 0); + gl.uniform1i(pr.locations['u_stroke_texture_size'], config.stroke_texture_size); + + gl.enableVertexAttribArray(pr.locations['a_pos']); + gl.enableVertexAttribArray(pr.locations['a_a']); + gl.enableVertexAttribArray(pr.locations['a_b']); + gl.enableVertexAttribArray(pr.locations['a_stroke_id']); + gl.enableVertexAttribArray(pr.locations['a_pressure']); + + gl.vertexAttribDivisor(pr.locations['a_pos'], 0); + gl.vertexAttribDivisor(pr.locations['a_a'], 1); + gl.vertexAttribDivisor(pr.locations['a_b'], 1); + gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 1); + gl.vertexAttribDivisor(pr.locations['a_pressure'], 1); + + for (let b = 0; b < batches_tv.size - 2; b += 2) { + const batch_from = batches_tv.data[b + 0]; + const batch_size = batches_tv.data[b + 2] - batch_from; + let lod_level = batches_tv.data[b + 1]; + + if (config.debug_force_lod !== null) { + lod_level = config.debug_force_lod; + } + + const level = lod_levels[lod_level]; + + if (batch_size > 0) { + //stat_total_vertices += batch_size * level.data.indices.size; + + gl.uniform1i(pr.locations['u_circle_points'], level.data.points.size / 2 - 4); + gl.uniform3f(pr.locations['u_debug_color'], + (lod_level * 785892 + 125127) % 8 / 7, + (lod_level * 901824 + 985835) % 8 / 7, + (lod_level * 232181 + 838533) % 8 / 7, + ); + + // Points (a, b) and stroke ids are stored in separate cpu buffers so that points can be reused (look at stride and offset values) + gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, batch_from * 2 * 4); + gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, batch_from * 2 * 4 + 2 * 4); + gl.vertexAttribIPointer(pr.locations['a_stroke_id'], 1, gl.INT, 4, points_tv.size * 4 + batch_from * 4); + gl.vertexAttribPointer(pr.locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, points_tv.size * 4 + ids_tv.size * 4 + batch_from); + gl.vertexAttribPointer(pr.locations['a_pos'], 2, gl.FLOAT, false, 2 * 4, base_lod_points_offset + level.vertices_offset, 4); + + gl.drawElementsInstanced(gl.TRIANGLES, level.data.indices.size, gl.UNSIGNED_INT, level.indices_offset, batch_size); + } + } + + // I don't really know why I need to do this, but it + // makes background patter drawcall work properly + gl.vertexAttribDivisor(pr.locations['a_pos'], 0); + gl.vertexAttribDivisor(pr.locations['a_a'], 0); + gl.vertexAttribDivisor(pr.locations['a_b'], 0); + gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 0); + gl.vertexAttribDivisor(pr.locations['a_pressure'], 0); + + tv_pop(batches_tv); + tv_pop(batches_tv); +} + async function draw(state, context, animate, ts) { const dt = ts - context.last_frame_ts; const cpu_before = performance.now(); @@ -291,173 +404,40 @@ async function draw(state, context, animate, ts) { // "Static" data upload if (segment_count > 0) { - const pr = programs['main']; - - // Last pair (lod unused) to have a proper from;to - tv_add2(context.instance_data_batches, segment_count); - tv_add2(context.instance_data_batches, -1); - - gl.clear(gl.DEPTH_BUFFER_BIT); // draw strokes above the images - gl.useProgram(pr.program); - - const total_static_size = context.instance_data_points.size * 4 + - context.instance_data_ids.size * 4 + - round_to_pow2(context.instance_data_pressures.size, 4) + - total_lod_floats * 4; - - gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_strokes_static']); - gl.bufferData(gl.ARRAY_BUFFER, total_static_size, gl.STREAM_DRAW); - - // Segment points, segment stroke ids, segment pressures - gl.bufferSubData(gl.ARRAY_BUFFER, 0, tv_data(context.instance_data_points)); - gl.bufferSubData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4, tv_data(context.instance_data_ids)); - gl.bufferSubData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4, - tv_data(context.instance_data_pressures)); - - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers['i_strokes_static']); - gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, total_lod_indices * 4, gl.STREAM_DRAW); - - // Upload all variants of LOD vertices/indices - const base_lod_points_offset = context.instance_data_points.size * 4 + context.instance_data_ids.size * 4 + round_to_pow2(context.instance_data_pressures.size, 4); - - for (const level of lod_levels) { - gl.bufferSubData(gl.ARRAY_BUFFER, base_lod_points_offset + level.vertices_offset, tv_data(level.data.points)); - gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, level.indices_offset, tv_data(level.data.indices)); - } - - // Per-stroke data (base width, color) - gl.bindTexture(gl.TEXTURE_2D, textures['stroke_data']); - upload_square_rgba16ui_texture(gl, context.stroke_data, config.stroke_texture_size); - - gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height); - gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom); - gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); - gl.uniform1i(pr.locations['u_stroke_count'], state.events.length); - gl.uniform1i(pr.locations['u_debug_mode'], state.debug.red); - gl.uniform1i(pr.locations['u_stroke_data'], 0); - gl.uniform1i(pr.locations['u_stroke_texture_size'], config.stroke_texture_size); - - gl.enableVertexAttribArray(pr.locations['a_pos']); - gl.enableVertexAttribArray(pr.locations['a_a']); - gl.enableVertexAttribArray(pr.locations['a_b']); - gl.enableVertexAttribArray(pr.locations['a_stroke_id']); - gl.enableVertexAttribArray(pr.locations['a_pressure']); - - - gl.vertexAttribDivisor(pr.locations['a_pos'], 0); - gl.vertexAttribDivisor(pr.locations['a_a'], 1); - gl.vertexAttribDivisor(pr.locations['a_b'], 1); - gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 1); - gl.vertexAttribDivisor(pr.locations['a_pressure'], 1); - - for (let b = 0; b < context.instance_data_batches.size - 2; b += 2) { - const batch_from = context.instance_data_batches.data[b + 0]; - const batch_size = context.instance_data_batches.data[b + 2] - batch_from; - let lod_level = context.instance_data_batches.data[b + 1]; - - if (config.debug_force_lod !== null) { - lod_level = config.debug_force_lod; - } - - const level = lod_levels[lod_level]; - - if (batch_size > 0) { - stat_total_vertices += batch_size * level.data.indices.size; - - gl.uniform1i(pr.locations['u_circle_points'], level.data.points.size / 2 - 4); - gl.uniform3f(pr.locations['u_debug_color'], - (lod_level * 785892 + 125127) % 8 / 7, - (lod_level * 901824 + 985835) % 8 / 7, - (lod_level * 232181 + 838533) % 8 / 7, - ); - - // Points (a, b) and stroke ids are stored in separate cpu buffers so that points can be reused (look at stride and offset values) - gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, batch_from * 2 * 4); - gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, batch_from * 2 * 4 + 2 * 4); - gl.vertexAttribIPointer(pr.locations['a_stroke_id'], 1, gl.INT, 4, context.instance_data_points.size * 4 + batch_from * 4); - gl.vertexAttribPointer(pr.locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4 + batch_from); - - gl.vertexAttribPointer(pr.locations['a_pos'], 2, gl.FLOAT, false, 2 * 4, base_lod_points_offset + level.vertices_offset, 4); - - gl.drawElementsInstanced(gl.TRIANGLES, level.data.indices.size, gl.UNSIGNED_INT, level.indices_offset, batch_size); - } - } - - // I don't really know why I need to do this, but it - // makes background patter drawcall work properly - gl.vertexAttribDivisor(pr.locations['a_pos'], 0); - gl.vertexAttribDivisor(pr.locations['a_a'], 0); - gl.vertexAttribDivisor(pr.locations['a_b'], 0); - gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 0); - gl.vertexAttribDivisor(pr.locations['a_pressure'], 0); + draw_strokes(state, context.canvas.width, context.canvas.height, programs, gl, lod_levels, segment_count, + total_lod_floats, + total_lod_indices, + context.instance_data_batches, + context.instance_data_points, + context.instance_data_ids, + context.instance_data_pressures, + buffers['b_strokes_static'], + buffers['i_strokes_static'], + textures['stroke_data'], + context.stroke_data, + state.events.length, // not really + ); } // Dynamic draw (strokes currently being drawn) - if (false && dynamic_segment_count > 0) { - const pr = programs['main']; // same as static - + if (dynamic_segment_count > 0) { // Dynamic strokes should be drawn above static strokes gl.clear(gl.DEPTH_BUFFER_BIT); - gl.useProgram(pr.program); - - gl.uniform1i(pr.locations['u_stroke_count'], dynamic_stroke_count); - gl.uniform1i(pr.locations['u_stroke_data'], 0); - gl.uniform1i(pr.locations['u_stroke_texture_size'], config.dynamic_stroke_texture_size); - - gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_strokes_dynamic']); - - // Dynamic data upload - const total_dynamic_size = - context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4 + - context.dynamic_instance_pressure.size; - - gl.bufferData(gl.ARRAY_BUFFER, total_dynamic_size, gl.STREAM_DRAW); - gl.bufferSubData(gl.ARRAY_BUFFER, 0, tv_data(context.dynamic_instance_points)); - gl.bufferSubData(gl.ARRAY_BUFFER, context.dynamic_instance_points.size * 4, tv_data(context.dynamic_instance_ids)); - gl.bufferSubData(gl.ARRAY_BUFFER, context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4, - tv_data(context.dynamic_instance_pressure)); - gl.bindTexture(gl.TEXTURE_2D, textures['dynamic_stroke_data']); - upload_square_rgba16ui_texture(gl, context.dynamic_stroke_data, config.dynamic_stroke_texture_size); - gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height); - gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom); - gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); - - gl.uniform1i(pr.locations['u_stroke_count'], context.dynamic_stroke_count); - gl.uniform1i(pr.locations['u_debug_mode'], state.debug.red); - gl.uniform1i(pr.locations['u_stroke_data'], 0); - gl.uniform1i(pr.locations['u_stroke_texture_size'], config.dynamic_stroke_texture_size); - - gl.enableVertexAttribArray(pr.locations['a_a']); - gl.enableVertexAttribArray(pr.locations['a_b']); - gl.enableVertexAttribArray(pr.locations['a_stroke_id']); - gl.enableVertexAttribArray(pr.locations['a_pressure']); - - // Points (a, b) and stroke ids are stored in separate cpu buffers so that points can be reused (look at stride and offset values) - if (context.dynamic_instance_ids.size > 1) { - gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0); - gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4); - } else { - // A special case where there is no second point. Reuse the first point and handle the zero length segment in the shader - gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0); - gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 0); - } - - gl.vertexAttribIPointer(pr.locations['a_stroke_id'], 1, gl.INT, 4, context.dynamic_instance_points.size * 4); - gl.vertexAttribPointer(pr.locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4); - - gl.vertexAttribDivisor(pr.locations['a_a'], 1); - gl.vertexAttribDivisor(pr.locations['a_b'], 1); - gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 1); - gl.vertexAttribDivisor(pr.locations['a_pressure'], 1); - - gl.drawArraysInstanced(gl.TRIANGLES, 0, 32 * 3 + 6 + 32 * 3, dynamic_segment_count); - - gl.vertexAttribDivisor(pr.locations['a_a'], 0); - gl.vertexAttribDivisor(pr.locations['a_b'], 0); - gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 0); - gl.vertexAttribDivisor(pr.locations['a_pressure'], 0); + draw_strokes(state, context.canvas.width, context.canvas.height, programs, gl, lod_levels, dynamic_segment_count, + total_lod_floats, + total_lod_indices, + context.dynamic_instance_batches, + context.dynamic_instance_points, + context.dynamic_instance_ids, + context.dynamic_instance_pressure, + buffers['b_strokes_dynamic'], + buffers['i_strokes_dynamic'], + textures['dynamic_stroke_data'], + context.dynamic_stroke_data, + context.dynamic_stroke_count, + ); } // HUD: resize handles, etc diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 982fa27..1bde4c7 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -84,15 +84,19 @@ function recompute_dynamic_data(state, context) { tv_ensure(context.dynamic_instance_points, round_to_pow2(total_points * 2, 4096)); tv_ensure(context.dynamic_instance_pressure, round_to_pow2(total_points, 4096)); tv_ensure(context.dynamic_instance_ids, round_to_pow2(total_points, 4096)); + tv_ensure(context.dynamic_instance_batches, round_to_pow2(total_strokes * 2, 4096)); tv_clear(context.dynamic_instance_points); tv_clear(context.dynamic_instance_pressure); tv_clear(context.dynamic_instance_ids); + tv_clear(context.dynamic_instance_batches); context.dynamic_stroke_data = ser_ensure(context.dynamic_stroke_data, config.bytes_per_stroke * total_strokes); ser_clear(context.dynamic_stroke_data); let stroke_index = 0; + let last_lod = -100; + let batch_size = 0; for (const player_id in state.players) { // player has the same data as their current stroke: points, color, width @@ -124,13 +128,54 @@ function recompute_dynamic_data(state, context) { ser_u16(context.dynamic_stroke_data, b); ser_u16(context.dynamic_stroke_data, stroke.width); + let lod; + const perceptual_width = stroke.width * state.canvas.zoom; + // Copypaste from the WASM version @lod + if (perceptual_width < 1.9) { + lod = 0; + } else if (perceptual_width < 4.56) { + lod = 1; + } else if (perceptual_width < 6.12) { + lod = 2; + } else if (perceptual_width < 25.08) { + lod = 3; + } else if (perceptual_width < 122.74) { + lod = 4; + } else if (perceptual_width < 1710.0) { + lod = 5; + } else { + lod = 6; + } + + if (batch_size > 0 && lod !== last_lod) { + tv_add(context.dynamic_instance_batches, batch_size); + tv_add(context.dynamic_instance_batches, last_lod); + batch_size = 0; + } + + batch_size += stroke.points.length; + last_lod = lod; + stroke_index += 1; // TODO: proper player Z order } } } + if (batch_size > 0) { + tv_add(context.dynamic_instance_batches, batch_size); + tv_add(context.dynamic_instance_batches, last_lod); + } + context.dynamic_segment_count = total_points; context.dynamic_stroke_count = total_strokes; + + let batch_base = 0; + + for (let i = 0; i < context.dynamic_instance_batches.size; i += 2) { + const nbatches = context.dynamic_instance_batches.data[i * 2 + 0]; + context.dynamic_instance_batches.data[i * 2 + 0] = batch_base; + batch_base += nbatches; + } } function geometry_start_prestroke(state, player_id) { diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index acddca3..a2c65bb 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -323,8 +323,10 @@ function init_webgl(state, context) { gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + /* gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.NOTEQUAL); + */ context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query_webgl2'); if (context.gpu_timer_ext === null) { @@ -358,6 +360,7 @@ function init_webgl(state, context) { 'b_strokes_static': gl.createBuffer(), 'i_strokes_static': gl.createBuffer(), 'b_strokes_dynamic': gl.createBuffer(), + 'i_strokes_dynamic': gl.createBuffer(), 'b_instance_dot': gl.createBuffer(), 'b_instance_grid': gl.createBuffer(), 'b_dot': gl.createBuffer(), @@ -379,7 +382,7 @@ function init_webgl(state, context) { gl.bindTexture(gl.TEXTURE_2D, context.textures['dynamic_stroke_data']); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.dynamic_stroke_texture_size, config.dynamic_stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.dynamic_stroke_texture_size * config.dynamic_stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.stroke_texture_size, config.stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.stroke_texture_size * config.stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload gl.bindTexture(gl.TEXTURE_2D, context.textures['ui']); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);