Skip to content

Commit b11b808

Browse files
committed
refactor(vision pipeline): producer preflight/armed lifecycle refactor, heavy checks off hotpath, integration test added.
1 parent 4b94ad0 commit b11b808

16 files changed

Lines changed: 950 additions & 144 deletions

vision/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ if(VISION_BUILD_TESTS)
124124
target_compile_options(rga_preprocess_test PRIVATE ${RGA_TEST_CFLAGS_OTHER})
125125

126126
add_test(NAME rga_preprocess_test COMMAND rga_preprocess_test)
127+
128+
add_executable(producer_pipeline_test
129+
apps/producer_pipeline_test.cpp
130+
src/dma_heap_alloc.cpp
131+
src/image_buffer_pool.cpp
132+
src/pipeline.cpp
133+
src/rga_preprocess.cpp
134+
src/spsc_ring.cpp
135+
src/time_utils.cpp
136+
src/v4l2_capture.cpp
137+
)
138+
target_include_directories(producer_pipeline_test PRIVATE include ${RGA_TEST_INCLUDE_DIRS})
139+
target_link_libraries(producer_pipeline_test PRIVATE Threads::Threads GTest::gtest_main ${RGA_TEST_LIBRARIES})
140+
target_compile_options(producer_pipeline_test PRIVATE ${RGA_TEST_CFLAGS_OTHER})
141+
142+
add_test(NAME producer_pipeline_test COMMAND producer_pipeline_test)
127143
else()
128144
message(STATUS "librga not found; rga_preprocess_test will not be built")
129145
endif()
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#include <cerrno>
2+
#include <chrono>
3+
#include <cstdlib>
4+
#include <cstring>
5+
#include <gtest/gtest.h>
6+
#include <linux/videodev2.h>
7+
#include <string>
8+
#include <thread>
9+
#include <unistd.h>
10+
#include <vector>
11+
12+
#include "omniseer/vision/dma_heap_alloc.hpp"
13+
#include "omniseer/vision/pipeline.hpp"
14+
15+
namespace
16+
{
17+
uint32_t env_u32(const char* key, uint32_t def)
18+
{
19+
const char* v = std::getenv(key);
20+
if (!v || !*v)
21+
return def;
22+
char* end = nullptr;
23+
unsigned long n = std::strtoul(v, &end, 10);
24+
if (end && *end)
25+
return def;
26+
if (n > 0xffffffffUL)
27+
return def;
28+
return static_cast<uint32_t>(n);
29+
}
30+
31+
const char* tick_status_name(omniseer::vision::ProducerTickStatus status)
32+
{
33+
using omniseer::vision::ProducerTickStatus;
34+
switch (status)
35+
{
36+
case ProducerTickStatus::Produced:
37+
return "produced";
38+
case ProducerTickStatus::NoFrame:
39+
return "no-frame";
40+
case ProducerTickStatus::CaptureRetryableError:
41+
return "capture-retryable-error";
42+
case ProducerTickStatus::CaptureFatalError:
43+
return "capture-fatal-error";
44+
case ProducerTickStatus::NoWritableBuffer:
45+
return "no-writable-buffer";
46+
case ProducerTickStatus::PreprocessError:
47+
return "preprocess-error";
48+
}
49+
return "unknown";
50+
}
51+
} // namespace
52+
53+
TEST(ProducerPipeline, PreflightThenProducesFrames)
54+
{
55+
const char* dev_env = std::getenv("VISION_V4L2_DEV");
56+
const std::string dev = (dev_env && *dev_env) ? dev_env : "/dev/video12";
57+
58+
if (::access(dev.c_str(), R_OK | W_OK) != 0)
59+
GTEST_SKIP() << "no access to " << dev << " (" << std::strerror(errno) << ")";
60+
61+
if (::access("/dev/rga", R_OK | W_OK) != 0 && ::access("/dev/rga0", R_OK | W_OK) != 0)
62+
GTEST_SKIP() << "no access to /dev/rga(/dev/rga0) (" << std::strerror(errno) << ")";
63+
64+
const uint32_t src_w = env_u32("VISION_V4L2_W", 1280);
65+
const uint32_t src_h = env_u32("VISION_V4L2_H", 720);
66+
const uint32_t buffers = env_u32("VISION_V4L2_BUFFERS", 4);
67+
const int dst_w = static_cast<int>(env_u32("VISION_DST_W", 640));
68+
const int dst_h = static_cast<int>(env_u32("VISION_DST_H", 640));
69+
70+
omniseer::vision::V4l2Capture capture({
71+
.device = dev,
72+
.width = src_w,
73+
.height = src_h,
74+
.fourcc = V4L2_PIX_FMT_NV12,
75+
.buffer_count = buffers,
76+
});
77+
ASSERT_NO_THROW(capture.start());
78+
79+
omniseer::vision::ImageBufferPool pool;
80+
try
81+
{
82+
omniseer::vision::DmaHeapAllocator allocator;
83+
pool.allocate_all(allocator, dst_w, dst_h, omniseer::vision::PixelFormat::RGB888);
84+
}
85+
catch (const std::exception& e)
86+
{
87+
GTEST_SKIP() << e.what();
88+
}
89+
90+
omniseer::vision::RgaPreprocess preprocess({
91+
.src_w = static_cast<int>(src_w),
92+
.src_h = static_cast<int>(src_h),
93+
.dst_w = dst_w,
94+
.dst_h = dst_h,
95+
.pad_value = 114,
96+
});
97+
98+
{
99+
std::vector<omniseer::vision::ImageBufferPool::WriteLease> init_leases{};
100+
for (;;)
101+
{
102+
auto lease = pool.acquire_write_lease();
103+
if (!lease)
104+
break;
105+
ASSERT_NO_THROW(preprocess.prefill(lease->buffer()));
106+
init_leases.emplace_back(std::move(*lease));
107+
}
108+
ASSERT_GT(init_leases.size(), 0u);
109+
}
110+
111+
omniseer::vision::ProducerPipeline producer(capture, preprocess, pool);
112+
113+
const auto pre_preflight = producer.run();
114+
EXPECT_EQ(pre_preflight.status, omniseer::vision::ProducerTickStatus::CaptureFatalError);
115+
EXPECT_EQ(pre_preflight.capture.status, omniseer::vision::CaptureStatus::FatalError);
116+
EXPECT_EQ(pre_preflight.capture.sys_errno, EPERM);
117+
118+
ASSERT_NO_THROW(producer.preflight());
119+
120+
int produced = 0;
121+
int nonfatal = 0;
122+
constexpr int max_ticks = 400;
123+
for (int i = 0; i < max_ticks && produced < 3; ++i)
124+
{
125+
const auto tick = producer.run();
126+
if (tick.status == omniseer::vision::ProducerTickStatus::Produced)
127+
{
128+
++produced;
129+
EXPECT_TRUE(tick.preprocess.ok());
130+
EXPECT_EQ(tick.capture.status, omniseer::vision::CaptureStatus::Ok);
131+
continue;
132+
}
133+
134+
if (tick.status == omniseer::vision::ProducerTickStatus::CaptureFatalError ||
135+
tick.status == omniseer::vision::ProducerTickStatus::PreprocessError)
136+
{
137+
FAIL() << "unexpected fatal-ish status: " << tick_status_name(tick.status)
138+
<< ", capture_errno=" << tick.capture.sys_errno;
139+
}
140+
141+
++nonfatal;
142+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
143+
}
144+
145+
EXPECT_GT(produced, 0) << "producer did not produce any frames in " << max_ticks << " ticks";
146+
EXPECT_GE(nonfatal, 0);
147+
148+
ASSERT_NO_THROW(capture.stop());
149+
}

