Browse Source

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
sdf
A.Olokhtonov 1 month ago
parent
commit
0c3259d00f
  1. 95
      client/bvh.js
  2. 5
      client/config.js
  3. 40
      client/webgl_draw.js

95
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) { function bvh_intersect_quad(state, bvh, quad, result_buffer) {
if (bvh.root === null) { if (bvh.root === null) {
return; return;
@ -206,8 +214,8 @@ function bvh_intersect_quad(state, bvh, quad, result_buffer) {
} }
if (node.is_fullnode) { if (node.is_fullnode) {
if (quad_fully_inside(quad, node.bbox)) { const fully_inside = bvh_copy_fullnode(quad, node, result_buffer);
tv_append(result_buffer, tv_data(node.stroke_indices)); if (fully_inside) {
continue; continue;
} }
} }
@ -292,11 +300,40 @@ function bvh_point(state, p) {
return null; return null;
} }
function bvh_construct_rec(state, bvh, vertical, strokes, depth) { function bvh_construct_rec(state, bvh, strokes, depth) {
if (strokes.length > 1) { if (strokes.length > 1) {
// internal // internal
let sorted_strokes; 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) { if (vertical) {
sorted_strokes = strokes.toSorted((a, b) => a.bbox.cy - b.bbox.cy); sorted_strokes = strokes.toSorted((a, b) => a.bbox.cy - b.bbox.cy);
} else { } else {
@ -306,8 +343,8 @@ function bvh_construct_rec(state, bvh, vertical, strokes, depth) {
const node_index = bvh_make_internal(bvh); const node_index = bvh_make_internal(bvh);
const left_of_split_count = Math.floor(strokes.length / 2); 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 child1 = bvh_construct_rec(state, bvh, 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 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[child1].parent_index = node_index;
bvh.nodes[child2].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) { function bvh_construct(state) {
const strokes = state.events.filter(e => e.type === EVENT.STROKE && e.deleted !== true); const strokes = state.events.filter(e => e.type === EVENT.STROKE && e.deleted !== true);
if (strokes.length > 0) { 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;
} }

5
client/config.js

@ -9,8 +9,9 @@ const config = {
buffer_first_touchmoves: 5, buffer_first_touchmoves: 5,
debug_print: false, debug_print: false,
draw_bvh: false, draw_bvh: false,
draw_fullnodes: false,
zoom_delta: 0.05, zoom_delta: 0.05,
min_zoom_level: -250, min_zoom_level: -275,
max_zoom_level: 100, max_zoom_level: 100,
initial_offline_timeout: 1000, initial_offline_timeout: 1000,
default_color: 0x00, 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) 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 dynamic_stroke_texture_size: 128, // means no more than 128^2 = 16K dynamic strokes at once
ui_texture_size: 16, ui_texture_size: 16,
bvh_fullnode_depth: 5, bvh_fullnode_depth: 6,
pattern_fadeout_min: 0.3, pattern_fadeout_min: 0.3,
pattern_fadeout_max: 0.75, pattern_fadeout_max: 0.75,
min_pressure: 50, min_pressure: 50,

40
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_topleft'], 1);
gl.vertexAttribDivisor(pr.locations['a_bottomright'], 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.drawArraysInstanced(gl.TRIANGLES, 0, 6, quad_count);
gl.vertexAttribDivisor(pr.locations['a_topleft'], 0); gl.vertexAttribDivisor(pr.locations['a_topleft'], 0);

Loading…
Cancel
Save