From 0c3259d00f73d83e30330cd59e3d3b17f1d0939f Mon Sep 17 00:00:00 2001 From: "A.Olokhtonov" Date: Sun, 27 Oct 2024 17:58:52 +0300 Subject: [PATCH] Tweak initial BVH construction to split along the longest axis, instead of alternating between vertical and horizontal. Big perf wins on large boards because of more sane distribution of fullnodes --- client/bvh.js | 95 +++++++++++++++++++++++++++++++++++++++++--- client/config.js | 5 ++- client/webgl_draw.js | 40 ++++++++++++++++++- 3 files changed, 131 insertions(+), 9 deletions(-) diff --git a/client/bvh.js b/client/bvh.js index fd57c2b..0ce7712 100644 --- a/client/bvh.js +++ b/client/bvh.js @@ -189,6 +189,14 @@ function bvh_undelete_stroke(state, stroke) { } } +function bvh_copy_fullnode(quad, node, result_buffer) { + if (quad_fully_inside(quad, node.bbox)) { + tv_append(result_buffer, tv_data(node.stroke_indices)); + return true; + } + return false; +} + function bvh_intersect_quad(state, bvh, quad, result_buffer) { if (bvh.root === null) { return; @@ -206,8 +214,8 @@ function bvh_intersect_quad(state, bvh, quad, result_buffer) { } if (node.is_fullnode) { - if (quad_fully_inside(quad, node.bbox)) { - tv_append(result_buffer, tv_data(node.stroke_indices)); + const fully_inside = bvh_copy_fullnode(quad, node, result_buffer); + if (fully_inside) { continue; } } @@ -292,11 +300,40 @@ function bvh_point(state, p) { return null; } -function bvh_construct_rec(state, bvh, vertical, strokes, depth) { +function bvh_construct_rec(state, bvh, strokes, depth) { if (strokes.length > 1) { // internal let sorted_strokes; + let min_x = strokes[0].bbox.cx; + let min_y = strokes[0].bbox.cy; + let max_x = strokes[0].bbox.cx; + let max_y = strokes[0].bbox.cy; + + for (let i = 0; i < strokes.length; ++i) { + const stroke = strokes[i]; + const cx = stroke.bbox.cx; + const cy = stroke.bbox.cy; + + if (cx < min_x) { + min_x = cx; + } + + if (cy < min_y) { + min_y = cx; + } + + if (cx > max_x) { + max_x = cx; + } + + if (cy > max_y) { + max_y = cy; + } + } + + const vertical = (max_y - min_y) > (max_x - min_x); + if (vertical) { sorted_strokes = strokes.toSorted((a, b) => a.bbox.cy - b.bbox.cy); } else { @@ -306,8 +343,8 @@ function bvh_construct_rec(state, bvh, vertical, strokes, depth) { const node_index = bvh_make_internal(bvh); const left_of_split_count = Math.floor(strokes.length / 2); - const child1 = bvh_construct_rec(state, bvh, !vertical, sorted_strokes.slice(0, left_of_split_count), depth + 1); - const child2 = bvh_construct_rec(state, bvh, !vertical, sorted_strokes.slice(left_of_split_count, sorted_strokes.length), depth + 1); + const child1 = bvh_construct_rec(state, bvh, sorted_strokes.slice(0, left_of_split_count), depth + 1); + const child2 = bvh_construct_rec(state, bvh, sorted_strokes.slice(left_of_split_count, sorted_strokes.length), depth + 1); bvh.nodes[child1].parent_index = node_index; bvh.nodes[child2].parent_index = node_index; @@ -339,6 +376,52 @@ function bvh_construct_rec(state, bvh, vertical, strokes, depth) { function bvh_construct(state) { const strokes = state.events.filter(e => e.type === EVENT.STROKE && e.deleted !== true); if (strokes.length > 0) { - state.bvh.root = bvh_construct_rec(state, state.bvh, true, strokes, 0); + state.bvh.root = bvh_construct_rec(state, state.bvh, strokes, 0); + } +} + +function bvh_get_fullnodes_debug(state, context) { + const bvh = state.bvh; + const result = []; + const stack = []; + + const screen_topleft = screen_to_canvas(state, {'x': 0, 'y': 0}); + const screen_bottomright = screen_to_canvas(state, {'x': context.canvas.width, 'y': context.canvas.height}); + const screen_topright = { 'x': screen_bottomright.x, 'y': screen_topleft.y }; + const screen_bottomleft = { 'x': screen_topleft.x, 'y': screen_bottomright.y }; + + const quad = { + 'x1': screen_topleft.x, + 'y1': screen_topleft.y, + 'x2': screen_bottomright.x, + 'y2': screen_bottomright.y + }; + + if (bvh.root === null) { + return; } + + stack.push({'depth': 0, 'node_index': bvh.root}); + + while (stack.length > 0) { + const entry = stack.pop(); + const node = bvh.nodes[entry.node_index]; + + if (!quads_intersect(node.bbox, quad)) { + continue; + } + + if (node.is_fullnode) { + result.push({...node.bbox}); + continue; + } + + if (!node.is_leaf && entry.depth < config.bvh_fullnode_depth) { + stack.push({'depth': entry.depth + 1, 'node_index': node.child1}); + stack.push({'depth': entry.depth + 1, 'node_index': node.child2}); + } + } + + return result; } + diff --git a/client/config.js b/client/config.js index cb381ff..3485ac9 100644 --- a/client/config.js +++ b/client/config.js @@ -9,8 +9,9 @@ const config = { buffer_first_touchmoves: 5, debug_print: false, draw_bvh: false, + draw_fullnodes: false, zoom_delta: 0.05, - min_zoom_level: -250, + min_zoom_level: -275, max_zoom_level: 100, initial_offline_timeout: 1000, default_color: 0x00, @@ -23,7 +24,7 @@ const config = { 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: 5, + bvh_fullnode_depth: 6, pattern_fadeout_min: 0.3, pattern_fadeout_max: 0.75, min_pressure: 50, diff --git a/client/webgl_draw.js b/client/webgl_draw.js index ec2a163..479d819 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -462,7 +462,45 @@ async function draw(state, context, animate, ts) { gl.vertexAttribDivisor(pr.locations['a_topleft'], 1); gl.vertexAttribDivisor(pr.locations['a_bottomright'], 1); - // Static draw (everything already bound) + gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, quad_count); + + gl.vertexAttribDivisor(pr.locations['a_topleft'], 0); + gl.vertexAttribDivisor(pr.locations['a_bottomright'], 0); + } + + if (config.draw_fullnodes) { + const quads = bvh_get_fullnodes_debug(state, context); + const pr = programs['iquad']; + const bboxes = tv_create(Float32Array, quads.length * 4); + + for (let i = 0; i < quads.length; ++i) { + const bbox = quads[i]; + tv_add(bboxes, bbox.x1); + tv_add(bboxes, bbox.y1); + tv_add(bboxes, bbox.x2); + tv_add(bboxes, bbox.y2); + } + + const quad_count = bboxes.size / 4; + + gl.useProgram(pr.program); + + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_iquads']); + gl.bufferData(gl.ARRAY_BUFFER, tv_data(bboxes), gl.STREAM_DRAW); + + 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.enableVertexAttribArray(pr.locations['a_topleft']); + gl.enableVertexAttribArray(pr.locations['a_bottomright']); + + gl.vertexAttribPointer(pr.locations['a_topleft'], 2, gl.FLOAT, false, 4 * 4, 0); + gl.vertexAttribPointer(pr.locations['a_bottomright'], 2, gl.FLOAT, false, 4 * 4, 2 * 4); + + gl.vertexAttribDivisor(pr.locations['a_topleft'], 1); + gl.vertexAttribDivisor(pr.locations['a_bottomright'], 1); + gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, quad_count); gl.vertexAttribDivisor(pr.locations['a_topleft'], 0);