Browse Source

Redraw HTML on canvas move, first draft of wasm LOD core

ssao
A.Olokhtonov 10 months ago
parent
commit
28fec7f306
  1. 2
      .gitignore
  2. 5
      README.md
  3. 1
      client/index.html
  4. 6
      client/index.js
  5. 2
      client/math.js
  6. 114
      client/speed.js
  7. 115
      client/wasm/lod.c
  8. BIN
      client/wasm/lod.wasm
  9. 133
      client/webgl_geometry.js
  10. 3
      client/webgl_listeners.js

2
.gitignore vendored

@ -3,3 +3,5 @@ doca.txt
data/ data/
client/*.dot client/*.dot
server/points.txt server/points.txt
*.o
*.out

5
README.md

@ -3,12 +3,14 @@ Release:
+ Benchmark harness + 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) + 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) + Draw dynamic data (strokes in progress)
* Webassembly for core LOD generation
- Z-prepass fringe bug (also, when do we enable the prepass?) - Z-prepass fringe bug (also, when do we enable the prepass?)
- Textured quads (pictures, code already written in older version) - Textured quads (pictures, code already written in older version)
- Resize and move pictures (draw handles) - Resize and move pictures (draw handles)
+ Bugs + Bugs
+ GC stalls!!! + GC stalls!!!
+ Stroke previews get connected when drawn without panning on touch devices + Stroke previews get connected when drawn without panning on touch devices
+ Redraw HTML (cursors) on local canvas moves
- Debug - Debug
- Restore ability to limit event range - Restore ability to limit event range
* Listeners/events/multiplayer * Listeners/events/multiplayer
@ -26,10 +28,11 @@ Release:
+ Player list + Player list
+ Follow player + Follow player
+ Color picker (or at the very least an Open Color color pallete) + Color picker (or at the very least an Open Color color pallete)
- EYE DROPPER!
- Dynamic svg cursor to represent the brush
- Eraser - Eraser
- Line drawing - Line drawing
- Undo/redo - Undo/redo
- Dynamic svg cursor to represent the brush
* Polish * Polish
* Use typedvector where appropriate * Use typedvector where appropriate
- Show what's happening while the desk is loading (downloading, processing, uploading to gpu) - Show what's happening while the desk is loading (downloading, processing, uploading to gpu)

1
client/index.html

@ -14,6 +14,7 @@
<script type="text/javascript" src="bvh.js?v=73"></script> <script type="text/javascript" src="bvh.js?v=73"></script>
<script type="text/javascript" src="math.js?v=73"></script> <script type="text/javascript" src="math.js?v=73"></script>
<script type="text/javascript" src="tools.js?v=73"></script> <script type="text/javascript" src="tools.js?v=73"></script>
<script type="text/javascript" src="speed.js?v=73"></script>
<script type="text/javascript" src="webgl_geometry.js?v=73"></script> <script type="text/javascript" src="webgl_geometry.js?v=73"></script>
<script type="text/javascript" src="webgl_shaders.js?v=73"></script> <script type="text/javascript" src="webgl_shaders.js?v=73"></script>
<script type="text/javascript" src="webgl_listeners.js?v=73"></script> <script type="text/javascript" src="webgl_listeners.js?v=73"></script>

6
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) 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
benchmark: { benchmark: {
zoom: 0.00003, zoom: 0.03,
offset: { x: 1400, y: 400 }, offset: { x: 720, y: 400 },
frames: 500, frames: 500,
}, },
}; };
@ -169,6 +169,7 @@ function main() {
'current_strokes': {}, 'current_strokes': {},
'rdp_mask': new Uint8Array(1024), 'rdp_mask': new Uint8Array(1024),
'rdp_traverse_stack': new Uint32Array(4096),
'queue': [], 'queue': [],
'events': [], 'events': [],
@ -177,6 +178,7 @@ function main() {
'total_points': 0, 'total_points': 0,
'coordinates': tv_create(Float32Array, 4096), 'coordinates': tv_create(Float32Array, 4096),
'line_threshold': tv_create(Float32Array, 4096),
'segments_from': { 'segments_from': {
'data': null, 'data': null,

2
client/math.js

@ -70,7 +70,7 @@ function process_rdp_indices_r(state, zoom, mask, stroke, start, end) {
while (stack.length > 0) { while (stack.length > 0) {
const region = stack.pop(); 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) { if (max !== -1) {
mask[max] = 1; mask[max] = 1;

114
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;
}

115
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);
}

BIN
client/wasm/lod.wasm

Binary file not shown.

133
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) { function geometry_write_instances(state, context) {
if (state.segments_from.cap < context.clipped_indices.count + 1) { 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_max_count = 0;
state.stats.rdp_segments = 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.data[context.clipped_indices.count] = state.segments.count;
state.segments_from.count = context.clipped_indices.count + 1; 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_points = tv_ensure(context.instance_data_points, state.segments.count * 2);
context.instance_data_ids = tv_ensure(context.instance_data_ids, state.segments.count); context.instance_data_ids = tv_ensure(context.instance_data_ids, state.segments.count);
tv_clear(context.instance_data_points); tv_clear(context.instance_data_points);
tv_clear(context.instance_data_ids); 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) { for (let i = 0; i < state.segments_from.count - 1; ++i) {
const stroke_index = context.clipped_indices.data[i]; const stroke_index = clipped[i];
const stroke = state.events[stroke_index]; const coords_from = events[stroke_index].coords_from;
const from = state.segments_from.data[i]; const from = segments_from[i];
const to = state.segments_from.data[i + 1]; const to = segments_from[i + 1];
for (let j = from; j < to; ++j) { 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 ax = coords[coords_from + base_this * 2 + 0];
const ay = state.coordinates.data[stroke.coords_from + base_this * 2 + 1]; const ay = coords[coords_from + base_this * 2 + 1];
tv_add(context.instance_data_points, ax); tv_add(context.instance_data_points, ax);
tv_add(context.instance_data_points, ay); 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.bbox = stroke_bbox(state, stroke);
stroke.area = box_area(stroke.bbox); 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); 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 color_u32 = stroke.color;
const r = (color_u32 >> 16) & 0xFF; const r = (color_u32 >> 16) & 0xFF;
const g = (color_u32 >> 8) & 0xFF; const g = (color_u32 >> 8) & 0xFF;

3
client/webgl_listeners.js

@ -235,6 +235,7 @@ function mousemove(e, state, context) {
} }
fire_event(state, movecanvas_event(state)); fire_event(state, movecanvas_event(state));
draw_html(state, context);
do_draw = true; do_draw = true;
} }
@ -504,7 +505,7 @@ function touchmove(e, state, context) {
state.touch.second_finger_position = second_finger_position; state.touch.second_finger_position = second_finger_position;
fire_event(state, movecanvas_event(state)); fire_event(state, movecanvas_event(state));
draw_html(state, context);
schedule_draw(state, context); schedule_draw(state, context);
return; return;

Loading…
Cancel
Save