vision/apps/rga_preprocess_test.cpp

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,23 @@
1818

1919
namespace
2020
{
21+
const char* capture_status_name(omniseer::vision::CaptureStatus status)
22+
{
23+
using omniseer::vision::CaptureStatus;
24+
switch (status)
25+
{
26+
case CaptureStatus::Ok:
27+
return "ok";
28+
case CaptureStatus::NoFrame:
29+
return "no-frame";
30+
case CaptureStatus::RetryableError:
31+
return "retryable-error";
32+
case CaptureStatus::FatalError:
33+
return "fatal-error";
34+
}
35+
return "unknown";
36+
}
37+
2138
uint32_t env_u32(const char* key, uint32_t def)
2239
{
2340
const char* v = std::getenv(key);
@@ -130,16 +147,25 @@ TEST(RgaPreprocess, LetterboxPaddingIs114_RealV4l2Frame)
130147

131148
omniseer::vision::FrameDescriptor src{};
132149
bool got = false;
150+
omniseer::vision::CaptureResult last{};
133151
for (int spins = 0; spins < 2000; ++spins)
134152
{
135-
if (cap.dequeue(src))
153+
last = cap.dequeue(src);
154+
if (last.ok())
136155
{
137156
got = true;
138157
break;
139158
}
159+
if (last.status != omniseer::vision::CaptureStatus::NoFrame &&
160+
last.status != omniseer::vision::CaptureStatus::RetryableError)
161+
{
162+
FAIL() << "dequeue failed with status=" << capture_status_name(last.status)
163+
<< " errno=" << last.sys_errno << " (" << std::strerror(last.sys_errno) << ")";
164+
}
140165
std::this_thread::sleep_for(std::chrono::milliseconds(1));
141166
}
142-
ASSERT_TRUE(got) << "timeout waiting for frame (dequeue kept returning EAGAIN)";
167+
ASSERT_TRUE(got) << "timeout waiting for frame (status=" << capture_status_name(last.status)
168+
<< ", errno=" << last.sys_errno << ")";
143169

144170
ASSERT_EQ(src.fmt, omniseer::vision::PixelFormat::NV12);
145171
ASSERT_EQ(src.num_planes, 2u);
@@ -159,8 +185,6 @@ TEST(RgaPreprocess, LetterboxPaddingIs114_RealV4l2Frame)
159185
omniseer::vision::DmaHeapAllocator alloc;
160186
omniseer::vision::AllocatedImageBuffer allocated =
161187
alloc.allocate(dst_w, dst_h, omniseer::vision::PixelFormat::RGB888);
162-
if (!allocated.valid())
163-
GTEST_SKIP() << "failed to allocate RGB DMA-BUF via dma-heap";
164188
dst = allocated.buf;
165189
dst_alloc = std::move(allocated.alloc);
166190
}
@@ -177,10 +201,14 @@ TEST(RgaPreprocess, LetterboxPaddingIs114_RealV4l2Frame)
177201
.pad_value = 114,
178202
});
179203

