1 /*
2  * This software is licensed under the terms of the MIT License.
3  * See COPYING for further information.
4  * ---
5  * Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
6  * Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
7  */
8 
9 #include "taisei.h"
10 
11 #include "global.h"
12 #include "stagedraw.h"
13 #include "stagetext.h"
14 #include "video.h"
15 #include "resource/postprocess.h"
16 #include "entity.h"
17 #include "util/fbmgr.h"
18 
19 #ifdef DEBUG
20 	#define GRAPHS_DEFAULT 1
21 	#define OBJPOOLSTATS_DEFAULT 0
22 #else
23 	#define GRAPHS_DEFAULT 0
24 	#define OBJPOOLSTATS_DEFAULT 0
25 #endif
26 
27 static struct {
28 	struct {
29 		ShaderProgram *shader;
30 		Font *font;
31 
32 		struct {
33 			Color active;
34 			Color inactive;
35 			Color dark;
36 			Color label;
37 			Color label_power;
38 			Color label_value;
39 			Color label_graze;
40 			Color label_voltage;
41 		} color;
42 	} hud_text;
43 
44 	struct {
45 		ShaderProgram *fxaa;
46 		ShaderProgram *copy_depth;
47 	} shaders;
48 
49 	struct {
50 		float alpha;
51 		float target_alpha;
52 	} clear_screen;
53 
54 	PostprocessShader *viewport_pp;
55 	FBPair fb_pairs[NUM_FBPAIRS];
56 	FBPair powersurge_fbpair;
57 
58 	bool framerate_graphs;
59 	bool objpool_stats;
60 
61 	ManagedFramebufferGroup *mfb_group;
62 
63 	#ifdef DEBUG
64 		Sprite dummy;
65 	#endif
66 } stagedraw = {
67 	.hud_text.color = {
68 		// NOTE: premultiplied alpha
69 		.active        = { 1.00, 1.00, 1.00, 1.00 },
70 		.inactive      = { 0.50, 0.50, 0.50, 0.70 },
71 		.dark          = { 0.30, 0.30, 0.30, 0.70 },
72 		.label         = { 1.00, 1.00, 1.00, 1.00 },
73 		.label_power   = { 1.00, 0.50, 0.50, 1.00 },
74 		.label_value   = { 0.30, 0.60, 1.00, 1.00 },
75 		.label_graze   = { 0.50, 1.00, 0.50, 1.00 },
76 		.label_voltage = { 0.80, 0.50, 1.00, 1.00 },
77 	}
78 };
79 
fb_scale(void)80 static double fb_scale(void) {
81 	float vp_width, vp_height;
82 	video_get_viewport_size(&vp_width, &vp_height);
83 	return (double)vp_height / SCREEN_H;
84 }
85 
set_fb_size(StageFBPair fb_id,int * w,int * h,float scale_worst,float scale_best)86 static void set_fb_size(StageFBPair fb_id, int *w, int *h, float scale_worst, float scale_best) {
87 	double scale = fb_scale();
88 
89 	if(fb_id == FBPAIR_FG_AUX || fb_id == FBPAIR_BG_AUX) {
90 		scale_worst *= 0.5;
91 	}
92 
93 	int pp_qual = config_get_int(CONFIG_POSTPROCESS);
94 
95 	// We might lerp between these in the future
96 	if(pp_qual < 2) {
97 		scale *= scale_worst;
98 	} else {
99 		scale *= scale_best;
100 	}
101 
102 	log_debug("%u %f %f %f %i", fb_id, scale, scale_worst, scale_best, pp_qual);
103 
104 	switch(fb_id) {
105 		case FBPAIR_BG:
106 			scale *= config_get_float(CONFIG_BG_QUALITY);
107 			// fallthrough
108 
109 		default:
110 			scale *= config_get_float(CONFIG_FG_QUALITY);
111 			break;
112 	}
113 
114 	scale = sanitize_scale(scale);
115 	*w = round(VIEWPORT_W * scale);
116 	*h = round(VIEWPORT_H * scale);
117 }
118 
119 typedef struct StageFramebufferResizeParams {
120 	struct { float worst, best; } scale;
121 	StageFBPair scaling_base;
122 	int refs;
123 } StageFramebufferResizeParams;
124 
stage_framebuffer_resize_strategy(void * userdata,IntExtent * out_dimensions,FloatRect * out_viewport)125 static void stage_framebuffer_resize_strategy(void *userdata, IntExtent *out_dimensions, FloatRect *out_viewport) {
126 	StageFramebufferResizeParams *rp = userdata;
127 	set_fb_size(rp->scaling_base, &out_dimensions->w, &out_dimensions->h, rp->scale.worst, rp->scale.best);
128 	*out_viewport = (FloatRect) { 0, 0, out_dimensions->w, out_dimensions->h };
129 }
130 
stage_framebuffer_resize_strategy_cleanup(void * userdata)131 static void stage_framebuffer_resize_strategy_cleanup(void *userdata) {
132 	StageFramebufferResizeParams *rp = userdata;
133 	if(--rp->refs <= 0) {
134 		free(rp);
135 	}
136 }
137 
stage_draw_event(SDL_Event * e,void * arg)138 static bool stage_draw_event(SDL_Event *e, void *arg) {
139 	if(!IS_TAISEI_EVENT(e->type)) {
140 		return false;
141 	}
142 
143 	switch(TAISEI_EVENT(e->type)) {
144 		case TE_FRAME: {
145 			fapproach_p(&stagedraw.clear_screen.alpha, stagedraw.clear_screen.target_alpha, 0.01);
146 			break;
147 		}
148 	}
149 
150 	return false;
151 }
152 
stage_draw_fbpair_create(FBPair * pair,int num_attachments,FBAttachmentConfig * attachments,const StageFramebufferResizeParams * resize_params,const char * name)153 static void stage_draw_fbpair_create(
154 	FBPair *pair,
155 	int num_attachments,
156 	FBAttachmentConfig *attachments,
157 	const StageFramebufferResizeParams *resize_params,
158 	const char *name
159 ) {
160 	StageFramebufferResizeParams *rp = memdup(resize_params, sizeof(*resize_params));
161 	rp->refs = 2;
162 
163 	FramebufferConfig fbconf = { 0 };
164 	fbconf.attachments = attachments;
165 	fbconf.num_attachments = num_attachments;
166 	fbconf.resize_strategy.resize_func = stage_framebuffer_resize_strategy;
167 	fbconf.resize_strategy.userdata = rp;
168 	fbconf.resize_strategy.cleanup_func = stage_framebuffer_resize_strategy_cleanup;
169 	fbmgr_group_fbpair_create(stagedraw.mfb_group, name, &fbconf, pair);
170 }
171 
stage_draw_setup_framebuffers(void)172 static void stage_draw_setup_framebuffers(void) {
173 	FBAttachmentConfig a[2], *a_color, *a_depth;
174 	memset(a, 0, sizeof(a));
175 
176 	a_color = &a[0];
177 	a_depth = &a[1];
178 
179 	a_color->attachment = FRAMEBUFFER_ATTACH_COLOR0;
180 	a_depth->attachment = FRAMEBUFFER_ATTACH_DEPTH;
181 
182 	StageFramebufferResizeParams rp_fg =     { .scaling_base = FBPAIR_FG,     .scale.best = 1, .scale.worst = 1 };
183 	StageFramebufferResizeParams rp_fg_aux = { .scaling_base = FBPAIR_FG_AUX, .scale.best = 1, .scale.worst = 1 };
184 	StageFramebufferResizeParams rp_bg =     { .scaling_base = FBPAIR_BG,     .scale.best = 1, .scale.worst = 1 };
185 	StageFramebufferResizeParams rp_bg_aux = { .scaling_base = FBPAIR_BG_AUX, .scale.best = 1, .scale.worst = 1 };
186 
187 	// Set up some parameters shared by all attachments
188 	TextureParams tex_common = {
189 		.filter.min = TEX_FILTER_LINEAR,
190 		.filter.mag = TEX_FILTER_LINEAR,
191 		.wrap.s = TEX_WRAP_MIRROR,
192 		.wrap.t = TEX_WRAP_MIRROR,
193 	};
194 
195 	memcpy(&a_color->tex_params, &tex_common, sizeof(tex_common));
196 	memcpy(&a_depth->tex_params, &tex_common, sizeof(tex_common));
197 
198 	a_depth->tex_params.type = TEX_TYPE_DEPTH;
199 
200 	// Foreground: 1 RGB texture per FB
201 	a_color->tex_params.type = TEX_TYPE_RGB_16;
202 	stage_draw_fbpair_create(stagedraw.fb_pairs + FBPAIR_FG, 1, a, &rp_fg, "Stage FG");
203 
204 	// Foreground auxiliary: 1 RGBA texture per FB
205 	a_color->tex_params.type = TEX_TYPE_RGBA_8;
206 	stage_draw_fbpair_create(stagedraw.fb_pairs + FBPAIR_FG_AUX, 1, a, &rp_fg_aux, "Stage FG AUX");
207 
208 	// Background: 1 RGB texture + depth per FB
209 	a_color->tex_params.type = TEX_TYPE_RGB_8;
210 	stage_draw_fbpair_create(stagedraw.fb_pairs + FBPAIR_BG, 2, a, &rp_bg, "Stage BG");
211 
212 	// Background auxiliary: 1 RGBA texture per FB
213 	a_color->tex_params.type = TEX_TYPE_RGBA_8;
214 	stage_draw_fbpair_create(stagedraw.fb_pairs + FBPAIR_BG_AUX, 1, a, &rp_bg_aux, "Stage BG AUX");
215 
216 	// CAUTION: should be at least 16-bit, lest the feedback shader do an oopsie!
217 	a_color->tex_params.type = TEX_TYPE_RGBA_16;
218 	stagedraw.powersurge_fbpair.front = stage_add_background_framebuffer("Powersurge effect FB 1", 0.125, 0.25, 1, a);
219 	stagedraw.powersurge_fbpair.back  = stage_add_background_framebuffer("Powersurge effect FB 2", 0.125, 0.25, 1, a);
220 }
221 
add_custom_framebuffer(const char * label,StageFBPair fbtype,float scale_worst,float scale_best,uint num_attachments,FBAttachmentConfig attachments[num_attachments])222 static Framebuffer *add_custom_framebuffer(
223 	const char *label,
224 	StageFBPair fbtype,
225 	float scale_worst,
226 	float scale_best,
227 	uint num_attachments,
228 	FBAttachmentConfig attachments[num_attachments]
229 ) {
230 	StageFramebufferResizeParams rp = { 0 };
231 	rp.scaling_base = fbtype;
232 	rp.scale.worst = scale_worst;
233 	rp.scale.best = scale_best;
234 
235 	FramebufferConfig fbconf = { 0 };
236 	fbconf.attachments = attachments;
237 	fbconf.num_attachments = num_attachments;
238 	fbconf.resize_strategy.resize_func = stage_framebuffer_resize_strategy;
239 	fbconf.resize_strategy.userdata = memdup(&rp, sizeof(rp));
240 	fbconf.resize_strategy.cleanup_func = stage_framebuffer_resize_strategy_cleanup;
241 	return fbmgr_group_framebuffer_create(stagedraw.mfb_group, label, &fbconf);
242 }
243 
stage_add_foreground_framebuffer(const char * label,float scale_worst,float scale_best,uint num_attachments,FBAttachmentConfig attachments[num_attachments])244 Framebuffer* stage_add_foreground_framebuffer(const char *label, float scale_worst, float scale_best, uint num_attachments, FBAttachmentConfig attachments[num_attachments]) {
245 	return add_custom_framebuffer(label, FBPAIR_FG, scale_worst, scale_best, num_attachments, attachments);
246 }
247 
stage_add_background_framebuffer(const char * label,float scale_worst,float scale_best,uint num_attachments,FBAttachmentConfig attachments[num_attachments])248 Framebuffer* stage_add_background_framebuffer(const char *label, float scale_worst, float scale_best, uint num_attachments, FBAttachmentConfig attachments[num_attachments]) {
249 	return add_custom_framebuffer(label, FBPAIR_BG, scale_worst, scale_best, num_attachments, attachments);
250 }
251 
stage_draw_destroy_framebuffers(void)252 static void stage_draw_destroy_framebuffers(void) {
253 	fbmgr_group_destroy(stagedraw.mfb_group);
254 	stagedraw.mfb_group = NULL;
255 }
256 
stage_draw_pre_init(void)257 void stage_draw_pre_init(void) {
258 	stagedraw.mfb_group = fbmgr_group_create();
259 
260 	preload_resources(RES_POSTPROCESS, RESF_OPTIONAL,
261 		"viewport",
262 	NULL);
263 
264 	preload_resources(RES_SPRITE, RESF_PERMANENT,
265 		"hud/heart",
266 		"hud/star",
267 		"star",
268 	NULL);
269 
270 	preload_resources(RES_TEXTURE, RESF_PERMANENT,
271 		"powersurge_flow",
272 		"titletransition",
273 		"hud",
274 	NULL);
275 
276 	preload_resources(RES_MODEL, RESF_PERMANENT,
277 		"hud",
278 	NULL);
279 
280 	preload_resources(RES_SHADER_PROGRAM, RESF_PERMANENT,
281 		"copy_depth",
282 		"ingame_menu",
283 		"powersurge_effect",
284 		"powersurge_feedback",
285 		"sprite_circleclipped_indicator",
286 		"text_hud",
287 		"text_stagetext",
288 
289 		// TODO: Maybe don't preload this if FXAA is disabled.
290 		// But then we also have to handle the edge case of it being enabled later mid-game.
291 		"fxaa",
292 
293 		#ifdef DEBUG
294 		"sprite_filled_circle",
295 		#endif
296 	NULL);
297 
298 	preload_resources(RES_FONT, RESF_PERMANENT,
299 		"mono",
300 		"small",
301 		"monosmall",
302 	NULL);
303 
304 	stagedraw.framerate_graphs = env_get("TAISEI_FRAMERATE_GRAPHS", GRAPHS_DEFAULT);
305 	stagedraw.objpool_stats = env_get("TAISEI_OBJPOOL_STATS", OBJPOOLSTATS_DEFAULT);
306 
307 	if(stagedraw.framerate_graphs) {
308 		preload_resources(RES_SHADER_PROGRAM, RESF_PERMANENT,
309 			"graph",
310 		NULL);
311 	}
312 
313 	if(stagedraw.objpool_stats) {
314 		preload_resources(RES_FONT, RESF_PERMANENT,
315 			"monotiny",
316 		NULL);
317 	}
318 }
319 
stage_draw_init(void)320 void stage_draw_init(void) {
321 	stagedraw.viewport_pp = get_resource_data(RES_POSTPROCESS, "viewport", RESF_OPTIONAL);
322 	stagedraw.hud_text.shader = r_shader_get("text_hud");
323 	stagedraw.hud_text.font = get_font("standard");
324 	stagedraw.shaders.fxaa = r_shader_get("fxaa");
325 	stagedraw.shaders.copy_depth = r_shader_get("copy_depth");
326 
327 	r_shader_standard();
328 
329 	#ifdef DEBUG
330 	stagedraw.dummy.tex = get_sprite("star")->tex;
331 	stagedraw.dummy.w = 1;
332 	stagedraw.dummy.h = 1;
333 	#endif
334 
335 	stage_draw_setup_framebuffers();
336 
337 	stagedraw.clear_screen.alpha = 0;
338 	stagedraw.clear_screen.target_alpha = 0;
339 
340 	events_register_handler(&(EventHandler) {
341 		stage_draw_event, NULL, EPRIO_SYSTEM,
342 	});
343 }
344 
stage_draw_shutdown(void)345 void stage_draw_shutdown(void) {
346 	events_unregister_handler(stage_draw_event);
347 	stage_draw_destroy_framebuffers();
348 }
349 
stage_get_fbpair(StageFBPair id)350 FBPair* stage_get_fbpair(StageFBPair id) {
351 	assert(id >= 0 && id < NUM_FBPAIRS);
352 	return stagedraw.fb_pairs + id;
353 }
354 
stage_draw_collision_areas(void)355 static void stage_draw_collision_areas(void) {
356 #ifdef DEBUG
357 	static bool enabled, keystate_saved;
358 	bool keystate = gamekeypressed(KEY_HITAREAS);
359 
360 	if(keystate ^ keystate_saved) {
361 		if(keystate) {
362 			enabled = !enabled;
363 		}
364 
365 		keystate_saved = keystate;
366 	}
367 
368 	if(!enabled) {
369 		return;
370 	}
371 
372 	r_shader("sprite_filled_circle");
373 	r_uniform_vec4("color_inner", 0, 0, 0, 1);
374 	r_uniform_vec4("color_outer", 1, 1, 1, 0.1);
375 
376 	for(Projectile *p = global.projs.first; p; p = p->next) {
377 		cmplx gsize = projectile_graze_size(p);
378 
379 		if(creal(gsize)) {
380 			r_draw_sprite(&(SpriteParams) {
381 				.color = RGB(0, 0.5, 0.5),
382 				.sprite_ptr = &stagedraw.dummy,
383 				.pos = { creal(p->pos), cimag(p->pos) },
384 				.rotation.angle = p->angle + M_PI/2,
385 				.scale = { .x = creal(gsize), .y = cimag(gsize) },
386 				.blend = BLEND_SUB,
387 			});
388 		}
389 	}
390 
391 	r_flush_sprites();
392 	r_uniform_vec4("color_inner", 0.0, 1.0, 0.0, 0.75);
393 	r_uniform_vec4("color_outer", 0.0, 0.5, 0.5, 0.75);
394 
395 	for(Projectile *p = global.projs.first; p; p = p->next) {
396 		r_draw_sprite(&(SpriteParams) {
397 			.sprite_ptr = &stagedraw.dummy,
398 			.pos = { creal(p->pos), cimag(p->pos) },
399 			.rotation.angle = p->angle + M_PI/2,
400 			.scale = { .x = creal(p->collision_size), .y = cimag(p->collision_size) },
401 			.blend = BLEND_ALPHA,
402 		});
403 	}
404 
405 	for(Enemy *e = global.enemies.first; e; e = e->next) {
406 		if(e->hp > ENEMY_IMMUNE && e->alpha >= 1.0) {
407 			r_draw_sprite(&(SpriteParams) {
408 				.sprite_ptr = &stagedraw.dummy,
409 				.pos = { creal(e->pos), cimag(e->pos) },
410 				.scale = { .x = ENEMY_HURT_RADIUS * 2, .y = ENEMY_HURT_RADIUS * 2 },
411 				.blend = BLEND_ALPHA,
412 			});
413 		}
414 	}
415 
416 	if(global.boss && global.boss->current && !dialog_is_active(global.dialog)) {
417 		r_draw_sprite(&(SpriteParams) {
418 			.sprite_ptr = &stagedraw.dummy,
419 			.pos = { creal(global.boss->pos), cimag(global.boss->pos) },
420 			.scale = { .x = BOSS_HURT_RADIUS * 2, .y = BOSS_HURT_RADIUS * 2 },
421 			.blend = BLEND_ALPHA,
422 		});
423 	}
424 
425 	r_draw_sprite(&(SpriteParams) {
426 		.sprite_ptr = &stagedraw.dummy,
427 		.pos = { creal(global.plr.pos), cimag(global.plr.pos) },
428 		.scale.both = 2, // NOTE: actual player is a singular point
429 	});
430 
431 	// TODO: perhaps handle lasers somehow
432 
433 	r_flush_sprites();
434 #endif
435 }
436 
apply_shader_rules(ShaderRule * shaderrules,FBPair * fbos)437 static void apply_shader_rules(ShaderRule *shaderrules, FBPair *fbos) {
438 	if(!shaderrules) {
439 		return;
440 	}
441 
442 	for(ShaderRule *rule = shaderrules; *rule; ++rule) {
443 		r_framebuffer(fbos->back);
444 
445 		if((*rule)(fbos->front)) {
446 			fbpair_swap(fbos);
447 		}
448 	}
449 
450 	return;
451 }
452 
draw_wall_of_text(float f,const char * txt)453 static void draw_wall_of_text(float f, const char *txt) {
454 	Sprite spr;
455 	BBox bbox;
456 
457 	char buf[strlen(txt) + 4];
458 	memcpy(buf, txt, sizeof(buf) - 4);
459 	memcpy(buf + sizeof(buf) - 4, " ~ ", 4);
460 
461 	text_render(buf, get_font("standard"), &spr, &bbox);
462 
463 	// FIXME: The shader currently assumes that the sprite takes up the entire texture.
464 	// If it could handle any arbitrary sprite, then text_render wouldn't have to resize
465 	// the texture per every new string of text.
466 
467 	float w = VIEWPORT_W;
468 	float h = VIEWPORT_H;
469 
470 	r_mat_mv_push();
471 	r_mat_mv_translate(w/2, h/2, 0);
472 	r_mat_mv_scale(w, h, 1.0);
473 
474 	uint tw, th;
475 	r_texture_get_size(spr.tex, 0, &tw, &th);
476 
477 	r_shader("spellcard_walloftext");
478 	r_uniform_float("w", spr.tex_area.w / tw);
479 	r_uniform_float("h", spr.tex_area.h / th);
480 	r_uniform_float("ratio", h/w);
481 	r_uniform_vec2("origin", creal(global.boss->pos)/h, cimag(global.boss->pos)/w); // what the fuck?
482 	r_uniform_float("t", f);
483 	r_uniform_sampler("tex", spr.tex);
484 	r_draw_quad();
485 	r_shader_standard();
486 
487 	r_mat_mv_pop();
488 }
489 
draw_spellbg(int t)490 static void draw_spellbg(int t) {
491 	Boss *b = global.boss;
492 
493 	r_state_push();
494 	b->current->draw_rule(b, t);
495 	r_state_pop();
496 
497 	if(b->current->type == AT_ExtraSpell) {
498 		draw_extraspell_bg(b, t);
499 	}
500 
501 	float scale = 1;
502 
503 	if(t < 0) {
504 		scale = 1.0 - t/(float)ATTACK_START_DELAY;
505 	}
506 
507 	r_draw_sprite(&(SpriteParams) {
508 		.sprite = "boss_spellcircle0",
509 		.shader = "sprite_default",
510 		.pos = { creal(b->pos), cimag(b->pos) },
511 		.rotation.angle = global.frames * 7.0 * DEG2RAD,
512 		.rotation.vector = { 0, 0, -1 },
513 		.scale.both = scale,
514 	});
515 }
516 
draw_spellbg_overlay(int t)517 static void draw_spellbg_overlay(int t) {
518 	Boss *b = global.boss;
519 
520 	float delay = ATTACK_START_DELAY;
521 
522 	if(b->current->type == AT_ExtraSpell) {
523 		delay = ATTACK_START_DELAY_EXTRA;
524 	}
525 
526 	float f = (ATTACK_START_DELAY - t) / (delay + ATTACK_START_DELAY);
527 
528 	if(f > 0) {
529 		draw_wall_of_text(f, b->current->name);
530 	}
531 }
532 
should_draw_stage_bg(void)533 static inline bool should_draw_stage_bg(void) {
534 	if(!global.boss || !global.boss->current)
535 		return true;
536 
537 	int render_delay = 1.25*ATTACK_START_DELAY; // hand tuned... not ideal
538 	if(global.boss->current->type == AT_ExtraSpell)
539 		render_delay = 0;
540 
541 	return (
542 		!global.boss
543 		|| !global.boss->current
544 		|| !global.boss->current->draw_rule
545 		|| global.boss->current->endtime
546 		|| (global.frames - global.boss->current->starttime) < render_delay
547 	);
548 }
549 
fxaa_rule(Framebuffer * fb)550 static bool fxaa_rule(Framebuffer *fb) {
551 	r_state_push();
552 	r_enable(RCAP_DEPTH_TEST);
553 	r_depth_func(DEPTH_ALWAYS);
554 	r_blend(BLEND_NONE);
555 	r_shader_ptr(stagedraw.shaders.fxaa);
556 	r_uniform_sampler("depth", r_framebuffer_get_attachment(fb, FRAMEBUFFER_ATTACH_DEPTH));
557 	draw_framebuffer_tex(fb, VIEWPORT_W, VIEWPORT_H);
558 	r_state_pop();
559 
560 	return true;
561 }
562 
copydepth_rule(Framebuffer * fb)563 static bool copydepth_rule(Framebuffer *fb) {
564 	r_state_push();
565 	r_enable(RCAP_DEPTH_TEST);
566 	r_depth_func(DEPTH_ALWAYS);
567 	r_blend(BLEND_NONE);
568 	r_shader_ptr(stagedraw.shaders.copy_depth);
569 	draw_framebuffer_attachment(fb, VIEWPORT_W, VIEWPORT_H, FRAMEBUFFER_ATTACH_DEPTH);
570 	r_state_pop();
571 
572 	return false;
573 }
574 
powersurge_draw_predicate(EntityInterface * ent)575 static bool powersurge_draw_predicate(EntityInterface *ent) {
576 	uint layer = ent->draw_layer & ~LAYER_LOW_MASK;
577 
578 	if(player_is_powersurge_active(&global.plr)) {
579 		switch(layer) {
580 			case LAYER_PLAYER_SLAVE:
581 			case LAYER_PLAYER_SHOT:
582 			case LAYER_PLAYER_SHOT_HIGH:
583 			case LAYER_PLAYER_FOCUS:
584 			case LAYER_PLAYER:
585 				return true;
586 		}
587 
588 		if(ent->type == ENT_PROJECTILE) {
589 			Projectile *p = ENT_CAST(ent, Projectile);
590 			return p->flags & PFLAG_PLRSPECIALPARTICLE;
591 		}
592 	}
593 
594 	if(ent->type == ENT_ITEM) {
595 		Item *i = ENT_CAST(ent, Item);
596 		return i->type == ITEM_VOLTAGE;
597 	}
598 
599 	return false;
600 }
601 
draw_powersurge_effect(Framebuffer * target_fb,BlendMode blend)602 static bool draw_powersurge_effect(Framebuffer *target_fb, BlendMode blend) {
603 	r_state_push();
604 	r_blend(BLEND_NONE);
605 	r_disable(RCAP_DEPTH_TEST);
606 
607 	// if(player_is_powersurge_active(&global.plr)) {
608 		r_state_push();
609 		r_framebuffer(stagedraw.powersurge_fbpair.front);
610 		r_blend(BLEND_PREMUL_ALPHA);
611 		r_shader("sprite_default");
612 		ent_draw(powersurge_draw_predicate);
613 		r_state_pop();
614 	// }
615 
616 	// TODO: Add heuristic to not run the effect if the buffer can be reasonably assumed to be empty.
617 
618 	r_shader("powersurge_feedback");
619 	r_uniform_vec2("blur_resolution", 0.5*VIEWPORT_W, 0.5*VIEWPORT_H);
620 
621 	r_framebuffer(stagedraw.powersurge_fbpair.back);
622 	r_uniform_vec2("blur_direction", 1, 0);
623 	r_uniform_vec4("fade", 1, 1, 1, 1);
624 	draw_framebuffer_tex(stagedraw.powersurge_fbpair.front, VIEWPORT_W, VIEWPORT_H);
625 	fbpair_swap(&stagedraw.powersurge_fbpair);
626 
627 	r_framebuffer(stagedraw.powersurge_fbpair.back);
628 	r_uniform_vec2("blur_direction", 0, 1);
629 	r_uniform_vec4("fade", 0.9, 0.9, 0.9, 0.9);
630 	draw_framebuffer_tex(stagedraw.powersurge_fbpair.front, VIEWPORT_W, VIEWPORT_H);
631 
632 	r_framebuffer(target_fb);
633 	r_shader("powersurge_effect");
634 	r_uniform_sampler("shotlayer", r_framebuffer_get_attachment(stagedraw.powersurge_fbpair.back, FRAMEBUFFER_ATTACH_COLOR0));
635 	r_uniform_sampler("flowlayer", "powersurge_flow");
636 	r_uniform_float("time", global.frames/60.0);
637 	r_blend(blend);
638 	r_cull(CULL_BACK);
639 	r_mat_mv_push();
640 	r_mat_mv_scale(VIEWPORT_W, VIEWPORT_H, 1);
641 	r_mat_mv_translate(0.5, 0.5, 0);
642 	r_draw_quad();
643 	r_mat_mv_pop();
644 	fbpair_swap(&stagedraw.powersurge_fbpair);
645 
646 	r_state_pop();
647 
648 	return true;
649 }
650 
boss_distortion_rule(Framebuffer * fb)651 static bool boss_distortion_rule(Framebuffer *fb) {
652 	if(global.boss == NULL) {
653 		return false;
654 	}
655 
656 	r_state_push();
657 	r_blend(BLEND_NONE);
658 	r_disable(RCAP_DEPTH_TEST);
659 
660 	cmplx fpos = global.boss->pos;
661 	cmplx pos = fpos + 15*cexp(I*global.frames/4.5);
662 
663 	r_shader("boss_zoom");
664 	r_uniform_vec2("blur_orig", creal(pos)  / VIEWPORT_W,  1-cimag(pos)  / VIEWPORT_H);
665 	r_uniform_vec2("fix_orig",  creal(fpos) / VIEWPORT_W,  1-cimag(fpos) / VIEWPORT_H);
666 	r_uniform_float("blur_rad", 1.5*(0.2+0.025*sin(global.frames/15.0)));
667 	r_uniform_float("rad", 0.24);
668 	r_uniform_float("ratio", (float)VIEWPORT_H/VIEWPORT_W);
669 	r_uniform_vec4_rgba("color", &global.boss->zoomcolor);
670 	draw_framebuffer_tex(fb, VIEWPORT_W, VIEWPORT_H);
671 
672 	r_state_pop();
673 
674 	return true;
675 }
676 
finish_3d_scene(FBPair * fbpair)677 static void finish_3d_scene(FBPair *fbpair) {
678 	// Here we synchronize the depth buffers of both framebuffers in the pair.
679 	// The FXAA shader has this built-in, so we don't need to do the copy_depth
680 	// pass in that case.
681 
682 	// The reason for this is that, normally, depth testing is disabled during
683 	// the stage-specific post-processing ping-pong. This applies to depth writes
684 	// as well. Therefore, only one of the framebuffers will get a depth write in
685 	// a given frame. In the worst case, this will be the same framebuffer every
686 	// time, leaving the other one's depth buffer undefined. In the best case,
687 	// they will alternate. If a shader down the post-processing pipeline happens
688 	// to sample the "unlucky" buffer, it'll probably either completely destroy
689 	// the background rendering, or the sample will lag 1 frame behind.
690 
691 	// This flew past the radar for a long time. Now that I actually had to waste
692 	// my evening debugging this peculiarity, I figured I might as well document
693 	// it here. It is possible to solve this problem without an additional pass,
694 	// but that would require a bit of refactoring. This is the simplest solution
695 	// as far as I can tell.
696 
697 	apply_shader_rules((ShaderRule[]) {
698 		config_get_int(CONFIG_FXAA)
699 			? fxaa_rule
700 			: copydepth_rule,
701 		NULL
702 	}, fbpair);
703 }
704 
draw_full_spellbg(int t,FBPair * fbos)705 static void draw_full_spellbg(int t, FBPair *fbos) {
706 	BlendMode blend_old = r_blend_current();
707 	r_framebuffer(fbos->back);
708 	r_blend(BLEND_PREMUL_ALPHA);
709 	draw_spellbg(t);
710 	fbpair_swap(fbos);
711 	r_blend(BLEND_NONE);
712 	apply_shader_rules((ShaderRule[]) { boss_distortion_rule, NULL }, fbos);
713 	r_blend(BLEND_PREMUL_ALPHA);
714 	draw_spellbg_overlay(t);
715 	r_blend(blend_old);
716 }
717 
apply_bg_shaders(ShaderRule * shaderrules,FBPair * fbos)718 static void apply_bg_shaders(ShaderRule *shaderrules, FBPair *fbos) {
719 	Boss *b = global.boss;
720 
721 	r_state_push();
722 	r_blend(BLEND_NONE);
723 
724 	if(should_draw_stage_bg()) {
725 		finish_3d_scene(fbos);
726 		apply_shader_rules(shaderrules, fbos);
727 	}
728 
729 	if(b && b->current && b->current->draw_rule) {
730 		int t = global.frames - b->current->starttime;
731 
732 		bool trans_intro = t < ATTACK_START_DELAY;
733 		bool trans_outro = b->current->endtime;
734 
735 		if(!trans_intro && !trans_outro) {
736 			draw_full_spellbg(t, fbos);
737 		} else {
738 			FBPair *aux = stage_get_fbpair(FBPAIR_BG_AUX);
739 			draw_full_spellbg(t, aux);
740 
741 			apply_shader_rules((ShaderRule[]) { boss_distortion_rule, NULL }, fbos);
742 			fbpair_swap(fbos);
743 			r_framebuffer(fbos->back);
744 
745 			cmplx pos = b->pos;
746 			float ratio = (float)VIEWPORT_H/VIEWPORT_W;
747 			float delay;
748 
749 			if(trans_intro) {
750 				float duration = ATTACK_START_DELAY_EXTRA;
751 
752 				if(b->current->type == AT_ExtraSpell) {
753 					delay = ATTACK_START_DELAY_EXTRA;
754 				} else {
755 					delay = ATTACK_START_DELAY;
756 				}
757 
758 				r_shader("spellcard_intro");
759 				r_uniform_float("ratio", ratio);
760 				r_uniform_vec2("origin", creal(pos) / VIEWPORT_W, 1 - cimag(pos) / VIEWPORT_H);
761 				r_uniform_float("t", (t + delay) / duration);
762 			} else {
763 				int tn = global.frames - b->current->endtime;
764 				delay = b->current->endtime - b->current->endtime_undelayed;
765 
766 				r_shader("spellcard_outro");
767 				r_uniform_float("ratio", ratio);
768 				r_uniform_vec2("origin", creal(pos) / VIEWPORT_W, 1 - cimag(pos) / VIEWPORT_H);
769 				r_uniform_float("t", max(0, tn / delay + 1));
770 			}
771 
772 			r_blend(BLEND_PREMUL_ALPHA);
773 			draw_framebuffer_tex(aux->front, VIEWPORT_W, VIEWPORT_H);
774 			r_blend(BLEND_NONE);
775 			fbpair_swap(fbos);
776 		}
777 	} else {
778 		apply_shader_rules((ShaderRule[]) { boss_distortion_rule, NULL }, fbos);
779 	}
780 
781 	r_state_pop();
782 }
783 
stage_render_bg(StageInfo * stage)784 static void stage_render_bg(StageInfo *stage) {
785 	FBPair *background = stage_get_fbpair(FBPAIR_BG);
786 
787 	r_framebuffer(background->back);
788 	r_clear(CLEAR_ALL, RGBA(0, 0, 0, 1), 1);
789 
790 	if(should_draw_stage_bg()) {
791 		r_mat_mv_push();
792 		r_enable(RCAP_DEPTH_TEST);
793 		stage->procs->draw();
794 		r_mat_mv_pop();
795 		fbpair_swap(background);
796 	}
797 
798 	set_ortho(VIEWPORT_W, VIEWPORT_H);
799 	r_disable(RCAP_DEPTH_TEST);
800 
801 	apply_bg_shaders(stage->procs->shader_rules, background);
802 
803 	int pp = config_get_int(CONFIG_POSTPROCESS);
804 
805 	if(pp > 1) {
806 		draw_powersurge_effect(background->front, BLEND_PREMUL_ALPHA);
807 	} else if(pp > 0) {
808 		Framebuffer *staging = stage_get_fbpair(FBPAIR_BG_AUX)->back;
809 
810 		r_state_push();
811 		r_framebuffer_clear(staging, CLEAR_COLOR, RGBA(0, 0, 0, 0), 1);
812 		draw_powersurge_effect(staging, BLEND_NONE);
813 		r_shader_standard();
814 		r_framebuffer(background->front);
815 		r_blend(BLEND_PREMUL_ALPHA);
816 		draw_framebuffer_tex(staging, VIEWPORT_W, VIEWPORT_H);
817 		r_state_pop();
818 	}
819 }
820 
stage_should_draw_particle(Projectile * p)821 bool stage_should_draw_particle(Projectile *p) {
822 	return (p->flags & PFLAG_REQUIREDPARTICLE) || config_get_int(CONFIG_PARTICLES);
823 }
824 
stage_draw_predicate(EntityInterface * ent)825 static bool stage_draw_predicate(EntityInterface *ent) {
826 	if(ent->type == ENT_PROJECTILE) {
827 		Projectile *p = ENT_CAST(ent, Projectile);
828 
829 		if(p->type == PROJ_PARTICLE) {
830 			return stage_should_draw_particle(p);
831 		}
832 	}
833 
834 	return true;
835 }
836 
stage_draw_objects(void)837 static void stage_draw_objects(void) {
838 	r_shader("sprite_default");
839 
840 	if(global.boss) {
841 		draw_boss_background(global.boss);
842 	}
843 
844 	ent_draw(
845 		config_get_int(CONFIG_PARTICLES)
846 			? NULL
847 			: stage_draw_predicate
848 	);
849 
850 	if(global.boss) {
851 		draw_boss_fake_overlay(global.boss);
852 	}
853 
854 	stage_draw_collision_areas();
855 	r_shader_standard();
856 }
857 
stage_draw_overlay(void)858 void stage_draw_overlay(void) {
859 	r_state_push();
860 	r_shader("sprite_default");
861 	r_blend(BLEND_PREMUL_ALPHA);
862 
863 	if(global.boss) {
864 		draw_boss_overlay(global.boss);
865 	}
866 
867 	if(stagedraw.clear_screen.alpha > 0) {
868 		fade_out(stagedraw.clear_screen.alpha * 0.5);
869 	}
870 
871 	r_shader_standard();
872 	stagetext_draw();
873 	player_draw_overlay(&global.plr);
874 	r_state_pop();
875 }
876 
postprocess_prepare(Framebuffer * fb,ShaderProgram * s,void * arg)877 static void postprocess_prepare(Framebuffer *fb, ShaderProgram *s, void *arg) {
878 	r_uniform_int("frames", global.frames);
879 	r_uniform_vec2("viewport", VIEWPORT_W, VIEWPORT_H);
880 	r_uniform_vec2("player", creal(global.plr.pos), VIEWPORT_H - cimag(global.plr.pos));
881 }
882 
begin_viewport_shake(void)883 static inline void begin_viewport_shake(void) {
884 	if(global.shake_view) {
885 		r_mat_mv_push();
886 		r_mat_mv_translate(
887 			global.shake_view * sin(global.frames),
888 			global.shake_view * sin(global.frames * 1.1 + 3),
889 			0
890 		);
891 		r_mat_mv_scale(
892 			1 + 2 * global.shake_view / VIEWPORT_W,
893 			1 + 2 * global.shake_view / VIEWPORT_H,
894 			1
895 		);
896 		r_mat_mv_translate(
897 			-global.shake_view,
898 			-global.shake_view,
899 			0
900 		);
901 	}
902 }
903 
end_viewport_shake(void)904 static inline void end_viewport_shake(void) {
905 	if(global.shake_view) {
906 		r_mat_mv_pop();
907 	}
908 }
909 
910 /*
911  * Small helpers for entities draw code that might want to suppress viewport shake temporarily.
912  * This is mostly useful when multiple framebuffers are involved.
913  */
914 static int shake_suppressed = 0;
915 
stage_draw_begin_noshake(void)916 void stage_draw_begin_noshake(void) {
917 	assert(!shake_suppressed);
918 	shake_suppressed = 1;
919 
920 	if(global.shake_view) {
921 		shake_suppressed = 2;
922 		r_mat_mv_push_identity();
923 	}
924 }
925 
stage_draw_end_noshake(void)926 void stage_draw_end_noshake(void) {
927 	assert(shake_suppressed);
928 
929 	if(global.shake_view) {
930 		// make sure shake_view doesn't change in-between the begin/end calls;
931 		// that would've been *really* nasty to debug.
932 		assert(shake_suppressed == 2);
933 		r_mat_mv_pop();
934 	}
935 
936 	shake_suppressed = 0;
937 }
938 
stage_draw_viewport(void)939 void stage_draw_viewport(void) {
940 	FloatRect dest_vp;
941 	r_framebuffer_viewport_current(r_framebuffer_current(), &dest_vp);
942 	r_uniform_sampler("tex", r_framebuffer_get_attachment(stagedraw.fb_pairs[FBPAIR_FG].front, FRAMEBUFFER_ATTACH_COLOR0));
943 
944 	// CAUTION: Very intricate pixel perfect scaling that will ruin your day.
945 	float facw = dest_vp.w / SCREEN_W;
946 	float fach = dest_vp.h / SCREEN_H;
947 	// fach is equal to facw up to roundoff error.
948 	float scale = fach;
949 
950 	r_mat_mv_push();
951 	r_mat_mv_scale(1/facw, 1/fach, 1);
952 	r_mat_mv_translate(roundf(facw * VIEWPORT_X), roundf(fach * VIEWPORT_Y), 0);
953 	r_mat_mv_scale(roundf(scale * VIEWPORT_W), roundf(scale * VIEWPORT_H), 1);
954 	r_mat_mv_translate(0.5, 0.5, 0);
955 	r_draw_quad();
956 	r_mat_mv_pop();
957 }
958 
stage_draw_scene(StageInfo * stage)959 void stage_draw_scene(StageInfo *stage) {
960 #ifdef DEBUG
961 	bool key_nobg = gamekeypressed(KEY_NOBACKGROUND);
962 #else
963 	bool key_nobg = false;
964 #endif
965 
966 	FBPair *background = stage_get_fbpair(FBPAIR_BG);
967 	FBPair *foreground = stage_get_fbpair(FBPAIR_FG);
968 
969 	bool draw_bg = !config_get_int(CONFIG_NO_STAGEBG) && !key_nobg;
970 
971 	if(draw_bg) {
972 		stage_render_bg(stage);
973 	}
974 
975 	// prepare for 2D rendering into the game viewport framebuffer
976 	r_framebuffer(foreground->back);
977 	set_ortho(VIEWPORT_W, VIEWPORT_H);
978 	r_disable(RCAP_DEPTH_TEST);
979 	r_blend(BLEND_PREMUL_ALPHA);
980 	r_cull(CULL_BACK);
981 	r_shader_standard();
982 
983 	begin_viewport_shake();
984 
985 	if(draw_bg) {
986 		// blit the background
987 		r_state_push();
988 		r_blend(BLEND_NONE);
989 		draw_framebuffer_tex(background->front, VIEWPORT_W, VIEWPORT_H);
990 		r_state_pop();
991 
992 		// draw bomb background
993 		// FIXME: we need a more flexible and consistent way for entities to hook
994 		// into the various stages of scene drawing code.
995 		if(global.plr.mode->procs.bombbg /*&& player_is_bomb_active(&global.plr)*/) {
996 			global.plr.mode->procs.bombbg(&global.plr);
997 		}
998 	} else if(!key_nobg) {
999 		r_clear(CLEAR_COLOR, RGBA(0, 0, 0, 1), 1);
1000 	}
1001 
1002 	// draw the 2D objects
1003 	stage_draw_objects();
1004 
1005 	end_viewport_shake();
1006 
1007 	// prepare to apply postprocessing
1008 	fbpair_swap(foreground);
1009 	r_blend(BLEND_NONE);
1010 
1011 	// bomb effects shader if present and player bombing
1012 	if(global.plr.mode->procs.bomb_shader && player_is_bomb_active(&global.plr)) {
1013 		ShaderRule rules[] = { global.plr.mode->procs.bomb_shader, NULL };
1014 		apply_shader_rules(rules, foreground);
1015 	}
1016 
1017 	// draw overlay: in-viewport text and HUD elements, etc.
1018 	// this stuff is not affected by the screen shake effect
1019 	stage_draw_overlay();
1020 
1021 	// stage postprocessing
1022 	apply_shader_rules(global.stage->procs->postprocess_rules, foreground);
1023 
1024 	// custom postprocessing
1025 	postprocess(
1026 		stagedraw.viewport_pp,
1027 		foreground,
1028 		postprocess_prepare,
1029 		draw_framebuffer_tex,
1030 		VIEWPORT_W,
1031 		VIEWPORT_H,
1032 		NULL
1033 	);
1034 
1035 	// prepare for 2D rendering into the main framebuffer (actual screen)
1036 	r_framebuffer(video_get_screen_framebuffer());
1037 	set_ortho(SCREEN_W, SCREEN_H);
1038 
1039 	// draw viewport contents
1040 	stage_draw_viewport();
1041 
1042 	// draw HUD
1043 	stage_draw_hud();
1044 
1045 	// draw dialog
1046 	dialog_draw(global.dialog);
1047 
1048 	// draw "bottom text" (FPS, replay info, etc.)
1049 	stage_draw_bottom_text();
1050 }
1051 
1052 #define HUD_X_PADDING 16
1053 #define HUD_X_OFFSET (VIEWPORT_W + VIEWPORT_X)
1054 #define HUD_WIDTH (SCREEN_W - HUD_X_OFFSET)
1055 #define HUD_EFFECTIVE_WIDTH (HUD_WIDTH - HUD_X_PADDING * 2)
1056 #define HUD_X_SECONDARY_OFS_ICON 18
1057 #define HUD_X_SECONDARY_OFS_LABEL (HUD_X_SECONDARY_OFS_ICON + 12)
1058 #define HUD_X_SECONDARY_OFS_VALUE (HUD_X_SECONDARY_OFS_LABEL + 60)
1059 
1060 struct glyphcb_state {
1061 	Color *color1, *color2;
1062 };
1063 
draw_numeric_callback(Font * font,charcode_t charcode,SpriteInstanceAttribs * spr_attribs,void * userdata)1064 static int draw_numeric_callback(Font *font, charcode_t charcode, SpriteInstanceAttribs *spr_attribs, void *userdata) {
1065 	struct glyphcb_state *st = userdata;
1066 
1067 	if(charcode != '0' && charcode != ',') {
1068 		st->color1 = st->color2;
1069 	}
1070 
1071 	spr_attribs->rgba = *st->color1;
1072 	return 0;
1073 }
1074 
stage_draw_hud_power_value(float xpos,float ypos)1075 static inline void stage_draw_hud_power_value(float xpos, float ypos) {
1076 	Font *fnt_int = get_font("standard");
1077 	Font *fnt_fract = get_font("small");
1078 
1079 	int pw = global.plr.power + global.plr.power_overflow;
1080 
1081 	Color *c_whole, c_whole_buf, *c_fract, c_fract_buf;
1082 	Color *c_op_mod = RGBA(1, 0.2 + 0.3 * psin(global.frames / 10.0), 0.2, 1.0);
1083 
1084 	if(pw <= PLR_MAX_POWER) {
1085 		c_whole = &stagedraw.hud_text.color.active;
1086 		c_fract = &stagedraw.hud_text.color.inactive;
1087 	} else if(pw - PLR_MAX_POWER < 100) {
1088 		c_whole = &stagedraw.hud_text.color.active;
1089 		c_fract = color_mul(color_copy(&c_fract_buf, &stagedraw.hud_text.color.inactive), c_op_mod);
1090 	} else {
1091 		c_whole = color_mul(color_copy(&c_whole_buf, &stagedraw.hud_text.color.active), c_op_mod);
1092 		c_fract = color_mul(color_copy(&c_fract_buf, &stagedraw.hud_text.color.inactive), c_op_mod);
1093 	}
1094 
1095 	xpos = draw_fraction(
1096 		pw / 100.0,
1097 		ALIGN_LEFT,
1098 		xpos,
1099 		ypos,
1100 		fnt_int,
1101 		fnt_fract,
1102 		c_whole,
1103 		c_fract,
1104 		false
1105 	);
1106 
1107 	xpos += text_draw(" / ", &(TextParams) {
1108 		.pos = { xpos, ypos },
1109 		.color = &stagedraw.hud_text.color.active,
1110 		.align = ALIGN_LEFT,
1111 		.font_ptr = fnt_int,
1112 	});
1113 
1114 	draw_fraction(
1115 		PLR_MAX_POWER / 100.0,
1116 		ALIGN_LEFT,
1117 		xpos,
1118 		ypos,
1119 		fnt_int,
1120 		fnt_fract,
1121 		&stagedraw.hud_text.color.active,
1122 		&stagedraw.hud_text.color.inactive,
1123 		false
1124 	);
1125 }
1126 
stage_draw_hud_score(Alignment a,float xpos,float ypos,char * buf,size_t bufsize,uint32_t score)1127 static void stage_draw_hud_score(Alignment a, float xpos, float ypos, char *buf, size_t bufsize, uint32_t score) {
1128 	format_huge_num(10, score, bufsize, buf);
1129 
1130 	Font *fnt = get_font("standard");
1131 	bool kern_saved = font_get_kerning_enabled(fnt);
1132 	font_set_kerning_enabled(fnt, false);
1133 
1134 	text_draw(buf, &(TextParams) {
1135 		.pos = { xpos, ypos },
1136 		.font = "standard",
1137 		.align = ALIGN_RIGHT,
1138 		.glyph_callback = {
1139 			draw_numeric_callback,
1140 			&(struct glyphcb_state) { &stagedraw.hud_text.color.inactive, &stagedraw.hud_text.color.active },
1141 		}
1142 	});
1143 
1144 	font_set_kerning_enabled(fnt, kern_saved);
1145 }
1146 
stage_draw_hud_scores(float ypos_hiscore,float ypos_score,char * buf,size_t bufsize)1147 static void stage_draw_hud_scores(float ypos_hiscore, float ypos_score, char *buf, size_t bufsize) {
1148 	stage_draw_hud_score(ALIGN_RIGHT, HUD_EFFECTIVE_WIDTH, ypos_hiscore, buf, bufsize, progress.hiscore);
1149 	stage_draw_hud_score(ALIGN_RIGHT, HUD_EFFECTIVE_WIDTH, ypos_score,   buf, bufsize, global.plr.points);
1150 }
1151 
stage_draw_hud_objpool_stats(float x,float y,float width)1152 static void stage_draw_hud_objpool_stats(float x, float y, float width) {
1153 	ObjectPool **last = &stage_object_pools.first + (sizeof(StageObjectPools)/sizeof(ObjectPool*) - 1);
1154 	Font *font = get_font("monotiny");
1155 
1156 	ShaderProgram *sh_prev = r_shader_current();
1157 	r_shader("text_default");
1158 	for(ObjectPool **pool = &stage_object_pools.first; pool <= last; ++pool) {
1159 		ObjectPoolStats stats;
1160 		char buf[32];
1161 		objpool_get_stats(*pool, &stats);
1162 
1163 		snprintf(buf, sizeof(buf), "%zu | %5zu", stats.usage, stats.peak_usage);
1164 		// draw_text(ALIGN_LEFT  | AL_Flag_NoAdjust, (int)x,           (int)y, stats.tag, font);
1165 		// draw_text(ALIGN_RIGHT | AL_Flag_NoAdjust, (int)(x + width), (int)y, buf,       font);
1166 		// y += stringheight(buf, font) * 1.1;
1167 
1168 		text_draw(stats.tag, &(TextParams) {
1169 			.pos = { x, y },
1170 			.font_ptr = font,
1171 			.align = ALIGN_LEFT,
1172 		});
1173 
1174 		text_draw(buf, &(TextParams) {
1175 			.pos = { x + width, y },
1176 			.font_ptr = font,
1177 			.align = ALIGN_RIGHT,
1178 		});
1179 
1180 		y += font_get_lineskip(font);
1181 	}
1182 	r_shader_ptr(sh_prev);
1183 }
1184 
1185 struct labels_s {
1186 	struct {
1187 		float next_life;
1188 		float next_bomb;
1189 	} x;
1190 
1191 	struct {
1192 		float hiscore;
1193 		float score;
1194 		float lives;
1195 		float bombs;
1196 		float power;
1197 		float value;
1198 		float voltage;
1199 		float graze;
1200 	} y;
1201 
1202 	struct {
1203 		float lives_display;
1204 		float lives_text;
1205 		float bombs_display;
1206 		float bombs_text;
1207 	} y_ofs;
1208 
1209 	Color lb_baseclr;
1210 };
1211 
draw_graph(float x,float y,float w,float h)1212 static void draw_graph(float x, float y, float w, float h) {
1213 	r_mat_mv_push();
1214 	r_mat_mv_translate(x + w/2, y + h/2, 0);
1215 	r_mat_mv_scale(w, h, 1);
1216 	r_draw_quad();
1217 	r_mat_mv_pop();
1218 }
1219 
draw_label(const char * label_str,double y_ofs,struct labels_s * labels,Color * clr)1220 static void draw_label(const char *label_str, double y_ofs, struct labels_s* labels, Color *clr) {
1221 	text_draw(label_str, &(TextParams) {
1222 		.font_ptr = stagedraw.hud_text.font,
1223 		.shader_ptr = stagedraw.hud_text.shader,
1224 		.pos = { 0, y_ofs },
1225 		.color = clr,
1226 	});
1227 }
1228 
stage_draw_hud_text(struct labels_s * labels)1229 static void stage_draw_hud_text(struct labels_s* labels) {
1230 	char buf[64];
1231 	Font *font;
1232 	bool kern_saved;
1233 
1234 	r_shader_ptr(stagedraw.hud_text.shader);
1235 
1236 	Color *lb_label_clr = color_mul(COLOR_COPY(&labels->lb_baseclr), &stagedraw.hud_text.color.label);
1237 
1238 	// Labels
1239 	draw_label("Hi-Score:",    labels->y.hiscore, labels, &stagedraw.hud_text.color.label);
1240 	draw_label("Score:",       labels->y.score,   labels, &stagedraw.hud_text.color.label);
1241 	draw_label("Lives:",       labels->y.lives,   labels, lb_label_clr);
1242 	draw_label("Spell Cards:", labels->y.bombs,   labels, lb_label_clr);
1243 
1244 	r_mat_mv_push();
1245 	r_mat_mv_translate(HUD_X_SECONDARY_OFS_LABEL, 0, 0);
1246 	draw_label("Power:",       labels->y.power,   labels, &stagedraw.hud_text.color.label_power);
1247 	draw_label("Value:",       labels->y.value,   labels, &stagedraw.hud_text.color.label_value);
1248 	draw_label("Volts:",       labels->y.voltage, labels, &stagedraw.hud_text.color.label_voltage);
1249 	draw_label("Graze:",       labels->y.graze,   labels, &stagedraw.hud_text.color.label_graze);
1250 	r_mat_mv_pop();
1251 
1252 	if(stagedraw.objpool_stats) {
1253 		stage_draw_hud_objpool_stats(0, 390, HUD_EFFECTIVE_WIDTH);
1254 	}
1255 
1256 	// Score/Hi-Score values
1257 	stage_draw_hud_scores(labels->y.hiscore, labels->y.score, buf, sizeof(buf));
1258 
1259 	// Lives and Bombs (N/A)
1260 	if(global.stage->type == STAGE_SPELL) {
1261 		r_color(color_mul_scalar(COLOR_COPY(&labels->lb_baseclr), 0.7));
1262 		text_draw("N/A", &(TextParams) { .pos = { HUD_EFFECTIVE_WIDTH, labels->y.lives }, .font_ptr = stagedraw.hud_text.font, .align = ALIGN_RIGHT });
1263 		text_draw("N/A", &(TextParams) { .pos = { HUD_EFFECTIVE_WIDTH, labels->y.bombs }, .font_ptr = stagedraw.hud_text.font, .align = ALIGN_RIGHT });
1264 		r_color4(1, 1, 1, 1.0);
1265 	}
1266 
1267 	const float res_text_padding = 4;
1268 
1269 	// Score left to next extra life
1270 	if(labels->x.next_life > 0) {
1271 		Color *next_clr = color_mul(RGBA(0.5, 0.3, 0.4, 0.5), &labels->lb_baseclr);
1272 		format_huge_num(0, global.plr.extralife_threshold - global.plr.points, sizeof(buf), buf);
1273 		font = get_font("small");
1274 
1275 		text_draw("Next:", &(TextParams) {
1276 			.pos = { labels->x.next_life + res_text_padding, labels->y.lives + labels->y_ofs.lives_text },
1277 			.font_ptr = font,
1278 			.align = ALIGN_LEFT,
1279 			.color = next_clr,
1280 		});
1281 
1282 		kern_saved = font_get_kerning_enabled(font);
1283 		font_set_kerning_enabled(font, false);
1284 
1285 		text_draw(buf, &(TextParams) {
1286 			.pos = { HUD_EFFECTIVE_WIDTH - res_text_padding, labels->y.lives + labels->y_ofs.lives_text },
1287 			.font_ptr = font,
1288 			.align = ALIGN_RIGHT,
1289 			.color = next_clr,
1290 		});
1291 
1292 		font_set_kerning_enabled(font, kern_saved);
1293 	}
1294 
1295 	// Bomb fragments (numeric)
1296 	if(labels->x.next_bomb > 0) {
1297 		Color *next_clr = color_mul(RGBA(0.3, 0.5, 0.3, 0.5), &labels->lb_baseclr);
1298 		snprintf(buf, sizeof(buf), "%d / %d", global.plr.bomb_fragments, PLR_MAX_BOMB_FRAGMENTS);
1299 		font = get_font("small");
1300 
1301 		text_draw("Fragments:", &(TextParams) {
1302 			.pos = { labels->x.next_bomb + res_text_padding, labels->y.bombs + labels->y_ofs.bombs_text },
1303 			.font_ptr = font,
1304 			.align = ALIGN_LEFT,
1305 			.color = next_clr,
1306 		});
1307 
1308 		kern_saved = font_get_kerning_enabled(font);
1309 		font_set_kerning_enabled(font, false);
1310 
1311 		text_draw(buf, &(TextParams) {
1312 			.pos = { HUD_EFFECTIVE_WIDTH - res_text_padding, labels->y.bombs + labels->y_ofs.bombs_text },
1313 			.font_ptr = font,
1314 			.align = ALIGN_RIGHT,
1315 			.color = next_clr,
1316 		});
1317 
1318 		font_set_kerning_enabled(font, kern_saved);
1319 	}
1320 
1321 	r_mat_mv_push();
1322 	r_mat_mv_translate(HUD_X_SECONDARY_OFS_VALUE, 0, 0);
1323 
1324 	// Power value
1325 	stage_draw_hud_power_value(0, labels->y.power);
1326 
1327 	font = get_font("standard");
1328 	kern_saved = font_get_kerning_enabled(font);
1329 	font_set_kerning_enabled(font, false);
1330 
1331 	// Point Item Value... value
1332 	format_huge_num(6, global.plr.point_item_value, sizeof(buf), buf);
1333 	text_draw(buf, &(TextParams) {
1334 		.pos = { 0, labels->y.value },
1335 		.shader_ptr = stagedraw.hud_text.shader,
1336 		.font_ptr = font,
1337 		.glyph_callback = {
1338 			draw_numeric_callback,
1339 			&(struct glyphcb_state) { &stagedraw.hud_text.color.inactive, &stagedraw.hud_text.color.active },
1340 		}
1341 	});
1342 
1343 	// Voltage value
1344 	format_huge_num(4, global.plr.voltage, sizeof(buf), buf);
1345 	float volts_x = 0;
1346 
1347 	Color *voltage_tint = global.plr.voltage >= global.voltage_threshold
1348 		? RGB(1.0, 0.9, 0.7) // RGB(0.9, 0.7, 1.0)
1349 		: RGB(1.0, 1.0, 1.0);
1350 
1351 	volts_x += text_draw(buf, &(TextParams) {
1352 		.pos = { volts_x, labels->y.voltage },
1353 		.shader_ptr = stagedraw.hud_text.shader,
1354 		.font_ptr = font,
1355 		.glyph_callback = {
1356 			draw_numeric_callback,
1357 			&(struct glyphcb_state) {
1358 				color_mul(COLOR_COPY(&stagedraw.hud_text.color.inactive), voltage_tint),
1359 				color_mul(COLOR_COPY(&stagedraw.hud_text.color.active), voltage_tint),
1360 			},
1361 		}
1362 	});
1363 
1364 	volts_x += text_draw("V", &(TextParams) {
1365 		.pos = { volts_x, labels->y.voltage },
1366 		.shader_ptr = stagedraw.hud_text.shader,
1367 		.font_ptr = font,
1368 		.color = &stagedraw.hud_text.color.dark,
1369 	});
1370 
1371 	volts_x += text_draw(" / ", &(TextParams) {
1372 		.pos = { volts_x, labels->y.voltage },
1373 		.shader_ptr = stagedraw.hud_text.shader,
1374 		.font_ptr = font,
1375 		.color = &stagedraw.hud_text.color.active,
1376 	});
1377 
1378 	format_huge_num(4, global.voltage_threshold, sizeof(buf), buf);
1379 
1380 	volts_x += text_draw(buf, &(TextParams) {
1381 		.pos = { volts_x, labels->y.voltage },
1382 		.shader_ptr = stagedraw.hud_text.shader,
1383 		.font_ptr = font,
1384 		.glyph_callback = {
1385 			draw_numeric_callback,
1386 			&(struct glyphcb_state) { &stagedraw.hud_text.color.inactive, &stagedraw.hud_text.color.active },
1387 		}
1388 	});
1389 
1390 	volts_x += text_draw("V", &(TextParams) {
1391 		.pos = { volts_x, labels->y.voltage },
1392 		.shader_ptr = stagedraw.hud_text.shader,
1393 		.font_ptr = font,
1394 		.color = &stagedraw.hud_text.color.dark,
1395 	});
1396 
1397 	// Graze value
1398 	format_huge_num(6, global.plr.graze, sizeof(buf), buf);
1399 	text_draw(buf, &(TextParams) {
1400 		.pos = { 0, labels->y.graze },
1401 		.shader_ptr = stagedraw.hud_text.shader,
1402 		.font_ptr = font,
1403 		.glyph_callback = {
1404 			draw_numeric_callback,
1405 			&(struct glyphcb_state) { &stagedraw.hud_text.color.inactive, &stagedraw.hud_text.color.active  },
1406 		}
1407 	});
1408 
1409 	font_set_kerning_enabled(font, kern_saved);
1410 	r_mat_mv_pop();
1411 
1412 	// God Mode indicator
1413 	if(global.plr.iddqd) {
1414 		text_draw("God Mode is enabled!", &(TextParams) {
1415 			.pos = { HUD_EFFECTIVE_WIDTH * 0.5, 450 },
1416 			.font_ptr = font,
1417 			.shader_ptr = stagedraw.hud_text.shader,
1418 			.align = ALIGN_CENTER,
1419 			.color = RGB(1.0, 0.5, 0.2),
1420 		});
1421 	}
1422 }
1423 
stage_draw_bottom_text(void)1424 void stage_draw_bottom_text(void) {
1425 	char buf[64];
1426 	Font *font;
1427 
1428 #ifdef DEBUG
1429 	snprintf(buf, sizeof(buf), "%.2f lfps, %.2f rfps, timer: %d, frames: %d",
1430 		global.fps.logic.fps,
1431 		global.fps.render.fps,
1432 		global.timer,
1433 		global.frames
1434 	);
1435 #else
1436 	if(get_effective_frameskip() > 1) {
1437 		snprintf(buf, sizeof(buf), "%.2f lfps, %.2f rfps",
1438 			global.fps.logic.fps,
1439 			global.fps.render.fps
1440 		);
1441 	} else {
1442 		snprintf(buf, sizeof(buf), "%.2f fps",
1443 			global.fps.logic.fps
1444 		);
1445 	}
1446 #endif
1447 
1448 	font = get_font("monosmall");
1449 
1450 	text_draw(buf, &(TextParams) {
1451 		.align = ALIGN_RIGHT,
1452 		.pos = { SCREEN_W, SCREEN_H - 0.5 * text_height(font, buf, 0) },
1453 		.font_ptr = font,
1454 	});
1455 
1456 	if(global.replaymode == REPLAY_PLAY) {
1457 		r_shader_ptr(stagedraw.hud_text.shader);
1458 		// XXX: does it make sense to use the monospace font here?
1459 
1460 		snprintf(buf, sizeof(buf), "Replay: %s (%i fps)", global.replay.playername, global.replay_stage->fps);
1461 		int x = 0, y = SCREEN_H - 0.5 * text_height(font, buf, 0);
1462 
1463 		x += text_draw(buf, &(TextParams) {
1464 			.pos = { x, y },
1465 			.font_ptr = font,
1466 			.color = &stagedraw.hud_text.color.inactive,
1467 		});
1468 
1469 		if(global.replay_stage->desynced) {
1470 			strlcpy(buf, " (DESYNCED)", sizeof(buf));
1471 
1472 			text_draw(buf, &(TextParams) {
1473 				.pos = { x, y },
1474 				.font_ptr = font,
1475 				.color = RGBA_MUL_ALPHA(1.00, 0.20, 0.20, 0.60),
1476 			});
1477 		}
1478 	}
1479 #ifdef PLR_DPS_STATS
1480 	else if(global.frames) {
1481 		int totaldmg = 0;
1482 		int framespan = sizeof(global.plr.dmglog)/sizeof(*global.plr.dmglog);
1483 		int graphspan = framespan;
1484 		static int max = 0;
1485 		float graph[framespan];
1486 
1487 		if(graphspan > 120) {
1488 			// shader limitation
1489 			graphspan = 120;
1490 		}
1491 
1492 		// hack to update the graph every frame
1493 		player_register_damage(&global.plr, NULL, &(DamageInfo) { .amount = 0, .type = DMG_PLAYER_SHOT });
1494 
1495 		for(int i = 0; i < framespan; ++i) {
1496 			totaldmg += global.plr.dmglog[i];
1497 
1498 			if(global.plr.dmglog[i] > max) {
1499 				max = global.plr.dmglog[i];
1500 			}
1501 		}
1502 
1503 		for(int i = 0; i < graphspan; ++i) {
1504 			if(max > 0) {
1505 				graph[i] = (float)global.plr.dmglog[i] / max;
1506 			} else {
1507 				graph[i] = 0;
1508 			}
1509 		}
1510 
1511 		snprintf(buf, sizeof(buf), "%.02f", totaldmg / (framespan / (double)FPS));
1512 		double text_h = text_height(font, buf, 0);
1513 		double x = 0, y = SCREEN_H - 0.5 * text_h;
1514 
1515 		x += text_draw("Avg DPS: ", &(TextParams) {
1516 			.pos = { x, y },
1517 			.font_ptr = font,
1518 			.color = &stagedraw.hud_text.color.inactive,
1519 		});
1520 
1521 		text_draw(buf, &(TextParams) {
1522 			.pos = { x, y },
1523 			.font_ptr = font,
1524 			.color = &stagedraw.hud_text.color.active,
1525 		});
1526 
1527 		r_shader("graph");
1528 		r_uniform_vec3("color_low",  1.0, 0.0, 0.0);
1529 		r_uniform_vec3("color_mid",  1.0, 1.0, 0.0);
1530 		r_uniform_vec3("color_high", 0.0, 1.0, 0.0);
1531 		r_uniform_float_array("points[0]", 0, graphspan, graph);
1532 		draw_graph(142, SCREEN_H - text_h, graphspan, text_h);
1533 	}
1534 #endif
1535 
1536 	r_shader_standard();
1537 }
1538 
fill_graph(int num_samples,float * samples,FPSCounter * fps)1539 static void fill_graph(int num_samples, float *samples, FPSCounter *fps) {
1540 	for(int i = 0; i < num_samples; ++i) {
1541 		samples[i] = fps->frametimes[i] / (2.0 * (HRTIME_RESOLUTION / (float64x)FPS));
1542 
1543 		if(samples[i] > 1.0) {
1544 			samples[i] = 1.0;
1545 		}
1546 	}
1547 }
1548 
stage_draw_framerate_graphs(float x,float y,float w,float h)1549 static void stage_draw_framerate_graphs(float x, float y, float w, float h) {
1550 	#define NUM_SAMPLES (sizeof(((FPSCounter){{0}}).frametimes) / sizeof(((FPSCounter){{0}}).frametimes[0]))
1551 	static float samples[NUM_SAMPLES];
1552 
1553 	r_state_push();
1554 	r_shader("graph");
1555 
1556 	fill_graph(NUM_SAMPLES, samples, &global.fps.logic);
1557 	r_uniform_vec3("color_low",  0.0, 1.0, 1.0);
1558 	r_uniform_vec3("color_mid",  1.0, 1.0, 0.0);
1559 	r_uniform_vec3("color_high", 1.0, 0.0, 0.0);
1560 	r_uniform_float_array("points[0]", 0, NUM_SAMPLES, samples);
1561 	draw_graph(x, y, w, h);
1562 
1563 	// x -= w * 1.1;
1564 	y += h + 1;
1565 
1566 	fill_graph(NUM_SAMPLES, samples, &global.fps.busy);
1567 	r_uniform_vec3("color_low",  0.0, 1.0, 0.0);
1568 	r_uniform_vec3("color_mid",  1.0, 0.0, 0.0);
1569 	r_uniform_vec3("color_high", 1.0, 0.0, 0.5);
1570 	r_uniform_float_array("points[0]", 0, NUM_SAMPLES, samples);
1571 	draw_graph(x, y, w, h);
1572 
1573 	r_state_pop();
1574 }
1575 
stage_draw_hud(void)1576 void stage_draw_hud(void) {
1577 	// Background
1578 	r_mat_mv_push();
1579 	r_mat_mv_translate(SCREEN_W*0.5, SCREEN_H*0.5, 0);
1580 	r_shader_standard();
1581 	r_uniform_sampler("tex", "hud");
1582 	r_draw_model("hud");
1583 	r_mat_mv_pop();
1584 
1585 	r_blend(BLEND_PREMUL_ALPHA);
1586 
1587 	// Set up positions of most HUD elements
1588 	struct labels_s labels = { 0 };
1589 
1590 	const float label_spacing = 32;
1591 	float label_ypos = 0;
1592 
1593 	label_ypos = 16;
1594 	labels.y.hiscore = label_ypos += label_spacing;
1595 	labels.y.score   = label_ypos += label_spacing;
1596 
1597 	label_ypos = 108;
1598 	labels.y.lives   = label_ypos += label_spacing;
1599 	labels.y.bombs   = label_ypos += label_spacing * 1.25;
1600 
1601 	label_ypos = 210;
1602 	labels.y.power   = label_ypos += label_spacing;
1603 	labels.y.value   = label_ypos += label_spacing;
1604 	labels.y.voltage = label_ypos += label_spacing;
1605 	labels.y.graze   = label_ypos += label_spacing;
1606 
1607 	r_mat_mv_push();
1608 	r_mat_mv_translate(HUD_X_OFFSET + HUD_X_PADDING, 0, 0);
1609 
1610 	// Set up Extra Spell indicator opacity early; some other elements depend on it
1611 	float extraspell_alpha = 0;
1612 	float extraspell_fadein = 1;
1613 
1614 	if(global.boss && global.boss->current && global.boss->current->type == AT_ExtraSpell) {
1615 		extraspell_fadein  = min(1, -min(0, global.frames - global.boss->current->starttime) / (float)ATTACK_START_DELAY);
1616 		float fadeout = global.boss->current->finished * (1 - (global.boss->current->endtime - global.frames) / (float)ATTACK_END_DELAY_EXTRA) / 0.74;
1617 		float fade = max(extraspell_fadein, fadeout);
1618 		extraspell_alpha = 1 - fade;
1619 	}
1620 
1621 	labels.lb_baseclr.r = 1 - extraspell_alpha;
1622 	labels.lb_baseclr.g = 1 - extraspell_alpha;
1623 	labels.lb_baseclr.b = 1 - extraspell_alpha;
1624 	labels.lb_baseclr.a = 1 - extraspell_alpha * 0.5;
1625 
1626 	// Lives and Bombs
1627 	if(global.stage->type != STAGE_SPELL) {
1628 		r_mat_mv_push();
1629 		r_mat_mv_translate(0, font_get_descent(get_font("standard")), 0);
1630 
1631 		Sprite *spr_life = get_sprite("hud/heart");
1632 		Sprite *spr_bomb = get_sprite("hud/star");
1633 
1634 		float spacing = 1;
1635 		float pos_lives = HUD_EFFECTIVE_WIDTH - spr_life->w * (PLR_MAX_LIVES - 0.5) - spacing * (PLR_MAX_LIVES - 1);
1636 		float pos_bombs = HUD_EFFECTIVE_WIDTH - spr_bomb->w * (PLR_MAX_BOMBS - 0.5) - spacing * (PLR_MAX_BOMBS - 1);
1637 
1638 		labels.y_ofs.lives_display = 0 /* spr_life->h * -0.25 */;
1639 		labels.y_ofs.bombs_display = 0 /* spr_bomb->h * -0.25 */;
1640 
1641 		labels.y_ofs.lives_text = labels.y_ofs.lives_display + spr_life->h;
1642 		labels.y_ofs.bombs_text = labels.y_ofs.bombs_display + spr_bomb->h;
1643 
1644 		labels.x.next_life = pos_lives - spr_life->w * 0.5;
1645 		labels.x.next_bomb = pos_bombs - spr_bomb->w * 0.5;
1646 
1647 		draw_fragments(&(DrawFragmentsParams) {
1648 			.fill = spr_life,
1649 			.pos = { pos_lives, labels.y.lives + labels.y_ofs.lives_display },
1650 			.origin_offset = { 0, 0 },
1651 			.limits = { PLR_MAX_LIVES, PLR_MAX_LIFE_FRAGMENTS },
1652 			.filled = { global.plr.lives, global.plr.life_fragments },
1653 			.alpha = 1,
1654 			.spacing = spacing,
1655 			.color = {
1656 				.fill = color_mul(RGBA(1, 1, 1, 1), &labels.lb_baseclr),
1657 				.back = RGBA(0, 0, 0, 0.5),
1658 				.frag = RGBA(0.5, 0.5, 0.6, 0.5),
1659 			}
1660 		});
1661 
1662 		draw_fragments(&(DrawFragmentsParams) {
1663 			.fill = spr_bomb,
1664 			.pos = { pos_bombs, labels.y.bombs + labels.y_ofs.bombs_display },
1665 			.origin_offset = { 0, 0.05 },
1666 			.limits = { PLR_MAX_BOMBS, PLR_MAX_BOMB_FRAGMENTS },
1667 			.filled = { global.plr.bombs, global.plr.bomb_fragments },
1668 			.alpha = 1,
1669 			.spacing = spacing,
1670 			.color = {
1671 				.fill = color_mul(RGBA(1, 1, 1, 1), &labels.lb_baseclr),
1672 				.back = color_mul(RGBA(0, 0, 0, 0.5), &labels.lb_baseclr),
1673 				.frag = color_mul(RGBA(0.5, 0.5, 0.6, 0.5), &labels.lb_baseclr),
1674 			}
1675 		});
1676 
1677 		r_mat_mv_pop();
1678 	}
1679 
1680 	// Difficulty indicator
1681 	r_draw_sprite(&(SpriteParams) {
1682 		.sprite = difficulty_sprite_name(global.diff),
1683 		.pos = { HUD_EFFECTIVE_WIDTH * 0.5, 400 },
1684 		.scale.both = 0.6,
1685 		.shader = "sprite_default",
1686 	});
1687 
1688 	// Power/Item/Voltage icons
1689 	r_mat_mv_push();
1690 	r_mat_mv_translate(HUD_X_SECONDARY_OFS_ICON, font_get_descent(get_font("standard")) * 0.5 - 1, 0);
1691 
1692 	r_draw_sprite(&(SpriteParams) {
1693 		.pos = { 2, labels.y.power + 2 },
1694 		.sprite = "item/power",
1695 		.shader = "sprite_default",
1696 		.color = RGBA(0, 0, 0, 0.5),
1697 	});
1698 
1699 	r_draw_sprite(&(SpriteParams) {
1700 		.pos = { 0, labels.y.power },
1701 		.sprite = "item/power",
1702 		.shader = "sprite_default",
1703 	});
1704 
1705 	r_draw_sprite(&(SpriteParams) {
1706 		.pos = { 2, labels.y.value + 2 },
1707 		.sprite = "item/point",
1708 		.shader = "sprite_default",
1709 		.color = RGBA(0, 0, 0, 0.5),
1710 	});
1711 
1712 	r_draw_sprite(&(SpriteParams) {
1713 		.pos = { 0, labels.y.value },
1714 		.sprite = "item/point",
1715 		.shader = "sprite_default",
1716 	});
1717 
1718 	r_draw_sprite(&(SpriteParams) {
1719 		.pos = { 2, labels.y.voltage + 2 },
1720 		.sprite = "item/voltage",
1721 		.shader = "sprite_default",
1722 		.color = RGBA(0, 0, 0, 0.5),
1723 	});
1724 
1725 	r_draw_sprite(&(SpriteParams) {
1726 		.pos = { 0, labels.y.voltage },
1727 		.sprite = "item/voltage",
1728 		.shader = "sprite_default",
1729 	});
1730 
1731 	r_mat_mv_pop();
1732 	stage_draw_hud_text(&labels);
1733 
1734 	// Extra Spell indicator
1735 	if(extraspell_alpha > 0) {
1736 		float s2 = max(0, swing(extraspell_alpha, 3));
1737 		r_state_push();
1738 		r_shader("text_default");
1739 		r_mat_mv_push();
1740 		r_mat_mv_translate(lerp(-HUD_X_OFFSET - HUD_X_PADDING, HUD_EFFECTIVE_WIDTH * 0.5, pow(2*extraspell_fadein-1, 2)), 128, 0);
1741 		r_mat_mv_rotate((360 * (1-s2) - 25) * DEG2RAD, 0, 0, 1);
1742 		r_mat_mv_scale(s2, s2, 0);
1743 		r_color(RGBA_MUL_ALPHA(0.3, 0.6, 0.7, 0.7 * extraspell_alpha));
1744 
1745 		Font *font = get_font("big");
1746 
1747 		// TODO: replace this with a shader
1748 		text_draw("Voltage        \n    Overdrive!", &(TextParams) { .pos = {  1,  1 }, .font_ptr = font, .align = ALIGN_CENTER });
1749 		text_draw("Voltage        \n    Overdrive!", &(TextParams) { .pos = { -1, -1 }, .font_ptr = font, .align = ALIGN_CENTER });
1750 		r_color4(extraspell_alpha, extraspell_alpha, extraspell_alpha, extraspell_alpha);
1751 		text_draw("Voltage        \n    Overdrive!", &(TextParams) { .pos = {  0,  0 }, .font_ptr = font, .align = ALIGN_CENTER });
1752 
1753 		r_mat_mv_pop();
1754 		r_state_pop();
1755 	}
1756 
1757 	if(stagedraw.framerate_graphs) {
1758 		stage_draw_framerate_graphs(0, 360, HUD_EFFECTIVE_WIDTH, 30);
1759 	}
1760 
1761 	r_mat_mv_pop();
1762 
1763 	// Boss indicator ("Enemy")
1764 	if(global.boss) {
1765 		float red = 0.5*exp(-0.5*(global.frames-global.boss->lastdamageframe)); // hit indicator
1766 		if(red > 1)
1767 			red = 0;
1768 
1769 		r_draw_sprite(&(SpriteParams) {
1770 			.sprite = "boss_indicator",
1771 			.shader = "sprite_default",
1772 			.pos = { VIEWPORT_X+creal(global.boss->pos), 590 },
1773 			.color = RGBA(1 - red, 1 - red, 1 - red, 1 - red),
1774 		});
1775 	}
1776 }
1777 
stage_display_clear_screen(const StageClearBonus * bonus)1778 void stage_display_clear_screen(const StageClearBonus *bonus) {
1779 	StageTextTable tbl;
1780 	stagetext_begin_table(&tbl, bonus->all_clear ? "All Clear!" : "Stage Clear!", RGB(1, 1, 1), RGB(1, 1, 1), VIEWPORT_W/2,
1781 		20, 5184000, 60, 60);
1782 	stagetext_table_add_numeric_nonzero(&tbl, "Clear bonus", bonus->base);
1783 	stagetext_table_add_numeric_nonzero(&tbl, "Life bonus", bonus->lives);
1784 	stagetext_table_add_numeric_nonzero(&tbl, "Voltage bonus", bonus->voltage);
1785 	stagetext_table_add_numeric_nonzero(&tbl, "Graze bonus", bonus->graze);
1786 	stagetext_table_add_separator(&tbl);
1787 	stagetext_table_add_numeric(&tbl, "Total", bonus->total);
1788 	stagetext_end_table(&tbl);
1789 
1790 	stagetext_add(
1791 		"Press Fire to continue",
1792 		VIEWPORT_W/2 + VIEWPORT_H*0.7*I,
1793 		ALIGN_CENTER,
1794 		get_font("standard"),
1795 		RGB(1, 0.5, 0),
1796 		tbl.delay,
1797 		tbl.lifetime,
1798 		tbl.fadeintime,
1799 		tbl.fadeouttime
1800 	);
1801 
1802 	stagedraw.clear_screen.target_alpha = 1;
1803 }
1804