A poll loop that waits for a hardware trigger looks harmless until the driver underneath it changes. Engineers maintaining capture code on a legacy frame grabber API hit exactly that: a gate that had idled correctly for years started firing early.
The last valid buffer read 0 before any frame existed. The copy call then blocked until the trigger finally arrived, and the timer callback froze with it. The fix is one line — poll the session status call instead of the buffer attribute. The reason it works matters, because the same gate sits in ring-buffer logging, burst capture and every triggered line that idles between parts.
By the end you can rewrite the gate so it behaves the same on both driver generations, size the onboard-memory ring behind the burst, and prove the fix on the bench with the trigger cable unplugged.
Why does the last valid buffer read 0 before the first trigger?
On the newer driver the last-valid-buffer counter starts at 0 the moment the session opens, and 0 is also the index of the buffer the first trigger will fill, so the attribute can no longer tell an idle session from one that has captured buffer 0.
On the older generation, imgGetAttribute(Sid, IMG_ATTR_LAST_VALID_BUFFER, &currBufNum) returned the invalid sentinel 0xFFFFFFFF until the first frame was written, and the classic gate leaned on that:
/* Sid: session handle from imgSessionOpen */
static unsigned int lastBufNum = 0xFFFFFFFF;
imgGetAttribute(Sid, IMG_ATTR_LAST_VALID_BUFFER, &currBufNum);
if ((currBufNum == lastBufNum) || (currBufNum == 0xFFFFFFFF))
return 0; /* still waiting for the first trigger */
On the newer driver the first tick reads 0: it fails the sentinel test, it differs from lastBufNum, and the gate falls straight through to the copy. Re-initializing lastBufNum to 0 is no rescue either — then the gate swallows the real frame in buffer 0 as "nothing new". The two states your code must separate, waiting and first-frame-captured, now share one reading.
What goes wrong if you copy buffer 0 anyway?
The copy call does not fail, it waits: imgSessionCopyBuffer blocks until the trigger finally arrives, so your timer callback hangs for the entire idle period and the frame it eventually hands back is not really the content of buffer 0.
Two costs follow. The thread that polled is the thread that froze: every tick that should keep a status lamp, a panel or a watchdog alive runs late, and the wait is unbounded — on a line where a part shows up every 2 s, the callback can sit blocked for the full 2 s. And the index you passed stopped meaning what the code assumed, because the call waited on hardware instead of reading memory that already held a frame.
A numeric reading with no validity flag will be consumed at the wrong moment eventually. Supervisory software faces the same fork when a link drops: holding the last valid value on a communication loss is one policy and marking the tag stale is the other, and both exist because consumers cannot tell fresh from stale on their own. Stacking a blocking copy on top of a poll also breaks the one-wait-per-frame discipline that keeps grab loops honest; the same mistake resurfaces as Frame didn't arrive within timeout: one wait call per frame.
Which call tells the truth before the first trigger?
The session status call, imgSessionStatus(Sid, &status, &currBufNum), keeps currBufNum at the 0xFFFFFFFF sentinel until the first image is recorded, on both driver generations, which makes it the only idle reading your gate can trust.
imgSessionStatus fills two outputs: a status structure carrying the acquisition-state flags, and the current buffer number in the same out-parameter style as the attribute call. The rewrite is one line:
/* before */ ImgErrCode = imgGetAttribute(Sid, IMG_ATTR_LAST_VALID_BUFFER, &currBufNum);
/* after */ ImgErrCode = imgSessionStatus(Sid, &status, &currBufNum);
Same out-parameter, same sentinel, trustworthy idle value. Read the acquisition-state flags from the status structure when you need to distinguish armed from idle; for the has-a-frame-landed question this gate exists to answer, the sentinel alone decides.
How do you make the poll loop safe on both driver generations?
Gate the tick on the status call's sentinel, never on the attribute alone, and treat a driver that reports 0 from the attribute before the first frame as a counter that starts at the first buffer index rather than at an invalid value.
static unsigned int lastBufNum = 0xFFFFFFFF; /* last ring position you handled */
unsigned int currBufNum = 0xFFFFFFFF; /* invalid until the first frame */
unsigned int acqStatus = 0; /* acquisition-state flags */
int ImgErrCode = 0; /* return code from the call */
ImgErrCode = imgSessionStatus(Sid, &acqStatus, &currBufNum);
if (ImgErrCode != 0 || currBufNum == 0xFFFFFFFF)
return 0; /* no frame yet: keep the timer, copy nothing */
if (currBufNum == lastBufNum)
return 0; /* nothing new since the previous tick */
lastBufNum = currBufNum;
/* only now is a copy of buffer currBufNum safe to issue */
lastBufNum, currBufNum, acqStatus and ImgErrCode are the loop's own variables, declared and initialized above; Sid is the session handle from imgSessionOpen. Type acqStatus as your header declares the status output; an unsigned int stands in for it here. Once the first frame has landed, the attribute and the status call agree, so you can drop back to the attribute read for ring-position tracking.
How much onboard memory does a two-image burst need?
Size the ring from the frame size and the shortest gap you must survive: at 1,024 × 1,024 pixels and 8 bits per pixel each frame is 1,048,576 bytes, so a four-buffer ring needs 4,194,304 bytes, about 4 MB, of onboard memory.
The 40 MB/s is the number to argue with: a faster bus shrinks the 26 ms, but nothing shrinks the 100 µs gap, which the process sets. Both frames therefore sit in the card's memory before your first timer tick, so the poll never races the camera — its only job is deciding whether valid frames exist, the question the attribute answers wrongly on the newer driver and the sentinel answers correctly. Getting the frames out to the host afterwards is its own problem; Save an image sequence from a frame grabber with a ring buffer covers the host-side ring.
When is the last valid buffer attribute still the right tool?
Once acquisition is running, IMG_ATTR_LAST_VALID_BUFFER is a lightweight, correct read of which ring buffer was written last, so keep it for continuous mode and post-trigger housekeeping, and reserve the status poll for the idle window before the first frame.
After the first frame the two calls agree, and the attribute is the faster register read. Issue the copy into memory you allocated once at start-up rather than per frame; Reuse image buffers instead of per-frame allocation shows the pattern. And if the first frame's content surprises you, suspect the trigger path rather than the gate: Trigger call versus exposure time: understand capture timing separates when the camera was told to expose from when the frame landed in the ring.
Which frame grabbers does this poll pattern apply to?
Any capture card whose API pairs an acquisition-status call with a last-buffer attribute can drift like this, and that pairing is exactly what the legacy onboard-memory grabber APIs exposed.
The trap lives in the API layer, not in one card, so read the idle value off your own driver before carrying a gate across a driver upgrade. When the legacy card itself is due for replacement, the frame grabbers category lists 58 cards across GigE, 10GigE, XoFLink, CoaXPress, Camera Link and USB3, and the accessories finder filters them by interface and port count. The cards below cover the Camera Link, CoaXPress and XoFLink families:
- Interface
- Camera Link
- Interface
- CoaXPress
- Interface
- XoFLink
- Ports
- 4 ports
The MV-GC1102IOL is the Camera Link card. The MV-GX1008 is the Octo CXP-6 frame grabber for CoaXPress. The MV-GS1104F-DF is the XoFLink card with four ports, an SFP+ slot and 40G total bandwidth supported.
How do you verify the fix before the line runs?
Prove the gate with the trigger cable unplugged: run the poll at 100 ms for 60 s, confirm all 600 ticks read 0xFFFFFFFF from the status call and that no copy fires, then fire one trigger and watch the reading flip to 0 within a single tick.
- Disconnect the trigger source, open the interface and session, and arm the two-buffer onboard-memory ring with the external trigger configured.
- Let the 100 ms timer run for 60 s — 600 ticks. Expect
currBufNumto read 0xFFFFFFFF on every tick and zeroimgSessionCopyBuffercalls. Log the attribute too: a 0 from it during this minute is the quirk, not an acquisition. - Fire one trigger. Expect the sentinel to flip to 0 within one tick and the copy of buffer 0 to return immediately, with no wait.
- Fire the second trigger and expect buffer 1; copy it and save it.
- Repeat on the old driver generation if your fleet runs both — the same gate passing on each is the point of the rewrite.
If the sentinel never clears even with the trigger firing, the failure has moved downstream to memory or wiring; Diagnose frame grabber buffer allocation errors is the next probe.
| Your situation | What decides the tick |
|---|---|
| Idling before the first trigger | the imgSessionStatus sentinel 0xFFFFFFFF, on either driver generation |
| Ring position while frames flow |
IMG_ATTR_LAST_VALID_BUFFER; both calls agree once capture has started |
| Continuous free-run capture | the attribute is fine; the ambiguous window is one frame period |
For headless capture, drop the timer altogether: one blocking wait with a timeout inside a worker thread, one wait per frame, interrupted cleanly at shutdown — Hardware trigger grab timeout: interrupt the wait cleanly shows how to break out of the wait.
The decision, condensed: poll the status call and gate on 0xFFFFFFFF before the first trigger, keep the attribute for position once frames flow, and never let a copy run in the idle branch. More driver-level fixes like this sit on the systems topic page. To replace the legacy card behind all of this, start the configurator below, or send the camera, trigger and burst sketch through the contact page and we will size the ring and the gate with you.
The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.
Questions
Is 0xFFFFFFFF the same thing as -1?
Yes. In two's-complement arithmetic, 0xFFFFFFFF is the unsigned 32-bit pattern of -1, which is why legacy capture APIs picked it as the no-valid-buffer-yet sentinel. Keep your buffer variables unsigned and compare against the full 0xFFFFFFFF constant; casting the value to a signed int turns the sentinel into -1 and invites sign-comparison warnings that bury the real test.
How many triggers fit in a card with 64 MB of onboard memory?
Divide the card's onboard memory by one frame. With 1,024 × 1,024 monochrome 8-bit frames of 1,048,576 bytes, 64 MB holds 67,108,864 ÷ 1,048,576 = 64 frames, and a 16 MB card holds 16. The depth you need is the number of triggers that can fire before the host drains the ring, so a 64-frame card rides out a burst of 64 parts arriving faster than the bus copies.
Does the same trap exist in continuous, trigger-free capture?
It exists, but it costs less. In free-run mode the first frame lands within one frame period, about 33 ms at 30 fps, so a wrong gate delays or duplicates at most one frame instead of blocking until an external event. Keep the sentinel gate anyway: the same code tends to migrate onto a triggered line, where the idle window is unbounded.
Should I poll from a timer or block on a wait in a worker thread?
Poll when a user interface must stay responsive while the operator waits for a part: query the status call from a 50-100 ms timer, which costs nothing measurable. Block when the capture is headless: one wait call with a timeout inside a worker thread, one wait per frame, interrupted cleanly at shutdown. Mixing the two, a blocking copy inside a timer tick, is the freeze described above.