180-
ASSERT_TRUE(stage.prefill(dst));
204+
ASSERT_NO_THROW(stage.prefill(dst));
181205

182206
omniseer::vision::LetterboxMeta meta{};
183-
ASSERT_TRUE(stage.run(src, dst, &meta));
207+
const auto preflight_result = stage.preflight(src, dst, &meta);
208+
ASSERT_TRUE(preflight_result.ok());
209+
210+
const auto run_result = stage.run(src, dst);
211+
ASSERT_TRUE(run_result.ok());
184212

185213
EXPECT_EQ(meta.resized_w, 640);
186214
EXPECT_EQ(meta.resized_h, 360);
@@ -211,7 +239,10 @@ TEST(RgaPreprocess, LetterboxPaddingIs114_RealV4l2Frame)
211239

212240
dmabuf_sync(dst.planes[0].fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ);
213241

214-
ASSERT_NO_THROW(cap.requeue(src.v4l2_index));
242+
const omniseer::vision::CaptureResult rq = cap.requeue(src.v4l2_index);
243+
ASSERT_TRUE(rq.ok()) << "requeue failed with status=" << capture_status_name(rq.status)
244+
<< " errno=" << rq.sys_errno << " (" << std::strerror(rq.sys_errno)
245+
<< ")";
215246
ASSERT_NO_THROW(cap.stop());
216247
}
217248

@@ -238,7 +269,7 @@ TEST(RgaPreprocess, RejectsInvalidDescriptors)
238269
src.num_planes = 2;
239270
src.planes[0].fd = 0;
240271
src.planes[1].fd = 0;
241-
EXPECT_FALSE(stage.run(src, dst));
272+
EXPECT_FALSE(stage.preflight(src, dst).ok());
242273
}
243274

244275
{
@@ -248,7 +279,7 @@ TEST(RgaPreprocess, RejectsInvalidDescriptors)
248279
src.fmt = omniseer::vision::PixelFormat::NV12;
249280
src.num_planes = 1;
250281
src.planes[0].fd = 0;
251-
EXPECT_FALSE(stage.run(src, dst));
282+
EXPECT_FALSE(stage.preflight(src, dst).ok());
252283
}
253284

254285
{
@@ -259,7 +290,7 @@ TEST(RgaPreprocess, RejectsInvalidDescriptors)
259290
src.num_planes = 2;
260291
src.planes[0].fd = 0;
261292
src.planes[1].fd = 1;
262-
EXPECT_FALSE(stage.run(src, dst));
293+
EXPECT_FALSE(stage.preflight(src, dst).ok());
263294
}
264295

