diff --git a/client/bvh.js b/client/bvh.js index fc270f5..f1f9b61 100644 --- a/client/bvh.js +++ b/client/bvh.js @@ -155,9 +155,9 @@ function bvh_add_stroke(bvh, index, stroke) { } } -function bvh_intersect_quad(bvh, quad) { +function bvh_intersect_quad(bvh, quad, result_buffer) { if (bvh.root === null) { - return []; + return; } const stack = [bvh.root]; @@ -172,20 +172,25 @@ function bvh_intersect_quad(bvh, quad) { } if (node.is_leaf) { - result.push(node.stroke_index); + result_buffer.data[result_buffer.count] = node.stroke_index; + result_buffer.count += 1; } else { stack.push(node.child1, node.child2); } } - - return result; } -function bvh_clip(state, context, lod_level) { - const lod = context.lods[lod_level]; +function bvh_clip(state, context) { + if (state.stroke_count === 0) { + return; + } - lod.indices = ser_ensure(lod.indices, lod.total_points * 6 * 4); - ser_clear(lod.indices); + if (context.clipped_indices.cap < state.stroke_count) { + context.clipped_indices.cap = round_to_pow2(state.stroke_count, 4096); + context.clipped_indices.data = new Uint32Array(context.clipped_indices.cap); + } + + context.clipped_indices.count = 0; 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}); @@ -199,35 +204,9 @@ function bvh_clip(state, context, lod_level) { 'y2': screen_bottomright.y }; - const stroke_indices = bvh_intersect_quad(state.bvh, screen); - - stroke_indices.sort((a, b) => a - b); - - for (const i of stroke_indices) { - if (state.debug.limit_to && i >= state.debug.render_to) break; - - const event = state.events[i]; - - if (!(state.debug.limit_from && i < state.debug.render_from)) { - if (event.type === EVENT.STROKE && !event.deleted && event.points.length > 0) { - const points = event.lods[lod_level].points; - - for (let j = 0; j < points.length - 1; ++j) { - const base = event.lods[lod_level].starting_index + j * 4; - - // We draw quads as [1, 2, 3, 4, 3, 2] - ser_u32(lod.indices, base + 0); - ser_u32(lod.indices, base + 1); - ser_u32(lod.indices, base + 2); - ser_u32(lod.indices, base + 3); - ser_u32(lod.indices, base + 2); - ser_u32(lod.indices, base + 1); - } - } - } - } + bvh_intersect_quad(state.bvh, screen, context.clipped_indices); - return lod.indices.offset / 4; + new Uint32Array(context.clipped_indices.data.buffer, 0, context.clipped_indices.count).sort(); // we need to draw back to front still! } function bvh_construct_rec(bvh, vertical, strokes) { diff --git a/client/client_recv.js b/client/client_recv.js index e3743b5..97075bc 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -32,22 +32,23 @@ function des_f32(d) { return value; } -function des_f32array(d, count) { - const result = []; - - for (let i = 0; i < count; ++i) { - const item = d.view.getFloat32(d.offset, true); - d.offset += 4; - result.push(item); +function des_align(d, to) { + // TODO: non-stupid version of this + while (d.offset % to != 0) { + d.offset++; } +} +function des_f32array(d, count) { + const result = new Float32Array(d.buffer, d.offset, count); + d.offset += 4 * count; return result; } -function des_event(d) { +function des_event(d, state = null) { const event = {}; - event.type = des_u8(d); + event.type = des_u32(d); event.user_id = des_u32(d); switch (event.type) { @@ -76,18 +77,18 @@ function des_event(d) { const point_count = des_u16(d); const width = des_u16(d); const color = des_u32(d); + const coords = des_f32array(d, point_count * 2); + event.coords_from = state.coordinates.count; + event.coords_to = state.coordinates.count + point_count * 2; + + state.coordinates.data.set(coords, state.coordinates.count); + state.coordinates.count += point_count * 2; + event.stroke_id = stroke_id; - event.points = []; event.lods = []; - for (let i = 0; i < point_count; ++i) { - const x = coords[2 * i + 0]; - const y = coords[2 * i + 1]; - event.points.push({'x': x, 'y': y}); - } - event.color = color; event.width = width; @@ -178,6 +179,8 @@ function handle_event(state, context, event, options = {}) { need_draw = true; //} + event.index = state.events.length; + geometry_add_stroke(state, context, event, state.events.length, options.skip_bvh === true); state.stroke_count++; @@ -342,6 +345,9 @@ async function handle_message(state, context, d) { const event_count = des_u32(d); const user_count = des_u32(d); + const total_points = des_u32(d); + + state.coordinates.data = new Float32Array(round_to_pow2(total_points * 2, 4096)); if (config.debug_print) console.debug(`${event_count} events in init`); @@ -355,11 +361,13 @@ async function handle_message(state, context, d) { init_player_defaults(state, user_id, user_color, user_width); } + des_align(d, 4); + for (let i = 0; i < event_count; ++i) { - const event = des_event(d); + const event = des_event(d, state); handle_event(state, context, event, {'skip_bvh': true}); - if (event.type !== EVENT.STROKE || event.points.length > 0) { + if (event.type !== EVENT.STROKE || event.coords_to - event.coords_from > 0) { state.events.push(event); } } diff --git a/client/client_send.js b/client/client_send.js index 980cdc9..e0a9a15 100644 --- a/client/client_send.js +++ b/client/client_send.js @@ -152,6 +152,7 @@ async function send_ack(sn) { } async function sync_queue(state) { + if (ws === null) { if (config.debug_print) console.debug('socket has closed, stopping SYNs'); return; diff --git a/client/index.js b/client/index.js index 8ec8845..07f9724 100644 --- a/client/index.js +++ b/client/index.js @@ -24,7 +24,7 @@ const config = { initial_offline_timeout: 1000, default_color: 0x00, default_width: 8, - bytes_per_quad: 4 * 4 + 4, // axy, bxy, stroke_id + bytes_per_instance: 4 * 4 + 4, // axy, bxy, stroke_id bytes_per_stroke: 3 + 1, // r, g, b, width initial_static_bytes: 4096 * 16, initial_dynamic_bytes: 4096, @@ -172,6 +172,11 @@ function main() { 'starting_index': 0, 'total_points': 0, + 'coordinates': { + 'data': null, + 'count': 0, + }, + 'bvh': { 'nodes': [], 'root': null, @@ -215,10 +220,19 @@ function main() { 'buffers': {}, 'locations': {}, 'textures': {}, - + 'dynamic_serializer': serializer_create(config.initial_dynamic_bytes), 'dynamic_index_serializer': serializer_create(config.initial_dynamic_bytes), + // TODO: i seem to have a lot of these, maybe make a few utility functions? similar to serializer, but for pure typedarray + 'clipped_indices': { + 'data': null, + 'count': 0, + 'cap': 0, + }, + + 'instance_data': serializer_create(config.initial_static_bytes), + 'lods': [], 'stroke_data': serializer_create(config.initial_static_bytes), diff --git a/client/math.js b/client/math.js index f6503f3..a4714c7 100644 --- a/client/math.js +++ b/client/math.js @@ -1,3 +1,7 @@ +function round_to_pow2(value, multiple) { + return (value + multiple - 1) & -multiple; +} + function screen_to_canvas(state, p) { // should be called with coordinates obtained from MouseEvent.clientX/clientY * window.devicePixelRatio const xc = (p.x - state.canvas.offset.x) / state.canvas.zoom; @@ -6,36 +10,39 @@ function screen_to_canvas(state, p) { return {'x': xc, 'y': yc}; } -function rdp_find_max(zoom, points, start, end) { +function rdp_find_max(state, zoom, stroke, start, end) { const EPS = 1.0 / zoom; // const EPS = 10.0; let result = -1; let max_dist = 0; - const a = points[start]; - const b = points[end]; + 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 = b.x - a.x; - const dy = b.y - a.y; + const dx = bx - ax; + const dy = by - ay; const dist_ab = Math.sqrt(dx * dx + dy * dy); const sin_theta = dy / dist_ab; const cos_theta = dx / dist_ab; for (let i = start; i < end; ++i) { - const p = points[i]; + const px = state.coordinates.data[stroke.coords_from + i * 2 + 0]; + const py = state.coordinates.data[stroke.coords_from + i * 2 + 1]; - const ox = p.x - a.x; - const oy = p.y - a.y; + const ox = px - ax; + const oy = py - ay; const rx = cos_theta * ox + sin_theta * oy; const ry = -sin_theta * ox + cos_theta * oy; - const x = rx + a.x; - const y = ry + a.y; + const x = rx + ax; + const y = ry + ay; - const dist = Math.abs(y - a.y); + const dist = Math.abs(y - ay); if (dist > EPS && dist > max_dist) { result = i; @@ -46,45 +53,37 @@ function rdp_find_max(zoom, points, start, end) { return result; } -function process_rdp_r(zoom, mask, points, start, end) { +function process_rdp_indices_r(state, zoom, mask, stroke, start, end) { let result = 0; - const max = rdp_find_max(zoom, points, start, end); + const max = rdp_find_max(state, zoom, stroke, start, end); if (max !== -1) { mask[max] = 1; result += 1; - result += process_rdp_r(zoom, mask, points, start, max); - result += process_rdp_r(zoom, mask, points, max, end); + result += process_rdp_indices_r(state, zoom, mask, stroke, start, max); + result += process_rdp_indices_r(state, zoom, mask, stroke, max, end); } return result; } -function process_rdp(state, zoom, points) { - if (state.rdp_mask.length < points.length) { - state.rdp_mask = new Uint8Array(points.length); +function process_rdp_indices(state, zoom, stroke) { + const point_count = (stroke.coords_to - stroke.coords_from) / 2; + + if (state.rdp_mask.length < point_count) { + state.rdp_mask = new Uint8Array(point_count); } - state.rdp_mask.fill(0, 0, points.length); + state.rdp_mask.fill(0, 0, point_count); const mask = state.rdp_mask; - const npoints = process_rdp_r(zoom, mask, points, 0, points.length - 1); + const npoints = 2 + process_rdp_indices_r(state, zoom, mask, stroke, 0, point_count - 1); // 2 is for the first and last vertex, which do not get included by the recursive functions, but should always be there at any lod level mask[0] = 1; - mask[points.length - 1] = 1; - - const result = new Array(npoints); - let j = 0; - - for (let i = 0; i < points.length; ++i) { - if (mask[i] === 1) { - result[j] = points[i]; - ++j; - } - } + mask[point_count - 1] = 1; - return result; + return npoints; } function process_ewmv(points, round = false) { @@ -103,9 +102,9 @@ function process_ewmv(points, round = false) { return result; } -function process_stroke(state, zoom, points) { +function process_stroke(state, zoom, stroke) { // const result0 = process_ewmv(points); - const result1 = process_rdp(state, zoom, points, true); + const result1 = process_rdp_indices(state, zoom, stroke, true); return result1; } @@ -202,20 +201,23 @@ function segment_interesects_quad(a, b, quad_topleft, quad_bottomright, quad_top return false; } -function stroke_bbox(stroke) { +function stroke_bbox(state, stroke) { const radius = stroke.width / 2; - - let min_x = stroke.points[0].x - radius; - let max_x = stroke.points[0].x + radius; - let min_y = stroke.points[0].y - radius; - let max_y = stroke.points[0].y + radius; + let min_x = state.coordinates.data[stroke.coords_from + 0] - radius; + let max_x = state.coordinates.data[stroke.coords_from + 0] + radius; + + let min_y = state.coordinates.data[stroke.coords_from + 1] - radius; + let max_y = state.coordinates.data[stroke.coords_from + 1] + radius; - for (const p of stroke.points) { - min_x = Math.min(min_x, p.x - radius); - min_y = Math.min(min_y, p.y - radius); - max_x = Math.max(max_x, p.x + radius); - max_y = Math.max(max_y, p.y + radius); + for (let i = stroke.coords_from + 2; i < stroke.coords_to; i += 2) { + const px = state.coordinates.data[i + 0]; + const py = state.coordinates.data[i + 1]; + + min_x = Math.min(min_x, px - radius); + min_y = Math.min(min_y, py - radius); + max_x = Math.max(max_x, px + radius); + max_y = Math.max(max_y, py + radius); } return {'x1': min_x, 'y1': min_y, 'x2': max_x, 'y2': max_y, 'cx': (max_x + min_x) / 2, 'cy': (max_y + min_y) / 2}; @@ -246,6 +248,10 @@ function quad_union(a, b) { }; } +function box_area(box) { + return (box.x2 - box.x1) * (box.y2 - box.y1); +} + function segments_onscreen(state, context, do_clip) { // TODO: handle stroke width diff --git a/client/webgl_draw.js b/client/webgl_draw.js index 28ecf32..ad082e2 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -48,18 +48,6 @@ function upload_square_rgba16ui_texture(gl, serializer, texture_size) { function draw(state, context) { const cpu_before = performance.now(); - let lod_level = -1; - - for (let i = context.lods.length - 1; i >= 0; --i) { - const level = context.lods[i]; - if (state.canvas.zoom <= level.max_zoom) { - lod_level = i; - break; - } - } - - const lod = context.lods[lod_level]; - state.timers.raf = false; const gl = context.gl; @@ -79,19 +67,19 @@ function draw(state, context) { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - gl.bindBuffer(gl.ARRAY_BUFFER, lod.data_buffer); - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, lod.index_buffer); - - // static data, per-quad: points, stroke_ids - // static data, per-stroke (texture): color, width (radius) - upload_if_needed(gl, gl.ARRAY_BUFFER, lod.segments); - locations = context.locations['sdf'].main; + buffers = context.buffers['sdf']; gl.useProgram(context.programs['sdf'].main); - const segment_count = lod.segments.offset / config.bytes_per_quad; - + + bvh_clip(state, context); + const segment_count = geometry_write_instances(state, context); + + // TODO: maybe have a pool of buffers (pow2?) and select an appropriate one + gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance']); + gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array(context.instance_data.buffer, 0, segment_count * config.bytes_per_instance), gl.DYNAMIC_DRAW); + gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); @@ -103,23 +91,26 @@ function draw(state, context) { gl.enableVertexAttribArray(locations['a_ab']); gl.enableVertexAttribArray(locations['a_stroke_id']); - gl.vertexAttribPointer(locations['a_ab'], 4, gl.FLOAT, false, 5 * 4, 0); - gl.vertexAttribIPointer(locations['a_stroke_id'], 1, gl.INT, 5 * 4, 4 * 4); + gl.vertexAttribPointer(locations['a_ab'], 4, gl.FLOAT, false, config.bytes_per_instance, 0); + gl.vertexAttribIPointer(locations['a_stroke_id'], 1, gl.INT, config.bytes_per_instance, 4 * 4); gl.vertexAttribDivisor(locations['a_ab'], 1); gl.vertexAttribDivisor(locations['a_stroke_id'], 1); gl.bindTexture(gl.TEXTURE_2D, context.textures['stroke_data']); + // TODO: this is stable data, only upload new strokes as they arrive upload_square_rgba16ui_texture(gl, context.stroke_data, config.stroke_texture_size); gl.activeTexture(gl.TEXTURE0); gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, segment_count); // TODO: based on clipping results - + + document.getElementById('debug-stats').innerHTML = ` + Segments onscreen: ${segment_count} + Canvas offset: (${state.canvas.offset.x}, ${state.canvas.offset.y}) + Canvas zoom: ${Math.round(state.canvas.zoom * 100000) / 100000}`; /* - const before_clip = performance.now(); - const index_count = bvh_clip(state, context, lod_level); const after_clip = performance.now(); gl.bindBuffer(gl.ARRAY_BUFFER, lod.data_buffer); @@ -128,12 +119,7 @@ function draw(state, context) { upload_if_needed(gl, gl.ARRAY_BUFFER, lod.vertices); upload_if_needed(gl, gl.ELEMENT_ARRAY_BUFFER, lod.indices); - document.getElementById('debug-stats').innerHTML = ` - LOD level: ${lod_level} - Segments onscreen: ${index_count} - Canvas offset: (${state.canvas.offset.x}, ${state.canvas.offset.y}) - Canvas zoom: ${Math.round(state.canvas.zoom * 100000) / 100000}`; - + if (index_count > 0) { // DEPTH PREPASS if (state.debug.do_prepass) { diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 58c3e3f..ff37c01 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -36,40 +36,50 @@ function geometry_prepare_stroke(state) { }; } -function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) { - if (!state.online || !stroke || stroke.points.length === 0) return; - - stroke.index = state.events.length; +function geometry_write_instances(state, context) { + context.instance_data = ser_ensure(context.instance_data, state.coordinates.count / 2 * config.bytes_per_instance); + ser_clear(context.instance_data); - for (let i = 0; i < config.lod_levels; ++i) { - const lod = context.lods[i]; + let segment_count = 0; - const points = (i > 0 ? process_stroke(state, lod.max_zoom, stroke.points) : stroke.points); - const segment_serializer = lod.segments = ser_ensure_by(lod.segments, (points.length - 1) * config.bytes_per_quad); + 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 lod_indices_count = process_stroke(state, state.canvas.zoom, stroke); - let starting_index = 0; + segment_count += lod_indices_count - 1; - if (state.events.length > 0) { - const last_stroke = state.events[stroke_index - 1].lods[i]; - starting_index = last_stroke.starting_index + (last_stroke.points.length - 1) * 4; - } + let base_this = 0; + let base_next = 0; - stroke.lods.push({ - 'points': points, - 'starting_index': starting_index, - 'width': stroke.width, - 'color': stroke.color, - }); + for (let j = 0; j < lod_indices_count - 1; ++j) { + while (state.rdp_mask[base_this] == 0) base_this++; + base_next = base_this + 1; + while (state.rdp_mask[base_next] == 0) base_next++; - context.lods[i].total_points += points.length; + 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 bx = state.coordinates.data[stroke.coords_from + base_next * 2 + 0]; + const by = state.coordinates.data[stroke.coords_from + base_next * 2 + 1]; - push_stroke(segment_serializer, stroke.lods[stroke.lods.length - 1], stroke_index); + ser_f32(context.instance_data, ax); + ser_f32(context.instance_data, ay); + ser_f32(context.instance_data, bx); + ser_f32(context.instance_data, by); + ser_u32(context.instance_data, stroke_index); - if (i === 0) { - stroke.bbox = stroke_bbox(stroke); - stroke.area = (stroke.bbox.x2 - stroke.bbox.x1) * (stroke.bbox.y2 - stroke.bbox.y1); + base_this = base_next; } } + + return segment_count; +} + +function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) { + if (!state.online || !stroke || stroke.coords_to - stroke.coords_from === 0) return; + + stroke.bbox = stroke_bbox(state, stroke); + stroke.area = box_area(stroke.bbox); context.stroke_data = ser_ensure_by(context.stroke_data, config.bytes_per_stroke); diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 387fe59..ce9c254 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -451,7 +451,7 @@ function touchend(e, state, context) { const stroke = geometry_prepare_stroke(state); - if (stroke) { + if (false && stroke) { // TODO: FIX! geometry_add_stroke(state, context, stroke, 0); // TODO: stroke index queue_event(state, stroke_event(state)); geometry_clear_player(state, context, state.me); diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index ad5ca51..2a65f0b 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -333,6 +333,7 @@ function init_webgl(state, context) { context.buffers['sdf'] = { 'b_packed_dynamic': gl.createBuffer(), 'b_packed_dynamic_index': gl.createBuffer(), + 'b_instance': gl.createBuffer(), }; context.textures = { diff --git a/server/data-local.sqlite b/server/data-local.sqlite new file mode 100644 index 0000000..ec150a9 Binary files /dev/null and b/server/data-local.sqlite differ diff --git a/server/recv.js b/server/recv.js index daa0e65..9dd29b0 100644 --- a/server/recv.js +++ b/server/recv.js @@ -127,6 +127,8 @@ function handle_event(session, event) { '$y': 0, }); + desks[session.desk_id].total_points += event.points.length; + break; } diff --git a/server/send.js b/server/send.js index 5032630..b040c26 100644 --- a/server/send.js +++ b/server/send.js @@ -7,7 +7,7 @@ import { MESSAGE, SESSION, EVENT } from './enums'; import { sessions, desks } from './storage'; function event_size(event) { - let size = 1 + 4; // type + user_id + let size = 4 + 4; // type + user_id switch (event.type) { case EVENT.PREDRAW: { @@ -94,7 +94,7 @@ export async function send_init(ws) { const desk = desks[desk_id]; let opcode = MESSAGE.INIT; - let size = 1 + 4 + 4 + 4 + 4; // opcode + user_id + lsn + event count + stroke count + user count + let size = 1 + 4 + 4 + 4 + 4 + 4 + 3; // opcode + user_id + lsn + event count + stroke count + user count + total_point_count + align on 4 let session = null; if (session_id in sessions && sessions[session_id].desk_id == desk_id) { @@ -142,6 +142,7 @@ export async function send_init(ws) { ser.u32(s, desk.events.length); ser.u32(s, user_count); + ser.u32(s, desk.total_points); for (const sid in sessions) { const other_session = sessions[sid]; @@ -153,6 +154,8 @@ export async function send_init(ws) { } } + ser.align(s, 4); + for (const event of desk.events) { ser.event(s, event); } diff --git a/server/serializer.js b/server/serializer.js index c7d6d7f..edcbe50 100644 --- a/server/serializer.js +++ b/server/serializer.js @@ -36,8 +36,15 @@ export function bytes(s, bytes) { s.offset += bytes.byteLength; } +export function align(s, to) { + // TODO: non-stupid version of this + while (s.offset % to != 0) { + s.offset++; + } +} + export function event(s, event) { - u8(s, event.type); + u32(s, event.type); // for alignment reasons u32(s, event.user_id); switch (event.type) { diff --git a/server/storage.js b/server/storage.js index 61af85c..de99b2b 100644 --- a/server/storage.js +++ b/server/storage.js @@ -100,22 +100,25 @@ export function startup() { const stroke_dict = {}; - for (const stroke of stored_strokes) { - stroke.points = new Float32Array(stroke.points.buffer); - stroke_dict[stroke.id] = stroke; - } - for (const desk of stored_desks) { desks[desk.id] = desk; desks[desk.id].events = []; + desks[desk.id].total_points = 0; } - + + for (const stroke of stored_strokes) { + stroke.points = new Float32Array(stroke.points.buffer); + stroke_dict[stroke.id] = stroke; + } + for (const event of stored_events) { if (event.type === EVENT.STROKE) { const stroke = stroke_dict[event.stroke_id]; event.points = stroke.points; event.color = stroke.color; event.width = stroke.width; + + desks[event.desk_id].total_points += stroke.points.length / 2; } desks[event.desk_id].events.push(event);