diff --git a/.gitignore b/.gitignore index 661a671..72b65f9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ doca.txt data/ client/*.dot server/points.txt +*.o +*.out diff --git a/README.md b/README.md index 6711eae..0b51884 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,14 @@ Release: + Benchmark harness + Reuse points, pack "nodraw" in high bit of stroke id (probably have at least one more bit, so up to 4 flag configurations) + Draw dynamic data (strokes in progress) + * Webassembly for core LOD generation - Z-prepass fringe bug (also, when do we enable the prepass?) - Textured quads (pictures, code already written in older version) - Resize and move pictures (draw handles) + Bugs + GC stalls!!! + Stroke previews get connected when drawn without panning on touch devices + + Redraw HTML (cursors) on local canvas moves - Debug - Restore ability to limit event range * Listeners/events/multiplayer @@ -26,10 +28,11 @@ Release: + Player list + Follow player + Color picker (or at the very least an Open Color color pallete) + - EYE DROPPER! + - Dynamic svg cursor to represent the brush - Eraser - Line drawing - Undo/redo - - Dynamic svg cursor to represent the brush * Polish * Use typedvector where appropriate - Show what's happening while the desk is loading (downloading, processing, uploading to gpu) diff --git a/client/index.html b/client/index.html index 851670c..2bbdaf0 100644 --- a/client/index.html +++ b/client/index.html @@ -14,6 +14,7 @@ + diff --git a/client/index.js b/client/index.js index 5d17f7e..ce9976a 100644 --- a/client/index.js +++ b/client/index.js @@ -25,8 +25,8 @@ 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 benchmark: { - zoom: 0.00003, - offset: { x: 1400, y: 400 }, + zoom: 0.03, + offset: { x: 720, y: 400 }, frames: 500, }, }; @@ -169,6 +169,7 @@ function main() { 'current_strokes': {}, 'rdp_mask': new Uint8Array(1024), + 'rdp_traverse_stack': new Uint32Array(4096), 'queue': [], 'events': [], @@ -177,6 +178,7 @@ function main() { 'total_points': 0, 'coordinates': tv_create(Float32Array, 4096), + 'line_threshold': tv_create(Float32Array, 4096), 'segments_from': { 'data': null, diff --git a/client/math.js b/client/math.js index e5db202..5ccdabd 100644 --- a/client/math.js +++ b/client/math.js @@ -70,7 +70,7 @@ function process_rdp_indices_r(state, zoom, mask, stroke, start, end) { while (stack.length > 0) { const region = stack.pop(); - const max = rdp_find_max(state, zoom, stroke, region.start, region.end); + const max = rdp_find_max(state, zoom, stroke.coords_from, region.start, region.end); if (max !== -1) { mask[max] = 1; diff --git a/client/speed.js b/client/speed.js new file mode 100644 index 0000000..5f3b617 --- /dev/null +++ b/client/speed.js @@ -0,0 +1,114 @@ +function rdp_find_max(state, zoom, coords_from, start, end) { + // Finds a point from the range [start, end) with the maximum distance from the line (start--end) that is also further than EPS + const EPS = 1.0 / zoom; + + let result = -1; + let max_dist = 0; + + const ax = state.coordinates.data[coords_from + start * 2 + 0]; + const ay = state.coordinates.data[coords_from + start * 2 + 1]; + const bx = state.coordinates.data[coords_from + end * 2 + 0]; + const by = state.coordinates.data[coords_from + end * 2 + 1]; + + const dx = bx - ax; + const dy = by - ay; + + const dist_ab = Math.sqrt(dx * dx + dy * dy); + const dir_nx = dy / dist_ab; + const dir_ny = -dx / dist_ab; + + for (let i = start + 1; i < end; ++i) { + const px = state.coordinates.data[coords_from + i * 2 + 0]; + const py = state.coordinates.data[coords_from + i * 2 + 1]; + + const apx = px - ax; + const apy = py - ay; + + const dist = Math.abs(apx * dir_nx + apy * dir_ny); + + if (dist > EPS && dist > max_dist) { + result = i; + max_dist = dist; + } + } + + state.stats.rdp_max_count++; + state.stats.rdp_segments += end - start - 1; + + return result; +} + +function do_lod(state, context) { + const zoom = state.canvas.zoom; + const segments_data = state.segments.data; + + let segments_head = 0; + + for (let i = 0; i < context.clipped_indices.count; ++i) { + const stroke_index = context.clipped_indices.data[i]; + const stroke = state.events[stroke_index]; + const point_count = (stroke.coords_to - stroke.coords_from) / 2; + const coords_from = stroke.coords_from; + + if (point_count > state.rdp_traverse_stack.length) { + //console.count('allocate') + state.rdp_traverse_stack = new Uint32Array(round_to_pow2(point_count, 4096)); + } + + const stack = state.rdp_traverse_stack; + + // Basic CSR crap + state.segments_from.data[i] = segments_head; + + if (state.canvas.zoom <= state.line_threshold.data[stroke_index]) { + segments_data[segments_head++] = 0; + segments_data[segments_head++] = point_count - 1; + } else { + let segment_count = 2; + + segments_data[segments_head++] = 0; + + let head = 0; + // Using stack.push() allocates even if the stack is pre-allocated! + + stack[head++] = 0; + stack[head++] = 0; + stack[head++] = point_count - 1; + + while (head > 0) { + const end = stack[--head]; + const value = start = stack[--head]; + const type = stack[--head]; + + if (type === 1) { + segments_data[segments_head++] = value; + } else { + const max = rdp_find_max(state, zoom, coords_from, start, end); + if (max !== -1) { + segment_count += 1; + + stack[head++] = 0; + stack[head++] = max; + stack[head++] = end; + + stack[head++] = 1; + stack[head++] = max; + stack[head++] = -1; + + stack[head++] = 0; + stack[head++] = start; + stack[head++] = max; + } + } + } + + segments_data[segments_head++] = point_count - 1; + + if (segment_count === 2 && state.canvas.zoom > state.line_threshold.data[stroke_index]) { + state.line_threshold.data[stroke_index] = state.canvas.zoom; + } + } + } + + return segments_head; +} diff --git a/client/wasm/lod.c b/client/wasm/lod.c new file mode 100644 index 0000000..947b717 --- /dev/null +++ b/client/wasm/lod.c @@ -0,0 +1,115 @@ +float sqrtf(float x); +float fabsf(float x); + +static int +rdp_find_max(float *coordinates, float zoom, int coords_from, + int segment_start, int segment_end) +{ + float EPS = 1.0 / zoom; + + int result = -1; + float max_dist = 0.0f; + + float ax = coordinates[coords_from + segment_start * 2 + 0]; + float ay = coordinates[coords_from + segment_start * 2 + 1]; + float bx = coordinates[coords_from + segment_end * 2 + 0]; + float by = coordinates[coords_from + segment_end * 2 + 1]; + + float dx = bx - ax; + float dy = by - ay; + + float dist_ab = sqrtf(dx * dx + dy * dy); + float dir_nx = dy / dist_ab; + float dir_ny = -dx / dist_ab; + + for (int i = segment_start + 1; i < segment_end; ++i) { + float px = coordinates[coords_from + i * 2 + 0]; + float py = coordinates[coords_from + i * 2 + 1]; + + float apx = px - ax; + float apy = py - ay; + + float dist = fabsf(apx * dir_nx + apy * dir_ny); + + if (dist > EPS && dist > max_dist) { + result = i; + max_dist = dist; + } + } + + return(result); +} + +int +do_lod(int *clipped_indices, int clipped_count, float zoom, + int *stroke_coords_from, int *stroke_coords_to, + float *line_threshold, float *coordinates, + int *segments_from, int *segments) +{ + int segments_head = 0; + int stack[4096]; + + for (int i = 0; i < clipped_count; ++i) { + int stroke_index = clipped_indices[i]; + + // TODO: convert to a proper CSR, save half the memory + int coords_from = stroke_coords_from[stroke_index]; + int coords_to = stroke_coords_to[stroke_index]; + + int point_count = (coords_to - coords_from) / 2; + + // Basic CSR crap + segments_from[i] = segments_head; + + if (zoom < line_threshold[stroke_index]) { + // Fast paths for collapsing to a single line segment + segments[segments_head++] = 0; + segments[segments_head++] = point_count - 1; + continue; + } + + int segment_count = 2; + int stack_head = 0; + + segments[segments_head++] = 0; + + stack[stack_head++] = 0; + stack[stack_head++] = 0; + stack[stack_head++] = point_count - 1; + + while (stack_head > 0) { + int end = stack[--stack_head]; + int start = stack[--stack_head]; + int type = stack[--stack_head]; + + if (type == 1) { + segments[segments_head++] = start; + } else { + int max = rdp_find_max(coordinates, zoom, coords_from, start, end); + if (max != -1) { + segment_count += 1; + + stack[stack_head++] = 0; + stack[stack_head++] = max; + stack[stack_head++] = end; + + stack[stack_head++] = 1; + stack[stack_head++] = max; + stack[stack_head++] = -1; + + stack[stack_head++] = 0; + stack[stack_head++] = start; + stack[stack_head++] = max; + } + } + } + + segments[segments_head++] = point_count - 1; + + if (segment_count == 2 && zoom > line_threshold[stroke_index]) { + line_threshold[stroke_index] = zoom; + } + } + + return(segments_head); +} \ No newline at end of file diff --git a/client/wasm/lod.wasm b/client/wasm/lod.wasm new file mode 100755 index 0000000..d78724f Binary files /dev/null and b/client/wasm/lod.wasm differ diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 2792d73..9970f2d 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -36,111 +36,6 @@ function geometry_prepare_stroke(state) { }; } -function rdp_find_max(state, zoom, stroke, start, end) { - // Finds a point from the range [start, end) with the maximum distance from the line (start--end) that is also further than EPS - const EPS = 1.0 / zoom; - - let result = -1; - let max_dist = 0; - - const ax = state.coordinates.data[stroke.coords_from + start * 2 + 0]; - const ay = state.coordinates.data[stroke.coords_from + start * 2 + 1]; - const bx = state.coordinates.data[stroke.coords_from + end * 2 + 0]; - const by = state.coordinates.data[stroke.coords_from + end * 2 + 1]; - - const dx = bx - ax; - const dy = by - ay; - - const dist_ab = Math.sqrt(dx * dx + dy * dy); - const dir_nx = dy / dist_ab; - const dir_ny = -dx / dist_ab; - - for (let i = start + 1; i < end; ++i) { - const px = state.coordinates.data[stroke.coords_from + i * 2 + 0]; - const py = state.coordinates.data[stroke.coords_from + i * 2 + 1]; - - const apx = px - ax; - const apy = py - ay; - - const dist = Math.abs(apx * dir_nx + apy * dir_ny); - - if (dist > EPS && dist > max_dist) { - result = i; - max_dist = dist; - } - } - - state.stats.rdp_max_count++; - state.stats.rdp_segments += end - start - 1; - - return result; -} - -function do_lod(state, context) { - let stack = new Array(4096); - - for (let i = 0; i < context.clipped_indices.count; ++i) { - const stroke_index = context.clipped_indices.data[i]; - const stroke = state.events[stroke_index]; - const point_count = (stroke.coords_to - stroke.coords_from) / 2; - - if (point_count > 4096) { - stack = new Array(round_to_pow2(point_count, 4096)); - } - - // Basic CSR crap - state.segments_from.data[i] = state.segments.count; - - if (state.canvas.zoom <= stroke.turns_into_straight_line_zoom) { - state.segments.data[state.segments.count++] = 0; - state.segments.data[state.segments.count++] = point_count - 1; - } else { - let segment_count = 2; - - state.segments.data[state.segments.count++] = 0; - - let head = 0; - // Using stack.push() allocates even if the stack is pre-allocated! - - stack[head++] = 0; - stack[head++] = 0; - stack[head++] = point_count - 1; - - while (head > 0) { - const end = stack[--head]; - const value = start = stack[--head]; - const type = stack[--head]; - - if (type === 1) { - state.segments.data[state.segments.count++] = value; - } else { - const max = rdp_find_max(state, state.canvas.zoom, stroke, start, end); - if (max !== -1) { - segment_count += 1; - - stack[head++] = 0; - stack[head++] = max; - stack[head++] = end; - - stack[head++] = 1; - stack[head++] = max; - stack[head++] = -1; - - stack[head++] = 0; - stack[head++] = start; - stack[head++] = max; - } - } - } - - state.segments.data[state.segments.count++] = point_count - 1; - - if (segment_count === 2 && state.canvas.zoom > stroke.turns_into_straight_line_zoom) { - stroke.turns_into_straight_line_zoom = state.canvas.zoom; - } - } - } -} function geometry_write_instances(state, context) { if (state.segments_from.cap < context.clipped_indices.count + 1) { @@ -159,28 +54,36 @@ function geometry_write_instances(state, context) { state.stats.rdp_max_count = 0; state.stats.rdp_segments = 0; - do_lod(state, context); + const segment_count = do_lod(state, context); + state.segments.count = segment_count; state.segments_from.data[context.clipped_indices.count] = state.segments.count; state.segments_from.count = context.clipped_indices.count + 1; context.instance_data_points = tv_ensure(context.instance_data_points, state.segments.count * 2); context.instance_data_ids = tv_ensure(context.instance_data_ids, state.segments.count); + tv_clear(context.instance_data_points); tv_clear(context.instance_data_ids); + const clipped = context.clipped_indices.data; + const segments_from = state.segments_from.data; + const segments = state.segments.data; + const coords = state.coordinates.data; + const events = state.events; + for (let i = 0; i < state.segments_from.count - 1; ++i) { - const stroke_index = context.clipped_indices.data[i]; - const stroke = state.events[stroke_index]; - const from = state.segments_from.data[i]; - const to = state.segments_from.data[i + 1]; + const stroke_index = clipped[i]; + const coords_from = events[stroke_index].coords_from; + const from = segments_from[i]; + const to = segments_from[i + 1]; for (let j = from; j < to; ++j) { - const base_this = state.segments.data[j]; + const base_this = segments[j]; - const ax = state.coordinates.data[stroke.coords_from + base_this * 2 + 0]; - const ay = state.coordinates.data[stroke.coords_from + base_this * 2 + 1]; + const ax = coords[coords_from + base_this * 2 + 0]; + const ay = coords[coords_from + base_this * 2 + 1]; tv_add(context.instance_data_points, ax); tv_add(context.instance_data_points, ay); @@ -205,10 +108,12 @@ function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = fa stroke.bbox = stroke_bbox(state, stroke); stroke.area = box_area(stroke.bbox); - stroke.turns_into_straight_line_zoom = -1; context.stroke_data = ser_ensure_by(context.stroke_data, config.bytes_per_stroke); + state.line_threshold = tv_ensure(state.line_threshold, round_to_pow2(state.stroke_count, 4096)); + tv_add(state.line_threshold, -1); + const color_u32 = stroke.color; const r = (color_u32 >> 16) & 0xFF; const g = (color_u32 >> 8) & 0xFF; diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 1086f05..f89e8d7 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -235,6 +235,7 @@ function mousemove(e, state, context) { } fire_event(state, movecanvas_event(state)); + draw_html(state, context); do_draw = true; } @@ -504,7 +505,7 @@ function touchmove(e, state, context) { state.touch.second_finger_position = second_finger_position; fire_event(state, movecanvas_event(state)); - + draw_html(state, context); schedule_draw(state, context); return;