vtgl

terminal emulator implemented in OpenGL
git clone anongit@rnpnr.xyz:vtgl.git
Log | Files | Refs | Feed | LICENSE

vtgl.c (34737B)


      1 /* See LICENSE for copyright details */
      2 
      3 /* TODO(rnp):
      4  * [ ]: refactor: instead of having a callback for key input it is probably better for
      5  *      the platform to pass an in order list of key events last frame
      6  * [ ]: refactor: remove os_... includes from vtgl.h
      7  * [ ]: refactor: stream_ensure_terminator
      8  * [ ]: refactor: push_s8 (into arena); rename push_s8 to draw_s8
      9  * [ ]: refactor: resize renderer work item
     10  * [ ]: refactor: remove queued_render (just use a work item)
     11  * [ ]: refactor: why is there interaction code in both threads?
     12  */
     13 
     14 /* TODO: define this ourselves since we should be loading it at runtime */
     15 /* TODO(rnp): this is only valid on platforms where we can directly link libGL */
     16 #define GL_GLEXT_PROTOTYPES
     17 #include <GL/glcorearb.h>
     18 
     19 #include "vtgl.h"
     20 
     21 #include "config.h"
     22 
     23 #include "font.c"
     24 #include "terminal.c"
     25 
     26 #define REVERSE_VIDEO_MASK (Colour){.r = 0xff, .g = 0xff, .b = 0xff}.rgba
     27 
     28 #define VERTEX_SHADER_TEXT                                       \
     29 "#version 430 core\n"                                            \
     30 "\n"                                                             \
     31 "layout(location = 0) in  vec2 vertex_position;\n"               \
     32 "layout(location = 1) in  vec2 vertex_texture_coordinate;\n"     \
     33 "layout(location = 2) in  vec4 vertex_colour;\n"                 \
     34 "\n"                                                             \
     35 "layout(location = 0) out vec2 fragment_texture_coordinate;\n"   \
     36 "layout(location = 1) out vec4 fragment_colour;\n"               \
     37 "\n"                                                             \
     38 "layout(location = 0) uniform mat4 u_Pmat;\n"                    \
     39 "\n"                                                             \
     40 "void main()\n"                                                  \
     41 "{\n"                                                            \
     42 "    fragment_texture_coordinate = vertex_texture_coordinate;\n" \
     43 "    fragment_colour             = vertex_colour;\n"             \
     44 "\n"                                                             \
     45 "    gl_Position = u_Pmat * vec4(vertex_position, 0.0, 1.0);\n"  \
     46 "}\n"
     47 
     48 function void
     49 set_projection_matrix(GLCtx *gl, u32 stage)
     50 {
     51 	f32 w = gl->window_size.w;
     52 	f32 h = gl->window_size.h;
     53 
     54 	f32 pmat[4 * 4] = {
     55 		2.0/w,   0.0,      0.0,    -1.0,
     56 		0.0,     2.0/h,    0.0,    -1.0,
     57 		0.0,     0.0,     -1.0,     0.0,
     58 		0.0,     0.0,      0.0,     1.0,
     59 	};
     60 
     61 	glProgramUniformMatrix4fv(gl->programs[stage], SHADER_PMAT_LOC, 1, GL_TRUE, pmat);
     62 }
     63 
     64 function u32
     65 compile_shader(Arena a, u32 type, s8 shader, s8 name)
     66 {
     67 	u32 result = glCreateShader(type);
     68 	glShaderSource(result, 1, (const char **)&shader.data, (int *)&shader.len);
     69 	glCompileShader(result);
     70 
     71 	i32 res = 0;
     72 	glGetShaderiv(result, GL_COMPILE_STATUS, &res);
     73 	if (res != GL_TRUE) {
     74 		Stream s = arena_stream(a);
     75 		stream_push_s8(&s, name);
     76 		stream_push_s8(&s, s8(": failed to compile\n"));
     77 
     78 		i32 len = 0, out_len = 0;
     79 		glGetShaderiv(result, GL_INFO_LOG_LENGTH, &len);
     80 		glGetShaderInfoLog(result, len, &out_len, (c8 *)(s.data + s.count));
     81 		stream_commit(&s, out_len);
     82 		os_write_err_msg(stream_to_s8(&s));
     83 		glDeleteShader(result);
     84 		result = 0;
     85 	}
     86 
     87 	return result;
     88 }
     89 
     90 function u32
     91 link_shader_program(Arena a, u32 *shader_ids, u32 shader_id_count)
     92 {
     93 	u32 result = glCreateProgram();
     94 	for (u32 i = 0; i < shader_id_count; i++)
     95 		glAttachShader(result, shader_ids[i]);
     96 	glLinkProgram(result);
     97 
     98 	i32 success = 0;
     99 	glGetProgramiv(result, GL_LINK_STATUS, &success);
    100 	if (success == GL_FALSE) {
    101 		Stream s = arena_stream(a);
    102 		stream_push_s8(&s, s8("shader link error: "));
    103 
    104 		i32 len = 0;
    105 		glGetProgramInfoLog(result, s.capacity - s.count, &len, (c8 *)(s.data + s.count));
    106 		stream_commit(&s, len);
    107 		os_write_err_msg(stream_to_s8(&s));
    108 		glDeleteProgram(result);
    109 		result = 0;
    110 	}
    111 
    112 	return result;
    113 }
    114 
    115 function u32
    116 load_shader(Arena arena, s8 vertex_text, s8 fragment_text, s8 info, s8 label)
    117 {
    118 	u32 result = 0;
    119 	u32 vs_id  = compile_shader(arena, GL_VERTEX_SHADER,   vertex_text,   s8("Vertex Shader"));
    120 	u32 fs_id  = compile_shader(arena, GL_FRAGMENT_SHADER, fragment_text, info);
    121 
    122 	if (vs_id && fs_id) result = link_shader_program(arena, (u32 []){vs_id, fs_id}, 2);
    123 	glDeleteShader(vs_id);
    124 	glDeleteShader(fs_id);
    125 
    126 	if (result) {
    127 		Stream s = arena_stream(arena);
    128 		stream_push_s8(&s, s8("loaded: "));
    129 		stream_push_s8(&s, info);
    130 		stream_push_byte(&s, '\n');
    131 		os_write_err_msg(stream_to_s8(&s));
    132 		LABEL_GL_OBJECT(GL_PROGRAM, result, label);
    133 	}
    134 
    135 	return result;
    136 }
    137 
    138 function void
    139 update_uniforms(GLCtx *gl, ShaderID stage)
    140 {
    141 	switch (stage) {
    142 	case SID_POST: {
    143 		#define X(name) gl->post.name = glGetUniformLocation(gl->programs[stage], "u_" #name);
    144 		GL_POST_UNIFORMS
    145 		#undef X
    146 	} break;
    147 	default: break;
    148 	}
    149 }
    150 
    151 function void
    152 reload_shader(OS *os, GLCtx *gl, ShaderReloadContext *src, Arena a)
    153 {
    154 	s8 fs_text = os->read_file(src->path.data, &a);
    155 	if (fs_text.len) {
    156 		u32 new_program = load_shader(a, s8(VERTEX_SHADER_TEXT), fs_text, src->path, src->label);
    157 		if (new_program) {
    158 			glDeleteProgram(gl->programs[src->shader]);
    159 			gl->programs[src->shader] = new_program;
    160 			update_uniforms(gl, src->shader);
    161 			set_projection_matrix(gl, src->shader);
    162 		}
    163 	}
    164 }
    165 
    166 function v4
    167 normalize_colour(Colour c)
    168 {
    169 	return (v4){.r = c.r / 255.0f, .g = c.g / 255.0f, .b = c.b / 255.0f, .a = c.a / 255.0f};
    170 }
    171 
    172 function void
    173 clear_colour(void)
    174 {
    175 	Colour c = g_colours.data[g_colours.bgidx];
    176 	v4 cc = normalize_colour(c);
    177 	glClearColor(cc.r, cc.g, cc.b, cc.a);
    178 	glClear(GL_COLOR_BUFFER_BIT);
    179 }
    180 
    181 function v2
    182 get_occupied_size(Term *t)
    183 {
    184 	v2 cs     = fa_cell_size(&t->render_thread.fa);
    185 	v2 result = {.x = t->size.w * cs.w, .y = t->size.h * cs.h};
    186 	return result;
    187 }
    188 
    189 function v2
    190 get_terminal_top_left(Term *t)
    191 {
    192 	iv2 ws    = t->render_thread.gl.window_size;
    193 	v2 os     = get_occupied_size(t);
    194 	v2 delta  = {.x = ws.w - os.w, .y = ws.h - os.h};
    195 	v2 result = {.x = delta.x / 2, .y = ws.h - delta.y / 2};
    196 	return result;
    197 }
    198 
    199 function void
    200 resize_terminal(Term *t, OS *os, iv2 window_size)
    201 {
    202 	v2 ws  = v2_from_iv2(window_size);
    203 	ws.w  -= 2 * g_term_margin.w;
    204 	ws.h  -= 2 * g_term_margin.h;
    205 
    206 	iv2 old_size = t->size;
    207 	v2 cs        = fa_cell_size(&t->render_thread.fa);
    208 	t->size.w    = (i32)(ws.w / cs.w);
    209 	t->size.h    = (i32)(ws.h / cs.h);
    210 
    211 	if (t->size.w > ARRAY_COUNT(t->tabs) * 32) {
    212 		t->size.w = ARRAY_COUNT(t->tabs) * 32u;
    213 		stream_push_s8(&t->error_stream, s8("resize: max terminal width is "));
    214 		stream_push_u64(&t->error_stream, t->size.w);
    215 		stream_push_s8(&t->error_stream, s8("; clamping\n"));
    216 		os_write_err_msg(stream_to_s8(&t->error_stream));
    217 		stream_reset(&t->error_stream, 0);
    218 	}
    219 
    220 	if (!equal_iv2(old_size, t->size)) {
    221 		t->size = initialize_framebuffer(&t->views[0].fb, t->size);
    222 		initialize_framebuffer(&t->views[1].fb, t->size);
    223 		t->state |= TS_NEEDS_REFILL;
    224 	}
    225 
    226 	os->set_terminal_size(t->child, t->size.h, t->size.w, ws.w, ws.h);
    227 
    228 	/* TODO(rnp): queue renderer resize */
    229 	t->render_thread.gl.flags |= RESIZE_RENDERER;
    230 	t->state    &= ~TS_NEEDS_RESIZE;
    231 }
    232 
    233 function void
    234 resize(Term *t, OS *os, iv2 window_size)
    235 {
    236 	GLCtx *gl = &t->render_thread.gl;
    237 	gl->window_size = window_size;
    238 
    239 	glViewport(0, 0, window_size.w, window_size.h);
    240 
    241 	glActiveTexture(GL_TEXTURE0 + gl->fb_tex_unit);
    242 	glBindTexture(GL_TEXTURE_2D, gl->fb_tex);
    243 	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, window_size.w, window_size.h, 0,
    244 	             GL_RGBA, GL_UNSIGNED_BYTE, 0);
    245 
    246 	/* NOTE: reactive the glyph texture unit */
    247 	glActiveTexture(GL_TEXTURE0);
    248 
    249 	u32 buffer_size = t->size.w * t->size.h * sizeof(RenderCell);
    250 	glDeleteBuffers(1, &gl->render_shader_ssbo);
    251 	glGenBuffers(1, &gl->render_shader_ssbo);
    252 	glBindBuffer(GL_SHADER_STORAGE_BUFFER, gl->render_shader_ssbo);
    253 	glBufferData(GL_SHADER_STORAGE_BUFFER, buffer_size, 0, GL_DYNAMIC_DRAW);
    254 	glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, gl->render_shader_ssbo);
    255 	LABEL_GL_OBJECT(GL_BUFFER, gl->render_shader_ssbo, s8("RenderCells"));
    256 	gl->queued_render = 1;
    257 
    258 
    259 	FontAtlas *fa        = &t->render_thread.fa;
    260 	v2 cs                = fa_cell_size(fa);
    261 	ShaderParameters *sp = &gl->shader_parameters;
    262 	sp->cell_size        = (iv2){.w = cs.w, .h = cs.h};
    263 	sp->strike_min       = (fa->info.baseline + 0.40 * fa->info.h);
    264 	sp->strike_max       = (fa->info.baseline + 0.48 * fa->info.h);
    265 	sp->underline_min    = (0.89 * fa->info.h);
    266 	sp->underline_max    = (0.96 * fa->info.h);
    267 	sp->top_left_margin  = g_term_margin;
    268 	sp->margin_colour    = g_colours.data[g_colours.bgidx].rgba;
    269 	//sp->margin_colour    = 0x7f003f00;
    270 
    271 	sp->term_size_in_pixels = gl->window_size;
    272 	sp->term_size_in_cells  = t->size;
    273 
    274 	for (u32 i = 0; i < SID_LAST; i++)
    275 		set_projection_matrix(gl, i);
    276 
    277 	gl->flags &= ~RESIZE_RENDERER;
    278 }
    279 
    280 function RenderCtx
    281 make_render_ctx(Arena *a, GLCtx *gl, FontAtlas *fa)
    282 {
    283 	RenderCtx result;
    284 	result.gl  = gl;
    285 	result.fa  = fa;
    286 	result.rpb = alloc(a, RenderPushBuffer, 1);
    287 	result.a   = sub_arena(a, MB(4));
    288 	return result;
    289 }
    290 
    291 function iv2
    292 get_gpu_texture_position(v2 cs, u32 gpu_tile_index)
    293 {
    294 	uv2 gpu_position = {.x = gpu_tile_index & 0xFFFF, .y = gpu_tile_index >> 16};
    295 	iv2 result = {.y = gpu_position.y * cs.y, .x = gpu_position.x * cs.x};
    296 	return result;
    297 }
    298 
    299 function u32
    300 get_gpu_glyph_index(Arena a, GLCtx *gl, FontAtlas *fa, u32 codepoint, u32 font_id,
    301                     enum face_style style, CachedGlyph **out)
    302 {
    303 	u32 *data = render_glyph(&a, fa, codepoint, font_id, style, out);
    304 	if (data) {
    305 		CachedGlyph *cg = *out;
    306 		cg->uploaded_to_gpu = 1;
    307 
    308 		v2  cell_size      = fa_cell_size(fa);
    309 		iv2 glyph_position = get_gpu_texture_position(cell_size, cg->gpu_tile_index);
    310 		ASSERT(glyph_position.x + cell_size.w * cg->tile_count < gl->glyph_bitmap_dim.x);
    311 		ASSERT(glyph_position.y + cell_size.h < gl->glyph_bitmap_dim.y);
    312 
    313 		glTexSubImage2D(GL_TEXTURE_2D, 0, glyph_position.x, glyph_position.y,
    314 		                cell_size.w * cg->tile_count, cell_size.h,
    315 		                GL_RGBA, GL_UNSIGNED_BYTE, data);
    316 	}
    317 	ASSERT((*out)->uploaded_to_gpu);
    318 	return (*out)->gpu_tile_index;
    319 }
    320 
    321 /* NOTE: this function assumes we are drawing quads */
    322 function void
    323 flush_render_push_buffer(RenderCtx *rc)
    324 {
    325 	BEGIN_TIMED_BLOCK();
    326 	if (rc->rpb->count > 0) {
    327 		u32 n     = rc->rpb->count;
    328 		GLCtx *gl = rc->gl;
    329 		RenderPushBuffer *rpb = rc->rpb;
    330 		ASSERT((n % 4) == 0);
    331 
    332 		BEGIN_NAMED_BLOCK(upload);
    333 		glBindBuffer(GL_ARRAY_BUFFER, gl->vbos[0]);
    334 		glBufferSubData(GL_ARRAY_BUFFER, 0, n * sizeof(*rpb->positions), rpb->positions);
    335 		glBindBuffer(GL_ARRAY_BUFFER, gl->vbos[1]);
    336 		glBufferSubData(GL_ARRAY_BUFFER, 0, n * sizeof(*rpb->texture_coordinates), rpb->texture_coordinates);
    337 		glBindBuffer(GL_ARRAY_BUFFER, gl->vbos[2]);
    338 		glBufferSubData(GL_ARRAY_BUFFER, 0, n * sizeof(*rpb->colours), rpb->colours);
    339 		END_NAMED_BLOCK(upload);
    340 
    341 		glDrawElements(GL_TRIANGLES, 6 * n / 4, GL_UNSIGNED_INT, 0);
    342 	}
    343 	rc->rpb->count = 0;
    344 	END_TIMED_BLOCK();
    345 }
    346 
    347 function u32
    348 get_render_push_buffer_idx(RenderCtx *rc, u32 count)
    349 {
    350 	if (rc->rpb->count + count > RENDER_PUSH_BUFFER_CAP)
    351 		flush_render_push_buffer(rc);
    352 	u32 result  = rc->rpb->count;
    353 	rc->rpb->count += count;
    354 	return result;
    355 }
    356 
    357 function void
    358 push_rect_full(RenderCtx *rc, Rect r, v4 colour, v2 min_tex_coord, v2 max_tex_coord)
    359 {
    360 	BEGIN_TIMED_BLOCK();
    361 
    362 	u32 idx = get_render_push_buffer_idx(rc, 4);
    363 	v2 start = r.pos;
    364 	v2 end   = {.x = r.pos.x + r.size.w, .y = r.pos.y + r.size.h};
    365 
    366 	RenderPushBuffer *rpb = rc->rpb;
    367 	rpb->positions[idx + 0] = (v2){.x = end.x,   .y = end.y  };
    368 	rpb->positions[idx + 1] = (v2){.x = end.x,   .y = start.y};
    369 	rpb->positions[idx + 2] = (v2){.x = start.x, .y = start.y};
    370 	rpb->positions[idx + 3] = (v2){.x = start.x, .y = end.y  };
    371 
    372 	rpb->texture_coordinates[idx + 0] = (v2){.x = max_tex_coord.x, .y = max_tex_coord.y};
    373 	rpb->texture_coordinates[idx + 1] = (v2){.x = max_tex_coord.x, .y = min_tex_coord.y};
    374 	rpb->texture_coordinates[idx + 2] = (v2){.x = min_tex_coord.x, .y = min_tex_coord.y};
    375 	rpb->texture_coordinates[idx + 3] = (v2){.x = min_tex_coord.x, .y = max_tex_coord.y};
    376 
    377 	rpb->colours[idx + 0] = colour;
    378 	rpb->colours[idx + 1] = colour;
    379 	rpb->colours[idx + 2] = colour;
    380 	rpb->colours[idx + 3] = colour;
    381 
    382 	END_TIMED_BLOCK();
    383 }
    384 
    385 function void
    386 push_rect_textured(RenderCtx *rc, Rect r, v4 colour, b32 flip_texture)
    387 {
    388 	v2 min_tex_coord, max_tex_coord;
    389 	if (!flip_texture) {
    390 		max_tex_coord = (v2){.x = 1.0f, .y = 1.0f};
    391 		min_tex_coord = (v2){.x = 0.0f, .y = 0.0f};
    392 	} else {
    393 		max_tex_coord = (v2){.x = 1.0f, .y = 0.0f};
    394 		min_tex_coord = (v2){.x = 0.0f, .y = 1.0f};
    395 	}
    396 	push_rect_full(rc, r, colour, min_tex_coord, max_tex_coord);
    397 }
    398 
    399 function void
    400 push_rect(RenderCtx *rc, Rect r, v4 colour)
    401 {
    402 	f32 max_x = 1.0f / rc->gl->glyph_bitmap_dim.x;
    403 	f32 max_y = 1.0f / rc->gl->glyph_bitmap_dim.y;
    404 	push_rect_full(rc, r, colour, (v2){0}, (v2){.x = max_x, .y = max_y});
    405 }
    406 
    407 function v2
    408 push_s8(RenderCtx *rc, v2 pos, v4 colour, u32 font_id, s8 s)
    409 {
    410 	BEGIN_TIMED_BLOCK();
    411 	CachedGlyph *cg;
    412 	v2 start, end, text_size = {0};
    413 	v2 scale = {.x = 1.0f / rc->gl->glyph_bitmap_dim.x, .y = 1.0f / rc->gl->glyph_bitmap_dim.y};
    414 
    415 	/* TODO: implement GPU based glyph rasterizing */
    416 	pos.x = (u32)pos.x;
    417 	pos.y = (u32)pos.y;
    418 
    419 	while (s.len) {
    420 		u32 cp = get_utf8(&s);
    421 		if (cp == (u32)-1)
    422 			break;
    423 
    424 		get_gpu_glyph_index(rc->a, rc->gl, rc->fa, cp, font_id, FS_NORMAL, &cg);
    425 		cached_glyph_to_uv(rc->fa, cg, &start, &end, scale);
    426 
    427 		Rect r = {.pos = pos, .size = {.x = cg->width, .y = cg->height}};
    428 		r.pos.x += cg->x0;
    429 		r.pos.y -= (cg->height + cg->y0);
    430 
    431 		push_rect_full(rc, r, colour, start, end);
    432 
    433 		text_size.x += cg->advance;
    434 		pos.x       += cg->advance;
    435 		if (cg->height > text_size.y)
    436 			text_size.y = cg->height;
    437 	}
    438 	text_size.x -= cg->advance;
    439 	text_size.w += cg->width;
    440 
    441 	END_TIMED_BLOCK();
    442 
    443 	return text_size;
    444 }
    445 
    446 function v2
    447 measure_text(RenderCtx *rc, u32 font_id, s8 text)
    448 {
    449 	BEGIN_TIMED_BLOCK();
    450 
    451 	v2  result = {0};
    452 
    453 	CachedGlyph *cg;
    454 	while (text.len) {
    455 		u32 cp = get_utf8(&text);
    456 		if (cp == (u32)-1)
    457 			break;
    458 
    459 		get_gpu_glyph_index(rc->a, rc->gl, rc->fa, cp, font_id, FS_NORMAL, &cg);
    460 		if (cg->height > result.y)
    461 			result.y = cg->height;
    462 		result.x += cg->advance;
    463 	}
    464 	result.x -= cg->advance;
    465 	result.x += cg->width;
    466 
    467 	END_TIMED_BLOCK();
    468 
    469 	return result;
    470 }
    471 
    472 /* TODO: this is outdated */
    473 /* NOTE: In this program we render to an offscreen render target that is later drawn
    474  * to the screen as a full window quad. Therefore render_framebuffer must take care
    475  * to handle all necessary padding (window and cell). Outside of here everyone should
    476  * simply care about the terminal in terms of rows and columns (t->size). */
    477 function void
    478 render_framebuffer(Term *t, RenderCell *render_buf, TerminalInput *input, Arena arena)
    479 {
    480 	BEGIN_TIMED_BLOCK();
    481 
    482 	RenderThreadContext *ctx = &t->render_thread;
    483 	TermView *tv  = t->views + t->view_idx;
    484 	iv2 term_size = t->size;
    485 	/* NOTE: draw whole framebuffer */
    486 	for (i32 row = 0; row < term_size.h; row++) {
    487 		for (i32 col = 0; col < term_size.w; col++) {
    488 			Cell c         = tv->fb.rows[row][col];
    489 			RenderCell *rc = render_buf + (row * term_size.w + col);
    490 
    491 			CachedGlyph *cg;
    492 			rc->gpu_glyph = get_gpu_glyph_index(arena, &ctx->gl, &ctx->fa, c.cp, 0,
    493 			                                    c.bg & FS_MASK, &cg);
    494 			rc->fg = c.fg;
    495 			rc->bg = c.bg;
    496 
    497 			/* TODO: there is probably a better way to do this */
    498 			u32 tiles = cg->tile_count;
    499 			if (tiles > 1) {
    500 				ASSERT(tiles == 2);
    501 				rc[1].gpu_glyph = rc->gpu_glyph + 1;
    502 				rc[1].fg        = rc->fg;
    503 				rc[1].bg        = rc->bg;
    504 				col++;
    505 			}
    506 		}
    507 	}
    508 
    509 	/* NOTE: draw selection if active */
    510 	SelectionIterator si = selection_iterator(t->selection.range, tv->fb.rows, term_size.w);
    511 	for (Cell *c = selection_next(&si); c; c = selection_next(&si)) {
    512 		RenderCell *rc = render_buf + si.cursor.y * term_size.w + si.cursor.x;
    513 		rc->fg ^= SHADER_PACK_ATTR(ATTR_INVERSE);
    514 	}
    515 
    516 	END_TIMED_BLOCK();
    517 }
    518 
    519 function void
    520 render_cursor(Term *t, b32 focused, Arena a)
    521 {
    522 	BEGIN_TIMED_BLOCK();
    523 
    524 	RenderThreadContext *ctx = &t->render_thread;
    525 	iv2 curs = t->cursor.pos;
    526 	Cell *c  = &t->views[t->view_idx].fb.rows[curs.y][curs.x];
    527 	RenderCell *rc = alloc(&a, RenderCell, 3);
    528 
    529 	iz rc_off = 1;
    530 	iz length = sizeof(RenderCell);
    531 	iz offset = sizeof(RenderCell) * (curs.y * t->size.w + curs.x);
    532 
    533 	CachedGlyph *cg;
    534 	rc[1].gpu_glyph = get_gpu_glyph_index(a, &ctx->gl, &ctx->fa, c->cp, 0, c->bg & FS_MASK, &cg);
    535 	rc[1].fg        = c->fg;
    536 	rc[1].bg        = c->bg;
    537 
    538 	/* NOTE: draw cursor */
    539 	if (focused && (!(t->mode.win & WM_HIDECURSOR) && t->scroll_offset == 0)) {
    540 		rc[1].fg ^= SHADER_PACK_ATTR(ATTR_INVERSE);
    541 		//if ((t->mode.term & TM_ALTSCREEN) == 0)
    542 		//	rc[1].fg |= SHADER_PACK_ATTR(ATTR_BLINK);
    543 
    544 		if (c->bg & ATTR_WIDE) {
    545 			length   *= 2;
    546 			rc[2].fg ^= SHADER_PACK_ATTR(ATTR_INVERSE);
    547 			//if ((t->mode.term & TM_ALTSCREEN) == 0)
    548 			//	rc[2].fg |= SHADER_PACK_ATTR(ATTR_BLINK);
    549 		} else if (c->bg & ATTR_WDUMMY) {
    550 			rc_off    = 0;
    551 			length   *= 2;
    552 			offset   -= sizeof(RenderCell);
    553 			rc[0].fg ^= SHADER_PACK_ATTR(ATTR_INVERSE);
    554 			//if ((t->mode.term & TM_ALTSCREEN) == 0)
    555 			//	rc[0].fg |= SHADER_PACK_ATTR(ATTR_BLINK);
    556 		}
    557 	}
    558 
    559 	glBufferSubData(GL_SHADER_STORAGE_BUFFER, offset, length, rc + rc_off);
    560 
    561 	END_TIMED_BLOCK();
    562 }
    563 
    564 function iv2
    565 mouse_to_cell_space(Term *t, v2 mouse)
    566 {
    567 	v2 cell_size = fa_cell_size(&t->render_thread.fa);
    568 	v2 top_left  = get_terminal_top_left(t);
    569 
    570 	iv2 result;
    571 	result.x = (i32)((mouse.x - top_left.x) / cell_size.w + 0.5);
    572 	result.y = (i32)((top_left.y - mouse.y) / cell_size.h + 0.5);
    573 
    574 	CLAMP(result.x, 0, t->size.w - 1);
    575 	CLAMP(result.y, 0, t->size.h - 1);
    576 
    577 	return result;
    578 }
    579 
    580 function void
    581 stream_push_selection(Stream *s, Row *rows, Range sel, u32 term_width)
    582 {
    583 	s->errors |= !is_valid_range(sel);
    584 	if (s->errors) return;
    585 
    586 	SelectionIterator si = selection_iterator(sel, rows, term_width);
    587 	u32 last_non_space_idx = 0;
    588 	for (Cell *c = selection_next(&si); c; c = selection_next(&si)) {
    589 		if (c->bg & ATTR_WDUMMY)
    590 			continue;
    591 
    592 		stream_push_s8(s, utf8_encode(c->cp));
    593 		if (!ISSPACE(c->cp))
    594 			last_non_space_idx = s->count;
    595 
    596 		if (si.next.y != si.cursor.y) {
    597 			stream_reset(s, last_non_space_idx);
    598 			stream_push_byte(s, '\n');
    599 		}
    600 	}
    601 
    602 	stream_reset(s, last_non_space_idx);
    603 }
    604 
    605 function void
    606 begin_selection(Term *t, u32 click_count, v2 mouse)
    607 {
    608 	Selection *sel = &t->selection;
    609 	sel->state     = MIN(click_count, SS_WORDS);
    610 	sel->range.end = INVALID_RANGE_END;
    611 
    612 	iv2 cell = mouse_to_cell_space(t, mouse);
    613 
    614 	switch (sel->state) {
    615 	case SS_WORDS: sel->anchor = sel->range = get_word_around_cell(t, cell); break;
    616 	case SS_CHAR:  sel->anchor = get_char_around_cell(t, cell);              break;
    617 	case SS_NONE:  break;
    618 	}
    619 
    620 	t->render_thread.gl.queued_render = 1;
    621 }
    622 
    623 function void
    624 update_selection(Term *t, TerminalInput *input)
    625 {
    626 	if (!input->keys[MOUSE_LEFT].ended_down)
    627 		return;
    628 
    629 	if (input->mouse.x == input->last_mouse.x && input->mouse.y == input->last_mouse.y)
    630 		return;
    631 
    632 	Selection *sel = &t->selection;
    633 	iv2 new_p      = mouse_to_cell_space(t, input->mouse);
    634 	Range new, old_range = sel->range;
    635 	switch (sel->state) {
    636 	case SS_WORDS: new = get_word_around_cell(t, new_p); break;
    637 	case SS_CHAR:  new = get_char_around_cell(t, new_p); break;
    638 	case SS_NONE:  /*TODO: INVALID_CODE_PATH;*/ return;  break;
    639 	}
    640 
    641 	if (sel->anchor.start.y < new.start.y) {
    642 		sel->range.start = sel->anchor.start;
    643 		sel->range.end   = new.end;
    644 	} else if (sel->anchor.start.y > new.start.y) {
    645 		sel->range.start = new.start;
    646 		sel->range.end   = sel->anchor.end;
    647 	} else {
    648 		if (new.start.x < sel->anchor.start.x) {
    649 			sel->range.start = new.start;
    650 			sel->range.end   = sel->anchor.end;
    651 		} else {
    652 			sel->range.start = sel->anchor.start;
    653 			sel->range.end   = new.end;
    654 		}
    655 	}
    656 	sel->range = normalize_range(sel->range);
    657 
    658 	if (!equal_range(old_range, sel->range))
    659 		t->render_thread.gl.queued_render = 1;
    660 }
    661 
    662 KEYBIND_FN(copy)
    663 {
    664 	Stream buf = arena_stream(t->arena_for_frame);
    665 	stream_push_selection(&buf, t->views[t->view_idx].fb.rows, t->selection.range, t->size.w);
    666 	stream_push_byte(&buf, 0);
    667 	os->set_clipboard(buf.data, buf.count - 1, a.i);
    668 	return 1;
    669 }
    670 
    671 KEYBIND_FN(paste)
    672 {
    673 	b32 bracketed = t->mode.win & WM_BRACKPASTE;
    674 
    675 	/* TODO(rnp): we may need to replace '\n' with '\r\n' */
    676 	s8 text = c_str_to_s8((char *)os->get_clipboard(a.i));
    677 	if (text.len) {
    678 		if (bracketed) os->write(t->child, s8("\033[200~"));
    679 		os->write(t->child, text);
    680 		if (bracketed) os->write(t->child, s8("\033[201~"));
    681 	}
    682 
    683 	return 1;
    684 }
    685 
    686 KEYBIND_FN(scroll)
    687 {
    688 	if (t->mode.term & TM_ALTSCREEN)
    689 		return 0;
    690 
    691 	TermView *tv = t->views + t->view_idx;
    692 
    693 	/* NOTE: do nothing if there aren't enough lines to scrollback */
    694 	if (tv->lines.filled < t->size.h)
    695 		return 1;
    696 
    697 	t->scroll_offset += a.i;
    698 	CLAMP(t->scroll_offset, 0, tv->lines.filled - (t->size.h - 1));
    699 
    700 	t->state |= TS_NEEDS_REFILL;
    701 
    702 	return 1;
    703 }
    704 
    705 KEYBIND_FN(zoom)
    706 {
    707 	shift_font_sizes(&t->render_thread.fa, a.i);
    708 	font_atlas_update(&t->render_thread.fa, t->render_thread.gl.glyph_bitmap_dim);
    709 	t->state |= TS_NEEDS_RESIZE;
    710 	return 1;
    711 }
    712 
    713 function void
    714 report_mouse(Term *t, TerminalInput *input, b32 released, b32 beginning)
    715 {
    716 	if ((t->mode.win & WM_MOUSE_X10) && released)
    717 		return;
    718 
    719 	iv2 pos = mouse_to_cell_space(t, input->mouse);
    720 	if ((pos.x > (255 - 32)) || (pos.y > (255 - 32)))
    721 		return;
    722 
    723 	/* TODO: pass the button into this function once they are given in order */
    724 	/* TODO: extended mouse buttons (up to button 11) should also be encoded */
    725 	i32 button = 0;
    726 	if      (input->keys[MOUSE_LEFT].ended_down)   button = 1;
    727 	else if (input->keys[MOUSE_MIDDLE].ended_down) button = 2;
    728 	else if (input->keys[MOUSE_RIGHT].ended_down)  button = 3;
    729 	else if (input->mouse_scroll.y > 0)            button = 4;
    730 	else if (input->mouse_scroll.y < 0)            button = 5;
    731 
    732 	i32 value;
    733 	if (t->mode.win & WM_MOUSE_TRK && !beginning && !released) {
    734 		if (equal_iv2(t->interaction.last_cell_report, pos))
    735 			return;
    736 		value = 32;
    737 	} else {
    738 		value = 0;
    739 	}
    740 	t->interaction.last_cell_report = pos;
    741 
    742 	if ((t->mode.win & WM_MOUSE_SGR) && !button)
    743 		value += 0;
    744 	else if (!button)
    745 		value += 3;
    746 	else if (button >= 4)
    747 		value += 64 + button - 4;
    748 	else
    749 		value += button - 1;
    750 
    751 	if (!(t->mode.win & WM_MOUSE_X10)) {
    752 		value += ((input->modifiers & MOD_SHIFT)   ?  4 : 0)
    753 		       + ((input->modifiers & MOD_ALT)     ?  8 : 0)
    754 		       + ((input->modifiers & MOD_CONTROL) ? 16 : 0);
    755 	}
    756 
    757 	Stream buf = arena_stream(t->arena_for_frame);
    758 	if (t->mode.win & WM_MOUSE_SGR) {
    759 		stream_push_s8(&buf, s8("\x1B[<"));
    760 		stream_push_i64(&buf, value);
    761 		stream_push_byte(&buf, ';');
    762 		stream_push_i64(&buf, pos.x + 1);
    763 		stream_push_byte(&buf, ';');
    764 		stream_push_i64(&buf, pos.y + 1);
    765 		stream_push_byte(&buf, released? 'm' : 'M');
    766 	} else if ((pos.x < (255 - 32)) && (pos.y < (255 - 32))) {
    767 		stream_push_s8(&buf, s8("\x1B[M"));
    768 		stream_push_byte(&buf, 32 + value);
    769 		stream_push_byte(&buf, 32 + pos.x + 1);
    770 		stream_push_byte(&buf, 32 + pos.y + 1);
    771 	} else {
    772 		INVALID_CODE_PATH;
    773 		return;
    774 	}
    775 
    776 	t->os->write(t->child, stream_to_s8(&buf));
    777 }
    778 
    779 function void
    780 begin_terminal_interaction(Term *t, TerminalInput *input, u32 click_count)
    781 {
    782 	if (t->mode.win & WM_MOUSE_MASK) {
    783 		report_mouse(t, input, 0, 1);
    784 	} else {
    785 		if (pressed_last_frame(input->keys + MOUSE_LEFT))
    786 			begin_selection(t, click_count, input->mouse);
    787 	}
    788 }
    789 
    790 function b32
    791 terminal_interaction(Term *t, OS *os, TerminalInput *input, u32 click_count)
    792 {
    793 
    794 	b32 should_end_interaction = all_mouse_up(input);
    795 	if (t->mode.win & WM_MOUSE_MASK) {
    796 		if (t->mode.win & WM_MOUSE_TRK)
    797 			report_mouse(t, input, should_end_interaction, 0);
    798 	} else {
    799 		update_selection(t, input);
    800 		if (pressed_last_frame(input->keys + MOUSE_MIDDLE))
    801 			paste(t, os, (Arg){.i = OS_CLIPBOARD_SECONDARY});
    802 
    803 		b32 shift_down = input->modifiers & MOD_SHIFT;
    804 		if (input->mouse_scroll.y) {
    805 			if (t->mode.term & TM_ALTSCREEN) {
    806 				iptr child = t->child;
    807 				if (input->mouse_scroll.y > 0) {
    808 					if (shift_down) os->write(child, s8("\x1B[5;2~"));
    809 					else            os->write(child, s8("\x19"));
    810 				} else {
    811 					if (shift_down) os->write(child, s8("\x1B[6;2~"));
    812 					else            os->write(child, s8("\x05"));
    813 				}
    814 			} else {
    815 				Arg a = {.i = (i32)input->mouse_scroll.y};
    816 				if (shift_down)
    817 				    a.i *= 5;
    818 				scroll(t, os, a);
    819 			}
    820 		}
    821 	}
    822 
    823 	return should_end_interaction;
    824 }
    825 
    826 DEBUG_EXPORT VTGL_HANDLE_KEYS_FN(vtgl_handle_keys)
    827 {
    828 	Term *t    = memory->memory;
    829 	iptr child = t->child;
    830 
    831 	#ifdef _DEBUG
    832 	if (key == KEY_F1 && action == BUTTON_PRESS) {
    833 		dump_lines_to_file(t);
    834 		return;
    835 	}
    836 	if (key == KEY_F11 && action == BUTTON_PRESS) {
    837 		/* TODO: probably move this into the debug frame start */
    838 		DebugState *ds = memory->debug_memory;
    839 		ds->paused = !ds->paused;
    840 		return;
    841 	}
    842 	if (key == KEY_F12 && action == BUTTON_PRESS) {
    843 		t->render_thread.gl.flags ^= DRAW_DEBUG_OVERLAY;
    844 		input->window_refreshed = 1;
    845 		return;
    846 	}
    847 	#endif
    848 
    849 	/* NOTE: handle mapped keybindings */
    850 	u32 enc = ENCODE_KEY(action, modifiers, key);
    851 	for (u32 i = 0; i < ARRAY_COUNT(g_hotkeys); i++) {
    852 		struct hotkey *hk = g_hotkeys + i;
    853 		if (hk->key == enc) {
    854 			b32 handled = hk->fn(t, os, hk->arg);
    855 			if (handled)
    856 				return;
    857 		}
    858 	}
    859 
    860 	/* NOTE: send control sequences */
    861 	if (modifiers & MOD_CONTROL && action != BUTTON_RELEASE) {
    862 		/* TODO: this is wrong. look up where 8-bit modifiers should be sent */
    863 		if (0 && t->mode.win & WM_8BIT) {
    864 			if (key < 0x7F) {
    865 				os->write(child, utf8_encode(key | 0x80));
    866 				return;
    867 			}
    868 		} else if (BETWEEN(key, 0x40, 0x5F)) {
    869 			os->write(child, utf8_encode(key - 0x40));
    870 			return;
    871 		}
    872 	}
    873 
    874 	/* TODO: construct a hash table of bound keys */
    875 	switch (ENCODE_KEY(action, 0, key)) {
    876 	case ENCODE_KEY(BUTTON_PRESS,  0, KEY_ESCAPE):
    877 	case ENCODE_KEY(BUTTON_REPEAT, 0, KEY_ESCAPE): {
    878 		os->write(child, s8("\x1B"));
    879 	} break;
    880 	case ENCODE_KEY(BUTTON_PRESS,  0, KEY_TAB):
    881 	case ENCODE_KEY(BUTTON_REPEAT, 0, KEY_TAB): {
    882 		os->write(child, s8("\t"));
    883 	} break;
    884 	case ENCODE_KEY(BUTTON_PRESS,  0, KEY_ENTER):
    885 	case ENCODE_KEY(BUTTON_REPEAT, 0, KEY_ENTER): {
    886 		os->write(child, s8("\r"));
    887 	} break;
    888 	case ENCODE_KEY(BUTTON_PRESS,  0, KEY_BACKSPACE):
    889 	case ENCODE_KEY(BUTTON_REPEAT, 0, KEY_BACKSPACE): {
    890 		os->write(child, s8("\x7F"));
    891 	} break;
    892 	case ENCODE_KEY(BUTTON_PRESS,  0, KEY_UP):
    893 	case ENCODE_KEY(BUTTON_REPEAT, 0, KEY_UP): {
    894 		if (t->mode.win & WM_APPCURSOR) os->write(child, s8("\x1BOA"));
    895 		else                            os->write(child, s8("\x1B[A"));
    896 	} break;
    897 	case ENCODE_KEY(BUTTON_PRESS,  0, KEY_DOWN):
    898 	case ENCODE_KEY(BUTTON_REPEAT, 0, KEY_DOWN): {
    899 		if (t->mode.win & WM_APPCURSOR) os->write(child, s8("\x1BOB"));
    900 		else                            os->write(child, s8("\x1B[B"));
    901 	} break;
    902 	case ENCODE_KEY(BUTTON_PRESS,  0, KEY_RIGHT):
    903 	case ENCODE_KEY(BUTTON_REPEAT, 0, KEY_RIGHT): {
    904 		if (t->mode.win & WM_APPCURSOR) os->write(child, s8("\x1BOC"));
    905 		else                            os->write(child, s8("\x1B[C"));
    906 	} break;
    907 	case ENCODE_KEY(BUTTON_PRESS,  0, KEY_LEFT):
    908 	case ENCODE_KEY(BUTTON_REPEAT, 0, KEY_LEFT): {
    909 		if (t->mode.win & WM_APPCURSOR) os->write(child, s8("\x1BOD"));
    910 		else                            os->write(child, s8("\x1B[D"));
    911 	} break;
    912 	case ENCODE_KEY(BUTTON_PRESS,  0, KEY_PAGE_UP):
    913 	case ENCODE_KEY(BUTTON_REPEAT, 0, KEY_PAGE_UP): {
    914 		if      (modifiers & MOD_CONTROL) os->write(child, s8("\x1B[5;5~"));
    915 		else if (modifiers & MOD_SHIFT)   os->write(child, s8("\x1B[5;2~"));
    916 		else                              os->write(child, s8("\x1B[5~"));
    917 	} break;
    918 	case ENCODE_KEY(BUTTON_PRESS,  0, KEY_PAGE_DOWN):
    919 	case ENCODE_KEY(BUTTON_REPEAT, 0, KEY_PAGE_DOWN): {
    920 		if      (modifiers & MOD_CONTROL) os->write(child, s8("\x1B[6;5~"));
    921 		else if (modifiers & MOD_SHIFT)   os->write(child, s8("\x1B[6;2~"));
    922 		else                              os->write(child, s8("\x1B[6~"));
    923 	} break;
    924 	}
    925 }
    926 
    927 function b32
    928 should_start_interaction(TerminalInput *input)
    929 {
    930 	b32 result = input->mouse_scroll.y || input->mouse_scroll.x ||
    931 	             pressed_last_frame(input->keys + MOUSE_LEFT)   ||
    932 	             pressed_last_frame(input->keys + MOUSE_MIDDLE) ||
    933 	             pressed_last_frame(input->keys + MOUSE_RIGHT);
    934 	return result;
    935 }
    936 
    937 function void
    938 begin_interaction(Term *t, InteractionState *is, TerminalInput *input)
    939 {
    940 	is->click_count++;
    941 	if (is->multi_click_t < 0)
    942 		is->multi_click_t = INTERACTION_MULTI_CLICK_TIME;
    943 
    944 	if (is->hot.type != IS_NONE) {
    945 		if (is->hot.type == IS_AUTO) {
    946 			//switch (is->hot.var.type) {
    947 			//	/* TODO: start the interaction */
    948 			//}
    949 		}
    950 
    951 		is->active = is->hot;
    952 
    953 		switch (is->active.type) {
    954 		case IS_TERM:
    955 			begin_terminal_interaction(t, input, is->click_count);
    956 			break;
    957 		default:
    958 			break;
    959 		}
    960 	} else {
    961 		is->active = (Interaction){.type = IS_NOP};
    962 	}
    963 }
    964 
    965 function void
    966 end_interaction(InteractionState *is, TerminalInput *input)
    967 {
    968 	is->active = (Interaction){.type = IS_NONE};
    969 }
    970 
    971 function void
    972 handle_interactions(Term *t, TerminalInput *input, OS *os)
    973 {
    974 	InteractionState *is = &t->interaction;
    975 
    976 	ButtonState *mouse_left = input->keys + MOUSE_LEFT;
    977 
    978 	is->multi_click_t -= dt_for_frame;
    979 	if (!mouse_left->ended_down && is->multi_click_t < 0) {
    980 		is->click_count = 0;
    981 	}
    982 
    983 	if (is->hot.type != IS_NONE) {
    984 		if (should_start_interaction(input)) {
    985 			end_interaction(is, input);
    986 			begin_interaction(t, is, input);
    987 		}
    988 	}
    989 
    990 	switch (is->active.type) {
    991 	case IS_NONE: break;
    992 	case IS_NOP:  break;
    993 	case IS_SET:  end_interaction(is, input); break;
    994 	case IS_AUTO:  /* TODO */ break;
    995 	case IS_DRAG:  /* TODO */ break;
    996 	case IS_DEBUG: /* TODO */ break;
    997 	case IS_TERM: {
    998 		if (terminal_interaction(t, os, input, is->click_count))
    999 			end_interaction(is, input);
   1000 	} break;
   1001 	}
   1002 }
   1003 
   1004 DEBUG_EXPORT VTGL_RENDER_FRAME_FN(vtgl_render_frame)
   1005 {
   1006 	BEGIN_TIMED_BLOCK();
   1007 
   1008 	Term *t = memory->memory;
   1009 	RenderThreadContext *ctx = &t->render_thread;
   1010 
   1011 	Arena arena  = ctx->arena;
   1012 	dt_for_frame = input->dt;
   1013 
   1014 	WorkQueueWork *work = work_queue_pop(&ctx->work_queue);
   1015 	while (work) {
   1016 		switch (work->kind) {
   1017 		case WQK_RELOAD_SHADER: {
   1018 			reload_shader(os, &ctx->gl, work->shader_reload_context, arena);
   1019 		} break;
   1020 		case WQK_RELOAD_ALL_SHADERS: {
   1021 			for (i32 i = 0; i < SID_LAST; i++) {
   1022 				reload_shader(os, &ctx->gl, ctx->shader_reload_contexts + i, arena);
   1023 			}
   1024 		} break;
   1025 		default: INVALID_CODE_PATH;
   1026 		}
   1027 		work_queue_pop_commit(&ctx->work_queue);
   1028 		work = work_queue_pop(&ctx->work_queue);
   1029 	}
   1030 
   1031 	/* NOTE: default state which can be overwritten later in the frame */
   1032 	/* TODO: if (!t->ui_active) */
   1033 	{
   1034 		t->interaction.hot.type = IS_TERM;
   1035 	}
   1036 
   1037 	glBindTexture(GL_TEXTURE_2D, ctx->gl.glyph_bitmap_tex);
   1038 
   1039 	BEGIN_NAMED_BLOCK(update_render);
   1040 
   1041 	RenderCtx rc = make_render_ctx(&arena, &ctx->gl, &ctx->fa);
   1042 
   1043 	glUseProgram(ctx->gl.programs[SID_RENDER]);
   1044 	glBindFramebuffer(GL_FRAMEBUFFER, ctx->gl.fb);
   1045 	clear_colour();
   1046 
   1047 	/* NOTE(rnp): spin lock here so that we don't have to deal with rescheduling
   1048 	 * a redraw. Remember that this failing is already unlikely. */
   1049 	while (atomic_exchange_n(&t->resize_lock, 1) != 0);
   1050 
   1051 	if (ctx->gl.flags & RESIZE_RENDERER)
   1052 		resize(t, os, input->window_size);
   1053 
   1054 	if (ctx->gl.queued_render) {
   1055 		ctx->gl.queued_render = 0;
   1056 		u32 cell_count = t->size.h * t->size.w;
   1057 		RenderCell *render_buf = alloc(&arena, RenderCell, cell_count);
   1058 		render_framebuffer(t, render_buf, input, arena);
   1059 		glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
   1060 		                cell_count * sizeof(*render_buf), render_buf);
   1061 	}
   1062 	render_cursor(t, input->window_focused, arena);
   1063 
   1064 	t->resize_lock = 0;
   1065 
   1066 	ShaderParameters *sp = &ctx->gl.shader_parameters;
   1067 	sp->blink_parameter += 2 * PI * g_blink_speed * dt_for_frame;
   1068 	if (sp->blink_parameter > 2 * PI) sp->blink_parameter -= 2 * PI;
   1069 	sp->reverse_video_mask = REVERSE_VIDEO_MASK * !!(t->mode.win & WM_REVERSE);
   1070 
   1071 	glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(*sp), sp);
   1072 
   1073 	push_rect_textured(&rc, (Rect){.size = v2_from_iv2(ctx->gl.window_size)}, (v4){0}, 0);
   1074 	flush_render_push_buffer(&rc);
   1075 
   1076 	static f32 param = 0;
   1077 	static f32 p_scale = 1;
   1078 	param += p_scale * 0.005 * dt_for_frame;
   1079 	if (param > 1.0f || param < 0)
   1080 		p_scale *= -1.0f;
   1081 
   1082 	glBindFramebuffer(GL_FRAMEBUFFER, 0);
   1083 
   1084 	clear_colour();
   1085 	glUseProgram(ctx->gl.programs[SID_POST]);
   1086 	glUniform1i(ctx->gl.post.texslot, ctx->gl.fb_tex_unit);
   1087 	glUniform1f(ctx->gl.post.param, param);
   1088 
   1089 	push_rect_textured(&rc, (Rect){.size = v2_from_iv2(ctx->gl.window_size)}, (v4){0}, 1);
   1090 	flush_render_push_buffer(&rc);
   1091 	END_NAMED_BLOCK(update_render);
   1092 
   1093 	/* NOTE: this happens at the end so that ui stuff doesn't go through the post
   1094 	 * processing/effects shader */
   1095 	glUseProgram(ctx->gl.programs[SID_RECTS]);
   1096 
   1097 	BEGIN_NAMED_BLOCK(debug_overlay);
   1098 	draw_debug_overlay(memory, input, &rc);
   1099 	END_NAMED_BLOCK(debug_overlay);
   1100 
   1101 	END_TIMED_BLOCK();
   1102 }
   1103 
   1104 DEBUG_EXPORT VTGL_ACTIVE_SELECTION_FN(vtgl_active_selection)
   1105 {
   1106 	Term *t      = memory->memory;
   1107 	Range result = t->selection.range;
   1108 	if (out) stream_push_selection(out, t->views[t->view_idx].fb.rows, result, t->size.w);
   1109 	return result;
   1110 }
   1111 
   1112 DEBUG_EXPORT VTGL_FRAME_STEP_FN(vtgl_frame_step)
   1113 {
   1114 	BEGIN_NAMED_BLOCK(debug_end_frame);
   1115 	debug_frame_end(memory, input);
   1116 	END_NAMED_BLOCK(debug_end_frame);
   1117 
   1118 	BEGIN_TIMED_BLOCK();
   1119 
   1120 	Term *t = memory->memory;
   1121 
   1122 	dt_for_frame = input->dt;
   1123 
   1124 	t->temp_arena = begin_temp_arena(&t->arena_for_frame);
   1125 
   1126 	if (t->state & TS_NEEDS_RESIZE || !equal_iv2(input->window_size, t->render_thread.gl.window_size)) {
   1127 		/* NOTE(rnp): we skip the resize this time through so that we don't add
   1128 		 * input latency waiting for the render thread to release this lock */
   1129 		if (atomic_exchange_n(&t->resize_lock, 1) == 0) {
   1130 			resize_terminal(t, os, input->window_size);
   1131 			t->resize_lock = 0;
   1132 		} else {
   1133 			input->pending_updates = 1;
   1134 		}
   1135 	}
   1136 
   1137 	if (input->executable_reloaded) {
   1138 		work_queue_insert(&t->render_thread.work_queue, WQK_RELOAD_ALL_SHADERS, 0);
   1139 	}
   1140 
   1141 	BEGIN_NAMED_BLOCK(mouse_and_keyboard_input);
   1142 	if (input->character_input.len) {
   1143 		if (t->scroll_offset) {
   1144 			t->scroll_offset = 0;
   1145 			t->state |= TS_NEEDS_REFILL;
   1146 		}
   1147 		os->write(t->child, input->character_input);
   1148 	}
   1149 
   1150 	handle_interactions(t, input, os);
   1151 	END_NAMED_BLOCK(mouse_and_keyboard_input);
   1152 
   1153 	if (t->state & TS_NEEDS_REFILL) {
   1154 		blit_lines(t, t->arena_for_frame);
   1155 		t->render_thread.gl.queued_render = 1;
   1156 	}
   1157 
   1158 	BEGIN_NAMED_BLOCK(input_from_child);
   1159 	if (input->data_available) {
   1160 		OSRingBuffer *rb = &t->views[t->view_idx].log;
   1161 		s8 buffer = {.len = rb->capacity - t->unprocessed_bytes, .data = rb->data + rb->write_index};
   1162 
   1163 		iz bytes_read = os->read(t->child, buffer);
   1164 		ASSERT(bytes_read <= rb->capacity);
   1165 		commit_to_rb(t->views + t->view_idx, bytes_read);
   1166 
   1167 		t->unprocessed_bytes += bytes_read;
   1168 		s8 raw = {
   1169 			.len  = t->unprocessed_bytes,
   1170 			.data = rb->data + (rb->write_index - t->unprocessed_bytes)
   1171 		};
   1172 		handle_input(t, t->arena_for_frame, raw);
   1173 		t->render_thread.gl.queued_render = 1;
   1174 	}
   1175 	END_NAMED_BLOCK(input_from_child);
   1176 
   1177 	end_temp_arena(t->temp_arena);
   1178 
   1179 	END_TIMED_BLOCK();
   1180 
   1181 	if (t->render_thread.gl.queued_render || input->window_refreshed ||
   1182 	    t->render_thread.gl.flags & DRAW_DEBUG_OVERLAY ||
   1183 	    !work_queue_empty(&t->render_thread.work_queue))
   1184 	{
   1185 		os->wake_waiters(t->render_thread.sync);
   1186 	}
   1187 }
   1188 
   1189 #ifdef _DEBUG
   1190 #include "debug.c"
   1191 #endif