function schedule_draw(state, context) {
    if (!state.timers.raf) {
        window.requestAnimationFrame(() => {
            draw(state, context)
        });
        state.timers.raf = true;
    }
}
function upload_if_needed(gl, buffer_kind, serializer) {
    if (serializer.need_gpu_allocate) {
        if (config.debug_print) console.debug('gpu allocate');
        gl.bufferData(buffer_kind, serializer.size, gl.DYNAMIC_DRAW);
        serializer.need_gpu_allocate = false;
        serializer.gpu_upload_from = 0;
    }
    if (serializer.gpu_upload_from < serializer.offset) {
        if (config.debug_print) console.debug('gpu upload');
        const upload_offset = serializer.gpu_upload_from;
        const upload_size = serializer.offset - upload_offset;
        gl.bufferSubData(buffer_kind, upload_offset, new Uint8Array(serializer.buffer, upload_offset, upload_size));
        serializer.gpu_upload_from = serializer.offset;
    }
}
function upload_square_rgba16ui_texture(gl, serializer, texture_size) {
    const bpp = 2 * 4;
    const data_size = serializer.offset;
    const data_pixels = data_size / bpp; // data_size % bpp is expected to always be zero here
    const rows = Math.ceil(data_pixels / texture_size);
    const last_row = data_pixels % texture_size;
    const whole_upload = (rows - 1) * texture_size * bpp;
    // Upload whole rows
    if (rows > 1) {
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, texture_size, rows - 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, 0, whole_upload / 2));
    }
    // Upload last row
    if (last_row > 0) {        
        const last_row_upload = last_row * bpp;
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, rows - 1, last_row, 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, whole_upload, last_row_upload / 2));
    }
}
function draw(state, context) {
    const cpu_before = performance.now();
    state.timers.raf = false;
    const gl = context.gl;
    const width = window.innerWidth;
    const height = window.innerHeight;
    
    let query = null;
    if (context.gpu_timer_ext !== null) {
        query = gl.createQuery();
        gl.beginQuery(context.gpu_timer_ext.TIME_ELAPSED_EXT, query);
    }
       
    gl.viewport(0, 0, context.canvas.width, context.canvas.height);
    gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1);
    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);
    const dynamic_segment_count = context.dynamic_segment_count;
    const dynamic_stroke_count = context.dynamic_stroke_count;
    // "Static" data upload
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance']);
    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.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.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.events.length);
    gl.uniform1i(locations['u_debug_mode'], state.debug.red);
    gl.uniform1i(locations['u_stroke_data'], 0);
    gl.uniform1i(locations['u_stroke_texture_size'], config.stroke_texture_size);
    gl.enableVertexAttribArray(locations['a_a']);
    gl.enableVertexAttribArray(locations['a_b']);
    gl.enableVertexAttribArray(locations['a_stroke_id']);
    
    // Points (a, b) and stroke ids are stored in separate cpu buffers so that points can be reused (look at stride and offset values)
    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.vertexAttribDivisor(locations['a_a'], 1);
    gl.vertexAttribDivisor(locations['a_b'], 1);
    gl.vertexAttribDivisor(locations['a_stroke_id'], 1);
    
    // Static draw (everything already bound)
    gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, segment_count); 
    
    // Dynamic strokes should be drawn above static strokes
    gl.clear(gl.DEPTH_BUFFER_BIT);
    // Dynamic draw (strokes currently being drawn)
    gl.uniform1i(locations['u_stroke_count'], dynamic_stroke_count);
    gl.uniform1i(locations['u_stroke_data'], 0);
    gl.uniform1i(locations['u_stroke_texture_size'], config.dynamic_stroke_texture_size);
    
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_dynamic_instance']);
    // Dynamic data upload
    gl.bufferData(gl.ARRAY_BUFFER, context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4, gl.STREAM_DRAW);
    gl.bufferSubData(gl.ARRAY_BUFFER, 0, tv_data(context.dynamic_instance_points)); 
    gl.bufferSubData(gl.ARRAY_BUFFER, context.dynamic_instance_points.size * 4, tv_data(context.dynamic_instance_ids));
    gl.bindTexture(gl.TEXTURE_2D, context.textures['dynamic_stroke_data']);
    upload_square_rgba16ui_texture(gl, context.dynamic_stroke_data, config.dynamic_stroke_texture_size);
    gl.enableVertexAttribArray(locations['a_a']);
    gl.enableVertexAttribArray(locations['a_b']);
    gl.enableVertexAttribArray(locations['a_stroke_id']);
    
    // Points (a, b) and stroke ids are stored in separate cpu buffers so that points can be reused (look at stride and offset values)
    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.dynamic_instance_points.size * 4);
    gl.vertexAttribDivisor(locations['a_a'], 1);
    gl.vertexAttribDivisor(locations['a_b'], 1);
    gl.vertexAttribDivisor(locations['a_stroke_id'], 1);
    gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, dynamic_segment_count);
    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}`;
    if (context.gpu_timer_ext) {
        gl.endQuery(context.gpu_timer_ext.TIME_ELAPSED_EXT);
        const next_tick = () => {
            if (query) {
                // At some point in the future, after returning control to the browser
                const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);
                const disjoint = gl.getParameter(context.gpu_timer_ext.GPU_DISJOINT_EXT);
                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);
                    document.querySelector('.debug-timings .gpu').innerHTML = 'Last GPU Frametime: ' + Math.round(timeElapsed / 10000) / 100 + 'ms';
                }
                if (available || disjoint) {
                    // Clean up the query object.
                    gl.deleteQuery(query);
                    // Don't re-enter this polling loop.
                    query = null;
                } else if (!available) {
                    setTimeout(next_tick, 0);
                }
            }
        }
        setTimeout(next_tick, 0);
    }
    
    const cpu_after = performance.now();
    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);
        }
    }
}