Improve frame handling in UVCStream to prevent corruption and ensure proper pacing

maybe fix to camera glitches
This commit is contained in:
PhosphorosVR
2025-09-17 12:30:19 +02:00
parent 69406e62b3
commit 15641753f0
+43 -33
View File
@@ -6,6 +6,10 @@
static const char *UVC_STREAM_TAG = "[UVC DEVICE]"; static const char *UVC_STREAM_TAG = "[UVC DEVICE]";
// Tracks whether a frame has been handed to TinyUSB and not yet returned.
// File scope so both get_cb and return_cb can access it safely.
static bool s_frame_inflight = false;
extern "C" extern "C"
{ {
static char serial_number_str[13]; static char serial_number_str[13];
@@ -85,55 +89,57 @@ static void UVCStreamHelpers::camera_stop_cb(void *cb_ctx)
static uvc_fb_t *UVCStreamHelpers::camera_fb_get_cb(void *cb_ctx) static uvc_fb_t *UVCStreamHelpers::camera_fb_get_cb(void *cb_ctx)
{ {
auto *mgr = static_cast<UVCStreamManager *>(cb_ctx); auto *mgr = static_cast<UVCStreamManager *>(cb_ctx);
s_fb.cam_fb_p = esp_camera_fb_get();
if (!s_fb.cam_fb_p) // Guard against requesting a new frame while previous is still in flight.
{ // This was causing intermittent corruption/glitches because the pointer
return nullptr; // to the underlying camera buffer was overwritten before TinyUSB returned it.
}
//-------------------------------------------------------------------------------------------------------------- // --- Frame pacing BEFORE grabbing a new camera frame ---
// Pace frames to exactly 60 fps (drop extras). Uses fixed-point accumulator static int64_t next_deadline_us = 0; // next permitted capture time
// to achieve an exact average of 60.000 fps without drifting. static int rem_acc = 0; // fractional remainder accumulator
static int64_t next_deadline_us = 0; constexpr int target_fps = 60; // desired FPS
static int rem_acc = 0; // remainder accumulator for 1e6 % fps distribution constexpr int64_t us_per_sec = 1000000; // 1e6 microseconds
constexpr int target_fps = 60;
constexpr int64_t us_per_sec = 1000000LL;
constexpr int base_interval_us = us_per_sec / target_fps; // 16666 constexpr int base_interval_us = us_per_sec / target_fps; // 16666
constexpr int rem_us = us_per_sec % target_fps; // 40 constexpr int rem_us = us_per_sec % target_fps; // 40 (distributed)
const int64_t now_us = esp_timer_get_time(); const int64_t now_us = esp_timer_get_time();
if (next_deadline_us == 0) if (next_deadline_us == 0)
{ {
// First frame: allow immediately and schedule next slot from now // First allowed capture immediately
next_deadline_us = now_us; next_deadline_us = now_us;
} }
if (now_us < next_deadline_us)
// If a frame is still being transmitted or we are too early, just signal no frame
if (s_frame_inflight || now_us < next_deadline_us)
{
return nullptr; // host will poll again
}
// Acquire a fresh frame only when allowed and no frame in flight
camera_fb_t *cam_fb = esp_camera_fb_get();
if (!cam_fb)
{ {
// Too early for next frame: drop this camera buffer
esp_camera_fb_return(s_fb.cam_fb_p);
s_fb.cam_fb_p = nullptr;
return nullptr; return nullptr;
} }
//--------------------------------------------------------------------------------------------------------------
s_fb.uvc_fb.buf = s_fb.cam_fb_p->buf; s_fb.cam_fb_p = cam_fb;
s_fb.uvc_fb.len = s_fb.cam_fb_p->len; s_fb.uvc_fb.buf = cam_fb->buf;
s_fb.uvc_fb.width = s_fb.cam_fb_p->width; s_fb.uvc_fb.len = cam_fb->len;
s_fb.uvc_fb.height = s_fb.cam_fb_p->height; s_fb.uvc_fb.width = cam_fb->width;
s_fb.uvc_fb.format = UVC_FORMAT_JPEG; // we gotta make sure we're ALWAYS using JPEG s_fb.uvc_fb.height = cam_fb->height;
s_fb.uvc_fb.timestamp = s_fb.cam_fb_p->timestamp; s_fb.uvc_fb.format = UVC_FORMAT_JPEG;
s_fb.uvc_fb.timestamp = cam_fb->timestamp;
// Ensure frame fits into configured UVC transfer buffer // Validate size fits into transfer buffer
if (mgr && s_fb.uvc_fb.len > mgr->getUvcBufferSize()) if (mgr && s_fb.uvc_fb.len > mgr->getUvcBufferSize())
{ {
ESP_LOGE(UVC_STREAM_TAG, "Frame size %d exceeds UVC buffer size %u", (int)s_fb.uvc_fb.len, (unsigned)mgr->getUvcBufferSize()); ESP_LOGE(UVC_STREAM_TAG, "Frame size %d exceeds UVC buffer size %u", (int)s_fb.uvc_fb.len, (unsigned)mgr->getUvcBufferSize());
esp_camera_fb_return(s_fb.cam_fb_p); esp_camera_fb_return(cam_fb);
s_fb.cam_fb_p = nullptr;
return nullptr; return nullptr;
} }
//-------------------------------------------------------------------------------------------------------------- // Schedule next frame time (distribute remainder for exact longterm 60.000 fps)
// Schedule the next allowed frame time: base interval plus distributed remainder
rem_acc += rem_us; rem_acc += rem_us;
int extra_us = 0; int extra_us = 0;
if (rem_acc >= target_fps) if (rem_acc >= target_fps)
@@ -141,11 +147,10 @@ static uvc_fb_t *UVCStreamHelpers::camera_fb_get_cb(void *cb_ctx)
rem_acc -= target_fps; rem_acc -= target_fps;
extra_us = 1; extra_us = 1;
} }
// Accumulate from the previous deadline to avoid drift; if we are badly late, catch up from now const int64_t candidate_next = next_deadline_us + base_interval_us + extra_us;
const int64_t base_next = next_deadline_us + base_interval_us + extra_us; next_deadline_us = (candidate_next < now_us) ? now_us : candidate_next;
next_deadline_us = (base_next < now_us) ? now_us : base_next;
//--------------------------------------------------------------------------------------------------------------
s_frame_inflight = true;
return &s_fb.uvc_fb; return &s_fb.uvc_fb;
} }
@@ -153,7 +158,12 @@ static void UVCStreamHelpers::camera_fb_return_cb(uvc_fb_t *fb, void *cb_ctx)
{ {
(void)cb_ctx; (void)cb_ctx;
assert(fb == &s_fb.uvc_fb); assert(fb == &s_fb.uvc_fb);
if (s_fb.cam_fb_p)
{
esp_camera_fb_return(s_fb.cam_fb_p); esp_camera_fb_return(s_fb.cam_fb_p);
s_fb.cam_fb_p = nullptr;
}
s_frame_inflight = false;
} }
esp_err_t UVCStreamManager::setup() esp_err_t UVCStreamManager::setup()