Skip to content

ambarella-osdrv-s3l: generate the OV4689 ISP configuration at build time - #2793

Merged
openipc-ai merged 1 commit into
OpenIPC:masterfrom
DeathCamel58:s3l-isp-config
Nov 8, 2026
Merged

openipc-ai merged 1 commit into
OpenIPC:masterfrom
DeathCamel58:s3l-isp-config

Conversation

@DeathCamel58

@DeathCamel58 DeathCamel58 commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A streamer that runs its own 3A on S3L loads an ISP configuration into the DSP before preview starts: the three image blocks (LISO, HDR, CE) and the tables they point at. Without one the picture stays black. The tree has none, so the Divinus S3L backend (OpenIPC/divinus#71) can't give video on an OpenIPC image.

Change

  • The generator: src/isp/s3l_isp_gen.py builds the file from a sensor preset.

    • Format: the block layout, field offsets, table encodings and the limits the DSP enforces come from reverse engineering the S3L ISP library and microcode, checked on the camera.
    • Values: the generator's own, nothing copied from a camera's firmware or tuning files:
      • neutral or identity settings;
      • values measured on an OV4689 (black level, starting white balance);
      • noise-reduction and sharpening curves designed and measured here.
    • Values whose meaning couldn't be derived were tested on the camera. The ones that change the picture keep a measured value; the ones that change nothing measurable on a still or a moving scene are 0. Each choice is commented in the code.
    • Input checks: malformed presets and out-of-range values stop the build with a usage error.
  • The file format (S3LISP03): table pointers are offsets into the IMG buffer, with a relocation list. The loader (Adding Ambarella S3L support divinus#71) adds the buffer address the driver reports, so the file doesn't depend on where the buffer is.

  • The OV4689 preset: src/isp/ov4689.yaml holds:

    • the output size and Bayer phase;
    • the measured black level;
    • a starting white balance for the streamer's auto white balance;
    • dynamic bad-pixel correction, sharpening and temporal noise reduction.

    These are the sensor's settings, not one board's. A board that needs its own file points system.sensor_config at it.

  • The package: with BR2_PACKAGE_DIVINUS only, it runs the generator with host-python3 and installs /etc/sensors/ov4689.bin. That's the name Divinus loads when system.sensor_config doesn't point at a file. Another sensor needs its own <sensor>.yaml (with its size and Bayer phase) and an entry in AMBARELLA_OSDRV_S3L_ISP_SENSORS.

Majestic images aren't affected: without Divinus, nothing is generated or installed, and no host Python is built.

Hardware tested on

Amcrest IP4M-1026E (S3L, OV4689), with the SDK microcode this tree ships (2018/10/28 284712):

  • The package: in a Divinus image, it builds the file with host-python3 and installs /etc/sensors/ov4689.bin (49248 bytes).

  • Loading: Divinus with the Adding Ambarella S3L support divinus#71 loader loads and relocates the file:

    [s3l_isp] Loaded /tmp/isp.bin (3 ops, 12 pages)
    

    It then streams 2688×1512 with auto exposure and white balance running, and the DSP reports no errors.

  • The value changes: each changed value was compared with the previous file on the camera, about 150 paired runs:

    • detail, temporal noise and brightness on a still scene at high gain;
    • detail and frame-to-frame change on a moving pattern, which is where ghosting would show.

    The final file matches the previous one within run-to-run spread.

  • Temporal noise reduction takes effect as generated: noise is 14–61 % higher with temporal_nr: off.

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

Copy link
Copy Markdown

PR Summary by Qodo

Generate the S3L OV4689 ISP configuration at build time

🐞 Bug fix ✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Generate an OV4689 ISP configuration so Divinus can produce video instead of a black frame.
• Define sensor tuning as an editable preset rather than shipping a firmware-derived binary.
• Install the generated configuration where Divinus looks when its configured file is missing.
Diagram

graph TD
  B["S3L package build"] --> G["ISP generator"] --> C[/"S3LISP02 image"/] --> F[/"OV4689 sensor file"/] --> D["Divinus streamer"] --> S["S3L DSP"]
  P[/"OV4689 preset"/] --> G
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Check in a generated binary
  • ➕ Avoids running a Python generator during every package build.
  • ➕ Keeps the package build simpler.
  • ➖ Makes sensor settings and binary provenance harder to inspect.
  • ➖ Requires regenerating and committing an opaque artifact for each sensor change.

Recommendation: Keep build-time generation: it makes the configuration inspectable and supports additional sensor presets without shipping opaque binaries. Given the hardware-specific encoding, add automated checks for deterministic output and representative invalid options before broadening sensor support.

Files changed (3) +712 / -2

Enhancement (2) +706 / -2
ambarella-osdrv-s3l.mkGenerate and install ISP files for configured sensors +12/-2

Generate and install ISP files for configured sensors

• Runs the generator with host Python for each listed sensor during the package build, then installs the resulting binary under /etc/sensors. Updates the license declaration to cover the MIT-licensed generator.

general/package/legacy/ambarella-osdrv-s3l/ambarella-osdrv-s3l.mk

s3l_isp_gen.pyBuild S3L ISP images from validated options +694/-0

Build S3L ISP images from validated options

• Adds a CLI generator that constructs LISO, HDR, and CE blocks and their tables, assigns IMG-buffer addresses, and writes the S3LISP02 file. It accepts presets and overrides and rejects several settings known to violate DSP or driver limits.

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py

Other (1) +6 / -0
ov4689.yamlDefine the OV4689 ISP preset +6/-0

Define the OV4689 ISP preset

• Sets the measured black level, initial white-balance gains, bad-pixel correction, sharpening, and temporal noise reduction used to generate the OV4689 configuration.

general/package/legacy/ambarella-osdrv-s3l/src/isp/ov4689.yaml

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

qodo-free-for-open-source-projects Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Remediation recommended

1. Sensor tuning from one unit ships to every image 📘 Rule violation ⚙ Maintainability
Description
The OV4689 preset hard-codes a black level of 244 and a starting white balance of 1.5,1.8, both
taken from one Amcrest IP4M-1026E bench measurement. The .mk builds ov4689.bin for every board
that enables this package and installs it into /etc/sensors/, so these values become the default
for any OV4689 camera that Divinus loads without a sensor_config. Other OV4689 units or lenses with
different black levels will be affected too, and changing the values later means editing a shared
package rather than that board's configuration.
Code

general/package/legacy/ambarella-osdrv-s3l/src/isp/ov4689.yaml[R2-3]

+black_level: 244        # dark-frame median leaves the 12-bit output floor at ~244 (sensor BLC target 0x40 -> nominal 256)
+wb_gain: 1.5,1.8        # starting white balance; the streamer's auto white balance takes it from here
Evidence
The yaml values are single-board measurements that the shared package installs unconditionally for
all builds that include it. The rule asks that device-specific values go through per-board
configuration, not shared package files.

general/package/legacy/ambarella-osdrv-s3l/src/isp/ov4689.yaml[2-3]
general/package/legacy/ambarella-osdrv-s3l/ambarella-osdrv-s3l.mk[27-27]
Best Practice: Repository guidelines

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

## Issue description
The OV4689 ISP preset in `ov4689.yaml` hard-codes a black level and white balance measured on one Amcrest IP4M-1026E. The shared S3L package installs the resulting `/etc/sensors/ov4689.bin` for every board that builds it, so these single-unit measurements become the default for all OV4689 cameras.
## Fix Focus Areas
- general/package/legacy/ambarella-osdrv-s3l/src/isp/ov4689.yaml[2-3]
- general/package/legacy/ambarella-osdrv-s3l/ambarella-osdrv-s3l.mk[27-27]
## Recommended Fix
Keep only sensor-generic defaults in the shared preset. Supply the measured black level and white balance through the board's OpenIPC/builder per-device configuration, or document the values as a generic OV4689 starting point that a board can override.

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


2. Comments narrate code instead of giving reasons 📘 Rule violation ⚙ Maintainability
Description
Several new comments in the generator only restate the next statement: `# bayer pattern (sensor
property) and # WB 1.0 sit beside the struct.pack_into` calls that write those values, and they
say nothing about why the value is needed. Repeated across the file, this pattern gives a later
maintainer no reason to trust or change these constants. Comments that do give a reason, such as the
one on the 0x40 black-level target, are the form the rule asks for.
Code

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[R65-66]

+        struct.pack_into('<I', L, 0x044, bayer & 3)                       # bayer pattern (sensor property)
+        struct.pack_into('<4I', L, 0x0e4, *[0x10000] * 4)                 # WB 1.0
Evidence
The new comments on these lines describe the value's type or name rather than why the register is
set to that value, which is the failure case the rule describes.

CLAUDE.md: Write comments that explain why rather than narrate code: CLAUDE.md: Write comments that explain why rather than narrate code: CLAUDE.md: Write comments that explain why rather than narrate code: CLAUDE.md: Write comments that explain why rather than narrate code: CLAUDE.md: Write comments that explain why rather than narrate code: CLAUDE.md: Write comments that explain why rather than narrate code: CLAUDE.md: Write comments that explain why rather than narrate code: CLAUDE.md: Write comments that explain why rather than narrate code
general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[65-66]

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

## Issue description
New comments in `s3l_isp_gen.py` restate the next statement, for example `# bayer pattern (sensor property)` and `# WB 1.0`, without giving a reason for the value. The rule requires comments to explain why the value is set.
## Fix Focus Areas
- general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[65-66]
## Recommended Fix
Rewrite these comments to state the reason for each value, such as why WB is unity by default or which sensor property requires the bayer pattern. Apply the same change to other comments in the file that only restate their line.

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


3. Test-only knobs ship in the generator 📘 Rule violation ⚙ Maintainability
Description
The generator defines several options marked as test-only, including cc3d_zero_field,
chroma_curve_split, liso_neutral, and the test-labelled fs_scale and mctf_counts. These
functions poke individual ISP tables and register fields to find out how the DSP behaves, and they
are registered in OPTIONS alongside production options. Adding them to the firmware repository's
shipped generator grows a bring-up register-poking tool here, when the rule asks that such utilities
live in OpenIPC/ipctool.
Code

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[R266-267]

+def opt_cc3d_zero_field(cfg, v):
+    """Test only: zero one 10-bit output field of every 3-D LUT node (a = bits 0-9, b = 10-19, c = 20-29)."""
Evidence
opt_cc3d_zero_field and opt_chroma_curve_split are self-described as test-only experiments on
DSP tables, and opt_liso_neutral is labelled test-only. They are registered in the shipped
OPTIONS table, so bring-up probing code lands in firmware tooling.

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[266-267]
general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[338-340]
Best Practice: Repository guidelines

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

## Issue description
The generator in `s3l_isp_gen.py` includes options documented as test-only (`cc3d_zero_field`, `chroma_curve_split`, `liso_neutral`, and the test-labelled `fs_scale` and `mctf_counts`). They are registered in the shipped `OPTIONS` table. Bring-up and register-poking experiments belong in OpenIPC/ipctool, not in the firmware repository.
## Fix Focus Areas
- general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[266-267]
- general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[338-340]
## Recommended Fix
Remove the test-only functions and their `OPTIONS` entries from this file, or move them to OpenIPC/ipctool. Keep only the options the shipped OV4689 preset uses.

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


View review recommended (6)
4. Other sensors get OV4689 image settings ✓ Resolved
Description
main() always calls Config.neutral() with its fixed 2688×1520 dimensions and Bayer pattern, and
the preset options cannot change those fields. Following the documented path of adding another
sensor’s YAML file and package-list entry therefore embeds OV4689-specific image settings even when
that sensor needs different ones.

5. Enabling contrast enhancement fails builds ✓ Resolved
Description
opt_ce() enables the CE descriptor, but Config.neutral() leaves its batch table zero-filled and
validate() rejects its first zero command code. A sensor preset containing ce: on consequently
stops generation rather than producing the requested configuration.

6. Enabling HDR leaves blending disabled 🐞 Bug ≡ Correctness
Description
opt_hdr() only clears the HDR block and hdr_tables when switching off; its on path restores
neither. Because main() starts from a neutral configuration that has already switched HDR off, a
later hdr: on preset still serializes a zero HDR block with no blend-table pointers.
Code

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[R212-216]

+def opt_hdr(cfg, v):
+    """HDR blending: 'off' = 68 zero bytes (linear sensor; stock's blend queues only hold the terminator)."""
+    if not onoff(v):
+        cfg.hdr[:] = bytes(HDR_SIZE)
+        cfg.hdr_tables = False
Evidence
The neutral constructor calls opt_hdr('off'); opt_hdr('on') does nothing, and build() omits
HDR tables whenever the resulting flag remains false.

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[60-85]
general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[100-120]
general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[212-216]

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

## Issue description
An `hdr: on` preset succeeds but leaves the HDR block and its table pointers disabled.
## Fix Focus Areas
- general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[60-85]
- general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[100-120]
- general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[212-216]
## Recommended Fix
Either construct and validate usable HDR blending data for the on setting, or reject `hdr: on` explicitly until that data can be generated. Test the setting through the preset-driven command-line path.

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


7. Zero gamma produces a saturated image 🐞 Bug ≡ Correctness
Description
encoding() accepts gamma=0 without a range check, and opt_color_chain() consequently evaluates
every input knot to one, including zero. A preset or command-line override using that exponent
generates an all-4095 input color lookup table instead of rejecting the value.
Code

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[R246-248]

+    if spec.startswith('gamma'):
+        g = float(spec[5:].lstrip('=') or 0.45)
+        return lambda t: t ** g
Evidence
The knots include zero, the parsed exponent has no lower bound, and the encoded values are written
directly into the input color table.

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[233-264]

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

## Issue description
A zero color-chain gamma is accepted and maps every input lookup-table knot to its maximum value.
## Fix Focus Areas
- general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[243-264]
## Recommended Fix
Require a finite, positive gamma within a documented useful range before generating the input table. Add a test that rejects zero and checks that the zero knot stays zero for accepted exponents.

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


8. Out-of-range noise settings change silently 🐞 Bug ≡ Correctness
Description
opt_temporal_nr() passes sub and thr to pack12() without checking their documented 0–4095
range, and pack12() masks away higher bits. Supplying sub=4096 or thr=4096 therefore succeeds
but writes zero, so a sensor preset can contain different temporal-adjust values from those its
author requested.
Code

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[R573-579]

+        def pack12(off, val):                                     # 12 bits per channel: y b0-11, cb b12-23, cr b24-35
+            v = (val & 0xfff) | (val & 0xfff) << 12 | (val & 0xfff) << 24
+            m[off:off + 5] = (v | (m[off + 4] & 0xf0) << 32).to_bytes(5, 'little')
+        if 'sub' in kw:                                           # temporal_adjust noise_base (sub), 0..4095
+            pack12(0x2c8, int(kw['sub']))
+        if 'thr' in kw:                                           # temporal_adjust still_thresh, 0..4095
+            pack12(0x2e8, int(kw['thr']))
Evidence
Both settings are documented as 12-bit quantities but reach pack12() unchecked; its & 0xfff
operation changes 4096 to zero.

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[573-584]

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

## Issue description
Temporal `sub` and `thr` values outside their documented range are silently truncated into different table values.
## Fix Focus Areas
- general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[573-584]
## Recommended Fix
Validate each value against its field range before packing instead of relying on bit masking. Apply the same explicit check to adjacent documented numeric fields such as `mul`, and test rejection of boundary violations.

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


9. Malformed preset lines go unnoticed 🐞 Bug ☼ Reliability
Description
load_preset() silently skips every nonempty line without a colon rather than treating it as an
invalid setting. If a sensor preset loses the colon on black_level or another image option, the
build still installs a file using the neutral default for that option.
Code

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[R663-665]

+        if line and ':' in line:
+            k, v = line.split(':', 1)
+            out.append((k.strip(), v.strip()))
Evidence
Only lines containing a colon enter the returned option list. The OV4689 preset supplies image
settings through those lines, while the neutral configuration provides fallback values when an
option is absent.

general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[659-685]
general/package/legacy/ambarella-osdrv-s3l/src/isp/ov4689.yaml[1-6]
general/package/legacy/ambarella-osdrv-s3l/ambarella-osdrv-s3l.mk[29-33]

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

## Issue description
A malformed nonempty preset line is ignored, allowing a successfully built sensor profile to omit a requested image setting.
## Fix Focus Areas
- general/package/legacy/ambarella-osdrv-s3l/src/isp/s3l_isp_gen.py[659-666]
## Recommended Fix
After removing comments and whitespace, require each nonempty line to contain a key and colon-separated value. Exit with the file and line number for malformed entries, and test that a missing colon fails generation.

ⓘ 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 pick a display mode instead of tweaking every Display option by hand

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@DeathCamel58

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I've force-pushed an updated commit. The generated /etc/sensors/ov4689.bin is byte-for-byte the same as before, so the hardware test in the description still applies.

  1. Sensor tuning from one unit: these are the sensor's values, not one board's. The black level comes from the OV4689's BLC target (registers 0x4004/0x4005 = 0x40, 10-bit, so a nominal 256 at 12 bits), checked against a dark frame. The white balance is only a starting point: the streamer's auto white balance replaces it within the first frames. A board that needs different values gives the streamer its own file (Divinus: system.sensor_config), which takes precedence over /etc/sensors/ov4689.bin. The preset now says this.
  2. Comments that narrate: the two flagged comments now give the reason (the sensor's CFA phase; unity until a preset or AWB sets it).
  3. Test-only knobs: removed cc3d_zero_field, chroma_curve_split, liso_neutral, fs_scale and mctf_counts. I also removed zero_tables, update_masks and drop_tables, which were bring-up tools too.
  4. Other sensors get OV4689 geometry: width, height and bayer are now options, applied when the base configuration is built. ov4689.yaml sets them explicitly, so a new sensor's preset has them to copy.
  5. ce: on: there's no CE table generator yet, so ce: on now stops with a clear error instead of failing validation on an empty batch.
  6. hdr: on: same. It's refused with an error rather than writing an empty HDR block.
  7. gamma=0: the colour-chain gamma is range-checked (0.2–3), as the tone-curve gamma already was.
  8. sub/thr out of range: both are checked against 0–4095 (and mul against 0–16383) instead of being masked.
  9. Malformed preset lines: a non-empty line without key: value now stops the build with the file and line.

@openipc-ai openipc-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. Generating the ISP config from source is a much better direction than shipping a blob. Three things need fixing before it can merge, and a few more are worth a look.

Blocking

  1. Nothing on the image reads the installed file. The only defconfig with BR2_PACKAGE_AMBARELLA_OSDRV_S3L is s3l_lite. It ships majestic, not Divinus, and the PR says itself that majestic never opens /etc/sensors/ov4689.bin. So every s3l image gets ~49 KB nothing reads ("nothing ships that nothing runs", CLAUDE.md). Please key the install on BR2_PACKAGE_DIVINUS, either through general/scripts/late-overlays.list or a conditional in the .mk.

  2. Where the constants come from. The generator writes in values tagged "libimg fixed" / "as stock" / "stock 3" and the stock -4096 13-bit encoding. It cites vcap_liso.c:485, idspdrv_api.c:2065 and idspdrv_sharpen.c:185, and refers to an FS-MCTF-RECIPES document that isn't in the tree. The tree's rule is that a register table copied from a vendor driver is a blob in another language, whatever language it's written in. Could you say how each of these was derived (the public microcode interface, your own measurements, or copied from libimg / driver source), and either add the recipes document or drop the reference to it? Reviewers can't check sources they can't see.

  3. Host Python isn't declared. The build runs bare python3. Please add host-python3 to AMBARELLA_OSDRV_S3L_DEPENDENCIES and invoke $(HOST_DIR)/bin/python3, so the build doesn't depend on whatever the host has (a host without python3, or with ≤3.5 and no f-strings, fails partway through).

Correctness

  1. IMG_BASE = 0x0851d000 is an absolute address baked into every table pointer (link() writes IMG_BASE + offset). If a mem= or DSP carveout change moves the IMG buffer (QUERY_BUF id 8), every LISO/HDR/CE pointer goes stale and the DSP reads garbage, with nothing to flag it. The ops are already buffer offsets, so relocating against the queried base at load time would remove this hazard.

  2. Microcode version. The docstring says the layout was checked against microcode dated 2017-02-22 to 2018-07-19, but the package installs the SDK 2.5.0 microcode dated 2018/10/28. Were the block offsets, LISO_PTRS, TABLE_SIZES and the 12752 B final_sharpen read checked against the microcode this tree actually ships?

  3. TNR update mask. temporal_nr writes the video_mctf table but leaves LISO +0x234 bits 0-15 at 0 unless mask= is given, and ov4689.yaml (temporal_nr: med) gives none. Final sharpen always sets bits 16-29. If the DSP honours the update mask, the shipped TNR setting is never applied. Can you confirm on hardware that it takes effect, for example by toggling it off and seeing a visible difference?

Smaller

  1. The docstring says options are range-checked, but many knobs aren't (sharpness u/amul/fir2/scale1/dc/ugl, temporal_nr iso/dir/dadj/sb2/wide/tn/mf/mcD/thD). Out-of-range values crash with ValueError: byte must be in range. In-range nonsense (wide=9, fir2=7) goes straight to the DSP.
  2. Malformed input gives a traceback instead of a usage error: --set key without =, a trailing --preset, temporal_nr: iso=1 with no positional value, sharpness: u=8.
  3. Roughly half the script can't run under the shipped preset: the CE batch/link/validate path (opt_ce refuses on), hdr_tables, OPS = OPS_SETS with always-zero flags, the printer/dcurve/sb2c/mcD/thD/ugl/dc knobs, and final_sharpen r1, which duplicates sharpness off. A generator limited to what the presets use would be much easier to review and maintain.
  4. The opt_stage_off docstring contains the literal ' + ', '.join(sorted(STAGES)) + ', so the help prints code, not the stage list. grgb has a special case that STAGES could cover with two (offset, mask) pairs.

@openipc-ai

Copy link
Copy Markdown
Collaborator

Correction to my review above. In point 1 I said s3l_lite is the only defconfig with this package. I checked a stale tree: since #2787 it's s3l_ultimate. The point stands, though. s3l_ultimate, and the Amcrest profile in OpenIPC/builder#228, ship Majestic and not Divinus, so nothing on either image reads /etc/sensors/ov4689.bin. The install still needs to be keyed on BR2_PACKAGE_DIVINUS.

A streamer that runs its own 3A on S3L (the Divinus S3L backend, OpenIPC/
divinus#71) loads an ISP configuration into the DSP's IMG buffer before
preview starts: the three image blocks (LISO, HDR, CE) and the tables they
point at. Without one the picture stays black, and the tree had none.

src/isp/s3l_isp_gen.py builds that file from a sensor preset. The block
layout, field offsets, table encodings and the limits the DSP enforces
come from reverse engineering the S3L ISP library and microcode, checked
on the camera; the values are the generator's own. They are neutral or
identity settings, values measured on an OV4689 (black level, starting
white balance), and noise-reduction and sharpening curves designed and
measured here. Values whose meaning could not be derived were tested on
the camera: those that change the picture keep a measured value, and
those that change nothing measurable on a still or a moving scene are 0.
Malformed presets and out-of-range values stop the build with a usage
error. Table pointers are written as IMG-buffer offsets with a list of
relocations, which the loader adds the buffer's address to, so the file
does not depend on where the driver puts the buffer.

src/isp/ov4689.yaml holds the OV4689's settings: output size and Bayer
phase, black level, a starting white balance for the streamer's AWB,
dynamic bad-pixel correction, sharpening and temporal noise reduction.
They are the sensor's, not one board's; a board that needs its own file
points system.sensor_config at it.

Only Divinus reads the file, so the package generates it, with
host-python3, and installs /etc/sensors/ov4689.bin only when Divinus is
selected. Divinus loads that file when system.sensor_config does not
point at one.

Tested on an Amcrest IP4M-1026E (S3L, OV4689) with the SDK microcode
OpenIPC ships: the package builds the file with host-python3 and installs
it in a Divinus image; Divinus loads and relocates it and streams
2688x1512 with auto exposure and white balance running. Each changed
value was compared with the previous file on the camera (detail, noise
and brightness on a still scene at high gain, and detail and frame
change on a moving pattern); the final file matches the previous one
within run-to-run spread. Temporal noise reduction takes effect as
generated: noise is 14-61% higher with it off.
@DeathCamel58

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. I've force-pushed a reworked commit. Point by point:

1. Nothing reads it on a Majestic image. Right, on s3l_ultimate too, as you said. The file is now generated and installed only with BR2_PACKAGE_DIVINUS (a conditional in the .mk), and host-python3 is only a dependency then. A Majestic image gets no /etc/sensors and no host Python.

2. Where the constants come from.

  • The format: the block layout, field offsets, table sizes and encodings, and the limits the microcode enforces were worked out by reverse engineering the S3L ISP library and the DSP microcode, and checked against the camera. That's the interface: what a driver needs to know to write the blocks at all.

  • The values are a different matter, and some had come from what the vendor library writes. I went through every value the generator wrote that wasn't its own (marked "libimg fixed" / "as stock" / "stock 3", the level-record pattern, and the rest). I changed each one on the camera and measured the result against the previous file: detail (mean |Laplacian|), temporal noise and brightness on a still scene at high gain, and detail and frame-to-frame change on a moving pattern (a rotating spiral, which is where ghosting would show). Each value then got one of three outcomes:

    • Set to 0, because nothing measurable changes:
      • the level-record thresholds, deltas and areas. With low = base = high value, the boundaries can't matter, so only the values are written;
      • the sharpen iso/dir alpha, alpha_mul, narrow/wide and disable_when_noise_reduce;
      • the MCTF a7s_mode, mf_y, auto_TA, temporal_adjust_max and d_lvl_adjust. None of these changed the still or the moving scene, and none showed ghosting.
    • Kept, with the measurement in the comment:
      • USE_GENERATED_LOWPASS = 1 (at 0 the stage blurs: −20–24 % detail, −31–35 % noise);
      • the final-sharpen +0x288 = 1 (0 loses about 20 % of the detail, in every pair);
      • dir_filter_sel = 2 (0 and 1 lose 12–24 % of the detail at the same noise);
      • MCTF wide 2 / narrow 6 (about 4 % less noise than 0/8);
      • temporal_neighbor = 1, changed from 3 (1 is as quiet as 3; 0 is noisier).
    • Derived from the format:
      • the tone curve is the bit-shift identity from an 8-bit index to 10-bit output, min(4i, 1023). A curve with any step of 5, such as round(1023i/255), breaks the picture;
      • chroma upsampling on both axes for the 4:2:0 input;
      • the clip limits at the full range of their 13-bit fields.

    The final file matches the previous one within run-to-run spread. The references to FS-MCTF-RECIPES and to *.c:line asserts are gone. Each limit the generator checks is now stated in words where it's checked.

3. Host Python. host-python3 is a dependency (with Divinus), and the generator runs as $(HOST_DIR)/bin/python3. One thing I hit on the way, FYI: host-libffi's configure reads $TARGET from the environment and uses it as its build directory. So make TARGET=$(PWD)/output-<board> ... (the multi-board pattern CLAUDE.md suggests) fails in host-libffi for any board that pulls in host-python3. CI runs plain make BOARD=..., so it isn't affected. I haven't changed anything for that here.

4. Absolute IMG address. The format is now S3LISP03. Table pointers are written as offsets into the IMG buffer, with a relocation list in the file. The Divinus loader checks each relocation against the file's own image range, then adds the buffer address the driver reports (QUERY_BUF). OpenIPC/divinus#71 carries the loader side. Tested on the camera: the relocated file loads, and the picture and 3A come up normally.

5. Microcode version. Every hardware test of the file, the statistics and the picture ran with the SDK 2.5.0 microcode this tree ships (2018/10/28 284712), and also with the camera's own 267837. The docstring now says that rather than listing the survey range.

6. TNR update mask. It does take effect as generated, with LISO +0x234 bits 0–15 at 0. At high gain on a still scene, temporal noise was 14–61 % higher with temporal_nr: off than with med, across paired runs.

7–10.

  • The generator is cut down to what the presets use, 432 lines instead of 694:
    • options width/height/bayer, black_level, wb_gain, dbp, sharpness (off/mild/medium/strong) and temporal_nr (off/low/med/high);
    • the CE batch, HDR tables, update ops, the test and expert knobs, final_sharpen r1 and stage_off are gone.
  • Every value is range-checked, and malformed input is a usage error, not a traceback: --set key, a trailing --preset, temporal_nr: iso=1, sharpness: u=8, a preset line without :, an unknown key.

@openipc-ai openipc-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough rework and the measurements. All three blockers are resolved:

  • Gated on Divinus. The file and host-python3 come in only with BR2_PACKAGE_DIVINUS, and a Majestic image gets neither.
  • Provenance. The vendor-derived values are gone or re-derived, each with a measurement or a format argument. The cited sources and the recipes reference are gone, and the docstring says plainly where the format comes from.
  • Host Python. The generator runs as $(HOST_DIR)/bin/python3.

The relocation format, the tests on the microcode we ship, the TNR measurement and the cut-down, range-checked generator cover the rest. Approving.

Non-blocking notes for whoever merges:

  1. Nothing in the tree consumes the file yet. Divinus has no S3L support on master (OpenIPC/divinus#71 is open) and the package tracks HEAD. Merging first is harmless, since nothing ships, but the generator does nothing until the loader lands.
  2. No Ambarella defconfig selects Divinus, so CI never runs the generator. A small .github/scripts/test_s3l_isp_gen.sh that runs it on ov4689.yaml (and maybe checks the header and size) would catch it breaking. Fine as a follow-up.
  3. Majestic images no longer get the empty /etc/sensors that master created unconditionally. It looks like a leftover placeholder; just calling it out as a change on shipped images.

And thanks for the host-libffi / TARGET= report. I'll take that one to a separate issue.

@openipc-ai
openipc-ai merged commit e5a6e0b into OpenIPC:master Nov 8, 2026
35 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.

2 participants