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 }