From 28fec7f30670b8ac1f67b48267e083ed675b9983 Mon Sep 17 00:00:00 2001 From: "A.Olokhtonov" Date: Fri, 12 Jan 2024 02:39:23 +0300 Subject: [PATCH] Redraw HTML on canvas move, first draft of wasm LOD core --- .gitignore | 2 + README.md | 5 +- client/index.html | 1 + client/index.js | 6 +- client/math.js | 2 +- client/speed.js | 114 ++++++++++++++++++++++++++++++++ client/wasm/lod.c | 115 ++++++++++++++++++++++++++++++++ client/wasm/lod.wasm | Bin 0 -> 4528 bytes client/webgl_geometry.js | 133 ++++++-------------------------------- client/webgl_listeners.js | 3 +- 10 files changed, 262 insertions(+), 119 deletions(-) create mode 100644 client/speed.js create mode 100644 client/wasm/lod.c create mode 100755 client/wasm/lod.wasm 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 0000000000000000000000000000000000000000..d78724f6d643fb64a609e06672717d6e82778c7c GIT binary patch literal 4528 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 literal 0 HcmV?d00001 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;