From 847fb703815b71ee0498c2fb546b170d6cffd34e Mon Sep 17 00:00:00 2001 From: "A.Olokhtonov" Date: Sun, 7 Jan 2024 15:14:10 +0300 Subject: [PATCH] Don't repeat points segment points. Introduce "tv" (typedvector) --- .gitignore | 1 + README.md | 55 ++++++++++-- client/aux.js | 43 +++++++++- client/index.html | 3 +- client/index.js | 36 ++++---- client/math.js | 25 ++---- client/webgl_draw.js | 131 +++++------------------------ client/webgl_geometry.js | 170 ++++++++++++++++++++++++++++---------- client/webgl_listeners.js | 40 +++++++-- client/webgl_shaders.js | 25 +++--- 10 files changed, 311 insertions(+), 218 deletions(-) diff --git a/.gitignore b/.gitignore index 9029ada..661a671 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ server/images doca.txt data/ client/*.dot +server/points.txt diff --git a/README.md b/README.md index c943d7f..aa8d7b8 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,48 @@ -test +Release: + * Engine + + 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) + - Textured quads (pictures, code already written in older version) + - Resize and move pictures (draw handles) + - Z-prepass fringe bug (also, when do we enable the prepass?) + - Restore ability to limit event range + - Only upload stroke data to texture as it arrives (texSubImage2D) + - Listeners/events + - Investigate skipped inputs on mobile (panning, zooming) + - Save events to indexeddb (as some kind of a blob), restore on reconnect and page reload + - Separate events and other data clearly (events are self-contained, other data is temporal/non-vital) + - Do NOT use session id as player id LUL + - Local prediction for tools! + - Drag with mouse button 3 + - Missing features I do not consider bonus + - Eraser + - Line drawing + - Player screens/pointers + - Follow player (like Ligma) + - Color picker (or at the very least an Open Color color pallete) + - Undo/redo + - Dynamic svg cursor to represent the brush + - Polish + - Show what's happening while the desk is loading (downloading, processing, uploading to gpu) + - Settings panel (including the setting for "offline mode") + - Presentation / "marketing" + - Title + - Icon + - Product page (github readme, demo videos) -# test +Bonus: + - Handle pressure + - Add pressure data to quads + - Draw capsules instead of segments + - Adjust curve simplification to include pressure info + - Curve modification + - Select curves (with a lasso?) + - Move whole curve + - Move single point + - Move multiple points + - Customizable background + - Color, textures, procedural -a -- b -- c - -*d* - -**e** +Bonus-bonus: + - Actually infinite canvas (replace floats with something, some kind of fixed point scheme? chunks? multilevel scheme?) diff --git a/client/aux.js b/client/aux.js index aab8c57..faa0fa5 100644 --- a/client/aux.js +++ b/client/aux.js @@ -101,4 +101,45 @@ function find_image(state, image_id) { return event; } } -} \ No newline at end of file +} + +// TODO: move these to a file? TypedVector +function tv_create(class_name, capacity) { + return { + 'class_name': class_name, + 'data': new class_name(capacity), + 'capacity': capacity, + 'size': 0, + }; +} + +function tv_data(tv) { + return tv.data.subarray(0, tv.size); +} + +function tv_ensure(tv, capacity) { + if (tv.capacity < capacity) { + const new_tv = tv_create(tv.class_name, capacity); + + new_tv.data.set(tv_data(tv)); + new_tv.size = tv.size; + + return new_tv; + } + + return tv; +} + +function tv_ensure_by(tv, by) { + return tv_ensure(tv, tv.capacity + by); +} + +function tv_add(tv, item) { + tv.data[tv.size++] = item; +} + +function tv_clear(tv) { + tv.size = 0; +} + + diff --git a/client/index.html b/client/index.html index c8c94f7..341d140 100644 --- a/client/index.html +++ b/client/index.html @@ -41,7 +41,6 @@ -
@@ -52,6 +51,8 @@
+ +
diff --git a/client/index.js b/client/index.js index f18ef44..ba26382 100644 --- a/client/index.js +++ b/client/index.js @@ -1,9 +1,3 @@ -// NEXT: pan with m3, place dot, cursor size and color, YELLOW and gray, show user cursor, background styles, -// view desks, undo, eraser, ruler, images (movable), quadtree for clipping, f5 without redownload, progress bar -// -// use returning ID on insert intead of (COLLIDING!) rand_32 -// look into using u64 for point coordinates? fix bad drawings on zoomout - document.addEventListener('DOMContentLoaded', main); const config = { @@ -24,14 +18,16 @@ const config = { initial_offline_timeout: 1000, default_color: 0x00, default_width: 8, - bytes_per_instance: 4 * 4 + 4, // axy, bxy, stroke_id - bytes_per_stroke: 3 + 1, // r, g, b, width + bytes_per_instance: 4 * 2 + 4, // axy, stroke_id + bytes_per_stroke: 2 * 3 + 2, // r, g, b, width initial_static_bytes: 4096 * 16, initial_dynamic_bytes: 4096, - tile_size: 16, - clip_zoom_threshold: 0.00003, stroke_texture_size: 1024, - rdp_cache_threshold: 100, + benchmark: { + zoom: 0.035, + offset: { x: 900, y: 400 }, + frames: 500, + }, }; const EVENT = Object.freeze({ @@ -178,6 +174,18 @@ function main() { 'count': 0, }, + 'segments_from': { + 'data': null, + 'count': 0, + 'cap': 0, + }, + + 'segments': { + 'data': null, + 'count': 0, + 'cap': 0, + }, + 'bvh': { 'nodes': [], 'root': null, @@ -209,7 +217,6 @@ function main() { 'limit_to': false, 'render_from': 0, 'render_to': 0, - 'draw_bvh': false, }, 'rdp_cache': {}, @@ -236,8 +243,9 @@ function main() { 'cap': 0, }, - 'instance_data': serializer_create(config.initial_static_bytes), - + 'instance_data_points': tv_create(Float32Array, 4096), + 'instance_data_ids': tv_create(Uint32Array, 4096), + 'lods': [], 'stroke_data': serializer_create(config.initial_static_bytes), diff --git a/client/math.js b/client/math.js index 1960d2f..8d44689 100644 --- a/client/math.js +++ b/client/math.js @@ -9,18 +9,10 @@ function screen_to_canvas(state, p) { return {'x': xc, 'y': yc}; } - +/* 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) + // 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 cache_key = null; - - if (end - start > config.rdp_cache_threshold) { - cache_key = stroke.index + '-' + zoom + '-' + start + '-' + end; - if (cache_key in state.rdp_cache) { - return state.rdp_cache[cache_key]; - } - } let result = -1; let max_dist = 0; @@ -54,14 +46,10 @@ function rdp_find_max(state, zoom, stroke, start, end) { state.stats.rdp_max_count++; state.stats.rdp_segments += end - start - 1; - - if (end - start > config.rdp_cache_threshold) { - state.rdp_cache[cache_key] = result; - } return result; } - +*/ function process_rdp_indices_r(state, zoom, mask, stroke, start, end) { // Looks like the recursive implementation spends most of its time in the function call overhead // Let's try to use an explicit stack instead to give the js engine more room to play with @@ -127,14 +115,13 @@ function process_stroke(state, zoom, stroke) { return 2; } - // const result0 = process_ewmv(points); - const result1 = process_rdp_indices(state, zoom, stroke, true); + const npoints = process_rdp_indices(state, zoom, stroke, true); - if (result1 === 2 && zoom > stroke.turns_into_straight_line_zoom) { + if (npoints === 2 && zoom > stroke.turns_into_straight_line_zoom) { stroke.turns_into_straight_line_zoom = zoom; } - return result1; + return npoints; } function strokes_intersect_line(state, a, b) { diff --git a/client/webgl_draw.js b/client/webgl_draw.js index ad082e2..31a8937 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -66,19 +66,18 @@ function draw(state, context) { gl.clearDepth(0.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - locations = context.locations['sdf'].main; buffers = context.buffers['sdf']; gl.useProgram(context.programs['sdf'].main); - 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.bufferData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4, gl.STREAM_DRAW); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, tv_data(context.instance_data_points)); + gl.bufferSubData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4, tv_data(context.instance_data_ids)); gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); @@ -88,13 +87,17 @@ function draw(state, context) { gl.uniform1i(locations['u_stroke_data'], 0); gl.uniform1i(locations['u_stroke_texture_size'], config.stroke_texture_size); - gl.enableVertexAttribArray(locations['a_ab']); + gl.enableVertexAttribArray(locations['a_a']); + gl.enableVertexAttribArray(locations['a_b']); gl.enableVertexAttribArray(locations['a_stroke_id']); + + // Points (a, b) and stroke ids are not stored in separate cpu buffers so that points can be resued + gl.vertexAttribPointer(locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0); + gl.vertexAttribPointer(locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4); + gl.vertexAttribIPointer(locations['a_stroke_id'], 1, gl.INT, 4, context.instance_data_points.size * 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_a'], 1); + gl.vertexAttribDivisor(locations['a_b'], 1); gl.vertexAttribDivisor(locations['a_stroke_id'], 1); gl.bindTexture(gl.TEXTURE_2D, context.textures['stroke_data']); @@ -109,69 +112,6 @@ function draw(state, context) { Canvas offset: (${state.canvas.offset.x}, ${state.canvas.offset.y}) Canvas zoom: ${Math.round(state.canvas.zoom * 100000) / 100000}`; - - /* - const after_clip = performance.now(); - - gl.bindBuffer(gl.ARRAY_BUFFER, lod.data_buffer); - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, lod.index_buffer); - - upload_if_needed(gl, gl.ARRAY_BUFFER, lod.vertices); - upload_if_needed(gl, gl.ELEMENT_ARRAY_BUFFER, lod.indices); - - - if (index_count > 0) { - // DEPTH PREPASS - if (state.debug.do_prepass) { - gl.drawBuffers([gl.NONE]); - - locations = context.locations['sdf'].opaque; - - gl.useProgram(context.programs['sdf'].opaque); - - 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); - gl.uniform1i(locations['u_stroke_count'], state.stroke_count); - - gl.enableVertexAttribArray(locations['a_pos']); - gl.enableVertexAttribArray(locations['a_line']); - gl.enableVertexAttribArray(locations['a_stroke_id']); - - gl.vertexAttribPointer(locations['a_pos'], 3, gl.FLOAT, false, config.bytes_per_point, 0); - gl.vertexAttribPointer(locations['a_line'], 4, gl.FLOAT, false, config.bytes_per_point, 4 * 3); - gl.vertexAttribIPointer(locations['a_stroke_id'], 1, gl.INT, config.bytes_per_point, 4 * 3 + 4 * 4 + 4); - - gl.drawElements(gl.TRIANGLES, index_count, gl.UNSIGNED_INT, 0); - } - - // MAIN PASS - gl.drawBuffers([gl.BACK]); - - locations = context.locations['sdf'].main; - - gl.useProgram(context.programs['sdf'].main); - - 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); - gl.uniform1i(locations['u_stroke_count'], state.stroke_count); - gl.uniform1i(locations['u_debug_mode'], state.debug.red); - - gl.enableVertexAttribArray(locations['a_pos']); - gl.enableVertexAttribArray(locations['a_line']); - gl.enableVertexAttribArray(locations['a_color']); - gl.enableVertexAttribArray(locations['a_stroke_id']); - - gl.vertexAttribPointer(locations['a_pos'], 3, gl.FLOAT, false, config.bytes_per_point, 0); - gl.vertexAttribPointer(locations['a_line'], 4, gl.FLOAT, false, config.bytes_per_point, 4 * 3); - gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, config.bytes_per_point, 4 * 3 + 4 * 4); - gl.vertexAttribIPointer(locations['a_stroke_id'], 1, gl.INT, config.bytes_per_point, 4 * 3 + 4 * 4 + 4); - - //index_buffer.reverse(); - gl.drawElements(gl.TRIANGLES, index_count, gl.UNSIGNED_INT, 0); - } -*/ /* // Dynamic data (stroke previews that are currently in progress) const dynamic_points = context.dynamic_serializer.offset / config.bytes_per_point; @@ -234,46 +174,6 @@ function draw(state, context) { gl.drawElements(gl.TRIANGLES, dynamic_indices.length, gl.UNSIGNED_INT, 0); } */ - - if (state.debug.draw_bvh) { - const points = new Float32Array(state.bvh.nodes.length * 6 * 2); - - for (let i = 0; i < state.bvh.nodes.length; ++i) { - const box = state.bvh.nodes[i].bbox; - - points[i * 12 + 0] = box.x1; - points[i * 12 + 1] = box.y1; - points[i * 12 + 2] = box.x2; - points[i * 12 + 3] = box.y1; - points[i * 12 + 4] = box.x1; - points[i * 12 + 5] = box.y2; - - points[i * 12 + 6] = box.x2; - points[i * 12 + 7] = box.y2; - points[i * 12 + 8] = box.x1; - points[i * 12 + 9] = box.y2; - points[i * 12 + 10] = box.x2; - points[i * 12 + 11] = box.y1; - } - - locations = context.locations['debug']; - buffers = context.buffers['debug']; - - gl.useProgram(context.programs['debug']); - gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']); - - 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); - - gl.enableVertexAttribArray(locations['a_pos']); - gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 8, 0); - - gl.clear(gl.DEPTH_BUFFER_BIT); - gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW); - gl.drawArrays(gl.TRIANGLES, 0, points.length / 2); - } - if (context.gpu_timer_ext) { gl.endQuery(context.gpu_timer_ext.TIME_ELAPSED_EXT); @@ -308,4 +208,11 @@ function draw(state, context) { document.querySelector('.debug-timings .cpu').innerHTML = 'Last CPU Frametime: ' + Math.round((cpu_after - cpu_before) * 100) / 100 + 'ms'; + + if (state.debug.benchmark_mode) { + const redraw = state.debug.on_benchmark(); + if (redraw) { + schedule_draw(state, context); + } + } } diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 8965913..20c2dd6 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -36,68 +36,148 @@ 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 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); + if (state.segments_from.cap < context.clipped_indices.count + 1) { + state.segments_from.cap = round_to_pow2(context.clipped_indices.count + 1, 4096); + state.segments_from.data = new Uint32Array(state.segments_from.cap); + } + + if (state.segments.cap < state.coordinates.count / 2) { + state.segments.cap = round_to_pow2(state.coordinates.count, 4096); + state.segments.data = new Uint32Array(state.segments.cap); + } + + state.segments_from.count = 0; + state.segments.count = 0; state.stats.rdp_max_count = 0; state.stats.rdp_segments = 0; - let segment_count = 0; - let fast_path_count = 0; let slow_path_count = 0; + const stack = []; + 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); - - segment_count += lod_indices_count - 1; - - if (lod_indices_count === 2) { - fast_path_count++; - // Fast path - const ax = state.coordinates.data[stroke.coords_from + 0]; - const ay = state.coordinates.data[stroke.coords_from + 1]; - const bx = state.coordinates.data[stroke.coords_to - 2]; - const by = state.coordinates.data[stroke.coords_to - 1]; - - 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); + const point_count = (stroke.coords_to - stroke.coords_from) / 2; + + // 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 { - slow_path_count++; - let base_this = 0; - let base_next = 0; - - 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++; - - 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]; - - 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); - - base_this = base_next; + let segment_count = 2; + + state.segments.data[state.segments.count++] = 0; + + stack.length = 0; + stack.push({'type': 0, 'start': 0, 'end': point_count - 1}); + + while (stack.length > 0) { + const entry = stack.pop(); + + if (entry.type === 1) { + state.segments.data[state.segments.count++] = entry.value; + } else { + const max = rdp_find_max(state, state.canvas.zoom, stroke, entry.start, entry.end); + if (max !== -1) { + segment_count += 1; + stack.push({'type': 0, 'start': max, 'end': entry.end}); + stack.push({'type': 1, 'value': max}); + stack.push({'type': 0, 'start': entry.start, 'end': 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; + } + } + } + + 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); + + 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]; + + for (let j = from; j < to; ++j) { + const base_this = state.segments.data[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]; + + tv_add(context.instance_data_points, ax); + tv_add(context.instance_data_points, ay); + + // Pack 1 into highest bit of stroke_index if we should not draw a segemtn from this + // point to the next one + if (j != to - 1) { + tv_add(context.instance_data_ids, stroke_index); + } else { + tv_add(context.instance_data_ids, stroke_index | (1 << 31)); } } } - console.debug('fast:', fast_path_count, 'slow:', slow_path_count); - console.debug('rdp max:', state.stats.rdp_max_count, 'rdp segments:', state.stats.rdp_segments); + if (config.debug_print) console.debug('instances:', state.segments.count, 'rdp max:', state.stats.rdp_max_count, 'rdp segments:', state.stats.rdp_segments); - return segment_count; + return state.segments.count; } function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) { diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index ce9c254..3cc8cad 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -26,12 +26,6 @@ function debug_panel_init(state, context) { document.getElementById('debug-do-prepass').checked = state.debug.do_prepass; document.getElementById('debug-limit-from').checked = state.debug.limit_from; document.getElementById('debug-limit-to').checked = state.debug.limit_to; - document.getElementById('debug-draw-bvh').checked = state.debug.draw_bvh; - - document.getElementById('debug-draw-bvh').addEventListener('change', (e) => { - state.debug.draw_bvh = e.target.checked; - schedule_draw(state, context); - }); document.getElementById('debug-red').addEventListener('change', (e) => { state.debug.red = e.target.checked; @@ -62,6 +56,40 @@ function debug_panel_init(state, context) { state.debug.render_to = parseInt(e.target.value); schedule_draw(state, context); }); + + document.getElementById('debug-begin-benchmark').addEventListener('click', (e) => { + state.canvas.zoom = config.benchmark.zoom; + state.canvas.offset.x = config.benchmark.offset.x; + state.canvas.offset.y = config.benchmark.offset.y; + + state.debug.benchmark_mode = true; + + const origin_x = state.canvas.offset.x; + const origin_y = state.canvas.offset.y; + const original_button_text = e.target.innerText; + + let frame = 0; + + state.debug.on_benchmark = () => { + if (frame >= config.benchmark.frames) { + state.debug.benchmark_mode = false; + e.target.disabled = false; + e.target.innerText = original_button_text; + return false; + } + + state.canvas.offset.x = origin_x + Math.round(100 * Math.cos(frame / 360)); + state.canvas.offset.y = origin_y + Math.round(100 * Math.sin(frame / 360)); + frame += 1; + + return true; + } + + e.target.disabled = true; + e.target.innerText = 'Benchmark in progress...'; + + schedule_draw(state, context); + }); } function cancel(e) { diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index 2a65f0b..1520e9e 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -102,7 +102,8 @@ const nop_fs_src = `#version 300 es `; const sdf_vs_src = `#version 300 es - in vec4 a_ab; // original points + in vec2 a_a; // point from + in vec2 a_b; // point to in float a_radius; in int a_stroke_id; @@ -130,10 +131,7 @@ const sdf_vs_src = `#version 300 es uvec4 stroke_data = texelFetch(u_stroke_data, ivec2(stroke_data_x, stroke_data_y), 0); float radius = float(stroke_data.w); - vec2 a = a_ab.xy; - vec2 b = a_ab.zw; - - vec2 line_dir = normalize(b - a); + vec2 line_dir = normalize(a_b - a_a); vec2 up_dir = vec2(line_dir.y, -line_dir.x); vec2 pixel = vec2(2.0) / u_res * apron; float rscale = apron / u_scale.x; @@ -145,19 +143,19 @@ const sdf_vs_src = `#version 300 es if (vertex_index == 0) { // "top left" aka "p1" - origin = a; + origin = a_a; outwards = up_dir - line_dir; } else if (vertex_index == 1 || vertex_index == 5) { // "top right" aka "p2" - origin = b; + origin = a_b; outwards = up_dir + line_dir; } else if (vertex_index == 2 || vertex_index == 4) { // "bottom left" aka "p3" - origin = a; + origin = a_a; outwards = -up_dir - line_dir; } else { // "bottom right" aka "p4" - origin = b; + origin = a_b; outwards = -up_dir + line_dir; } @@ -167,10 +165,14 @@ const sdf_vs_src = `#version 300 es screen02.y = 2.0 - screen02.y; - v_line = vec4(a, b); + v_line = vec4(a_a, a_b); v_thickness = radius; v_color = vec3(stroke_data.xyz) / 255.0; + if (a_stroke_id >> 31 != 0) { + screen02 += vec2(100.0); // shift offscreen + } + gl_Position = vec4(screen02 - 1.0, (float(a_stroke_id) / float(u_stroke_count)) * 2.0 - 1.0, 1); } `; @@ -307,7 +309,8 @@ function init_webgl(state, context) { }, 'main': { - 'a_ab': gl.getAttribLocation(context.programs['sdf'].main, 'a_ab'), + 'a_a': gl.getAttribLocation(context.programs['sdf'].main, 'a_a'), + 'a_b': gl.getAttribLocation(context.programs['sdf'].main, 'a_b'), 'a_stroke_id': gl.getAttribLocation(context.programs['sdf'].main, 'a_stroke_id'), 'u_res': gl.getUniformLocation(context.programs['sdf'].main, 'u_res'),