diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fe0561..8093e34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Users can specify access key ID and secret access key for S3 storage in `StorageProperties`. + ### Fixed - A bug where changing device identifiers for the storage device was not being handled correctly. @@ -15,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `reserve_image_shape` is now called in `acquire_configure` rather than `acquire_start`. +- Users can now specify the names, ordering, and number of acquisition dimensions. +- The `StorageProperties::filename` field is now `StorageProperties::uri`. +- Files can be specified by URI with an optional `file://` prefix. ## 0.2.0 - 2024-01-05 diff --git a/README.md b/README.md index 92fb96b..17ed016 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ This is an Acquire Driver that exposes commonly used devices. - **tiff** - Streams to a [bigtiff] file. Metadata is stored in the `ImageDescription` tag for each frame as a `json` string. - **tiff-json** - Stores the video stream in a *bigtiff* (as above) and stores metadata in a `json` file. Both are - located in a folder identified by the `filename` property. + located in a folder identified by the `uri` property. - **Trash** - Writes nothing. Discards incoming data. [bigtiff]: http://bigtiff.org/ diff --git a/acquire-core-libs/src/acquire-device-properties/device/props/components.c b/acquire-core-libs/src/acquire-device-properties/device/props/components.c index 67c7ee2..2a80801 100644 --- a/acquire-core-libs/src/acquire-device-properties/device/props/components.c +++ b/acquire-core-libs/src/acquire-device-properties/device/props/components.c @@ -52,6 +52,12 @@ bytes_of_type(enum SampleType type) return table[type]; } +size_t +bytes_of_image(const struct ImageShape* const shape) +{ + return shape->strides.planes * bytes_of_type(shape->type); +} + // // UNIT TESTS // diff --git a/acquire-core-libs/src/acquire-device-properties/device/props/components.h b/acquire-core-libs/src/acquire-device-properties/device/props/components.h index 7b0ef82..c1aaca4 100644 --- a/acquire-core-libs/src/acquire-device-properties/device/props/components.h +++ b/acquire-core-libs/src/acquire-device-properties/device/props/components.h @@ -143,6 +143,7 @@ extern "C" const char* sample_type_as_string(enum SampleType type); size_t bytes_of_type(enum SampleType type); + size_t bytes_of_image(const struct ImageShape* shape); #ifdef __cplusplus } diff --git a/acquire-core-libs/src/acquire-device-properties/device/props/storage.c b/acquire-core-libs/src/acquire-device-properties/device/props/storage.c index d16d4aa..d43db6d 100644 --- a/acquire-core-libs/src/acquire-device-properties/device/props/storage.c +++ b/acquire-core-libs/src/acquire-device-properties/device/props/storage.c @@ -1,415 +1,687 @@ -#include "storage.h" -#include "logger.h" - -#include -#include - -#define countof(e) (sizeof(e) / sizeof((e)[0])) - -#define LOG(...) aq_logger(0, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__) -#define LOGE(...) aq_logger(1, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__) -#define EXPECT(e, ...) \ - do { \ - if (!(e)) { \ - LOGE(__VA_ARGS__); \ - goto Error; \ - } \ - } while (0) -#define CHECK(e) EXPECT(e, #e) - -/// Copies contents, reallocating string storage if necessary. -static int -copy_string(struct String* dst, const struct String* src) -{ - const struct String empty = { .is_ref = 1, .str = "", .nbytes = 1 }; - - // if src string is null/empty, make an empty string - if (!(src && src->str && src->nbytes)) { - src = ∅ - } - - if (!dst->str || dst->is_ref) { - // dst string pointer refers to caller-allocated memory. - // Allocate a new string on the heap. - CHECK(dst->str = malloc(src->nbytes)); // NOLINT - dst->nbytes = src->nbytes; - dst->is_ref = 0; // mark as owned - } - - CHECK(dst->is_ref == 0); - if (src->nbytes > dst->nbytes) { - char* str = realloc(dst->str, src->nbytes); - if (!str) { - LOGE("Failed to allocate %llu bytes for string copy.", - (unsigned long long)src->nbytes); - goto Error; - } - dst->str = str; - } - - dst->nbytes = src->nbytes; - - memset(dst->str, 0, dst->nbytes); // NOLINT - memcpy(dst->str, src->str, src->nbytes); // NOLINT - // strings must be null terminated - if (dst->nbytes > 0) - dst->str[dst->nbytes - 1] = '\0'; - return 1; -Error: - return 0; -} - -int -storage_properties_set_filename(struct StorageProperties* out, - const char* filename, - size_t bytes_of_filename) -{ - const struct String s = { .is_ref = 1, - .nbytes = bytes_of_filename, - .str = (char*)filename }; - return copy_string(&out->filename, &s); -} - -int -storage_properties_set_external_metadata(struct StorageProperties* out, - const char* metadata, - size_t bytes_of_metadata) -{ - const struct String s = { .is_ref = 1, - .nbytes = bytes_of_metadata, - .str = (char*)metadata }; - return copy_string(&out->external_metadata_json, &s); -} - -int -storage_properties_set_chunking_props(struct StorageProperties* out, - uint32_t chunk_width, - uint32_t chunk_height, - uint32_t chunk_planes) -{ - CHECK(out); - out->chunk_dims_px.width = chunk_width; - out->chunk_dims_px.height = chunk_height; - out->chunk_dims_px.planes = chunk_planes; - return 1; -Error: - return 0; -} - -int -storage_properties_set_sharding_props(struct StorageProperties* out, - uint32_t shard_width, - uint32_t shard_height, - uint32_t shard_planes) -{ - CHECK(out); - out->shard_dims_chunks.width = shard_width; - out->shard_dims_chunks.height = shard_height; - out->shard_dims_chunks.planes = shard_planes; - return 1; -Error: - return 0; -} - -int -storage_properties_set_enable_multiscale(struct StorageProperties* out, - uint8_t enable) -{ - CHECK(out); - out->enable_multiscale = enable; - return 1; -Error: - return 0; -} - -int -storage_properties_init(struct StorageProperties* out, - uint32_t first_frame_id, - const char* filename, - size_t bytes_of_filename, - const char* metadata, - size_t bytes_of_metadata, - struct PixelScale pixel_scale_um) -{ - // Allocate and copy filename - memset(out, 0, sizeof(*out)); // NOLINT - CHECK(storage_properties_set_filename(out, filename, bytes_of_filename)); - CHECK(storage_properties_set_external_metadata( - out, metadata, bytes_of_metadata)); - out->first_frame_id = first_frame_id; - out->pixel_scale_um = pixel_scale_um; - - return 1; -Error: - return 0; -} - -int -storage_properties_copy(struct StorageProperties* dst, - const struct StorageProperties* src) -{ - // 1. Copy everything except the strings - { - struct String tmp_fname, tmp_meta; - memcpy(&tmp_fname, &dst->filename, sizeof(struct String)); // NOLINT - memcpy(&tmp_meta, // NOLINT - &dst->external_metadata_json, - sizeof(struct String)); - memcpy(dst, src, sizeof(*dst)); // NOLINT - memcpy(&dst->filename, &tmp_fname, sizeof(struct String)); // NOLINT - memcpy(&dst->external_metadata_json, // NOLINT - &tmp_meta, - sizeof(struct String)); - } - - // 2. Reallocate and copy the Strings - CHECK(copy_string(&dst->filename, &src->filename)); - CHECK( - copy_string(&dst->external_metadata_json, &src->external_metadata_json)); - - return 1; -Error: - return 0; -} - -void -storage_properties_destroy(struct StorageProperties* self) -{ - struct String* const strings[] = { &self->filename, - &self->external_metadata_json }; - for (int i = 0; i < countof(strings); ++i) { - if (strings[i]->is_ref == 0 && strings[i]->str) { - free(strings[i]->str); - memset(strings[i], 0, sizeof(struct String)); // NOLINT - } - } -} - -#ifndef NO_UNIT_TESTS - -/// Check that a==b -/// example: `ASSERT_EQ(int,"%d",42,meaning_of_life())` -#define ASSERT_EQ(T, fmt, a, b) \ - do { \ - T a_ = (T)(a); \ - T b_ = (T)(b); \ - EXPECT(a_ == b_, "Expected %s==%s but " fmt "!=" fmt, #a, #b, a_, b_); \ - } while (0) - -int -unit_test__storage__storage_property_string_check() -{ - struct StorageProperties props; - { - const char filename[] = "out.tif"; - const char metadata[] = "{\"hello\":\"world\"}"; - const struct PixelScale pixel_scale_um = { 1, 2 }; - CHECK(storage_properties_init(&props, - 0, - filename, - sizeof(filename), - metadata, - sizeof(metadata), - pixel_scale_um)); - CHECK(props.filename.str[props.filename.nbytes - 1] == '\0'); - ASSERT_EQ(int, "%d", props.filename.nbytes, sizeof(filename)); - ASSERT_EQ(int, "%d", props.filename.is_ref, 0); - - CHECK(props.external_metadata_json - .str[props.external_metadata_json.nbytes - 1] == '\0'); - ASSERT_EQ( - int, "%d", props.external_metadata_json.nbytes, sizeof(metadata)); - ASSERT_EQ(int, "%d", props.external_metadata_json.is_ref, 0); - ASSERT_EQ(double, "%g", props.pixel_scale_um.x, 1); - ASSERT_EQ(double, "%g", props.pixel_scale_um.y, 2); - } - - { - const char filename[] = "longer_file_name.tif"; - const char metadata[] = "{\"hello\":\"world\"}"; - const struct PixelScale pixel_scale_um = { 1, 2 }; - struct StorageProperties src; - CHECK( // NOLINT - storage_properties_init(&src, - 0, - filename, - sizeof(filename), - metadata, - sizeof(metadata), - pixel_scale_um)); - CHECK(src.filename.str[src.filename.nbytes - 1] == '\0'); - CHECK(src.filename.nbytes == sizeof(filename)); - CHECK(src.filename.is_ref == 0); - CHECK(src.pixel_scale_um.x == 1); - CHECK(src.pixel_scale_um.y == 2); - - CHECK(src.external_metadata_json - .str[src.external_metadata_json.nbytes - 1] == '\0'); - CHECK(src.external_metadata_json.nbytes == sizeof(metadata)); - CHECK(src.external_metadata_json.is_ref == 0); - - CHECK(storage_properties_copy(&props, &src)); - storage_properties_destroy(&src); - CHECK(props.filename.str[props.filename.nbytes - 1] == '\0'); - CHECK(props.filename.nbytes == sizeof(filename)); - CHECK(props.filename.is_ref == 0); - - CHECK(props.external_metadata_json - .str[props.external_metadata_json.nbytes - 1] == '\0'); - CHECK(props.external_metadata_json.nbytes == sizeof(metadata)); - CHECK(props.external_metadata_json.is_ref == 0); - CHECK(props.pixel_scale_um.x == 1); - CHECK(props.pixel_scale_um.y == 2); - } - storage_properties_destroy(&props); - return 1; -Error: - storage_properties_destroy(&props); - return 0; -} - -int -unit_test__storage__copy_string() -{ - const char* abcde = "abcde"; - const char* vwxyz = "vwxyz"; - const char* fghi = "fghi"; - const char* jklmno = "jklmno"; - - struct String src = { .str = (char*)malloc(strlen(abcde) + 1), - .nbytes = strlen(abcde) + 1, - .is_ref = 1 }; - struct String dst = { .str = (char*)malloc(strlen(vwxyz) + 1), - .nbytes = strlen(vwxyz) + 1, - .is_ref = 0 }; - CHECK(src.str); - CHECK(dst.str); - - // dst is_ref = 1 - // lengths equal - memcpy(src.str, abcde, strlen(abcde) + 1); - memcpy(dst.str, vwxyz, strlen(vwxyz) + 1); - CHECK(copy_string(&dst, &src)); - // src should be unchanged - CHECK(0 == strcmp(src.str, abcde)); - CHECK(strlen(abcde) + 1 == src.nbytes); - CHECK(1 == src.is_ref); - - // dst should be identical to src, except is_ref - CHECK(0 == strcmp(dst.str, src.str)); - CHECK(dst.nbytes == src.nbytes); - CHECK(0 == dst.is_ref); // no matter what happens, this String is owned - - // copy longer to shorter - memcpy(dst.str, fghi, strlen(fghi) + 1); - dst.nbytes = strlen(fghi) + 1; - dst.is_ref = 1; - - CHECK(copy_string(&dst, &src)); - CHECK(0 == strcmp(dst.str, src.str)); - CHECK(dst.nbytes == src.nbytes); - CHECK(0 == dst.is_ref); // no matter what happens, this String is owned - - // copy shorter to longer - memcpy(dst.str, jklmno, strlen(jklmno) + 1); - dst.nbytes = strlen(jklmno) + 1; - dst.is_ref = 1; - - CHECK(copy_string(&dst, &src)); - CHECK(0 == strcmp(dst.str, src.str)); - CHECK(dst.nbytes == src.nbytes); - CHECK(0 == dst.is_ref); // no matter what happens, this String is owned - - // dst is_ref = 0 - // lengths equal - memcpy(dst.str, vwxyz, strlen(vwxyz) + 1); - dst.nbytes = strlen(vwxyz) + 1; - dst.is_ref = 0; - - CHECK(copy_string(&dst, &src)); - // src should be unchanged - CHECK(0 == strcmp(src.str, abcde)); - CHECK(strlen(abcde) + 1 == src.nbytes); - CHECK(1 == src.is_ref); - - // dst should be identical to src, except is_ref - CHECK(0 == strcmp(dst.str, src.str)); - CHECK(dst.nbytes == src.nbytes); - CHECK(0 == dst.is_ref); // no matter what happens, this String is owned - - // copy longer to shorter - memcpy(dst.str, fghi, strlen(fghi) + 1); - dst.nbytes = strlen(fghi) + 1; - dst.is_ref = 0; - - CHECK(copy_string(&dst, &src)); - CHECK(0 == strcmp(dst.str, src.str)); - CHECK(dst.nbytes == src.nbytes); - CHECK(0 == dst.is_ref); // no matter what happens, this String is owned - - // copy shorter to longer - free(dst.str); - CHECK(dst.str = malloc(strlen(jklmno) + 1)); - memcpy(dst.str, jklmno, strlen(jklmno) + 1); - dst.nbytes = strlen(jklmno) + 1; - dst.is_ref = 0; - - CHECK(copy_string(&dst, &src)); - CHECK(0 == strcmp(dst.str, src.str)); - CHECK(dst.nbytes == src.nbytes); - CHECK(0 == dst.is_ref); // no matter what happens, this String is owned - - free(src.str); - free(dst.str); - - return 1; -Error: - return 0; -} - -int -unit_test__storage_properties_set_chunking_props() -{ - struct StorageProperties props = { 0 }; - CHECK(0 == props.chunk_dims_px.width); - CHECK(0 == props.chunk_dims_px.height); - CHECK(0 == props.chunk_dims_px.planes); - - const uint32_t chunk_width = 1, chunk_height = 2, chunk_planes = 3; - CHECK(storage_properties_set_chunking_props( - &props, chunk_width, chunk_height, chunk_planes)); - - CHECK(chunk_width == props.chunk_dims_px.width); - CHECK(chunk_height == props.chunk_dims_px.height); - CHECK(chunk_planes == props.chunk_dims_px.planes); - - storage_properties_destroy(&props); - - return 1; -Error: - return 0; -} - -int -unit_test__storage_properties_set_sharding_props() -{ - struct StorageProperties props = { 0 }; - CHECK(0 == props.shard_dims_chunks.width); - CHECK(0 == props.shard_dims_chunks.height); - CHECK(0 == props.shard_dims_chunks.planes); - - const uint32_t shard_width = 1, shard_height = 2, shard_planes = 3; - CHECK(storage_properties_set_sharding_props( - &props, shard_width, shard_height, shard_planes)); - - CHECK(shard_width == props.shard_dims_chunks.width); - CHECK(shard_height == props.shard_dims_chunks.height); - CHECK(shard_planes == props.shard_dims_chunks.planes); - - storage_properties_destroy(&props); - - return 1; -Error: - return 0; -} -#endif +#include "storage.h" +#include "logger.h" + +#include +#include + +#define countof(e) (sizeof(e) / sizeof((e)[0])) + +#define LOG(...) aq_logger(0, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__) +#define LOGE(...) aq_logger(1, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__) +#define EXPECT(e, ...) \ + do { \ + if (!(e)) { \ + LOGE(__VA_ARGS__); \ + goto Error; \ + } \ + } while (0) +#define CHECK(e) EXPECT(e, #e) + +/// Copies contents, reallocating string storage if necessary. +static int +copy_string(struct String* dst, const struct String* src) +{ + const struct String empty = { .is_ref = 1, .str = "", .nbytes = 1 }; + + // if src string is null/empty, make an empty string + if (!(src && src->str && src->nbytes)) { + src = ∅ + } + + if (!dst->str || dst->is_ref) { + // dst string pointer refers to caller-allocated memory. + // Allocate a new string on the heap. + CHECK(dst->str = malloc(src->nbytes)); // NOLINT + dst->nbytes = src->nbytes; + dst->is_ref = 0; // mark as owned + } + + CHECK(dst->is_ref == 0); + if (src->nbytes > dst->nbytes) { + char* str = realloc(dst->str, src->nbytes); + if (!str) { + LOGE("Failed to allocate %llu bytes for string copy.", + (unsigned long long)src->nbytes); + goto Error; + } + dst->str = str; + } + + dst->nbytes = src->nbytes; + + memset(dst->str, 0, dst->nbytes); // NOLINT + memcpy(dst->str, src->str, src->nbytes); // NOLINT + // strings must be null terminated + if (dst->nbytes > 0) + dst->str[dst->nbytes - 1] = '\0'; + return 1; +Error: + return 0; +} + +static const char* +dimension_type_as_string(enum DimensionType type) +{ + switch (type) { + case DimensionType_Space: + return "Spatial"; + case DimensionType_Channel: + return "Channel"; + case DimensionType_Time: + return "Time"; + case DimensionType_Other: + return "Other"; + default: + return "(unknown)"; + } +} + +static int +storage_dimension_array_init(struct StorageDimension** data, size_t size) +{ + if (size == 0) { + *data = 0; + return 1; + } + + CHECK(*data = malloc(size * sizeof(struct StorageDimension))); + memset(*data, 0, size * sizeof(struct StorageDimension)); // NOLINT + return 1; +Error: + return 0; +} + +static int +storage_properties_dimensions_init(struct StorageProperties* self, size_t size) +{ + CHECK(self); + CHECK(size > 0); + CHECK(self->acquisition_dimensions.data == 0); + + CHECK( + storage_dimension_array_init(&self->acquisition_dimensions.data, size)); + CHECK(self->acquisition_dimensions.data); + + self->acquisition_dimensions.size = size; + + return 1; +Error: + return 0; +} + +static int +storage_dimension_copy(struct StorageDimension* dst, + const struct StorageDimension* src) +{ + CHECK(dst); + CHECK(src); + + CHECK(copy_string(&dst->name, &src->name)); + dst->kind = src->kind; + dst->array_size_px = src->array_size_px; + dst->chunk_size_px = src->chunk_size_px; + dst->shard_size_chunks = src->shard_size_chunks; + + return 1; +Error: + return 0; +} + +static void +storage_dimension_destroy(struct StorageDimension* self) +{ + CHECK(self); + + if (self->name.is_ref == 0 && self->name.str) { + free(self->name.str); + } + + memset(self, 0, sizeof(*self)); // NOLINT +Error:; +} + +static void +storage_properties_dimensions_destroy(struct StorageProperties* self) +{ + CHECK(self); + CHECK(self->acquisition_dimensions.data); + + // destroy each dimension + for (int i = 0; i < self->acquisition_dimensions.size; ++i) { + storage_dimension_destroy(&self->acquisition_dimensions.data[i]); + } + + // destroy the array + free(self->acquisition_dimensions.data); + + memset( + &self->acquisition_dimensions, 0, sizeof(self->acquisition_dimensions)); +Error:; +} + +int +storage_properties_set_uri(struct StorageProperties* out, + const char* uri, + size_t bytes_of_uri) +{ + const struct String s = { .is_ref = 1, + .nbytes = bytes_of_uri, + .str = (char*)uri }; + return copy_string(&out->uri, &s); +} + +int +storage_properties_set_external_metadata(struct StorageProperties* out, + const char* metadata, + size_t bytes_of_metadata) +{ + const struct String s = { .is_ref = 1, + .nbytes = bytes_of_metadata, + .str = (char*)metadata }; + return copy_string(&out->external_metadata_json, &s); +} + +int +storage_properties_set_access_key_and_secret(struct StorageProperties* out, + const char* access_key_id, + size_t bytes_of_access_key_id, + const char* secret_access_key, + size_t bytes_of_secret_access_key) +{ + const struct String s = { .is_ref = 1, + .nbytes = bytes_of_access_key_id, + .str = (char*)access_key_id }; + CHECK(copy_string(&out->access_key_id, &s)); + + const struct String t = { .is_ref = 1, + .nbytes = bytes_of_secret_access_key, + .str = (char*)secret_access_key }; + return copy_string(&out->secret_access_key, &t); +Error: + return 0; +} + +int +storage_properties_set_dimension(struct StorageProperties* out, + int index, + const char* name, + size_t bytes_of_name, + enum DimensionType kind, + uint32_t array_size_px, + uint32_t chunk_size_px, + uint32_t shard_size_chunks) +{ + CHECK(out); + + EXPECT(index < out->acquisition_dimensions.size, + "Index %d out of range [0,%d).", + index, + out->acquisition_dimensions.size); + + EXPECT(name, "Dimension name cannot be null."); + EXPECT(bytes_of_name > 0, "Bytes of name must be positive."); + EXPECT(strlen(name) > 0, "Dimension name cannot be empty."); + EXPECT(kind < DimensionTypeCount, + "Invalid dimension type: %s.", + dimension_type_as_string(kind)); + + struct StorageDimension* dim = &out->acquisition_dimensions.data[index]; + + memset(dim, 0, sizeof(*dim)); // NOLINT + + struct String s = { .is_ref = 1, + .nbytes = bytes_of_name, + .str = (char*)name }; + CHECK(copy_string(&dim->name, &s)); + + dim->kind = kind; + dim->array_size_px = array_size_px; + dim->chunk_size_px = chunk_size_px; + dim->shard_size_chunks = shard_size_chunks; + + return 1; +Error: + return 0; +} + +int +storage_properties_set_enable_multiscale(struct StorageProperties* out, + uint8_t enable) +{ + CHECK(out); + out->enable_multiscale = enable; + return 1; +Error: + return 0; +} + +int +storage_properties_init(struct StorageProperties* out, + uint32_t first_frame_id, + const char* uri, + size_t bytes_of_uri, + const char* metadata, + size_t bytes_of_metadata, + struct PixelScale pixel_scale_um, + uint8_t dimension_count) +{ + // Allocate and copy filename + memset(out, 0, sizeof(*out)); // NOLINT + CHECK(storage_properties_set_uri(out, uri, bytes_of_uri)); + + // Set external metadata + CHECK(storage_properties_set_external_metadata( + out, metadata, bytes_of_metadata)); + + out->first_frame_id = first_frame_id; + out->pixel_scale_um = pixel_scale_um; + + // Initialize the dimensions array + if (dimension_count > 0) { + CHECK(storage_properties_dimensions_init(out, dimension_count)); + } + + return 1; +Error: + return 0; +} + +int +storage_properties_copy(struct StorageProperties* dst, + const struct StorageProperties* src) +{ + // 1. Copy everything except the strings + { + struct String tmp_uri, tmp_meta, tmp_access_key, tmp_secret_key; + memcpy(&tmp_uri, &dst->uri, sizeof(struct String)); // NOLINT + memcpy(&tmp_meta, // NOLINT + &dst->external_metadata_json, + sizeof(struct String)); + memcpy(&tmp_access_key, + &dst->access_key_id, + sizeof(struct String)); // NOLINT + memcpy(&tmp_secret_key, + &dst->secret_access_key, + sizeof(struct String)); // NOLINT + + memcpy(dst, src, sizeof(*dst)); // NOLINT + memcpy(&dst->uri, &tmp_uri, sizeof(struct String)); // NOLINT + memcpy(&dst->external_metadata_json, // NOLINT + &tmp_meta, + sizeof(struct String)); + memcpy(&dst->access_key_id, + &tmp_access_key, + sizeof(struct String)); // NOLINT + memcpy(&dst->secret_access_key, + &tmp_secret_key, + sizeof(struct String)); // NOLINT + } + + // 2. Reallocate and copy the Strings + CHECK(copy_string(&dst->uri, &src->uri)); + CHECK( + copy_string(&dst->external_metadata_json, &src->external_metadata_json)); + CHECK(copy_string(&dst->access_key_id, &src->access_key_id)); + CHECK(copy_string(&dst->secret_access_key, &src->secret_access_key)); + + // 3. Copy the dimensions + if (src->acquisition_dimensions.data) { + storage_properties_dimensions_destroy(dst); + + CHECK(storage_properties_dimensions_init( + dst, src->acquisition_dimensions.size)); + for (size_t i = 0; i < src->acquisition_dimensions.size; ++i) { + CHECK(storage_dimension_copy(&dst->acquisition_dimensions.data[i], + &src->acquisition_dimensions.data[i])); + } + } + + return 1; +Error: + return 0; +} + +void +storage_properties_destroy(struct StorageProperties* self) +{ + struct String* const strings[] = { &self->uri, + &self->external_metadata_json, + &self->access_key_id, + &self->secret_access_key }; + for (int i = 0; i < countof(strings); ++i) { + if (strings[i]->is_ref == 0 && strings[i]->str) { + free(strings[i]->str); + memset(strings[i], 0, sizeof(struct String)); // NOLINT + } + } + + storage_properties_dimensions_destroy(self); +} + +#ifndef NO_UNIT_TESTS + +/// Check that a==b +/// example: `ASSERT_EQ(int,"%d",42,meaning_of_life())` +#define ASSERT_EQ(T, fmt, a, b) \ + do { \ + T a_ = (T)(a); \ + T b_ = (T)(b); \ + EXPECT(a_ == b_, "Expected %s==%s but " fmt "!=" fmt, #a, #b, a_, b_); \ + } while (0) + +int +unit_test__storage__storage_property_string_check() +{ + struct StorageProperties props; + { + const char filename[] = "out.tif"; + const char metadata[] = "{\"hello\":\"world\"}"; + const struct PixelScale pixel_scale_um = { 1, 2 }; + CHECK(storage_properties_init(&props, + 0, + filename, + sizeof(filename), + metadata, + sizeof(metadata), + pixel_scale_um, + 0)); + CHECK(props.uri.str[props.uri.nbytes - 1] == '\0'); + ASSERT_EQ(int, "%d", props.uri.nbytes, sizeof(filename)); + ASSERT_EQ(int, "%d", props.uri.is_ref, 0); + + CHECK(props.external_metadata_json + .str[props.external_metadata_json.nbytes - 1] == '\0'); + ASSERT_EQ( + int, "%d", props.external_metadata_json.nbytes, sizeof(metadata)); + ASSERT_EQ(int, "%d", props.external_metadata_json.is_ref, 0); + ASSERT_EQ(double, "%g", props.pixel_scale_um.x, 1); + ASSERT_EQ(double, "%g", props.pixel_scale_um.y, 2); + } + + { + const char filename[] = "longer_file_name.tif"; + const char metadata[] = "{\"hello\":\"world\"}"; + const struct PixelScale pixel_scale_um = { 1, 2 }; + struct StorageProperties src; + CHECK( // NOLINT + storage_properties_init(&src, + 0, + filename, + sizeof(filename), + metadata, + sizeof(metadata), + pixel_scale_um, + 0)); + CHECK(src.uri.str[src.uri.nbytes - 1] == '\0'); + CHECK(src.uri.nbytes == sizeof(filename)); + CHECK(src.uri.is_ref == 0); + CHECK(src.pixel_scale_um.x == 1); + CHECK(src.pixel_scale_um.y == 2); + + CHECK(src.external_metadata_json + .str[src.external_metadata_json.nbytes - 1] == '\0'); + CHECK(src.external_metadata_json.nbytes == sizeof(metadata)); + CHECK(src.external_metadata_json.is_ref == 0); + + CHECK(storage_properties_copy(&props, &src)); + storage_properties_destroy(&src); + CHECK(props.uri.str[props.uri.nbytes - 1] == '\0'); + CHECK(props.uri.nbytes == sizeof(filename)); + CHECK(props.uri.is_ref == 0); + + CHECK(props.external_metadata_json + .str[props.external_metadata_json.nbytes - 1] == '\0'); + CHECK(props.external_metadata_json.nbytes == sizeof(metadata)); + CHECK(props.external_metadata_json.is_ref == 0); + CHECK(props.pixel_scale_um.x == 1); + CHECK(props.pixel_scale_um.y == 2); + } + storage_properties_destroy(&props); + return 1; +Error: + storage_properties_destroy(&props); + return 0; +} + +int +unit_test__storage__copy_string() +{ + const char* abcde = "abcde"; + const char* vwxyz = "vwxyz"; + const char* fghi = "fghi"; + const char* jklmno = "jklmno"; + + struct String src = { .str = (char*)malloc(strlen(abcde) + 1), + .nbytes = strlen(abcde) + 1, + .is_ref = 1 }; + struct String dst = { .str = (char*)malloc(strlen(vwxyz) + 1), + .nbytes = strlen(vwxyz) + 1, + .is_ref = 0 }; + CHECK(src.str); + CHECK(dst.str); + + // dst is_ref = 1 + // lengths equal + memcpy(src.str, abcde, strlen(abcde) + 1); + memcpy(dst.str, vwxyz, strlen(vwxyz) + 1); + CHECK(copy_string(&dst, &src)); + // src should be unchanged + CHECK(0 == strcmp(src.str, abcde)); + CHECK(strlen(abcde) + 1 == src.nbytes); + CHECK(1 == src.is_ref); + + // dst should be identical to src, except is_ref + CHECK(0 == strcmp(dst.str, src.str)); + CHECK(dst.nbytes == src.nbytes); + CHECK(0 == dst.is_ref); // no matter what happens, this String is owned + + // copy longer to shorter + memcpy(dst.str, fghi, strlen(fghi) + 1); + dst.nbytes = strlen(fghi) + 1; + dst.is_ref = 1; + + CHECK(copy_string(&dst, &src)); + CHECK(0 == strcmp(dst.str, src.str)); + CHECK(dst.nbytes == src.nbytes); + CHECK(0 == dst.is_ref); // no matter what happens, this String is owned + + // copy shorter to longer + memcpy(dst.str, jklmno, strlen(jklmno) + 1); + dst.nbytes = strlen(jklmno) + 1; + dst.is_ref = 1; + + CHECK(copy_string(&dst, &src)); + CHECK(0 == strcmp(dst.str, src.str)); + CHECK(dst.nbytes == src.nbytes); + CHECK(0 == dst.is_ref); // no matter what happens, this String is owned + + // dst is_ref = 0 + // lengths equal + memcpy(dst.str, vwxyz, strlen(vwxyz) + 1); + dst.nbytes = strlen(vwxyz) + 1; + dst.is_ref = 0; + + CHECK(copy_string(&dst, &src)); + // src should be unchanged + CHECK(0 == strcmp(src.str, abcde)); + CHECK(strlen(abcde) + 1 == src.nbytes); + CHECK(1 == src.is_ref); + + // dst should be identical to src, except is_ref + CHECK(0 == strcmp(dst.str, src.str)); + CHECK(dst.nbytes == src.nbytes); + CHECK(0 == dst.is_ref); // no matter what happens, this String is owned + + // copy longer to shorter + memcpy(dst.str, fghi, strlen(fghi) + 1); + dst.nbytes = strlen(fghi) + 1; + dst.is_ref = 0; + + CHECK(copy_string(&dst, &src)); + CHECK(0 == strcmp(dst.str, src.str)); + CHECK(dst.nbytes == src.nbytes); + CHECK(0 == dst.is_ref); // no matter what happens, this String is owned + + // copy shorter to longer + free(dst.str); + CHECK(dst.str = malloc(strlen(jklmno) + 1)); + memcpy(dst.str, jklmno, strlen(jklmno) + 1); + dst.nbytes = strlen(jklmno) + 1; + dst.is_ref = 0; + + CHECK(copy_string(&dst, &src)); + CHECK(0 == strcmp(dst.str, src.str)); + CHECK(dst.nbytes == src.nbytes); + CHECK(0 == dst.is_ref); // no matter what happens, this String is owned + + free(src.str); + free(dst.str); + + return 1; +Error: + return 0; +} + +int +unit_test__storage_properties_set_access_key_and_secret() +{ + struct StorageProperties props = { 0 }; + const char access_key_id[] = "access_key_id"; + const char secret_access_key[] = "secret_access_key"; + + CHECK( + storage_properties_set_access_key_and_secret(&props, + access_key_id, + sizeof(access_key_id), + secret_access_key, + sizeof(secret_access_key))); + + CHECK(0 == strcmp(props.access_key_id.str, access_key_id)); + CHECK(props.access_key_id.nbytes == sizeof(access_key_id)); + CHECK(0 == props.access_key_id.is_ref); + + CHECK(0 == strcmp(props.secret_access_key.str, secret_access_key)); + CHECK(props.secret_access_key.nbytes == sizeof(secret_access_key)); + CHECK(0 == props.secret_access_key.is_ref); + + return 1; +Error: + return 0; +} + +int +unit_test__dimension_init() +{ + struct StorageProperties props = { 0 }; + CHECK(storage_properties_dimensions_init(&props, 1)); + + struct StorageDimension* dim = &props.acquisition_dimensions.data[0]; + + // can't set with a null char pointer + CHECK(!storage_properties_set_dimension( + &props, 0, NULL, 0, DimensionType_Space, 1, 1, 1)); + CHECK(dim->name.str == NULL); + CHECK(dim->kind == DimensionType_Space); + CHECK(dim->array_size_px == 0); + CHECK(dim->chunk_size_px == 0); + CHECK(dim->shard_size_chunks == 0); + + // can't set with 0 bytes + CHECK(!storage_properties_set_dimension( + &props, 0, "", 0, DimensionType_Space, 1, 1, 1)); + CHECK(dim->name.str == NULL); + CHECK(dim->kind == DimensionType_Space); + CHECK(dim->array_size_px == 0); + CHECK(dim->chunk_size_px == 0); + CHECK(dim->shard_size_chunks == 0); + + // can't set with an empty name + CHECK(!storage_properties_set_dimension( + &props, 0, "", 1, DimensionType_Space, 1, 1, 1)); + CHECK(dim->name.str == NULL); + CHECK(dim->kind == DimensionType_Space); + CHECK(dim->array_size_px == 0); + CHECK(dim->chunk_size_px == 0); + CHECK(dim->shard_size_chunks == 0); + + // can't set with an invalid dimension type + CHECK(!storage_properties_set_dimension( + &props, 0, "x", 2, DimensionTypeCount, 1, 1, 1)); + CHECK(dim->name.str == NULL); + CHECK(dim->kind == DimensionType_Space); + CHECK(dim->array_size_px == 0); + CHECK(dim->chunk_size_px == 0); + CHECK(dim->shard_size_chunks == 0); + + // can't set beyond the size of the array + CHECK(!storage_properties_set_dimension( + &props, 1, "x", 2, DimensionType_Space, 1, 1, 1)); + CHECK(dim->name.str == NULL); + CHECK(dim->kind == DimensionType_Space); + CHECK(dim->array_size_px == 0); + CHECK(dim->chunk_size_px == 0); + CHECK(dim->shard_size_chunks == 0); + + // set with valid values + CHECK(storage_properties_set_dimension( + &props, 0, "x", 2, DimensionType_Space, 1, 1, 1)); + CHECK(0 == strcmp(dim->name.str, "x")); + CHECK(dim->kind == DimensionType_Space); + CHECK(dim->array_size_px == 1); + CHECK(dim->chunk_size_px == 1); + CHECK(dim->shard_size_chunks == 1); + + return 1; +Error: + return 0; +} + +int +unit_test__storage_properties_dimensions_init() +{ + struct StorageProperties props = { 0 }; + CHECK(storage_properties_dimensions_init(&props, 5)); + + CHECK(props.acquisition_dimensions.size == 5); + CHECK(props.acquisition_dimensions.data != NULL); + + free(props.acquisition_dimensions.data); + + return 1; +Error: + return 0; +} + +int +unit_test__storage_properties_dimensions_destroy() +{ + struct StorageProperties props = { 0 }; + + props.acquisition_dimensions.data = + malloc(5 * sizeof(struct StorageDimension)); + props.acquisition_dimensions.size = 5; + memset(props.acquisition_dimensions.data, + 0, + 5 * sizeof(struct StorageDimension)); + + storage_properties_dimensions_destroy(&props); + CHECK(props.acquisition_dimensions.size == 0); + CHECK(props.acquisition_dimensions.data == NULL); + + return 1; +Error: + return 0; +} + +int +unit_test__dimension_type_as_string__is_defined_for_all() +{ + for (int i = 0; i < DimensionTypeCount; ++i) { + // Check this isn't returning "(unknown)" for known values + CHECK(dimension_type_as_string(i)[0] != '('); + } + return 1; +Error: + return 0; +} +#endif diff --git a/acquire-core-libs/src/acquire-device-properties/device/props/storage.h b/acquire-core-libs/src/acquire-device-properties/device/props/storage.h index 709d978..8133a28 100644 --- a/acquire-core-libs/src/acquire-device-properties/device/props/storage.h +++ b/acquire-core-libs/src/acquire-device-properties/device/props/storage.h @@ -1,160 +1,191 @@ -#ifndef H_ACQUIRE_PROPS_STORAGE_V0 -#define H_ACQUIRE_PROPS_STORAGE_V0 - -#include "components.h" -#include "metadata.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - struct DeviceManager; - struct VideoFrame; - - /// Properties for a storage driver. - struct StorageProperties - { - struct String filename; - struct String external_metadata_json; - uint32_t first_frame_id; - struct PixelScale pixel_scale_um; - - /// Dimensions of chunks, in pixels. - struct storage_properties_chunking_s - { - uint32_t width, height, planes; - } chunk_dims_px; - - /// Dimensions of shards, in chunks. - struct storage_properties_sharding_s - { - uint32_t width, height, planes; - } shard_dims_chunks; - - /// Enable multiscale storage if true. - uint8_t enable_multiscale; - }; - - struct StoragePropertyMetadata - { - /// Metadata for chunking. - /// Indicates whether chunking is supported, and if so, bounds on what - /// the dimensions (in px) of the chunks are. - struct storage_property_metadata_chunking_s - { - uint8_t is_supported; - struct Property width, height, planes; - } chunk_dims_px; - - /// Metadata for sharding. - /// Indicates whether sharding is supported, and if so, bounds on what - /// the dimensions (in chunks) of the shards are. - struct storage_property_metadata_sharding_s - { - uint8_t is_supported; - struct Property width, height, planes; - } shard_dims_chunks; - - struct storage_property_metadata_multiscale_s - { - uint8_t is_supported; - } multiscale; - }; - - /// Initializes StorageProperties, allocating string storage on the heap - /// and filling out the struct fields. - /// @returns 0 when `bytes_of_out` is not large enough, otherwise 1. - /// @param[out] out The constructed StorageProperties object. - /// @param[in] first_frame_id (unused; aiming for future file rollover - /// support - /// @param[in] filename A c-style null-terminated string. The file to create - /// for streaming. - /// @param[in] bytes_of_filename Number of bytes in the `filename` buffer - /// including the terminating null. - /// @param[in] metadata A c-style null-terminated string. Metadata string - /// to save along side the created file. - /// @param[in] bytes_of_metadata Number of bytes in the `metadata` buffer - /// including the terminating null. - /// @param[in] pixel_scale_um The pixel scale or size in microns. - int storage_properties_init(struct StorageProperties* out, - uint32_t first_frame_id, - const char* filename, - size_t bytes_of_filename, - const char* metadata, - size_t bytes_of_metadata, - struct PixelScale pixel_scale_um); - - /// Copies contents, reallocating string storage if necessary. - /// @returns 1 on success, otherwise 0 - /// @param[in,out] dst Must be zero initialized or previously initialized - /// via `storage_properties_init()` - /// @param[in] src Copied to `dst` - int storage_properties_copy(struct StorageProperties* dst, - const struct StorageProperties* src); - - /// @brief Set the filename string in `out`. - /// Copies the string into storage owned by the properties struct. - /// @returns 1 on success, otherwise 0 - /// @param[in,out] out The storage properties to change. - /// @param[in] filename pointer to the beginning of the filename buffer. - /// @param[in] bytes_of_filename the number of bytes in the filename buffer. - /// Should include the terminating NULL. - int storage_properties_set_filename(struct StorageProperties* out, - const char* filename, - size_t bytes_of_filename); - - /// @brief Set the metadata string in `out`. - /// Copies the string into storage owned by the properties struct. - /// @returns 1 on success, otherwise 0 - /// @param[in,out] out The storage properties to change. - /// @param[in] metadata pointer to the beginning of the metadata buffer. - /// @param[in] bytes_of_filename the number of bytes in the metadata buffer. - /// Should include the terminating NULL. - int storage_properties_set_external_metadata(struct StorageProperties* out, - const char* metadata, - size_t bytes_of_metadata); - - /// @brief Set chunking properties for `out`. - /// Convenience function to set chunking properties in a single call. - /// @returns 1 on success, otherwise 0 - /// @param[in, out] out The storage properties to change. - /// @param[in] chunk_width The width, in px, of a chunk. - /// @param[in] chunk_height The height, in px, of a chunk. - /// @param[in] chunk_planes The number of @p chunk_width x @p chunk_height - /// planes in a single chunk. - int storage_properties_set_chunking_props(struct StorageProperties* out, - uint32_t chunk_width, - uint32_t chunk_height, - uint32_t chunk_planes); - - /// @brief Set sharding properties for `out`. - /// Convenience function to set sharding properties in a single call. - /// @returns 1 on success, otherwise 0 - /// @param[in, out] out The storage properties to change. - /// @param[in] shard_width The number of chunks in a shard along the x - /// dimension. - /// @param[in] shard_height The number of chunks in a shard along the y - /// dimension. - /// @param[in] shard_planes The number of chunks in a shard along the append - /// dimension. - int storage_properties_set_sharding_props(struct StorageProperties* out, - uint32_t shard_width, - uint32_t shard_height, - uint32_t shard_planes); - - /// @brief Set multiscale properties for `out`. - /// Convenience function to enable multiscale. - /// @returns 1 on success, otherwise 0 - /// @param[in, out] out The storage properties to change. - /// @param[in] enable A flag to enable or disable multiscale. - int storage_properties_set_enable_multiscale(struct StorageProperties* out, - uint8_t enable); - - /// Free's allocated string storage. - void storage_properties_destroy(struct StorageProperties* self); - -#ifdef __cplusplus -} -#endif - -#endif // H_ACQUIRE_PROPS_STORAGE_V0 +#ifndef H_ACQUIRE_PROPS_STORAGE_V0 +#define H_ACQUIRE_PROPS_STORAGE_V0 + +#include "components.h" +#include "metadata.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + struct DeviceManager; + struct VideoFrame; + + enum DimensionType + { + DimensionType_Space = 0, + DimensionType_Channel, + DimensionType_Time, + DimensionType_Other, + DimensionTypeCount + }; + + struct StorageDimension + { + // the name of the dimension as it appears in the metadata, e.g., + // "x", "y", "z", "c", "t" + struct String name; + + // the type of dimension, e.g., spatial, channel, time + enum DimensionType kind; + + // the expected size of the full output array along this dimension + uint32_t array_size_px; + + // the size of a chunk along this dimension + uint32_t chunk_size_px; + + // the number of chunks in a shard along this dimension + uint32_t shard_size_chunks; + }; + + /// Properties for a storage driver. + struct StorageProperties + { + struct String uri; + struct String external_metadata_json; + struct String access_key_id; + struct String secret_access_key; + + uint32_t first_frame_id; + struct PixelScale pixel_scale_um; + + /// Dimensions of the output array, with array extents, chunk sizes, and + /// shard sizes. The first dimension is the fastest varying dimension. + /// The last dimension is the append dimension. + struct storage_properties_dimensions_s + { + // The dimensions of the output array. + struct StorageDimension* data; + + // The number of dimensions in the output array. + size_t size; + } acquisition_dimensions; + + /// Enable multiscale storage if true. + uint8_t enable_multiscale; + }; + + struct StoragePropertyMetadata + { + uint8_t chunking_is_supported; + uint8_t sharding_is_supported; + uint8_t multiscale_is_supported; + uint8_t s3_is_supported; + }; + + /// Initializes StorageProperties, allocating string storage on the heap + /// and filling out the struct fields. + /// @returns 0 when `bytes_of_out` is not large enough, otherwise 1. + /// @param[out] out The constructed StorageProperties object. + /// @param[in] first_frame_id (unused; aiming for future file rollover + /// support + /// @param[in] uri A c-style null-terminated string. The file to create + /// for streaming. + /// @param[in] bytes_of_uri Number of bytes in the `uri` buffer, + /// including the terminating null. + /// @param[in] metadata A c-style null-terminated string. Metadata string + /// to save along side the created file. + /// @param[in] bytes_of_metadata Number of bytes in the `metadata` buffer + /// including the terminating null. + /// @param[in] pixel_scale_um The pixel scale or size in microns. + /// @param[in] dimension_count The number of dimensions in the storage + /// array. Each of the @p dimension_count dimensions will be initialized + /// to zero. + int storage_properties_init(struct StorageProperties* out, + uint32_t first_frame_id, + const char* uri, + size_t bytes_of_uri, + const char* metadata, + size_t bytes_of_metadata, + struct PixelScale pixel_scale_um, + uint8_t dimension_count); + + /// Copies contents, reallocating string storage if necessary. + /// @returns 1 on success, otherwise 0 + /// @param[in,out] dst Must be zero initialized or previously initialized + /// via `storage_properties_init()` + /// @param[in] src Copied to `dst` + int storage_properties_copy(struct StorageProperties* dst, + const struct StorageProperties* src); + + /// @brief Set the uri string in `out`. + /// Copies the string into storage owned by the properties struct. + /// @returns 1 on success, otherwise 0 + /// @param[in,out] out The storage properties to change. + /// @param[in] uri Pointer to the beginning of the uri buffer. + /// @param[in] bytes_of_uri The number of bytes in the uri buffer. + /// Should include the terminating NULL. + int storage_properties_set_uri(struct StorageProperties* out, + const char* uri, + size_t bytes_of_uri); + + /// @brief Set the metadata string in `out`. + /// Copies the string into storage owned by the properties struct. + /// @returns 1 on success, otherwise 0 + /// @param[in,out] out The storage properties to change. + /// @param[in] metadata pointer to the beginning of the metadata buffer. + /// @param[in] bytes_of_metadata the number of bytes in the metadata buffer. + /// Should include the terminating NULL. + int storage_properties_set_external_metadata(struct StorageProperties* out, + const char* metadata, + size_t bytes_of_metadata); + + /// @brief Set the access key id string in `out`. + /// Copies the string into storage owned by the properties struct. + /// @returns 1 on success, otherwise 0 + /// @param[in,out] out The storage properties to change. + /// @param[in] access_key_id Pointer to the beginning of the access key id + /// buffer. + /// @param[in] bytes_of_access_key_id The number of bytes in the access key + /// id. + /// @param[in] secret_access_key Pointer to the beginning of the secret + /// access key buffer. + /// @param[in] bytes_of_secret_access_key The number of bytes in the secret + /// access key. + int storage_properties_set_access_key_and_secret( + struct StorageProperties* out, + const char* access_key_id, + size_t bytes_of_access_key_id, + const char* secret_access_key, + size_t bytes_of_secret_access_key); + + /// @brief Set the value of the StorageDimension struct at index `index` in + /// `out`. + /// @param[out] out The StorageProperties struct containing the + /// StorageDimension array. + /// @param[in] index The index of the dimension to set. + /// @param[in] name The name of the dimension. + /// @param[in] bytes_of_name The number of bytes in the name buffer. + /// Should include the terminating NULL. + /// @param[in] kind The type of dimension. + /// @param[in] array_size_px The size of the array along this dimension. + /// @param[in] chunk_size_px The size of a chunk along this dimension. + /// @param[in] shard_size_chunks The number of chunks in a shard along this + /// dimension. + /// @returns 1 on success, otherwise 0 + int storage_properties_set_dimension(struct StorageProperties* out, + int index, + const char* name, + size_t bytes_of_name, + enum DimensionType kind, + uint32_t array_size_px, + uint32_t chunk_size_px, + uint32_t shard_size_chunks); + + /// @brief Set multiscale properties for `out`. + /// Convenience function to enable multiscale. + /// @returns 1 on success, otherwise 0 + /// @param[in, out] out The storage properties to change. + /// @param[in] enable A flag to enable or disable multiscale. + int storage_properties_set_enable_multiscale(struct StorageProperties* out, + uint8_t enable); + + /// Free allocated string storage. + void storage_properties_destroy(struct StorageProperties* self); + +#ifdef __cplusplus +} +#endif + +#endif // H_ACQUIRE_PROPS_STORAGE_V0 diff --git a/acquire-core-libs/tests/unit-tests.cpp b/acquire-core-libs/tests/unit-tests.cpp index b9d81ec..02ab050 100644 --- a/acquire-core-libs/tests/unit-tests.cpp +++ b/acquire-core-libs/tests/unit-tests.cpp @@ -70,11 +70,14 @@ extern "C" // device-properties int unit_test__storage__storage_property_string_check(); int unit_test__storage__copy_string(); - int unit_test__storage_properties_set_chunking_props(); - int unit_test__storage_properties_set_sharding_props(); + int unit_test__storage_properties_set_access_key_and_secret(); + int unit_test__dimension_init(); + int unit_test__storage_properties_dimensions_init(); + int unit_test__storage_properties_dimensions_destroy(); int unit_test__device_state_as_string__is_defined_for_all(); int unit_test__device_kind_as_string__is_defined_for_all(); int unit_test__sample_type_as_string__is_defined_for_all(); + int unit_test__dimension_type_as_string__is_defined_for_all(); int unit_test__bytes_of_type__is_defined_for_all(); } @@ -96,11 +99,14 @@ main() CASE(unit_test__monotonic_clock_increases_monotonically), CASE(unit_test__storage__storage_property_string_check), CASE(unit_test__storage__copy_string), - CASE(unit_test__storage_properties_set_chunking_props), - CASE(unit_test__storage_properties_set_sharding_props), + CASE(unit_test__storage_properties_set_access_key_and_secret), + CASE(unit_test__dimension_init), + CASE(unit_test__storage_properties_dimensions_init), + CASE(unit_test__storage_properties_dimensions_destroy), CASE(unit_test__device_state_as_string__is_defined_for_all), CASE(unit_test__device_kind_as_string__is_defined_for_all), CASE(unit_test__sample_type_as_string__is_defined_for_all), + CASE(unit_test__dimension_type_as_string__is_defined_for_all), CASE(unit_test__bytes_of_type__is_defined_for_all), #undef CASE }; diff --git a/acquire-driver-common/src/simcams/simulated.camera.c b/acquire-driver-common/src/simcams/simulated.camera.c index 3910d3c..1d85486 100644 --- a/acquire-driver-common/src/simcams/simulated.camera.c +++ b/acquire-driver-common/src/simcams/simulated.camera.c @@ -88,12 +88,6 @@ struct SimulatedCamera struct Camera camera; }; -static size_t -bytes_of_image(const struct ImageShape* const shape) -{ - return shape->strides.planes * bytes_of_type(shape->type); -} - static size_t aligned_bytes_of_image(const struct ImageShape* const shape) { diff --git a/acquire-driver-common/src/storage/raw.c b/acquire-driver-common/src/storage/raw.c index 2447bba..a20264e 100644 --- a/acquire-driver-common/src/storage/raw.c +++ b/acquire-driver-common/src/storage/raw.c @@ -35,8 +35,15 @@ static enum DeviceState raw_set(struct Storage* self_, const struct StorageProperties* properties) { struct Raw* self = containerof(self_, struct Raw, writer); - const char* filename = properties->filename.str; - const size_t nbytes = properties->filename.nbytes; + CHECK(properties->uri.str); + CHECK(properties->uri.nbytes); + + const size_t offset = strlen(properties->uri.str) >= 7 && + strncmp(properties->uri.str, "file://", 7) == 0 + ? 7 + : 0; + const char* filename = properties->uri.str + offset; + const size_t nbytes = properties->uri.nbytes - offset; // Validate CHECK(file_is_writable(filename, nbytes)); @@ -44,6 +51,11 @@ raw_set(struct Storage* self_, const struct StorageProperties* properties) // copy in the properties CHECK(storage_properties_copy(&self->properties, properties)); + // update the URI if it has a file:// prefix + if (offset) { + storage_properties_set_uri(&self->properties, filename, nbytes); + } + return DeviceState_Armed; Error: return DeviceState_AwaitingConfiguration; @@ -69,9 +81,8 @@ static enum DeviceState raw_start(struct Storage* self_) { struct Raw* self = containerof(self_, struct Raw, writer); - CHECK(file_create(&self->file, - self->properties.filename.str, - self->properties.filename.nbytes)); + CHECK(file_create( + &self->file, self->properties.uri.str, self->properties.uri.nbytes)); LOG("RAW: Frame header size %d bytes", (int)sizeof(struct VideoFrame)); return DeviceState_Running; Error: @@ -132,7 +143,8 @@ raw_init() sizeof("out.raw"), 0, 0, - pixel_scale_um)); + pixel_scale_um, + 0)); self->writer = (struct Storage){ .state = DeviceState_AwaitingConfiguration, .set = raw_set, diff --git a/acquire-driver-common/src/storage/side-by-side-tiff.cpp b/acquire-driver-common/src/storage/side-by-side-tiff.cpp index b9b837e..e0fa238 100644 --- a/acquire-driver-common/src/storage/side-by-side-tiff.cpp +++ b/acquire-driver-common/src/storage/side-by-side-tiff.cpp @@ -17,6 +17,7 @@ #include "logger.h" #include +#include #include #include @@ -51,8 +52,7 @@ struct SideBySideTiff fs::path as_path(const StorageProperties& props) { - return { props.filename.str, - props.filename.str + props.filename.nbytes - 1 }; + return { props.uri.str, props.uri.str + props.uri.nbytes - 1 }; } void @@ -88,8 +88,16 @@ validate(const struct StorageProperties* props) props->external_metadata_json.nbytes); { - fs::path path(props->filename.str, - props->filename.str + props->filename.nbytes); + CHECK(props->uri.str); + CHECK(props->uri.nbytes); + + const size_t offset = strlen(props->uri.str) >= 7 && + strncmp(props->uri.str, "file://", 7) == 0 + ? 7 + : 0; + + fs::path path(props->uri.str + offset, + (props->uri.str + offset) + (props->uri.nbytes - offset)); auto parent_path = path.parent_path(); if (parent_path.empty()) { parent_path = fs::path("."); @@ -110,7 +118,19 @@ side_by_side_tiff_set(struct Storage* self_, struct SideBySideTiff* self = containerof(self_, struct SideBySideTiff, storage); validate(props); - self->props = *props; + CHECK(storage_properties_copy(&self->props, props)); + + const size_t offset = strlen(props->uri.str) >= 7 && + strncmp(props->uri.str, "file://", 7) == 0 + ? 7 + : 0; + + // update the URI if it has a file:// prefix + if (offset) { + const char* filename = props->uri.str + offset; + const size_t nbytes = props->uri.nbytes - offset; + CHECK(storage_properties_set_uri(&self->props, filename, nbytes)); + } } catch (const std::exception& e) { LOGE("Exception: %s\n", e.what()); @@ -190,7 +210,7 @@ side_by_side_tiff_start(struct Storage* self_) noexcept const auto video_path = (path / "data.tif").generic_string(); StorageProperties props{}; storage_properties_copy(&props, &self->props); - props.filename = { + props.uri = { .str = (char*)video_path.c_str(), .nbytes = video_path.length(), .is_ref = 1, diff --git a/acquire-driver-common/src/storage/tiff.cpp b/acquire-driver-common/src/storage/tiff.cpp index e672b96..46858ce 100644 --- a/acquire-driver-common/src/storage/tiff.cpp +++ b/acquire-driver-common/src/storage/tiff.cpp @@ -4,6 +4,7 @@ #include "platform.h" #include +#include #include #include #include @@ -424,10 +425,15 @@ validate_json(const char* str, size_t nbytes) int Tiff::set(const struct StorageProperties* settings) noexcept { - EXPECT(settings->filename.str, "Filename string is NULL."); - EXPECT(settings->filename.nbytes, "Filename string is zero size."); + EXPECT(settings->uri.str, "Filename string is NULL."); + EXPECT(settings->uri.nbytes, "Filename string is zero size."); { - string filename(settings->filename.str); + const size_t offset = strlen(settings->uri.str) >= 7 && + strncmp(settings->uri.str, "file://", 7) == 0 + ? 7 + : 0; + string filename(settings->uri.str + offset, + settings->uri.nbytes - offset); // Validate and copy the filename CHECK(file_is_writable(filename.c_str(), filename.length())); @@ -451,8 +457,8 @@ Tiff::set(const struct StorageProperties* settings) noexcept void Tiff::get(struct StorageProperties* settings) const noexcept { - settings->filename.str = (char*)filename_.c_str(); - settings->filename.nbytes = filename_.size(); + settings->uri.str = (char*)filename_.c_str(); + settings->uri.nbytes = filename_.size(); settings->pixel_scale_um = pixel_scale_um_; } diff --git a/acquire-driver-common/tests/devkit/storage-get-meta.cpp b/acquire-driver-common/tests/devkit/storage-get-meta.cpp index 937acac..15c8812 100644 --- a/acquire-driver-common/tests/devkit/storage-get-meta.cpp +++ b/acquire-driver-common/tests/devkit/storage-get-meta.cpp @@ -72,9 +72,10 @@ main() storage = containerof(device, struct Storage, device); CHECK(Device_Ok == storage_get_meta(storage, &metadata)); - CHECK(0 == metadata.chunk_dims_px.is_supported); - CHECK(0 == metadata.shard_dims_chunks.is_supported); - CHECK(0 == metadata.multiscale.is_supported); + CHECK(0 == metadata.chunking_is_supported); + CHECK(0 == metadata.sharding_is_supported); + CHECK(0 == metadata.multiscale_is_supported); + CHECK(0 == metadata.s3_is_supported); CHECK(Device_Ok == driver_close_device(device)); } diff --git a/acquire-driver-common/tests/integration/CMakeLists.txt b/acquire-driver-common/tests/integration/CMakeLists.txt index 4f0d257..47e1ccf 100644 --- a/acquire-driver-common/tests/integration/CMakeLists.txt +++ b/acquire-driver-common/tests/integration/CMakeLists.txt @@ -11,6 +11,7 @@ else () # set(tests abort-while-waiting-for-trigger + can-set-with-file-uri configure-triggering list-digital-lines simcam-will-not-stall diff --git a/acquire-driver-common/tests/integration/abort-while-waiting-for-trigger.cpp b/acquire-driver-common/tests/integration/abort-while-waiting-for-trigger.cpp index f5ba794..31d4b64 100644 --- a/acquire-driver-common/tests/integration/abort-while-waiting-for-trigger.cpp +++ b/acquire-driver-common/tests/integration/abort-while-waiting-for-trigger.cpp @@ -8,6 +8,7 @@ #include "logger.h" #include "platform.h" #include +#include #include void diff --git a/acquire-driver-common/tests/integration/can-set-with-file-uri.cpp b/acquire-driver-common/tests/integration/can-set-with-file-uri.cpp new file mode 100644 index 0000000..755e1b2 --- /dev/null +++ b/acquire-driver-common/tests/integration/can-set-with-file-uri.cpp @@ -0,0 +1,233 @@ +/// @file can-set-with-file-uri.cpp +/// Test that we can use a file:// URI when configuring basic Storage devices. + +#include "acquire.h" +#include "device/hal/device.manager.h" +#include "logger.h" + +#include +#include +#include +#include + +namespace fs = std::filesystem; + +void +reporter(int is_error, + const char* file, + int line, + const char* function, + const char* msg) +{ + fprintf(is_error ? stderr : stdout, + "%s%s(%d) - %s: %s\n", + is_error ? "ERROR " : "", + file, + line, + function, + msg); +} + +/// Helper for passing size static strings as function args. +/// For a function: `f(char*,size_t)` use `f(SIZED("hello"))`. +/// Expands to `f("hello",5)`. +#define SIZED(str) str, sizeof(str) - 1 + +#define L (aq_logger) +#define LOG(...) L(0, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__) +#define ERR(...) L(1, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__) +#define EXPECT(e, ...) \ + do { \ + if (!(e)) { \ + char buf[1 << 8] = { 0 }; \ + ERR(__VA_ARGS__); \ + snprintf(buf, sizeof(buf) - 1, __VA_ARGS__); \ + throw std::runtime_error(buf); \ + } \ + } while (0) +#define CHECK(e) EXPECT(e, "Expression evaluated as false: %s", #e) +#define DEVOK(e) CHECK(Device_Ok == (e)) +#define OK(e) CHECK(AcquireStatus_Ok == (e)) + +const static uint32_t nframes = 32; + +void +configure_camera(AcquireRuntime* runtime) +{ + CHECK(runtime); + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + auto* dm = acquire_device_manager(runtime); + CHECK(dm); + + DEVOK(device_manager_select(dm, + DeviceKind_Camera, + SIZED(".*empty.*"), + &props.video[0].camera.identifier)); + + props.video[0].camera.settings.binning = 1; + props.video[0].camera.settings.pixel_type = SampleType_u8; + props.video[0].camera.settings.shape = { .x = 64, .y = 48 }; + props.video[0].camera.settings.exposure_time_us = 1e4; + props.video[0].max_frame_count = nframes; + + OK(acquire_configure(runtime, &props)); +} + +void +configure_storage_tiff(AcquireRuntime* runtime, const std::string& filename) +{ + CHECK(runtime); + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + auto* dm = acquire_device_manager(runtime); + CHECK(dm); + + props.video[0].storage = { 0 }; + DEVOK(device_manager_select(dm, + DeviceKind_Storage, + SIZED("tiff"), + &props.video[0].storage.identifier)); + + storage_properties_set_uri( + &props.video[0].storage.settings, filename.c_str(), filename.size() + 1); + + OK(acquire_configure(runtime, &props)); +} + +void +validate_storage_tiff(AcquireRuntime* runtime) +{ + const std::string file_path = TEST ".tif"; + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + CHECK(0 == strncmp(props.video[0].storage.settings.uri.str, + file_path.c_str(), + file_path.size() + 1)); +} + +void +configure_storage_side_by_side_tiff(AcquireRuntime* runtime, + const std::string& filename) +{ + CHECK(runtime); + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + auto* dm = acquire_device_manager(runtime); + CHECK(dm); + + props.video[0].storage = { 0 }; + DEVOK(device_manager_select(dm, + DeviceKind_Storage, + SIZED("tiff-json"), + &props.video[0].storage.identifier)); + + storage_properties_set_uri( + &props.video[0].storage.settings, filename.c_str(), filename.size() + 1); + + OK(acquire_configure(runtime, &props)); +} + +void +validate_storage_side_by_side_tiff(AcquireRuntime* runtime) +{ + const std::string file_path = TEST ".tif"; + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + CHECK(0 == strncmp(props.video[0].storage.settings.uri.str, + file_path.c_str(), + file_path.size() + 1)); +} + +void +configure_storage_raw(AcquireRuntime* runtime, const std::string& filename) +{ + CHECK(runtime); + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + auto* dm = acquire_device_manager(runtime); + CHECK(dm); + + props.video[0].storage = { 0 }; + DEVOK(device_manager_select(dm, + DeviceKind_Storage, + SIZED("Raw"), + &props.video[0].storage.identifier)); + + storage_properties_set_uri(&props.video[0].storage.settings, + filename.c_str(), + filename.size() + 1); + + OK(acquire_configure(runtime, &props)); +} + +void +validate_storage_raw(AcquireRuntime* runtime) +{ + const std::string file_path = TEST ".bin"; + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + CHECK(0 == strncmp(props.video[0].storage.settings.uri.str, + file_path.c_str(), + file_path.size() + 1)); +} + +void +acquire(AcquireRuntime* runtime) +{ + CHECK(runtime); + + OK(acquire_start(runtime)); + OK(acquire_stop(runtime)); +} + +int +main() +{ + auto* runtime = acquire_init(reporter); + try { + configure_camera(runtime); + + configure_storage_raw(runtime, TEST ".bin"); + validate_storage_raw(runtime); + + configure_storage_raw(runtime, "file://" TEST ".bin"); + validate_storage_raw(runtime); + + configure_storage_tiff(runtime, TEST ".tif"); + validate_storage_tiff(runtime); + + configure_storage_tiff(runtime, "file://" TEST ".tif"); + validate_storage_tiff(runtime); + + configure_storage_side_by_side_tiff(runtime, TEST ".tif"); + validate_storage_side_by_side_tiff(runtime); + + configure_storage_side_by_side_tiff(runtime, "file://" TEST ".tif"); + validate_storage_side_by_side_tiff(runtime); + + } catch (const std::exception& exc) { + acquire_shutdown(runtime); + return 1; + } catch (...) { + acquire_shutdown(runtime); + return 1; + } + + acquire_shutdown(runtime); + return 0; +} \ No newline at end of file diff --git a/acquire-driver-common/tests/integration/switch-storage-identifier.cpp b/acquire-driver-common/tests/integration/switch-storage-identifier.cpp index 0fe0247..6ed0d79 100644 --- a/acquire-driver-common/tests/integration/switch-storage-identifier.cpp +++ b/acquire-driver-common/tests/integration/switch-storage-identifier.cpp @@ -112,8 +112,8 @@ configure_storage_tiff(AcquireRuntime* runtime) SIZED("tiff"), &props.video[0].storage.identifier)); - storage_properties_set_filename(&props.video[0].storage.settings, - SIZED(TEST ".tif") + 1); + storage_properties_set_uri(&props.video[0].storage.settings, + SIZED(TEST ".tif") + 1); OK(acquire_configure(runtime, &props)); } @@ -150,8 +150,8 @@ configure_storage_raw(AcquireRuntime* runtime) SIZED("Raw"), &props.video[0].storage.identifier)); - storage_properties_set_filename(&props.video[0].storage.settings, - SIZED(TEST ".bin") + 1); + storage_properties_set_uri(&props.video[0].storage.settings, + SIZED(TEST ".bin") + 1); OK(acquire_configure(runtime, &props)); } diff --git a/acquire-driver-common/tests/integration/write-side-by-side-tiff.cpp b/acquire-driver-common/tests/integration/write-side-by-side-tiff.cpp index bf44725..5911125 100644 --- a/acquire-driver-common/tests/integration/write-side-by-side-tiff.cpp +++ b/acquire-driver-common/tests/integration/write-side-by-side-tiff.cpp @@ -1,5 +1,6 @@ /// @file write-side-by-side-tiff.cpp -/// Test that the side-by-side tiff/JSON writer writes a TIFF file and a JSON file. +/// Test that the side-by-side tiff/JSON writer writes a TIFF file and a JSON +/// file. #include "acquire.h" #include "device/hal/device.manager.h" @@ -77,7 +78,7 @@ acquire(AcquireRuntime* runtime, const char* filename) .is_ref = 1, }; - props.video[0].storage.settings.filename = { + props.video[0].storage.settings.uri = { .str = (char*)filename, .nbytes = strlen(filename) + 1, .is_ref = 1, diff --git a/acquire-video-runtime/src/acquire.c b/acquire-video-runtime/src/acquire.c index 367a5b8..b7ddf01 100644 --- a/acquire-video-runtime/src/acquire.c +++ b/acquire-video-runtime/src/acquire.c @@ -12,6 +12,7 @@ #include #include +#undef max #define max(a, b) ((a) < (b) ? (b) : (a)) #define containerof(ptr, T, V) ((T*)(((char*)(ptr)) - offsetof(T, V))) @@ -262,7 +263,8 @@ configure_video_stream(struct video_s* const video, int is_ok = 1; if (pcamera->identifier.kind == DeviceKind_None) { - is_ok &= (Device_Ok == device_manager_select_default( + is_ok &= (Device_Ok == + device_manager_select_default( device_manager, DeviceKind_Camera, &pcamera->identifier)); } @@ -277,8 +279,9 @@ configure_video_stream(struct video_s* const video, pvideo->frame_average_count) == Device_Ok); if (pstorage->identifier.kind == DeviceKind_None) { - is_ok &= (Device_Ok == device_manager_select_default( - device_manager, DeviceKind_Storage, &pstorage->identifier)); + is_ok &= (Device_Ok == + device_manager_select_default( + device_manager, DeviceKind_Storage, &pstorage->identifier)); } is_ok &= (video_sink_configure(&video->sink, device_manager, @@ -559,9 +562,9 @@ acquire_stop(struct AcquireRuntime* self_) ECHO(thread_join(&video->sink.thread)); channel_accept_writes(&video->sink.in, 1); - // Flush the monitor's read region if it hasn't already been released. - // This takes at most 2 iterations. - { + // If the monitor has been initialized and its read region hasn't + // already been released, flush it. This takes at most 2 iterations. + if (video->monitor.reader.id) { size_t nbytes; do { struct slice slice = diff --git a/acquire-video-runtime/src/runtime/filter.c b/acquire-video-runtime/src/runtime/filter.c index b240473..917837e 100644 --- a/acquire-video-runtime/src/runtime/filter.c +++ b/acquire-video-runtime/src/runtime/filter.c @@ -31,12 +31,6 @@ slice_size_bytes(const struct slice* slice) return (uint8_t*)slice->end - (uint8_t*)slice->beg; } -static size_t -bytes_of_image(const struct ImageShape* const shape) -{ - return shape->strides.planes * bytes_of_type(shape->type); -} - static int assert_consistent_shape(const struct VideoFrame* acc, const struct VideoFrame* in) @@ -113,8 +107,11 @@ process_data(struct video_filter_s* self, if (!*accumulator) { struct ImageShape shape = in->shape; shape.type = SampleType_f32; - size_t bytes_of_accumulator = + + const size_t nbytes = bytes_of_image(&shape) + sizeof(struct VideoFrame); + const size_t bytes_of_accumulator = 8*((nbytes+7)/8); + *accumulator = (struct VideoFrame*)channel_write_map( self->out, bytes_of_accumulator); if (*accumulator) { diff --git a/acquire-video-runtime/src/runtime/source.c b/acquire-video-runtime/src/runtime/source.c index c1bdc47..af72dae 100644 --- a/acquire-video-runtime/src/runtime/source.c +++ b/acquire-video-runtime/src/runtime/source.c @@ -24,12 +24,6 @@ } while (0) #define CHECK(e) EXPECT(e, "Expression evaluated as false:\n\t%s", #e) -static size_t -bytes_of_image(const struct ImageShape* const shape) -{ - return shape->strides.planes * bytes_of_type(shape->type); -} - static int check_frame_id(uint8_t stream_id, uint64_t iframe, @@ -66,6 +60,8 @@ video_source_thread(struct video_source_s* self) size_t sz = bytes_of_image(&info.shape); size_t nbytes = sizeof(struct VideoFrame) + sz; + // padding to 8-byte aligned size + const size_t nbytes_aligned = 8*((nbytes+7)/8); struct channel* channel = (self->enable_filter) ? self->to_filter : self->to_sink; @@ -76,7 +72,7 @@ video_source_thread(struct video_source_s* self) last_stream = channel; struct VideoFrame* im = - (struct VideoFrame*)channel_write_map(channel, nbytes); + (struct VideoFrame*)channel_write_map(channel, nbytes_aligned); if (im) { CHECK(camera_get_frame(self->camera, im->data, &sz, &info) == Device_Ok); @@ -88,7 +84,7 @@ video_source_thread(struct video_source_s* self) last_hardware_frame_id = info.hardware_frame_id; *im = (struct VideoFrame){ .shape = info.shape, - .bytes_of_frame = nbytes, + .bytes_of_frame = nbytes_aligned, .frame_id = iframe, .hardware_frame_id = info.hardware_frame_id, .timestamps.hardware = info.hardware_timestamp, diff --git a/acquire-video-runtime/tests/CMakeLists.txt b/acquire-video-runtime/tests/CMakeLists.txt index b5a6c85..efd7969 100644 --- a/acquire-video-runtime/tests/CMakeLists.txt +++ b/acquire-video-runtime/tests/CMakeLists.txt @@ -4,7 +4,7 @@ else () # # PARAMETERS # - set(project acquire-driver-runtime) # CMAKE_PROJECT_NAME gets overridden if this is a subtree of another project + set(project acquire-video-runtime) # CMAKE_PROJECT_NAME gets overridden if this is a subtree of another project set(driver acquire-driver-common) # @@ -27,6 +27,8 @@ else () unit-tests zero-config-start filter-video-average + repeat-start-no-monitor + aligned-videoframe-pointers ) foreach (name ${tests}) diff --git a/acquire-video-runtime/tests/aligned-videoframe-pointers.cpp b/acquire-video-runtime/tests/aligned-videoframe-pointers.cpp new file mode 100644 index 0000000..79f119f --- /dev/null +++ b/acquire-video-runtime/tests/aligned-videoframe-pointers.cpp @@ -0,0 +1,190 @@ +/// @file aligned-videoframe-pointers.cpp +/// Test that VideoFrame pointers are aligned at 8 bytes. + +#include "acquire.h" +#include "device/hal/device.manager.h" +#include "platform.h" +#include "logger.h" + +#include +#include + +void +reporter(int is_error, + const char* file, + int line, + const char* function, + const char* msg) +{ + fprintf(is_error ? stderr : stdout, + "%s%s(%d) - %s: %s\n", + is_error ? "ERROR " : "", + file, + line, + function, + msg); +} + +/// Helper for passing size static strings as function args. +/// For a function: `f(char*,size_t)` use `f(SIZED("hello"))`. +/// Expands to `f("hello",5)`. +#define SIZED(str) str, sizeof(str) + +#define L (aq_logger) +#define LOG(...) L(0, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__) +#define ERR(...) L(1, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__) +#define EXPECT(e, ...) \ + do { \ + if (!(e)) { \ + char buf[1 << 8] = { 0 }; \ + ERR(__VA_ARGS__); \ + snprintf(buf, sizeof(buf) - 1, __VA_ARGS__); \ + throw std::runtime_error(buf); \ + } \ + } while (0) +#define CHECK(e) EXPECT(e, "Expression evaluated as false: %s", #e) +#define DEVOK(e) CHECK(Device_Ok == (e)) +#define OK(e) CHECK(AcquireStatus_Ok == (e)) + +void +configure(AcquireRuntime* runtime) +{ + CHECK(runtime); + const DeviceManager* dm = acquire_device_manager(runtime); + CHECK(dm); + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + // configure camera + DEVOK(device_manager_select(dm, + DeviceKind_Camera, + SIZED("simulated.*empty.*") - 1, + &props.video[0].camera.identifier)); + + // These settings are chosen to exercise the 8-byte alignment constraint. + props.video[0].camera.settings.binning = 1; + props.video[0].camera.settings.pixel_type = SampleType_u8; + props.video[0].camera.settings.shape = { + .x = 33, + .y = 47, + }; + + // configure acquisition + props.video[0].max_frame_count = 10; + + // configure storage + DEVOK(device_manager_select(dm, + DeviceKind_Storage, + SIZED("trash") - 1, + &props.video[0].storage.identifier)); + storage_properties_init( + &props.video[0].storage.settings, 0, nullptr, 0, nullptr, 0, { 0 }, 0); + + OK(acquire_configure(runtime, &props)); + storage_properties_destroy(&props.video[0].storage.settings); +} + +static size_t +align_up(size_t n, size_t align) +{ + return (n + align - 1) & ~(align - 1); +} + +void +acquire(AcquireRuntime* runtime) +{ + CHECK(runtime); + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + const auto next = [](VideoFrame* cur) -> VideoFrame* { + return (VideoFrame*)(((uint8_t*)cur) + + align_up(cur->bytes_of_frame, 8)); + }; + + const auto consumed_bytes = [](const VideoFrame* const cur, + const VideoFrame* const end) -> size_t { + return (uint8_t*)end - (uint8_t*)cur; + }; + + struct clock clock + {}; + // expected time to acquire frames + 100% + static double time_limit_ms = props.video[0].max_frame_count * 1000.0 * 3.0; + clock_init(&clock); + clock_shift_ms(&clock, time_limit_ms); + OK(acquire_start(runtime)); + { + uint64_t nframes = 0; + while (nframes < props.video[0].max_frame_count) { + struct clock throttle + {}; + clock_init(&throttle); + EXPECT(clock_cmp_now(&clock) < 0, + "Timeout at %f ms", + clock_toc_ms(&clock) + time_limit_ms); + + VideoFrame *beg, *end, *cur; + OK(acquire_map_read(runtime, 0, &beg, &end)); + + for (cur = beg; cur < end; cur = next(cur)) { + LOG("stream %d counting frame w id %d", 0, cur->frame_id); + + const size_t unpadded_bytes = + bytes_of_image(&cur->shape) + sizeof(*cur); + const size_t nbytes_aligned = 8*((unpadded_bytes+7)/8); + + // check data is correct + CHECK(bytes_of_image(&cur->shape) == 33 * 47); + CHECK(cur->bytes_of_frame == nbytes_aligned); + CHECK(cur->frame_id == nframes); + CHECK(cur->shape.dims.width == + props.video[0].camera.settings.shape.x); + CHECK(cur->shape.dims.height == + props.video[0].camera.settings.shape.y); + + // check pointer is aligned + CHECK((size_t)cur % 8 == 0); + ++nframes; + } + + { + const auto n = (uint32_t)consumed_bytes(beg, end); + CHECK(n % 8 == 0); + OK(acquire_unmap_read(runtime, 0, n)); + if (n) + LOG("stream %d consumed bytes %d", 0, n); + } + clock_sleep_ms(&throttle, 100.0f); + + LOG("stream %d nframes %d. remaining time %f s", + 0, + nframes, + -1e-3 * clock_toc_ms(&clock)); + } + + CHECK(nframes == props.video[0].max_frame_count); + } + + OK(acquire_stop(runtime)); +} + +int +main() +{ + int retval = 1; + AcquireRuntime* runtime = acquire_init(reporter); + + try { + configure(runtime); + acquire(runtime); + retval = 0; + } catch (const std::exception& e) { + ERR("Exception: %s", e.what()); + } + + acquire_shutdown(runtime); + return retval; +} diff --git a/acquire-video-runtime/tests/change-external-metadata.cpp b/acquire-video-runtime/tests/change-external-metadata.cpp index 5349247..e2b2fca 100644 --- a/acquire-video-runtime/tests/change-external-metadata.cpp +++ b/acquire-video-runtime/tests/change-external-metadata.cpp @@ -58,7 +58,8 @@ acquire(AcquireRuntime* runtime, 0, external_metadata_json, strlen(external_metadata_json) + 1, - { 0, 0 }); + { 0, 0 }, + 0); OK(acquire_configure(runtime, props)); OK(acquire_start(runtime)); @@ -105,6 +106,8 @@ main() acquire(runtime, &props, R"({"hurley": "burley"})"); acquire(runtime, &props, R"({})"); + storage_properties_destroy(&props.video[0].storage.settings); + LOG("DONE (OK)"); acquire_shutdown(runtime); return 0; diff --git a/acquire-video-runtime/tests/change-file-name.cpp b/acquire-video-runtime/tests/change-file-name.cpp index 006713b..4335b28 100644 --- a/acquire-video-runtime/tests/change-file-name.cpp +++ b/acquire-video-runtime/tests/change-file-name.cpp @@ -53,7 +53,7 @@ acquire(AcquireRuntime* runtime, struct AcquireProperties* props, const char* filename) { - storage_properties_set_filename( + storage_properties_set_uri( &props->video[0].storage.settings, filename, strlen(filename) + 1); OK(acquire_configure(runtime, props)); @@ -91,7 +91,7 @@ main() const char filename[] = ""; storage_properties_init( - &props.video[0].storage.settings, 0, SIZED(filename), 0, 0, { 1, 1 }); + &props.video[0].storage.settings, 0, SIZED(filename), 0, 0, { 1, 1 }, 0); acquire(runtime, &props, "out1.tif"); acquire(runtime, &props, "quite a bit longer.tif"); @@ -99,6 +99,8 @@ main() acquire(runtime, &props, "quite a bit longer.tif"); // overwrite? LOG("DONE (OK)"); + storage_properties_destroy(&props.video[0].storage.settings); + acquire_shutdown(runtime); return 0; } diff --git a/acquire-video-runtime/tests/one-video-stream.cpp b/acquire-video-runtime/tests/one-video-stream.cpp index 367bea7..5f5e882 100644 --- a/acquire-video-runtime/tests/one-video-stream.cpp +++ b/acquire-video-runtime/tests/one-video-stream.cpp @@ -3,6 +3,7 @@ #include "acquire.h" #include "device/hal/device.manager.h" +#include "device/props/components.h" #include "platform.h" #include "logger.h" @@ -25,6 +26,12 @@ reporter(int is_error, msg); } +static size_t +bytes_of_frame(const VideoFrame* frame) +{ + return sizeof(*frame) + bytes_of_image(&frame->shape); +} + /// Helper for passing size static strings as function args. /// For a function: `f(char*,size_t)` use `f(SIZED("hello"))`. /// Expands to `f("hello",5)`. @@ -46,13 +53,12 @@ reporter(int is_error, #define DEVOK(e) CHECK(Device_Ok == (e)) #define OK(e) CHECK(AcquireStatus_Ok == (e)) -int -main() +void +configure(AcquireRuntime* runtime) { - - auto runtime = acquire_init(reporter); - auto dm = acquire_device_manager(runtime); CHECK(runtime); + + const DeviceManager* dm = acquire_device_manager(runtime); CHECK(dm); AcquireProperties props = {}; @@ -68,7 +74,7 @@ main() &props.video[0].storage.identifier)); storage_properties_init( - &props.video[0].storage.settings, 0, SIZED("out.tif"), 0, 0, { 0 }); + &props.video[0].storage.settings, 0, SIZED("out.tif"), 0, 0, { 0 }, 0); OK(acquire_configure(runtime, &props)); @@ -84,9 +90,19 @@ main() props.video[0].max_frame_count = 10; OK(acquire_configure(runtime, &props)); + storage_properties_destroy(&props.video[0].storage.settings); +} + +void +acquire(AcquireRuntime* runtime) +{ + CHECK(runtime); + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); const auto next = [](VideoFrame* cur) -> VideoFrame* { - return (VideoFrame*)(((uint8_t*)cur) + cur->bytes_of_frame); + return (VideoFrame*)(((uint8_t*)cur) + bytes_of_frame(cur)); }; const auto consumed_bytes = [](const VideoFrame* const cur, @@ -94,8 +110,7 @@ main() return (uint8_t*)end - (uint8_t*)cur; }; - struct clock clock - {}; + struct clock clock = {}; // expected time to acquire frames + 100% static double time_limit_ms = (props.video[0].max_frame_count / 6.0) * 1000.0 * 2.0; @@ -139,6 +154,24 @@ main() } OK(acquire_stop(runtime)); - OK(acquire_shutdown(runtime)); - return 0; +} + +int +main() +{ + int retval = 1; + AcquireRuntime* runtime = acquire_init(reporter); + + try { + configure(runtime); + acquire(runtime); + retval = 0; + } catch (const std::exception& e) { + ERR("Failed to configure runtime: %s", e.what()); + } catch (...) { + ERR("Failed to configure runtime: unknown error"); + } + + acquire_shutdown(runtime); + return retval; } diff --git a/acquire-video-runtime/tests/repeat-start-no-monitor.cpp b/acquire-video-runtime/tests/repeat-start-no-monitor.cpp new file mode 100644 index 0000000..541cdff --- /dev/null +++ b/acquire-video-runtime/tests/repeat-start-no-monitor.cpp @@ -0,0 +1,115 @@ +/// @file repeat-start-no-monitor.cpp +/// Test that we can repeatedly acquire without unwittingly initializing the +/// monitor reader. + +#include "acquire.h" +#include "device/hal/device.manager.h" +#include "platform.h" +#include "logger.h" + +#include +#include + +void +reporter(int is_error, + const char* file, + int line, + const char* function, + const char* msg) +{ + fprintf(is_error ? stderr : stdout, + "%s%s(%d) - %s: %s\n", + is_error ? "ERROR " : "", + file, + line, + function, + msg); +} + +/// Helper for passing size static strings as function args. +/// For a function: `f(char*,size_t)` use `f(SIZED("hello"))`. +/// Expands to `f("hello",5)`. +#define SIZED(str) str, sizeof(str) + +#define L (aq_logger) +#define LOG(...) L(0, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__) +#define ERR(...) L(1, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__) +#define EXPECT(e, ...) \ + do { \ + if (!(e)) { \ + char buf[1 << 8] = { 0 }; \ + ERR(__VA_ARGS__); \ + snprintf(buf, sizeof(buf) - 1, __VA_ARGS__); \ + throw std::runtime_error(buf); \ + } \ + } while (0) +#define CHECK(e) EXPECT(e, "Expression evaluated as false: %s", #e) +#define DEVOK(e) CHECK(Device_Ok == (e)) +#define OK(e) CHECK(AcquireStatus_Ok == (e)) + +void +configure(AcquireRuntime* runtime) +{ + CHECK(runtime); + + const DeviceManager* dm = acquire_device_manager(runtime); + CHECK(dm); + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + // configure camera + DEVOK(device_manager_select(dm, + DeviceKind_Camera, + SIZED("simulated.*empty.*") - 1, + &props.video[0].camera.identifier)); + + props.video[0].camera.settings.binning = 1; + props.video[0].camera.settings.pixel_type = SampleType_u16; + props.video[0].camera.settings.shape = { + .x = 2304, + .y = 2304, + }; + + // configure acquisition + props.video[0].max_frame_count = 500; + + // configure storage + DEVOK(device_manager_select(dm, + DeviceKind_Storage, + SIZED("trash") - 1, + &props.video[0].storage.identifier)); + storage_properties_init( + &props.video[0].storage.settings, 0, nullptr, 0, nullptr, 0, { 0 }, 0); + + OK(acquire_configure(runtime, &props)); +} + +void +acquire(AcquireRuntime* runtime) +{ + CHECK(runtime); + + OK(acquire_start(runtime)); + OK(acquire_stop(runtime)); +} + +int +main() +{ + AcquireRuntime* runtime = acquire_init(reporter); + + try { + for (auto i = 0; i < 2; ++i) { + configure(runtime); + acquire(runtime); + } + } catch (const std::exception& e) { + ERR("Caught exception: %s", e.what()); + acquire_shutdown(runtime); + return 1; + } + + OK(acquire_shutdown(runtime)); + return 0; +} diff --git a/acquire-video-runtime/tests/two-video-streams.cpp b/acquire-video-runtime/tests/two-video-streams.cpp index 76250aa..455efc5 100644 --- a/acquire-video-runtime/tests/two-video-streams.cpp +++ b/acquire-video-runtime/tests/two-video-streams.cpp @@ -85,7 +85,8 @@ main() sizeof(filenames[0]), external_metadata, sizeof(external_metadata), - px_scale_um)); + px_scale_um, + 0)); CHECK(storage_properties_init(&props.video[1].storage.settings, 0, @@ -93,7 +94,8 @@ main() sizeof(filenames[1]), external_metadata, sizeof(external_metadata), - { .x = 0, .y = 0 })); + { .x = 0, .y = 0 }, + 0)); props.video[0].camera.settings.binning = 1; props.video[0].camera.settings.pixel_type = SampleType_u8; @@ -167,6 +169,9 @@ main() } OK(acquire_stop(runtime)); + storage_properties_destroy(&props.video[0].storage.settings); + storage_properties_destroy(&props.video[1].storage.settings); + acquire_shutdown(runtime); return 0; }