| author | 2026-09-07 05:22:19 +0700 | |
|---|---|---|
| committer | 2026-09-07 05:22:19 +0700 | |
| commit | 01a5d7692c9de7d505918a5cbd9394ecbbc9b667 (patch) | |
| tree | 5105df75e0567683890c9efba57ffd1d764b2bc4 | |
| download | libcamera-01a5d7692c9de7d505918a5cbd9394ecbbc9b667.tar.gz libcamera-01a5d7692c9de7d505918a5cbd9394ecbbc9b667.zip | |
initial commit
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Jenkinsfile | 157 | ||||
| -rw-r--r-- | README.md | 76 | ||||
| -rwxr-xr-x | build.sh | 101 | ||||
| -rw-r--r-- | patches/0001-libipa-camera_sensor-add-ov02c10.patch | 67 |
5 files changed, 404 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d22324c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +RPMS/ +.libcamera-build/ +*.src.rpm diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..24d235b --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,157 @@ +// Builds are driven by the tag name. The pipeline does nothing at all unless +// the checkout is a tag of the form +// +// fedora-<fedora_ver>-libcamera-<release_suffix> +// +// e.g. fedora-45-libcamera-sp12in1. +TAG_PATTERN = /^fedora-(\d+)-libcamera-([a-z0-9]+)$/ + +def fedoraVersionFromTag() { + def tag = env.TAG_NAME ?: '' + tag ==~ TAG_PATTERN ? tag.split('-')[1] : null +} + +pipeline { + agent none + + options { + timestamps() + } + + environment { + B2_ENDPOINT = 'https://s3.eu-central-003.backblazeb2.com' + AWS_DEFAULT_REGION = 'eu-central-003' + + RPM_BUCKET = 'dist-sayagit-fedora-rpm' + + // awscli2 sends CRC32 checksums by default, which B2 rejects. Ask for + // them only where the S3 API requires them. + AWS_REQUEST_CHECKSUM_CALCULATION = 'when_required' + AWS_RESPONSE_CHECKSUM_VALIDATION = 'when_required' + } + + stages { + stage('Build and publish libcamera RPMs (ARM64)') { + when { + beforeAgent true + allOf { + buildingTag() + expression { env.TAG_NAME ==~ TAG_PATTERN } + } + } + + agent { + kubernetes { + defaultContainer 'rpmbuild' + yaml """ +apiVersion: v1 +kind: Pod +metadata: + namespace: jenkins +spec: + nodeSelector: + kubernetes.io/arch: arm64 + containers: + - name: rpmbuild + image: fedora:${fedoraVersionFromTag()} + imagePullPolicy: Always + command: [ 'sleep' ] + args: [ 'infinity' ] + tty: true + resources: + requests: + cpu: "4" + memory: 4Gi + ephemeral-storage: 10Gi + limits: + memory: 8Gi + ephemeral-storage: 30Gi +""" + } + } + steps { + checkout scm + container('rpmbuild') { + withCredentials([usernamePassword( + credentialsId: 'backblaze-b2-dist-rpm', + usernameVariable: 'AWS_ACCESS_KEY_ID', + passwordVariable: 'AWS_SECRET_ACCESS_KEY')]) { + sh ''' + set -eux + + dnf --assumeyes install \\ + git rpm-build rpmdevtools dnf5-plugins awscli2 curl \\ + createrepo_c + + git config --global --add safe.directory '*' + + tag_fedora=${TAG_NAME#fedora-} + tag_fedora=${tag_fedora%%-libcamera-*} + tag_suffix=${TAG_NAME##*-libcamera-} + fedora_version=${tag_fedora} + + export RELEASE_SUFFIX=${tag_suffix} + + # A numbered tag has to agree with the container it + # selected, which is what stamps %{?dist} onto the + # release. + container_fedora=$(rpm --eval '%{fedora}') + if [ "${tag_fedora}" != "${container_fedora}" ]; then + echo "tag names Fedora ${tag_fedora}, container is ${container_fedora}" >&2 + exit 1 + fi + + work="${WORKSPACE}/work" + out="${WORKSPACE}/rpms" + prefix="s3://${RPM_BUCKET}/fedora/${fedora_version}/aarch64" + + # Resolving is a source-RPM download and a couple of + # spec edits, so ask what the build would produce + # before paying for the build itself. + rpm_files=$(./build.sh -n -w "${work}") + + missing=false + for rpm_name in ${rpm_files}; do + if ! aws s3 ls --endpoint-url "${B2_ENDPOINT}" \\ + "${prefix}/${rpm_name}"; then + missing=true + fi + done + + if [ "${missing}" = false ]; then + echo "${TAG_NAME} is already published in full, nothing to do" + exit 0 + fi + + ./build.sh -o "${out}" -w "${work}" + + # Every subpackage goes up, not just the two the + # image needs: Fedora's libcamera-qcam and the rest + # carry a versioned Requires on this exact release, + # so a partial set would leave them unresolvable. + for rpm_name in ${rpm_files}; do + aws s3 cp --endpoint-url "${B2_ENDPOINT}" \\ + "${out}/${rpm_name}" "${prefix}/${rpm_name}" + done + + repo="${WORKSPACE}/repo" + mkdir -p "${repo}" + aws s3 sync --endpoint-url "${B2_ENDPOINT}" \\ + --exclude '*' --include '*.rpm' \\ + "${prefix}/" "${repo}/" + + createrepo_c --update "${repo}" + + aws s3 sync --endpoint-url "${B2_ENDPOINT}" \\ + --exclude 'repomd.xml*' \\ + "${repo}/repodata/" "${prefix}/repodata/" + aws s3 cp --endpoint-url "${B2_ENDPOINT}" \\ + "${repo}/repodata/repomd.xml" \\ + "${prefix}/repodata/repomd.xml" + ''' + } + } + } + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b3b0ca --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# libcamera (Surface Pro 12" Gen 1) + +Fedora's `libcamera`, rebuilt with the patches this board needs. + +Used by [Fedora KIWI descriptions, modified by me for Surface Pro 12"](https://sayag.it/fedora-linux-surface-pro-12in/kiwi-descriptions). + +## Why a rebuild + +libcamera compiles its per-sensor knowledge into `libcamera.so`. A +`CameraSensorHelper` carries the analogue gain model and the black level, and +there is no plugin mechanism for either, so a sensor libcamera does not know +gets no usable AGC from the software ISP. + +The Surface Pro 12"'s front camera is an `ov02c10`, which upstream libcamera has +never heard of: + +``` +WARN IPASoft soft_simple.cpp:104 IPASoft: Failed to create camera sensor helper for ov02c10 +INFO IPASoft soft_simple.cpp:257 IPASoft: Exposure 4-2320, gain 16-248 (1) +``` + +Those numbers are the raw gain register, not a gain: AGC believes the sensor +cannot go below 16x, balances exposure against a fiction, and leaves the preview +dark under a green-grey veil, because it also has no black level to subtract. +With the helper in place the same sensor reports what it really is: + +``` +INFO IPASoft soft_simple.cpp:258 IPASoft: Exposure 4-3206, gain 1-15.5 (0.145) +``` + +which matches the kernel driver, where `OV02C10_REG_ANALOG_GAIN` runs from +`0x10` to `0xf8` with four fractional bits. + +## What gets applied + +| Patch | Fixes | +| --- | --- | +| `patches/0001-libipa-camera_sensor-add-ov02c10.patch` | Adds the `ov02c10` `CameraSensorHelper` (gain `code/16`, black level `0x40` at 10 bits) and its `camera_sensor_properties` entry. | + +## Versioning + +The rebuild takes Fedora's release and appends a suffix, so +`0.7.2-4.fc45` becomes `0.7.2-4.sp12in1.fc45`. That sorts above the build it +came from, which is what makes the repository's `priority="1"` produce the +patched package. + +## Building + +Must run on aarch64, and needs the source repositories reachable +(`dnf download --source` comes from `dnf5-plugins`). + +```bash +# Resolve only: print the RPMs the build would produce, and exit +./build.sh -n + +# Build them into ./RPMS +./build.sh + +# Override the release suffix (the pipeline sets it from the tag) +RELEASE_SUFFIX=sp12in2 ./build.sh +``` + +`build.sh` downloads Fedora's source RPM, copies `patches/*.patch` into it, +appends a `PatchNN:` line for each after the last one Fedora ships (the spec +uses `%autosetup -p1`, so nothing has to call `%patch`), rewrites `Release:`, +then runs `dnf builddep` and `rpmbuild -bb`. The scratch tree is reused between +runs, so `-n` followed by a real build does not download twice. + +## Publishing + +Tagging `fedora-<fedora_ver>-libcamera-<release_suffix>`, e.g. +`fedora-45-libcamera-sp12in1`, runs the pipeline: the tag picks the build +container and supplies `RELEASE_SUFFIX`, and the resulting packages go to the +same repository as the kernel, served at + + https://rpm.sayag.it/kernel-sp12in/fedora/45/aarch64/ diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..7e32f3b --- /dev/null +++ b/build.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# +# Rebuild Fedora's libcamera with this board's patches on top. +# +# libcamera has no plugin mechanism for CameraSensorHelper: the per-sensor gain +# models are compiled into libcamera.so, so teaching the software ISP about the +# Surface Pro 12"'s front sensor means shipping a rebuilt package. The patches +# in patches/ are held against Fedora's source RPM rather than an upstream git +# checkout, so the result stays a drop-in replacement for the distribution +# package -- same name, same subpackages, same soname. +# +# Usage: ./build.sh [-o OUTPUT_DIR] [-w WORK_DIR] [-n] +# +# -o where the built binary RPMs are copied (default: ./RPMS) +# -w scratch directory, reused between runs (default: ./.libcamera-build) +# -n resolve only: print the binary RPM file names the build would produce, +# one per line, and exit without building. Lets a caller check whether +# the packages are already published before paying for a build. +# +# Environment: +# +# RELEASE_SUFFIX what to append to Fedora's release (default: sp12in1) + +set -euo pipefail + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +patch_dir="${script_dir}/patches" +suffix=${RELEASE_SUFFIX:-sp12in1} + +output_dir="$(pwd)/RPMS" +work_dir="$(pwd)/.libcamera-build" +resolve_only=false + +while getopts ':o:w:nh' opt; do + case "${opt}" in + o) output_dir=${OPTARG} ;; + w) work_dir=${OPTARG} ;; + n) resolve_only=true ;; + h) sed -n '2,30p' "${BASH_SOURCE[0]}"; exit 0 ;; + *) echo "unknown option -${OPTARG}" >&2; exit 2 ;; + esac +done + +topdir="${work_dir}/rpmbuild" +spec="${topdir}/SPECS/libcamera.spec" + +if [[ ! -f ${spec} ]]; then + rm -rf "${work_dir}" + mkdir -p "${work_dir}" "${topdir}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} + + ( cd "${work_dir}" && dnf download --source libcamera ) + srpm=$(echo "${work_dir}"/libcamera-*.src.rpm) + [[ -f ${srpm} ]] || { echo "no libcamera source RPM was downloaded" >&2; exit 1; } + rpm --define "_topdir ${topdir}" -i "${srpm}" + + shopt -s nullglob + patches=("${patch_dir}"/*.patch) + shopt -u nullglob + [[ ${#patches[@]} -gt 0 ]] || { echo "no patches in ${patch_dir}" >&2; exit 1; } + + # The spec uses %autosetup -p1, so a PatchNN: line is the whole of it -- + # nothing has to call %patch. Number them after the last one Fedora ships. + last_num=$(grep -oE '^Patch[0-9]+:' "${spec}" | grep -oE '[0-9]+' | sort -n | tail -1) + last_num=${last_num:-0} + last_line=$(grep -nE '^Patch[0-9]+:' "${spec}" | tail -1 | cut -d: -f1) + [[ -n ${last_line} ]] || { echo "no Patch lines in ${spec} to append after" >&2; exit 1; } + + insert="${work_dir}/patch-lines" + : > "${insert}" + for patch in "${patches[@]}"; do + cp "${patch}" "${topdir}/SOURCES/" + last_num=$((10#${last_num} + 1)) + printf 'Patch%02d: %s\n' "${last_num}" "$(basename "${patch}")" >> "${insert}" + done + sed -i "${last_line}r ${insert}" "${spec}" + + sed -i -E "0,/^Release:/s/^(Release:[[:space:]]*[^%]*)(%\{\?dist\})/\1.${suffix}\2/" "${spec}" + grep -qE "^Release:.*\.${suffix}%\{\?dist\}" "${spec}" || { + echo "failed to add the .${suffix} release suffix to ${spec}" >&2 + exit 1 + } +fi + +mapfile -t rpm_files < <(rpmspec -q --queryformat '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}.rpm\n' "${spec}") +[[ ${#rpm_files[@]} -gt 0 ]] || { echo "rpmspec -q produced no packages" >&2; exit 1; } + +if [[ ${resolve_only} == true ]]; then + printf '%s\n' "${rpm_files[@]}" + exit 0 +fi + +dnf --assumeyes builddep "${spec}" +rpmbuild --define "_topdir ${topdir}" -bb "${spec}" + +mkdir -p "${output_dir}" +for rpm_file in "${rpm_files[@]}"; do + built=$(find "${topdir}/RPMS" -type f -name "${rpm_file}" -print -quit) + [[ -n ${built} ]] || { echo "rpmbuild did not produce ${rpm_file}" >&2; exit 1; } + cp "${built}" "${output_dir}/" + echo "${output_dir}/${rpm_file}" +done diff --git a/patches/0001-libipa-camera_sensor-add-ov02c10.patch b/patches/0001-libipa-camera_sensor-add-ov02c10.patch new file mode 100644 index 0000000..c917c1d --- /dev/null +++ b/patches/0001-libipa-camera_sensor-add-ov02c10.patch @@ -0,0 +1,67 @@ +libipa: camera_sensor: Add support for the OV02C10 + +The OV02C10 is the front camera of the Microsoft Surface Pro 12in. Without +a CameraSensorHelper the software ISP cannot map the V4L2_CID_ANALOGUE_GAIN +control value onto a real gain, so its AGC has no usable gain model and just +ramps the control to its maximum. The preview stays close to black regardless +of the scene: + + WARN IPASoft soft_simple.cpp:104 IPASoft: Failed to create camera sensor helper for ov02c10 + WARN CameraSensorProperties camera_sensor_properties.cpp:548 No static properties available for 'ov02c10' + +The kernel driver programs the analogue gain into the 16 bit register pair at +0x3508 with a range of 0x10 to 0xf8, i.e. 4 fractional bits and 1x to 15.5x, +so the gain is simply code / 16. + +The black level was measured on a Surface Pro 12in from raw frames captured +off the CAMSS RDI video node: the 1st percentile of a dark frame sits at code +64 in 10 bits, matching the usual OmniVision 0x40. + +Pixel size and optical format (1.116 um, 1/7.25") are from the OMNIVISION +product page for the OV02C10. + +--- a/src/ipa/libipa/camera_sensor_helper.cpp ++++ b/src/ipa/libipa/camera_sensor_helper.cpp +@@ -677,6 +677,18 @@ + }; + REGISTER_CAMERA_SENSOR_HELPER("ov01a10", CameraSensorHelperOv01a10) + ++class CameraSensorHelperOv02c10 : public CameraSensorHelper ++{ ++public: ++ CameraSensorHelperOv02c10() ++ { ++ /* From dark frame measurement: 0x40 at 10bits. */ ++ blackLevel_ = 4096; ++ gain_ = AnalogueGainLinear{ 1, 0, 0, 16 }; ++ } ++}; ++REGISTER_CAMERA_SENSOR_HELPER("ov02c10", CameraSensorHelperOv02c10) ++ + class CameraSensorHelperOv08x40 : public CameraSensorHelper + { + public: +--- a/src/libcamera/sensor/camera_sensor_properties.cpp ++++ b/src/libcamera/sensor/camera_sensor_properties.cpp +@@ -335,6 +335,21 @@ + .hblankDelay = 3 + }, + } }, ++ { "ov02c10", { ++ .unitCellSize = { 1116, 1116 }, ++ .testPatternModes = { ++ { controls::draft::TestPatternModeOff, 0 }, ++ { controls::draft::TestPatternModeColorBars, 1 }, ++ /* ++ * No corresponding test patterns in ++ * MIPI CCS specification for sensor's ++ * 2: "Top-Bottom Darker Color Bar" ++ * 3: "Right-Left Darker Color Bar" ++ * 4: "Color Bar type 4" ++ */ ++ }, ++ .sensorDelays = { }, ++ } }, + { "ov2685", { + .unitCellSize = { 1750, 1750 }, + .testPatternModes = { |