ogl_beamforming

Ultrasound Beamforming Implemented with OpenGL
git clone anongit@rnpnr.xyz:ogl_beamforming.git
Log | Files | Refs | Feed | Submodules | README | LICENSE

throughput.c (19382B)


      1 /* See LICENSE for license details. */
      2 /* TODO(rnp):
      3  * [ ]: for finer grained evaluation of throughput latency just queue a data upload
      4  *      without replacing the data.
      5  * [ ]: bug: we aren't inserting rf data between each frame
      6  */
      7 
      8 #define BEAMFORMER_LIB_EXPORT function
      9 #include "ogl_beamformer_lib.c"
     10 
     11 #include <signal.h>
     12 #include <stdarg.h>
     13 #include <stdio.h>
     14 #include <stdlib.h>
     15 #include <zstd.h>
     16 
     17 global iv3 g_output_points    = {{512, 1, 1024}};
     18 global v2  g_axial_extent     = {{ 10e-3f, 165e-3f}};
     19 global v2  g_lateral_extent   = {{-60e-3f,  60e-3f}};
     20 global f32 g_f_number         = 0.5f;
     21 
     22 typedef struct {
     23 	b32 loop;
     24 	b32 cuda;
     25 	u32 frame_number;
     26 
     27 	char **remaining;
     28 	i32    remaining_count;
     29 } Options;
     30 
     31 #include "external/zemp_bp.h"
     32 
     33 typedef struct {
     34 	ZBP_DataKind            kind;
     35 	ZBP_DataCompressionKind compression_kind;
     36 	s8                      bytes;
     37 } ZBP_Data;
     38 
     39 global b32 g_should_exit;
     40 
     41 #define die(...) die_((char *)__func__, __VA_ARGS__)
     42 function no_return void
     43 die_(char *function_name, char *format, ...)
     44 {
     45 	if (function_name)
     46 		fprintf(stderr, "%s: ", function_name);
     47 
     48 	va_list ap;
     49 
     50 	va_start(ap, format);
     51 	vfprintf(stderr, format, ap);
     52 	va_end(ap);
     53 
     54 	os_exit(1);
     55 }
     56 
     57 #if OS_LINUX
     58 
     59 #include <fcntl.h>
     60 #include <sys/stat.h>
     61 #include <unistd.h>
     62 
     63 function s8
     64 os_read_file_simp(char *fname)
     65 {
     66 	s8 result;
     67 	i32 fd = open(fname, O_RDONLY);
     68 	if (fd < 0)
     69 		die("couldn't open file: %s\n", fname);
     70 
     71 	struct stat st;
     72 	if (stat(fname, &st) < 0)
     73 		die("couldn't stat file\n");
     74 
     75 	result.len  = st.st_size;
     76 	result.data = malloc((uz)st.st_size);
     77 	if (!result.data)
     78 		die("couldn't alloc space for reading\n");
     79 
     80 	iz rlen = read(fd, result.data, (u32)st.st_size);
     81 	close(fd);
     82 
     83 	if (rlen != st.st_size)
     84 		die("couldn't read file: %s\n", fname);
     85 
     86 	return result;
     87 }
     88 
     89 #elif OS_WINDOWS
     90 
     91 function s8
     92 os_read_file_simp(char *fname)
     93 {
     94 	s8 result;
     95 	iptr h = CreateFileA(fname, GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0);
     96 	if (h == INVALID_FILE)
     97 		die("couldn't open file: %s\n", fname);
     98 
     99 	w32_file_info fileinfo;
    100 	if (!GetFileInformationByHandle(h, &fileinfo))
    101 		die("couldn't get file info\n", stderr);
    102 
    103 	result.len  = fileinfo.nFileSizeLow;
    104 	result.data = malloc(fileinfo.nFileSizeLow);
    105 	if (!result.data)
    106 		die("couldn't alloc space for reading\n");
    107 
    108 	i32 rlen = 0;
    109 	if (!ReadFile(h, result.data, (i32)fileinfo.nFileSizeLow, &rlen, 0) && rlen != (i32)fileinfo.nFileSizeLow)
    110 		die("couldn't read file: %s\n", fname);
    111 	CloseHandle(h);
    112 
    113 	return result;
    114 }
    115 
    116 #else
    117 #error Unsupported Platform
    118 #endif
    119 
    120 function void
    121 stream_ensure_termination(Stream *s, u8 byte)
    122 {
    123 	b32 found = 0;
    124 	if (!s->errors && s->widx > 0)
    125 		found = s->data[s->widx - 1] == byte;
    126 	if (!found) {
    127 		s->errors |= s->cap - 1 < s->widx;
    128 		if (!s->errors)
    129 			s->data[s->widx++] = byte;
    130 	}
    131 }
    132 
    133 function void *
    134 decompress_zstd_data(s8 raw)
    135 {
    136 	uz requested_size = ZSTD_getFrameContentSize(raw.data, (uz)raw.len);
    137 	void *out         = malloc(requested_size);
    138 	if (out) {
    139 		uz decompressed = ZSTD_decompress(out, requested_size, raw.data, (uz)raw.len);
    140 		if (decompressed != requested_size) {
    141 			free(out);
    142 			out = 0;
    143 		}
    144 	}
    145 	return out;
    146 }
    147 
    148 function b32
    149 beamformer_simple_parameters_from_zbp_file(BeamformerSimpleParameters *bp, char *path, ZBP_Data *raw_data)
    150 {
    151 	s8 raw = os_read_file_simp(path);
    152 	if (raw.len < (iz)sizeof(ZBP_BaseHeader) || ((ZBP_BaseHeader *)raw.data)->magic != ZBP_HeaderMagic)
    153 		return 0;
    154 
    155 	switch (((ZBP_BaseHeader *)raw.data)->major) {
    156 
    157 	case 1:{
    158 		ZBP_HeaderV1 *header       = (ZBP_HeaderV1 *)raw.data;
    159 
    160 		bp->sample_count           = header->sample_count;
    161 		bp->channel_count          = header->channel_count;
    162 		bp->acquisition_count      = header->receive_event_count;
    163 
    164 		bp->sampling_mode          = BeamformerSamplingMode_4X;
    165 		bp->acquisition_kind       = header->beamform_mode;
    166 		bp->decode_mode            = header->decode_mode;
    167 		bp->sampling_frequency     = header->sampling_frequency;
    168 		bp->demodulation_frequency = header->sampling_frequency / 4;
    169 		bp->speed_of_sound         = header->speed_of_sound;
    170 		bp->time_offset            = header->time_offset;
    171 
    172 		mem_copy(bp->channel_mapping,       header->channel_mapping,             sizeof(*bp->channel_mapping) * bp->channel_count);
    173 		mem_copy(bp->xdc_transform.E,       header->transducer_transform_matrix, sizeof(bp->xdc_transform));
    174 		mem_copy(bp->xdc_element_pitch.E,   header->transducer_element_pitch,    sizeof(bp->xdc_element_pitch));
    175 		// NOTE(rnp): ignores emission count and ensemble count
    176 		mem_copy(bp->raw_data_dimensions.E, header->raw_data_dimension,          sizeof(bp->raw_data_dimensions));
    177 
    178 		bp->data_kind              = ZBP_DataKind_Int16;
    179 		raw_data->kind             = ZBP_DataKind_Int16;
    180 		raw_data->compression_kind = ZBP_DataCompressionKind_ZSTD;
    181 
    182 		read_only local_persist u8 transmit_mode_to_orientation[] = {
    183 			[0] = (ZBP_RCAOrientation_Rows    << 4) | ZBP_RCAOrientation_Rows,
    184 			[1] = (ZBP_RCAOrientation_Rows    << 4) | ZBP_RCAOrientation_Columns,
    185 			[2] = (ZBP_RCAOrientation_Columns << 4) | ZBP_RCAOrientation_Rows,
    186 			[3] = (ZBP_RCAOrientation_Columns << 4) | ZBP_RCAOrientation_Columns,
    187 		};
    188 		if (header->transmit_mode >= countof(transmit_mode_to_orientation))
    189 			return 0;
    190 
    191 		bp->transmit_receive_orientation = transmit_mode_to_orientation[header->transmit_mode];
    192 
    193 		ZBP_AcquisitionKind acquisition_kind = header->beamform_mode;
    194 		if (acquisition_kind == ZBP_AcquisitionKind_FORCES   ||
    195 		    acquisition_kind == ZBP_AcquisitionKind_HERCULES ||
    196 		    acquisition_kind == ZBP_AcquisitionKind_UFORCES  ||
    197 		    acquisition_kind == ZBP_AcquisitionKind_UHERCULES)
    198 		{
    199 			bp->single_focus       = 1;
    200 			bp->single_orientation = 1;
    201 			bp->focal_vector.E[0]  = header->steering_angles[0];
    202 			bp->focal_vector.E[1]  = header->focal_depths[0];
    203 		}
    204 
    205 		if (acquisition_kind == ZBP_AcquisitionKind_UFORCES ||
    206 		    acquisition_kind == ZBP_AcquisitionKind_UHERCULES)
    207 		{
    208 			mem_copy(bp->sparse_elements, header->sparse_elements, sizeof(*bp->sparse_elements) * bp->acquisition_count);
    209 		}
    210 
    211 		if (acquisition_kind == ZBP_AcquisitionKind_RCA_TPW ||
    212 		    acquisition_kind == ZBP_AcquisitionKind_RCA_VLS)
    213 		{
    214 			mem_copy(bp->focal_depths,    header->focal_depths,    sizeof(*bp->focal_depths) * bp->acquisition_count);
    215 			mem_copy(bp->steering_angles, header->steering_angles, sizeof(*bp->steering_angles) * bp->acquisition_count);
    216 			for EachIndex(bp->acquisition_count, it)
    217 				bp->transmit_receive_orientations[it] = bp->transmit_receive_orientation;
    218 		}
    219 
    220 		bp->emission_kind = BeamformerEmissionKind_Sine;
    221 		bp->emission_parameters.sine.cycles    = 2;
    222 		bp->emission_parameters.sine.frequency = bp->demodulation_frequency;
    223 	}break;
    224 
    225 	case 2:{
    226 		ZBP_HeaderV2 *header       = (ZBP_HeaderV2 *)raw.data;
    227 
    228 		bp->sample_count           = header->sample_count;
    229 		bp->channel_count          = header->channel_count;
    230 		bp->acquisition_count      = header->receive_event_count;
    231 
    232 		read_only local_persist BeamformerSamplingMode zbp_sampling_mode_to_beamformer[] = {
    233 			[ZBP_SamplingMode_Standard] = BeamformerSamplingMode_4X,
    234 			[ZBP_SamplingMode_Bandpass] = BeamformerSamplingMode_2X,
    235 		};
    236 		bp->sampling_mode = zbp_sampling_mode_to_beamformer[header->sampling_mode];
    237 
    238 		bp->acquisition_kind       = header->acquisition_mode;
    239 		bp->decode_mode            = header->decode_mode;
    240 		bp->sampling_frequency     = header->sampling_frequency;
    241 		bp->demodulation_frequency = header->demodulation_frequency;
    242 		bp->speed_of_sound         = header->speed_of_sound;
    243 		bp->time_offset            = header->time_offset;
    244 
    245 		if (header->channel_mapping_offset != -1) {
    246 			mem_copy(bp->channel_mapping, raw.data + header->channel_mapping_offset,
    247 			         sizeof(*bp->channel_mapping) * bp->channel_count);
    248 		} else {
    249 			for EachIndex(bp->channel_count, it)
    250 				bp->channel_mapping[it] = it;
    251 		}
    252 
    253 		mem_copy(bp->xdc_transform.E,       header->transducer_transform_matrix, sizeof(bp->xdc_transform));
    254 		mem_copy(bp->xdc_element_pitch.E,   header->transducer_element_pitch,    sizeof(bp->xdc_element_pitch));
    255 		// NOTE(rnp): ignores group count and ensemble count
    256 		mem_copy(bp->raw_data_dimensions.E, header->raw_data_dimension,          sizeof(bp->raw_data_dimensions));
    257 
    258 		bp->data_kind              = header->raw_data_kind;
    259 		raw_data->kind             = header->raw_data_kind;
    260 		raw_data->compression_kind = header->raw_data_compression_kind;
    261 
    262 		if (header->raw_data_offset != -1) {
    263 			raw_data->bytes.data = raw.data + header->raw_data_offset;
    264 			if (raw_data->compression_kind == ZBP_DataCompressionKind_ZSTD) {
    265 				// NOTE(rnp): limitation in the header format
    266 				raw_data->bytes.len  = raw.len - header->raw_data_offset;
    267 			} else {
    268 				raw_data->bytes.len  = header->raw_data_dimension[0] * header->raw_data_dimension[1] *
    269 				                       header->raw_data_dimension[2] * header->raw_data_dimension[3];
    270 				raw_data->bytes.len *= beamformer_data_kind_byte_size[header->raw_data_kind];
    271 			}
    272 		}
    273 
    274 		// NOTE(rnp): only look at the first emission descriptor, other cases aren't currently relevant
    275 		{
    276 			ZBP_EmissionDescriptor *ed = (ZBP_EmissionDescriptor *)(raw.data + header->emission_descriptors_offset);
    277 			switch (ed->emission_kind) {
    278 
    279 			case ZBP_EmissionKind_Sine:{
    280 				ZBP_EmissionSineParameters *ep = (ZBP_EmissionSineParameters *)(raw.data + ed->parameters_offset);
    281 				bp->emission_kind = BeamformerEmissionKind_Sine;
    282 				bp->emission_parameters.sine.cycles    = ep->cycles;
    283 				bp->emission_parameters.sine.frequency = ep->frequency;
    284 			}break;
    285 
    286 			case ZBP_EmissionKind_Chirp:{
    287 				ZBP_EmissionChirpParameters *ep = (ZBP_EmissionChirpParameters *)(raw.data + ed->parameters_offset);
    288 				bp->emission_kind = BeamformerEmissionKind_Chirp;
    289 				bp->emission_parameters.chirp.duration      = ep->duration;
    290 				bp->emission_parameters.chirp.min_frequency = ep->min_frequency;
    291 				bp->emission_parameters.chirp.max_frequency = ep->max_frequency;
    292 			}break;
    293 
    294 			InvalidDefaultCase;
    295 			static_assert(ZBP_EmissionKind_Count == (ZBP_EmissionKind_Chirp + 1), "");
    296 			}
    297 		}
    298 
    299 		switch (header->acquisition_mode) {
    300 		case ZBP_AcquisitionKind_FORCES:{}break;
    301 
    302 		case ZBP_AcquisitionKind_HERCULES:{
    303 			ZBP_HERCULESParameters *p = (ZBP_HERCULESParameters *)(raw.data + header->acquisition_parameters_offset);
    304 			bp->transmit_receive_orientation = p->transmit_focus.transmit_receive_orientation;
    305 			bp->focal_vector.E[0] = p->transmit_focus.steering_angle;
    306 			bp->focal_vector.E[1] = p->transmit_focus.focal_depth;
    307 
    308 			bp->single_focus       = 1;
    309 			bp->single_orientation = 1;
    310 		}break;
    311 
    312 		case ZBP_AcquisitionKind_UFORCES:{
    313 			ZBP_uFORCESParameters *p = (ZBP_uFORCESParameters *)(raw.data + header->acquisition_parameters_offset);
    314 			mem_copy(bp->sparse_elements, raw.data + p->sparse_elements_offset,
    315 			         sizeof(*bp->sparse_elements) * bp->acquisition_count);
    316 		}break;
    317 
    318 		case ZBP_AcquisitionKind_UHERCULES:{
    319 			ZBP_uHERCULESParameters *p = (ZBP_uHERCULESParameters *)(raw.data + header->acquisition_parameters_offset);
    320 			bp->transmit_receive_orientation = p->transmit_focus.transmit_receive_orientation;
    321 			bp->focal_vector.E[0] = p->transmit_focus.steering_angle;
    322 			bp->focal_vector.E[1] = p->transmit_focus.focal_depth;
    323 
    324 			bp->single_focus       = 1;
    325 			bp->single_orientation = 1;
    326 
    327 			mem_copy(bp->sparse_elements, raw.data + p->sparse_elements_offset,
    328 			         sizeof(*bp->sparse_elements) * bp->acquisition_count);
    329 		}break;
    330 
    331 		case ZBP_AcquisitionKind_RCA_TPW:{
    332 			ZBP_TPWParameters *p = (ZBP_TPWParameters *)(raw.data + header->acquisition_parameters_offset);
    333 
    334 			mem_copy(bp->transmit_receive_orientations, raw.data + p->transmit_receive_orientations_offset,
    335 			         sizeof(*bp->transmit_receive_orientations) * bp->acquisition_count);
    336 			mem_copy(bp->steering_angles, raw.data + p->tilting_angles_offset,
    337 			         sizeof(*bp->steering_angles) * bp->acquisition_count);
    338 
    339 			for EachIndex(bp->acquisition_count, it)
    340 				bp->focal_depths[it] = F32_INFINITY;
    341 		}break;
    342 
    343 		case ZBP_AcquisitionKind_RCA_VLS:{
    344 			ZBP_VLSParameters *p = (ZBP_VLSParameters *)(raw.data + header->acquisition_parameters_offset);
    345 
    346 			mem_copy(bp->transmit_receive_orientations, raw.data + p->transmit_receive_orientations_offset,
    347 			         sizeof(*bp->transmit_receive_orientations) * bp->acquisition_count);
    348 
    349 			f32 *focal_depths   = (f32 *)(raw.data + p->focal_depths_offset);
    350 			f32 *origin_offsets = (f32 *)(raw.data + p->origin_offsets_offset);
    351 
    352 			for EachIndex(bp->acquisition_count, it) {
    353 				f32 sign   = Sign(focal_depths[it]);
    354 				f32 depth  = focal_depths[it];
    355 				f32 origin = origin_offsets[it];
    356 				bp->steering_angles[it] = atan2_f32(origin, -depth) * 180.0f / PI;
    357 				bp->focal_depths[it]    = sign * sqrt_f32(depth * depth + origin * origin);
    358 			}
    359 		}break;
    360 
    361 		InvalidDefaultCase;
    362 		}
    363 
    364 	}break;
    365 
    366 	default:{return 0;}break;
    367 	}
    368 
    369 	return 1;
    370 }
    371 
    372 #define shift_n(v, c, n) v += n, c -= n
    373 #define shift(v, c) shift_n(v, c, 1)
    374 
    375 function void
    376 usage(char *argv0)
    377 {
    378 	die("%s [--loop] [--cuda] [--frame n] base_path study\n"
    379 	    "    --loop:    reupload data forever\n"
    380 	    "    --cuda:    use cuda for decoding\n"
    381 	    "    --frame n: use frame n of the data for display\n",
    382 	    argv0);
    383 }
    384 
    385 function Options
    386 parse_argv(i32 argc, char *argv[])
    387 {
    388 	Options result = {0};
    389 
    390 	char *argv0 = argv[0];
    391 	shift(argv, argc);
    392 
    393 	while (argc > 0) {
    394 		s8 arg = c_str_to_s8(*argv);
    395 
    396 		if (s8_equal(arg, s8("--loop"))) {
    397 			shift(argv, argc);
    398 			result.loop = 1;
    399 		} else if (s8_equal(arg, s8("--cuda"))) {
    400 			shift(argv, argc);
    401 			result.cuda = 1;
    402 		} else if (s8_equal(arg, s8("--frame"))) {
    403 			shift(argv, argc);
    404 			if (argc) {
    405 				result.frame_number = (u32)atoi(*argv);
    406 				shift(argv, argc);
    407 			}
    408 		} else if (arg.len > 0 && arg.data[0] == '-') {
    409 			usage(argv0);
    410 		} else {
    411 			break;
    412 		}
    413 	}
    414 
    415 	result.remaining       = argv;
    416 	result.remaining_count = argc;
    417 
    418 	return result;
    419 }
    420 
    421 function b32
    422 send_frame(void *restrict data, BeamformerSimpleParameters *restrict bp)
    423 {
    424 	u32 data_size = bp->raw_data_dimensions.E[0] * bp->raw_data_dimensions.E[1]
    425 	                * beamformer_data_kind_byte_size[bp->data_kind];
    426 	b32 result    = beamformer_push_data_with_compute(data, data_size, BeamformerViewPlaneTag_XZ, 0);
    427 	if (!result && !g_should_exit) printf("lib error: %s\n", beamformer_get_last_error_string());
    428 
    429 	return result;
    430 }
    431 
    432 function void
    433 execute_study(s8 study, Arena arena, Stream path, Options *options)
    434 {
    435 	fprintf(stderr, "showing: %.*s\n", (i32)study.len, study.data);
    436 
    437 	stream_ensure_termination(&path, OS_PATH_SEPARATOR_CHAR);
    438 	stream_append_s8(&path, study);
    439 	i32 path_work_index = path.widx;
    440 
    441 	stream_append_s8(&path, s8(".bp"));
    442 	stream_ensure_termination(&path, 0);
    443 
    444 	ZBP_Data raw_data = {0};
    445 	BeamformerSimpleParameters bp = {0};
    446 	if (!beamformer_simple_parameters_from_zbp_file(&bp, (char *)path.data, &raw_data))
    447 		die("failed to load parameters file: %s\n", (char *)path.data);
    448 
    449 	v3 min_coordinate = (v3){{g_lateral_extent.x, g_axial_extent.x, 0}};
    450 	v3 max_coordinate = (v3){{g_lateral_extent.y, g_axial_extent.y, 0}};
    451 	bp.das_voxel_transform = das_transform(min_coordinate, max_coordinate, &g_output_points);
    452 
    453 	bp.output_points.xyz = g_output_points;
    454 	bp.output_points.w   = 1;
    455 
    456 	bp.f_number           = g_f_number;
    457 	bp.interpolation_mode = BeamformerInterpolationMode_Cubic;
    458 
    459 	bp.decimation_rate = 1;
    460 
    461 	if (bp.data_kind != BeamformerDataKind_Float32Complex &&
    462 	    bp.data_kind != BeamformerDataKind_Int16Complex)
    463 	{
    464 		bp.compute_stages[bp.compute_stages_count++] = BeamformerShaderKind_Demodulate;
    465 	}
    466 	if (options->cuda) bp.compute_stages[bp.compute_stages_count++] = BeamformerShaderKind_CudaDecode;
    467 	else               bp.compute_stages[bp.compute_stages_count++] = BeamformerShaderKind_Decode;
    468 	bp.compute_stages[bp.compute_stages_count++] = BeamformerShaderKind_DAS;
    469 
    470 	{
    471 		BeamformerFilterParameters filter = {0};
    472 		BeamformerFilterKind filter_kind  = 0;
    473 		b32 complex = 0;
    474 		u32 size    = 0;
    475 
    476 		BeamformerEmissionParameters *ep = &bp.emission_parameters;
    477 		switch (bp.emission_kind) {
    478 
    479 		case BeamformerEmissionKind_Sine:{
    480 			filter_kind = BeamformerFilterKind_Kaiser;
    481 			filter.kaiser.beta             = 5.65f;
    482 			filter.kaiser.cutoff_frequency = 0.5f * ep->sine.frequency;
    483 			filter.kaiser.length           = 36;
    484 			size = sizeof(filter.kaiser);
    485 		}break;
    486 
    487 		case BeamformerEmissionKind_Chirp:{
    488 			filter_kind = BeamformerFilterKind_MatchedChirp;
    489 
    490 			filter.matched_chirp.duration      = ep->chirp.duration;
    491 			filter.matched_chirp.min_frequency = ep->chirp.min_frequency - bp.demodulation_frequency;
    492 			filter.matched_chirp.max_frequency = ep->chirp.max_frequency - bp.demodulation_frequency;
    493 			size = sizeof(filter.matched_chirp);
    494 			complex = 1;
    495 
    496 			//bp.time_offset += ep->chirp.duration / 2;
    497 		}break;
    498 
    499 		InvalidDefaultCase;
    500 		}
    501 
    502 		beamformer_create_filter(filter_kind, (f32 *)&filter.kaiser, size, bp.sampling_frequency / 2,
    503 		                         complex, 0, 0);
    504 
    505 		bp.compute_stage_parameters[0] = 0;
    506 	}
    507 
    508 	beamformer_push_simple_parameters(&bp);
    509 
    510 	beamformer_set_global_timeout(1000);
    511 
    512 	void *data = 0;
    513 	if (raw_data.bytes.len == 0) {
    514 		stream_reset(&path, path_work_index);
    515 		stream_append_byte(&path, '_');
    516 		stream_append_u64_width(&path, options->frame_number, 2);
    517 		stream_append_s8(&path, s8(".zst"));
    518 		stream_ensure_termination(&path, 0);
    519 		s8 compressed_data = os_read_file_simp((char *)path.data);
    520 
    521 		data = decompress_zstd_data(compressed_data);
    522 		if (!data)
    523 			die("failed to decompress data: %s\n", path.data);
    524 		free(compressed_data.data);
    525 	} else {
    526 		if (raw_data.compression_kind == ZBP_DataCompressionKind_ZSTD) {
    527 			data = decompress_zstd_data(raw_data.bytes);
    528 			if (!data)
    529 				die("failed to decompress data: %s\n", path.data);
    530 		} else {
    531 			data = raw_data.bytes.data;
    532 		}
    533 	}
    534 
    535 	if (options->loop) {
    536 		BeamformerLiveImagingParameters lip = {.active = 1, .save_enabled = 1};
    537 		s8 short_name = s8("Throughput");
    538 		mem_copy(lip.save_name_tag, short_name.data, (uz)short_name.len);
    539 		lip.save_name_tag_length = (i32)short_name.len;
    540 		beamformer_set_live_parameters(&lip);
    541 
    542 		u32 frame = 0;
    543 		f32 times[32] = {0};
    544 		f32 data_size = (f32)(bp.raw_data_dimensions.E[0] * bp.raw_data_dimensions.E[1]
    545 		                      * beamformer_data_kind_byte_size[bp.data_kind]);
    546 		u64 start = os_timer_count();
    547 		f64 frequency = os_timer_frequency();
    548 		for (;!g_should_exit;) {
    549 			if (send_frame(data, &bp)) {
    550 				u64 now   = os_timer_count();
    551 				f64 delta = (now - start) / frequency;
    552 				start = now;
    553 
    554 				if ((frame % 16) == 0) {
    555 					f32 sum = 0;
    556 					for (u32 i = 0; i < countof(times); i++)
    557 						sum += times[i] / countof(times);
    558 					printf("Frame Time: %8.3f [ms] | 32-Frame Average: %8.3f [ms] | %8.3f GB/s\n",
    559 					       delta * 1e3, sum * 1e3, data_size / (sum * (GB(1))));
    560 				}
    561 
    562 				times[frame % countof(times)] = delta;
    563 				frame++;
    564 			}
    565 			i32 flag = beamformer_live_parameters_get_dirty_flag();
    566 			if (flag != -1 && (1 << flag) == BeamformerLiveImagingDirtyFlags_StopImaging)
    567 				break;
    568 		}
    569 
    570 		lip.active = 0;
    571 		beamformer_set_live_parameters(&lip);
    572 	} else {
    573 		send_frame(data, &bp);
    574 	}
    575 }
    576 
    577 function void
    578 sigint(i32 _signo)
    579 {
    580 	g_should_exit = 1;
    581 }
    582 
    583 extern i32
    584 main(i32 argc, char *argv[])
    585 {
    586 	Options options = parse_argv(argc, argv);
    587 
    588 	if (!BETWEEN(options.remaining_count, 1, 2))
    589 		usage(argv[0]);
    590 
    591 	signal(SIGINT, sigint);
    592 
    593 	Arena arena = os_alloc_arena(KB(8));
    594 	Stream path = stream_alloc(&arena, KB(4));
    595 	stream_append_s8(&path, c_str_to_s8(options.remaining[0]));
    596 
    597 	execute_study(c_str_to_s8(options.remaining[1]), arena, path, &options);
    598 
    599 	return 0;
    600 }