Skip to content

novatek: the HDAL test under load -- H.265 runs out of time, not order; IVE's driver races itself - #216

Merged
widgetii merged 2 commits into
masterfrom
novatek-h26x-timing
Oct 27, 2026
Merged

widgetii merged 2 commits into
masterfrom
novatek-h26x-timing

Conversation

@widgetii

Copy link
Copy Markdown
Member

Under a loaded host (two suites side by side) the HDAL test failed in
three places. None of them was a wrong picture; guest-error logs and
the encoder's own completion path show no picture dropped, no buffer
full and no H.26x error.

  • hd_video_record's tiled H.265: the sample runs for a fixed 6 s, and a
    loaded host codes fewer 2880x1620 pictures in that time (3.7..7 KB
    when it failed, 8.6..19 KB when it passed); under two pictures, the
    check has nothing to see move. run() now holds q until the bitstream
    reaches WAIT_BYTES (16000), for up to 40 s more.
  • hd_video_encode_only's "decodes to the input, in order": the sample
    pushes a frame every 30 ms with a timeout of 0, and drops it ("push_in
    error") when the emulated pipeline is behind. Every PSNR-10 failure
    had at least one such drop early on: the check aligned the first
    picture and then demanded each next input frame, so one drop shifted
    every comparison. The check now matches each picture to its input
    frame and requires each to be 1..4 frames after the last (still in
    order, never backwards); push_in drops are reported apart from HDAL
    errors and may be at most a quarter of the pictures. Checked offline:
    a clean stream and one with a frame dropped pass, one out of order
    fails.
  • alg_ive_lut_sample_nonblocking: one thread checked an empty buffer.
    Its trigger had been refused ("ive_stateMachine: Operation 2 illegal
    in RUN state", "Error to do trigger_internal(), channel=0"), and the
    sample does not look at the trigger's result. The race is the vendor
    driver's: kdrv_ive_trigger_nonblock -> trigger_internal runs setMode and
    ive_start (to RUN) under a semaphore, releases it, returns, and only
    then calls ive_pause (out of RUN), so the other channel's thread,
    woken by the release, can run setMode in RUN. Silicon's microsecond
    setMode makes the threads meet rarely; a loaded host makes it common
    (3 of 4 runs of four side by side). The test reruns the sample, up to
    three times, only when it failed after such a refusal.

The IVE's jobs now complete ~1 ms after START on the engine timer, as
the other engines' do and as silicon's take, instead of within the
START write; it made no difference to the race (which is between two
pieces of guest code) but is the more faithful timing, and the
non-blocking sample's 0-timeout poll simply retries a job not yet done.
The IVE qtests wait for frame end instead of expecting it at once.

Two full runs side by side: 130/130 each.


Design record for #200 (squash commit 21fbe55 went in with an empty body)

#200's squash merge, "novatek: the AI turnkey's face recognition network -- frcnn tells Obama from Biden (#200)", lost its message to an empty body file. Its record, from the PR branch's commits:

novatek: the AI turnkey's face recognition network -- frcnn tells Obama from Biden

The v1.01.007 AI turnkey ships CNNLib/para/frcnn/nvt_model.bin and
nothing else: no library, sample, parameters or documentation, in any
SDK release. Its tensor descriptors (the OINF section) say what it is:
input "data" 112x112x3, output "BatchNormalization_fc1_Y" 1x1x128, s16
with 13 fraction bits (fmt 0xa110020d) -- the InsightFace / ArcFace
face embedding, a MobileFaceNet of depthwise layers, PReLU, a global
pooling, a fully connected layer and BN.

qemu-boot/novatek-frcnn-sample.c runs it through vendor_ai_net alone
(the ai_net_with_buf flow), on 112x112 NV12 faces, and writes each
face's 128 values to det_results/fr_features.txt. The turnkey's models
start with 32 bytes of their own (a type word, zeros) before the network
(chip id 4); the detector libraries' version checks step past them, and
without that kflow_ai_net reads chip id 0 and refuses the model.

Running it found four rules the classifier and detectors had not
exercised, and one they had hidden:

  • ACT bit 16 negates the activation's input. The compiler builds PReLU
    from three jobs -- ReLU(-x) by bit 16 (u8), ReLU(x) (u16), and an
    element-wise job whose BN scales the first by the negated slopes --
    and the model logged bit 16 as unimplemented and skipped the job.
  • BN alpha [23:0] is signed. The PReLU BNs' alphas are small negatives
    (0xfff44c and the like); pvdcnn's alphas are spread symmetrically
    about 0 too (2113 of 32108 with bit 23). Read unsigned, every
    frcnn embedding saturated to the same +-4 pattern.
  • A 16-bit out1 keeps 8 fraction bits more than an 8-bit one, as out0
    does. Only frcnn's BN + global average writes one; without the 8
    bits its output used under 0.5% of s16, against a next BN whose
    means expect ~256x more.
  • The NUE takes 16-bit operands by their top 12 bits (8-bit ones enter
    x 16), the embedding layer's 512 inputs; without it the FC output
    saturates.
  • A conv bias without BN goes after the scale-shift 0x0AC at any output
    width. The rule fitted in novatek: the AI turnkey's detectors -- people, vehicles and faces found #197 (before it for 16-bit outputs) had
    been fitted with alpha unsigned. With alpha signed and the bias
    after it everywhere, frcnn's same-person similarities are 0.68..0.82
    and different people's -0.05..0.18 (with the old rule: all 0.95+, a
    grey square included), and pvdcnn finds all three vehicles of the
    dashcam frame (truck, white car ahead, small car), where before it
    found only the small car. pdcnn now also finds the small facing man
    and both Messis; it still misses the man turned away.

test-novatek.py checks each rule at register level. The test data is
face_recognition's example photos (pinned commit): three of Obama, two
of Biden, one each of Alex Lacamoire and Lin-Manuel Miranda. Each face
is aligned to ArcFace's 112x112 template by a similarity transform of
its five landmarks, which alg_fdcnn_ai2_sample gave under QEMU. A
mirrored Obama and a grey square are the controls. test-novatek-hdal.sh
checks that:

  • each Obama / Biden photo is nearest another of the same man;
  • every pair of one man scores above every pair of two (margin +0.51);
  • the mirror is nearest its original (0.96);
  • the grey square scores below every pair of one man.
    pvdcnn's expectation becomes the two people it finds with confidence
    (the large Messi, the facing man) and the three vehicles.

Full run with --kmods, the v1.01.007 model and the detectors: 118/118
(the classifier still names the sea snake at 0.514828); without the
samples 44/44; qtests pass.

novatek: frcnn check -- an all-zero embedding is unlike everything

Review fix: cos() divided by the product of the norms, so an all-zero
embedding (an emulation regression, say) raised ZeroDivisionError. That
stopped the one script that prints every turnkey detector's summary, and
pdcnn, pvdcnn, fdcnn and cddcnn would all have failed as "no result"
with frcnn. It now returns 0 for a zero vector; checked with the grey
square's embedding zeroed, which leaves every other result in place.

…r; IVE's driver races itself

Under a loaded host (two suites side by side) the HDAL test failed in
three places.  None of them was a wrong picture; guest-error logs and
the encoder's own completion path show no picture dropped, no buffer
full and no H.26x error.

- hd_video_record's tiled H.265: the sample runs for a fixed 6 s, and a
  loaded host codes fewer 2880x1620 pictures in that time (3.7..7 KB
  when it failed, 8.6..19 KB when it passed); under two pictures, the
  check has nothing to see move.  run() now holds q until the bitstream
  reaches WAIT_BYTES (16000), for up to 40 s more.
- hd_video_encode_only's "decodes to the input, in order": the sample
  pushes a frame every 30 ms with a timeout of 0, and drops it ("push_in
  error") when the emulated pipeline is behind.  Every PSNR-10 failure
  had at least one such drop early on: the check aligned the first
  picture and then demanded each next input frame, so one drop shifted
  every comparison.  The check now matches each picture to its input
  frame and requires each to be 1..4 frames after the last (still in
  order, never backwards); push_in drops are reported apart from HDAL
  errors and may be at most a quarter of the pictures.  Checked offline:
  a clean stream and one with a frame dropped pass, one out of order
  fails.
- alg_ive_lut_sample_nonblocking: one thread checked an empty buffer.
  Its trigger had been refused ("ive_stateMachine: Operation 2 illegal
  in RUN state", "Error to do trigger_internal(), channel=0"), and the
  sample does not look at the trigger's result.  The race is the vendor
  driver's: kdrv_ive_trigger_nonblock -> trigger_internal runs setMode and
  ive_start (to RUN) under a semaphore, releases it, returns, and only
  then calls ive_pause (out of RUN), so the other channel's thread,
  woken by the release, can run setMode in RUN.  Silicon's microsecond
  setMode makes the threads meet rarely; a loaded host makes it common
  (3 of 4 runs of four side by side).  The test reruns the sample, up to
  three times, only when it failed after such a refusal.

The IVE's jobs now complete ~1 ms after START on the engine timer, as
the other engines' do and as silicon's take, instead of within the
START write; it made no difference to the race (which is between two
pieces of guest code) but is the more faithful timing, and the
non-blocking sample's 0-timeout poll simply retries a job not yet done.
The IVE qtests wait for frame end instead of expecting it at once.

Two full runs side by side: 130/130 each.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Stabilize Novatek HDAL tests under load and correct IVE job timing

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Give H.265 recording time to produce enough pictures for meaningful validation under load.
• Validate encoded frames despite bounded input drops, while preserving picture-quality and ordering
 checks.
• Model delayed IVE completion and retry its sample only after a documented driver race.
Diagram

graph TD
  T["IVE job timer"] --> S["HDAL samples"] --> H["Guest runner"] --> W["Record threshold"]
  H --> I["IVE race retry"]
  H --> C["Console output"] --> F["Frame matcher"]
  T --> Q["IVE qtests"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fix the vendor IVE driver race
  • ➕ Eliminates refused triggers rather than retrying their effects.
  • ➖ Requires changing and maintaining the vendor kernel driver, beyond this emulator and test adjustment.
2. Pace the encoder sample to avoid input drops
  • ➕ Retains contiguous input-frame comparisons.
  • ➖ Changes vendor sample behavior and no longer tests its zero-timeout feed under load.

Recommendation: Keep the PR's bounded waits, drop-aware validation, and refusal-specific retry: they preserve checks for bad pictures, wrong order, and unrelated IVE failures without modifying vendor samples. A driver lock fix would be preferable if the vendor driver becomes maintainable.

Files changed (5) +91 / -42

Bug fix (2) +66 / -36
test-novatek-hdal.shMake HDAL validation resilient to bounded load effects +58/-20

Make HDAL validation resilient to bounded load effects

• Waits up to 40 additional seconds for a 16 KB recording bitstream and reports encoder push-in drops separately from other errors. Retries the nonblocking IVE sample only when a data-check failure follows a RUN-state trigger refusal. Matches decoded pictures individually to source frames, requiring adequate PSNR, forward steps of one to four frames, and drops no greater than a quarter of decoded pictures.

qemu-boot/test-novatek-hdal.sh

nvt-media.cComplete IVE jobs on the shared engine timer +8/-16

Complete IVE jobs on the shared engine timer

• Removes IVE's synchronous START-write completion so its work and completion flags follow the existing approximately 1 ms IPP timer path. Retains IVE reset behavior, including cancellation of a running job.

qemu/hw/misc/nvt-media.c

Tests (1) +2 / -1
test-novatek.pyWait for IVE frame end in register-level tests +2/-1

Wait for IVE frame end in register-level tests

• Makes the IVE job helper wait for frame-end status before reading output. Renames the LUT assertion to reflect delayed completion rather than completion within the START write.

qemu/tests/test-novatek.py

Documentation (2) +23 / -5
CLAUDE.mdDocument delayed IVE completion and the driver race +5/-1

Document delayed IVE completion and the driver race

• Updates the IVE implementation notes to reflect timer-based completion. Explains why the nonblocking sample is retried after a refused trigger.

CLAUDE.md

novatek.mdExplain IVE timing and the nonblocking trigger race +18/-4

Explain IVE timing and the nonblocking trigger race

• Replaces the immediate-completion description with approximately 1 ms job timing. Documents the vendor driver's lock-release gap, its empty-buffer consequence, and the test's conditional retry policy.

docs/novatek.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Concurrent IVE jobs can lose a result ✓ Resolved
Description
Removing the synchronous IVE execution lets ipp_write() reschedule the device’s sole timer for a
second trigger without retaining the first job’s registers. If the nonblocking sample’s other
channel submits after the first trigger returns but before its timer fires, completion executes only
the latest register configuration and raises only one completion signal.
Code

qemu/hw/misc/nvt-media.c[L399-401]

-        if (val & p->trig) {
-            nvt_ive_run(s->regs);
-            *reg(s, p->ints) |= p->trig_end;
Evidence
IVE triggers schedule the same timer without a pending-job check. Its callback runs IVE work against
the current register array once, then emits one completion. The documented nonblocking sample has
two submitting threads, and the driver can leave RUN after its first trigger returns, allowing the
next channel to submit before the 1 ms completion.

qemu/hw/misc/nvt-media.c[391-416]
qemu/hw/misc/nvt-media.c[610-635]
qemu/hw/misc/nvt-media.c[730-753]
docs/novatek.md[1495-1521]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Delayed IVE execution uses one replaceable timer and the live register array. Two accepted triggers within the delay can collapse into one execution and completion.
## Fix Focus Areas
- qemu/hw/misc/nvt-media.c[391-416]
- qemu/hw/misc/nvt-media.c[610-635]
- qemu/hw/misc/nvt-media.c[726-752]
## Recommended Fix
Define how a trigger received while an IVE job is pending must behave, and enforce it without replacing an accepted job. Retain the pending job’s configuration until it completes, or queue accepted jobs with their own register snapshots; ensure each accepted job receives its corresponding completion. Add a test that submits two triggers within the timer interval.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread qemu/hw/misc/nvt-media.c
… gets its own frame end

Review fix: with the job deferred to the 1 ms timer, a second START in
that millisecond (the non-blocking sample's other channel) reprogrammed
the registers and re-armed the one timer.  The first job then never ran
and raised no frame end.  The IVE now runs a job as its START is
written, with the registers it started with.  It counts the frame ends
still to raise and raises them 1 ms apart, the first 1 ms after its
START, so each accepted job gets its own, in order.  The count is reset
by the engine's reset and the device's, and migrates (vmstate 3).

New qtest: two LUT jobs started back to back with different thresholds
and output buffers each write their own result.  No frame end is raised
at once; one comes, and after it is cleared, the other.

Two full runs side by side: 130/130 each.
@widgetii
widgetii merged commit b011673 into master Oct 27, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant