aboutsummaryrefslogtreecommitdiffci
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
authorGravatar Saya Andy <saya.andy@posteo.com> 2026-09-07 05:22:19 +0700
committerGravatar Saya Andy <saya.andy@posteo.com> 2026-09-07 05:22:19 +0700
commit01a5d7692c9de7d505918a5cbd9394ecbbc9b667 (patch)
tree5105df75e0567683890c9efba57ffd1d764b2bc4
downloadlibcamera-01a5d7692c9de7d505918a5cbd9394ecbbc9b667.tar.gz
libcamera-01a5d7692c9de7d505918a5cbd9394ecbbc9b667.zip
initial commit
-rw-r--r--.gitignore3
-rw-r--r--Jenkinsfile157
-rw-r--r--README.md76
-rwxr-xr-xbuild.sh101
-rw-r--r--patches/0001-libipa-camera_sensor-add-ov02c10.patch67
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 = {