diff --git a/acquire-driver-common/src/simcams/simulated.camera.c b/acquire-driver-common/src/simcams/simulated.camera.c index 672d12a..677a86a 100644 --- a/acquire-driver-common/src/simcams/simulated.camera.c +++ b/acquire-driver-common/src/simcams/simulated.camera.c @@ -67,12 +67,14 @@ struct SimulatedCamera struct { - void* data; + void* frame_data; // for storing the image + void* render_data; // for rendering the image struct ImageShape shape; struct lock lock; int64_t frame_id; int64_t last_emitted_frame_id; struct condition_variable frame_ready; + uint8_t frame_wanted; } im; struct @@ -192,6 +194,19 @@ compute_strides(struct ImageShape* shape) st[i] = st[i - 1] * dims[i - 1]; } +static void* +checked_realloc(void* in, size_t nbytes) +{ + void* out = realloc(in, nbytes); + EXPECT(out, "Allocation of %llu bytes failed.", nbytes); + +Finalize: + return out; +Error: + free(in); + goto Finalize; +} + static void compute_full_resolution_shape_and_offset(const struct SimulatedCamera* self, struct ImageShape* shape, @@ -217,20 +232,38 @@ simulated_camera_streamer_thread(struct SimulatedCamera* self) { clock_init(&self->streamer.throttle); + int64_t frame_id = self->im.frame_id; while (self->streamer.is_running) { struct ImageShape full = { 0 }; uint32_t origin[2] = { 0, 0 }; ECHO(lock_acquire(&self->im.lock)); + while (self->properties.input_triggers.frame_start.enable && + !self->software_trigger.triggered) { + ECHO(condition_variable_wait(&self->software_trigger.trigger_ready, + &self->im.lock)); + } + self->software_trigger.triggered = 0; + + // compute the full resolution shape and offset ECHO(compute_full_resolution_shape_and_offset(self, &full, origin)); + const float exposure_time_ms = + self->properties.exposure_time_us * 1e-3f; + ECHO(lock_release(&self->im.lock)); + + clock_tic(&self->streamer.throttle); + + // generate the image switch (self->kind) { case BasicDevice_Camera_Random: - im_fill_rand(&full, self->im.data); + im_fill_rand(&full, self->im.render_data); break; case BasicDevice_Camera_Sin: - ECHO(im_fill_pattern( - &full, (float)origin[0], (float)origin[1], self->im.data)); + ECHO(im_fill_pattern(&full, + (float)origin[0], + (float)origin[1], + self->im.render_data)); break; case BasicDevice_Camera_Empty: break; // do nothing @@ -239,35 +272,44 @@ simulated_camera_streamer_thread(struct SimulatedCamera* self) "Unexpected index for the kind of simulated camera. Got: %d", self->kind); } - { + + // apply binning if applicable + if (self->properties.binning > 1) { int w = full.dims.width; int h = full.dims.height; int b = self->properties.binning >> 1; while (b) { - ECHO(bin2(self->im.data, w, h)); + ECHO(bin2(self->im.render_data, w, h)); b >>= 1; w >>= 1; h >>= 1; } } - if (self->properties.input_triggers.frame_start.enable) { - while (!self->software_trigger.triggered) { - ECHO(condition_variable_wait( - &self->software_trigger.trigger_ready, &self->im.lock)); - } - self->software_trigger.triggered = 0; + ++frame_id; + + // sleep for the remainder of the exposure time + float toc = (float)clock_toc_ms(&self->streamer.throttle); + if (self->streamer.is_running) { + clock_sleep_ms(&self->streamer.throttle, exposure_time_ms - toc); } - self->hardware_timestamp = clock_tic(0); - ++self->im.frame_id; + if (self->im.frame_wanted) { + ECHO(lock_acquire(&self->im.lock)); - ECHO(condition_variable_notify_all(&self->im.frame_ready)); - ECHO(lock_release(&self->im.lock)); + { + void* const tmp = self->im.frame_data; + self->im.frame_data = self->im.render_data; + self->im.render_data = tmp; + } + + self->hardware_timestamp = clock_tic(0); + self->im.frame_id = frame_id; + self->im.frame_wanted = 0; - if (self->streamer.is_running) - clock_sleep_ms(&self->streamer.throttle, - self->properties.exposure_time_us * 1e-3f); + ECHO(condition_variable_notify_all(&self->im.frame_ready)); + ECHO(lock_release(&self->im.lock)); + } } } @@ -335,9 +377,10 @@ simcam_set(struct Camera* camera, struct CameraProperties* settings) if (!settings->binning) settings->binning = 1; - EXPECT(popcount_u8(settings->binning) == 1, - "Binning must be a power of two. Got %d.", - settings->binning); + if (popcount_u8(settings->binning) != 1) { + LOGE("Binning must be a power of two. Got %d.", settings->binning); + return Device_Err; + } if (self->properties.input_triggers.frame_start.enable && !settings->input_triggers.frame_start.enable) { @@ -345,6 +388,9 @@ simcam_set(struct Camera* camera, struct CameraProperties* settings) simcam_execute_trigger(camera); } + enum DeviceStatusCode status = Device_Ok; + + ECHO(lock_acquire(&self->im.lock)); self->properties = *settings; self->properties.pixel_type = settings->pixel_type; self->properties.input_triggers = (struct camera_properties_input_triggers_s){ @@ -376,12 +422,15 @@ simcam_set(struct Camera* camera, struct CameraProperties* settings) }; size_t nbytes = aligned_bytes_of_image(shape); - self->im.data = malloc(nbytes); - EXPECT(self->im.data, "Allocation of %llu bytes failed.", nbytes); + CHECK(self->im.frame_data = checked_realloc(self->im.frame_data, nbytes)); + CHECK(self->im.render_data = checked_realloc(self->im.render_data, nbytes)); - return Device_Ok; +Finalize: + lock_release(&self->im.lock); + return status; Error: - return Device_Err; + status = Device_Err; + goto Finalize; } static enum DeviceStatusCode @@ -426,6 +475,7 @@ simcam_execute_trigger(struct Camera* camera) containerof(camera, struct SimulatedCamera, camera); lock_acquire(&self->im.lock); + self->im.frame_wanted = 1; self->software_trigger.triggered = 1; condition_variable_notify_all(&self->software_trigger.trigger_ready); lock_release(&self->im.lock); @@ -464,6 +514,8 @@ simcam_get_frame(struct Camera* camera, self->im.last_emitted_frame_id, self->im.frame_id); ECHO(lock_acquire(&self->im.lock)); + self->im.frame_wanted = 1; + while (self->streamer.is_running && self->im.last_emitted_frame_id >= self->im.frame_id) { ECHO(condition_variable_wait(&self->im.frame_ready, &self->im.lock)); @@ -473,7 +525,7 @@ simcam_get_frame(struct Camera* camera, goto Shutdown; } - memcpy(im, self->im.data, bytes_of_image(&self->im.shape)); // NOLINT + memcpy(im, self->im.frame_data, bytes_of_image(&self->im.shape)); // NOLINT info_out->shape = self->im.shape; info_out->hardware_frame_id = self->im.frame_id; info_out->hardware_timestamp = self->hardware_timestamp; @@ -491,8 +543,9 @@ simcam_close_camera(struct Camera* camera_) containerof(camera_, struct SimulatedCamera, camera); EXPECT(camera_, "Invalid NULL parameter"); simcam_stop(&camera->camera); - if (camera->im.data) - free(camera->im.data); + + free(camera->im.frame_data); + free(camera->im.render_data); free(camera); return Device_Ok; Error: @@ -521,7 +574,8 @@ simcam_make_camera(enum BasicDeviceKind kind) .properties = properties, .kind=kind, .im={ - .data=0, + .frame_data=0, + .render_data=0, .shape = { .dims = { .channels = 1, @@ -537,6 +591,7 @@ simcam_make_camera(enum BasicDeviceKind kind) }, .type=properties.pixel_type }, + .frame_wanted = 0, }, .camera={ .state = DeviceState_AwaitingConfiguration, diff --git a/acquire-driver-common/tests/integration/CMakeLists.txt b/acquire-driver-common/tests/integration/CMakeLists.txt index c1ca5ea..47e1ccf 100644 --- a/acquire-driver-common/tests/integration/CMakeLists.txt +++ b/acquire-driver-common/tests/integration/CMakeLists.txt @@ -14,6 +14,7 @@ else () can-set-with-file-uri configure-triggering list-digital-lines + simcam-will-not-stall software-trigger-acquires-single-frames switch-storage-identifier write-side-by-side-tiff diff --git a/acquire-driver-common/tests/integration/simcam-will-not-stall.cpp b/acquire-driver-common/tests/integration/simcam-will-not-stall.cpp new file mode 100644 index 0000000..d9c280a --- /dev/null +++ b/acquire-driver-common/tests/integration/simcam-will-not-stall.cpp @@ -0,0 +1,202 @@ +/// @file simcam-will-not-stall.cpp +/// Test that we can acquire frames from a slow-moving camera without hanging. + +#include "acquire.h" +#include "device/hal/device.manager.h" +#include "platform.h" +#include "logger.h" + +#include +#include +#include +#include + +namespace { +class IntrospectiveLogger +{ + public: + IntrospectiveLogger() + : dropped_frames(0) + , re("Dropped\\s*(\\d+)"){}; + + // inspect for "[stream 0] Dropped", otherwise pass the message through + void report_and_inspect(int is_error, + const char* file, + int line, + const char* function, + const char* msg) + { + std::string m(msg); + + std::smatch matches; + if (std::regex_search(m, matches, re)) { + dropped_frames += std::stoi(matches[1]); + } + + printf("%s%s(%d) - %s: %s\n", + is_error ? "ERROR " : "", + file, + line, + function, + msg); + } + + size_t get_dropped_frames() const { return dropped_frames; } + void reset() { dropped_frames = 0; } + + private: + size_t dropped_frames; + std::regex re; +} introspective_logger; +} // namespace + +static void +reporter(int is_error, + const char* file, + int line, + const char* function, + const char* msg) +{ + introspective_logger.report_and_inspect( + is_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)) + +const static size_t frame_count = 100; + +void +configure(AcquireRuntime* runtime, std::string_view camera_type) +{ + CHECK(runtime); + + const DeviceManager* dm = acquire_device_manager(runtime); + CHECK(dm); + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + DEVOK(device_manager_select(dm, + DeviceKind_Camera, + camera_type.data(), + camera_type.size(), + &props.video[0].camera.identifier)); + DEVOK(device_manager_select(dm, + DeviceKind_Storage, + SIZED("trash") - 1, + &props.video[0].storage.identifier)); + + OK(acquire_configure(runtime, &props)); + + props.video[0].camera.settings.binning = 1; + props.video[0].camera.settings.pixel_type = SampleType_u16; + props.video[0].camera.settings.shape = { + .x = 1920, + .y = 1080, + }; + props.video[0].camera.settings.exposure_time_us = 1; // very small exposure + + props.video[0].max_frame_count = frame_count; + + OK(acquire_configure(runtime, &props)); +} + +void +acquire(AcquireRuntime* runtime) +{ + const auto next = [](VideoFrame* cur) -> VideoFrame* { + return (VideoFrame*)(((uint8_t*)cur) + cur->bytes_of_frame); + }; + + const auto consumed_bytes = [](const VideoFrame* const cur, + const VideoFrame* const end) -> size_t { + return (uint8_t*)end - (uint8_t*)cur; + }; + + AcquireProperties props = {}; + OK(acquire_get_configuration(runtime, &props)); + + // expected time to acquire frames + 100% + static double time_limit_ms = + (props.video[0].max_frame_count / 3.0) * 1000.0 * 2.0; + + struct clock clock = {}; + 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)) { + ++nframes; + } + + uint32_t n = (uint32_t)consumed_bytes(beg, end); + OK(acquire_unmap_read(runtime, 0, n)); + + clock_sleep_ms(&throttle, 100.0f); + } + + 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, "simulated.*sin*"); + acquire(runtime); + CHECK(introspective_logger.get_dropped_frames() < frame_count); + + introspective_logger.reset(); + + configure(runtime, "simulated.*empty.*"); + acquire(runtime); + CHECK(introspective_logger.get_dropped_frames() >= frame_count); + + retval = 0; + } catch (const std::exception& e) { + ERR("Exception: %s", e.what()); + } + + acquire_shutdown(runtime); + + return retval; +}