265296
{
@@ -272,7 +303,7 @@ TEST(RgaPreprocess, RejectsInvalidDescriptors)
272303
src.planes[1].fd = 0;
273304
src.planes[0].stride = 1280;
274305
src.planes[1].offset = static_cast<uint32_t>(1280u * 720u + 1u);
275-
EXPECT_FALSE(stage.run(src, dst));
306+
EXPECT_FALSE(stage.preflight(src, dst).ok());
276307
}
277308

278309
{
@@ -288,7 +319,7 @@ TEST(RgaPreprocess, RejectsInvalidDescriptors)
288319

289320
omniseer::vision::ImageBuffer bad_dst = dst;
290321
bad_dst.planes[0].stride = 640 * 3 + 1;
291-
EXPECT_FALSE(stage.run(src, bad_dst));
322+
EXPECT_FALSE(stage.preflight(src, bad_dst).ok());
292323
}
293324

294325
// No additional configuration error cases: RgaPreprocess is RGB888-only.

vision/apps/v4l2_capture_test.cpp

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@
1414

1515
namespace
1616
{
17+
const char* capture_status_name(omniseer::vision::CaptureStatus status)
18+
{
19+
using omniseer::vision::CaptureStatus;
20+
switch (status)
21+
{
22+
case CaptureStatus::Ok:
23+
return "ok";
24+
case CaptureStatus::NoFrame:
25+
return "no-frame";
26+
case CaptureStatus::RetryableError:
27+
return "retryable-error";
28+
case CaptureStatus::FatalError:
29+
return "fatal-error";
30+
}
31+
return "unknown";
32+
}
33+
1734
std::string readlink_fd(int fd)
1835
{
1936
std::string path = "/proc/self/fd/" + std::to_string(fd);
@@ -71,16 +88,25 @@ TEST(V4l2Capture, NegotiatesAndStreamsDmabufNv12_1280x720)
7188
{
7289
omniseer::vision::FrameDescriptor f{};
7390
bool got = false;
91+
omniseer::vision::CaptureResult last{};
7492
for (int spins = 0; spins < 2000; ++spins)
7593
{
76-
if (cap.dequeue(f))
94+
last = cap.dequeue(f);
95+
if (last.ok())
7796
{
7897
got = true;
7998
break;
8099
}
100+
if (last.status != omniseer::vision::CaptureStatus::NoFrame &&
101+
last.status != omniseer::vision::CaptureStatus::RetryableError)
102+
{
103+
FAIL() << "dequeue failed with status=" << capture_status_name(last.status)
104+
<< " errno=" << last.sys_errno << " (" << std::strerror(last.sys_errno) << ")";
105+
}
81106
std::this_thread::sleep_for(std::chrono::milliseconds(1));
82107
}
83-
ASSERT_TRUE(got) << "timeout waiting for frame (dequeue kept returning EAGAIN)";
108+
ASSERT_TRUE(got) << "timeout waiting for frame (status="
109+
<< capture_status_name(last.status) << ", errno=" << last.sys_errno << ")";
84110

85111
EXPECT_EQ(f.size.w, static_cast<int>(width));
86112
EXPECT_EQ(f.size.h, static_cast<int>(height));
@@ -113,7 +139,10 @@ TEST(V4l2Capture, NegotiatesAndStreamsDmabufNv12_1280x720)
113139
EXPECT_EQ(f.planes[0].fd, f.planes[1].fd);
114140
EXPECT_GT(f.planes[0].bytesused, 0u);
115141

116-
ASSERT_NO_THROW(cap.requeue(f.v4l2_index));
142+
const omniseer::vision::CaptureResult rq = cap.requeue(f.v4l2_index);
143+
ASSERT_TRUE(rq.ok()) << "requeue failed with status=" << capture_status_name(rq.status)
144+
<< " errno=" << rq.sys_errno << " (" << std::strerror(rq.sys_errno)
145+
<< ")";
117146
}
118147

119148
ASSERT_NO_THROW(cap.stop());

vision/include/omniseer/vision/dma_heap_alloc.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ namespace omniseer::vision
6868
// Allocate a DMA-BUF suitable for RGA write / RKNN read and return an ImageBuffer
6969
// descriptor that points at it.
7070
// Supported formats: RGB888, BGR888.
71+
// Throws on invalid arguments or allocation failures.
7172
AllocatedImageBuffer allocate(int width, int height, PixelFormat fmt);
7273

7374
private:

0 commit comments

Comments
 (0)