1 #include "device.hpp"
2 #include "renderer/renderer.hpp"
3 #include "stb_image_write.h"
4 
5 #ifdef VULKAN_WSI
6 #include "wsi.hpp"
7 #endif
8 #include <cmath>
9 #include <random>
10 #include <renderer.hpp>
11 #include <stdio.h>
12 #include <string.h>
13 #include <vector>
14 
15 using namespace PSX;
16 using namespace std;
17 using namespace Vulkan;
18 
19 struct CLIParser;
20 struct CLICallbacks
21 {
addCLICallbacks22 	void add(const char *cli, const function<void(CLIParser &)> &func)
23 	{
24 		callbacks[cli] = func;
25 	}
26 	unordered_map<string, function<void(CLIParser &)>> callbacks;
27 	function<void()> error_handler;
28 	function<void(const char *)> default_handler;
29 };
30 
31 struct CLIParser
32 {
CLIParserCLIParser33 	CLIParser(CLICallbacks cbs, int argc, char *argv[])
34 	    : cbs(move(cbs))
35 	    , argc(argc)
36 	    , argv(argv)
37 	{
38 	}
39 
parseCLIParser40 	bool parse()
41 	{
42 		try
43 		{
44 			while (argc && !ended_state)
45 			{
46 				const char *next = *argv++;
47 				argc--;
48 
49 				if (*next != '-' && cbs.default_handler)
50 				{
51 					cbs.default_handler(next);
52 				}
53 				else
54 				{
55 					auto itr = cbs.callbacks.find(next);
56 					if (itr == ::end(cbs.callbacks))
57 					{
58 						throw logic_error("Invalid argument.\n");
59 					}
60 
61 					itr->second(*this);
62 				}
63 			}
64 
65 			return true;
66 		}
67 		catch (...)
68 		{
69 			if (cbs.error_handler)
70 			{
71 				cbs.error_handler();
72 			}
73 			return false;
74 		}
75 	}
76 
endCLIParser77 	void end()
78 	{
79 		ended_state = true;
80 	}
81 
next_uintCLIParser82 	uint32_t next_uint()
83 	{
84 		if (!argc)
85 		{
86 			throw logic_error("Tried to parse uint, but nothing left in arguments.\n");
87 		}
88 
89 		uint32_t val = stoul(*argv);
90 		if (val > numeric_limits<uint32_t>::max())
91 		{
92 			throw out_of_range("next_uint() out of range.\n");
93 		}
94 
95 		argc--;
96 		argv++;
97 
98 		return val;
99 	}
100 
next_doubleCLIParser101 	double next_double()
102 	{
103 		if (!argc)
104 		{
105 			throw logic_error("Tried to parse double, but nothing left in arguments.\n");
106 		}
107 
108 		double val = stod(*argv);
109 
110 		argc--;
111 		argv++;
112 
113 		return val;
114 	}
115 
next_stringCLIParser116 	const char *next_string()
117 	{
118 		if (!argc)
119 		{
120 			throw logic_error("Tried to parse string, but nothing left in arguments.\n");
121 		}
122 
123 		const char *ret = *argv;
124 		argc--;
125 		argv++;
126 		return ret;
127 	}
128 
129 	CLICallbacks cbs;
130 	int argc;
131 	char **argv;
132 	bool ended_state = false;
133 };
134 
135 struct CLIArguments
136 {
137 	const char *dump = nullptr;
138 	const char *frame_output = nullptr;
139 	const char *trace_output = nullptr;
140 	unsigned trace_frame = 0;
141 	unsigned scale = 4;
142 	bool trace = false;
143 	bool verbose = false;
144 };
145 
146 //#define DUMP_VRAM
147 #define SCALING 4
148 //#define DETAIL_DUMP_FRAME 153
149 //#define BREAK_FRAME 40
150 //#define BREAK_DRAW 216
151 
152 #define BREAKPOINT __builtin_trap
153 
154 // This enum should always be kept equivalent to the enum in rsx_dump.cpp
155 enum
156 {
157 	RSX_END = 0,
158 	RSX_PREPARE_FRAME,
159 	RSX_FINALIZE_FRAME,
160 	RSX_TEX_WINDOW,
161 	RSX_DRAW_OFFSET,
162 	RSX_DRAW_AREA,
163 	RSX_VRAM_COORDS,
164 	RSX_HORIZONTAL_RANGE,
165 	RSX_VERTICAL_RANGE,
166 	RSX_DISPLAY_MODE,
167 	RSX_TRIANGLE,
168 	RSX_QUAD,
169 	RSX_LINE,
170 	RSX_LOAD_IMAGE,
171 	RSX_FILL_RECT,
172 	RSX_COPY_RECT,
173 	RSX_TOGGLE_DISPLAY
174 };
175 
read_tag(FILE * file)176 static void read_tag(FILE *file)
177 {
178 	char buffer[8];
179 	if (fread(buffer, sizeof(buffer), 1, file) != 1)
180 		throw runtime_error("Failed to read tag.");
181 	if (memcmp(buffer, "RSXDUMP3", sizeof(buffer)))
182 		throw runtime_error("Failed to read tag.");
183 }
184 
read_u32(FILE * file)185 static uint32_t read_u32(FILE *file)
186 {
187 	uint32_t val;
188 	if (fread(&val, sizeof(val), 1, file) != 1)
189 		throw runtime_error("Failed to read u32");
190 	return val;
191 }
192 
read_i32(FILE * file)193 static int32_t read_i32(FILE *file)
194 {
195 	int32_t val;
196 	if (fread(&val, sizeof(val), 1, file) != 1)
197 		throw runtime_error("Failed to read i32");
198 	return val;
199 }
200 
read_f32(FILE * file)201 static int32_t read_f32(FILE *file)
202 {
203 	float val;
204 	if (fread(&val, sizeof(val), 1, file) != 1)
205 		throw runtime_error("Failed to read f32");
206 	return val;
207 }
208 
209 struct CommandVertex
210 {
211 	float x, y, w;
212 	uint32_t color;
213 	uint16_t tx, ty;
214 };
215 
216 struct RenderState
217 {
218 	uint16_t texpage_x, texpage_y;
219 	uint16_t clut_x, clut_y;
220 	uint8_t texture_blend_mode;
221 	uint8_t depth_shift;
222 	bool dither;
223 	uint32_t blend_mode;
224 	bool mask_test;
225 	bool set_mask;
226 };
227 
read_vertex(FILE * file)228 CommandVertex read_vertex(FILE *file)
229 {
230 	CommandVertex buf = {};
231 	buf.x = read_f32(file);
232 	buf.y = read_f32(file);
233 	buf.w = read_f32(file);
234 	buf.color = read_u32(file);
235 	buf.tx = uint16_t(read_u32(file));
236 	buf.ty = uint16_t(read_u32(file));
237 	return buf;
238 }
239 
read_state(FILE * file)240 RenderState read_state(FILE *file)
241 {
242 	RenderState state = {};
243 	state.texpage_x = read_u32(file);
244 	state.texpage_y = read_u32(file);
245 	state.clut_x = read_u32(file);
246 	state.clut_y = read_u32(file);
247 	state.texture_blend_mode = read_u32(file);
248 	state.depth_shift = read_u32(file);
249 	state.dither = read_u32(file) != 0;
250 	state.blend_mode = read_u32(file);
251 	state.mask_test = read_u32(file) != 0;
252 	state.set_mask = read_u32(file) != 0;
253 	return state;
254 }
255 
256 struct CommandLine
257 {
258 	int16_t x0, y0, x1, y1;
259 	uint32_t c0, c1;
260 	bool dither;
261 	uint32_t blend_mode;
262 	bool mask_test;
263 	bool set_mask;
264 };
265 
read_line(FILE * file)266 CommandLine read_line(FILE *file)
267 {
268 	CommandLine line = {};
269 	line.x0 = read_i32(file);
270 	line.y0 = read_i32(file);
271 	line.x1 = read_i32(file);
272 	line.y1 = read_i32(file);
273 	line.c0 = read_u32(file);
274 	line.c1 = read_u32(file);
275 	line.dither = read_u32(file) != 0;
276 	line.blend_mode = read_u32(file);
277 	line.mask_test = read_u32(file) != 0;
278 	line.set_mask = read_u32(file) != 0;
279 	return line;
280 }
281 
282 #if 0
283 static void log_vertex(const CommandVertex &v)
284 {
285 	fprintf(stderr, "  x = %.1f, y = %.1f, w = %.1f, c = 0x%x, u = %u, v = %u\n", v.x, v.y, v.w, v.color, v.tx, v.ty);
286 }
287 
288 static void log_state(const RenderState &s)
289 {
290 	fprintf(
291 	    stderr,
292 	    " Page = (%u, %u), CLUT = (%u, %u), texture_blend_mode = %u, depth_shift = %u, dither = %s, blend_mode = %u\n",
293 	    s.texpage_x, s.texpage_y, s.clut_x, s.clut_y, s.texture_blend_mode, s.depth_shift, s.dither ? "on" : "off",
294 	    s.blend_mode);
295 }
296 #endif
297 
set_renderer_state(Renderer & renderer,const RenderState & state)298 static void set_renderer_state(Renderer &renderer, const RenderState &state)
299 {
300 	renderer.set_texture_color_modulate(state.texture_blend_mode == 2);
301 	renderer.set_palette_offset(state.clut_x, state.clut_y);
302 	renderer.set_texture_offset(state.texpage_x, state.texpage_y);
303 	renderer.set_dither(state.dither);
304 	renderer.set_mask_test(state.mask_test);
305 	renderer.set_force_mask_bit(state.set_mask);
306 	if (state.texture_blend_mode != 0)
307 	{
308 		switch (state.depth_shift)
309 		{
310 		default:
311 		case 0:
312 			renderer.set_texture_mode(TextureMode::ABGR1555);
313 			break;
314 		case 1:
315 			renderer.set_texture_mode(TextureMode::Palette8bpp);
316 			break;
317 		case 2:
318 			renderer.set_texture_mode(TextureMode::Palette4bpp);
319 			break;
320 		}
321 	}
322 	else
323 		renderer.set_texture_mode(TextureMode::None);
324 
325 	switch (state.blend_mode)
326 	{
327 	default:
328 		renderer.set_semi_transparent(SemiTransparentMode::None);
329 		break;
330 
331 	case 0:
332 		renderer.set_semi_transparent(SemiTransparentMode::Average);
333 		break;
334 	case 1:
335 		renderer.set_semi_transparent(SemiTransparentMode::Add);
336 		break;
337 	case 2:
338 		renderer.set_semi_transparent(SemiTransparentMode::Sub);
339 		break;
340 	case 3:
341 		renderer.set_semi_transparent(SemiTransparentMode::AddQuarter);
342 		break;
343 	}
344 }
345 
dump_to_file(const CLIArguments & args,Device & device,Renderer & renderer,unsigned index,unsigned subindex)346 static void dump_to_file(const CLIArguments &args, Device &device, Renderer &renderer, unsigned index,
347                          unsigned subindex)
348 {
349 	unsigned width, height;
350 	auto buffer = renderer.scanout_vram_to_buffer(width, height);
351 	if (!buffer)
352 		return;
353 
354 	char path[1024];
355 	snprintf(path, sizeof(path), "%s-%06u-%06u.bmp", args.trace_output, index, subindex);
356 
357 	uint32_t *data = static_cast<uint32_t *>(device.map_host_buffer(*buffer, MEMORY_ACCESS_READ));
358 	for (unsigned i = 0; i < width * height; i++)
359 		data[i] |= 0xff000000u;
360 
361 	if (!stbi_write_bmp(path, width, height, 4, data))
362 		LOG("Failed to write image.");
363 	device.unmap_host_buffer(*buffer);
364 }
365 
dump_vram_to_file(const CLIArguments & args,Device & device,Renderer & renderer,unsigned index)366 static void dump_vram_to_file(const CLIArguments &args, Device &device, Renderer &renderer, unsigned index)
367 {
368 	unsigned width, height;
369 	auto buffer = renderer.scanout_vram_to_buffer(width, height);
370 	if (!buffer)
371 		return;
372 
373 	char path[1024];
374 	snprintf(path, sizeof(path), "%s-vram-%06u.bmp", args.frame_output, index);
375 
376 	uint32_t *data = static_cast<uint32_t *>(device.map_host_buffer(*buffer, MEMORY_ACCESS_READ));
377 	for (unsigned i = 0; i < width * height; i++)
378 		data[i] |= 0xff000000u;
379 
380 	if (!stbi_write_bmp(path, width, height, 4, data))
381 		LOG("Failed to write image.");
382 	device.unmap_host_buffer(*buffer);
383 }
384 
read_command(const CLIArguments & args,FILE * file,Device & device,Renderer & renderer,bool & eof,unsigned & frame,unsigned & draw_call)385 static bool read_command(const CLIArguments &args, FILE *file, Device &device, Renderer &renderer, bool &eof,
386                          unsigned &frame, unsigned &draw_call)
387 {
388 	auto op = read_u32(file);
389 	eof = false;
390 	switch (op)
391 	{
392 	case RSX_PREPARE_FRAME:
393 		break;
394 	case RSX_FINALIZE_FRAME:
395 		return false;
396 	case RSX_END:
397 		eof = true;
398 		return false;
399 
400 	case RSX_TEX_WINDOW:
401 	{
402 		auto tww = read_u32(file);
403 		auto twh = read_u32(file);
404 		auto twx = read_u32(file);
405 		auto twy = read_u32(file);
406 
407 		auto tex_x_mask = ~(tww << 3);
408 		auto tex_y_mask = ~(twh << 3);
409 		auto tex_x_or = (twx & tww) << 3;
410 		auto tex_y_or = (twy & twh) << 3;
411 
412 		renderer.set_texture_window({ uint8_t(tex_x_mask), uint8_t(tex_y_mask), uint8_t(tex_x_or), uint8_t(tex_y_or) });
413 		break;
414 	}
415 
416 	case RSX_DRAW_OFFSET:
417 	{
418 		auto x = read_i32(file);
419 		auto y = read_i32(file);
420 
421 		renderer.set_draw_offset(x, y);
422 		break;
423 	}
424 
425 	case RSX_DRAW_AREA:
426 	{
427 		auto x0 = read_u32(file);
428 		auto y0 = read_u32(file);
429 		auto x1 = read_u32(file);
430 		auto y1 = read_u32(file);
431 
432 		int width = x1 - x0 + 1;
433 		int height = y1 - y0 + 1;
434 		width = max(width, 0);
435 		height = max(height, 0);
436 
437 		width = min(width, int(FB_WIDTH - x0));
438 		height = min(height, int(FB_HEIGHT - y0));
439 		renderer.set_draw_rect({ x0, y0, unsigned(width), unsigned(height) });
440 		break;
441 	}
442 
443 	case RSX_VRAM_COORDS:
444 	{
445 		auto xstart = read_u32(file);
446 		auto ystart = read_u32(file);
447 
448 		renderer.set_vram_framebuffer_coords(xstart, ystart);
449 		break;
450 	}
451 
452 	case RSX_HORIZONTAL_RANGE:
453 	{
454 		auto x1 = read_u32(file);
455 		auto x2 = read_u32(file);
456 
457 		renderer.set_horizontal_display_range(x1, x2);
458 		break;
459 	}
460 
461 	case RSX_VERTICAL_RANGE:
462 	{
463 		auto y1 = read_u32(file);
464 		auto y2 = read_u32(file);
465 
466 		renderer.set_vertical_display_range(y1, y2);
467 		break;
468 	}
469 
470 	case RSX_DISPLAY_MODE:
471 	{
472 		auto depth_24bpp = read_u32(file);
473 		auto is_pal = readu32(file);
474 		auto is_480i = readu32(file);
475 		auto width_mode = readu32(file);
476 
477 		renderer.set_display_mode(depth_24bpp != 0, is_pal != 0, is_480i != 0,
478 		                          static_cast<Renderer::WidthMode>(width_mode));
479 		break;
480 	}
481 
482 	case RSX_TRIANGLE:
483 	{
484 		auto v0 = read_vertex(file);
485 		auto v1 = read_vertex(file);
486 		auto v2 = read_vertex(file);
487 		auto state = read_state(file);
488 
489 		Vertex vertices[3] = {
490 			{ v0.x, v0.y, v0.w, v0.color, v0.tx, v0.ty },
491 			{ v1.x, v1.y, v1.w, v1.color, v1.tx, v1.ty },
492 			{ v2.x, v2.y, v2.w, v2.color, v2.tx, v2.ty },
493 		};
494 
495 		set_renderer_state(renderer, state);
496 		renderer.draw_triangle(vertices);
497 
498 		if (args.trace && frame == args.trace_frame)
499 			dump_to_file(args, device, renderer, frame, draw_call);
500 
501 		draw_call++;
502 		break;
503 	}
504 
505 	case RSX_QUAD:
506 	{
507 		auto v0 = read_vertex(file);
508 		auto v1 = read_vertex(file);
509 		auto v2 = read_vertex(file);
510 		auto v3 = read_vertex(file);
511 		auto state = read_state(file);
512 
513 		Vertex vertices[4] = {
514 			{ v0.x, v0.y, v0.w, v0.color, v0.tx, v0.ty },
515 			{ v1.x, v1.y, v1.w, v1.color, v1.tx, v1.ty },
516 			{ v2.x, v2.y, v2.w, v2.color, v2.tx, v2.ty },
517 			{ v3.x, v3.y, v3.w, v3.color, v3.tx, v3.ty },
518 		};
519 
520 		set_renderer_state(renderer, state);
521 		renderer.draw_quad(vertices);
522 
523 		if (args.trace && frame == args.trace_frame)
524 			dump_to_file(args, device, renderer, frame, draw_call);
525 
526 		draw_call++;
527 		break;
528 	}
529 
530 	case RSX_LINE:
531 	{
532 		auto line = read_line(file);
533 
534 		Vertex vertices[2] = {
535 			{ float(line.x0), float(line.y0), 1.0f, line.c0, 0, 0 },
536 			{ float(line.x1), float(line.y1), 1.0f, line.c1, 0, 0 },
537 		};
538 
539 		renderer.set_texture_color_modulate(false);
540 		renderer.set_texture_mode(TextureMode::None);
541 		renderer.set_dither(line.dither);
542 		renderer.set_mask_test(line.mask_test);
543 		renderer.set_force_mask_bit(line.set_mask);
544 		switch (line.blend_mode)
545 		{
546 		default:
547 			renderer.set_semi_transparent(SemiTransparentMode::None);
548 			break;
549 
550 		case 0:
551 			renderer.set_semi_transparent(SemiTransparentMode::Average);
552 			break;
553 		case 1:
554 			renderer.set_semi_transparent(SemiTransparentMode::Add);
555 			break;
556 		case 2:
557 			renderer.set_semi_transparent(SemiTransparentMode::Sub);
558 			break;
559 		case 3:
560 			renderer.set_semi_transparent(SemiTransparentMode::AddQuarter);
561 			break;
562 		}
563 
564 		renderer.draw_line(vertices);
565 
566 		if (args.trace && frame == args.trace_frame)
567 			dump_to_file(args, device, renderer, frame, draw_call);
568 
569 		draw_call++;
570 		break;
571 	}
572 
573 	case RSX_LOAD_IMAGE:
574 	{
575 		auto x = read_u32(file);
576 		auto y = read_u32(file);
577 		auto width = read_u32(file);
578 		auto height = read_u32(file);
579 		bool mask_test = read_u32(file) != 0;
580 		bool set_mask = read_u32(file) != 0;
581 
582 		renderer.set_mask_test(mask_test);
583 		renderer.set_force_mask_bit(set_mask);
584 		auto handle = renderer.copy_cpu_to_vram({ x, y, width, height });
585 		uint16_t *ptr = renderer.begin_copy(handle);
586 		fread(ptr, sizeof(uint16_t), width * height, file);
587 		renderer.end_copy(handle);
588 
589 		if (args.trace && frame == args.trace_frame)
590 			dump_to_file(args, device, renderer, frame, draw_call);
591 		draw_call++;
592 		break;
593 	}
594 
595 	case RSX_FILL_RECT:
596 	{
597 		auto color = read_u32(file);
598 		auto x = read_u32(file);
599 		auto y = read_u32(file);
600 		auto w = read_u32(file);
601 		auto h = read_u32(file);
602 
603 		renderer.clear_rect({ x, y, w, h }, color);
604 
605 		if (args.trace && frame == args.trace_frame)
606 			dump_to_file(args, device, renderer, frame, draw_call);
607 		draw_call++;
608 		break;
609 	}
610 
611 	case RSX_COPY_RECT:
612 	{
613 		auto src_x = read_u32(file);
614 		auto src_y = read_u32(file);
615 		auto dst_x = read_u32(file);
616 		auto dst_y = read_u32(file);
617 		auto w = read_u32(file);
618 		auto h = read_u32(file);
619 		bool mask_test = read_u32(file) != 0;
620 		bool set_mask = read_u32(file) != 0;
621 		renderer.set_mask_test(mask_test);
622 		renderer.set_force_mask_bit(set_mask);
623 		if (src_x != dst_x || src_y != dst_y)
624 			renderer.blit_vram({ dst_x, dst_y, w, h }, { src_x, src_y, w, h });
625 
626 		if (args.trace && frame == args.trace_frame)
627 			dump_to_file(args, device, renderer, frame, draw_call);
628 		draw_call++;
629 		break;
630 	}
631 
632 	case RSX_TOGGLE_DISPLAY:
633 	{
634 		auto toggle = read_u32(file);
635 		renderer.toggle_display(toggle == 0);
636 		break;
637 	}
638 
639 	default:
640 		throw runtime_error("Invalid opcode.");
641 	}
642 	return true;
643 }
644 
gettime()645 static double gettime()
646 {
647 	timespec ts;
648 	clock_gettime(CLOCK_MONOTONIC, &ts);
649 	return ts.tv_sec + 1e-9 * ts.tv_nsec;
650 }
651 
print_help()652 static void print_help()
653 {
654 	fprintf(stderr, "rsx-player [dump] [--scale <scale>] [--dump-vram <path>] [--trace-frame <frame> <path>] "
655 	                "[--verbose] [--help]\n");
656 }
657 
main(int argc,char * argv[])658 int main(int argc, char *argv[])
659 {
660 	CLIArguments args;
661 	CLICallbacks cbs;
662 
663 	cbs.add("--help", [](CLIParser &parser) {
664 		print_help();
665 		parser.end();
666 	});
667 	cbs.add("--dump-vram", [&args](CLIParser &parser) { args.frame_output = parser.next_string(); });
668 	cbs.add("--trace-frame", [&args](CLIParser &parser) {
669 		args.trace_frame = parser.next_uint();
670 		args.trace_output = parser.next_string();
671 		args.trace = true;
672 	});
673 	cbs.add("--scale", [&args](CLIParser &parser) { args.scale = parser.next_uint(); });
674 	cbs.add("--verbose", [&args](CLIParser &) { args.verbose = true; });
675 	cbs.error_handler = [] { print_help(); };
676 	cbs.default_handler = [&args](const char *value) { args.dump = value; };
677 	CLIParser parser{ move(cbs), argc - 1, argv + 1 };
678 	if (!parser.parse())
679 		return 1;
680 	else if (parser.ended_state)
681 		return 0;
682 
683 	if (!args.dump)
684 	{
685 		fprintf(stderr, "Didn't specify input file.\n");
686 		print_help();
687 		return 1;
688 	}
689 
690 	WSI wsi;
691 	wsi.init(1280, 960);
692 	auto &device = wsi.get_device();
693 	Renderer renderer(device, args.scale, nullptr);
694 
695 	FILE *file = fopen(args.dump, "rb");
696 	if (!file)
697 		return 1;
698 
699 	read_tag(file);
700 
701 	bool eof = false;
702 	unsigned frames = 0;
703 	unsigned draw_call = 0;
704 	double total_time = 0.0;
705 	while (!eof && wsi.alive())
706 	{
707 		draw_call = 0;
708 
709 		double start = gettime();
710 		wsi.begin_frame();
711 		renderer.reset_counters();
712 		while (read_command(args, file, device, renderer, eof, frames, draw_call))
713 			;
714 		renderer.scanout();
715 
716 		if (args.frame_output)
717 			dump_vram_to_file(args, device, renderer, frames);
718 
719 		renderer.flush();
720 		wsi.end_frame();
721 		double end = gettime();
722 		total_time += end - start;
723 		frames++;
724 
725 		if (args.verbose)
726 		{
727 			if (renderer.counters.render_passes)
728 			{
729 				LOG("========================\n");
730 				LOG("Completed frame %u.\n", frames);
731 				LOG("Render passes: %u\n", renderer.counters.render_passes);
732 				LOG("Readback pixels: %u\n", renderer.counters.fragment_readback_pixels);
733 				LOG("Writeout pixels: %u\n", renderer.counters.fragment_writeout_pixels);
734 				LOG("Draw calls: %u\n", renderer.counters.draw_calls);
735 				LOG("Vertices: %u\n", renderer.counters.vertices);
736 				LOG("========================\n");
737 			}
738 			else
739 			{
740 				LOG("========================\n");
741 				LOG("Completed frame %u.\n", frames);
742 				LOG("========================\n");
743 			}
744 		}
745 	}
746 
747 	LOG("Ran %u frames in %f s! (%.3f ms / frame).\n", frames, total_time, 1000.0 * total_time / frames);
748 }
749