diff --git a/client/client_recv.js b/client/client_recv.js index 7f335a2..74d4f38 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -179,6 +179,9 @@ function handle_event(state, context, event) { state.stroke_count++; + document.getElementById('debug-render-from').max = state.stroke_count; + document.getElementById('debug-render-to').max = state.stroke_count; + break; } diff --git a/client/default.css b/client/default.css index ebe2eaf..a6927fc 100644 --- a/client/default.css +++ b/client/default.css @@ -27,6 +27,11 @@ body.offline .main { display: none !important; } +.flexcol { + display: flex; + flex-direction: column; +} + canvas { width: 100%; height: 100%; @@ -171,17 +176,17 @@ canvas.movemode.moving { filter: invert(100%); } -input[type=range] { +.sizer input[type=range] { -webkit-appearance: none; width: 200px; background: transparent; } -input[type=range]:focus { +.sizer input[type=range]:focus { outline: none; } -input[type=range]::-webkit-slider-thumb { +.sizer input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; border: none; background: white; @@ -193,7 +198,7 @@ input[type=range]::-webkit-slider-thumb { margin-top: -6px; /* You need to specify a margin in Chrome, but in Firefox and IE it is automatic */ } -input[type=range]::-moz-range-thumb { +.sizer input[type=range]::-moz-range-thumb { border: none; background: white; height: 16px; @@ -203,7 +208,7 @@ input[type=range]::-moz-range-thumb { border: 2px solid var(--dark-blue); } -input[type=range]::-webkit-slider-runnable-track { +.sizer input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 8px; cursor: pointer; @@ -212,7 +217,7 @@ input[type=range]::-webkit-slider-runnable-track { border: none; } -input[type=range]:focus::-webkit-slider-runnable-track { +.sizer input[type=range]:focus::-webkit-slider-runnable-track { width: 100%; height: 8px; cursor: pointer; @@ -221,7 +226,7 @@ input[type=range]:focus::-webkit-slider-runnable-track { border: none; } -input[type=range]::-moz-range-track { +.sizer input[type=range]::-moz-range-track { width: 100%; height: 8px; cursor: pointer; @@ -311,4 +316,18 @@ body.offline * { .loader.hidden { opacity: 0; -} \ No newline at end of file +} + +.debug-window { + position: absolute; + min-width: 256px; + top: 20px; + right: 20px; + display: flex; + flex-direction: column; + gap: 10px; + user-select: none; + padding: 5px; + background: white; + border: 1px solid var(--dark-blue); +} diff --git a/client/index.html b/client/index.html index ff4c8d5..f947474 100644 --- a/client/index.html +++ b/client/index.html @@ -7,20 +7,20 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + +
@@ -30,6 +30,23 @@ --> +
+
+
+ + + + +
+ + +
+ +
+ + +
+
diff --git a/client/index.js b/client/index.js index ae9c5ae..6282f7d 100644 --- a/client/index.js +++ b/client/index.js @@ -3,9 +3,12 @@ document.addEventListener('DOMContentLoaded', main); const config = { - ws_url: 'wss://desk.some.website/ws/', - ping_url: 'https://desk.some.website/api/ping', - image_url: 'https://desk.some.website/images/', +// ws_url: 'wss://desk.some.website/ws/', +// ping_url: 'https://desk.some.website/api/ping', +// image_url: 'https://desk.some.website/images/', + ws_url: 'wss://192.168.100.2/ws/', + ping_url: 'https://192.168.100.2/api/ping', + image_url: 'https://192.168.100.2/images/', sync_timeout: 1000, ws_reconnect_timeout: 2000, brush_preview_timeout: 1000, @@ -22,6 +25,7 @@ const config = { initial_dynamic_bytes: 4096, frametime_window_size: 100, tile_size: 16, + clip_zoom_threshold: 0.3, }; const EVENT = Object.freeze({ @@ -176,13 +180,20 @@ function main() { 'players': {}, 'onscreen_segments': null, + + 'debug': { + 'red': false, + 'do_prepass': true, + 'limit_from': false, + 'limit_to': false, + 'render_from': 0, + 'render_to': 0, + } }; const context = { 'canvas': null, 'gl': null, - 'debug_mode': false, - 'do_prepass': true, 'frametime_window': [], 'frametime_window_head': 0, @@ -190,6 +201,9 @@ function main() { 'need_static_allocate': true, 'need_static_upload': true, 'need_dynamic_upload': false, + 'need_index_upload': true, + + 'full_index_count': 0, 'programs': {}, 'buffers': {}, diff --git a/client/math.js b/client/math.js index ff41fcd..ef82c92 100644 --- a/client/math.js +++ b/client/math.js @@ -215,7 +215,7 @@ function quad_fully_onscreen(screen, bbox) { return false; } -function segments_onscreen(state, context) { +function segments_onscreen(state, context, do_clip) { // TODO: handle stroke width if (state.onscreen_segments === null) { @@ -251,30 +251,36 @@ function segments_onscreen(state, context) { let head = 0; for (let i = 0; i < state.events.length; ++i) { + if (state.debug.limit_to && i >= state.debug.render_to) break; + const event = state.events[i]; - if (event.type === EVENT.STROKE && !event.deleted) { - if (quad_onscreen(screen, event.bbox)) { - const fully_onscreen = quad_fully_onscreen(screen, event.bbox); - for (let j = 0; j < event.points.length - 1; ++j) { - const a = event.points[j + 0]; - const b = event.points[j + 1]; - - if (fully_onscreen || segment_interesects_quad(a, b, screen_topleft, screen_bottomright, screen_topright, screen_bottomleft)) { - let base = head + j * 4; - // We draw quads as [1, 2, 3, 4, 3, 2] - state.onscreen_segments[at + 0] = base + 0; - state.onscreen_segments[at + 1] = base + 1; - state.onscreen_segments[at + 2] = base + 2; - state.onscreen_segments[at + 3] = base + 3; - state.onscreen_segments[at + 4] = base + 2; - state.onscreen_segments[at + 5] = base + 1; - - at += 6; + + if (!(state.debug.limit_from && i < state.debug.render_from)) { + if (event.type === EVENT.STROKE && !event.deleted) { + if (!do_clip || quad_onscreen(screen, event.bbox)) { + const fully_onscreen = !do_clip || quad_fully_onscreen(screen, event.bbox); + for (let j = 0; j < event.points.length - 1; ++j) { + const a = event.points[j + 0]; + const b = event.points[j + 1]; + + if (fully_onscreen || segment_interesects_quad(a, b, screen_topleft, screen_bottomright, screen_topright, screen_bottomleft)) { + let base = head + j * 4; + // We draw quads as [1, 2, 3, 4, 3, 2] + state.onscreen_segments[at + 0] = base + 0; + state.onscreen_segments[at + 1] = base + 1; + state.onscreen_segments[at + 2] = base + 2; + state.onscreen_segments[at + 3] = base + 3; + state.onscreen_segments[at + 4] = base + 2; + state.onscreen_segments[at + 5] = base + 1; + + at += 6; + } } } } - head += (event.points.length - 1) * 4; } + + head += (event.points.length - 1) * 4; } return at; diff --git a/client/webgl_draw.js b/client/webgl_draw.js index defeadd..b97d2a8 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -1,10 +1,6 @@ function schedule_draw(state, context) { if (!state.timers.raf) { window.requestAnimationFrame(() => { - context._DRAW_TO_TEXTURE = true; - draw(state, context); - - context._DRAW_TO_TEXTURE = false; draw(state, context) }); state.timers.raf = true; @@ -58,11 +54,29 @@ function draw(state, context) { gl.clearDepth(0.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - const before_clip = performance.now(); - const index_count = segments_onscreen(state, context); - const after_clip = performance.now(); + let index_count; + const do_clip = (state.canvas.zoom > config.clip_zoom_threshold); + + if (do_clip) { + context.need_index_upload = true; + } + + if (do_clip || context.need_index_upload) { + const before_clip = performance.now(); + index_count = segments_onscreen(state, context, do_clip); + const after_clip = performance.now(); + } + + if (!do_clip && !context.need_index_upload) { + index_count = context.full_index_count; + } //console.debug('clip', after_clip - before_clip); - + + document.getElementById('debug-stats').innerHTML = ` + Segments onscreen: ${index_count} + Canvas offset: (${state.canvas.offset.x}, ${state.canvas.offset.y}) + Canvas zoom: ${Math.round(state.canvas.zoom * 100) / 100}`; + if (index_count > 0) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers['b_packed_static_index']); @@ -70,12 +84,18 @@ function draw(state, context) { const static_points = context.static_serializer.offset / config.bytes_per_point; //const dynamic_points = context.dynamic_serializer.offset / config.bytes_per_point; + if (!do_clip) { + // Almost everything on screen anyways. Only upload indices once + if (context.need_index_upload) { + context.full_index_count = index_count; + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index_buffer, gl.STATIC_DRAW); + context.need_index_upload = false; + } + } + if (static_points > 0) { // DEPTH PREPASS - - index_buffer.reverse(); - - if (context.do_prepass) { + if (state.debug.do_prepass && do_clip) { gl.drawBuffers([gl.NONE]); locations = context.locations['sdf'].opaque; @@ -95,11 +115,14 @@ function draw(state, context) { 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.bufferData(gl.ELEMENT_ARRAY_BUFFER, index_buffer, gl.DYNAMIC_DRAW); + if (do_clip) { + index_buffer.reverse(); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index_buffer, gl.DYNAMIC_DRAW); + } + gl.drawElements(gl.TRIANGLES, index_count, gl.UNSIGNED_INT, 0); } - // MAIN PASS gl.drawBuffers([gl.BACK]); @@ -111,6 +134,7 @@ function draw(state, context) { 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']); @@ -122,8 +146,13 @@ function draw(state, context) { 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.bufferData(gl.ELEMENT_ARRAY_BUFFER, index_buffer, gl.DYNAMIC_DRAW); + if (do_clip) { + if (state.debug.do_prepass) { + index_buffer.reverse(); + } + + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index_buffer, gl.DYNAMIC_DRAW); + } gl.drawElements(gl.TRIANGLES, index_count, gl.UNSIGNED_INT, 0); } @@ -162,7 +191,8 @@ function draw(state, context) { if (available && !disjoint) { // See how much time the rendering of the object took in nanoseconds. const timeElapsed = gl.getQueryParameter(query, gl.QUERY_RESULT); - console.debug(timeElapsed / 1000000); + //console.debug(timeElapsed / 1000000); + document.getElementById('debug-timings').innerHTML = 'Frametime: ' + Math.round(timeElapsed / 10000) / 100 + 'ms'; } if (available || disjoint) { diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index ad0d5c4..00b0822 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -17,6 +17,45 @@ function init_listeners(state, context) { context.canvas.addEventListener('drop', (e) => on_drop(e, state, context)); context.canvas.addEventListener('dragover', (e) => mousemove(e, state, context)); + + debug_panel_init(state, context); +} + +function debug_panel_init(state, context) { + document.getElementById('debug-red').checked = state.debug.red; + 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-red').addEventListener('click', (e) => { + state.debug.red = e.target.checked; + schedule_draw(state, context); + }); + + document.getElementById('debug-do-prepass').addEventListener('click', (e) => { + state.debug.do_prepass = e.target.checked; + schedule_draw(state, context); + }); + + document.getElementById('debug-limit-from').addEventListener('click', (e) => { + state.debug.limit_from = e.target.checked; + schedule_draw(state, context); + }); + + document.getElementById('debug-limit-to').addEventListener('click', (e) => { + state.debug.limit_to = e.target.checked; + schedule_draw(state, context); + }); + + document.getElementById('debug-render-from').addEventListener('input', (e) => { + state.debug.render_from = parseInt(e.target.value); + schedule_draw(state, context); + }); + + document.getElementById('debug-render-to').addEventListener('input', (e) => { + state.debug.render_to = parseInt(e.target.value); + schedule_draw(state, context); + }); } function cancel(e) { @@ -69,11 +108,7 @@ function keydown(e, state, context) { } } } else if (e.code === 'KeyD') { - context.debug_mode = !context.debug_mode; - schedule_draw(state, context); - } else if (e.code === 'KeyP') { - context.do_prepass = !context.do_prepass; - schedule_draw(state, context); + document.querySelector('.debug-window').classList.toggle('dhide'); } } diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index 3753292..bac05b2 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -122,20 +122,20 @@ const sdf_fs_src = `#version 300 es out vec4 FragColor; void main() { - vec2 a = v_line.xy; - vec2 b = v_line.zw; + if (u_debug_mode == 0) { + vec2 a = v_line.xy; + vec2 b = v_line.zw; - vec2 pa = v_texcoord - a.xy, ba = b.xy - a.xy; - float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); - float dist = length(pa - ba * h) - v_thickness / 2.0; + vec2 pa = v_texcoord - a.xy, ba = b.xy - a.xy; + float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); + float dist = length(pa - ba * h) - v_thickness / 2.0; - float fade = 0.5 * length(fwidth(v_texcoord)); - float alpha = 1.0 - smoothstep(0.0, fade, dist); + float fade = 0.5 * length(fwidth(v_texcoord)); + float alpha = 1.0 - smoothstep(-fade, fade, dist); - if (u_debug_mode == 1) { - FragColor = vec4(1.0, 0.0, 0.0, 0.1); - } else { FragColor = vec4(v_color * alpha, alpha); + } else { + FragColor = vec4(1.0, 0.0, 0.0, 1.0 / 32.0); } } `; @@ -191,6 +191,7 @@ function init_webgl(state, context) { gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + //gl.blendFunc(gl.SRC_ALPHA, gl.DST_ALPHA); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.GEQUAL);